import { ROArrayArray, ROArrayArrayArray } from '../../common/util/types.js';
import { assert } from '../../common/util/util.js';
import {
  Float16Array,
  getFloat16,
  hfround,
  setFloat16,
} from '../../external/petamoriken/float16/float16.js';

import { kBit, kValue } from './constants.js';
import {
  reinterpretF64AsU64,
  reinterpretU64AsF64,
  reinterpretU32AsF32,
  reinterpretU16AsF16,
} from './reinterpret.js';

/**
 * A multiple of 8 guaranteed to be way too large to allocate (just under 8 pebibytes).
 * This is a "safe" integer (ULP <= 1.0) very close to MAX_SAFE_INTEGER.
 *
 * Note: allocations of this size are likely to exceed limitations other than just the system's
 * physical memory, so test cases are also needed to try to trigger "true" OOM.
 */
export const kMaxSafeMultipleOf8 = Number.MAX_SAFE_INTEGER - 7;

/** Round `n` up to the next multiple of `alignment` (inclusive). */
// MAINTENANCE_TODO: Rename to `roundUp`
export function align(n: number, alignment: number): number {
  assert(Number.isInteger(n) && n >= 0, 'n must be a non-negative integer');
  assert(Number.isInteger(alignment) && alignment > 0, 'alignment must be a positive integer');
  return Math.ceil(n / alignment) * alignment;
}

/** Round `n` down to the next multiple of `alignment` (inclusive). */
export function roundDown(n: number, alignment: number): number {
  assert(Number.isInteger(n) && n >= 0, 'n must be a non-negative integer');
  assert(Number.isInteger(alignment) && alignment > 0, 'alignment must be a positive integer');
  return Math.floor(n / alignment) * alignment;
}

/** Clamp a number to the provided range. */
export function clamp(n: number, { min, max }: { min: number; max: number }): number {
  assert(max >= min);
  return Math.min(Math.max(n, min), max);
}

/** @returns 0 if |val| is a subnormal f64 number, otherwise returns |val| */
export function flushSubnormalNumberF64(val: number): number {
  return isSubnormalNumberF64(val) ? 0 : val;
}

/** @returns if number is within subnormal range of f64 */
export function isSubnormalNumberF64(n: number): boolean {
  return n > kValue.f64.negative.max && n < kValue.f64.positive.min;
}

/** @returns 0 if |val| is a subnormal f32 number, otherwise returns |val| */
export function flushSubnormalNumberF32(val: number): number {
  return isSubnormalNumberF32(val) ? 0 : val;
}

/** @returns if number is within subnormal range of f32 */
export function isSubnormalNumberF32(n: number): boolean {
  return n > kValue.f32.negative.max && n < kValue.f32.positive.min;
}

/** @returns if number is in the finite range of f32 */
export function isFiniteF32(n: number) {
  return n >= kValue.f32.negative.min && n <= kValue.f32.positive.max;
}

/** @returns 0 if |val| is a subnormal f16 number, otherwise returns |val| */
export function flushSubnormalNumberF16(val: number): number {
  return isSubnormalNumberF16(val) ? 0 : val;
}

/** @returns if number is within subnormal range of f16 */
export function isSubnormalNumberF16(n: number): boolean {
  return n > kValue.f16.negative.max && n < kValue.f16.positive.min;
}

/** @returns if number is in the finite range of f16 */
export function isFiniteF16(n: number) {
  return n >= kValue.f16.negative.min && n <= kValue.f16.positive.max;
}

/** Should FTZ occur during calculations or not */
export type FlushMode = 'flush' | 'no-flush';

/** Should nextAfter calculate towards positive infinity or negative infinity */
export type NextDirection = 'positive' | 'negative';

/**
 * Once-allocated ArrayBuffer/views to avoid overhead of allocation when
 * converting between numeric formats
 *
 * Usage of a once-allocated pattern like this makes nextAfterF64 non-reentrant,
 * so cannot call itself directly or indirectly.
 */
const nextAfterF64Data = new ArrayBuffer(8);
const nextAfterF64Int = new BigUint64Array(nextAfterF64Data);
const nextAfterF64Float = new Float64Array(nextAfterF64Data);

/**
 * @returns the next f64 value after |val|, towards +inf or -inf as specified by |dir|.

 * If |mode| is 'flush', all subnormal values will be flushed to 0,
 * before processing and for -/+0 the nextAfterF64 will be the closest normal in
 * the correct direction.

 * If |mode| is 'no-flush', the next subnormal will be calculated when appropriate,
 * and for -/+0 the nextAfterF64 will be the closest subnormal in the correct
 * direction.
 *
 * val needs to be in [min f64, max f64]
 */
export function nextAfterF64(val: number, dir: NextDirection, mode: FlushMode): number {
  if (Number.isNaN(val)) {
    return val;
  }

  if (val === Number.POSITIVE_INFINITY) {
    return kValue.f64.positive.infinity;
  }

  if (val === Number.NEGATIVE_INFINITY) {
    return kValue.f64.negative.infinity;
  }

  assert(
    val <= kValue.f64.positive.max && val >= kValue.f64.negative.min,
    `${val} is not in the range of f64`
  );

  val = mode === 'flush' ? flushSubnormalNumberF64(val) : val;

  // -/+0 === 0 returns true
  if (val === 0) {
    if (dir === 'positive') {
      return mode === 'flush' ? kValue.f64.positive.min : kValue.f64.positive.subnormal.min;
    } else {
      return mode === 'flush' ? kValue.f64.negative.max : kValue.f64.negative.subnormal.max;
    }
  }

  nextAfterF64Float[0] = val;
  const is_positive = (nextAfterF64Int[0] & 0x8000_0000_0000_0000n) === 0n;
  if (is_positive === (dir === 'positive')) {
    nextAfterF64Int[0] += 1n;
  } else {
    nextAfterF64Int[0] -= 1n;
  }

  // Checking for overflow
  if ((nextAfterF64Int[0] & 0x7ff0_0000_0000_0000n) === 0x7ff0_0000_0000_0000n) {
    if (dir === 'positive') {
      return kValue.f64.positive.infinity;
    } else {
      return kValue.f64.negative.infinity;
    }
  }

  return mode === 'flush' ? flushSubnormalNumberF64(nextAfterF64Float[0]) : nextAfterF64Float[0];
}

/**
 * Once-allocated ArrayBuffer/views to avoid overhead of allocation when
 * converting between numeric formats
 *
 * Usage of a once-allocated pattern like this makes nextAfterF32 non-reentrant,
 * so cannot call itself directly or indirectly.
 */
const nextAfterF32Data = new ArrayBuffer(4);
const nextAfterF32Int = new Uint32Array(nextAfterF32Data);
const nextAfterF32Float = new Float32Array(nextAfterF32Data);

/**
 * @returns the next f32 value after |val|, towards +inf or -inf as specified by |dir|.

 * If |mode| is 'flush', all subnormal values will be flushed to 0,
 * before processing and for -/+0 the nextAfterF32 will be the closest normal in
 * the correct direction.

 * If |mode| is 'no-flush', the next subnormal will be calculated when appropriate,
 * and for -/+0 the nextAfterF32 will be the closest subnormal in the correct
 * direction.
 *
 * val needs to be in [min f32, max f32]
 */
export function nextAfterF32(val: number, dir: NextDirection, mode: FlushMode): number {
  if (Number.isNaN(val)) {
    return val;
  }

  if (val === Number.POSITIVE_INFINITY) {
    return kValue.f32.positive.infinity;
  }

  if (val === Number.NEGATIVE_INFINITY) {
    return kValue.f32.negative.infinity;
  }

  assert(
    val <= kValue.f32.positive.max && val >= kValue.f32.negative.min,
    `${val} is not in the range of f32`
  );

  val = mode === 'flush' ? flushSubnormalNumberF32(val) : val;

  // -/+0 === 0 returns true
  if (val === 0) {
    if (dir === 'positive') {
      return mode === 'flush' ? kValue.f32.positive.min : kValue.f32.positive.subnormal.min;
    } else {
      return mode === 'flush' ? kValue.f32.negative.max : kValue.f32.negative.subnormal.max;
    }
  }

  nextAfterF32Float[0] = val; // This quantizes from number (f64) to f32
  if (
    (dir === 'positive' && nextAfterF32Float[0] <= val) ||
    (dir === 'negative' && nextAfterF32Float[0] >= val)
  ) {
    // val is either f32 precise or quantizing rounded in the opposite direction
    // from what is needed, so need to calculate the value in the correct
    // direction.
    const is_positive = (nextAfterF32Int[0] & 0x80000000) === 0;
    if (is_positive === (dir === 'positive')) {
      nextAfterF32Int[0] += 1;
    } else {
      nextAfterF32Int[0] -= 1;
    }
  }

  // Checking for overflow
  if ((nextAfterF32Int[0] & 0x7f800000) === 0x7f800000) {
    if (dir === 'positive') {
      return kValue.f32.positive.infinity;
    } else {
      return kValue.f32.negative.infinity;
    }
  }

  return mode === 'flush' ? flushSubnormalNumberF32(nextAfterF32Float[0]) : nextAfterF32Float[0];
}

/**
 * Once-allocated ArrayBuffer/views to avoid overhead of allocation when
 * converting between numeric formats
 *
 * Usage of a once-allocated pattern like this makes nextAfterF16 non-reentrant,
 * so cannot call itself directly or indirectly.
 */
const nextAfterF16Data = new ArrayBuffer(2);
const nextAfterF16Hex = new Uint16Array(nextAfterF16Data);
const nextAfterF16Float = new Float16Array(nextAfterF16Data);

/**
 * @returns the next f16 value after |val|, towards +inf or -inf as specified by |dir|.

 * If |mode| is 'flush', all subnormal values will be flushed to 0,
 * before processing and for -/+0 the nextAfterF16 will be the closest normal in
 * the correct direction.

 * If |mode| is 'no-flush', the next subnormal will be calculated when appropriate,
 * and for -/+0 the nextAfterF16 will be the closest subnormal in the correct
 * direction.
 *
 * val needs to be in [min f16, max f16]
 */
export function nextAfterF16(val: number, dir: NextDirection, mode: FlushMode): number {
  if (Number.isNaN(val)) {
    return val;
  }

  if (val === Number.POSITIVE_INFINITY) {
    return kValue.f16.positive.infinity;
  }

  if (val === Number.NEGATIVE_INFINITY) {
    return kValue.f16.negative.infinity;
  }

  assert(
    val <= kValue.f16.positive.max && val >= kValue.f16.negative.min,
    `${val} is not in the range of f16`
  );

  val = mode === 'flush' ? flushSubnormalNumberF16(val) : val;

  // -/+0 === 0 returns true
  if (val === 0) {
    if (dir === 'positive') {
      return mode === 'flush' ? kValue.f16.positive.min : kValue.f16.positive.subnormal.min;
    } else {
      return mode === 'flush' ? kValue.f16.negative.max : kValue.f16.negative.subnormal.max;
    }
  }

  nextAfterF16Float[0] = val; // This quantizes from number (f64) to f16
  if (
    (dir === 'positive' && nextAfterF16Float[0] <= val) ||
    (dir === 'negative' && nextAfterF16Float[0] >= val)
  ) {
    // val is either f16 precise or quantizing rounded in the opposite direction
    // from what is needed, so need to calculate the value in the correct
    // direction.
    const is_positive = (nextAfterF16Hex[0] & 0x8000) === 0;
    if (is_positive === (dir === 'positive')) {
      nextAfterF16Hex[0] += 1;
    } else {
      nextAfterF16Hex[0] -= 1;
    }
  }

  // Checking for overflow
  if ((nextAfterF16Hex[0] & 0x7c00) === 0x7c00) {
    if (dir === 'positive') {
      return kValue.f16.positive.infinity;
    } else {
      return kValue.f16.negative.infinity;
    }
  }

  return mode === 'flush' ? flushSubnormalNumberF16(nextAfterF16Float[0]) : nextAfterF16Float[0];
}

