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

import std.io.InputStream
import std.io.IOException
import std.fs.exists
import std.fs.canonicalize
import std.fs.Path
import std.fs.FileInfo
import std.collection.Map

public import zip4cj.io.inputstream.*
public import zip4cj.io.outputstream.*
public import zip4cj.util.*
public import zip4cj.model.*
public import zip4cj.tasks.*
public import zip4cj.util.*
public import zip4cj.crypto.*
public import zip4cj.headers.*
public import zip4cj.progress.*
public import zip4cj.exception.*
public import zip4cj.model.enums.*
import zip4cj.util.internals.*


let zipParameters = ZipParameters()
let unZipParameters = UnzipParameters()

public class ZipFile <: Resource {
    private let zipFile: Path
    private var isClose = false
    private var zipModelOpt: ZipModel = unsafe { zeroValue<ZipModel>() }

    private prop zipModel: ZipModel {
        get() {
            if (isNullzipModel) {
                throw NoneValueException("ZipModel not initialized.")
            }
            this.zipModelOpt
        }
    }
    private var isNullzipModel: Bool = true
    private var isEncrypte: Bool = false
    private var progressMonitor: ProgressMonitor
    private var runInThread: Bool
    private var password: ?Array<Rune>
    private let headerWriter = HeaderWriter()
    private var executorService: ?ExecutorService = None
    private var bufferSize: Int64 = InternalZipConstants.BUFF_SIZE
    private let openInputStreams = ArrayList<Resource>()
    private var useUtf8CharsetForPasswords = InternalZipConstants.USE_UTF8_FOR_PASSWORD_ENCODING_DECODING

    public init(zipFile: String) {
        this(Path(zipFile), None)
    }

    public init(zipFile: Path) {
        this(zipFile, None)
    }

    public init(zipFile: String, password: Array<Rune>) {
        this(Path(zipFile), password)
    }

    public init(zipFile: Path, password: ?Array<Rune>) {
        this.zipFile = if (exists(zipFile)) {
            canonicalize(zipFile)
        } else {
            if (zipFile.isAbsolute()) {
                zipFile
            } else {
                InternalZipConstants.FILE_CURRENT_PATH.join(zipFile)
            }
        }
        this.password = password
        this.runInThread = false
        this.progressMonitor = ProgressMonitor()
    }

    ~init() {
        for (i in 0..openInputStreams.size) {
            openInputStreams[i].close()
        }
        this.openInputStreams.clear()
        if (let Some(v) <- this.executorService) {
            v.close()
        } 
    }

    public func createSplitZipFile(
        filesToAdd: Collection<Path>,
        parameters: ZipParameters,
        splitArchive: Bool,
        splitLength: Int64) {
        if (let Some(v) <- (filesToAdd as ArrayList<Path>)) {
            createSplitZipFile(unsafe{v.getRawArray()[0..v.size]}, parameters, splitArchive, splitLength)
        } else if (let Some(v) <- (filesToAdd as Array<Path>)) {
            createSplitZipFile(v, parameters, splitArchive, splitLength)
        } else {
            createSplitZipFile(filesToAdd.toArray(), parameters, splitArchive, splitLength)
        }
    }

    public func createSplitZipFile(
        filesToAdd: Array<Path>,
        parameters: ZipParameters,
        splitArchive: Bool,
        splitLength: Int64
    ) {
        if (exists(zipFile)) {
            throw ZipException(
                "zip file: ${zipFile} already exists. To add files to existing zip file use addFile method")
        }
        if (filesToAdd.size == 0) {
            throw ZipException("input file List is None, cannot create zip file")
        }
        createNewZipModel()
        zipModel.setSplitArchive(splitArchive)
        zipModel.setSplitLength(if (splitArchive) {
            splitLength
        } else {
            -1
        })
        AddFilesToZipTask(zipModel, password, headerWriter, buildAsyncParameters()).execute(
            AddFilesToZipTaskParameters(filesToAdd, parameters, buildConfig()))
    }

