defmodule QueryBuilder.Schema do
  @moduledoc false

  # The library code relies on named bindings to refer to associations. This module
  # allows to define a named binding for a schema, when it is used in a join.
  #
  # The module provides the caller with the following functions:
  # * `_binding/1`: binding name for a join between this schema and the given
  # association field. The list of association fields must be given in `opts` when
  # `use`-ing this module for this function to return an atom.
  # * `_binding/0`: if we don't have a specific binding name between the schema and
  # the association field (i.e. the list of association fields has not been given),
  # use the schema name as the binding name.
  # * `_join/4`: creates a join query expression.
  #
  # Important to understand:
  # ------------------------
  # When a named binding needs to be assigned for a join, first the code calls
  # `_binding/1` on the **source schema**, with the association field as argument.
  # If a name cannot be found, then `_binding/0` is called on the
  # **associated schema**.
  # `_binding/1` returns an atom in the format: `schema_name <> "__" <> assoc_field`.
  #
  # In order to join, `_join/4` needs to be called either on the source schema or the
  # schema of the association field. This again, depends whether the association
  # fields have been given to `opts` or not; whether a binding name for this
  # association has been retrieved through `_binding/0` or `_binding/1`. If we have no
  # specific binding name for this schema and association field, then `_join/4` is
  # called on the schema of the association field; otherwise, `_join/4`is called on
  # the source schema.

  defmacro __using__(opts) do
    this_module = __MODULE__
    assoc_fields = Keyword.get(opts, :assoc_fields, [])

    [
      quote do
        require Ecto.Query

        Module.register_attribute(__MODULE__, :bindings, accumulate: true)
        @before_compile unquote(this_module)

        # assign a named binding (the schema's module name) to the schema of the root query
        def _query(), do: Ecto.Query.from(x in __MODULE__, as: unquote(__CALLER__.module))

        def _binding(), do: __MODULE__
      end,
      for assoc_field <- assoc_fields do
        binding = String.to_atom("#{__CALLER__.module}__#{assoc_field}")

        quote do
          Module.put_attribute(__MODULE__, :bindings, unquote(binding))

          def _join(query, type, source_binding, unquote(assoc_field), on) do
            Ecto.Query.join(
              query,
              type,
              [{^source_binding, x}],
              y in assoc(x, ^unquote(assoc_field)),
              as: unquote(binding),
              on: ^on
            )
          end

          def _binding(unquote(assoc_field)),
              do: unquote(binding)
        end
      end,
      quote do
        def _binding(_), do: nil
      end,
      quote do
        def _join(query, type, source_binding, assoc_field, on) do
          Ecto.Query.join(query, type, [{^source_binding, x}], y in assoc(x, ^assoc_field),
            as: unquote(__CALLER__.module),
            on: ^on
          )
        end
      end
    ]
  end

  defmacro __before_compile__(_env) do
    quote do
      def _bindings(), do: @bindings
    end
  end
end
