///|
/// The value actually returned by one gate cell, normalized to the four shapes
/// the L3 table can expect (`EDouble` is reduced to its IEEE-754 bit pattern;
/// a raised `TextstatError` becomes `ARaised`).
enum Actual {
  AInt(Int32)
  ABits(UInt64)
  AList(Array[String])
  ARaised
}

///|
/// Run `run` and keep its integer result; any `TextstatError` maps to
/// `ARaised`, which is what `EError` cells require (A-5.2 failure semantics).
fn capture_int(run : () -> Int32 raise @moon_textstat.TextstatError) -> Actual {
  AInt(run()) catch {
    _ => ARaised
  }
}

///|
/// Run `run` and keep the bit pattern of its double result (AD section 2.2:
/// the bit pattern is the contract, printed text is not).
fn capture_double(
  run : () -> Double raise @moon_textstat.TextstatError,
) -> Actual {
  ABits(run().reinterpret_as_uint64()) catch {
    _ => ARaised
  }
}

///|
/// Run `run` and keep its word list; the comparison against `EList` is a set
/// comparison, so the returned order is irrelevant.
fn capture_list(
  run : () -> Array[String] raise @moon_textstat.TextstatError,
) -> Actual {
  AList(run()) catch {
    _ => ARaised
  }
}

///|
/// The input text of one frozen cell.
///
/// Sample-side cells read their text by sample id; language-side cells
/// (`lang`, `errors`) use the shared probe sentence. An unknown id is a hard
/// EXIT=2 environment failure, never an empty text that could false-pass.
fn input_text(cell : @gate_data.Cell) -> String {
  match cell.group {
    "lang" | "errors" => probe_text
    _ =>
      match sample_text(cell.sample_id) {
        Some(value) => value
        None => gate_fail("unknown sample id: " + cell.sample_id)
      }
  }
}

///|
/// Evaluate one frozen cell against the public entries of the root package.
///
/// The text always comes from `input_text`, which is also the text a failure
/// record hashes (AD section 2.5), so the digest and the evaluated bytes can
/// never drift apart. The language always comes from the cell: `en_US` for the
/// eleven `results` sets, `es_ES` for `spanish_text.results_es`, and the probe
/// code for language cells.
fn evaluate_cell(cell : @gate_data.Cell) -> Actual {
  let text = input_text(cell)
  let lang = cell.lang
  match cell.shape {
    "automated_readability_index" =>
      ABits(
        @moon_textstat.automated_readability_index(text).reinterpret_as_uint64(),
      )
    "char_count" => AInt(@moon_textstat.char_count(text))
    "char_count[ignore_spaces=False]" =>
      AInt(@moon_textstat.char_count(text, ignore_spaces=false))
    "coleman_liau_index" =>
      ABits(@moon_textstat.coleman_liau_index(text).reinterpret_as_uint64())
    "dale_chall_readability_score" =>
      capture_double(() => {
        @moon_textstat.dale_chall_readability_score(text, lang~)
      })
    "difficult_words" =>
      capture_int(() => @moon_textstat.difficult_words(text, lang~))
    "difficult_words_list" =>
      capture_list(() => @moon_textstat.difficult_words_list(text, lang~))
    "flesch_kincaid_grade" =>
      capture_double(() => @moon_textstat.flesch_kincaid_grade(text, lang~))
    "flesch_reading_ease" =>
      capture_double(() => @moon_textstat.flesch_reading_ease(text, lang~))
    "gunning_fog" =>
      capture_double(() => @moon_textstat.gunning_fog(text, lang~))
    "letter_count" => AInt(@moon_textstat.letter_count(text))
    "lexicon_count" => AInt(@moon_textstat.lexicon_count(text))
    "lexicon_count[removepunct=False]" =>
      AInt(@moon_textstat.lexicon_count(text, removepunct=false))
    "lexicon_count[split_contractions=True]" =>
      AInt(@moon_textstat.lexicon_count(text, split_contractions=true))
    "lexicon_count[split_hyphens=True]" =>
      AInt(@moon_textstat.lexicon_count(text, split_hyphens=true))
    "linsear_write_formula" =>
      capture_double(() => @moon_textstat.linsear_write_formula(text, lang~))
    "linsear_write_formula[strict_lower=True,strict_upper=True]" =>
      capture_double(() => {
        @moon_textstat.linsear_write_formula(
          text,
          lang~,
          strict_lower=true,
          strict_upper=true,
        )
      })
    "long_word_count" => AInt(@moon_textstat.long_word_count(text))
    "monosyllabcount" =>
      capture_int(() => @moon_textstat.monosyllabcount(text, lang~))
    "polysyllabcount" =>
      capture_int(() => @moon_textstat.polysyllabcount(text, lang~))
    "reading_time" =>
      ABits(@moon_textstat.reading_time(text).reinterpret_as_uint64())
    "sentence_count" => AInt(@moon_textstat.sentence_count(text))
    "smog_index" => capture_double(() => @moon_textstat.smog_index(text, lang~))
    "syllable_count" =>
      capture_int(() => @moon_textstat.syllable_count(text, lang~))
    _ => gate_fail("unknown call shape in the frozen cell table: " + cell.shape)
  }
}

///|
/// Set equality with case-sensitive string identity: every element of one
/// side must appear in the other, in both directions (AD section 2.3). Word
/// order and multiplicity are not contractual, so duplicates are ignored;
/// string identity itself stays exact, so a case change is a mismatch.
fn same_word_set(left : Array[String], right : Array[String]) -> Bool {
  for word in left {
    if !right.contains(word) {
      return false
    }
  }
  for word in right {
    if !left.contains(word) {
      return false
    }
  }
  true
}

///|
/// Compare one frozen expectation with the observed value. `EError` passes
/// only when the public entry raised; a returned value is a failure even when
/// it looks plausible (A-5.2).
fn cell_matches(expected : @gate_data.Expected, actual : Actual) -> Bool {
  match (expected, actual) {
    (@gate_data.EInt(want), AInt(got)) => want == got
    (@gate_data.EDouble(want), ABits(got)) => want == got
    (@gate_data.EList(want), AList(got)) => same_word_set(want, got)
    (@gate_data.EError(_), ARaised) => true
    _ => false
  }
}