    public func createSplitZipFile(
        inputStream: InputStream,
        parameters: ZipParameters,
        splitArchive: Bool,
        splitLength: Int64
    ) {
        if (exists(zipFile)) {
            throw ZipException(
                "zip file: ${zipFile} already exists. To add files to existing zip file use addFile method")
        }
        createNewZipModel()
        zipModel.setSplitArchive(splitArchive)
        zipModel.setSplitLength(if (splitArchive) {
            splitLength
        } else {
            -1
        })
        AddStreamToZipTask(zipModel, password, headerWriter, buildAsyncParameters()).execute(
        AddStreamToZipTaskParameters(inputStream, parameters, buildConfig()))
    }

    public func createSplitZipFileFromFolder(
        folderToAdd: Path,
        parameters: ZipParameters,
        splitArchive: Bool,
        splitLength: Int64
    ) {
        createNewZipModel()
        zipModel.setSplitArchive(splitArchive)
        if (splitArchive) {
            zipModel.setSplitLength(splitLength)
        }
        addFolder(folderToAdd, parameters, false)
    }

    public func addFile(fileToAdd: String) {
        this.addFile(fileToAdd, zipParameters)
    }
    public func addFile(fileToAdd: String, parameters: ZipParameters) {
        if (!Zip4cjUtil.isStringNotNullAndNotEmpty(fileToAdd)) {
            throw ZipException("file to add is None or empty")
        }
        addFiles([Path(fileToAdd)], parameters)
    }

    public func addFile(fileToAdd: Path) {
        this.addFiles([fileToAdd], zipParameters)
    }
    public func addFile(fileToAdd: Path, parameters: ZipParameters) {
        addFiles([fileToAdd], parameters)
    }

    public func addFiles(filesToAdd: Collection<Path>) {
        addFiles(filesToAdd, zipParameters)
    }
    public func addFiles(filesToAdd: Collection<Path>, parameters: ZipParameters ) {
        if (let Some(v) <- (filesToAdd as ArrayList<Path>)) {
            addFiles(unsafe{v.getRawArray()[0..v.size]}, parameters)
        } else if (let Some(v) <- (filesToAdd as Array<Path>)) {
            addFiles(v, parameters)
        } else {
            addFiles(filesToAdd.toArray(), parameters)
        }
    }

    public func addFiles(filesToAdd: Array<Path>) {
        this.addFiles(filesToAdd, zipParameters)
    }
    public func addFiles(filesToAdd: Array<Path>, parameters: ZipParameters) {
        if (filesToAdd.size == 0) {
            throw ZipException("input file List is None or empty")
        }
        this.readZipInfo()
        if (exists(zipFile) && zipModel.isSplitArchive()) {
            throw ZipException("Zip file already exists. Zip file format does not allow updating split/spanned files")
        }
        AddFilesToZipTask(zipModel, password, headerWriter, buildAsyncParameters()).execute(
            AddFilesToZipTaskParameters(filesToAdd, parameters, buildConfig()))
    }

    public func addFolder(folderToAdd: Path) {
        this.addFolder(folderToAdd, zipParameters, true)
    }
    public func addFolder(folderToAdd: Path, parameters: ZipParameters ) {
        this.addFolder(folderToAdd, parameters, true)
    }
    public func addFolder(folderToAdd: Path, parameters: ZipParameters, checkSplitArchive: Bool) {
        if (!FileInfo(folderToAdd).isDirectory()) {
            throw ZipException("input folder is not a directory")
        }
        this.readZipInfo()
        if (checkSplitArchive) {
            if (zipModel.isSplitArchive()) {
                throw ZipException(
                    "This is a split archive. Zip file format does not allow updating split/spanned files")
            }
        }
        AddFolderToZipTask(zipModel, password, headerWriter, buildAsyncParameters()).execute(
            AddFolderToZipTaskParameters(folderToAdd, parameters, buildConfig()))
    }

