defmodule Absinthe.Relay.Node.Notation do
  @moduledoc """
  Macros used to define Node-related schema entities

  See `Absinthe.Relay.Node` for examples of use.

  If you wish to use this module on its own without `use Absinthe.Relay` you
  need to include
  ```
  @pipeline_modifier Absinthe.Relay.Schema
  ```
  in your root schema module.
  """

  alias Absinthe.Blueprint.Schema

  @doc """
  Define a node interface, field, or object type for a schema.

  See the `Absinthe.Relay.Node` module documentation for examples.
  """

  defmacro node({:interface, meta, [attrs]}, do: block) when is_list(attrs) do
    do_interface(meta, attrs, block)
  end

  defmacro node({:interface, meta, attrs}, do: block) do
    do_interface(meta, attrs, block)
  end

  defmacro node({:field, meta, [attrs]}, do: block) when is_list(attrs) do
    do_field(meta, attrs, block)
  end

  defmacro node({:field, meta, attrs}, do: block) do
    do_field(meta, attrs, block)
  end

  defmacro node({:object, meta, [identifier, attrs]}, do: block) when is_list(attrs) do
    do_object(meta, identifier, attrs, block)
  end

  defmacro node({:object, meta, [identifier]}, do: block) do
    do_object(meta, identifier, [], block)
  end

  defp do_interface(meta, attrs, block) do
    attrs = attrs || []
    {id_type, attrs} = Keyword.pop(attrs, :id_type, get_id_type())

    block = [interface_body(id_type), block]
    attrs = [:node | [attrs]]
    {:interface, meta, attrs ++ [[do: block]]}
  end

  defp do_field(meta, attrs, block) do
    attrs = attrs || []
    {id_type, attrs} = Keyword.pop(attrs, :id_type, get_id_type())

    {:field, meta, [:node, :node, attrs ++ [do: [field_body(id_type), block]]]}
  end

  defp do_object(meta, identifier, attrs, block) do
    {id_fetcher, attrs} = Keyword.pop(attrs, :id_fetcher)
    {id_type, attrs} = Keyword.pop(attrs, :id_type, get_id_type())

    block = [
      quote do
        private(:absinthe_relay, :node, {:fill, unquote(__MODULE__)})
        private(:absinthe_relay, :id_fetcher, unquote(id_fetcher))
      end,
      object_body(id_fetcher, id_type),
      block
    ]

    {:object, meta, [identifier, attrs] ++ [[do: block]]}
  end

  def additional_types(_, _), do: []

  # def fillout(:node, %Schema.ObjectTypeDefinition{} = obj) do
  #   id_field = id_field_template() |> Map.put(:middleware, [])

  #   %{obj | interfaces: [:node | obj.interfaces], fields: [id_field | obj.fields]}
  # end

  def fillout(_, %Schema.ObjectTypeDefinition{identifier: :faction} = obj) do
    obj
  end

  def fillout(_, node) do
    node
  end

  defp get_id_type() do
    Absinthe.Relay
    |> Application.get_env(:node_id_type, :id)
  end

  # An id field is automatically configured
  defp interface_body(id_type) do
    quote do
      field(:id, non_null(unquote(id_type)), description: "The ID of the object.")
    end
  end

  # An id arg is automatically added
  defp field_body(id_type) do
    quote do
      @desc "The ID of an object."
      arg(:id, non_null(unquote(id_type)))

      middleware({Absinthe.Relay.Node, :resolve_with_global_id})
    end
  end

  # Automatically add:
  # - An id field that resolves to the generated global ID
  #   for an object of this type
  # - A declaration that this implements the node interface
  defp object_body(id_fetcher, id_type) do
    quote do
      @desc "The ID of an object"
      field :id, non_null(unquote(id_type)) do
        middleware {Absinthe.Relay.Node, :global_id_resolver}, unquote(id_fetcher)
      end

      interface(:node)
    end
  end
end
