/**
 * # fair/detmath — vendored, bit-deterministic `exp` / `log` for the certified-grade path
 *
 * ## Why this file exists
 * A certified Grade must be **byte-identical across every runtime we grade on** — V8 on Node,
 * V8 on Cloudflare Workers, and JSC on Bun. JavaScript's `+ − × ÷` and `Math.sqrt` are pinned
 * by IEEE-754 (correctly-rounded binary64) and reproduce bit-for-bit everywhere. The *library*
 * transcendentals `Math.exp` and `Math.log` are **not** pinned — the ECMAScript spec explicitly
 * allows implementation-defined results, so V8 and JSC can (and do) return answers that differ
 * in the last ulp. On the certified-grade path that is a cross-runtime float divergence: two
 * engines could stamp two different grades on the same tape.
 *
 * This module removes that risk by **vendoring a fixed, portable double-precision `exp`/`log`**
 * (the SunPro / fdlibm `__ieee754_exp` and `__ieee754_log`, the same code musl and the BSDs
 * ship) reimplemented in terms of *only* the pinned IEEE-754 primitives. Because every operation
 * inside is a correctly-rounded binary64 op, the result is the same bits on every conforming
 * engine — which the checked-in cross-engine golden bit-vectors (`tests/detmath.golden.test.ts`)
 * assert.
 *
 * These are drop-in replacements for `Math.exp` / `Math.log` on the pricing + fill-hazard path
 * ({@link ../fair/black76.ts}, {@link ../fill/model.ts}). They are pure (no I/O, no clock, no
 * RNG — RUNTIME §0); the shared scratch buffer is single-threaded mutable state used only to read
 * a double's raw words, never observable output.
 *
 * ## Provenance
 * Ported from FreeBSD/SunPro `msun` `e_exp.c` and `e_log.c` (fdlibm). Original notice:
 * ```
 * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
 * Developed at SunPro, a Sun Microsystems, Inc. business. Permission to use, copy, modify, and
 * distribute this software is freely granted, provided that this notice is preserved.
 * ```
 * The algorithm, constants and comments are Sun's; only the surface syntax is TypeScript.
 */

// ─────────────────────────────────────────────────────────────────────────────
// Raw double ⇄ 32-bit word access (the fdlibm GET/SET_HIGH_WORD primitives)
//
// A single 8-byte scratch buffer aliased as a float64 and two uint32s lets us read/write the
// high and low 32-bit halves of a double. `HI`/`LO` are resolved once from the platform's
// endianness so the port is correct on both LE and (hypothetical) BE hosts — the words are what
// matters, never their byte order.
// ─────────────────────────────────────────────────────────────────────────────

const _buf = new ArrayBuffer(8);
const _f64 = new Float64Array(_buf);
const _u32 = new Uint32Array(_buf);

// 1.0 === 0x3FF0000000000000: the word holding 0x3ff00000 is the HIGH word.
_f64[0] = 1.0;
const HI = _u32[1] === 0x3ff00000 ? 1 : 0;
const LO = 1 - HI;

/** High 32 bits of `x` as an unsigned word (fdlibm `GET_HIGH_WORD`). */
function getHighWord(x: number): number {
  _f64[0] = x;
  return _u32[HI]! >>> 0;
}

/** Low 32 bits of `x` as an unsigned word (fdlibm `GET_LOW_WORD`). */
function getLowWord(x: number): number {
  _f64[0] = x;
  return _u32[LO]! >>> 0;
}

/** Return `x` with its high 32 bits replaced by `hw` (fdlibm `SET_HIGH_WORD`). */
function withHighWord(x: number, hw: number): number {
  _f64[0] = x;
  _u32[HI] = hw >>> 0;
  return _f64[0]!;
}

/** Compose a double from a high and low word (fdlibm `INSERT_WORDS`). */
function fromWords(hw: number, lw: number): number {
  _u32[HI] = hw >>> 0;
  _u32[LO] = lw >>> 0;
  return _f64[0]!;
}

/**
 * The one NaN this module is allowed to *return*: quiet NaN with a clear sign bit, built from its
 * bits rather than obtained from the hardware.
 *
 * fdlibm mints NaN by evaluating an invalid operation — `(x-x)/0`, `0/0`, `Inf-Inf` — and lets the
 * FPU hand back its default NaN. That is fine in C, where only NaN-*ness* is promised, and it is
 * **wrong here**: IEEE-754 does not specify the default NaN's sign bit, and the architectures we
 * grade on disagree. arm64 delivers `7ff8000000000000`; x86_64 delivers `fff8000000000000`. Both
 * are quiet NaNs, both satisfy `Number.isNaN`, and both are invisible to `===` — but this module's
 * whole contract is that its output is the *same bits* everywhere (the golden compares hex, not
 * values), so a sign bit that tracks the CPU is a cross-engine divergence on the certified-grade
 * path. It is the exact defect the vendoring exists to prevent, one level below the ulps.
 *
 * The JS `NaN` literal is no fix: its bit pattern is implementation-defined too (ECMAScript lets
 * an engine pick any NaN, observable precisely through the typed-array reads above), so `return NaN`
 * would trade one unpinned pattern for another. Only writing the words settles it.
 *
 * `7ff8000000000000` — the sign bit clear — is chosen because it is the pattern already recorded in
 * `tests/golden/detmath.golden.json`, and it is the canonical qNaN in the IEEE-754 sense.
 *
 * Do not "simplify" this away. Know which half CI defends: an arithmetic mint IS caught — the golden
 * goes red on x86_64. `return NaN` is NOT — every engine we currently test happens to materialize the
 * literal as `7ff8000000000000`, so CI would stay green. That is luck, not a guarantee: the paragraph
 * above is the reason, and it holds by spec reasoning and review, not by the golden. Writing the words
 * is the only form that does not rest on an engine choice we cannot pin.
 */
