defmodule Membrane.RTC.Engine.Track do
  @moduledoc """
  Module representing media track.

  Media track is a single audio or video. Tracks that are related to each other
  (e.g. audio from microphone that corresponds to video from a web cam) can be grouped into the same stream by
  assigning each of them the same stream id.
  """
  use Bunch.Access

  alias ExSDP.Attribute.FMTP

  @enforce_keys [
    :type,
    :stream_id,
    :id,
    :origin,
    :fmtp,
    :encoding,
    :variants,
    :disabled_variants,
    :clock_rate,
    :active?,
    :metadata,
    :ctx,
    :framerate
  ]
  defstruct @enforce_keys ++ [:payload_type]

  @supported_variants [:high, :medium, :low]

  @type id :: String.t()
  @type encoding :: atom()

  @typedoc """
  Possible track variants.

  The usage should be as follows:
  * `:high` - main track variant
  * `:medium` -  lower (in terms of quality) track variant
  * `:low` - the lowest track variant

  Audio tracks can have only one variant - `:high`.
  """
  @type variant :: :high | :medium | :low

  @type framerate :: {non_neg_integer(), non_neg_integer()} | nil

  @typedoc """
  This module contains:
  * `type` - audio or video,
  * `stream_id` - media stream this track belongs to. Relationship between tracks (e.g. audio and video)
  can be indicated by assigning each of them the same `stream_id`. One `stream_id` can be assign to any
  number of tracks.
  * `id` - track id
  * `origin` - id of Endpoint this track belongs to
  * `encoding` - track encoding
  * `variants` - list of possible track variants. Refer to `t:variant/0`.
  * `clock_rate` - track clock rate
  * `fmtp` - struct describing format specific parameters e.g. for H264 it contains `profile_level_id`
  * `active?` - indicates whether track is still available or not (because client left a room)
  * `metadata` - any data passed by user to be linked with this track
  * `ctx` - any data Endpoints need to associate with `#{inspect(__MODULE__)}.t()` for internal usage
  * `framerate` - track framerate.  It's up to endpoints whether they set this field or not but if they do, they should only set it for video tracks with constant framerate. For audio tracks, framerate should always be `nil`. Defaults to `nil`.
  """
  @type t :: %__MODULE__{
          type: :audio | :video,
          stream_id: String.t(),
          # TODO: remove pt and clock-rate and replace it with RTP mapping field
          payload_type: non_neg_integer() | nil,
          id: id,
          origin: String.t(),
          encoding: encoding,
          variants: [variant()],
          disabled_variants: [variant()],
          clock_rate: Membrane.RTP.clock_rate_t(),
          fmtp: FMTP.t(),
          active?: boolean(),
          metadata: any(),
          ctx: map(),
          framerate: framerate()
        }

  @typedoc """
  Options that can be passed to `new/7`.

  If not provided:
  * `id` - will be generated
  * `active?` - true
  * `variants` - `[:high]`
  * `metadata` - `nil`
  * `ctx` - `%{}`

  For more information refer to `t:t/0`.
  """
  @type opts_t :: [
          id: String.t(),
          active?: boolean(),
          variants: [variant()],
          metadata: any(),
          ctx: map(),
          payload_type: non_neg_integer() | nil,
          framerate: framerate()
        ]

  @doc """
  Creates a new track.

  Tracks belonging to the same stream should have the same `stream_id`,
  that can be generated with `stream_id/0`.
  """
  @spec new(
          :audio | :video,
          String.t(),
          String.t(),
          encoding(),
          Membrane.RTP.clock_rate_t(),
          FMTP.t(),
          opts_t()
        ) :: t
  def new(type, stream_id, origin, encoding, clock_rate, fmtp, opts \\ []) do
    id = Keyword.get(opts, :id, Base.encode16(:crypto.strong_rand_bytes(8)))

    %__MODULE__{
      type: type,
      payload_type: Keyword.get(opts, :payload_type),
      stream_id: stream_id,
      origin: origin,
      encoding: encoding,
      clock_rate: clock_rate,
      fmtp: fmtp,
      id: id,
      active?: Keyword.get(opts, :active?, true),
      variants: Keyword.get(opts, :variants, [:high]),
      disabled_variants: [],
      metadata: Keyword.get(opts, :metadata),
      ctx: Keyword.get(opts, :ctx, %{}),
      framerate: Keyword.get(opts, :framerate)
    }
  end

  @doc """
  Generates stream id, that can be used to mark tracks belonging to the same stream.
  """
  @spec stream_id() :: String.t()
  def stream_id(), do: UUID.uuid4()

  @doc """
  Returns list of supported track variants.
  """
  @spec supported_variants() :: [variant()]
  def supported_variants(), do: @supported_variants

  @doc """
  Returns depayloader for given track.

  Depayloader can be used to unpack RTP stream i.e. get
  data out of RTP packets.

  Returns Membrane child specification or `nil` if depayloader
  couldn't be determined.

  ## Examples

      @impl true
      def handle_pad_added(Pad.ref(:input, track_id) = pad, _ctx, state) do
        children = %{
          {:track_receiver, track_id} => %TrackReceiver{
            track: Map.fetch!(state.tracks, track_id),
            initial_target_variant: :high
          },
          {:depayloader, track.id} => get_depayloader(track),
          {:serializer, track_id} => Membrane.Stream.Serializer,
          {:sink, track_id} => %Membrane.File.Sink{
            location: Path.join(state.output_dir_path, track_id)
          }
        }

        links = [
          link_bin_input(pad)
          |> to({:track_receiver, track_id})
          |> to({:depayloader, track.id})
          |> to({:serializer, track_id})
          |> to({:sink, track_id})
        ]

        {{:ok, spec: %ParentSpec{children: children, links: links}}, state}
      end
  """
  @spec get_depayloader(t()) :: Membrane.ChildrenSpec.child_definition() | nil
  def get_depayloader(track) do
    case Membrane.RTP.PayloadFormat.get(track.encoding).depayloader do
      nil ->
        nil

      depayloader ->
        %Membrane.RTP.DepayloaderBin{
          depayloader: depayloader,
          clock_rate: track.clock_rate
        }
    end
  end

  @doc """
  Checks whether track is a simulcast one or not.
  """
  @spec simulcast?(t()) :: boolean()
  def simulcast?(%__MODULE__{} = track), do: track.variants != [:high]
end
