///|
/// A sentence-scoped incremental scorer. Failed batches leave the state unchanged.
pub struct Scorer {
  model : Model
  options : ScoreOptions
  history : Array[String]
  results : Array[TokenScore]
  mut total : Double
  mut text_units : Int32
  mut oovs : Int32
  mut closed : Bool
}

///|
pub fn Model::scorer(
  self : Model,
  options~ : ScoreOptions = ScoreOptions::default(),
) -> Scorer raise ArpaError {
  if options.bos && !self.contains("") {
    raise Missing("BOS unigram")
  }
  if options.eos && !self.contains("") {
    raise Missing("EOS unigram")
  }
  {
    model: self,
    options,
    history: if options.bos {
      [""]
    } else {
      []
    },
    results: [],
    total: 0.0,
    text_units: 0,
    oovs: 0,
    closed: false,
  }
}

///|
pub fn Scorer::fork(self : Scorer) -> Scorer {
  {
    model: self.model,
    options: self.options,
    history: self.history.copy(),
    results: self.results.copy(),
    total: self.total,
    text_units: self.text_units,
    oovs: self.oovs,
    closed: self.closed,
  }
}

///|
pub fn Scorer::history(self : Scorer) -> Array[String] {
  self.history.copy()
}

///|
pub fn Scorer::is_closed(self : Scorer) -> Bool {
  self.closed
}

///|
pub fn Scorer::snapshot(self : Scorer) -> SentenceScore {
  {
    log_probability: self.total,
    predicted: self.results.length(),
    oov_count: self.oovs,
    tokens: self.results.copy(),
  }
}

///|
fn Scorer::accept(self : Scorer, token : String) -> TokenScore raise ArpaError {
  if token.length() > self.model.limits.max_text - self.text_units {
    raise Budget("stream cumulative text limit")
  }
  let score = self.model.conditional(self.history, token, oov=self.options.oov)
  let total = add_log(self.total, score.log_probability)
  self.results.push(score)
  self.total = total
  self.text_units += token.length()
  if score.oov {
    self.oovs += 1
  }
  self.history.push(score.mapped)
  while self.history.length() > self.model.order - 1 {
    let _ = self.history.remove(0)
  }
  score
}

///|
pub fn Scorer::feed(
  self : Scorer,
  token : String,
) -> TokenScore raise ArpaError {
  if self.closed {
    raise Invalid("scorer already finished")
  }
  checked_token_budget([token], self.model.limits)
  if self.results.length() >= self.model.limits.max_tokens {
    raise Budget("stream token limit")
  }
  self.accept(token)
}

///|
pub fn Scorer::feed_batch(
  self : Scorer,
  tokens : Array[String],
) -> SentenceScore raise ArpaError {
  if self.closed {
    raise Invalid("scorer already finished")
  }
  checked_token_budget(tokens, self.model.limits)
  if tokens.length() > self.model.limits.max_tokens - self.results.length() {
    raise Budget("stream token limit")
  }
  let trial = self.fork()
  for token in tokens {
    let _ = trial.accept(token)
  }
  self.history.clear()
  for token in trial.history {
    self.history.push(token)
  }
  self.results.clear()
  for result in trial.results {
    self.results.push(result)
  }
  self.total = trial.total
  self.text_units = trial.text_units
  self.oovs = trial.oovs
  self.snapshot()
}

///|
pub fn Scorer::finish(self : Scorer) -> SentenceScore raise ArpaError {
  if !self.closed {
    if self.options.eos {
      if self.results.length() >= self.model.limits.max_tokens {
        raise Budget("stream EOS token limit")
      }
      let _ = self.accept("")
    }
    self.closed = true
  }
  self.snapshot()
}

///|
pub fn Scorer::reset(self : Scorer) -> Unit {
  self.history.clear()
  if self.options.bos {
    self.history.push("")
  }
  self.results.clear()
  self.total = 0.0
  self.text_units = 0
  self.oovs = 0
  self.closed = false
}