///|
pub(all) enum WasmOpcode {
  LoadMemBase(Int)
  MemoryGrow(Int, Int?)
  MemorySize(Int)
  MemoryFill(Int)
  MemoryCopy(Int, Int)
  MemoryInit(Int, Int)
  DataDrop(Int)
  TableGrow(Int)
  TableFill(Int)
  TableCopy(Int, Int)
  TableInit(Int, Int)
  ElemDrop(Int)
  WasmCall(Int)
  WasmCallIndirect(Int, Int)
  CallRef(Int)
  ReturnCall(Int)
  ReturnCallIndirect(Int, Int)
  ReturnCallRef(Int)
  GetFuncRef(Int)
  StructNew(Int)
  StructNewDefault(Int)
  StructGet(Int, Int)
  StructGetS(Int, Int, Int)
  StructGetU(Int, Int, Int)
  StructSet(Int, Int)
  ArrayNew(Int)
  ArrayNewDefault(Int)
  ArrayNewFixed(Int, Int)
  ArrayGet(Int)
  ArrayGetS(Int, Int)
  ArrayGetU(Int, Int)
  ArraySet(Int)
  ArrayLen
  ArrayFill(Int)
  ArrayCopy(Int, Int)
  ArrayNewData(Int, Int)
  ArrayNewElem(Int, Int)
  ArrayInitData(Int, Int)
  ArrayInitElem(Int, Int)
  I31New
  I31GetS
  I31GetU
  RefTest(Int, Bool)
  RefCast(Int, Bool)
  AnyConvertExtern
  ExternConvertAny
  RefEq
  Throw(Int)
  ThrowRef
  TryTableBegin(Int)
  TryTableEnd(Int)
  GetExceptionTag
  GetExceptionValue(Int)
  GetExceptionValueCount
  Delegate(Int)
  SpillLocalsForThrow(Int)
  GetSpilledLocal(Int)
} derive(Debug, Eq, Hash)

///|
fn imm(values : Array[Int]) -> FixedArray[Int] {
  FixedArray::makei(values.length(), fn(i) { values[i] })
}

///|
fn bool_imm(value : Bool) -> Int {
  if value {
    1
  } else {
    0
  }
}

///|
fn imm_bool(value : Int) -> Bool {
  value != 0
}

