import std/[strutils, strformat]
import token, source_location

type
  LexerDiagnosticSeverity* = enum
    ldsWarning
    ldsError

  LexerDiagnostic* = object
    severity*: LexerDiagnosticSeverity
    loc*: SourceLocation
    message*: string

  LexerResult* = object
    tokens*: seq[Token]
    diagnostics*: seq[LexerDiagnostic]

proc hasErrors*(res: LexerResult): bool =
  for d in res.diagnostics:
    if d.severity == ldsError:
      return true
  return false

proc `$`*(d: LexerDiagnostic): string =
  let sev = if d.severity == ldsError: "error" else: "warning"
  result = &"{sev}: {d.message} at {d.loc}"

type
  Lexer* = object
    source: string
    sourceName: string
    pos: int
    line: uint32
    col: uint32
    tokens: seq[Token]
    diagnostics: seq[LexerDiagnostic]

proc initLexer*(source, sourceName: string): Lexer =
  result.source = source
  result.sourceName = sourceName
  result.pos = 0
  result.line = 1
  result.col = 1

proc isAtEnd(lex: Lexer): bool =
  lex.pos >= lex.source.len

proc peek(lex: Lexer, ahead: int = 0): char =
  let i = lex.pos + ahead
  if i < lex.source.len:
    return lex.source[i]
  return '\0'

proc advance(lex: var Lexer): char =
  result = lex.peek()
  if not lex.isAtEnd():
    inc lex.pos
    if result == '\n':
      inc lex.line
      lex.col = 1
    else:
      inc lex.col

proc matchStr(lex: var Lexer, s: string): bool =
  for i, c in s:
    if lex.peek(i) != c:
      return false
  for _ in s:
    discard lex.advance()
  return true

proc currentLocation(lex: Lexer): SourceLocation =
  result = SourceLocation(line: lex.line, column: lex.col, offset: uint32(lex.pos),
                          file: lex.sourceName)

proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
  lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))

proc makeToken(lex: Lexer, kind: TokenKind, startLoc: SourceLocation, startPos: int): Token =
  let text = lex.source[startPos ..< lex.pos]
  result = Token(kind: kind, text: text, loc: startLoc)

# ---------------------------------------------------------------------------
# Whitespace / comments
# ---------------------------------------------------------------------------

proc skipLineComment(lex: var Lexer) =
  while not lex.isAtEnd() and lex.peek() != '\n':
    discard lex.advance()

proc skipBlockComment(lex: var Lexer) =
  let startLoc = lex.currentLocation()
  var depth = 1
  while not lex.isAtEnd() and depth > 0:
    if lex.peek() == '/' and lex.peek(1) == '*':
      discard lex.advance()
      discard lex.advance()
      inc depth
    elif lex.peek() == '*' and lex.peek(1) == '/':
      discard lex.advance()
      discard lex.advance()
      dec depth
    else:
      discard lex.advance()
  if depth > 0:
    lex.emitError(startLoc, "unterminated block comment")

proc skipWhitespace(lex: var Lexer) =
  while not lex.isAtEnd():
    let c = lex.peek()
    if c in {' ', '\t', '\r'}:
      discard lex.advance()
    elif c == '/' and lex.peek(1) == '/':
      lex.skipLineComment()
    elif c == '/' and lex.peek(1) == '*':
      discard lex.advance()
      discard lex.advance()
      lex.skipBlockComment()
    else:
      break

# ---------------------------------------------------------------------------
# Identifiers
# ---------------------------------------------------------------------------

proc isIdentStart(c: char): bool =
  c in {'a'..'z', 'A'..'Z', '_'}

proc isIdentChar(c: char): bool =
  c in {'a'..'z', 'A'..'Z', '0'..'9', '_'}

proc scanIdent(lex: var Lexer, startLoc: SourceLocation): Token =
  let startPos = lex.pos
  while not lex.isAtEnd() and isIdentChar(lex.peek()):
    discard lex.advance()
  let text = lex.source[startPos ..< lex.pos]
  var kind = keywordKind(text)
  if text == "_":
    kind = tkUnderscore
  result = Token(kind: kind, text: text, loc: startLoc)

# ---------------------------------------------------------------------------
# Numbers
# ---------------------------------------------------------------------------

