package space.nstart.pcp.complan.apl

import org.locationtech.jts.geom.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.lang.Double.max
import java.lang.Double.min
import kotlin.collections.get
import kotlin.compareTo
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.cos

/**
 * Класс построения интервалов съемки
 * Под интервалами съемки полагаются марш
 */
@Service
class IntervalsBuilder(private val writeLog : Boolean = false) {
    /** Логирование*/
    private val log = LoggerFactory.getLogger(this::class.java)
    /** Фабрика geometry*/
    private val geomFactory = GeometryFactory()

    private val erthEcvator = 111.3213777 * (180 / PI)

    /**
     * Создание линии параллельной заданной подвинутой на delX
     * @param delX дельта перемещения по оси Х
     * @param line эталонная линия
     * @return Возвращает линию параллельную эталонной подвинутой на значение delX
     */
    private fun createParallelLine(
        line: LineString,
        delX: Double
    ): LineString {
        val newCoordinates = line.coordinates.map {
            Coordinate(it.x + delX, it.y)
        }.toTypedArray()

        return geomFactory.createLineString(newCoordinates)
    }

    /**
     * Создание полигона параллельного заданному подвинутому на delX
     * @param delX дельта перемещения по оси Х
     * @param pol эталонная полигон
     * @return Возвращает полигон параллельный эталонному подвинутому на значение delX
     */
    private fun createParallelPol(
        pol: Polygon,
        delX: Double
    ): Polygon {
        val newCoordinates = pol.coordinates.map {
            Coordinate(it.x + delX, it.y)
        }.toTypedArray()

        return geomFactory.createPolygon(newCoordinates)
    }

    /**
     * Получение описывающего многоугольника
     * @param geom контур геометрии
     * @return Возвращает описывающий многоугольник [org.locationtech.jts.geom.Envelope]
     */
    private fun getBoxInternal(geom: Geometry) = geom.envelopeInternal

    /**
     * Возвращает расстояние в км на определенной широте
     * @param distanceKm дистанция в км
     * @param lat широта
     * @return Возвращает значение в радианах эквивалентное дистанции в километрах на данной широте
     */
    private fun getCaptureDistanceInKm(distanceKm: Double, lat: Double) = (distanceKm / (erthEcvator * cos(lat / 180 * PI ))) * 180 / PI //.toRad()
    //для градусов, но мы считаем в радианах
//    private fun getCaptureDistanceInKm(distanceKm: Double, lat: Double) = (distanceKm / (erthecvator * cos(lat))).toRad()

    /**
     * Возвращает одно значение из двух, которое по модулю ближе к 0
     * @param a ервое значение
     * @param b второе значение
     * @return Возвращает значение по модулю близкое к 0
     */
    private fun closerToZero(a: Double, b: Double) = if(abs(a) < abs(b)) a else b

    /**
     * Передвижение маршрута ближе к контуру
     * Точность до 1/10 ширины маршрута
     * @param line линия
     * @param contour контур
     * @param offset ширина маршрута
     * @return Возвращает маршрут отступающий от контура не более чем на 0,1 ширины маршрута
     */
    private fun moveBorderMarToContour(
        line: LineString,
        contour: Polygon,
        offset: Double
    ): Polygon {
        var line = line
        var step = offset

        val leftLine = line
        val rightLine = createParallelLine(line, -1 * step) //3
        var pol = geomFactory.createPolygon(createMar(rightLine, leftLine))

        while (true) {
            if (!pol.intersects(contour)) {
//                if(i > 150) throw IllegalArgumentException("Бесконечный цикл нахождения граничных лииний")
                pol = createParallelPol(pol, step)
            } else {
                pol = createParallelPol(pol, -step)
                if (step == offset / 10)
                    break
                step = step / 10
            }
        }
        return pol
    }

    /**
     * Нахождение ширины региона
     * @param borderLines граничные линии для контура
     * @return Возвращает ширину региона между его граничными линиями
     */
    private fun getRegionWidth(borderpols: Pair<Polygon, Polygon>, offset: Double): Double {
        val firstEnvelope = borderpols.first.envelopeInternal
        val secondEnvelope = borderpols.second.envelopeInternal

        return (secondEnvelope.maxX - firstEnvelope.maxX) - 1*offset //3
    }

    /**
     * Нахождение граничных полигонов
     * @param flightLine линия полета
     * @param contour контур региона
     * @param internalBox описывающий многоугольник
     * @param offset ширина маршрута
     * @return Возвращает пару полигонов в порядке: **ЛЕВЫЙ**, **ПРАВЫЙ**
     */
    private fun findBorderPols(
        flightLine: LineString,
        contour: Polygon,
        internalBox: Envelope,
        offset: Double
    ): Pair<Polygon, Polygon> {
        val xUp = LineShifter().getPointOnLineY(flightLine, internalBox.maxY) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
        val xDown = LineShifter()
            .getPointOnLineY(flightLine, internalBox.minY) ?: throw IllegalStateException("Ошибка нахождения граничных точек")

        val minXValue = min(min(internalBox.minX - xUp.x, internalBox.maxX - xUp.x), min(internalBox.minX - xDown.x, internalBox.maxX - xDown.x))
        val maxXValue = max(max(internalBox.minX - xUp.x, internalBox.maxX - xUp.x), max(internalBox.minX - xDown.x, internalBox.maxX - xDown.x))

        val leftLine = LineShifter().lineShift(flightLine, minXValue) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
        val rightLine = LineShifter().lineShift(flightLine, maxXValue) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
        //двигаем линии ближе к контуру региона
        //первая линия
        val firstPol = moveBorderMarToContour(leftLine, contour, offset)
        //последняя линия
        val lastPol = moveBorderMarToContour(rightLine, contour, -offset)

        if (writeLog) { log.info("Граничные точки найдены") }

        return Pair(firstPol, lastPol)
    }

