///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2022 Jon Beniston, M7RCE <jon@beniston.com>                //
// Copyright (C) 2022 Peter Beckman <beckman@angryox.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/>.          //
///////////////////////////////////////////////////////////////////////////////////

#ifndef INCLUDE_APRS_H
#define INCLUDE_APRS_H

#include <QString>
#include <QStringList>
#include <QByteArray>
#include <QDateTime>
#include <QDebug>

#include "export.h"
#include "ax25.h"
#include "util/units.h"

struct SDRBASE_API APRSPacket {
    QString m_from;
    QString m_to;
    QString m_via;
    QByteArray m_data;          // Original binary data

    QDateTime m_dateTime;       // Date/time of reception / decoding

    // Timestamp (where fields are not transmitted, time of decoding is used)
    QDateTime m_timestamp;
    bool m_utc; // Whether UTC (true) or local time (false)
    bool m_hasTimestamp;

    // Position
    float m_latitude;
    float m_longitude;
    bool m_hasPosition;

    float m_altitudeFt;
    bool m_hasAltitude;

    // Symbol
    char m_symbolTable;
    char m_symbolCode;
    bool m_hasSymbol;
    QString m_symbolImage;      // Image filename for the symbol

    // Course and speed
    int m_course;
    int m_speed;
    bool m_hasCourseAndSpeed;

    // Power, antenna height, gain, directivity
    int m_powerWatts;
    int m_antennaHeightFt;
    int m_antennaGainDB;
    QString m_antennaDirectivity; // Omni, or N, NE...
    bool m_hasStationDetails;

    // Radio range
    int m_radioRangeMiles;
    bool m_hasRadioRange;

    // Omni-DF
    int m_dfStrength;
    int m_dfHeightFt;
    int m_dfGainDB;
    QString m_dfAntennaDirectivity;
    bool m_hasDf;

    QString m_objectName;       // Also used for items
    bool m_objectLive;
    bool m_objectKilled;

    QString m_comment;

    // Weather reports
    int m_windDirection;        // In degrees
    bool m_hasWindDirection;
    int m_windSpeed;            // In mph
    bool m_hasWindSpeed;
    int m_gust;                 // Peak wind speed in last 5 minutes in mph
    bool m_hasGust;
    int m_temp;                 // Fahrenheit, can be negative down to -99
    bool m_hasTemp;
    int m_rainLastHr;           // Hundredths of an inch
    bool m_hasRainLastHr;
    int m_rainLast24Hrs;
    bool m_hasRainLast24Hrs;
    int m_rainSinceMidnight;
    bool m_hasRainSinceMidnight;
    int m_humidity;             // %
    bool m_hasHumidity;
    int m_barometricPressure;   // Tenths of millibars / tenths of hPascal
    bool m_hasBarometricPressure;
    int m_luminosity;           // Watts per m^2
    bool m_hasLuminsoity;
    int m_snowfallLast24Hrs;    // In inches
    bool m_hasSnowfallLast24Hrs;
    int m_rawRainCounter;
    bool m_hasRawRainCounter;
    int m_radiationLevel;
    bool m_hasRadiationLevel;
    int m_floodLevel;           // Tenths of a foot. Can be negative
    bool m_hasFloodLevel;
    int m_batteryVolts;         // Tenths of a volt
    bool m_hasBatteryVolts;
    QString m_weatherUnitType;
    bool m_hasWeather;

    int m_stormDirection;
    int m_stormSpeed;
    QString m_stormType;
    int m_stormSustainedWindSpeed;      // knots
    int m_stormPeakWindGusts;           // knots
    int m_stormCentralPresure;          // millibars/hPascal
    int m_stormRadiusHurricanWinds;     // nautical miles
    int m_stormRadiusTropicalStormWinds;// nautical miles
    int m_stormRadiusWholeGail;         // nautical miles
    bool m_hasStormRadiusWholeGail;
    bool m_hasStormData;

    // Status messages
    QString m_status;
    QString m_maidenhead;
    int m_beamHeading;
    int m_beamPower;
    bool m_hasBeam;
    bool m_hasStatus;

