///|
priv enum WasmImmediateKind {
  IntegerImmediate
  BooleanImmediate
  PackedWidthImmediate
}

///|
priv enum WasmTypePattern {
  ExactType(@milkir.Type)
  IntegerType
  ReferenceLike
  ContextualType
}

///|
priv struct WasmOpcodeSpec {
  id : Int
  typed_constructor : String
  wire_name : String
  immediate_kinds : Array[WasmImmediateKind]
  operand_types : Array[WasmTypePattern]
  result_types : Array[WasmTypePattern]
  variadic_operands : Bool
  variadic_results : Bool
}

///|
let wasm_opcode_ids_by_wire_name : Map[String, Int] = {
  let ids : Map[String, Int] = Map([])
  for spec in wasm_opcode_specs {
    ids[spec.wire_name] = spec.id
  }
  ids
}

///|
fn wasm_opcode_spec(id : Int) -> WasmOpcodeSpec {
  if id < 0 || id >= wasm_opcode_specs.length() {
    abort("Wasm opcode id \{id} is absent from the generated opcode table")
  }
  wasm_opcode_specs[id]
}

///|
fn wasm_opcode_spec_by_wire_name(name : String) -> WasmOpcodeSpec? {
  match wasm_opcode_ids_by_wire_name.get(name) {
    Some(id) => Some(wasm_opcode_specs[id])
    None => None
  }
}

///|
fn validate_wasm_opcode_immediates(
  spec : WasmOpcodeSpec,
  immediates : FixedArray[Int],
) -> String? {
  if immediates.length() != spec.immediate_kinds.length() {
    return Some(
      "malformed Wasm MilkIR extension '\{spec.wire_name}': expected \{spec.immediate_kinds.length()} immediates, got \{immediates.length()}",
    )
  }
  for i, kind in spec.immediate_kinds {
    match kind {
      IntegerImmediate => ()
      BooleanImmediate => {
        let value = immediates[i]
        if value != 0 && value != 1 {
          return Some(
            "malformed Wasm MilkIR extension '\{spec.wire_name}': immediate \{i} is a bool flag encoded as 0 or 1, got \{value}",
          )
        }
      }
      PackedWidthImmediate => {
        let value = immediates[i]
        if value != 1 && value != 2 {
          return Some(
            "malformed Wasm MilkIR extension '\{spec.wire_name}': immediate \{i} is a packed byte width encoded as 1 or 2, got \{value}",
          )
        }
      }
    }
  }
  None
}

///|
fn type_pattern_matches(pattern : WasmTypePattern, ty : @milkir.Type) -> Bool {
  match pattern {
    ExactType(expected) => ty == expected
    IntegerType => ty == I32 || ty == I64
    ReferenceLike =>
      match ty {
        Ref | CallableRef | OpaqueRef => true
        _ => false
      }
    ContextualType => true
  }
}

///|
fn type_pattern_name(pattern : WasmTypePattern) -> String {
  match pattern {
    ExactType(ty) => ty.to_string()
    IntegerType => "i32 | i64"
    ReferenceLike => "ref_like"
    ContextualType => "contextual"
  }
}

///|
fn expected_type_list(
  patterns : Array[WasmTypePattern],
  variadic : Bool,
) -> String {
  let names = patterns.map(type_pattern_name)
  if variadic {
    names.push("...")
  }
  "[\{names.join(", ")}]"
}

///|
fn actual_type_list(types : ReadOnlyArray[@milkir.Type]) -> String {
  "[\{types.map(ty => ty.to_string()).join(", ")}]"
}

///|
fn validate_type_list(
  wire_name : String,
  label : String,
  actual : ReadOnlyArray[@milkir.Type],
  expected : Array[WasmTypePattern],
  variadic : Bool,
) -> String? {
  if actual.length() < expected.length() ||
    (!variadic && actual.length() != expected.length()) {
    return Some(
      "malformed Wasm MilkIR extension '\{wire_name}': expected \{label} \{expected_type_list(expected, variadic)}, got \{actual_type_list(actual)}",
    )
  }
  for i, pattern in expected {
    if !type_pattern_matches(pattern, actual[i]) {
      return Some(
        "malformed Wasm MilkIR extension '\{wire_name}': expected \{label} \{expected_type_list(expected, variadic)}, got \{actual_type_list(actual)}",
      )
    }
  }
  None
}

///|
fn validate_wasm_instruction_shape(
  spec : WasmOpcodeSpec,
  view : @milkir.ExtensionInstView,
) -> String? {
  if validate_type_list(
      spec.wire_name,
      "operands",
      view.operand_types,
      spec.operand_types,
      spec.variadic_operands,
    )
    is Some(message) {
    return Some(message)
  }
  validate_type_list(
    spec.wire_name,
    "results",
    view.result_types,
    spec.result_types,
    spec.variadic_results,
  )
}