package space.nstart.pcp.pcp_coverage_scheme_service.service

import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.CoordinateSequenceFilter
import org.locationtech.jts.geom.Envelope
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.GeometryCollection
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.geom.LineString
import org.locationtech.jts.geom.LinearRing
import org.locationtech.jts.geom.MultiPolygon
import org.locationtech.jts.geom.Point
import org.locationtech.jts.geom.Polygon
import org.locationtech.jts.io.WKTReader
import org.locationtech.jts.io.WKTWriter
import org.locationtech.jts.operation.buffer.BufferOp
import org.locationtech.jts.operation.buffer.BufferParameters
import org.springframework.stereotype.Component
import space.nstart.pcp.pcp_coverage_scheme_service.configuration.CustomValidationException
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sin

@Component
class GeometrySupport {

    enum class LongitudeModel {
        STANDARD_180,
        POSITIVE_360
    }

    data class StripOrientation(
        val axisX: Double,
        val axisY: Double,
        val normalX: Double,
        val normalY: Double
    )

    private val geometryFactory = GeometryFactory()
    private val wktReader = WKTReader(geometryFactory)
    private val wktWriter = WKTWriter()

    fun resolveLongitudeModel(wkt: String): LongitudeModel {
        if (wkt.isBlank()) {
            throw CustomValidationException("polygonWkt must not be blank")
        }
        val geometry = try {
            wktReader.read(wkt)
        } catch (ex: Exception) {
            throw CustomValidationException("Некорректный WKT полигона: ${ex.message}")
        }
        return resolveLongitudeModel(geometry)
    }

    fun parseAndNormalizeTarget(wkt: String, longitudeModel: LongitudeModel? = null): Geometry {
        if (wkt.isBlank()) {
            throw CustomValidationException("polygonWkt must not be blank")
        }
        val geometry = try {
            wktReader.read(wkt)
        } catch (ex: Exception) {
            throw CustomValidationException("Некорректный WKT полигона: ${ex.message}")
        }
        val normalized = normalizeToLongitudeModel(geometry, longitudeModel ?: resolveLongitudeModel(geometry))
        if (normalized !is Polygon && normalized !is MultiPolygon) {
            throw CustomValidationException("Поддерживаются только Polygon или MultiPolygon")
        }
        return ensureValid(normalized)
    }

    fun toWkt(geometry: Geometry): String =
        if (geometry.isEmpty) "" else wktWriter.write(ensureValid(geometry))

    fun safeIntersection(left: Geometry, right: Geometry): Geometry =
        safeOverlay(left, right) { first, second -> first.intersection(second) }

    fun safeDifference(left: Geometry, right: Geometry): Geometry =
        safeOverlay(left, right) { first, second -> first.difference(second) }

    fun safeUnion(left: Geometry, right: Geometry): Geometry =
        safeOverlay(left, right) { first, second -> first.union(second) }

    fun centroid(geometry: Geometry): Point =
        if (geometry.isEmpty) point(0.0, 0.0) else geometry.centroid

    fun point(x: Double, y: Double): Point =
        geometryFactory.createPoint(Coordinate(x, y))

    fun buildStripOrientation(directionDegrees: Double): StripOrientation {
        val radians = Math.toRadians(directionDegrees)
        val axisX = cos(radians)
        val axisY = sin(radians)
        return StripOrientation(
            axisX = axisX,
            axisY = axisY,
            normalX = -axisY,
            normalY = axisX
        )
    }

    fun projectionRange(geometry: Geometry, orientation: StripOrientation): Pair<Double, Double> {
        if (geometry.isEmpty) return 0.0 to 0.0
        val values = geometry.coordinates.map { project(it.x, it.y, orientation.axisX, orientation.axisY) }
        return (values.minOrNull() ?: 0.0) to (values.maxOrNull() ?: 0.0)
    }

    fun projectPoint(point: Point, orientation: StripOrientation): Double =
        project(point.x, point.y, orientation.axisX, orientation.axisY)

