defmodule QueryBuilder.Extension do
  @moduledoc ~S"""
  Use this module to create an extension module to `QueryBuilder` for app specific query utilities.
  Use your query builder extension module wherever you would normally use `QueryBuilder`

  Example:
  ```
  defmodule MyApp.QueryBuilder do
    use QueryBuilder.Extension

    defmacro __using__(opts) do
      quote do
        require QueryBuilder
        QueryBuilder.__using__(unquote(opts))
      end
    end

    # Add app specific query functions
    #---------------------------------

    def where_initcap(query, field, value) do
      text_equals_condition = fn field, value, get_binding_fun ->
        {field, binding} = get_binding_fun.(field)
        Ecto.Query.dynamic([{^binding, x}], fragment("initcap(?)", ^value) == field(x, ^field))
      end

      query
      |> where(&text_equals_condition.(field, value, &1))
    end
  end

  defmodule MyApp.Accounts.User do
    use MyApp.QueryBuilder

    schema "users" do
      field :name, :string
      field :active, :boolean
    end
  end

  defmodule MyApp.Accounts do
    alias MyApp.QueryBuilder, as: QB

    def list_users(opts \\ []) do
      # Query list can include custom query functions as well:
      # [where_initcap: {:name, "john"}, where: {:active, true}]
      MyApp.Accounts.User
      |> QB.from_list(opts)
      |> Repo.all()
    end
  end
  ```
  """

  defmacro __using__(_opts) do
    quote do
      # Expose all QueryBuilder functions: QueryBuilder.__info__(:functions)

      defdelegate left_join(query, assoc_fields, filters \\ [], or_filters \\ []),
        to: QueryBuilder

      defdelegate maybe_where(query, bool, filters), to: QueryBuilder

      defdelegate maybe_where(query, condition, assoc_fields, filters, or_filters \\ []),
        to: QueryBuilder

        defdelegate maybe_order_by(query, bool, value), to: QueryBuilder

        defdelegate maybe_order_by(query, condition, assoc_fields, value),
          to: QueryBuilder

      defdelegate new(ecto_query), to: QueryBuilder
      defdelegate paginate(query, repo, opts \\ []), to: QueryBuilder
      defdelegate order_by(query, value), to: QueryBuilder
      defdelegate order_by(query, assoc_fields, value), to: QueryBuilder
      defdelegate preload(query, assoc_fields), to: QueryBuilder
      defdelegate where(query, filters), to: QueryBuilder
      defdelegate where(query, assoc_fields, filters, or_filters \\ []), to: QueryBuilder
      defdelegate offset(query, value), to: QueryBuilder
      defdelegate limit(query, value), to: QueryBuilder

      @doc ~S"""
      Allows to pass a list of operations through a keyword list.

      Example:
      ```
      QueryBuilder.from_list(query, [
        where: [name: "John", city: "Anytown"],
        preload: [articles: :comments]
      ])
      ```
      """
      def from_list(query, nil), do: query
      def from_list(query, []), do: query

      def from_list(query, [{operation, arguments} | tail]) do
        arguments =
          cond do
            is_tuple(arguments) -> Tuple.to_list(arguments)
            is_list(arguments) -> [arguments]
            true -> List.wrap(arguments)
          end

        apply(__MODULE__, operation, [query | arguments])
        |> from_list(tail)
      end
    end
  end
end