///|
pub fn encode(opcode : WasmOpcode) -> @milkir.ExtOp {
  let (name, immediates) = match opcode {
    LoadMemBase(memidx) => ("load_mem_base", imm([memidx]))
    MemoryGrow(memidx, max_pages) =>
      (
        "memory_grow",
        match max_pages {
          Some(max) => imm([memidx, max])
          None => imm([memidx])
        },
      )
    MemorySize(memidx) => ("memory_size", imm([memidx]))
    MemoryFill(memidx) => ("memory_fill", imm([memidx]))
    MemoryCopy(dst_memidx, src_memidx) =>
      ("memory_copy", imm([dst_memidx, src_memidx]))
    MemoryInit(memidx, data_idx) => ("memory_init", imm([memidx, data_idx]))
    DataDrop(data_idx) => ("data_drop", imm([data_idx]))
    TableGrow(table_idx) => ("table_grow", imm([table_idx]))
    TableFill(table_idx) => ("table_fill", imm([table_idx]))
    TableCopy(dst_table_idx, src_table_idx) =>
      ("table_copy", imm([dst_table_idx, src_table_idx]))
    TableInit(table_idx, elem_idx) => ("table_init", imm([table_idx, elem_idx]))
    ElemDrop(elem_idx) => ("elem_drop", imm([elem_idx]))
    WasmCall(func_idx) => ("call", imm([func_idx]))
    WasmCallIndirect(type_idx, table_idx) =>
      ("call_indirect", imm([type_idx, table_idx]))
    CallRef(type_idx) => ("call_ref", imm([type_idx]))
    ReturnCall(func_idx) => ("return_call", imm([func_idx]))
    ReturnCallIndirect(type_idx, table_idx) =>
      ("return_call_indirect", imm([type_idx, table_idx]))
    ReturnCallRef(type_idx) => ("return_call_ref", imm([type_idx]))
    GetFuncRef(func_idx) => ("get_func_ref", imm([func_idx]))
    StructNew(type_idx) => ("struct_new", imm([type_idx]))
    StructNewDefault(type_idx) => ("struct_new_default", imm([type_idx]))
    StructGet(type_idx, field_idx) => ("struct_get", imm([type_idx, field_idx]))
    StructGetS(type_idx, field_idx, byte_width) =>
      ("struct_get_s", imm([type_idx, field_idx, byte_width]))
    StructGetU(type_idx, field_idx, byte_width) =>
      ("struct_get_u", imm([type_idx, field_idx, byte_width]))
    StructSet(type_idx, field_idx) => ("struct_set", imm([type_idx, field_idx]))
    ArrayNew(type_idx) => ("array_new", imm([type_idx]))
    ArrayNewDefault(type_idx) => ("array_new_default", imm([type_idx]))
    ArrayNewFixed(type_idx, len) => ("array_new_fixed", imm([type_idx, len]))
    ArrayGet(type_idx) => ("array_get", imm([type_idx]))
    ArrayGetS(type_idx, byte_width) =>
      ("array_get_s", imm([type_idx, byte_width]))
    ArrayGetU(type_idx, byte_width) =>
      ("array_get_u", imm([type_idx, byte_width]))
    ArraySet(type_idx) => ("array_set", imm([type_idx]))
    ArrayLen => ("array_len", imm([]))
    ArrayFill(type_idx) => ("array_fill", imm([type_idx]))
    ArrayCopy(dst_type_idx, src_type_idx) =>
      ("array_copy", imm([dst_type_idx, src_type_idx]))
    ArrayNewData(type_idx, data_idx) =>
      ("array_new_data", imm([type_idx, data_idx]))
    ArrayNewElem(type_idx, elem_idx) =>
      ("array_new_elem", imm([type_idx, elem_idx]))
    ArrayInitData(type_idx, data_idx) =>
      ("array_init_data", imm([type_idx, data_idx]))
    ArrayInitElem(type_idx, elem_idx) =>
      ("array_init_elem", imm([type_idx, elem_idx]))
    I31New => ("i31_new", imm([]))
    I31GetS => ("i31_get_s", imm([]))
    I31GetU => ("i31_get_u", imm([]))
    RefTest(type_idx, nullable) =>
      ("ref_test", imm([type_idx, bool_imm(nullable)]))
    RefCast(type_idx, nullable) =>
      ("ref_cast", imm([type_idx, bool_imm(nullable)]))
    AnyConvertExtern => ("any_convert_extern", imm([]))
    ExternConvertAny => ("extern_convert_any", imm([]))
    RefEq => ("ref_eq", imm([]))
    Throw(tag_idx) => ("throw", imm([tag_idx]))
    ThrowRef => ("throw_ref", imm([]))
    TryTableBegin(handler_id) => ("try_table_begin", imm([handler_id]))
    TryTableEnd(handler_id) => ("try_table_end", imm([handler_id]))
    GetExceptionTag => ("get_exception_tag", imm([]))
    GetExceptionValue(idx) => ("get_exception_value", imm([idx]))
    GetExceptionValueCount => ("get_exception_value_count", imm([]))
    Delegate(depth) => ("delegate", imm([depth]))
    SpillLocalsForThrow(count) => ("spill_locals_for_throw", imm([count]))
    GetSpilledLocal(idx) => ("get_spilled_local", imm([idx]))
  }
  ExtOp("wasm", name, immediates)
}

