"use strict";
/**
 * ZIP file format builder
 *
 * Implements ZIP file structure according to PKWARE's APPNOTE.TXT specification
 * https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
 *
 * ZIP file structure:
 * ┌──────────────────────────┐
 * │   Local File Header 1    │
 * │   File Data 1            │
 * ├──────────────────────────┤
 * │   Local File Header 2    │
 * │   File Data 2            │
 * ├──────────────────────────┤
 * │         ...              │
 * ├──────────────────────────┤
 * │  Central Directory 1     │
 * │  Central Directory 2     │
 * │         ...              │
 * ├──────────────────────────┤
 * │ End of Central Directory │
 * └──────────────────────────┘
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZipBuilder = void 0;
exports.createZip = createZip;
exports.createZipSync = createZipSync;
const compress_1 = require("./compress.js");
const crc32_1 = require("./crc32.js");
const bytes_1 = require("./utils/bytes.js");
const zip_entry_metadata_1 = require("./zip-entry-metadata.js");
const defaults_1 = require("./defaults.js");
const zip_records_1 = require("./zip-records.js");
const zip_constants_1 = require("./zip-constants.js");
const LOCAL_FILE_HEADER_FIXED_SIZE = 30;
function encodeZipComment(comment) {
    // Keep empty comment as empty bytes (no encoding surprises).
    return comment ? new TextEncoder().encode(comment) : new Uint8Array(0);
}
function shouldDeflate(level, data) {
    return level > 0 && data.length > 0;
}
function computeLocalRecordSize(entry) {
    return (LOCAL_FILE_HEADER_FIXED_SIZE +
        entry.name.length +
        entry.extraField.length +
        entry.compressedData.length);
}
function buildProcessedEntry(entry, offset, settings, compressedData) {
    const modDate = entry.modTime ?? settings.defaultModTime;
    const isCompressed = shouldDeflate(settings.level, entry.data);
    const metadata = (0, zip_entry_metadata_1.buildZipEntryMetadata)({
        name: entry.name,
        comment: entry.comment,
        modTime: modDate,
        timestamps: settings.timestamps,
        useDataDescriptor: false,
        deflate: isCompressed
    });
    return {
        name: metadata.nameBytes,
        data: entry.data,
        compressedData,
        crc: (0, crc32_1.crc32)(entry.data),
        compressionMethod: (0, zip_entry_metadata_1.resolveZipCompressionMethod)(isCompressed),
        modTime: metadata.dosTime,
        modDate: metadata.dosDate,
        extraField: metadata.extraField,
        comment: metadata.commentBytes,
        offset
    };
}
function appendProcessedEntry(processedEntries, entry, compressedData, currentOffset, settings) {
    const processedEntry = buildProcessedEntry(entry, currentOffset, settings, compressedData);
    processedEntries.push(processedEntry);
    return {
        processedEntry,
        nextOffset: currentOffset + computeLocalRecordSize(processedEntry)
    };
}
function finalizeZip(processedEntries, zipComment, centralDirOffset) {
    // Build ZIP structure
    const chunks = [];
    // Local file headers and data
    for (const entry of processedEntries) {
        chunks.push(buildLocalFileHeaderChunk(entry));
        chunks.push(entry.compressedData);
    }
    chunks.push(...buildCentralDirectorySection(processedEntries, centralDirOffset, zipComment));
    return (0, bytes_1.concatUint8Arrays)(chunks);
}
function buildLocalFileHeaderChunk(entry) {
    return (0, zip_records_1.buildLocalFileHeader)({
        fileName: entry.name,
        extraField: entry.extraField,
        flags: zip_constants_1.FLAG_UTF8,
        compressionMethod: entry.compressionMethod,
        dosTime: entry.modTime,
        dosDate: entry.modDate,
        crc32: entry.crc,
        compressedSize: entry.compressedData.length,
        uncompressedSize: entry.data.length,
        versionNeeded: zip_constants_1.VERSION_NEEDED
    });
}
function buildCentralDirHeaderChunk(entry) {
    return (0, zip_records_1.buildCentralDirectoryHeader)({
        fileName: entry.name,
        extraField: entry.extraField,
        comment: entry.comment,
        flags: zip_constants_1.FLAG_UTF8,
        compressionMethod: entry.compressionMethod,
        dosTime: entry.modTime,
        dosDate: entry.modDate,
        crc32: entry.crc,
        compressedSize: entry.compressedData.length,
        uncompressedSize: entry.data.length,
        localHeaderOffset: entry.offset,
        versionMadeBy: zip_constants_1.VERSION_MADE_BY,
        versionNeeded: zip_constants_1.VERSION_NEEDED
    });
}
function buildCentralDirectorySection(processedEntries, centralDirOffset, zipComment) {
    const chunks = [];
    for (const entry of processedEntries) {
        chunks.push(buildCentralDirHeaderChunk(entry));
    }
    const centralDirSize = (0, bytes_1.sumUint8ArrayLengths)(chunks);
    chunks.push((0, zip_records_1.buildEndOfCentralDirectory)({
        entryCount: processedEntries.length,
        centralDirSize,
        centralDirOffset,
        comment: zipComment
    }));
    return chunks;
}
/**
 * Create a ZIP file from entries (async)
 *
 * @param entries - Files to include in ZIP
 * @param options - ZIP options
 * @returns ZIP file as Uint8Array
 *
 * @example
 * ```ts
 * const zip = await createZip([
 *   { name: "hello.txt", data: new TextEncoder().encode("Hello!") },
 *   { name: "folder/file.txt", data: new TextEncoder().encode("Nested!") }
 * ], { level: 6 });
 * ```
 */
