export function uid(prefix = ''): string {
  return prefix + Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4);
}

export function clamp(v: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, v));
}

export function formatBytes(n: number): string {
  if (n < 1024) return `${n} B`;
  const units = ['KB', 'MB', 'GB'];
  let i = -1;
  do { n /= 1024; i++; } while (n >= 1024 && i < units.length - 1);
  return `${n.toFixed(n < 10 ? 1 : 0)} ${units[i]}`;
}

export function debounce<T extends (...a: any[]) => void>(fn: T, ms: number): T {
  let t: ReturnType<typeof setTimeout>;
  return ((...a: any[]) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...a), ms);
  }) as T;
}

/** Trigger a browser download of bytes/blob. */
export function download(data: Uint8Array | Blob, filename: string, mime = 'application/pdf') {
  const blob = data instanceof Blob ? data : new Blob([bufferOf(data)], { type: mime });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 4000);
}

/** Read a File/Blob into a Uint8Array. */
export async function readBytes(file: Blob): Promise<Uint8Array> {
  return new Uint8Array(await file.arrayBuffer());
}

/** Copy into a fresh ArrayBuffer-backed Uint8Array (detached-safe for Blob/WASM). */
export function bufferOf(u: Uint8Array): ArrayBuffer {
  const out = new ArrayBuffer(u.byteLength);
  new Uint8Array(out).set(u);
  return out;
}

/** Swap or strip a filename extension. */
export function withExt(name: string, ext: string): string {
  const base = name.replace(/\.[^./\\]+$/, '');
  return `${base}.${ext.replace(/^\./, '')}`;
}
export function baseName(name: string): string {
  return name.replace(/\.[^./\\]+$/, '');
}

export function suffixName(name: string, suffix: string): string {
  const dot = name.lastIndexOf('.');
  if (dot < 0) return name + suffix;
  return name.slice(0, dot) + suffix + name.slice(dot);
}

/** Parse a page-range string like "1-3, 5, 8-10" into 0-based indices within [0, total). */
export function parsePageRanges(spec: string, total: number): number[] {
  const out = new Set<number>();
  const clean = spec.trim();
  if (!clean) return [];
  for (const part of clean.split(',')) {
    const seg = part.trim();
    if (!seg) continue;
    const range = seg.match(/^(\d+)\s*-\s*(\d+)$/);
    if (range) {
      let a = parseInt(range[1], 10);
      let b = parseInt(range[2], 10);
      if (a > b) [a, b] = [b, a];
      for (let i = a; i <= b; i++) if (i >= 1 && i <= total) out.add(i - 1);
    } else if (/^\d+$/.test(seg)) {
      const n = parseInt(seg, 10);
      if (n >= 1 && n <= total) out.add(n - 1);
    }
  }
  return [...out].sort((x, y) => x - y);
}

/** Sniff a magic-byte file type for helpful UX (not security-critical). */
export function sniffType(bytes: Uint8Array): 'pdf' | 'png' | 'jpg' | 'gif' | 'webp' | 'bmp' | 'unknown' {
  const b = bytes;
  if (b[0] === 0x25 && b[1] === 0x50 && b[2] === 0x44 && b[3] === 0x46) return 'pdf'; // %PDF
  if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) return 'png';
  if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'jpg';
  if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46) return 'gif';
  if (b[0] === 0x42 && b[1] === 0x4d) return 'bmp';
  if (b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[8] === 0x57) return 'webp';
  return 'unknown';
}

export function isProbablyPdf(bytes: Uint8Array): boolean {
  return sniffType(bytes) === 'pdf';
}

/** Whether the browser context is cross-origin isolated (multi-threaded WASM available). */
export function isCrossOriginIsolated(): boolean {
  return typeof crossOriginIsolated !== 'undefined' && crossOriginIsolated === true;
}

export async function sha256Hex(bytes: Uint8Array): Promise<string> {
  const digest = await crypto.subtle.digest('SHA-256', bufferOf(bytes));
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
}
