lib/zipper.ex

defmodule ZipZip do
  @moduledoc File.read!("README.md")

  @initial_position -1

  @enforce_keys [:l, :r, :current, :size]
  # credo:disable-for-next-line
  defstruct @enforce_keys ++ [position: @initial_position]

  @opaque t :: %__MODULE__{
            l: [term] | [],
            current: term,
            r: [term] | [],
            size: non_neg_integer,
            position: integer
          }

  @opaque t(val) :: %__MODULE__{
            r: [val] | [],
            l: [val] | [],
            current: val | nil,
            size: non_neg_integer,
            position: integer
          }

  defmodule TraversalError do
    defexception [:message]

    @impl Exception
    def exception(value) do
      msg = "cannot traverse the zipper in this direction. #{inspect(value)}"
      %TraversalError{message: msg}
    end
  end

  defguard is_zipper(term) when is_struct(term, __MODULE__)

  defguard is_at_start(zipper)
           when zipper.l == [] and
                  is_nil(zipper.current) and
                  zipper.position == @initial_position

  defguard is_at_end(zipper) when zipper.position == zipper.size

  defguard is_out_of_bounds(zipper)
           when is_zipper(zipper) and
                  (zipper.position >= zipper.size or zipper.position < @initial_position)

  @spec new([val]) :: t(val) when val: term
  def new(list) when is_list(list) do
    struct!(__MODULE__,
      l: [],
      current: nil,
      r: list,
      size: length(list),
      position: @initial_position
    )
  end

  defdelegate zip(list), to: __MODULE__, as: :new

  @spec node(t) :: term
  def node(%__MODULE__{} = zipper), do: zipper.current

  @spec right(t) :: t | no_return
  def right(zipper) when is_out_of_bounds(zipper),
    do: raise(TraversalError, zipper)

  def right(zipper) when is_at_start(zipper) do
    zipper
    |> Map.put(:current, hd(zipper.r))
    |> Map.put(:r, tl(zipper.r))
    |> Map.update(:position, nil, &(&1 + 1))
  end

  def right(%__MODULE__{r: []} = zipper) do
    zipper
    |> Map.put(:l, append_to_list(zipper.l, zipper.current))
    |> Map.update(:position, nil, &(&1 + 1))
  end

  def right(%__MODULE__{} = zipper) do
    zipper
    |> Map.put(:l, append_to_list(zipper.l, zipper.current))
    |> Map.put(:current, hd(zipper.r))
    |> Map.put(:r, tl(zipper.r))
    |> Map.update(:position, nil, &(&1 + 1))
  end

  defp append_to_list(list, item),
    do: Enum.reverse([item] ++ list)

  @spec replace_at(t, integer, term) :: t
  def replace_at(%__MODULE__{size: size} = zipper, index, _)
      when index >= size,
      do: zipper

  def replace_at(zipper, 0, new_value) when is_at_start(zipper) do
    Map.update(zipper, :r, nil, fn r ->
      List.replace_at(r, 0, new_value)
    end)
  end

  def replace_at(%__MODULE__{position: position} = zipper, index, new_value)
      when index == position do
    Map.put(zipper, :current, new_value)
  end

  def replace_at(%__MODULE__{position: position} = zipper, index, new_value)
      when index < position do
    Map.update(zipper, :l, nil, fn l ->
      List.replace_at(l, index, new_value)
    end)
  end

  def replace_at(%__MODULE__{position: position} = zipper, index, new_value)
      when index > position do
    Map.update(zipper, :r, nil, fn r ->
      r_index = index - position - 1
      List.replace_at(r, r_index, new_value)
    end)
  end

  @spec put_current(t, term) :: t
  def put_current(%__MODULE__{} = zipper, new_value),
    do: replace_at(zipper, zipper.position, new_value)

  @spec rightmost(t) :: term
  def rightmost(%__MODULE__{} = zipper),
    do: zipper |> right_items() |> List.last()

  @spec left_items(t) :: list
  def left_items(%__MODULE__{} = zipper), do: zipper.l

  @spec right_items(t) :: list
  def right_items(%__MODULE__{} = zipper), do: zipper.r

  @spec to_list(t) :: list
  def to_list(zipper) when is_at_start(zipper), do: zipper.r

  def to_list(zipper) when is_at_start(zipper) or is_at_end(zipper),
    do: zipper.l ++ zipper.r

  def to_list(%__MODULE__{} = zipper),
    do: zipper.l ++ [zipper.current] ++ zipper.r

  @spec end?(t) :: boolean
  def end?(zipper) when is_at_end(zipper), do: true
  def end?(_), do: false

  @spec zipper?(term) :: boolean
  def zipper?(zipper) when is_zipper(zipper), do: true
  def zipper?(_), do: false
end