"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 stream_base_1 = require("./stream.base.js");
const pattern_scanner_1 = require("../utils/pattern-scanner.js");
const deflate_fallback_1 = require("../compression/deflate-fallback.js");
const byte_queue_1 = require("../internal/byte-queue.js");
const compress_base_1 = require("../compression/compress.base.js");
const DEFAULT_UNZIP_STREAM_HIGH_WATER_MARK = 256 * 1024;
// =============================================================================
// 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({
            // Keep the internal buffer bounded; this stream is used in tight parse loops.
            writableHighWaterMark: 512 * 1024,
            readableHighWaterMark: 512 * 1024,
            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);
        }
    }
    _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();
                }
            });
        });
    }
    destroy(error) {
        if (!this.writeClosed) {
            this.writer.abort(error || undefined).catch(() => { });
        }
        this.reader.cancel(error || undefined).catch(() => { });
        return super.destroy(error);
    }
}
// =============================================================================
// Worker-based InflateRaw (optional)
// =============================================================================
let _inflateWorkerUrl = null;
function getInflateWorkerUrl(customUrl) {
    if (typeof customUrl === "string" && customUrl.length > 0) {
        return customUrl;
    }
    if (_inflateWorkerUrl) {
        return _inflateWorkerUrl;
    }
    // Inline worker to avoid bundler-specific worker loaders.
    // It streams deflate-raw through DecompressionStream and posts decompressed chunks back.
    const code = `
let ds;
let writer;
let reader;
let junkError = false;
let pendingWrites = 0;

function isJunkErrorMessage(msg) {
  return typeof msg === 'string' && (msg.includes('Junk') || msg.includes('junk'));
}

async function ensureStarted() {
  if (ds) return;
  ds = new DecompressionStream('deflate-raw');
  writer = ds.writable.getWriter();
  reader = ds.readable.getReader();

  (async () => {
    try {
      while (true) {
        const r = await reader.read();
        if (r.done) break;
        const chunk = r.value;
        postMessage({ t: 'data', chunk }, [chunk.buffer]);
      }
      postMessage({ t: 'end' });
    } catch (e) {
      const msg = e && e.message ? e.message : String(e);
      if (isJunkErrorMessage(msg)) {
        junkError = true;
        postMessage({ t: 'end' });
      } else {
        postMessage({ t: 'error', message: msg });
      }
    }
  })();
}

onmessage = async (ev) => {
  const msg = ev.data;
  try {
    await ensureStarted();
    if (msg.t === 'write') {
      if (junkError) {
        postMessage({ t: 'ack', id: msg.id });
        return;
      }
      pendingWrites++;
      await writer.write(msg.chunk);
      pendingWrites--;
      postMessage({ t: 'ack', id: msg.id });
      return;
    }
    if (msg.t === 'close') {
      // Wait for in-flight writes to finish (best-effort).
      while (pendingWrites > 0) {
        await new Promise(r => setTimeout(r, 0));
      }
      try { await writer.close(); } catch (_) {}
      postMessage({ t: 'closed' });
      return;
    }
    if (msg.t === 'abort') {
      try { await writer.abort(); } catch (_) {}
      postMessage({ t: 'aborted' });
      return;
    }
  } catch (e) {
    const m = e && e.message ? e.message : String(e);
    postMessage({ t: 'error', message: m, id: msg && msg.id });
  }
};
`;
    const blob = new Blob([code], { type: "text/javascript" });
    _inflateWorkerUrl = URL.createObjectURL(blob);
    return _inflateWorkerUrl;
}
class WorkerInflateRaw extends _stream_1.Duplex {
    constructor(workerUrl) {
        super({
            write: (chunk, _encoding, callback) => {
                this._doWrite(chunk, callback);
            },
            final: (callback) => {
                this._doClose(callback);
            }
        });
        this._nextId = 1;
        this._pendingAcks = new Map();
        this._closed = false;
        this._junkError = false;
        this._terminated = false;
        const url = getInflateWorkerUrl(workerUrl);
        this.worker = new Worker(url);
        this.worker.onmessage = (ev) => {
            const msg = ev.data;
            if (!msg || typeof msg.t !== "string") {
                return;
            }
            if (msg.t === "data") {
                const chunk = msg.chunk;
                this.push(chunk);
                return;
            }
            if (msg.t === "end") {
                this.push(null);
                this._terminateWorker();
                return;
            }
            if (msg.t === "aborted") {
                this._terminateWorker();
                return;
            }
            if (msg.t === "ack") {
                const id = msg.id;
                const cb = this._pendingAcks.get(id);
                if (cb) {
                    this._pendingAcks.delete(id);
                    cb();
                }
                return;
            }
            if (msg.t === "error") {
                const message = typeof msg.message === "string" ? msg.message : "Worker inflate error";
                if (message.includes("Junk") || message.includes("junk")) {
                    this._junkError = true;
                    // Treat as end-of-stream.
                    this.push(null);
                    this._terminateWorker();
                    // Resolve any pending writes.
                    for (const cb of this._pendingAcks.values()) {
                        cb();
                    }
                    this._pendingAcks.clear();
                    return;
                }
                const err = new Error(message);
                // Fail any pending writes.
                for (const cb of this._pendingAcks.values()) {
                    cb(err);
                }
                this._pendingAcks.clear();
                this.emit("error", err);
                this._terminateWorker();
                return;
            }
        };
        this.worker.onerror = (e) => {
            const err = new Error(e.message || "Worker error");
            for (const cb of this._pendingAcks.values()) {
                cb(err);
            }
            this._pendingAcks.clear();
            this.emit("error", err);
            this._terminateWorker();
        };
    }
    _terminateWorker() {
        if (this._terminated) {
            return;
        }
        this._terminated = true;
        try {
            this.worker.terminate();
        }
        catch {
            // ignore
        }
    }
    _doWrite(chunk, callback) {
        if (this._closed || this._junkError) {
            callback();
            return;
        }
        const id = this._nextId++;
        this._pendingAcks.set(id, callback);
        // Transfer the underlying ArrayBuffer to reduce copies.
        // If chunk is a view into a larger buffer, slice to avoid transferring unrelated bytes.
        const transferable = chunk.byteOffset === 0 && chunk.byteLength === chunk.buffer.byteLength
            ? chunk
            : chunk.slice();
        this.worker.postMessage({ t: "write", id, chunk: transferable }, [transferable.buffer]);
    }
    _doClose(callback) {
        if (this._closed) {
            callback();
            return;
        }
        this._closed = true;
        this.worker.postMessage({ t: "close" });
        callback();
    }
    destroy(error) {
        if (!this._closed) {
            this._closed = true;
            try {
                this.worker.postMessage({ t: "abort" });
            }
            catch {
                // ignore
            }
        }
        this._terminateWorker();
        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 = stream_base_1.DATA_DESCRIPTOR_SIGNATURE_BYTES;
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._maybeReleaseWriteCallback();
                    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;
            // Default values are intentionally conservative to avoid memory spikes
            // when parsing large archives under slow consumers.
            const hi = Math.max(64 * 1024, opts.inputHighWaterMarkBytes ?? 2 * 1024 * 1024);
            const lo = Math.max(32 * 1024, opts.inputLowWaterMarkBytes ?? Math.floor(hi / 4));
            this._inputHighWaterMarkBytes = hi;
            this._inputLowWaterMarkBytes = Math.min(lo, hi);
            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;
                    // Ensure upstream writers don't hang waiting for a deferred write callback.
                    if (this._writeCb) {
                        const cb = this._writeCb;
                        this._writeCb = undefined;
                        cb(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.
                const inflateFactory = () => {
                    if (this._opts.useWorkerInflate && typeof Worker !== "undefined") {
                        // Worker path requires DecompressionStream support.
                        if ((0, compress_base_1.hasDeflateRawDecompressionStream)()) {
                            try {
                                return new WorkerInflateRaw(this._opts.workerInflateUrl);
                            }
                            catch {
                                // If Worker construction fails (e.g. CSP/CORS), fall back.
                                return createInflateRawFn();
                            }
                        }
                    }
                    return createInflateRawFn();
                };
                this._parsingDone = (0, stream_base_1.runParseLoop)(this._opts, io, emitter, inflateFactory, 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, callback) {
            this._buffer.append(chunk);
            // Apply writable backpressure by deferring the callback when the input buffer is large.
            // The callback will be released once the parser drains the buffer.
            if (this._buffer.length >= this._inputHighWaterMarkBytes) {
                this._writeCb = callback;
            }
            else {
                callback();
            }
            this._wakeUp();
            this.emit("data-available");
            this.emit("chunk");
        }
        get buffer() {
            return this._buffer.view();
        }
        set buffer(value) {
            this._buffer.reset(value);
        }
        _wakeUp() {
            if (this._pendingResolve) {
                const resolve = this._pendingResolve;
                this._pendingResolve = undefined;
                resolve();
            }
        }
        _maybeReleaseWriteCallback() {
            if (!this._writeCb) {
                return;
            }
            if (this._buffer.length > this._inputLowWaterMarkBytes) {
                return;
            }
            const cb = this._writeCb;
            this._writeCb = undefined;
            cb();
        }
        _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);
                        this._maybeReleaseWriteCallback();
                        return data;
                    }
                    throw new Error("FILE_ENDED");
                }
                await this._waitForData();
            }
            const out = this._buffer.read(length);
            this._maybeReleaseWriteCallback();
            return out;
        }
        async _pullUntilInternal(pattern, includeEof = false) {
            const chunks = [];
            const scanner = new pattern_scanner_1.PatternScanner(pattern);
            while (true) {
                const bufLen = this._buffer.length;
                const match = scanner.find(this._buffer);
                if (match !== -1) {
                    this.match = match;
                    const toRead = match + (includeEof ? pattern.length : 0);
                    if (toRead > 0) {
                        chunks.push(this._buffer.read(toRead));
                        this._maybeReleaseWriteCallback();
                    }
                    return chunks.length === 1 ? chunks[0] : (0, _stream_1.concatUint8Arrays)(chunks);
                }
                // No match yet. Avoid rescanning bytes that can't start a match.
                scanner.onNoMatch(bufLen);
                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));
                    scanner.onConsume(safeLen);
                    this._maybeReleaseWriteCallback();
                }
                await this._waitForData();
            }
        }
        _streamFixedLength(length) {
            const output = new _stream_1.PassThrough({ highWaterMark: DEFAULT_UNZIP_STREAM_HIGH_WATER_MARK });
            let remaining = length;
            let done = false;
            let waitingDrain = false;
            const pull = () => {
                if (done) {
                    return;
                }
                if (waitingDrain) {
                    return;
                }
                while (remaining > 0 && this._buffer.length > 0) {
                    const toRead = Math.min(remaining, this._buffer.length);
                    const chunk = this._buffer.read(toRead);
                    remaining -= toRead;
                    const ok = output.write(chunk);
                    this._maybeReleaseWriteCallback();
                    if (!ok) {
                        waitingDrain = true;
                        output.once("drain", () => {
                            waitingDrain = false;
                            pull();
                        });
                        return;
                    }
                }
                if (remaining === 0) {
                    done = true;
                    this.removeListener("data-available", pull);
                    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({ highWaterMark: DEFAULT_UNZIP_STREAM_HIGH_WATER_MARK });
            let done = false;
            const patternLen = pattern.length;
            const scanner = new pattern_scanner_1.PatternScanner(pattern);
            let waitingDrain = false;
            const pull = () => {
                if (done || waitingDrain) {
                    return;
                }
                while (true) {
                    if (this._buffer.length <= 0) {
                        break;
                    }
                    const bufLen = this._buffer.length;
                    const match = scanner.find(this._buffer);
                    if (match !== -1) {
                        this.match = match;
                        const endIndex = includeEof ? match + patternLen : match;
                        if (endIndex > 0) {
                            const ok = output.write(this._buffer.read(endIndex));
                            scanner.onConsume(endIndex);
                            this._maybeReleaseWriteCallback();
                            if (!ok) {
                                waitingDrain = true;
                                output.once("drain", () => {
                                    waitingDrain = false;
                                    pull();
                                });
                                return;
                            }
                        }
                        done = true;
                        this.removeListener("data-available", pull);
                        output.end();
                        return;
                    }
                    // No match yet. Avoid rescanning bytes that can't start a match.
                    scanner.onNoMatch(bufLen);
                    if (this.finished) {
                        done = true;
                        this.removeListener("data-available", pull);
                        output.destroy(new Error("FILE_ENDED"));
                        return;
                    }
                    const safeLen = bufLen - patternLen;
                    if (safeLen <= 0) {
                        // Keep enough bytes to detect a split signature.
                        if (this._buffer.length <= patternLen) {
                            this._maybeReleaseWriteCallback();
                        }
                        break;
                    }
                    const ok = output.write(this._buffer.read(safeLen));
                    scanner.onConsume(safeLen);
                    this._maybeReleaseWriteCallback();
                    if (!ok) {
                        waitingDrain = true;
                        output.once("drain", () => {
                            waitingDrain = false;
                            pull();
                        });
                        return;
                    }
                }
            };
            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) {
                    return Promise.resolve(this._buffer.read(eof));
                }
                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, stream_base_1.streamUntilValidatedDataDescriptor)({
                source: {
                    getLength: () => this._buffer.length,
                    read: (length) => this._buffer.read(length),
                    peekChunks: (length) => this._buffer.peekChunks(length),
                    discard: (length) => this._buffer.discard(length),
                    indexOfPattern: (pattern, startIndex) => this._buffer.indexOfPattern(pattern, startIndex),
                    peekUint32LE: (offset) => this._buffer.peekUint32LE(offset),
                    isFinished: () => this.finished,
                    onDataAvailable: (cb) => {
                        this.on("data-available", cb);
                        return () => this.removeListener("data-available", cb);
                    },
                    maybeReleaseWriteCallback: () => this._maybeReleaseWriteCallback()
                },
                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);
}