    fun createCorridor(centerLine: List<Coordinate>, width: Double, centerShift: Double = 0.0): Polygon {
        val shifted = shiftCoordinates(centerLine, buildStripOrientation(directionDegrees(centerLine)), centerShift)
        val line = when {
            shifted.isEmpty() -> geometryFactory.createLineString(arrayOf(Coordinate(0.0, 0.0), Coordinate(0.0, 0.0)))
            shifted.size == 1 -> geometryFactory.createLineString(arrayOf(shifted.first(), shifted.first()))
            else -> geometryFactory.createLineString(shifted.toTypedArray())
        }
        val buffered = BufferOp.bufferOp(
            line,
            max(width / 2.0, 0.000001),
            BufferParameters(8, BufferParameters.CAP_FLAT, BufferParameters.JOIN_ROUND, BufferParameters.DEFAULT_MITRE_LIMIT)
        )
        return ensureValid(buffered) as Polygon
    }

    fun shiftPoint(point: Point, orientation: StripOrientation, distance: Double): Point =
        this.point(
            point.x + orientation.normalX * distance,
            point.y + orientation.normalY * distance
        )

    fun directionDegrees(coordinates: List<Coordinate>): Double {
        if (coordinates.size < 2) return 0.0
        val normalized = normalizeTrackCoordinates(coordinates)
        val first = normalized.first()
        val last = normalized.last()
        return Math.toDegrees(atan2(last.y - first.y, last.x - first.x))
    }

    fun normalizeTrackCoordinates(coordinates: List<Coordinate>): List<Coordinate> {
        if (coordinates.isEmpty()) return emptyList()
        val normalized = mutableListOf(Coordinate(coordinates.first()))
        var previousX = coordinates.first().x
        coordinates.drop(1).forEach { coordinate ->
            var adjustedX = coordinate.x
            while (adjustedX - previousX > 180.0) adjustedX -= 360.0
            while (adjustedX - previousX < -180.0) adjustedX += 360.0
            normalized += Coordinate(adjustedX, coordinate.y, coordinate.z)
            previousX = adjustedX
        }
        return normalized
    }

    fun alignTrackCoordinatesToTarget(coordinates: List<Coordinate>, target: Geometry): List<Coordinate> {
        val normalized = normalizeTrackCoordinates(coordinates)
        if (normalized.isEmpty()) return normalized
        val referenceX = target.envelopeInternal.centre().x
        val averageX = normalized.map { it.x }.average()
        val shiftMultiplier = kotlin.math.round((referenceX - averageX) / 360.0)
        val shift = shiftMultiplier * 360.0
        if (shift == 0.0) return normalized
        return normalized.map { coordinate ->
            Coordinate(coordinate.x + shift, coordinate.y, coordinate.z)
        }
    }

    fun distance(first: Coordinate, second: Coordinate): Double {
        val dx = second.x - first.x
        val dy = second.y - first.y
        return kotlin.math.sqrt(dx * dx + dy * dy)
    }

    fun boundaryTouchArea(coverage: Geometry, target: Geometry, orientation: StripOrientation, buffer: Double): Double {
        val bands = boundaryBands(target, buffer)
        return bands.maxOf { (_, band) -> safeIntersection(coverage, band).area }
    }

    fun frontierGeometry(target: Geometry, covered: Geometry): Geometry {
        if (covered.isEmpty) return ensureValid(target.boundary.buffer(0.0001))
        val uncovered = safeDifference(target, covered)
        if (uncovered.isEmpty) return geometryFactory.createPolygon()
        return ensureValid(safeIntersection(covered.buffer(0.0001), uncovered.buffer(0.0001)))
    }

    fun uncoveredFrontierGeometry(target: Geometry, covered: Geometry): Geometry {
        val uncovered = safeDifference(target, covered)
        if (uncovered.isEmpty) return geometryFactory.createPolygon()
        return ensureValid(safeIntersection(uncovered, frontierGeometry(target, covered).buffer(0.0001)))
    }

    fun startBoundaryLabel(coverage: Geometry, target: Geometry, orientation: StripOrientation): String {
        val bands = boundaryBands(target, 0.05)
        return bands.maxByOrNull { (_, band) -> safeIntersection(coverage, band).area }?.first ?: "INTERIOR"
    }

