import isUnicodeIDStartRegex from '@unicode/unicode-16.0.0/Binary_Property/ID_Start/regex.js';
import isUnicodeIDContinueRegex from '@unicode/unicode-16.0.0/Binary_Property/ID_Continue/regex.js';
import isSpaceSeparatorRegex from '@unicode/unicode-16.0.0/General_Category/Space_Separator/regex.js';
import { UTF16SurrogatePairToCodePoint } from '../static-semantics/all.mts';
import {
  Assert, CallFrame, isLeadingSurrogate, isTrailingSurrogate, ObjectValue, surroundingAgent,
  ThrowCompletion,
} from '../index.mts';
import type { ErrorObject } from '../intrinsics/Error.mts';
import { __ts_cast__, getHostDefinedErrorStack } from '../helpers.mts';
import {
  Token,
  TokenNames,
  TokenValues,
  KeywordLookup,
  isKeywordRaw,
} from './tokens.mts';
import type { Location, Position } from './ParseNode.mts';

export type Locatable =
  | TokenData
  | Position
  | Location
  | { readonly location: Location };

const isUnicodeIDStart = (c: string) => c && isUnicodeIDStartRegex.test(c);
const isUnicodeIDContinue = (c: string) => c && isUnicodeIDContinueRegex.test(c);
export const isDecimalDigit = (c: string) => c && /\d/u.test(c);
export const isHexDigit = (c: string) => c && /[\da-f]/ui.test(c);
const isOctalDigit = (c: string) => c && /[0-7]/u.test(c);
const isBinaryDigit = (c: string) => (c === '0' || c === '1');
export const isWhitespace = (c: string) => c && (/[\u0009\u000B\u000C\u0020\u00A0\uFEFF]/u.test(c) || isSpaceSeparatorRegex.test(c)); // eslint-disable-line no-control-regex
export const isLineTerminator = (c: string | number) => {
  // Line Separator (U+2028) and Paragraph Separator (U+2029)
  // Line Feed (U+000A) and Carriage Return (U+000D)
  if (typeof c === 'string') {
    return !!c && /[\r\n\u2028\u2029]/u.test(c);
  }
  return c === 0x2028 || c === 0x2029 || c === 0xa || c === 0xd;
};
const isRegularExpressionFlagPart = (c: string) => c && (isUnicodeIDContinue(c) || c === '$');
export const isIdentifierStart = (c: string) => SingleCharTokens[c] === Token.IDENTIFIER || isUnicodeIDStart(c);
export const isIdentifierPart = (c: string) => SingleCharTokens[c] === Token.IDENTIFIER || c === '\u{200C}' || c === '\u{200D}' || isUnicodeIDContinue(c);

