defmodule Funx.Monad.Maybe.Just do
  @moduledoc """
  Represents the `Just` variant of the `Maybe` monad, used to model the presence of a value.

  A `Just` wraps a single value and participates in functional composition by propagating the contained value through monadic operations.

  This module implements the following protocols:

    - `Funx.Monad`: Implements `bind/2`, `map/2`, and `ap/2` for monadic composition.
    - `Funx.Foldable`: Provides `fold_l/3` and `fold_r/3` to fold over the wrapped value.
    - `Funx.Filterable`: Supports filtering with `filter/2`, `filter_map/2`, and `guard/2`.
    - `Funx.Eq`: Enables equality checks between `Just` and other `Maybe` values.
    - `Funx.Ord`: Defines ordering behavior between `Just` and `Nothing`.

  These protocol implementations allow `Just` to participate in structured computation, validation, filtering, and comparison within the `Maybe` context.
  """

  @enforce_keys [:value]
  defstruct [:value]

  @type t(value) :: %__MODULE__{value: value}

  @doc """
  Creates a new `Just` value.

  The `pure/1` function wraps a value in the `Just` monad, representing the presence of the value.

  ## Examples

      iex> Funx.Monad.Maybe.Just.pure(5)
      %Funx.Monad.Maybe.Just{value: 5}

  ### Raises
  - `ArgumentError` if `nil` is provided.

      iex> Funx.Monad.Maybe.Just.pure(nil)
      ** (ArgumentError) Cannot wrap nil in a Just
  """
  @spec pure(value) :: t(value) when value: term()
  def pure(nil), do: raise(ArgumentError, "Cannot wrap nil in a Just")
  def pure(value), do: %__MODULE__{value: value}
end

defimpl String.Chars, for: Funx.Monad.Maybe.Just do
  alias Funx.Monad.Maybe.Just

  def to_string(%Just{value: value}), do: "Just(#{value})"
end

defimpl Funx.Monad, for: Funx.Monad.Maybe.Just do
  alias Funx.Monad.Maybe.{Just, Nothing}

  @spec map(Just.t(value), (value -> result)) :: Just.t(result)
        when value: term(), result: term()
  def map(%Just{value: value}, func), do: Just.pure(func.(value))

  @spec ap(Just.t((value -> result)) | Nothing.t(), Just.t(value) | Nothing.t()) ::
          Just.t(result) | Nothing.t()
        when value: term(), result: term()
  def ap(%Just{value: func}, %Just{value: value}),
    do: Just.pure(func.(value))

  def ap(%Just{}, %Nothing{}), do: %Nothing{}

  @spec bind(Just.t(value), (value -> Just.t(result))) :: Just.t(result)
        when value: term(), result: term()
  def bind(%Just{value: value}, func), do: func.(value)
end

defimpl Funx.Foldable, for: Funx.Monad.Maybe.Just do
  alias Funx.Monad.Maybe.Just

  @spec fold_l(Just.t(value), (value -> result), (-> result)) :: result
        when value: term(), result: term()
  def fold_l(%Just{value: value}, just_func, _nothing_func) do
    just_func.(value)
  end

  @spec fold_r(Just.t(value), (value -> result), (-> result)) :: result
        when value: term(), result: term()
  def fold_r(%Just{} = just, just_func, nothing_func) do
    fold_l(just, just_func, nothing_func)
  end
end

defimpl Funx.Filterable, for: Funx.Monad.Maybe.Just do
  alias Funx.Monad.Maybe
  alias Funx.Monad.Maybe.Just
  alias Funx.Monad

  @spec guard(Funx.Monad.Maybe.Just.t(value), boolean()) :: Funx.Monad.Maybe.t(value)
        when value: var
  def guard(%Just{} = maybe, true), do: maybe
  def guard(%Just{}, false), do: Maybe.nothing()

  @spec filter(Funx.Monad.Maybe.Just.t(value), (value -> boolean())) :: Funx.Monad.Maybe.t(value)
        when value: var
  def filter(%Just{} = maybe, predicate) do
    Monad.bind(maybe, fn value ->
      if predicate.(value) do
        Maybe.pure(value)
      else
        Maybe.nothing()
      end
    end)
  end

  @spec filter_map(Funx.Monad.Maybe.Just.t(value), (value -> Funx.Monad.Maybe.t(result))) ::
          Funx.Monad.Maybe.t(result)
        when value: var, result: var
  def filter_map(%Just{value: value}, func) do
    case func.(value) do
      %Just{} = just -> just
      _ -> Maybe.nothing()
    end
  end
end

defimpl Funx.Eq, for: Funx.Monad.Maybe.Just do
  alias Funx.Monad.Maybe.{Just, Nothing}
  alias Funx.Eq

  def eq?(%Just{value: v1}, %Just{value: v2}), do: Eq.eq?(v1, v2)
  def eq?(%Just{}, %Nothing{}), do: false

  def not_eq?(%Just{value: v1}, %Just{value: v2}), do: not Eq.eq?(v1, v2)
  def not_eq?(%Just{}, %Nothing{}), do: true
end

defimpl Funx.Ord, for: Funx.Monad.Maybe.Just do
  alias Funx.Monad.Maybe.{Just, Nothing}
  alias Funx.Ord

  def lt?(%Just{value: v1}, %Just{value: v2}), do: Ord.lt?(v1, v2)
  def lt?(%Just{}, %Nothing{}), do: false

  def le?(%Just{value: v1}, %Just{value: v2}), do: Ord.le?(v1, v2)
  def le?(%Just{}, %Nothing{}), do: false

  def gt?(%Just{value: v1}, %Just{value: v2}), do: Ord.gt?(v1, v2)
  def gt?(%Just{}, %Nothing{}), do: true

  def ge?(%Just{value: v1}, %Just{value: v2}), do: Ord.ge?(v1, v2)
  def ge?(%Just{}, %Nothing{}), do: true
end

defimpl Funx.Summarizable, for: Funx.Monad.Maybe.Just do
  def summarize(%{value: value}), do: {:maybe_just, Funx.Summarizable.summarize(value)}
end