/**
 * @returns ulp(x), the unit of least precision for a specific number as a 64-bit float
 *
 * ulp(x) is the distance between the two floating point numbers nearest x.
 * This value is also called unit of last place, ULP, and 1 ULP.
 * See the WGSL spec and http://www.ens-lyon.fr/LIP/Pub/Rapports/RR/RR2005/RR2005-09.pdf
 * for a more detailed/nuanced discussion of the definition of ulp(x).
 *
 * @param target number to calculate ULP for
 * @param mode should FTZ occurring during calculation or not
 */
export function oneULPF64(target: number, mode: FlushMode = 'flush'): number {
  if (Number.isNaN(target)) {
    return Number.NaN;
  }

  target = mode === 'flush' ? flushSubnormalNumberF64(target) : target;

  // For values out of bounds for f64 ulp(x) is defined as the
  // distance between the two nearest f64 representable numbers to the
  // appropriate edge, which also happens to be the maximum possible ULP.
  if (
    target === Number.POSITIVE_INFINITY ||
    target >= kValue.f64.positive.max ||
    target === Number.NEGATIVE_INFINITY ||
    target <= kValue.f64.negative.min
  ) {
    return kValue.f64.max_ulp;
  }

  // ulp(x) is min(after - before), where
  //     before <= x <= after
  //     before =/= after
  //     before and after are f64 representable
  const before = nextAfterF64(target, 'negative', mode);
  const after = nextAfterF64(target, 'positive', mode);
  // Since number is internally a f64, |target| is always f64 representable, so
  // either before or after will be x
  return Math.min(target - before, after - target);
}

/**
 * @returns ulp(x), the unit of least precision for a specific number as a 32-bit float
 *
 * ulp(x) is the distance between the two floating point numbers nearest x.
 * This value is also called unit of last place, ULP, and 1 ULP.
 * See the WGSL spec and http://www.ens-lyon.fr/LIP/Pub/Rapports/RR/RR2005/RR2005-09.pdf
 * for a more detailed/nuanced discussion of the definition of ulp(x).
 *
 * @param target number to calculate ULP for
 * @param mode should FTZ occurring during calculation or not
 */
export function oneULPF32(target: number, mode: FlushMode = 'flush'): number {
  if (Number.isNaN(target)) {
    return Number.NaN;
  }

  target = mode === 'flush' ? flushSubnormalNumberF32(target) : target;

  // For values out of bounds for f32 ulp(x) is defined as the
  // distance between the two nearest f32 representable numbers to the
  // appropriate edge, which also happens to be the maximum possible ULP.
  if (
    target === Number.POSITIVE_INFINITY ||
    target >= kValue.f32.positive.max ||
    target === Number.NEGATIVE_INFINITY ||
    target <= kValue.f32.negative.min
  ) {
    return kValue.f32.max_ulp;
  }

  // ulp(x) is min(after - before), where
  //     before <= x <= after
  //     before =/= after
  //     before and after are f32 representable
  const before = nextAfterF32(target, 'negative', mode);
  const after = nextAfterF32(target, 'positive', mode);
  const converted: number = quantizeToF32(target);
  if (converted === target) {
    // |target| is f32 representable, so either before or after will be x
    return Math.min(target - before, after - target);
  } else {
    // |target| is not f32 representable so taking distance of neighbouring f32s.
    return after - before;
  }
}

/**
 * @returns an integer value between 0..0xffffffff using a simple non-cryptographic hash function
 * @param values integers to generate hash from.
 */
export function hashU32(...values: number[]) {
  let n = 0x3504_f333;
  for (const v of values) {
    n = v + (n << 7) + (n >>> 1);
    n = (n * 0x29493) & 0xffff_ffff;
  }
  n ^= n >>> 8;
  n += n << 15;
  n = n & 0xffff_ffff;
  if (n < 0) {
    n = ~n * 2 + 1;
  }
  return n;
}

/**
 * @returns ulp(x), the unit of least precision for a specific number as a 32-bit float
 *
 * ulp(x) is the distance between the two floating point numbers nearest x.
 * This value is also called unit of last place, ULP, and 1 ULP.
 * See the WGSL spec and http://www.ens-lyon.fr/LIP/Pub/Rapports/RR/RR2005/RR2005-09.pdf
 * for a more detailed/nuanced discussion of the definition of ulp(x).
 *
 * @param target number to calculate ULP for
 * @param mode should FTZ occurring during calculation or not
 */
export function oneULPF16(target: number, mode: FlushMode = 'flush'): number {
  if (Number.isNaN(target)) {
    return Number.NaN;
  }

  target = mode === 'flush' ? flushSubnormalNumberF16(target) : target;

  // For values out of bounds for f16 ulp(x) is defined as the
  // distance between the two nearest f16 representable numbers to the
  // appropriate edge, which also happens to be the maximum possible ULP.
  if (
    target === Number.POSITIVE_INFINITY ||
    target >= kValue.f16.positive.max ||
    target === Number.NEGATIVE_INFINITY ||
    target <= kValue.f16.negative.min
  ) {
    return kValue.f16.max_ulp;
  }

  // ulp(x) is min(after - before), where
  //     before <= x <= after
  //     before =/= after
  //     before and after are f16 representable
  const before = nextAfterF16(target, 'negative', mode);
  const after = nextAfterF16(target, 'positive', mode);
  const converted: number = quantizeToF16(target);
  if (converted === target) {
    // |target| is f16 representable, so either before or after will be x
    return Math.min(target - before, after - target);
  } else {
    // |target| is not f16 representable so taking distance of neighbouring f16s.
    return after - before;
  }
}

/**
 * Calculate the valid roundings when quantizing to 64-bit floats
 *
 * TS/JS's number type is internally a f64, so the supplied value will be
 * quanitized by definition. The only corner cases occur if a non-finite value
 * is provided, since the valid roundings include the appropriate min or max
 * value.
 *
 * @param n number to be quantized
 * @returns all of the acceptable roundings for quantizing to 64-bits in
 *          ascending order.
 */
export function correctlyRoundedF64(n: number): readonly number[] {
  assert(!Number.isNaN(n), `correctlyRoundedF32 not defined for NaN`);
  // Above f64 range
  if (n === Number.POSITIVE_INFINITY) {
    return [kValue.f64.positive.max, Number.POSITIVE_INFINITY];
  }

  // Below f64 range
  if (n === Number.NEGATIVE_INFINITY) {
    return [Number.NEGATIVE_INFINITY, kValue.f64.negative.min];
  }

  return [n];
}

/**
 * Calculate the valid roundings when quantizing to 32-bit floats
 *
 * TS/JS's number type is internally a f64, so quantization needs to occur when
 * converting to f32 for WGSL. WGSL does not specify a specific rounding mode,
 * so if a number is not precisely representable in 32-bits, but in the
 * range, there are two possible valid quantizations. If it is precisely
 * representable, there is only one valid quantization. This function calculates
 * the valid roundings and returns them in an array.
 *
 * This function does not consider flushing mode, so subnormals are maintained.
 * The caller is responsible to flushing before and after as appropriate.
 *
 * Out of bounds values need to consider how they interact with the overflow
 * rules.
 *  * If a value is OOB but not too far out, an implementation may choose to round
 * to nearest finite value or the correct infinity. This boundary is at
 * 2^(f32.emax + 1) and -(2^(f32.emax + 1)) respectively.
 * Values that are at or beyond these limits must be rounded towards the
 * appropriate infinity.
 *
 * @param n number to be quantized
 * @returns all of the acceptable roundings for quantizing to 32-bits in
 *          ascending order.
 */
export function correctlyRoundedF32(n: number): readonly number[] {
  if (Number.isNaN(n)) {
    return [n];
  }

  // Greater than or equal to the upper overflow boundry
  if (n >= 2 ** (kValue.f32.emax + 1)) {
    return [Number.POSITIVE_INFINITY];
  }

  // OOB, but less than the upper overflow boundary
  if (n > kValue.f32.positive.max) {
    return [kValue.f32.positive.max, Number.POSITIVE_INFINITY];
  }

  // f32 finite
  if (n <= kValue.f32.positive.max && n >= kValue.f32.negative.min) {
    const n_32 = quantizeToF32(n);
    if (n === n_32) {
      // n is precisely expressible as a f32, so should not be rounded
      return [n];
    }

    if (n_32 > n) {
      // n_32 rounded towards +inf, so is after n
      const other = nextAfterF32(n_32, 'negative', 'no-flush');
      return [other, n_32];
    } else {
      // n_32 rounded towards -inf, so is before n
      const other = nextAfterF32(n_32, 'positive', 'no-flush');
      return [n_32, other];
    }
  }

  // OOB, but greater the lower overflow boundary
  if (n > -(2 ** (kValue.f32.emax + 1))) {
    return [Number.NEGATIVE_INFINITY, kValue.f32.negative.min];
  }

  // Less than or equal to the lower overflow boundary
  return [Number.NEGATIVE_INFINITY];
}

/**
 * Calculate the valid roundings when quantizing to 16-bit floats
 *
 * TS/JS's number type is internally a f64, so quantization needs to occur when
 * converting to f16 for WGSL. WGSL does not specify a specific rounding mode,
 * so if a number is not precisely representable in 16-bits, but in the
 * range, there are two possible valid quantizations. If it is precisely
 * representable, there is only one valid quantization. This function calculates
 * the valid roundings and returns them in an array.
 *
 * This function does not consider flushing mode, so subnormals are maintained.
 * The caller is responsible to flushing before and after as appropriate.
 *
 * Out of bounds values need to consider how they interact with the overflow
 * rules.
 *  * If a value is OOB but not too far out, an implementation may choose to round
 * to nearest finite value or the correct infinity. This boundary is at
 * 2^(f16.emax + 1) and -(2^(f16.emax + 1)) respectively.
 * Values that are at or beyond these limits must be rounded towards the
 * appropriate infinity.
 *
 * @param n number to be quantized
 * @returns all of the acceptable roundings for quantizing to 16-bits in
 *          ascending order.
 */
export function correctlyRoundedF16(n: number): readonly number[] {
  if (Number.isNaN(n)) {
    return [n];
  }

  // Greater than or equal to the upper overflow boundry
  if (n >= 2 ** (kValue.f16.emax + 1)) {
    return [Number.POSITIVE_INFINITY];
  }

  // OOB, but less than the upper overflow boundary
  if (n > kValue.f16.positive.max) {
    return [kValue.f16.positive.max, Number.POSITIVE_INFINITY];
  }

  // f16 finite
  if (n <= kValue.f16.positive.max && n >= kValue.f16.negative.min) {
    const n_16 = quantizeToF16(n);
    if (n === n_16) {
      // n is precisely expressible as a f16, so should not be rounded
      return [n];
    }

    if (n_16 > n) {
      // n_16 rounded towards +inf, so is after n
      const other = nextAfterF16(n_16, 'negative', 'no-flush');
      return [other, n_16];
    } else {
      // n_16 rounded towards -inf, so is before n
      const other = nextAfterF16(n_16, 'positive', 'no-flush');
      return [n_16, other];
    }
  }

  // OOB, but greater the lower overflow boundary
  if (n > -(2 ** (kValue.f16.emax + 1))) {
    return [Number.NEGATIVE_INFINITY, kValue.f16.negative.min];
  }

  // Less than or equal to the lower overflow boundary
  return [Number.NEGATIVE_INFINITY];
}

