import { ZipParser } from "./zip-parser.js";
import { createParse } from "./stream.js";
import { pipeIterableToSink } from "../io/archive-sink.js";
import { toAsyncIterable, toUint8Array } from "../io/archive-source.js";
const textDecoderCache = new Map();
function getTextDecoder(encoding) {
    const key = encoding ?? "utf-8";
    const cached = textDecoderCache.get(key);
    if (cached) {
        return cached;
    }
    const decoder = new TextDecoder(key);
    textDecoderCache.set(key, decoder);
    return decoder;
}
export class UnzipEntry {
    constructor(args) {
        if (args.kind === "buffer") {
            this._parser = args.parser;
            this._info = args.info;
            this.path = args.info.path;
            this.isDirectory = args.info.isDirectory;
        }
        else {
            this._parseEntry = args.entry;
            this.path = args.entry.path;
            this.isDirectory = args.entry.type === "Directory";
        }
    }
    async bytes() {
        if (this._parser && this._info) {
            const out = await this._parser.extract(this._info.path);
            return out ?? new Uint8Array(0);
        }
        if (this._parseEntry) {
            const data = await this._parseEntry.buffer();
            // In Node.js, `entry.buffer()` may return a Buffer, which causes
            // deep-equality mismatches against Uint8Array in tests.
            if (typeof Buffer !== "undefined" && data instanceof Buffer) {
                return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
            }
            return data;
        }
        return new Uint8Array(0);
    }
    async *stream() {
        if (this._parser && this._info) {
            const data = await this.bytes();
            if (data.length) {
                yield data;
            }
            return;
        }
        if (this._parseEntry) {
            for await (const chunk of this._parseEntry) {
                yield chunk;
            }
        }
    }
    async pipeTo(sink) {
        await pipeIterableToSink(this.stream(), sink);
    }
    async text(encoding) {
        const bytes = await this.bytes();
        return getTextDecoder(encoding).decode(bytes);
    }
    discard() {
        if (this._parseEntry) {
            this._parseEntry.autodrain();
        }
    }
}
export class ZipReader {
    constructor(source, options = {}) {
        this._bufferParser = null;
        this._source = source;
        this._options = options;
    }
    async _ensureBufferParser() {
        if (this._bufferParser) {
            return this._bufferParser;
        }
        if (this._source instanceof Uint8Array ||
            this._source instanceof ArrayBuffer ||
            typeof this._source === "string" ||
            (typeof Blob !== "undefined" && this._source instanceof Blob)) {
            const bytes = await toUint8Array(this._source);
            this._bufferParser = new ZipParser(bytes, {
                decodeStrings: this._options.decodeStrings
            });
            return this._bufferParser;
        }
        throw new Error("This ZIP source is streaming; random access is not available");
    }
    async get(path) {
        const parser = await this._ensureBufferParser();
        const info = parser.getEntry(path);
        if (!info) {
            return null;
        }
        return new UnzipEntry({ kind: "buffer", parser, info });
    }
    async *entries() {
        // Buffer mode
        if (this._source instanceof Uint8Array ||
            this._source instanceof ArrayBuffer ||
            typeof this._source === "string" ||
            (typeof Blob !== "undefined" && this._source instanceof Blob)) {
            const parser = await this._ensureBufferParser();
            for (const info of parser.getEntries()) {
                yield new UnzipEntry({ kind: "buffer", parser, info });
            }
            return;
        }
        // Streaming mode
        // Always prefer forceStream mode for backpressure and bounded buffering.
        const parse = createParse({ ...(this._options.parse ?? {}), forceStream: true });
        const feedPromise = (async () => {
            try {
                for await (const chunk of toAsyncIterable(this._source)) {
                    await new Promise((resolve, reject) => {
                        parse.write(chunk, (err) => {
                            if (err) {
                                reject(err);
                            }
                            else {
                                resolve();
                            }
                        });
                    });
                }
                parse.end();
                await parse.promise();
            }
            catch (e) {
                parse.destroy(e instanceof Error ? e : new Error(String(e)));
                throw e;
            }
        })();
        try {
            for await (const entry of parse) {
                yield new UnzipEntry({ kind: "stream", entry });
            }
            await feedPromise;
        }
        finally {
            // Ensure the feed task does not get stranded.
            await feedPromise.catch(() => { });
        }
    }
    async bytes(path) {
        const entry = await this.get(path);
        if (!entry) {
            return null;
        }
        return entry.bytes();
    }
    async close() {
        // No persistent resources in buffer mode.
    }
}
export function unzip(source, options) {
    return new ZipReader(source, options);
}