const SingleCharTokens: { [key: string]: number } = {
  '__proto__': null!,
  '0': Token.NUMBER,
  '1': Token.NUMBER,
  '2': Token.NUMBER,
  '3': Token.NUMBER,
  '4': Token.NUMBER,
  '5': Token.NUMBER,
  '6': Token.NUMBER,
  '7': Token.NUMBER,
  '8': Token.NUMBER,
  '9': Token.NUMBER,
  'a': Token.IDENTIFIER,
  'b': Token.IDENTIFIER,
  'c': Token.IDENTIFIER,
  'd': Token.IDENTIFIER,
  'e': Token.IDENTIFIER,
  'f': Token.IDENTIFIER,
  'g': Token.IDENTIFIER,
  'h': Token.IDENTIFIER,
  'i': Token.IDENTIFIER,
  'j': Token.IDENTIFIER,
  'k': Token.IDENTIFIER,
  'l': Token.IDENTIFIER,
  'm': Token.IDENTIFIER,
  'n': Token.IDENTIFIER,
  'o': Token.IDENTIFIER,
  'p': Token.IDENTIFIER,
  'q': Token.IDENTIFIER,
  'r': Token.IDENTIFIER,
  's': Token.IDENTIFIER,
  't': Token.IDENTIFIER,
  'u': Token.IDENTIFIER,
  'v': Token.IDENTIFIER,
  'w': Token.IDENTIFIER,
  'x': Token.IDENTIFIER,
  'y': Token.IDENTIFIER,
  'z': Token.IDENTIFIER,
  'A': Token.IDENTIFIER,
  'B': Token.IDENTIFIER,
  'C': Token.IDENTIFIER,
  'D': Token.IDENTIFIER,
  'E': Token.IDENTIFIER,
  'F': Token.IDENTIFIER,
  'G': Token.IDENTIFIER,
  'H': Token.IDENTIFIER,
  'I': Token.IDENTIFIER,
  'J': Token.IDENTIFIER,
  'K': Token.IDENTIFIER,
  'L': Token.IDENTIFIER,
  'M': Token.IDENTIFIER,
  'N': Token.IDENTIFIER,
  'O': Token.IDENTIFIER,
  'P': Token.IDENTIFIER,
  'Q': Token.IDENTIFIER,
  'R': Token.IDENTIFIER,
  'S': Token.IDENTIFIER,
  'T': Token.IDENTIFIER,
  'U': Token.IDENTIFIER,
  'V': Token.IDENTIFIER,
  'W': Token.IDENTIFIER,
  'X': Token.IDENTIFIER,
  'Y': Token.IDENTIFIER,
  'Z': Token.IDENTIFIER,
  '$': Token.IDENTIFIER,
  '_': Token.IDENTIFIER,
  '\\': Token.IDENTIFIER,
  '.': Token.PERIOD,
  ',': Token.COMMA,
  ':': Token.COLON,
  ';': Token.SEMICOLON,
  '%': Token.MOD,
  '~': Token.BIT_NOT,
  '!': Token.NOT,
  '+': Token.ADD,
  '-': Token.SUB,
  '*': Token.MUL,
  '<': Token.LT,
  '>': Token.GT,
  '=': Token.ASSIGN,
  '?': Token.CONDITIONAL,
  '[': Token.LBRACK,
  ']': Token.RBRACK,
  '(': Token.LPAREN,
  ')': Token.RPAREN,
  '/': Token.DIV,
  '^': Token.BIT_XOR,
  '`': Token.TEMPLATE,
  '{': Token.LBRACE,
  '}': Token.RBRACE,
  '&': Token.BIT_AND,
  '|': Token.BIT_OR,
  '"': Token.STRING,
  '\'': Token.STRING,
  '#': Token.PRIVATE_IDENTIFIER,
  '@': Token.AT,
};

export class TokenData {
  readonly type: Token;

  readonly startIndex: number;

  readonly endIndex: number;

  readonly line: number;

  readonly column: number;

  readonly hadLineTerminatorBefore: boolean;

  readonly name: string;

  readonly value: string | number | bigint | boolean | null;

  readonly escaped: boolean;

  constructor({
    type,
    startIndex,
    endIndex,
    line,
    column,
    hadLineTerminatorBefore,
    name,
    value,
    escaped,
  }: Pick<TokenData, 'type' | 'startIndex' | 'endIndex' | 'line' | 'column' | 'hadLineTerminatorBefore' | 'name' | 'value' | 'escaped'>) {
    this.type = type;
    this.startIndex = startIndex;
    this.endIndex = endIndex;
    this.line = line;
    this.column = column;
    this.hadLineTerminatorBefore = hadLineTerminatorBefore;
    this.name = name;
    this.value = value;
    this.escaped = escaped;
  }

  valueAsString() {
    Assert(typeof this.value === 'string');
    return this.value;
  }

  valueAsNumeric() {
    Assert(typeof this.value === 'number' || typeof this.value === 'bigint');
    return this.value;
  }

  valueAsBoolean() {
    Assert(typeof this.value === 'boolean');
    return this.value;
  }
}

export abstract class Lexer {
  protected abstract readonly source: string;

  protected currentToken!: TokenData; // NOTE: unsound definite assignment operator (`!`)

  protected peekToken!: TokenData; // NOTE: unsound definite assignment operator (`!`)

  protected peekAheadToken: TokenData | undefined;

  protected position = 0;

  protected line = 1;

  protected columnOffset = 0;

  protected scannedValue!: string | number | Token | bigint | boolean; // NOTE: unsound definite assignment operator (`!`)

  protected lineTerminatorBeforeNextToken = false;

  protected positionForNextToken = 0;

  protected lineForNextToken = 0;

  protected columnForNextToken = 0;

  protected escapeIndex = -1;

  earlyErrors2 = new Set<ErrorObject>();

