defmodule Exzeitable.Params do
  @moduledoc """
  Gets default parameters, replaces with module opts and then with the function opts.

  Validates that parameters are valid.
  """
  alias Exzeitable.HTML.Format
  alias Exzeitable.Params.Validations

  @enforce_keys [:query, :repo, :routes, :path, :fields, :module, :csrf_token]

  @type column :: atom
  @type function_name :: atom

  @type t :: %__MODULE__{
          # required
          query: Ecto.Query.t(),
          repo: module,
          routes: module,
          path: atom,
          fields: [{column, keyword}],
          module: module,
          csrf_token: String.t(),
          # optional
          order: [{:asc | :desc, column}] | nil,
          html_classes: Keyword.t(String.t()) | nil,
          parent: struct | nil,
          belongs_to: atom | nil,
          page: pos_integer,
          count: non_neg_integer,
          list: [struct],
          search: String.t(),
          show_field_buttons: boolean,
          action_buttons: [:new | :show | :edit | :delete],
          per_page: pos_integer,
          debounce: pos_integer,
          refresh: boolean,
          disable_hide: boolean,
          pagination: [:top | :bottom],
          assigns: map,
          text: module,
          formatter: {module, function_name} | {module, function_name, list}
        }

  defstruct [
    # required
    :query,
    :repo,
    :routes,
    :path,
    :fields,
    :module,
    :csrf_token,
    # optional
    :query_modifier,
    :order,
    :parent,
    :belongs_to,
    :html_classes,
    page: 1,
    count: 0,
    list: [],
    search: "",
    show_field_buttons: false,
    action_buttons: [:new, :show, :edit, :delete],
    per_page: 20,
    debounce: 300,
    refresh: false,
    disable_hide: false,
    pagination: [:top, :bottom],
    assigns: %{},
    text: Exzeitable.Text.Default,
    formatter: {Format, :format_field}
  ]

  @default_fields [
    label: nil,
    function: false,
    hidden: false,
    search: true,
    order: true,
    formatter: {Format, :format_field}
  ]

  @virtual_fields [
    function: true,
    search: false,
    order: false
  ]

  @doc "Gets fields from options and merges it into the defaults"
  @spec set_fields(keyword) :: [any]
  def set_fields(opts) do
    opts
    |> Keyword.get(:fields, [])
    |> Enum.map(fn {key, field} -> {key, merge_fields(field)} end)
  end

  # If virtual: true, a number of other options have to be overridden
  @spec merge_fields(keyword) :: keyword
  defp merge_fields(field) do
    if Keyword.get(field, :virtual) do
      @default_fields
      |> Keyword.merge(field)
      |> Keyword.merge(@virtual_fields)
    else
      Keyword.merge(@default_fields, field)
    end
  end

  @doc "Creates a new params struct, holds state for liveview"
  @spec new(keyword, keyword, atom) :: map
  def new(function_opts, module_opts, module) do
    fields =
      module_opts
      |> set_fields()
      |> Enum.map(fn {k, f} -> {k, Enum.into(f, %{})} end)

    token = Phoenix.Controller.get_csrf_token()

    module_opts
    |> Keyword.merge(function_opts)
    |> Map.new()
    |> Map.merge(%{fields: fields, module: module, csrf_token: token})
    |> Validations.paired_options()
    |> Validations.required_keys_present(@enforce_keys)
    |> then(&struct!(__MODULE__, &1))
  end
end
