///|
struct DecoderEntry {
  selected_indices : Array[Int]
  inverse : Matrix
}

///|
/// Reuse decode matrices when many stripes have the same erasure pattern.
/// Each session owns a small bounded FIFO cache; separate sessions should be
/// used for concurrent callers. Cached matrices contain no payload bytes.
pub struct RecoverySession {
  codec : Codec
  entries : Array[DecoderEntry]
  capacity : Int
  mut next_evict : Int
  mut hits : Int
  mut misses : Int
}

///|
pub fn RecoverySession::new(
  codec : Codec,
  max_patterns? : Int = 16,
) -> RecoverySession raise ErasureError {
  if max_patterns < 1 || max_patterns > 64 {
    raise InvalidConfiguration("recovery cache must hold 1..64 patterns")
  }
  {
    codec,
    entries: [],
    capacity: max_patterns,
    next_evict: 0,
    hits: 0,
    misses: 0,
  }
}

///|
pub fn RecoverySession::cache_size(self : RecoverySession) -> Int {
  self.entries.length()
}

///|
pub fn RecoverySession::cache_hits(self : RecoverySession) -> Int {
  self.hits
}

///|
pub fn RecoverySession::cache_misses(self : RecoverySession) -> Int {
  self.misses
}

///|
fn same_indices(left : Array[Int], right : Array[Int]) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for index = 0; index < left.length(); index = index + 1 {
    if left[index] != right[index] {
      return false
    }
  }
  true
}

///|
/// Reconstruct with the same validation as `Codec::reconstruct`. Data-only
/// stripes skip matrix work and do not alter cache counters. Cache misses
/// calculate a dense inverse once and reuse it on subsequent matching stripes.
pub fn RecoverySession::reconstruct(
  self : RecoverySession,
  shards : Array[Bytes?],
) -> Array[Bytes] raise ErasureError {
  if shards.length() != self.codec.total_count() {
    raise InvalidShardCount(
      expected=self.codec.total_count(),
      actual=shards.length(),
    )
  }
  let flags : Array[Bool] = []
  for shard in shards {
    flags.push(shard is Some(_))
  }
  let plan = self.codec.plan(flags)
  if !plan.recoverable() || !plan.needs_decode() {
    return self.codec.reconstruct(shards)
  }
  let selected = plan.selected_indices()
  for entry in self.entries {
    if same_indices(entry.selected_indices, selected) {
      let result = self.codec.reconstruct_with_decoder(
        shards,
        Some(entry.inverse),
      )
      self.hits = self.hits + 1
      return result
    }
  }
  let inverse = self.codec.generator
    .selected_rows(selected)
    .inverse(self.codec.field)
  let result = self.codec.reconstruct_with_decoder(shards, Some(inverse))
  self.misses = self.misses + 1
  let entry = { selected_indices: selected, inverse, }
  if self.entries.length() < self.capacity {
    self.entries.push(entry)
  } else {
    self.entries[self.next_evict] = entry
    self.next_evict = (self.next_evict + 1) % self.capacity
  }
  result
}