package de.arxsilex.maptools.converter.core.elevation

import de.arxsilex.maptools.converter.core.ElevationOutputFormat
import de.arxsilex.maptools.converter.core.GeoBounds
import de.arxsilex.maptools.converter.core.GeoCircleRegion
import de.arxsilex.maptools.converter.core.GeoRectangleRegion
import de.arxsilex.maptools.converter.core.InputFormat
import java.awt.image.DataBuffer
import java.nio.file.Files
import java.util.concurrent.atomic.AtomicInteger
import javax.imageio.ImageIO
import kotlin.io.path.createTempDirectory
import kotlin.io.path.exists
import kotlin.io.path.readBytes
import kotlin.io.path.readLines
import kotlin.io.path.writeText
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue

class ElevationConverterTest {
    @Test
    fun `merges overlapping grids with finest source taking precedence`() {
        val directory = createTempDirectory("elevation-merge-")
        try {
            val coarse = directory.resolve("coarse.asc").also { writeGrid(it, 1.0, listOf("1 2", "3 4")) }
            val fine = directory.resolve("fine.asc").also { writeGrid(it, 0.5, List(4) { "9 9 9 9" }) }
            val output = directory.resolve("merged.asc")

            val report = ElevationConverter.convert(
                ElevationConversionRequest(
                    inputs = listOf(coarse, fine),
                    output = output,
                    outputFormat = ElevationOutputFormat.ESRI_ASCII_GRID,
                    areas = listOf(GeoRectangleRegion(GeoBounds(0.0, 0.0, 2.0, 2.0))),
                ),
            )

            assertEquals(16L, report.sampledCells)
            assertEquals(16L, report.populatedCells)
            assertTrue(output.readLines().drop(6).all { row -> row.split(' ').all { it == "9" } })
        } finally {
            directory.toFile().deleteRecursively()
        }
    }

    @Test
    fun `writes readable GeoTIFF HGT and DTED outputs`() {
        val directory = createTempDirectory("elevation-formats-")
        try {
            val source = directory.resolve("source.asc").also { writeGrid(it, 0.5, List(4) { "123 123 123 123" }) }
            val area = listOf(GeoRectangleRegion(GeoBounds(0.1, 0.1, 0.9, 0.9)))

            val geotiff = directory.resolve("output.tif")
            ElevationConverter.convert(
                ElevationConversionRequest(listOf(source), geotiff, ElevationOutputFormat.GEOTIFF, area),
            )
            val geotiffMetadata = ElevationMetadataReader.read(geotiff, InputFormat.ELEVATION_GEOTIFF)
            assertTrue(geotiffMetadata.width > 0 && geotiffMetadata.height > 0)
            assertTrue(geotiffMetadata.coordinateReferenceSystem.orEmpty().contains("WGS 84"))
            assertEquals(-32768.0, geotiffMetadata.noDataValue)
            ImageIO.createImageInputStream(geotiff.toFile()).use { input ->
                val reader = ImageIO.getImageReaders(input).asSequence().first()
                try {
                    reader.input = input
                    val sampleModel = reader.getRawImageType(0).sampleModel
                    assertEquals(1, sampleModel.numBands)
                    assertEquals(32, sampleModel.getSampleSize(0))
                    assertEquals(DataBuffer.TYPE_FLOAT, sampleModel.dataType)
                } finally {
                    reader.dispose()
                }
            }
            val geotiffRoundTrip = directory.resolve("geotiff-roundtrip.asc")
            ElevationConverter.convert(
                ElevationConversionRequest(
                    listOf(geotiff),
                    geotiffRoundTrip,
                    ElevationOutputFormat.ESRI_ASCII_GRID,
                    area,
                ),
            )
            val roundTripValues = geotiffRoundTrip.readLines().drop(6).flatMap { it.split(' ') }
                .filter { it != "-32768" }
            assertTrue(roundTripValues.isNotEmpty())
            assertTrue(roundTripValues.all { it == "123" })

            val hgtDirectory = directory.resolve("hgt")
            ElevationConverter.convert(
                ElevationConversionRequest(listOf(source), hgtDirectory, ElevationOutputFormat.SRTM_HGT, area),
            )
            val hgt = hgtDirectory.resolve("N00E000.hgt")
            assertTrue(Files.isRegularFile(hgt))
            assertEquals(1201, ElevationMetadataReader.read(hgt, InputFormat.ELEVATION_HGT).width)
            assertRoundTripValues(directory.resolve("hgt-roundtrip.asc"), hgt, area)

            val dtedDirectory = directory.resolve("dted")
            ElevationConverter.convert(
                ElevationConversionRequest(listOf(source), dtedDirectory, ElevationOutputFormat.DTED, area),
            )
            val dted = dtedDirectory.resolve("N00E000.dt0")
            assertTrue(Files.isRegularFile(dted))
            assertEquals(121, ElevationMetadataReader.read(dted, InputFormat.ELEVATION_DTED).width)
            assertEquals(0xAA, Files.newInputStream(dted).use { input ->
                input.skipNBytes(80L + 648L + 2700L)
                input.read()
            })
            assertRoundTripValues(directory.resolve("dted-roundtrip.asc"), dted, area)
        } finally {
            directory.toFile().deleteRecursively()
        }
    }