const CANONICAL_NAN = fromWords(0x7ff80000, 0x00000000);

// ─────────────────────────────────────────────────────────────────────────────
// exp — SunPro/fdlibm `__ieee754_exp`
//
// exp(x) is computed by argument reduction x = k·ln2 + r (|r| <= 0.5·ln2), a degree-5 minimax
// remez polynomial for expm1 on r, then reassembly exp(x) = 2^k · exp(r). Accurate to < 1 ulp.
// ─────────────────────────────────────────────────────────────────────────────

const one = 1.0;
const halF = [0.5, -0.5];
const o_threshold = 7.09782712893383973096e2; // 0x40862E42 0xFEFA39EF — overflow above this
const u_threshold = -7.45133219101941108420e2; // 0xC0874910 0xD52D3051 — underflow below this
const ln2HI = [6.93147180369123816490e-1, -6.93147180369123816490e-1]; // 0x3FE62E42 0xFEE00000
const ln2LO = [1.90821492927058770002e-10, -1.90821492927058770002e-10]; // 0x3DEA39EF 0x35793C76
const invln2 = 1.44269504088896341351; // 0x3FF71547 0x652B82FE
const P1 = 1.66666666666666019037e-1; // 0x3FC55555 0x5555553E
const P2 = -2.77777777770155933842e-3; // 0xBF66C16C 0x16BEBD93
const P3 = 6.61375632143793436117e-5; // 0x3F11566A 0xAF25DE2C
const P4 = -1.65339022054652515390e-6; // 0xBEBBBD41 0xC5D26BF1
const P5 = 4.13813679705723846039e-8; // 0x3E663769 0x72BEA4D0
const huge = 1.0e300;
const twom1000 = 9.33263618503218878990e-302; // 2**-1000 = 0x01700000 0x00000000

/**
 * Bit-deterministic `Math.exp`. Same value on every IEEE-754 engine (< 1 ulp of the true exp),
 * used everywhere `Math.exp` sat on the certified-grade path. Edge cases match the library:
 * `exp(NaN)=NaN`, `exp(+Inf)=+Inf`, `exp(-Inf)=0`, overflow → `+Inf`, underflow → `0`.
 */
export function dexp(x: number): number {
  let hi = 0.0;
  let lo = 0.0;
  let k = 0;

  let hx = getHighWord(x);
  const xsb = (hx >>> 31) & 1; // sign bit of x
  hx &= 0x7fffffff; // high word of |x|

  // Filter out non-finite / overflow / underflow arguments.
  if (hx >= 0x40862e42) {
    // |x| >= 709.78...
    if (hx >= 0x7ff00000) {
      const lx = getLowWord(x);
      if (((hx & 0xfffff) | lx) !== 0) return x + x; // NaN
      return xsb === 0 ? x : 0.0; // exp(+-inf) = {inf, 0}
    }
    if (x > o_threshold) return huge * huge; // overflow → +Inf
    if (x < u_threshold) return twom1000 * twom1000; // underflow → 0
  }

  // Argument reduction: x = k·ln2 + (hi - lo).
  if (hx > 0x3fd62e42) {
    // |x| > 0.5·ln2
    if (hx < 0x3ff0a2b2) {
      // |x| < 1.5·ln2
      hi = x - ln2HI[xsb]!;
      lo = ln2LO[xsb]!;
      k = 1 - xsb - xsb;
    } else {
      k = (invln2 * x + halF[xsb]!) | 0; // truncate toward zero (C int cast)
      const t = k;
      hi = x - t * ln2HI[0]!; // t·ln2HI is exact here
      lo = t * ln2LO[0]!;
    }
    x = hi - lo;
  } else if (hx < 0x3e300000) {
    // |x| < 2**-28: exp(x) ≈ 1 + x
    if (huge + x > one) return one + x;
  } else {
    k = 0;
  }

  // x is now in the primary range [-0.5·ln2, 0.5·ln2].
  const t = x * x;
  const c = x - t * (P1 + t * (P2 + t * (P3 + t * (P4 + t * P5))));
  let y: number;
  if (k === 0) return one - ((x * c) / (c - 2.0) - x);
  y = one - ((lo - (x * c) / (2.0 - c)) - hi);

  // Reassemble exp(x) = 2^k · y by adding k to y's exponent word.
  if (k >= -1021) {
    const hy = getHighWord(y);
    return withHighWord(y, (hy + (k << 20)) >>> 0);
  }
  const hy = getHighWord(y);
  return withHighWord(y, (hy + ((k + 1000) << 20)) >>> 0) * twom1000;
}

