defmodule Absinthe.Blueprint.Schema do
  @moduledoc false

  alias __MODULE__

  alias Absinthe.Blueprint

  @type directive_t :: Schema.DirectiveDefinition.t()

  @type type_t ::
          Blueprint.Schema.EnumTypeDefinition.t()
          | Blueprint.Schema.InputObjectTypeDefinition.t()
          | Blueprint.Schema.InterfaceTypeDefinition.t()
          | Blueprint.Schema.ObjectTypeDefinition.t()
          | Blueprint.Schema.ScalarTypeDefinition.t()
          | Blueprint.Schema.UnionTypeDefinition.t()

  @type t ::
          Blueprint.Schema.EnumValueDefinition.t()
          | Blueprint.Schema.InputValueDefinition.t()
          | Blueprint.Schema.SchemaDeclaration.t()
          | Blueprint.Schema.SchemaDefinition.t()
          | type_t()
          | directive_t()

  @doc """
  Lookup a type definition that is part of a schema.
  """
  @spec lookup_type(Blueprint.t(), atom) :: nil | Blueprint.Schema.t()
  def lookup_type(blueprint, identifier) do
    blueprint.schema_definitions
    |> List.first()
    |> Map.get(:type_definitions)
    |> Enum.find(fn
      %{identifier: ^identifier} ->
        true

      _ ->
        false
    end)
  end

  @doc """
  Lookup a directive definition that is part of a schema.
  """
  @spec lookup_directive(Blueprint.t(), atom) :: nil | Blueprint.Schema.directive_t()
  def lookup_directive(blueprint, identifier) do
    blueprint.schema_definitions
    |> List.first()
    |> Map.get(:directive_definitions)
    |> Enum.find(fn
      %{identifier: ^identifier} ->
        true

      _ ->
        false
    end)
  end

  def functions(module) do
    if function_exported?(module, :functions, 0) do
      module.functions()
    else
      []
    end
  end

  def build([%Absinthe.Blueprint{} = bp | attrs]) do
    build_types(attrs, [bp], [])
  end

  def struct_to_kind(%struct{}), do: struct_to_kind(struct)
  def struct_to_kind(Blueprint.Schema.DirectiveDefinition), do: "directive"
  def struct_to_kind(Blueprint.Schema.EnumTypeDefinition), do: "enum type"
  def struct_to_kind(Blueprint.Schema.EnumValueDefinition), do: "enum value"
  def struct_to_kind(Blueprint.Schema.FieldDefinition), do: "field"
  def struct_to_kind(Blueprint.Schema.InputObjectTypeDefinition), do: "input object"
  def struct_to_kind(Blueprint.Schema.InputValueDefinition), do: "argument"
  def struct_to_kind(Blueprint.Schema.InterfaceTypeDefinition), do: "interface"
  def struct_to_kind(Blueprint.Schema.ObjectTypeDefinition), do: "object"
  def struct_to_kind(Blueprint.Schema.ScalarTypeDefinition), do: "scalar"
  def struct_to_kind(Blueprint.Schema.UnionTypeDefinition), do: "union"
  def struct_to_kind(Blueprint.Schema.TypeExtensionDefinition), do: "type extension"
  def struct_to_kind(_), do: "type"

  defp build_types([], [bp], buffer) do
    if buffer != [] do
      raise """
      Unused buffer! #{inspect(buffer)}
      """
    end

    Map.update!(bp, :schema_definitions, &Enum.reverse/1)
  end

  # this rather insane scheme lets interior macros get back out to exterior
  # scopes so that they can define top level entities as necessary, and then
  # return to the regularly scheduled programming.
  defp build_types([:stash | rest], [head | tail], buff) do
    build_types(rest, tail, [head | buff])
  end

  defp build_types([:pop | rest], remaining, [head | buff]) do
    build_types(rest, [head | remaining], buff)
  end

  defp build_types([%Schema.SchemaDefinition{} = schema | rest], stack, buff) do
    build_types(rest, [schema | stack], buff)
  end

  @simple_open [
    Schema.ScalarTypeDefinition,
    Schema.ObjectTypeDefinition,
    Schema.FieldDefinition,
    Schema.EnumTypeDefinition,
    Schema.DirectiveDefinition,
    Schema.InputObjectTypeDefinition,
    Schema.InputValueDefinition,
    Schema.InterfaceTypeDefinition,
    Schema.UnionTypeDefinition,
    Schema.EnumValueDefinition,
    Schema.TypeExtensionDefinition,
    Schema.SchemaDeclaration
  ]

  defp build_types([%module{} = type | rest], stack, buff) when module in @simple_open do
    build_types(rest, [type | stack], buff)
  end

  defp build_types([{:import_fields, criterion} | rest], [obj | stack], buff) do
    build_types(rest, [push(obj, :imports, criterion) | stack], buff)
  end

  defp build_types([{:desc, desc} | rest], [item | stack], buff) do
    build_types(rest, [%{item | description: desc} | stack], buff)
  end

  defp build_types([{:middleware, middleware} | rest], [field, obj | stack], buff) do
    field = Map.update!(field, :middleware, &(middleware ++ &1))
    build_types(rest, [field, obj | stack], buff)
  end

  defp build_types([{:config, config} | rest], [field | stack], buff) do
    field = %{field | config: config}
    build_types(rest, [field | stack], buff)
  end

  defp build_types([{:directive, trigger} | rest], [field | stack], buff) do
    field = Map.update!(field, :directives, &[trigger | &1])
    build_types(rest, [field | stack], buff)
  end

  defp build_types([{:trigger, trigger} | rest], [field | stack], buff) do
    field = Map.update!(field, :triggers, &[trigger | &1])
    build_types(rest, [field | stack], buff)
  end

  defp build_types([{:interface, interface} | rest], [obj | stack], buff) do
    obj = Map.update!(obj, :interfaces, &[interface | &1])
    build_types(rest, [obj | stack], buff)
  end

  defp build_types([{:type, type} | rest], [obj | stack], buff) do
    obj = Map.update!(obj, :types, &[type | &1])
    build_types(rest, [obj | stack], buff)
  end

  defp build_types([{:__private__, private} | rest], [entity | stack], buff) do
    entity = Map.update!(entity, :__private__, &update_private(&1, private))
    build_types(rest, [entity | stack], buff)
  end

  defp build_types([{:values, values} | rest], [enum | stack], buff) do
    enum = Map.update!(enum, :values, &(List.wrap(values) ++ &1))
    build_types(rest, [enum | stack], buff)
  end

  defp build_types([{:sdl, sdl_definitions} | rest], [schema | stack], buff) do
    # TODO: Handle directives, etc
    build_types(rest, [concat(schema, :type_definitions, sdl_definitions) | stack], buff)
  end

  defp build_types([{:locations, locations} | rest], [directive | stack], buff) do
    directive = Map.update!(directive, :locations, &(locations ++ &1))
    build_types(rest, [directive | stack], buff)
  end

  defp build_types([{attr, value} | rest], [entity | stack], buff) do
    entity = %{entity | attr => value}
    build_types(rest, [entity | stack], buff)
  end

  defp build_types([:close | rest], [%Schema.EnumValueDefinition{} = value, enum | stack], buff) do
    build_types(rest, [push(enum, :values, value) | stack], buff)
  end

  defp build_types([:close | rest], [%Schema.InputValueDefinition{} = arg, field | stack], buff) do
    build_types(rest, [push(field, :arguments, arg) | stack], buff)
  end

  @extendable_types [
    Schema.EnumTypeDefinition,
    Schema.InputObjectTypeDefinition,
    Schema.InterfaceTypeDefinition,
    Schema.ObjectTypeDefinition,
    Schema.ScalarTypeDefinition,
    Schema.SchemaDeclaration,
    Schema.UnionTypeDefinition
  ]
  defp build_types(
         [:close | rest],
         [
           %extendable_type{} = def,
           %Schema.TypeExtensionDefinition{} = extend | stack
         ],
         buff
       )
       when extendable_type in @extendable_types do
    build_types(rest, [%{extend | definition: def} | stack], buff)
  end

  defp build_types(
         [:close | rest],
         [%Schema.FieldDefinition{} = field, %Schema.SchemaDeclaration{} = declaration | stack],
         buff
       ) do
    build_types(rest, [push(declaration, :field_definitions, field) | stack], buff)
  end

  defp build_types([:close | rest], [%Schema.FieldDefinition{} = field, obj | stack], buff) do
    field =
      field
      |> Map.update!(:middleware, &Enum.reverse/1)
      |> Map.update!(:arguments, &Enum.reverse/1)
      |> Map.update!(:triggers, &{:%{}, [], &1})
      |> Map.put(:function_ref, {obj.identifier, field.identifier})

    build_types(rest, [push(obj, :fields, field) | stack], buff)
  end

  defp build_types(
         [:close | rest],
         [%Schema.TypeExtensionDefinition{} = extension, schema | stack],
         buff
       ) do
    build_types(rest, [push(schema, :type_extensions, extension) | stack], buff)
  end

  defp build_types([:close | rest], [%Schema.ObjectTypeDefinition{} = obj, schema | stack], buff) do
    obj = Map.update!(obj, :fields, &Enum.reverse/1)
    build_types(rest, [push(schema, :type_definitions, obj) | stack], buff)
  end

  defp build_types(
         [:close | rest],
         [%Schema.InputObjectTypeDefinition{} = obj, schema | stack],
         buff
       ) do
    obj = Map.update!(obj, :fields, &Enum.reverse/1)
    build_types(rest, [push(schema, :type_definitions, obj) | stack], buff)
  end

  defp build_types(
         [:close | rest],
         [%Schema.InterfaceTypeDefinition{} = iface, schema | stack],
         buff
       ) do
    iface = Map.update!(iface, :fields, &Enum.reverse/1)
    build_types(rest, [push(schema, :type_definitions, iface) | stack], buff)
  end

  defp build_types([:close | rest], [%Schema.UnionTypeDefinition{} = union, schema | stack], buff) do
    union = Map.update!(union, :types, &Enum.reverse/1)
    build_types(rest, [push(schema, :type_definitions, union) | stack], buff)
  end

  defp build_types([:close | rest], [%Schema.DirectiveDefinition{} = dir, schema | stack], buff) do
    build_types(rest, [push(schema, :directive_definitions, dir) | stack], buff)
  end

  defp build_types([:close | rest], [%Schema.EnumTypeDefinition{} = type, schema | stack], buff) do
    type = Map.update!(type, :values, &Enum.reverse/1)
    schema = push(schema, :type_definitions, type)
    build_types(rest, [schema | stack], buff)
  end

  defp build_types([:close | rest], [%Schema.ScalarTypeDefinition{} = type, schema | stack], buff) do
    schema = push(schema, :type_definitions, type)
    build_types(rest, [schema | stack], buff)
  end

  defp build_types(
         [:close | rest],
         [%Schema.SchemaDeclaration{} = schema_declaration, schema | stack],
         buff
       ) do
    # The declaration is pushed into the :type_definitions instead of the :schema_declaration
    # as it will be split off later in the ApplyDeclaration phase
    schema = push(schema, :type_definitions, schema_declaration)
    build_types(rest, [schema | stack], buff)
  end

  defp build_types([:close | rest], [%Schema.SchemaDefinition{} = schema, bp], buff) do
    bp = push(bp, :schema_definitions, schema)
    build_types(rest, [bp], buff)
  end

  defp push(entity, key, value) do
    Map.update!(entity, key, &[value | &1])
  end

  defp concat(entity, key, value) do
    Map.update!(entity, key, &(&1 ++ value))
  end

  defp update_private(existing_private, private) do
    Keyword.merge(existing_private, private, fn
      _, v1, v2 ->
        update_private(v1, v2)
    end)
  end
end
