import { coercionInput } from '../utils.js';
import { createModule } from '../common.js';
import * as common from './common.js';
import { Worker as WorkerPolyfill, MAX_WORKERS, TransformStream } from '#worker-polyfill';
await common.ModuleReady;
const IDLE_WORKERS = [];
const BUSY_WORKERS = new Set();
const PENDING_BORROW = [];
let SEQ = 0;
/** create and initialize worker */
async function initWorker() {
    return new Promise((resolve, reject) => {
        const worker = typeof Worker == 'function'
            ? new Worker(new URL('./worker.js', import.meta.url), {
                type: 'module',
                name: `@cloudpss/zstd/worker`,
            })
            : new WorkerPolyfill(new URL('./worker.js', import.meta.url), {
                type: 'module',
                name: `@cloudpss/zstd/worker`,
            });
        BUSY_WORKERS.add(worker);
        const onMessage = (ev) => {
            if (ev.data !== 'ready')
                return;
            cleanup();
            worker.addEventListener('error', (ev) => {
                // eslint-disable-next-line no-console
                console.error('@cloudpss/zstd worker error', ev);
                worker.terminate();
                BUSY_WORKERS.delete(worker);
                IDLE_WORKERS.splice(IDLE_WORKERS.indexOf(worker), 1);
                handlePendingBorrow();
            });
            resolve(worker);
        };
        const onError = (ev) => {
            cleanup();
            reject(new Error(ev.message, { cause: ev.error }));
            BUSY_WORKERS.delete(worker);
        };
        const cleanup = () => {
            worker.removeEventListener('message', onMessage);
            worker.removeEventListener('error', onError);
        };
        worker.addEventListener('message', onMessage);
        worker.addEventListener('error', onError);
    });
}
/** handle pending borrow */
function handlePendingBorrow() {
    void Promise.resolve().then(async () => {
        while (PENDING_BORROW.length > 0 && IDLE_WORKERS.length > 0) {
            const worker = IDLE_WORKERS.pop();
            BUSY_WORKERS.add(worker);
            PENDING_BORROW.shift()(worker);
        }
        while (PENDING_BORROW.length > 0 && BUSY_WORKERS.size < MAX_WORKERS) {
            const worker = await initWorker();
            PENDING_BORROW.shift()(worker);
        }
    });
}
/** return worker to pool */
function returnWorker(worker) {
    if (!BUSY_WORKERS.delete(worker))
        return;
    IDLE_WORKERS.push(worker);
    handlePendingBorrow();
}
/** destroy worker */
function destroyWorker(worker) {
    worker.terminate();
    BUSY_WORKERS.delete(worker);
    IDLE_WORKERS.splice(IDLE_WORKERS.indexOf(worker), 1);
}
/** get or wait for an idle worker */
async function borrowWorker() {
    if (IDLE_WORKERS.length > 0) {
        const worker = IDLE_WORKERS.pop();
        BUSY_WORKERS.add(worker);
        return worker;
    }
    if (BUSY_WORKERS.size < MAX_WORKERS) {
        const worker = await initWorker();
        return worker;
    }
    return await new Promise((resolve) => {
        PENDING_BORROW.push(resolve);
    });
}
const MAX_COPY_OVERHEAD = 1024 * 16; // 16KB
/** Call to a specific worker */
async function callWorker(worker, method, args, transferable) {
    const seq = SEQ++;
    const request = [seq, method, args];
    if (transferable == null &&
        ArrayBuffer.isView(args[0]) &&
        args[0].byteLength + MAX_COPY_OVERHEAD < args[0].buffer.byteLength) {
        // Avoid OOM of chrome when copying large buffers
        const buffer = args[0].buffer.slice(args[0].byteOffset, args[0].byteOffset + args[0].byteLength);
        args[0] = new Uint8Array(buffer);
        transferable = [buffer];
    }
    return new Promise((resolve, reject) => {
        if (transferable?.length) {
            worker.postMessage(request, { transfer: transferable });
        }
        else {
            worker.postMessage(request);
        }
        const onMessage = (ev) => {
            const [resSeq, data, error] = ev.data;
            if (resSeq !== seq)
                return;
            cleanup();
            if (error) {
                reject(error);
            }
            else {
                resolve(data);
            }
        };
        const onError = (ev) => {
            cleanup();
            reject(new Error(ev.message, { cause: ev.error }));
        };
        const cleanup = () => {
            worker.removeEventListener('message', onMessage);
            worker.removeEventListener('error', onError);
        };
        worker.addEventListener('message', onMessage);
        worker.addEventListener('error', onError);
    });
}
/** Call to worker pool */
async function call(method, args) {
    const worker = await borrowWorker();
    try {
        return await callWorker(worker, method, args);
    }
    finally {
        returnWorker(worker);
    }
}
/** cleanup all workers */
export function terminate() {
    for (const worker of IDLE_WORKERS) {
        worker.terminate();
    }
    for (const worker of BUSY_WORKERS) {
        worker.terminate();
    }
    IDLE_WORKERS.length = 0;
    BUSY_WORKERS.clear();
}
/** get current worker status */
export function workers() {
    return { idle: IDLE_WORKERS.length, busy: BUSY_WORKERS.size };
}
/** Proxy to worker */
class TransformProxy {
    constructor(method, args) {
        this.method = method;
        this.args = args;
        this.ctx = null;
        /** receive from worker */
        this.onMessage = (ev) => {
            const [seq, data, error] = ev.data;
            if (seq != null)
                return;
            if (error) {
                this.controller.error(error);
                this.end(error);
            }
            else {
                this.controller.enqueue(data);
            }
        };
    }
    /** end transform */
    end(error) {
        const { ctx } = this;
        if (ctx) {
            ctx.removeEventListener('message', this.onMessage);
        }
        if (error != null) {
            this.controller.error(error);
            if (ctx)
                destroyWorker(ctx);
        }
        else {
            if (ctx)
                returnWorker(ctx);
        }
        this.ctx = null;
    }
    /** @inheritdoc */
    async start(controller) {
        this.controller = controller;
        try {
            this.ctx = await borrowWorker();
            this.ctx.addEventListener('message', this.onMessage);
            await callWorker(this.ctx, this.method, this.args);
        }
        catch (ex) {
            this.end(ex);
        }
    }
    /** @inheritdoc */
    transform(chunk) {
        try {
            const src = coercionInput(chunk, false);
            const transferable = src.byteLength === src.buffer.byteLength ? [src.buffer] : [];
            this.ctx.postMessage([null, 'push', [src]], transferable);
        }
        catch (ex) {
            this.end(ex);
        }
    }
    /** @inheritdoc */
    async flush() {
        try {
            await callWorker(this.ctx, 'end', []);
            this.end(null);
        }
        catch (ex) {
            this.end(ex);
        }
    }
}
/** Stream compressor */
export class WebCompressor extends TransformProxy {
    constructor(level) {
        super('Compressor', [level]);
        this.level = level;
    }
}
/** Stream compressor */
export class WebDecompressor extends TransformProxy {
    constructor() {
        super('Decompressor', []);
    }
}
export const { compressSync, compress, decompressSync, decompress, compressor, decompressor } = createModule({
    compressSync: common.compress,
    decompressSync: common.decompress,
    compress: async (data, level) => await call('compress', [data, level]),
    decompress: async (data) => await call('decompress', [data]),
    Compressor: WebCompressor,
    Decompressor: WebDecompressor,
    TransformStream,
});
export const { ZSTD_VERSION } = common;
export const TYPE = 'wasm';
/** For debug usage */
export const _WasmModule = common.Module;
export default null;
//# sourceMappingURL=index.js.map