package de.arxsilex.maptools.converter.core.pbf

import de.arxsilex.maptools.converter.core.GeoBounds
import de.arxsilex.maptools.converter.core.PbfHeaderMetadata
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.InflaterInputStream

object PbfHeaderReader {
    private const val MAX_BLOB_HEADER_SIZE = 64 * 1024
    private const val MAX_HEADER_BLOCK_SIZE = 32 * 1024 * 1024

    fun read(path: Path): PbfHeaderMetadata = Files.newInputStream(path).buffered().use(::read)

    internal fun read(input: InputStream): PbfHeaderMetadata {
        val blobHeaderSize = input.readBigEndianInt()
        if (blobHeaderSize !in 1..MAX_BLOB_HEADER_SIZE) {
            throw IOException("Ungültige PBF-BlobHeader-Größe: $blobHeaderSize Byte.")
        }

        val blobHeader = parseBlobHeader(input.readExactly(blobHeaderSize))
        if (blobHeader.type != "OSMHeader") {
            throw IOException("Die PBF-Datei beginnt nicht mit einem OSMHeader.")
        }
        if (blobHeader.dataSize !in 1..MAX_HEADER_BLOCK_SIZE) {
            throw IOException("Ungültige PBF-HeaderBlock-Größe: ${blobHeader.dataSize} Byte.")
        }

        val headerBlock = unpackBlob(input.readExactly(blobHeader.dataSize))
        return parseHeaderBlock(headerBlock)
    }

    private fun parseBlobHeader(bytes: ByteArray): BlobHeader {
        val reader = ProtoReader(bytes)
        var type: String? = null
        var dataSize: Int? = null
        while (reader.hasRemaining()) {
            val tag = reader.readTag()
            when (tag.fieldNumber) {
                1 -> type = reader.readString(tag)
                3 -> dataSize = reader.readInt32(tag)
                else -> reader.skip(tag)
            }
        }
        return BlobHeader(
            type = type ?: throw IOException("Im PBF-BlobHeader fehlt der Typ."),
            dataSize = dataSize ?: throw IOException("Im PBF-BlobHeader fehlt die Datengröße."),
        )
    }

    private fun unpackBlob(bytes: ByteArray): ByteArray {
        val reader = ProtoReader(bytes)
        var raw: ByteArray? = null
        var rawSize: Int? = null
        var zlibData: ByteArray? = null
        var unsupportedCompression = false
        while (reader.hasRemaining()) {
            val tag = reader.readTag()
            when (tag.fieldNumber) {
                1 -> raw = reader.readBytes(tag)
                2 -> rawSize = reader.readInt32(tag)
                3 -> zlibData = reader.readBytes(tag)
                4, 5, 6, 7 -> {
                    unsupportedCompression = true
                    reader.skip(tag)
                }
                else -> reader.skip(tag)
            }
        }

        raw?.let {
            if (it.size > MAX_HEADER_BLOCK_SIZE) throw IOException("Der PBF-Header ist zu groß.")
            return it
        }
        zlibData?.let { compressed ->
            if (rawSize == null || rawSize !in 0..MAX_HEADER_BLOCK_SIZE) {
                throw IOException("Die entpackte PBF-Headergröße ist ungültig.")
            }
            val unpacked = InflaterInputStream(ByteArrayInputStream(compressed)).use {
                it.readBounded(MAX_HEADER_BLOCK_SIZE)
            }
            if (unpacked.size != rawSize) {
                throw IOException("Die entpackte PBF-Headergröße stimmt nicht mit raw_size überein.")
            }
            return unpacked
        }
        if (unsupportedCompression) {
            throw IOException("Die Komprimierung des PBF-Headers wird noch nicht unterstützt.")
        }
        throw IOException("Der PBF-Blob enthält weder RAW- noch ZLIB-Daten.")
    }

    private fun parseHeaderBlock(bytes: ByteArray): PbfHeaderMetadata {
        val reader = ProtoReader(bytes)
        var bounds: GeoBounds? = null
        val requiredFeatures = mutableListOf<String>()
        val optionalFeatures = mutableListOf<String>()
        var writingProgram: String? = null
        var source: String? = null
        while (reader.hasRemaining()) {
            val tag = reader.readTag()
            when (tag.fieldNumber) {
                1 -> bounds = parseBounds(reader.readBytes(tag))
                4 -> requiredFeatures += reader.readString(tag)
                5 -> optionalFeatures += reader.readString(tag)
                16 -> writingProgram = reader.readString(tag)
                17 -> source = reader.readString(tag)
                else -> reader.skip(tag)
            }
        }
        return PbfHeaderMetadata(bounds, requiredFeatures, optionalFeatures, writingProgram, source)
    }

    private fun parseBounds(bytes: ByteArray): GeoBounds {
        val reader = ProtoReader(bytes)
        var left: Long? = null
        var right: Long? = null
        var top: Long? = null
        var bottom: Long? = null
        while (reader.hasRemaining()) {
            val tag = reader.readTag()
            when (tag.fieldNumber) {
                1 -> left = reader.readSInt64(tag)
                2 -> right = reader.readSInt64(tag)
                3 -> top = reader.readSInt64(tag)
                4 -> bottom = reader.readSInt64(tag)
                else -> reader.skip(tag)
            }
        }
        fun Long?.degrees(name: String): Double =
            (this ?: throw IOException("Im PBF-Header fehlt die Grenze $name.")) / 1_000_000_000.0
        return try {
            GeoBounds(
                west = left.degrees("West"),
                south = bottom.degrees("Süd"),
                east = right.degrees("Ost"),
                north = top.degrees("Nord"),
            )
        } catch (exception: IllegalArgumentException) {
            throw IOException("Der PBF-Header enthält ungültige geografische Grenzen.", exception)
        }
    }