proc scanIntSuffix(lex: var Lexer) =
  # i8, i16, i32, i64, u8, u16, u32, u64, f32, f64
  if lex.isAtEnd(): return
  let c = lex.peek()
  if c in {'i', 'u', 'f'}:
    discard lex.advance()
    while not lex.isAtEnd() and lex.peek() in {'0'..'9'}:
      discard lex.advance()

proc scanHexDigits(lex: var Lexer) =
  while not lex.isAtEnd() and lex.peek() in {'0'..'9', 'a'..'f', 'A'..'F'}:
    discard lex.advance()

proc scanBinDigits(lex: var Lexer) =
  while not lex.isAtEnd() and lex.peek() in {'0', '1'}:
    discard lex.advance()

proc scanOctDigits(lex: var Lexer) =
  while not lex.isAtEnd() and lex.peek() in {'0'..'7'}:
    discard lex.advance()

proc scanDecDigits(lex: var Lexer) =
  while not lex.isAtEnd() and lex.peek() in {'0'..'9'}:
    discard lex.advance()

proc scanNumber(lex: var Lexer, startLoc: SourceLocation): Token =
  let startPos = lex.pos
  var isFloat = false

  if lex.peek() == '0' and lex.peek(1) in {'x', 'X', 'b', 'B', 'o', 'O'}:
    discard lex.advance()  # '0'
    let prefix = lex.advance()
    case prefix
    of 'x', 'X': lex.scanHexDigits()
    of 'b', 'B': lex.scanBinDigits()
    of 'o', 'O': lex.scanOctDigits()
    else: discard
    lex.scanIntSuffix()
    return lex.makeToken(tkIntLiteral, startLoc, startPos)

  lex.scanDecDigits()

  # Fractional part
  if lex.peek() == '.' and lex.peek(1) in {'0'..'9'}:
    isFloat = true
    discard lex.advance()  # '.'
    lex.scanDecDigits()

  # Exponent
  if lex.peek() in {'e', 'E'}:
    isFloat = true
    discard lex.advance()
    if lex.peek() in {'+', '-'}:
      discard lex.advance()
    if lex.peek() notin {'0'..'9'}:
      lex.emitError(lex.currentLocation(), "expected digits in exponent")
    else:
      lex.scanDecDigits()

  if isFloat:
    # Optional f32/f64 suffix
    if lex.peek() == 'f' and lex.peek(1) in {'3', '6'}:
      discard lex.advance()
      discard lex.advance()
    result = lex.makeToken(tkFloatLiteral, startLoc, startPos)
  else:
    lex.scanIntSuffix()
    result = lex.makeToken(tkIntLiteral, startLoc, startPos)

# ---------------------------------------------------------------------------
# Strings and chars
# ---------------------------------------------------------------------------

proc scanEscapeSequence(lex: var Lexer): string =
  if lex.isAtEnd():
    return ""
  let c = lex.advance()
  case c
  of '\\': result = "\\"
  of '"': result = "\""
  of '\'': result = "\'"
  of 'n': result = "\n"
  of 'r': result = "\r"
  of 't': result = "\t"
  of '0': result = "\0"
  of '{', '}':
    # Preserve \{ and \} so f"..." interpolation can treat them as literal braces
    result = "\\" & $c
  of 'x':
    var hexVal = ""
    for _ in 0..<2:
      if lex.isAtEnd() or lex.peek() notin {'0'..'9', 'a'..'f', 'A'..'F'}:
        lex.emitError(lex.currentLocation(), "expected two hex digits after \\x")
        break
      hexVal.add(lex.advance())
    if hexVal.len == 2:
      try:
        let code = parseHexInt(hexVal)
        result = $chr(code)
      except ValueError:
        lex.emitError(lex.currentLocation(), &"invalid hex escape \\x{hexVal}")
        result = ""
    else:
      result = ""
  else:
    lex.emitError(lex.currentLocation(), &"unknown escape sequence \\\\{c}")
    result = $c

