///|
/// An incremental scanner for ASCII dictionary workloads stored as bytes.
pub struct ByteScanner {
  automaton : Automaton
  mut state : Int
  mut position : Int
} derive(Debug)

///|
pub fn ByteScanner::new(automaton : Automaton) -> ByteScanner {
  { automaton, state: 0, position: 0 }
}

///|
/// Scan one byte chunk and emit matches with byte offsets.
pub fn ByteScanner::push(
  self : ByteScanner,
  bytes : Bytes,
) -> Array[LexiconMatch] {
  let output : Array[LexiconMatch] = []
  for byte in bytes {
    let ch = byte.to_char()
    self.state = self.automaton.step(self.state, ch)
    for id in self.automaton.states[self.state].outputs {
      let item = self.automaton.entries[id]
      if is_ascii_entry(item) {
        output.push({
          keyword: item.keyword,
          label: item.label,
          start: self.position + 1 - item.keyword.length(),
          end: self.position + 1,
          weight: item.weight,
        })
      }
    }
    self.position += 1
  }
  output
}

///|
/// Reset a byte scanner at an end-of-stream boundary.
pub fn ByteScanner::finish(self : ByteScanner) -> Array[LexiconMatch] {
  self.state = 0
  self.position = 0
  []
}

///|
/// Scan a complete ASCII byte payload in one pass.
pub fn Automaton::find_ascii_bytes(
  self : Automaton,
  bytes : Bytes,
) -> Array[LexiconMatch] {
  let scanner = ByteScanner::new(self)
  scanner.push(bytes)
}

///|
/// Alias suitable for code that supports both text and byte backends.
pub fn Automaton::find_bytes(
  self : Automaton,
  bytes : Bytes,
) -> Array[LexiconMatch] {
  self.find_ascii_bytes(bytes)
}

///|
/// True when every byte is a seven-bit ASCII value.
pub fn bytes_are_ascii(bytes : Bytes) -> Bool {
  for byte in bytes {
    if byte.to_int() > 127 {
      return false
    }
  }
  true
}

///|
/// Count bytes outside the ASCII range.
pub fn non_ascii_byte_count(bytes : Bytes) -> Int {
  let mut count = 0
  for byte in bytes {
    if byte.to_int() > 127 {
      count += 1
    }
  }
  count
}

///|
/// Return the number of ASCII-compatible entries in an automaton.
pub fn Automaton::ascii_entry_count(self : Automaton) -> Int {
  let mut count = 0
  for item in self.entries {
    if is_ascii_entry(item) {
      count += 1
    }
  }
  count
}

///|
/// Summary of a byte scan.
pub(all) struct ByteScanStats {
  byte_count : Int
  ascii : Bool
  non_ascii_count : Int
  match_count : Int
  distinct_surface_count : Int
  covered_bytes : Int
} derive(Debug, Eq, ToJson)

///|
/// Scan bytes and calculate operational counters.
pub fn scan_byte_stats(automaton : Automaton, bytes : Bytes) -> ByteScanStats {
  let matches = automaton.find_ascii_bytes(bytes)
  {
    byte_count: bytes.length(),
    ascii: bytes_are_ascii(bytes),
    non_ascii_count: non_ascii_byte_count(bytes),
    match_count: matches.length(),
    distinct_surface_count: match_stats(matches).distinct_surface_count,
    covered_bytes: covered_byte_count(bytes.length(), matches),
  }
}

///|
/// Return bytes in a source span, clamped to valid byte boundaries.
pub fn byte_slice(bytes : Bytes, start : Int, end : Int) -> Bytes {
  let safe_start = byte_clamp(start, 0, bytes.length())
  let safe_end = byte_clamp(end, safe_start, bytes.length())
  match bytes.get_view(start=safe_start, end=safe_end) {
    Some(view) => view.to_owned()
    None => Bytes::new(0)
  }
}

///|
/// Convert ASCII bytes to text, returning an empty string for non-ASCII input.
pub fn ascii_bytes_to_string(bytes : Bytes) -> String {
  if !bytes_are_ascii(bytes) {
    return ""
  }
  let output = StringBuilder()
  for byte in bytes {
    output.write_char(byte.to_char())
  }
  output.to_string()
}

///|
/// Count covered byte positions using merged match intervals.
pub fn covered_byte_count(
  byte_length : Int,
  matches : ArrayView[LexiconMatch],
) -> Int {
  covered_characters(byte_length, matches)
}

///|
/// Build an ASCII byte payload from a string, if every character fits.
pub fn string_to_ascii_bytes(text : String) -> Bytes? {
  let output : Array[Byte] = []
  for ch in text {
    if ch.to_int() > 127 {
      return None
    }
    output.push(ch.to_int().to_byte())
  }
  Some(Bytes::from_array(output))
}

///|
/// Return a stable diagnostic for the byte backend.
pub fn byte_backend_description() -> String {
  "ASCII byte scanner: incremental Aho-Corasick with byte offsets"
}

///|
/// Scan multiple byte chunks with one retained automaton state.
pub fn scan_byte_chunks(
  automaton : Automaton,
  chunks : ArrayView[Bytes],
) -> Array[LexiconMatch] {
  let scanner = ByteScanner::new(automaton)
  let output : Array[LexiconMatch] = []
  for chunk in chunks {
    for hit in scanner.push(chunk) {
      output.push(hit)
    }
  }
  output
}

///|
/// Filter byte matches by a label without recompiling the automaton.
pub fn scan_bytes_for_label(
  automaton : Automaton,
  bytes : Bytes,
  label : String,
) -> Array[LexiconMatch] {
  [
    for hit in automaton.find_ascii_bytes(bytes) if hit.label == label => hit
  ]
}

///|
/// Return true when all dictionary keywords can be represented by the byte API.
pub fn Automaton::supports_ascii_backend(self : Automaton) -> Bool {
  self.ascii_entry_count() == self.entries.length()
}

///|
/// Return the first byte position containing a match, if any.
pub fn first_byte_match(automaton : Automaton, bytes : Bytes) -> LexiconMatch? {
  automaton.find_ascii_bytes(bytes).get(0)
}

///|
/// Return a hexadecimal preview for diagnostics.
pub fn bytes_hex_preview(bytes : Bytes, limit : Int) -> String {
  let output = StringBuilder()
  let safe_limit = if limit < 0 { 0 } else { limit }
  for index, byte in bytes {
    if index >= safe_limit {
      break
    }
    if index > 0 {
      output.write_char(' ')
    }
    output.write_string(byte.to_hex())
  }
  output.to_string()
}

///|
fn is_ascii_entry(item : Entry) -> Bool {
  for ch in item.keyword {
    if ch.to_int() > 127 {
      return false
    }
  }
  true
}

///|
fn byte_clamp(value : Int, minimum : Int, maximum : Int) -> Int {
  if value < minimum {
    minimum
  } else if value > maximum {
    maximum
  } else {
    value
  }
}