///|
/// Store-local identities retain recursive-group equivalence across modules.
priv struct RegisteredTypeGraph {
  id : Int64
  types : ArrayView[@types.SubType]
  groups : ArrayView[Int32]
  mapping : Array[Int32]
} derive(Debug)

///|
priv struct RegisteredInstanceTypes {
  instance : ModuleInstance
  graph : RegisteredTypeGraph
} derive(Debug)

///|
priv struct StoreTypeRegistry {
  representatives : Array[(RegisteredTypeGraph, Int32)]
  group_buckets : Map[Int64, Array[(RegisteredTypeGraph, Array[Int32])]]
  mappings : Map[Int32, RegisteredInstanceTypes]
  mut initialization_graph : RegisteredTypeGraph?
  mut next_graph_id : Int64
  parents : Array[Int32]
  views : Array[NativeCallableTypeView]
  mut native_registry : @native_bridge.NativeCallableRegistry?
  entries : Map[Int64, Int32]
  heap_types : Map[Int32, Int32]
} derive(Debug)

///|
fn StoreTypeRegistry::new() -> StoreTypeRegistry {
  {
    representatives: [],
    group_buckets: Map([]),
    mappings: Map([]),
    initialization_graph: None,
    next_graph_id: 0L,
    parents: [],
    views: [],
    native_registry: None,
    entries: Map([]),
    heap_types: Map([]),
  }
}

///|
fn StoreTypeRegistry::register(
  self : StoreTypeRegistry,
  instance : ModuleInstance,
) -> Array[Int32] {
  if self.mappings.get(instance.store_idx) is Some(cached) &&
    physical_equal(cached.instance, instance) {
    return cached.graph.mapping
  }
  // These snapshots are immutable. Compare them only when a new live instance
  // adopts the initialization graph, never on an already registered lookup.
  if self.initialization_graph is Some(graph) &&
    graph.types == instance.types &&
    graph.groups == instance.type_rec_groups {
    self.mappings[instance.store_idx] = { instance, graph, }
    return graph.mapping
  }
  self.register_graph(instance.types, instance.type_rec_groups, Some(instance)).mapping
}

///|
fn StoreTypeRegistry::register_graph(
  self : StoreTypeRegistry,
  types : ArrayView[@types.SubType],
  groups : ArrayView[Int32],
  instance : ModuleInstance?,
) -> RegisteredTypeGraph {
  let mapping = Array::make(types.length(), -1)
  let graph = { id: self.next_graph_id, types, groups, mapping, }
  self.next_graph_id += 1L
  // Reuse graph indices and completed equivalence results within this registration.
  // These contexts must not outlive the two immutable graphs they compare.
  let comparisons : Map[Int64, @types.TypeEquivalenceContext] = Map([])
  let first_new = self.representatives.length()
  @types.fold_type_groups(
    types,
    type_rec_groups=groups,
    external_identity=fn(index) {
      if index < mapping.length() {
        mapping[index]
      } else {
        -1
      }
    },
    visit=fn(members, fingerprint) {
      let candidates = self.group_buckets.get_or_default(fingerprint, [])
      let mut matched = false
      for (other, previous) in candidates {
        let equivalent = match comparisons.get(other.id) {
          Some(context) => context
          None => {
            let context = @types.TypeEquivalenceContext::cross_module(
              types,
              other.types,
              rec_groups1=groups,
              rec_groups2=other.groups,
            )
            comparisons[other.id] = context
            context
          }
        }
        if equivalent.types_equivalent(members[0], previous[0]) {
          for position, index in members {
            mapping[index] = other.mapping[previous[position]]
          }
          matched = true
          break
        }
      }
      if !matched {
        for index in members {
          mapping[index] = self.representatives.length()
          self.representatives.push((graph, index))
          self.parents.push(-1)
        }
        candidates.push((graph, members.to_owned()))
        self.group_buckets[fingerprint] = candidates
      }
    },
  )
  // Install ownership before publication: a refresh callback may reenter,
  // replace the initialization scope, roll back, or close the Store.
  match instance {
    Some(instance) => self.mappings[instance.store_idx] = { instance, graph, }
    None => self.initialization_graph = Some(graph)
  }
  for identity in first_new.. first_new {
    self.publish()
  }
  graph
}

///|
pub fn Store::native_type_indices(
  self : Store,
  instance : ModuleInstance,
) -> Array[Int32] {
  self.check_instance(instance) catch {
    _ => return []
  }
  self.native_types.register(instance).copy()
}

///|
pub fn Store::native_type_parents(self : Store) -> Array[Int32] {
  self.native_types.parents.copy()
}

///|
priv struct NativeCallableTypeView {
  refresh : (Array[Int32], Array[(Int64, Int32)]) -> Unit
}

///|
impl Debug for NativeCallableTypeView with fn to_repr(_self) {
  Repr::literal("NativeCallableTypeView")
}

///|
/// Publish the complete Store type universe to all installed callable contexts.
pub fn Store::bind_native_callable_view(
  self : Store,
  refresh : (Array[Int32], Array[(Int64, Int32)]) -> Unit,
) -> Unit {
  self.native_types.views.push({ refresh, })
  for entry in self.jit_funcref_entries() {
    self.native_types.entries[entry.func_ptr] = self.get_func_canonical_type_idx(
      entry.store_addr,
    )
  }
  self.native_types.publish()
}

