/*
 * Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
 * Use of this file is governed by the BSD 3-clause license that
 * can be found in the LICENSE.txt file in the project root.
 */

import 'dart:developer';

import 'atn/atn.dart';
import 'error/error.dart';
import 'input_stream.dart';
import 'interval_set.dart';
import 'misc/misc.dart';
import 'recognizer.dart';
import 'token.dart';
import 'token_factory.dart';
import 'token_source.dart';
import 'util/utils.dart';

abstract class Lexer extends Recognizer<LexerATNSimulator>
    implements TokenSource {
  static final DEFAULT_MODE = 0;
  static final MORE = -2;
  static final SKIP = -3;

  static final DEFAULT_TOKEN_CHANNEL = Token.DEFAULT_CHANNEL;
  static final HIDDEN = Token.HIDDEN_CHANNEL;
  static final MIN_CHAR_VALUE = 0x0000;
  static final MAX_CHAR_VALUE = 0x10FFFF;

  CharStream _input;

  late Pair<TokenSource, CharStream?> _tokenFactorySourcePair;

  @override
  TokenFactory get tokenFactory {
    return _factory;
  }

  @override
  set tokenFactory(TokenFactory factory) {
    _factory = factory;
  }

  TokenFactory _factory = CommonTokenFactory.DEFAULT;

  /// The goal of all lexer rules/methods is to create a token object.
  /// this is an instance variable as multiple rules may collaborate to
  /// create a single token. nextToken will return this object after
  /// matching lexer rule(s). If you subclass to allow multiple token
  /// emissions, then set this to the last token to be matched or
  /// something nonnull so that the auto token emit mechanism will not
  /// emit another token.
  Token? _token;

  /// What character index in the stream did the current token start at?
  /// Needed, for example, to get the text for current token. Set at
  /// the start of nextToken.
  int tokenStartCharIndex = -1;

  /// The line on which the first character of the token resides
  int tokenStartLine = -1;

  /// The character position of first character within the line
  int tokenStartCharPositionInLine = -1;

  /// Once we see EOF on char stream, next token will be EOF.
  /// If you have DONE : EOF ; then you see DONE EOF.
  bool _hitEOF = false;

  /// The channel number for the current token
  int channel = Token.DEFAULT_CHANNEL;

  /// The token type for the current token
  int type = Token.INVALID_TYPE;

  final List<int> _modeStack = [];
  int mode_ = Lexer.DEFAULT_MODE;

  /// You can set the text for the current token to override what is in
  /// the input char buffer. Use setText() or can set this instance var.
  String? _text;

  Lexer(CharStream input) : _input = input {
    _tokenFactorySourcePair = Pair(this, input);
  }

  void reset([bool resetInput = false]) {
    // wacky Lexer state variables
    if (resetInput) {
      _input.seek(0); // rewind the input
    }
    _token = null;
    type = Token.INVALID_TYPE;
    channel = Token.DEFAULT_CHANNEL;
    tokenStartCharIndex = -1;
    tokenStartCharPositionInLine = -1;
    tokenStartLine = -1;
    _text = null;

    _hitEOF = false;
    mode_ = Lexer.DEFAULT_MODE;
    _modeStack.clear();

    interpreter?.reset();
  }

  /// Return a token from this source; i.e., match a token on the char stream.
  @override
  Token nextToken() {
    // Mark start location in char stream so unbuffered streams are
    // guaranteed at least have text of current token
    final tokenStartMarker = _input.mark();
    try {
      outer:
      while (true) {
        if (_hitEOF) {
          emitEOF();
          return _token!;
        }

        _token = null;
        channel = Token.DEFAULT_CHANNEL;
        tokenStartCharIndex = _input.index;
        tokenStartCharPositionInLine = interpreter!.charPositionInLine;
        tokenStartLine = interpreter!.line;
        _text = null;
        do {
          type = Token.INVALID_TYPE;
//				System.out.println("nextToken line "+tokenStartLine+" at "+((char)input.LA(1))+
//								   " in mode "+mode+
//								   " at index "+input.index());
          late int ttype;
          try {
            ttype = interpreter!.match(_input, mode_);
          } on LexerNoViableAltException catch (e) {
            notifyListeners(e); // report error
            recover(e);
            ttype = SKIP;
          }
          if (_input.LA(1) == IntStream.EOF) {
            _hitEOF = true;
          }
          if (type == Token.INVALID_TYPE) type = ttype;
          if (type == SKIP) {
            continue outer;
          }
        } while (type == MORE);
        if (_token == null) emit();
        return _token!;
      }
    } finally {
      // make sure we release marker after match or
      // unbuffered char stream will keep buffering
      _input.release(tokenStartMarker);
    }
  }

  /// Instruct the lexer to skip creating a token for current lexer rule
  /// and look for another token. nextToken() knows to keep looking when
  /// a lexer rule finishes with token set to SKIP_TOKEN. Recall that
  /// if token==null at end of any token rule, it creates one for you
  /// and emits it.
  void skip() {
    type = Lexer.SKIP;
  }

  void more() {
    type = Lexer.MORE;
  }

  void mode(int m) {
    mode_ = m;
  }

  void pushMode(int m) {
    if (LexerATNSimulator.debug) {
      log('pushMode $m');
    }
    _modeStack.add(mode_);
    mode(m);
  }

  int popMode() {
    if (_modeStack.isEmpty) throw StateError('');
    if (LexerATNSimulator.debug) log('popMode back to ${_modeStack.last}');
    mode(_modeStack.removeLast());
    return mode_;
  }

  /// Set the char stream and reset the lexer
  @override
  set inputStream(CharStream input) {
    _tokenFactorySourcePair = Pair(this, null);
    reset(false);
    _input = input;
    _tokenFactorySourcePair = Pair(this, _input);
  }

  @override
  String get sourceName {
    return _input.sourceName;
  }

  @override
  CharStream get inputStream {
    return _input;
  }

  /// By default does not support multiple emits per nextToken invocation
  ///  for efficiency reasons.  Subclass and override this method, nextToken,
  ///  and getToken (to push tokens into a list and pull from that list
  ///  rather than a single variable as this implementation does).
  void emitToken(Token token) {
    //System.err.println("emit "+token);
    _token = token;
  }

  /// The standard method called to automatically emit a token at the
  ///  outermost lexical rule.  The token object should point into the
  ///  char buffer start..stop.  If there is a text override in 'text',
  ///  use that to set the token's text.  Override this method to emit
  ///  custom Token objects or provide a new factory.
  Token emit() {
    final t = tokenFactory.create(
        type,
        _text,
        _tokenFactorySourcePair,
        channel,
        tokenStartCharIndex,
        charIndex - 1,
        tokenStartLine,
        tokenStartCharPositionInLine);
    emitToken(t);
    return t;
  }

  Token emitEOF() {
    final cpos = charPositionInLine;
    final eof = tokenFactory.create(
      Token.EOF,
      null,
      _tokenFactorySourcePair,
      Token.DEFAULT_CHANNEL,
      _input.index,
      _input.index - 1,
      line,
      cpos,
    );
    emitToken(eof);
    return eof;
  }

  @override
  int get charPositionInLine {
    return interpreter!.charPositionInLine;
  }

  @override
  int get line {
    return interpreter!.line;
  }

  set line(int line) {
    interpreter!.line = line;
  }

  set charPositionInLine(int charPositionInLine) {
    interpreter!.charPositionInLine = charPositionInLine;
  }

  /// What is the index of the current character of lookahead?
  int get charIndex {
    return _input.index;
  }

  /// Return the text matched so far for the current token or any
  ///  text override.
  String get text {
    if (_text != null) {
      return _text!;
    }
    return interpreter!.getText(_input);
  }

  /// Set the complete text of this token; it wipes any previous
  ///  changes to the text.
  set text(String text) {
    _text = text;
  }

  /// Override if emitting multiple tokens.
  Token? get token {
    return _token;
  }

  void setToken(Token _token) {
    this._token = _token;
  }

  List<String>? get channelNames => null;

  List<String>? get modeNames => null;

  /// Return a list of all Token objects in input char stream.
  ///  Forces load of all tokens. Does not include EOF token.
  List<Token> get allTokens {
    final tokens = <Token>[];
    var t = nextToken();
    while (t.type != Token.EOF) {
      tokens.add(t);
      t = nextToken();
    }
    return tokens;
  }

  void notifyListeners(LexerNoViableAltException e) {
    final text = _input.getText(Interval.of(tokenStartCharIndex, _input.index));
    final msg = "token recognition error at: '" + getErrorDisplay(text) + "'";

    final listener = errorListenerDispatch;
    listener.syntaxError(
      this,
      null,
      tokenStartLine,
      tokenStartCharPositionInLine,
      msg,
      e,
    );
  }

  String getErrorDisplay(String s) {
    return escapeWhitespace(s);
  }

  String getCharErrorDisplay(int c) {
    final s = getErrorDisplay(String.fromCharCode(c));
    return "'$s'";
  }

  /// Lexers can normally match any char in it's vocabulary after matching
  ///  a token, so do the easy thing and just kill a character and hope
  ///  it all works out.  You can instead use the rule invocation stack
  ///  to do sophisticated error recovery if you are in a fragment rule.
  void recover(RecognitionException re) {
    if (re is LexerNoViableAltException) {
      if (_input.LA(1) != IntStream.EOF) {
        // skip a char and try again
        interpreter!.consume(_input);
      }
    } else {
      //System.out.println("consuming char "+(char)input.LA(1)+" during recovery");
      //re.printStackTrace();
      // TODO: Do we lose character or line position information?
      _input.consume();
    }
  }
}