/**
 * Calculates WGSL frexp
 *
 * Splits val into a fraction and an exponent so that
 * val = fraction * 2 ^ exponent.
 * The fraction is 0.0 or its magnitude is in the range [0.5, 1.0).
 *
 * @param val the float to split
 * @param trait the float type, f32 or f16 or f64
 * @returns the results of splitting val
 */
export function frexp(val: number, trait: 'f32' | 'f16' | 'f64'): { fract: number; exp: number } {
  const buffer = new ArrayBuffer(8);
  const dataView = new DataView(buffer);

  // expBitCount and fractBitCount is the bitwidth of exponent and fractional part of the given FP type.
  // expBias is the bias constant of exponent of the given FP type.
  // Biased exponent (unsigned integer, i.e. the exponent part of float) = unbiased exponent (signed integer) + expBias.
  let expBitCount: number, fractBitCount: number, expBias: number;
  // To handle the exponent bits of given FP types (f16, f32, and f64), considering the highest 16
  // bits is enough.
  // expMaskForHigh16Bits indicates the exponent bitfield in the highest 16 bits of the given FP
  // type, and targetExpBitsForHigh16Bits is the exponent bits that corresponding to unbiased
  // exponent -1, i.e. the exponent bits when the FP values is in range [0.5, 1.0).
  let expMaskForHigh16Bits: number, targetExpBitsForHigh16Bits: number;
  // Helper function that store the given FP value into buffer as the given FP types
  let setFloatToBuffer: (v: number) => void;
  // Helper function that read back FP value from buffer as the given FP types
  let getFloatFromBuffer: () => number;

  let isFinite: (v: number) => boolean;
  let isSubnormal: (v: number) => boolean;

  if (trait === 'f32') {
    // f32 bit pattern: s_eeeeeeee_fffffff_ffffffffffffffff
    expBitCount = 8;
    fractBitCount = 23;
    expBias = 127;
    // The exponent bitmask for high 16 bits of f32.
    expMaskForHigh16Bits = 0x7f80;
    // The target exponent bits is equal to those for f32 0.5 = 0x3f000000.
    targetExpBitsForHigh16Bits = 0x3f00;
    isFinite = isFiniteF32;
    isSubnormal = isSubnormalNumberF32;
    // Enforce big-endian so that offset 0 is highest byte.
    setFloatToBuffer = (v: number) => dataView.setFloat32(0, v, false);
    getFloatFromBuffer = () => dataView.getFloat32(0, false);
  } else if (trait === 'f16') {
    // f16 bit pattern: s_eeeee_ffffffffff
    expBitCount = 5;
    fractBitCount = 10;
    expBias = 15;
    // The exponent bitmask for 16 bits of f16.
    expMaskForHigh16Bits = 0x7c00;
    // The target exponent bits is equal to those for f16 0.5 = 0x3800.
    targetExpBitsForHigh16Bits = 0x3800;
    isFinite = isFiniteF16;
    isSubnormal = isSubnormalNumberF16;
    // Enforce big-endian so that offset 0 is highest byte.
    setFloatToBuffer = (v: number) => setFloat16(dataView, 0, v, false);
    getFloatFromBuffer = () => getFloat16(dataView, 0, false);
  } else {
    assert(trait === 'f64');
    // f64 bit pattern: s_eeeeeeeeeee_ffff_ffffffffffffffffffffffffffffffffffffffffffffffff
    expBitCount = 11;
    fractBitCount = 52;
    expBias = 1023;
    // The exponent bitmask for 16 bits of f64.
    expMaskForHigh16Bits = 0x7ff0;
    // The target exponent bits is equal to those for f64 0.5 = 0x3fe0_0000_0000_0000.
    targetExpBitsForHigh16Bits = 0x3fe0;
    isFinite = Number.isFinite;
    isSubnormal = isSubnormalNumberF64;
    // Enforce big-endian so that offset 0 is highest byte.
    setFloatToBuffer = (v: number) => dataView.setFloat64(0, v, false);
    getFloatFromBuffer = () => dataView.getFloat64(0, false);
  }
  // Helper function that extract the unbiased exponent of the float in buffer.
  const extractUnbiasedExpFromNormalFloatInBuffer = () => {
    // Assert the float in buffer is finite normal float.
    assert(isFinite(getFloatFromBuffer()) && !isSubnormal(getFloatFromBuffer()));
    // Get the highest 16 bits of float as uint16, which can contain the whole exponent part for both f16, f32, and f64.
    const high16BitsAsUint16 = dataView.getUint16(0, false);
    // Return the unbiased exp by masking, shifting and unbiasing.
    return ((high16BitsAsUint16 & expMaskForHigh16Bits) >> (16 - 1 - expBitCount)) - expBias;
  };
  // Helper function that modify the exponent of float in buffer to make it in range [0.5, 1.0).
  // By setting the unbiased exponent to -1, the fp value will be in range 2**-1 * [1.0, 2.0), i.e. [0.5, 1.0).
  const modifyExpOfNormalFloatInBuffer = () => {
    // Assert the float in buffer is finite normal float.
    assert(isFinite(getFloatFromBuffer()) && !isSubnormal(getFloatFromBuffer()));
    // Get the highest 16 bits of float as uint16, which contains the whole exponent part for both f16, f32, and f64.
    const high16BitsAsUint16 = dataView.getUint16(0, false);
    // Modify the exponent bits.
    const modifiedHigh16Bits =
      (high16BitsAsUint16 & ~expMaskForHigh16Bits) | targetExpBitsForHigh16Bits;
    // Set back to buffer
    dataView.setUint16(0, modifiedHigh16Bits, false);
  };

  // +/- 0.0
  if (val === 0) {
    return { fract: val, exp: 0 };
  }
  // NaN and Inf
  if (!isFinite(val)) {
    return { fract: val, exp: 0 };
  }

  setFloatToBuffer(val);
  // Don't use val below. Use helper functions working with buffer instead.

  let exp = 0;
  // Normailze the value if it is subnormal. Increase the exponent by multiplying a subnormal value
  // with 2**fractBitCount will result in a finite normal FP value of the given FP type.
  if (isSubnormal(getFloatFromBuffer())) {
    setFloatToBuffer(getFloatFromBuffer() * 2 ** fractBitCount);
    exp = -fractBitCount;
  }
  // A normal FP value v is represented as v = ((-1)**s)*(2**(unbiased exponent))*f, where f is in
  // range [1.0, 2.0). By moving a factor 2 from f to exponent, we have
  // v = ((-1)**s)*(2**(unbiased exponent + 1))*(f / 2), where (f / 2) is in range [0.5, 1.0), so
  // the exp = (unbiased exponent + 1) and fract = ((-1)**s)*(f / 2) is what we expect to get from
  // frexp function. Note that fract and v only differs in exponent bitfield as long as v is normal.
  // Calc the result exp by getting the unbiased float exponent and plus 1.
  exp += extractUnbiasedExpFromNormalFloatInBuffer() + 1;
  // Modify the exponent of float in buffer to make it be in range [0.5, 1.0) to get fract.
  modifyExpOfNormalFloatInBuffer();

  return { fract: getFloatFromBuffer(), exp };
}

/**
 * Calculates the linear interpolation between two values of a given fractional.
 *
 * If |t| is 0, |a| is returned, if |t| is 1, |b| is returned, otherwise
 * interpolation/extrapolation equivalent to a + t(b - a) is performed.
 *
 * Numerical stable version is adapted from http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0811r2.html
 */
export function lerp(a: number, b: number, t: number): number {
  if (!Number.isFinite(a) || !Number.isFinite(b)) {
    return Number.NaN;
  }

  if ((a <= 0.0 && b >= 0.0) || (a >= 0.0 && b <= 0.0)) {
    return t * b + (1 - t) * a;
  }

  if (t === 1.0) {
    return b;
  }

  const x = a + t * (b - a);
  return t > 1.0 === b > a ? Math.max(b, x) : Math.min(b, x);
}

/**
 * Version of lerp that operates on bigint values
 *
 * lerp was not made into a generic or to take in (number|bigint), because that
 * introduces a bunch of complexity overhead related to type differentiation
 */
export function lerpBigInt(a: bigint, b: bigint, idx: number, steps: number): bigint {
  assert(Math.trunc(idx) === idx);
  assert(Math.trunc(steps) === steps);

  // This constrains t to [0.0, 1.0]
  assert(idx >= 0);
  assert(steps > 0);
  assert(idx < steps);

  if (steps === 1) {
    return a;
  }
  if (idx === 0) {
    return a;
  }
  if (idx === steps - 1) {
    return b;
  }

  const min = (x: bigint, y: bigint): bigint => {
    return x < y ? x : y;
  };
  const max = (x: bigint, y: bigint): bigint => {
    return x > y ? x : y;
  };

  // For number the variable t is used, there t = idx / (steps - 1),
  // but that is a fraction on [0, 1], so becomes either 0 or 1 when converted
  // to bigint, so need to expand things out.
  const big_idx = BigInt(idx);
  const big_steps = BigInt(steps);
  if ((a <= 0n && b >= 0n) || (a >= 0n && b <= 0n)) {
    return (b * big_idx) / (big_steps - 1n) + (a - (a * big_idx) / (big_steps - 1n));
  }

  const x = a + (b * big_idx) / (big_steps - 1n) - (a * big_idx) / (big_steps - 1n);
  return !(b > a) ? max(b, x) : min(b, x);
}

/** @returns a linear increasing range of numbers. */
export function linearRange(a: number, b: number, num_steps: number): readonly number[] {
  if (num_steps <= 0) {
    return [];
  }

  // Avoid division by 0
  if (num_steps === 1) {
    return [a];
  }

  return Array.from(Array(num_steps).keys()).map(i => lerp(a, b, i / (num_steps - 1)));
}

/**
 * Version of linearRange that operates on bigint values
 *
 * linearRange was not made into a generic or to take in (number|bigint),
 * because that introduces a bunch of complexity overhead related to type
 * differentiation
 */
export function linearRangeBigInt(a: bigint, b: bigint, num_steps: number): Array<bigint> {
  if (num_steps <= 0) {
    return [];
  }

  // Avoid division by 0
  if (num_steps === 1) {
    return [a];
  }

  return Array.from(Array(num_steps).keys()).map(i => lerpBigInt(a, b, i, num_steps));
}

/**
 * @returns a non-linear increasing range of numbers, with a bias towards the beginning.
 *
 * Generates a linear range on [0,1] with |num_steps|, then squares all the values to make the curve be quadratic,
 * thus biasing towards 0, but remaining on the [0, 1] range.
 * This biased range is then scaled to the desired range using lerp.
 * Different curves could be generated by changing c, where greater values of c will bias more towards 0.
 */
