package de.arxsilex.maptools.converter.desktop

import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import de.arxsilex.maptools.converter.desktop.generated.resources.Res
import de.arxsilex.maptools.converter.desktop.generated.resources.maptools_logo
import org.jetbrains.compose.resources.painterResource
import java.awt.FileDialog
import java.awt.Frame
import java.awt.BorderLayout
import java.awt.Dimension
import java.io.File
import java.awt.Desktop
import java.nio.file.Path
import java.net.URI
import javax.swing.DefaultListModel
import javax.swing.JList
import javax.swing.JPanel
import javax.swing.JScrollPane
import javax.swing.JTextArea
import javax.swing.ListSelectionModel
import javax.swing.JOptionPane

fun main(arguments: Array<String>) {
    var currentStrings = LocalizationCatalog.strings(AppPreferencesStore.load().language)
    val runtime = try {
        PlatformDirectories.prepareRuntime()
    } catch (exception: Exception) {
        JOptionPane.showMessageDialog(
            null,
            currentStrings["dialog.storage_error"] + "\n\n" +
                (exception.message ?: exception.javaClass.simpleName),
            currentStrings["dialog.storage_error_title"],
            JOptionPane.ERROR_MESSAGE,
        )
        return
    }
    System.setProperty("java.io.tmpdir", runtime.sessionTemporaryDirectory.toString())
    val cleanupHook = Thread(
        { PlatformDirectories.cleanupRuntime(runtime) },
        "maptools-portable-temp-cleanup",
    )
    Runtime.getRuntime().addShutdownHook(cleanupHook)
    try {
        application {
            val appIcon = painterResource(Res.drawable.maptools_logo)
            Window(
                onCloseRequest = ::exitApplication,
                title = "ArxSilex MapTools Converter",
                icon = appIcon,
                state = rememberWindowState(width = 1280.dp, height = 800.dp),
            ) {
                MapToolsConverterApp(
                    initialInputs = arguments.map(::File).filter(File::isFile),
                    onChooseInputs = { chooseInputFiles(window, currentStrings) },
                    onChooseArchive = { chooseArchiveFiles(window, currentStrings) },
                    onChooseOutputBase = { suggestedName -> chooseOutputBase(window, suggestedName, currentStrings) },
                    onOpenContainingDirectory = ::openContainingDirectory,
                    onExportCompleted = { outputs -> notifyExportCompleted(window, outputs, currentStrings) },
                    onOpenExternalLink = { url -> Desktop.getDesktop().browse(URI(url)) },
                    onConfirmOverwrite = { files -> confirmOverwrite(window, files, currentStrings) },
                    onPreferencesChanged = { preferences ->
                        currentStrings = LocalizationCatalog.strings(preferences.language)
                    },
                )
            }
        }
    } finally {
        runCatching { Runtime.getRuntime().removeShutdownHook(cleanupHook) }
        PlatformDirectories.cleanupRuntime(runtime)
    }
}

private fun openContainingDirectory(path: Path) {
    Desktop.getDesktop().open(path.toAbsolutePath().parent.toFile())
}

private fun notifyExportCompleted(owner: Frame, outputs: List<Path>, strings: AppStrings) {
    val outputDirectory = outputs.firstOrNull()?.toAbsolutePath()?.parent
    val openFailure = runCatching {
        requireNotNull(outputDirectory) { strings["dialog.no_output_folder"] }
        Desktop.getDesktop().open(outputDirectory.toFile())
    }.exceptionOrNull()
    val outputNames = outputs.joinToString(separator = "\n") { "• ${it.fileName}" }
    val message = buildString {
        append(strings["dialog.export_completed"])
        if (outputNames.isNotEmpty()) append("\n\n$outputNames")
        if (outputDirectory != null) append("\n\n${strings["export.output_folder"]}:\n$outputDirectory")
        if (openFailure != null) {
            append("\n\n${strings["dialog.open_folder_failed"]}: ")
            append(openFailure.message ?: strings["dialog.unknown_error"])
        }
    }
    JOptionPane.showMessageDialog(
        owner,
        message,
        strings["dialog.export_success_title"],
        JOptionPane.INFORMATION_MESSAGE,
    )
}

private fun confirmOverwrite(owner: Frame, files: List<Path>, strings: AppStrings): Boolean {
    val names = files.joinToString(separator = "\n") { "• ${it.fileName}" }
    return JOptionPane.showConfirmDialog(
        owner,
        strings("dialog.overwrite_message", names),
        strings["dialog.overwrite_title"],
        JOptionPane.YES_NO_OPTION,
        JOptionPane.WARNING_MESSAGE,
    ) == JOptionPane.YES_OPTION
}