    // Messages
    QString m_addressee;
    QString m_message;
    QString m_messageNo;
    bool m_hasMessage;
    QList <QString> m_telemetryNames;
    QList <QString> m_telemetryLabels;
    double m_telemetryCoefficientsA[5];
    double m_telemetryCoefficientsB[5];
    double m_telemetryCoefficientsC[5];
    int m_hasTelemetryCoefficients;
    int m_telemetryBitSense[8];
    bool m_hasTelemetryBitSense;
    QString m_telemetryProjectName;

    // Telemetry
    int m_seqNo;
    bool m_hasSeqNo;
    int m_a1;
    bool m_a1HasValue;
    int m_a2;
    bool m_a2HasValue;
    int m_a3;
    bool m_a3HasValue;
    int m_a4;
    bool m_a4HasValue;
    int m_a5;
    bool m_a5HasValue;
    bool m_b[8];
    bool m_bHasValue;
    QString m_telemetryComment;
    bool m_hasTelemetry;

    bool decode(AX25Packet packet);

    APRSPacket() :
        m_hasTimestamp(false),
        m_hasPosition(false),
        m_hasAltitude(false),
        m_hasSymbol(false),
        m_hasCourseAndSpeed(false),
        m_hasStationDetails(false),
        m_hasRadioRange(false),
        m_hasDf(false),
        m_objectLive(false),
        m_objectKilled(false),
        m_hasWindDirection(false),
        m_hasWindSpeed(false),
        m_hasGust(false),
        m_hasTemp(false),
        m_hasRainLastHr(false),
        m_hasRainLast24Hrs(false),
        m_hasRainSinceMidnight(false),
        m_hasHumidity(false),
        m_hasBarometricPressure(false),
        m_hasLuminsoity(false),
        m_hasSnowfallLast24Hrs(false),
        m_hasRawRainCounter(false),
        m_hasRadiationLevel(false),
        m_hasFloodLevel(false),
        m_hasBatteryVolts(false),
        m_hasWeather(false),
        m_hasStormRadiusWholeGail(false),
        m_hasStormData(false),
        m_hasBeam(false),
        m_hasStatus(false),
        m_hasMessage(false),
        m_hasTelemetryCoefficients(0),
        m_hasTelemetryBitSense(false),
        m_hasSeqNo(false),
        m_a1HasValue(false),
        m_a2HasValue(false),
        m_a3HasValue(false),
        m_a4HasValue(false),
        m_a5HasValue(false),
        m_bHasValue(false),
        m_hasTelemetry(false)
    {
    }

    QString date()
    {
        if (m_hasTimestamp)
            return m_timestamp.date().toString("yyyy/MM/dd");
        else
            return QString("");
    }

    QString time()
    {
        if (m_hasTimestamp)
            return m_timestamp.time().toString("hh:mm:ss");
        else
            return QString("");
    }

    QString dateTime()
    {
        return QString("%1 %2").arg(date()).arg(time());
    }

    QString position()
    {
        return QString("%1,%2").arg(m_latitude).arg(m_longitude);
    }

    QByteArray toTNC2(QString igateCallsign)
    {
        QByteArray data;

        data.append(m_from.toLatin1());
        data.append('>');
        data.append(m_to.toLatin1());
        if (!m_via.isEmpty())
        {
            data.append(',');
            data.append(m_via.toLatin1());
        }
        data.append(",qAR,");
        data.append(igateCallsign.toLatin1());
        data.append(':');

        // #2028 - Protect against APRS-IS server command injection, by only sending up to first CR/LF
        int idx = m_data.indexOf("\r");
        if (idx == -1) {
            idx = m_data.indexOf("\n");
        }
        if (idx >= 0) {
            data.append(m_data.left(idx));
        } else {
            data.append(m_data);
        }
        data.append('\r');
        data.append('\n');

        return data;
    }