///|
/// Share Store-wide callable identities while each context owns its local maps.
/// Returns None after the Store is closed.
pub fn Store::native_callable_registry(
  self : Store,
) -> @native_bridge.NativeCallableRegistry? {
  guard !self.closed.val else { return None }
  let mut changed = false
  let registry = match self.native_types.native_registry {
    Some(registry) => registry
    None => {
      changed = true
      let registry = @native_bridge.NativeCallableRegistry::new()
      self.native_types.native_registry = Some(registry)
      registry
    }
  }
  if self.funcref_registry.native is None {
    self.funcref_registry.native = Some(registry)
    self.funcref_registry.publish_functions()
  }
  for entry in self.jit_funcref_entries() {
    let identity = self.get_func_canonical_type_idx(entry.store_addr)
    if self.native_types.entries.get(entry.func_ptr) != Some(identity) {
      self.native_types.entries[entry.func_ptr] = identity
      changed = true
    }
  }
  // Type identity does not depend on whether executable code is registered.
  for address in 0..= 0 &&
      self.native_types.entries.get(reference) != Some(identity) {
      self.native_types.entries[reference] = identity
      changed = true
    }
  }
  if changed {
    self.native_types.publish()
  }
  Some(registry)
}

///|
#cfg(target="native")
fn Store::record_gc_type(
  self : Store,
  index : Int32,
  instance : ModuleInstance?,
  type_index : Int32,
) -> Unit {
  let identity = match instance {
    Some(instance) => self.native_types.register(instance)[type_index]
    None =>
      if type_index >= 0 && type_index < self.module_types.length() {
        self.module_type_indices()[type_index]
      } else {
        -1
      }
  }
  match self.references.native.c_heap {
    Some(heap) => heap.set_runtime_type(index, identity)
    None => self.native_types.heap_types[index] = identity
  }
}

///|
#cfg(target="native")
pub fn Store::gc_type_is_subtype(
  self : Store,
  reference : Int32,
  instance : ModuleInstance,
  expected : Int32,
) -> Bool {
  self.check_instance(instance) catch {
    _ => return false
  }
  guard expected >= 0 && expected < instance.types.length() else {
    return false
  }
  let actual = match self.references.native.c_heap {
    Some(heap) => heap.runtime_type(reference)
    None => self.native_types.heap_types.get(reference).unwrap_or(-1)
  }
  if actual < 0 {
    let local_index = self.get_gc_type_idx(reference) catch {
      _ => return false
    }
    return @types.SubtypingContext::same_module(
      instance.types,
      rec_groups=instance.type_rec_groups,
    ).is_subtype(local_index, expected)
  }
  let target = self.native_types.register(instance)[expected]
  self.native_types.is_subtype(actual, target)
}

///|
fn StoreTypeRegistry::is_subtype(
  self : StoreTypeRegistry,
  actual : Int32,
  expected : Int32,
) -> Bool {
  for current = actual; current >= 0 && current < self.parents.length(); {
    if current == expected {
      return true
    }
    let parent = self.parents[current]
    if parent == current {
      return false
    }
    continue parent
  }
  false
}

///|
#cfg(target="native")
fn Store::gc_type_definition(
  self : Store,
  reference : Int32,
  fallback : Int32,
) -> @types.SubType? {
  let identity = match self.references.native.c_heap {
    Some(heap) => heap.runtime_type(reference)
    None => self.native_types.heap_types.get(reference).unwrap_or(-1)
  }
  if identity >= 0 && identity < self.native_types.representatives.length() {
    let (graph, index) = self.native_types.representatives[identity]
    return Some(graph.types[index])
  }
  if fallback >= 0 && fallback < self.module_types.length() {
    Some(self.module_types[fallback])
  } else {
    None
  }
}

///|
fn StoreTypeRegistry::publish(self : StoreTypeRegistry) -> Unit {
  if self.native_registry is None && self.views.is_empty() {
    return
  }
  let entries : Array[(Int64, Int32)] = []
  for pointer, identity in self.entries {
    entries.push((pointer, identity))
  }
  if self.native_registry is Some(registry) {
    if !registry.replace(self.parents, entries) {
      abort("failed to publish native callable identities")
    }
  }
  if self.views.is_empty() {
    return
  }
  for view in self.views {
    (view.refresh)(self.parents.copy(), entries.copy())
  }
}

///|
pub fn Store::func_type_is_subtype(
  self : Store,
  address : Int32,
  instance : ModuleInstance,
  expected : Int32,
) -> Bool {
  self.check_instance(instance) catch {
    _ => return false
  }
  guard expected >= 0 && expected < instance.types.length() else {
    return false
  }
  let actual = self.get_func_canonical_type_idx(address)
  if actual < 0 {
    return false
  }
  self.native_types.is_subtype(
    actual,
    self.native_types.register(instance)[expected],
  )
}

///|
fn Store::module_type_indices(self : Store) -> Array[Int32] {
  if self.native_types.initialization_graph is Some(graph) {
    return graph.mapping
  }
  let graph = self.native_types.register_graph(
    self.module_types,
    self.module_rec_groups,
    None,
  )
  graph.mapping
}