defmodule Yex do
  @moduledoc """
  Yex is wrapper library for the Yjs CRDT library.
  """

  @type input_type ::
          :binary
          | :string
          | :map
          | :list
          | :integer
          | :float
          | :boolean
          | nil
          | Yex.PrelimType.t()

  @doc """
  Computes the state vector and encodes it into an Binary.
  A state vector describes the state of the local client.
  The remote client can use this to exchange only the missing differences.

  ## Examples
      iex> doc = Yex.Doc.new()
      iex> {:ok, _binary} = Yex.encode_state_vector(doc)
  """
  @spec encode_state_vector(Yex.Doc.t()) :: {:ok, binary()} | {:error, term()}
  def encode_state_vector(%Yex.Doc{} = doc) do
    encode_state_vector_v1(doc)
  end

  @doc """
  @see encode_state_vector/1
  """
  @spec encode_state_vector!(Yex.Doc.t()) :: binary()
  def encode_state_vector!(%Yex.Doc{} = doc) do
    case encode_state_vector(doc) do
      {:ok, binary} -> binary
      {:error, reason} -> raise reason
    end
  end

  def encode_state_vector_v1(%Yex.Doc{} = doc) do
    Yex.Nif.encode_state_vector_v1(doc, cur_txn(doc))
  end

  def encode_state_vector_v2(%Yex.Doc{} = doc) do
    Yex.Nif.encode_state_vector_v2(doc, cur_txn(doc))
  end

  @doc """
  Encode the document state as a single update message that can be applied on the remote document. Optionally, specify the target state vector to only write the missing differences to the update message.

  ## Examples
      iex> doc = Yex.Doc.new()
      iex> {:ok, _binary} = Yex.encode_state_as_update(doc)
  """
  @spec encode_state_as_update(Yex.Doc.t(), binary() | nil) :: {:ok, binary()} | {:error, term()}
  def encode_state_as_update(%Yex.Doc{} = doc, encoded_state_vector \\ nil) do
    encode_state_as_update_v1(doc, encoded_state_vector)
  end

  @doc """
  @see encode_state_as_update/2
  """
  @spec encode_state_as_update!(Yex.Doc.t(), binary() | nil) :: binary()
  def encode_state_as_update!(%Yex.Doc{} = doc, encoded_state_vector \\ nil) do
    case encode_state_as_update(doc, encoded_state_vector) do
      {:ok, binary} -> binary
      {:error, {:encoding_exception, message}} -> raise ArgumentError, message: message
      {:error, reason} -> raise reason
    end
  end

  def encode_state_as_update_v1(%Yex.Doc{} = doc, encoded_state_vector \\ nil) do
    Yex.Nif.encode_state_as_update_v1(doc, cur_txn(doc), encoded_state_vector)
  end

  def encode_state_as_update_v2(%Yex.Doc{} = doc, encoded_state_vector \\ nil) do
    Yex.Nif.encode_state_as_update_v2(doc, cur_txn(doc), encoded_state_vector)
  end

  @doc """
  Apply a document update on the shared document.

  ## Examples syncs two clients by exchanging the complete document structure
      iex> doc1 = Yex.Doc.new()
      iex> doc2 = Yex.Doc.new()
      iex> {:ok, state1} = Yex.encode_state_as_update(doc1)
      iex> {:ok, state2} = Yex.encode_state_as_update(doc2)
      iex> Yex.apply_update(doc1, state2)
      :ok
      iex> Yex.apply_update(doc2, state1)
      :ok
  """
  @spec apply_update(Yex.Doc.t(), binary()) :: :ok | {:error, term()}
  def apply_update(%Yex.Doc{} = doc, update) do
    apply_update_v1(doc, update)
  end

  @spec apply_update_v1(Yex.Doc.t(), binary()) :: :ok | {:error, term()}
  def apply_update_v1(%Yex.Doc{} = doc, update) do
    Yex.Nif.apply_update_v1(doc, cur_txn(doc), update)
  end

  @spec apply_update_v2(Yex.Doc.t(), binary()) :: :ok | {:error, term()}
  def apply_update_v2(%Yex.Doc{} = doc, update) do
    Yex.Nif.apply_update_v2(doc, cur_txn(doc), update)
  end

  @doc """
  Normalize a number to a format that can be used in Yjs.
  """
  def normalize(number) when is_number(number) do
    Yex.Nif.normalize_number(number)
  end

  def normalize(number) do
    number
  end

  defp cur_txn(%Yex.Doc{reference: doc_ref}) do
    Process.get(doc_ref, nil)
  end
end