  decorateSyntaxError(error: ErrorObject, location: number | Locatable) {
    // if (template === 'UnexpectedToken' && typeof context !== 'number' && 'type' in context && context.type === Token.EOS) {
    //   return this.createSyntaxError(context, 'UnexpectedEOS', []);
    // }

    let startIndex;
    // @ts-ignore unused
    let endIndex;
    let line;
    let column;
    if (typeof location === 'number') {
      line = this.line;
      if (location === this.source.length) {
        while (isLineTerminator(this.source[location - 1])) {
          line -= 1;
          location -= 1;
        }
      }
      startIndex = location;
      endIndex = location + 1;
    } else if ('type' in location && location.type === Token.EOS) {
      line = this.line;
      startIndex = location.startIndex;
      while (isLineTerminator(this.source[startIndex - 1])) {
        line -= 1;
        startIndex -= 1;
      }
      endIndex = startIndex + 1;
    } else {
      if ('location' in location && location.location) {
        location = location.location;
      }
      ({
        startIndex,
        endIndex,
        start: {
          line,
          column,
        } = location as Position, // NOTE: unsound cast
      } = location as Location); // NOTE: unsound cast
    }

    /*
       * Source looks like:
       *
       *  const a = 1;
       *  const b 'string string string'; // a string
       *  const c = 3;                  |            |
       *  |       |                     |            |
       *  |       | startIndex          | endIndex   |
       *  | lineStart                                | lineEnd
       *
       * Exception looks like:
       *
       *  const b 'string string string'; // a string
       *          ^^^^^^^^^^^^^^^^^^^^^^
       *  SyntaxError: unexpected token
      */

    let lineStart = startIndex;
    while (!isLineTerminator(this.source[lineStart - 1]) && this.source[lineStart - 1] !== undefined) {
      lineStart -= 1;
    }

    let lineEnd = startIndex;
    while (!isLineTerminator(this.source[lineEnd]) && this.source[lineEnd] !== undefined) {
      lineEnd += 1;
    }

    if (column === undefined) {
      column = startIndex - lineStart + 1;
    }

    const callFrame = new CallFrame();
    callFrame.columnNumber = column;
    callFrame.lineNumber = line;
    error.HostDefinedErrorStack = [callFrame];
  }

  static decorateSyntaxErrorWithScriptId(error: ObjectValue, scriptId: string | undefined) {
    const stack = getHostDefinedErrorStack(error);
    if (stack?.[0] instanceof CallFrame) {
      stack[0].scriptId = scriptId;
    }
  }

  // parseFailure(completion: ThrowCompletion): never;

  addEarlyError({ Value: error }: ThrowCompletion, location: Locatable): void {
    __ts_cast__<ErrorObject>(error);
    this.decorateSyntaxError(error, location);
    this.earlyErrors2.add(error);
  }

  abstract isStrictMode(): boolean;

  abstract createSyntaxError<K extends keyof typeof import('../messages.mjs')>(context: number | Locatable | undefined, template: K, templateArgs: Parameters<typeof import('../messages.mjs')[K]>): SyntaxError;

  abstract raiseEarly<K extends keyof typeof import('../messages.mjs')>(template: K, context?: number | Locatable, ...templateArgs: Parameters<typeof import('../messages.mjs')[K]>): SyntaxError;

  abstract raise<K extends keyof typeof import('../messages.mjs')>(template: K, context?: number | Locatable, ...templateArgs: Parameters<typeof import('../messages.mjs')[K]>): never;

  abstract unexpected(...args: [(number | Locatable)?, ...Parameters<typeof import('../messages.mjs')['UnexpectedToken']>]): never;

  try<T>(callback: () => T): T | undefined {
    const currentToken = this.currentToken;
    const peekToken = this.peekToken;
    const peekAheadToken = this.peekAheadToken;
    const lineTerminatorBeforeNextToken = this.lineTerminatorBeforeNextToken;
    const escapeIndex = this.escapeIndex;
    const positionForNextToken = this.positionForNextToken;
    const lineForNextToken = this.lineForNextToken;
    const columnForNextToken = this.columnForNextToken;
    const position = this.position;
    const earlyErrors = [...this.earlyErrors2];
    let result: T | undefined;
    try {
      result = callback();
    } catch {}
    if (!result) {
      this.currentToken = currentToken;
      this.peekToken = peekToken;
      this.peekAheadToken = peekAheadToken;
      this.lineTerminatorBeforeNextToken = lineTerminatorBeforeNextToken;
      this.escapeIndex = escapeIndex;
      this.positionForNextToken = positionForNextToken;
      this.lineForNextToken = lineForNextToken;
      this.columnForNextToken = columnForNextToken;
      this.position = position;
      this.earlyErrors2 = new Set(earlyErrors);
    }
    return result;
  }

