defmodule Orb.Ops do
  @moduledoc false

  # See: https://webassembly.github.io/spec/core/syntax/instructions.html#numeric-instructions
  @i_unary_ops ~w(clz ctz popcnt)a
  @i_binary_ops ~w(add sub mul div_u div_s rem_u rem_s and or xor shl shr_u shr_s rotl rotr)a
  @i_test_ops ~w(eqz)a
  @i_relative_ops ~w(eq ne lt_u lt_s gt_u gt_s le_u le_s ge_u ge_s)a
  # https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Numeric/Wrap
  @i32_wrap_ops ~w(wrap_i64)a
  # https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Numeric/Reinterpret
  @i32_reinterpret_ops ~w(reinterpret_f32)a
  # https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Numeric/Extend
  @i64_extend_ops ~w(extend_i32_s extend_i32_u)a
  # https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Numeric/Reinterpret
  @i64_reinterpret_ops ~w(reinterpret_f64)a
  # https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Memory/Load
  @i32_load_ops ~w(load load8_u load8_s load16_s load16_u)a
  @i64_load_ops @i32_load_ops ++ ~w(load32_s load32_u)a
  # https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Memory/Store
  @i32_store_ops ~w(store store8 store16)a
  @i64_store_ops @i32_store_ops ++ ~w(store32)a

  # https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Numeric/Truncate_float_to_int
  @i_trunc_ops ~w(trunc_f32_s trunc_f32_u trunc_f64_s trunc_f64_u)a

  @i32_ops_1 @i_unary_ops ++ @i_test_ops ++ @i_trunc_ops ++ @i32_wrap_ops ++ @i32_reinterpret_ops
  @i32_ops_2 @i_binary_ops ++ @i_relative_ops
  @i32_ops_all @i32_ops_1 ++ @i32_ops_2 ++ @i32_load_ops ++ @i32_store_ops

  @i64_ops_1 @i_unary_ops ++
               @i_test_ops ++ @i_trunc_ops ++ @i64_extend_ops ++ @i64_reinterpret_ops
  @i64_ops_2 @i_binary_ops ++ @i_relative_ops
  @i64_ops_all @i64_ops_1 ++ @i64_ops_2 ++ @i64_load_ops ++ @i64_store_ops

  @f32_ops_1 ~w(floor ceil trunc nearest abs neg sqrt convert_i32_s convert_i32_u demote_f64 reinterpret_i32)a
  @f32_ops_2 ~w(add sub mul div eq ne lt gt le ge copysign min max)a

  @f64_ops_1 ~w(floor ceil trunc nearest abs neg sqrt convert_i32_s convert_i32_u convert_i64_s convert_i64_u promote_f32 reinterpret_i64)a
  @f64_ops_2 ~w(add sub mul div eq ne lt gt le ge copysign min max)a

  # TODO: add conversions ops https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Numeric#conversion

  @integer_types ~w(i64 i32)a
  @float_types ~w(f64 f32)a
  @primitive_types @integer_types ++ @float_types
  # TODO: remove effects
  @effects ~w(unknown_effect memory_effect global_effect local_effect)a
  @elixir_types [Elixir.Integer, Elixir.Float]
  # @base_types @primitive_types ++ @effects ++ @elixir_types

  defguard is_primitive_type(type) when type in @primitive_types
  defguard is_primitive_integer_type(type) when type in @integer_types
  defguard is_primitive_float_type(type) when type in @float_types
  defguard is_effect(type) when type in @effects

  def to_primitive_type(type) when is_primitive_type(type), do: type
  def to_primitive_type(type) when is_effect(type), do: type
  def to_primitive_type(type) when type in @elixir_types, do: type
  # TODO: remove these as they don’t match is_primitive_type/1 above
  def to_primitive_type(nil), do: raise(CompileError, description: "nil cannot be a type")

  def to_primitive_type(type) when is_tuple(type) do
    for nested <- Tuple.to_list(type) do
      to_primitive_type(nested)
    end
    |> List.flatten()
    |> List.to_tuple()
  end

  def to_primitive_type(mod) when is_atom(mod) do
    Orb.CustomType.resolve!(mod) |> to_primitive_type()
  end

  def type_stack_count(type) when is_primitive_type(type), do: 1
  def type_stack_count(type) when is_effect(type), do: 0
  def type_stack_count(type) when is_atom(type), do: 1
  def type_stack_count(type) when is_tuple(type), do: tuple_size(type)

  def types_compatible?(Elixir.Integer, b),
    do: to_primitive_type(b) in @integer_types

  def types_compatible?(a, Elixir.Integer),
    do: to_primitive_type(a) in @integer_types

  def types_compatible?(Elixir.Float, b),
    do: to_primitive_type(b) in @float_types

  def types_compatible?(a, Elixir.Float),
    do: to_primitive_type(a) in @float_types

  def types_compatible?(a, b),
    do: to_primitive_type(a) === to_primitive_type(b)

  def typeof(n) when is_integer(n), do: Elixir.Integer
  def typeof(n) when is_float(n), do: Elixir.Float
  def typeof(%{push_type: type}), do: type
  def typeof(%{type_signature: type_signature}), do: %{type_signature: type_signature}
  def typeof(%{if: _}), do: nil
  def typeof(str) when is_binary(str), do: Orb.Str
  # def typeof(_), do: :unknown_effect

  def typeof(value, :primitive), do: typeof(value) |> to_primitive_type()

  def pop_push_of(%{pop_type: pop_type, push_type: push_type}), do: {pop_type, push_type}
  def pop_push_of(type), do: {nil, typeof(type)}

  def extract_common_type(a, b) do
    {nil, push_a} = pop_push_of(a)
    {nil, push_b} = pop_push_of(b)

    case {to_primitive_type(push_a), to_primitive_type(push_b)} do
      {same, same} -> same
      {a, b} when is_effect(a) and is_effect(b) -> nil
      {type, Elixir.Integer} when type in @integer_types -> type
      {Elixir.Integer, type} when type in @integer_types -> type
      {type, Elixir.Float} when type in @float_types -> type
      {Elixir.Float, type} when type in @float_types -> type
      _ -> nil
    end
  end

  defmacro i32(arity_or_type)
  defmacro i32(1), do: @i32_ops_1 |> Macro.escape()
  defmacro i32(2), do: @i32_ops_2 |> Macro.escape()
  defmacro i32(:load), do: @i32_load_ops |> Macro.escape()
  defmacro i32(:store), do: @i32_store_ops |> Macro.escape()
  defmacro i32(:all), do: @i32_ops_all |> Macro.escape()

  # FIXME: conditional ops like i64.eq and i64.ge_u return i32 not i64
  defmacro i64(arity_or_type)
  defmacro i64(1), do: @i64_ops_1 |> Macro.escape()
  defmacro i64(2), do: @i64_ops_2 |> Macro.escape()
  defmacro i64(:load), do: @i64_load_ops |> Macro.escape()
  defmacro i64(:store), do: @i64_store_ops |> Macro.escape()
  defmacro i64(:all), do: @i64_ops_all |> Macro.escape()

  defmacro f32(arity_or_type)
  defmacro f32(1), do: @f32_ops_1 |> Macro.escape()
  defmacro f32(2), do: @f32_ops_2 |> Macro.escape()

  defmacro f64(arity_or_type)
  defmacro f64(1), do: @f64_ops_1 |> Macro.escape()
  defmacro f64(2), do: @f64_ops_2 |> Macro.escape()

  defp i32_arity(op) when op in @i32_ops_1, do: 1
  defp i32_arity(op) when op in @i32_ops_2, do: 2
  defp i64_arity(op) when op in @i64_ops_1, do: 1
  defp i64_arity(op) when op in @i64_ops_2, do: 2
  defp f32_arity(op) when op in @f32_ops_1, do: 1
  defp f32_arity(op) when op in @f32_ops_2, do: 2
  defp f64_arity(op) when op in @f64_ops_1, do: 1
  defp f64_arity(op) when op in @f64_ops_2, do: 2

  defp i32_param_type(op, param_index)
  defp i32_param_type(:const, 0), do: Elixir.Integer
  defp i32_param_type(op, 0) when op in @i32_load_ops, do: :i32
  defp i32_param_type(op, i) when op in @i32_store_ops and i in [0, 1], do: :i32
  defp i32_param_type(op, 0) when op in @i_trunc_ops, do: :f32
  defp i32_param_type(op, 0) when op in @i_unary_ops, do: :i32
  defp i32_param_type(op, 0) when op in @i_test_ops, do: :i32
  defp i32_param_type(op, 0) when op in @i32_wrap_ops, do: :i64
  defp i32_param_type(op, i) when op in @i_binary_ops and i in [0, 1], do: :i32
  defp i32_param_type(op, i) when op in @i_relative_ops and i in [0, 1], do: :i32
  defp i32_param_type(_, _), do: :error

  defp i64_param_type(op, param_index)
  defp i64_param_type(:const, 0), do: Elixir.Integer
  defp i64_param_type(op, 0) when op in @i64_load_ops, do: :i64
  defp i64_param_type(op, i) when op in @i64_store_ops and i in [0, 1], do: :i64
  defp i64_param_type(op, 0) when op in @i_trunc_ops, do: :f64
  defp i64_param_type(op, 0) when op in @i_unary_ops, do: :i64
  defp i64_param_type(op, 0) when op in @i_test_ops, do: :i64
  defp i64_param_type(op, 0) when op in @i64_extend_ops, do: :i32
  defp i64_param_type(op, i) when op in @i_binary_ops and i in [0, 1], do: :i64
  defp i64_param_type(op, i) when op in @i_relative_ops and i in [0, 1], do: :i64
  defp i64_param_type(_, _), do: :error

  defp f32_param_type(op, param_index)
  defp f32_param_type(:const, 0), do: Elixir.Float
  defp f32_param_type(:convert_i32_s, 0), do: :i32
  defp f32_param_type(:convert_i32_u, 0), do: :i32
  defp f32_param_type(:demote_f64, 0), do: :f64
  defp f32_param_type(op, 0) when op in @f32_ops_1, do: :f32
  defp f32_param_type(op, i) when op in @f32_ops_2 and i in [0, 1], do: :f32
  defp f32_param_type(_, _), do: :error

  defp f64_param_type(op, param_index)
  defp f64_param_type(:const, 0), do: Elixir.Float
  defp f64_param_type(:convert_i32_s, 0), do: :i32
  defp f64_param_type(:convert_i32_u, 0), do: :i32
  defp f64_param_type(:convert_i64_s, 0), do: :i64
  defp f64_param_type(:convert_i64_u, 0), do: :i64
  defp f64_param_type(:promote_f32, 0), do: :f32
  defp f64_param_type(op, 0) when op in @f64_ops_1, do: :f64
  defp f64_param_type(op, i) when op in @f64_ops_2 and i in [0, 1], do: :f64
  defp f64_param_type(_, _), do: :error

  def i32_param_type!(op, param_index) do
    case i32_param_type(op, param_index) do
      :error ->
        raise ArgumentError,
              "WebAssembly instruction i32.#{op}/#{i32_arity(op)} does not accept a #{nth(param_index)} argument."

      type ->
        type
    end
  end

  def i64_param_type!(op, param_index) do
    case i64_param_type(op, param_index) do
      :error ->
        raise ArgumentError,
              "WebAssembly instruction i64.#{op}/#{i64_arity(op)} does not accept a #{nth(param_index)} argument."

      type ->
        type
    end
  end

  def f32_param_type!(op, param_index) do
    case f32_param_type(op, param_index) do
      :error ->
        raise ArgumentError,
              "WebAssembly instruction f32.#{op}/#{f32_arity(op)} does not accept a #{nth(param_index)} argument."

      type ->
        type
    end
  end

  def f64_param_type!(op, param_index) do
    case f64_param_type(op, param_index) do
      :error ->
        raise ArgumentError,
              "WebAssembly instruction f64.#{op}/#{f64_arity(op)} does not accept a #{nth(param_index)} argument."

      type ->
        type
    end
  end

  def param_type!(:i32, op, param_index), do: i32_param_type!(op, param_index)
  def param_type!(:i64, op, param_index), do: i64_param_type!(op, param_index)
  def param_type!(:f32, op, param_index), do: f32_param_type!(op, param_index)
  def param_type!(:f64, op, param_index), do: f64_param_type!(op, param_index)

  defp nth(0), do: "1st"
  defp nth(1), do: "2nd"
  defp nth(2), do: "3rd"
  defp nth(n), do: "#{n + 1}th"

  def doc(type, op)
  def doc(:i64, :extend_i32_u), do: "Convert unsigned i32 to unsigned i64."
  def doc(:i64, :extend_i32_s), do: "Convert signed i32 to signed i64."
  def doc(_type, _op), do: nil
end
