/*
 * SDK Pullenti Unitext, version 4.33, fabruary 2026. Copyright (c) 2013-2026, Pullenti. All rights reserved. 
 * Non-Commercial Freeware and Commercial Software. 
 * This class is generated using the converter UniSharping (www.unisharping.ru) from Pullenti C# project. 
 * The latest version of the code is available on the site www.pullenti.ru
 */

const Utils = require("./../../../unisharp/Utils");
const Stream = require("./../../../unisharp/Stream");

const GeneralBitFlags = require("./GeneralBitFlags");
const ZipConstants = require("./ZipConstants");
const DeflaterOutputStream = require("./DeflaterOutputStream");
const ZipExtraData = require("./ZipExtraData");
const ZipEntry = require("./ZipEntry");
const ZipHelperStream = require("./ZipHelperStream");
const Deflater = require("./Deflater");
const UseZip64 = require("./UseZip64");
const CompressionMethod = require("./CompressionMethod");
const Crc32 = require("./Crc32");

// This is a DeflaterOutputStream that writes the files into a zip
// archive one after another.  It has a special method to start a new
// zip entry.  The zip entries contains information about the file name
// size, compressed size, CRC, etc.
// It includes support for Stored and Deflated entries.
// This class is not thread safe.
class ZipOutputStream extends DeflaterOutputStream {
    
    constructor(baseOutputStream, bufferSize = 512) {
        super(baseOutputStream, new Deflater(Deflater.DEFAULT_COMPRESSION, true), bufferSize);
        this.entries = new Array();
        this.crc = new Crc32();
        this.curEntry = null;
        this.defaultCompressionLevel = Deflater.DEFAULT_COMPRESSION;
        this.curMethod = CompressionMethod.DEFLATED;
        this.size = 0;
        this.offset = 0;
        this.zipComment = new Uint8Array(0);
        this.patchEntryHeader = false;
        this.crcPatchPos = -1;
        this.sizePatchPos = -1;
        this.useZip64_ = UseZip64.DYNAMIC;
    }
    
    get isFinished() {
        return this.entries === null;
    }
    
    setComment(comment) {
        let commentBytes = ZipConstants.convertToArrayStr(comment);
        if (commentBytes.length > 0xffff) 
            throw new Error("comment");
        this.zipComment = commentBytes;
    }
    
    setLevel(level) {
        this.deflater_.setLevel(level);
        this.defaultCompressionLevel = level;
    }
    
    getLevel() {
        return this.deflater_.getLevel();
    }
    
    get useZip64() {
        return this.useZip64_;
    }
    set useZip64(value) {
        this.useZip64_ = value;
        return value;
    }
    
    writeLeShort(value) {
        /* unchecked C# block !!! */ {
            this.baseOutputStream_.writeByte((value & 0xff));
            this.baseOutputStream_.writeByte((((value >> 8)) & 0xff));
        }
    }
    
    writeLeInt(value) {
        /* unchecked C# block !!! */ {
            this.writeLeShort(value);
            this.writeLeShort(value >> 16);
        }
    }
    
    writeLeLong(value) {
        /* unchecked C# block !!! */ {
            this.writeLeInt(value);
            this.writeLeInt((value >> 32));
        }
    }
    
