///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2023 Jon Beniston, M7RCE <jon@beniston.com>                //
//                                                                               //
// This program is free software; you can redistribute it and/or modify          //
// it under the terms of the GNU General Public License as published by          //
// the Free Software Foundation as version 3 of the License, or                  //
// (at your option) any later version.                                           //
//                                                                               //
// This program is distributed in the hope that it will be useful,               //
// but WITHOUT ANY WARRANTY; without even the implied warranty of                //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                  //
// GNU General Public License V3 for more details.                               //
//                                                                               //
// You should have received a copy of the GNU General Public License             //
// along with this program. If not, see <http://www.gnu.org/licenses/>.          //
///////////////////////////////////////////////////////////////////////////////////

#include <cmath>

#include <CoordTopocentric.h>
#include <CoordGeodetic.h>
#include <Observer.h>
#include <SGP4.h>

#include <QTimeZone>

#include "util/units.h"

#include "satellitetrackersgp4.h"

using namespace libsgp4;

// Convert QGP4 DateTime to Qt QDataTime
static QDateTime dateTimeToQDateTime(DateTime dt)
{
    QDateTime qdt(QDate(dt.Year(), dt.Month(), dt.Day()), QTime(dt.Hour(), dt.Minute(), dt.Second(), (int)(dt.Microsecond()/1000.0)), Qt::UTC);
    return qdt;
}

// Convert Qt QDataTime to QGP4 DateTime
static DateTime qDateTimeToDateTime(QDateTime qdt)
{
    QDateTime utc = qdt.toUTC();
    QDate date = utc.date();
    QTime time = utc.time();
    DateTime dt;
    dt.Initialise(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second(), time.msec() * 1000);
    return dt;
}

// Get ground track
// Throws SatelliteException, DecayedException and TleException
void getGroundTrack(QDateTime dateTime,
                        const QString& tle0, const QString& tle1, const QString& tle2,
                        int steps, bool forward,
                        QList<QGeoCoordinate *>& coordinates,
                        QList<QDateTime *>& coordinateDateTimes)
{
    Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString());
    SGP4 sgp4(tle);
    OrbitalElements ele(tle);
    double periodMins;
    double timeStep;

    // For 3D map, we want to quantize to minutes, so we replace previous
    // position data, rather than insert additional positions alongside the old
    // which can result is the camera view jumping around
#if QT_CONFIG(timezone)
    dateTime = QDateTime(dateTime.date(), QTime(dateTime.time().hour(), dateTime.time().minute()), dateTime.timeZone());
#else
    dateTime = QDateTime(dateTime.date(), QTime(dateTime.time().hour(), dateTime.time().minute()));
#endif

    // Note 2D map doesn't support paths wrapping around Earth several times
    // So we just have a slight overlap here, with the future track being longer
    DateTime currentTime = qDateTimeToDateTime(dateTime);
    DateTime endTime;
    if (forward)
    {
        periodMins = ele.Period() * 0.9;
        endTime = currentTime.AddMinutes(periodMins);
        timeStep = periodMins / (steps * 0.9);
    }
    else
    {
        periodMins = ele.Period() * 0.4;
        endTime = currentTime.AddMinutes(-periodMins);
        timeStep = -periodMins / (steps * 0.4);
    }

    // Quantize time step to 30 seconds
    timeStep *= 2.0;
    if (timeStep > 0.0) {
        timeStep = std::max(timeStep, 1.0);
    } else if (timeStep < 0.0) {
        timeStep = std::min(timeStep, -1.0);
    }
    timeStep = round(timeStep);
    timeStep /= 2.0;

    while ((forward && (currentTime < endTime)) || (!forward && (currentTime > endTime)))
    {
        // Calculate satellite position
        Eci eci = sgp4.FindPosition(currentTime);

        // Convert satellite position to geodetic coordinates (lat and long)
        CoordGeodetic geo = eci.ToGeodetic();

        QGeoCoordinate *coord = new QGeoCoordinate(Units::radiansToDegrees(geo.latitude),
                                                   Units::radiansToDegrees(geo.longitude),
                                                   geo.altitude * 1000.0);
        coordinates.append(coord);

        QDateTime *coordDateTime = new QDateTime(dateTimeToQDateTime(currentTime));
        coordinateDateTimes.append(coordDateTime);

        // 2D map is stretched at poles, so use finer steps
        if (std::abs(Units::radiansToDegrees(geo.latitude)) >= 70)
            currentTime = currentTime.AddMinutes(timeStep/4);
        else
            currentTime = currentTime.AddMinutes(timeStep);
    }
}