export function biasedRange(a: number, b: number, num_steps: number): readonly number[] {
  const c = 2;
  if (num_steps <= 0) {
    return [];
  }

  // Avoid division by 0
  if (num_steps === 1) {
    return [a];
  }

  return Array.from(Array(num_steps).keys()).map(i => lerp(a, b, Math.pow(i / (num_steps - 1), c)));
}

/**
 * Version of biasedRange that operates on bigint values
 *
 * biasedRange was not made into a generic or to take in (number|bigint),
 * because that introduces a bunch of complexity overhead related to type
 * differentiation.
 *
 * Scaling is used internally so that the number of possible indices is
 * significantly larger than num_steps. This is done to avoid duplicate entries
 * in the resulting range due to quantizing to integers during the calculation.
 *
 *  If a and b are close together, such that the number of integers between them
 *  is close to num_steps, then duplicates will occur regardless of scaling.
 */
export function biasedRangeBigInt(a: bigint, b: bigint, num_steps: number): readonly bigint[] {
  if (num_steps <= 0) {
    return [];
  }

  // Avoid division by 0
  if (num_steps === 1) {
    return [a];
  }

  const c = 2;
  const scaling = 1000;
  const scaled_num_steps = num_steps * scaling;

  return Array.from(Array(num_steps).keys()).map(i => {
    const biased_i = Math.pow(i / (num_steps - 1), c); // Floating Point on [0, 1]
    const scaled_i = Math.trunc((scaled_num_steps - 1) * biased_i); // Integer on [0, scaled_num_steps - 1]
    return lerpBigInt(a, b, scaled_i, scaled_num_steps);
  });
}

/**
 * @returns an ascending sorted array of numbers spread over the entire range of 32-bit floats
 *
 * Numbers are divided into 4 regions: negative normals, negative subnormals, positive subnormals & positive normals.
 * Zero is included.
 *
 * Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
 * means that number of precise f32 values between each returned value in a region should be about the same. This allows
 * for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f32 range.
 *
 * This function is intended to provide dense coverage of the f32 range, for a minimal list of values to use to cover
 * f32 behaviour, use sparseScalarF32Range instead.
 *
 * @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries
 *               must be 0 or greater.
 */
export function scalarF32Range(
  counts: {
    neg_norm?: number;
    neg_sub?: number;
    pos_sub: number;
    pos_norm: number;
  } = { pos_sub: 10, pos_norm: 50 }
): Array<number> {
  counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
  counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;

  let special_pos: number[] = [];
  // The first interior point for 'pos_norm' is at 3. Because we have two special values we start allowing these
  // special values as soon as they will fit as interior values.
  if (counts.pos_norm >= 4) {
    special_pos = [
      // Largest float as signed integer
      0x4effffff,
      // Largest float as unsigned integer
      0x4f7fffff,
    ];
  }
  // Generating bit fields first and then converting to f32, so that the spread across the possible f32 values is more
  // even. Generating against the bounds of f32 values directly results in the values being extremely biased towards the
  // extremes, since they are so much larger.
  const bit_fields = [
    ...linearRange(kBit.f32.negative.min, kBit.f32.negative.max, counts.neg_norm),
    ...linearRange(
      kBit.f32.negative.subnormal.min,
      kBit.f32.negative.subnormal.max,
      counts.neg_sub
    ),
    // -0.0
    0x80000000,
    // +0.0
    0,
    ...linearRange(
      kBit.f32.positive.subnormal.min,
      kBit.f32.positive.subnormal.max,
      counts.pos_sub
    ),
    ...[
      ...linearRange(
        kBit.f32.positive.min,
        kBit.f32.positive.max,
        counts.pos_norm - special_pos.length
      ),
      ...special_pos,
    ].sort((n1, n2) => n1 - n2),
  ].map(Math.trunc);
  return bit_fields.map(reinterpretU32AsF32);
}

/**
 * @returns an ascending sorted array of numbers.
 *
 * The numbers returned are based on the `full32Range` as described above. The difference comes depending
 * on the `source` parameter. If the `source` is `const` then the numbers will be restricted to be
 * in the range `[low, high]`. This allows filtering out a set of `f32` values which are invalid for
 * const-evaluation but are needed to test the non-const implementation.
 *
 * @param source the input source for the test. If the `source` is `const` then the return will be filtered
 * @param low the lowest f32 value to permit when filtered
 * @param high the highest f32 value to permit when filtered
 */
export function filteredScalarF32Range(source: String, low: number, high: number): Array<number> {
  return scalarF32Range().filter(x => source !== 'const' || (x >= low && x <= high));
}

/**
 * @returns an ascending sorted array of numbers spread over the entire range of 16-bit floats
 *
 * Numbers are divided into 4 regions: negative normals, negative subnormals, positive subnormals & positive normals.
 * Zero is included.
 *
 * Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
 * means that number of precise f16 values between each returned value in a region should be about the same. This allows
 * for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f16 range.
 *
 * This function is intended to provide dense coverage of the f16 range, for a minimal list of values to use to cover
 * f16 behaviour, use sparseScalarF16Range instead.
 *
 * @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries
 *               must be 0 or greater.
 */
export function scalarF16Range(
  counts: {
    neg_norm?: number;
    neg_sub?: number;
    pos_sub: number;
    pos_norm: number;
  } = { pos_sub: 10, pos_norm: 50 }
): Array<number> {
  counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
  counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;

  // Generating bit fields first and then converting to f16, so that the spread across the possible f16 values is more
  // even. Generating against the bounds of f16 values directly results in the values being extremely biased towards the
  // extremes, since they are so much larger.
  const bit_fields = [
    ...linearRange(kBit.f16.negative.min, kBit.f16.negative.max, counts.neg_norm),
    ...linearRange(
      kBit.f16.negative.subnormal.min,
      kBit.f16.negative.subnormal.max,
      counts.neg_sub
    ),
    // -0.0
    0x8000,
    // +0.0
    0,
    ...linearRange(
      kBit.f16.positive.subnormal.min,
      kBit.f16.positive.subnormal.max,
      counts.pos_sub
    ),
    ...linearRange(kBit.f16.positive.min, kBit.f16.positive.max, counts.pos_norm),
  ].map(Math.trunc);
  return bit_fields.map(reinterpretU16AsF16);
}

/**
 * @returns an ascending sorted array of numbers spread over the entire range of 64-bit floats
 *
 * Numbers are divided into 4 regions: negative normals, negative subnormals, positive subnormals & positive normals.
 * Zero is included.
 *
 * Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
 * means that number of precise f64 values between each returned value in a region should be about the same. This allows
 * for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f64 range.
 *
 * This function is intended to provide dense coverage of the f64 range, for a minimal list of values to use to cover
 * f64 behaviour, use sparseScalarF64Range instead.
 *
 * @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries
 *               must be 0 or greater.
 */
export function scalarF64Range(
  counts: {
    neg_norm?: number;
    neg_sub?: number;
    pos_sub: number;
    pos_norm: number;
  } = { pos_sub: 10, pos_norm: 50 }
): Array<number> {
  counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
  counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;

  // Generating bit fields first and then converting to f64, so that the spread across the possible f64 values is more
  // even. Generating against the bounds of f64 values directly results in the values being extremely biased towards the
  // extremes, since they are so much larger.
  const bit_fields = [
    ...linearRangeBigInt(kBit.f64.negative.min, kBit.f64.negative.max, counts.neg_norm),
    ...linearRangeBigInt(
      kBit.f64.negative.subnormal.min,
      kBit.f64.negative.subnormal.max,
      counts.neg_sub
    ),
    // -0.0
    0x8000_0000_0000_0000n,
    // +0.0
    0n,
    ...linearRangeBigInt(
      kBit.f64.positive.subnormal.min,
      kBit.f64.positive.subnormal.max,
      counts.pos_sub
    ),
    ...linearRangeBigInt(kBit.f64.positive.min, kBit.f64.positive.max, counts.pos_norm),
  ];
  return bit_fields.map(reinterpretU64AsF64);
}

/**
 * @returns an ascending sorted array of f64 values spread over specific range of f64 normal floats
 *
 * Numbers are divided into 4 regions: negative 64-bit normals, negative 64-bit subnormals, positive 64-bit subnormals &
 * positive 64-bit normals.
 * Zero is included.
 *
 * Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
 * means that number of precise f64 values between each returned value in a region should be about the same. This allows
 * for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the range.
 *
 * @param begin a negative f64 normal float value
 * @param end a positive f64 normal float value
 * @param counts structure param with 4 entries indicating the number of entries
 *               to be generated each region, entries must be 0 or greater.
 */
export function limitedScalarF64Range(
  begin: number,
  end: number,
  counts: { neg_norm?: number; neg_sub?: number; pos_sub: number; pos_norm: number } = {
    pos_sub: 10,
    pos_norm: 50,
  }
): Array<number> {
  assert(
    begin <= kValue.f64.negative.max,
    `Beginning of range ${begin} must be negative f64 normal`
  );
  assert(end >= kValue.f64.positive.min, `Ending of range ${end} must be positive f64 normal`);

  counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
  counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;

  const u64_begin = reinterpretF64AsU64(begin);
  const u64_end = reinterpretF64AsU64(end);
  // Generating bit fields first and then converting to f64, so that the spread across the possible f64 values is more
  // even. Generating against the bounds of f64 values directly results in the values being extremely biased towards the
  // extremes, since they are so much larger.
  const bit_fields = [
    ...linearRangeBigInt(u64_begin, kBit.f64.negative.max, counts.neg_norm),
    ...linearRangeBigInt(
      kBit.f64.negative.subnormal.min,
      kBit.f64.negative.subnormal.max,
      counts.neg_sub
    ),
    // -0.0
    0x8000_0000_0000_0000n,
    // +0.0
    0n,
    ...linearRangeBigInt(
      kBit.f64.positive.subnormal.min,
      kBit.f64.positive.subnormal.max,
      counts.pos_sub
    ),
    ...linearRangeBigInt(kBit.f64.positive.min, u64_end, counts.pos_norm),
  ];
  return bit_fields.map(reinterpretU64AsF64);
}

/** Short list of i32 values of interest to test against */
const kInterestingI32Values: readonly number[] = [
  kValue.i32.negative.max,
  Math.trunc(kValue.i32.negative.max / 2),
  -256,
  -10,
  -1,
  0,
  1,
  10,
  256,
  Math.trunc(kValue.i32.positive.max / 2),
  kValue.i32.positive.max,
];

/** @returns minimal i32 values that cover the entire range of i32 behaviours
 *
 * This is used instead of fullI32Range when the number of test cases being
 * generated is a super linear function of the length of i32 values which is
 * leading to time outs.
 */
export function sparseI32Range(): readonly number[] {
  return kInterestingI32Values;
}

const kVectorI32Values = {
  2: kInterestingI32Values.flatMap(f => [
    [f, 1],
    [-1, f],
  ]),
  3: kInterestingI32Values.flatMap(f => [
    [f, 1, -2],
    [-1, f, 2],
    [1, -2, f],
  ]),
  4: kInterestingI32Values.flatMap(f => [
    [f, -1, 2, 3],
    [1, f, -2, 3],
    [1, 2, f, -3],
    [-1, 2, -3, f],
  ]),
};

