lib/exshome_player/services/playlist.ex

defmodule ExshomePlayer.Services.Playlist do
  @moduledoc """
  Module responsible for a playlist.
  """

  alias ExshomePlayer.Events.{PlayerFileEnd, TrackEvent}
  alias ExshomePlayer.Schemas.Track
  alias ExshomePlayer.Services.Playback
  alias ExshomePlayer.Variables.Title

  use Exshome.Dependency.GenServerDependency,
    dependencies: [{Title, :title}],
    events: [PlayerFileEnd, TrackEvent],
    name: "playlist"

  defstruct [:current_id, tracks: []]

  @type t() :: %__MODULE__{
          current_id: String.t() | nil,
          tracks: list(Track.t())
        }

  defmodule Data do
    @moduledoc """
    Inner data format for playback.
    """

    defstruct next: [], previous: []

    @type t() :: %__MODULE__{
            previous: list(Track.t()),
            next: list(Track.t())
          }
  end

  @spec play(id :: String.t()) :: :ok
  def play(id) when is_binary(id) do
    call({:play, id})
  end

  @spec next() :: :ok
  def next, do: call(:next)

  @spec previous() :: :ok
  def previous, do: call(:previous)

  @impl GenServerDependency
  def on_init(%DependencyState{} = state) do
    Track.refresh_tracklist()
    update_playlist(state, fn _ -> %Data{previous: Enum.reverse(Track.list())} end)
  end

  @impl GenServerDependency
  def handle_dependency_change(
        %DependencyState{
          data: %Data{next: [%Track{type: :file} = track | _]},
          deps: %{title: title}
        } = state
      )
      when title != "" do
    Track.update!(track, %{title: title})
    state
  end

  def handle_dependency_change(%DependencyState{} = state), do: state

  @impl GenServerDependency
  def handle_call(
        {:play, id},
        _from,
        %DependencyState{value: %__MODULE__{tracks: tracks}} = state
      )
      when is_binary(id) do
    {previous, next} = Enum.split_while(tracks, &(&1.id != id))

    state =
      state
      |> update_playlist(fn _ ->
        %Data{
          previous: Enum.reverse(previous),
          next: next
        }
      end)
      |> load_track()

    {:reply, :ok, state}
  end

  def handle_call(:next, _from, %DependencyState{} = state) do
    state =
      state
      |> update_playlist(&move_to_next_track/1)
      |> load_track()

    {:reply, :ok, state}
  end

  def handle_call(:previous, _from, %DependencyState{} = state) do
    state =
      state
      |> update_playlist(&move_to_previous_track/1)
      |> load_track()

    {:reply, :ok, state}
  end

  @impl GenServerDependency
  def handle_event(%PlayerFileEnd{reason: reason}, %DependencyState{} = state)
      when reason in ["eof", "error"] do
    state
    |> update_playlist(&move_to_next_track/1)
    |> load_track()
  end

  def handle_event(%PlayerFileEnd{}, %DependencyState{} = state), do: state

  def handle_event(%TrackEvent{action: :created, track: track}, %DependencyState{} = state) do
    if Enum.any?(state.value.tracks, &(&1.id == track.id)) do
      state
    else
      update_playlist(state, fn %Data{} = data ->
        %Data{data | previous: data.previous ++ [track]}
      end)
    end
  end

  def handle_event(
        %TrackEvent{action: :deleted, track: %Track{id: id}},
        %DependencyState{data: %Data{next: [%Track{id: id} | next]}} = state
      ) do
    state
    |> update_playlist(fn %Data{} = data ->
      %Data{previous: data.previous ++ Enum.reverse(next), next: []}
    end)
    |> load_track()
  end

  def handle_event(
        %TrackEvent{action: :deleted, track: %Track{id: id}},
        %DependencyState{} = state
      ) do
    update_playlist(state, fn %Data{} = data ->
      %Data{
        previous: Enum.reject(data.previous, &(&1.id == id)),
        next: Enum.reject(data.next, &(&1.id == id))
      }
    end)
  end

  def handle_event(
        %TrackEvent{action: :updated, track: %Track{} = track},
        %DependencyState{} = state
      ) do
    update_fn = fn %Track{} = current ->
      if current.id == track.id do
        track
      else
        current
      end
    end

    update_playlist(
      state,
      fn %Data{} = data ->
        %Data{
          previous: Enum.map(data.previous, update_fn),
          next: Enum.map(data.next, update_fn)
        }
      end
    )
  end

  @spec update_playlist(DependencyState.t(), (Data.t() -> Data.t())) :: DependencyState.t()
  defp update_playlist(%DependencyState{} = state, update_fn) do
    state
    |> update_data(update_fn)
    |> refresh_playlist()
  end

  @spec refresh_playlist(DependencyState.t()) :: DependencyState.t()
  defp refresh_playlist(%DependencyState{data: %Data{next: [%Track{id: id} | _]} = data} = state) do
    update_value(
      state,
      %__MODULE__{
        current_id: id,
        tracks: Enum.reverse(data.previous) ++ data.next
      }
    )
  end

  defp refresh_playlist(%DependencyState{data: %Data{} = data} = state) do
    update_value(
      state,
      %__MODULE__{
        current_id: nil,
        tracks: Enum.reverse(data.previous) ++ data.next
      }
    )
  end

  @spec load_track(DependencyState.t()) :: DependencyState.t()
  defp load_track(%DependencyState{data: %Data{next: []}} = state) do
    Playback.stop()
    state
  end

  defp load_track(%DependencyState{data: %Data{next: [track | _]}} = state) do
    track
    |> Track.url()
    |> Playback.load_url()

    state
  end

  @spec move_to_next_track(Data.t()) :: Data.t()
  defp move_to_next_track(%Data{next: []} = data), do: data

  defp move_to_next_track(%Data{previous: previous, next: [current | next]}) do
    %Data{previous: [current | previous], next: next}
  end

  @spec move_to_previous_track(Data.t()) :: Data.t()
  defp move_to_previous_track(%Data{previous: []} = data), do: data

  defp move_to_previous_track(%Data{previous: [current | previous], next: next}) do
    %Data{previous: previous, next: [current | next]}
  end
end