use Protoss

defprotocol Zig.Type do
  @moduledoc false

  alias Zig.Type.Array
  alias Zig.Type.Bool
  alias Zig.Type.Cpointer
  alias Zig.Type.Error
  alias Zig.Type.Float
  alias Zig.Type.Integer
  alias Zig.Type.Manypointer
  alias Zig.Type.Optional
  alias Zig.Type.Pointer
  alias Zig.Type.Slice
  alias Zig.Type.Struct
  alias Zig.Type.Resource

  @type t ::
          Array.t()
          | Bool.t()
          | Cpointer.t()
          | Error.t()
          | Zig.Type.Enum.t()
          | Float.t()
          | Integer.t()
          | ManyPointer.t()
          | Optional.t()
          | Pointer.t()
          | Slice.t()
          | Struct.t()
          | :void
          | :anyopaque
          | :env
          | :pid
          | :port
          | :term
          | :erl_nif_term
          | :erl_nif_binary
          | :erl_nif_event
          | :erl_nif_binary_pointer
          | :stacktrace

  @type json :: number | String.t() | boolean | nil | [json] | %{optional(String.t()) => json}

  @spec marshal_param(t, Macro.t(), non_neg_integer, Elixir | :erlang) :: Macro.t()
  def marshal_param(type, variable_ast, index, platform)

  @spec marshal_return(t, Macro.t(), Elixir | :erlang) :: Macro.t()
  def marshal_return(type, variable_ast, platform)

  # validations:

  @spec get_allowed?(t) :: boolean
  def get_allowed?(type)

  @spec make_allowed?(t) :: boolean
  def make_allowed?(type)

  @spec in_out_allowed?(t) :: boolean
  def in_out_allowed?(type)

  @spec binary_size(t) :: nil | non_neg_integer | {:var, non_neg_integer}
  def binary_size(type)

  # rendered zig code:
  @spec render_accessory_variables(t, term, iodata) :: iodata
  def render_accessory_variables(type, opts, prefix)

  @spec payload_options(t, String.t()) :: iodata
  def payload_options(type, prefix)

  @spec render_cleanup(t, non_neg_integer) :: iodata
  def render_cleanup(type, index)

  @spec render_zig(t) :: String.t()
  def render_zig(type)

  @spec render_elixir_spec(t, Return.t() | Param.t() | Return.type()) :: Macro.t()
  def render_elixir_spec(type, context)