    // Convert a TNC2 formatted packet (as sent by APRS-IS Igates) to an AX25 byte array
    static QByteArray toByteArray(QString tnc2)
    {
        QByteArray bytes;
        QString tmp = "";
        QString from;
        int state = 0;

        for (int i = 0; i < tnc2.length(); i++)
        {
            if (state == 0)
            {
                // From
                if (tnc2[i] == '>')
                {
                    from = tmp;
                    tmp = "";
                    state = 1;
                }
                else
                    tmp.append(tnc2[i]);
            }
            else if (state == 1)
            {
                 // To
                 if (tnc2[i] == ':')
                 {
                    bytes.append(AX25Packet::encodeAddress(tmp));
                    bytes.append(AX25Packet::encodeAddress(from, 1));
                    state = 3;
                 }
                 else if (tnc2[i] == ',')
                 {
                    bytes.append(AX25Packet::encodeAddress(tmp));
                    bytes.append(AX25Packet::encodeAddress(from));
                    tmp = "";
                    state = 2;
                 }
                 else
                    tmp.append(tnc2[i]);
            }
            else if (state == 2)
            {
                 // Via
                 if (tnc2[i] == ':')
                 {
                    bytes.append(AX25Packet::encodeAddress(tmp, 1));
                    state = 3;
                 }
                 else if (tnc2[i] == ',')
                 {
                    bytes.append(AX25Packet::encodeAddress(tmp));
                    tmp = "";
                 }
                 else
                    tmp.append(tnc2[i]);
            }
            else if (state == 3)
            {
                // UI Type and PID
                bytes.append(3);
                bytes.append(-16); // 0xf0
                // APRS message
                bytes.append(tnc2.mid(i).toLatin1().trimmed());
                // CRC
                bytes.append((char)0);
                bytes.append((char)0);
                break;
            }
        }

        return bytes;
    }

