import { DEFAULT_ZIP_LEVEL, DEFAULT_ZIP_TIMESTAMPS } from "../defaults.js";
import { ZipDeflateFile, StreamingZip } from "./stream.js";
import { createZip, createZipSync } from "./zip-bytes.js";
import { collect, pipeIterableToSink } from "../io/archive-sink.js";
import { toAsyncIterable, toUint8Array, toUint8ArraySync } from "../io/archive-source.js";
import { createAsyncQueue } from "../utils/async-queue.js";
const REPRODUCIBLE_ZIP_MOD_TIME = new Date(1980, 0, 1, 0, 0, 0);
export class ZipArchive {
    constructor(options = {}) {
        this._entries = [];
        this._sealed = false;
        const reproducible = options.reproducible ?? false;
        this._options = {
            level: options.level ?? DEFAULT_ZIP_LEVEL,
            timestamps: options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS),
            comment: options.comment,
            modTime: options.modTime ?? (reproducible ? REPRODUCIBLE_ZIP_MOD_TIME : new Date()),
            smartStore: options.smartStore ?? true
        };
    }
    add(name, source, options) {
        if (this._sealed) {
            throw new Error("Cannot add entries after output has started");
        }
        if (!name) {
            throw new Error("Entry name is required");
        }
        this._entries.push({ name, source, options });
        return this;
    }
    stream() {
        this._sealed = true;
        const queue = createAsyncQueue();
        const zip = new StreamingZip((err, data, final) => {
            if (err) {
                queue.fail(err);
                return;
            }
            if (data.length) {
                queue.push(data);
            }
            if (final) {
                queue.close();
            }
        }, { comment: this._options.comment });
        (async () => {
            try {
                for (const entry of this._entries) {
                    const level = entry.options?.level ?? this._options.level;
                    const file = new ZipDeflateFile(entry.name, {
                        level,
                        modTime: entry.options?.modTime ?? this._options.modTime,
                        timestamps: this._options.timestamps,
                        comment: entry.options?.comment,
                        smartStore: this._options.smartStore
                    });
                    zip.add(file);
                    // Feed data
                    if (entry.source instanceof Uint8Array ||
                        entry.source instanceof ArrayBuffer ||
                        typeof entry.source === "string" ||
                        (typeof Blob !== "undefined" && entry.source instanceof Blob)) {
                        const bytes = await toUint8Array(entry.source);
                        await file.push(bytes, true);
                    }
                    else {
                        for await (const chunk of toAsyncIterable(entry.source)) {
                            await file.push(chunk, false);
                        }
                        await file.push(new Uint8Array(0), true);
                    }
                    await file.complete();
                }
                zip.end();
            }
            catch (e) {
                queue.fail(e instanceof Error ? e : new Error(String(e)));
            }
        })();
        return queue.iterable;
    }
    async bytes() {
        this._sealed = true;
        const allSourcesInMemory = this._entries.every(e => e.source instanceof Uint8Array ||
            e.source instanceof ArrayBuffer ||
            typeof e.source === "string" ||
            (typeof Blob !== "undefined" && e.source instanceof Blob));
        const hasBlobSource = this._entries.some(e => typeof Blob !== "undefined" && e.source instanceof Blob);
        // Fast-path: when all sources are already in memory and there are no
        // per-entry compression overrides, use the single-buffer ZIP builder.
        // This avoids the overhead of chunking + collecting from the streaming writer.
        if (allSourcesInMemory) {
            // Prefer the sync builder when possible (Node.js hot path): it avoids
            // async/Promise overhead and uses zlib sync fast paths.
            if (!hasBlobSource) {
                const entries = this._entries.map(e => ({
                    name: e.name,
                    data: toUint8ArraySync(e.source),
                    level: e.options?.level,
                    modTime: e.options?.modTime,
                    comment: e.options?.comment
                }));
                return createZipSync(entries, {
                    level: this._options.level,
                    timestamps: this._options.timestamps,
                    modTime: this._options.modTime,
                    comment: this._options.comment,
                    smartStore: this._options.smartStore
                });
            }
            const entries = await Promise.all(this._entries.map(async (e) => ({
                name: e.name,
                data: await toUint8Array(e.source),
                level: e.options?.level,
                modTime: e.options?.modTime,
                comment: e.options?.comment
            })));
            return createZip(entries, {
                level: this._options.level,
                timestamps: this._options.timestamps,
                modTime: this._options.modTime,
                comment: this._options.comment,
                smartStore: this._options.smartStore
            });
        }
        return collect(this.stream());
    }
    bytesSync() {
        this._sealed = true;
        const entries = this._entries.map(e => {
            if (!(e.source instanceof Uint8Array) &&
                !(e.source instanceof ArrayBuffer) &&
                typeof e.source !== "string") {
                throw new Error("bytesSync() only supports Uint8Array/ArrayBuffer/string sources");
            }
            return {
                name: e.name,
                data: toUint8ArraySync(e.source),
                modTime: e.options?.modTime,
                comment: e.options?.comment
            };
        });
        return createZipSync(entries, {
            level: this._options.level,
            timestamps: this._options.timestamps,
            modTime: this._options.modTime,
            comment: this._options.comment,
            smartStore: this._options.smartStore
        });
    }
    async pipeTo(sink) {
        await pipeIterableToSink(this.stream(), sink);
    }
}
export function zip(options) {
    return new ZipArchive(options);
}
