package de.arxsilex.maptools.converter.desktop

import kotlin.math.roundToLong

internal enum class ExportProgressKind {
    SOURCE,
    PREPARE,
    MAP,
    POI,
    ELEVATION,
    VALIDATE,
}

internal data class ExportProgressStage(
    val key: String,
    val kind: ExportProgressKind,
    val weight: Double,
)

internal data class ExportProgressSnapshot(
    val message: String,
    val progress: Float,
    val phase: String?,
)

internal class ExportProgressPlan private constructor(
    val stages: List<ExportProgressStage>,
) {
    init {
        require(stages.isNotEmpty()) { "Der Exportfortschrittsplan darf nicht leer sein." }
        require(stages.map(ExportProgressStage::key).distinct().size == stages.size) {
            "Fortschrittsstufen benötigen eindeutige Schlüssel."
        }
    }

    companion object {
        fun create(
            mapsforgeSourceCount: Int,
            preparesMapData: Boolean,
            writesMap: Boolean,
            writesPoi: Boolean,
            writesElevation: Boolean = false,
        ): ExportProgressPlan {
            require(mapsforgeSourceCount >= 0)
            val raw = buildList {
                if (mapsforgeSourceCount > 0) {
                    repeat(mapsforgeSourceCount) { index ->
                        add(ExportProgressStage("source-$index", ExportProgressKind.SOURCE, 15.0 / mapsforgeSourceCount))
                    }
                }
                if (preparesMapData) add(ExportProgressStage("prepare", ExportProgressKind.PREPARE, 25.0))
                if (writesMap) add(ExportProgressStage("map", ExportProgressKind.MAP, 45.0))
                if (writesPoi) add(ExportProgressStage("poi", ExportProgressKind.POI, 10.0))
                if (writesElevation) add(ExportProgressStage("elevation", ExportProgressKind.ELEVATION, 30.0))
                add(ExportProgressStage("validate", ExportProgressKind.VALIDATE, 5.0))
            }
            val total = raw.sumOf(ExportProgressStage::weight)
            return ExportProgressPlan(raw.map { it.copy(weight = it.weight / total) })
        }
    }
}

/**
 * Combines the independently reported worker phases into one monotonic job progress value.
 * Mapsforge reports percentages per zoom interval; treating those values as job percentages caused
 * the old progress bar to jump backwards or remain at e.g. 30% for several minutes.
 */