/**
 * Returns set of vectors, indexed by dimension containing interesting i32
 * values.
 *
 * The tests do not do the simple option for coverage of computing the cartesian
 * product of all of the interesting i32 values N times for vecN tests,
 * because that creates a huge number of tests for vec3 and vec4, leading to
 * time outs.
 *
 * Instead they insert the interesting i32 values into each location of the
 * vector to get a spread of testing over the entire range. This reduces the
 * number of cases being run substantially, but maintains coverage.
 */
export function vectorI32Range(dim: number): ROArrayArray<number> {
  assert(dim === 2 || dim === 3 || dim === 4, 'vectorI32Range only accepts dimensions 2, 3, and 4');
  return kVectorI32Values[dim];
}

const kSparseVectorI32Values = {
  2: sparseI32Range().map((i, idx) => [idx % 2 === 0 ? i : idx, idx % 2 === 1 ? i : -idx]),
  3: sparseI32Range().map((i, idx) => [
    idx % 3 === 0 ? i : idx,
    idx % 3 === 1 ? i : -idx,
    idx % 3 === 2 ? i : idx,
  ]),
  4: sparseI32Range().map((i, idx) => [
    idx % 4 === 0 ? i : idx,
    idx % 4 === 1 ? i : -idx,
    idx % 4 === 2 ? i : idx,
    idx % 4 === 3 ? i : -idx,
  ]),
};

/**
 * Minimal set of vectors, indexed by dimension, that contain interesting
 * abstract integer values.
 *
 * This is an even more stripped down version of `vectorI32Range` for when
 * pairs of vectors are being tested.
 * All interesting integers from sparseI32Range are guaranteed to be
 * tested, but not in every position.
 */
export function sparseVectorI32Range(dim: number): ROArrayArray<number> {
  assert(
    dim === 2 || dim === 3 || dim === 4,
    'sparseVectorI32Range only accepts dimensions 2, 3, and 4'
  );
  return kSparseVectorI32Values[dim];
}

/**
 * @returns an ascending sorted array of numbers spread over the entire range of 32-bit signed ints
 *
 * Numbers are divided into 2 regions: negatives, and positives, with their spreads biased towards 0
 * Zero is included in range.
 *
 * @param counts structure param with 2 entries indicating the number of entries to be generated each region, values must be 0 or greater.
 */
export function fullI32Range(
  counts: {
    negative?: number;
    positive: number;
  } = { positive: 50 }
): Array<number> {
  counts.negative = counts.negative === undefined ? counts.positive : counts.negative;
  return [
    ...biasedRange(kValue.i32.negative.min, -1, counts.negative),
    0,
    ...biasedRange(1, kValue.i32.positive.max, counts.positive),
  ].map(Math.trunc);
}

/** Short list of u32 values of interest to test against */
const kInterestingU32Values: readonly number[] = [
  0,
  1,
  10,
  256,
  Math.trunc(kValue.u32.max / 2),
  kValue.u32.max,
];

/** @returns minimal u32 values that cover the entire range of u32 behaviours
 *
 * This is used instead of fullU32Range when the number of test cases being
 * generated is a super linear function of the length of u32 values which is
 * leading to time outs.
 */
export function sparseU32Range(): readonly number[] {
  return kInterestingU32Values;
}

const kVectorU32Values = {
  2: kInterestingU32Values.flatMap(f => [
    [f, 1],
    [1, f],
  ]),
  3: kInterestingU32Values.flatMap(f => [
    [f, 1, 2],
    [1, f, 2],
    [1, 2, f],
  ]),
  4: kInterestingU32Values.flatMap(f => [
    [f, 1, 2, 3],
    [1, f, 2, 3],
    [1, 2, f, 3],
    [1, 2, 3, f],
  ]),
};

/**
 * Returns set of vectors, indexed by dimension containing interesting u32
 * values.
 *
 * The tests do not do the simple option for coverage of computing the cartesian
 * product of all of the interesting u32 values N times for vecN tests,
 * because that creates a huge number of tests for vec3 and vec4, leading to
 * time outs.
 *
 * Instead they insert the interesting u32 values into each location of the
 * vector to get a spread of testing over the entire range. This reduces the
 * number of cases being run substantially, but maintains coverage.
 */
export function vectorU32Range(dim: number): ROArrayArray<number> {
  assert(dim === 2 || dim === 3 || dim === 4, 'vectorU32Range only accepts dimensions 2, 3, and 4');
  return kVectorU32Values[dim];
}

const kSparseVectorU32Values = {
  2: sparseU32Range().map((i, idx) => [idx % 2 === 0 ? i : idx, idx % 2 === 1 ? i : -idx]),
  3: sparseU32Range().map((i, idx) => [
    idx % 3 === 0 ? i : idx,
    idx % 3 === 1 ? i : -idx,
    idx % 3 === 2 ? i : idx,
  ]),
  4: sparseU32Range().map((i, idx) => [
    idx % 4 === 0 ? i : idx,
    idx % 4 === 1 ? i : -idx,
    idx % 4 === 2 ? i : idx,
    idx % 4 === 3 ? i : -idx,
  ]),
};

/**
 * Minimal set of vectors, indexed by dimension, that contain interesting
 * abstract integer values.
 *
 * This is an even more stripped down version of `vectorU32Range` for when
 * pairs of vectors are being tested.
 * All interesting integers from sparseU32Range are guaranteed to be
 * tested, but not in every position.
 */
export function sparseVectorU32Range(dim: number): ROArrayArray<number> {
  assert(
    dim === 2 || dim === 3 || dim === 4,
    'sparseVectorU32Range only accepts dimensions 2, 3, and 4'
  );
  return kSparseVectorU32Values[dim];
}

/**
 * @returns an ascending sorted array of numbers spread over the entire range of 32-bit unsigned ints
 *
 * Numbers are biased towards 0, and 0 is included in the range.
 *
 * @param count number of entries to include in the range, in addition to 0, must be greater than 0, defaults to 50
 */
export function fullU32Range(count: number = 50): Array<number> {
  return [0, ...biasedRange(1, kValue.u32.max, count)].map(Math.trunc);
}

/** Short list of i64 values of interest to test against */
const kInterestingI64Values: readonly bigint[] = [
  kValue.i64.negative.max,
  kValue.i64.negative.max / 2n,
  -256n,
  -10n,
  -1n,
  0n,
  1n,
  10n,
  256n,
  kValue.i64.positive.max / 2n,
  kValue.i64.positive.max,
];

/** @returns minimal i64 values that cover the entire range of i64 behaviours
 *
 * This is used instead of fullI64Range when the number of test cases being
 * generated is a super linear function of the length of i64 values which is
 * leading to time outs.
 */
export function sparseI64Range(): readonly bigint[] {
  return kInterestingI64Values;
}

const kVectorI64Values = {
  2: kInterestingI64Values.flatMap(f => [
    [f, 1n],
    [-1n, f],
  ]),
  3: kInterestingI64Values.flatMap(f => [
    [f, 1n, -2n],
    [-1n, f, 2n],
    [1n, -2n, f],
  ]),
  4: kInterestingI64Values.flatMap(f => [
    [f, -1n, 2n, 3n],
    [1n, f, -2n, 3n],
    [1n, 2n, f, -3n],
    [-1n, 2n, -3n, f],
  ]),
};

/**
 * Returns set of vectors, indexed by dimension containing interesting i64
 * values.
 *
 * The tests do not do the simple option for coverage of computing the cartesian
 * product of all of the interesting i64 values N times for vecN tests,
 * because that creates a huge number of tests for vec3 and vec4, leading to
 * time outs.
 *
 * Instead they insert the interesting i64 values into each location of the
 * vector to get a spread of testing over the entire range. This reduces the
 * number of cases being run substantially, but maintains coverage.
 */
export function vectorI64Range(dim: number): ROArrayArray<bigint> {
  assert(dim === 2 || dim === 3 || dim === 4, 'vectorI64Range only accepts dimensions 2, 3, and 4');
  return kVectorI64Values[dim];
}

const kSparseVectorI64Values = {
  2: sparseI64Range().map((i, idx) => [
    idx % 2 === 0 ? i : BigInt(idx),
    idx % 2 === 1 ? i : -BigInt(idx),
  ]),
  3: sparseI64Range().map((i, idx) => [
    idx % 3 === 0 ? i : BigInt(idx),
    idx % 3 === 1 ? i : -BigInt(idx),
    idx % 3 === 2 ? i : BigInt(idx),
  ]),
  4: sparseI64Range().map((i, idx) => [
    idx % 4 === 0 ? i : BigInt(idx),
    idx % 4 === 1 ? i : -BigInt(idx),
    idx % 4 === 2 ? i : BigInt(idx),
    idx % 4 === 3 ? i : -BigInt(idx),
  ]),
};

/**
 * Minimal set of vectors, indexed by dimension, that contain interesting
 * abstract integer values.
 *
 * This is an even more stripped down version of `vectorI64Range` for when
 * pairs of vectors are being tested.
 * All interesting integers from sparseI64Range are guaranteed to be
 * tested, but not in every position.
 */
export function sparseVectorI64Range(dim: number): ROArrayArray<bigint> {
  assert(
    dim === 2 || dim === 3 || dim === 4,
    'sparseVectorI64Range only accepts dimensions 2, 3, and 4'
  );
  return kSparseVectorI64Values[dim];
}

/**
 * @returns an ascending sorted array of numbers spread over the entire range of 64-bit signed ints
 *
 * Numbers are divided into 2 regions: negatives, and positives, with their spreads biased towards 0
 * Zero is included in range.
 *
 * @param counts structure param with 2 entries indicating the number of entries to be generated each region, values must be 0 or greater.
 */
export function fullI64Range(
  counts: {
    negative?: number;
    positive: number;
  } = { positive: 50 }
): Array<bigint> {
  counts.negative = counts.negative === undefined ? counts.positive : counts.negative;
  return [
    ...biasedRangeBigInt(kValue.i64.negative.min, -1n, counts.negative),
    0n,
    ...biasedRangeBigInt(1n, kValue.i64.positive.max, counts.positive),
  ];
}

/** Short list of f32 values of interest to test against */
const kInterestingF32Values: readonly number[] = [
  kValue.f32.negative.min,
  -10.0,
  -1.0,
  -0.125,
  kValue.f32.negative.max,
  kValue.f32.negative.subnormal.min,
  kValue.f32.negative.subnormal.max,
  -0.0,
  0.0,
  kValue.f32.positive.subnormal.min,
  kValue.f32.positive.subnormal.max,
  kValue.f32.positive.min,
  0.125,
  1.0,
  10.0,
  kValue.f32.positive.max,
];

/** @returns minimal f32 values that cover the entire range of f32 behaviours
 *
 * Has specially selected values that cover edge cases, normals, and subnormals.
 * This is used instead of fullF32Range when the number of test cases being
 * generated is a super linear function of the length of f32 values which is
 * leading to time outs.
 *
 * These values have been chosen to attempt to test the widest range of f32
 * behaviours in the lowest number of entries, so may potentially miss function
 * specific values of interest. If there are known values of interest they
 * should be appended to this list in the test generation code.
 */
export function sparseScalarF32Range(): readonly number[] {
  return kInterestingF32Values;
}

