/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2024. All rights reserved.
 */
package zip4cj.io.outputstream

func initializeZipModel(zipModel: ZipModel, countingOutputStream: CountingOutputStream): ZipModel {
    if (countingOutputStream.isSplitZipFile()) {
        zipModel.setSplitArchive(true)
        zipModel.setSplitLength(countingOutputStream.getSplitLength())
    }

    return zipModel
}

public class ZipOutputStream <: OutputStream & Resource {
    private var countingOutputStream: CountingOutputStream
    private var password: ?Array<Rune>
    private var zipModel: ZipModel
    private var compressedOutputStream: CompressedOutputStream = unsafe { zeroValue<CompressedOutputStream>() }
    private var compressedOutputStreamInit: Bool = false
    private var fileHeader: ?FileHeader = None
    private var localFileHeader: ?LocalFileHeader = None
    private var fileHeaderFactory: FileHeaderFactory = FileHeaderFactory()
    private var headerWriter: HeaderWriter = HeaderWriter()
    private var crc32: CRC32 = CRC32()
    private var rawIO: RawIO = RawIO()
    private var uncompressedSizeForThisEntry: Int64 = 0
    private var zip4cjConfig: Zip4cjConfig
    private var streamClosed: Bool
    private var entryClosed: Bool = true

    public init(outputStream: OutputStream, 
                password!: ?Array<Rune> = None, 
                zip4cjConfig!: Zip4cjConfig = Zip4cjConfig(InternalZipConstants.BUFF_SIZE, InternalZipConstants.USE_UTF8_FOR_PASSWORD_ENCODING_DECODING), 
                zipModel!: ZipModel = ZipModel()) {
        if (zip4cjConfig.getBufferSize() < InternalZipConstants.MIN_BUFF_SIZE) {
            throw IllegalArgumentException(
                "Buffer size cannot be less than ${InternalZipConstants.MIN_BUFF_SIZE} bytes")
        }
        this.countingOutputStream = CountingOutputStream(outputStream)
        this.password = password
        this.zip4cjConfig = zip4cjConfig
        this.zipModel = initializeZipModel(zipModel, countingOutputStream)
        this.streamClosed = false
        if (countingOutputStream.isSplitZipFile()) {
            rawIO.writeIntLittleEndian(countingOutputStream, Int32(HeaderSignature.SPLIT_ZIP.getValue()))
        }
    }

    public func putNextEntry(zipParameters: ZipParameters): Unit {
        verifyZipParameters(zipParameters)
        var clonedZipParameters: ZipParameters = cloneAndPrepareZipParameters(zipParameters)
        initializeAndWriteFileHeader(clonedZipParameters)

        compressedOutputStream = initializeCompressedOutputStream(clonedZipParameters)
        this.compressedOutputStreamInit = true
        this.entryClosed = false
    }

    func checkCompressedOutputStream() {
        if (!this.compressedOutputStreamInit) {
            throw NoneValueException("please add an entry first, which requires calling the putNextEntry function before calling the other function operation.")
        }
    }

    public func write(b: Array<UInt8>): Unit {
        this.checkCompressedOutputStream()
        ensureStreamOpen()
        crc32.update(b)
        compressedOutputStream.write(b)
        uncompressedSizeForThisEntry += b.size
    }

    public func closeEntry(): FileHeader {
        // this.checkCompressedOutputStream()
        compressedOutputStream.closeEntry()

        var compressedSize: Int64 = compressedOutputStream.getCompressedSize()
        fileHeader.getOrThrow().setCompressedSize(compressedSize)
        localFileHeader.getOrThrow().setCompressedSize(compressedSize)

        fileHeader.getOrThrow().setUncompressedSize(uncompressedSizeForThisEntry)
        localFileHeader.getOrThrow().setUncompressedSize(uncompressedSizeForThisEntry)
        if (writeCrc(fileHeader.getOrThrow())) {
            fileHeader.getOrThrow().setCrc(crc32.getValue())
            localFileHeader.getOrThrow().setCrc(crc32.getValue())
        }
        zipModel.getLocalFileHeaders().add(localFileHeader.getOrThrow())
        zipModel.getCentralDirectory().getOrThrow().getFileHeaders().add(fileHeader.getOrThrow())

        if (localFileHeader.getOrThrow().isDataDescriptorExists()) {
            headerWriter.writeExtendedLocalHeader(localFileHeader, countingOutputStream)
        }
        reset()
        this.entryClosed = true
        return fileHeader.getOrThrow()
    }

    public func close(): Unit {
        if (!this.entryClosed) {
            closeEntry()
        }
        zipModel.getEndOfCentralDirectoryRecord().setOffsetOfStartOfCentralDirectory(
            countingOutputStream.getNumberOfBytesWritten())
        headerWriter.finalizeZipFile(zipModel, countingOutputStream)
        countingOutputStream.close()
        this.streamClosed = true
    }


    func ensureStreamOpen(): Unit {
        if (streamClosed) {
            throw Exception("Stream is closed")
        }
    }

