defmodule Membrane.Connector do
  @moduledoc """
  Membrane Filter with input and output dynamic pads, that forwards incoming data to the opposite
  side than the one from which it came.

  If pad from one side is not linked yet, the data will be buffered until both pads are linked.
  """

  use Membrane.Filter

  def_input_pad :input,
    accepted_format: _any,
    availability: :on_request,
    max_instances: 1

  def_output_pad :output,
    accepted_format: _any,
    availability: :on_request,
    max_instances: 1

  def_options notify_on_stream_format?: [
                description: """
                If `true`, #{inspect(__MODULE__)} will send `{:stream_format, pad_ref, stream_format}` \
                notification to parent on every stream format arrival.

                Defaults to `false`.
                """,
                spec: boolean(),
                default: false
              ],
              notify_on_event?: [
                description: """
                If `true`, #{inspect(__MODULE__)} will send `{:event, pad_ref, event}` notification to \
                parent on every event arrival.

                Defaults to `false`.
                """,
                spec: boolean(),
                default: false
              ]

  defguardp flowing?(ctx, state)
            when ctx.playback == :playing and state.input != nil and state.output != nil

  @impl true
  def handle_init(_ctx, opts) do
    state =
      opts
      |> Map.from_struct()
      |> Map.merge(%{input: nil, output: nil, queue: [], input_demand_paused?: false})

    {[], state}
  end

  @impl true
  def handle_pad_added(Pad.ref(direction, _ref) = pad, ctx, state) do
    state = state |> Map.put(direction, pad)
    {maybe_resume, state} = maybe_resume_auto_demand(state)
    {maybe_flush, state} = maybe_flush_queue(ctx, state)
    {maybe_resume ++ maybe_flush, state}
  end

  @impl true
  def handle_start_of_stream(_input_pad, ctx, state), do: maybe_pause_auto_demand(ctx, state)

  [handle_buffer: :buffer, handle_event: :event, handle_stream_format: :stream_format]
  |> Enum.map(fn {callback, action} ->
    @impl true
    def unquote(callback)(pad, item, ctx, state) when flowing?(ctx, state) do
      actions = [{unquote(action), {opposite_pad(pad, state), item}}]
      actions = actions ++ maybe_notify_parent(unquote(action), pad, item, state)
      {actions, state}
    end

    @impl true
    def unquote(callback)(pad, item, _ctx, state) do
      state = unquote(action) |> store_in_queue(pad, item, state)
      actions = unquote(action) |> maybe_notify_parent(pad, item, state)
      {actions, state}
    end
  end)

  @impl true
  def handle_end_of_stream(pad, ctx, state) when flowing?(ctx, state) do
    {[end_of_stream: opposite_pad(pad, state)], state}
  end

  @impl true
  def handle_end_of_stream(pad, _ctx, state) do
    {[], store_in_queue(:end_of_stream, pad, nil, state)}
  end

  defp store_in_queue(action, pad, item, state) do
    state |> Map.update!(:queue, &[{action, pad, item} | &1])
  end

  defp maybe_pause_auto_demand(ctx, state) do
    if state.output == nil and not state.input_demand_paused? and
         ctx.pads[state.input].stream_format != nil,
       do: {[pause_auto_demand: state.input], %{state | input_demand_paused?: true}},
       else: {[], state}
  end

  defp maybe_resume_auto_demand(state) do
    if state.input_demand_paused? and state.output != nil,
      do: {[resume_auto_demand: state.input], %{state | input_demand_paused?: false}},
      else: {[], state}
  end

  defp maybe_flush_queue(ctx, state) when flowing?(ctx, state) do
    actions =
      state.queue
      |> Enum.reverse()
      |> Enum.map(fn
        {:end_of_stream, _input, nil} -> {:end_of_stream, state.output}
        {action, pad, item} -> {action, {opposite_pad(pad, state), item}}
      end)

    {actions, %{state | queue: nil}}
  end

  defp maybe_flush_queue(_ctx, state), do: {[], state}

  defp opposite_pad(Pad.ref(:input, _ref), state), do: state.output
  defp opposite_pad(Pad.ref(:output, _ref), state), do: state.input

  defp maybe_notify_parent(:event, pad, event, %{notify_on_event?: true}),
    do: [notify_parent: {:event, pad, event}]

  defp maybe_notify_parent(:stream_format, pad, format, %{notify_on_stream_format?: true}),
    do: [notify_parent: {:stream_format, pad, format}]

  defp maybe_notify_parent(_type, _pad, _item, _state), do: []
end