const kVectorF32Values = {
  2: kInterestingF32Values.flatMap(f => [
    [f, 1.0],
    [-1.0, f],
  ]),
  3: kInterestingF32Values.flatMap(f => [
    [f, 1.0, -2.0],
    [-1.0, f, 2.0],
    [1.0, -2.0, f],
  ]),
  4: kInterestingF32Values.flatMap(f => [
    [f, -1.0, 2.0, 3.0],
    [1.0, f, -2.0, 3.0],
    [1.0, 2.0, f, -3.0],
    [-1.0, 2.0, -3.0, f],
  ]),
};

/**
 * Returns set of vectors, indexed by dimension containing interesting float
 * values.
 *
 * The tests do not do the simple option for coverage of computing the cartesian
 * product of all of the interesting float values N times for vecN tests,
 * because that creates a huge number of tests for vec3 and vec4, leading to
 * time outs.
 *
 * Instead they insert the interesting f32 values into each location of the
 * vector to get a spread of testing over the entire range. This reduces the
 * number of cases being run substantially, but maintains coverage.
 */
export function vectorF32Range(dim: number): ROArrayArray<number> {
  assert(dim === 2 || dim === 3 || dim === 4, 'vectorF32Range only accepts dimensions 2, 3, and 4');
  return kVectorF32Values[dim];
}

const kSparseVectorF32Values = {
  2: sparseScalarF32Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]),
  3: sparseScalarF32Range().map((f, idx) => [
    idx % 3 === 0 ? f : idx,
    idx % 3 === 1 ? f : -idx,
    idx % 3 === 2 ? f : idx,
  ]),
  4: sparseScalarF32Range().map((f, idx) => [
    idx % 4 === 0 ? f : idx,
    idx % 4 === 1 ? f : -idx,
    idx % 4 === 2 ? f : idx,
    idx % 4 === 3 ? f : -idx,
  ]),
};

/**
 * Minimal set of vectors, indexed by dimension, that contain interesting float
 * values.
 *
 * This is an even more stripped down version of `vectorF32Range` for when
 * pairs of vectors are being tested.
 * All of the interesting floats from sparseScalarF32 are guaranteed to be
 * tested, but not in every position.
 */
export function sparseVectorF32Range(dim: number): ROArrayArray<number> {
  assert(
    dim === 2 || dim === 3 || dim === 4,
    'sparseVectorF32Range only accepts dimensions 2, 3, and 4'
  );
  return kSparseVectorF32Values[dim];
}

const kSparseMatrixF32Values = {
  2: {
    2: kInterestingF32Values.map((f, idx) => [
      [idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx],
      [idx % 4 === 2 ? f : -idx, idx % 4 === 3 ? f : idx],
    ]),
    3: kInterestingF32Values.map((f, idx) => [
      [idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx, idx % 6 === 2 ? f : idx],
      [idx % 6 === 3 ? f : -idx, idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
    ]),
    4: kInterestingF32Values.map((f, idx) => [
      [
        idx % 8 === 0 ? f : idx,
        idx % 8 === 1 ? f : -idx,
        idx % 8 === 2 ? f : idx,
        idx % 8 === 3 ? f : -idx,
      ],
      [
        idx % 8 === 4 ? f : -idx,
        idx % 8 === 5 ? f : idx,
        idx % 8 === 6 ? f : -idx,
        idx % 8 === 7 ? f : idx,
      ],
    ]),
  },
  3: {
    2: kInterestingF32Values.map((f, idx) => [
      [idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx],
      [idx % 6 === 2 ? f : -idx, idx % 6 === 3 ? f : idx],
      [idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
    ]),
    3: kInterestingF32Values.map((f, idx) => [
      [idx % 9 === 0 ? f : idx, idx % 9 === 1 ? f : -idx, idx % 9 === 2 ? f : idx],
      [idx % 9 === 3 ? f : -idx, idx % 9 === 4 ? f : idx, idx % 9 === 5 ? f : -idx],
      [idx % 9 === 6 ? f : idx, idx % 9 === 7 ? f : -idx, idx % 9 === 8 ? f : idx],
    ]),
    4: kInterestingF32Values.map((f, idx) => [
      [
        idx % 12 === 0 ? f : idx,
        idx % 12 === 1 ? f : -idx,
        idx % 12 === 2 ? f : idx,
        idx % 12 === 3 ? f : -idx,
      ],
      [
        idx % 12 === 4 ? f : -idx,
        idx % 12 === 5 ? f : idx,
        idx % 12 === 6 ? f : -idx,
        idx % 12 === 7 ? f : idx,
      ],
      [
        idx % 12 === 8 ? f : idx,
        idx % 12 === 9 ? f : -idx,
        idx % 12 === 10 ? f : idx,
        idx % 12 === 11 ? f : -idx,
      ],
    ]),
  },
  4: {
    2: kInterestingF32Values.map((f, idx) => [
      [idx % 8 === 0 ? f : idx, idx % 8 === 1 ? f : -idx],
      [idx % 8 === 2 ? f : -idx, idx % 8 === 3 ? f : idx],
      [idx % 8 === 4 ? f : idx, idx % 8 === 5 ? f : -idx],
      [idx % 8 === 6 ? f : -idx, idx % 8 === 7 ? f : idx],
    ]),
    3: kInterestingF32Values.map((f, idx) => [
      [idx % 12 === 0 ? f : idx, idx % 12 === 1 ? f : -idx, idx % 12 === 2 ? f : idx],
      [idx % 12 === 3 ? f : -idx, idx % 12 === 4 ? f : idx, idx % 12 === 5 ? f : -idx],
      [idx % 12 === 6 ? f : idx, idx % 12 === 7 ? f : -idx, idx % 12 === 8 ? f : idx],
      [idx % 12 === 9 ? f : -idx, idx % 12 === 10 ? f : idx, idx % 12 === 11 ? f : -idx],
    ]),
    4: kInterestingF32Values.map((f, idx) => [
      [
        idx % 16 === 0 ? f : idx,
        idx % 16 === 1 ? f : -idx,
        idx % 16 === 2 ? f : idx,
        idx % 16 === 3 ? f : -idx,
      ],
      [
        idx % 16 === 4 ? f : -idx,
        idx % 16 === 5 ? f : idx,
        idx % 16 === 6 ? f : -idx,
        idx % 16 === 7 ? f : idx,
      ],
      [
        idx % 16 === 8 ? f : idx,
        idx % 16 === 9 ? f : -idx,
        idx % 16 === 10 ? f : idx,
        idx % 16 === 11 ? f : -idx,
      ],
      [
        idx % 16 === 12 ? f : -idx,
        idx % 16 === 13 ? f : idx,
        idx % 16 === 14 ? f : -idx,
        idx % 16 === 15 ? f : idx,
      ],
    ]),
  },
};

/**
 * Returns a minimal set of matrices, indexed by dimension containing
 * interesting float values.
 *
 * This is the matrix analogue of `sparseVectorF32Range`, so it is producing a
 * minimal coverage set of matrices that test all of the interesting f32 values.
 * There is not a more expansive set of matrices, since matrices are even more
 * expensive than vectors for increasing runtime with coverage.
 *
 * All of the interesting floats from sparseScalarF32 are guaranteed to be
 * tested, but not in every position.
 */
export function sparseMatrixF32Range(c: number, r: number): ROArrayArrayArray<number> {
  assert(
    c === 2 || c === 3 || c === 4,
    'sparseMatrixF32Range only accepts column counts of 2, 3, and 4'
  );
  assert(
    r === 2 || r === 3 || r === 4,
    'sparseMatrixF32Range only accepts row counts of 2, 3, and 4'
  );
  return kSparseMatrixF32Values[c][r];
}

/** Short list of f16 values of interest to test against */
const kInterestingF16Values: readonly number[] = [
  kValue.f16.negative.min,
  -10.0,
  -1.0,
  -0.125,
  kValue.f16.negative.max,
  kValue.f16.negative.subnormal.min,
  kValue.f16.negative.subnormal.max,
  -0.0,
  0.0,
  kValue.f16.positive.subnormal.min,
  kValue.f16.positive.subnormal.max,
  kValue.f16.positive.min,
  0.125,
  1.0,
  10.0,
  kValue.f16.positive.max,
];

/** @returns minimal f16 values that cover the entire range of f16 behaviours
 *
 * Has specially selected values that cover edge cases, normals, and subnormals.
 * This is used instead of fullF16Range when the number of test cases being
 * generated is a super linear function of the length of f16 values which is
 * leading to time outs.
 *
 * These values have been chosen to attempt to test the widest range of f16
 * behaviours in the lowest number of entries, so may potentially miss function
 * specific values of interest. If there are known values of interest they
 * should be appended to this list in the test generation code.
 */
export function sparseScalarF16Range(): readonly number[] {
  return kInterestingF16Values;
}

const kVectorF16Values = {
  2: kInterestingF16Values.flatMap(f => [
    [f, 1.0],
    [-1.0, f],
  ]),
  3: kInterestingF16Values.flatMap(f => [
    [f, 1.0, -2.0],
    [-1.0, f, 2.0],
    [1.0, -2.0, f],
  ]),
  4: kInterestingF16Values.flatMap(f => [
    [f, -1.0, 2.0, 3.0],
    [1.0, f, -2.0, 3.0],
    [1.0, 2.0, f, -3.0],
    [-1.0, 2.0, -3.0, f],
  ]),
};

/**
 * Returns set of vectors, indexed by dimension containing interesting f16
 * values.
 *
 * The tests do not do the simple option for coverage of computing the cartesian
 * product of all of the interesting float values N times for vecN tests,
 * because that creates a huge number of tests for vec3 and vec4, leading to
 * time outs.
 *
 * Instead they insert the interesting f16 values into each location of the
 * vector to get a spread of testing over the entire range. This reduces the
 * number of cases being run substantially, but maintains coverage.
 */
export function vectorF16Range(dim: number): ROArrayArray<number> {
  assert(dim === 2 || dim === 3 || dim === 4, 'vectorF16Range only accepts dimensions 2, 3, and 4');
  return kVectorF16Values[dim];
}

const kSparseVectorF16Values = {
  2: sparseScalarF16Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]),
  3: sparseScalarF16Range().map((f, idx) => [
    idx % 3 === 0 ? f : idx,
    idx % 3 === 1 ? f : -idx,
    idx % 3 === 2 ? f : idx,
  ]),
  4: sparseScalarF16Range().map((f, idx) => [
    idx % 4 === 0 ? f : idx,
    idx % 4 === 1 ? f : -idx,
    idx % 4 === 2 ? f : idx,
    idx % 4 === 3 ? f : -idx,
  ]),
};

/**
 * Minimal set of vectors, indexed by dimension, that contain interesting f16
 * values.
 *
 * This is an even more stripped down version of `vectorF16Range` for when
 * pairs of vectors are being tested.
 * All of the interesting floats from sparseScalarF16 are guaranteed to be
 * tested, but not in every position.
 */
export function sparseVectorF16Range(dim: number): ROArrayArray<number> {
  assert(
    dim === 2 || dim === 3 || dim === 4,
    'sparseVectorF16Range only accepts dimensions 2, 3, and 4'
  );
  return kSparseVectorF16Values[dim];
}

