defmodule Growth do
  @moduledoc """
  Follow up child growth from 0 to 19 years, calculating z-scores for the following measurements:

  * weight between ages 0 and 10 years
  * height between ages 0 and 19 years
  * body mass index (bmi) between ages 0 and 19 years
  * head circumference between ages 0 and 5 years
  * arm circumference between ages 3 months and 5 years
  * subscapular skinfold between ages 3 months and 5 years
  * triceps skinfold between ages 3 months and 5 years
  """

  alias __MODULE__.Calc.Age
  alias __MODULE__.Calc.BMI
  alias __MODULE__.Score

  @type gender :: :male | :female

  @type measure :: number() | nil

  @type opts :: [
          date_of_measurement: Date.t(),
          weight: measure(),
          height: measure(),
          arm_circumference: measure(),
          head_circumference: measure(),
          subscapular_skinfold: measure(),
          triceps_skinfold: measure()
        ]

  @type t :: %__MODULE__{
          name: String.t(),
          gender: gender(),
          date_of_birth: Date.t(),
          date_of_measurement: Date.t(),
          age_in_days: pos_integer(),
          age_in_weeks: pos_integer(),
          age_in_months: pos_integer(),
          weight: measure(),
          height: measure(),
          arm_circumference: measure(),
          head_circumference: measure(),
          subscapular_skinfold: measure(),
          triceps_skinfold: measure(),
          bmi: measure(),
          results: list()
        }

  @enforce_keys [:name, :gender, :date_of_birth]
  defstruct [
    :name,
    :gender,
    :date_of_birth,
    :date_of_measurement,
    :age_in_days,
    :age_in_weeks,
    :age_in_months,
    :weight,
    :height,
    :arm_circumference,
    :head_circumference,
    :subscapular_skinfold,
    :triceps_skinfold,
    :bmi,
    :results
  ]

  @valid_measures [
    :date_of_measurement,
    :weight,
    :height,
    :arm_circumference,
    :head_circumference,
    :subscapular_skinfold,
    :triceps_skinfold
  ]

  @doc """
  Create a new growth measurement for a children with name, gender, date of birth, and the following optional arguments:

  * `:date_of_measurement`: date when the measures were collected, defaults to today.
  * `:weight`: weight in kilograms, defaults to `nil`.
  * `:height`: height in centimeters, defaults to `nil`.
  * `:arm_circumference`: arm circumference in centimeters, defaults to `nil`.
  * `:head_circumference`: head circumference in centimeters, defaults to `nil`.
  * `:subscapular_skinfold`: subscapular skinfold in milimeters, defaults to `nil`.
  * `:triceps_skinfold`: triceps skinfold in milimeters, defaults to `nil`.

  ## Examples

      iex> Growth.new(
      ...>   "child a",
      ...>   :male,
      ...>   ~D[2024-01-01],
      ...>   date_of_measurement: ~D[2024-04-01],
      ...>   weight: 8,
      ...>   height: 65.4,
      ...>   arm_circumference: 15.5,
      ...>   head_circumference: 42.8,
      ...>   subscapular_skinfold: 10.9,
      ...>   triceps_skinfold: 13.5
      ...> )
      %Growth{
        name: "child a",
        gender: :male,
        date_of_birth: ~D[2024-01-01],
        date_of_measurement: ~D[2024-04-01],
        age_in_days: 91,
        age_in_weeks: 13,
        age_in_months: 2,
        weight: 8,
        height: 65.4,
        arm_circumference: 15.5,
        head_circumference: 42.8,
        subscapular_skinfold: 10.9,
        triceps_skinfold: 13.5,
        bmi: 18.703999850368,
        results: [
          head_circumference: [
            day: {1.945484886994137, 0.9741416765426315},
            week: {1.945484886994137, 0.9741416765426315},
            month: {3.130859582465616, 0.9991285226182205}
          ],
          arm_circumference: [day: {1.9227031505630465, 0.9727413295221268}],
          subscapular_skinfold: [day: {1.9437372448689536, 0.9740364275897885}],
          triceps_skinfold: [day: {1.950277062993091, 0.974428447506235}],
          weight: [
            day: {1.982458622036091, 0.9762860329545557},
            week: {1.982458622036091, 0.9762860329545557},
            month: {3.0355951313091745, 0.9987996926038037}
          ],
          height: [
            day: {1.956263992749136, 0.9747829682259178},
            week: {1.956263992749136, 0.9747829682259178},
            month: {3.4867331002754054, 0.9997555204670452}
          ],
          bmi: [
            day: {1.1977344927294398, 0.8844898016950435},
            week: {1.1977344927294398, 0.8844898016950435},
            month: {1.5837461190318038, 0.9433742474306444}
          ]
        ]
      }

  """
  @spec new(String.t(), gender(), Date.t(), opts()) :: t()
  def new(name, gender, date_of_birth, opts \\ []) do
    default_opts()
    |> Enum.reduce(
      %__MODULE__{
        name: name,
        gender: gender,
        date_of_birth: date_of_birth,
        results: []
      },
      fn {key, default_value}, measurement ->
        opts
        |> Keyword.get(key, default_value)
        |> then(&Map.put(measurement, key, &1))
      end
    )
    |> with_age_in_days()
    |> with_age_in_weeks()
    |> with_age_in_months()
    |> with_bmi()
    |> with_results()
  end

  for precision <- [:day, :week, :month] do
    @doc """
    Add age with given precision in growth measurement.
    """
    @spec unquote(:"with_age_in_#{precision}s")(t()) :: t()

    def unquote(:"with_age_in_#{precision}s")(
          %__MODULE__{date_of_birth: date_of_birth, date_of_measurement: date_of_measurement} =
            growth
        )
        when not is_nil(date_of_birth) and not is_nil(date_of_measurement) do
      age = Age.calculate(unquote(precision), date_of_birth, date_of_measurement)
      Map.put(growth, unquote(:"age_in_#{precision}s"), age)
    end

    def unquote(:"with_age_in_#{precision}s")(%__MODULE__{date_of_birth: date_of_birth} = growth)
        when not is_nil(date_of_birth) do
      unquote(:"with_age_in_#{precision}s")(%{growth | date_of_measurement: Date.utc_today()})
    end

    def unquote(:"with_age_in_#{precision}s")(growth) do
      growth
    end
  end

  def with_bmi(%__MODULE__{weight: weight, height: height} = growth)
      when is_number(weight) and is_number(height) do
    %{growth | bmi: BMI.calculate(:metric, weight, height)}
  end

  def with_bmi(growth) do
    growth
  end

  def with_results(growth) do
    Score.Scorer.results(growth, [
      Score.BMI,
      Score.Height,
      Score.Weight,
      Score.TricepsSkinfold,
      Score.SubscapularSkinfold,
      Score.ArmCircumference,
      Score.HeadCircumference
    ])
  end

  defp default_opts do
    Enum.map(@valid_measures, fn
      :date_of_measurement = key ->
        {key, Date.utc_today()}

      key ->
        {key, nil}
    end)
  end
end
