///|
pub(all) struct MemoryStoreOptions {
  max_entries : Int
  max_body_bytes : Int64
  max_body_size : Int64
  strip_set_cookie : Bool
  strip_request_credentials : Bool
} derive(Eq, Debug)

///|
pub fn MemoryStoreOptions::default() -> MemoryStoreOptions {
  MemoryStoreOptions::{
    max_entries: 256,
    max_body_bytes: 64 * 1_024 * 1_024,
    max_body_size: 8 * 1_024 * 1_024,
    strip_set_cookie: true,
    strip_request_credentials: true,
  }
}

///|
pub fn MemoryStoreOptions::with_limits(
  max_entries : Int,
  max_body_bytes : Int64,
  max_body_size : Int64,
) -> MemoryStoreOptions {
  MemoryStoreOptions::{
    ..MemoryStoreOptions::default(),
    max_entries,
    max_body_bytes,
    max_body_size,
  }
}

///|
pub fn MemoryStoreOptions::with_entry_limit(
  self : MemoryStoreOptions,
  max_entries : Int,
) -> MemoryStoreOptions {
  { ..self, max_entries, }
}

///|
pub fn MemoryStoreOptions::with_total_body_limit(
  self : MemoryStoreOptions,
  max_body_bytes : Int64,
) -> MemoryStoreOptions {
  { ..self, max_body_bytes, }
}

///|
pub fn MemoryStoreOptions::with_single_body_limit(
  self : MemoryStoreOptions,
  max_body_size : Int64,
) -> MemoryStoreOptions {
  { ..self, max_body_size, }
}

///|
pub fn MemoryStoreOptions::with_set_cookie_stripping(
  self : MemoryStoreOptions,
  strip_set_cookie : Bool,
) -> MemoryStoreOptions {
  { ..self, strip_set_cookie, }
}

///|
pub fn MemoryStoreOptions::with_request_credential_stripping(
  self : MemoryStoreOptions,
  strip_request_credentials : Bool,
) -> MemoryStoreOptions {
  { ..self, strip_request_credentials, }
}

///|
pub(all) enum StorePutResult {
  StoreInserted
  StoreReplacedResult
  StoreRejectedNoStore
  StoreRejectedOversized
  StoreRejectedCapacity
} derive(Eq, Compare, Debug)

///|
pub(all) struct StoreStats {
  entries : Int
  body_bytes : Int64
  puts : Int64
  replacements : Int64
  evictions : Int64
  lookups : Int64
  hits : Int64
  misses : Int64
  invalidated_entries : Int64
  rejected_entries : Int64
} derive(Eq, Debug)

///|
pub(open) trait CacheStore {
  fn find_variants(Self, PrimaryCacheKey) -> Array[StoredEntry]
  fn put(Self, StoredEntry) -> StorePutResult
  fn remove_variant(Self, PrimaryCacheKey, VariantKey) -> Bool
  fn invalidate_uri(Self, String) -> Int
  fn clear(Self) -> Unit
  fn stats(Self) -> StoreStats
}

///|
priv struct StoreRecord {
  entry : StoredEntry
  mut access_order : Int64
  insertion_order : Int64
}

///|
/// HTTP-specific in-memory store with deterministic logical access ordering.
pub struct MemoryStore {
  priv options : MemoryStoreOptions
  priv records : Array[StoreRecord]
  priv mut body_bytes : Int64
  priv mut logical_clock : Int64
  priv mut puts : Int64
  priv mut replacements : Int64
  priv mut evictions : Int64
  priv mut lookups : Int64
  priv mut hits : Int64
  priv mut misses : Int64
  priv mut invalidated_entries : Int64
  priv mut rejected_entries : Int64
}

///|
pub fn MemoryStore::new(options : MemoryStoreOptions) -> MemoryStore {
  MemoryStore::{
    options,
    records: [],
    body_bytes: 0,
    logical_clock: 0,
    puts: 0,
    replacements: 0,
    evictions: 0,
    lookups: 0,
    hits: 0,
    misses: 0,
    invalidated_entries: 0,
    rejected_entries: 0,
  }
}

///|
pub fn MemoryStore::default() -> MemoryStore {
  MemoryStore::new(MemoryStoreOptions::default())
}

///|
fn next_store_order(store : MemoryStore) -> Int64 {
  if store.logical_clock < MAX_DELTA_SECONDS {
    store.logical_clock = store.logical_clock + 1
  }
  store.logical_clock
}

