defmodule Benchee.Conversion.Memory do
  @moduledoc """
  Unit scaling for memory converting from bytes to kilobytes and others.

  Only Benchee plugins should use this code.
  """

  alias Benchee.Conversion.{Format, Scale, Unit}

  @behaviour Scale
  @behaviour Format

  @bytes_per_kilobyte 1024
  @bytes_per_megabyte @bytes_per_kilobyte * @bytes_per_kilobyte
  @bytes_per_gigabyte @bytes_per_megabyte * @bytes_per_kilobyte
  @bytes_per_terabyte @bytes_per_gigabyte * @bytes_per_kilobyte

  @units_map %{
    terabyte: %Unit{
      name: :terabyte,
      magnitude: @bytes_per_terabyte,
      label: "TB",
      long: "Terabytes"
    },
    gigabyte: %Unit{
      name: :gigabyte,
      magnitude: @bytes_per_gigabyte,
      label: "GB",
      long: "Gigabytes"
    },
    megabyte: %Unit{
      name: :megabyte,
      magnitude: @bytes_per_megabyte,
      label: "MB",
      long: "Megabytes"
    },
    kilobyte: %Unit{
      name: :kilobyte,
      magnitude: @bytes_per_kilobyte,
      label: "KB",
      long: "Kilobytes"
    },
    byte: %Unit{
      name: :byte,
      magnitude: 1,
      label: "B",
      long: "Bytes"
    }
  }

  @units Map.values(@units_map)

  @type unit_atom :: :byte | :kilobyte | :megabyte | :gigabyte | :terabyte
  @type any_unit :: unit_atom | Unit.t()

  @doc """
  Converts a value for a specified %Unit or unit atom and converts it to the equivalent of another unit of measure.

  ## Examples

    iex> {value, unit} = convert({1024, :kilobyte}, :megabyte)
    ...> value
    1.0
    iex> unit.name
    :megabyte

    iex> current_unit = unit_for(:kilobyte)
    ...> {value, unit} = convert({1024, current_unit}, :megabyte)
    ...> value
    1.0
    iex> unit.name
    :megabyte
  """
  @spec convert({number, any_unit}, any_unit) :: Scale.scaled_number()
  def convert(number_and_unit, desired_unit) do
    Scale.convert(number_and_unit, desired_unit, __MODULE__)
  end

  # Scaling functions

  @doc """
  Scales a memory value in bytes into a larger unit if appropriate

  ## Examples

    iex> {value, unit} = scale(1)
    ...> value
    1.0
    iex> unit.name
    :byte

    iex> {value, unit} = scale(1_234)
    ...> value
    1.205078125
    iex> unit.name
    :kilobyte

    iex> {value, unit} = scale(11_234_567_890.123)
    ...> value
    10.463006692121736
    iex> unit.name
    :gigabyte

    iex> {value, unit} = scale(1_111_234_567_890.123)
    ...> value
    1.0106619519229962
    iex> unit.name
    :terabyte
  """
  def scale(memory) when memory >= @bytes_per_terabyte do
    scale_with_unit(memory, :terabyte)
  end

  def scale(memory) when memory >= @bytes_per_gigabyte do
    scale_with_unit(memory, :gigabyte)
  end

  def scale(memory) when memory >= @bytes_per_megabyte do
    scale_with_unit(memory, :megabyte)
  end

  def scale(memory) when memory >= @bytes_per_kilobyte do
    scale_with_unit(memory, :kilobyte)
  end

  def scale(memory) do
    scale_with_unit(memory, :byte)
  end

  # Helper function for returning a tuple of {value, unit}
  defp scale_with_unit(nil, _) do
    {nil, nil}
  end

  defp scale_with_unit(memory, unit) do
    {scale(memory, unit), unit_for(unit)}
  end

  @doc """
  Get a unit by its atom representation. If handed already a %Unit{} struct it
  just returns it.

  ## Examples

      iex> unit_for(:gigabyte)
      %Benchee.Conversion.Unit{
        name: :gigabyte,
        magnitude: 1_073_741_824,
        label: "GB",
        long: "Gigabytes"
      }

      iex> unit_for(%Benchee.Conversion.Unit{
      ...>   name: :gigabyte,
      ...>   magnitude: 1_073_741_824,
      ...>   label: "GB",
      ...>   long: "Gigabytes"
      ...> })
      %Benchee.Conversion.Unit{
        name: :gigabyte,
        magnitude: 1_073_741_824,
        label: "GB",
        long: "Gigabytes"
      }
  """
  def unit_for(unit) do
    Scale.unit_for(@units_map, unit)
  end

  def units do
    @units
  end

  @doc """
  Scales a memory value in bytes into a value in the specified unit

  ## Examples

      iex> scale(12345, :kilobyte)
      12.0556640625

      iex> scale(12345, :megabyte)
      0.011773109436035156

      iex> scale(123_456_789, :gigabyte)
      0.11497809458523989

  """
  def scale(count, unit) do
    Scale.scale(count, unit, __MODULE__)
  end

  @doc """
  Finds the best unit for a list of memory units. By default, chooses the most common
  unit. In case of tie, chooses the largest of the most common units.

  Pass `[strategy: :smallest]` to always return the smallest unit in the list.
  Pass `[strategy: :largest]` to always return the largest unit in the list.
  Pass `[strategy: :best]` to always return the most frequent unit in the list.
  Pass `[strategy: :none]` to always return :byte.

  ## Examples

      iex> best([23, 23_000, 34_000, 2_340_000]).name
      :kilobyte

      iex> best([23, 23_000, 34_000, 2_340_000, 3_450_000]).name
      :megabyte

      iex> best([23, 23_000, 34_000, 2_340_000], strategy: :smallest).name
      :byte

      iex> best([23, 23_000, 34_000, 2_340_000], strategy: :largest).name
      :megabyte
  """
  def best(list, opts \\ [strategy: :best])

  def best(list, opts) do
    Scale.best_unit(list, __MODULE__, opts)
  end

  @doc """
  The most basic unit in which memory occur, byte.

  ## Examples

      iex> base_unit().name
      :byte

  """
  def base_unit, do: unit_for(:byte)

  @doc """
  Formats a memory as a string, with a unit label.

  To specify the unit, pass a tuple of `{value, unit_atom}` like `{1_234, :kilobyte}`.

  ## Examples

      iex> format(45_678.9)
      "44.61 KB"

      iex> format(45.6789)
      "45.68 B"

      iex> format({45.6789, :kilobyte})
      "45.68 KB"

      iex> format(
      ...>   {45.6789,
      ...>    %Benchee.Conversion.Unit{
      ...>      long: "Kilobytes",
      ...>      magnitude: 1024,
      ...>      label: "KB"
      ...>    }}
      ...> )
      "45.68 KB"

  """
  def format(memory) do
    Format.format(memory, __MODULE__)
  end

  @doc """
  Formats in a more human way, where multiple units are used.

  ## Examples

      iex> format_human(45_678.9)
      "44 KB 622.90 B"

      iex> format_human(1024 * 1024 * 1024)
      "1 GB"
  """
  def format_human(memory) do
    Format.format_human(memory, __MODULE__)
  end
end
