package de.arxsilex.maptools.converter.core.preview

import de.arxsilex.maptools.converter.core.GeoBounds
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import kotlin.io.path.fileSize
import kotlin.io.path.getLastModifiedTime

data class CachedPreviewDataset(
    val sourceSize: Long,
    val sourceLastModifiedMillis: Long,
    val sourceSha256: String,
    val dataset: PreviewDataset,
)

object PreviewDatasetStore {
    private const val MAGIC = 0x4D545056
    private const val VERSION = 6
    private const val MAX_FEATURES = 2_000_000
    private const val MAX_COORDINATES_PER_LINE = 2_000_000

    fun write(cached: CachedPreviewDataset, destination: Path) {
        val target = destination.toAbsolutePath().normalize()
        Files.createDirectories(target.parent)
        val temporary = Files.createTempFile(target.parent, target.fileName.toString(), ".tmp")
        try {
            DataOutputStream(BufferedOutputStream(Files.newOutputStream(temporary))).use { output ->
                output.writeInt(MAGIC)
                output.writeInt(VERSION)
                output.writeLong(cached.sourceSize)
                output.writeLong(cached.sourceLastModifiedMillis)
                output.writeUTF(cached.sourceSha256)
                cached.dataset.bounds.writeTo(output)
                output.writeLong(cached.dataset.sourceNodeCount)
                output.writeLong(cached.dataset.sourceWayCount)
                output.writeInt(cached.dataset.points.size)
                cached.dataset.points.forEach { point ->
                    output.writeDouble(point.latitude)
                    output.writeDouble(point.longitude)
                    output.writeByte(point.featureClass.ordinal)
                }
                output.writeInt(cached.dataset.polylines.size)
                cached.dataset.polylines.forEach { line ->
                    output.writeByte(line.featureClass.ordinal)
                    output.writeInt(line.coordinates.size)
                    line.coordinates.forEach(output::writeDouble)
                }
                output.writeInt(cached.dataset.labels.size)
                cached.dataset.labels.forEach { label ->
                    output.writeDouble(label.latitude)
                    output.writeDouble(label.longitude)
                    output.writeByte(label.labelClass.ordinal)
                    output.writeByte(label.importance)
                    output.writeUTF(label.text)
                }
            }
            try {
                Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
            } catch (_: AtomicMoveNotSupportedException) {
                Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING)
            }
        } finally {
            Files.deleteIfExists(temporary)
        }
    }

    fun writeBestEffort(cached: CachedPreviewDataset, destination: Path): String? = try {
        write(cached, destination)
        null
    } catch (exception: Exception) {
        val cause = generateSequence<Throwable>(exception) { it.cause }.last()
        "Vorschau ist verfügbar, konnte aber nicht im Cache gespeichert werden: " +
            (cause.message ?: cause.javaClass.simpleName)
    }

    fun read(source: Path): CachedPreviewDataset =
        DataInputStream(BufferedInputStream(Files.newInputStream(source))).use { input ->
            if (input.readInt() != MAGIC) throw IOException("Die Datei ist kein MapTools-Vorschaucache.")
            if (input.readInt() != VERSION) throw IOException("Nicht unterstützte Vorschaucache-Version.")
            val sourceSize = input.readLong()
            val modified = input.readLong()
            val hash = input.readUTF()
            if (!hash.matches(Regex("[0-9a-f]{64}"))) throw IOException("Ungültiger Quellhash im Vorschaucache.")
            val bounds = GeoBounds(input.readDouble(), input.readDouble(), input.readDouble(), input.readDouble())
            val nodeCount = input.readLong()
            val wayCount = input.readLong()
            val classes = PreviewFeatureClass.entries
            val pointCount = input.readBoundedCount("Punkte")
            val points = List(pointCount) {
                PreviewPoint(input.readDouble(), input.readDouble(), classes.readClass(input))
            }
            val lineCount = input.readBoundedCount("Linien")
            val lines = List(lineCount) {
                val featureClass = classes.readClass(input)
                val coordinateCount = input.readInt()
                if (coordinateCount !in 4..MAX_COORDINATES_PER_LINE || coordinateCount % 2 != 0) {
                    throw IOException("Ungültige Koordinatenanzahl im Vorschaucache.")
                }
                PreviewPolyline(DoubleArray(coordinateCount) { input.readDouble() }, featureClass)
            }
            val labelClasses = PreviewLabelClass.entries
            val labelCount = input.readBoundedCount("Beschriftungen")
            val labels = List(labelCount) {
                val latitude = input.readDouble()
                val longitude = input.readDouble()
                val labelClass = labelClasses.getOrNull(input.readUnsignedByte())
                    ?: throw IOException("Unbekannte Beschriftungsklasse im Vorschaucache.")
                val importance = input.readUnsignedByte()
                val text = input.readUTF()
                PreviewLabel(latitude, longitude, text, labelClass, importance)
            }
            CachedPreviewDataset(
                sourceSize,
                modified,
                hash,
                PreviewDataset(bounds, points, lines, nodeCount, wayCount, labels),
            )
        }

    fun matchesSource(cached: CachedPreviewDataset, source: Path): Boolean =
        Files.isRegularFile(source) && source.fileSize() == cached.sourceSize &&
            source.getLastModifiedTime().toMillis() == cached.sourceLastModifiedMillis

    private fun GeoBounds.writeTo(output: DataOutputStream) {
        output.writeDouble(west)
        output.writeDouble(south)
        output.writeDouble(east)
        output.writeDouble(north)
    }

    private fun DataInputStream.readBoundedCount(label: String): Int = readInt().also {
        if (it !in 0..MAX_FEATURES) throw IOException("Ungültige Anzahl für $label im Vorschaucache: $it.")
    }

    private fun List<PreviewFeatureClass>.readClass(input: DataInputStream): PreviewFeatureClass {
        val ordinal = input.readUnsignedByte()
        return getOrNull(ordinal) ?: throw IOException("Unbekannte Objektklasse im Vorschaucache.")
    }
}
