lib/memorex/scheduler/card_reviewer.ex

defmodule Memorex.Scheduler.CardReviewer do
  @moduledoc """
  `Memorex.Scheduler.CardReviewer` is used by the `MemorexWeb.ReviewLive` Live View to review cards.  `Meorex.Domain.Card`s
  are answered, and in the process `Memorex.Domain.CardLog`s are created.

  Note that in contrast to `Memorex.Scheduler.CardStateMachine`, `Memorex.Scheduler.CardReviewer` writes content to
  the database (the updated `Memorex.Domain.Card`, along with the `Memorex.Domain.CardLog`.)
  """

  alias Timex.Duration
  alias Memorex.Cards
  alias Memorex.Scheduler.{CardStateMachine, Config}
  alias Memorex.Ecto.Repo
  alias Memorex.Domain.{Card, CardLog}

  @doc "Answers a `Memorex.Domain.Card`, and updates it in the database (and also creates a `Memorex.Domain.CardLog`)"
  @spec answer_card_and_create_log_entry(Card.t(), Card.answer_choice(), DateTime.t(), DateTime.t(), Config.t()) :: {Card.t(), CardLog.t()}
  def answer_card_and_create_log_entry(card_before, answer, start_time, time_now, config) do
    card_after = answer_card(card_before, answer, time_now, config)
    time_to_answer = time_to_answer(start_time, time_now, config)
    card_log = CardLog.new(answer, card_before, card_after, time_to_answer) |> Repo.insert!() |> Repo.preload([:card, :note])
    {card_after, card_log}
  end

  @doc "Answers a `Memorex.Domain.Card`, and updates it in the database (note no `Memorex.Domain.CardLog` is created)"
  @spec answer_card(Card.t(), Card.answer_choice(), DateTime.t(), Config.t()) :: Card.t()
  def answer_card(card_before, answer, time_now, config) do
    changes = CardStateMachine.answer_card(card_before, answer, config, time_now)
    Cards.update_card_when_reviewing!(card_before, changes, time_now)
  end

  @doc """
  Computes the time to answer this card.  Note that the time to answer is bracketed by `:min_time_to_answer` and
  `:max_time_to_answer` on `Memorex.Scheduler.Config`.  The thought is that if you walk away from the computer when
  drilling, the stored `time_to_answer` will be stored as at most `:max_time_to_answer`.
  """
  @spec time_to_answer(DateTime.t(), DateTime.t(), Config.t()) :: Duration.t()
  def time_to_answer(start_time, end_time, config) do
    Timex.diff(end_time, start_time, :duration) |> bracket_time_to_answer(config)
  end

  @spec bracket_time_to_answer(Duration.t(), Config.t()) :: Duration.t()
  def bracket_time_to_answer(time_to_answer, config) do
    time_to_answer_in_sec = Duration.to_seconds(time_to_answer)

    if time_to_answer_in_sec > Duration.to_seconds(config.min_time_to_answer) do
      if time_to_answer_in_sec > Duration.to_seconds(config.max_time_to_answer) do
        config.max_time_to_answer
      else
        time_to_answer
      end
    else
      config.min_time_to_answer
    end
  end
end