lib/ex_pomodoro.ex

defmodule ExPomodoro do
  @moduledoc """
  Documentation for `ExPomodoro`.
  """

  alias ExPomodoro.{
    Pomodoro,
    PomodoroServer,
    PomodoroSupervisor
  }

  @type success_response :: {:ok, Pomodoro.t()}
  @type started_response :: {:ok, {:started, pid()}}
  @type already_started_response :: {:ok, {:already_started, Pomodoro.t()}}
  @type already_finished_response :: {:ok, {:already_finished, Pomodoro.t()}}
  @type resumed_response :: {:ok, {:resumed, Pomodoro.t()}}
  @type not_found_response :: {:error, :not_found}

  @doc """
  Returns the `#{ExPomodoro}` child spec. It is intended for appliations to
  add an `#{ExPomodoro}` child spec to their application trees to have an
  `#{ExPomodoro.Supervisor}` started before interacting with the rest of the
  `#{ExPomodoro}` commands.
  """
  @spec child_spec(keyword) :: Supervisor.child_spec()
  defdelegate child_spec(options \\ []), to: ExPomodoro.Supervisor

  @doc """
  This is the main function to start a pomodoro.

  Given an `id` and an optional keyword of options returns a successful response
  if a Pomodoro has been started or resumed. Every successful responses returns
  the current `#{Pomodoro}` struct.

  ### Options

  * `exercise_duration`: The duration in milliseconds of the exercise duration,
  `non_negative_integer()`.
  * `break_duration`: The duration in milliseconds of the break duration,
  `non_negative_integer()`.
  * `rounds`: The number of rounds until a long break, `non_negative_integer()`.

  ### Examples:

      # Start a pomodoro with default options.
      iex> ExPomodoro.start("some id")
      {:ok, %ExPomodoro.Pomodoro{
        id: "some id",
        activity: :exercise,
        exercise_duration: 1_500_000,
        break_duration: 300_000,
        rounds: 4
      }}

      # Start a pomodoro with some options.
      iex> ExPomodoro.start("some id", [
      ...>  exercise_duration: 150_000,
      ...>  break_duration: 25_000,
      ...>  rounds: 8
      ...> ])
      {:ok, %ExPomodoro.Pomodoro{
        id: "some id",
        activity: :exercise,
        exercise_duration: 150_000,
        break_duration: 25_000,
        rounds: 8
      }}

      # Start a pomodoro that is already running.
      iex> ExPomodoro.start("some_id")
      {:ok, {:already_started, %Pomodoro{id: "some id"}}}

      # Start a pomodoro that already finished.
      iex> ExPomodoro.start("some_id")
      {:ok, {:already_finished, %Pomodoro{id: "some id"}}}

      # Start a pomodoro that was paused or finished a break.
      iex> ExPomodoro.start("some_id")
      {:ok, {:resumed, %Pomodoro{id: "some id"}}}

  """
  @spec start(Pomodoro.id(), Pomodoro.opts()) ::
          success_response()
          | already_started_response()
          | already_finished_response()
          | resumed_response()
  def start(id, opts \\ []) do
    with {:ok, {:started, pid}} <- start_child(id, opts),
         {:ok, {^pid, %Pomodoro{} = pomodoro}} <- get_by_id(id) do
      {:ok, pomodoro}
    end
  end

  @doc """
  Generally this function is used to check whether a Pomodoro exists or not.

  Given an `id`, if a Pomodoro exists, a `#{Pomodoro}` struct is returned,
  othwerise returns an error tuple.

  ### Examples:

      # Return a pomodoro.
      iex> ExPomodoro.get("some id")
      {:ok, %ExPomodoro.Pomodoro{id: "some id"}}

      # Get a pomodoro that does not exist.
      iex> ExPomodoro.get("some other id")
      {:error, :not_found}

  """
  @spec get(Pomodoro.id()) :: success_response() | not_found_response()
  def get(id) do
    with {:ok, {_pid, %Pomodoro{} = pomodoro}} <- get_by_id(id) do
      {:ok, pomodoro}
    end
  end

  @doc """
  The Pomodoro timer can be paused using this function. While this function
  will cause a pomodoro to pause, it can still finish by timeout, defined in the
  `#{ExPomodoro.PomodoroServer}` implementation.

  Given an `id`, returns a `#{Pomodoro}` struct or an error tuple if the
  pomodoro does not exist.

  ### Examples:

      # Pause a pomodoro and returns the remaining timeleft to complete the
      # current activity.
      iex> ExPomodoro.pause("some id")
      {:ok, %Pomodoro{id: "some id", activity: :idle, current_duration: timeleft}}

      # Pause a pomodoro that does not exist.
      iex> ExPomodoro.pause("some id")
      {:error, :not_found}

  """
  @spec pause(Pomodoro.id()) :: success_response() | not_found_response()
  def pause(id) do
    with {:ok, {pid, %Pomodoro{}}} <- get_by_id(id),
         {:ok, %{id: ^id, pomodoro: %Pomodoro{} = pomodoro}} <-
           PomodoroServer.pause(pid) do
      {:ok, pomodoro}
    end
  end

  @spec get_by_id(Pomodoro.id()) ::
          {:ok, {pid(), Pomodoro.t()}} | not_found_response()
  defp get_by_id(id) do
    with {pid, %Pomodoro{id: ^id}} <- PomodoroSupervisor.get_child(id),
         %Pomodoro{} = pomodoro <- PomodoroServer.get_state(pid) do
      {:ok, {pid, pomodoro}}
    else
      nil ->
        {:error, :not_found}
    end
  end

  @spec start_child(Pomodoro.id(), Pomodoro.opts()) ::
          started_response()
          | already_started_response()
          | already_finished_response()
          | resumed_response()
  defp start_child(id, opts) do
    case get_by_id(id) do
      {:ok, {pid, %Pomodoro{activity: :idle}}} ->
        {:ok, %Pomodoro{} = pomodoro} = PomodoroServer.resume(pid)
        {:ok, {:resumed, pomodoro}}

      {:ok, {_pid, %Pomodoro{activity: :finished} = pomodoro}} ->
        {:ok, {:already_finished, pomodoro}}

      {:ok, {_pid, %Pomodoro{activity: activity} = pomodoro}}
      when activity in [:exercise, :break] ->
        {:ok, {:already_started, pomodoro}}

      {:error, :not_found} ->
        {:ok, pid} =
          PomodoroSupervisor.start_child(
            PomodoroSupervisor,
            Keyword.merge([id: id], opts)
          )

        {:ok, {:started, pid}}
    end
  end
end