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

  @type eq_map() :: %{
          eq?: (any(), any() -> boolean()),
          not_eq?: (any(), any() -> boolean())
        }

  @type eq_t() :: Funx.Eq.t() | eq_map()

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

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

  The `eq` parameter can be an `Eq` module or a custom comparator map with an `:eq?` function.
  If an `Eq` module is provided, it wraps the module’s function to apply `f` to each value before invoking the equality check.
  If a custom comparator map is provided, it wraps the function in the map to apply `f` to each value.

  ## Examples

      iex> eq = Funx.Eq.Utils.contramap(& &1.age)
      iex> eq.eq?.(%{age: 30}, %{age: 30})
      true
      iex> eq.eq?.(%{age: 30}, %{age: 25})
      false
  """
  @spec contramap((a -> b), eq_t()) :: eq_map()
        when a: any, b: any
  def contramap(f, eq \\ Eq) do
    eq = to_eq_map(eq)

    %{
      eq?: fn a, b -> eq.eq?.(f.(a), f.(b)) end,
      not_eq?: fn a, b -> eq.not_eq?.(f.(a), f.(b)) end
    }
  end

  @doc """
  Checks equality of values by applying a projection function, using a specified or default `Eq`.

  The `eq` parameter can be an `Eq` module or a custom comparator map with an `:eq?` function.

  ## Examples

      iex> Funx.Eq.Utils.eq_by?(& &1.age, %{age: 30}, %{age: 30})
      true
      iex> Funx.Eq.Utils.eq_by?(& &1.age, %{age: 30}, %{age: 25})
      false
  """
  @spec eq_by?((a -> b), a, a, eq_t()) :: boolean()
        when a: any, b: any
  def eq_by?(f, a, b, eq \\ Eq) do
    eq = to_eq_map(eq)
    eq.eq?.(f.(a), f.(b))
  end

  @doc """
  Returns true if two values are equal, using a specified or default `Eq`.

  ## Examples

      iex> Funx.Eq.Utils.eq?(42, 42)
      true
      iex> Funx.Eq.Utils.eq?("foo", "bar")
      false
  """
  @spec eq?(a, a, eq_t()) :: boolean()
        when a: any
  def eq?(a, b, eq \\ Eq) do
    eq = to_eq_map(eq)
    eq.eq?.(a, b)
  end

  @doc """
  Returns false if two values are not equal, using a specified or default `Eq`.

  ## Examples

      iex> Funx.Eq.Utils.not_eq?(42, 99)
      true
      iex> Funx.Eq.Utils.not_eq?("foo", "foo")
      false
  """
  @spec not_eq?(a, a, eq_t()) :: boolean()
        when a: any
  def not_eq?(a, b, eq \\ Eq) do
    eq = to_eq_map(eq)
    eq.not_eq?.(a, b)
  end

  @doc """
  Combines two equality comparators using the `Eq.All` monoid.

  This function merges two equality comparisons, requiring **both** to return `true`
  for the final result to be considered equal. This enforces a **strict** equality rule,
  where all comparators must agree.

  ## Examples

      iex> eq1 = Funx.Eq.Utils.contramap(& &1.name)
      iex> eq2 = Funx.Eq.Utils.contramap(& &1.age)
      iex> combined = Funx.Eq.Utils.append_all(eq1, eq2)
      iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 30}, combined)
      true
      iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
      false
  """
  @spec append_all(Monoid.Eq.All.t(), Monoid.Eq.All.t()) :: Monoid.Eq.All.t()
  def append_all(a, b) do
    m_append(%Monoid.Eq.All{}, a, b)
  end

  @doc """
  Combines two equality comparators using the `Eq.Any` monoid.

  This function merges two equality comparisons, where **at least one**
  must return `true` for the final result to be considered equal.

  ## Examples

      iex> eq1 = Funx.Eq.Utils.contramap(& &1.name)
      iex> eq2 = Funx.Eq.Utils.contramap(& &1.age)
      iex> combined = Funx.Eq.Utils.append_any(eq1, eq2)
      iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
      true
      iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, combined)
      false
  """
  @spec append_any(Monoid.Eq.Any.t(), Monoid.Eq.Any.t()) :: Monoid.Eq.Any.t()
  def append_any(a, b) do
    m_append(%Monoid.Eq.Any{}, a, b)
  end

  @doc """
  Concatenates a list of equality comparators using the `Eq.All` monoid.

  The resulting comparator requires **all** comparators in the list to agree
  that two values are equal.

  ## Examples

      iex> eq1 = Funx.Eq.Utils.contramap(& &1.name)
      iex> eq2 = Funx.Eq.Utils.contramap(& &1.age)
      iex> combined = Funx.Eq.Utils.concat_all([eq1, eq2])
      iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 30}, combined)
      true
      iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
      false
  """
  @spec concat_all([Monoid.Eq.All.t()]) :: Monoid.Eq.All.t()
  def concat_all(eq_list) when is_list(eq_list) do
    m_concat(%Monoid.Eq.All{}, eq_list)
  end

  @doc """
  Concatenates a list of equality comparators using the `Eq.Any` monoid.

  The resulting comparator allows **any** comparator in the list to determine
  equality, making it more permissive.

  ## Examples

      iex> eq1 = Funx.Eq.Utils.contramap(& &1.name)
      iex> eq2 = Funx.Eq.Utils.contramap(& &1.age)
      iex> combined = Funx.Eq.Utils.concat_any([eq1, eq2])
      iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Alice", age: 25}, combined)
      true
      iex> Funx.Eq.Utils.eq?(%{name: "Alice", age: 30}, %{name: "Bob", age: 25}, combined)
      false
  """
  @spec concat_any([Monoid.Eq.Any.t()]) :: Monoid.Eq.Any.t()
  def concat_any(eq_list) when is_list(eq_list) do
    m_concat(%Monoid.Eq.Any{}, eq_list)
  end

  @doc """
  Converts an `Eq` comparator into a single-argument predicate function for use in `Enum` functions.

  The resulting predicate takes a single element and returns `true` if it matches the `target`
  based on the specified `Eq`. If no custom `Eq` is provided, it defaults to `Funx.Eq`.

  ## Examples

      iex> eq = Funx.Eq.Utils.contramap(& &1.name)
      iex> predicate = Funx.Eq.Utils.to_predicate(%{name: "Alice"}, eq)
      iex> Funx.Filterable.filter([%{name: "Alice"}, %{name: "Bob"}], predicate)
      [%{name: "Alice"}]
  """
  @spec to_predicate(a, eq_t()) :: (a -> boolean())
        when a: any
  def to_predicate(target, eq \\ Eq) do
    eq = to_eq_map(eq)

    fn elem -> eq.eq?.(elem, target) end
  end

  def to_eq_map(%{eq?: eq_fun, not_eq?: not_eq_fun} = eq_map)
      when is_function(eq_fun, 2) and is_function(not_eq_fun, 2) do
    eq_map
  end

  def to_eq_map(module) when is_atom(module) do
    %{
      eq?: &module.eq?/2,
      not_eq?: &module.not_eq?/2
    }
  end
end
