/**
 * ZIP file format encoder (single-buffer output)
 *
 * Implements ZIP file structure according to PKWARE's APPNOTE.TXT specification
 * https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
 *
 * This module focuses on producing a complete ZIP as a single Uint8Array.
 * For true streaming (push chunks while reading sources), use `zip()` / `ZipArchive.stream()`.
 */
import { compress, compressSync } from "../compression/compress.browser.js";
import { crc32 } from "../compression/crc32.browser.js";
import { DEFAULT_ZIP_LEVEL, DEFAULT_ZIP_TIMESTAMPS } from "../defaults.js";
import { isProbablyIncompressible } from "../utils/compressibility.js";
import { encodeUtf8 } from "../utils/text.js";
import {} from "../utils/timestamps.js";
import { buildZipEntryMetadata, resolveZipCompressionMethod } from "./zip-entry-metadata.js";
import { FLAG_UTF8, ZIP_CENTRAL_DIR_HEADER_FIXED_SIZE, ZIP_END_OF_CENTRAL_DIR_FIXED_SIZE, ZIP_LOCAL_FILE_HEADER_FIXED_SIZE, writeCentralDirectoryHeaderInto, writeEndOfCentralDirectoryInto, writeLocalFileHeaderInto } from "../zip-spec/zip-records.js";
const REPRODUCIBLE_ZIP_MOD_TIME = new Date(1980, 0, 1, 0, 0, 0);
function encodeZipComment(comment) {
    // Keep empty comment as empty bytes (no encoding surprises).
    return comment ? encodeUtf8(comment) : new Uint8Array(0);
}
function shouldDeflate(level, data) {
    return level > 0 && data.length > 0;
}
async function compressEntryMaybe(entry, level, compressOptions, smartStore) {
    if (!shouldDeflate(level, entry.data)) {
        return { compressedData: entry.data, deflate: false };
    }
    if (!smartStore) {
        const compressed = await compress(entry.data, compressOptions);
        return { compressedData: compressed, deflate: true };
    }
    // Heuristic: skip deflate for high-entropy inputs.
    if (isProbablyIncompressible(entry.data)) {
        return { compressedData: entry.data, deflate: false };
    }
    const compressed = await compress(entry.data, compressOptions);
    if (compressed.length >= entry.data.length) {
        return { compressedData: entry.data, deflate: false };
    }
    return { compressedData: compressed, deflate: true };
}
function compressEntryMaybeSync(entry, level, compressOptions, smartStore) {
    if (!shouldDeflate(level, entry.data)) {
        return { compressedData: entry.data, deflate: false };
    }
    if (!smartStore) {
        const compressed = compressSync(entry.data, compressOptions);
        return { compressedData: compressed, deflate: true };
    }
    if (isProbablyIncompressible(entry.data)) {
        return { compressedData: entry.data, deflate: false };
    }
    const compressed = compressSync(entry.data, compressOptions);
    if (compressed.length >= entry.data.length) {
        return { compressedData: entry.data, deflate: false };
    }
    return { compressedData: compressed, deflate: true };
}
function computeLocalRecordSize(entry) {
    return (ZIP_LOCAL_FILE_HEADER_FIXED_SIZE +
        entry.name.length +
        entry.extraField.length +
        entry.compressedData.length);
}
function computeCentralDirHeaderSize(entry) {
    return (ZIP_CENTRAL_DIR_HEADER_FIXED_SIZE +
        entry.name.length +
        entry.extraField.length +
        entry.comment.length);
}
function buildProcessedEntry(entry, offset, settings, compressedData, deflate) {
    const modDate = entry.modTime ?? settings.defaultModTime;
    const metadata = buildZipEntryMetadata({
        name: entry.name,
        comment: entry.comment,
        modTime: modDate,
        timestamps: settings.timestamps,
        useDataDescriptor: false,
        deflate
    });
    return {
        name: metadata.nameBytes,
        uncompressedSize: entry.data.length,
        compressedData,
        crc: crc32(entry.data),
        compressionMethod: resolveZipCompressionMethod(deflate),
        modTime: metadata.dosTime,
        modDate: metadata.dosDate,
        extraField: metadata.extraField,
        comment: metadata.commentBytes,
        offset
    };
}
function appendProcessedEntry(processedEntries, entry, compressedData, deflate, currentOffset, settings) {
    const processedEntry = buildProcessedEntry(entry, currentOffset, settings, compressedData, deflate);
    processedEntries.push(processedEntry);
    return {
        processedEntry,
        nextOffset: currentOffset + computeLocalRecordSize(processedEntry)
    };
}
function finalizeZip(processedEntries, zipComment) {
    // Assemble ZIP into a single buffer to reduce allocations and copying.
    let localSectionSize = 0;
    let centralDirSize = 0;
    for (const entry of processedEntries) {
        localSectionSize += computeLocalRecordSize(entry);
        centralDirSize += computeCentralDirHeaderSize(entry);
    }
    // The central directory should start immediately after local section.
    const centralDirOffset = localSectionSize;
    const totalSize = localSectionSize + centralDirSize + ZIP_END_OF_CENTRAL_DIR_FIXED_SIZE + zipComment.length;
    const out = new Uint8Array(totalSize);
    const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
    let offset = 0;
    // Local file headers and data
    for (const entry of processedEntries) {
        offset += writeLocalFileHeaderInto(out, view, offset, {
            fileName: entry.name,
            extraField: entry.extraField,
            flags: FLAG_UTF8,
            compressionMethod: entry.compressionMethod,
            dosTime: entry.modTime,
            dosDate: entry.modDate,
            crc32: entry.crc,
            compressedSize: entry.compressedData.length,
            uncompressedSize: entry.uncompressedSize
        });
        out.set(entry.compressedData, offset);
        offset += entry.compressedData.length;
    }
    // Central directory headers
    for (const entry of processedEntries) {
        offset += writeCentralDirectoryHeaderInto(out, view, offset, {
            fileName: entry.name,
            extraField: entry.extraField,
            comment: entry.comment,
            flags: FLAG_UTF8,
            compressionMethod: entry.compressionMethod,
            dosTime: entry.modTime,
            dosDate: entry.modDate,
            crc32: entry.crc,
            compressedSize: entry.compressedData.length,
            uncompressedSize: entry.uncompressedSize,
            localHeaderOffset: entry.offset
        });
    }
    // End of central directory
    writeEndOfCentralDirectoryInto(out, view, offset, {
        entryCount: processedEntries.length,
        centralDirSize,
        centralDirOffset,
        comment: zipComment
    });
    return out;
}
/**
 * Create a ZIP file from entries (async)
 */
