package de.arxsilex.maptools.converter.desktop

import de.arxsilex.maptools.converter.core.preview.PreviewDataset
import de.arxsilex.maptools.converter.core.preview.PreviewFeatureClass

/**
 * Compact viewport index used by the immediate-mode map renderer.
 *
 * Geometry bounds are converted to the same normalized coordinate system as the viewport once.
 * A compact grid then limits each frame to visible lines, areas, points, and labels instead of
 * walking the complete preview collections on every zoom or pan operation.
 */
internal class PreviewRenderIndex private constructor(
    private val normalizedBounds: FloatArray,
    val areaIndices: IntArray,
    val lineIndices: IntArray,
    private val areaGrid: GeometryGrid,
    private val lineGrid: GeometryGrid,
    private val pointGrid: PointGrid,
    private val labelGrid: PointGrid,
) {
    private val queryMarks = IntArray(normalizedBounds.size / VALUES_PER_BOUNDS)
    private var queryGeneration = 0

    fun intersects(
        polylineIndex: Int,
        left: Float,
        top: Float,
        right: Float,
        bottom: Float,
    ): Boolean {
        val offset = polylineIndex * VALUES_PER_BOUNDS
        return normalizedBounds[offset + MAX_X] >= left &&
            normalizedBounds[offset + MIN_X] <= right &&
            normalizedBounds[offset + MAX_Y] >= top &&
            normalizedBounds[offset + MIN_Y] <= bottom
    }

    fun hasMinimumScreenExtent(
        polylineIndex: Int,
        viewportWidth: Float,
        viewportHeight: Float,
        canvasWidth: Float,
        canvasHeight: Float,
        minimumPixels: Float,
    ): Boolean {
        val offset = polylineIndex * VALUES_PER_BOUNDS
        val widthPixels = (normalizedBounds[offset + MAX_X] - normalizedBounds[offset + MIN_X]) /
            viewportWidth * canvasWidth
        val heightPixels = (normalizedBounds[offset + MAX_Y] - normalizedBounds[offset + MIN_Y]) /
            viewportHeight * canvasHeight
        return widthPixels >= minimumPixels || heightPixels >= minimumPixels
    }

    fun queryAreaIndices(left: Float, top: Float, right: Float, bottom: Float): IntArray =
        queryGeometry(areaGrid, areaIndices, left, top, right, bottom)

    fun queryLineIndices(left: Float, top: Float, right: Float, bottom: Float): IntArray =
        queryGeometry(lineGrid, lineIndices, left, top, right, bottom)

    fun queryPointIndices(left: Float, top: Float, right: Float, bottom: Float): IntArray =
        pointGrid.query(left, top, right, bottom)

    fun queryLabelIndices(left: Float, top: Float, right: Float, bottom: Float): IntArray =
        labelGrid.query(left, top, right, bottom)

    private fun queryGeometry(
        grid: GeometryGrid,
        allIndices: IntArray,
        left: Float,
        top: Float,
        right: Float,
        bottom: Float,
    ): IntArray {
        if (left <= 0f && top <= 0f && right >= 1f && bottom >= 1f) return allIndices
        if (queryGeneration == Int.MAX_VALUE) {
            queryMarks.fill(0)
            queryGeneration = 1
        } else {
            queryGeneration++
        }
        val generation = queryGeneration
        val result = IntBucket()
        fun addIfVisible(index: Int) {
            if (queryMarks[index] == generation) return
            queryMarks[index] = generation
            if (intersects(index, left, top, right, bottom)) result.add(index)
        }
        grid.globalIndices.forEach(::addIfVisible)
        grid.forEachCandidate(left, top, right, bottom, ::addIfVisible)
        return result.toIntArray()
    }

    companion object {
        private const val VALUES_PER_BOUNDS = 4
        private const val MIN_X = 0
        private const val MIN_Y = 1
        private const val MAX_X = 2
        private const val MAX_Y = 3
        private const val GRID_SIZE = 96
        private const val MAX_CELLS_PER_GEOMETRY = 64

        private fun coordinateToCell(value: Float): Int =
            (value.coerceIn(0f, 1f) * GRID_SIZE).toInt().coerceAtMost(GRID_SIZE - 1)

        private fun cellBounds(minX: Float, minY: Float, maxX: Float, maxY: Float): CellBounds = CellBounds(
            coordinateToCell(minX),
            coordinateToCell(minY),
            coordinateToCell(maxX),
            coordinateToCell(maxY),
        )

        fun build(preview: PreviewDataset): PreviewRenderIndex {
            val longitudeSpan = preview.bounds.east - preview.bounds.west
            val latitudeSpan = preview.bounds.north - preview.bounds.south
            val bounds = FloatArray(preview.polylines.size * VALUES_PER_BOUNDS)
            val areas = IntArray(preview.polylines.size)
            val lines = IntArray(preview.polylines.size)
            val areaGrid = GeometryGridBuilder()
            val lineGrid = GeometryGridBuilder()
            var areaCount = 0
            var lineCount = 0

            preview.polylines.forEachIndexed { index, polyline ->
                var minX = Float.POSITIVE_INFINITY
                var minY = Float.POSITIVE_INFINITY
                var maxX = Float.NEGATIVE_INFINITY
                var maxY = Float.NEGATIVE_INFINITY
                var coordinateIndex = 0
                while (coordinateIndex < polyline.coordinates.size) {
                    val x = ((polyline.coordinates[coordinateIndex] - preview.bounds.west) / longitudeSpan).toFloat()
                    val y = ((preview.bounds.north - polyline.coordinates[coordinateIndex + 1]) / latitudeSpan).toFloat()
                    minX = minOf(minX, x)
                    minY = minOf(minY, y)
                    maxX = maxOf(maxX, x)
                    maxY = maxOf(maxY, y)
                    coordinateIndex += 2
                }
                val offset = index * VALUES_PER_BOUNDS
                bounds[offset + MIN_X] = minX
                bounds[offset + MIN_Y] = minY
                bounds[offset + MAX_X] = maxX
                bounds[offset + MAX_Y] = maxY
                if (polyline.featureClass.isAreaFeature()) {
                    areas[areaCount++] = index
                    areaGrid.add(index, minX, minY, maxX, maxY)
                } else {
                    lines[lineCount++] = index
                    lineGrid.add(index, minX, minY, maxX, maxY)
                }
            }
            val points = PointGridBuilder()
            preview.points.forEachIndexed { index, point ->
                points.add(
                    index,
                    ((point.longitude - preview.bounds.west) / longitudeSpan).toFloat(),
                    ((preview.bounds.north - point.latitude) / latitudeSpan).toFloat(),
                )
            }
            val labels = PointGridBuilder()
            preview.labels.forEachIndexed { index, label ->
                labels.add(
                    index,
                    ((label.longitude - preview.bounds.west) / longitudeSpan).toFloat(),
                    ((preview.bounds.north - label.latitude) / latitudeSpan).toFloat(),
                )
            }
            return PreviewRenderIndex(
                normalizedBounds = bounds,
                areaIndices = areas.copyOf(areaCount),
                lineIndices = lines.copyOf(lineCount),
                areaGrid = areaGrid.build(),
                lineGrid = lineGrid.build(),
                pointGrid = points.build(preview.points.size),
                labelGrid = labels.build(preview.labels.size),
            )
        }
    }

    private class GeometryGridBuilder {
        private val buckets = arrayOfNulls<IntBucket>(GRID_SIZE * GRID_SIZE)
        private val global = IntBucket()

        fun add(index: Int, minX: Float, minY: Float, maxX: Float, maxY: Float) {
            val cellBounds = cellBounds(minX, minY, maxX, maxY)
            val coveredCells = (cellBounds.maxX - cellBounds.minX + 1) *
                (cellBounds.maxY - cellBounds.minY + 1)
            if (coveredCells > MAX_CELLS_PER_GEOMETRY) {
                global.add(index)
                return
            }
            for (cellY in cellBounds.minY..cellBounds.maxY) {
                for (cellX in cellBounds.minX..cellBounds.maxX) {
                    val cellIndex = cellY * GRID_SIZE + cellX
                    val bucket = buckets[cellIndex] ?: IntBucket().also { buckets[cellIndex] = it }
                    bucket.add(index)
                }
            }
        }

        fun build(): GeometryGrid = GeometryGrid(
            buckets = Array(buckets.size) { index -> buckets[index]?.toIntArray() },
            globalIndices = global.toIntArray(),
        )
    }

    private class PointGridBuilder {
        private val buckets = arrayOfNulls<IntBucket>(GRID_SIZE * GRID_SIZE)

        fun add(index: Int, x: Float, y: Float) {
            val cellX = coordinateToCell(x)
            val cellY = coordinateToCell(y)
            val cellIndex = cellY * GRID_SIZE + cellX
            val bucket = buckets[cellIndex] ?: IntBucket().also { buckets[cellIndex] = it }
            bucket.add(index)
        }

        fun build(totalSize: Int): PointGrid = PointGrid(
            buckets = Array(buckets.size) { index -> buckets[index]?.toIntArray() },
            allIndices = IntArray(totalSize) { it },
        )
    }

    private class GeometryGrid(
        private val buckets: Array<IntArray?>,
        val globalIndices: IntArray,
    ) {
        fun forEachCandidate(
            left: Float,
            top: Float,
            right: Float,
            bottom: Float,
            action: (Int) -> Unit,
        ) {
            val cells = cellBounds(left, top, right, bottom)
            for (cellY in cells.minY..cells.maxY) {
                for (cellX in cells.minX..cells.maxX) {
                    buckets[cellY * GRID_SIZE + cellX]?.forEach(action)
                }
            }
        }
    }

    private class PointGrid(
        private val buckets: Array<IntArray?>,
        private val allIndices: IntArray,
    ) {
        fun query(left: Float, top: Float, right: Float, bottom: Float): IntArray {
            if (left <= 0f && top <= 0f && right >= 1f && bottom >= 1f) return allIndices
            val cells = cellBounds(left, top, right, bottom)
            val result = IntBucket()
            for (cellY in cells.minY..cells.maxY) {
                for (cellX in cells.minX..cells.maxX) {
                    buckets[cellY * GRID_SIZE + cellX]?.forEach(result::add)
                }
            }
            return result.toIntArray()
        }
    }

    private class IntBucket(initialCapacity: Int = 8) {
        private var values = IntArray(initialCapacity)
        private var size = 0

        fun add(value: Int) {
            if (size == values.size) values = values.copyOf((values.size * 2).coerceAtLeast(1))
            values[size++] = value
        }

        fun forEach(action: (Int) -> Unit) {
            for (index in 0 until size) action(values[index])
        }

        fun toIntArray(): IntArray = values.copyOf(size)

    }

    private data class CellBounds(val minX: Int, val minY: Int, val maxX: Int, val maxY: Int)
}

internal fun PreviewFeatureClass.isAreaFeature(): Boolean = when (this) {
    PreviewFeatureClass.LAND,
    PreviewFeatureClass.URBAN_AREA,
    PreviewFeatureClass.WATER_AREA,
    PreviewFeatureClass.BUILDING,
    -> true
    else -> false
}