// Find azimuth and elevation points during a pass
void getPassAzEl(QLineSeries* azimuth, QLineSeries* elevation, QLineSeries* polar,
                        const QString& tle0, const QString& tle1, const QString& tle2,
                        double latitude, double longitude, double altitude,
                        const QDateTime& aos, const QDateTime& los)
{
    try
    {
        Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString());
        SGP4 sgp4(tle);
        Observer obs(latitude, longitude, altitude);

        DateTime aosTime = qDateTimeToDateTime(aos);
        DateTime losTime = qDateTimeToDateTime(los);
        DateTime currentTime(aosTime);
        int steps = 150; // Needs to be high enough, so rotator intersect with satellite position

        double timeStep = (losTime - aosTime).TotalSeconds() / steps;
        if (timeStep <= 0.0)
        {
            qDebug() << "getPassAzEl: AOS is the same as or after LOS";
            return;
        }

        while (currentTime <= losTime)
        {
            // Calculate satellite position
            Eci eci = sgp4.FindPosition(currentTime);

            // Calculate angle to satellite from antenna
            CoordTopocentric topo = obs.GetLookAngle(eci);

            // Save azimuth and elevation in series
            QDateTime qdt = dateTimeToQDateTime(currentTime);
            if (azimuth != nullptr)
                azimuth->append(qdt.toMSecsSinceEpoch(), Units::radiansToDegrees(topo.azimuth));
            if (elevation != nullptr)
                elevation->append(qdt.toMSecsSinceEpoch(), Units::radiansToDegrees(topo.elevation));
            if (polar != nullptr)
                polar->append(Units::radiansToDegrees(topo.azimuth), 90.0-Units::radiansToDegrees(topo.elevation));

            currentTime = currentTime.AddSeconds(timeStep);
        }
    }
    catch (SatelliteException& se)
    {
        qDebug() << se.what();
    }
    catch (DecayedException& de)
    {
        qDebug() << de.what();
    }
    catch (TleException& tlee)
    {
        qDebug() << tlee.what();
    }
}

// Get whether a pass passes through 0 degrees
bool getPassesThrough0Deg(const QString& tle0, const QString& tle1, const QString& tle2,
                          double latitude, double longitude, double altitude,
                          QDateTime& aos, QDateTime& los)
{
    try
    {
        Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString());
        SGP4 sgp4(tle);
        Observer obs(latitude, longitude, altitude);

        DateTime aosTime = qDateTimeToDateTime(aos);
        DateTime losTime = qDateTimeToDateTime(los);
        DateTime currentTime(aosTime);
        int steps = 20;

        double timeStep = (losTime - aosTime).TotalSeconds() / steps;

        double prevAz;
        for (int i = 0; i < steps; i++)
        {
            // Calculate satellite position
            Eci eci = sgp4.FindPosition(currentTime);

            // Calculate angle to satellite from antenna
            CoordTopocentric topo = obs.GetLookAngle(eci);

            double az = Units::radiansToDegrees(topo.azimuth);
            if (i == 0)
                prevAz = az;

            // Does it cross 0 degrees?
            if (((prevAz > 270.0) && (az < 90.0)) || ((prevAz < 90.0) && (az >= 270.0)))
                return true;

            prevAz = az;
            currentTime = currentTime.AddSeconds(timeStep);
        }
    }
    catch (SatelliteException& se)
    {
        qDebug() << se.what();
    }
    catch (DecayedException& de)
    {
        qDebug() << de.what();
    }
    catch (TleException& tlee)
    {
        qDebug() << tlee.what();
    }
    return false;
}