// ─────────────────────────────────────────────────────────────────────────────
// log — SunPro/fdlibm `__ieee754_log`
//
// log(x): reduce x = 2^k · (1 + f) with sqrt(2)/2 < 1+f < sqrt(2), then log(1+f) via the odd
// series in s = f/(2+f). Accurate to < 1 ulp.
// ─────────────────────────────────────────────────────────────────────────────

const ln2_hi = 6.93147180369123816490e-1; // 0x3FE62E42 0xFEE00000
const ln2_lo = 1.90821492927058770002e-10; // 0x3DEA39EF 0x35793C76
const two54 = 1.80143985094819840000e16; // 0x43500000 0x00000000
const Lg1 = 6.666666666666735130e-1; // 0x3FE55555 0x55555593
const Lg2 = 3.999999999940941908e-1; // 0x3FD99999 0x9997FA04
const Lg3 = 2.857142874366239149e-1; // 0x3FD24924 0x94229359
const Lg4 = 2.222219843214978396e-1; // 0x3FCC71C5 0x1D8E78B0
const Lg5 = 1.818357216161805012e-1; // 0x3FC74664 0x96CB03DE
const Lg6 = 1.531383769920937332e-1; // 0x3FC39A09 0xD078C69F
const Lg7 = 1.479819860511658591e-1; // 0x3FC2F112 0xDF3E5244
const zero = 0.0;

/**
 * Bit-deterministic `Math.log` (natural log). Same value on every IEEE-754 engine (< 1 ulp),
 * used everywhere `Math.log` sat on the certified-grade path. Edge cases match the library:
 * `log(+-0)=-Inf`, `log(x<0)=NaN`, `log(NaN)=NaN`, `log(+Inf)=+Inf`.
 */
export function dlog(x: number): number {
  let hx = getHighWord(x) | 0; // signed high word
  const lx = getLowWord(x);

  let k = 0;
  if (hx < 0x00100000) {
    // x < 2**-1022 (subnormal, zero, or negative)
    if (((hx & 0x7fffffff) | lx) === 0) return -two54 / zero; // log(+-0) = -Inf
    // log(-#) = NaN. fdlibm writes `(x-x)/zero` here; we return the constructed qNaN instead, because
    // the hardware's default NaN has an architecture-defined sign bit. See {@link CANONICAL_NAN}.
    if (hx < 0) return CANONICAL_NAN;
    k -= 54;
    x *= two54; // scale subnormal up
    hx = getHighWord(x) | 0;
  }
  if (hx >= 0x7ff00000) return x + x; // +Inf or NaN
  k += (hx >> 20) - 1023;
  hx &= 0x000fffff;
  const i = (hx + 0x95f64) & 0x100000;
  x = withHighWord(x, (hx | (i ^ 0x3ff00000)) >>> 0); // normalize x or x/2 into [sqrt(1/2), sqrt(2))
  k += i >> 20;
  const f = x - 1.0;

  if ((0x000fffff & (2 + hx)) < 3) {
    // -2**-20 <= f < 2**-20: f is tiny, use the short series.
    if (f === zero) {
      if (k === 0) return zero;
      const dk0 = k;
      return dk0 * ln2_hi + dk0 * ln2_lo;
    }
    const R0 = f * f * (0.5 - 0.33333333333333333 * f);
    if (k === 0) return f - R0;
    const dk1 = k;
    return dk1 * ln2_hi - (R0 - dk1 * ln2_lo - f);
  }

  const s = f / (2.0 + f);
  const dk = k;
  const z = s * s;
  let i2 = hx - 0x6147a;
  const w = z * z;
  const j = 0x6b851 - hx;
  const t1 = w * (Lg2 + w * (Lg4 + w * Lg6));
  const t2 = z * (Lg1 + w * (Lg3 + w * (Lg5 + w * Lg7)));
  i2 |= j;
  const R = t2 + t1;
  if (i2 > 0) {
    const hfsq = 0.5 * f * f;
    if (k === 0) return f - (hfsq - s * (hfsq + R));
    return dk * ln2_hi - (hfsq - (s * (hfsq + R) + dk * ln2_lo) - f);
  }
  if (k === 0) return f - s * (f - R);
  return dk * ln2_hi - (s * (f - R) - dk * ln2_lo - f);
}

/** Raw IEEE-754 bits of a double as a stable 16-hex-digit string (big-endian, high word first).
 * The canonical form the cross-engine golden bit-vectors are recorded and compared in — two
 * engines agree iff these strings are equal. */
export function f64ToHex(x: number): string {
  const hi = getHighWord(x).toString(16).padStart(8, "0");
  const lo = getLowWord(x).toString(16).padStart(8, "0");
  return hi + lo;
}

/** Inverse of {@link f64ToHex}: reconstruct the double a golden hex string encodes. */
export function hexToF64(hex: string): number {
  return fromWords(parseInt(hex.slice(0, 8), 16), parseInt(hex.slice(8, 16), 16));
}
