defmodule Absinthe.Blueprint do
  @moduledoc """
  Represents the graphql document to be executed.

  Please see the code itself for more information on individual blueprint sub
  modules.
  """

  alias __MODULE__

  defstruct operations: [],
            directives: [],
            fragments: [],
            name: nil,
            schema_definitions: [],
            schema: nil,
            prototype_schema: nil,
            adapter: nil,
            initial_phases: [],
            # Added by phases
            telemetry: %{},
            flags: %{},
            errors: [],
            input: nil,
            source: nil,
            execution: %Blueprint.Execution{},
            result: %{}

  @type t :: %__MODULE__{
          operations: [Blueprint.Document.Operation.t()],
          schema_definitions: [Blueprint.Schema.SchemaDefinition.t()],
          directives: [Blueprint.Schema.DirectiveDefinition.t()],
          name: nil | String.t(),
          fragments: [Blueprint.Document.Fragment.Named.t()],
          schema: nil | Absinthe.Schema.t(),
          prototype_schema: nil | Absinthe.Schema.t(),
          adapter: nil | Absinthe.Adapter.t(),
          # Added by phases
          telemetry: map,
          errors: [Absinthe.Phase.Error.t()],
          flags: flags_t,
          input: nil | Absinthe.Language.Document.t(),
          source: nil | String.t() | Absinthe.Language.Source.t(),
          execution: Blueprint.Execution.t(),
          result: result_t,
          initial_phases: [Absinthe.Phase.t()] | {module(), atom, list()}
        }

  @type result_t :: %{
          optional(:data) => term,
          optional(:errors) => [term],
          optional(:extensions) => term
        }

  @type node_t ::
          t()
          | Blueprint.Directive.t()
          | Blueprint.Document.t()
          | Blueprint.Schema.t()
          | Blueprint.Input.t()
          | Blueprint.TypeReference.t()

  @type use_t ::
          Blueprint.Document.Fragment.Named.Use.t()
          | Blueprint.Input.Variable.Use.t()

  @type flags_t :: %{atom => module}

  defdelegate prewalk(blueprint, fun), to: Absinthe.Blueprint.Transform
  defdelegate prewalk(blueprint, acc, fun), to: Absinthe.Blueprint.Transform
  defdelegate postwalk(blueprint, fun), to: Absinthe.Blueprint.Transform
  defdelegate postwalk(blueprint, acc, fun), to: Absinthe.Blueprint.Transform

  def find(blueprint, fun) do
    {_, found} =
      Blueprint.prewalk(blueprint, nil, fn
        node, nil ->
          if fun.(node) do
            {node, node}
          else
            {node, nil}
          end

        node, found ->
          # Already found
          {node, found}
      end)

    found
  end

  @doc false
  # This is largely a debugging tool which replaces `schema_node` struct values
  # with just the type identifier, rendering the blueprint tree much easier to read
  def __compress__(blueprint) do
    prewalk(blueprint, fn
      %{schema_node: %{identifier: id}} = node ->
        %{node | schema_node: id}

      node ->
        node
    end)
  end

  @spec fragment(t, String.t()) :: nil | Blueprint.Document.Fragment.Named.t()
  def fragment(blueprint, name) do
    Enum.find(blueprint.fragments, &(&1.name == name))
  end

  @doc """
  Add a flag to a node.
  """
  @spec put_flag(node_t, atom, module) :: node_t
  def put_flag(node, flag, mod) do
    update_in(node.flags, &Map.put(&1, flag, mod))
  end

  @doc """
  Determine whether a flag has been set on a node.
  """
  @spec flagged?(node_t, atom) :: boolean
  def flagged?(node, flag) do
    Map.has_key?(node.flags, flag)
  end

  @doc """
  Get the currently selected operation.
  """
  @spec current_operation(t) :: nil | Blueprint.Document.Operation.t()
  def current_operation(blueprint) do
    Enum.find(blueprint.operations, &(&1.current == true))
  end

  @doc """
  Update the current operation.
  """
  @spec update_current(t, (Blueprint.Document.Operation.t() -> Blueprint.Document.Operation.t())) ::
          t
  def update_current(blueprint, change) do
    ops =
      Enum.map(blueprint.operations, fn
        %{current: true} = op ->
          change.(op)

        other ->
          other
      end)

    %{blueprint | operations: ops}
  end

  @doc """
  Append the given field or fields to the given type
  """
  def extend_fields(blueprint = %Blueprint{}, ext_blueprint = %Blueprint{}) do
    ext_types = types_by_name(ext_blueprint)

    schema_defs =
      for schema_def = %{type_definitions: type_defs} <- blueprint.schema_definitions do
        type_defs =
          for type_def <- type_defs do
            case ext_types[type_def.name] do
              nil ->
                type_def

              %{fields: new_fields} ->
                %{type_def | fields: type_def.fields ++ new_fields}
            end
          end

        %{schema_def | type_definitions: type_defs}
      end

    %{blueprint | schema_definitions: schema_defs}
  end

  def extend_fields(blueprint, ext_blueprint) when is_atom(ext_blueprint) do
    extend_fields(blueprint, ext_blueprint.__absinthe_blueprint__())
  end

  def add_field(blueprint = %Blueprint{}, type_def_name, new_field) do
    schema_defs =
      for schema_def = %{type_definitions: type_defs} <- blueprint.schema_definitions do
        type_defs =
          for type_def <- type_defs do
            if type_def.name == type_def_name do
              %{type_def | fields: type_def.fields ++ List.wrap(new_field)}
            else
              type_def
            end
          end

        %{schema_def | type_definitions: type_defs}
      end

    %{blueprint | schema_definitions: schema_defs}
  end

  def find_field(%{fields: fields}, name) do
    Enum.find(fields, fn %{name: field_name} -> field_name == name end)
  end

  @doc """
  Index the types by their name
  """
  def types_by_name(blueprint = %Blueprint{}) do
    for %{type_definitions: type_defs} <- blueprint.schema_definitions,
        type_def <- type_defs,
        into: %{} do
      {type_def.name, type_def}
    end
  end

  def types_by_name(module) when is_atom(module) do
    types_by_name(module.__absinthe_blueprint__())
  end

  defimpl Inspect do
    defdelegate inspect(term, options),
      to: Absinthe.Schema.Notation.SDL.Render
  end
end