///|
fn stored_connection_fields(headers : HeaderMap) -> Array[String] {
  let fields : Array[String] = []
  match headers.get("connection") {
    Some(value) =>
      for part in value.split(",") {
        let name = normalize_header_name(part.to_owned())
        if name != "" && !fields.contains(name) {
          fields.push(name)
        }
      }
    None => ()
  }
  fields
}

///|
fn is_hop_by_hop(name : String, connection_fields : Array[String]) -> Bool {
  name == "connection" ||
  name == "keep-alive" ||
  name == "proxy-authenticate" ||
  name == "proxy-authorization" ||
  name == "te" ||
  name == "trailer" ||
  name == "transfer-encoding" ||
  name == "upgrade" ||
  connection_fields.contains(name)
}

///|
fn copy_stored_response_headers(
  headers : HeaderMap,
  strip_set_cookie : Bool,
) -> HeaderMap {
  let result = HeaderMap::new()
  let connection_fields = stored_connection_fields(headers)
  for name in headers.names() {
    if !is_hop_by_hop(name, connection_fields) &&
      !(strip_set_cookie && name == "set-cookie") {
      replace_header_values(result, name, headers.get_all(name))
    }
  }
  result
}

///|
fn copy_stored_request_headers(
  headers : HeaderMap,
  vary : VarySpec,
  strip_credentials : Bool,
) -> HeaderMap {
  let result = HeaderMap::new()
  for name in headers.names() {
    let sensitive = name == "authorization" ||
      name == "proxy-authorization" ||
      name == "cookie"
    if !strip_credentials || !sensitive || vary.fields.contains(name) {
      replace_header_values(result, name, headers.get_all(name))
    }
  }
  result
}

///|
fn sanitize_stored_entry(
  entry : StoredEntry,
  options : MemoryStoreOptions,
) -> StoredEntry {
  let vary = parse_vary(entry.response.headers)
  StoredEntry::{
    ..entry,
    request: RequestMeta::{
      ..entry.request,
      headers: copy_stored_request_headers(
        entry.request.headers,
        vary,
        options.strip_request_credentials,
      ),
    },
    response: ResponseMeta::{
      ..entry.response,
      headers: copy_stored_response_headers(
        entry.response.headers,
        options.strip_set_cookie,
      ),
    },
  }
}

///|
fn MemoryStore::remove_record_at(
  self : MemoryStore,
  index : Int,
) -> StoreRecord {
  let removed = self.records.remove(index)
  self.body_bytes = self.body_bytes - removed.entry.body_size()
  removed
}

///|
fn MemoryStore::replacement_index(
  self : MemoryStore,
  key : PrimaryCacheKey,
  variant : VariantKey,
) -> Int? {
  for index = 0; index < self.records.length(); index = index + 1 {
    let entry = self.records[index].entry
    if entry.primary_key == key && entry.variant_key == variant {
      return Some(index)
    }
  }
  None
}

///|
fn MemoryStore::eviction_index(self : MemoryStore) -> Int {
  let mut selected = 0
  for index = 1; index < self.records.length(); index = index + 1 {
    let candidate = self.records[index]
    let current = self.records[selected]
    if candidate.access_order < current.access_order ||
      (
        candidate.access_order == current.access_order &&
        candidate.insertion_order < current.insertion_order
      ) {
      selected = index
    }
  }
  selected
}

///|
fn MemoryStore::evict_to_limits(self : MemoryStore) -> Unit {
  while self.records.length() > self.options.max_entries ||
        self.body_bytes > self.options.max_body_bytes {
    let index = self.eviction_index()
    ignore(self.remove_record_at(index))
    self.evictions = self.evictions + 1
  }
}

///|
pub fn MemoryStore::find_variants(
  self : MemoryStore,
  key : PrimaryCacheKey,
) -> Array[StoredEntry] {
  self.lookups = self.lookups + 1
  let order = next_store_order(self)
  let result : Array[StoredEntry] = []
  for record in self.records {
    if record.entry.primary_key == key {
      record.access_order = order
      result.push(record.entry.copy())
    }
  }
  if result.length() == 0 {
    self.misses = self.misses + 1
  } else {
    self.hits = self.hits + 1
  }
  result
}

