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

public class RemoveFilesFromZipTask <: AbstractModifyFileTask<RemoveFilesFromZipTaskParameters> {
    private let zipModel: ZipModel
    private let headerWriter: HeaderWriter

    public RemoveFilesFromZipTask(
        zipModel: ZipModel,
        headerWriter: HeaderWriter,
        asyncTaskParameters: AsyncTaskParameters
    ) {
        super(asyncTaskParameters)
        this.zipModel = zipModel
        this.headerWriter = headerWriter
    }

    protected func executeTask(taskParameters: RemoveFilesFromZipTaskParameters, progressMonitor: ProgressMonitor): Unit {
        if (zipModel.isSplitArchive()) {
            throw ZipException("This is a split archive. Zip file format does not allow updating split/spanned files")
        }

        var entriesToRemove: ArrayList<String> = filterNonExistingEntries(taskParameters.filesToRemove)

        if (entriesToRemove.isEmpty()) {
            return
        }

        var temporaryZipFile: Path = getTemporaryFile(zipModel.getZipFile().toString())
        var successFlag: Bool = false

        let outputStream: SplitOutputStream = SplitOutputStream(temporaryZipFile)
        let inputStream: RandomAccessFile = RandomAccessFile(zipModel.getZipFile().toString(), OpenMode.Read)
        try {
            var currentFileCopyPointer: Int64 = 0
            var sortedFileHeaders: Array<FileHeader> = cloneAndSortFileHeadersByOffset(
                zipModel.getCentralDirectory().getOrThrow().getFileHeaders())

            for (i in 0..sortedFileHeaders.size) {
                let fileHeader: FileHeader = sortedFileHeaders[i]
                var lengthOfCurrentEntry: Int64 = getOffsetOfNextEntry(sortedFileHeaders, fileHeader, zipModel) -
                    outputStream.getFilePointer()
                if (shouldEntryBeRemoved(fileHeader, entriesToRemove)) {
                    updateHeaders(sortedFileHeaders, fileHeader, lengthOfCurrentEntry)

                    ListRmove(zipModel.getCentralDirectory().getOrThrow().getFileHeaders(), fileHeader)
                    currentFileCopyPointer = currentFileCopyPointer + lengthOfCurrentEntry
                } else {
                    // copy complete entry without any changes
                    currentFileCopyPointer = currentFileCopyPointer + super.copyFile(inputStream, outputStream, currentFileCopyPointer,
                        lengthOfCurrentEntry, progressMonitor, taskParameters.zip4cjConfig.getBufferSize())
                }
                this.verifyIfTaskIsCancelled()
            }
            headerWriter.finalizeZipFile(zipModel, outputStream)
            successFlag = true
        } finally {
            this.cleanupFile(successFlag, zipModel.getZipFile(), temporaryZipFile)
            inputStream.close()
            outputStream.close()
        }
    }

    protected func calculateTotalWork(taskParameters: RemoveFilesFromZipTaskParameters): Int64 {
        return FileInfo(zipModel.getZipFile()).size
    }

    private func filterNonExistingEntries(filesToRemove: Array<String>): ArrayList<String> {
        var filteredFilesToRemove = ArrayList<String>()
        for (i in 0..filesToRemove.size) {
            let fileToRemove = filesToRemove[i]
            if (HeaderUtil.getFileHeader(zipModel, fileToRemove).isSome()) {
                filteredFilesToRemove.add(fileToRemove)
            }
        }

        return filteredFilesToRemove
    }

    private func shouldEntryBeRemoved(fileHeaderToBeChecked: FileHeader, fileNamesToBeRemoved: ArrayList<String>): Bool {
        for (i in 0..fileNamesToBeRemoved.size) {
            let fileNameToBeRemoved = fileNamesToBeRemoved[i]
            // If any of the files to be removed is a directory, check if the fileHeaderToBeChecked is a sub-file or
            // a sub-directory of that directory
            if (fileNameToBeRemoved.endsWith(InternalZipConstants.ZIP_FILE_SEPARATOR) &&
                fileHeaderToBeChecked.getFileName().getOrThrow().startsWith(fileNameToBeRemoved)) {
                return true
            } else if (fileHeaderToBeChecked.getFileName().getOrThrow() == (fileNameToBeRemoved)) {
                return true
            }
        }

        return false
    }

    private func updateHeaders(
        sortedFileHeaders: Array<FileHeader>,
        fileHeaderThatWasRemoved: FileHeader,
        offsetToSubtract: Int64
    ) {
        updateOffsetsForAllSubsequentFileHeaders(sortedFileHeaders, zipModel, fileHeaderThatWasRemoved,
            negate(offsetToSubtract))

        var end: EndOfCentralDirectoryRecord = zipModel.getEndOfCentralDirectoryRecord()
        end.setOffsetOfStartOfCentralDirectory(
            end.getOffsetOfStartOfCentralDirectory() - offsetToSubtract)
        end.setTotalNumberOfEntriesInCentralDirectory(
            end.getTotalNumberOfEntriesInCentralDirectory() - 1)

        if (end.getTotalNumberOfEntriesInCentralDirectoryOnThisDisk() > 0) {
            end.setTotalNumberOfEntriesInCentralDirectoryOnThisDisk(
                end.getTotalNumberOfEntriesInCentralDirectoryOnThisDisk() - 1)
        }

        if (zipModel.isZip64Format()) {
            zipModel.getZip64EndOfCentralDirectoryRecord().setOffsetStartCentralDirectoryWRTStartDiskNumber(
                zipModel.getZip64EndOfCentralDirectoryRecord().getOffsetStartCentralDirectoryWRTStartDiskNumber() -
                offsetToSubtract)

            zipModel.getZip64EndOfCentralDirectoryRecord().setTotalNumberOfEntriesInCentralDirectoryOnThisDisk(
                zipModel.getZip64EndOfCentralDirectoryRecord().getTotalNumberOfEntriesInCentralDirectory() - 1)

            zipModel.getZip64EndOfCentralDirectoryLocator().getOrThrow().setOffsetZip64EndOfCentralDirectoryRecord(
                zipModel.getZip64EndOfCentralDirectoryLocator().getOrThrow().getOffsetZip64EndOfCentralDirectoryRecord() -
                offsetToSubtract)
        }
    }

    private func negate(val: Int64): Int64 {
        if (val == -9223372036854775808) {
            throw ArithmeticException("long overflow")
        }

        return -val
    }

    protected func getTask(): ProgressMonitorTask {
        return ProgressMonitorTask.REMOVE_ENTRY
    }
}