  advance(): TokenData {
    this.lineTerminatorBeforeNextToken = false;
    this.escapeIndex = -1;
    const type = this.nextToken();
    return new TokenData({
      type,
      startIndex: this.positionForNextToken,
      endIndex: this.position,
      line: this.lineForNextToken,
      column: this.columnForNextToken,
      hadLineTerminatorBefore: this.lineTerminatorBeforeNextToken,
      name: TokenNames[type],
      value: TokenValues[type] ?? this.scannedValue,
      escaped: this.escapeIndex !== -1,
    });
  }

  next() {
    this.currentToken = this.peekToken;
    if (this.peekAheadToken !== undefined) {
      this.peekToken = this.peekAheadToken;
      this.peekAheadToken = undefined;
    } else {
      this.peekToken = this.advance();
    }
    return this.currentToken;
  }

  peek() {
    if (this.peekToken === undefined) {
      this.next();
    }
    return this.peekToken;
  }

  peekAhead() {
    if (this.peekAheadToken === undefined) {
      this.peek();
      this.peekAheadToken = this.advance();
    }
    return this.peekAheadToken;
  }

  matches(token: string | Token, peek: TokenData) {
    if (typeof token === 'string') {
      if (peek.type === Token.IDENTIFIER && peek.value === token) {
        const escapeIndex = this.source.slice(peek.startIndex, peek.endIndex).indexOf('\\');
        if (escapeIndex !== -1) {
          return false;
        }
        return true;
      } else {
        return false;
      }
    }
    return peek.type === token;
  }

  test(token: string | Token) {
    return this.matches(token, this.peek());
  }

  testAhead(token: string | Token) {
    return this.matches(token, this.peekAhead());
  }

  eat(token: string | Token) {
    if (this.test(token)) {
      this.next();
      return true;
    }
    return false;
  }

  expect(token: string | Token) {
    if (this.test(token)) {
      return this.next();
    }
    return this.unexpected();
  }

  skipSpace() {
    loop: // eslint-disable-line no-labels
    while (this.position < this.source.length) {
      const c = this.source[this.position];
      switch (c) {
        case ' ':
        case '\t':
          this.position += 1;
          break;
        case '/':
          switch (this.source[this.position + 1]) {
            case '/':
              this.skipLineComment();
              break;
            case '*':
              this.skipBlockComment();
              break;
            default:
              break loop; // eslint-disable-line no-labels
          }
          break;
        default:
          if (isWhitespace(c)) {
            this.position += 1;
          } else if (isLineTerminator(c)) {
            this.position += 1;
            if (c === '\r' && this.source[this.position] === '\n') {
              this.position += 1;
            }
            this.line += 1;
            this.columnOffset = this.position;
            this.lineTerminatorBeforeNextToken = true;
            break;
          } else {
            break loop; // eslint-disable-line no-labels
          }
          break;
      }
    }
  }

  skipHashbangComment() {
    if (this.position === 0
        && this.source[0] === '#'
        && this.source[1] === '!') {
      this.skipLineComment();
    }
  }

  skipLineComment() {
    while (this.position < this.source.length) {
      const c = this.source[this.position];
      this.position += 1;
      if (isLineTerminator(c)) {
        if (c === '\r' && this.source[this.position] === '\n') {
          this.position += 1;
        }
        this.line += 1;
        this.columnOffset = this.position;
        this.lineTerminatorBeforeNextToken = true;
        break;
      }
    }
  }

  skipBlockComment() {
    const end = this.source.indexOf('*/', this.position + 2);
    if (end === -1) {
      this.raise('UnterminatedComment', this.position);
    }
    this.position += 2;
    for (const match of this.source.slice(this.position, end).matchAll(/\r\n?|[\n\u2028\u2029]/ug)) {
      this.position = match.index!;
      this.line += 1;
      this.columnOffset = this.position;
      this.lineTerminatorBeforeNextToken = true;
    }
    this.position = end + 2;
  }

