defmodule CubDB.Btree.Enumerable do
  @moduledoc false

  # `CubDB.Btree.Enumerable` is a module providing a generic `reduce/4` function
  # used to implement the `Enumerable` protocol on various modules
  # (`CubDB.Btree`, `CubDB.Btree.Diff`, `CubDB.Btree.KeyRange`). Different
  # implementations differ only by the `get_children` function argument of
  # `reduce/4`.

  alias CubDB.Btree
  alias CubDB.Store

  import Btree, only: [leaf: 1, branch: 1, value: 1, deleted: 0]

  @spec reduce(
          Btree.t(),
          Enumerable.acc(),
          Enumerable.reducer(),
          (Btree.btree_node(), Store.t() -> any)
        ) :: Enumerable.result()

  # `reduce/4` is used by several modules to implement the function `reduce/3`
  # of the `Enumerable` protocol. The various implementations only differ by the
  # `get_children` function.
  # The `get_children` function takes a Btree node of any kind (branch, leaf,
  # value, or deleted) and should return the children of that node as relevant
  # for the particular enumerable being implemented. Filtering, sorting, and
  # transformations can all be done within `get_children`.
  def reduce(%Btree{root: root, store: store}, cmd_acc, fun, get_children) do
    do_reduce({[], [[{nil, root}]]}, cmd_acc, fun, get_children, store)
  end

  defp do_reduce(_, {:halt, acc}, _, _, _), do: {:halted, acc}

  defp do_reduce(t, {:suspend, acc}, fun, get_children, store) do
    {:suspended, acc, &do_reduce(t, &1, fun, get_children, store)}
  end

  defp do_reduce(t, {:cont, acc}, fun, get_children, store) do
    case next(t, store, get_children) do
      {t, item} -> do_reduce(t, fun.(item, acc), fun, get_children, store)
      :done -> {:done, acc}
    end
  end

  defp next({[], [[] | todo]}, store, get_children) do
    case todo do
      [] -> :done
      _ -> next({[], todo}, store, get_children)
    end
  end

  defp next({[], [[{_, leaf = leaf(children: _)} | rest] | todo]}, store, get_children) do
    children = get_children.(leaf, store)

    next({children, [rest | todo]}, store, get_children)
  end

  defp next({[], [[{_, branch = branch(children: _)} | rest] | todo]}, store, get_children) do
    children = get_children.(branch, store)

    next({[], [children | [rest | todo]]}, store, get_children)
  end

  defp next({[{k, value = value(val: _)} | rest], todo}, store, get_children) do
    {{rest, todo}, {k, get_children.(value, store)}}
  end

  defp next({[{k, deleted()} | rest], todo}, store, get_children) do
    {{rest, todo}, {k, get_children.(deleted(), store)}}
  end
end