    fun shiftCoordinates(centerLine: List<Coordinate>, orientation: StripOrientation, distance: Double): List<Coordinate> =
        centerLine.map { coordinate ->
            Coordinate(
                coordinate.x + orientation.normalX * distance,
                coordinate.y + orientation.normalY * distance,
                coordinate.z
            )
        }

    private fun boundaryBands(target: Geometry, buffer: Double): List<Pair<String, Geometry>> {
        val envelope = target.envelopeInternal
        val dx = max(buffer, (envelope.width * 0.02).coerceAtLeast(0.0001))
        val dy = max(buffer, (envelope.height * 0.02).coerceAtLeast(0.0001))
        return listOf(
            "WEST" to geometryFactory.toGeometry(Envelope(envelope.minX - dx, envelope.minX + dx, envelope.minY - dy, envelope.maxY + dy)),
            "EAST" to geometryFactory.toGeometry(Envelope(envelope.maxX - dx, envelope.maxX + dx, envelope.minY - dy, envelope.maxY + dy)),
            "SOUTH" to geometryFactory.toGeometry(Envelope(envelope.minX - dx, envelope.maxX + dx, envelope.minY - dy, envelope.minY + dy)),
            "NORTH" to geometryFactory.toGeometry(Envelope(envelope.minX - dx, envelope.maxX + dx, envelope.maxY - dy, envelope.maxY + dy))
        )
    }

    private fun ensureValid(geometry: Geometry): Geometry =
        if (geometry.isValid) geometry else geometry.buffer(0.0)

    private fun safeOverlay(left: Geometry, right: Geometry, operation: (Geometry, Geometry) -> Geometry): Geometry =
        try {
            ensureValid(operation(ensureValid(left), ensureValid(right)))
        } catch (_: Exception) {
            ensureValid(operation(left.buffer(0.0), right.buffer(0.0)))
        }

    private fun project(x: Double, y: Double, axisX: Double, axisY: Double): Double = x * axisX + y * axisY

    private fun resolveLongitudeModel(geometry: Geometry): LongitudeModel {
        val longitudes = geometry.coordinates.map { normalizeToStandardLongitude(it.x) }
        val negativeMin = longitudes.filter { it < 0.0 }.minOrNull()
        val positiveMax = longitudes.filter { it >= 0.0 }.maxOrNull()
        val crossesZeroMeridian = negativeMin != null && positiveMax != null && positiveMax - negativeMin < 180.0
        return if (crossesZeroMeridian) LongitudeModel.STANDARD_180 else LongitudeModel.POSITIVE_360
    }

    private fun normalizeToLongitudeModel(geometry: Geometry, longitudeModel: LongitudeModel): Geometry {
        val normalized = when (longitudeModel) {
            LongitudeModel.STANDARD_180 -> shiftLongitudesToStandardBand(geometry.copy())
            LongitudeModel.POSITIVE_360 -> shiftNegativeLongitudesTo360(geometry.copy())
        }
        return when (normalized) {
            is Polygon -> normalizePolygon(normalized, longitudeModel)
            is MultiPolygon -> normalizeMultiPolygon(normalized, longitudeModel)
            else -> normalized
        }.let { ensureValid(it) }
    }

    private fun shiftNegativeLongitudesTo360(geometry: Geometry): Geometry {
        geometry.apply(object : CoordinateSequenceFilter {
            override fun filter(seq: org.locationtech.jts.geom.CoordinateSequence, i: Int) {
                val lon = seq.getX(i)
                if (lon < 0.0) {
                    seq.setOrdinate(i, 0, lon + 360.0)
                }
            }

            override fun isDone(): Boolean = false
            override fun isGeometryChanged(): Boolean = true
        })
        geometry.geometryChanged()
        return geometry
    }

    private fun shiftLongitudesToStandardBand(geometry: Geometry): Geometry {
        geometry.apply(object : CoordinateSequenceFilter {
            override fun filter(seq: org.locationtech.jts.geom.CoordinateSequence, i: Int) {
                seq.setOrdinate(i, 0, normalizeToStandardLongitude(seq.getX(i)))
            }

            override fun isDone(): Boolean = false
            override fun isGeometryChanged(): Boolean = true
        })
        geometry.geometryChanged()
        return geometry
    }

