defprotocol Membrane.Payload do
  @moduledoc """
  This protocol describes actions common to all payload types.

  The most basic payload type is simply a binary for which `#{__MODULE__}`
  is implemented by the Membrane Core.
  """

  defmodule Behaviour do
    @moduledoc """
    Behaviour that should be implemented by every module that has
    `Membrane.Payload` protocol implementation.
    """

    @doc """
    Creates an empty payload
    """
    @callback empty() :: Membrane.Payload.t()

    @doc """
    Creates a new payload initialized with the given binary
    """
    @callback new(binary()) :: Membrane.Payload.t()
  end

  @typedoc """
  A type describing all the types that implement `Membrane.Payload` protocol.
  """
  @type t :: any()

  @doc """
  Returns total size of payload in bytes
  """
  @spec size(payload :: t()) :: non_neg_integer()
  def size(payload)

  @doc """
  Splits the payload at given position (1st part has the size equal to `at_pos` argument)

  `at_pos` has to be greater than 0 and smaller than the size of payload, otherwise
  an error is raised. This guarantees returned payloads are never empty.
  """
  @spec split_at(payload :: t(), at_pos :: pos_integer()) :: {t(), t()}
  def split_at(payload, at_pos)

  @doc """
  Concatenates the contents of two payloads.
  """
  @spec concat(left :: t(), right :: t()) :: t()
  def concat(left, right)

  @doc """
  Drops first `n` bytes of payload.
  """
  @spec drop(payload :: t(), n :: non_neg_integer()) :: t()
  def drop(payload, n)

  @doc """
  Converts payload into binary
  """
  @spec to_binary(t()) :: binary()
  def to_binary(payload)

  @doc """
  Returns a module responsible for this type of payload
  and implementing `Membrane.Payload.Behaviour`
  """
  @spec module(t()) :: module()
  def module(payload)
end

defmodule Membrane.Payload.Binary do
  @moduledoc """
  `Membrane.Payload.Behaviour` implementation for binary payload.
  Complements `Membrane.Payload` protocol implementation.
  """
  @behaviour Membrane.Payload.Behaviour

  @impl true
  def empty(), do: <<>>

  @impl true
  def new(data) when is_binary(data) do
    data
  end
end

defimpl Membrane.Payload, for: BitString do
  alias Membrane.Payload

  @compile {:inline, module: 1}

  @impl true
  @spec size(payload :: binary()) :: pos_integer
  def size(payload) when is_binary(payload) do
    payload |> byte_size()
  end

  @impl true
  @spec split_at(binary(), pos_integer) :: {binary(), binary()}
  def split_at(payload, at_pos)
      when is_binary(payload) and 0 < at_pos and at_pos < byte_size(payload) do
    <<part1::binary-size(at_pos), part2::binary>> = payload
    {part1, part2}
  end

  @impl true
  @spec concat(left :: binary(), right :: binary()) :: binary()
  def concat(left, right) when is_binary(left) and is_binary(right) do
    left <> right
  end

  @impl true
  @spec drop(payload :: binary(), bytes :: non_neg_integer()) :: binary()
  def drop(payload, bytes) when is_binary(payload) do
    <<_dropped::binary-size(bytes), rest::binary>> = payload
    rest
  end

  @impl true
  @spec to_binary(binary()) :: binary()
  def to_binary(payload) when is_binary(payload) do
    payload
  end

  @impl true
  @spec module(binary()) :: module()
  def module(_payload), do: Payload.Binary
end
