defmodule Membrane.Core.TimerController do
  @moduledoc false
  use Bunch
  alias Membrane.Clock
  alias Membrane.Core.{CallbackHandler, Component, Timer}

  require Membrane.Core.Component

  defguardp is_timer_present(timer_id, state)
            when is_map_key(state.synchronization.timers, timer_id)

  @spec start_timer(Timer.id(), Timer.interval(), Clock.t(), Component.state()) ::
          Component.state()
  def start_timer(id, _interval, _clock, state) when is_timer_present(id, state) do
    raise Membrane.TimerError, "Timer #{inspect(id)} already exists"
  end

  def start_timer(id, interval, clock, state) do
    clock |> Clock.subscribe()
    timer = Timer.start(id, interval, clock)
    put_in(state, [:synchronization, :timers, id], timer)
  end

  @spec timer_interval(Timer.id(), Timer.interval(), Component.state()) ::
          Component.state()
  def timer_interval(id, interval, state) do
    with {:ok, timer} <- state.synchronization.timers |> Map.fetch(id) do
      put_in(
        state,
        [:synchronization, :timers, id],
        Timer.set_interval(timer, interval)
      )
    else
      :error -> raise Membrane.TimerError, "Timer #{inspect(id)} doesn't exist"
    end
  end

  @spec stop_all_timers(Component.state()) :: Component.state()
  def stop_all_timers(state) do
    for {_id, timer} <- state.synchronization.timers do
      stop_and_unsubscribe(timer)
    end

    put_in(state, [:synchronization, :timers], %{})
  end

  @spec stop_timer(Timer.id(), Component.state()) :: Component.state()
  def stop_timer(id, state) do
    {timer, state} = state |> pop_in([:synchronization, :timers, id])

    if timer |> is_nil do
      raise Membrane.TimerError, "Timer #{inspect(id)} doesn't exist"
    else
      stop_and_unsubscribe(timer)
      state
    end
  end

  @spec handle_tick(Timer.id(), Component.state()) :: Component.state()
  def handle_tick(timer_id, state) when is_timer_present(timer_id, state) do
    state =
      update_in(
        state,
        [:synchronization, :timers, timer_id],
        &Timer.handle_message_arrived/1
      )

    state =
      CallbackHandler.exec_and_handle_callback(
        :handle_tick,
        Component.action_handler(state),
        %{context: &Component.context_from_state/1},
        [timer_id],
        state
      )

    # in case the timer was removed in handle_tick
    if is_timer_present(timer_id, state) do
      update_in(state, [:synchronization, :timers, timer_id], &Timer.tick/1)
    else
      state
    end
  end

  def handle_tick(_timer_id, state) do
    state
  end

  @spec handle_clock_update(Timer.id(), Clock.ratio(), Component.state()) ::
          Component.state()
  def handle_clock_update(clock, ratio, state) do
    update_in(
      state,
      [:synchronization, :timers],
      &Bunch.Map.map_values(&1, fn
        %Timer{clock: ^clock} = timer -> timer |> Timer.update_ratio(ratio)
        timer -> timer
      end)
    )
  end

  @spec stop_and_unsubscribe(Timer.t()) :: :ok
  defp stop_and_unsubscribe(timer) do
    Timer.stop(timer)
    Clock.unsubscribe(timer.clock)
  end
end
