///| Scanner utilities for markdown parsing

///|
/// Scanner state - uses Array[Char] for O(1) character access
/// Handles Unicode correctly by tracking UTF-16 offsets for non-BMP characters
pub(all) struct Scanner {
  source : String
  chars : Array[Char] // Pre-converted for fast access (code points)
  mut pos : Int // Position in code points
  len : Int // Length in code points
  utf16_offsets : Array[Int]? // Maps code point index -> UTF-16 index (None if all BMP)
}

///|
/// Check if a character is outside BMP (needs surrogate pair in UTF-16)
fn is_non_bmp(c : Char) -> Bool {
  c.to_int() > 0xFFFF
}

///|
/// Create a new scanner
pub fn Scanner::new(source : String) -> Scanner {
  let chars = source.to_array()
  let len = chars.length()

  // Fast check: if UTF-16 length == code point count, all chars are BMP
  // String.length() returns UTF-16 code units
  // chars.length() returns code points
  // Difference means non-BMP characters exist (surrogate pairs)
  let has_non_bmp = source.length() != len

  // Only build UTF-16 offset array if needed (has non-BMP characters)
  let utf16_offsets : Array[Int]? = if has_non_bmp {
    let offsets : Array[Int] = Array::make(len + 1, 0)
    let mut utf16_pos = 0
    for i = 0; i < len; i = i + 1 {
      offsets[i] = utf16_pos
      if is_non_bmp(chars[i]) {
        utf16_pos += 2 // Surrogate pair
      } else {
        utf16_pos += 1
      }
    }
    offsets[len] = utf16_pos
    Some(offsets)
  } else {
    None // All BMP: code point index == UTF-16 index
  }
  { source, chars, pos: 0, len, utf16_offsets }
}

///|
/// Convert code point index to UTF-16 index
fn Scanner::to_utf16_index(self : Scanner, cp_index : Int) -> Int {
  match self.utf16_offsets {
    Some(offsets) => offsets[cp_index]
    None => cp_index // All BMP: same index
  }
}

///|
/// Check if at end of input
pub fn Scanner::is_eof(self : Scanner) -> Bool {
  self.pos >= self.len
}

///|
/// Peek current character (O(1) with Array[Char])
pub fn Scanner::peek(self : Scanner) -> Char? {
  if self.pos >= self.len {
    None
  } else {
    Some(self.chars[self.pos])
  }
}

///|
/// Peek character at offset from current position (O(1))
pub fn Scanner::peek_at(self : Scanner, offset : Int) -> Char? {
  let idx = self.pos + offset
  if idx >= self.len || idx < 0 {
    None
  } else {
    Some(self.chars[idx])
  }
}

///|
/// Advance position by n characters
pub fn Scanner::advance(self : Scanner, n : Int) -> Unit {
  self.pos = self.pos + n
  if self.pos > self.len {
    self.pos = self.len
  }
}

///|
/// Consume and return current character (O(1))
pub fn Scanner::consume(self : Scanner) -> Char? {
  if self.pos >= self.len {
    None
  } else {
    let c = self.chars[self.pos]
    self.pos += 1
    Some(c)
  }
}

///|
/// Get remaining substring from current position
pub fn Scanner::remaining(self : Scanner) -> String {
  if self.pos >= self.len {
    ""
  } else {
    // Convert code point position to UTF-16 position
    let utf16_start = self.to_utf16_index(self.pos)
    let utf16_end = self.to_utf16_index(self.len)
    self.source.unsafe_substring(start=utf16_start, end=utf16_end)
  }
}

///|
/// Get substring from start to end position (code point indices)
pub fn Scanner::substring(self : Scanner, start : Int, end : Int) -> String {
  // Clamp indices to valid range
  let clamped_start = if start < 0 {
    0
  } else if start > self.len {
    self.len
  } else {
    start
  }
  let clamped_end = if end < 0 {
    0
  } else if end > self.len {
    self.len
  } else {
    end
  }
  if clamped_start >= clamped_end {
    ""
  } else {
    // Convert code point positions to UTF-16 positions
    let utf16_start = self.to_utf16_index(clamped_start)
    let utf16_end = self.to_utf16_index(clamped_end)
    self.source.unsafe_substring(start=utf16_start, end=utf16_end)
  }
}

///|
/// Skip whitespace (space and tab only) - O(1) per char
pub fn Scanner::skip_spaces(self : Scanner) -> Int {
  let start = self.pos
  while self.pos < self.len {
    let c = self.chars[self.pos]
    if c == ' ' || c == '\t' {
      self.pos += 1
    } else {
      break
    }
  }
  self.pos - start
}

