// Host-retained path meshes.
//
// A wasm-gc module has no linear memory to share, so a tessellated path crosses
// the boundary through `begin_create_string`/`string_append_char`: one import
// call per code point. A path vertex is fifteen `Double`s printed in full
// decimal, so one vertex costs roughly 150 calls, and a scene that repaints the
// same vector content every frame pays that again and again. Measured on the
// pelican scene, `wasm-to-js`, `string_append_char` and the host-side re-parse
// together took ~60% of the main thread while the rasterizer sat idle.
//
// The host now keeps the parsed mesh behind an integer handle and this adapter
// remembers it, keyed by the `PathSpec` whose tessellation produced it. A
// repeated path costs one two-scalar call; only geometry the host has not seen
// before pays tessellation, serialization and transfer.

///|
fn WebGpuPathMeshCache::new(limit : Int32) -> WebGpuPathMeshCache {
  { entries: [], frame: 0, limit, }
}

///|
fn WebGpuPathMeshCache::advance_frame(self : WebGpuPathMeshCache) -> Unit {
  self.frame = self.frame + 1
}

///|
/// The entry that belongs to `path`, whether or not its host mesh is still
/// alive. Identity is exact structural `PathSpec` equality, so a hit can never
/// draw the wrong geometry; two paths that hold the same verbs and brushes
/// share one mesh, which is what the host would have drawn anyway.
fn WebGpuPathMeshCache::find_entry(
  self : WebGpuPathMeshCache,
  path : @core.PathSpec,
) -> WebGpuPathMeshEntry? {
  for entry in self.entries {
    if entry.path == path {
      return Some(entry)
    }
  }
  None
}

///|
/// The host mesh to draw `path` with, if this adapter holds one the host still
/// has. Either way the entry's recency is touched, so a path that keeps being
/// looked up is the last one to leave the cache.
fn WebGpuPathMeshCache::find(
  self : WebGpuPathMeshCache,
  path : @core.PathSpec,
) -> Int32? {
  match self.find_entry(path) {
    Some(entry) => {
      entry.used_at = self.frame
      if entry.host_id > 0 {
        Some(entry.host_id)
      } else {
        None
      }
    }
    None => None
  }
}

///|
/// Mark `path` as needing a fresh upload and keep its slot. Used when the host
/// reports the mesh is gone, which is what happens after WebGPU recovery swaps
/// the page runtime this wasm module was instantiated against.
fn WebGpuPathMeshCache::forget(
  self : WebGpuPathMeshCache,
  path : @core.PathSpec,
) -> Unit {
  match self.find_entry(path) {
    Some(entry) => entry.host_id = 0
    None => ()
  }
}

///|
/// Remember that `host_id` is the host mesh for `path`. Returns the host mesh to
/// release because its slot was recycled to make room, or None when nothing was
/// displaced.
///
/// Space comes from the least recently used entry, and a mesh the host already
/// dropped goes first, so a long-running app cannot grow this cache past `limit`.
fn WebGpuPathMeshCache::record(
  self : WebGpuPathMeshCache,
  path : @core.PathSpec,
  host_id : Int32,
) -> Int32? {
  match self.find_entry(path) {
    Some(entry) => {
      let retired = entry.host_id
      entry.path = path
      entry.host_id = host_id
      entry.used_at = self.frame
      webgpu_retired_mesh(retired, host_id)
    }
    None =>
      if self.entries.length() < self.limit {
        self.entries.push({ path, host_id, used_at: self.frame, })
        None
      } else {
        match self.recyclable_entry() {
          None => {
            self.entries.push({ path, host_id, used_at: self.frame, })
            None
          }
          Some(recycled) => {
            let retired = recycled.host_id
            recycled.path = path
            recycled.host_id = host_id
            recycled.used_at = self.frame
            webgpu_retired_mesh(retired, host_id)
          }
        }
      }
  }
}

///|
/// The entry to hand to a new path: one whose host mesh is already gone, else
/// the one used longest ago.
fn WebGpuPathMeshCache::recyclable_entry(
  self : WebGpuPathMeshCache,
) -> WebGpuPathMeshEntry? {
  let mut chosen : WebGpuPathMeshEntry? = None
  for entry in self.entries {
    let better = match chosen {
      None => true
      Some(current) => webgpu_mesh_entry_is_more_reclaimable(entry, current)
    }
    if better {
      chosen = Some(entry)
    }
  }
  chosen
}

///|
fn webgpu_mesh_entry_is_more_reclaimable(
  candidate : WebGpuPathMeshEntry,
  current : WebGpuPathMeshEntry,
) -> Bool {
  if candidate.host_id == 0 && current.host_id > 0 {
    true
  } else if candidate.host_id > 0 && current.host_id == 0 {
    false
  } else {
    candidate.used_at < current.used_at
  }
}

///|
/// A recycled slot only leaks a host mesh when the incoming handle differs from
/// the one it held.
fn webgpu_retired_mesh(retired : Int32, incoming : Int32) -> Int32? {
  if retired > 0 && retired != incoming {
    Some(retired)
  } else {
    None
  }
}

///|
let webgpu_path_mesh_cache_limit : Int32 = 256

///|
/// One cache per wasm instance: the host meshes it names live in the page
/// runtime this module was instantiated against, which is also per page.
let webgpu_path_mesh_cache : WebGpuPathMeshCache = WebGpuPathMeshCache::new(
  webgpu_path_mesh_cache_limit,
)