package org.nstart.dep265.tletools.zeptomoby.core

import org.nstart.dep265.tletools.tools.get
import org.nstart.dep265.tletools.tools.isParsableToNum
import org.nstart.dep265.tletools.tools.set
import org.nstart.dep265.tletools.tools.substringByLength
import org.nstart.dep265.tletools.zeptomoby.core.TLE.ProtectedStatic.convertUnits

@Suppress("unused")
open class TLE {

    object TLEPartLength {
        const val TLE_LEN_LINE_DATA      = 69; const val TLE_LEN_LINE_NAME      = 24

        const val TLE1_COL_SATNUM        =  2; const val TLE1_LEN_SATNUM        =  5
        const val TLE1_COL_INTLDESC_A    =  9; const val TLE1_LEN_INTLDESC_A    =  2
        const val TLE1_COL_INTLDESC_B    = 11; const val TLE1_LEN_INTLDESC_B    =  3
        const val TLE1_COL_INTLDESC_C    = 14; const val TLE1_LEN_INTLDESC_C    =  3
        const val TLE1_COL_EPOCH_A       = 18; const val TLE1_LEN_EPOCH_A       =  2
        const val TLE1_COL_EPOCH_B       = 20; const val TLE1_LEN_EPOCH_B       = 12
        const val TLE1_COL_MEANMOTIONDT  = 33; const val TLE1_LEN_MEANMOTIONDT  = 10
        const val TLE1_COL_MEANMOTIONDT2 = 44; const val TLE1_LEN_MEANMOTIONDT2 =  8
        const val TLE1_COL_BSTAR         = 53; const val TLE1_LEN_BSTAR         =  8
        const val TLE1_COL_EPHEMTYPE     = 62; const val TLE1_LEN_EPHEMTYPE     =  1
        const val TLE1_COL_ELNUM         = 64; const val TLE1_LEN_ELNUM         =  4

        const val TLE2_COL_SATNUM        = 2;  const val TLE2_LEN_SATNUM        =  5
        const val TLE2_COL_INCLINATION   = 8;  const val TLE2_LEN_INCLINATION   =  8
        const val TLE2_COL_RAASCENDNODE  = 17; const val TLE2_LEN_RAASCENDNODE  =  8
        const val TLE2_COL_ECCENTRICITY  = 26; const val TLE2_LEN_ECCENTRICITY  =  7
        const val TLE2_COL_ARGPERIGEE    = 34; const val TLE2_LEN_ARGPERIGEE    =  8
        const val TLE2_COL_MEANANOMALY   = 43; const val TLE2_LEN_MEANANOMALY   =  8
        const val TLE2_COL_MEANMOTION    = 52; const val TLE2_LEN_MEANMOTION    = 11
        const val TLE2_COL_REVATEPOCH    = 63; const val TLE2_LEN_REVATEPOCH    =  5
    }

    object Static {
        fun isValidLine(str: String, num: org.nstart.dep265.tletools.zeptomoby.core.TLE.Lines): Boolean {
            val testStr = str.trim()

            return if (num == org.nstart.dep265.tletools.zeptomoby.core.TLE.Lines.LINE_ZERO) {
                testStr.length <= org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE_LEN_LINE_NAME
            } else {
                testStr.length == org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE_LEN_LINE_DATA &&
                str[0].digitToInt() == num.ordinal &&
                str[1] == ' '
            }
        }
    }

    protected object ProtectedStatic {
        fun expToAtof(exp: String): String {
            val colSign = 0
            val lenSign = 1

            val colMantissa = 1
            val lenMantissa = 5

            val colExponent = 6
            val lenExponent = 2

            val strBuilder = StringBuilder()
            strBuilder.append(exp.substringByLength(colSign, lenSign))
            strBuilder.append("0.")
            strBuilder.append(exp.substringByLength(colMantissa, lenMantissa))
            strBuilder.append("e")
            strBuilder.append(exp.substringByLength(colExponent, lenExponent).trimStart())

            return strBuilder.toString()
        }

        fun convertUnits(value: Double, fld: org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields, units: org.nstart.dep265.tletools.zeptomoby.core.TLE.Units): Double =
            when (fld) {
                org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_I,
                org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_RAAN,
                org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_ARGPER,
                org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_M -> if (units == org.nstart.dep265.tletools.zeptomoby.core.TLE.Units.U_RAD) org.nstart.dep265.tletools.zeptomoby.core.Globals.deg2rad(
                    value
                ) else value
                else -> value
            }

        fun checkSum(cs: String): Int =
            cs.dropLast(1).fold(0) {
                acc: Int, ch: Char ->
                when (true) {
                    ch.isDigit() -> acc + ch.digitToInt()
                    (ch == '-') -> acc + 1
                    else -> acc
                }
            }
    }

    private object PrivateStatic {
        const val strDegrees: String = " degrees"
        const val strRevsPerDay: String = " revs / day"

        fun getUnits(fld: org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields): String =
            when (fld) {
                org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_I,
                org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_RAAN,
                org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_ARGPER,
                org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_M -> org.nstart.dep265.tletools.zeptomoby.core.TLE.PrivateStatic.strDegrees
                org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_MMOTION -> org.nstart.dep265.tletools.zeptomoby.core.TLE.PrivateStatic.strRevsPerDay
                else -> ""
            }
    }

    enum class Lines {
        LINE_ZERO,
        LINE_ONE,
        LINE_TWO
    }