    /**
     * Построение интервалов
     * @param mars список маршрутов
     * @param contour контур региона
     * @return Возвращает список интервалов
     */
    private fun buildIntervals(
        mars: List<Geometry>,
        contour: Polygon
    ): MutableList<Polygon> {
        var intervals = mutableListOf<Polygon>()
        for (mar in mars) {
            val interval = contour.intersection(mar)
            if(interval is Polygon) {
                intervals.add(interval)
            }
            else if(interval is MultiPolygon) {
                for (i in 0 until interval.numGeometries) {
                    intervals.add(interval.getGeometryN(i) as Polygon)
                }
            }
        }

        return intervals
    }

    /**
     * Создание маршрута
     * @param leftLine левая граничная линия маршрута
     * @param rightLine правая граничная линия маршрута
     * @return Возвращает маршрут заключенный между граничными линиями
     */
    private fun createMar(
        rightLine: LineString,
        leftLine: LineString
    ): Array<Coordinate>{
        val coordinates = mutableListOf<Coordinate>()

        for (j in rightLine.coordinates)
            coordinates.add(Coordinate(j.x, j.y))

        for (j in leftLine.coordinates.size - 1 downTo 0)
            coordinates.add(Coordinate(leftLine.coordinates[j].x, leftLine.coordinates[j].y))

        coordinates.add(Coordinate(rightLine.coordinates[0].x, rightLine.coordinates[0].y))
        return coordinates.toTypedArray()
    }

    /**
     * Построение маршрутов
     * @param width ширина региона
     * @param lines граничные линии первого маршрута
     * @param offset ширина маршрута
     * @param overlap перекрытие маршрутов
     * @return Возвращает список маршрутов для покрытия региона
     */
    private fun buildMars(
        width: Double,
        firstPol: Polygon,
        offset: Double,
        overlap: Double
    ): List<Geometry> {
        val offsetWithOverlap = offset - overlap
        //расчет минимального количества маршрутов для накрытия полигона
        var marCount = ((width - offset) / offsetWithOverlap) + 1
        val intMarCount = ceil(marCount)

        //расчет дополнительного перекрытия
        val deltaOverlapMars = ((intMarCount * offsetWithOverlap - width) / (intMarCount - 1))//((intMarCount - marCount) / (intMarCount - 1)) * offset

        val marOffset = offsetWithOverlap - deltaOverlapMars // if(deltaOverlapMars > overlap)  (offset - deltaOverlapMars) else (offset - overlap)

        var mars = mutableListOf<Geometry>()
        var marPol = firstPol
        for (i in 0 until intMarCount.toInt()) {
            if(i == 0){
                marPol = createParallelPol(firstPol, offset)
            }
            else {
                val step = marOffset
                marPol = createParallelPol(marPol, step)
            }

            mars.add(marPol)
        }
        if (writeLog) { log.info("Модель накрытия района выполнена") }
        return mars
    }

    /**
     * Получение интервалов съемки
     * @param contour контур региона
     * @param flightLine линия полета
     * @param offsetKm ширина маршрута в км
     * @param overlapKm значение минимального перекрытия в км
     * @return Возвращает список интервалов для заданных параметров
     */
    fun  getIntervals(
        contour: Polygon,
        flightLine: LineString,
        offsetKm: Double,
        overlapKm: Double
    ): List<Polygon> {
        //TODO(Исправить определение граничных точек района.
        // При построении интервалов некоторая часть района(граничная слева/справа) пропадает.)
        //расчет описывающего прямоугольника
        val internalBox = getBoxInternal(contour)

        if (internalBox.maxY > 80.0 || internalBox.minY < -80.0) {
            return emptyList()
            //TODO("Такой район пока что не обрабатывается, нужно определить способ построения интервалов для таких районов")
        }

        //получаем минимальную широту
        val minY = closerToZero(internalBox.maxY, internalBox.minY)

        //находим перекрытие и ширину маршрута учитывая широту
        val offset = getCaptureDistanceInKm(offsetKm, minY)
        val overlap = getCaptureDistanceInKm(overlapKm, minY)

        //если ширина контура не больше чем ширина маршрута - возвращаем контур как интервал
        if((internalBox.maxX - internalBox.minX) < offset) {
            if (writeLog) { log.info("Контур района менее контура маршрута") }
            return listOf(contour)
        }

        try {
            //ищем граничные линии для контура
            val borderPols = findBorderPols(flightLine, contour, internalBox, offset)

            //строим маршруты
            val mars = buildMars(getRegionWidth(borderPols, offset), borderPols.first, offset, overlap)

            return buildIntervals(mars, contour)
        }catch (e: Exception){
            log.info(e.toString())
            throw e
        }
    }
}
