lib/memorex/scheduler/config.ex

defmodule Memorex.Scheduler.Config do
  @moduledoc """
  The configuration for the [Anki SM-2 algorithm](https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html).

  See a detailed list of the Anki settings in this [Anki forum](https://forums.ankiweb.net/t/deck-options-explained/213)
  The [Anki manual](https://docs.ankiweb.net/deck-options.html) also contains a lot of information.

  These values are modified by deck config `toml` files, and also by environment variables (see `config/runtime.exs`).


   | Memorex Setting           | Anki Setting                    |    Anki Default |
   |:--------------------------|:--------------------------------|----------------:|
   | new_cards_per_day         | new cards per day               |              20 |
   | max_reviews_per_day       | maximum reviews per day         |             200 |
   | -                         | -                               |               - |
   | learn_ahead_time_interval | learn ahead time (in settings?) |      20 minutes |
   | -                         | -                               |               - |
   | learn_steps               | learning steps                  | [1 min, 10 min] |
   | graduating_interval_good  | graduating interval             |           1 day |
   | graduating_interval_easy  | easy interval                   |          4 days |
   | -                         | -                               |               - |
   | relearn_steps             | relearning steps                |        [10 min] |
   | -                         | -                               |               - |
   | initial_ease              | starting ease                   |             2.5 |
   | -                         | -                               |               - |
   | easy_multiplier           | easy bonus                      |             1.3 |
   | hard_multiplier           | hard interval                   |             1.2 |
   | lapse_multiplier          | new interval (? I think so)     |             0.0 |
   | interval_multiplier       | interval modifier               |             1.0 |
   | -                         | -                               |               - |
   | ease_again                | <not in settings>               |            -0.2 |
   | ease_hard                 | <not in settings>               |           -0.15 |
   | ease_good                 | <not in settings>               |             0.0 |
   | ease_easy                 | <not in settings>               |            0.15 |
   | ease_minimum              | <not in settings>               |             1.3 |
   | -                         | -                               |               - |
   | max_review_interval       | maximum interval                |       100 years |
   | min_review_interval       | minimum interval                |           1 day |
   | -                         | -                               |               - |
   | leech_threshold           | leech threshhold                |        8 lapses |
   | -                         | -                               |               - |
   | min_time_to_answer        | <not in settings>               |           1 sec |
   | max_time_to_answer        | maximum answer seconds          |           1 min |
   | -                         | -                               |               - |
   | relearn_easy_adj          | NOT SURE WHERE THIS IS FROM     |           1 day |
   | -                         | -                               |               - |
   | timezone                  | timezone                        |           1 day |

  """

  alias Timex.Duration

  @type t() :: %__MODULE__{
          new_cards_per_day: nil | non_neg_integer(),
          max_reviews_per_day: nil | non_neg_integer(),
          #
          learn_ahead_time_interval: nil | Duration.t(),
          #
          learn_steps: nil | [Duration.t()],
          graduating_interval_good: nil | Duration.t(),
          graduating_interval_easy: nil | Duration.t(),
          #
          relearn_steps: nil | [Duration.t()],
          #
          initial_ease: nil | float(),
          #
          easy_multiplier: nil | float(),
          hard_multiplier: nil | float(),
          lapse_multiplier: nil | float(),
          interval_multiplier: nil | float(),
          #
          ease_again: nil | float(),
          ease_hard: nil | float(),
          ease_good: nil | float(),
          ease_easy: nil | float(),
          ease_minimum: nil | float(),
          #
          max_review_interval: nil | Duration.t(),
          min_review_interval: nil | Duration.t(),
          #
          #
          leech_threshold: nil | non_neg_integer(),
          #
          min_time_to_answer: nil | Duration.t(),
          max_time_to_answer: nil | Duration.t(),
          #
          relearn_easy_adj: nil | Duration.t(),
          #
          timezone: nil | String.t()
        }

  defstruct [
    :new_cards_per_day,
    :max_reviews_per_day,
    #
    :learn_ahead_time_interval,
    #
    :learn_steps,
    :graduating_interval_good,
    :graduating_interval_easy,
    #
    :relearn_steps,
    #
    :initial_ease,
    #
    :easy_multiplier,
    :hard_multiplier,
    :lapse_multiplier,
    :interval_multiplier,
    #
    :ease_again,
    :ease_hard,
    :ease_good,
    :ease_easy,
    :ease_minimum,
    #
    :max_review_interval,
    :min_review_interval,
    #
    :leech_threshold,
    #
    :min_time_to_answer,
    :max_time_to_answer,
    #
    :relearn_easy_adj,
    #
    :timezone
  ]

  @duration_fields [
    :learn_ahead_time_interval,
    #
    :graduating_interval_good,
    :graduating_interval_easy,
    #
    :max_review_interval,
    :min_review_interval,
    #
    :min_time_to_answer,
    :max_time_to_answer,
    #
    :relearn_easy_adj
  ]

  @duration_array_fields [:learn_steps, :relearn_steps]

  @doc """
  Returns the default `Memorex.Schedler.Config` as returned from the application environment (e.g., environment
  variables.  Converts from the application env map to an actual `Memorex.Scheduler.Config` struct.
  """
  @spec default() :: map()
  def default() do
    Application.get_env(:memorex, __MODULE__)
    |> Enum.reduce(%__MODULE__{}, fn {key, value}, acc ->
      Map.put(acc, key, value)
    end)
  end

  @doc """
  Merges in a map into the `Memorex.Scheduler.Config`.  This is used to merge in deck-specific configuration from
  toml config files.
  """
  @spec merge(map(), t()) :: t()
  def merge(default_config, deck_config) do
    config = Map.merge(default_config, atomize_keys(deck_config))

    config =
      @duration_fields
      |> Enum.reduce(config, fn field_name, config ->
        Map.put(config, field_name, convert_string_to_duration(Map.get(config, field_name)))
      end)

    @duration_array_fields
    |> Enum.reduce(config, fn field_name, config ->
      converted_array_values = Map.get(config, field_name) |> Enum.map(&convert_string_to_duration(&1))
      Map.put(config, field_name, converted_array_values)
    end)
  end

  @spec atomize_keys(map()) :: map()
  defp atomize_keys(map), do: map |> Enum.into(%{}, fn {key, value} -> {String.to_atom(key), value} end)

  @spec convert_string_to_duration(String.t() | Duration.t()) :: Duration.t()
  defp convert_string_to_duration(duration) when is_binary(duration), do: Duration.parse!(duration)
  defp convert_string_to_duration(duration), do: duration
end