  nextToken() {
    this.skipSpace();

    // set token location info after skipping space
    this.positionForNextToken = this.position;
    this.lineForNextToken = this.line;
    this.columnForNextToken = this.position - this.columnOffset + 1;

    if (this.position >= this.source.length) {
      return Token.EOS;
    }
    const c = this.source[this.position];
    this.position += 1;
    const c1 = this.source[this.position];
    if (c.charCodeAt(0) <= 127) {
      const single = SingleCharTokens[c];
      switch (single) {
        case Token.LPAREN:
        case Token.RPAREN:
        case Token.LBRACE:
        case Token.RBRACE:
        case Token.LBRACK:
        case Token.RBRACK:
        case Token.COLON:
        case Token.SEMICOLON:
        case Token.COMMA:
        case Token.BIT_NOT:
        case Token.TEMPLATE:
          return single;
        case Token.AT:
          if (surroundingAgent.feature('decorators')) {
            return single;
          } else {
            return this.unexpected(single);
          }

        case Token.CONDITIONAL:
          // ? ?. ?? ??=
          if (c1 === '.' && !isDecimalDigit(this.source[this.position + 1])) {
            this.position += 1;
            return Token.OPTIONAL;
          }
          if (c1 === '?') {
            this.position += 1;
            if (this.source[this.position] === '=') {
              this.position += 1;
              return Token.ASSIGN_NULLISH;
            }
            return Token.NULLISH;
          }
          return Token.CONDITIONAL;

        case Token.LT:
          // < <= << <<=
          if (c1 === '=') {
            this.position += 1;
            return Token.LTE;
          }
          if (c1 === '<') {
            this.position += 1;
            if (this.source[this.position] === '=') {
              this.position += 1;
              return Token.ASSIGN_SHL;
            }
            return Token.SHL;
          }
          return Token.LT;

        case Token.GT:
          // > >= >> >>= >>> >>>=
          if (c1 === '=') {
            this.position += 1;
            return Token.GTE;
          }
          if (c1 === '>') {
            this.position += 1;
            if (this.source[this.position] === '>') {
              this.position += 1;
              if (this.source[this.position] === '=') {
                this.position += 1;
                return Token.ASSIGN_SHR;
              }
              return Token.SHR;
            }
            if (this.source[this.position] === '=') {
              this.position += 1;
              return Token.ASSIGN_SAR;
            }
            return Token.SAR;
          }
          return Token.GT;

        case Token.ASSIGN:
          // = == === =>
          if (c1 === '=') {
            this.position += 1;
            if (this.source[this.position] === '=') {
              this.position += 1;
              return Token.EQ_STRICT;
            }
            return Token.EQ;
          }
          if (c1 === '>') {
            this.position += 1;
            return Token.ARROW;
          }
          return Token.ASSIGN;

        case Token.NOT:
          // ! != !==
          if (c1 === '=') {
            this.position += 1;
            if (this.source[this.position] === '=') {
              this.position += 1;
              return Token.NE_STRICT;
            }
            return Token.NE;
          }
          return Token.NOT;

        case Token.ADD:
          // + ++ +=
          if (c1 === '+') {
            this.position += 1;
            return Token.INC;
          }
          if (c1 === '=') {
            this.position += 1;
            return Token.ASSIGN_ADD;
          }
          return Token.ADD;

        case Token.SUB:
          // - -- -=
          if (c1 === '-') {
            this.position += 1;
            return Token.DEC;
          }
          if (c1 === '=') {
            this.position += 1;
            return Token.ASSIGN_SUB;
          }
          return Token.SUB;

        case Token.MUL:
          // * *= ** **=
          if (c1 === '=') {
            this.position += 1;
            return Token.ASSIGN_MUL;
          }
          if (c1 === '*') {
            this.position += 1;
            if (this.source[this.position] === '=') {
              this.position += 1;
              return Token.ASSIGN_EXP;
            }
            return Token.EXP;
          }
          return Token.MUL;

        case Token.MOD:
          // % %=
          if (c1 === '=') {
            this.position += 1;
            return Token.ASSIGN_MOD;
          }
          return Token.MOD;

        case Token.DIV:
          // / /=
          if (c1 === '=') {
            this.position += 1;
            return Token.ASSIGN_DIV;
          }
          return Token.DIV;

        case Token.BIT_AND:
          // & && &= &&=
          if (c1 === '&') {
            this.position += 1;
            if (this.source[this.position] === '=') {
              this.position += 1;
              return Token.ASSIGN_AND;
            }
            return Token.AND;
          }
          if (c1 === '=') {
            this.position += 1;
            return Token.ASSIGN_BIT_AND;
          }
          return Token.BIT_AND;

        case Token.BIT_OR:
          // | || |= ||=
          if (c1 === '|') {
            this.position += 1;
            if (this.source[this.position] === '=') {
              this.position += 1;
              return Token.ASSIGN_OR;
            }
            return Token.OR;
          }
          if (c1 === '=') {
            this.position += 1;
            return Token.ASSIGN_BIT_OR;
          }
          return Token.BIT_OR;

        case Token.BIT_XOR:
          // ^ ^=
          if (c1 === '=') {
            this.position += 1;
            return Token.ASSIGN_BIT_XOR;
          }
          return Token.BIT_XOR;

        case Token.PERIOD:
          // . ... NUMBER
          if (isDecimalDigit(c1)) {
            this.position -= 1;
            return this.scanNumber();
          }
          if (c1 === '.') {
            if (this.source[this.position + 1] === '.') {
              this.position += 2;
              return Token.ELLIPSIS;
            }
          }
          return Token.PERIOD;

        case Token.STRING:
          return this.scanString(c);

        case Token.NUMBER:
          this.position -= 1;
          return this.scanNumber();

        case Token.IDENTIFIER:
          this.position -= 1;
          return this.scanIdentifierOrKeyword();

        case Token.PRIVATE_IDENTIFIER:
          return this.scanIdentifierOrKeyword(true);

        default:
          this.unexpected(single);
      }
    }

    this.position -= 1;

    if (isLeadingSurrogate(c.charCodeAt(0)) || isIdentifierStart(c)) {
      return this.scanIdentifierOrKeyword();
    }

    return this.unexpected(this.position);
  }

