defmodule Oban.Met.Values.Gauge do
  @moduledoc """
  One or more non-negative integers captured over time.
  """

  alias __MODULE__, as: Gauge

  @encoder if Code.ensure_loaded?(JSON.Encoder), do: JSON.Encoder, else: Jason.Encoder

  @derive @encoder
  defstruct data: 0

  @type t :: %__MODULE__{data: [non_neg_integer()]}

  @doc """
  Initialize a new gauge.

  ## Examples

      iex> Gauge.new(1).data
      [1]

      iex> Gauge.new([1, 2]).data
      [1, 2]
  """
  @spec new(non_neg_integer() | [non_neg_integer()]) :: t()
  def new(data) when is_integer(data) and data > 0 do
    %Gauge{data: [data]}
  end

  def new(data) when is_list(data) do
    %Gauge{data: data}
  end

  @doc """
  Add to the gauge.

  ## Examples

      iex> 1
      ...> |> Gauge.new()
      ...> |> Gauge.add(1)
      ...> |> Gauge.add(1)
      ...> |> Map.fetch!(:data)
      [3]
  """
  @spec add(t(), pos_integer()) :: t()
  def add(%Gauge{data: [head | tail]}, value) when value > 0 do
    %Gauge{data: [head + value | tail]}
  end

  @doc """
  Merges two gauges into a one.

  ## Examples

  Merging two gauge retains all values:

      iex> gauge_1 = Gauge.new([1, 2])
      ...> gauge_2 = Gauge.new([2, 3])
      ...> Gauge.merge(gauge_1, gauge_2).data
      [1, 2, 2, 3]
  """
  @spec merge(t(), t()) :: t()
  def merge(%Gauge{data: data_1}, %Gauge{data: data_2}) do
    %Gauge{data: data_1 ++ data_2}
  end

  @doc """
  Compute the quantile for a gauge.

  ## Examples

  With single values:

      iex> Gauge.quantile(Gauge.new([1, 2, 3]), 1.0)
      3

      iex> Gauge.quantile(Gauge.new([1, 2, 3]), 0.5)
      2
  """
  @spec quantile(t(), float()) :: non_neg_integer()
  def quantile(%Gauge{data: data}, ntile) do
    length = length(data) - 1

    data
    |> Enum.sort()
    |> Enum.at(floor(length * ntile))
  end

  @doc """
  Compute the sum for a gauge.

  ## Examples

      iex> Gauge.sum(Gauge.new(3))
      3

      iex> Gauge.sum(Gauge.new([1, 2, 3]))
      6
  """
  @spec sum(t()) :: non_neg_integer()
  def sum(%Gauge{data: data}), do: Enum.sum(data)

  @doc """
  Union two gauges into a single value.

  ## Examples

  Merging two gauge results in a single value:

      iex> gauge_1 = Gauge.new([1, 2])
      ...> gauge_2 = Gauge.new([2, 3])
      ...> Gauge.union(gauge_1, gauge_2).data
      [8]
  """
  @spec union(t(), t()) :: t()
  def union(%Gauge{data: data_1}, %Gauge{data: data_2}) do
    data =
      (data_1 ++ data_2)
      |> Enum.sum()
      |> List.wrap()

    %Gauge{data: data}
  end

  @doc """
  Initialize a gauge struct from a stringified map, e.g. encoded JSON.

  ## Examples

      iex> Gauge.new(2)
      ...> |> Oban.JSON.encode!()
      ...> |> Oban.JSON.decode!()
      ...> |> Gauge.from_map()
      ...> |> Map.fetch!(:data)
      [2]
  """
  @spec from_map(%{optional(String.t()) => term()}) :: t()
  def from_map(%{"data" => data}), do: %Gauge{data: data}
end