proc scanString(lex: var Lexer, startLoc: SourceLocation, prefixLen: int): Token =
  let startPos = lex.pos - prefixLen
  # prefixLen characters before the opening quote were already consumed by caller
  # but we need to handle the quote itself
  # Collect resolved string content to properly handle escape sequences
  var resolved = ""
  if lex.peek() == '"':
    discard lex.advance()
  while not lex.isAtEnd() and lex.peek() != '"':
    if lex.peek() == '\n':
      lex.emitError(lex.currentLocation(), "unterminated string literal")
      break
    if lex.peek() == '\\':
      discard lex.advance()
      resolved.add(lex.scanEscapeSequence())
    else:
      resolved.add(lex.advance())
  if lex.isAtEnd():
    lex.emitError(startLoc, "unterminated string literal")
  else:
    discard lex.advance()  # closing "
  # Rebuild text with resolved escapes: prefix + " + resolved + "
  var text = lex.source[startPos ..< startPos + prefixLen]
  text.add('"')
  text.add(resolved)
  text.add('"')
  result = Token(kind: tkStringLiteral, text: text, loc: startLoc)

proc scanBacktickString(lex: var Lexer, startLoc: SourceLocation): Token =
  ## Scan a backtick-delimited raw string literal: content is literal,
  ## no escape processing, newlines are preserved.
  let startPos = lex.pos - 1  # include the opening backtick
  while not lex.isAtEnd() and lex.peek() != '`':
    discard lex.advance()
  if lex.isAtEnd():
    lex.emitError(startLoc, "unterminated backtick string literal")
  else:
    discard lex.advance()  # closing backtick
  result = lex.makeToken(tkStringLiteral, startLoc, startPos)

proc scanChar(lex: var Lexer, startLoc: SourceLocation, prefixLen: int): Token =
  let startPos = lex.pos - prefixLen
  # Collect resolved char content to properly handle escape sequences
  var resolved = ""
  if lex.peek() == '\'':
    discard lex.advance()
  if lex.isAtEnd():
    lex.emitError(startLoc, "unterminated char literal")
  elif lex.peek() == '\n':
    lex.emitError(lex.currentLocation(), "newline in char literal")
  elif lex.peek() == '\\':
    discard lex.advance()
    resolved.add(lex.scanEscapeSequence())
  else:
    resolved.add(lex.advance())
  if lex.isAtEnd() or lex.peek() != '\'':
    lex.emitError(lex.currentLocation(), "expected closing ' for char literal")
  else:
    discard lex.advance()
  # Rebuild text with resolved escape: prefix + ' + resolved + '
  var text = lex.source[startPos ..< startPos + prefixLen]
  text.add('\'')
  text.add(resolved)
  text.add('\'')
  result = Token(kind: tkCharLiteral, text: text, loc: startLoc)

# ---------------------------------------------------------------------------
# Symbols / operators
# ---------------------------------------------------------------------------

proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
  let startPos = lex.pos
  let c1 = lex.advance()

  template check2(c2: char, kind2: TokenKind, kind1: TokenKind) =
    if lex.peek() == c2:
      discard lex.advance()
      return lex.makeToken(kind2, startLoc, startPos)
    else:
      return lex.makeToken(kind1, startLoc, startPos)

  case c1
  of '(': return lex.makeToken(tkLParen, startLoc, startPos)
  of ')': return lex.makeToken(tkRParen, startLoc, startPos)
  of '{': return lex.makeToken(tkLBrace, startLoc, startPos)
  of '}': return lex.makeToken(tkRBrace, startLoc, startPos)
  of '[': return lex.makeToken(tkLBracket, startLoc, startPos)
  of ']': return lex.makeToken(tkRBracket, startLoc, startPos)
  of ',': return lex.makeToken(tkComma, startLoc, startPos)
  of ';': return lex.makeToken(tkSemicolon, startLoc, startPos)
  of '@': return lex.makeToken(tkAt, startLoc, startPos)
  of '?': return lex.makeToken(tkQuestion, startLoc, startPos)
  of '~': return lex.makeToken(tkTilde, startLoc, startPos)
  of ':': check2(':', tkColonColon, tkColon)
  of '.':
    if lex.peek() == '.' and lex.peek(1) == '.':
      discard lex.advance()
      discard lex.advance()
      return lex.makeToken(tkDotDotDot, startLoc, startPos)
    elif lex.peek() == '.' and lex.peek(1) == '=':
      discard lex.advance()
      discard lex.advance()
      return lex.makeToken(tkDotDotEqual, startLoc, startPos)
    elif lex.peek() == '.':
      discard lex.advance()
      return lex.makeToken(tkDotDot, startLoc, startPos)
    else:
      return lex.makeToken(tkDot, startLoc, startPos)
  of '-':
    if lex.peek() == '>':
      discard lex.advance()
      return lex.makeToken(tkArrow, startLoc, startPos)
    elif lex.peek() == '-':
      discard lex.advance()
      return lex.makeToken(tkMinusMinus, startLoc, startPos)
    elif lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkMinusAssign, startLoc, startPos)
    else:
      return lex.makeToken(tkMinus, startLoc, startPos)
  of '+':
    if lex.peek() == '+':
      discard lex.advance()
      return lex.makeToken(tkPlusPlus, startLoc, startPos)
    elif lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkPlusAssign, startLoc, startPos)
    else:
      return lex.makeToken(tkPlus, startLoc, startPos)
  of '*':
    if lex.peek() == '*':
      discard lex.advance()
      return lex.makeToken(tkStarStar, startLoc, startPos)
    elif lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkStarAssign, startLoc, startPos)
    else:
      return lex.makeToken(tkStar, startLoc, startPos)
  of '/':
    if lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkSlashAssign, startLoc, startPos)
    else:
      return lex.makeToken(tkSlash, startLoc, startPos)
  of '%':
    if lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkPercentAssign, startLoc, startPos)
    else:
      return lex.makeToken(tkPercent, startLoc, startPos)
  of '=':
    if lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkEq, startLoc, startPos)
    elif lex.peek() == '>':
      discard lex.advance()
      return lex.makeToken(tkFatArrow, startLoc, startPos)
    else:
      return lex.makeToken(tkAssign, startLoc, startPos)
  of '!':
    if lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkNe, startLoc, startPos)
    else:
      return lex.makeToken(tkBang, startLoc, startPos)
  of '<':
    if lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkLe, startLoc, startPos)
    elif lex.peek() == '<':
      discard lex.advance()
      if lex.peek() == '=':
        discard lex.advance()
        return lex.makeToken(tkShlAssign, startLoc, startPos)
      return lex.makeToken(tkShl, startLoc, startPos)
    else:
      return lex.makeToken(tkLt, startLoc, startPos)
  of '>':
    if lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkGe, startLoc, startPos)
    elif lex.peek() == '>':
      discard lex.advance()
      if lex.peek() == '=':
        discard lex.advance()
        return lex.makeToken(tkShrAssign, startLoc, startPos)
      return lex.makeToken(tkShr, startLoc, startPos)
    else:
      return lex.makeToken(tkGt, startLoc, startPos)
  of '&':
    if lex.peek() == '&':
      discard lex.advance()
      return lex.makeToken(tkAmpAmp, startLoc, startPos)
    elif lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkAmpAssign, startLoc, startPos)
    else:
      return lex.makeToken(tkAmp, startLoc, startPos)
  of '|':
    if lex.peek() == '|':
      discard lex.advance()
      return lex.makeToken(tkPipePipe, startLoc, startPos)
    elif lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkPipeAssign, startLoc, startPos)
    else:
      return lex.makeToken(tkPipe, startLoc, startPos)
  of '^':
    if lex.peek() == '=':
      discard lex.advance()
      return lex.makeToken(tkCaretAssign, startLoc, startPos)
    else:
      return lex.makeToken(tkCaret, startLoc, startPos)
  of '#':
    # Check for intrinsics: #line, #column, #file, #function, #date, #time, #module
    let afterHash = lex.peek()
    if afterHash == 'l' and lex.matchStr("line"):
      return lex.makeToken(tkHashLine, startLoc, startPos)
    elif afterHash == 'c' and lex.matchStr("column"):
      return lex.makeToken(tkHashColumn, startLoc, startPos)
    elif afterHash == 'f' and lex.peek(1) == 'i' and lex.peek(2) == 'l' and lex.peek(3) == 'e':
      discard lex.matchStr("file")
      return lex.makeToken(tkHashFile, startLoc, startPos)
    elif afterHash == 'f' and lex.peek(1) == 'u' and lex.peek(2) == 'n':
      discard lex.matchStr("function")
      return lex.makeToken(tkHashFunction, startLoc, startPos)
    elif afterHash == 'd' and lex.matchStr("date"):
      return lex.makeToken(tkHashDate, startLoc, startPos)
    elif afterHash == 't' and lex.matchStr("time"):
      return lex.makeToken(tkHashTime, startLoc, startPos)
    elif afterHash == 'm' and lex.matchStr("module"):
      return lex.makeToken(tkHashModule, startLoc, startPos)
    elif afterHash == 'e' and lex.matchStr("emit"):
      return lex.makeToken(tkHashEmit, startLoc, startPos)
    else:
      return lex.makeToken(tkHash, startLoc, startPos)
  else:
    lex.emitError(startLoc, &"unexpected character '{c1}'")
    return lex.makeToken(tkUnknown, startLoc, startPos)

