///|
pub(all) struct CwasmFunctionRecord {
  function_index : Int
  code_offset : Int
  code_size : Int
  frame_size : Int
  num_params : Int
  num_results : Int
} derive(Eq, Debug)

///|
pub(all) struct CwasmCallVeneer {
  function_index : Int
  func_idx : Int
  offset : Int
} derive(Eq, Debug)

///|
pub(all) struct CwasmArtifact {
  version : Int
  target : @cwasm.TargetArch
  entry_symbol : String
  code : Array[Int]
  functions : Array[CwasmFunctionRecord]
  call_veneers : Array[CwasmCallVeneer]
  runtime_relocations : Array[ResolvedRuntimeRelocation]
  func_addr_fixups : Array[@machv_emit.FuncAddrFixup]
  call_fixups : Array[@machv_emit.CallFixup]
  gc_safepoints : Array[GcSafepoint]
  gc_stackmap_blob : Bytes
} derive(Debug)

///|
fn emit_target_to_cwasm_target(
  target : @machv_emit.EmitTarget,
) -> @cwasm.TargetArch {
  match target {
    X64 => X86_64
    AArch64 => AArch64
  }
}

///|
fn cwasm_target_name(target : @cwasm.TargetArch) -> String {
  match target {
    Unknown => "unknown"
    X86_64 => "x86_64"
    AArch64 => "aarch64"
  }
}

///|
pub fn CwasmArtifact::from_linked_code_for_target(
  linked : LinkedCode,
  function_index : Int,
  target : @cwasm.TargetArch,
  entry_symbol : String,
) -> CwasmArtifact {
  CwasmArtifact::from_linked_code_for_target_with_signature(
    linked, function_index, target, entry_symbol, 0, 0,
  )
}

///|
pub fn CwasmArtifact::from_linked_code_for_target_with_signature(
  linked : LinkedCode,
  function_index : Int,
  target : @cwasm.TargetArch,
  entry_symbol : String,
  num_params : Int,
  num_results : Int,
) -> CwasmArtifact {
  let code = linked.code.copy()
  let call_veneers = collect_call_veneers(function_index, linked.call_fixups)
  {
    version: 1,
    target,
    entry_symbol,
    code,
    functions: [
      {
        function_index,
        code_offset: 0,
        code_size: code.length(),
        frame_size: linked.frame_size,
        num_params,
        num_results,
      },
    ],
    call_veneers,
    runtime_relocations: linked.runtime_relocations,
    func_addr_fixups: linked.func_addr_fixups,
    call_fixups: linked.call_fixups,
    gc_safepoints: linked.gc_safepoints,
    gc_stackmap_blob: linked.gc_stackmap_blob,
  }
}

///|
pub fn CwasmArtifact::from_jit_plan(
  plan : JitIntegrationPlan,
  resolver : RuntimeSymbolResolver,
  function_index : Int,
) -> CwasmArtifact raise LinkError {
  CwasmArtifact::from_jit_plan_with_signature(
    plan, resolver, function_index, 0, 0,
  )
}

///|
pub fn CwasmArtifact::from_jit_plan_with_signature(
  plan : JitIntegrationPlan,
  resolver : RuntimeSymbolResolver,
  function_index : Int,
  num_params : Int,
  num_results : Int,
) -> CwasmArtifact raise LinkError {
  CwasmArtifact::from_linked_code_for_target_with_signature(
    link_code_object(plan.object, resolver),
    function_index,
    emit_target_to_cwasm_target(plan.target),
    plan.entry_symbol,
    num_params,
    num_results,
  )
}

