package de.arxsilex.maptools.converter.core.mtci

import de.arxsilex.maptools.converter.core.GeoBounds
import de.arxsilex.maptools.converter.core.GeoRectangleRegion
import de.arxsilex.maptools.converter.core.GeoSelectionRegion
import de.arxsilex.maptools.converter.core.unionOrNull
import de.arxsilex.maptools.converter.core.pbf.OsmPreviewRelation
import de.arxsilex.maptools.converter.core.pbf.OsmRelationMemberType
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path

enum class MtciSpatialPhase {
    NODES,
    WAYS,
    RELATIONS,
    RELATION_DEPENDENCIES,
    WAY_DEPENDENCIES,
}

data class MtciSpatialProgress(
    val phase: MtciSpatialPhase,
    val visitedObjects: Long,
    val selectedObjects: Long,
)

data class MtciSpatialSelection(
    val bounds: GeoBounds,
    val nodesInsideBounds: Set<Long>,
    val requiredNodeIds: Set<Long>,
    val selectedWayIds: Set<Long>,
    val selectedRelationIds: Set<Long>,
    val visitedNodes: Long,
    val visitedWays: Long,
    val visitedRelations: Long,
    val relationClosurePasses: Int,
    val elapsedMillis: Long,
) {
    fun summaryJson(): String = """
        {
          "bounds": {"west": ${bounds.west}, "south": ${bounds.south}, "east": ${bounds.east}, "north": ${bounds.north}},
          "visitedNodes": $visitedNodes,
          "visitedWays": $visitedWays,
          "visitedRelations": $visitedRelations,
          "nodesInsideBounds": ${nodesInsideBounds.size},
          "requiredNodeIds": ${requiredNodeIds.size},
          "selectedWayIds": ${selectedWayIds.size},
          "selectedRelationIds": ${selectedRelationIds.size},
          "relationClosurePasses": $relationClosurePasses,
          "elapsedMillis": $elapsedMillis
        }
    """.trimIndent() + "\n"
}

class MtciSpatialSelectionCancelledException : IOException("Die räumliche MTCI-Auswahl wurde abgebrochen.")

object MtciSpatialSelector {
    fun analyze(
        directory: Path,
        bounds: GeoBounds,
        onProgress: (MtciSpatialProgress) -> Unit = {},
        isCancelled: () -> Boolean = { false },
    ): MtciSpatialSelection = analyze(directory, listOf(GeoRectangleRegion(bounds)), onProgress, isCancelled)

    fun analyze(
        directory: Path,
        areas: Collection<GeoBounds>,
        onProgress: (MtciSpatialProgress) -> Unit = {},
        isCancelled: () -> Boolean = { false },
    ): MtciSpatialSelection = analyze(
        directory,
        areas.map(::GeoRectangleRegion),
        onProgress,
        isCancelled,
    )