// Find maximum elevation in a pass
static double findMaxElevation(Observer& obs1, SGP4& sgp4, const DateTime& aos, const DateTime& los)
{
    Observer obs(obs1.GetLocation());
    bool running;
    double timeStep = (los - aos).TotalSeconds() / 9.0;
    DateTime currentTime(aos);
    DateTime time1(aos);
    DateTime time2(los);
    double maxElevation;

    do
    {
        running = true;
        maxElevation = -INFINITY;
        while (running && (currentTime < time2))
        {
            Eci eci = sgp4.FindPosition(currentTime);
            CoordTopocentric topo = obs.GetLookAngle(eci);
            if (topo.elevation > maxElevation)
            {
                maxElevation = topo.elevation;
                currentTime = currentTime.AddSeconds(timeStep);
                if (currentTime > time2)
                    currentTime = time2;
            }
            else
                running = false;
        }
        time1 = currentTime.AddSeconds(-2.0 * timeStep);
        time2 = currentTime;
        currentTime = time1;
        timeStep = (time2 - time1).TotalSeconds() / 9.0;
    }
    while (timeStep > 1.0);

    return Units::radiansToDegrees(maxElevation);
}

// Find the time at which the satellite crossed the minimum elevation required for AOS or LOS
static DateTime findCrossingPoint(Observer& obs, SGP4& sgp4, const DateTime& initialTime1, const DateTime& initialTime2, double minElevation, bool findingAOS)
{
    bool running;
    int cnt;
    DateTime time1(initialTime1);
    DateTime time2(initialTime2);
    DateTime middleTime;

    running = true;
    cnt = 0;
    while (running && (cnt++ < 16))
    {
        middleTime = time1.AddSeconds((time2 - time1).TotalSeconds() / 2.0);
        Eci eci = sgp4.FindPosition(middleTime);
        CoordTopocentric topo = obs.GetLookAngle(eci);
        if (topo.elevation > minElevation)
        {
            if (findingAOS)
                time2 = middleTime;
            else
                time1 = middleTime;
        }
        else
        {
            if (findingAOS)
                time1 = middleTime;
            else
                time2 = middleTime;
        }
        if ((time2 - time1).TotalSeconds() < 1.0)
        {
            running = false;
            int us = middleTime.Microsecond();
            middleTime = middleTime.AddMicroseconds(-us);
            middleTime = middleTime.AddSeconds(findingAOS ? 1 : -1);
        }
    }
    running = true;
    cnt = 0;
    while (running && (cnt++ < 6))
    {
        Eci eci = sgp4.FindPosition(middleTime);
        CoordTopocentric topo = obs.GetLookAngle(eci);
        if (topo.elevation > minElevation)
            middleTime = middleTime.AddSeconds(findingAOS ? -1 : 1);
        else
            running = false;
    }
    return middleTime;
}

// Find when AOS occurred, by stepping backwards
static DateTime findAOSBackwards(Observer& obs, SGP4& sgp4, DateTime& startTime,
                                int predictionPeriod, double minElevation, bool& aosUnknown)
{
    DateTime previousTime(startTime);
    DateTime currentTime(startTime);
    DateTime endTime(startTime.AddDays(-predictionPeriod));

    while (currentTime >= endTime)
    {
        Eci eci = sgp4.FindPosition(currentTime);
        CoordTopocentric topo = obs.GetLookAngle(eci);
        if (topo.elevation < minElevation)
        {
            aosUnknown = false;
            return findCrossingPoint(obs, sgp4, currentTime, previousTime, minElevation, true);
        }
        previousTime = currentTime;
        currentTime = currentTime - TimeSpan(0, 0, 180);
    }
    aosUnknown = true;
    return currentTime;
}

