defmodule Delta do
  alias Delta.{Attr, Op}

  @type t :: list(Op.t())

  @spec get_handler(atom) :: module | nil
  def get_handler(embed_type) do
    :delta
    |> Application.get_env(:custom_embeds, [])
    |> Enum.find(&(&1.name == embed_type))
  end

  @spec get_handler!(atom) :: module
  def get_handler!(embed_type) do
    case get_handler(embed_type) do
      nil -> raise("no embed handler configured for #{inspect(embed_type)}")
      handler -> handler
    end
  end

  @doc ~S"""
  Returns a new Delta that is equivalent to applying the operations of one Delta, followed by another Delta

  ## Examples
      iex> a = [Op.insert("abc")]
      iex> b = [Op.retain(1), Op.delete(1)]
      iex> Delta.compose(a, b)
      [%{"insert" => "ac"}]
  """
  @spec compose(t, t) :: t
  def compose(left, right) do
    [] |> do_compose(left, right) |> chop() |> Enum.reverse()
  end

  @doc ~S"""
  Returns a new Delta that is equivalent to applying Deltas one by one

  ## Examples
      iex> a = [Op.insert("ac")]
      iex> b = [Op.retain(1), Op.insert("b")]
      iex> c = [Op.delete(1)]
      iex> Delta.compose_all([a, b, c])
      [%{"insert" => "bc"}]
  """
  @spec compose_all([t]) :: t
  def compose_all(deltas) do
    Enum.reduce(deltas, [], &compose(&2, &1))
  end

  defp do_compose(result, [], []), do: result

  defp do_compose(result, [], [op | delta]) do
    Enum.reverse(delta, push(result, op))
  end

  defp do_compose(result, [op | delta], []) do
    Enum.reverse(delta, push(result, op))
  end

  defp do_compose(result, [op1 | delta1], [op2 | delta2]) do
    {op, delta1, delta2} =
      cond do
        Op.insert?(op2) ->
          {op2, [op1 | delta1], delta2}

        Op.delete?(op1) ->
          {op1, delta1, [op2 | delta2]}

        true ->
          {composed, op1, op2} = Op.compose(op1, op2)
          delta1 = push(delta1, op1)
          delta2 = push(delta2, op2)
          {composed, delta1, delta2}
      end

    result
    |> push(op)
    |> do_compose(delta1, delta2)
  end

  @spec chop(t) :: t
  defp chop([%{"retain" => n} = op | delta]) when is_number(n) and map_size(op) == 1, do: delta
  defp chop(delta), do: delta

  @doc ~S"""
  Compacts Delta to satisfy [compactness](https://quilljs.com/guides/designing-the-delta-format/#compact) requirement.

  ## Examples
      iex> delta = [Op.insert("Hel"), Op.insert("lo"), Op.insert("World", %{"bold" => true})]
      iex> Delta.compact(delta)
      [%{"insert" => "Hello"}, %{"insert" => "World", "attributes" => %{"bold" => true}}]
  """
  @spec compact(t) :: t
  def compact(delta) do
    delta
    |> Enum.reduce([], &push(&2, &1))
    |> Enum.reverse()
  end

  @doc ~S"""
  Concatenates two Deltas.

  ## Examples
      iex> a = [Op.insert("Hel")]
      iex> b = [Op.insert("lo")]
      iex> Delta.concat(a, b)
      [%{"insert" => "Hello"}]
  """
  @spec concat(t, t) :: t
  def concat(left, []), do: left
  def concat([], right), do: right

  def concat(left, [first | right]) do
    left =
      left
      |> Enum.reverse()
      |> push(first)
      |> Enum.reverse()

    left ++ right
  end

  @doc ~S"""
  Pushes an operation to a reversed Delta honouring semantics.

  Note: that reversed delta does not represent a reversed text, but rather a list
  of operations that was naively reversed during programmatic manipulations.
  This function is normally only used by other functions which reverse the list
  back in the end.

  ## Examples
      iex> delta = [Op.insert("World", %{"italic" => true}), Op.insert("Hello", %{"bold" => true})]
      iex> op = Op.insert("!")
      iex> Delta.push(delta, op)
      [%{"insert" => "!"}, %{"insert" => "World", "attributes" => %{"italic" => true}}, %{"insert" => "Hello", "attributes" => %{"bold" => true}}]

      iex> delta = [Op.insert("World"), Op.insert("Hello", %{"bold" => true})]
      iex> op = Op.insert("!")
      iex> Delta.push(delta, op)
      [%{"insert" => "World!"}, %{"insert" => "Hello", "attributes" => %{"bold" => true}}]
  """
  @spec push(t, false) :: t
  @spec push(t, Op.t()) :: t
  def push(delta, false), do: delta

  def push([], op) do
    if Op.size(op) > 0, do: [op], else: []
  end

  # Adds op to the beginning of delta (we expect a reverse)
  # TODO: Handle inserts after delete (should move insert before delete)
  def push(delta, op) do
    [last_op | partial_delta] = delta
    merged_op = do_push(last_op, op)

    if is_nil(merged_op) do
      [op | delta]
    else
      [merged_op | partial_delta]
    end
  end

  defp do_push(op, %{"delete" => 0}), do: op
  defp do_push(op, %{"insert" => ""}), do: op
  defp do_push(op, %{"retain" => 0}), do: op

  defp do_push(%{"delete" => left, "attributes" => attr}, %{
         "delete" => right,
         "attributes" => attr
       }) do
    Op.delete(left + right, attr)
  end

  defp do_push(%{"delete" => left} = last_op, %{"delete" => right} = op)
       when map_size(last_op) == 1 and map_size(op) == 1 do
    Op.delete(left + right)
  end

  defp do_push(%{"retain" => left, "attributes" => attr}, %{
         "retain" => right,
         "attributes" => attr
       })
       when is_integer(left) and is_integer(right) do
    Op.retain(left + right, attr)
  end

  defp do_push(%{"retain" => left} = last_op, %{"retain" => right} = op)
       when map_size(last_op) == 1 and map_size(op) == 1 and
              is_integer(left) and is_integer(right) do
    Op.retain(left + right)
  end

  defp do_push(%{"insert" => left, "attributes" => attr}, %{
         "insert" => right,
         "attributes" => attr
       })
       when is_bitstring(left) and is_bitstring(right) do
    Op.insert(left <> right, attr)
  end

  defp do_push(%{"insert" => left} = last_op, %{"insert" => right} = op)
       when is_bitstring(left) and is_bitstring(right) and map_size(last_op) == 1 and
              map_size(op) == 1 do
    Op.insert(left <> right)
  end

  defp do_push(_, _), do: nil

  @doc ~S"""
  Returns the size of delta.

  ## Examples
      iex> delta = [Op.insert("abc"), Op.retain(2), Op.delete(1)]
      iex> Delta.size(delta)
      6
  """
  @spec size(t) :: non_neg_integer
  def size(delta) do
    Enum.reduce(delta, 0, fn op, sum ->
      sum + Op.size(op)
    end)
  end

  @doc ~S"""
  Attempts to take `len` characters starting from `index`.

  Note: note that due to the way it's implemented this operation can potentially
  raise if the resulting text isn't a valid UTF-8 encoded string

  ## Examples
      iex> delta = [Op.insert("Hello World")]
      iex> Delta.slice(delta, 6, 3)
      [%{"insert" => "Wor"}]

      iex> delta = [Op.insert("01🙋45")]
      iex> Delta.slice(delta, 1, 2)
      ** (RuntimeError) Encoding failed in take_partial {"1🙋45", %{"insert" => "1🙋45"}, 2, {:incomplete, "1", <<216, 61>>}, {:error, "", <<222, 75, 0, 52, 0, 53>>}}
  """
  @spec slice(t, non_neg_integer, non_neg_integer) :: t
  def slice(delta, index, len) do
    {_left, right} = split(delta, index)
    {middle, _rest} = split(right, len)
    middle
  end

  @doc ~S"""
  Takes `len` or fewer characters from `index` position. Variable `len` allows
  to not cut things like emojis in half.

  ## Examples
      iex> delta = [Op.insert("Hello World")]
      iex> Delta.slice_max(delta, 6, 3)
      [%{"insert" => "Wor"}]

      iex> delta = [Op.insert("01🙋45")]
      iex> Delta.slice_max(delta, 1, 2)
      [%{"insert" => "1"}]
  """
  @doc since: "0.2.0"
  @spec slice_max(t, non_neg_integer, non_neg_integer) :: t
  def slice_max(delta, index, len) do
    {_left, right} = split(delta, index, align: true)
    {middle, _rest} = split(right, len, align: true)
    middle
  end

  @doc ~S"""
  Splits delta at the given index.

  ## Options

    * `:align` - when `true`, allow moving index left if
      we're likely to split a grapheme otherwise.

  ## Examples
      iex> delta = [Op.insert("Hello World")]
      iex> Delta.split(delta, 5)
      {[%{"insert" => "Hello"}], [%{"insert" => " World"}]}

      iex> delta = [Op.insert("01🙋45")]
      iex> Delta.split(delta, 3, align: true)
      {[%{"insert" => "01"}], [%{"insert" => "🙋45"}]}

      iex> delta = [Op.insert("a"), Op.insert("b", %{"bold" => true})]
      iex> Delta.split(delta, 3)
      {[%{"insert" => "a"}, %{"insert" => "b", "attributes" => %{"bold" => true}}], []}
  """
  @spec split(t, non_neg_integer | fun, Keyword.t()) :: {t, t}
  def split(delta, index, opts \\ [])

  def split(delta, 0, _), do: {[], delta}

  def split(delta, index, opts) when is_integer(index) do
    do_split(
      [],
      delta,
      fn op, index ->
        op_size = Op.size(op)

        if index <= op_size do
          index
        else
          {:cont, index - op_size}
        end
      end,
      index,
      opts
    )
  end

  def split(delta, func, opts) when is_function(func) do
    do_split([], delta, func, nil, opts)
  end

  defp do_split(passed, [], _, _, _), do: {Enum.reverse(passed), []}

  defp do_split(passed, remaining, func, context, opts) when is_function(func, 1) do
    do_split(passed, remaining, fn op, _ -> func.(op) end, context, opts)
  end

  defp do_split(passed, remaining, func, context, opts) when is_function(func, 2) do
    [first | remaining] = remaining

    case func.(first, context) do
      :cont ->
        do_split([first | passed], remaining, func, context, opts)

      {:cont, context} ->
        do_split([first | passed], remaining, func, context, opts)

      0 ->
        {Enum.reverse(passed), [first | remaining]}

      index ->
        case Op.take(first, index, opts) do
          {left, false} ->
            {Enum.reverse([left | passed]), remaining}

          {left, right} ->
            {Enum.reverse([left | passed]), [right | remaining]}
        end
    end
  end

  @doc ~S"""
  Transforms given delta against another's operations.

  This accepts an optional priority argument (default: false), used to break ties.
  If true, the first delta takes priority over other, that is, its actions are considered to happen "first."

  ## Examples
      iex> a = [Op.insert("a")]
      iex> b = [Op.insert("b"), Op.retain(5), Op.insert("c")]
      iex> Delta.transform(a, b, true)
      [
        %{"retain" => 1},
        %{"insert" => "b"},
        %{"retain" => 5},
        %{"insert" => "c"},
      ]
      iex> Delta.transform(a, b)
      [
        %{"insert" => "b"},
        %{"retain" => 6},
        %{"insert" => "c"},
      ]
  """
  @spec transform(t, t, boolean) :: t
  def transform(_, _, priority \\ false)

  def transform(index, delta, priority) when is_integer(index) do
    do_transform(0, index, delta, priority)
  end

  def transform(left, right, priority) do
    delta = do_transform([], left, right, priority)
    delta |> chop() |> Enum.reverse()
  end

  defp do_transform(offset, index, _, _) when is_integer(index) and offset > index, do: index
  defp do_transform(_, index, [], _) when is_integer(index), do: index

  defp do_transform(offset, index, [%{"delete" => length} | delta], priority)
       when is_integer(index) do
    do_transform(offset, index - min(length, index - offset), delta, priority)
  end

  defp do_transform(offset, index, [op | delta], priority) when is_integer(index) do
    {offset, index} = Op.transform(offset, index, op, priority)
    do_transform(offset, index, delta, priority)
  end

  defp do_transform(result, [], [], _), do: result

  defp do_transform(result, [], [op | delta], priority) do
    do_transform(result, [Op.retain(Op.size(op))], [op | delta], priority)
  end

  defp do_transform(result, [op | delta], [], priority) do
    do_transform(result, [op | delta], [Op.retain(Op.size(op))], priority)
  end

  defp do_transform(result, [op1 | delta1], [op2 | delta2], priority) do
    {op, delta1, delta2} =
      cond do
        Op.insert?(op1) and (priority or not Op.insert?(op2)) ->
          {Op.retain(Op.size(op1)), delta1, [op2 | delta2]}

        Op.insert?(op2) ->
          {op2, [op1 | delta1], delta2}

        true ->
          {transformed, op1, op2} = Op.transform(op1, op2, priority)
          delta1 = push(delta1, op1)
          delta2 = push(delta2, op2)
          {transformed, delta1, delta2}
      end

    result
    |> push(op)
    |> do_transform(delta1, delta2, priority)
  end

  @doc ~S"""
  Returns an inverted delta that has the opposite effect of against a base document delta.

  That is base |> Delta.compose(change) |> Delta.compose(inverted) == base.

  ## Examples
      iex> base = [Op.insert("Hello\nWorld")]
      iex> change = [
      ...>   Op.retain(6, %{"bold" => true}),
      ...>   Op.delete(5),
      ...>   Op.insert("!"),
      ...> ]
      iex> inverted = Delta.invert(change, base)
      [
        %{"retain" => 6, "attributes" => %{"bold" => nil}},
        %{"insert" => "World"},
        %{"delete" => 1},
      ]
      iex> base |> Delta.compose(change) |> Delta.compose(inverted) == base
      true
  """
  @spec invert(t, t) :: t
  def invert(change, base) do
    change
    |> Enum.reduce({[], 0}, fn op, {inverted, base_index} ->
      length = Op.size(op)

      cond do
        Op.insert?(op) ->
          inverted = push(inverted, Op.delete(length))
          {inverted, base_index}

        Op.retain?(op, :number) && !Op.has_attributes?(op) ->
          inverted = push(inverted, Op.retain(length))
          {inverted, base_index + length}

        Op.retain?(op, :number) || Op.delete?(op) ->
          inverted =
            base
            |> slice(base_index, length)
            |> Enum.reduce(inverted, &do_invert_slice(op, &1, &2))

          {inverted, base_index + length}

        # Delegate to the embed handler when change op is an embed
        Op.retain?(op, :map) ->
          base_op =
            base
            |> slice(base_index, length)
            |> hd

          {embed_type, embed1, embed2} = Op.get_embed_data!(op["retain"], base_op["insert"])
          handler = get_handler!(embed_type)

          embed = %{embed_type => handler.invert(embed1, embed2)}
          attrs = Attr.invert(op["attributes"], base_op["attributes"])
          inverted = push(inverted, Op.retain(embed, attrs))

          {inverted, base_index + 1}

        true ->
          {inverted, base_index}
      end
    end)
    |> elem(0)
    |> chop()
    |> Enum.reverse()
  end

  defp do_invert_slice(op, base_op, inverted) do
    cond do
      Op.delete?(op) ->
        push(inverted, base_op)

      Op.retain?(op) && Op.has_attributes?(op) ->
        attrs = Attr.invert(op["attributes"], base_op["attributes"])
        retain_op = base_op |> Op.size() |> Op.retain(attrs)
        push(inverted, retain_op)

      true ->
        inverted
    end
  end

  @doc ~S"""
  Returns a delta representing the difference between two documents.

  ## Examples
      iex> a = [Op.insert("Hello")]
      iex> b = [Op.insert("Hello!")]
      iex> diff = Delta.diff(a, b)
      [
        %{"retain" => 5},
        %{"insert" => "!"}
      ]
      iex> Delta.compose(a, diff) == b
      true
  """
  @doc since: "0.4.0"
  @spec diff(t, t) :: t
  def diff(base, other)

  def diff(base, other) when base == other, do: []

  def diff(base, other) do
    base_string = diffable_string(base)
    other_string = diffable_string(other)

    diff =
      base_string
      |> :diffy.diff(other_string)
      |> Dmp.Diff.cleanup_semantic()

    do_diff(base, other, diff, [], nil, 0)
  end

  defp diffable_string(delta) do
    delta
    |> Enum.map(fn
      %{"insert" => str} when is_binary(str) ->
        str

      %{"insert" => data} when not is_nil(data) ->
        <<0>>

      _ ->
        raise "Delta.diff called with non-document"
    end)
    |> Enum.join()
  end

  defp do_diff(_, _, [], delta, _, 0) do
    delta
    |> chop()
    |> Enum.reverse()
  end

  defp do_diff(base, other, [{action, str} | rest_diffs], delta, _cur_action, 0) do
    do_diff(base, other, rest_diffs, delta, action, Op.text_size(str))
  end

  defp do_diff(base, [first | rest], diffs, delta, :insert, len) do
    op_len = min(Op.size(first), len)
    {op, remaining} = Op.take(first, op_len)

    do_diff(base, push(rest, remaining), diffs, push(delta, op), :insert, len - op_len)
  end

  defp do_diff([first | rest], other, diffs, delta, :delete, len) do
    op_len = min(Op.size(first), len)
    {_op, remaining} = Op.take(first, op_len)

    do_diff(
      push(rest, remaining),
      other,
      diffs,
      push(delta, Op.delete(op_len)),
      :delete,
      len - op_len
    )
  end

  defp do_diff([base_first | base_rest], [other_first | other_rest], diffs, delta, :equal, len) do
    op_len = Enum.min([Op.size(base_first), Op.size(other_first), len])
    {base_op, base_remaining} = Op.take(base_first, op_len)
    {other_op, other_remaining} = Op.take(other_first, op_len)

    base = push(base_rest, base_remaining)
    other = push(other_rest, other_remaining)

    ops_diff =
      with %{"insert" => %{} = base_insert} <- base_op,
           %{"insert" => %{} = other_insert} <- other_op,
           [{embed_type, _embed}] <- Map.to_list(base_insert),
           [{^embed_type, _embed}] <- Map.to_list(other_insert),
           handler = get_handler(embed_type),
           true <- function_exported?(handler, :diff, 2) do
        handler.diff(base_op, other_op)
      else
        _ ->
          diff_ops_generic(base_op, other_op)
      end

    delta = Enum.reduce(ops_diff, delta, fn op, acc -> push(acc, op) end)

    do_diff(base, other, diffs, delta, :equal, len - op_len)
  end

  defp diff_ops_generic(base_op = %{"insert" => ins}, other_op = %{"insert" => ins}) do
    attrs = Delta.Attr.diff(base_op["attributes"], other_op["attributes"])
    [Op.retain(Op.size(base_op), attrs)]
  end

  defp diff_ops_generic(base_op, other_op) do
    [Op.delete(Op.size(base_op)), other_op]
  end
end
