lib/dmp/cursor.ex

defmodule Dmp.Cursor do
  @moduledoc """
  A container for Elixir lists, that can be used to iterate forward
  and backward, with a focused "current" item in the list, and "prev" and "next"
  lists of items that come before and after the current item.

  In Elm this has been described as a "Zipper".
  """

  defstruct prev: [], current: nil, next: []

  alias __MODULE__

  @type t() :: %Cursor{
          prev: list(),
          current: term(),
          next: list()
        }

  @typedoc """
  A value used to set the Cursor position.

    * `-1` means before the first item in the container.
    * `0` means at the first item.
    * `:last` means at the last item.
    * `:tail` means after the last item.
  """
  @type position_value() :: -1 | non_neg_integer() | :last | :tail

  @type init_option() :: {:position, position_value()}

  @typedoc """
  Use `position: 0` for example, to set the initial
  position of the Cursor to the first item.
  """
  @type init_options() :: list(init_option())

  @doc """
  Create a Cursor containing no items.

  ## Examples

      iex> Cursor.new()
      %Cursor{current: nil, next: [], prev: []}

  """

  @spec new() :: t()
  def new(), do: %Cursor{prev: [], current: nil, next: []}

  @doc """
  Create a Cursor from a list of items.

    * `opts` - The keyword `:position` can specify a position value,
      such as `-1`, `0`, `1`, `:last`, or `tail` in order to set the
      position of the created Cursor.

  ## Examples

      iex> Cursor.from_list([1, 2, 3])
      %Cursor{current: nil, next: [1, 2, 3], prev: []}

      iex> Cursor.from_list([1, 2, 3]) |> Cursor.move_forward()
      %Cursor{current: 1, next: [2, 3], prev: []}

      iex> Cursor.from_list([1, 2, 3], position: 1)
      %Cursor{current: 2, next: [3], prev: [1]}

  """
  @spec from_list(list(), init_options()) :: t()
  def from_list(items, opts \\ []) when is_list(items) do
    %Cursor{prev: [], current: nil, next: items} |> with_init_options(opts)
  end

  @doc """
  Create a Cursor from two lists.

    * `opts` - the keyword `:position` can specify a position value,
      such as `-1`, `0`, `1`, `:last`, or `tail` in order to set the
      position of the created Cursor.

  ## Examples

      iex> Cursor.from_split([1, 2, 3], [4, 5, 6])
      %Cursor{current: 4, next: [5, 6], prev: [3, 2, 1]}

  """
  @spec from_split(list(), list(), init_options()) :: t()
  def from_split(prev, next, opts \\ [])

  def from_split([], next, opts) when is_list(next) do
    %Cursor{prev: [], current: nil, next: next} |> with_init_options(opts)
  end

  def from_split(prev, [], opts) when is_list(prev) do
    %Cursor{prev: Enum.reverse(prev), current: nil, next: []} |> with_init_options(opts)
  end

  def from_split(prev, [h | t], opts) when is_list(prev) do
    %Cursor{prev: Enum.reverse(prev), current: h, next: t} |> with_init_options(opts)
  end

  @spec with_init_options(t(), init_options()) :: t()
  defp with_init_options(%Cursor{} = c, opts) do
    case Keyword.get(opts, :position, nil) do
      pos when is_integer(pos) -> move_to(c, pos)
      _ -> c
    end
  end

  @doc """
  Extract the list from a Cursor.

  ## Examples

      iex> Cursor.from_list([1, 2, 3]) |> Cursor.to_list()
      [1, 2, 3]

  """
  @spec to_list(t()) :: list()
  def to_list(%Cursor{prev: prev, current: nil, next: next}) do
    Enum.reverse(prev) ++ next
  end

  def to_list(%Cursor{prev: prev, current: current, next: next}) do
    Enum.reverse(prev) ++ [current | next]
  end

  @doc """
  Returns `true` if there are no items in the Cursor.

  ## Examples

      iex> Cursor.from_list([]) |> Cursor.empty?()
      true

      iex> Cursor.from_list([1, 2, 3]) |> Cursor.empty?()
      false

  """
  @spec empty?(t()) :: boolean()
  def empty?(%Cursor{prev: prev, current: nil, next: next}),
    do: Enum.empty?(prev) && Enum.empty?(next)

  def empty?(%Cursor{}), do: false

  @doc """
  Returns `true` if the Cursor is positioned before the first item.

  ## Examples

      iex> Cursor.from_list([1, 2, 3]) |> Cursor.before_first?()
      true

      iex> Cursor.from_list([1, 2, 3]) |> Cursor.move_forward() |> Cursor.before_first?()
      false

  """
  @spec before_first?(t()) :: boolean()
  def before_first?(%Cursor{prev: [], current: nil}), do: true
  def before_first?(%Cursor{}), do: false

  @doc """
  Returns `false` if the Cursor is positioned at or before the first item.

  ## Examples

      iex> Cursor.from_list([1, 2]) |> Cursor.has_previous?()
      false

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward() |> Cursor.has_previous?()
      false

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.has_previous?()
      true

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.has_previous?()
      true

  """
  @spec has_previous?(t()) :: boolean()
  def has_previous?(%Cursor{prev: []}), do: false
  def has_previous?(%Cursor{}), do: true

  @doc """
  Returns `true` if the Cursor is positioned after the last item.

  ## Examples

      iex> Cursor.from_list([1, 2]) |> Cursor.after_last?()
      false

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.after_last?()
      false

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.after_last?()
      true

  """
  @spec after_last?(t()) :: boolean()
  def after_last?(%Cursor{current: nil, next: []}), do: true
  def after_last?(%Cursor{}), do: false

  @doc """
  Returns `false` if the Cursor is positioned at or after the last item.

  ## Examples

      iex> Cursor.from_list([1, 2]) |> Cursor.has_next?()
      true

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.has_next?()
      false

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.has_next?()
      false

  """
  @spec has_next?(t()) :: boolean()
  def has_next?(%Cursor{next: []}), do: false
  def has_next?(%Cursor{}), do: true

  @doc """
  Returns the total number of items in the Cursor.

  ## Examples

      iex> Cursor.from_list([1, 2, 3]) |> Cursor.count()
      3

  """
  @spec count(t()) :: non_neg_integer()
  def count(%Cursor{prev: prev, current: nil, next: next}),
    do: Enum.count(prev) + Enum.count(next)

  def count(%Cursor{prev: prev, next: next}), do: Enum.count(prev) + Enum.count(next) + 1

  @doc """
  Returns the current position of the Cursor.

  A return value of `-1` means the Cursor is positioned before the first item.
  A return value of `Cursor.count(c)` means the Cursor is positioned after the last item.

  ## Examples

      iex> Cursor.from_list([1, 2]) |> Cursor.position()
      -1

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward() |> Cursor.position()
      0

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.position()
      1

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.position()
      2

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(4) |> Cursor.position()
      2

  """
  @spec position(t()) :: integer()
  def position(%Cursor{prev: [], current: nil}), do: -1
  def position(%Cursor{prev: prev}), do: Enum.count(prev)

  @doc """
  Changes the current position of the Cursor.

    * `pos` - The desired position.

    * `-1` means the Cursor is positioned before the first item.
    * `0` means the Cursor is positioned at the first item (if it is not empty).
    * `:last` means the Cursor is positioned at the last item (if it is not empty).
    * `:tail` means the Cursor is positioned after the last item.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(-1)
      %Cursor{current: nil, next: [1, 2, 3, 4, 5], prev: []}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(0)
      %Cursor{current: 1, next: [2, 3, 4, 5], prev: []}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(1)
      %Cursor{current: 2, next: [3, 4, 5], prev: [1]}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(:last)
      %Cursor{current: 5, next: [], prev: [4, 3, 2, 1]}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_to(:tail)
      %Cursor{current: nil, next: [], prev: [5, 4, 3, 2, 1]}

  """
  @spec move_to(t(), position_value()) :: t()
  def move_to(%Cursor{prev: prev, current: current, next: next}, :tail) do
    current_and_next =
      if is_nil(current) do
        prev
      else
        [current | next]
      end

    %Cursor{prev: Enum.reverse(current_and_next) ++ prev, current: nil, next: []}
  end

  def move_to(%Cursor{} = c, :last), do: c |> move_to(:tail) |> move_back(1)

  def move_to(%Cursor{prev: prev, current: current, next: next}, -1) do
    current_and_next =
      if is_nil(current) do
        next
      else
        [current | next]
      end

    %Cursor{prev: [], current: nil, next: Enum.reverse(prev) ++ current_and_next}
  end

  def move_to(%Cursor{} = c, pos) when is_integer(pos) and pos >= 0 do
    c |> move_to(-1) |> move_forward(pos + 1)
  end

  @doc """
  Resets the position of the Cursor to before the first item.
  An alias for `Cursor.move_to(c, -1)`.

  ## Examples

      iex> Cursor.new() |> Cursor.reset()
      %Cursor{current: nil, next: [], prev: []}

      iex> Cursor.from_list([1]) |> Cursor.move_forward() |> Cursor.reset()
      %Cursor{current: nil, next: [1], prev: []}

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward() |> Cursor.reset()
      %Cursor{current: nil, next: [1, 2], prev: []}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.reset()
      %Cursor{current: nil, next: [1, 2, 3, 4, 5], prev: []}

  """
  def reset(%Cursor{} = c), do: move_to(c, -1)

  @doc """
  Moves the position of the Cursor to the first item.
  An alias of `Cursor.move_to(c, 0)`.

  ## Examples

      iex> Cursor.new() |> Cursor.move_first()
      %Cursor{current: nil, next: [], prev: []}

      iex> Cursor.from_list([1]) |> Cursor.move_first()
      %Cursor{current: 1, next: [], prev: []}

      iex> Cursor.from_list([1, 2]) |> Cursor.move_first()
      %Cursor{current: 1, next: [2], prev: []}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_first()
      %Cursor{current: 1, next: [2, 3, 4, 5], prev: []}

  """
  @spec move_first(t()) :: t()
  def move_first(%Cursor{} = c), do: move_to(c, 0)

  @doc """
  Moves the position of the Cursor forward a number of steps.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_forward()
      %Cursor{current: 4, next: [5], prev: [3, 2, 1]}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_forward(2)
      %Cursor{current: 5, next: [], prev: [4, 3, 2, 1]}

      iex> Cursor.from_list([1, 2, 3, 4, 5]) |> Cursor.move_forward(3)
      %Cursor{current: 3, next: [4, 5], prev: [2, 1]}

      iex> Cursor.from_list([1, 2, 3, 4, 5]) |> Cursor.move_forward(5)
      %Cursor{current: 5, next: [], prev: [4, 3, 2, 1]}

      iex> Cursor.from_list([1, 2, 3, 4, 5]) |> Cursor.move_forward(6)
      %Cursor{current: nil, next: [], prev: [5, 4, 3, 2, 1]}

      iex> %Cursor{current: {:delete, "abc"}, next: [{:equal, "xxx"}, {:insert, "def"}], prev: []} |> Cursor.move_forward(1)
      %Cursor{current: {:equal, "xxx"}, next: [{:insert, "def"}], prev: [{:delete, "abc"}]}

      iex> %Cursor{current: {:delete, "abc"}, next: [{:equal, "xxx"}, {:insert, "def"}], prev: []} |> Cursor.move_forward(2)
      %Cursor{current: {:insert, "def"}, next: [], prev: [{:equal, "xxx"}, {:delete, "abc"}]}

      iex> %Cursor{current: {:delete, "abc"}, next: [{:equal, "xxx"}, {:insert, "def"}], prev: []} |> Cursor.move_forward(3)
      %Cursor{current: nil, next: [], prev: [{:insert, "def"}, {:equal, "xxx"}, {:delete, "abc"}]}

  """
  @spec move_forward(t(), integer()) :: t()
  def move_forward(cursor, count \\ 1)
  def move_forward(%Cursor{} = c, count) when is_integer(count) and count <= 0, do: c

  def move_forward(%Cursor{prev: prev, current: current, next: next}, count)
      when is_integer(count) do
    current_and_prev =
      if is_nil(current) do
        prev
      else
        [current | prev]
      end

    if count > Enum.count(next) do
      %Cursor{prev: Enum.reverse(next) ++ current_and_prev, current: nil, next: []}
    else
      {moved, next_next} = Enum.split(next, count)

      {next_cur, to_prev} =
        if Enum.empty?(moved) do
          {nil, []}
        else
          [next_cur | to_prev] = Enum.reverse(moved)
          {next_cur, to_prev}
        end

      %Cursor{prev: to_prev ++ current_and_prev, current: next_cur, next: next_next}
    end
  end

  @doc """
  Moves the position of the Cursor forward through the "next" list
  until the given item is found. Returns `nil` and if the item cannot be found.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_forward(5)
      %Cursor{current: 5, next: [], prev: [4, 3, 2, 1]}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_forward(1)
      nil

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_forward(3)
      nil

  """
  @spec find_forward(t(), term()) :: nil | t()
  def find_forward(%Cursor{next: next} = c, item) do
    case Enum.find_index(next, fn i -> i == item end) do
      nil -> nil
      i -> move_forward(c, i + 1)
    end
  end

  @doc """
  Moves the position of the Cursor back a number of steps.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_back(1)
      %Cursor{current: 2, next: [3, 4, 5], prev: [1]}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_back(2)
      %Cursor{current: 1, next: [2, 3, 4, 5], prev: []}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.move_back(3)
      %Cursor{current: nil, next: [1, 2, 3, 4, 5], prev: []}

  """
  @spec move_back(t(), integer()) :: t()
  def move_back(cursor, count \\ 1)
  def move_back(%Cursor{} = c, count) when is_integer(count) and count <= 0, do: c

  def move_back(%Cursor{prev: prev, current: current, next: next}, count)
      when is_integer(count) do
    current_and_next =
      if is_nil(current) do
        next
      else
        [current | next]
      end

    if count > Enum.count(prev) do
      %Cursor{prev: [], current: nil, next: Enum.reverse(prev) ++ current_and_next}
    else
      {moved, next_prev} = Enum.split(prev, count)

      {next_cur, to_next} =
        if Enum.empty?(moved) do
          {nil, []}
        else
          [next_cur | to_next] = Enum.reverse(moved)
          {next_cur, to_next}
        end

      %Cursor{prev: next_prev, current: next_cur, next: to_next ++ current_and_next}
    end
  end

  @doc """
  Moves the position of the Cursor back through the "prev" list
  until the given item is found. Returns `nil` and if the item cannot be found.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_back(1)
      %Cursor{current: 1, next: [2, 3, 4, 5], prev: []}

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_back(5)
      nil

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.find_back(3)
      nil

  """
  @spec find_back(t(), term()) :: nil | t()
  def find_back(%Cursor{prev: prev} = c, item) do
    case Enum.find_index(prev, fn i -> i == item end) do
      nil -> nil
      i -> move_back(c, i + 1)
    end
  end

  @doc """
  Moves the position of the Cursor back through the "prev" list to the given item.

  Raises if the item cannot be found.
  """
  @spec find_back!(t(), term()) :: t()
  def find_back!(%Cursor{} = c, item) do
    case find_back(c, item) do
      nil -> raise "item #{inspect(item)} not found in Cursor"
      found -> found
    end
  end

  @doc """
  Return a 3-tuple of the previous, current, and next items relative to
  the Cursor's current position.

  Any elements of the tuple may be `nil`.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.get()
      {2, 3, 4}

      iex> Cursor.from_list([]) |> Cursor.get()
      {nil, nil, nil}

      iex> Cursor.from_list([1, 2]) |> Cursor.get()
      {nil, nil, 1}

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward() |> Cursor.get()
      {nil, 1, 2}

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(2) |> Cursor.get()
      {1, 2, nil}

      iex> Cursor.from_list([1, 2]) |> Cursor.move_forward(3) |> Cursor.get()
      {2, nil, nil}

  """
  @spec get(t()) :: {term(), term(), term()}
  def get(%Cursor{prev: prev, current: current, next: next}) do
    {List.first(prev), current, List.first(next)}
  end

  @doc """
  Remove items at the Cursor's current position, leaving the previous items alone.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.delete(1)
      %Cursor{current: 4, next: [5], prev: [2, 1]}

  """
  @spec delete(t(), integer()) :: t()
  def delete(cursor, count \\ 1)
  def delete(%Cursor{} = c, count) when is_integer(count) and count <= 0, do: c

  def delete(%Cursor{current: current, next: next} = c, count) when is_integer(count) do
    next_next =
      if is_nil(current) do
        Enum.drop(next, count)
      else
        Enum.drop(next, count - 1)
      end

    case next_next do
      [] -> %Cursor{c | current: nil, next: []}
      [next_cur | rest] -> %Cursor{c | current: next_cur, next: rest}
    end
  end

  @doc """
  Remove items before the Cursor's current position, leaving the current and next items alone.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.delete_before(1)
      %Cursor{current: 3, next: [4, 5], prev: [1]}

  """
  @spec delete_before(t(), integer()) :: t()
  def delete_before(cursor, count \\ 1)
  def delete_before(%Cursor{} = c, count) when is_integer(count) and count <= 0, do: c

  def delete_before(%Cursor{prev: prev} = c, count) when is_integer(count) do
    %Cursor{c | prev: Enum.drop(prev, count)}
  end

  @doc """
  Insert items at the Cursor's current position, leaving the previous items alone.

  After the insertion, the Cursor points to the first inserted item.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.insert([10, 11])
      %Cursor{current: 10, next: [11, 3, 4, 5], prev: [2, 1]}

  """
  @spec insert(t(), list()) :: t()
  def insert(%Cursor{} = c, []), do: c

  def insert(%Cursor{current: nil, next: next} = c, [next_cur | rest]) do
    %Cursor{c | current: next_cur, next: rest ++ next}
  end

  def insert(%Cursor{current: current, next: next} = c, [next_cur | rest]) do
    %Cursor{c | current: next_cur, next: rest ++ [current | next]}
  end

  @doc """
  Insert items before the Cursor's current position, leaving the current and next items alone.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.insert_before([10, 11])
      %Cursor{current: 3, next: [4, 5], prev: [11, 10, 2, 1]}

  """
  @spec insert_before(t(), list()) :: t()
  def insert_before(%Cursor{} = c, []), do: c

  def insert_before(%Cursor{prev: prev} = c, items) when is_list(items) do
    %Cursor{c | prev: Enum.reverse(items) ++ prev}
  end

  @doc """
  Insert items at the Cursor's head position, leaving the current position pointer alone.

  ## Examples

      iex> %Cursor{current: 3, next: [4, 5], prev: [2, 1]} |> Cursor.insert_at_head([10, 11])
      %Cursor{current: 3, next: [4, 5], prev: [2, 1, 11, 10]}

  """
  @spec insert_at_head(t(), list()) :: t()
  def insert_at_head(%Cursor{} = c, []), do: c

  def insert_at_head(%Cursor{prev: prev} = c, items) when is_list(items) do
    %Cursor{c | prev: prev ++ Enum.reverse(items)}
  end
end