import Kernel, except: [apply: 2]

defmodule Ecto.Query.Builder.Windows do
  @moduledoc false

  alias Ecto.Query.Builder
  alias Ecto.Query.Builder.{GroupBy, OrderBy}
  @sort_order [:partition_by, :order_by, :frame]

  @doc """
  Escapes a window params.

  ## Examples

      iex> escape(quote do [order_by: [desc: 13]] end, {[], %{}}, [x: 0], __ENV__)
      {[order_by: [desc: 13]], [], {[], %{}}}

  """
  @spec escape([Macro.t], {list, term}, Keyword.t, Macro.Env.t | {Macro.Env.t, fun})
          :: {Macro.t, [{atom, term}], {list, term}}
  def escape(kw, params_acc, vars, env) when is_list(kw) do
    {compile, runtime} = sort(@sort_order, kw, :compile, [], [])
    {compile, params_acc} = Enum.map_reduce(compile, params_acc, &escape_compile(&1, &2, vars, env))
    {compile, runtime, params_acc}
  end

  def escape(kw, _params_acc, _vars, _env) do
    error!(kw)
  end

  defp sort([key | keys], kw, mode, compile, runtime) do
    case Keyword.pop(kw, key) do
      {nil, kw} ->
        sort(keys, kw, mode, compile, runtime)

      {{:^, _, [var]}, kw} ->
        sort(keys, kw, :runtime, compile, [{key, var} | runtime])

      {_, _} when mode == :runtime ->
        [{runtime_key, _} | _] = runtime
        raise ArgumentError, "window has an interpolated value under `#{runtime_key}` " <>
                             "and therefore `#{key}` must also be interpolated"

      {expr, kw} ->
        sort(keys, kw, mode, [{key, expr} | compile], runtime)
    end
  end

  defp sort([], [], _mode, compile, runtime) do
    {Enum.reverse(compile), Enum.reverse(runtime)}
  end

  defp sort([], kw, _mode, _compile, _runtime) do
    error!(kw)
  end

  defp escape_compile({:partition_by, fields}, params_acc, vars, env) do
    {fields, params_acc} = GroupBy.escape(:partition_by, fields, params_acc, vars, env)
    {{:partition_by, fields}, params_acc}
  end

  defp escape_compile({:order_by, fields}, params_acc, vars, env) do
    {fields, params_acc} = OrderBy.escape(:order_by, fields, params_acc, vars, env)
    {{:order_by, fields}, params_acc}
  end

  defp escape_compile({:frame, frame_clause}, params_acc, vars, env) do
    {frame_clause, params_acc} = escape_frame(frame_clause, params_acc, vars, env)
    {{:frame, frame_clause}, params_acc}
  end

  defp escape_frame({:fragment, _, _} = fragment, params_acc, vars, env) do
    Builder.escape(fragment, :any, params_acc, vars, env)
  end
  defp escape_frame(other, _, _, _) do
    Builder.error!("expected a dynamic or fragment in `:frame`, got: `#{inspect other}`")
  end

  defp error!(other) do
    Builder.error!(
      "expected window definition to be a keyword list " <>
        "with partition_by, order_by or frame as keys, got: `#{inspect other}`"
    )
  end

  @doc """
  Builds a quoted expression.

  The quoted expression should evaluate to a query at runtime.
  If possible, it does all calculations at compile time to avoid
  runtime work.
  """
  @spec build(Macro.t, [Macro.t], Keyword.t, Macro.Env.t) :: Macro.t
  def build(query, binding, windows, env) when is_list(windows) do
    {query, binding} = Builder.escape_binding(query, binding, env)

    {compile, runtime} =
      windows
      |> Enum.map(&escape_window(binding, &1, env))
      |> Enum.split_with(&elem(&1, 2) == [])

    compile = Enum.map(compile, &build_compile_window(&1, env))
    runtime = Enum.map(runtime, &build_runtime_window(&1, env))
    query = Builder.apply_query(query, __MODULE__, [compile], env)

    if runtime == [] do
      query
    else
      quote do
        Ecto.Query.Builder.Windows.runtime!(
          unquote(query),
          unquote(runtime),
          unquote(env.file),
          unquote(env.line)
        )
      end
    end
  end

  def build(_, _, windows, _) do
    Builder.error!(
      "expected window definitions to be a keyword list with window names as keys and " <>
        "a keyword list with the window definition as value, got: `#{inspect windows}`"
    )
  end

  defp escape_window(vars, {name, expr}, env) do
    {compile_acc, runtime_acc, {params, _acc}} = escape(expr, {[], %{}}, vars, env)
    {name, compile_acc, runtime_acc, Builder.escape_params(params)}
  end

  defp build_compile_window({name, compile_acc, _, params}, env) do
    {name,
     quote do
       %Ecto.Query.QueryExpr{
         expr: unquote(compile_acc),
         params: unquote(params),
         file: unquote(env.file),
         line: unquote(env.line)
       }
     end}
  end

  defp build_runtime_window({name, compile_acc, runtime_acc, params}, _env) do
    {:{}, [], [name, Enum.reverse(compile_acc), runtime_acc, Enum.reverse(params)]}
  end

  @doc """
  Invoked for runtime windows.
  """
  def runtime!(query, runtime, file, line) do
    windows =
      Enum.map(runtime, fn {name, compile_acc, runtime_acc, params} ->
        {acc, params} = do_runtime_window!(runtime_acc, query, compile_acc, params)
        expr = %Ecto.Query.QueryExpr{expr: Enum.reverse(acc), params: Enum.reverse(params), file: file, line: line}
        {name, expr}
      end)

    apply(query, windows)
  end

  defp do_runtime_window!([{:order_by, order_by} | kw], query, acc, params) do
    {order_by, params} = OrderBy.order_by_or_distinct!(:order_by, query, order_by, params)
    do_runtime_window!(kw, query, [{:order_by, order_by} | acc], params)
  end

  defp do_runtime_window!([{:partition_by, partition_by} | kw], query, acc, params) do
    {partition_by, params} = GroupBy.group_or_partition_by!(:partition_by, query, partition_by, params)
    do_runtime_window!(kw, query, [{:partition_by, partition_by} | acc], params)
  end

  defp do_runtime_window!([{:frame, frame} | kw], query, acc, params) do
    case frame do
      %Ecto.Query.DynamicExpr{} ->
        {frame, params, _count} = Builder.Dynamic.partially_expand(:windows, query, frame, params, length(params))
        do_runtime_window!(kw, query, [{:frame, frame} | acc], params)

      _ ->
        raise ArgumentError,
                "expected a dynamic or fragment in `:frame`, got: `#{inspect frame}`"
    end
  end

  defp do_runtime_window!([], _query, acc, params), do: {acc, params}

  @doc """
  The callback applied by `build/4` to build the query.
  """
  @spec apply(Ecto.Queryable.t, Keyword.t) :: Ecto.Query.t
  def apply(%Ecto.Query{windows: windows} = query, definitions) do
    merged = Keyword.merge(windows, definitions, fn name, _, _ ->
      Builder.error! "window with name #{name} is already defined"
    end)

    %{query | windows: merged}
  end

  def apply(query, definitions) do
    apply(Ecto.Queryable.to_query(query), definitions)
  end
end