    public func addStream(inputStream: InputStream, parameters: ZipParameters) {
        this.setRunInThread(false)
        this.readZipInfo()
        if (exists(zipFile) && zipModel.isSplitArchive()) {
            throw ZipException("Zip file already exists. Zip file format does not allow updating split/spanned files")
        }
        AddStreamToZipTask(zipModel, password, headerWriter, buildAsyncParameters()).execute(
            AddStreamToZipTaskParameters(inputStream, parameters, buildConfig()))
    }

    public func extractAll(destinationPath: String) {
        this.extractAll(destinationPath, unZipParameters)
    }
    public func extractAll(destinationPath: String, unzipParameters: UnzipParameters ) {
        if (!Zip4cjUtil.isStringNotNullAndNotEmpty(destinationPath)) {
            throw ZipException("output path is None or invalid")
        }
        if (!Zip4cjUtil.createDirectoryIfNotExists(Path(destinationPath))) {
            throw ZipException("invalid output path")
        }
        if (isNullzipModel) {
            this.readZipInfo()
        }
        // Throw an exception if zipModel is still None
        if (isNullzipModel) {
            throw ZipException("Internal error occurred when extracting zip file")
        }
        ExtractAllFilesTask(zipModel, password, unzipParameters, buildAsyncParameters()).execute(
            ExtractAllFilesTaskParameters(destinationPath, buildConfig()))
    }

    public func extractFile(fileHeader: FileHeader, destinationPath: String) {
        this.extractFile(fileHeader, destinationPath, None, unZipParameters)
    }

    public func extractFile(fileHeader: FileHeader, destinationPath: String, newFileName: ?String) {
        this.extractFile(fileHeader, destinationPath, newFileName, unZipParameters)
    }

    public func extractFile(fileHeader: FileHeader, destinationPath: String, parameters: UnzipParameters) {
        this.extractFile(fileHeader, destinationPath, None, parameters)
    }
    public func extractFile(fileHeader: FileHeader, destinationPath: String, newFileName: ?String, parameters: UnzipParameters) {
        extractFile(fileHeader.getFileName().getOrThrow(), destinationPath, newFileName, parameters)
    }
    public func extractFile(fileName: String, destinationPath: String) {
        this.extractFile(fileName, destinationPath, None, unZipParameters)
    }
    public func extractFile(fileName: String, destinationPath: String, newFileName: ?String) {
        this.extractFile(fileName, destinationPath, newFileName, unZipParameters)
    }
    public func extractFile(fileName: String, destinationPath: String, parameters: UnzipParameters) {
        this.extractFile(fileName, destinationPath, None, parameters)
    }
    public func extractFile(fileName: String, destinationPath: String, newFileName: ?String, parameters: UnzipParameters) {
        if (!Zip4cjUtil.isStringNotNullAndNotEmpty(fileName)) {
            throw ZipException("file to extract is None or empty, cannot extract file")
        }
        if (!Zip4cjUtil.isStringNotNullAndNotEmpty(destinationPath)) {
            throw ZipException("destination path is empty or None, cannot extract file")
        }
        this.readZipInfo()
        ExtractFileTask(zipModel, password, parameters, buildAsyncParameters()).execute(
            ExtractFileTaskParameters(destinationPath, fileName, newFileName, buildConfig()))
    }

    public func getFileHeaders(): ArrayList<FileHeader> {
        readZipInfo()
        match(zipModel.getCentralDirectory()) {
            case Some(v) => v.getFileHeaders()
            case None => ArrayList<FileHeader>()
        }
    }

    public func getFileHeader(fileName: String ): ?FileHeader {
        if (!Zip4cjUtil.isStringNotNullAndNotEmpty(fileName)) {
            throw ZipException("file name is empty or None, cannot remove file")
        }
        readZipInfo()
        if (zipModel.getCentralDirectory().isNone()) {
            return None
        }
        return HeaderUtil.getFileHeader(zipModel, fileName)
    }