# ---------------------------------------------------------------------------
# Main scanning loop
# ---------------------------------------------------------------------------

proc nextToken(lex: var Lexer): Token =
  lex.skipWhitespace()
  let startLoc = lex.currentLocation()
  let startPos = lex.pos

  if lex.isAtEnd():
    return Token(kind: tkEndOfFile, text: "", loc: startLoc)

  let c = lex.peek()

  if c == '\n':
    discard lex.advance()
    return Token(kind: tkNewLine, text: "\n", loc: startLoc)

  # String prefixes: f" c8" c16" c32" — must come before ident check
  if c == 'f' and lex.peek(1) == '"':
    discard lex.advance()  # f
    return lex.scanString(startLoc, 1)
  if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
    let d = lex.peek(1)
    if d == '8' and lex.peek(2) == '"':
      discard lex.advance()  # c
      discard lex.advance()  # 8
      return lex.scanString(startLoc, 2)
    elif d == '1' and lex.peek(2) == '6' and lex.peek(3) == '"':
      discard lex.advance()
      discard lex.advance()
      discard lex.advance()
      return lex.scanString(startLoc, 3)
    elif d == '3' and lex.peek(2) == '2' and lex.peek(3) == '"':
      discard lex.advance()
      discard lex.advance()
      discard lex.advance()
      return lex.scanString(startLoc, 3)

  if c == '"':
    return lex.scanString(startLoc, 0)

  # Backtick-delimited raw string: `...`
  if c == '`':
    discard lex.advance()
    return lex.scanBacktickString(startLoc)

  # Char prefixes: c8' c16' c32' — must come before ident check
  if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
    let d = lex.peek(1)
    if d == '8' and lex.peek(2) == '\'':
      discard lex.advance()
      discard lex.advance()
      return lex.scanChar(startLoc, 2)
    elif d == '1' and lex.peek(2) == '6' and lex.peek(3) == '\'':
      discard lex.advance()
      discard lex.advance()
      discard lex.advance()
      return lex.scanChar(startLoc, 3)
    elif d == '3' and lex.peek(2) == '2' and lex.peek(3) == '\'':
      discard lex.advance()
      discard lex.advance()
      discard lex.advance()
      return lex.scanChar(startLoc, 3)

  if c == '\'':
    # Check if this is a lifetime (e.g., 'a) or char literal (e.g., 'a')
    # Lifetime: ' followed by identifier chars, no closing '
    # Char literal: ' followed by one char/escape, then closing '
    let afterQuote = lex.peek(1)
    if isIdentStart(afterQuote):
      # Could be lifetime or char literal like 'x'
      # If next char after ident start is NOT ', it's a lifetime
      # (for char literals, the char/escape is consumed and then ')
      # Simple heuristic: if peek(2) is ', it's a char literal; else lifetime
      if lex.peek(2) == '\'':
        return lex.scanChar(startLoc, 0)
      elif lex.peek(2) == '\0':
        return lex.scanChar(startLoc, 0)
      else:
        # Lifetime: consume ' and then identifier chars
        discard lex.advance()  # '
        while not lex.isAtEnd() and isIdentChar(lex.peek()):
          discard lex.advance()
        return lex.makeToken(tkLifetime, startLoc, startPos)
    else:
      return lex.scanChar(startLoc, 0)

  if isIdentStart(c):
    return lex.scanIdent(startLoc)

  if c in {'0'..'9'}:
    return lex.scanNumber(startLoc)

  return lex.scanSymbol(startLoc)

proc tokenize*(lex: var Lexer): LexerResult =
  while true:
    let tok = lex.nextToken()
    lex.tokens.add(tok)
    if tok.kind == tkEndOfFile:
      break
  result = LexerResult(tokens: lex.tokens, diagnostics: lex.diagnostics)

proc tokenize*(source, sourceName: string): LexerResult =
  var lex = initLexer(source, sourceName)
  result = lex.tokenize()

proc dumpTokens*(res: LexerResult): string =
  for tok in res.tokens:
    result.add($tok & "\n")
