package de.arxsilex.maptools.converter.desktop

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import de.arxsilex.maptools.converter.core.CircleSelection
import de.arxsilex.maptools.converter.core.GeoBounds
import de.arxsilex.maptools.converter.core.GeoCircleRegion
import kotlin.math.PI
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin

/** A complete geodetic circle projected to the current canvas, without clipping it to the source bounds. */
internal data class CircleScreenGeometry(
    val points: List<Offset>,
    val bounds: Rect,
) {
    fun contains(point: Offset): Boolean {
        if (points.size < 3 || !bounds.contains(point)) return false
        var inside = false
        var previous = points.last()
        points.forEach { current ->
            val crossesHorizontalRay = (current.y > point.y) != (previous.y > point.y)
            if (crossesHorizontalRay) {
                val intersectionX = (previous.x - current.x) * (point.y - current.y) /
                    (previous.y - current.y) + current.x
                if (point.x < intersectionX) inside = !inside
            }
            previous = current
        }
        return inside
    }
}

/**
 * Samples the real WGS-84 selection circumference before projecting it. Using the clipped source
 * envelope here would turn every circle that crosses the map bounds into an oval.
 */
internal fun circleScreenGeometry(
    circle: CircleSelection,
    sourceBounds: GeoBounds,
    transform: MapCanvasTransform,
    segmentCount: Int = DEFAULT_CIRCLE_SEGMENT_COUNT,
): CircleScreenGeometry {
    require(segmentCount >= 16) { "Ein Kreis benötigt mindestens 16 Segmente." }
    val region = circle.toGeoRegion(sourceBounds) as GeoCircleRegion
    val centerLatitude = region.centerLatitude.toRadians()
    val centerLongitude = region.centerLongitude.toRadians()
    val angularRadius = region.radiusKilometers / EARTH_MEAN_RADIUS_KM
    val sinCenterLatitude = sin(centerLatitude)
    val cosCenterLatitude = cos(centerLatitude)
    val sinRadius = sin(angularRadius)
    val cosRadius = cos(angularRadius)

    val points = List(segmentCount) { index ->
        val bearing = 2.0 * PI * index / segmentCount
        val latitude = asin(
            sinCenterLatitude * cosRadius +
                cosCenterLatitude * sinRadius * cos(bearing),
        )
        // Keep the longitude continuous around the source instead of wrapping it at ±180°.
        val longitude = centerLongitude + atan2(
            sin(bearing) * sinRadius * cosCenterLatitude,
            cosRadius - sinCenterLatitude * sin(latitude),
        )
        transform.geoToScreen(longitude.toDegrees(), latitude.toDegrees())
    }
    return CircleScreenGeometry(
        points = points,
        bounds = Rect(
            left = points.minOf { it.x },
            top = points.minOf { it.y },
            right = points.maxOf { it.x },
            bottom = points.maxOf { it.y },
        ),
    )
}

private fun Double.toRadians(): Double = this * PI / 180.0
private fun Double.toDegrees(): Double = this * 180.0 / PI

private const val EARTH_MEAN_RADIUS_KM = 6_371.0088
private const val DEFAULT_CIRCLE_SEGMENT_COUNT = 128