    enum class Fields {
        FLD_NORADNUM,
        FLD_INTLDESC,
        FLD_SET,       // TLE set number
        FLD_EPOCHYEAR, // Epoch: Last two digits of year
        FLD_EPOCHDAY,  // Epoch: Fractional Julian Day of year
        FLD_ORBITNUM,  // Orbit at epoch
        FLD_I,         // Inclination
        FLD_RAAN,      // R.A. ascending node
        FLD_E,         // Eccentricity
        FLD_ARGPER,    // Argument of perigee
        FLD_M,         // Mean anomaly
        FLD_MMOTION,   // Mean motion
        FLD_MMOTIONDT, // First time derivative of mean motion
        FLD_MMOTIONDT2,// Second time derivative of mean motion
        FLD_BSTAR,     // BSTAR Drag
        FLD_LAST       // MUST be last
    }

    enum class Units {
        U_RAD,            // radians
        U_DEG,            // degrees
        U_NATIVE,         // TLE format native units (no conversion)
        U_LAST            // MUST be last
    }

    val header: String
    val first: String
    val second: String

    private var field = arrayOfNulls<String>(org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_LAST.ordinal)
    private var mapCache = mutableMapOf<Int, Double>()

    fun getField(fld: org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields, units: org.nstart.dep265.tletools.zeptomoby.core.TLE.Units = org.nstart.dep265.tletools.zeptomoby.core.TLE.Units.U_NATIVE, bStrUnits: Boolean = false): Pair<Double, String> {
        assert((0 <= fld.ordinal) && (fld < org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_LAST))
        assert((0 <= units.ordinal) && (units < org.nstart.dep265.tletools.zeptomoby.core.TLE.Units.U_LAST))

        val strBuilder = StringBuilder(field[fld])
        if (bStrUnits) strBuilder.append(org.nstart.dep265.tletools.zeptomoby.core.TLE.PrivateStatic.getUnits(fld))
        strBuilder.trim()

        val key = key(units, fld)
        val valueInDouble: Double

        if (!mapCache.contains(key)) {
            val valStr = field[fld.ordinal]

            if (!valStr.isParsableToNum()) valueInDouble = 0.0
            else {
                valueInDouble = convertUnits(valStr!!.toDouble(), fld, units)
                mapCache[key] = valueInDouble
            }
        }
        else valueInDouble = mapCache[key] ?: 0.0

        return Pair(valueInDouble, strBuilder.toString())
    }

    constructor(h: String, f: String, s: String) {
        header = h.trimEnd()
        first = f
        second = s

        initialize()
    }

    constructor(tle: org.nstart.dep265.tletools.zeptomoby.core.TLE) {
        header = tle.header
        first = tle.first
        second = tle.second

        field = tle.field
        mapCache = tle.mapCache
    }

    protected fun initialize() {
        if (field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_NORADNUM]?.isNotEmpty() == true) return

        assert(first.isNotEmpty())
        assert(second.isNotEmpty())

        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_NORADNUM] = first.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_COL_SATNUM,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_SATNUM
        )
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_INTLDESC] = first.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_COL_INTLDESC_A,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_INTLDESC_A + org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_INTLDESC_B + org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_INTLDESC_C
        )
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_EPOCHYEAR] = first.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_COL_EPOCH_A,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_EPOCH_A
        )

        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_EPOCHDAY] = first.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_COL_EPOCH_B,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_EPOCH_B
        )

        if (first[org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_COL_MEANMOTIONDT] == '-') field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_MMOTIONDT] = "-0"
        else field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_MMOTIONDT] = "0"

        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_MMOTIONDT] += first.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_COL_MEANMOTIONDT + 1,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_MEANMOTIONDT
        )

        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_MMOTIONDT2] =
            org.nstart.dep265.tletools.zeptomoby.core.TLE.ProtectedStatic.expToAtof(
                first.substringByLength(
                    org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_COL_MEANMOTIONDT2,
                    org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_MEANMOTIONDT2
                )
            )
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_BSTAR] =
            org.nstart.dep265.tletools.zeptomoby.core.TLE.ProtectedStatic.expToAtof(
                first.substringByLength(
                    org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_COL_BSTAR,
                    org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_BSTAR
                )
            )
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_SET] = first.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_COL_ELNUM,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE1_LEN_ELNUM
        ).trimStart()

        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_I] = second.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_COL_INCLINATION,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_LEN_INCLINATION
        ).trimStart()
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_RAAN] = second.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_COL_RAASCENDNODE,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_LEN_RAASCENDNODE
        ).trimStart()

        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_E] = "0."
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_E] += second.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_COL_ECCENTRICITY,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_LEN_ECCENTRICITY
        )
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_E] = field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_E.ordinal]!!.trimStart()

        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_ARGPER] = second.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_COL_ARGPERIGEE,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_LEN_ARGPERIGEE
        ).trimStart()
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_M] = second.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_COL_MEANANOMALY,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_LEN_MEANANOMALY
        ).trimStart()
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_MMOTION] = second.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_COL_MEANMOTION,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_LEN_MEANMOTION
        ).trimStart()
        field[org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields.FLD_ORBITNUM] = second.substringByLength(
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_COL_REVATEPOCH,
            org.nstart.dep265.tletools.zeptomoby.core.TLE.TLEPartLength.TLE2_LEN_REVATEPOCH
        ).trimStart()
    }

    private fun key(u: org.nstart.dep265.tletools.zeptomoby.core.TLE.Units, f: org.nstart.dep265.tletools.zeptomoby.core.TLE.Fields): Int = (u.ordinal * 100) + f.ordinal
}