internal class ExportProgressTracker(
    private val plan: ExportProgressPlan,
    private val strings: AppStrings,
    private val nanoTime: () -> Long = System::nanoTime,
    private val onUpdate: (ExportProgressSnapshot) -> Unit,
) {
    private val startedAt = nanoTime()
    private val progressByKey = plan.stages.associate { it.key to 0.0 }.toMutableMap()
    private var lastOverallProgress = 0.0

    fun listener(defaultStageKey: String): (String) -> Unit {
        require(plan.stages.any { it.key == defaultStageKey }) { "Unbekannte Exportstufe: $defaultStageKey" }
        var activeStageKey = defaultStageKey
        return { line ->
            val update = WorkerProgressFormatter.progressOrNull(line, strings)
            val workerPhase = update?.phase
            activeStageKey = when (workerPhase) {
                "mapsforge-map" -> keyFor(ExportProgressKind.MAP) ?: activeStageKey
                "mapsforge-poi", "mapsforge-poi-schema-v3", "poi-convert", "poi-filter", "poi-merge" ->
                    keyFor(ExportProgressKind.POI) ?: activeStageKey
                "elevation" -> keyFor(ExportProgressKind.ELEVATION) ?: activeStageKey
                "crop", "combine-crop", "combine-merge", "mtci-index", "mtci-build", "mtci-select",
                "mtci-materialize", "mtci-pbf", "mtci-materialize-nodes", "mtci-materialize-ways",
                "mtci-materialize-relations", "mtci-pbf-nodes", "mtci-pbf-ways", "mtci-pbf-relations" ->
                    keyFor(ExportProgressKind.PREPARE) ?: activeStageKey
                else -> activeStageKey
            }
            if (update != null || progressHint(activeStageKey, line, update) != null) {
                update(activeStageKey, line, update)
            }
        }
    }

    @Synchronized
    fun start(stageKey: String, message: String) {
        require(progressByKey.containsKey(stageKey)) { "Unbekannte Exportstufe: $stageKey" }
        emit(message, stageKey)
    }

    @Synchronized
    fun complete(stageKey: String, message: String? = null) {
        progressByKey[stageKey] = 1.0
        emit(message ?: strings["progress.completed"], stageKey)
    }

    @Synchronized
    fun finish(message: String = strings["progress.completed"]) {
        progressByKey.keys.forEach { progressByKey[it] = 1.0 }
        lastOverallProgress = 1.0
        emit(message, null)
    }

    @Synchronized
    private fun update(stageKey: String, line: String, workerUpdate: WorkerProgressUpdate?) {
        val stage = plan.stages.first { it.key == stageKey }
        val completed = line.contains(" status=completed") || line.contains(" status=cached")
        val hint = if (completed) 1.0 else progressHint(stageKey, line, workerUpdate)
        if (hint != null) {
            progressByKey[stageKey] = maxOf(progressByKey.getValue(stageKey), hint.coerceIn(0.0, 1.0))
        }
        val message = detailedMessage(stage.kind, line, workerUpdate) ?: workerUpdate?.message ?: return
        emit(message, workerUpdate?.phase ?: stage.kind.name.lowercase())
    }

    private fun progressHint(
        stageKey: String,
        rawLine: String,
        workerUpdate: WorkerProgressUpdate?,
    ): Double? {
        val stage = plan.stages.first { it.key == stageKey }
        val line = rawLine.replace(ANSI_ESCAPE, "")
        val progress = workerUpdate?.progress?.toDouble()
        return when (stage.kind) {
            ExportProgressKind.SOURCE -> when {
                "writePhase=NODES" in line -> 0.75 + 0.13 * (progress ?: 0.0)
                "writePhase=WAYS" in line -> 0.88 + 0.10 * (progress ?: 0.0)
                "writePhase=RELATIONS" in line -> 0.98 + 0.019 * (progress ?: 0.0)
                progress != null -> 0.75 * progress
                else -> null
            }
            ExportProgressKind.PREPARE -> when (workerUpdate?.phase) {
                "combine-crop" -> progress?.let { 0.05 * it }
                "mtci-index" -> progress?.let { 0.05 + 0.10 * it }
                "mtci-build" -> progress?.let { 0.15 + 0.20 * it }
                "mtci-select" -> progress?.let { 0.35 + 0.25 * it }
                "mtci-materialize-nodes" -> progress?.let { 0.60 + 0.08 * it }
                "mtci-materialize-ways" -> progress?.let { 0.68 + 0.07 * it }
                "mtci-materialize-relations" -> progress?.let { 0.75 + 0.03 * it }
                "mtci-pbf-nodes" -> progress?.let { 0.78 + 0.04 * it }
                "mtci-pbf-ways" -> progress?.let { 0.82 + 0.05 * it }
                "mtci-pbf-relations" -> progress?.let { 0.87 + 0.01 * it }
                "combine-merge" -> if ("status=completed" in line) 1.0 else 0.90
                "crop" -> if ("status=completed" in line) 1.0 else progress
                else -> progress
            }
            ExportProgressKind.MAP -> mapProgressHint(line, progress)
            ExportProgressKind.POI -> poiProgressHint(line, progress)
            ExportProgressKind.ELEVATION -> progress
            ExportProgressKind.VALIDATE -> progress
        }
    }

    private fun mapProgressHint(line: String, quantitativeProgress: Double?): Double? {
        SUBFILE_PROGRESS.find(line)?.let { match ->
            val percent = match.groupValues[1].toInt().coerceIn(0, 100) / 100.0
            val interval = match.groupValues[2].toInt().coerceIn(1, 3)
            return 0.60 + 0.40 * (((interval - 1) + percent) / 3.0)
        }
        return when {
            "start reading data" in line -> 0.02
            ADD_NODES.containsMatchIn(line) -> null
            ADD_WAYS.containsMatchIn(line) -> 0.12
            "completing read" in line -> 0.20
            "handle coastlines" in line -> 0.24
            "handle relations" in line -> 0.28
            line.startsWith("Progress: Relations") && quantitativeProgress != null ->
                0.28 + 0.10 * quantitativeProgress
            "handle ways" in line -> 0.40
            "start writing file" in line -> 0.60
            "Finished writing file" in line -> 0.995
            else -> null
        }
    }

    private fun poiProgressHint(line: String, quantitativeProgress: Double?): Double? = when {
        "Processing nodes" in line -> 0.10
        line.startsWith("Progress: Nodes") -> 0.10
        "Processing ways" in line -> 0.45
        line.startsWith("Progress: Ways") -> maxOf(0.45, quantitativeProgress ?: 0.45)
        "Committing" in line -> 0.75
        "Filtering categories" in line -> 0.82
        "Writing metadata" in line -> 0.90
        "Post-processing" in line -> 0.95
        "Added " in line && " POIs" in line -> 0.99
        else -> quantitativeProgress
    }

    private fun detailedMessage(
        kind: ExportProgressKind,
        rawLine: String,
        workerUpdate: WorkerProgressUpdate?,
    ): String? {
        val line = rawLine.replace(ANSI_ESCAPE, "")
        val countMatch = ADD_OBJECTS.find(line)
        if (countMatch != null) {
            val type = if (countMatch.groupValues[1].equals("nodes", ignoreCase = true)) {
                strings["progress.nodes"]
            } else {
                strings["progress.ways"]
            }
            return strings("progress.processing", type) + " · " + formatCount(countMatch.groupValues[2])
        }
        if (kind == ExportProgressKind.VALIDATE) return strings["phase.validation"]
        return workerUpdate?.message
    }

    @Synchronized
    private fun emit(message: String, phase: String?) {
        val weighted = plan.stages.sumOf { stage -> stage.weight * progressByKey.getValue(stage.key) }
        lastOverallProgress = maxOf(lastOverallProgress, weighted).coerceIn(0.0, 1.0)
        val elapsedSeconds = ((nanoTime() - startedAt).coerceAtLeast(0L) / 1_000_000_000.0).roundToLong()
        val remainingSeconds = if (lastOverallProgress in 0.05..0.97 && elapsedSeconds >= 10L) {
            (elapsedSeconds * (1.0 - lastOverallProgress) / lastOverallProgress).roundToLong()
        } else null
        val timing = if (remainingSeconds != null) {
            strings("progress.overall_with_remaining", formatDuration(elapsedSeconds), formatDuration(remainingSeconds))
        } else {
            strings("progress.overall_elapsed", formatDuration(elapsedSeconds))
        }
        onUpdate(
            ExportProgressSnapshot(
                message = "$message\n$timing",
                progress = lastOverallProgress.toFloat(),
                phase = phase,
            ),
        )
    }

    private fun keyFor(kind: ExportProgressKind): String? = plan.stages.firstOrNull { it.kind == kind }?.key

    private fun formatDuration(seconds: Long): String = if (seconds >= 3600) {
        "%d:%02d:%02d".format(seconds / 3600, (seconds / 60) % 60, seconds % 60)
    } else {
        "%02d:%02d".format(seconds / 60, seconds % 60)
    }

    private fun formatCount(raw: String): String = raw.replace(",", "").replace(".", "").toLongOrNull()
        ?.let { String.format(strings.language.locale, "%,d", it) }
        ?: raw

    private companion object {
        val ANSI_ESCAPE = Regex("\u001B\\[[0-9;]*[A-Za-z]")
        val ADD_NODES = Regex("Add nodes:", RegexOption.IGNORE_CASE)
        val ADD_WAYS = Regex("Add ways:", RegexOption.IGNORE_CASE)
        val ADD_OBJECTS = Regex("Add (nodes|ways):\\s*([0-9.,]+)", RegexOption.IGNORE_CASE)
        val SUBFILE_PROGRESS = Regex(
            "written\\s+([0-9]+)%.*zoom interval\\s+([0-9]+)",
            RegexOption.IGNORE_CASE,
        )
    }
}
