///|
/// Cache policy for repeated waveform queries.
pub enum TraceCachePolicy {
  NoCache
  LastValue
  SignalSnapshots
  WindowResults
} derive(Debug, Eq)

///|
/// A cached value lookup with source provenance.
pub struct TraceCacheEntry {
  signal_id : String
  time : Int64
  value : Value?
  source_time : Int64?
  hits : Int
} derive(Debug)

///|
/// Reusable cache for signal-at-time and snapshot queries.
pub struct TraceCache {
  policy : TraceCachePolicy
  entries : Array[TraceCacheEntry]
  lookups : Int
  hits : Int
  misses : Int
  capacity : Int
} derive(Debug)

///|
/// List cache policies.
pub fn trace_cache_policy_catalog() -> Array[TraceCachePolicy] {
  [NoCache, LastValue, SignalSnapshots, WindowResults]
}

///|
/// Create an empty bounded cache.
pub fn trace_cache(policy : TraceCachePolicy, capacity : Int) -> TraceCache {
  {
    policy,
    entries: [],
    lookups: 0,
    hits: 0,
    misses: 0,
    capacity: Int::max(1, capacity),
  }
}

///|
/// Look up a signal value and remember the result.
pub fn TraceCache::value_at(
  self : TraceCache,
  trace : Trace,
  signal_id : String,
  time : Int64,
) -> (TraceCache, Value?) {
  let lookups = self.lookups + 1
  if self.policy is NoCache {
    return (
      { ..self, lookups, misses: self.misses + 1 },
      value_at(trace, signal_id, time),
    )
  }
  for index, entry in self.entries {
    if entry.signal_id == signal_id && entry.time == time {
      let entries = self.entries.copy()
      entries[index] = { ..entry, hits: entry.hits + 1 }
      return ({ ..self, entries, lookups, hits: self.hits + 1 }, entry.value)
    }
  }
  let value = value_at(trace, signal_id, time)
  let source_time = trace.changes
    .filter(fn(change) { change.id == signal_id && change.time <= time })
    .fold(init=None, fn(current, item) {
      match current {
        Some(previous) if previous >= item.time => Some(previous)
        _ => Some(item.time)
      }
    })
  let entries = self.entries.copy()
  if entries.length() >= self.capacity {
    let _ = entries.remove(0)
  }
  entries.push({ signal_id, time, value, source_time, hits: 0 })
  ({ ..self, entries, lookups, misses: self.misses + 1 }, value)
}

///|
/// Precompute a cache for every change timestamp.
pub fn TraceCache::warm_signal(
  self : TraceCache,
  trace : Trace,
  signal_id : String,
) -> TraceCache {
  let mut cache = self
  for change in trace.changes {
    if change.id == signal_id {
      let result = cache.value_at(trace, signal_id, change.time)
      cache = result.0
    }
  }
  cache
}

///|
/// Remove all entries for one signal.
pub fn TraceCache::invalidate_signal(
  self : TraceCache,
  signal_id : String,
) -> TraceCache {
  {
    ..self,
    entries: self.entries.filter(fn(item) { item.signal_id != signal_id }),
  }
}

///|
/// Remove every cached entry.
pub fn TraceCache::clear(self : TraceCache) -> TraceCache {
  { ..self, entries: [] }
}

///|
/// Return the cache hit ratio.
pub fn TraceCache::hit_ratio(self : TraceCache) -> Double {
  if self.lookups == 0 {
    0.0
  } else {
    self.hits.to_double() / self.lookups.to_double()
  }
}

///|
/// Return a stable cache summary.
pub fn TraceCache::summary(self : TraceCache) -> String {
  "policy=" +
  trace_cache_policy_name(self.policy) +
  " entries=" +
  self.entries.length().to_string() +
  " lookups=" +
  self.lookups.to_string() +
  " hits=" +
  self.hits.to_string() +
  " misses=" +
  self.misses.to_string() +
  " hit_ratio=" +
  self.hit_ratio().to_string()
}

///|
/// Export cache entries as CSV.
pub fn trace_cache_entries_csv(entries : Array[TraceCacheEntry]) -> String {
  let out = StringBuilder()
  out.write_string("signal,time,value,source_time,hits\n")
  for entry in entries {
    out.write_string(
      escape_text(entry.signal_id) + "," + entry.time.to_string() + ",",
    )
    out.write_string(
      match entry.value {
        Some(value) => escape_text(value.display())
        None => ""
      },
    )
    out.write_string(
      "," +
      trace_cache_optional_i64(entry.source_time) +
      "," +
      entry.hits.to_string() +
      "\n",
    )
  }
  out.to_string()
}

///|
/// Name a cache policy.
pub fn trace_cache_policy_name(policy : TraceCachePolicy) -> String {
  match policy {
    NoCache => "none"
    LastValue => "last-value"
    SignalSnapshots => "signal-snapshots"
    WindowResults => "window-results"
  }
}

///|
/// Encode an optional timestamp.
fn trace_cache_optional_i64(value : Int64?) -> String {
  match value {
    Some(item) => item.to_string()
    None => ""
  }
}