defmodule Algae.Tree.BinarySearch do
  @moduledoc """
  Represent a `BinarySearch` tree.

  ## Examples

      iex> alias Algae.Tree.BinarySearch, as: BSTree
      ...>
      ...> BSTree.Node.new(
      ...>   42,
      ...>   BSTree.Node.new(77),
      ...>   BSTree.Node.new(
      ...>     1234,
      ...>     BSTree.Node.new(98),
      ...>     BSTree.Node.new(32)
      ...>   )
      ...> )
      %Algae.Tree.BinarySearch.Node{
        node: 42,
        left: %Algae.Tree.BinarySearch.Node{
          node:  77,
          left:  %Algae.Tree.BinarySearch.Empty{},
          right: %Algae.Tree.BinarySearch.Empty{}
        },
        right: %Algae.Tree.BinarySearch.Node{
          node:  1234,
          left:  %Algae.Tree.BinarySearch.Node{
            node:  98,
            left:  %Algae.Tree.BinarySearch.Empty{},
            right: %Algae.Tree.BinarySearch.Empty{}
          },
          right: %Algae.Tree.BinarySearch.Node{
            node:  32,
            left:  %Algae.Tree.BinarySearch.Empty{},
            right: %Algae.Tree.BinarySearch.Empty{}
          }
        }
      }

  """

  alias __MODULE__
  alias BinarySearch.{Empty, Node}

  import Algae
  use Witchcraft, except: [to_list: 1]

  defsum do
    defdata(Empty :: none())

    defdata Node do
      node :: any()
      left :: BinarySearch.t() \\ BinarySearch.Empty.new()
      right :: BinarySearch.t() \\ BinarySearch.Empty.new()
    end
  end

  @doc """
  Create an empty tree.

  ## Examples

      iex> new()
      %Algae.Tree.BinarySearch.Empty{}

  """
  @spec new() :: Empty.t()
  def new, do: %Empty{}

  @doc """
  Bring a value into an otherwise empty tree.

  ## Examples

      iex> new(42)
      %Algae.Tree.BinarySearch.Node{
        node:  42,
        left:  %Algae.Tree.BinarySearch.Empty{},
        right: %Algae.Tree.BinarySearch.Empty{}
      }

  """
  @spec new(any()) :: Node.t()
  def new(value), do: %Node{node: value}

  @doc """
  Insert a new element into a tree.

  ## Examples

      iex> insert(new(42), 43)
      %Algae.Tree.BinarySearch.Node{
        node: 42,
        right: %Algae.Tree.BinarySearch.Node{
          node: 43
        }
      }

  """
  @spec insert(t(), any()) :: t()
  def insert(%Empty{}, value), do: new(value)

  def insert(tree = %Node{node: node, left: left, right: right}, orderable) do
    case compare(orderable, node) do
      :equal -> tree
      :greater -> %{tree | right: insert(right, orderable)}
      :lesser -> %{tree | left: insert(left, orderable)}
    end
  end

  def insert(%Empty{}, value), do: new(value)

  def insert(tree = %Node{node: node, left: left, right: right}, orderable) do
    case compare(orderable, node) do
      :equal -> tree
      :greater -> %{tree | right: insert(right, orderable)}
      :lesser -> %{tree | left: insert(left, orderable)}
    end
  end

  @doc """
  Remove an element from a tree by value.

  ## Examples

      iex> alias Algae.Tree.BinarySearch, as: BSTree
      ...>
      ...> BSTree.Node.new(
      ...>   42,
      ...>   BSTree.Node.new(77),
      ...>   BSTree.Node.new(
      ...>     1234,
      ...>     BSTree.Node.new(98),
      ...>     BSTree.Node.new(32)
      ...>   )
      ...> ) |> delete(98)
      %Algae.Tree.BinarySearch.Node{
        node: 42,
        left: %Algae.Tree.BinarySearch.Node{
          node: 77
        },
        right: %Algae.Tree.BinarySearch.Node{
          node: 1234,
          right: %Algae.Tree.BinarySearch.Node{
            node: 32
          }
        }
      }

  """
  @spec delete(t(), any()) :: t()
  def delete(%Empty{}, _), do: %Empty{}

  def delete(tree = %Node{node: node, left: left, right: right}, orderable) do
    case compare(orderable, node) do
      :greater ->
        %{tree | right: delete(right, orderable)}

      :lesser ->
        %{tree | left: delete(left, orderable)}

      :equal ->
        case tree do
          %{left: %Empty{}} -> right
          %{right: %Empty{}} -> left
          %{right: %{node: shift}} -> %{tree | node: shift, right: delete(right, shift)}
        end
    end
  end

  @doc """
  Flatten a tree into a list.

  ## Examples

      iex> alias Algae.Tree.BinarySearch, as: BSTree
      ...>
      ...> BSTree.Node.new(
      ...>   42,
      ...>   BSTree.Node.new(77),
      ...>   BSTree.Node.new(
      ...>     1234,
      ...>     BSTree.Node.new(98),
      ...>     BSTree.Node.new(32)
      ...>   )
      ...> )
      ...> |> BSTree.to_list()
      [42, 77, 1234, 98, 32]

  """
  @spec to_list(t()) :: list()
  def to_list(tree), do: Witchcraft.Foldable.to_list(tree)

  @doc """
  Flatten a tree into a list with elements sorted.

  ## Examples

      iex> alias Algae.Tree.BinarySearch, as: BSTree
      ...>
      ...> BSTree.Node.new(
      ...>   42,
      ...>   BSTree.Node.new(77),
      ...>   BSTree.Node.new(
      ...>     1234,
      ...>     BSTree.Node.new(98),
      ...>     BSTree.Node.new(32)
      ...>   )
      ...> )
      ...> |> BSTree.to_ordered_list()
      [32, 42, 77, 98, 1234]

  """
  @spec to_ordered_list(t()) :: list()
  def to_ordered_list(tree), do: tree |> to_list() |> Enum.sort()

  @doc """
  Build a `BinarySearch` tree from a list.

  ## Examples

      iex> Algae.Tree.BinarySearch.from_list([42, 77, 1234, 98, 32])
      %Algae.Tree.BinarySearch.Node{
        node: 42,
        left: %Algae.Tree.BinarySearch.Node{
          node:  32
        },
        right: %Algae.Tree.BinarySearch.Node{
          node: 77,
          right: %Algae.Tree.BinarySearch.Node{
            node: 1234,
            left: %Algae.Tree.BinarySearch.Node{
              node:  98
            }
          }
        }
      }

  """
  @spec from_list(list()) :: t()
  def from_list([]), do: %Empty{}
  def from_list([head | tail]), do: from_list(tail, new(head))

  @doc """
  Build a `BinarySearch` tree from a list and attach to an existing tree.

  ## Examples

      iex> Algae.Tree.BinarySearch.from_list([42, 77, 1234, 98, 32], new(-9))
      %Algae.Tree.BinarySearch.Node{
        node:  -9,
        right: %Algae.Tree.BinarySearch.Node{
          left: %Algae.Tree.BinarySearch.Node{
            node:  32
          },
          node: 42,
          right: %Algae.Tree.BinarySearch.Node{
            node: 77,
            right: %Algae.Tree.BinarySearch.Node{
              node: 1234,
              left: %Algae.Tree.BinarySearch.Node{
                node: 98
              },
              right: %Algae.Tree.BinarySearch.Empty{}
            }
          }
        }
      }

  """
  @spec from_list(list(), t()) :: t()
  def from_list([], seed), do: seed
  def from_list([head | tail], seed), do: from_list(tail, insert(seed, head))
end
