// Worker client: lazy-spawn dedicated workers and talk to them via promises.
// Each worker is kept alive across calls so we don't pay startup cost per op.
// If Workers or OffscreenCanvas aren't available, callers can fall back to
// main-thread implementations (see window.supportsImageWorker).

(function () {
  const workers = {};       // name → Worker
  const pending = {};       // name → Map<id, { resolve, reject }>
  let nextId = 1;

  function getWorker(name, url) {
    if (workers[name]) return workers[name];
    const w = new Worker(url);
    pending[name] = new Map();
    w.onmessage = (ev) => {
      const { id, ok, error, ...rest } = ev.data || {};
      const slot = pending[name].get(id);
      if (!slot) return;
      pending[name].delete(id);
      if (ok) slot.resolve(rest);
      else slot.reject(new Error(error || 'worker error'));
    };
    w.onerror = (ev) => {
      // Reject every outstanding call if the worker explodes.
      for (const slot of pending[name].values()) slot.reject(new Error(ev.message || 'worker crash'));
      pending[name].clear();
      workers[name] = null;
    };
    workers[name] = w;
    return w;
  }

  function callWorker(name, url, payload, transfers = []) {
    return new Promise((resolve, reject) => {
      const id = nextId++;
      const w = getWorker(name, url);
      pending[name].set(id, { resolve, reject });
      w.postMessage({ id, ...payload }, transfers);
    });
  }

  window.supportsImageWorker = typeof Worker !== 'undefined'
    && typeof OffscreenCanvas !== 'undefined'
    && typeof createImageBitmap !== 'undefined';

  window.supportsFflateWorker = typeof Worker !== 'undefined';

  // Encode a File/Blob to a compressed Blob off the main thread.
  // Falls through to canvas.toBlob on the main thread if OffscreenCanvas
  // isn't available.
  window.workerEncodeImage = async function workerEncodeImage(file, { format = 'jpeg', quality = 0.7, flatten = false } = {}) {
    if (window.supportsImageWorker) {
      try {
        const bitmap = await createImageBitmap(file);
        const { blob, width, height, size } = await callWorker(
          'image', 'workers/image.js',
          { op: 'encode', bitmap, format, quality, flatten },
          [bitmap],
        );
        return { blob, width, height, size };
      } catch (e) {
        // Fall through to main-thread path.
        console.warn('[workers] image encode failed, falling back:', e.message);
      }
    }
    // Main-thread fallback.
    const { img } = await window.loadImageFromFile(file);
    const c = document.createElement('canvas');
    c.width = img.width; c.height = img.height;
    const ctx = c.getContext('2d');
    if (flatten) { ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, c.width, c.height); }
    ctx.drawImage(img, 0, 0);
    const mime = format === 'png' ? 'image/png' : format === 'webp' ? 'image/webp' : 'image/jpeg';
    const blob = await new Promise((resolve) => c.toBlob(resolve, mime, quality));
    return { blob, width: img.width, height: img.height, size: blob.size };
  };

  // Gzip a File/Blob/ArrayBuffer off the main thread (fflate in a worker).
  window.workerGzip = async function workerGzip(input, { level = 6 } = {}) {
    const ab = input instanceof ArrayBuffer ? input : await input.arrayBuffer();
    if (window.supportsFflateWorker) {
      try {
        const { bytes } = await callWorker(
          'fflate', 'workers/fflate.js',
          { op: 'gzip', bytes: ab, options: { level } },
          [ab],
        );
        return new Blob([bytes], { type: 'application/gzip' });
      } catch (e) {
        console.warn('[workers] gzip worker failed, falling back:', e.message);
      }
    }
    // Main-thread fallback requires fflate UMD loaded.
    await window.loadScript('https://cdn.jsdelivr.net/npm/fflate@0.8.2/umd/index.js');
    const u8 = new Uint8Array(ab);
    const gz = window.fflate.gzipSync(u8, { level });
    return new Blob([gz], { type: 'application/gzip' });
  };

  // Zip a map of name → ArrayBuffer off the main thread.
  window.workerZip = async function workerZip(files) {
    if (window.supportsFflateWorker) {
      try {
        const transfers = Object.values(files);
        const payload = { op: 'zip', files };
        const { bytes } = await callWorker('fflate', 'workers/fflate.js', payload, transfers);
        return new Blob([bytes], { type: 'application/zip' });
      } catch (e) {
        console.warn('[workers] zip worker failed, falling back:', e.message);
      }
    }
    await window.loadScript('https://cdn.jsdelivr.net/npm/fflate@0.8.2/umd/index.js');
    const pack = {};
    for (const [n, ab] of Object.entries(files)) pack[n] = new Uint8Array(ab);
    return new Promise((resolve, reject) => {
      window.fflate.zip(pack, (err, data) => {
        if (err) return reject(err);
        resolve(new Blob([data], { type: 'application/zip' }));
      });
    });
  };

  // Unzip: returns { name: ArrayBuffer }
  window.workerUnzip = async function workerUnzip(input) {
    const ab = input instanceof ArrayBuffer ? input : await input.arrayBuffer();
    if (window.supportsFflateWorker) {
      try {
        const { files } = await callWorker('fflate', 'workers/fflate.js', { op: 'unzip', bytes: ab }, [ab]);
        return files;
      } catch (e) {
        console.warn('[workers] unzip worker failed, falling back:', e.message);
      }
    }
    await window.loadScript('https://cdn.jsdelivr.net/npm/fflate@0.8.2/umd/index.js');
    const u8 = new Uint8Array(ab);
    return new Promise((resolve, reject) => {
      window.fflate.unzip(u8, (err, data) => {
        if (err) return reject(err);
        const out = {};
        for (const [n, arr] of Object.entries(data)) out[n] = arr.buffer;
        resolve(out);
      });
    });
  };
})();