    @Test
    fun `parallel GeoTIFF strips are byte identical to sequential output`() {
        val directory = createTempDirectory("elevation-parallel-")
        try {
            val source = directory.resolve("source.asc").also {
                writeGrid(it, 0.01, List(200) { row ->
                    List(200) { column -> (row * 200 + column).toString() }.joinToString(" ")
                })
            }
            val area = listOf(GeoRectangleRegion(GeoBounds(0.0, 0.0, 2.0, 2.0)))
            val sequential = directory.resolve("sequential.tif")
            val parallel = directory.resolve("parallel.tif")

            val sequentialReport = ElevationConverter.convert(
                ElevationConversionRequest(
                    listOf(source),
                    sequential,
                    ElevationOutputFormat.GEOTIFF,
                    area,
                    ElevationExecutionOptions(workerCount = 1, bufferBytesPerWorker = 256 * 1024),
                ),
            )
            val parallelReport = ElevationConverter.convert(
                ElevationConversionRequest(
                    listOf(source),
                    parallel,
                    ElevationOutputFormat.GEOTIFF,
                    area,
                    ElevationExecutionOptions(workerCount = 4, bufferBytesPerWorker = 256 * 1024),
                ),
            )

            assertEquals(sequentialReport, parallelReport.copy(outputs = sequentialReport.outputs))
            assertTrue(sequential.readBytes().contentEquals(parallel.readBytes()))
        } finally {
            directory.toFile().deleteRecursively()
        }
    }

    @Test
    fun `parallel GeoTIFF preserves the exact circular selection`() {
        val directory = createTempDirectory("elevation-circle-")
        try {
            val source = directory.resolve("source.asc").also {
                writeGrid(it, 0.01, List(200) { List(200) { 123 }.joinToString(" ") })
            }
            val circle = GeoCircleRegion(centerLatitude = 1.0, centerLongitude = 1.0, radiusKilometers = 50.0)
            val output = directory.resolve("circle.tif")
            val report = ElevationConverter.convert(
                ElevationConversionRequest(
                    listOf(source),
                    output,
                    ElevationOutputFormat.GEOTIFF,
                    listOf(circle),
                    ElevationExecutionOptions(workerCount = 4, bufferBytesPerWorker = 256 * 1024),
                ),
            )
            val metadata = ElevationMetadataReader.read(output, InputFormat.ELEVATION_GEOTIFF)
            val longitudeStep = (metadata.bounds.east - metadata.bounds.west) / metadata.width
            val latitudeStep = (metadata.bounds.north - metadata.bounds.south) / metadata.height
            var expectedPopulated = 0L

            ImageIO.createImageInputStream(output.toFile()).use { input ->
                val reader = ImageIO.getImageReaders(input).asSequence().first()
                try {
                    reader.input = input
                    val raster = reader.read(0).raster
                    for (row in 0 until metadata.height) {
                        val latitude = metadata.bounds.north - (row + 0.5) * latitudeStep
                        for (column in 0 until metadata.width) {
                            val longitude = metadata.bounds.west + (column + 0.5) * longitudeStep
                            val expected = circle.contains(latitude, longitude)
                            val actual = raster.getSampleFloat(column, row, 0) != -32768.0f
                            assertEquals(expected, actual, "Unexpected circle mask at row=$row, column=$column")
                            if (expected) expectedPopulated++
                        }
                    }
                } finally {
                    reader.dispose()
                }
            }

            assertEquals(expectedPopulated, report.populatedCells)
        } finally {
            directory.toFile().deleteRecursively()
        }
    }

    @Test
    fun `cancelled parallel GeoTIFF export removes its partial output`() {
        val directory = createTempDirectory("elevation-cancel-")
        try {
            val source = directory.resolve("source.asc").also {
                writeGrid(it, 0.01, List(200) { "${List(200) { 123 }.joinToString(" ")}" })
            }
            val output = directory.resolve("cancelled.tif")
            val checks = AtomicInteger()

            assertFailsWith<InterruptedException> {
                ElevationConverter.convert(
                    request = ElevationConversionRequest(
                        listOf(source),
                        output,
                        ElevationOutputFormat.GEOTIFF,
                        listOf(GeoRectangleRegion(GeoBounds(0.0, 0.0, 2.0, 2.0))),
                        ElevationExecutionOptions(workerCount = 4, bufferBytesPerWorker = 256 * 1024),
                    ),
                    isCancelled = { checks.incrementAndGet() > 2 },
                )
            }

            assertTrue(!output.exists())
            assertTrue(!output.resolveSibling("${output.fileName}.partial").exists())
        } finally {
            directory.toFile().deleteRecursively()
        }
    }

    private fun writeGrid(path: java.nio.file.Path, cellSize: Double, rows: List<String>) {
        path.writeText(
            """
            ncols ${rows.first().split(' ').size}
            nrows ${rows.size}
            xllcorner 0
            yllcorner 0
            cellsize $cellSize
            NODATA_value -9999
            ${rows.joinToString("\n")}
            """.trimIndent() + "\n",
        )
    }

    private fun assertRoundTripValues(
        output: java.nio.file.Path,
        source: java.nio.file.Path,
        area: List<GeoRectangleRegion>,
    ) {
        ElevationConverter.convert(
            ElevationConversionRequest(listOf(source), output, ElevationOutputFormat.ESRI_ASCII_GRID, area),
        )
        val values = output.readLines().drop(6).flatMap { it.split(' ') }.filter { it != "-32768" }
        assertTrue(values.isNotEmpty())
        assertTrue(values.all { it == "123" })
    }
}
