package ballistics.zrv

import ballistics.orbitalPoints.AbstractOrbPointsCalculator
import ballistics.types.*
import ballistics.utils.earth.getEarth
import ballistics.utils.math.Vector3D
import ballistics.utils.math.equations.EquationCalculator2Div
import ballistics.utils.math.minmax.MinMaxGoldenSectCalculator
import reactor.core.publisher.Flux
import reactor.core.publisher.FluxSink
import java.io.IOException
import kotlin.math.PI
import kotlin.math.atan
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt

internal class ZRVStepperCalculatorAsync(
    private val opc: AbstractOrbPointsCalculator,
    ppi: List<PPI>,
) {

    private val ppiExt: List<PPIExt>
    private var step = 60.0
    private val earth = getEarth(opc.earthType)

    init {
        ppiExt =
            ppi.map { p ->
                PPIExt().apply {
                    params.nip = p.ppiNum
                    params.au = p.auNum
                    params.sb = sin(p.lat)
                    params.cb = cos(p.lat)
                    params.sl = sin(p.long)
                    params.cl = cos(p.long)
                    elevMax = p.elevMax
                    elevMin = p.elevMin
                    val r = earth.blh2xyz(p.lat, p.long, p.height)
                    params.xn = r.x
                    params.yn = r.y
                    params.zn = r.z
                    params.bn = p.lat
                    shadowMin = p.shadowMin
                    shadowMax = p.shadowMax
                }
            }
    }

    private fun gskToPointSK(
        point: OrbitalPoint,
        ppi: PPIExt,
    ): Vector3D {
        val r1 =
            Vector3D(
                point.r.z - ppi.params.zn,
                point.r.x - ppi.params.xn,
                point.r.y - ppi.params.yn,
            )
        val r4 = r1.y * ppi.params.cl + r1.z * ppi.params.sl

        return Vector3D(
            ppi.params.cb * r1.x - ppi.params.sb * r4,
            ppi.params.sb * r1.x + ppi.params.cb * r4,
            ppi.params.cl * r1.z - ppi.params.sl * r1.y,
        )
    }

    private fun elevationAngle(
        t: Double,
        ppi: PPIExt,
    ): Double {
        val stepper = opc.getStepper()
        val point = stepper.calculate(t)
        val r = gskToPointSK(point!!, ppi)

        return atan(r.y / sqrt(r.z * r.z + r.x * r.x))
    }

    private fun calcZoneParams(
        t: Double,
        ppi: PPIExt,
    ): VisibilityParametersZRV {
        val z = VisibilityParametersZRV()
        val stepper = opc.getStepper()
        val point: OrbitalPoint = stepper.calculate(t) ?: return z
        val r = gskToPointSK(point, ppi)

        z.t = t
        z.elevation = atan(r.y / sqrt(r.x * r.x + r.z * r.z))
        z.range = r.module()
        z.azimuth = atan2(r.z, r.x)

        if (z.azimuth < 0) {
            z.azimuth += 2 * PI
        }

        return z
    }

    private fun checkZone(ppi : PPIExt) =
        (ppi.elevMax < ppi.zoneMax.elevation) ||
                ((ppi.shadowMin?.count() ?: 0) != 0) ||
                ((ppi.shadowMax?.count() ?: 0) != 0)



    private fun parceZone(
        sink: FluxSink<ZRV>,
        ppi: PPIExt
    ){
        var tn: Double? = null
        var inZone = false
        var inZoneNext = false
        var vzp = calcZoneParams(ppi.zoneIn.t, ppi)

        if (ppi.isVisible(vzp.azimuth, vzp.elevation)) {
            tn = ppi.zoneIn.t
            inZone = true
        }

        val step = 0.1
        var t = ppi.zoneIn.t + step

        while (t <= ppi.zoneOut.t) {
            vzp = calcZoneParams(t, ppi)
            inZoneNext = ppi.isVisible(vzp.azimuth, vzp.elevation)

            // конец видимости
            if (inZone && !inZoneNext) {
                sink.next(
                    ZRV(
                        ppi.params.nip,
                        ppi.params.au,
                        ppi.vit,
                        calcZoneParams(tn!!, ppi),
                        VisibilityParametersZRV(ppi.zoneMax.t, ppi.zoneMax.range, ppi.zoneMax.azimuth, ppi.zoneMax.elevation),
                        calcZoneParams(t - step, ppi),
                    ),
                )
            }

            // начало видимости
            if (!inZone && inZoneNext) {
                tn = t
            }

            inZone = inZoneNext
            t += step
        }

        if (inZone && inZoneNext) {
            sink.next(
                ZRV(
                    ppi.params.nip,
                    ppi.params.au,
                    ppi.vit,
                    calcZoneParams(tn!!, ppi),
                    VisibilityParametersZRV(ppi.zoneMax.t, ppi.zoneMax.range, ppi.zoneMax.azimuth, ppi.zoneMax.elevation),
                    calcZoneParams(ppi.zoneOut.t, ppi),
                ),
            )
        }
    }


    private fun addZone(
        sink: FluxSink<ZRV>,
        ppi: PPIExt,
    ) {
        // разбиение зоны при наличии затенений
        if ( checkZone(ppi) ) {
            parceZone(sink, ppi)
        } else {
            sink.next(
                ZRV(
                    ppi.params.nip,
                    ppi.params.au,
                    ppi.vit,
                    calcZoneParams(ppi.zoneIn.t, ppi),
                    calcZoneParams(ppi.zoneMax.t, ppi),
                    calcZoneParams(ppi.zoneOut.t, ppi),
                ),
            )
        }
    }

    private fun calculateViewParams(
        point: OrbitalPoint,
        ppi: PPIExt,
    ) {
        ppi.t2 = point.t

        val r1 = point.v.x * ppi.params.cl + point.v.y * ppi.params.sl
        ppi.vy2 = point.v.z * ppi.params.sb + r1 * ppi.params.cb

        val r = gskToPointSK(point, ppi)
        ppi.gam2 = atan2(r.y, sqrt(r.x * r.x + r.z * r.z))
        ppi.kaY = r.y
        ppi.a2 = atan2(r.z, r.x)

        if (ppi.a2 < 0) {
            ppi.a2 += 2 * PI
        }
    }

    fun calculate(
        tn: Double,
        tk: Double,
    ): Flux<ZRV> {
        if (ppiExt.isEmpty()) {
            return Flux.error(IOException()) // replace with custom exception
        }

        var gam1 : Double
        var gam2 : Double

        step = 60.0

        val eqc = EquationCalculator2Div()
        val minmax = MinMaxGoldenSectCalculator()

        minmax.sign = -1
        minmax.delta = 0.0001

        eqc.delta = 0.001 * PI / 180.0
        eqc.maxIterations = 50

        val stepper = opc.getStepper()
        var ct = tn

        return Flux.create { sink ->
            // первая точка интервала расчета
            var point = stepper.calculate(tn) ?: return@create sink.error(IOException())

            ppiExt.forEach {
                calculateViewParams(point, it)

                if (it.gam2 > it.elevMin) {
                    it.zoneIn.t = point.t
                    it.vit = point.vit
                }
            }

            ct += step
            while (ct < tk) {
                point = stepper.calculate(ct) ?: return@create sink.error(IOException())

                ppiExt.forEach { innerPpiLoop ->
                    innerPpiLoop.gam1 = innerPpiLoop.gam2
                    innerPpiLoop.t1 = innerPpiLoop.t2
                    innerPpiLoop.vy1 = innerPpiLoop.vy2
                    innerPpiLoop.a1 = innerPpiLoop.a2

                    calculateViewParams(point, innerPpiLoop)

                    gam1 = innerPpiLoop.gam1
                    gam2 = innerPpiLoop.gam2
                    // начало зоны
                    if (((gam1 < innerPpiLoop.elevMin) && (gam2 > innerPpiLoop.elevMin)) ||
                        (gam1 > innerPpiLoop.elevMin && gam2 > innerPpiLoop.elevMin && innerPpiLoop.vit == 0)
                    ) {
                        eqc.value = innerPpiLoop.elevMin
                        var t =
                            eqc.calculate(innerPpiLoop.t1, innerPpiLoop.t2) {
                                elevationAngle(it, innerPpiLoop)
                            }

                        if (t != null) {
                            var buf = calcZoneParams(t, innerPpiLoop)
                            var it = 0

                            while (buf.elevation <= innerPpiLoop.elevMin && it < 15) {
                                t += 0.005
                                buf = calcZoneParams(t, innerPpiLoop)
                                ++it
                            }

                            innerPpiLoop.zoneIn.t = t
                            innerPpiLoop.vit = point.vit
                        } else {
                            println(
                                """
                                Ошибка поиска момента входа в ЗРВ. 
                                ${innerPpiLoop.gam1 * 180 / PI} - ${innerPpiLoop.elevMin * 180 / PI} - ${innerPpiLoop.gam2 * 180 / PI}
                                """.trimIndent(),
                            )
                            innerPpiLoop.zoneIn.t = innerPpiLoop.t2
                        }
                    }

                    // / максимум!
                    if ((innerPpiLoop.vy1 * innerPpiLoop.vy2 < 0) &&
                        (innerPpiLoop.gam1 > innerPpiLoop.elevMin) &&
                        (innerPpiLoop.kaY > 0)
                    ) {
                        val tmax =
                            minmax.calculate(innerPpiLoop.t1, innerPpiLoop.t2) {
                                elevationAngle(it, innerPpiLoop)
                            }

                        if (tmax == null) {
                            println("Ошибка при поиске максимума ЗРВ")
                            innerPpiLoop.zoneMax = calcZoneParams((innerPpiLoop.t1 + innerPpiLoop.t2) / 2, innerPpiLoop)
                        } else {
                            innerPpiLoop.zoneMax = calcZoneParams(tmax, innerPpiLoop)
                        }
                    }
                    // конец зоны
                    if ((
                            (innerPpiLoop.gam1 > innerPpiLoop.elevMin) &&
                                (gam2 < innerPpiLoop.elevMin && innerPpiLoop.vit != 0)
                        ) ||
                        (
                            gam1 < innerPpiLoop.elevMin && gam2 < innerPpiLoop.elevMin &&
                                innerPpiLoop.vit != 0
                        )
                    ) {
                        eqc.value = innerPpiLoop.elevMin

                        var t =
                            eqc.calculate(innerPpiLoop.t1, innerPpiLoop.t2) {
                                elevationAngle(it, innerPpiLoop)
                            }

                        if (t != null) {
                            var buf = calcZoneParams(t, innerPpiLoop)
                            var it = 0

                            while (buf.elevation <= innerPpiLoop.elevMin && it < 15) {
                                t -= 0.005
                                buf = calcZoneParams(t, innerPpiLoop)
                                ++it
                            }

                            innerPpiLoop.zoneOut.t = t
                        } else {
                            println("Ошибка поиска момента выхода в ЗРВ.")
                            innerPpiLoop.zoneOut.t = innerPpiLoop.t1
                        }

                        if (innerPpiLoop.zoneOut.t * innerPpiLoop.zoneIn.t > 1) {
                            addZone(sink, innerPpiLoop)
                        } else {
                            println(
                                "Ошибка расчета граничной точки зоны ${innerPpiLoop.params.nip}    ${innerPpiLoop.zoneIn.t}   ${
                                    innerPpiLoop.zoneOut.t
                                }   ",
                            )
                        }

                        innerPpiLoop.vit = 0
                    }
                }

                ct += step
            }

            sink.complete()
        }
    }
}
