defmodule Membrane.Funnel do
  @moduledoc """
  Element that can be used for collecting data from multiple inputs and sending it through one
  output.

  When a new input connects in the `:playing` state, the funnel sends
  `#{inspect(__MODULE__)}.NewInputEvent` via output.
  """

  use Membrane.Filter

  def_input_pad :input, accepted_format: _any, flow_control: :auto, availability: :on_request
  def_output_pad :output, accepted_format: _any, flow_control: :auto

  def_options end_of_stream: [spec: :on_last_pad | :on_first_pad | :never, default: :on_last_pad]

  @impl true
  def handle_init(_ctx, opts) do
    {[], %{end_of_stream: opts.end_of_stream}}
  end

  @impl true
  def handle_buffer(Pad.ref(:input, _id), buffer, _ctx, state) do
    {[buffer: {:output, buffer}], state}
  end

  @impl true
  def handle_pad_added(Pad.ref(:input, _id), %{playback_state: :playing}, state) do
    {[event: {:output, %__MODULE__.NewInputEvent{}}], state}
  end

  @impl true
  def handle_pad_added(Pad.ref(:input, _id), _ctx, state) do
    {[], state}
  end

  @impl true
  def handle_end_of_stream(Pad.ref(:input, _id), _ctx, %{end_of_stream: :never} = state) do
    {[], state}
  end

  @impl true
  def handle_end_of_stream(Pad.ref(:input, _id), ctx, %{end_of_stream: :on_first_pad} = state) do
    if ctx.pads.output.end_of_stream? do
      {[], state}
    else
      {[end_of_stream: :output], state}
    end
  end

  @impl true
  def handle_end_of_stream(Pad.ref(:input, _id), ctx, %{end_of_stream: :on_last_pad} = state) do
    if ctx |> inputs_data() |> Enum.all?(& &1.end_of_stream?) do
      {[end_of_stream: :output], state}
    else
      {[], state}
    end
  end

  defp inputs_data(ctx) do
    Enum.flat_map(ctx.pads, fn
      {Pad.ref(:input, _id), data} -> [data]
      _output -> []
    end)
  end
end