///|
/// Count leading spaces (without advancing) - O(1) per char
pub fn Scanner::count_leading_spaces(self : Scanner) -> Int {
  let mut count = 0
  let mut idx = self.pos
  while idx < self.len {
    let c = self.chars[idx]
    if c == ' ' {
      count += 1
      idx += 1
    } else if c == '\t' {
      // Tab counts as up to 4 spaces to next multiple of 4
      count = (count / 4 + 1) * 4
      idx += 1
    } else {
      break
    }
  }
  count
}

///|
/// Read until end of line (not consuming newline) - O(1) per char
pub fn Scanner::read_line(self : Scanner) -> String {
  let start = self.pos
  while self.pos < self.len {
    if self.chars[self.pos] == '\n' {
      break
    }
    self.pos += 1
  }
  // Convert code point positions to UTF-16 positions
  let utf16_start = self.to_utf16_index(start)
  let utf16_end = self.to_utf16_index(self.pos)
  self.source.unsafe_substring(start=utf16_start, end=utf16_end)
}

///|
/// Skip to next line (consuming newline if present) - O(1) per char
pub fn Scanner::skip_line(self : Scanner) -> Unit {
  while self.pos < self.len {
    let c = self.chars[self.pos]
    self.pos += 1
    if c == '\n' {
      break
    }
  }
}

///|
/// Check if current line is blank (only whitespace) - O(1) per char
pub fn Scanner::is_blank_line(self : Scanner) -> Bool {
  let mut idx = self.pos
  while idx < self.len {
    let c = self.chars[idx]
    if c == '\n' {
      return true
    }
    if c != ' ' && c != '\t' {
      return false
    }
    idx += 1
  }
  true
}

///|
/// Match a string at current position - optimized with Array[Char]
pub fn Scanner::matches(self : Scanner, s : String) -> Bool {
  let s_len = s.length()
  if self.pos + s_len > self.len {
    return false
  }
  // Note: s.get_char still O(n), but s is usually short (e.g. "---", "```")
  for i = 0; i < s_len; i = i + 1 {
    match s.get_char(i) {
      Some(b) if self.chars[self.pos + i] == b => continue
      _ => return false
    }
  }
  true
}

///|
/// Match and consume a string
pub fn Scanner::consume_str(self : Scanner, s : String) -> Bool {
  if self.matches(s) {
    self.pos += s.length()
    true
  } else {
    false
  }
}

///|
/// Count consecutive occurrences of a character from current position - O(1) per char
pub fn Scanner::count_char(self : Scanner, c : Char) -> Int {
  let mut count = 0
  let mut idx = self.pos
  while idx < self.len {
    if self.chars[idx] == c {
      count += 1
      idx += 1
    } else {
      break
    }
  }
  count
}

///|
/// Save current position
pub fn Scanner::save(self : Scanner) -> Int {
  self.pos
}

///|
/// Restore to saved position
pub fn Scanner::restore(self : Scanner, pos : Int) -> Unit {
  self.pos = pos
}

// =============================================================================
// Character utilities
// =============================================================================

///|
/// Check if character is ASCII whitespace
pub fn is_whitespace(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r'
}

///|
/// Check if character is a digit
pub fn is_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
/// Check if character is ASCII letter
pub fn is_letter(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

///|
/// Check if character is alphanumeric
pub fn is_alphanumeric(c : Char) -> Bool {
  is_digit(c) || is_letter(c)
}

///|
/// Check if character is a punctuation mark
pub fn is_punctuation(c : Char) -> Bool {
  match c {
    '!'
    | '"'
    | '#'
    | '$'
    | '%'
    | '&'
    | '\''
    | '('
    | ')'
    | '*'
    | '+'
    | ','
    | '-'
    | '.'
    | '/'
    | ':'
    | ';'
    | '<'
    | '='
    | '>'
    | '?'
    | '@'
    | '['
    | '\\'
    | ']'
    | '^'
    | '_'
    | '`'
    | '{'
    | '|'
    | '}'
    | '~' => true
    _ => false
  }
}

///|
/// Check if char option matches specific char
pub fn char_is(opt : Char?, c : Char) -> Bool {
  match opt {
    Some(ch) => ch == c
    None => false
  }
}

///|
/// Check if char option is a digit
pub fn char_is_digit(opt : Char?) -> Bool {
  match opt {
    Some(c) => is_digit(c)
    None => false
  }
}