package de.arxsilex.maptools.converter.desktop

import de.arxsilex.maptools.converter.core.elevation.ElevationPreviewRaster
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.math.sqrt

/** CPU-side colorization keeps the Compose canvas limited to one image draw per frame. */
internal object ElevationPreviewRenderer {
    val hypsometricColorStops: List<Int> = listOf(
        0xFF315F55.toInt(),
        0xFF66825A.toInt(),
        0xFFAAA05C.toInt(),
        0xFF9A724D.toInt(),
        0xFFD0C4A4.toInt(),
        0xFFF1EEE5.toInt(),
    )

    fun legendColorStops(minimumElevationMeters: Float): List<Int> =
        if (minimumElevationMeters < 0f) {
            listOf(0xFF285A71.toInt(), 0xFF72AEB6.toInt()) + hypsometricColorStops
        } else {
            hypsometricColorStops
        }

    fun rgbaPixels(raster: ElevationPreviewRaster): ByteArray {
        val output = ByteArray(raster.elevations.size * 4)
        val longitudeCellMeters = (raster.bounds.east - raster.bounds.west) / raster.width *
            METERS_PER_LONGITUDE_DEGREE * cos(Math.toRadians((raster.bounds.north + raster.bounds.south) / 2.0))
        val latitudeCellMeters = (raster.bounds.north - raster.bounds.south) / raster.height *
            METERS_PER_LATITUDE_DEGREE
        for (row in 0 until raster.height) {
            for (column in 0 until raster.width) {
                val elevation = raster.elevationAt(column, row)
                val byteOffset = (row * raster.width + column) * 4
                if (!elevation.isFinite()) {
                    output[byteOffset + 3] = 0
                    continue
                }
                val color = hypsometricColor(
                    elevation,
                    raster.minimumElevationMeters,
                    raster.maximumElevationMeters,
                )
                val shade = hillshade(
                    raster,
                    column,
                    row,
                    longitudeCellMeters.coerceAtLeast(1.0),
                    latitudeCellMeters.coerceAtLeast(1.0),
                )
                output[byteOffset] = (((color ushr 16) and 0xFF) * shade).roundToInt().coerceIn(0, 255).toByte()
                output[byteOffset + 1] = (((color ushr 8) and 0xFF) * shade).roundToInt().coerceIn(0, 255).toByte()
                output[byteOffset + 2] = ((color and 0xFF) * shade).roundToInt().coerceIn(0, 255).toByte()
                output[byteOffset + 3] = 0xFF.toByte()
            }
        }
        return output
    }

    internal fun hypsometricColor(elevation: Float, minimum: Float, maximum: Float): Int {
        if (maximum - minimum < 0.001f) return hypsometricColorStops[2]
        if (elevation < 0f) {
            val lower = if (minimum < 0f) minimum else elevation
            val fraction = ((elevation - lower) / (0f - lower).coerceAtLeast(0.001f)).coerceIn(0f, 1f)
            return interpolate(0xFF285A71.toInt(), 0xFF72AEB6.toInt(), fraction)
        }
        val positiveMinimum = maxOf(0f, minimum)
        val fraction = ((elevation - positiveMinimum) / (maximum - positiveMinimum).coerceAtLeast(0.001f))
            .coerceIn(0f, 1f)
        val scaled = fraction * (hypsometricColorStops.size - 1)
        val lowerIndex = scaled.toInt().coerceIn(0, hypsometricColorStops.lastIndex)
        val upperIndex = (lowerIndex + 1).coerceAtMost(hypsometricColorStops.lastIndex)
        return interpolate(hypsometricColorStops[lowerIndex], hypsometricColorStops[upperIndex], scaled - lowerIndex)
    }

    private fun hillshade(
        raster: ElevationPreviewRaster,
        column: Int,
        row: Int,
        cellWidthMeters: Double,
        cellHeightMeters: Double,
    ): Double {
        val center = raster.elevationAt(column, row).toDouble()
        fun neighbor(x: Int, y: Int): Double = raster.elevationAt(
            x.coerceIn(0, raster.width - 1),
            y.coerceIn(0, raster.height - 1),
        ).takeIf(Float::isFinite)?.toDouble() ?: center

        val gradientX = (neighbor(column + 1, row) - neighbor(column - 1, row)) /
            (2.0 * cellWidthMeters)
        val gradientY = (neighbor(column, row + 1) - neighbor(column, row - 1)) /
            (2.0 * cellHeightMeters)
        var normalX = -gradientX * VERTICAL_EXAGGERATION
        var normalY = -gradientY * VERTICAL_EXAGGERATION
        var normalZ = 1.0
        val normalLength = sqrt(normalX * normalX + normalY * normalY + normalZ * normalZ)
        normalX /= normalLength
        normalY /= normalLength
        normalZ /= normalLength
        val azimuth = Math.toRadians(315.0)
        val altitude = Math.toRadians(45.0)
        val lightX = sin(azimuth) * cos(altitude)
        val lightY = -cos(azimuth) * cos(altitude)
        val lightZ = sin(altitude)
        val illumination = (normalX * lightX + normalY * lightY + normalZ * lightZ).coerceIn(0.0, 1.0)
        return (0.58 + illumination * 0.58).coerceIn(0.48, 1.16)
    }

    private fun interpolate(first: Int, second: Int, fraction: Float): Int {
        fun channel(shift: Int): Int {
            val start = first ushr shift and 0xFF
            val end = second ushr shift and 0xFF
            return (start + (end - start) * fraction).roundToInt().coerceIn(0, 255)
        }
        return (0xFF shl 24) or (channel(16) shl 16) or (channel(8) shl 8) or channel(0)
    }

    private const val METERS_PER_LONGITUDE_DEGREE = 111_320.0
    private const val METERS_PER_LATITUDE_DEGREE = 110_574.0
    private const val VERTICAL_EXAGGERATION = 1.8
}