const kSparseMatrixF16Values = {
  2: {
    2: kInterestingF16Values.map((f, idx) => [
      [idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx],
      [idx % 4 === 2 ? f : -idx, idx % 4 === 3 ? f : idx],
    ]),
    3: kInterestingF16Values.map((f, idx) => [
      [idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx, idx % 6 === 2 ? f : idx],
      [idx % 6 === 3 ? f : -idx, idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
    ]),
    4: kInterestingF16Values.map((f, idx) => [
      [
        idx % 8 === 0 ? f : idx,
        idx % 8 === 1 ? f : -idx,
        idx % 8 === 2 ? f : idx,
        idx % 8 === 3 ? f : -idx,
      ],
      [
        idx % 8 === 4 ? f : -idx,
        idx % 8 === 5 ? f : idx,
        idx % 8 === 6 ? f : -idx,
        idx % 8 === 7 ? f : idx,
      ],
    ]),
  },
  3: {
    2: kInterestingF16Values.map((f, idx) => [
      [idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx],
      [idx % 6 === 2 ? f : -idx, idx % 6 === 3 ? f : idx],
      [idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
    ]),
    3: kInterestingF16Values.map((f, idx) => [
      [idx % 9 === 0 ? f : idx, idx % 9 === 1 ? f : -idx, idx % 9 === 2 ? f : idx],
      [idx % 9 === 3 ? f : -idx, idx % 9 === 4 ? f : idx, idx % 9 === 5 ? f : -idx],
      [idx % 9 === 6 ? f : idx, idx % 9 === 7 ? f : -idx, idx % 9 === 8 ? f : idx],
    ]),
    4: kInterestingF16Values.map((f, idx) => [
      [
        idx % 12 === 0 ? f : idx,
        idx % 12 === 1 ? f : -idx,
        idx % 12 === 2 ? f : idx,
        idx % 12 === 3 ? f : -idx,
      ],
      [
        idx % 12 === 4 ? f : -idx,
        idx % 12 === 5 ? f : idx,
        idx % 12 === 6 ? f : -idx,
        idx % 12 === 7 ? f : idx,
      ],
      [
        idx % 12 === 8 ? f : idx,
        idx % 12 === 9 ? f : -idx,
        idx % 12 === 10 ? f : idx,
        idx % 12 === 11 ? f : -idx,
      ],
    ]),
  },
  4: {
    2: kInterestingF16Values.map((f, idx) => [
      [idx % 8 === 0 ? f : idx, idx % 8 === 1 ? f : -idx],
      [idx % 8 === 2 ? f : -idx, idx % 8 === 3 ? f : idx],
      [idx % 8 === 4 ? f : idx, idx % 8 === 5 ? f : -idx],
      [idx % 8 === 6 ? f : -idx, idx % 8 === 7 ? f : idx],
    ]),
    3: kInterestingF16Values.map((f, idx) => [
      [idx % 12 === 0 ? f : idx, idx % 12 === 1 ? f : -idx, idx % 12 === 2 ? f : idx],
      [idx % 12 === 3 ? f : -idx, idx % 12 === 4 ? f : idx, idx % 12 === 5 ? f : -idx],
      [idx % 12 === 6 ? f : idx, idx % 12 === 7 ? f : -idx, idx % 12 === 8 ? f : idx],
      [idx % 12 === 9 ? f : -idx, idx % 12 === 10 ? f : idx, idx % 12 === 11 ? f : -idx],
    ]),
    4: kInterestingF16Values.map((f, idx) => [
      [
        idx % 16 === 0 ? f : idx,
        idx % 16 === 1 ? f : -idx,
        idx % 16 === 2 ? f : idx,
        idx % 16 === 3 ? f : -idx,
      ],
      [
        idx % 16 === 4 ? f : -idx,
        idx % 16 === 5 ? f : idx,
        idx % 16 === 6 ? f : -idx,
        idx % 16 === 7 ? f : idx,
      ],
      [
        idx % 16 === 8 ? f : idx,
        idx % 16 === 9 ? f : -idx,
        idx % 16 === 10 ? f : idx,
        idx % 16 === 11 ? f : -idx,
      ],
      [
        idx % 16 === 12 ? f : -idx,
        idx % 16 === 13 ? f : idx,
        idx % 16 === 14 ? f : -idx,
        idx % 16 === 15 ? f : idx,
      ],
    ]),
  },
};

/**
 * Returns a minimal set of matrices, indexed by dimension containing interesting
 * f16 values.
 *
 * This is the matrix analogue of `sparseVectorF16Range`, so it is producing a
 * minimal coverage set of matrices that test all of the interesting f16 values.
 * There is not a more expansive set of matrices, since matrices are even more
 * expensive than vectors for increasing runtime with coverage.
 *
 * All of the interesting floats from sparseScalarF16 are guaranteed to be tested, but
 * not in every position.
 */
export function sparseMatrixF16Range(c: number, r: number): ROArrayArray<number>[] {
  assert(
    c === 2 || c === 3 || c === 4,
    'sparseMatrixF16Range only accepts column counts of 2, 3, and 4'
  );
  assert(
    r === 2 || r === 3 || r === 4,
    'sparseMatrixF16Range only accepts row counts of 2, 3, and 4'
  );
  return kSparseMatrixF16Values[c][r];
}

/** Short list of f64 values of interest to test against */
const kInterestingF64Values: readonly number[] = [
  kValue.f64.negative.min,
  -10.0,
  -1.0,
  -0.125,
  kValue.f64.negative.max,
  kValue.f64.negative.subnormal.min,
  kValue.f64.negative.subnormal.max,
  -0.0,
  0.0,
  kValue.f64.positive.subnormal.min,
  kValue.f64.positive.subnormal.max,
  kValue.f64.positive.min,
  0.125,
  1.0,
  10.0,
  kValue.f64.positive.max,
];

/** @returns minimal F64 values that cover the entire range of F64 behaviours
 *
 * Has specially selected values that cover edge cases, normals, and subnormals.
 * This is used instead of fullF64Range when the number of test cases being
 * generated is a super linear function of the length of F64 values which is
 * leading to time outs.
 *
 * These values have been chosen to attempt to test the widest range of F64
 * behaviours in the lowest number of entries, so may potentially miss function
 * specific values of interest. If there are known values of interest they
 * should be appended to this list in the test generation code.
 */
export function sparseScalarF64Range(): readonly number[] {
  return kInterestingF64Values;
}

const kVectorF64Values = {
  2: kInterestingF64Values.flatMap(f => [
    [f, 1.0],
    [-1.0, f],
  ]),
  3: kInterestingF64Values.flatMap(f => [
    [f, 1.0, -2.0],
    [-1.0, f, 2.0],
    [1.0, -2.0, f],
  ]),
  4: kInterestingF64Values.flatMap(f => [
    [f, -1.0, 2.0, 3.0],
    [1.0, f, -2.0, 3.0],
    [1.0, 2.0, f, -3.0],
    [-1.0, 2.0, -3.0, f],
  ]),
};

/**
 * Returns set of vectors, indexed by dimension containing interesting float
 * values.
 *
 * The tests do not do the simple option for coverage of computing the cartesian
 * product of all of the interesting float values N times for vecN tests,
 * because that creates a huge number of tests for vec3 and vec4, leading to
 * time outs.
 *
 * Instead they insert the interesting F64 values into each location of the
 * vector to get a spread of testing over the entire range. This reduces the
 * number of cases being run substantially, but maintains coverage.
 */
export function vectorF64Range(dim: number): ROArrayArray<number> {
  assert(dim === 2 || dim === 3 || dim === 4, 'vectorF64Range only accepts dimensions 2, 3, and 4');
  return kVectorF64Values[dim];
}

const kSparseVectorF64Values = {
  2: sparseScalarF64Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]),
  3: sparseScalarF64Range().map((f, idx) => [
    idx % 3 === 0 ? f : idx,
    idx % 3 === 1 ? f : -idx,
    idx % 3 === 2 ? f : idx,
  ]),
  4: sparseScalarF64Range().map((f, idx) => [
    idx % 4 === 0 ? f : idx,
    idx % 4 === 1 ? f : -idx,
    idx % 4 === 2 ? f : idx,
    idx % 4 === 3 ? f : -idx,
  ]),
};

/**
 * Minimal set of vectors, indexed by dimension, that contain interesting f64
 * values.
 *
 * This is an even more stripped down version of `vectorF64Range` for when
 * pairs of vectors are being tested.
 * All the interesting floats from sparseScalarF64 are guaranteed to be tested, but
 * not in every position.
 */
export function sparseVectorF64Range(dim: number): ROArrayArray<number> {
  assert(
    dim === 2 || dim === 3 || dim === 4,
    'sparseVectorF64Range only accepts dimensions 2, 3, and 4'
  );
  return kSparseVectorF64Values[dim];
}

