defmodule Igniter.Code.List do
  @moduledoc """
  Utilities for working with lists.
  """

  require Igniter.Code.Common
  alias Igniter.Code.Common
  alias Sourceror.Zipper

  @type equality_pred :: (Zipper.t(), Macro.t() -> boolean)

  @doc "Returns true if the `zipper` is at a list literal"
  @spec list?(Zipper.t()) :: boolean()
  def list?(zipper) do
    Common.node_matches_pattern?(zipper, value when is_list(value))
  end

  @doc "Prepends `quoted` to the list unless it is already present, determined by `equality_pred`."
  @spec prepend_new_to_list(Zipper.t(), quoted :: Macro.t(), equality_pred) ::
          {:ok, Zipper.t()} | :error
  def prepend_new_to_list(zipper, quoted, equality_pred \\ &Common.nodes_equal?/2) do
    Common.within(zipper, fn zipper ->
      if list?(zipper) do
        zipper
        |> find_list_item_index(fn value ->
          equality_pred.(value, quoted)
        end)
        |> case do
          nil ->
            prepend_to_list(zipper, quoted)

          _ ->
            {:ok, zipper}
        end
      else
        :error
      end
    end)
  end

  @doc "Appends `quoted` to the list unless it is already present, determined by `equality_pred`."
  @spec append_new_to_list(Zipper.t(), quoted :: Macro.t(), equality_pred) ::
          {:ok, Zipper.t()} | :error
  def append_new_to_list(zipper, quoted, equality_pred \\ &Common.nodes_equal?/2) do
    if list?(zipper) do
      zipper
      |> find_list_item_index(fn value ->
        equality_pred.(value, quoted)
      end)
      |> case do
        nil ->
          append_to_list(zipper, quoted)

        _ ->
          {:ok, zipper}
      end
    else
      :error
    end
  end

  @doc "Prepends `quoted` to the list"
  @spec prepend_to_list(Zipper.t(), quoted :: Macro.t()) :: {:ok, Zipper.t()} | :error
  def prepend_to_list(zipper, quoted) do
    if list?(zipper) do
      {:ok,
       zipper
       |> Common.maybe_move_to_single_child_block()
       |> Zipper.insert_child(quoted)}
    else
      :error
    end
  end

  @doc "Appends `quoted` to the list"
  @spec append_to_list(Zipper.t(), quoted :: Macro.t()) :: {:ok, Zipper.t()} | :error
  def append_to_list(zipper, quoted) do
    if list?(zipper) do
      {:ok,
       zipper
       |> Common.maybe_move_to_single_child_block()
       |> Zipper.append_child(quoted)}
    else
      :error
    end
  end

  @spec remove_from_list(Zipper.t(), predicate :: (Zipper.t() -> boolean())) ::
          {:ok, Zipper.t()} | :error
  def remove_from_list(zipper, predicate) do
    zipper = Igniter.Code.Common.maybe_move_to_single_child_block(zipper)

    if list?(zipper) do
      Common.within(zipper, fn zipper ->
        zipper
        |> Zipper.down()
        |> case do
          nil ->
            {:ok, zipper}

          zipper ->
            zipper
            |> Common.move_right(predicate)
            |> case do
              :error ->
                :error

              {:ok, zipper} ->
                {:ok, Zipper.remove(zipper)}
            end
        end
      end)
      |> case do
        :error ->
          {:ok, zipper}

        {:ok, ^zipper} ->
          {:ok, zipper}

        {:ok, zipper} ->
          remove_from_list(zipper, predicate)
      end
    else
      :error
    end
  end

  @spec replace_in_list(Zipper.t(), predicate :: (Zipper.t() -> boolean()), term :: any()) ::
          {:ok, Zipper.t()} | :error
  def replace_in_list(zipper, predicate, value) do
    if list?(zipper) do
      Common.within(zipper, fn zipper ->
        zipper
        |> Zipper.down()
        |> Common.move_right(predicate)
        |> case do
          :error ->
            :error

          {:ok, zipper} ->
            {:ok, Igniter.Code.Common.replace_code(zipper, value)}
        end
      end)
      |> case do
        :error ->
          {:ok, zipper}

        {:ok, zipper} ->
          replace_in_list(zipper, predicate, value)
      end
    else
      :error
    end
  end

  @doc "Removes the item at the given index, returning `:error` if nothing is at that index"
  @spec remove_index(Zipper.t(), index :: non_neg_integer()) :: {:ok, Zipper.t()} | :error
  def remove_index(zipper, index) do
    if list?(zipper) do
      Common.within(zipper, fn zipper ->
        zipper
        |> Igniter.Code.Common.maybe_move_to_single_child_block()
        |> Zipper.down()
        |> Common.move_right(index)
        |> case do
          :error ->
            :error

          {:ok, zipper} ->
            {:ok, Zipper.remove(zipper)}
        end
      end)
    else
      :error
    end
  end

  @doc "Finds the index of the first list item that satisfies `pred`"
  @spec find_list_item_index(Zipper.t(), (Zipper.t() -> boolean())) :: integer() | nil
  def find_list_item_index(zipper, pred) do
    # go into first list item
    zipper
    |> Common.maybe_move_to_single_child_block()
    |> Zipper.down()
    |> case do
      nil ->
        nil

      zipper ->
        find_index_right(zipper, pred, 0)
    end
  end

  @doc "Moves to the list item matching the given predicate"
  @spec move_to_list_item(Zipper.t(), (Zipper.t() -> boolean())) :: {:ok, Zipper.t()} | :error
  def move_to_list_item(zipper, pred) do
    # go into first list item
    zipper
    |> Common.maybe_move_to_single_child_block()
    |> Zipper.down()
    |> case do
      nil ->
        :error

      zipper ->
        do_move_to_list_item(zipper, pred)
    end
  end

  @doc "Moves to the list item matching the given predicate"
  @spec map(Zipper.t(), (Zipper.t() -> {:ok, Zipper.t()})) :: {:ok, Zipper.t()} | :error
  def map(zipper, fun) do
    # go into first list item
    zipper
    |> Common.maybe_move_to_single_child_block()
    |> Zipper.down()
    |> case do
      nil ->
        :error

      zipper ->
        do_map(zipper, fun)
    end
  end

  defp do_map(zipper, fun) do
    case Igniter.Code.Common.within(zipper, fun) do
      {:ok, zipper} ->
        case Zipper.right(zipper) do
          nil ->
            {:ok, zipper}

          right ->
            do_map(right, fun)
        end

      :error ->
        :error
    end
  end

  @doc "Moves to the list item matching the given predicate, assuming you are currently inside the list"
  def do_move_to_list_item(zipper, pred) do
    if pred.(zipper) do
      {:ok, zipper}
    else
      zipper
      |> Zipper.right()
      |> case do
        nil ->
          :error

        right ->
          do_move_to_list_item(right, pred)
      end
    end
  end

  defp find_index_right(zipper, pred, index) do
    if pred.(Common.maybe_move_to_single_child_block(zipper)) do
      index
    else
      case Zipper.right(zipper) do
        nil ->
          nil

        zipper ->
          zipper
          |> find_index_right(pred, index + 1)
      end
    end
  end
end