    putNextEntry(entry) {
        if (entry === null) 
            throw new Error("entry");
        if (this.entries === null) 
            throw new Error("ZipOutputStream was finished");
        if (this.curEntry !== null) 
            this.closeEntry();
        if (this.entries.length === 2147483647) 
            throw new Error("Too many entries for Zip file");
        let method = entry._CompressionMethod;
        let compressionLevel = this.defaultCompressionLevel;
        entry.flags &= (GeneralBitFlags.UNICODETEXT.value());
        this.patchEntryHeader = false;
        let headerInfoAvailable = false;
        if (entry.size === 0) {
            entry.compressedSize = entry.size;
            entry.crc = 0;
            method = CompressionMethod.STORED;
            headerInfoAvailable = true;
        }
        else {
            headerInfoAvailable = (entry.size >= 0) && entry.hasCrc;
            if (method === CompressionMethod.STORED) {
                if (!headerInfoAvailable) {
                    if (!this.canPatchEntries) {
                        method = CompressionMethod.DEFLATED;
                        compressionLevel = 0;
                    }
                }
                else {
                    entry.compressedSize = entry.size;
                    headerInfoAvailable = entry.hasCrc;
                }
            }
        }
        if (headerInfoAvailable === false) {
            if (this.canPatchEntries === false) 
                entry.flags |= 8;
            else 
                this.patchEntryHeader = true;
        }
        entry.offset = this.offset;
        entry._CompressionMethod = method;
        this.curMethod = method;
        this.sizePatchPos = -1;
        if ((this.useZip64_ === UseZip64.ON) || ((((entry.size < 0)) && (this.useZip64_ === UseZip64.DYNAMIC)))) 
            entry.forceZip64();
        this.writeLeInt(ZipConstants.LOCAL_HEADER_SIGNATURE);
        this.writeLeShort(entry.version);
        this.writeLeShort(entry.flags);
        this.writeLeShort(entry.compressionMethodForHeader.value());
        this.writeLeInt(entry.dosTime);
        if (headerInfoAvailable === true) {
            this.writeLeInt(entry.crc);
            if (entry.localHeaderRequiresZip64) {
                this.writeLeInt(-1);
                this.writeLeInt(-1);
            }
            else {
                this.writeLeInt((entry.isCrypted ? (entry.compressedSize) + ZipConstants.CRYPTO_HEADER_SIZE : entry.compressedSize));
                this.writeLeInt(entry.size);
            }
        }
        else {
            if (this.patchEntryHeader) 
                this.crcPatchPos = this.baseOutputStream_.position;
            this.writeLeInt(0);
            if (this.patchEntryHeader) 
                this.sizePatchPos = this.baseOutputStream_.position;
            if (entry.localHeaderRequiresZip64 || this.patchEntryHeader) {
                this.writeLeInt(-1);
                this.writeLeInt(-1);
            }
            else {
                this.writeLeInt(0);
                this.writeLeInt(0);
            }
        }
        let name = ZipConstants.convertToArray(entry.flags, entry.name);
        if (name.length > 0xFFFF) 
            throw new Error("Entry name too long.");
        let ed = new ZipExtraData(entry.extraData);
        if (entry.localHeaderRequiresZip64) {
            ed.startNewEntry();
            if (headerInfoAvailable) {
                ed.addLeLong(entry.size, 0);
                ed.addLeLong(entry.compressedSize, 0);
            }
            else {
                ed.addLeLong(-1, -1);
                ed.addLeLong(-1, -1);
            }
            ed.addNewEntry(1);
            if (!ed.find(1)) 
                throw new Error("Internal error cant find extra data");
            if (this.patchEntryHeader) 
                this.sizePatchPos = ed.currentReadIndex;
        }
        else 
            ed._delete(1);
        let extra = ed.getEntryData();
        this.writeLeShort(name.length);
        this.writeLeShort(extra.length);
        if (name.length > 0) 
            this.baseOutputStream_.write(name, 0, name.length);
        if (entry.localHeaderRequiresZip64 && this.patchEntryHeader) 
            this.sizePatchPos += this.baseOutputStream_.position;
        if (extra.length > 0) 
            this.baseOutputStream_.write(extra, 0, extra.length);
        this.offset += (ZipConstants.LOCAL_HEADER_BASE_SIZE + name.length + extra.length);
        if (entry.aESKeySize > 0) 
            this.offset += entry.aESOverheadSize;
        this.curEntry = entry;
        this.crc.reset();
        if (method === CompressionMethod.DEFLATED) {
            this.deflater_.reset();
            this.deflater_.setLevel(compressionLevel);
        }
        this.size = 0;
    }
    
    closeEntry() {
        if (this.curEntry === null) 
            throw new Error("No open entry");
        let csize = this.size;
        if (this.curMethod === CompressionMethod.DEFLATED) {
            if (this.size >= 0) {
                super.finish();
                csize = this.deflater_.totalOut;
            }
            else 
                this.deflater_.reset();
        }
        if (this.curEntry.size < 0) 
            this.curEntry.size = this.size;
        else if (this.curEntry.size !== this.size) 
            throw new Error(("size was " + (String(this.size)) + ", but I expected ") + (String(this.curEntry.size)));
        if (this.curEntry.compressedSize < 0) 
            this.curEntry.compressedSize = csize;
        else if (this.curEntry.compressedSize !== csize) 
            throw new Error(("compressed size was " + (String(csize)) + ", but I expected ") + (String(this.curEntry.compressedSize)));
        if (!this.curEntry.crcOk) 
            this.curEntry.crc = this.crc.value;
        else if (Utils.compareUnsigned(this.curEntry.crc, this.crc.value) != 0) 
            throw new Error(("crc was " + (String(this.crc.value)) + ", but I expected ") + (String(this.curEntry.crc)));
        this.offset += csize;
        if (this.curEntry.isCrypted) {
            if (this.curEntry.aESKeySize > 0) 
                this.curEntry.compressedSize = this.curEntry.compressedSize + this.curEntry.aESOverheadSize;
            else 
                this.curEntry.compressedSize = this.curEntry.compressedSize + ZipConstants.CRYPTO_HEADER_SIZE;
        }
        if (this.patchEntryHeader) {
            this.patchEntryHeader = false;
            let curPos = this.baseOutputStream_.position;
            this.baseOutputStream_.seek(this.crcPatchPos, 0);
            this.writeLeInt(this.curEntry.crc);
            if (this.curEntry.localHeaderRequiresZip64) {
                if (this.sizePatchPos === (-1)) 
                    throw new Error("Entry requires zip64 but this has been turned off");
                this.baseOutputStream_.seek(this.sizePatchPos, 0);
                this.writeLeLong(this.curEntry.size);
                this.writeLeLong(this.curEntry.compressedSize);
            }
            else {
                this.writeLeInt(this.curEntry.compressedSize);
                this.writeLeInt(this.curEntry.size);
            }
            this.baseOutputStream_.seek(curPos, 0);
        }
        if (((this.curEntry.flags & 8)) !== 0) {
            this.writeLeInt(ZipConstants.DATA_DESCRIPTOR_SIGNATURE);
            this.writeLeInt(this.curEntry.crc);
            if (this.curEntry.localHeaderRequiresZip64) {
                this.writeLeLong(this.curEntry.compressedSize);
                this.writeLeLong(this.curEntry.size);
                this.offset += ZipConstants.ZIP64DATA_DESCRIPTOR_SIZE;
            }
            else {
                this.writeLeInt(this.curEntry.compressedSize);
                this.writeLeInt(this.curEntry.size);
                this.offset += ZipConstants.DATA_DESCRIPTOR_SIZE;
            }
        }
        this.entries.push(this.curEntry);
        this.curEntry = null;
    }
    
