"use strict";
/**
 * ZIP Stream Parser - Browser Version
 *
 * A streaming ZIP parser for browsers using native DecompressionStream.
 * Falls back to pure JavaScript implementation for older browsers.
 * Uses the browser Duplex stream implementation for compatibility.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parse = void 0;
exports.createParseClass = createParseClass;
exports.createParse = createParse;
const _stream_1 = require("../stream/index.js");
const binary_1 = require("./utils/binary.js");
const bytes_1 = require("./utils/bytes.js");
const parse_base_1 = require("./parse.base.js");
const deflate_fallback_1 = require("./deflate-fallback.js");
const byte_queue_1 = require("./byte-queue.js");
const zip_constants_1 = require("./zip-constants.js");
const compress_base_1 = require("./compress.base.js");
// =============================================================================
// Browser InflateRaw using DecompressionStream
// =============================================================================
/**
 * Duplex stream that wraps browser's native DecompressionStream.
 * Handles the "Junk found after end of compressed data" error gracefully
 * by treating it as end of stream when using data descriptors.
 *
 * Uses Duplex instead of Transform because DecompressionStream's output
 * is inherently async and doesn't fit the Transform's sync callback model.
 */
class BrowserInflateRaw extends _stream_1.Duplex {
    constructor() {
        // Pass write handler to Duplex so pipe() calls our write method
        // Also pass final handler to close the DecompressionStream when _writable ends
        super({
            write: (chunk, _encoding, callback) => {
                this._doWrite(chunk, callback);
            },
            final: (callback) => {
                this._closeWriter(() => {
                    callback();
                });
            }
        });
        this.reading = false;
        this.writeClosed = false;
        this._junkError = false;
        this._bytesIn = 0;
        this._bytesOut = 0;
        this._readingDone = false;
        // Track pending write count for proper ordering
        this._pendingWrites = 0;
        this._writeFinishedPromise = null;
        this._resolveWriteFinished = null;
        this.decompressionStream = new DecompressionStream("deflate-raw");
        this.writer =
            this.decompressionStream.writable.getWriter();
        this.reader = this.decompressionStream.readable.getReader();
        this._readingDonePromise = new Promise(resolve => {
            this._resolveReadingDone = resolve;
        });
        this._startReading();
    }
    // Internal write implementation
    _doWrite(chunk, callback) {
        if (this._junkError) {
            // Already got junk error, don't write more
            if (callback) {
                callback();
            }
            return;
        }
        this._bytesIn += chunk.length;
        this._pendingWrites++;
        this.writer
            .write(chunk)
            .then(() => {
            this._pendingWrites--;
            if (this._pendingWrites === 0 && this._resolveWriteFinished) {
                this._resolveWriteFinished();
            }
            if (callback) {
                callback();
            }
        })
            .catch(e => {
            this._pendingWrites--;
            if (this._pendingWrites === 0 && this._resolveWriteFinished) {
                this._resolveWriteFinished();
            }
            const msg = e instanceof Error ? e.message : String(e);
            if (msg.includes("Junk") || msg.includes("junk")) {
                this._junkError = true;
                if (callback) {
                    callback();
                }
            }
            else {
                if (callback) {
                    callback(e);
                }
                else {
                    this.emit("error", e);
                }
            }
        });
    }
    async _startReading() {
        if (this.reading) {
            return;
        }
        this.reading = true;
        try {
            while (true) {
                const { done, value } = await this.reader.read();
                if (done) {
                    break;
                }
                this._bytesOut += value.length;
                // Directly push to the readable side of Duplex
                this.push(value);
            }
        }
        catch (e) {
            // "Junk found after end of compressed data" is expected when using data descriptors
            // because we can't know the exact compressed size upfront
            const msg = e instanceof Error ? e.message : String(e);
            if (msg.includes("Junk") || msg.includes("junk")) {
                this._junkError = true;
                // This is OK - we've read all decompressed data
            }
            else {
                // Re-throw other errors
                this.emit("error", e);
            }
        }
        finally {
            this._readingDone = true;
            this._resolveReadingDone();
            // Signal end of readable side
            this.push(null);
        }
    }
    // Override write to feed data into DecompressionStream
    write(chunk, encodingOrCallback, callback) {
        // Handle overload
        let cb;
        if (typeof encodingOrCallback === "function") {
            cb = encodingOrCallback;
        }
        else {
            cb = callback;
        }
        this._doWrite(chunk, cb);
        return true;
    }
    // Override end to close the DecompressionStream writer
    end(chunkOrCallback, encodingOrCallback, callback) {
        // Handle overloads
        let chunk;
        let cb;
        if (typeof chunkOrCallback === "function") {
            cb = chunkOrCallback;
        }
        else if (chunkOrCallback !== undefined) {
            chunk = chunkOrCallback;
            if (typeof encodingOrCallback === "function") {
                cb = encodingOrCallback;
            }
            else {
                cb = callback;
            }
        }
        // Write final chunk if provided
        if (chunk) {
            this.write(chunk, () => {
                this._closeWriter(cb);
            });
        }
        else {
            this._closeWriter(cb);
        }
        return this;
    }
    _closeWriter(callback) {
        if (this.writeClosed) {
            this._readingDonePromise.then(() => {
                if (callback) {
                    callback();
                }
            });
            return;
        }
        this.writeClosed = true;
        // Wait for pending writes to complete before closing
        const waitForWrites = this._pendingWrites > 0
            ? new Promise(resolve => {
                this._writeFinishedPromise = new Promise(r => {
                    this._resolveWriteFinished = r;
                });
                this._writeFinishedPromise.then(resolve);
            })
            : Promise.resolve();
        waitForWrites
            .then(() => this.writer.close())
            .catch(() => { })
            .finally(() => {
            this._readingDonePromise.then(() => {
                if (callback) {
                    callback();
                }
                this.emit("finish");
            });
        });
    }
    destroy(error) {
        if (!this.writeClosed) {
            this.writer.abort(error || undefined).catch(() => { });
        }
        this.reader.cancel(error || undefined).catch(() => { });
        return super.destroy(error);
    }
}
// =============================================================================
// Fallback InflateRaw for browsers without DecompressionStream
// =============================================================================
/**
 * Fallback Inflate that buffers all data, then decompresses at end.
 * Used for older browsers without native DecompressionStream support.
 */