async function createZip(entries, options = {}) {
    const level = options.level ?? defaults_1.DEFAULT_ZIP_LEVEL;
    const timestamps = options.timestamps ?? defaults_1.DEFAULT_ZIP_TIMESTAMPS;
    const zipComment = encodeZipComment(options.comment);
    const defaultModTime = new Date();
    const settings = {
        level,
        timestamps,
        defaultModTime
    };
    const compressOptions = {
        level,
        thresholdBytes: options.thresholdBytes
    };
    const compressedDatas = await Promise.all(entries.map(async (entry) => {
        return shouldDeflate(level, entry.data) ? (0, compress_1.compress)(entry.data, compressOptions) : entry.data;
    }));
    // Process entries
    const processedEntries = [];
    let currentOffset = 0;
    for (let i = 0; i < entries.length; i++) {
        const entry = entries[i];
        const compressedData = compressedDatas[i];
        const result = appendProcessedEntry(processedEntries, entry, compressedData, currentOffset, settings);
        currentOffset = result.nextOffset;
    }
    return finalizeZip(processedEntries, zipComment, currentOffset);
}
/**
 * Create a ZIP file from entries (sync)
 *
 * This is supported in both Node.js and browser builds.
 */
function createZipSync(entries, options = {}) {
    const level = options.level ?? defaults_1.DEFAULT_ZIP_LEVEL;
    const timestamps = options.timestamps ?? defaults_1.DEFAULT_ZIP_TIMESTAMPS;
    const zipComment = encodeZipComment(options.comment);
    const defaultModTime = new Date();
    const settings = {
        level,
        timestamps,
        defaultModTime
    };
    const compressOptions = {
        level,
        thresholdBytes: options.thresholdBytes
    };
    // Process entries
    const processedEntries = [];
    let currentOffset = 0;
    for (const entry of entries) {
        // Compress data
        const compressedData = shouldDeflate(level, entry.data)
            ? (0, compress_1.compressSync)(entry.data, compressOptions)
            : entry.data;
        const result = appendProcessedEntry(processedEntries, entry, compressedData, currentOffset, settings);
        currentOffset = result.nextOffset;
    }
    return finalizeZip(processedEntries, zipComment, currentOffset);
}
/**
 * Streaming ZIP builder for large files
 * Writes chunks to a callback as they are generated
 */
class ZipBuilder {
    /**
     * Create a new ZIP builder
     * @param options - ZIP options
     */
    constructor(options = {}) {
        this.entries = [];
        this.currentOffset = 0;
        this.finalized = false;
        this.level = options.level ?? defaults_1.DEFAULT_ZIP_LEVEL;
        this.zipComment = encodeZipComment(options.comment);
        this.timestamps = options.timestamps ?? defaults_1.DEFAULT_ZIP_TIMESTAMPS;
        this.compressOptions = {
            level: this.level,
            thresholdBytes: options.thresholdBytes
        };
        this.settings = {
            level: this.level,
            timestamps: this.timestamps,
            defaultModTime: new Date()
        };
    }
    /**
     * Add a file to the ZIP (async)
     * @param entry - File entry
     * @returns Local file header and compressed data chunks
     */
    async addFile(entry) {
        if (this.finalized) {
            throw new Error("Cannot add files after finalizing");
        }
        // Compress data
        const compressedData = shouldDeflate(this.level, entry.data)
            ? await (0, compress_1.compress)(entry.data, this.compressOptions)
            : entry.data;
        const result = appendProcessedEntry(this.entries, entry, compressedData, this.currentOffset, this.settings);
        this.currentOffset = result.nextOffset;
        return [buildLocalFileHeaderChunk(result.processedEntry), compressedData];
    }
    /**
     * Add a file to the ZIP (sync)
     * @param entry - File entry
     * @returns Local file header and compressed data chunks
     */
    addFileSync(entry) {
        if (this.finalized) {
            throw new Error("Cannot add files after finalizing");
        }
        // Compress data
        const compressedData = shouldDeflate(this.level, entry.data)
            ? (0, compress_1.compressSync)(entry.data, this.compressOptions)
            : entry.data;
        const result = appendProcessedEntry(this.entries, entry, compressedData, this.currentOffset, this.settings);
        this.currentOffset = result.nextOffset;
        return [buildLocalFileHeaderChunk(result.processedEntry), compressedData];
    }
    /**
     * Finalize the ZIP and return central directory + end record
     * @returns Central directory and end of central directory chunks
     */
    finalize() {
        if (this.finalized) {
            throw new Error("ZIP already finalized");
        }
        this.finalized = true;
        return buildCentralDirectorySection(this.entries, this.currentOffset, this.zipComment);
    }
    /**
     * Get current number of entries
     */
    get entryCount() {
        return this.entries.length;
    }
    /**
     * Get current ZIP data size (without central directory)
     */
    get dataSize() {
        return this.currentOffset;
    }
}
exports.ZipBuilder = ZipBuilder;
