///| Scanner utilities for markdown parsing

///|
/// Scanner state - uses UTF-16 indexing directly for BMP input and Array[Char]
/// for non-BMP input. Handles Unicode correctly by tracking UTF-16 offsets for
/// non-BMP characters.
pub(all) struct Scanner {
  source : String
  chars : Array[Char] // Empty for BMP input, pre-converted code points for non-BMP
  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
}

///|
fn has_surrogate_pair(source : String) -> Bool {
  let len = source.length()
  let mut idx = 0
  while idx + 1 < len {
    let c = source.unsafe_get(idx)
    if c.is_leading_surrogate() &&
      source.unsafe_get(idx + 1).is_trailing_surrogate() {
      return true
    }
    idx += 1
  }
  false
}

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

  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
  { source, chars, pos: 0, len, utf16_offsets: Some(offsets) }
}

///|
fn Scanner::new_bmp(source : String) -> Scanner {
  let chars : Array[Char] = []
  { source, chars, pos: 0, len: source.length(), utf16_offsets: None }
}

///|
fn Scanner::is_bmp(self : Scanner) -> Bool {
  self.utf16_offsets is None
}

///|
/// 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
  }
}

///|
fn Scanner::unsafe_char_at(self : Scanner, index : Int) -> Char {
  match self.utf16_offsets {
    Some(_) => self.chars[index]
    None => self.source.unsafe_get(index).unsafe_to_char()
  }
}

///|
fn Scanner::move_to_line_end(self : Scanner) -> (Int, Int) {
  let start = self.pos
  match self.utf16_offsets {
    Some(_) =>
      while self.pos < self.len {
        if self.chars[self.pos] == '\n' {
          break
        }
        self.pos += 1
      }
    None =>
      while self.pos < self.len {
        if self.source.unsafe_get(self.pos) == 0x0A {
          break
        }
        self.pos += 1
      }
  }
  (start, self.pos)
}

///|
fn Scanner::write_range_to(
  self : Scanner,
  buf : StringBuilder,
  start : Int,
  end : Int,
) -> Unit {
  if end <= start {
    return
  }
  let utf16_start = self.to_utf16_index(start)
  let utf16_end = self.to_utf16_index(end)
  buf.write_view(self.source[utf16_start:utf16_end])
}

///|
fn Scanner::append_line_to(self : Scanner, buf : StringBuilder) -> Unit {
  let (start, end) = self.move_to_line_end()
  self.write_range_to(buf, start, end)
}

///|
fn Scanner::append_line_stripped_indent_to(
  self : Scanner,
  buf : StringBuilder,
  indent : Int,
) -> Unit {
  let (start, end) = self.move_to_line_end()
  let mut stripped = 0
  let mut idx = start
  while idx < end && stripped < indent {
    match self.unsafe_char_at(idx) {
      ' ' => {
        stripped += 1
        idx += 1
      }
      '\t' => {
        stripped += 4
        idx += 1
      }
      _ => break
    }
  }
  self.write_range_to(buf, idx, end)
}

///|
fn Scanner::line_contains(self : Scanner, needle : String) -> Bool {
  let needle_len = needle.length()
  if needle_len == 0 {
    return true
  }
  match self.utf16_offsets {
    Some(_) => {
      let mut idx = self.pos
      while idx + needle_len <= self.len && self.chars[idx] != '\n' {
        let mut matched = true
        for j = 0; j < needle_len; j = j + 1 {
          if idx + j >= self.len || self.chars[idx + j] == '\n' {
            matched = false
            break
          }
          let code = self.chars[idx + j].to_int()
          if code > 0xFFFF || code.to_uint16() != needle.unsafe_get(j) {
            matched = false
            break
          }
        }
        if matched {
          return true
        }
        idx += 1
      }
    }
    None => {
      let mut idx = self.pos
      while idx + needle_len <= self.len && self.source.unsafe_get(idx) != 0x0A {
        let mut matched = true
        for j = 0; j < needle_len; j = j + 1 {
          if idx + j >= self.len || self.source.unsafe_get(idx + j) == 0x0A {
            matched = false
            break
          }
          if self.source.unsafe_get(idx + j) != needle.unsafe_get(j) {
            matched = false
            break
          }
        }
        if matched {
          return true
        }
        idx += 1
      }
    }
  }
  false
}

///|
/// 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 {
    match self.utf16_offsets {
      Some(_) => Some(self.chars[self.pos])
      None => Some(self.source.unsafe_get(self.pos).unsafe_to_char())
    }
  }
}

///|
/// 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 {
    match self.utf16_offsets {
      Some(_) => Some(self.chars[idx])
      None => Some(self.source.unsafe_get(idx).unsafe_to_char())
    }
  }
}

///|
/// 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 = match self.utf16_offsets {
      Some(_) => self.chars[self.pos]
      None => self.source.unsafe_get(self.pos).unsafe_to_char()
    }
    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
  match self.utf16_offsets {
    Some(_) =>
      while self.pos < self.len {
        let c = self.chars[self.pos]
        if c == ' ' || c == '\t' {
          self.pos += 1
        } else {
          break
        }
      }
    None =>
      while self.pos < self.len {
        let c = self.source.unsafe_get(self.pos)
        if c == 0x20 || c == 0x09 {
          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
  match self.utf16_offsets {
    Some(_) =>
      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
        }
      }
    None =>
      while idx < self.len {
        let c = self.source.unsafe_get(idx)
        if c == 0x20 {
          count += 1
          idx += 1
        } else if c == 0x09 {
          // 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
  match self.utf16_offsets {
    Some(_) =>
      while self.pos < self.len {
        if self.chars[self.pos] == '\n' {
          break
        }
        self.pos += 1
      }
    None =>
      while self.pos < self.len {
        if self.source.unsafe_get(self.pos) == 0x0A {
          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 {
  match self.utf16_offsets {
    Some(_) =>
      while self.pos < self.len {
        let c = self.chars[self.pos]
        self.pos += 1
        if c == '\n' {
          break
        }
      }
    None =>
      while self.pos < self.len {
        let c = self.source.unsafe_get(self.pos)
        self.pos += 1
        if c == 0x0A {
          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
  match self.utf16_offsets {
    Some(_) =>
      while idx < self.len {
        let c = self.chars[idx]
        if c == '\n' {
          return true
        }
        if c != ' ' && c != '\t' {
          return false
        }
        idx += 1
      }
    None =>
      while idx < self.len {
        let c = self.source.unsafe_get(idx)
        if c == 0x0A {
          return true
        }
        if c != 0x20 && c != 0x09 {
          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.unsafe_char_at(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
  match self.utf16_offsets {
    Some(_) =>
      while idx < self.len {
        if self.chars[idx] == c {
          count += 1
          idx += 1
        } else {
          break
        }
      }
    None => {
      if c.to_int() > 0xFFFF {
        return 0
      }
      let code = c.to_int().to_uint16()
      while idx < self.len {
        if self.source.unsafe_get(idx) == code {
          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
  }
}