  scanNumber() {
    const start = this.position;
    let base: 2 | 8 | 10 | 16 = 10;
    let nonDecimalPrefixLength = 2;
    let zeroLeading = false;
    let check = isDecimalDigit;
    if (this.source[this.position] === '0') {
      this.scannedValue = 0;
      this.position += 1;
      switch (this.source[this.position]) {
        case 'x':
        case 'X':
          base = 16;
          break;
        case 'o':
        case 'O':
          base = 8;
          break;
        case 'b':
        case 'B':
          base = 2;
          break;
        case '.':
        case 'e':
        case 'E':
          break;
        case 'n':
          this.position += 1;
          this.scannedValue = 0n;
          return Token.BIGINT;
        default: {
          if (!isDecimalDigit(this.source[this.position])) {
            return Token.NUMBER;
          }
          // Legacy octal literal (0123)
          if (this.isStrictMode()) {
            this.raise('LegacyOctalLiteralInStrictMode', start);
          }
          this.position -= 1;
          nonDecimalPrefixLength = 1;
          zeroLeading = true;
          const oldPos = this.position;
          base = 8;
          while (this.position < this.source.length) {
            const c = this.source[this.position];
            if (isDecimalDigit(c) && !isOctalDigit(c)) {
              base = 10;
              break;
            } else if (!isOctalDigit(c)) {
              // A single 0
              break;
            } else {
              this.position += 1;
            }
          }
          this.position = oldPos;
          break;
        }
      }
      check = {
        16: isHexDigit,
        10: isDecimalDigit,
        8: isOctalDigit,
        2: isBinaryDigit,
      }[base];
      if (base !== 10) {
        if (!check(this.source[this.position + 1])) {
          return Token.NUMBER;
        }
        this.position += 1;
      }
    }
    while (this.position < this.source.length) {
      const c = this.source[this.position];
      if (check(c)) {
        this.position += 1;
      } else if (c === '_') {
        if (zeroLeading) {
          this.raise('SeparatorIsNotAllowed', this.position);
        }
        if (!check(this.source[this.position + 1])) {
          this.unexpected(this.position + 1);
        }
        this.position += 1;
      } else {
        break;
      }
    }
    if (this.source[this.position] === 'n') {
      if (zeroLeading) {
        this.raise('BigIntLiteralCannotLeadingZero', this.position);
      }
      const buffer = this.source.slice(start, this.position).replace(/_/g, '');
      this.position += 1;
      this.scannedValue = BigInt(buffer);
      return Token.BIGINT;
    }
    if (base === 10 && this.source[this.position] === '.') {
      this.position += 1;
      if (this.source[this.position] === '_') {
        this.unexpected(this.position);
      }
      while (this.position < this.source.length) {
        const c = this.source[this.position];
        if (isDecimalDigit(c)) {
          this.position += 1;
        } else if (c === '_') {
          if (!isDecimalDigit(this.source[this.position + 1])) {
            this.unexpected(this.position + 1);
          }
          this.position += 1;
        } else {
          break;
        }
      }
    }
    if (base === 10 && (this.source[this.position] === 'E' || this.source[this.position] === 'e')) {
      this.position += 1;
      if (this.source[this.position] === '_') {
        this.unexpected(this.position);
      }
      if (this.source[this.position] === '-' || this.source[this.position] === '+') {
        this.position += 1;
      }
      if (this.source[this.position] === '_') {
        this.unexpected(this.position);
      }
      while (this.position < this.source.length) {
        const c = this.source[this.position];
        if (isDecimalDigit(c)) {
          this.position += 1;
        } else if (c === '_') {
          if (!isDecimalDigit(this.source[this.position + 1])) {
            this.unexpected(this.position + 1);
          }
          this.position += 1;
        } else {
          break;
        }
      }
    }
    if (isIdentifierStart(this.source[this.position])) {
      this.unexpected(this.position);
    }
    const buffer = this.source
      .slice(base === 10 ? start : start + nonDecimalPrefixLength, this.position)
      .replace(/_/g, '');
    this.scannedValue = base === 10
      ? Number.parseFloat(buffer)
      : Number.parseInt(buffer, base);
    return Token.NUMBER;
  }

