defmodule Algae.Maybe do
  @moduledoc ~S"""
  The sum of `Algae.Maybe.Just` and `Algae.Maybe.Nothing`.
  Maybe represents the presence or absence of something.

  Please note that `nil` is actually a value, as it can be passed to functions!
  `nil` is not bottom!

  ## Examples

      iex> [1,2,3]
      ...> |> List.first()
      ...> |> case do
      ...>      nil  -> new()
      ...>      head -> new(head)
      ...>    end
      %Algae.Maybe.Just{just: 1}

      iex> []
      ...> |> List.first()
      ...> |> case do
      ...>      nil  -> new()
      ...>      head -> new(head)
      ...>    end
      %Algae.Maybe.Nothing{}

  """

  import Algae
  alias Algae.Maybe.{Just, Nothing}

  defsum do
    defdata Nothing :: none()
    defdata Just    :: any()
  end

  @doc ~S"""
  Put no value into the `Maybe` context (ie: make it a `Nothing`)

  ## Examples

      iex> new()
      %Algae.Maybe.Nothing{}

  """
  @spec new() :: Nothing.t()
  defdelegate new, to: Nothing, as: :new

  @doc ~S"""
  Put a value into the `Maybe` context (ie: make it a `Just`)

  ## Examples

      iex> new(9)
      %Algae.Maybe.Just{just: 9}

      iex> new(nil)
      %Algae.Maybe.Just{just: nil}

      iex> new(nil, nothing: nil)
      %Algae.Maybe.Nothing{}

      iex> new(9, nothing: 9)
      %Algae.Maybe.Nothing{}

      iex> new(9, nothing: 1)
      %Algae.Maybe.Just{just: 9}

  """
  @spec new(any(), [nothing: any()]) :: Just.t() | Nothing.t()
  def new(nothing_value, [nothing: nothing_value]), do: Nothing.new()
  def new(value, _), do: Just.new(value)

  @spec new(any()) :: Just.t()
  def new(value), do: Just.new(value)

  @doc """
  Alias for `new(value, nothing: nil)`.

  ## Examples

      iex> from_nillable(9)
      %Algae.Maybe.Just{just: 9}

      iex> from_nillable(nil)
      %Algae.Maybe.Nothing{}

  """
  @spec from_nillable(any()) :: Just.t()
  def from_nillable(value), do: new(value, nothing: nil)

  @doc """
  Extract a value from a `Maybe`, falling back to a set value in the `Nothing` case.

  ## Examples

      iex> from_maybe(%Algae.Maybe.Nothing{}, else: 42)
      42

      iex> %Algae.Maybe.Just{just: 1955} |> from_maybe(else: 42)
      1955

  """
  @spec from_maybe(t(), any()) :: any()
  def from_maybe(%Nothing{}, [else: fallback]), do: fallback
  def from_maybe(%Just{just: inner}, _), do: inner
end

alias Algae.Maybe.{Just, Nothing}
import TypeClass
use Witchcraft

#############
# Generator #
#############

defimpl TypeClass.Property.Generator, for: Algae.Maybe.Nothing do
  def generate(_), do: Nothing.new()
end

defimpl TypeClass.Property.Generator, for: Algae.Maybe.Just do
  def generate(_) do
    [1, 1.1, "", []]
    |> Enum.random()
    |> TypeClass.Property.Generator.generate()
    |> Just.new()
  end
end

##########
# Setoid #
##########

definst Witchcraft.Setoid, for: Algae.Maybe.Nothing do
  def equivalent?(_, %Nothing{}), do: true
  def equivalent?(_, %Just{}),    do: false
end

definst Witchcraft.Setoid, for: Algae.Maybe.Just do
  def equivalent?(%Just{just: a}, %Just{just: b}), do: Witchcraft.Setoid.equivalent?(a, b)
  def equivalent?(%Just{}, %Nothing{}), do: false
end

#######
# Ord #
#######

definst Witchcraft.Ord, for: Algae.Maybe.Nothing do
  def compare(_, %Nothing{}), do: :equal
  def compare(_, %Just{}),    do: :lesser
end

definst Witchcraft.Ord, for: Algae.Maybe.Just do
  custom_generator(_) do
    1
    |> TypeClass.Property.Generator.generate()
    |> Just.new()
  end

  def compare(%Just{just: a}, %Just{just: b}), do: Witchcraft.Ord.compare(a, b)
  def compare(%Just{}, %Nothing{}), do: :greater
end

#############
# Semigroup #
#############

definst Witchcraft.Semigroup, for: Algae.Maybe.Nothing do
  def append(_, right), do: right
end

definst Witchcraft.Semigroup, for: Algae.Maybe.Just do
  custom_generator(_) do
    1
    |> TypeClass.Property.Generator.generate()
    |> Just.new()
  end

  def append(%Just{just: a}, %Just{just: b}), do: %Just{just: a <> b}
  def append(just, %Nothing{}), do: just
end

##########
# Monoid #
##########

definst Witchcraft.Monoid, for: Algae.Maybe.Nothing do
  def empty(nothing), do: nothing
end

definst Witchcraft.Monoid, for: Algae.Maybe.Just do
  def empty(_), do: %Algae.Maybe.Nothing{}
end

###########
# Functor #
###########

definst Witchcraft.Functor, for: Algae.Maybe.Nothing do
  def map(_, _), do: %Algae.Maybe.Nothing{}
end

definst Witchcraft.Functor, for: Algae.Maybe.Just do
  def map(%{just: data}, fun), do: data |> fun.() |> Algae.Maybe.Just.new()
end

############
# Foldable #
############

definst Witchcraft.Foldable, for: Algae.Maybe.Nothing do
  def right_fold(_, seed, _), do: seed
end

definst Witchcraft.Foldable, for: Algae.Maybe.Just do
  def right_fold(%Just{just: inner}, seed, fun), do: fun.(inner, seed)
end

###############
# Traversable #
###############

# Not traversable because we don't have enough type information for Nothing

#########
# Apply #
#########

definst Witchcraft.Apply, for: Algae.Maybe.Nothing do
  def convey(_, _), do: %Algae.Maybe.Nothing{}
end

definst Witchcraft.Apply, for: Algae.Maybe.Just do
  alias Algae.Maybe.{Just, Nothing}

  def convey(data, %Nothing{}), do: %Nothing{}
  def convey(data, %Just{just: fun}), do: map(data, fun)
end

###############
# Applicative #
###############

definst Witchcraft.Applicative, for: Algae.Maybe.Nothing do
  def of(_, data), do: Just.new(data)
end

definst Witchcraft.Applicative, for: Algae.Maybe.Just do
  def of(_, data), do: Just.new(data)
end

#########
# Chain #
#########

definst Witchcraft.Chain, for: Algae.Maybe.Nothing do
  def chain(_, _), do: %Nothing{}
end

definst Witchcraft.Chain, for: Algae.Maybe.Just do
  def chain(%{just: data}, link), do: link.(data)
end

#########
# Monad #
#########

definst Witchcraft.Monad, for: Algae.Maybe.Nothing
definst Witchcraft.Monad, for: Algae.Maybe.Just

##########
# Extend #
##########

definst Witchcraft.Extend, for: Algae.Maybe.Nothing do
  def nest(_), do: %Nothing{}
end

definst Witchcraft.Extend, for: Algae.Maybe.Just do
  def nest(inner), do: Just.new(inner)
end