    private fun normalizePolygon(polygon: Polygon, longitudeModel: LongitudeModel): Geometry {
        val shell = unwrapRing(polygon.exteriorRing)
        val holes = Array(polygon.numInteriorRing) { index -> unwrapRing(polygon.getInteriorRingN(index)) }
        val normalized = ensureValid(polygon.factory.createPolygon(shell, holes))
        return when (longitudeModel) {
            LongitudeModel.STANDARD_180 -> shiftIntoStandardBand(normalized)
            LongitudeModel.POSITIVE_360 -> shiftIntoPositiveBand(normalized)
        }
    }

    private fun normalizeMultiPolygon(multiPolygon: MultiPolygon, longitudeModel: LongitudeModel): Geometry {
        val polygons = mutableListOf<Polygon>()
        for (index in 0 until multiPolygon.numGeometries) {
            collectPolygons(normalizePolygon(multiPolygon.getGeometryN(index) as Polygon, longitudeModel), polygons)
        }
        return when (polygons.size) {
            0 -> geometryFactory.createPolygon()
            1 -> polygons.first()
            else -> geometryFactory.createMultiPolygon(polygons.toTypedArray())
        }
    }

    private fun collectPolygons(geometry: Geometry, polygons: MutableList<Polygon>) {
        when (geometry) {
            is Polygon -> polygons += geometry
            is MultiPolygon -> {
                for (index in 0 until geometry.numGeometries) {
                    polygons += geometry.getGeometryN(index) as Polygon
                }
            }
            is GeometryCollection -> {
                for (index in 0 until geometry.numGeometries) {
                    collectPolygons(geometry.getGeometryN(index), polygons)
                }
            }
        }
    }

    private fun shiftIntoPositiveBand(geometry: Geometry): Geometry {
        val minX = geometry.coordinates.minOfOrNull { it.x } ?: return geometry
        if (minX >= 0.0) return geometry
        val shift = 360.0 * (floor(-minX / 360.0) + 1.0)
        return shiftLongitude(geometry, shift)
    }

    private fun shiftIntoStandardBand(geometry: Geometry): Geometry {
        val centerX = geometry.envelopeInternal.centre().x
        val shift = -360.0 * floor((centerX + 180.0) / 360.0)
        return shiftLongitude(geometry, shift)
    }

    private fun shiftLongitude(geometry: Geometry, delta: Double): Geometry {
        if (delta == 0.0) return geometry
        val shifted = geometry.copy()
        shifted.apply(object : CoordinateSequenceFilter {
            override fun filter(seq: org.locationtech.jts.geom.CoordinateSequence, i: Int) {
                seq.setOrdinate(i, 0, seq.getX(i) + delta)
            }

            override fun isDone(): Boolean = false
            override fun isGeometryChanged(): Boolean = true
        })
        shifted.geometryChanged()
        return shifted
    }

    private fun unwrapRing(ring: LineString): LinearRing {
        val source = ring.coordinates
        if (source.isEmpty()) return ring.factory.createLinearRing()
        val unwrapped = ArrayList<Coordinate>(source.size)
        var previousX = source.first().x
        unwrapped.add(Coordinate(source.first().x, source.first().y, source.first().z))
        for (index in 1 until source.size - 1) {
            val current = source[index]
            var adjustedX = current.x
            while (adjustedX - previousX > 180.0) adjustedX -= 360.0
            while (adjustedX - previousX < -180.0) adjustedX += 360.0
            unwrapped += Coordinate(adjustedX, current.y, current.z)
            previousX = adjustedX
        }
        val first = unwrapped.first()
        unwrapped += Coordinate(first.x, first.y, first.z)
        return ring.factory.createLinearRing(unwrapped.toTypedArray())
    }

    private fun normalizeToStandardLongitude(longitude: Double): Double {
        var normalized = longitude
        while (normalized > 180.0) normalized -= 360.0
        while (normalized <= -180.0) normalized += 360.0
        return normalized
    }
}