    public func isEncrypted(): Bool {
        if (isNullzipModel) {
            readZipInfo()
            if (isNullzipModel) {
                throw ZipException("Zip Model is null")
            }
        }
        match (zipModel.getCentralDirectory()) {
            case Some(v) => 
                let fileHeaders = v.getFileHeaders()
                for (i in 0..fileHeaders.size where fileHeaders[i].isEncrypted()) {
                    isEncrypte = true
                    break
                }
            case None => throw ZipException("invalid zip file")
        }
        return isEncrypte
    }

    public func isSplitArchive(): Bool {
        if (isNullzipModel) {
            this.readZipInfo()
            if (isNullzipModel) {
                throw ZipException("Zip Model is None")
            }
        }
        return zipModel.isSplitArchive()
    }

    public func removeFile(fileHeader: FileHeader) {
        removeFile(fileHeader.getFileName().getOrThrow())
    }

    public func removeFile(fileName: String) {
        if (!Zip4cjUtil.isStringNotNullAndNotEmpty(fileName)) {
            throw ZipException("file name is empty or None, cannot remove file")
        }
        removeFiles([fileName])
    }

    public func removeFiles(fileNames: Collection<String>) {
        if (let Some(v) <- (fileNames as ArrayList<String>)) {
            removeFiles(unsafe {v.getRawArray()[0..v.size]})
        } else if (let Some(v) <- (fileNames as Array<String>)) {
            removeFiles(v)
        } else {
            removeFiles(fileNames.toArray())
        }
    }
    public func removeFiles(fileNames: Array<String>) {
        if (fileNames.isEmpty()) {
            return
        }
        if (isNullzipModel) {
            this.readZipInfo()
        }
        if (zipModel.isSplitArchive()) {
            throw ZipException("Zip file format does not allow updating split/spanned files")
        }
        RemoveFilesFromZipTask(zipModel, headerWriter, buildAsyncParameters()).execute(
            RemoveFilesFromZipTaskParameters(fileNames, buildConfig()))
    }

    public func renameFile(fileHeader: FileHeader, newFileName: String) {
        if (let Some(v) <- fileHeader.getFileName()) {
            renameFile(v, newFileName)
        } else {
            throw NoneValueException("The fileName of the FileHeader class is None.")
        }
    }

    public func renameFile(fileNameToRename: String, newFileName: String) {
        if (Zip4cjUtil.isStringNullOrEmpty(fileNameToRename)) {
            throw ZipException("file name to be changed is None or empty")
        }
        if (Zip4cjUtil.isStringNullOrEmpty(newFileName)) {
            throw ZipException("newFileName is None or empty")
        }
        renameFiles(HashMap<String, String>([(fileNameToRename, newFileName)]))
    }

    public func renameFiles(fileNamesMap: Map<String, String>) {
        if (fileNamesMap.size == 0) {
            return
        }
        this.readZipInfo()
        if (zipModel.isSplitArchive()) {
            throw ZipException("Zip file format does not allow updating split/spanned files")
        }
        var asyncTaskParameters: AsyncTaskParameters = buildAsyncParameters()
        RenameFilesTask(zipModel, headerWriter, RawIO(), asyncTaskParameters).execute(
            RenameFilesTaskParameters(fileNamesMap, buildConfig()))
    }

    public func mergeSplitFiles(outputZipFile: Path) {
        if (exists(outputZipFile)) {
            throw ZipException("output Zip File already exists")
        }
        this.readZipInfo()
        if (this.isNullzipModel) {
            throw ZipException("zip model is None, corrupt zip file?")
        }
        MergeSplitZipFileTask(zipModel, buildAsyncParameters()).execute(
            MergeSplitZipFileTaskParameters(outputZipFile, buildConfig()))
    }

    public func setComment(comment: String): Unit {
        if (!exists(this.zipFile)) {
            throw ZipException("zip file does not exist, cannot set comment for zip file")
        }
        readZipInfo()
        SetCommentTask(zipModel, buildAsyncParameters()).execute(
            SetCommentTaskTaskParameters(comment, buildConfig()))
    }

