defmodule Funx.Ord.Utils do
  @moduledoc """
  Utility functions for working with the `Funx.Ord` protocol.
  These functions assume that types passed in either support Elixir's comparison operators
  or implement the `Funx.Ord` protocol.
  """

  @type ord_map() :: %{
          lt?: (any(), any() -> boolean()),
          le?: (any(), any() -> boolean()),
          gt?: (any(), any() -> boolean()),
          ge?: (any(), any() -> boolean())
        }

  @type ord_t() :: Funx.Ord.t() | ord_map()

  import Funx.Monoid.Utils, only: [m_append: 3, m_concat: 2]

  alias Funx.Ord

  @doc """
  Transforms an ordering by applying a function `f` to values before comparison.

  The `ord` parameter can be an `Ord` module or a custom comparator map with comparison functions (`:lt?`, `:le?`, `:gt?`, and `:ge?`).
  When an `Ord` module is provided, it wraps the module’s functions to apply `f` to each value before invoking the comparison.
  If a custom comparator map is provided, it wraps the functions in the map to apply `f` to each value.

  ## Examples

      iex> ord = Funx.Ord.Utils.contramap(&String.length/1, Funx.Ord.Any)
      iex> ord.lt?.("cat", "zebra")
      true
  """
  @spec contramap((a -> b), ord_t()) :: ord_map()
        when a: any, b: any
  def contramap(f, ord \\ Ord) do
    ord = to_ord_map(ord)

    %{
      lt?: fn a, b -> ord.lt?.(f.(a), f.(b)) end,
      le?: fn a, b -> ord.le?.(f.(a), f.(b)) end,
      gt?: fn a, b -> ord.gt?.(f.(a), f.(b)) end,
      ge?: fn a, b -> ord.ge?.(f.(a), f.(b)) end
    }
  end

  @doc """
  Returns the maximum of two values, with an optional custom `Ord`.

  ## Examples

      iex> Funx.Ord.Utils.max(3, 5)
      5

      iex> ord = Funx.Ord.Utils.contramap(&String.length/1, Funx.Ord.Any)
      iex> Funx.Ord.Utils.max("cat", "zebra", ord)
      "zebra"
  """
  @spec max(a, a, ord_t()) :: a
        when a: any
  def max(a, b, ord \\ Ord) do
    case compare(a, b, ord) do
      :lt -> b
      _ -> a
    end
  end

  @doc """
  Returns the minimum of two values, with an optional custom `Ord`.

  ## Examples

      iex> Funx.Ord.Utils.min(10, 7)
      7

      iex> ord = Funx.Ord.Utils.contramap(&String.length/1, Funx.Ord.Any)
      iex> Funx.Ord.Utils.min("apple", "kiwi", ord)
      "kiwi"
  """
  @spec min(a, a, ord_t()) :: a
        when a: any
  def min(a, b, ord \\ Ord) do
    case compare(a, b, ord) do
      :gt -> b
      _ -> a
    end
  end

  @doc """
  Clamps a value between `min` and `max`, with an optional custom `Ord`.

  ## Examples

      iex> Funx.Ord.Utils.clamp(5, 1, 10)
      5

      iex> Funx.Ord.Utils.clamp(0, 1, 10)
      1

      iex> Funx.Ord.Utils.clamp(15, 1, 10)
      10
  """
  @spec clamp(a, a, a, ord_t()) :: a
        when a: any
  def clamp(value, min, max, ord \\ Ord) do
    value
    |> max(min, ord)
    |> min(max, ord)
  end

  @doc """
  Checks if `value` is between `min` and `max`, inclusive, with an optional custom `Ord`.

  ## Examples

      iex> Funx.Ord.Utils.between(5, 1, 10)
      true

      iex> Funx.Ord.Utils.between(0, 1, 10)
      false

      iex> Funx.Ord.Utils.between(11, 1, 10)
      false
  """
  @spec between(a, a, a, ord_t()) :: boolean()
        when a: any
  def between(value, min, max, ord \\ Ord) do
    compare(value, min, ord) != :lt && compare(value, max, ord) != :gt
  end

  @doc """
  Compares two values and returns `:lt`, `:eq`, or `:gt`, with an optional custom `Ord`.

  ## Examples

      iex> Funx.Ord.Utils.compare(3, 5)
      :lt

      iex> Funx.Ord.Utils.compare(7, 7)
      :eq

      iex> Funx.Ord.Utils.compare(9, 4)
      :gt
  """
  @spec compare(a, a, ord_t()) :: :lt | :eq | :gt
        when a: any
  def compare(a, b, ord \\ Ord) do
    ord = to_ord_map(ord)

    cond do
      ord.lt?.(a, b) -> :lt
      ord.gt?.(a, b) -> :gt
      true -> :eq
    end
  end

  @doc """
  Reverses the ordering logic.

  ## Examples

      iex> ord = Funx.Ord.Utils.reverse(Funx.Ord.Any)
      iex> ord.lt?.(10, 5)
      true
  """
  @spec reverse(ord_t()) :: ord_map()
  def reverse(ord \\ Ord) do
    ord = to_ord_map(ord)

    %{
      lt?: ord.gt?,
      le?: ord.ge?,
      gt?: ord.lt?,
      ge?: ord.le?
    }
  end

  @doc """
  Creates a comparator function from the given `Ord` module, returning `true`
  if `a` is less than or equal to `b` according to the module’s ordering.

  Useful for sorting with `Enum.sort/2` or similar functions.

  ## Examples

      iex> comparator = Funx.Ord.Utils.comparator(Funx.Ord.Any)
      iex> Enum.sort([3, 1, 2], comparator)
      [1, 2, 3]
  """
  @spec comparator(ord_t()) :: (any(), any() -> boolean())
  def comparator(ord_module) do
    fn a, b -> compare(a, b, ord_module) != :gt end
  end

  @doc """
  Converts an `Ord` instance into an equality comparator.

  This function creates a map containing two functions:
    - `eq?/2`: Returns `true` if `a` and `b` are considered equal by the given `Ord`.
    - `not_eq?/2`: Returns `true` if `a` and `b` are not considered equal by the given `Ord`.

  ## Examples

      iex> eq = Funx.Ord.Utils.to_eq(Funx.Ord.Any)
      iex> eq.eq?.(5, 5)
      true
  """
  @spec to_eq(ord_t()) :: Funx.Eq.Utils.eq_map()
  def to_eq(ord \\ Ord) do
    %{
      eq?: fn a, b -> compare(a, b, ord) == :eq end,
      not_eq?: fn a, b -> compare(a, b, ord) != :eq end
    }
  end

  @doc """
  Appends two `Ord` instances, combining their comparison logic.

  If the first `Ord` comparator determines an order, that result is used.
  If not, the second comparator is used as a fallback.

  ## Examples

      iex> ord1 = Funx.Ord.Utils.contramap(& &1.age, Funx.Ord.Any)
      iex> ord2 = Funx.Ord.Utils.contramap(& &1.name, Funx.Ord.Any)
      iex> combined = Funx.Ord.Utils.append(ord1, ord2)
      iex> combined.lt?.(%{age: 30, name: "Alice"}, %{age: 30, name: "Bob"})
      true
  """
  @spec append(Funx.Monoid.Ord.t(), Funx.Monoid.Ord.t()) :: Funx.Monoid.Ord.t()
  def append(a, b) do
    m_append(%Funx.Monoid.Ord{}, a, b)
  end

  @doc """
  Concatenates a list of `Ord` instances into a single composite comparator.

  This function reduces a list of `Ord` comparators into a single `Ord`,
  applying them in sequence until an order is determined.

  ## Examples

      iex> ord_list = [
      ...>   Funx.Ord.Utils.contramap(& &1.age, Funx.Ord.Any),
      ...>   Funx.Ord.Utils.contramap(& &1.name, Funx.Ord.Any)
      ...> ]
      iex> combined = Funx.Ord.Utils.concat(ord_list)
      iex> combined.gt?.(%{age: 25, name: "Charlie"}, %{age: 25, name: "Bob"})
      true
  """
  @spec concat([Funx.Monoid.Ord.t()]) :: Funx.Monoid.Ord.t()
  def concat(ord_list) when is_list(ord_list) do
    m_concat(%Funx.Monoid.Ord{}, ord_list)
  end

  def to_ord_map(%{lt?: lt_fun, le?: le_fun, gt?: gt_fun, ge?: ge_fun} = ord_map)
      when is_function(lt_fun, 2) and
             is_function(le_fun, 2) and
             is_function(gt_fun, 2) and
             is_function(ge_fun, 2),
      do: ord_map

  def to_ord_map(module) when is_atom(module) do
    %{
      lt?: &module.lt?/2,
      le?: &module.le?/2,
      gt?: &module.gt?/2,
      ge?: &module.ge?/2
    }
  end
end
