defmodule Growth.Indicators.Load do
  @moduledoc """
  Load local indicators csv files into ets.

  For each measurement an ets table is created, so the system has the following tables:

  * `Growth.Score.ArmCircumference`
  * `Growth.Score.BMI`
  * `Growth.Score.HeadCircumference`
  * `Growth.Score.Height`
  * `Growth.Score.SubscapularSkinfold`
  * `Growth.Score.TricepsSkinfold`
  * `Growth.Score.Weight`
  * `Growth.Score.WeightForHeight`

  The rows in the csv files are converted to two tuple, representing the a key and value, with the following format:

  * **key**: `{gender, unit, t-value}`
  * **value**: map with the keys:
    * l
    * m
    * s
    * sd3neg
    * sd2neg
    * sd1neg
    * sd0
    * sd1
    * sd2
    * sd3
    * source
    * category

  """

  use Task

  require Logger

  alias Growth.Score

  @measure_to_score [
    arm_circumference_for_age: Score.ArmCircumference,
    bmi_for_age: Score.BMI,
    head_circumference_for_age: Score.HeadCircumference,
    height_for_age: Score.Height,
    subscapular_skinfold_for_age: Score.SubscapularSkinfold,
    triceps_skinfold_for_age: Score.TricepsSkinfold,
    weight_for_age: Score.Weight,
    weight_for_height: Score.WeightForHeight
  ]

  @doc false
  def start_link(_) do
    _ =
      Enum.map(@measure_to_score, fn {_, measure} ->
        create_ets(measure)
      end)

    Task.start_link(__MODULE__, :all, [])
  end

  @spec all :: [[boolean()]]
  @doc """
  Load indicators csv files into their own ets tables.
  """
  def all do
    Logger.debug("load growth indicators")

    :wabanex
    |> Application.app_dir(["priv", "growth", "indicators", "*.csv"])
    |> Path.wildcard()
    |> Enum.map(&create_ets_from_filename/1)
    |> Enum.map(&Task.async(__MODULE__, :load_measure, [&1]))
    |> Task.await_many()
  end

  @spec create_ets(module()) :: module()
  @doc """
  Create a public ets table for the growth module, using it as the table name.

  Returns the given module.
  """
  def create_ets(measure) do
    try do
      :ets.new(measure, [:set, :public, :named_table])
    rescue
      _ ->
        nil
    end

    measure
  end

  @spec create_ets_from_filename(String.t()) :: {atom(), String.t()}
  @doc """
  Create ets table based on filename and return a tuple with the ets table name and filename.
  """
  def create_ets_from_filename(filename) do
    measure =
      filename
      |> Path.basename()
      |> Path.rootname()
      |> String.to_atom()
      |> then(&Keyword.get(@measure_to_score, &1, &1))
      |> create_ets()

    {measure, filename}
  end

  @spec load_measure({atom(), String.t()}) :: [boolean()]
  @doc """
  Read, convert, and load a measure/filename into the proper ets table.
  """
  def load_measure({measure, filename}) do
    Logger.debug("load data from #{filename} into #{measure}")

    filename
    |> read()
    |> convert()
    |> load(measure)
  end

  @spec read(String.t()) :: Enumerable.t()
  @doc false
  def read(filename) do
    filename
    |> File.stream!()
    |> IndicatorParser.parse_stream()
  end

  @spec convert(Enumerable.t()) :: Enumerable.t()
  @doc false
  def convert(data) do
    Stream.map(data, fn [
                          source,
                          category,
                          gender,
                          unit,
                          t,
                          l,
                          m,
                          s,
                          sd3neg,
                          sd2neg,
                          sd1neg,
                          sd0,
                          sd1,
                          sd2,
                          sd3
                        ] ->
      converted_t =
        if unit in ~w(day week month) do
          as_integer(t)
        else
          as_float(t)
        end

      key = {String.to_atom(gender), String.to_atom(unit), converted_t}

      value = %{
        l: as_float(l),
        m: as_float(m),
        s: as_float(s),
        sd3neg: as_float(sd3neg),
        sd2neg: as_float(sd2neg),
        sd1neg: as_float(sd1neg),
        sd0: as_float(sd0),
        sd1: as_float(sd1),
        sd2: as_float(sd2),
        sd3: as_float(sd3),
        source: source,
        category: category
      }

      {key, value}
    end)
  end

  @spec load(Enumerable.t(), atom()) :: [boolean()]
  @doc false
  def load(data, ets_table) do
    Enum.map(data, fn {key, value} ->
      :ets.insert_new(ets_table, {key, value})
    end)
  end

  defp as_integer(value), do: value |> Integer.parse() |> elem(0)

  defp as_float(value), do: value |> Float.parse() |> elem(0)
end
