// Bounded compiled-pattern cache for repeated queries.
//
// The cache is explicit and value-based: every operation returns the updated
// cache together with its result.  That makes hit/miss behavior deterministic
// in tests and avoids hidden global state in library consumers.

///|
pub enum CacheError {
  InvalidCapacity
} derive(Debug, Eq)

///|
pub impl Show for CacheError with fn output(self, logger) {
  match self {
    CacheError::InvalidCapacity => logger.write_string("InvalidCapacity")
  }
}

///|
pub(all) struct CacheStats {
  entries : Int
  capacity : Int
  hits : Int
  misses : Int
  evictions : Int
} derive(Debug, Eq)

///|
pub(all) struct CachedPattern {
  source : String
  compiled : CompiledPattern
  hits : Int
} derive(Debug, Eq)

///|
pub(all) struct PatternCache {
  entries : Array[CachedPattern]
  capacity : Int
  hits : Int
  misses : Int
  evictions : Int
} derive(Debug, Eq)

///|
pub fn PatternCache::new(capacity : Int) -> Result[PatternCache, CacheError] {
  if capacity <= 0 {
    Err(CacheError::InvalidCapacity)
  } else {
    Ok({ entries: [], capacity, hits: 0, misses: 0, evictions: 0 })
  }
}

///|
fn PatternCache::find_index(self : PatternCache, source : String) -> Int? {
  let mut i = 0
  while i < self.entries.length() {
    if self.entries[i].source == source {
      return Some(i)
    }
    i = i + 1
  }
  None
}

///|
fn drop_oldest(entries : Array[CachedPattern]) -> Array[CachedPattern] {
  let result : Array[CachedPattern] = []
  let mut i = 1
  while i < entries.length() {
    result.push(entries[i])
    i = i + 1
  }
  result
}

///|
/// Returns a compiled pattern and an updated cache.
pub fn PatternCache::get_or_compile(
  self : PatternCache,
  source : String,
) -> Result[(PatternCache, CompiledPattern), GlobError] {
  match self.find_index(source) {
    Some(index) => {
      let entries = self.entries.copy()
      let current = entries[index]
      entries[index] = { ..current, hits: current.hits + 1 }
      let next = { ..self, entries, hits: self.hits + 1 }
      Ok((next, current.compiled))
    }
    None =>
      match compile_pattern(source) {
        Err(err) => Err(err)
        Ok(compiled) => {
          let mut entries = self.entries.copy()
          let mut evictions = self.evictions
          if entries.length() >= self.capacity {
            entries = drop_oldest(entries)
            evictions = evictions + 1
          }
          entries.push({ source, compiled, hits: 0 })
          let next = { ..self, entries, misses: self.misses + 1, evictions }
          Ok((next, compiled))
        }
      }
  }
}

///|
pub fn PatternCache::contains(self : PatternCache, source : String) -> Bool {
  self.find_index(source) is Some(_)
}

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

///|
pub fn PatternCache::stats(self : PatternCache) -> CacheStats {
  {
    entries: self.entries.length(),
    capacity: self.capacity,
    hits: self.hits,
    misses: self.misses,
    evictions: self.evictions,
  }
}

///|
/// Clears cached entries and counters while retaining the configured bound.
pub fn PatternCache::clear(self : PatternCache) -> PatternCache {
  { ..self, entries: [], hits: 0, misses: 0, evictions: 0 }
}

///|
/// Compiles a batch and preserves the first parser error.
pub fn PatternCache::compile_all(
  self : PatternCache,
  sources : Array[String],
) -> Result[(PatternCache, Array[CompiledPattern]), GlobError] {
  let mut cache = self
  let result : Array[CompiledPattern] = []
  for source in sources {
    match cache.get_or_compile(source) {
      Err(err) => return Err(err)
      Ok((next, compiled)) => {
        cache = next
        result.push(compiled)
      }
    }
  }
  Ok((cache, result))
}

///|
/// Tests whether at least one cached/compiled pattern matches any path.
pub fn PatternCache::match_any(
  self : PatternCache,
  sources : Array[String],
  paths : Array[String],
) -> Result[(PatternCache, Bool), GlobError] {
  match self.compile_all(sources) {
    Err(err) => Err(err)
    Ok((cache, patterns)) => {
      let mut matched = false
      for pattern in patterns {
        for path in paths {
          if pattern.matches(path) {
            matched = true
          }
        }
      }
      Ok((cache, matched))
    }
  }
}

///|
/// Tests whether every pattern matches one path.
pub fn PatternCache::match_all(
  self : PatternCache,
  sources : Array[String],
  path : String,
) -> Result[(PatternCache, Bool), GlobError] {
  match self.compile_all(sources) {
    Err(err) => Err(err)
    Ok((cache, patterns)) => {
      let mut matched = true
      for pattern in patterns {
        if !pattern.matches(path) {
          matched = false
        }
      }
      Ok((cache, matched))
    }
  }
}

///|
/// Filters paths with a cached pattern.
pub fn PatternCache::filter(
  self : PatternCache,
  source : String,
  paths : Array[String],
) -> Result[(PatternCache, Array[String]), GlobError] {
  match self.get_or_compile(source) {
    Err(err) => Err(err)
    Ok((cache, compiled)) => {
      let result : Array[String] = []
      for path in paths {
        if compiled.matches(path) {
          result.push(path)
        }
      }
      Ok((cache, result))
    }
  }
}

///|
/// Returns all cached source names in oldest-to-newest order.
pub fn PatternCache::sources(self : PatternCache) -> Array[String] {
  let result : Array[String] = []
  for entry in self.entries {
    result.push(entry.source)
  }
  result
}

///|
pub fn PatternCache::capacity(self : PatternCache) -> Int {
  self.capacity
}

///|
pub fn PatternCache::hit_rate_percent(self : PatternCache) -> Int {
  let total = self.hits + self.misses
  if total == 0 {
    0
  } else {
    self.hits * 100 / total
  }
}

///|
pub fn PatternCache::summary(self : PatternCache) -> String {
  "cache entries=" +
  self.entries.length().to_string() +
  "/" +
  self.capacity.to_string() +
  " hits=" +
  self.hits.to_string() +
  " misses=" +
  self.misses.to_string() +
  " evictions=" +
  self.evictions.to_string()
}