///|
/// A single ARPA record; probability and optional backoff use base-10 logarithms.
pub(all) struct Gram {
  tokens : Array[String]
  log_probability : Double
  log_backoff : Double?
} derive(Debug, Eq)

///|
fn Gram::owned(self : Gram) -> Gram {
  {
    tokens: self.tokens.copy(),
    log_probability: self.log_probability,
    log_backoff: self.log_backoff,
  }
}

///|
/// Owned validated records; callers cannot mutate the internal indices.
pub struct Model {
  order : Int32
  grams : Array[Gram]
  index : Map[String, Int32]
  vocabulary : Array[String]
  vocabulary_set : Map[String, Bool]
  limits : Limits
}

///|
pub fn Model::new(
  order : Int32,
  records : Array[Gram],
  limits~ : Limits = Limits::default(),
) -> Model raise ArpaError {
  limits.validate()
  if order < 1 || order > limits.max_order {
    raise Invalid("unsupported order")
  }
  if records.length() > limits.max_records {
    raise Budget("record limit")
  }
  let grams = []
  let index : Map[String, Int32] = Map([])
  let vocabulary = []
  let vocabulary_set : Map[String, Bool] = Map([])
  let mut units = 0
  for record in records {
    let size = record.tokens.length()
    if size < 1 || size > order {
      raise Invalid("record order outside model")
    }
    if !valid_log(record.log_probability) || record.log_probability > 0.0 {
      raise Invalid("probability must be nonpositive log10 or -inf")
    }
    match record.log_backoff {
      Some(b) => {
        if !valid_log(b) {
          raise Invalid("invalid backoff weight")
        }
        if size == order {
          raise Invalid("highest order cannot have backoff")
        }
      }
      None => ()
    }
    for token in record.tokens {
      if !token_valid(token) {
        raise Invalid("invalid vocabulary token")
      }
      if token.length() > limits.max_text - units {
        raise Budget("token text limit")
      }
      units += token.length()
    }
    let key = gram_key(record.tokens)
    if index.contains(key) {
      raise Invalid("duplicate n-gram")
    }
    index[key] = grams.length()
    grams.push(record.owned())
    if size == 1 {
      vocabulary.push(record.tokens[0])
      vocabulary_set[record.tokens[0]] = true
    }
  }
  if vocabulary.is_empty() {
    raise Invalid("unigrams are required")
  }
  if vocabulary.length() > limits.max_tokens {
    raise Budget("vocabulary limit")
  }
  for record in grams {
    for token in record.tokens {
      if !vocabulary_set.contains(token) {
        raise Invalid("ngram token has no unigram")
      }
    }
  }
  vocabulary.sort()
  { order, grams, index, vocabulary, vocabulary_set, limits, }
}

///|
pub fn Model::order(self : Model) -> Int32 {
  self.order
}

///|
pub fn Model::size(self : Model) -> Int32 {
  self.grams.length()
}

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

///|
pub fn Model::contains(self : Model, token : String) -> Bool {
  self.vocabulary_set.contains(token)
}

///|
pub fn Model::records(self : Model) -> Array[Gram] {
  self.grams.map(record => record.owned())
}

///|
pub fn Model::record(self : Model, tokens : Array[String]) -> Gram? {
  match self.index.get(gram_key(tokens)) {
    Some(i) => Some(self.grams[i].owned())
    None => None
  }
}

///|
pub fn Model::counts(self : Model) -> Array[Int32] {
  let counts = Array::make(self.order, 0)
  for record in self.grams {
    counts[record.tokens.length() - 1] += 1
  }
  counts
}

///|
pub extend Gram with @debug.Debug::{to_repr}

///|
pub extend Gram with Eq::{equal, not_equal}