class FallbackInflateRaw extends _stream_1.Duplex {
    constructor() {
        super({
            write: (chunk, _encoding, callback) => {
                if (this._finished) {
                    callback(new Error("write after end"));
                    return;
                }
                this.chunks.push(chunk);
                callback();
            },
            final: (callback) => {
                this._decompress(callback);
            }
        });
        this.chunks = [];
        this._finished = false;
    }
    _decompress(callback) {
        try {
            // Combine all chunks
            const data = (0, _stream_1.concatUint8Arrays)(this.chunks);
            // Decompress using fallback
            const decompressed = (0, deflate_fallback_1.inflateRaw)(data);
            this.push(decompressed);
            this.push(null);
            this._finished = true;
            callback();
        }
        catch (err) {
            callback(err instanceof Error ? err : new Error(String(err)));
        }
    }
    destroy(error) {
        this._finished = true;
        this.chunks = [];
        return super.destroy(error);
    }
}
// =============================================================================
// Factory function with fallback
// =============================================================================
function createInflateRaw() {
    if ((0, compress_base_1.hasDeflateRawDecompressionStream)()) {
        return new BrowserInflateRaw();
    }
    else {
        return new FallbackInflateRaw();
    }
}
// =============================================================================
// Utilities
// =============================================================================
const dataDescriptorSignature = (0, binary_1.writeUint32LE)(zip_constants_1.DATA_DESCRIPTOR_SIG);
function createParseClass(createInflateRawFn) {
    /**
     * ZIP Stream Parser for browsers.
     *
     * Extends Duplex to be compatible with stream.pipe(zip) pattern.
     * - Writable side: accepts ZIP data
     * - Readable side: emits ZipEntry objects
     */
    return class Parse extends _stream_1.Duplex {
        constructor(opts = {}) {
            super({
                objectMode: true,
                write: (chunk, _encoding, callback) => {
                    this._handleWrite(chunk);
                    callback();
                },
                final: (callback) => {
                    this.finished = true;
                    this._wakeUp();
                    this.emit("data-available");
                    this.emit("chunk", false);
                    this._parsingDone.then(() => callback()).catch(callback);
                }
            });
            this._buffer = new byte_queue_1.ByteQueue();
            this.finished = false;
            this._driverState = {};
            this._parsingDone = Promise.resolve();
            this._opts = opts;
            const io = {
                pull: (length) => this.pull(length),
                pullUntil: (pattern, includeEof) => this.pullUntil(pattern, includeEof),
                stream: (length) => this.stream(length),
                streamUntilDataDescriptor: () => this._streamUntilDataDescriptor(),
                setDone: () => {
                    this.push(null);
                }
            };
            const emitter = {
                emitEntry: (entry) => {
                    this.emit("entry", entry);
                },
                pushEntry: (entry) => {
                    this.push(entry);
                },
                // Browser version historically only pushed entries when forceStream=true.
                // Keep this behavior to avoid changing stream piping semantics.
                pushEntryIfPiped: (_entry) => {
                    return;
                },
                emitCrxHeader: (header) => {
                    this.crxHeader = header;
                    this.emit("crx-header", header);
                },
                emitError: (err) => {
                    this.__emittedError = err;
                    this.emit("error", err);
                },
                emitClose: () => {
                    this.emit("close");
                }
            };
            queueMicrotask(() => {
                // NOTE: We intentionally do NOT pass inflateRawSync to runParseLoop in browser.
                // Browser's native DecompressionStream is faster than our pure-JS fallback,
                // so we always use the streaming path for decompression in browsers.
                this._parsingDone = (0, parse_base_1.runParseLoop)(this._opts, io, emitter, () => createInflateRawFn(), this._driverState
                // No inflateRawSync - always use streaming DecompressionStream in browser
                );
                this._parsingDone.catch((e) => {
                    if (!this.__emittedError || this.__emittedError !== e) {
                        this.__emittedError = e;
                        this.emit("error", e);
                    }
                    this.emit("close");
                });
            });
        }
        _handleWrite(chunk) {
            this._buffer.append(chunk);
            this._wakeUp();
            this.emit("data-available");
            this.emit("chunk");
        }
        get buffer() {
            return this._buffer.view();
        }
        set buffer(value) {
            this._buffer.reset(value);
        }
        _maybeReleaseWriteCallback() {
            if (typeof this.cb === "function") {
                const callback = this.cb;
                this.cb = undefined;
                callback();
            }
        }
        _wakeUp() {
            if (this._pendingResolve) {
                const resolve = this._pendingResolve;
                this._pendingResolve = undefined;
                resolve();
            }
        }
        _waitForData() {
            return new Promise(resolve => {
                this._pendingResolve = resolve;
            });
        }
        async _pullInternal(length) {
            if (length === 0) {
                return new Uint8Array(0);
            }
            while (this._buffer.length < length) {
                if (this.finished) {
                    if (this._buffer.length > 0) {
                        const data = this._buffer.read(this._buffer.length);
                        return data;
                    }
                    throw new Error("FILE_ENDED");
                }
                await this._waitForData();
            }
            return this._buffer.read(length);
        }
        async _pullUntilInternal(pattern, includeEof = false) {
            const chunks = [];
            let searchFrom = 0;
            const overlap = Math.max(0, pattern.length - 1);
            while (true) {
                const view = this._buffer.view();
                const match = (0, bytes_1.indexOfUint8ArrayPattern)(view, pattern, searchFrom);
                if (match !== -1) {
                    this.match = match;
                    const toRead = match + (includeEof ? pattern.length : 0);
                    if (toRead > 0) {
                        chunks.push(this._buffer.read(toRead));
                    }
                    return (0, _stream_1.concatUint8Arrays)(chunks);
                }
                // No match yet. Avoid rescanning bytes that can't start a match.
                searchFrom = Math.max(searchFrom, Math.max(0, view.length - overlap));
                if (this.finished) {
                    throw new Error("FILE_ENDED");
                }
                const safeLen = Math.max(0, this._buffer.length - pattern.length);
                if (safeLen > 0) {
                    chunks.push(this._buffer.read(safeLen));
                    searchFrom = Math.max(0, searchFrom - safeLen);
                }
                await this._waitForData();
            }
        }
        _streamFixedLength(length) {
            const output = new _stream_1.PassThrough();
            let remaining = length;
            let done = false;
            const pull = () => {
                if (done) {
                    return;
                }
                while (remaining > 0 && this._buffer.length > 0) {
                    const toRead = Math.min(remaining, this._buffer.length);
                    const chunk = this._buffer.read(toRead);
                    remaining -= toRead;
                    output.write(chunk);
                }
                if (remaining === 0) {
                    done = true;
                    this.removeListener("data-available", pull);
                    this._maybeReleaseWriteCallback();
                    output.end();
                }
                else if (this.finished) {
                    done = true;
                    this.removeListener("data-available", pull);
                    output.destroy(new Error("FILE_ENDED"));
                }
            };
            this.on("data-available", pull);
            queueMicrotask(() => pull());
            return output;
        }
        _streamUntilPattern(pattern, includeEof = false) {
            const output = new _stream_1.PassThrough();
            let done = false;
            let searchFrom = 0;
            const overlap = Math.max(0, pattern.length - 1);
            const pull = () => {
                if (done) {
                    return;
                }
                const view = this._buffer.view();
                const match = (0, bytes_1.indexOfUint8ArrayPattern)(view, pattern, searchFrom);
                if (match !== -1) {
                    this.match = match;
                    const endIndex = includeEof ? match + pattern.length : match;
                    if (endIndex > 0) {
                        output.write(this._buffer.read(endIndex));
                    }
                    done = true;
                    this.removeListener("data-available", pull);
                    this._maybeReleaseWriteCallback();
                    output.end();
                    return;
                }
                // No match yet. Avoid rescanning bytes that can't start a match.
                searchFrom = Math.max(searchFrom, Math.max(0, view.length - overlap));
                if (this.finished) {
                    done = true;
                    this.removeListener("data-available", pull);
                    this._maybeReleaseWriteCallback();
                    output.destroy(new Error("FILE_ENDED"));
                    return;
                }
                const safeLen = Math.max(0, this._buffer.length - pattern.length);
                if (safeLen > 0) {
                    output.write(this._buffer.read(safeLen));
                    searchFrom = Math.max(0, searchFrom - safeLen);
                    this._maybeReleaseWriteCallback();
                }
            };
            this.on("data-available", pull);
            queueMicrotask(() => pull());
            return output;
        }
        stream(eof, includeEof) {
            if (typeof eof === "number") {
                return this._streamFixedLength(eof);
            }
            return this._streamUntilPattern(eof, includeEof ?? false);
        }
        pull(eof, includeEof) {
            if (eof === 0) {
                return Promise.resolve(new Uint8Array(0));
            }
            if (typeof eof === "number") {
                // Node-compatible behavior: if finished and not enough bytes, reject.
                if (this.finished && this._buffer.length < eof) {
                    return Promise.reject(new Error("FILE_ENDED"));
                }
                if (this._buffer.length >= eof) {
                    const data = this._buffer.read(eof);
                    if (this._buffer.length === 0) {
                        this._maybeReleaseWriteCallback();
                    }
                    return Promise.resolve(data);
                }
                return this._pullInternal(eof);
            }
            // Pattern mode
            if (this.finished) {
                return Promise.reject(new Error("FILE_ENDED"));
            }
            return this._pullUntilInternal(eof, includeEof ?? false);
        }
        pullUntil(pattern, includeEof) {
            return this.pull(pattern, includeEof);
        }
        _streamUntilDataDescriptor() {
            return (0, parse_base_1.streamUntilValidatedDataDescriptor)({
                source: {
                    getView: () => this._buffer.view(),
                    getLength: () => this._buffer.length,
                    read: (length) => this._buffer.read(length),
                    isFinished: () => this.finished,
                    onDataAvailable: (cb) => {
                        this.on("data-available", cb);
                        return () => this.removeListener("data-available", cb);
                    }
                },
                dataDescriptorSignature
            });
        }
        promise() {
            return new Promise((resolve, reject) => {
                this.on("finish", resolve);
                this.on("end", resolve);
                this.on("error", reject);
            });
        }
    };
}
const BaseParse = createParseClass(createInflateRaw);
class Parse extends BaseParse {
}
exports.Parse = Parse;
function createParse(opts) {
    return new Parse(opts);
}