after
  defmacro sigil_t({:<<>>, _, [string]}, _) do
    string
    |> parse
    |> Macro.escape()
  end

  def parse(string) do
    case string do
      "u" <> _ ->
        Integer.parse(string)

      "i" <> _ ->
        Integer.parse(string)

      "f" <> _ ->
        Float.parse(string)

      "c_uint" <> _ ->
        Integer.parse(string)

      "[]" <> rest ->
        Slice.of(parse(rest))

      "[:0]" <> rest ->
        Slice.of(parse(rest), has_sentinel?: true)

      "[*]" <> rest ->
        Manypointer.of(parse(rest))

      "[*:0]" <> rest ->
        Manypointer.of(parse(rest), has_sentinel?: true)

      "[*c]" <> rest ->
        Cpointer.of(parse(rest))

      "?" <> rest ->
        Optional.of(parse(rest))

      "[" <> maybe_array ->
        case Elixir.Integer.parse(maybe_array) do
          {count, "]" <> rest} ->
            Array.of(parse(rest), count)

          {count, ":0]" <> rest} ->
            Array.of(parse(rest), count, has_sentinel?: true)

          _ ->
            raise "unknown type #{string}"
        end

      "?*.cimport" <> rest ->
        if String.ends_with?(rest, "struct_enif_environment_t") do
          Env
        else
          unknown =
            rest
            |> String.split(".")
            |> List.last()

          raise "unknown type #{unknown}"
        end
    end
  end

  # derivative functions
  def binary_typespec(type) do
    case binary_size(type) do
      nil ->
        nil

      {:indirect, _} ->
        nil

      {:var, 1} ->
        quote context: Elixir do
          binary()
        end

      {:var, size} when size <= 32 ->
        quote context: Elixir do
          <<_::_*unquote(size * 8)>>
        end

      {:var, _} ->
        quote context: Elixir do
          binary()
        end

      size ->
        quote context: Elixir do
          <<_::unquote(size * 8)>>
        end
    end
  end

  # following two lines cause infinite loop
  # @callback from_json(json, term) :: t
  # @optional_callbacks [from_json: 2]

  def from_json(json, module) do
    case json do
      :null ->
        # only allow during documentation sema passes
        if module do
          raise CompileError, description: "zigler encountered anytype"
        else
          :anytype
        end

      %{"type" => "unusable:" <> typename} ->
        # only allow during documentation sema passes
        if module do
          raise CompileError, description: "zigler encountered the unusable type #{typename}"
        else
          String.to_atom(typename)
        end

      %{"type" => "bool"} ->
        Bool.from_json(json)

      %{"type" => "void"} ->
        :void

      %{"type" => "integer"} ->
        Integer.from_json(json)

      %{"type" => "enum"} ->
        Zig.Type.Enum.from_json(json, module)

      %{"type" => "float"} ->
        Float.from_json(json)

      %{
        "type" => "struct",
        "fields" => [%{"name" => "__payload"}, %{"name" => "__should_release"}]
      } ->
        Resource.from_json(json, module)

      %{"type" => "struct"} ->
        Struct.from_json(json, module)

      %{"type" => "array"} ->
        Array.from_json(json, module)

      %{"type" => "slice"} ->
        Slice.from_json(json, module)

      %{"type" => "pointer"} = type ->
        Pointer.from_json(type, module)

      %{"type" => "optional", "child" => %{"type" => "pointer"}} = type ->
        Pointer.from_json(type, module)

      %{"type" => "manypointer"} ->
        Manypointer.from_json(json, module)

      %{"type" => "cpointer"} ->
        Cpointer.from_json(json, module)

      %{"type" => "optional"} ->
        Optional.from_json(json, module)

      %{"type" => "error"} ->
        Error.from_json(json, module)

      %{"type" => "env"} ->
        :env

      %{"type" => "erl_nif_term"} ->
        :erl_nif_term

      %{"type" => "struct", "name" => "beam.term"} ->
        :term

      %{"type" => "pid"} ->
        :pid

      %{"type" => "port"} ->
        :port

      %{"type" => "term"} ->
        :term

      %{"type" => "e.ErlNifBinary"} ->
        :erl_nif_binary

      %{"type" => "e.ErlNifEvent"} ->
        :erl_nif_event

      %{"type" => "pointer", "child" => %{"type" => "e.ErlNifBinary"}} ->
        :erl_nif_binary_pointer

      %{"type" => "pointer", "child" => %{"type" => "builtin.StackTrace"}} ->
        :stacktrace

      %{"type" => "builtin.StackTrace"} ->
        :stacktrace
    end
  end

  def needs_make?(:erl_nif_term), do: false
  def needs_make?(:term), do: false
  def needs_make?(_), do: true

  # defaults

  def _default_payload_options, do: [error_info: "&error_info"]

  def _default_accessory_variables, do: []

  def _default_marshal_param(Elixir, _variable), do: []
  def _default_marshal_param(:erlang, variable), do: "#{variable}_m = #{variable},"

  def _default_marshal_return(Elixir, variable), do: variable
  def _default_marshal_return(:erlang, _variable), do: "Return"

  def _default_cleanup, do: ".{},"
end

defimpl Zig.Type, for: Atom do
  alias Zig.Type

  @impl true
  def get_allowed?(type), do: type in ~w[term erl_nif_term pid]a

  @impl true
  def make_allowed?(type), do: type in ~w[term erl_nif_term pid void]a

  @impl true
  def in_out_allowed?(_), do: false

  @impl true
  def binary_size(_), do: nil

  @impl true
  def render_zig(:term), do: "beam.term"
  def render_zig(:erl_nif_term), do: "e.erl_nif_term"
  def render_zig(:pid), do: "beam.pid"
  def render_zig(:env), do: "beam.env"
  def render_zig(:anyopaque), do: "anyopaque"
  def render_zig(atom), do: "#{atom}"

  @impl true
  def render_accessory_variables(_, _, _), do: Type._default_accessory_variables()

  @impl true
  def payload_options(:erl_nif_term, _), do: []
  def payload_options(:term, _), do: []

  def payload_options(type, _)
      when type in ~w[env stacktrace erl_nif_binary erl_nif_event erl_nif_binary_pointer]a,
      do: raise("unreachable")

  def payload_options(_, _), do: Type._default_payload_options()

  @impl true
  def render_cleanup(_, _), do: Type._default_cleanup()

  @impl true
  def render_elixir_spec(:void, %Zig.Return{}), do: :ok

  def render_elixir_spec(:pid, _) do
    quote do
      pid()
    end
  end

  @impl true
  def render_elixir_spec(term, _) when term in ~w[term erl_nif_term]a do
    quote do
      term()
    end
  end

  @impl true
  def marshal_param(_, variable, _, platform), do: Type._default_marshal_param(platform, variable)

  @impl true
  def marshal_return(_, variable, platform), do: Type._default_marshal_return(platform, variable)
end
