defmodule WaParser do
  @moduledoc """
  Documentation for `WaParser`.
  """

  alias WaParser.Types.Atomic

  def stream(data) do
    Stream.resource(
      fn -> {data, &parse/1} end,
      fn
        {"", _} = acc ->
          {:halt, acc}

        {data, parser} ->
          case parser.(data) do
            {:ok, {result, rest, next}} when is_list(result) ->
              {result, {rest, next}}

            {:ok, {result, rest, next}} ->
              {[result], {rest, next}}
          end
      end,
      fn _ -> nil end
    )
  end

  def parse(<<0x00, 0x61, 0x73, 0x6D, rest::binary>>) do
    {:ok, {{:magic, <<0x00, 0x61, 0x73, 0x6D>>}, rest, &parse_version/1}}
  end

  defp parse_version(<<0x01, 0x00, 0x00, 0x00, rest::binary>>) do
    {:ok, {{:version, <<0x01, 0x00, 0x00, 0x00>>}, rest, &parse_section/1}}
  end

  defp parse_section(<<id, rest::binary>>) do
    type = get_section_type(id)
    {:ok, {[{:section_id, id}, {:section_type, type}], rest, do_parse_section(type)}}
  end

  defp get_section_type(0), do: :custom
  defp get_section_type(1), do: :type
  defp get_section_type(3), do: :function
  defp get_section_type(4), do: :table
  defp get_section_type(5), do: :memory
  defp get_section_type(6), do: :global
  defp get_section_type(7), do: :export
  defp get_section_type(10), do: :code

  defp do_parse_section(type) do
    fn binary ->
      do_parse_section_len(type, binary)
    end
  end

  defp do_parse_section_len(type, binary) do
    {length, rest} = Atomic.u32(binary)
    {:ok, {{:section_length, length}, rest, parse_section_body(type, length)}}
  end

  defp parse_section_body(type, length) do
    fn binary ->
      do_parse_section_body(type, length, binary)
    end
  end

  defp do_parse_section_body(type, len, binary) do
    <<section::binary-size(len), rest::binary>> = binary

    parsed_body = section_body(type, section)

    {func_type, ""} = parsed_body

    {:ok, {{:section_body, func_type}, rest, &parse_section/1}}
  end

  defp section_body(:type, binary) do
    vec(&functype/1).(binary)
  end

  defp section_body(:function, binary) do
    vec(&typeidx/1).(binary)
  end

  defp section_body(:table, binary) do
    vec(&tabletype/1).(binary)
  end

  defp section_body(:memory, binary) do
    vec(&memtype/1).(binary)
  end

  defp section_body(:global, binary) do
    vec(&global/1).(binary)
  end

  defp section_body(:export, binary) do
    vec(&export/1).(binary)
  end

  defp section_body(:code, binary) do
    vec(&code/1).(binary)
  end

  defp section_body(:custom, binary) do
    {n, rest} = name(binary)
    {%{name: n, content: rest}, ""}
  end

  defp code(binary) do
    {code_size, rest} = Atomic.u32(binary)
    {code, rest} = func(rest)

    {%{size: code_size, code: code}, rest}
  end

  defp func(binary) do
    {locals, rest} = vec(&locals/1).(binary)
    {e, rest} = expr(rest)
    {%{locals: locals, expr: e}, rest}
  end

  defp locals(binary) do
    {num, rest} = Atomic.u32(binary)
    {type, rest} = valtype(rest)
    {%{num: num, type: type}, rest}
  end

  defp export(binary) do
    {name, rest} = name(binary)
    {export_desc, rest} = exportdesc(rest)
    {{name, export_desc}, rest}
  end

  defp name(binary) do
    {nm, rest} = vec(&byte/1).(binary)
    {List.to_string(nm), rest}
  end

  defp byte(<<b, rest::binary>>) do
    {b, rest}
  end

  defp exportdesc(<<0x00, rest::binary>>) do
    {fun, rest} = funcidx(rest)
    {{:func, fun}, rest}
  end

  defp exportdesc(<<0x02, rest::binary>>) do
    {mem, rest} = memidx(rest)
    {{:mem, mem}, rest}
  end

  defp exportdesc(<<0x03, rest::binary>>) do
    {glob, rest} = globalidx(rest)
    {{:global, glob}, rest}
  end

  defp global(binary) do
    {gl, rest} = globaltype(binary)
    {ex, rest} = expr(rest)
    {{gl, ex}, rest}
  end

  defp globaltype(binary) do
    {vt, rest} = valtype(binary)
    {m, rest} = mut(rest)
    {{vt, m}, rest}
  end

  defp mut(<<0x00, rest::binary>>) do
    {:const, rest}
  end

  defp mut(<<0x01, rest::binary>>) do
    {:var, rest}
  end

  defp expr(binary) do
    expr(binary, [])
  end

  defp expr(<<0x03, rest::binary>>, acc) do
    {bt, rest} = blocktype(rest)
    {exprs, rest} = expr(rest)

    expr(rest, [
      {:loop, %{blocktype: bt, instr: exprs}} | acc
    ])
  end

  defp expr(<<0x04, rest::binary>>, acc) do
    {bt, rest} = blocktype(rest)
    {exprs, rest} = expr(rest)

    expr(rest, [
      {:if, %{blocktype: bt, instr: exprs}}
      | acc
    ])
  end

  defp expr(<<0x0B, rest::binary>>, acc) do
    {acc |> Enum.reverse(), rest}
  end

  defp expr(<<0x1B, rest::binary>>, acc) do
    expr(rest, [:select | acc])
  end

  defp expr(<<0x0D, rest::binary>>, acc) do
    {l, rest} = labelidx(rest)
    expr(rest, [{:br_if, l} | acc])
  end

  defp expr(<<0x20, rest::binary>>, acc) do
    {idx, rest} = localidx(rest)
    expr(rest, [{:"local.get", idx} | acc])
  end

  defp expr(<<0x21, rest::binary>>, acc) do
    {idx, rest} = localidx(rest)
    expr(rest, [{:"local.set", idx} | acc])
  end

  defp expr(<<0x22, rest::binary>>, acc) do
    {idx, rest} = localidx(rest)
    expr(rest, [{:"local.tee", idx} | acc])
  end

  defp expr(<<0x41, rest::binary>>, acc) do
    {val, rest} = Atomic.i32(rest)
    expr(rest, [{:"i32.const", val} | acc])
  end

  @plain_numeric 0x45..0xC4

  defp expr(<<opcode, rest::binary>>, acc) when opcode in @plain_numeric do
    expr(rest, [plain_numeric_instr(opcode) | acc])
  end

  defp plain_numeric_instr(0x46), do: :"i32.eq"
  defp plain_numeric_instr(0x47), do: :"i32.ne"
  defp plain_numeric_instr(0x48), do: :"i32.lt_s"
  defp plain_numeric_instr(0x4A), do: :"i32.gt_s"
  defp plain_numeric_instr(0x4C), do: :"i32.le_s"
  defp plain_numeric_instr(0x4E), do: :"i32.ge_s"
  defp plain_numeric_instr(0x6A), do: :"i32.add"
  defp plain_numeric_instr(0x6B), do: :"i32.sub"
  defp plain_numeric_instr(0x6C), do: :"i32.mul"
  defp plain_numeric_instr(0x6D), do: :"i32.div_s"

  defp blocktype(<<0x40, rest::binary>>), do: {:empty, rest}

  defp memtype(binary) do
    limits(binary)
  end

  defp tabletype(binary) do
    {et, rest} = reftype(binary)
    {lim, rest} = limits(rest)
    {{et, lim}, rest}
  end

  defp reftype(<<0x70, rest::binary>>) do
    {:funcref, rest}
  end

  defp limits(<<0x00, rest::binary>>) do
    {min, rest} = Atomic.u32(rest)
    {{min, nil}, rest}
  end

  defp limits(<<0x01, rest::binary>>) do
    {min, rest} = Atomic.u32(rest)
    {max, rest} = Atomic.u32(rest)
    {{min, max}, rest}
  end

  defp vec(type) do
    fn binary ->
      {length, rest} = Atomic.u32(binary)
      do_vec(type, length, rest)
    end
  end

  defp do_vec(type, length, binary) do
    do_vec(type, length, binary, [])
  end

  defp do_vec(_type, 0, rest, acc) do
    {acc |> Enum.reverse(), rest}
  end

  defp do_vec(type, length, binary, acc) do
    {v, rest} = type.(binary)
    do_vec(type, length - 1, rest, [v | acc])
  end

  defp functype(<<0x60, rest::binary>>) do
    {param_type, rest} = resulttype(rest)
    {result_type, rest} = resulttype(rest)
    {{param_type, result_type}, rest}
  end

  defp resulttype(binary) do
    vec(&valtype/1).(binary)
  end

  defp valtype(<<0x7F, rest::binary>>), do: {:i32, rest}

  defp funcidx(binary), do: Atomic.u32(binary)
  defp globalidx(binary), do: Atomic.u32(binary)
  defp labelidx(binary), do: Atomic.u32(binary)
  defp localidx(binary), do: Atomic.u32(binary)
  defp memidx(binary), do: Atomic.u32(binary)
  defp typeidx(binary), do: Atomic.u32(binary)
end