export async function createZip(entries, options = {}) {
    const reproducible = options.reproducible ?? false;
    const level = options.level ?? DEFAULT_ZIP_LEVEL;
    const smartStore = options.smartStore ?? true;
    const concurrency = options.concurrency ?? 4;
    const timestamps = options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS);
    const zipComment = encodeZipComment(options.comment);
    const defaultModTime = options.modTime ?? (reproducible ? REPRODUCIBLE_ZIP_MOD_TIME : new Date());
    const settings = {
        level,
        timestamps,
        defaultModTime
    };
    const thresholdBytes = options.thresholdBytes;
    const limit = Math.max(1, Math.floor(concurrency));
    const processedEntries = new Array(entries.length);
    if (entries.length > 0) {
        let nextIndex = 0;
        const workerCount = Math.min(limit, entries.length);
        const workers = Array.from({ length: workerCount }, async () => {
            while (true) {
                const idx = nextIndex++;
                if (idx >= entries.length) {
                    return;
                }
                const entry = entries[idx];
                const entryLevel = entry.level ?? level;
                const compressOptions = {
                    level: entryLevel,
                    thresholdBytes
                };
                const { compressedData, deflate } = await compressEntryMaybe(entry, entryLevel, compressOptions, smartStore);
                processedEntries[idx] = buildProcessedEntry(entry, 0, settings, compressedData, deflate);
            }
        });
        await Promise.all(workers);
    }
    // Compute offsets in original order.
    let currentOffset = 0;
    for (let i = 0; i < processedEntries.length; i++) {
        const processedEntry = processedEntries[i];
        processedEntry.offset = currentOffset;
        currentOffset += computeLocalRecordSize(processedEntry);
    }
    return finalizeZip(processedEntries, zipComment);
}
/**
 * Create a ZIP file from entries (sync)
 *
 * This is supported in both Node.js and browser builds.
 */
export function createZipSync(entries, options = {}) {
    const reproducible = options.reproducible ?? false;
    const level = options.level ?? DEFAULT_ZIP_LEVEL;
    const smartStore = options.smartStore ?? true;
    const timestamps = options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS);
    const zipComment = encodeZipComment(options.comment);
    const defaultModTime = options.modTime ?? (reproducible ? REPRODUCIBLE_ZIP_MOD_TIME : new Date());
    const settings = {
        level,
        timestamps,
        defaultModTime
    };
    const thresholdBytes = options.thresholdBytes;
    const processedEntries = [];
    let currentOffset = 0;
    for (const entry of entries) {
        const entryLevel = entry.level ?? level;
        const compressOptions = {
            level: entryLevel,
            thresholdBytes
        };
        const { compressedData, deflate } = compressEntryMaybeSync(entry, entryLevel, compressOptions, smartStore);
        const result = appendProcessedEntry(processedEntries, entry, compressedData, deflate, currentOffset, settings);
        currentOffset = result.nextOffset;
    }
    return finalizeZip(processedEntries, zipComment);
}