///|
pub fn decode(ext : @milkir.ExtOp) -> WasmOpcode? {
  guard ext.dialect == "wasm" else { return None }
  let imms = ext.immediates
  match ext.opcode {
    "load_mem_base" if imms.length() == 1 => Some(LoadMemBase(imms[0]))
    "memory_grow" if imms.length() == 1 => Some(MemoryGrow(imms[0], None))
    "memory_grow" if imms.length() == 2 =>
      Some(MemoryGrow(imms[0], Some(imms[1])))
    "memory_size" if imms.length() == 1 => Some(MemorySize(imms[0]))
    "memory_fill" if imms.length() == 1 => Some(MemoryFill(imms[0]))
    "memory_copy" if imms.length() == 2 => Some(MemoryCopy(imms[0], imms[1]))
    "memory_init" if imms.length() == 2 => Some(MemoryInit(imms[0], imms[1]))
    "data_drop" if imms.length() == 1 => Some(DataDrop(imms[0]))
    "table_grow" if imms.length() == 1 => Some(TableGrow(imms[0]))
    "table_fill" if imms.length() == 1 => Some(TableFill(imms[0]))
    "table_copy" if imms.length() == 2 => Some(TableCopy(imms[0], imms[1]))
    "table_init" if imms.length() == 2 => Some(TableInit(imms[0], imms[1]))
    "elem_drop" if imms.length() == 1 => Some(ElemDrop(imms[0]))
    "call" if imms.length() == 1 => Some(WasmCall(imms[0]))
    "call_indirect" if imms.length() == 2 =>
      Some(WasmCallIndirect(imms[0], imms[1]))
    "call_ref" if imms.length() == 1 => Some(CallRef(imms[0]))
    "return_call" if imms.length() == 1 => Some(ReturnCall(imms[0]))
    "return_call_indirect" if imms.length() == 2 =>
      Some(ReturnCallIndirect(imms[0], imms[1]))
    "return_call_ref" if imms.length() == 1 => Some(ReturnCallRef(imms[0]))
    "get_func_ref" if imms.length() == 1 => Some(GetFuncRef(imms[0]))
    "struct_new" if imms.length() == 1 => Some(StructNew(imms[0]))
    "struct_new_default" if imms.length() == 1 =>
      Some(StructNewDefault(imms[0]))
    "struct_get" if imms.length() == 2 => Some(StructGet(imms[0], imms[1]))
    "struct_get_s" if imms.length() == 3 =>
      Some(StructGetS(imms[0], imms[1], imms[2]))
    "struct_get_u" if imms.length() == 3 =>
      Some(StructGetU(imms[0], imms[1], imms[2]))
    "struct_set" if imms.length() == 2 => Some(StructSet(imms[0], imms[1]))
    "array_new" if imms.length() == 1 => Some(ArrayNew(imms[0]))
    "array_new_default" if imms.length() == 1 => Some(ArrayNewDefault(imms[0]))
    "array_new_fixed" if imms.length() == 2 =>
      Some(ArrayNewFixed(imms[0], imms[1]))
    "array_get" if imms.length() == 1 => Some(ArrayGet(imms[0]))
    "array_get_s" if imms.length() == 2 => Some(ArrayGetS(imms[0], imms[1]))
    "array_get_u" if imms.length() == 2 => Some(ArrayGetU(imms[0], imms[1]))
    "array_set" if imms.length() == 1 => Some(ArraySet(imms[0]))
    "array_len" if imms.length() == 0 => Some(ArrayLen)
    "array_fill" if imms.length() == 1 => Some(ArrayFill(imms[0]))
    "array_copy" if imms.length() == 2 => Some(ArrayCopy(imms[0], imms[1]))
    "array_new_data" if imms.length() == 2 =>
      Some(ArrayNewData(imms[0], imms[1]))
    "array_new_elem" if imms.length() == 2 =>
      Some(ArrayNewElem(imms[0], imms[1]))
    "array_init_data" if imms.length() == 2 =>
      Some(ArrayInitData(imms[0], imms[1]))
    "array_init_elem" if imms.length() == 2 =>
      Some(ArrayInitElem(imms[0], imms[1]))
    "i31_new" if imms.length() == 0 => Some(I31New)
    "i31_get_s" if imms.length() == 0 => Some(I31GetS)
    "i31_get_u" if imms.length() == 0 => Some(I31GetU)
    "ref_test" if imms.length() == 2 =>
      Some(RefTest(imms[0], imm_bool(imms[1])))
    "ref_cast" if imms.length() == 2 =>
      Some(RefCast(imms[0], imm_bool(imms[1])))
    "any_convert_extern" if imms.length() == 0 => Some(AnyConvertExtern)
    "extern_convert_any" if imms.length() == 0 => Some(ExternConvertAny)
    "ref_eq" if imms.length() == 0 => Some(RefEq)
    "throw" if imms.length() == 1 => Some(Throw(imms[0]))
    "throw_ref" if imms.length() == 0 => Some(ThrowRef)
    "try_table_begin" if imms.length() == 1 => Some(TryTableBegin(imms[0]))
    "try_table_end" if imms.length() == 1 => Some(TryTableEnd(imms[0]))
    "get_exception_tag" if imms.length() == 0 => Some(GetExceptionTag)
    "get_exception_value" if imms.length() == 1 =>
      Some(GetExceptionValue(imms[0]))
    "get_exception_value_count" if imms.length() == 0 =>
      Some(GetExceptionValueCount)
    "delegate" if imms.length() == 1 => Some(Delegate(imms[0]))
    "spill_locals_for_throw" if imms.length() == 1 =>
      Some(SpillLocalsForThrow(imms[0]))
    "get_spilled_local" if imms.length() == 1 => Some(GetSpilledLocal(imms[0]))
    _ => None
  }
}

///|
pub fn decode_opcode(opcode : @milkir.Opcode) -> WasmOpcode? {
  match opcode {
    Ext(ext) => decode(ext)
    _ => None
  }
}