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

///|
fn dae_snapshot_module_unchanged(
  left : @lib.Module,
  right : @lib.Module,
) -> Bool {
  // Shared owned code storage proves that its bytes did not change, including
  // NaNs whose numeric Eq is non-reflexive. Check every other field under the
  // encoded identity contract without revisiting those same bodies.
  if physical_equal(left, right) {
    return true
  }
  if !dae_snapshot_code_unchanged(left.code_sec, right.code_sec) {
    return false
  }
  dae_snapshot_non_code_unchanged(left, right)
}

///|
fn dae_snapshot_code_unchanged(
  left : @lib.CodeSec?,
  right : @lib.CodeSec?,
) -> Bool {
  match (left, right) {
    (Some(@lib.CodeSec(lhs)), Some(@lib.CodeSec(rhs))) =>
      dae_snapshot_funcs_unchanged(lhs, rhs)
    (None, None) => true
    _ => false
  }
}

///|
fn dae_snapshot_funcs_unchanged(
  left : Array[@lib.Func],
  right : Array[@lib.Func],
) -> Bool {
  if physical_equal(left, right) {
    return true
  }
  if left.length() != right.length() {
    return false
  }
  for index = 0; index < left.length(); index = index + 1 {
    if !dae_snapshot_func_unchanged(left[index], right[index]) {
      return false
    }
  }
  true
}

///|
fn dae_snapshot_func_unchanged(left : @lib.Func, right : @lib.Func) -> Bool {
  if physical_equal(left, right) {
    return true
  }
  let @lib.Func(left_locals, left_expr) = left
  let @lib.Func(right_locals, right_expr) = right
  (physical_equal(left_locals, right_locals) || left_locals == right_locals) &&
  dae_snapshot_expr_unchanged(left_expr, right_expr)
}

///|
fn dae_snapshot_expr_unchanged(left : @lib.Expr, right : @lib.Expr) -> Bool {
  let @lib.Expr(left_body) = left
  let @lib.Expr(right_body) = right
  dae_snapshot_array_unchanged(
    left_body, right_body, dae_snapshot_instr_unchanged,
  )
}

///|
#inline
fn[T] dae_snapshot_array_unchanged(
  left : Array[T],
  right : Array[T],
  equal : (T, T) -> Bool,
) -> Bool {
  if physical_equal(left, right) {
    return true
  }
  if left.length() != right.length() {
    return false
  }
  for i in 0.. Bool,
) -> Bool {
  match (left, right) {
    (Some(left), Some(right)) =>
      physical_equal(left, right) || equal(left, right)
    (None, None) => true
    _ => false
  }
}

///|
fn dae_snapshot_instr_unchanged(
  left : @lib.Instruction,
  right : @lib.Instruction,
) -> Bool {
  if physical_equal(left, right) {
    return true
  }
  match (left, right) {
    (@lib.F32Const(@lib.F32(a)), @lib.F32Const(@lib.F32(b))) =>
      a.reinterpret_as_int() == b.reinterpret_as_int()
    (@lib.F64Const(@lib.F64(a)), @lib.F64Const(@lib.F64(b))) =>
      a.reinterpret_as_int64() == b.reinterpret_as_int64()
    (@lib.Block(a, x), @lib.Block(b, y)) | (@lib.Loop(a, x), @lib.Loop(b, y)) =>
      a == b && dae_snapshot_expr_unchanged(x, y)
    (@lib.If(a, x, tx), @lib.If(b, y, ty)) =>
      a == b &&
      dae_snapshot_array_unchanged(x, y, dae_snapshot_instr_unchanged) &&
      dae_snapshot_optional_unchanged(tx, ty, fn(x, y) {
        dae_snapshot_array_unchanged(x, y, dae_snapshot_instr_unchanged)
      })
    (@lib.TryTable(a, cx, x), @lib.TryTable(b, cy, y)) =>
      a == b && cx == cy && dae_snapshot_expr_unchanged(x, y)
    (@lib.Try(a, x, cx, dx), @lib.Try(b, y, cy, dy)) =>
      a == b &&
      dx == dy &&
      dae_snapshot_expr_unchanged(x, y) &&
      dae_snapshot_array_unchanged(cx, cy, fn(l, r) {
        match (l, r) {
          (
            @lib.LegacyCatch::LegacyCatch(a, x),
            @lib.LegacyCatch::LegacyCatch(b, y),
          ) => a == b && dae_snapshot_expr_unchanged(x, y)
          (
            @lib.LegacyCatch::LegacyCatchAll(x),
            @lib.LegacyCatch::LegacyCatchAll(y),
          ) => dae_snapshot_expr_unchanged(x, y)
          _ => false
        }
      })
    _ => left == right
  }
}

///|
fn dae_snapshot_globals_unchanged(
  left : @lib.GlobalSec,
  right : @lib.GlobalSec,
) -> Bool {
  dae_snapshot_array_unchanged(left.0, right.0, fn(l, r) {
    let @lib.Global(a, x) = l
    let @lib.Global(b, y) = r
    a == b && dae_snapshot_expr_unchanged(x, y)
  })
}

