///|
/// Character-filter output plus an offset-correction table. Each entry maps an
/// output UTF-8 byte boundary back to the corresponding input byte offset.
pub struct CharFilterResult {
  text : String
  corrections : ReadOnlyArray[Int]
}

///|
pub fn CharFilterResult::text(self : CharFilterResult) -> String {
  self.text
}

///|
pub(open) trait CharFilter {
  fn apply(Self, String) -> CharFilterResult
}

///|
fn analyzer_utf8_width(ch : Char) -> Int {
  let value = ch.to_int()
  if value <= 0x7F {
    1
  } else if value <= 0x7FF {
    2
  } else if value <= 0xFFFF {
    3
  } else {
    4
  }
}

///|
fn append_mapped_text(
  builder : StringBuilder,
  corrections : Array[Int],
  replacement : String,
  source_start : Int,
  source_end : Int,
) -> Unit {
  let bytes = @utf8.encode(replacement)
  builder.write_string(replacement)
  for _ in 0.. CharacterMapping {
  { source, replacement }
}

///|
/// Deterministic scalar-to-string mapping filter with offset correction.
pub struct MappingCharFilter {
  mappings : ReadOnlyArray[CharacterMapping]
}

///|
pub fn MappingCharFilter::new(
  mappings : Array[CharacterMapping],
) -> MappingCharFilter {
  { mappings: ReadOnlyArray::from_array(mappings.copy()) }
}

///|
pub impl CharFilter for MappingCharFilter with fn apply(self, text) {
  let builder = StringBuilder()
  let corrections : Array[Int] = [0]
  let mut source_offset = 0
  for ch in text {
    let width = analyzer_utf8_width(ch)
    let replacement = match
      self.mappings.search_by(mapping => mapping.source == ch) {
      Some(index) => self.mappings[index].replacement
      None => {
        let one = StringBuilder()
        one.write_char(ch)
        one.to_string()
      }
    }
    append_mapped_text(
      builder,
      corrections,
      replacement,
      source_offset,
      source_offset + width,
    )
    source_offset += width
  }
  {
    text: builder.to_string(),
    corrections: ReadOnlyArray::from_array(corrections),
  }
}

///|
/// Portable compatibility normalization for full-width ASCII and common
/// Unicode spaces. It is intentionally deterministic across all backends.
pub struct CompatibilityNormalizationCharFilter {}

///|
pub fn CompatibilityNormalizationCharFilter::new() -> CompatibilityNormalizationCharFilter {
  CompatibilityNormalizationCharFilter::{  }
}

///|
pub impl CharFilter for CompatibilityNormalizationCharFilter with fn apply(
  _self,
  text,
) {
  let builder = StringBuilder()
  let corrections : Array[Int] = [0]
  let mut source_offset = 0
  for ch in text {
    let code = ch.to_int()
    let normalized = if code >= 0xFF01 && code <= 0xFF5E {
      (code - 0xFEE0).unsafe_to_char()
    } else if code == 0x3000 || code == 0x00A0 {
      ' '
    } else {
      ch
    }
    let replacement = StringBuilder()
    replacement.write_char(normalized)
    let width = analyzer_utf8_width(ch)
    append_mapped_text(
      builder,
      corrections,
      replacement.to_string(),
      source_offset,
      source_offset + width,
    )
    source_offset += width
  }
  {
    text: builder.to_string(),
    corrections: ReadOnlyArray::from_array(corrections),
  }
}

///|
pub(all) struct SynonymRule {
  input : String
  output : ReadOnlyArray[String]
} derive(Eq, @debug.Debug)

///|
pub fn SynonymRule::new(input : String, output : Array[String]) -> SynonymRule {
  guard input.length() > 0 && output.length() > 0 else {
    abort("synonym rules require non-empty input and output")
  }
  { input, output: ReadOnlyArray::from_array(output.copy()) }
}

///|
/// Lazy single-input synonym graph filter. Multi-token outputs form an
/// alternate graph path while the original token spans that path.
pub struct SynonymGraphFilter {
  rules : ReadOnlyArray[SynonymRule]
  expand : Bool
}

///|
pub fn SynonymGraphFilter::new(
  rules : Array[SynonymRule],
  expand? : Bool = true,
) -> SynonymGraphFilter {
  { rules: ReadOnlyArray::from_array(rules.copy()), expand }
}

///|
priv struct SynonymGraphTokenStream {
  input : &TokenStream
  rules : ReadOnlyArray[SynonymRule]
  expand : Bool
  pending : Array[Token]
  mut cursor : Int
  mut current : Token?
}

///|
fn SynonymGraphTokenStream::load(
  self : SynonymGraphTokenStream,
  token : Token,
) -> Unit {
  self.pending.clear()
  self.cursor = 0
  let matching : Array[SynonymRule] = []
  for rule in self.rules {
    if rule.input == token.text {
      matching.push(rule)
    }
  }
  let mut maximum_length = 1
  for rule in matching {
    maximum_length = maximum_length.max(rule.output.length())
  }
  if self.expand || matching.length() == 0 {
    self.pending.push({
      text: token.text,
      position: token.position,
      position_length: maximum_length,
      start_offset: token.start_offset,
      end_offset: token.end_offset,
    })
  }
  for rule in matching {
    for output_index in 0.. {
        self.load(token)
        if self.pending.length() > 0 {
          self.current = Some(self.pending[0])
          self.cursor = 1
          return true
        }
      }
      None => ()
    }
  }
  self.current = None
  false
}

///|
impl TokenStream for SynonymGraphTokenStream with fn token(self) {
  self.current
}

///|
pub impl TokenFilter for SynonymGraphFilter with fn transform(self, input) {
  SynonymGraphTokenStream::{
    input,
    rules: self.rules,
    expand: self.expand,
    pending: [],
    cursor: 0,
    current: None,
  }
}

///|
/// Provenance record for optional user dictionaries and synonym resources.
/// MoonSearch stores only the fingerprint in Schema; applications own the
/// potentially licensed payload and can refuse redistribution explicitly.
pub struct AnalysisResource {
  name : String
  version : String
  license : String
  redistributable : Bool
}

///|
pub fn AnalysisResource::new(
  name : String,
  version : String,
  license : String,
  redistributable : Bool,
) -> AnalysisResource {
  guard name.length() > 0 && version.length() > 0 else {
    abort("analysis resource name and version must be non-empty")
  }
  { name, version, license, redistributable }
}

///|
pub fn AnalysisResource::fingerprint(self : AnalysisResource) -> String {
  self.name + "@" + self.version
}

///|
pub fn AnalysisResource::license(self : AnalysisResource) -> String {
  self.license
}

///|
pub fn AnalysisResource::is_redistributable(self : AnalysisResource) -> Bool {
  self.redistributable
}