bool inPassWindow(DateTime dateTime, QTime passStartTime, QTime passEndTime, bool utc)
{
    // Don't compare seconds as not currently settable in GUI
    QDateTime qdt = dateTimeToQDateTime(dateTime);
    if (!utc)
        qdt = qdt.toLocalTime();
    QTime qt(qdt.time().hour(), qdt.time().minute());
    passStartTime = QTime(passStartTime.hour(), passStartTime.minute());
    passEndTime = QTime(passEndTime.hour(), passEndTime.minute());
    // If passEndTime is before passStartTime, then we allow overnight passes
    if (passEndTime > passStartTime)
    {
        return (qt >= passStartTime) && (qt <= passEndTime);
    }
    else
    {
        return (qt <= passEndTime) || (qt >= passStartTime);
    }
}

// Create a list of satellite passes, between the given start and end times, that exceed the specified minimum elevation
// We return an uninitialised QDateTime if AOS or LOS is outside of predictionPeriod
static QList<SatellitePass> createPassList(Observer& obs, SGP4& sgp4, DateTime& startTime,
                                            int predictionPeriod, double minAOSElevation, double minPassElevationDeg,
                                            QTime passStartTime, QTime passEndTime, bool utc,
                                            int noOfPasses)
{
    QList<SatellitePass> passes;
    bool aos = false;
    bool aosUnknown = true;
    double aosAz;
    double losAz;
    DateTime previousTime(startTime);
    DateTime currentTime(startTime);
    DateTime endTime(startTime.AddDays(predictionPeriod));
    DateTime aosTime;
    DateTime losTime;

    while (currentTime < endTime)
    {
        bool endOfPass = false;
        Eci eci = sgp4.FindPosition(currentTime);
        CoordTopocentric topo = obs.GetLookAngle(eci);

        if (!aos && (topo.elevation > minAOSElevation))
        {
            if (startTime == currentTime)
            {
                // AOS is before startTime
                aosTime = findAOSBackwards(obs, sgp4, startTime, predictionPeriod, minAOSElevation, aosUnknown);
            }
            else
            {
                aosTime = findCrossingPoint(obs, sgp4, previousTime, currentTime, minAOSElevation, true);
                aosUnknown = false;
            }
            aos = true;
            eci = sgp4.FindPosition(aosTime);
            topo = obs.GetLookAngle(eci);
            aosAz = Units::radiansToDegrees(topo.azimuth);
        }
        else if (aos && (topo.elevation < minAOSElevation))
        {
            aos = false;
            endOfPass = true;
            losTime = findCrossingPoint(obs, sgp4, previousTime, currentTime, minAOSElevation, false);
            eci = sgp4.FindPosition(losTime);
            topo = obs.GetLookAngle(eci);
            losAz = Units::radiansToDegrees(topo.azimuth);
            double maxElevationDeg = findMaxElevation(obs, sgp4, aosTime, losTime);
            if ((maxElevationDeg >= minPassElevationDeg)
                && inPassWindow(aosTime, passStartTime, passEndTime, utc)
                && inPassWindow(losTime, passStartTime, passEndTime, utc))
            {
                SatellitePass pass;
                pass.m_aos = aosUnknown ? QDateTime() : dateTimeToQDateTime(aosTime);
                pass.m_los = dateTimeToQDateTime(losTime);
                pass.m_maxElevation = maxElevationDeg;
                pass.m_aosAzimuth = aosAz;
                pass.m_losAzimuth = losAz;
                pass.m_northToSouth = std::min(360.0-aosAz, aosAz-0.0) < std::min(360.0-losAz, losAz-0.0);
                passes.append(pass);
                noOfPasses--;
                if (noOfPasses <= 0)
                    return passes;
            }
        }
        previousTime = currentTime;
        if (endOfPass)
            currentTime = currentTime + TimeSpan(0, 30, 0); // 30 minutes - no orbit likely to be that fast
        else
            currentTime = currentTime + TimeSpan(0, 0, 180);
        if (currentTime > endTime)
            currentTime = endTime;
    }
    if (aos)
    {
        // Pass still in progress at end time
        Eci eci = sgp4.FindPosition(currentTime);
        CoordTopocentric topo = obs.GetLookAngle(eci);
        losAz = Units::radiansToDegrees(topo.azimuth);
        double maxElevationDeg = findMaxElevation(obs, sgp4, aosTime, losTime);
        if ((maxElevationDeg >= minPassElevationDeg)
            && inPassWindow(aosTime, passStartTime, passEndTime, utc)
            && inPassWindow(losTime, passStartTime, passEndTime, utc))
        {
            SatellitePass pass;
            pass.m_aos = aosUnknown ? QDateTime() : dateTimeToQDateTime(aosTime);
            pass.m_los = QDateTime();
            pass.m_aosAzimuth = aosAz;
            pass.m_losAzimuth = losAz;
            pass.m_maxElevation = maxElevationDeg;
            pass.m_northToSouth = std::min(360.0-aosAz, aosAz-0.0) < std::min(360.0-losAz, losAz-0.0);
            passes.append(pass);
        }
    }

    return passes;
}

