///|
/// A caching strategy for retrieval functions (Python passes e.g.
/// `functools.lru_cache(maxsize=...)` as `cache`): it wraps a retrieve
/// function into a caching one.
pub type RetrieveCache = ((String) -> Resource raise) -> (String) -> Resource raise

///|
/// An unbounded cache (Python's `lru_cache(maxsize=None)`). Errors are not
/// cached.
pub fn unbounded_cache() -> RetrieveCache {
  retrieve => {
    let cache : Map[String, Resource] = Map([])
    uri => {
      match cache.get(uri) {
        Some(resource) => resource
        None => {
          let resource = retrieve(uri)
          cache[uri] = resource
          resource
        }
      }
    }
  }
}

///|
/// A least-recently-used cache holding at most `maxsize` resources (Python's
/// `functools.lru_cache(maxsize=maxsize)`). Errors are not cached.
pub fn lru_cache(maxsize~ : Int) -> RetrieveCache {
  retrieve => {
    // `Map` preserves insertion order: the first entry is the least recently
    // used one.
    let cache : Map[String, Resource] = Map([])
    uri => {
      match cache.get(uri) {
        Some(resource) => {
          cache.remove(uri)
          cache[uri] = resource
          resource
        }
        None => {
          let resource = retrieve(uri)
          cache[uri] = resource
          if cache.length() > maxsize {
            if cache.keys().head() is Some(oldest) {
              cache.remove(oldest)
            }
          }
          resource
        }
      }
    }
  }
}

///|
/// Create a retriever which caches its return values from a simpler function
/// returning serialized documents (Python's
/// `referencing.retrieval.to_cached_resource(cache, loads, from_contents)`
/// applied to `retrieve`).
///
/// * `loads` deserializes the document (default: `@json.parse`).
/// * `from_contents` creates the resource (default:
///   `Resource::from_contents` without a default specification).
/// * `cache` is the caching strategy (default: `unbounded_cache()`).
///
/// The result is suitable for `Registry::new(retrieve=...)`.
///
/// ```mbt check
/// test {
///   let calls = []
///   let retrieve = @referencing.to_cached_resource(uri => {
///     calls.push(uri)
///     "{\"$schema\": \"https://json-schema.org/draft/2020-12/schema\", \"foo\": \"bar\"}"
///   })
///   let one = @referencing.Registry::new(retrieve~).get_or_retrieve(
///     "urn:example:foo",
///   )
///   let two = @referencing.Registry::new(retrieve~).get_or_retrieve(
///     "urn:example:foo",
///   )
///   inspect(
///     one.value.contents.stringify(),
///     content=(
///       #|{"$schema":"https://json-schema.org/draft/2020-12/schema","foo":"bar"}
///     ),
///   )
///   assert_true(physical_equal(one.value, two.value))
///   assert_eq(calls, ["urn:example:foo"])
/// }
/// ```
pub fn to_cached_resource(
  retrieve : (String) -> String raise,
  cache? : RetrieveCache = unbounded_cache(),
  loads? : (String) -> Json raise = s => @json.parse(s),
  from_contents? : (Json) -> Resource raise = contents => {
    Resource::from_contents(contents)
  },
) -> (String) -> Resource raise {
  cache(uri => from_contents(loads(retrieve(uri))))
}