defmodule LiveForm do
  @moduledoc """
  Functions and macros for validating requests to Phoenix
  Controllers.
  """

  alias LiveForm.Utils

  @doc """
  Takes a nested struct data structure and turns it into a map with
  atoms as keys.
  """
  def schema_to_atom_map(map) when is_map(map) do
    map
    |> Map.delete(:__struct__)
    |> Map.delete(:__meta__)
    |> Enum.map(fn
      {key, %{__struct__: _} = value} ->
        {key, schema_to_atom_map(value)}

      {key, val} when is_list(val) ->
        {key,
         Enum.map(val, fn entry ->
           schema_to_atom_map(entry)
         end)}

      key_val ->
        key_val
    end)
    |> Map.new()
  end

  def schema_to_atom_map(val) do
    val
  end

  @doc """
  Takes a nested struct data structure and turns it into a map with
  strings as keys
  """
  def schema_to_string_map(map) when is_map(map) do
    map
    |> Map.delete(:__struct__)
    |> Map.delete(:__meta__)
    |> Enum.map(fn
      {key, %{__struct__: _} = value} ->
        {Atom.to_string(key), schema_to_string_map(value)}

      {key, val} when is_list(val) ->
        {Atom.to_string(key),
         Enum.map(val, fn entry ->
           schema_to_string_map(entry)
         end)}

      {key, val} ->
        {Atom.to_string(key), val}
    end)
    |> Map.new()
  end

  def schema_to_string_map(val) do
    val
  end

  @doc """
  Go through the map of values and remove all fields which are nil
  """
  def drop_nil_fields(map) when is_map(map) do
    map
    |> Enum.reduce(
      %{},
      fn {key, val}, acc ->
        val = drop_nil_fields(val)

        if empty?(val) do
          acc
        else
          Map.put(acc, key, val)
        end
      end
    )
  end

  def drop_nil_fields(list) when is_list(list) do
    list
    |> Enum.reduce([], fn elem, acc ->
      elem = drop_nil_fields(elem)

      if empty?(elem) do
        acc
      else
        [elem | acc]
      end
    end)
    |> Enum.reverse()
  end

  def drop_nil_fields(val), do: val

  defp empty?(val) do
    val in [nil, %{}, "", []]
  end

  defmacro __using__(_opts) do
    quote do
      import LiveForm, only: [live_form: 2]
    end
  end

  defp generate_validation({:__block__, [], block_contents}, caller) do
    root_field_declarations = Utils.generate_basic_field_schema_definitions(block_contents, caller)
    root_fields = Utils.get_all_basic_fields(block_contents)
    root_required_fields = Utils.get_required_basic_fields(block_contents)
    root_validations = Utils.generate_basic_field_validations(block_contents, caller)
    root_group_declarations = Utils.generate_group_field_schema_definitions(block_contents, caller)
    root_sub_schema_casts = Utils.generate_group_field_schema_casts(block_contents, nil)
    group_schema_changesets = Utils.generate_group_field_schema_changesets(block_contents, nil, caller)

    module =
      [
        "@moduledoc false",
        "",
        "use Ecto.Schema",
        "import Ecto.Changeset",
        "",
        "@primary_key false",
        "embedded_schema do",
        root_field_declarations,
        root_group_declarations,
        "end",
        "",
        "def changeset(schema, params) do",
        "schema",
        "|> cast(params, #{inspect(root_fields)})",
        "|> validate_required(#{inspect(root_required_fields)})",
        root_validations,
        root_sub_schema_casts,
        "end",
        "",
        group_schema_changesets
      ]
      |> List.flatten()

    formatted_module =
      module
      |> Enum.join("\n")
      |> Code.format_string!()

    module =
      (module ++
         [
           "",
           "def dump do",
           "\"\"\"",
           "#{formatted_module}",
           "\"\"\"",
           "end"
         ])
      |> Enum.join("\n")

    Code.string_to_quoted!(module)
  end

  defp generate_validation(ast, caller) do
    generate_validation({:__block__, [], [ast]}, caller)
  end

  @doc """
  This macro provides the ability to define validation schemas for use in Phoenix controllers

  ## Example
  ```elixir
  use LiveForm, view_module: LiveForm.ErrorView, view_template: "errors.json", error_status: :unprocessable_entity

  live_form :index do
    required :terms_conditions, :boolean
    required :password, :string
    required :password_confirmation, :string
    optional :age, :integer
  end

  def index(conn, params) do
    # You will only get into this function if the request
    # parameters have passed the above validator. The params
    # variable is now just a map with atoms as keys.

    render(conn, "index.html")
  end
  ```
  """
  defmacro live_form(model, do: block) do
    camel_model =
      model
      |> Atom.to_string()
      |> Macro.camelize()

    calling_module = __CALLER__.module

    # Create validation module
    validation_module_name = Module.concat([calling_module, LiveForm, camel_model])
    validation_module_ast = generate_validation(block, __CALLER__)
    Module.create(validation_module_name, validation_module_ast, Macro.Env.location(__ENV__))

    form_function_name = String.to_atom("#{model}_form")
    changeset_function_name = String.to_atom("#{model}_changset")

    quote do
      def unquote(form_function_name)(params, form_opts \\ []) do
        form_opts = Keyword.merge([{:as, unquote(model)}], form_opts)

        %unquote(validation_module_name){}
        |> unquote(validation_module_name).changeset(params)
        |> Phoenix.Component.to_form(form_opts)
      end

      def unquote(changeset_function_name)(params) do
        %unquote(validation_module_name){}
        |> unquote(validation_module_name).changeset(params)
      end
    end
  end
end