void getSatelliteState(QDateTime dateTime,
                        const QString& tle0, const QString& tle1, const QString& tle2,
                        double latitude, double longitude, double altitude,
                        int predictionPeriod, int minAOSElevationDeg, int minPassElevationDeg,
                        QTime passStartTime, QTime passFinishTime, bool utc,
                        int noOfPasses, int groundTrackSteps, SatelliteState *satState)
{
    try
    {
        Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString());
        SGP4 sgp4(tle);
        Observer obs(latitude, longitude, altitude);

        DateTime dt = qDateTimeToDateTime(dateTime);

        // Calculate satellite position
        Eci eci = sgp4.FindPosition(dt);

        // Calculate angle to satellite from antenna
        CoordTopocentric topo = obs.GetLookAngle(eci);

        // Convert satellite position to geodetic coordinates (lat and long)
        CoordGeodetic geo = eci.ToGeodetic();

        satState->m_latitude = Units::radiansToDegrees(geo.latitude);
        satState->m_longitude = Units::radiansToDegrees(geo.longitude);
        satState->m_altitude = geo.altitude;
        satState->m_azimuth = Units::radiansToDegrees(topo.azimuth);
        satState->m_elevation = Units::radiansToDegrees(topo.elevation);
        satState->m_range = topo.range;
        satState->m_rangeRate = topo.range_rate;
        OrbitalElements ele(tle);
        satState->m_speed = eci.Velocity().Magnitude();
        satState->m_period = ele.Period();
        if (noOfPasses > 0)
        {
            satState->m_passes.clear();
            satState->m_passes = createPassList(obs, sgp4, dt, predictionPeriod,
                                                Units::degreesToRadians((double)minAOSElevationDeg),
                                                minPassElevationDeg,
                                                passStartTime, passFinishTime, utc,
                                                noOfPasses);
        }

        qDeleteAll(satState->m_groundTrack);
        satState->m_groundTrack.clear();
        qDeleteAll(satState->m_groundTrackDateTime);
        satState->m_groundTrackDateTime.clear();
        qDeleteAll(satState->m_predictedGroundTrack);
        satState->m_predictedGroundTrack.clear();
        qDeleteAll(satState->m_predictedGroundTrackDateTime);
        satState->m_predictedGroundTrackDateTime.clear();
        getGroundTrack(dateTime, tle0, tle1, tle2, groundTrackSteps, false, satState->m_groundTrack, satState->m_groundTrackDateTime);
        getGroundTrack(dateTime, tle0, tle1, tle2, groundTrackSteps, true, satState->m_predictedGroundTrack, satState->m_predictedGroundTrackDateTime);
    }
    catch (SatelliteException& se)
    {
        qDebug() << "getSatelliteState:SatelliteException " << satState->m_name << ": " << se.what();
    }
    catch (DecayedException& de)
    {
        qDebug() << "getSatelliteState:DecayedException " << satState->m_name << ": " << de.what();
    }
    catch (TleException& tlee)
    {
        qDebug() << "getSatelliteState:TleException " << satState->m_name << ": " << tlee.what() << "\n" << tle0 << "\n" << tle1 << "\n" << tle2;
    }
}