///|
fn dae_snapshot_tables_unchanged(
  left : @lib.TableSec,
  right : @lib.TableSec,
) -> Bool {
  dae_snapshot_array_unchanged(left.0, right.0, fn(l, r) {
    let @lib.Table(a, x) = l
    let @lib.Table(b, y) = r
    a == b && dae_snapshot_optional_unchanged(x, y, dae_snapshot_expr_unchanged)
  })
}

///|
fn dae_snapshot_elements_unchanged(
  left : @lib.ElemSec,
  right : @lib.ElemSec,
) -> Bool {
  dae_snapshot_array_unchanged(left.0, right.0, fn(l, r) {
    let @lib.Elem(lm, lk) = l
    let @lib.Elem(rm, rk) = r
    let mode = match (lm, rm) {
      (@lib.ElemMode::Active(a, x), @lib.ElemMode::Active(b, y)) =>
        a == b && dae_snapshot_expr_unchanged(x, y)
      _ => lm == rm
    }
    mode &&
    (match (lk, rk) {
      (@lib.ElemKind::FuncExprsElemKind(x), @lib.ElemKind::FuncExprsElemKind(y)) =>
        dae_snapshot_array_unchanged(x, y, dae_snapshot_expr_unchanged)
      (
        @lib.ElemKind::TypedExprsElemKind(a, x),
        @lib.ElemKind::TypedExprsElemKind(b, y),
      ) =>
        a == b &&
        dae_snapshot_array_unchanged(x, y, dae_snapshot_expr_unchanged)
      _ => lk == rk
    })
  })
}

///|
fn dae_snapshot_data_unchanged(
  left : @lib.DataSec,
  right : @lib.DataSec,
) -> Bool {
  dae_snapshot_array_unchanged(left.0, right.0, fn(l, r) {
    let @lib.Data(lm, lb) = l
    let @lib.Data(rm, rb) = r
    lb == rb &&
    (match (lm, rm) {
      (@lib.DataMode::Active(a, x), @lib.DataMode::Active(b, y)) =>
        a == b && dae_snapshot_expr_unchanged(x, y)
      _ => lm == rm
    })
  })
}

///|
#inline
fn[T : Eq] dae_snapshot_owned_optional_unchanged(left : T?, right : T?) -> Bool {
  match (left, right) {
    (Some(left), Some(right)) => physical_equal(left, right) || left == right
    (None, None) => true
    _ => false
  }
}

///|
// Keep these fields aligned with Module::equal in src/lib/eq.mbt. Shared owned
// fields prove bit identity, including NaN initializers. Distinct expressions
// compare float bits; other fields retain Module's equality contract, including
// unordered custom-section membership.
fn dae_snapshot_non_code_unchanged(l : @lib.Module, r : @lib.Module) -> Bool {
  let l_custom = l.custom_secs
  let r_custom = r.custom_secs
  if !physical_equal(l_custom, r_custom) {
    if l_custom.length() != r_custom.length() {
      return false
    }
    for lc in l_custom {
      if !r_custom.contains(lc) {
        return false
      }
    }
    for rc in r_custom {
      if !l_custom.contains(rc) {
        return false
      }
    }
  }
  dae_snapshot_owned_optional_unchanged(l.type_sec, r.type_sec) &&
  dae_snapshot_owned_optional_unchanged(l.name_sec, r.name_sec) &&
  dae_snapshot_owned_optional_unchanged(
    l.compiler_fact_custom_section,
    r.compiler_fact_custom_section,
  ) &&
  dae_snapshot_owned_optional_unchanged(l.import_sec, r.import_sec) &&
  dae_snapshot_owned_optional_unchanged(
    l.func_annotation_sec,
    r.func_annotation_sec,
  ) &&
  dae_snapshot_owned_optional_unchanged(l.func_sec, r.func_sec) &&
  dae_snapshot_optional_unchanged(
    l.table_sec,
    r.table_sec,
    dae_snapshot_tables_unchanged,
  ) &&
  dae_snapshot_owned_optional_unchanged(l.mem_sec, r.mem_sec) &&
  dae_snapshot_owned_optional_unchanged(l.tag_sec, r.tag_sec) &&
  dae_snapshot_owned_optional_unchanged(l.stringrefs_sec, r.stringrefs_sec) &&
  dae_snapshot_optional_unchanged(
    l.global_sec,
    r.global_sec,
    dae_snapshot_globals_unchanged,
  ) &&
  dae_snapshot_owned_optional_unchanged(l.export_sec, r.export_sec) &&
  dae_snapshot_owned_optional_unchanged(l.start_sec, r.start_sec) &&
  dae_snapshot_optional_unchanged(
    l.elem_sec,
    r.elem_sec,
    dae_snapshot_elements_unchanged,
  ) &&
  dae_snapshot_owned_optional_unchanged(l.data_cnt_sec, r.data_cnt_sec) &&
  dae_snapshot_optional_unchanged(
    l.data_sec,
    r.data_sec,
    dae_snapshot_data_unchanged,
  )
}