// Licensing scope: see ../../LICENSE and this directory's LICENSE and NOTICE.
// Potentially incorporated Binaryen expression retains Apache-2.0 terms.
// Original Starshine contributions use the scoped MIT grant in ../../LICENSE.
// Modified for Starshine through 2026: implementation and validation use
// Starshine's representations and APIs; this is not an unchanged upstream file.
// Conservative notice retention does not establish copying or independence.

///|
// Runtime type identity includes the entire recursive group. Use canonical
// external identities and relative internal references as structural keys.
fn dae2_identity_value(
  value : @lib.ValType,
  remap : (@lib.TypeIdx) -> @lib.TypeIdx,
  embedded : Array[Bool],
) -> @lib.ValType {
  match value {
    @lib.RefTypeValType(rt) => {
      let heap = match rt.get_heap_type() {
        @lib.HeapType(index) => @lib.HeapType::new(remap(index))
        @lib.DefTypeHeapType(_) as heap => {
          // Embedded definitions do not have a module-owned identity closure.
          embedded[0] = true
          heap
        }
        heap => heap
      }
      @lib.ValType::ref_type(
        @lib.RefType::new(rt.is_nullable(), heap, exact=rt.is_exact()),
      )
    }
    _ => value
  }
}

///|
fn dae2_identity_field(
  field : @lib.FieldType,
  remap : (@lib.TypeIdx) -> @lib.TypeIdx,
  embedded : Array[Bool],
) -> @lib.FieldType {
  let @lib.FieldType(storage, mutable_) = field
  let storage = match storage {
    @lib.ValTypeStorageType(value) =>
      @lib.StorageType::val_type(dae2_identity_value(value, remap, embedded))
    _ => storage
  }
  @lib.FieldType::new(storage, mutable_)
}

///|
fn dae2_identity_subtype(
  subtype : @lib.SubType,
  remap : (@lib.TypeIdx) -> @lib.TypeIdx,
  embedded : Array[Bool],
) -> @lib.SubType {
  let comp = match subtype.get_comptype() {
    @lib.FuncCompType(params, results) =>
      @lib.CompType::func(
        params.map(fn(v) { dae2_identity_value(v, remap, embedded) }),
        results.map(fn(v) { dae2_identity_value(v, remap, embedded) }),
      )
    @lib.StructCompType(fields) =>
      @lib.CompType::struct_(
        fields.map(fn(f) { dae2_identity_field(f, remap, embedded) }),
      )
    @lib.ArrayCompType(field) =>
      @lib.CompType::array(dae2_identity_field(field, remap, embedded))
    @lib.ContCompType(target) => @lib.CompType::cont(remap(target))
  }
  let { describes, descriptor, shared, } = subtype.metadata()
  let metadata = @lib.TypeMetadata::new(
    describes=describes.map(remap),
    descriptor=descriptor.map(remap),
    shared~,
  )
  // A bare composite type has the binary meaning of a final subtype without
  // declared supertypes; singleton rec syntax does not change its identity.
  let final_ = match subtype {
    @lib.SubType(final_, _, _, _) => final_
    @lib.CompTypeSubType(_, _) => true
  }
  @lib.SubType::new(final_, subtype.super_types().map(remap), comp, metadata~)
}

///|
fn dae2_identity_partition(types : @lib.TypeSec) -> Array[Int32] {
  let @lib.TypeSec(groups) = types
  let identities : Array[Int32] = []
  let interned : @hashmap.HashMap[Array[@lib.SubType], Int32] = @hashmap.new()
  let embedded = [false]
  for group in groups {
    let members = match group {
      @lib.SingleRecType(subtype) => [subtype]
      @lib.GroupRecType(members) => members
    }
    let base = identities.length()
    let end = base + members.length()
    let remap = fn(index : @lib.TypeIdx) -> @lib.TypeIdx {
      match index {
        @lib.RecIdx(_) => index
        @lib.TypeIdx(raw) => {
          let slot = raw.reinterpret_as_int()
          if slot >= base && slot < end {
            @lib.TypeIdx::rec((slot - base).reinterpret_as_uint())
          } else {
            @lib.TypeIdx::new(identities[slot].reinterpret_as_uint())
          }
        }
      }
    }
    let key = members.map(fn(subtype) {
      dae2_identity_subtype(subtype, remap, embedded)
    })
    let first = match interned.get(key) {
      Some(first) => first
      None => {
        interned.set(key, base)
        base
      }
    }
    for member = 0; member < members.length(); member = member + 1 {
      identities.push(first + member)
    }
  }
  identities
}