    write(buffer, _offset, count) {
        if (this.curEntry === null) 
            throw new Error("No open entry.");
        if (buffer === null) 
            throw new Error("null buffer");
        if (_offset < 0) 
            throw new Error("offset out of range");
        if (count < 0) 
            throw new Error("bad count");
        if (((buffer.length - _offset)) < count) 
            throw new Error("Invalid offset/count combination");
        try {
            this.crc.updateByBufEx(buffer, _offset, count);
        } catch (ex) {
            throw new Error(ex.message, ex);
        }
        this.size += count;
        try {
            switch (this.curMethod) { 
            case CompressionMethod.DEFLATED: {
                super.write(buffer, _offset, count);
                break;
            }
            case CompressionMethod.STORED: {
                this.baseOutputStream_.write(buffer, _offset, count);
                break;
            }
            }
        } catch (ex) {
            throw new Error(ex.message, ex);
        }
    }
    
    finish() {
        if (this.entries === null) 
            return;
        if (this.curEntry !== null) 
            this.closeEntry();
        let numEntries = this.entries.length;
        let sizeEntries = 0;
        for (const en of this.entries) {
            let entry = Utils.as(en, ZipEntry);
            if (entry === null) 
                continue;
            this.writeLeInt(ZipConstants.CENTRAL_HEADER_SIGNATURE);
            this.writeLeShort(ZipConstants.VERSION_MADE_BY);
            this.writeLeShort(entry.version);
            this.writeLeShort(entry.flags);
            this.writeLeShort(entry.compressionMethodForHeader.value());
            this.writeLeInt(entry.dosTime);
            this.writeLeInt(entry.crc);
            if (entry.isZip64Forced() || (Utils.compareUnsigned(entry.compressedSize, 4294967295) >= 0)) 
                this.writeLeInt(-1);
            else 
                this.writeLeInt(entry.compressedSize);
            if (entry.isZip64Forced() || (Utils.compareUnsigned(entry.size, 4294967295) >= 0)) 
                this.writeLeInt(-1);
            else 
                this.writeLeInt(entry.size);
            let name = ZipConstants.convertToArray(entry.flags, entry.name);
            if (name.length > 0xffff) 
                throw new Error("Name too long.");
            let ed = new ZipExtraData(entry.extraData);
            if (entry.centralHeaderRequiresZip64) {
                ed.startNewEntry();
                if (entry.isZip64Forced()) 
                    ed.addLeLong(entry.size, 0);
                if (entry.isZip64Forced()) 
                    ed.addLeLong(entry.compressedSize, 0);
                ed.addNewEntry(1);
            }
            else 
                ed._delete(1);
            let extra = ed.getEntryData();
            let entryComment = (entry.comment !== null ? ZipConstants.convertToArray(entry.flags, entry.comment) : new Uint8Array(0));
            if (entryComment.length > 0xffff) 
                throw new Error("Comment too long.");
            this.writeLeShort(name.length);
            this.writeLeShort(extra.length);
            this.writeLeShort(entryComment.length);
            this.writeLeShort(0);
            this.writeLeShort(0);
            if (entry.externalFileAttributes !== -1) 
                this.writeLeInt(entry.externalFileAttributes);
            else if (entry.isDirectory) 
                this.writeLeInt(16);
            else 
                this.writeLeInt(0);
            if (Utils.compareUnsigned(entry.offset, 4294967295) >= 0) 
                this.writeLeInt(-1);
            else 
                this.writeLeInt(entry.offset);
            if (name.length > 0) 
                this.baseOutputStream_.write(name, 0, name.length);
            if (extra.length > 0) 
                this.baseOutputStream_.write(extra, 0, extra.length);
            if (entryComment.length > 0) 
                this.baseOutputStream_.write(entryComment, 0, entryComment.length);
            sizeEntries += ((ZipConstants.CENTRAL_HEADER_BASE_SIZE + name.length + extra.length) + entryComment.length);
        }
        let zhs = new ZipHelperStream(null, this.baseOutputStream_); 
        try {
            zhs.writeEndOfCentralDirectory(numEntries, sizeEntries, this.offset, this.zipComment);
        }
        finally {
            zhs.close();
        }
        this.entries = null;
    }
}


module.exports = ZipOutputStream