  scanString(char: string) {
    let buffer = '';
    while (true) {
      if (this.position >= this.source.length) {
        this.raise('UnterminatedString', this.position);
      }
      const c = this.source[this.position];
      if (c === char) {
        this.position += 1;
        break;
      }
      if (c === '\r' || c === '\n') {
        this.raise('UnterminatedString', this.position);
      }
      this.position += 1;
      if (c === '\\') {
        const l = this.source[this.position];
        if (isLineTerminator(l)) {
          this.position += 1;
          if (l === '\r' && this.source[this.position] === '\n') {
            this.position += 1;
          }
          this.line += 1;
          this.columnOffset = this.position;
        } else {
          buffer += this.scanEscapeSequence();
        }
      } else {
        buffer += c;
      }
    }
    this.scannedValue = buffer;
    return Token.STRING;
  }

  scanEscapeSequence() {
    const c = this.source[this.position];
    switch (c) {
      case 'b':
        this.position += 1;
        return '\b';
      case 't':
        this.position += 1;
        return '\t';
      case 'n':
        this.position += 1;
        return '\n';
      case 'v':
        this.position += 1;
        return '\v';
      case 'f':
        this.position += 1;
        return '\f';
      case 'r':
        this.position += 1;
        return '\r';
      case 'x':
        this.position += 1;
        return String.fromCodePoint(this.scanHex(2));
      case 'u':
        this.position += 1;
        return String.fromCodePoint(this.scanCodePoint());
      default: {
        const lookahead = this.source[this.position + 1];
        if (c === '0' && !isDecimalDigit(lookahead)) {
          this.position += 1;
          return '\u{0000}';
        } else if (isDecimalDigit(c)) {
          if (this.isStrictMode()) {
            this.raise('IllegalOctalEscape', this.position);
          }
          const lookahead2 = this.source[this.position + 2];
          if (c === '0' && (lookahead === '8' || lookahead === '9')) {
            // LegacyOctalEscapeSequence :: 0 [lookahead ∈ { 8, 9 }]
            // evaluates to \u0000 + 8 or 9
            this.position += 2;
            return `\u{0000}${lookahead}`;
          } else if (c !== '0' && isOctalDigit(c) && !isOctalDigit(lookahead)) {
            // LegacyOctalEscapeSequence :: NonZeroOctalDigit [lookahead ∉ OctalDigit]
            // \1 is \u{0001}, etc...
            this.position += 1;
            return String.fromCodePoint(parseInt(c, 8));
          } else if ((c === '0' || c === '1' || c === '2' || c === '3') && isOctalDigit(lookahead) && !isOctalDigit(lookahead2)) {
            // LegacyOctalEscapeSequence :: ZeroToThree OctalDigit [lookahead ∉ OctalDigit]
            this.position += 2;
            return String.fromCodePoint(parseInt(c + lookahead, 8));
          } else if ((c === '4' || c === '5' || c === '6' || c === '7') && isOctalDigit(lookahead)) {
            // LegacyOctalEscapeSequence :: FourToSeven OctalDigit
            this.position += 2;
            return String.fromCodePoint(parseInt(c + lookahead, 8));
          } else if ((c === '0' || c === '1' || c === '2' || c === '3') && isOctalDigit(lookahead) && isOctalDigit(lookahead2)) {
            // LegacyOctalEscapeSequence ::  ZeroToThree OctalDigit OctalDigit
            this.position += 3;
            return String.fromCodePoint(parseInt(c + lookahead + lookahead2, 8));
          } else if (c === '8' || c === '9') {
            // NonOctalDecimalEscapeSequence
            // \8 or \9 is 8 or 9
            this.position += 1;
            return c;
          }
        }
        this.position += 1;
        return c;
      }
    }
  }