    func initializeAndWriteFileHeader(zipParameters: ZipParameters): Unit {
        fileHeader = fileHeaderFactory.generateFileHeader(zipParameters, countingOutputStream.isSplitZipFile(),
            Int64(countingOutputStream.getCurrentSplitFileCounter()), rawIO)
        fileHeader.getOrThrow().setOffsetLocalHeader(countingOutputStream.getOffsetForNextEntry())

        localFileHeader = fileHeaderFactory.generateLocalFileHeader(fileHeader.getOrThrow())
        headerWriter.writeLocalFileHeader(zipModel, localFileHeader.getOrThrow(), countingOutputStream)
    }

    func reset(): Unit {
        this.checkCompressedOutputStream()
        uncompressedSizeForThisEntry = 0
        crc32.reset()
        compressedOutputStream.close()
    }

    func initializeCompressedOutputStream(zipParameters: ZipParameters): CompressedOutputStream {
        var zipEntryOutputStream: ZipEntryOutputStream = ZipEntryOutputStream(this.countingOutputStream)
        var cipherOutputStream: ICipherOutputStream = initializeCipherOutputStream(zipEntryOutputStream, zipParameters)
        return initializeCompressedOutputStream(cipherOutputStream, zipParameters)
    }

    func initializeCipherOutputStream(zipEntryOutputStream: ZipEntryOutputStream, zipParameters: ZipParameters): ICipherOutputStream {
        if (!zipParameters.isEncryptFiles()) {
            return NoCipherOutputStream(zipEntryOutputStream, zipParameters, Array<Rune>())
        }
        if (password.isNone() || password.getOrThrow().size == 0) {
            throw ZipException("password not set")
        }
        if (zipParameters.getEncryptionMethod() == EncryptionMethod.AES) {
            return AesCipherOutputStream(zipEntryOutputStream, zipParameters, password, zip4cjConfig.isUseUtf8CharsetForPasswords())
            // throw Exception(" not supported")
        } else if (zipParameters.getEncryptionMethod() == EncryptionMethod.ZIP_STANDARD) {
            return ZipStandardCipherOutputStream(zipEntryOutputStream, zipParameters, password.getOrThrow(),
                zip4cjConfig.isUseUtf8CharsetForPasswords())
        } else if (zipParameters.getEncryptionMethod() == EncryptionMethod.ZIP_STANDARD_VARIANT_STRONG) {
            throw ZipException("ZIP_STANDARD_VARIANT_STRONG encryption method is not supported")
        } else {
            throw ZipException("Invalid encryption method")
        }
    }

    func initializeCompressedOutputStream(cipherOutputStream: ICipherOutputStream, zipParameters: ZipParameters): CompressedOutputStream {
        if (zipParameters.getCompressionMethod() == CompressionMethod.DEFLATE) {
            return DeflaterOutputStream(cipherOutputStream, zipParameters.getCompressionLevel(),
                zip4cjConfig.getBufferSize())
        }
        return StoreOutputStream(cipherOutputStream)
    }

    func verifyZipParameters(zipParameters: ZipParameters): Unit {
        if (Zip4cjUtil.isStringNullOrEmpty(zipParameters.getFileNameInZip())) {
            throw IllegalArgumentException("fileNameInZip is null or empty")
        }

        if (zipParameters.getCompressionMethod() == CompressionMethod.STORE && zipParameters.getEntrySize() < 0 &&
            !FileUtils.isZipEntryDirectory(zipParameters.getFileNameInZip().getOrThrow()) &&
            zipParameters.isWriteExtendedLocalFileHeader()) {
            throw IllegalArgumentException("uncompressed size should be set for zip entries of compression type store")
        }
    }

    func writeCrc(fileHeader: FileHeader): Bool {
        var isAesEncrypted: Bool = fileHeader.isEncrypted() && fileHeader.getEncryptionMethod() == (EncryptionMethod.AES)
        if (!isAesEncrypted) {
            return true
        }
        return fileHeader.getAesExtraDataRecord().getOrThrow().getAesVersion() == (AesVersion.ONE)
    }

    func cloneAndPrepareZipParameters(zipParameters: ZipParameters): ZipParameters {
        var clonedZipParameters: ZipParameters = ZipParameters(zipParameters)

        if (FileUtils.isZipEntryDirectory(zipParameters.getFileNameInZip().getOrThrow())) {
            clonedZipParameters.setWriteExtendedLocalFileHeader(false)
            clonedZipParameters.setCompressionMethod(CompressionMethod.STORE)
            clonedZipParameters.setEncryptFiles(false)
            clonedZipParameters.setEntrySize(0)
        }

        if (zipParameters.getLastModifiedFileTime() <= 0) {
            clonedZipParameters.setLastModifiedFileTime(DateTime.now().toUnixTimeStamp().toMilliseconds())
        }

        return clonedZipParameters
    }

    public func isClosed(): Bool {
        streamClosed
    }

}