defmodule Microwaveprop.Weather.Grib2.SimplePacking do
  @moduledoc false

  @doc """
  Extract a single value from GRIB2 simple-packed data at the given grid index.

  Formula: value = (R + X * 2^E) * 10^(-D)

  Where R = reference_value, E = binary_scale, D = decimal_scale,
  X = raw packed integer at the given index.
  """
  @spec extract_value(map(), binary(), non_neg_integer()) :: {:ok, float()} | {:error, :index_out_of_range}
  def extract_value(%{bits_per_value: 0} = params, _data, _index) do
    {:ok, params.reference_value * :math.pow(10, -params.decimal_scale)}
  end

  def extract_value(params, data, index) do
    %{
      reference_value: r,
      binary_scale: e,
      decimal_scale: d,
      bits_per_value: n,
      num_data_points: num
    } = params

    if index < 0 or index >= num do
      {:error, :index_out_of_range}
    else
      x = extract_bits(data, index, n)
      value = (r + x * :math.pow(2, e)) * :math.pow(10, -d)
      {:ok, value}
    end
  end

  @doc """
  Extract multiple values from GRIB2 simple-packed data at the given grid indices.

  Returns `{:ok, %{index => value}}`. Out-of-range indices are silently skipped.
  """
  @spec extract_values(map(), binary(), [non_neg_integer()]) :: {:ok, %{non_neg_integer() => float()}}
  def extract_values(%{bits_per_value: 0} = params, _data, indices) do
    value = params.reference_value * :math.pow(10, -params.decimal_scale)
    {:ok, Map.new(indices, fn i -> {i, value} end)}
  end

  def extract_values(params, data, indices) do
    %{
      reference_value: r,
      binary_scale: e,
      decimal_scale: d,
      bits_per_value: n,
      num_data_points: num
    } = params

    factor_2e = :math.pow(2, e)
    factor_10d = :math.pow(10, -d)

    results =
      Enum.reduce(indices, %{}, fn index, acc ->
        if index >= 0 and index < num do
          x = extract_bits(data, index, n)
          value = (r + x * factor_2e) * factor_10d
          Map.put(acc, index, value)
        else
          acc
        end
      end)

    {:ok, results}
  end

  defp extract_bits(data, index, bits_per_value) do
    bit_offset = index * bits_per_value
    byte_offset = div(bit_offset, 8)
    remainder = rem(bit_offset, 8)

    <<_skip::binary-size(byte_offset), _pad::size(remainder), x::size(bits_per_value)-unsigned-big, _rest::bitstring>> =
      data

    x
  end
end
