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

export type CborValue = number | string | boolean | null | undefined
  | CborValue[] | { [key: string]: CborValue };

interface DecodeState { offset: number }

const textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;

function decodeUint(
  view: DataView,
  bytes: Uint8Array,
  state: DecodeState,
  additional: number,
): number {
  const offset = state.offset;
  if (additional < 24) return additional;
  const widths: Partial<Record<number, number>> = { 24: 1, 25: 2, 26: 4, 27: 8 };
  const width = widths[additional];
  if (width === undefined) throw new Error("Unsupported CBOR additional info");
  if (offset + width > bytes.length) throw new Error("CBOR payload truncated");
  state.offset += width;
  if (additional === 24) return bytes[offset] ?? 0;
  if (additional === 25) return view.getUint16(offset);
  if (additional === 26) return view.getUint32(offset);
  const numeric = Number(view.getBigUint64(offset));
  if (!Number.isSafeInteger(numeric)) throw new Error("CBOR integer exceeds JS safe range");
  return numeric;
}

function decodeFloat16(bits: number): number {
  const sign = (bits & 0x8000) ? -1 : 1;
  const exponent = (bits >> 10) & 0x1f;
  const fraction = bits & 0x03ff;
  if (exponent === 0) return fraction === 0 ? sign * 0 : sign * 2 ** -14 * (fraction / 1024);
  if (exponent === 0x1f) return fraction === 0 ? sign * Infinity : Number.NaN;
  return sign * 2 ** (exponent - 15) * (1 + fraction / 1024);
}

function decodeItem(view: DataView, bytes: Uint8Array, state: DecodeState): CborValue {
  if (state.offset >= bytes.length) throw new Error("CBOR payload truncated");
  const initial = bytes[state.offset++];
  if (initial === undefined) throw new Error("CBOR payload truncated");
  const major = initial >> 5;
  const additional = initial & 0x1f;
  if (major === 0) return decodeUint(view, bytes, state, additional);
  if (major === 1) return -1 - decodeUint(view, bytes, state, additional);
  if (major === 2 || major === 3) {
    const length = decodeUint(view, bytes, state, additional);
    if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated");
    const chunk = bytes.subarray(state.offset, state.offset + length);
    state.offset += length;
    if (major === 2) return Array.from(chunk);
    return textDecoder ? textDecoder.decode(chunk) : String.fromCharCode(...chunk);
  }
  if (major === 4) {
    const length = decodeUint(view, bytes, state, additional);
    return Array.from({ length }, () => decodeItem(view, bytes, state));
  }
  if (major === 5) {
    const length = decodeUint(view, bytes, state, additional);
    const value: Record<string, CborValue> = {};
    for (let index = 0; index < length; index += 1) {
      const key = decodeItem(view, bytes, state);
      if (typeof key !== "string" && typeof key !== "number" && typeof key !== "boolean"
        && key !== null) {
        throw new Error("Unsupported composite CBOR map key");
      }
      value[String(key)] = decodeItem(view, bytes, state);
    }
    return value;
  }
  if (major === 6) {
    decodeUint(view, bytes, state, additional);
    return decodeItem(view, bytes, state);
  }
  if (major === 7) {
    if (additional === 20) return false;
    if (additional === 21) return true;
    if (additional === 22) return null;
    if (additional === 23) return undefined;
    const widths: Partial<Record<number, number>> = { 25: 2, 26: 4, 27: 8 };
    const width = widths[additional];
    if (width === undefined) throw new Error("Unsupported CBOR major type");
    if (state.offset + width > bytes.length) throw new Error("CBOR payload truncated");
    const offset = state.offset;
    state.offset += width;
    if (additional === 25) return decodeFloat16(view.getUint16(offset));
    if (additional === 26) return view.getFloat32(offset);
    return view.getFloat64(offset);
  }
  throw new Error("Unsupported CBOR major type");
}

export function decodeCbor(buffer: ArrayBuffer | Uint8Array): CborValue {
  const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  const state = { offset: 0 };
  const value = decodeItem(view, bytes, state);
  if (state.offset !== bytes.length) throw new Error("Unexpected trailing bytes in CBOR payload");
  return value;
}
