package ballistics.orbitalPoints.timeStepper

import ballistics.types.EarthType
import ballistics.types.OrbitalPoint
import ballistics.utils.astro.AstronomerJ2000
import ballistics.utils.math.Vector3D
import org.nstart.dep265.tletools.zeptomoby.core.Julian
import org.nstart.dep265.tletools.zeptomoby.core.TLE
import org.nstart.dep265.tletools.zeptomoby.orbit.Satellite
import java.util.Calendar
import java.util.GregorianCalendar
import java.util.TimeZone
import kotlin.math.floor
import kotlin.math.min

class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractStepper {
    val astro = AstronomerJ2000(earthType)
    val tleParser: TLE = TLE("", str1, str2)
    val satellite: Satellite = Satellite(tleParser)
    val baseEpoch: Double
    private val nextAscendingNodeAfterEpoch: Double
    private val ascendingNodeCache = mutableMapOf<Int, Double>()

    init {
        baseEpoch = extractUTCMillis(satellite) / 1000.0
        nextAscendingNodeAfterEpoch = findNextAscendingNodeAfterEpoch()
        ascendingNodeCache[0] = nextAscendingNodeAfterEpoch
    }

    override fun clear() {

    }

    private fun extractUTCMillis(
        year: Int,
        month: Int,
        dayOfMonth: Double,
    ): Long {
        val gc = GregorianCalendar()
        gc[Calendar.YEAR] = year
        gc[Calendar.MONTH] = month - 1
        gc[Calendar.DAY_OF_MONTH] = dayOfMonth.toInt()

        gc.timeZone = TimeZone.getTimeZone("UTC")

        var dfrac = dayOfMonth - dayOfMonth.toLong()
        dfrac *= 24.0

        gc[Calendar.HOUR_OF_DAY] = dfrac.toInt()
        dfrac = 60.0 * (dfrac - dfrac.toInt())

        gc[Calendar.MINUTE] = dfrac.toInt()
        dfrac = 60.0 * (dfrac - dfrac.toInt())

        gc[Calendar.SECOND] = dfrac.toInt()
        dfrac = 1000.0 * (dfrac - dfrac.toInt())

        gc[Calendar.MILLISECOND] = dfrac.toInt()

        return gc.timeInMillis
    }

    private fun extractUTCMillis(date: Julian.DateComponent): Long = extractUTCMillis(date.year, date.mon ?: 1, date.dom ?: 1.0)

    private fun extractUTCMillis(sat: Satellite): Long =
        extractUTCMillis(sat.orbit.epoch.getComponent())

    override fun calculate(t: Double): OrbitalPoint {

        try {
            val utcMillis = baseEpoch
            val dt = t - utcMillis
            val pos = satellite.positionEci(dt / 60)
            val ask =
                OrbitalPoint(
                    t,
                    calculateRevolution(t),
                    Vector3D(
                        pos.position.x * 1000.0,
                        pos.position.y * 1000.0,
                        pos.position.z * 1000.0,
                    ),
                    Vector3D(
                        pos.velocity.x * 1000.0,
                        pos.velocity.y * 1000.0,
                        pos.velocity.z * 1000.0,
                    ),
                )

            return astro.askToGrinvich(ask)
        } catch (ex : Exception){
            throw ex
        }
    }

    private fun calculateRevolution(t: Double): Int {
        var nodeIndex = floor((t - nextAscendingNodeAfterEpoch) / satellite.orbit.period).toInt()

        while (t < ascendingNode(nodeIndex)) {
            --nodeIndex
        }
        while (t >= ascendingNode(nodeIndex + 1)) {
            ++nodeIndex
        }

        return satellite.orbit.orbitNum + nodeIndex + 1
    }

    private fun ascendingNode(index: Int): Double =
        ascendingNodeCache.getOrPut(index) {
            val estimatedNode = nextAscendingNodeAfterEpoch + index * satellite.orbit.period
            findAscendingNodeNear(estimatedNode)
        }

    private fun findNextAscendingNodeAfterEpoch(): Double {
        val scanStep = min(60.0, satellite.orbit.period / 64.0)
        var left = baseEpoch
        var leftZ = eciZ(left)
        var right = left + scanStep
        val scanLimit = baseEpoch + satellite.orbit.period * 2.0

        while (right <= scanLimit) {
            val rightZ = eciZ(right)
            if (leftZ < 0.0 && rightZ >= 0.0) {
                return refineAscendingNode(left, right)
            }
            left = right
            leftZ = rightZ
            right += scanStep
        }

        return baseEpoch + satellite.orbit.period
    }

    private fun findAscendingNodeNear(estimatedNode: Double): Double {
        val halfPeriod = satellite.orbit.period / 2.0
        val scanStep = min(60.0, satellite.orbit.period / 64.0)
        var left = estimatedNode - halfPeriod
        var leftZ = eciZ(left)
        var right = left + scanStep
        val scanLimit = estimatedNode + halfPeriod

        while (right <= scanLimit) {
            val rightZ = eciZ(right)
            if (leftZ < 0.0 && rightZ >= 0.0) {
                return refineAscendingNode(left, right)
            }
            left = right
            leftZ = rightZ
            right += scanStep
        }

        return estimatedNode
    }

    private fun refineAscendingNode(
        intervalStart: Double,
        intervalStop: Double,
    ): Double {
        var left = intervalStart
        var right = intervalStop
        repeat(60) {
            val middle = (left + right) / 2.0
            if (eciZ(middle) < 0.0) {
                left = middle
            } else {
                right = middle
            }
        }
        return (left + right) / 2.0
    }

    private fun eciZ(t: Double): Double =
        satellite.positionEci((t - baseEpoch) / 60.0).position.z

    override fun calculate(
        t: Double,
        p: OrbitalPoint,
    ): OrbitalPoint? {
        return calculate(t)
    }
}
