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

public class RenameFilesTask <: AbstractModifyFileTask<RenameFilesTaskParameters> {
    private let zipModel: ZipModel
    private let headerWriter: HeaderWriter
    private let rawIO: RawIO

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

    protected func executeTask(taskParameters: RenameFilesTaskParameters, progressMonitor: ProgressMonitor): Unit {
        var fileNamesMap = filterNonExistingEntriesAndAddSeparatorIfNeeded(taskParameters.fileNamesMap)
        if (fileNamesMap.size == 0) {
            return
        }

        let temporaryFile = getTemporaryFile(zipModel.getZipFile().toString())
        var successFlag = false
        let inputStream = RandomAccessFile(zipModel.getZipFile(), OpenMode.ReadWrite)
        let outputStream = SplitOutputStream(temporaryFile)
        try {
            var currentFileCopyPointer = 0
            // Maintain a different list to iterate, so that when the file name is changed in the central directory
            // we still have access to the original file names. If iterating on the original list from central directory,
            // it might be that a file name has changed because of other file name, ex: if a directory name has to be changed
            // and the file is part of that directory, by the time the file has to be changed, its name might have changed
            // when changing the name of the directory. There is some overhead with this approach, but is safer.
            var sortedFileHeaders: Array<FileHeader> = cloneAndSortFileHeadersByOffset(zipModel.getCentralDirectory().getOrThrow().getFileHeaders())

            for (i in 0..sortedFileHeaders.size) {
                let fileHeader = sortedFileHeaders[i]
                var fileNameMapForThisEntry: Option<(String, String)> = getCorrespondingEntryFromMap(
                    fileHeader,
                    fileNamesMap
                )
                progressMonitor.setFileName(fileHeader.getFileName().getOrThrow())

                var lengthToCopy = getOffsetOfNextEntry(sortedFileHeaders, fileHeader, zipModel) -
                    outputStream.getFilePointer()
                if (let Some(entry) <- fileNameMapForThisEntry) {
                    var newFileName: String = getNewFileName(entry[1], entry[0], fileHeader.getFileName().getOrThrow())
                    var newFileNameBytes = HeaderUtil.getBytesFromString(newFileName)
                    var headersOffset = newFileNameBytes.size - fileHeader.getFileNameLength()

                    currentFileCopyPointer = copyEntryAndChangeFileName(newFileNameBytes, fileHeader,
                        currentFileCopyPointer, lengthToCopy, inputStream, outputStream, progressMonitor,
                        taskParameters.zip4cjConfig.getBufferSize())

                    updateHeadersInZipModel(sortedFileHeaders, fileHeader, newFileName, newFileNameBytes, headersOffset)
                } else {
                    // copy complete entry without any changes
                    currentFileCopyPointer += copyFile(inputStream, outputStream, currentFileCopyPointer, lengthToCopy,
                        progressMonitor, taskParameters.zip4cjConfig.getBufferSize())
                }

                verifyIfTaskIsCancelled()
            }

            headerWriter.finalizeZipFile(zipModel, outputStream)
            successFlag = true
        } finally {
            cleanupFile(successFlag, zipModel.getZipFile(), temporaryFile)
            outputStream.close()
            inputStream.close()
        }
    }

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

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

    private func copyEntryAndChangeFileName(
        newFileNameBytes: Array<Byte>,
        fileHeader: FileHeader,
        start: Int64,
        totalLengthOfEntry: Int64,
        inputStream: RandomAccessFile,
        outputStream: OutputStream,
        progressMonitor: ProgressMonitor,
        bufferSize: Int64
    ): Int64 {
        var currentFileCopyPointer = start

        currentFileCopyPointer += copyFile(inputStream, outputStream, currentFileCopyPointer, 26, progressMonitor,
            bufferSize) // 26 is offset until file name length

        rawIO.writeShortLittleEndian(outputStream, Int32(newFileNameBytes.size))

        currentFileCopyPointer += 2 // length of file name length
        currentFileCopyPointer += copyFile(inputStream, outputStream, currentFileCopyPointer, 2, progressMonitor,
            bufferSize) // 2 is for length of extra field length

        outputStream.write(newFileNameBytes)
        currentFileCopyPointer += fileHeader.getFileNameLength()

        var remainingLengthToCopy = totalLengthOfEntry - (currentFileCopyPointer - start)

        currentFileCopyPointer += copyFile(inputStream, outputStream, currentFileCopyPointer, remainingLengthToCopy,
            progressMonitor, bufferSize)

        return currentFileCopyPointer
    }

