// 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.

///|
// The HOT rewrite keeps the exact target type until writeback, where casts
// and their descriptor operands have already been checked. At this final
// encoding boundary, a bottom null is a subtype of the nullable result.
fn oi_cleanup_descriptor_encoding(
  mod_ : @lib.Module,
  touched~ : Array[Bool]? = None,
) -> @lib.Module {
  if !oi_descriptor_encoding_has_pair(mod_) ||
    !mod_.custom_secs.is_empty() ||
    mod_.name_sec is Some(_) ||
    mod_.raw_name_sec_payload is Some(_) ||
    oi_local_group_has_offset_metadata(mod_) {
    return mod_
  }
  let body_cleaned = oi_cleanup_descriptor_terminal_bodies(mod_, touched~)
  if pass_encoding_cleanup_may_remap_types(body_cleaned, touched) {
    oi_prune_unused_descriptor_func_types(body_cleaned)
  } else {
    body_cleaned
  }
}

///|
fn oi_descriptor_encoding_has_pair(mod_ : @lib.Module) -> Bool {
  match mod_.type_sec {
    Some(@lib.TypeSec(groups)) if groups.length() > 0 =>
      match groups[0] {
        @lib.GroupRecType(members) if members.length() == 2 =>
          members[0].descriptor_type() == Some(@lib.TypeIdx::new(1U)) &&
          members[1].describes_type() == Some(@lib.TypeIdx::new(0U))
        _ => false
      }
    _ => false
  }
}

///|
fn oi_cleanup_descriptor_terminal_bodies(
  mod_ : @lib.Module,
  touched~ : Array[Bool]? = None,
) -> @lib.Module {
  let funcs = match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) => funcs
    None => return mod_
  }
  let env = @validate.Env::new().with_module(mod_)
  let mut changed = false
  let rewritten = funcs.mapi(fn(index, func) {
    if touched is Some(selected) &&
      (index >= selected.length() || !selected[index]) {
      return func
    }
    let @lib.Func(locals, @lib.Expr(body)) = func
    if body.is_empty() {
      return func
    }
    let next = body.copy()
    let last = next.length() - 1
    // The prefix must have no control transfer or use of the block label.
    // Its effects stay ordered, and the terminal trap never produces a value.
    match next[last] {
      @lib.Block(block_type, @lib.Expr(child)) if !child.is_empty() &&
        child[child.length() - 1] is @lib.Unreachable => {
        let prefix = child[0:child.length() - 1].to_array()
        let (_, _, branchless) = pass_lower_flatten_branchless_blocks(
          prefix, env,
        )
        if branchless {
          match env.expand_blocktype(block_type) {
            Ok((params, _)) if params.is_empty() => {
              ignore(next.pop())
              for instr in child {
                next.push(instr)
              }
            }
            _ => ()
          }
        }
      }
      _ => ()
    }
    let final_idx = next.length() - 1
    match next[final_idx] {
      @lib.RefNull(ref_type) if ref_type.is_nullable() &&
        ref_type.get_heap_type() == @lib.HeapType::new(@lib.TypeIdx::new(0U)) =>
        next[final_idx] = @lib.Instruction::ref_null(
          @lib.HeapType::abs(@lib.AbsHeapType::none()),
        )
      _ => ()
    }
    if next == body {
      return func
    }
    changed = true
    @lib.Func::new(locals, @lib.Expr::new(next))
  })
  if !changed {
    return mod_
  }
  let candidate = mod_.with_code_sec(@lib.CodeSec::new(rewritten))
  let baseline = if mod_.compiler_fact_custom_section is Some(_) {
    mod_.without_compiler_fact_custom_section()
  } else {
    mod_
  }
  match @binary.encoded_module_sizes(baseline, candidate) {
    Ok((old_size, new_size)) if new_size < old_size =>
      if @validate.validate_module(candidate) is Ok(_) {
        return candidate
      }
    _ => ()
  }
  mod_
}

///|
// Keep this two-member recursive descriptor pair and remove only standalone
// function signatures that no module root or retained type references. The
// DFE collector and remapper cover the descriptor forms in this pair and the
// plain function types eligible for removal.
fn oi_prune_unused_descriptor_func_types(mod_ : @lib.Module) -> @lib.Module {
  let groups = match mod_.type_sec {
    Some(@lib.TypeSec(groups)) => groups
    None => return mod_
  }
  if groups.length() < 2 {
    return mod_
  }
  for idx = 1; idx < groups.length(); idx = idx + 1 {
    match groups[idx] {
      @lib.SingleRecType(subtype) => {
        if subtype.metadata().is_shared() ||
          subtype.super_types().length() > 0 ||
          subtype.describes_type() is Some(_) ||
          subtype.descriptor_type() is Some(_) {
          return mod_
        }
        match subtype.get_comptype() {
          @lib.FuncCompType(_, _) => ()
          _ => return mod_
        }
      }
      _ => return mod_
    }
  }
  // The recursive pair occupies flat indices 0 and 1. Each suffix
  // SingleRecType occupies one subsequent index.
  let live : Array[Bool] = Array::make(groups.length() + 1, false)
  live[0] = true
  live[1] = true
  let refs : DfeTypeRefs = { live, pending: [0], }
  dfe_type_refs_module_roots(mod_, refs)
  let mut cursor = 0
  while cursor < refs.pending.length() {
    let flat_idx = refs.pending[cursor]
    if flat_idx == 0 {
      dfe_type_refs_rec_type(groups[0], refs)
    } else if flat_idx >= 2 {
      dfe_type_refs_rec_type(groups[flat_idx - 1], refs)
    }
    cursor += 1
  }
  let kept : Array[@lib.RecType] = []
  let remap : Map[UInt32, UInt32] = Map([])
  kept.push(groups[0])
  remap[0U] = 0U
  remap[1U] = 1U
  for idx = 1; idx < groups.length(); idx = idx + 1 {
    if refs.live[idx + 1] {
      remap[(idx + 1).to_uint()] = (kept.length() + 1).to_uint()
      kept.push(groups[idx])
    }
  }
  if kept.length() == groups.length() {
    return mod_
  }
  let candidate = dfe_rewrite_module_type_idxs(
    mod_.with_type_sec(@lib.TypeSec::new(kept)),
    remap,
  )
  // Type edits invalidate compiler facts. Compare the same encoded surfaces
  // so discarded facts cannot be mistaken for a code-size saving.
  let baseline = if mod_.compiler_fact_custom_section is Some(_) {
    mod_.without_compiler_fact_custom_section()
  } else {
    mod_
  }
  match @binary.encoded_module_sizes(baseline, candidate) {
    Ok((old_size, new_size)) if new_size < old_size =>
      if @validate.validate_module(candidate) is Ok(_) {
        return candidate
      }
    _ => ()
  }
  mod_
}