private fun chooseOutputBase(owner: Frame, suggestedName: String, strings: AppStrings): File? {
    val dialog = FileDialog(owner, strings["dialog.save_output"], FileDialog.SAVE).apply {
        file = suggestedName
        isVisible = true
    }
    val directory = dialog.directory ?: return null
    val fileName = dialog.file ?: return null
    return File(directory, fileName)
}

private fun chooseInputFiles(owner: Frame, strings: AppStrings): List<File> {
    val dialog = FileDialog(owner, strings["dialog.choose_input"], FileDialog.LOAD).apply {
        isMultipleMode = true
        setFilenameFilter { _, name ->
            val normalizedName = name.lowercase()
            de.arxsilex.maptools.converter.core.InputFileInspector.detectFormat(normalizedName)?.let { format ->
                format.isMapData || format.isPoiData || format.isElevationData
            } == true
        }
        isVisible = true
    }

    return dialog.files.filter(File::isFile)
}

private fun chooseArchiveFiles(owner: Frame, strings: AppStrings): List<File> {
    val dialog = FileDialog(owner, strings["dialog.choose_archive"], FileDialog.LOAD).apply {
        isMultipleMode = true
        setFilenameFilter { _, name -> name.endsWith(".zip", ignoreCase = true) }
        isVisible = true
    }
    val extractionRoot = PlatformDirectories.sessionTemporaryDirectory().resolve("archive-imports")
    return dialog.files.filter(File::isFile).flatMap { archiveFile ->
        try {
            val inspection = ZipArchiveImporter.inspect(archiveFile.toPath())
            val selectedOrdinals = chooseArchiveEntries(owner, inspection, strings)
            if (selectedOrdinals.isEmpty()) {
                emptyList()
            } else {
                ZipArchiveImporter.extractSelected(
                    inspection = inspection,
                    selectedOrdinals = selectedOrdinals,
                    destinationRoot = extractionRoot,
                ).map(Path::toFile)
            }
        } catch (exception: Exception) {
            JOptionPane.showMessageDialog(
                owner,
                strings("archive.error", archiveFile.name, exception.message ?: exception.javaClass.simpleName),
                strings["archive.error_title"],
                JOptionPane.ERROR_MESSAGE,
            )
            emptyList()
        }
    }
}

private fun chooseArchiveEntries(
    owner: Frame,
    inspection: ZipArchiveInspection,
    strings: AppStrings,
): Set<Int> {
    if (inspection.entries.size == 1) return setOf(inspection.entries.single().ordinal)
    val model = DefaultListModel<String>()
    inspection.entries.forEach { entry ->
        model.addElement("${entry.archivePath}  ·  ${entry.format.displayName}  ·  ${formatFileSize(entry.uncompressedBytes)}")
    }
    val list = JList(model).apply {
        selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
        visibleRowCount = inspection.entries.size.coerceIn(5, 14)
        if (model.size > 0) setSelectionInterval(0, model.size - 1)
    }
    val summary = buildString {
        append(strings("archive.summary", inspection.archive.fileName, inspection.entries.size))
        if (inspection.ignoredEntryCount > 0) {
            append("\n")
            append(strings("archive.ignored", inspection.ignoredEntryCount))
        }
        if (inspection.nestedArchiveCount > 0) {
            append("\n")
            append(strings("archive.nested", inspection.nestedArchiveCount))
        }
        append("\n")
        append(strings["archive.select_hint"])
    }
    val text = JTextArea(summary).apply {
        isEditable = false
        isOpaque = false
        lineWrap = true
        wrapStyleWord = true
        border = null
    }
    val panel = JPanel(BorderLayout(0, 8)).apply {
        preferredSize = Dimension(760, 420)
        add(text, BorderLayout.NORTH)
        add(JScrollPane(list), BorderLayout.CENTER)
    }
    val decision = JOptionPane.showConfirmDialog(
        owner,
        panel,
        strings["archive.select_title"],
        JOptionPane.OK_CANCEL_OPTION,
        JOptionPane.PLAIN_MESSAGE,
    )
    if (decision != JOptionPane.OK_OPTION) return emptySet()
    return list.selectedIndices.mapTo(mutableSetOf()) { index -> inspection.entries[index].ordinal }
}
