// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:string_scanner/string_scanner.dart';

import 'token.dart';

/// A regular expression matching both whitespace and single-line comments.
///
/// This will only match if consumes at least one character.
final _whitespaceAndSingleLineComments = RegExp(r'([ \t\n]+|//[^\n]*(\n|$))+');

/// A regular expression matching the body of a multi-line comment, after `/*`
/// but before `*/` or a nested `/*`.
///
/// This will only match if it consumes at least one character.
final _multiLineCommentBody = RegExp(r'([^/*]|/[^*]|\*[^/])+');

/// A regular expression matching a hyphenated identifier.
///
/// This is like a standard Dart identifier, except that it can also contain
/// hyphens.
final _hyphenatedIdentifier = RegExp(r'[a-zA-Z_-][a-zA-Z0-9_-]*');

/// A scanner that converts a boolean selector string into a stream of tokens.
class Scanner {
  /// The underlying string scanner.
  final SpanScanner _scanner;

  /// The next token to emit.
  Token? _next;

  /// Whether the scanner has emitted a [TokenType.endOfFile] token.
  bool _endOfFileEmitted = false;

  Scanner(String selector) : _scanner = SpanScanner(selector);

  /// Returns the next token that will be returned by [next].
  ///
  /// Throws a [StateError] if a [TokenType.endOfFile] token has already been
  /// consumed.
  Token peek() => _next ??= _readNext();

  /// Consumes and returns the next token in the stream.
  ///
  /// Throws a [StateError] if a [TokenType.endOfFile] token has already been
  /// consumed.
  Token next() {
    var token = _next ?? _readNext();
    _endOfFileEmitted = token.type == TokenType.endOfFile;
    _next = null;
    return token;
  }

  /// If the next token matches [type], consumes it and returns `true`;
  /// otherwise, returns `false`.
  ///
  /// Throws a [StateError] if a [TokenType.endOfFile] token has already been
  /// consumed.
  bool scan(TokenType type) {
    if (peek().type != type) return false;
    next();
    return true;
  }

  /// Scan and return the next token in the stream.
  Token _readNext() {
    if (_endOfFileEmitted) throw StateError('No more tokens.');

    _consumeWhitespace();
    if (_scanner.isDone) {
      return Token(TokenType.endOfFile, _scanner.spanFrom(_scanner.state));
    }

    return switch (_scanner.peekChar()) {
      0x28 /* ( */ => _scanOperator(TokenType.leftParen),
      0x29 /* ) */ => _scanOperator(TokenType.rightParen),
      0x3F /* ? */ => _scanOperator(TokenType.questionMark),
      0x3A /* : */ => _scanOperator(TokenType.colon),
      0x21 /* ! */ => _scanOperator(TokenType.not),
      0x7C /* | */ => _scanOr(),
      0x26 /* & */ => _scanAnd(),
      _ => _scanIdentifier()
    };
  }

  /// Scans a single-character operator and returns a token of type [type].
  ///
  /// This assumes that the caller has already verified that the next character
  /// is correct for the given operator.
  Token _scanOperator(TokenType type) {
    var start = _scanner.state;
    _scanner.readChar();
    return Token(type, _scanner.spanFrom(start));
  }

  /// Scans a `||` operator and returns the appropriate token.
  ///
  /// This validates that the next two characters are `||`.
  Token _scanOr() {
    var start = _scanner.state;
    _scanner.expect('||');
    return Token(TokenType.or, _scanner.spanFrom(start));
  }

  /// Scans a `&&` operator and returns the appropriate token.
  ///
  /// This validates that the next two characters are `&&`.
  Token _scanAnd() {
    var start = _scanner.state;
    _scanner.expect('&&');
    return Token(TokenType.and, _scanner.spanFrom(start));
  }

  /// Scans and returns an identifier token.
  Token _scanIdentifier() {
    _scanner.expect(_hyphenatedIdentifier, name: 'expression');
    return IdentifierToken(_scanner.lastMatch![0]!, _scanner.lastSpan!);
  }

  /// Consumes all whitespace and comments immediately following the cursor's
  /// current position.
  void _consumeWhitespace() {
    while (_scanner.scan(_whitespaceAndSingleLineComments) ||
        _multiLineComment()) {
      // Do nothing.
    }
  }

  /// Consumes a single multi-line comment.
  ///
  /// Returns whether or not a comment was consumed.
  bool _multiLineComment() {
    if (!_scanner.scan('/*')) return false;

    while (_scanner.scan(_multiLineCommentBody) || _multiLineComment()) {
      // Do nothing.
    }
    _scanner.expect('*/');

    return true;
  }
}
