///|
pub(all) struct Loc {
  line : Int
  col : Int
  offset : Int
} derive(Eq, Debug)

///|
pub impl Show for Loc with fn output(self, logger) {
  Debug::to_repr(self).output(logger)
}

///|
pub fn Loc::default() -> Loc noraise {
  { line: 1, col: 1, offset: 0 }
}

///|
/// A half-open source range expressed in JavaScript source coordinates.
pub struct SourceSpan {
  start_ : Loc
  end_ : Loc
} derive(Eq, Debug)

///|
fn SourceSpan::SourceSpan(start : Loc, end : Loc) -> SourceSpan {
  { start_: start, end_: end }
}

///|
pub fn SourceSpan::start(self : SourceSpan) -> Loc {
  self.start_
}

///|
pub fn SourceSpan::end(self : SourceSpan) -> Loc {
  self.end_
}

///|
pub(all) enum LexForm {
  LexNormal
  StringLegacyOctalEscape
  NumberLegacyOctalInt
  NumberNonOctalDecimalInt
} derive(Eq, Debug)

///|
pub impl Show for LexForm with fn output(self, logger) {
  Debug::to_repr(self).output(logger)
}

///|
pub(all) enum TokenKind {
  // Literals
  Number(Double)
  String_(String)
  True
  False
  Null
  Undefined
  // Identifier
  Ident(String)
  // Private identifier (#name)
  PrivateName(String)
  // Keywords
  Let
  Const
  Var
  Function
  Return
  If
  Else
  While
  For
  Break
  Continue
  Typeof
  Throw
  Try
  Catch
  Finally
  New
  This
  Switch
  Case
  Default
  Void
  Delete
  Do
  In
  Instanceof
  // Class-related
  Class
  Extends
  Super
  Static
  Get
  Set
  // Generator-related
  Yield
  // Module-related
  Import
  Export
  From
  As
  // Operators
  Plus
  Minus
  Star
  StarStar
  Slash
  Percent
  Assign
  EqEq
  EqEqEq
  BangEq
  BangEqEq
  Lt
  Gt
  LtEq
  GtEq
  And
  Or
  QuestionQuestion // ??
  QuestionDot // ?.
  Bang
  // Update / Compound assignment
  PlusPlus
  MinusMinus
  PlusAssign
  MinusAssign
  StarAssign
  StarStarAssign
  SlashAssign
  PercentAssign
  // Bitwise
  BitAnd
  BitOr
  BitXor
  Tilde
  LShift
  RShift
  URShift
  // Compound bitwise assignment
  BitAndAssign
  BitOrAssign
  BitXorAssign
  LShiftAssign
  RShiftAssign
  URShiftAssign
  // Logical assignment
  AndAssign // &&=
  OrAssign // ||=
  NullishAssign // ??=
  // Delimiters
  LParen
  RParen
  LBrace
  RBrace
  LBracket
  RBracket
  Comma
  Semicolon
  Dot
  Colon
  Question
  // Template literals — (raw, cooked) where cooked is None for invalid escapes
  NoSubTemplate(String, String?)
  TemplateHead(String, String?)
  TemplateMiddle(String, String?)
  TemplateTail(String, String?)
  // Arrow
  Arrow
  // Spread/Rest
  DotDotDot
  // Regex
  Regex(String, String) // pattern, flags
  // Special
  Of
  EOF
} derive(Eq, Debug)

///|
pub impl Show for TokenKind with fn output(self, logger) {
  Debug::to_repr(self).output(logger)
}

///|
pub(all) struct Token {
  kind : TokenKind
  loc : Loc
  raw : String
  lex_form : LexForm
  end_offset : Int
} derive(Eq, Debug)

///|
pub impl Show for Token with fn output(self, logger) {
  Debug::to_repr(self).output(logger)
}

///|
/// `end_offset` is the exclusive UTF-16 code-unit offset in the original source
/// string at which this token ends. When -1 (the default for call sites that
/// don't track source position, e.g. tests), it is derived as
/// `loc.offset + raw.length()`, which is correct for all tokens whose `raw`
/// field is the verbatim source slice. The lexer always supplies an explicit
/// value.
pub fn Token::Token(
  kind : TokenKind,
  loc : Loc,
  raw : String,
  lex_form? : LexForm = LexNormal,
  end_offset? : Int = -1,
) -> Token noraise {
  let end_offset = if end_offset == -1 {
    loc.offset + raw.length()
  } else {
    end_offset
  }
  { kind, loc, raw, lex_form, end_offset }
}

///|
pub fn Token::new(
  kind : TokenKind,
  loc : Loc,
  raw : String,
  lex_form? : LexForm = LexNormal,
  end_offset? : Int = -1,
) -> Token noraise {
  Token(kind, loc, raw, lex_form~, end_offset~)
}

///|
pub fn Token::eof(loc : Loc) -> Token noraise {
  Token(EOF, loc, "", end_offset=loc.offset)
}

///|
/// Resolve this token's half-open source range. Offsets are UTF-16 code units,
/// columns count Unicode scalar values, and CRLF is treated as one line break.
/// Returns `None` when the token offsets do not describe a slice of `source`.
pub fn Token::source_span(self : Token, source : String) -> SourceSpan? {
  guard source.get_view(start=self.loc.offset, end=self.end_offset)
    is Some(token_source) else {
    return None
  }
  let (line, col, _) = token_source.fold(
    init=(self.loc.line, self.loc.col, false),
    (state, char) => {
      let (line, col, previous_was_cr) = state
      if previous_was_cr && char == '\n' {
        (line, col, false)
      } else {
        match char.to_int() {
          0x0A | 0x0D | 0x2028 | 0x2029 => (line + 1, 1, char == '\r')
          _ => (line, col + 1, false)
        }
      }
    },
  )
  Some(SourceSpan(self.loc, { line, col, offset: self.end_offset }))
}