defmodule ExRoseTree.Zipper.Location do
  @moduledoc """
  A `Location` in the `path` from the root of the `ExRoseTree.Zipper` to its
  current context.

  require ExRoseTree

  defstruct ~w(prev term next)a

  @typedoc """
  A `Location` is made up of the `term` of an `ExRoseTree` with lists of `prev` and `next` siblings.

  * `term` is an `ExRoseTree` `term`.
  * `prev` is a list of `ExRoseTree`s. They are the siblings that
      occur prior the `term`. It is reversed such that the
      head of the list is the nearest previous sibling.
  * `next` is a list of `ExRoseTree`s. They are the siblings that
      occur after the `term`.
  @type t :: %__MODULE__{
          prev: [ExRoseTree.t()],
          term: term(),
          next: [ExRoseTree.t()]

  @doc section: :guards
  defguard location?(value)
           when is_struct(value) and
                  value.__struct__ == __MODULE__ and
                  is_list(value.prev) and

  @doc """
  Builds a new `Location` given a `term()` or an `ExRoseTree` as the first
  argument, and optional `:prev` and `:next` keywords of lists of `ExRoseTree`s.

  If the first argument is an `ExRoseTree`, it will unwrap its `term` element.

  ## Examples

      iex>, prev: [], next: [])
      %ExRoseTree.Zipper.Location{prev: [], term: 5, next: []}

      iex> tree =
      ...>, prev: [tree], next: [])
        prev: [
          %ExRoseTree{term: 4, children: []}
        term: 5,
        next: []

  @spec new(ExRoseTree.t() | term(), keyword()) :: t() | nil
  def new(item, opts \\ [])

  def new(item, opts) when ExRoseTree.rose_tree?(item) do
    new(item.term, opts)

  def new(item, opts) do
    prev = Keyword.get(opts, :prev, [])
    next = Keyword.get(opts, :next, [])

    do_new(item, prev, next)

  @doc false
  @spec do_new(ExRoseTree.t() | term(), [ExRoseTree.t()], [ExRoseTree.t()]) :: t() | nil
  defp do_new(item, prev, next) when is_list(prev) and is_list(next) do
    case {ExRoseTree.all_rose_trees?(prev), ExRoseTree.all_rose_trees?(next)} do
      {true, true} ->
          prev: prev,
          term: item,
          next: next

      {true, false} ->
        raise ArgumentError, "invalid element in prev"

      {false, true} ->
        raise ArgumentError, "invalid element in next"

  @doc """
  Returns whether a list of values are all `Location`s or not. Will return
  `true` if passed an empty list.

  ## Examples

      iex> locs = for loc <- [5,4,3,2,1], do:
      ...> ExRoseTree.Zipper.Location.all_locations?(locs)

  @spec all_locations?([t()]) :: boolean()
  def all_locations?(values) when is_list(values) do
    Enum.all?(values, &location?(&1))

  @doc """
  Applies the given function to the `Location`'s `term` field.

  ## Examples

      iex> loc =, prev: [], next: [])
      ...> ExRoseTree.Zipper.Location.map_term(loc, &(&1*2))
      %ExRoseTree.Zipper.Location{prev: [], term: 10, next: []}

  @spec map_term(t(), (term() -> term())) :: t()
  def map_term(%__MODULE__{term: term} = location, map_fn) when is_function(map_fn) do
    %{location | term: map_fn.(term)}

  @doc """
  Returns the index of the `Location` in relation to its siblings.

  ## Examples

      iex> trees = for t <- [5,4,3,2,1], do:
      ...> loc =, prev: trees, next: [])
      ...> ExRoseTree.Zipper.Location.index_of_term(loc)

  @spec index_of_term(t()) :: non_neg_integer()
  def index_of_term(%__MODULE__{prev: prev}),
    do: Enum.count(prev)