///|
fn CwasmArtifact::function_code(
  self : CwasmArtifact,
  record : CwasmFunctionRecord,
) -> Array[Int] {
  let out : Array[Int] = []
  let end = record.code_offset + record.code_size
  for i in record.code_offset.. Array[CwasmCallVeneer] {
  let out : Array[CwasmCallVeneer] = []
  let seen : Map[Int, Unit] = {}
  for fixup in call_fixups {
    let func_idx = cwasm_func_idx_for_reloc_target(fixup.target)
    if fixup.veneer_offset >= 0 && seen.get(func_idx) is None {
      seen.set(func_idx, ())
      out.push({ function_index, func_idx, offset: fixup.veneer_offset })
    }
  }
  out
}

///|
fn cwasm_func_idx_for_reloc_target(target : @instr.RelocTarget) -> Int {
  match machv_reloc_target_to_cwasm_func_idx(target) {
    Some(func_idx) => func_idx
    None => abort("cannot encode external reloc target in cwasm: \{target}")
  }
}

///|
fn CwasmArtifact::find_call_veneer(
  self : CwasmArtifact,
  function_index : Int,
  func_idx : Int,
) -> Int? {
  for veneer in self.call_veneers {
    if veneer.function_index == function_index && veneer.func_idx == func_idx {
      return Some(veneer.offset)
    }
  }
  None
}

///|
fn CwasmArtifact::function_gc_safepoints(
  self : CwasmArtifact,
  record : CwasmFunctionRecord,
) -> Array[@cwasm.GcSafepoint] {
  let out : Array[@cwasm.GcSafepoint] = []
  let start = record.code_offset
  let end = record.code_offset + record.code_size
  for safepoint in self.gc_safepoints {
    if safepoint.code_offset >= start && safepoint.code_offset < end {
      out.push(
        GcSafepoint(
          safepoint.code_offset - start,
          safepoint.live_refs,
          safepoint.metadata,
        ),
      )
    }
  }
  out
}

///|
fn CwasmArtifact::function_func_addr_fixups(
  self : CwasmArtifact,
  record : CwasmFunctionRecord,
) -> Array[@cwasm.FuncAddrFixup] {
  let out : Array[@cwasm.FuncAddrFixup] = []
  let start = record.code_offset
  let end = record.code_offset + record.code_size
  for fixup in self.func_addr_fixups {
    if fixup.offset >= start && fixup.offset < end {
      let func_idx = cwasm_func_idx_for_reloc_target(fixup.target)
      out.push({ offset: fixup.offset - start, func_idx, reg: fixup.reg })
    }
  }
  out
}

///|
fn CwasmArtifact::function_call_fixups(
  self : CwasmArtifact,
  record : CwasmFunctionRecord,
) -> Array[@cwasm.CallFixup] {
  let out : Array[@cwasm.CallFixup] = []
  let start = record.code_offset
  let end = record.code_offset + record.code_size
  for fixup in self.call_fixups {
    if fixup.offset >= start && fixup.offset < end {
      let func_idx = cwasm_func_idx_for_reloc_target(fixup.target)
      let veneer_offset = match
        self.find_call_veneer(record.function_index, func_idx) {
        Some(offset) => offset - start
        None => fixup.veneer_offset
      }
      out.push({ offset: fixup.offset - start, func_idx, veneer_offset })
    }
  }
  out
}

///|
fn append_u32_le(buf : Array[Byte], value : Int) -> Unit {
  buf.push((value & 0xFF).to_byte())
  buf.push(((value >> 8) & 0xFF).to_byte())
  buf.push(((value >> 16) & 0xFF).to_byte())
  buf.push(((value >> 24) & 0xFF).to_byte())
}

///|
pub fn build_gc_stackmap_blob(root_counts : Array[Int]) -> Bytes {
  if root_counts.length() == 0 {
    b""
  } else {
    let bytes : Array[Byte] = []
    append_u32_le(bytes, 1)
    append_u32_le(bytes, root_counts.length())
    for count in root_counts {
      append_u32_le(bytes, count)
    }
    Bytes::from_array(bytes)
  }
}

///|
pub fn gc_safepoint_metadata_to_root_count(metadata : Int64) -> Int {
  if metadata <= 0L {
    0
  } else if metadata > @types.I32_MAX.to_int64() {
    @types.I32_MAX
  } else {
    metadata.to_int()
  }
}

///|
fn encode_stackmap_root_count(root_count : Int) -> Int {
  if root_count <= 0 {
    0
  } else if root_count > @types.I32_MAX {
    @types.I32_MAX
  } else {
    root_count
  }
}

///|
fn has_stackmap_live_ref_indices(
  safepoints : Array[@cwasm.GcSafepoint],
) -> Bool {
  let mut found = false
  for safepoint in safepoints {
    if safepoint.live_refs.length() > 0 {
      found = true
    }
  }
  found
}

///|
pub fn build_gc_stackmap_blob_v2(
  safepoints : Array[@cwasm.GcSafepoint],
  default_root_counts : Array[Int],
) -> Bytes {
  if safepoints.length() == 0 {
    b""
  } else {
    let bytes : Array[Byte] = []
    append_u32_le(bytes, 2)
    append_u32_le(bytes, safepoints.length())
    for i, safepoint in safepoints {
      let default_root_count = if i < default_root_counts.length() {
        default_root_counts[i]
      } else {
        safepoint.live_refs.length()
      }
      let metadata_root_count = gc_safepoint_metadata_to_root_count(
        safepoint.metadata,
      )
      let mut root_count = encode_stackmap_root_count(
        if metadata_root_count > 0 {
          metadata_root_count
        } else {
          default_root_count
        },
      )
      let valid_indices : Array[Int] = []
      for live_ref in safepoint.live_refs {
        if live_ref >= 0 && live_ref <= @types.I32_MAX {
          valid_indices.push(live_ref)
        }
      }
      if valid_indices.length() > 0 {
        if root_count <= 0 || root_count > valid_indices.length() {
          root_count = valid_indices.length()
        }
        append_u32_le(bytes, root_count)
        append_u32_le(bytes, root_count)
        for idx in 0.. Array[@cwasm.GcSafepoint] {
  let safepoints : Array[@cwasm.GcSafepoint] = []
  for i, offset in offsets {
    let metadata = if i < root_counts.length() {
      root_counts[i].to_int64()
    } else {
      0L
    }
    let live_refs = if i < root_indices.length() { root_indices[i] } else { [] }
    safepoints.push(GcSafepoint(offset, live_refs, metadata))
  }
  safepoints
}

///|
pub fn build_gc_stackmap_blob_for_safepoints(
  safepoints : Array[@cwasm.GcSafepoint],
  root_counts : Array[Int],
  explicit_blob? : Bytes = b"",
) -> Bytes {
  if explicit_blob.length() > 0 {
    explicit_blob
  } else if has_stackmap_live_ref_indices(safepoints) {
    build_gc_stackmap_blob_v2(safepoints, root_counts)
  } else if root_counts.length() > 0 {
    build_gc_stackmap_blob(root_counts)
  } else if safepoints.length() > 0 {
    let counts : Array[Int] = []
    for safepoint in safepoints {
      counts.push(gc_safepoint_metadata_to_root_count(safepoint.metadata))
    }
    build_gc_stackmap_blob(counts)
  } else {
    b""
  }
}

///|
pub fn build_cwasm_entry_from_code(
  func_idx : Int,
  name : String,
  code : Array[Int],
  frame_size : Int,
  entry_offset : Int,
  num_params : Int,
  num_results : Int,
  func_addr_fixups? : Array[@cwasm.FuncAddrFixup] = [],
  call_fixups? : Array[@cwasm.CallFixup] = [],
  gc_safepoint_offsets? : Array[Int] = [],
  gc_safepoint_root_counts? : Array[Int] = [],
  gc_safepoint_root_indices? : Array[Array[Int]] = [],
  gc_safepoints? : Array[@cwasm.GcSafepoint] = [],
  gc_stackmap_blob? : Bytes = b"",
) -> @cwasm.CompiledEntry {
  let resolved_gc_safepoints = if gc_safepoints.length() > 0 {
    gc_safepoints
  } else {
    derive_gc_safepoints(
      gc_safepoint_offsets, gc_safepoint_root_counts, gc_safepoint_root_indices,
    )
  }
  CompiledEntry(
    func_idx,
    name,
    code,
    frame_size,
    entry_offset,
    num_params,
    num_results,
    func_addr_fixups~,
    call_fixups~,
    gc_safepoints=resolved_gc_safepoints,
    gc_stackmap_blob=build_gc_stackmap_blob_for_safepoints(
      resolved_gc_safepoints,
      gc_safepoint_root_counts,
      explicit_blob=gc_stackmap_blob,
    ),
  )
}

///|
pub fn CwasmArtifact::to_precompiled_module(
  self : CwasmArtifact,
) -> @cwasm.PrecompiledModule {
  let module_ = @cwasm.PrecompiledModule::PrecompiledModule(self.target)
  for record in self.functions {
    let gc_safepoints = self.function_gc_safepoints(record)
    let gc_root_counts = Array::make(gc_safepoints.length(), 0)
    module_.add_function(
      build_cwasm_entry_from_code(
        record.function_index,
        self.entry_symbol,
        self.function_code(record),
        record.frame_size,
        0,
        record.num_params,
        record.num_results,
        func_addr_fixups=self.function_func_addr_fixups(record),
        gc_safepoints~,
        gc_safepoint_root_counts=gc_root_counts,
        call_fixups=self.function_call_fixups(record),
        gc_stackmap_blob=self.gc_stackmap_blob,
      ),
    )
  }
  module_
}

///|
pub fn CwasmArtifact::serialize(self : CwasmArtifact) -> Array[Int] {
  self.to_precompiled_module().serialize()
}

///|
pub fn CwasmArtifact::manifest(self : CwasmArtifact) -> Array[String] {
  [
    "cwasm.version=" + self.version.to_string(),
    "cwasm.target=" + cwasm_target_name(self.target),
    "cwasm.code_size=" + self.code.length().to_string(),
    "cwasm.functions=" + self.functions.length().to_string(),
    "cwasm.runtime_relocations=" + self.runtime_relocations.length().to_string(),
    "cwasm.call_fixups=" + self.call_fixups.length().to_string(),
    "cwasm.serialized_size=" + self.serialize().length().to_string(),
  ]
}