const kSparseMatrixF64Values = {
  2: {
    2: kInterestingF64Values.map((f, idx) => [
      [idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx],
      [idx % 4 === 2 ? f : -idx, idx % 4 === 3 ? f : idx],
    ]),
    3: kInterestingF64Values.map((f, idx) => [
      [idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx, idx % 6 === 2 ? f : idx],
      [idx % 6 === 3 ? f : -idx, idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
    ]),
    4: kInterestingF64Values.map((f, idx) => [
      [
        idx % 8 === 0 ? f : idx,
        idx % 8 === 1 ? f : -idx,
        idx % 8 === 2 ? f : idx,
        idx % 8 === 3 ? f : -idx,
      ],
      [
        idx % 8 === 4 ? f : -idx,
        idx % 8 === 5 ? f : idx,
        idx % 8 === 6 ? f : -idx,
        idx % 8 === 7 ? f : idx,
      ],
    ]),
  },
  3: {
    2: kInterestingF64Values.map((f, idx) => [
      [idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx],
      [idx % 6 === 2 ? f : -idx, idx % 6 === 3 ? f : idx],
      [idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
    ]),
    3: kInterestingF64Values.map((f, idx) => [
      [idx % 9 === 0 ? f : idx, idx % 9 === 1 ? f : -idx, idx % 9 === 2 ? f : idx],
      [idx % 9 === 3 ? f : -idx, idx % 9 === 4 ? f : idx, idx % 9 === 5 ? f : -idx],
      [idx % 9 === 6 ? f : idx, idx % 9 === 7 ? f : -idx, idx % 9 === 8 ? f : idx],
    ]),
    4: kInterestingF64Values.map((f, idx) => [
      [
        idx % 12 === 0 ? f : idx,
        idx % 12 === 1 ? f : -idx,
        idx % 12 === 2 ? f : idx,
        idx % 12 === 3 ? f : -idx,
      ],
      [
        idx % 12 === 4 ? f : -idx,
        idx % 12 === 5 ? f : idx,
        idx % 12 === 6 ? f : -idx,
        idx % 12 === 7 ? f : idx,
      ],
      [
        idx % 12 === 8 ? f : idx,
        idx % 12 === 9 ? f : -idx,
        idx % 12 === 10 ? f : idx,
        idx % 12 === 11 ? f : -idx,
      ],
    ]),
  },
  4: {
    2: kInterestingF64Values.map((f, idx) => [
      [idx % 8 === 0 ? f : idx, idx % 8 === 1 ? f : -idx],
      [idx % 8 === 2 ? f : -idx, idx % 8 === 3 ? f : idx],
      [idx % 8 === 4 ? f : idx, idx % 8 === 5 ? f : -idx],
      [idx % 8 === 6 ? f : -idx, idx % 8 === 7 ? f : idx],
    ]),
    3: kInterestingF64Values.map((f, idx) => [
      [idx % 12 === 0 ? f : idx, idx % 12 === 1 ? f : -idx, idx % 12 === 2 ? f : idx],
      [idx % 12 === 3 ? f : -idx, idx % 12 === 4 ? f : idx, idx % 12 === 5 ? f : -idx],
      [idx % 12 === 6 ? f : idx, idx % 12 === 7 ? f : -idx, idx % 12 === 8 ? f : idx],
      [idx % 12 === 9 ? f : -idx, idx % 12 === 10 ? f : idx, idx % 12 === 11 ? f : -idx],
    ]),
    4: kInterestingF64Values.map((f, idx) => [
      [
        idx % 16 === 0 ? f : idx,
        idx % 16 === 1 ? f : -idx,
        idx % 16 === 2 ? f : idx,
        idx % 16 === 3 ? f : -idx,
      ],
      [
        idx % 16 === 4 ? f : -idx,
        idx % 16 === 5 ? f : idx,
        idx % 16 === 6 ? f : -idx,
        idx % 16 === 7 ? f : idx,
      ],
      [
        idx % 16 === 8 ? f : idx,
        idx % 16 === 9 ? f : -idx,
        idx % 16 === 10 ? f : idx,
        idx % 16 === 11 ? f : -idx,
      ],
      [
        idx % 16 === 12 ? f : -idx,
        idx % 16 === 13 ? f : idx,
        idx % 16 === 14 ? f : -idx,
        idx % 16 === 15 ? f : idx,
      ],
    ]),
  },
};

/**
 * Returns a minimal set of matrices, indexed by dimension containing interesting
 * float values.
 *
 * This is the matrix analogue of `sparseVectorF64Range`, so it is producing a
 * minimal coverage set of matrices that test all the interesting f64 values.
 * There is not a more expansive set of matrices, since matrices are even more
 * expensive than vectors for increasing runtime with coverage.
 *
 * All the interesting floats from sparseScalarF64 are guaranteed to be tested, but
 * not in every position.
 */
export function sparseMatrixF64Range(cols: number, rows: number): ROArrayArrayArray<number> {
  assert(
    cols === 2 || cols === 3 || cols === 4,
    'sparseMatrixF64Range only accepts column counts of 2, 3, and 4'
  );
  assert(
    rows === 2 || rows === 3 || rows === 4,
    'sparseMatrixF64Range only accepts row counts of 2, 3, and 4'
  );
  return kSparseMatrixF64Values[cols][rows];
}

/**
 * @returns the result matrix in Array<Array<number>> type.
 *
 * Matrix multiplication. A is m x n and B is n x p. Returns
 * m x p result.
 */
// A is m x n. B is n x p. product is m x p.
export function multiplyMatrices(
  A: Array<Array<number>>,
  B: Array<Array<number>>
): Array<Array<number>> {
  assert(A.length > 0 && B.length > 0 && B[0].length > 0 && A[0].length === B.length);
  const product = new Array<Array<number>>(A.length);
  for (let i = 0; i < product.length; ++i) {
    product[i] = new Array<number>(B[0].length).fill(0);
  }

  for (let m = 0; m < A.length; ++m) {
    for (let p = 0; p < B[0].length; ++p) {
      for (let n = 0; n < B.length; ++n) {
        product[m][p] += A[m][n] * B[n][p];
      }
    }
  }

  return product;
}

/** Sign-extend the `bits`-bit number `n` to a 32-bit signed integer. */
export function signExtend(n: number, bits: number): number {
  const shift = 32 - bits;
  return (n << shift) >> shift;
}

export interface QuantizeFunc<T> {
  (num: T): T;
}

/** @returns the closest 32-bit floating point value to the input */
export function quantizeToF32(num: number): number {
  return Math.fround(num);
}

/** @returns the closest 16-bit floating point value to the input */
export function quantizeToF16(num: number): number {
  return hfround(num);
}

/**
 * @returns the closest 32-bit signed integer value to the input, rounding
 * towards 0, if not already an integer
 */
export function quantizeToI32(num: number): number {
  if (num >= kValue.i32.positive.max) {
    return kValue.i32.positive.max;
  }
  if (num <= kValue.i32.negative.min) {
    return kValue.i32.negative.min;
  }
  return Math.trunc(num);
}

/**
 * @returns the closest 32-bit unsigned integer value to the input, rounding
 * towards 0, if not already an integer
 */
export function quantizeToU32(num: number): number {
  if (num >= kValue.u32.max) {
    return kValue.u32.max;
  }
  if (num <= 0) {
    return 0;
  }
  return Math.trunc(num);
}

/**
 * @returns the closest 64-bit signed integer value to the input.
 */
export function quantizeToI64(num: bigint): bigint {
  if (num >= kValue.i64.positive.max) {
    return kValue.i64.positive.max;
  }
  if (num <= kValue.i64.negative.min) {
    return kValue.i64.negative.min;
  }
  return num;
}

/** @returns whether the number is an integer and a power of two */
export function isPowerOfTwo(n: number): boolean {
  if (!Number.isInteger(n)) {
    return false;
  }
  assert((n | 0) === n, 'isPowerOfTwo only supports 32-bit numbers');
  return n !== 0 && (n & (n - 1)) === 0;
}

/** @returns the Greatest Common Divisor (GCD) of the inputs */
export function gcd(a: number, b: number): number {
  assert(Number.isInteger(a) && a > 0);
  assert(Number.isInteger(b) && b > 0);

  while (b !== 0) {
    const bTemp = b;
    b = a % b;
    a = bTemp;
  }

  return a;
}

/** @returns the Least Common Multiplier (LCM) of the inputs */
export function lcm(a: number, b: number): number {
  return (a * b) / gcd(a, b);
}

/** @returns the cross of an array with the intermediate result of cartesianProduct
 *
 * @param elements array of values to cross with the intermediate result of
 *                 cartesianProduct
 * @param intermediate arrays of values representing the partial result of
 *                     cartesianProduct
 */
function cartesianProductImpl<T>(
  elements: readonly T[],
  intermediate: ROArrayArray<T>
): ROArrayArray<T> {
  const result: T[][] = [];
  elements.forEach((e: T) => {
    if (intermediate.length > 0) {
      intermediate.forEach((i: readonly T[]) => {
        result.push([...i, e]);
      });
    } else {
      result.push([e]);
    }
  });
  return result;
}

/** @returns the cartesian product (NxMx...) of a set of arrays
 *
 * This is implemented by calculating the cross of a single input against an
 * intermediate result for each input to build up the final array of arrays.
 *
 * There are examples of doing this more succinctly using map & reduce online,
 * but they are a bit more opaque to read.
 *
 * @param inputs arrays of numbers to calculate cartesian product over
 */
export function cartesianProduct<T>(...inputs: ROArrayArray<T>): ROArrayArray<T> {
  let result: ROArrayArray<T> = [];
  inputs.forEach((i: readonly T[]) => {
    result = cartesianProductImpl<T>(i, result);
  });

  return result;
}

/** @returns all of the permutations of an array
 *
 * Recursively calculates all of the permutations, does not cull duplicate
 * entries.
 *
 * Only feasible for inputs of lengths 5 or so, since the number of permutations
 * is (input.length)!, so will cause the stack to explode for longer inputs.
 *
 * This code could be made iterative using something like
 * Steinhaus–Johnson–Trotter and additionally turned into a generator to reduce
 * the stack size, but there is still a fundamental combinatorial explosion
 * here that will affect runtime.
 *
 * @param input the array to get permutations of
 */
export function calculatePermutations<T>(input: readonly T[]): ROArrayArray<T> {
  if (input.length === 0) {
    return [];
  }

  if (input.length === 1) {
    return [input];
  }

  if (input.length === 2) {
    return [input, [input[1], input[0]]];
  }

  const result: T[][] = [];
  input.forEach((head, idx) => {
    const tail = input.slice(0, idx).concat(input.slice(idx + 1));
    const permutations = calculatePermutations(tail);
    permutations.forEach(p => {
      result.push([head, ...p]);
    });
  });

  return result;
}

/**
 * Convert an Array of Arrays to linear array
 *
 * Caller is responsible to retaining the dimensions of the array for later
 * unflattening
 *
 * @param m Matrix to convert
 */
export function flatten2DArray<T>(m: ROArrayArray<T>): T[] {
  const c = m.length;
  const r = m[0].length;
  assert(
    m.every(c => c.length === r),
    `Unexpectedly received jagged array to flatten`
  );
  const result: T[] = Array<T>(c * r);
  for (let i = 0; i < c; i++) {
    for (let j = 0; j < r; j++) {
      result[j + i * r] = m[i][j];
    }
  }
  return result;
}

/**
 * Convert linear array to an Array of Arrays
 * @param n an array to convert
 * @param c number of elements in the array containing arrays
 * @param r number of elements in the arrays that are contained
 */
export function unflatten2DArray<T>(n: readonly T[], c: number, r: number): ROArrayArray<T> {
  assert(
    c > 0 && Number.isInteger(c) && r > 0 && Number.isInteger(r),
    `columns (${c}) and rows (${r}) need to be positive integers`
  );
  assert(n.length === c * r, `m.length(${n.length}) should equal c * r (${c * r})`);
  const result: T[][] = [...Array(c)].map(_ => [...Array(r)]);
  for (let i = 0; i < c; i++) {
    for (let j = 0; j < r; j++) {
      result[i][j] = n[j + i * r];
    }
  }
  return result;
}

/**
 * Performs a .map over a matrix and return the result
 * The shape of the input and output matrices will be the same
 *
 * @param m input matrix of type T
 * @param op operation that converts an element of type T to one of type S
 * @returns a matrix with elements of type S that are calculated by applying op element by element
 */
export function map2DArray<T, S>(m: ROArrayArray<T>, op: (input: T) => S): ROArrayArray<S> {
  const c = m.length;
  const r = m[0].length;
  assert(
    m.every(c => c.length === r),
    `Unexpectedly received jagged array to map`
  );
  const result: S[][] = [...Array(c)].map(_ => [...Array(r)]);
  for (let i = 0; i < c; i++) {
    for (let j = 0; j < r; j++) {
      result[i][j] = op(m[i][j]);
    }
  }
  return result;
}

/**
 * Performs a .every over a matrix and return the result
 *
 * @param m input matrix of type T
 * @param op operation that performs a test on an element
 * @returns a boolean indicating if the test passed for every element
 */
export function every2DArray<T>(m: ROArrayArray<T>, op: (input: T) => boolean): boolean {
  const r = m[0].length;
  assert(
    m.every(c => c.length === r),
    `Unexpectedly received jagged array to map`
  );
  return m.every(col => col.every(el => op(el)));
}

/**
 * Subtracts 2 vectors
 */
export function subtractVectors(v1: readonly number[], v2: readonly number[]) {
  return v1.map((v, i) => v - v2[i]);
}

/**
 * Computes the dot product of 2 vectors
 */
export function dotProduct(v1: readonly number[], v2: readonly number[]) {
  return v1.reduce((a, v, i) => a + v * v2[i], 0);
}

/** @returns the absolute value of a bigint */
export function absBigInt(v: bigint): bigint {
  return v < 0n ? -v : v;
}

/** @returns the maximum from a list of bigints */
export function maxBigInt(...vals: bigint[]): bigint {
  return vals.reduce((prev, cur) => (cur > prev ? cur : prev));
}

/** @returns the minimum from a list of bigints */
export function minBigInt(...vals: bigint[]): bigint {
  return vals.reduce((prev, cur) => (cur < prev ? cur : prev));
}