///|
fn dae2_identity_dependencies(
  types : @lib.TypeSec,
  total : Int32,
  embedded : Array[Bool],
) -> Dae2Graph {
  let dependencies = Dae2Graph::new()
  for _ in 0.. [subtype]
      @lib.GroupRecType(members) => members
    }
    for offset = 0; offset < members.length(); offset = offset + 1 {
      let owner = base + offset
      // One member's identity depends on all siblings in its recursive group.
      dependencies.edge(owner, base)
      dependencies.edge(base, owner)
      ignore(
        dae2_identity_subtype(
          members[offset],
          fn(index) {
            dependencies.edge(owner, dae2_type_slot(index, base))
            index
          },
          embedded,
        ),
      )
    }
    base += members.length()
  }
  dependencies
}

///|
fn dae2_preserve_type_identity(
  types : @lib.TypeSec,
  graph : Dae2Graph,
  infos : Array[Dae2Function],
  type_infos : Array[Int32],
) -> Unit {
  if !type_infos.any(fn(index) {
      index >= 0 && dae2_signature_changes(graph, infos[index])
    }) {
    return
  }
  let embedded = [false]
  let dependencies = dae2_identity_dependencies(
    types,
    type_infos.length(),
    embedded,
  )
  if embedded[0] {
    // Type-section pruning through embedded definitions is intentionally
    // unsupported; private direct function signatures remain independently live.
    for index in type_infos {
      if index >= 0 {
        for location in infos[index].param_locations {
          graph.observe(location)
        }
        graph.observe(infos[index].result_location)
      }
    }
    graph.solve()
    return
  }
  let original = dae2_identity_partition(types)
  while true {
    let proposed = dae2_identity_partition(
      dae2_rewrite_type_section(types, graph, infos, type_infos),
    )
    let old_representatives = Array::make(original.length(), -1)
    let new_representatives = Array::make(original.length(), -1)
    let mut conflict = false
    for index = 0; index < original.length(); index = index + 1 {
      let old_rep = old_representatives[original[index]]
      let new_rep = new_representatives[proposed[index]]
      if old_rep >= 0 && proposed[old_rep] != proposed[index] {
        dependencies.observe(old_rep)
        dependencies.observe(index)
        conflict = true
      }
      if new_rep >= 0 && original[new_rep] != original[index] {
        dependencies.observe(new_rep)
        dependencies.observe(index)
        conflict = true
      }
      old_representatives[original[index]] = index
      new_representatives[proposed[index]] = index
    }
    if !conflict {
      return
    }
    dependencies.solve()
    let before = graph.work.length()
    for index = 0; index < type_infos.length(); index = index + 1 {
      let info_index = type_infos[index]
      if dependencies.used[index] && info_index >= 0 {
        for location in infos[info_index].param_locations {
          graph.observe(location)
        }
        graph.observe(infos[info_index].result_location)
      }
    }
    graph.solve()
    if graph.work.length() == before {
      // A conservative full type closure is a bounded fallback if a future type
      // feature escapes the dependency inventory. It preserves all identities.
      for index in type_infos {
        if index >= 0 {
          for location in infos[index].param_locations {
            graph.observe(location)
          }
          graph.observe(infos[index].result_location)
        }
      }
      graph.solve()
      return
    }
  }
}