  scanCodePoint() {
    if (this.source[this.position] === '{') {
      const end = this.source.indexOf('}', this.position);
      this.position += 1;
      const code = this.scanHex(end - this.position);
      this.position += 1;
      if (code > 0x10FFFF) {
        this.raise('InvalidCodePoint', this.position);
      }
      return code;
    }
    return this.scanHex(4);
  }

  scanHex(length: number) {
    if (length === 0) {
      this.raise('InvalidCodePoint', this.position);
    }
    let n = 0;
    for (let i = 0; i < length; i += 1) {
      const c = this.source[this.position];
      if (isHexDigit(c)) {
        this.position += 1;
        n = (n << 4) | Number.parseInt(c, 16);
      } else {
        this.unexpected(this.position);
      }
    }
    return n;
  }

  scanIdentifierOrKeyword(isPrivate = false) {
    let buffer = '';
    let escapeIndex = -1;
    let check = isIdentifierStart;
    while (this.position < this.source.length) {
      const c = this.source[this.position];
      const code = c.charCodeAt(0);
      if (c === '\\') {
        if (escapeIndex === -1) {
          escapeIndex = this.position;
        }
        this.position += 1;
        if (this.source[this.position] !== 'u') {
          this.raise('InvalidUnicodeEscape', this.position);
        }
        this.position += 1;
        const raw = String.fromCodePoint(this.scanCodePoint());
        if (!check(raw)) {
          this.raise('InvalidUnicodeEscape', this.position);
        }
        buffer += raw;
      } else if (isLeadingSurrogate(code)) {
        const lowSurrogate = this.source.charCodeAt(this.position + 1);
        if (!isTrailingSurrogate(lowSurrogate)) {
          this.raise('InvalidUnicodeEscape', this.position);
        }
        const codePoint = UTF16SurrogatePairToCodePoint(code, lowSurrogate);
        const raw = String.fromCodePoint(codePoint);
        if (!check(raw)) {
          this.raise('InvalidUnicodeEscape', this.position);
        }
        this.position += 2;
        buffer += raw;
      } else if (check(c)) {
        buffer += c;
        this.position += 1;
      } else {
        break;
      }
      check = isIdentifierPart;
    }
    if (!isPrivate && isKeywordRaw(buffer)) {
      if (escapeIndex !== -1) {
        this.scannedValue = buffer;
        return Token.ESCAPED_KEYWORD;
      }
      return KeywordLookup[buffer];
    } else {
      this.scannedValue = buffer;
      this.escapeIndex = escapeIndex;
      return isPrivate ? Token.PRIVATE_IDENTIFIER : Token.IDENTIFIER;
    }
  }

  scanRegularExpressionBody() {
    let inClass = false;
    let buffer = this.peek().type === Token.ASSIGN_DIV ? '=' : '';
    while (true) {
      if (this.position >= this.source.length) {
        this.raise('UnterminatedRegExp', this.position);
      }
      const c = this.source[this.position];
      switch (c) {
        case '[':
          inClass = true;
          this.position += 1;
          buffer += c;
          break;
        case ']':
          if (inClass) {
            inClass = false;
          }
          buffer += c;
          this.position += 1;
          break;
        case '/':
          this.position += 1;
          if (!inClass) {
            this.scannedValue = buffer;
            return;
          }
          buffer += c;
          break;
        case '\\':
          buffer += c;
          this.position += 1;
          if (isLineTerminator(this.source[this.position])) {
            this.raise('UnterminatedRegExp', this.position);
          }
          buffer += this.source[this.position];
          this.position += 1;
          break;
        default:
          if (isLineTerminator(c)) {
            this.raise('UnterminatedRegExp', this.position);
          }
          this.position += 1;
          buffer += c;
          break;
      }
    }
  }

  scanRegularExpressionFlags() {
    let buffer = '';
    while (true) {
      if (this.position >= this.source.length) {
        this.scannedValue = buffer;
        return;
      }
      const c = this.source[this.position];
      if (isRegularExpressionFlagPart(c)
          && 'dgimsuyv'.includes(c)
          && !buffer.includes(c)) {
        this.position += 1;
        buffer += c;
      } else {
        this.scannedValue = buffer;
        return;
      }
    }
  }
}