    private data class BlobHeader(val type: String, val dataSize: Int)
}

internal data class ProtoTag(val fieldNumber: Int, val wireType: Int)

internal class ProtoReader(private val bytes: ByteArray) {
    private var position = 0

    fun hasRemaining(): Boolean = position < bytes.size

    fun readTag(): ProtoTag {
        val value = readVarint()
        val fieldNumber = (value ushr 3).toInt()
        val wireType = (value and 7).toInt()
        if (fieldNumber == 0) throw IOException("Ungültiges Protobuf-Feld 0.")
        return ProtoTag(fieldNumber, wireType)
    }

    fun readString(tag: ProtoTag): String = readBytes(tag).toString(Charsets.UTF_8)

    fun readBytes(tag: ProtoTag): ByteArray {
        requireWireType(tag, 2)
        val length = readVarint()
        if (length > Int.MAX_VALUE || length < 0 || length > bytes.size - position) {
            throw IOException("Ungültige Länge in den PBF-Metadaten.")
        }
        val end = position + length.toInt()
        return bytes.copyOfRange(position, end).also { position = end }
    }

    fun readInt32(tag: ProtoTag): Int {
        requireWireType(tag, 0)
        val value = readVarint()
        if (value > Int.MAX_VALUE) throw IOException("Ganzzahl in den PBF-Metadaten ist zu groß.")
        return value.toInt()
    }

    fun skipLengthDelimited(tag: ProtoTag): Int {
        requireWireType(tag, 2)
        val length = readVarint()
        if (length > Int.MAX_VALUE) throw IOException("Protobuf-Feld ist zu groß.")
        skipBytes(length.toInt())
        return length.toInt()
    }

    fun readSInt64(tag: ProtoTag): Long {
        requireWireType(tag, 0)
        val value = readVarint()
        return (value ushr 1) xor -(value and 1L)
    }

    fun readInt64(tag: ProtoTag): Long {
        requireWireType(tag, 0)
        return readVarint()
    }

    fun readPackedUInt32(tag: ProtoTag): IntArray = readPacked(tag) { packed ->
        val value = packed.readVarintValue()
        if (value > Int.MAX_VALUE) throw IOException("StringTable-Index ist zu groß.")
        value.toInt()
    }

    fun readPackedInt32(tag: ProtoTag): IntArray = readPackedUInt32(tag)

    fun readPackedSInt64(tag: ProtoTag): LongArray {
        val packedBytes = readBytes(tag)
        val packed = ProtoReader(packedBytes)
        val values = mutableListOf<Long>()
        while (packed.hasRemaining()) {
            val value = packed.readVarintValue()
            values += (value ushr 1) xor -(value and 1L)
        }
        return values.toLongArray()
    }

    fun skip(tag: ProtoTag) {
        when (tag.wireType) {
            0 -> readVarint()
            1 -> skipBytes(8)
            2 -> {
                val length = readVarint()
                if (length > Int.MAX_VALUE) throw IOException("Protobuf-Feld ist zu groß.")
                skipBytes(length.toInt())
            }
            5 -> skipBytes(4)
            else -> throw IOException("Nicht unterstützter Protobuf-Wire-Type ${tag.wireType}.")
        }
    }

    private fun requireWireType(tag: ProtoTag, expected: Int) {
        if (tag.wireType != expected) {
            throw IOException("Unerwarteter Protobuf-Wire-Type ${tag.wireType} für Feld ${tag.fieldNumber}.")
        }
    }

    internal fun readVarintValue(): Long = readVarint()

    private fun readPacked(tag: ProtoTag, readValue: (ProtoReader) -> Int): IntArray {
        val packedBytes = readBytes(tag)
        val packed = ProtoReader(packedBytes)
        val values = mutableListOf<Int>()
        while (packed.hasRemaining()) values += readValue(packed)
        return values.toIntArray()
    }

    private fun readVarint(): Long {
        var result = 0L
        for (shift in 0 until 64 step 7) {
            if (position >= bytes.size) throw EOFException("Unvollständige Protobuf-Varint.")
            val current = bytes[position++].toInt() and 0xFF
            if (shift == 63 && current and 0xFE != 0) {
                throw IOException("Protobuf-Varint überschreitet 64 Bit.")
            }
            result = result or ((current and 0x7F).toLong() shl shift)
            if (current and 0x80 == 0) return result
        }
        throw IOException("Protobuf-Varint ist zu lang.")
    }

    private fun skipBytes(count: Int) {
        if (count < 0 || count > bytes.size - position) throw EOFException("Unvollständiges Protobuf-Feld.")
        position += count
    }
}

private fun InputStream.readBigEndianInt(): Int {
    val bytes = readExactly(4)
    return ((bytes[0].toInt() and 0xFF) shl 24) or
        ((bytes[1].toInt() and 0xFF) shl 16) or
        ((bytes[2].toInt() and 0xFF) shl 8) or
        (bytes[3].toInt() and 0xFF)
}

internal fun InputStream.readExactly(count: Int): ByteArray {
    val result = ByteArray(count)
    var offset = 0
    while (offset < count) {
        val read = read(result, offset, count - offset)
        if (read < 0) throw EOFException("Die PBF-Datei endet unerwartet.")
        offset += read
    }
    return result
}

internal fun InputStream.readBounded(maxBytes: Int): ByteArray {
    val output = ByteArrayOutputStream()
    val buffer = ByteArray(8192)
    var total = 0
    while (true) {
        val read = read(buffer)
        if (read < 0) break
        total += read
        if (total > maxBytes) throw IOException("Der entpackte PBF-Header ist zu groß.")
        output.write(buffer, 0, read)
    }
    return output.toByteArray()
}