    public func getComment(): String {
        if (!exists(this.zipFile)) {
            throw ZipException("zip file does not exist, cannot read comment")
        }
        readZipInfo()
        return zipModel.getEndOfCentralDirectoryRecord().getComment()
    }


    public func getInputStream(fileHeader: FileHeader): ZipInputStream {
        this.readZipInfo()
        if (isNullzipModel) {
            throw ZipException("zip model is None, cannot get inputstream")
        }
        var zipInputStream = UnzipUtil.createZipInputStream(zipModel, fileHeader, password)
        openInputStreams.add(zipInputStream)
        return zipInputStream
    }

    public func isValidZipFile(): Bool {
        if (!exists(zipFile)) {
            return false
        }
        try {
            this.readZipInfo()
            if (zipModel.isSplitArchive() && !this.verifyAllSplitFilesOfZipExists(this.getSplitZipFiles())) {
                return false
            }
            return true
        } catch (e: Exception) {
            return false
        }
    }
    public func getSplitZipFiles(): ArrayList<Path> {
        this.readZipInfo()
        return FileUtils.getSplitZipFiles(zipModel)
    }

    public func close(): Unit {
        if (isClose) {
            return
        }
        for (i in 0..openInputStreams.size) {
            openInputStreams[i].close()
        }
        this.openInputStreams.clear()
        if (let Some(v) <- this.executorService) {
            v.close()
        } 
        this.isClose = true
    }

    public func isClosed(): Bool {
        return this.isClose
    }

    public func setPassword(password: ?Array<Rune>) {
        this.password = password
    }

    public func getBufferSize() {
        return bufferSize
    }

    public func setBufferSize(bufferSize: Int64) {
        if (bufferSize < InternalZipConstants.MIN_BUFF_SIZE) {
            throw IllegalArgumentException(
                "Buffer size cannot be less than ${InternalZipConstants.MIN_BUFF_SIZE} bytes")
        }
        this.bufferSize = bufferSize
    }

    private func readZipInfo() {
        if (!isNullzipModel) {
            return 
        }
        try (randomAccessFile = initializeRandomAccessFileForHeaderReading(zipFile)) {
            var headerReader = HeaderReader()
            this.zipModelOpt = headerReader.readAllHeaders(randomAccessFile, buildConfig())
            isNullzipModel = false
            zipModel.setZipFile(zipFile)
        } catch (e: ZipException) {
            throw e
        } catch (e: IOException | ZipIOException) {
            throw ZipException(e.message)
        }
    }

    private func createNewZipModel() {
        this.zipModelOpt = ZipModel()
        isNullzipModel = false
        zipModel.setZipFile(zipFile)
    }

    private func buildAsyncParameters(): AsyncTaskParameters {
        if (runInThread) {
            executorService = ExecutorService.new()
        }
        return AsyncTaskParameters(executorService, runInThread, progressMonitor)
    }

    private func verifyAllSplitFilesOfZipExists(allSplitFiles: ArrayList<Path>): Bool {
        for (i in 0..allSplitFiles.size where !exists(allSplitFiles[i])) {
            return false
        }
        return true
    }

    public func getProgressMonitor(): ProgressMonitor {
        return progressMonitor
    }

    public func isRunInThread(): Bool {
        return runInThread
    }

    public func setRunInThread(runInThread: Bool) {
        this.runInThread = runInThread
    }

    public func getFile() {
        return zipFile
    }

    public func getExecutorService(): ?ExecutorService {
        return executorService
    }

    public func toString(): String {
        return zipFile.toString()
    }

    private func buildConfig(): Zip4cjConfig {
        return Zip4cjConfig(bufferSize, useUtf8CharsetForPasswords)
    }

    public func isUseUtf8CharsetForPasswords(): Bool {
        return useUtf8CharsetForPasswords
    }

    public func setUseUtf8CharsetForPasswords(useUtf8CharsetForPasswords: Bool) {
        this.useUtf8CharsetForPasswords = useUtf8CharsetForPasswords
    }
}