    private func getCorrespondingEntryFromMap(fileHeaderToBeChecked: FileHeader, fileNamesMap: Map<String, String>): ?(String, String) {
        for (entry in fileNamesMap) {
            if (fileHeaderToBeChecked.getFileName().getOrThrow().startsWith(entry[0])) {
                return entry
            }
        }
        return None
    }

    private func updateHeadersInZipModel(
        sortedFileHeaders: Array<FileHeader>,
        fileHeader: FileHeader,
        newFileName: String,
        newFileNameBytes: Array<Byte>,
        headersOffset: Int64
    ) {
        var fileHeaderToBeChangedOption: ?FileHeader = HeaderUtil.getFileHeader(
            zipModel,
            fileHeader.getFileName().getOrThrow()
        )

        if (fileHeaderToBeChangedOption.isNone()) {
            // If this is the case, then the file name in the header that was passed to this method was already changed.
            // In theory, should never be here.
            throw ZipException("could not find any header with name: ${fileHeader.getFileName()}")
        }
        let fileHeaderToBeChanged = fileHeaderToBeChangedOption.getOrThrow()
        fileHeaderToBeChanged.setFileName(newFileName)
        fileHeaderToBeChanged.setFileNameLength(newFileNameBytes.size)

        updateOffsetsForAllSubsequentFileHeaders(sortedFileHeaders, zipModel, fileHeaderToBeChanged, headersOffset)

        zipModel.getEndOfCentralDirectoryRecord().setOffsetOfStartOfCentralDirectory(
            zipModel.getEndOfCentralDirectoryRecord().getOffsetOfStartOfCentralDirectory() + headersOffset)

        if (zipModel.isZip64Format()) {
            zipModel.getZip64EndOfCentralDirectoryRecord().setOffsetStartCentralDirectoryWRTStartDiskNumber(
                zipModel.getZip64EndOfCentralDirectoryRecord().getOffsetStartCentralDirectoryWRTStartDiskNumber() +
                headersOffset)

            zipModel.getZip64EndOfCentralDirectoryLocator().getOrThrow().setOffsetZip64EndOfCentralDirectoryRecord(
                zipModel.getZip64EndOfCentralDirectoryLocator().getOrThrow().getOffsetZip64EndOfCentralDirectoryRecord() +
                headersOffset)
        }
    }

    private func filterNonExistingEntriesAndAddSeparatorIfNeeded(inputFileNamesMap: Map<String, String>): Map<String,
        String> {
        let fileNamesMapToBeChanged = HashMap<String, String>()
        for ((key, value) in inputFileNamesMap) {
            if (!Zip4cjUtil.isStringNotNullAndNotEmpty(key)) {
                continue
            }

            if (let Some(fileHeaderToBeChanged) <- HeaderUtil.getFileHeader(zipModel, key)) {
                if (fileHeaderToBeChanged.isDirectory() && !value.endsWith(InternalZipConstants.ZIP_FILE_SEPARATOR)) {
                    fileNamesMapToBeChanged.add(key, "${value}${InternalZipConstants.ZIP_FILE_SEPARATOR}")
                } else {
                    fileNamesMapToBeChanged.add(key, value)
                }
            }
        }
        return fileNamesMapToBeChanged
    }

    private func getNewFileName(newFileName: String, oldFileName: String, fileNameFromHeaderToBeChanged: String): String {
        if (fileNameFromHeaderToBeChanged == (oldFileName)) {
            return newFileName
        } else if (fileNameFromHeaderToBeChanged.startsWith(oldFileName)) {
            var fileNameWithoutOldName = String(
                fileNameFromHeaderToBeChanged.toRuneArray()[0..oldFileName.toRuneArray().size])
            return newFileName + fileNameWithoutOldName
        }

        // Should never be here.
        // If here by any chance, it means that the file header was marked as to-be-modified, even when the file names do not
        // match. Logic in the method getCorrespondingEntryFromMap() has to be checked
        throw ZipException("old file name was neither an exact match nor a partial match")
    }
}