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

  See `Absinthe.Relay.Connection` for more information.

  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

  @naming_attrs [
    :node_type,
    :non_null,
    :non_null_edges,
    :non_null_edge,
    :non_null_connection,
    :connection
  ]

  defmodule Naming do
    @moduledoc false

    defstruct base_identifier: nil,
              node_type_identifier: nil,
              connection_type_identifier: nil,
              edge_type_identifier: nil,
              non_null_edges: false,
              non_null_edge: false,
              non_null_connection: false,
              attrs: []

    def from_attrs!(attrs) do
      node_type_identifier =
        attrs[:node_type] ||
          raise(
            "Must provide a `:node_type' option (an optional `:connection` option is also supported)"
          )

      base_identifier = attrs[:connection] || node_type_identifier
      non_null_edges = attrs[:non_null_edges] || attrs[:non_null] || false
      non_null_edge = attrs[:non_null_edge] || attrs[:non_null] || false
      non_null_connection = attrs[:non_null_connection] || attrs[:non_null] || false

      %__MODULE__{
        node_type_identifier: node_type_identifier,
        base_identifier: base_identifier,
        connection_type_identifier: ident(base_identifier, :connection),
        edge_type_identifier: ident(base_identifier, :edge),
        non_null_edges: non_null_edges,
        non_null_edge: non_null_edge,
        non_null_connection: non_null_connection,
        attrs: [
          node_type: node_type_identifier,
          connection: base_identifier,
          non_null_edges: non_null_edges,
          non_null_edge: non_null_edge,
          non_null_connection: non_null_connection
        ]
      }
    end

    defp ident(base, category) do
      :"#{base}_#{category}"
    end
  end

  @doc """
  Define a connection type for a given node type.

  ## Examples

  A basic connection for a node type, `:pet`. This well generate simple
  `:pet_connection` and `:pet_edge` types for you:

  ```
  connection node_type: :pet
  ```

  You can provide a custom name for the connection type (just don't include the
  word "connection"). You must still provide the `:node_type`. You can create as
  many different connections to a node type as you want.

  This example will create a connection type, `:favorite_pets_connection`, and
  an edge type, `:favorite_pets_edge`:

  ```
  connection :favorite_pets, node_type: :pet
  ```

  You can customize the connection object just like any other `object`:

  ```
  connection :favorite_pets, node_type: :pet do
    field :total_age, :float do
      resolve fn
        _, %{source: conn} ->
          sum = conn.edges
          |> Enum.map(fn edge -> edge.node.age)
          |> Enum.sum
          {:ok, sum}
      end
    end
    edge do
      # ...
    end
  end
  ```

  Just remember that if you use the block form of `connection`, you must call
  the `edge` macro within the block to make sure the edge type is generated.
  See the `edge` macro below for more information.
  """
  defmacro connection({:field, _, [identifier, attrs]}, do: block) when is_list(attrs) do
    do_connection_field(identifier, attrs, block)
  end

  defmacro connection(attrs, do: block) do
    naming = Naming.from_attrs!(attrs)
    do_connection_definition(naming, attrs, block)
  end

  defmacro connection(identifier, attrs) do
    naming = Naming.from_attrs!(attrs |> Keyword.put(:connection, identifier))
    do_connection_definition(naming, attrs, [])
  end

  defmacro connection(attrs) do
    naming = Naming.from_attrs!(attrs)
    do_connection_definition(naming, attrs, [])
  end

  defmacro connection(identifier, attrs, do: block) do
    naming = Naming.from_attrs!(attrs |> Keyword.put(:connection, identifier))
    do_connection_definition(naming, attrs, block)
  end

  defp do_connection_field(identifier, attrs, block) do
    naming = Naming.from_attrs!(attrs)

    paginate = Keyword.get(attrs, :paginate, :both)

    field_attrs =
      attrs
      |> Keyword.drop([:paginate] ++ @naming_attrs)
      |> Keyword.put(:type, build_connection_type(naming))

    quote do
      field unquote(identifier), unquote(field_attrs) do
        private(:absinthe_relay, {:paginate, unquote(paginate)}, {:fill, unquote(__MODULE__)})
        unquote(block)
      end
    end
  end

  defp do_connection_definition(naming, attrs, block) do
    identifier = naming.connection_type_identifier

    attrs = Keyword.drop(attrs, @naming_attrs)

    block = name_edge(block, naming.attrs)
    edge_field = build_edge_type(naming)

    quote do
      object unquote(identifier), unquote(attrs) do
        private(
          :absinthe_relay,
          {:connection, unquote(naming.attrs)},
          {:fill, unquote(__MODULE__)}
        )

        field(:page_info, type: non_null(:page_info))
        field(:edges, type: unquote(edge_field))
        unquote(block)
      end
    end
  end

  defp build_connection_type(%{non_null_connection: true} = naming) do
    quote do
      non_null(unquote(naming.connection_type_identifier))
    end
  end

  defp build_connection_type(%{non_null_connection: false} = naming) do
    naming.connection_type_identifier
  end

  defp build_edge_type(%{non_null_edge: true, non_null_edges: true} = naming) do
    quote do
      non_null(list_of(non_null(unquote(naming.edge_type_identifier))))
    end
  end

  defp build_edge_type(%{non_null_edge: true} = naming) do
    quote do
      list_of(non_null(unquote(naming.edge_type_identifier)))
    end
  end

  defp build_edge_type(%{non_null_edges: true} = naming) do
    quote do
      non_null(list_of(unquote(naming.edge_type_identifier)))
    end
  end

  defp build_edge_type(naming) do
    quote do
      list_of(unquote(naming.edge_type_identifier))
    end
  end

  defp name_edge([], _), do: []

  defp name_edge({:edge, meta, [[do: block]]}, conn_attrs) do
    {:edge, meta, [conn_attrs, [do: block]]}
  end

  defp name_edge({:__block__, meta, content}, conn_attrs) do
    content =
      Enum.map(content, fn
        {:edge, meta, [[do: block]]} ->
          {:edge, meta, [conn_attrs, [do: block]]}

        {:edge, meta, [attrs, [do: block]]} ->
          {:edge, meta, [conn_attrs ++ attrs, [do: block]]}

        node ->
          node
      end)

    {:__block__, meta, content}
  end

  @doc """
  Customize the edge type.

  ## Examples

  ```
  connection node_type: :pet do
    # ...
    edge do
      field :node_name_backwards, :string do
        resolve fn
          _, %{source: edge} ->
            {:ok, edge.node.name |> String.reverse}
        end
      end
    end
  end
  ```
  """
  defmacro edge(attrs, do: block) do
    naming = Naming.from_attrs!(attrs)

    attrs = Keyword.drop(attrs, @naming_attrs)

    quote do
      Absinthe.Schema.Notation.stash()

      object unquote(naming.edge_type_identifier), unquote(attrs) do
        private(:absinthe_relay, {:edge, unquote(naming.attrs)}, {:fill, unquote(__MODULE__)})
        unquote(block)
      end

      Absinthe.Schema.Notation.pop()
    end
  end

  def additional_types({:connection, attrs}, _) do
    naming = Naming.from_attrs!(attrs)
    identifier = naming.edge_type_identifier

    %Schema.ObjectTypeDefinition{
      name: identifier |> Atom.to_string() |> Macro.camelize(),
      identifier: identifier,
      module: __MODULE__,
      __private__: [absinthe_relay: [{{:edge, attrs}, {:fill, __MODULE__}}]],
      __reference__: Absinthe.Schema.Notation.build_reference(__ENV__)
    }
  end

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

  def fillout({:paginate, type}, node) do
    Map.update!(node, :arguments, fn arguments ->
      type
      |> paginate_args()
      |> Enum.map(fn {id, type} -> build_arg(id, type) end)
      |> put_uniq(arguments)
    end)
  end

  #   @desc "The item at the end of the edge"
  # field(:node, unquote(naming.node_type_identifier))
  # @desc "A cursor for use in pagination"
  # field(:cursor, non_null(:string))
  def fillout({:edge, attrs}, node) do
    naming = Naming.from_attrs!(attrs)

    Map.update!(node, :fields, fn fields ->
      naming.node_type_identifier
      |> edge_fields
      |> put_uniq(fields)
    end)
  end

  def fillout(_, node) do
    node
  end

  defp put_uniq(new, prior) do
    existing = MapSet.new(prior, & &1.identifier)

    new
    |> Enum.filter(&(!(&1.identifier in existing)))
    |> Enum.concat(prior)
  end

  defp edge_fields(node_type) do
    [
      %Schema.FieldDefinition{
        name: "node",
        identifier: :node,
        type: node_type,
        module: __MODULE__,
        __reference__: Absinthe.Schema.Notation.build_reference(__ENV__)
      },
      %Schema.FieldDefinition{
        name: "cursor",
        identifier: :cursor,
        type: :string,
        module: __MODULE__,
        __reference__: Absinthe.Schema.Notation.build_reference(__ENV__)
      }
    ]
  end

  defp paginate_args(:forward) do
    [after: :string, first: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :integer}]
  end

  defp paginate_args(:backward) do
    [before: :string, last: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :integer}]
  end

  defp paginate_args(:both) do
    [after: :string, first: :integer, before: :string, last: :integer]
  end

  defp build_arg(id, type) do
    %Schema.InputValueDefinition{
      name: id |> Atom.to_string(),
      identifier: id,
      type: type,
      module: __MODULE__,
      __reference__: Absinthe.Schema.Notation.build_reference(__ENV__)
    }
  end
end