    fun analyze(
        directory: Path,
        areas: List<GeoSelectionRegion>,
        onProgress: (MtciSpatialProgress) -> Unit = {},
        isCancelled: () -> Boolean = { false },
    ): MtciSpatialSelection {
        require(areas.isNotEmpty()) { "Mindestens ein räumlicher Auswahlbereich ist erforderlich." }
        val bounds = requireNotNull(areas.map { it.bounds }.unionOrNull())
        val startedAt = System.nanoTime()
        val nodesInside = HashSet<Long>()
        val requiredNodes = HashSet<Long>()
        val selectedWays = HashSet<Long>()
        val primaryWays = HashSet<Long>()
        val relations = ArrayList<OsmPreviewRelation>()
        var visitedNodes = 0L
        var visitedWays = 0L
        var visitedRelations = 0L

        MtciReader.forEachNode(directory) { node ->
            visitedNodes++
            if (areas.any { area -> area.contains(node.latitude, node.longitude) }) {
                nodesInside += node.id
                requiredNodes += node.id
            }
            if (visitedNodes % 1_000_000L == 0L) {
                checkCancellation(isCancelled)
                onProgress(MtciSpatialProgress(MtciSpatialPhase.NODES, visitedNodes, nodesInside.size.toLong()))
            }
        }

        MtciReader.forEachWay(directory) { way ->
            visitedWays++
            if (way.nodeIds.any(nodesInside::contains)) {
                selectedWays += way.id
                primaryWays += way.id
                way.nodeIds.forEach(requiredNodes::add)
            }
            if (visitedWays % 250_000L == 0L) {
                checkCancellation(isCancelled)
                onProgress(MtciSpatialProgress(MtciSpatialPhase.WAYS, visitedWays, selectedWays.size.toLong()))
            }
        }

        MtciReader.forEachRelation(directory) { relation ->
            visitedRelations++
            relations += relation
            if (visitedRelations % 10_000L == 0L) {
                checkCancellation(isCancelled)
                onProgress(MtciSpatialProgress(MtciSpatialPhase.RELATIONS, visitedRelations, 0))
            }
        }

        val selectedRelations = HashSet<Long>()
        val availableRelationIds = relations.mapTo(HashSet(relations.size)) { it.id }
        val expandedRelations = HashSet<Long>()
        var closurePasses = 0
        var changed: Boolean
        do {
            checkCancellation(isCancelled)
            changed = false
            closurePasses++
            relations.forEach { relation ->
                if (relation.id !in selectedRelations && relation.members.any { member ->
                        when (member.type) {
                            OsmRelationMemberType.NODE -> member.id in nodesInside
                            // Ways, die nur zur Vervollständigung einer bereits gewählten Relation
                            // hinzukamen, dürfen keine fachlich unabhängigen Elternrelationen wählen.
                            OsmRelationMemberType.WAY -> member.id in primaryWays
                            OsmRelationMemberType.RELATION -> member.id in selectedRelations
                        }
                    }
                ) {
                    selectedRelations += relation.id
                    changed = true
                }
            }
            relations.forEach { relation ->
                if (relation.id in selectedRelations && expandedRelations.add(relation.id)) {
                    relation.members.forEach { member ->
                        when (member.type) {
                            OsmRelationMemberType.NODE -> requiredNodes += member.id
                            OsmRelationMemberType.WAY -> if (selectedWays.add(member.id)) changed = true
                            OsmRelationMemberType.RELATION -> if (selectedRelations.add(member.id)) changed = true
                        }
                    }
                }
            }
            onProgress(
                MtciSpatialProgress(
                    MtciSpatialPhase.RELATION_DEPENDENCIES,
                    closurePasses.toLong(),
                    selectedRelations.size.toLong(),
                ),
            )
        } while (changed)

        var dependencyWaysVisited = 0L
        val foundSelectedWays = HashSet<Long>()
        MtciReader.forEachWay(directory) { way ->
            dependencyWaysVisited++
            if (way.id in selectedWays) {
                foundSelectedWays += way.id
                way.nodeIds.forEach(requiredNodes::add)
            }
            if (dependencyWaysVisited % 250_000L == 0L) {
                checkCancellation(isCancelled)
                onProgress(
                    MtciSpatialProgress(
                        MtciSpatialPhase.WAY_DEPENDENCIES,
                        dependencyWaysVisited,
                        requiredNodes.size.toLong(),
                    ),
                )
            }
        }
        selectedWays.retainAll(foundSelectedWays)
        selectedRelations.retainAll(availableRelationIds)
        checkCancellation(isCancelled)

        return MtciSpatialSelection(
            bounds = bounds,
            nodesInsideBounds = nodesInside,
            requiredNodeIds = requiredNodes,
            selectedWayIds = selectedWays,
            selectedRelationIds = selectedRelations,
            visitedNodes = visitedNodes,
            visitedWays = visitedWays,
            visitedRelations = visitedRelations,
            relationClosurePasses = closurePasses,
            elapsedMillis = (System.nanoTime() - startedAt) / 1_000_000L,
        )
    }

    fun writeSummary(selection: MtciSpatialSelection, output: Path) {
        output.toAbsolutePath().parent?.let(Files::createDirectories)
        Files.writeString(output, selection.summaryJson())
    }

    private fun checkCancellation(isCancelled: () -> Boolean) {
        if (isCancelled()) throw MtciSpatialSelectionCancelledException()
    }
}
