package de.arxsilex.maptools.converter.core

import kotlin.math.PI
import kotlin.math.cos

/** A user-visible selection shape. Coordinates stay normalized so editing remains source independent. */
sealed interface MapSelectionShape {
    fun toGeoRegion(sourceBounds: GeoBounds): GeoSelectionRegion
    fun normalizedEnvelope(sourceBounds: GeoBounds): NormalizedMapArea
}

data class RectangleSelection(val area: NormalizedMapArea) : MapSelectionShape {
    override fun toGeoRegion(sourceBounds: GeoBounds): GeoSelectionRegion =
        GeoRectangleRegion(sourceBounds.subset(area))

    override fun normalizedEnvelope(sourceBounds: GeoBounds): NormalizedMapArea = area
}

data class CircleSelection(
    val centerX: Float,
    val centerY: Float,
    val radiusKilometers: Double,
) : MapSelectionShape {
    init {
        require(centerX in 0f..1f && centerY in 0f..1f) { "Der Kreismittelpunkt liegt außerhalb der Quelle." }
        require(radiusKilometers.isFinite() && radiusKilometers > 0.0) { "Der Radius muss größer als null sein." }
    }

    override fun toGeoRegion(sourceBounds: GeoBounds): GeoSelectionRegion = GeoCircleRegion(
        centerLatitude = sourceBounds.north - (sourceBounds.north - sourceBounds.south) * centerY,
        centerLongitude = sourceBounds.west + (sourceBounds.east - sourceBounds.west) * centerX,
        radiusKilometers = radiusKilometers,
    )

    override fun normalizedEnvelope(sourceBounds: GeoBounds): NormalizedMapArea {
        val circleBounds = toGeoRegion(sourceBounds).bounds
        val clipped = GeoBounds(
            west = maxOf(sourceBounds.west, circleBounds.west),
            south = maxOf(sourceBounds.south, circleBounds.south),
            east = minOf(sourceBounds.east, circleBounds.east),
            north = minOf(sourceBounds.north, circleBounds.north),
        )
        return sourceBounds.normalizedAreaFor(clipped)
    }

    fun translated(deltaX: Float, deltaY: Float): CircleSelection = copy(
        centerX = (centerX + deltaX).coerceIn(0f, 1f),
        centerY = (centerY + deltaY).coerceIn(0f, 1f),
    )
}

sealed interface GeoSelectionRegion {
    val bounds: GeoBounds
    fun contains(latitude: Double, longitude: Double): Boolean
}

data class GeoRectangleRegion(override val bounds: GeoBounds) : GeoSelectionRegion {
    override fun contains(latitude: Double, longitude: Double): Boolean =
        latitude in bounds.south..bounds.north && longitude in bounds.west..bounds.east
}

data class GeoCircleRegion(
    val centerLatitude: Double,
    val centerLongitude: Double,
    val radiusKilometers: Double,
) : GeoSelectionRegion {
    init {
        require(centerLatitude in -90.0..90.0 && centerLongitude in -180.0..180.0)
        require(radiusKilometers.isFinite() && radiusKilometers > 0.0)
    }

    override val bounds: GeoBounds by lazy {
        val latitudeDelta = radiusKilometers / EARTH_MEAN_RADIUS_KM * 180.0 / PI
        val longitudeDelta = latitudeDelta / cos(centerLatitude * PI / 180.0).coerceAtLeast(0.000_001)
        val west = (centerLongitude - longitudeDelta).coerceAtLeast(-180.0)
        val east = (centerLongitude + longitudeDelta).coerceAtMost(180.0)
        val south = (centerLatitude - latitudeDelta).coerceAtLeast(-90.0)
        val north = (centerLatitude + latitudeDelta).coerceAtMost(90.0)
        GeoBounds(
            west = west,
            south = south,
            east = east.coerceAtLeast(west + 0.000_000_001),
            north = north.coerceAtLeast(south + 0.000_000_001),
        )
    }

    override fun contains(latitude: Double, longitude: Double): Boolean =
        greatCircleKilometers(centerLatitude, centerLongitude, latitude, longitude) <= radiusKilometers
}

fun Iterable<MapSelectionShape>.toGeoRegions(sourceBounds: GeoBounds): List<GeoSelectionRegion> =
    map { it.toGeoRegion(sourceBounds) }

fun Iterable<MapSelectionShape>.normalizedEnvelope(sourceBounds: GeoBounds): NormalizedMapArea {
    val envelopes = map { it.normalizedEnvelope(sourceBounds) }
    require(envelopes.isNotEmpty()) { "Mindestens ein Auswahlbereich ist erforderlich." }
    return NormalizedMapArea(
        left = envelopes.minOf(NormalizedMapArea::left),
        top = envelopes.minOf(NormalizedMapArea::top),
        right = envelopes.maxOf(NormalizedMapArea::right),
        bottom = envelopes.maxOf(NormalizedMapArea::bottom),
    )
}

internal const val EARTH_MEAN_RADIUS_KM = 6_371.0088
