#!/usr/bin/env python3
"""Generate the pow10 lookup table consumed by ``fl::ieee754_string``.

For each ``k`` in ``[K_MIN, K_MAX]`` produces a ``(mantissa: u64, binary_exp: i16)``
pair such that::

    10**k == mantissa * 2**binary_exp     # exact within rounding

with the mantissa normalized so the top bit is set
(``2**63 <= mantissa < 2**64``). The codec multiplies a parsed decimal
mantissa against this table to reach IEEE 754 single-precision bits using
only integer arithmetic -- no libgcc soft-FP. See ``src/fl/stl/ieee754_string.h``
for the integration story.

Run with::

    uv run python ci/tools/gen_pow10_table.py
"""

from __future__ import annotations

import sys
from decimal import Decimal, getcontext


# Single-precision spans ~ 1.4e-45 to ~ 3.4e38. The parser feeds in a raw
# 19-digit integer mantissa, so we cover an extra ~19 decades on the low end
# (K_MIN = -64) to avoid underflowing values like 1234567890123456789e-46
# (~1.23e-28) to zero just because decimal_exp is below the table floor.
# The +/- exponent space is generous so callers can serialize a wide JSON
# numeric range without clamping early.
K_MIN = -64
K_MAX = 38

# 60 decimal digits of working precision is plenty for round-to-nearest
# on a 64-bit mantissa within this exponent range.
getcontext().prec = 60


def normalize(value: Decimal) -> tuple[int, int]:
    """Return ``(mantissa: u64, binary_exp: int)`` with the top bit set."""

    # Find binary_exp such that value * 2**(-binary_exp) is in [2**63, 2**64).
    # value >= 0 always here.
    if value == 0:
        return 0, 0
    log2 = value.ln() / Decimal(2).ln()
    binary_exp = int(log2) - 63
    # Tighten via shifts until the bound holds (handle FP-like rounding edges).
    while True:
        scaled = (value * (Decimal(2) ** Decimal(-binary_exp))).to_integral_value(
            rounding="ROUND_HALF_EVEN"
        )
        mantissa = int(scaled)
        if mantissa >= (1 << 63) and mantissa < (1 << 64):
            return mantissa, binary_exp
        if mantissa >= (1 << 64):
            binary_exp += 1
        else:
            binary_exp -= 1


def main() -> int:
    entries: list[tuple[int, int]] = []
    for k in range(K_MIN, K_MAX + 1):
        v = Decimal(10) ** k
        m, e = normalize(v)
        entries.append((m, e))

    print("// AUTO-GENERATED by ci/tools/gen_pow10_table.py -- do not edit by hand.")
    print(f"// Table indexed by (k - {K_MIN}); k in [{K_MIN}, {K_MAX}].")
    print(
        "// Each entry encodes 10**k ~= mantissa * 2**binary_exp with the"
        " mantissa's top bit set."
    )
    print("")
    print(f"static constexpr int kPow10KMin = {K_MIN};")
    print(f"static constexpr int kPow10KMax = {K_MAX};")
    print(f"static constexpr fl::size kPow10Count = {K_MAX - K_MIN + 1};")
    print("")
    print("static constexpr fl::u64 kPow10Mant[kPow10Count] = {")
    for k, (m, _) in zip(range(K_MIN, K_MAX + 1), entries):
        print(f"    0x{m:016X}u,  // 10**{k}")
    print("};")
    print("")
    print("static constexpr fl::i16 kPow10BExp[kPow10Count] = {")
    for k, (_, e) in zip(range(K_MIN, K_MAX + 1), entries):
        print(f"    {e:>5},  // 10**{k}")
    print("};")
    return 0


if __name__ == "__main__":
    sys.exit(main())
