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

import de.arxsilex.maptools.converter.core.pbf.OsmPreviewNode
import de.arxsilex.maptools.converter.core.pbf.OsmPreviewRelation
import de.arxsilex.maptools.converter.core.pbf.OsmPreviewWay
import de.arxsilex.maptools.converter.core.pbf.OsmRelationMember
import de.arxsilex.maptools.converter.core.pbf.OsmRelationMemberType
import java.io.BufferedInputStream
import java.io.DataInputStream
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path

data class MtciReadStatistics(
    val nodeCount: Long,
    val wayCount: Long,
    val relationCount: Long,
)

object MtciReader {
    fun readAll(
        directory: Path,
        onNode: (OsmPreviewNode) -> Unit = {},
        onWay: (OsmPreviewWay) -> Unit = {},
        onRelation: (OsmPreviewRelation) -> Unit = {},
    ): MtciReadStatistics {
        val normalized = directory.toAbsolutePath().normalize()
        require(Files.isDirectory(normalized)) { "MTCI-Verzeichnis nicht gefunden: $normalized" }
        val nodeCount = forEachNode(normalized, onNode)
        val wayCount = forEachWay(normalized, onWay)
        val relationCount = forEachRelation(normalized, onRelation)
        return MtciReadStatistics(nodeCount, wayCount, relationCount)
    }

    fun forEachNode(directory: Path, onNode: (OsmPreviewNode) -> Unit): Long {
        val normalized = requireDirectory(directory)
        var nodeCount = 0L
        shardFiles(normalized, "nodes-", ".mtcn").forEach { path ->
            readShard(path, MTCI_NODE_MAGIC) { input ->
                onNode(
                    OsmPreviewNode(
                        id = input.readLong(),
                        latitude = input.readLong() / 1_000_000_000.0,
                        longitude = input.readLong() / 1_000_000_000.0,
                        tags = input.readTags(),
                    ),
                )
                nodeCount++
            }
        }
        return nodeCount
    }

    fun forEachWay(directory: Path, onWay: (OsmPreviewWay) -> Unit): Long {
        val normalized = requireDirectory(directory)
        var wayCount = 0L
        shardFiles(normalized, "ways-", ".mtcw").forEach { path ->
            readShard(path, MTCI_WAY_MAGIC) { input ->
                val id = input.readLong()
                val referenceCount = input.readBoundedCount("Way-Referenzen", 10_000_000)
                val references = LongArray(referenceCount) { input.readLong() }
                onWay(OsmPreviewWay(id, references, input.readTags()))
                wayCount++
            }
        }
        return wayCount
    }

    fun forEachRelation(directory: Path, onRelation: (OsmPreviewRelation) -> Unit): Long {
        val normalized = requireDirectory(directory)
        var relationCount = 0L
        shardFiles(normalized, "relations-", ".mtcr").forEach { path ->
            readShard(path, MTCI_RELATION_MAGIC) { input ->
                val id = input.readLong()
                val memberCount = input.readBoundedCount("Relation-Mitglieder", 10_000_000)
                val members = List(memberCount) {
                    val typeIndex = input.readUnsignedByte()
                    OsmRelationMember(
                        id = input.readLong(),
                        type = OsmRelationMemberType.entries.getOrNull(typeIndex)
                            ?: throw IOException("Ungültiger MTCI-Relationstyp $typeIndex."),
                        role = input.readUtf8(),
                    )
                }
                onRelation(OsmPreviewRelation(id, members, input.readTags()))
                relationCount++
            }
        }
        return relationCount
    }

    private fun readShard(path: Path, expectedMagic: Int, readRecord: (DataInputStream) -> Unit) {
        val counting = CountingInputStream(BufferedInputStream(Files.newInputStream(path), 128 * 1024))
        DataInputStream(counting).use { input ->
            val magic = input.readInt()
            if (magic != expectedMagic) throw IOException("Ungültige MTCI-Kennung in ${path.fileName}.")
            val version = input.readInt()
            if (version != MTCI_SCHEMA_VERSION) {
                throw IOException("Nicht unterstützte MTCI-Version $version in ${path.fileName}.")
            }
            val shard = input.readInt()
            val shardCount = input.readInt()
            if (shard !in 0 until shardCount || shardCount !in 1..1024) {
                throw IOException("Ungültiger MTCI-Shardheader in ${path.fileName}.")
            }
            val size = Files.size(path)
            while (counting.bytesRead < size) {
                try {
                    readRecord(input)
                } catch (exception: IOException) {
                    throw IOException("Beschädigter MTCI-Datensatz in ${path.fileName} bei Byte ${counting.bytesRead}.", exception)
                }
            }
            if (counting.bytesRead != size) {
                throw IOException("MTCI-Reader überschritt das Dateiende von ${path.fileName}.")
            }
        }
    }

    private fun shardFiles(directory: Path, prefix: String, suffix: String): List<Path> =
        Files.list(directory).use { files ->
            files.filter { path ->
                val name = path.fileName.toString()
                name.startsWith(prefix) && name.endsWith(suffix) && Files.isRegularFile(path)
            }.sorted().toList()
        }

    private fun requireDirectory(directory: Path): Path = directory.toAbsolutePath().normalize().also { normalized ->
        require(Files.isDirectory(normalized)) { "MTCI-Verzeichnis nicht gefunden: $normalized" }
    }
}

private class CountingInputStream(input: InputStream) : FilterInputStream(input) {
    var bytesRead: Long = 0
        private set

    override fun read(): Int = super.read().also { value -> if (value >= 0) bytesRead++ }

    override fun read(bytes: ByteArray, offset: Int, length: Int): Int =
        super.read(bytes, offset, length).also { count -> if (count > 0) bytesRead += count }
}

private fun DataInputStream.readTags(): Map<String, String> {
    val count = readBoundedCount("Tags", 1_000_000)
    val result = linkedMapOf<String, String>()
    repeat(count) {
        result[readUtf8()] = readUtf8()
    }
    return result
}

private fun DataInputStream.readUtf8(): String {
    val length = readBoundedCount("UTF-8-Text", 16 * 1024 * 1024)
    return readNBytes(length).also { bytes ->
        if (bytes.size != length) throw IOException("MTCI-Text endet unerwartet.")
    }.toString(Charsets.UTF_8)
}

private fun DataInputStream.readBoundedCount(name: String, maximum: Int): Int {
    val value = readInt()
    if (value !in 0..maximum) throw IOException("Ungültige Anzahl für $name: $value.")
    return value
}