///|
pub fn MemoryStore::put(
  self : MemoryStore,
  source_entry : StoredEntry,
) -> StorePutResult {
  if !source_entry.policy.cacheable ||
    source_entry.policy.no_store ||
    source_entry.policy.vary_star {
    self.rejected_entries = self.rejected_entries + 1
    return StoreRejectedNoStore
  }
  let size = source_entry.body_size()
  if size > self.options.max_body_size || size > self.options.max_body_bytes {
    self.rejected_entries = self.rejected_entries + 1
    return StoreRejectedOversized
  }
  if self.options.max_entries <= 0 || self.options.max_body_bytes <= 0 {
    self.rejected_entries = self.rejected_entries + 1
    return StoreRejectedCapacity
  }
  let entry = sanitize_stored_entry(source_entry.copy(), self.options)
  let mut replaced = false
  match self.replacement_index(entry.primary_key, entry.variant_key) {
    Some(index) => {
      ignore(self.remove_record_at(index))
      self.replacements = self.replacements + 1
      replaced = true
    }
    None => ()
  }
  let order = next_store_order(self)
  self.records.push(StoreRecord::{
    entry,
    access_order: order,
    insertion_order: order,
  })
  self.body_bytes = self.body_bytes + size
  self.puts = self.puts + 1
  self.evict_to_limits()
  if replaced {
    StoreReplacedResult
  } else {
    StoreInserted
  }
}

///|
pub fn MemoryStore::remove_variant(
  self : MemoryStore,
  key : PrimaryCacheKey,
  variant : VariantKey,
) -> Bool {
  match self.replacement_index(key, variant) {
    Some(index) => {
      ignore(self.remove_record_at(index))
      true
    }
    None => false
  }
}

///|
pub fn MemoryStore::invalidate_uri(self : MemoryStore, uri : String) -> Int {
  let target = normalize_cache_uri(uri).unwrap_or(uri.trim().to_owned())
  let mut removed = 0
  let mut index = self.records.length() - 1
  while index >= 0 && self.records.length() > 0 {
    if self.records[index].entry.primary_key.uri == target {
      ignore(self.remove_record_at(index))
      removed = removed + 1
    }
    index = index - 1
  }
  self.invalidated_entries = self.invalidated_entries + removed.to_int64()
  removed
}

///|
pub fn MemoryStore::clear(self : MemoryStore) -> Unit {
  self.records.clear()
  self.body_bytes = 0
}

///|
pub fn MemoryStore::stats(self : MemoryStore) -> StoreStats {
  StoreStats::{
    entries: self.records.length(),
    body_bytes: self.body_bytes,
    puts: self.puts,
    replacements: self.replacements,
    evictions: self.evictions,
    lookups: self.lookups,
    hits: self.hits,
    misses: self.misses,
    invalidated_entries: self.invalidated_entries,
    rejected_entries: self.rejected_entries,
  }
}

///|
/// Return the immutable configuration used by this Store.
pub fn MemoryStore::options(self : MemoryStore) -> MemoryStoreOptions {
  self.options
}

///|
/// Inspect variants for a URI without updating logical access order or lookup
/// counters. Returned entries are defensive copies.
pub fn MemoryStore::peek_variants(
  self : MemoryStore,
  uri : String,
) -> Array[StoredEntry] {
  let target = normalize_cache_uri(uri).unwrap_or(uri.trim().to_owned())
  let result : Array[StoredEntry] = []
  for record in self.records {
    if record.entry.primary_key.uri == target {
      result.push(record.entry.copy())
    }
  }
  result
}

///|
pub fn MemoryStore::variant_count(self : MemoryStore, uri : String) -> Int {
  self.peek_variants(uri).length()
}

///|
pub fn MemoryStore::contains_variant(
  self : MemoryStore,
  key : PrimaryCacheKey,
  variant : VariantKey,
) -> Bool {
  self.replacement_index(key, variant) is Some(_)
}

///|
fn compare_store_uris(left : String, right : String) -> Int {
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
/// Return each normalized cached URI once in deterministic lexical order.
pub fn MemoryStore::cached_uris(self : MemoryStore) -> Array[String] {
  let result : Array[String] = []
  for record in self.records {
    let uri = record.entry.primary_key.uri
    if !result.contains(uri) {
      result.push(uri)
    }
  }
  result.sort_by(compare_store_uris)
  result
}

///|
pub impl CacheStore for MemoryStore with fn find_variants(self, key) {
  MemoryStore::find_variants(self, key)
}

///|
pub impl CacheStore for MemoryStore with fn put(self, entry) {
  MemoryStore::put(self, entry)
}

///|
pub impl CacheStore for MemoryStore with fn remove_variant(self, key, variant) {
  MemoryStore::remove_variant(self, key, variant)
}

///|
pub impl CacheStore for MemoryStore with fn invalidate_uri(self, uri) {
  MemoryStore::invalidate_uri(self, uri)
}

///|
pub impl CacheStore for MemoryStore with fn clear(self) {
  MemoryStore::clear(self)
}

///|
pub impl CacheStore for MemoryStore with fn stats(self) {
  MemoryStore::stats(self)
}