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

  alias Growth.Calc.Centile
  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 centile 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, {z_score(indicator, growth, l, m, s), centile(indicator, growth, l, m, s)}}
      end)

    %{growth | results: [Map.new([{indicator.measure_name(), result}]) | growth.results]}
  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

  @spec z_score(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) :: number()
  @doc """
  Check `Growth.Calc.ZScore.compute/4`.
  """
  def z_score(indicator, growth, l, m, s) do
    growth
    |> Map.get(indicator.measure_name())
    |> ZScore.compute(l, m, s)
  end

  @spec centile(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) :: number() | :na
  @doc """
  Check `Growth.Calc.Centile.compute/4`.
  """
  def centile(indicator, growth, l, m, s) do
    growth
    |> Map.get(indicator.measure_name())
    |> Centile.compute(l, m, s)
  end
end