defmodule Ash.Type.Float do
  @constraints [
    max: [
      type: {:custom, __MODULE__, :float, []},
      doc: "Enforces a maximum on the value"
    ],
    min: [
      type: {:custom, __MODULE__, :float, []},
      doc: "Enforces a minimum on the value"
    ],
    greater_than: [
      type: {:custom, __MODULE__, :float, []},
      doc: "Enforces a minimum on the value (exclusive)"
    ],
    less_than: [
      type: {:custom, __MODULE__, :float, []},
      doc: "Enforces a maximum on the value (exclusive)"
    ]
  ]

  import Ash.Expr

  @moduledoc """
  Represents a float (floating point number)

  A builtin type that be referenced via `:float`

  ### Constraints

  #{Spark.Options.docs(@constraints)}
  """

  use Ash.Type

  @impl true
  def storage_type(_), do: :float

  @impl true
  def constraints, do: @constraints

  @doc false
  def float(value) do
    case cast_input(value, []) do
      {:ok, float} ->
        {:ok, float}

      :error ->
        {:error, "cannot be casted to float"}
    end
  end

  @impl true
  def matches_type?(v, _) do
    is_float(v)
  end

  @impl true
  def generator(constraints) do
    constraints
    |> Keyword.take([:min, :max])
    |> StreamData.float()
    |> StreamData.filter(fn value ->
      (!constraints[:less_than] || value < constraints[:less_than]) &&
        (!constraints[:greater_than] || value > constraints[:greater_than])
    end)
  end

  @impl true
  def apply_constraints(nil, _), do: {:ok, nil}

  def apply_constraints(value, constraints) do
    errors =
      Enum.reduce(constraints, [], fn
        {:max, max}, errors ->
          if value > max do
            [[message: "must be less than or equal to %{max}", max: max] | errors]
          else
            errors
          end

        {:min, min}, errors ->
          if value < min do
            [[message: "must be more than or equal to %{min}", min: min] | errors]
          else
            errors
          end

        {:less_than, less_than}, errors ->
          if value < less_than do
            errors
          else
            [[message: "must be less than %{less_than}", less_than: less_than] | errors]
          end

        {:greater_than, greater_than}, errors ->
          if value > greater_than do
            errors
          else
            [[message: "must be more than %{greater_than}", greater_than: greater_than] | errors]
          end
      end)

    case errors do
      [] -> {:ok, value}
      errors -> {:error, errors}
    end
  end

  @impl true
  def cast_input(nil, _), do: {:ok, nil}

  def cast_input(value, _) do
    Ecto.Type.cast(:float, value)
  end

  @impl true
  def cast_stored(nil, _), do: {:ok, nil}

  def cast_stored(value, _) do
    Ecto.Type.load(:float, value)
  end

  @impl true
  def cast_atomic(expr, _constraints) do
    {:atomic, expr}
  end

  def apply_atomic_constraints(expr, constraints) do
    expr =
      Enum.reduce(constraints, expr, fn
        {:max, max}, expr ->
          expr(
            if ^expr > ^max do
              error(
                Ash.Error.Changes.InvalidChanges,
                message: "must be less than or equal to %{max}",
                vars: %{max: ^max}
              )
            else
              ^expr
            end
          )

        {:min, min}, expr ->
          expr(
            if ^expr < ^min do
              error(
                Ash.Error.Changes.InvalidChanges,
                message: "must be greater than or equal to %{min}",
                vars: %{min: ^min}
              )
            else
              ^expr
            end
          )

        {:less_than, less_than}, expr ->
          expr(
            if ^expr < ^less_than do
              ^expr
            else
              error(
                Ash.Error.Changes.InvalidChanges,
                message: "must be greater than %{less_than}",
                vars: %{less_than: ^less_than}
              )
            end
          )

        {:greater_than, greater_than}, expr ->
          expr(
            if ^expr > ^greater_than do
              ^expr
            else
              error(
                Ash.Error.Changes.InvalidChanges,
                message: "must be greater than %{greater_than}",
                vars: %{greater_than: ^greater_than}
              )
            end
          )
      end)

    {:ok, expr}
  end

  @impl true
  def dump_to_native(nil, _), do: {:ok, nil}

  def dump_to_native(value, _) do
    Ecto.Type.dump(:float, value)
  end
end
