defmodule Growth.Score.Scorer do
  @moduledoc """
  Behaviour defining common interface to calculate z-score and percentile for a given measurement.
  """

  alias Growth.Calc.Percentile
  alias Growth.Calc.ZScore

  @callback measure_name() :: atom()

  @spec results(Growth.t(), [module()]) :: Growth.t()
  @doc """
  Add z-score and centile values in growth measurements `results` for each indicator.
  """
  def results(growth, indicators) do
    Enum.reduce(indicators, growth, &result/2)
  end

  @spec result(module(), Growth.t()) :: Growth.t()
  @doc """
  Calculate z-score and percentile values for the given indicator and add them to the growth measurement `results`.
  """
  def result(indicator, growth) do
    result =
      growth
      |> lms(indicator)
      |> Enum.map(fn {precision, {l, m, s}} ->
        {precision, scores(indicator, growth, l, m, s)}
      end)

    %{growth | results: Keyword.put(growth.results, indicator.measure_name(), result)}
  end

  @spec lms(Growth.t(), module()) :: [{String.t(), {number(), number(), number()}}]
  @doc """
  Get the indicaator fitted values of Box-Cox transformation:

  * power (`l`)
  * median (`m`)
  * coefficient of variation (`s`)

  """
  def lms(growth, indicator) do
    [
      {growth.gender, :day, growth.age_in_days},
      {growth.gender, :week, growth.age_in_weeks},
      {growth.gender, :month, growth.age_in_months}
    ]
    |> Enum.map(fn {_, precision, _} = key ->
      case :ets.lookup(indicator, key) do
        [{^key, %{l: l, m: m, s: s}} | _] ->
          {precision, {l, m, s}}

        _ ->
          nil
      end
    end)
    |> Enum.reject(&is_nil/1)
  end

  @doc """
  Calculate the z-score and percentile of an indicator measurement.
  """
  @spec scores(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) ::
          {Growth.measure(), Growth.measure()}
  def scores(indicator, growth, l, m, s) do
    growth
    |> Map.get(indicator.measure_name())
    |> z_score(l, m, s)
    |> then(fn score -> {score, percentile(score)} end)
  end

  @doc """
  Check `Growth.Calc.ZScore.compute/4`.
  """
  @spec z_score(Growth.measure(), ZScore.l(), ZScore.m(), ZScore.s()) :: Growth.measure()
  def z_score(nil, _, _, _) do
    nil
  end

  def z_score(value, l, m, s) do
    ZScore.compute(value, l, m, s)
  end

  @doc """
  Check `Growth.Calc.Percentile.compute/1`.
  """
  @spec percentile(Growth.measure()) :: Growth.measure()
  def percentile(nil) do
    nil
  end

  def percentile(score) do
    Percentile.compute(score)
  end
end
