lib/memorex/cards.ex

defmodule Memorex.Cards do
  @moduledoc """
  Functions for interacting with `Memorex.Domain.Card`s.
  """

  import Ecto.Query

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

  @doc "Updates a card, and in the process sets the `:due` field, and also increments the rep count"
  @spec update_card_when_reviewing!(Card.t(), map(), DateTime.t()) :: Card.t()
  def update_card_when_reviewing!(card, changes, time) do
    card
    |> Card.changeset(changes)
    |> Card.set_due_field_in_changeset(time)
    |> Card.increment_reps()
    |> Repo.update!()
  end

  @spec get_card!(Schema.id()) :: Card.t()
  def get_card!(card_id) do
    Repo.get!(Card, card_id) |> Repo.preload([:card_logs, :note])
  end

  @spec update(Card.t(), map()) :: {:ok, Card.t()} | {:error, Ecto.Changeset.t()}
  def update(card, card_params) do
    card |> Card.changeset(card_params) |> Repo.update()
  end

  @spec cards_for_deck(Schema.id(), Keyword.t()) :: Ecto.Query.t()
  def cards_for_deck(deck_id, opts \\ []) do
    query =
      from c in Card,
        join: n in Note,
        on: n.id == c.note_id,
        join: d in Deck,
        on: d.id == n.deck_id,
        where: d.id == ^deck_id

    # There _has_ to be a way to merge in the opts in a general way - find it!!!
    limit = Keyword.get(opts, :limit)
    if limit, do: from(c in query, limit: ^limit), else: query

    # Card
    # |> join(:inner, [c], n in Note, on: c.note_id == n.id)
    # |> join(:inner, [c, n], d in Deck, on: n.deck_id == d.id)
    # |> where([c, n, d], d.id == ^deck_id)
  end

  # @spec create_from_note(Note.t()) :: Note.t()
  @spec create_from_note(Note.t()) :: Ecto.Schema.t()
  def(create_from_note(note)) do
    card1 =
      if note.image_file_path do
        %Card{note: note, note_question_index: nil, note_answer_index: 0}
      else
        %Card{note: note, note_question_index: 0, note_answer_index: 1}
      end

    Repo.insert!(card1)

    if note.bidirectional? do
      card2 = %Card{note: note, note_question_index: 1, note_answer_index: 0}
      Repo.insert!(card2)
    end
  end

  @spec set_new_cards_in_deck_to_learn_cards(Schema.id(), Config.t(), DateTime.t(), Keyword.t()) :: :ok
  def set_new_cards_in_deck_to_learn_cards(deck_id, config, time_now, opts \\ []) do
    limit = Keyword.get(opts, :limit, 20)

    deck_id
    |> cards_for_deck(limit: limit)
    |> where(card_type: :new)
    # The following RANDOM line is untested (but without it cards are definitely NOT random):
    |> order_by(fragment("RANDOM()"))
    |> Repo.all()
    |> Enum.each(&convert_new_card_to_learn_card(&1, config, time_now))
  end

  @doc """
  Converts a `Memorex.Domain.Card` from a `:new` card to a `:learn` card (and in the process creates a
  `Memorex.Domain.CardLog` entry).
  """
  @spec convert_new_card_to_learn_card(Card.t(), Config.t(), DateTime.t()) :: Card.t()
  def convert_new_card_to_learn_card(card_before, config, time_now) do
    updates = CardStateMachine.convert_new_card_to_learn_card(card_before, config, time_now)
    card_after = card_before |> Card.changeset(updates) |> Repo.update!()
    CardLog.new(nil, card_before, card_after, nil) |> Repo.insert!()
    card_after
  end

  @spec where_due(Ecto.Query.t(), DateTime.t()) :: Ecto.Query.t()
  def where_due(query, time_now) do
    query
    |> where([c], c.due <= ^time_now)
  end

  @spec get_one_random_due_card(Schema.id(), DateTime.t()) :: Card.t() | nil
  def get_one_random_due_card(deck_id, time_now) do
    cards_for_deck(deck_id, limit: 1)
    |> where_due(time_now)
    |> where(card_status: :active)
    |> order_by(fragment("RANDOM()"))
    |> preload(:note)
    |> Repo.one()
  end

  @spec count(Schema.id()) :: non_neg_integer()
  def count(deck_id) do
    deck_id
    |> cards_for_deck()
    |> Repo.aggregate(:count, :id)
  end

  @spec count(Schema.id(), Keyword.t()) :: non_neg_integer()
  def count(deck_id, opts) do
    card_type = Keyword.get(opts, :card_type)
    card_status = Keyword.get(opts, :card_status)

    query = deck_id |> cards_for_deck()
    query = if card_type, do: query |> where(card_type: ^card_type), else: query
    query = if card_status, do: query |> where(card_status: ^card_status), else: query

    query |> Repo.aggregate(:count, :id)
  end

  @spec due_count(Schema.id(), DateTime.t()) :: non_neg_integer()
  def due_count(deck_id, time_now) do
    deck_id
    |> cards_for_deck()
    |> where_due(time_now)
    |> Repo.aggregate(:count, :id)
  end

  @spec get_interval_choices(Card.t(), Config.t(), DateTime.t()) :: [{Card.answer_choice(), Duration.t()}]
  def get_interval_choices(card, config, time_now) do
    Card.answer_choices()
    |> Enum.map(fn answer ->
      changes = CardStateMachine.answer_card(card, answer, config, time_now)
      interval = card |> Card.changeset(changes) |> Ecto.Changeset.get_field(:interval)
      {answer, interval}
    end)
  end
end