defmodule Parameter.Types do
  @moduledoc """
  Parameter supports different types to be used in the field inside a schema. The available types are:

  * `string`
  * `atom`
  * `any`
  * `integer`
  * `float`
  * `boolean`
  * `map`
  * `{map, nested_type}`
  * `array`
  * `{array, nested_type}`
  * `date`
  * `time`
  * `datetime`
  * `naive_datetime`
  * `decimal`*
  * `enum`**


  \\* For decimal type add the [decimal](https://hexdocs.pm/decimal) library into your project.

  \\*\\* Check the `Parameter.Enum` for more information on how to use enums.

  For implementing custom types check the `Parameter.Parametrizable` module. Implementing this behavour in a module makes eligible to be a field in the schema definition.
  """

  @type t :: base_types | composite_types

  @type base_types ::
          :string
          | :atom
          | :any
          | :boolean
          | :date
          | :datetime
          | :decimal
          | :float
          | :integer
          | :naive_datetime
          | :string
          | :time
          | :array
          | :map

  @type composite_types :: {:array, t()} | {:map, t()}

  @base_types ~w(atom any boolean date datetime decimal float integer naive_datetime string time)a
  @composite_types ~w(array map)a

  @spec base_type?(any) :: boolean
  def base_type?(type), do: type in @base_types

  @spec composite_inner_type?(any) :: boolean
  def composite_inner_type?({type, _}), do: type in @composite_types
  def composite_inner_type?(_), do: false

  @spec composite_type?(any) :: boolean
  def composite_type?({type, _}), do: type in @composite_types
  def composite_type?(type), do: type in @composite_types

  @types_mod %{
    any: Parameter.Types.AnyType,
    atom: Parameter.Types.Atom,
    boolean: Parameter.Types.Boolean,
    date: Parameter.Types.Date,
    datetime: Parameter.Types.DateTime,
    decimal: Parameter.Types.Decimal,
    float: Parameter.Types.Float,
    integer: Parameter.Types.Integer,
    array: Parameter.Types.Array,
    map: Parameter.Types.Map,
    naive_datetime: Parameter.Types.NaiveDateTime,
    string: Parameter.Types.String,
    time: Parameter.Types.Time
  }

  @spec load(atom(), any) :: {:ok, any()} | {:error, any()}
  def load(type, value) do
    type_module = Map.get(@types_mod, type, type)
    type_module.load(value)
  rescue
    error -> {:error, "invalid input value #{inspect(error)}"}
  end

  @spec dump(atom(), any()) :: {:ok, any()} | {:error, any()}
  def dump(type, value) do
    type_module = Map.get(@types_mod, type, type)
    type_module.dump(value)
  rescue
    error -> {:error, "invalid input value #{inspect(error)}"}
  end

  @spec validate!(t(), any()) :: :ok | no_return()
  def validate!(type, value) do
    case validate(type, value) do
      {:error, error} -> raise ArgumentError, message: error
      result -> result
    end
  end

  @spec validate(t(), any()) :: :ok | {:error, any()}
  def validate(type, values)

  def validate({:array, inner_type}, values) when is_list(values) do
    Enum.reduce_while(values, :ok, fn value, acc ->
      case validate(inner_type, value) do
        :ok -> {:cont, acc}
        error -> {:halt, error}
      end
    end)
  end

  def validate({:array, _inner_type}, _values) do
    {:error, "invalid array type"}
  end

  def validate({:map, inner_type}, values) when is_map(values) do
    Enum.reduce_while(values, :ok, fn {_key, value}, acc ->
      case validate(inner_type, value) do
        :ok -> {:cont, acc}
        error -> {:halt, error}
      end
    end)
  end

  def validate({:map, _inner_type}, _values) do
    {:error, "invalid map type"}
  end

  def validate(type, value) do
    type_module = Map.get(@types_mod, type, type)
    type_module.validate(value)
  rescue
    error -> {:error, "invalid input value #{inspect(error)}"}
  end
end
