defmodule Beamchmark.Suite.Measurements.SchedulerInfo do
  @moduledoc """
  Module representing different statistics about scheduler usage.
  """

  use Bunch.Access

  alias Beamchmark.Math

  @type sched_usage_t :: %{
          (sched_id :: integer()) =>
            {util :: float(), percent :: Math.percent_t() | Math.percent_diff_t()}
        }
  @type total_sched_usage_t ::
          {util :: float(), percent :: Math.percent_t() | Math.percent_diff_t()}
  @type weighted_sched_usage_t ::
          {util :: float(), percent :: Math.percent_t() | Math.percent_diff_t()}

  @type t :: %__MODULE__{
          normal: sched_usage_t(),
          cpu: sched_usage_t(),
          io: sched_usage_t(),
          total_normal: total_sched_usage_t(),
          total_cpu: total_sched_usage_t(),
          total_io: total_sched_usage_t(),
          total: total_sched_usage_t(),
          weighted: weighted_sched_usage_t()
        }

  defstruct normal: %{},
            cpu: %{},
            io: %{},
            total_normal: {0, 0},
            total_cpu: {0, 0},
            total_io: {0, 0},
            total: {0, 0},
            weighted: {0, 0}

  # converts output of `:scheduler.utilization/1 to `SchedulerInfo.t()`
  @spec from_sched_util_result(any()) :: t()
  def from_sched_util_result(sched_util_result) do
    scheduler_info =
      sched_util_result
      |> Enum.reduce(%__MODULE__{}, fn
        {sched_type, sched_id, util, percent}, scheduler_info
        when sched_type in [:normal, :cpu, :io] ->
          # convert from charlist to string, remove trailing percent sign and convert to float
          percent = String.slice("#{percent}", 0..-2//1) |> String.to_float()
          put_in(scheduler_info, [sched_type, sched_id], {util, percent})

        {type, util, percent}, scheduler_info when type in [:total, :weighted] ->
          percent = String.slice("#{percent}", 0..-2//1) |> String.to_float()
          put_in(scheduler_info[type], {util, percent})
      end)

    total_normal = typed_total(scheduler_info.normal)
    total_cpu = typed_total(scheduler_info.cpu)
    total_io = typed_total(scheduler_info.io)

    %__MODULE__{
      scheduler_info
      | total_normal: total_normal,
        total_cpu: total_cpu,
        total_io: total_io
    }
  end

  @spec diff(t(), t()) :: t()
  def diff(base, new) do
    normal_diff = sched_usage_diff(base.normal, new.normal)
    cpu_diff = sched_usage_diff(base.cpu, new.cpu)
    io_diff = sched_usage_diff(base.io, new.io)

    total_normal_diff = sched_usage_diff(base.total_normal, new.total_normal)
    total_cpu_diff = sched_usage_diff(base.total_cpu, new.total_cpu)
    total_io_diff = sched_usage_diff(base.total_io, new.total_io)
    total_diff = sched_usage_diff(base.total, new.total)

    weighted_diff = sched_usage_diff(base.weighted, new.weighted)

    %__MODULE__{
      normal: normal_diff,
      cpu: cpu_diff,
      io: io_diff,
      total_normal: total_normal_diff,
      total_cpu: total_cpu_diff,
      total_io: total_io_diff,
      total: total_diff,
      weighted: weighted_diff
    }
  end

  defp typed_total(scheduler_usage) do
    count = scheduler_usage |> Map.keys() |> Enum.count()

    if count != 0 do
      util_sum =
        scheduler_usage
        |> Map.values()
        |> Enum.reduce(0, fn {util, _percent}, util_sum ->
          util_sum + util
        end)

      {util_sum / count, Float.round(util_sum / count * 100, 1)}
    else
      {0, 0}
    end
  end

  defp sched_usage_diff(base, new) when is_map(base) and is_map(new) do
    Enum.zip(base, new)
    |> Map.new(fn
      {{sched_id, {base_util, base_percent}}, {sched_id, {new_util, new_percent}}} ->
        {sched_id, {new_util - base_util, Math.percent_diff(base_percent, new_percent)}}
    end)
  end

  defp sched_usage_diff({base_util, base_percent}, {new_util, new_percent}),
    do: {new_util - base_util, Math.percent_diff(base_percent, new_percent)}
end
