// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later

export type NumericBins = number[] | Int8Array | Uint8Array | Float32Array | Float64Array;

export function isNumericBins(value: unknown): value is NumericBins {
  return Array.isArray(value)
    ? value.every((item) => typeof item === "number")
    : ArrayBuffer.isView(value) && !(value instanceof DataView);
}

const base64Lookup = new Uint8Array(128).fill(255);
const base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (let index = 0; index < base64Alphabet.length; index += 1) {
  base64Lookup[base64Alphabet.charCodeAt(index)] = index;
}
let spectrumBinBuffer = new Int8Array(0);

export function decodeBase64Int8(value: string): Int8Array {
  let end = value.length;
  while (end > 0 && value.charCodeAt(end - 1) === 61) end -= 1;
  const outputLength = end * 3 >>> 2;
  if (spectrumBinBuffer.length !== outputLength) spectrumBinBuffer = new Int8Array(outputLength);
  let outputIndex = 0;
  for (let index = 0; index < end;) {
    const sextets = [0, 0, 0, 0];
    for (let offset = 0; offset < 4 && index < end; offset += 1, index += 1) {
      const code = value.charCodeAt(index);
      const decoded = code < base64Lookup.length ? base64Lookup[code] : undefined;
      if (decoded === undefined || decoded === 255) throw new TypeError("Invalid base64 spectrum frame");
      sextets[offset] = decoded;
    }
    const packed = ((sextets[0] ?? 0) << 18) | ((sextets[1] ?? 0) << 12)
      | ((sextets[2] ?? 0) << 6) | (sextets[3] ?? 0);
    if (outputIndex < outputLength) spectrumBinBuffer[outputIndex++] = packed >> 16 & 0xff;
    if (outputIndex < outputLength) spectrumBinBuffer[outputIndex++] = packed >> 8 & 0xff;
    if (outputIndex < outputLength) spectrumBinBuffer[outputIndex++] = packed & 0xff;
  }
  return spectrumBinBuffer;
}

let nthScratch = new Float64Array(0);

export function nthElement(values: NumericBins, target: number): number | null {
  if (values.length === 0 || target < 0 || target >= values.length) return null;
  if (nthScratch.length < values.length) nthScratch = new Float64Array(values.length);
  for (let index = 0; index < values.length; index += 1) nthScratch[index] = values[index] ?? 0;
  let low = 0;
  let high = values.length - 1;
  while (low < high) {
    const pivot = nthScratch[low + ((high - low) >> 1)] ?? 0;
    let left = low;
    let right = high;
    while (left <= right) {
      while ((nthScratch[left] ?? Infinity) < pivot) left += 1;
      while ((nthScratch[right] ?? -Infinity) > pivot) right -= 1;
      if (left <= right) {
        const temporary = nthScratch[left] ?? 0;
        nthScratch[left] = nthScratch[right] ?? 0;
        nthScratch[right] = temporary;
        left += 1;
        right -= 1;
      }
    }
    if (right < target) low = left;
    if (target < left) high = right;
  }
  return nthScratch[target] ?? null;
}

export function estimateNoiseFloorDb(bins: unknown): number | null {
  if (!isNumericBins(bins) || bins.length === 0) return null;
  return nthElement(bins, Math.floor(bins.length * 0.15));
}
