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

import java.io.IOException
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.nio.file.Path
import java.nio.file.StandardOpenOption

object PbfDataBlockReader {
    private const val MAX_UNPACKED_SIZE = 32 * 1024 * 1024

    fun open(path: Path): PbfDataBlockSource = PbfDataBlockSource(path)

    fun read(path: Path, entry: PbfBlockIndexEntry): ByteArray = open(path).use { it.read(entry) }

    internal fun read(channel: FileChannel, entry: PbfBlockIndexEntry): ByteArray {
        require(entry.blobSize in 1..MAX_UNPACKED_SIZE) { "Ungültige PBF-Blob-Größe." }
        val bytes = ByteArray(entry.blobSize)
        val payloadOffset = entry.fileOffset + 4L + entry.blobHeaderSize
        val buffer = ByteBuffer.wrap(bytes)
        var position = payloadOffset
        while (buffer.hasRemaining()) {
            val read = channel.read(buffer, position)
            if (read < 0) throw IOException("Die PBF-Datei endet innerhalb von Block ${entry.sequence}.")
            position += read
        }
        return unpackBlob(bytes, entry)
    }

    private fun unpackBlob(bytes: ByteArray, entry: PbfBlockIndexEntry): ByteArray {
        val reader = ProtoReader(bytes)
        var raw: ByteArray? = null
        var rawSize: Int? = null
        var zlib: ByteArray? = null
        while (reader.hasRemaining()) {
            val tag = reader.readTag()
            when (tag.fieldNumber) {
                1 -> raw = reader.readBytes(tag)
                2 -> rawSize = reader.readInt32(tag)
                3 -> zlib = reader.readBytes(tag)
                else -> reader.skip(tag)
            }
        }
        raw?.let {
            if (entry.compression != PbfCompression.RAW || it.size > MAX_UNPACKED_SIZE) {
                throw IOException("RAW-Daten in PBF-Block ${entry.sequence} sind inkonsistent.")
            }
            return it
        }
        zlib?.let { compressed ->
            if (entry.compression != PbfCompression.ZLIB || rawSize == null || rawSize !in 0..MAX_UNPACKED_SIZE) {
                throw IOException("ZLIB-Daten in PBF-Block ${entry.sequence} sind inkonsistent.")
            }
            return PbfDecompression.decompress(
                compression = PbfCompression.ZLIB,
                compressed = compressed,
                expectedSize = rawSize,
                maximumSize = MAX_UNPACKED_SIZE,
            )
        }
        throw IOException("Kompression ${entry.compression} wird beim Dekodieren noch nicht unterstützt.")
    }
}

class PbfDataBlockSource internal constructor(path: Path) : AutoCloseable {
    private val channel = FileChannel.open(path, StandardOpenOption.READ)

    fun read(entry: PbfBlockIndexEntry): ByteArray = PbfDataBlockReader.read(channel, entry)

    override fun close() {
        channel.close()
    }
}