    QString toText(bool includeFrom=true, bool includePosition=false, char separator='\n',
                        bool altitudeMetres=false, int speedUnits=0, bool tempCelsius=false, bool rainfallMM=false)
    {
        QStringList text;

        if (!m_objectName.isEmpty())
        {
            text.append(QString("%1 (%2)").arg(m_objectName).arg(m_from));
        }
        else
        {
            if (includeFrom)
                text.append(m_from);
        }

        if (m_hasTimestamp)
        {
            QStringList time;
            time.append(this->date());
            time.append(this->time());
            if (m_utc)
                time.append("UTC");
            else
                time.append("local");
            text.append(time.join(' '));
        }

        if (includePosition && m_hasPosition)
            text.append(QString("Latitude: %1 Longitude: %2").arg(m_latitude).arg(m_longitude));
        if (m_hasAltitude)
        {
            if (altitudeMetres)
                text.append(QString("Altitude: %1 m").arg((int)std::round(Units::feetToMetres(m_altitudeFt))));
            else
                text.append(QString("Altitude: %1 ft").arg(m_altitudeFt));
        }
        if (m_hasCourseAndSpeed)
        {
            if (speedUnits == 0)
                text.append(QString("Course: %1%3 Speed: %2 knts").arg(m_course).arg(m_speed).arg(QChar(0xb0)));
            else if (speedUnits == 1)
                text.append(QString("Course: %1%3 Speed: %2 mph").arg(m_course).arg(Units::knotsToIntegerMPH(m_speed)).arg(QChar(0xb0)));
            else
                text.append(QString("Course: %1%3 Speed: %2 kph").arg(m_course).arg(Units::knotsToIntegerKPH(m_speed)).arg(QChar(0xb0)));
        }

        if (m_hasStationDetails)
            text.append(QString("TX Power: %1 Watts Antenna Height: %2 m Gain: %3 dB Direction: %4").arg(m_powerWatts).arg(std::round(Units::feetToMetres(m_antennaHeightFt))).arg(m_antennaGainDB).arg(m_antennaDirectivity));
        if (m_hasRadioRange)
            text.append(QString("Range: %1 km").arg(Units::milesToKilometres(m_radioRangeMiles)));
        if (m_hasDf)
            text.append(QString("DF Strength: S %1 Height: %2 m Gain: %3 dB Direction: %4").arg(m_dfStrength).arg(std::round(Units::feetToMetres(m_dfHeightFt))).arg(m_dfGainDB).arg(m_dfAntennaDirectivity));

        if (m_hasWeather)
        {
            QStringList weather, wind, air, rain;

            wind.append(QString("Wind"));
            if (m_hasWindDirection)
                wind.append(QString("%1%2").arg(m_windDirection).arg(QChar(0xb0)));
            if (m_hasWindSpeed)
                wind.append(QString("%1 mph").arg(m_windSpeed));
            if (m_hasGust)
                wind.append(QString("Gusts %1 mph").arg(m_gust));
            weather.append(wind.join(' '));

            if (m_hasTemp || m_hasHumidity || m_hasBarometricPressure)
            {
                air.append("Air");
                if (m_hasTemp)
                {
                    if (tempCelsius)
                        air.append(QString("Temperature %1C").arg(Units::fahrenheitToCelsius(m_temp), 0, 'f', 1));
                    else
                        air.append(QString("Temperature %1F").arg(m_temp));
                }
                if (m_hasHumidity)
                    air.append(QString("Humidity %1%").arg(m_humidity));
                if (m_hasBarometricPressure)
                    air.append(QString("Pressure %1 mbar").arg(m_barometricPressure/10.0));
                weather.append(air.join(' '));
            }

            if (m_hasRainLastHr || m_hasRainLast24Hrs || m_hasRainSinceMidnight)
            {
                rain.append("Rain");
                if (m_hasRainLastHr)
                {
                    if (rainfallMM)
                        rain.append(QString("%1 mm last hour").arg(std::round(Units::inchesToMilimetres(m_rainLastHr/100.0))));
                    else
                        rain.append(QString("%1 1/100\" last hour").arg(m_rainLastHr));
                }
                if (m_hasRainLast24Hrs)
                {
                    if (rainfallMM)
                        rain.append(QString("%1 mm last 24 hours").arg(std::round(Units::inchesToMilimetres(m_rainLast24Hrs/100.0))));
                    else
                        rain.append(QString("%1 1/100\" last 24 hours").arg(m_rainLast24Hrs));
                }
                if (m_hasRainSinceMidnight)
                {
                    if (rainfallMM)
                        rain.append(QString("%1 mm since midnight").arg(std::round(Units::inchesToMilimetres(m_rainSinceMidnight/100.0))));
                    else
                        rain.append(QString("%1 1/100\" since midnight").arg(m_rainSinceMidnight));
                }
                weather.append(rain.join(' '));
            }

            if (!m_weatherUnitType.isEmpty())
                weather.append(m_weatherUnitType);

            text.append(weather.join(separator));
        }

        if (m_hasStormData)
        {
            QStringList storm;

            storm.append(m_stormType);

            storm.append(QString("Direction: %1%3 Speed: %2").arg(m_stormDirection).arg(m_stormSpeed).arg(QChar(0xb0)));
            storm.append(QString("Sustained wind speed: %1 knots Peak wind gusts: %2 knots Central pressure: %3 mbar").arg(m_stormSustainedWindSpeed).arg(m_stormPeakWindGusts).arg(m_stormCentralPresure));
            storm.append(QString("Hurrican winds radius: %1 nm Tropical storm winds radius: %2 nm%3").arg(m_stormRadiusHurricanWinds).arg(m_stormRadiusTropicalStormWinds).arg(m_hasStormRadiusWholeGail ? QString("") : QString(" Whole gail radius: %3 nm").arg(m_stormRadiusWholeGail)));

            text.append(storm.join(separator));
        }

        if (!m_comment.isEmpty())
            text.append(m_comment);

        return text.join(separator);
    }

private:
    int charToInt(QString &s, int idx);
    bool parseTime(QString& info, int& idx);
    bool parseTimeMDHM(QString& info, int& idx);
    bool isLatLongChar(const QChar c);
    bool parsePosition(QString& info, int& idx);
    bool parseDataExension(QString& info, int& idx);
    bool parseComment(QString& info, int& idx);
    bool parseInt(QString& info, int& idx, int chars, int& value, bool& hasValue);
    bool parseWeather(QString& info, int& idx, bool positionLess);
    bool parseStorm(QString& info, int& idx);
    bool parseObject(QString& info, int& idx);
    bool parseItem(QString& info, int& idx);
    bool parseStatus(QString& info, int& idx);
    bool parseMessage(QString& info, int& idx);
    bool parseTelemetry(QString& info, int& idx);
    bool parseMicE(QString& info, int& idx, QString& dest);
};

#endif // INCLUDE_APRS_H