#include "APRSISClient.h"

#include <cmath>

#include "DriftingDateTime.h"
#include "varicode.h"

const int PACKET_TIMEOUT_SECONDS = 300;

APRSISClient::APRSISClient(QString host, quint16 port, QObject *parent):
    QTcpSocket(parent)
{
    setServer(host, port);

    connect(&m_timer, &QTimer::timeout, this, &APRSISClient::sendReports);
    m_timer.setInterval(60*1000); // every 60 seconds
    m_timer.start();
}

quint32 APRSISClient::hashCallsign(QString callsign){
    // based on: https://github.com/hessu/aprsc/blob/master/src/passcode.c
    QByteArray rootCall = QString(callsign.split("-").first().toUpper()).toLocal8Bit() + '\0';
    quint32 hash = 0x73E2;

    int i = 0;
    int len = rootCall.length();

    while(i+1 < len){
        hash ^= rootCall.at(i) << 8;
        hash ^= rootCall.at(i+1);
        i += 2;
    }

    return hash & 0x7FFF;
}

QString APRSISClient::loginFrame(QString callsign){
    auto loginFrame = QString("user %1 pass %2 ver %3\n");
    loginFrame = loginFrame.arg(callsign);
    loginFrame = loginFrame.arg(hashCallsign(callsign));
    loginFrame = loginFrame.arg("JS8Call");
    return loginFrame;
}

QList<QStringList> findall(QRegularExpression re, QString content){
    int pos = 0;

    QList<QStringList> all;
    while(pos < content.length()){
        auto match = re.match(content, pos);
        if(!match.hasMatch()){
            break;
        }

        all.append(match.capturedTexts());
        pos = match.capturedEnd();
    }

    return all;
}


inline long
floordiv (long num, long den)
{
  if (0 < (num^den))
    return num/den;
  else
    {
      ldiv_t res = ldiv(num,den);
      return (res.rem)? res.quot-1
                      : res.quot;
    }
}

// convert an arbitrary length grid locator to a high precision lat/lon
QPair<float, float> APRSISClient::grid2deg(QString locator){
    QString grid = locator.toUpper();

    float lat = -90;
    float lon = -90;

    auto lats = findall(QRegularExpression("([A-X])([A-X])"), grid);
    auto lons = findall(QRegularExpression("(\\d)(\\d)"), grid);

    int valx[22];
    int valy[22];

    int i = 0;
    int tot = 0;
    char A = 'A';
    foreach(QStringList matched, lats){
        char x = matched.at(1).at(0).toLatin1();
        char y = matched.at(2).at(0).toLatin1();

        valx[i*2] = x-A;
        valy[i*2] = y-A;

        i++;
        tot++;
    }

    i = 0;
    foreach(QStringList matched, lons){
        int x = matched.at(1).toInt();
        int y = matched.at(2).toInt();
        valx[i*2+1]=x;
        valy[i*2+1]=y;

        i++;
        tot++;
    }

    for(int i = 0; i < tot; i++){
        int x = valx[i];
        int y = valy[i];

        int z = i - 1;
        float scale = pow(10, floordiv(-(z-1), 2)) * pow(24, floordiv(-z, 2));

        lon += scale * x;
        lat += scale * y;
    }

    lon *= 2;

    return {lat, lon};
}

// convert an arbitrary length grid locator to a high precision lat/lon in aprs format
QPair<QString, QString> APRSISClient::grid2aprs(QString grid){
    auto geo = APRSISClient::grid2deg(grid);
    auto lat = geo.first;
    auto lon = geo.second;

    QString latDir = "N";
    if(lat < 0){
        lat *= -1;
        latDir = "S";
    }

    QString lonDir = "E";
    if(lon < 0){
        lon *= -1;
        lonDir = "W";
    }

    double iLat, fLat, iLon, fLon, iLatMin, fLatMin, iLonMin, fLonMin, iLatSec, iLonSec;
    fLat = modf(lat, &iLat);
    fLon = modf(lon, &iLon);

    fLatMin = modf(fLat * 60, &iLatMin);
    fLonMin = modf(fLon * 60, &iLonMin);

    iLatSec = round(fLatMin * 60);
    iLonSec = round(fLonMin * 60);

    if(iLatSec == 60){
        iLatMin += 1;
        iLatSec = 0;
    }

    if(iLonSec == 60){
        iLonMin += 1;
        iLonSec = 0;
    }

    if(iLatMin == 60){
        iLat += 1;
        iLatMin = 0;
    }

    if(iLonMin == 60){
        iLon += 1;
        iLonMin = 0;
    }

    double aprsLat = iLat * 100 + iLatMin + (iLatSec / 60.0);
    double aprsLon = iLon * 100 + iLonMin + (iLonSec / 60.0);

    return {
        QString().sprintf("%07.2f%%1", aprsLat).arg(latDir),
        QString().sprintf("%08.2f%%1", aprsLon).arg(lonDir)
    };
}

QString APRSISClient::stripSSID(QString call){
    return QString(call.split("-").first().toUpper());
}

QString APRSISClient::replaceCallsignSuffixWithSSID(QString call, QString base){
    if(call != base){
        QRegularExpression re("[/](?<ssid>(P|\\d+))");
        auto matcher = re.globalMatch(call);
        if(matcher.hasNext()){
            auto match = matcher.next();
            auto ssid = match.captured("ssid");
            if(ssid == "P"){
                ssid = "16";
            }
            call = base + "-" + ssid;
        } else {
            call = base;
        }
    }
    return call;
}

void APRSISClient::enqueueSpot(QString theircall, QString grid, QString comment){
    if(m_localCall.isEmpty()) return;

    auto geo = APRSISClient::grid2aprs(grid);
    auto spotFrame = QString("%1>APRS,%2,TCPIP*:=%3/%4nJS8 %5\n");
    spotFrame = spotFrame.arg(theircall);
    spotFrame = spotFrame.arg(stripSSID(m_localCall));
    spotFrame = spotFrame.arg(geo.first);
    spotFrame = spotFrame.arg(geo.second);
    spotFrame = spotFrame.arg(comment.left(43));
    enqueueRaw(spotFrame);
}

void APRSISClient::enqueueThirdParty(QString theircall, QString payload){
    if(!isPasscodeValid()){
        return;
    }

    auto frame = QString("%1>APRS,%2,TCPIP*:%3\n");
    frame = frame.arg(theircall);
    frame = frame.arg(stripSSID(m_localCall));
    frame = frame.arg(payload);
    enqueueRaw(frame);
}

void APRSISClient::enqueueRaw(QString aprsFrame){
    m_frameQueue.enqueue({ aprsFrame, DriftingDateTime::currentDateTimeUtc() });
}

void APRSISClient::processQueue(bool disconnect){
    // don't process queue if we haven't set our local callsign
    if(m_localCall.isEmpty()) return;

    // don't process queue if there's nothing to process
    if(m_frameQueue.isEmpty()) return;

    // 1. connect (and read)
    // 2. login (and read)
    // 3. for each raw frame in queue, send
    // 4. disconnect

    if(state() != QTcpSocket::ConnectedState){
        qDebug() << "APRSISClient Connecting:" << m_host << m_port;
        connectToHost(m_host, m_port);
        if(!waitForConnected(5000)){
            qDebug() << "APRSISClient Connection Error:" << errorString();
            return;
        }
    }

    auto re = QRegExp("(full|unavailable|busy)");
    auto line = QString(readLine());
    if(line.toLower().indexOf(re) >= 0){
        qDebug() << "APRSISClient Connection Busy:" << line;
        return;
    }

    if(write(loginFrame(m_localCall).toLocal8Bit()) == -1){
        qDebug() << "APRSISClient Write Login Error:" << errorString();
        return;
    }

    if(!waitForReadyRead(5000)){
        qDebug() << "APRSISClient Login Error: Server Not Responding";
        return;
    }

    line = QString(readAll());
    if(line.toLower().indexOf(re) >= 0){
        qDebug() << "APRSISClient Server Busy:" << line;
        return;
    }

    QQueue<QPair<QString, QDateTime>> delayed;

    while(!m_frameQueue.isEmpty()){
        auto pair = m_frameQueue.head();
        auto frame = pair.first;
        auto timestamp = pair.second;

        // if the packet is older than the timeout, drop it.
        if(timestamp.secsTo(DriftingDateTime::currentDateTimeUtc()) > PACKET_TIMEOUT_SECONDS){
            qDebug() << "APRSISClient Packet Timeout:" << frame;
            m_frameQueue.dequeue();
            continue;
        }

        // random delay 25% of the time for throttling (a skip will add 60 seconds to the processing time)
        if(qrand() % 100 <= 25){
            qDebug() << "APRSISClient Throttle: Skipping Frame";
            delayed.enqueue(m_frameQueue.dequeue());
            continue;
        }

        QByteArray data = frame.toLocal8Bit();
        if(write(data) == -1){
            qDebug() << "APRSISClient Write Error:" << errorString();
            return;
        }

        qDebug() << "APRSISClient Write:" << data;
        if(waitForReadyRead(5000)){
            line = QString(readLine());

            qDebug() << "APRSISClient Read:" << line;

            if(line.toLower().indexOf(re) >= 0){
                qDebug() << "APRSISClient Cannot Write Error:" << line;
                return;
            }
        }

        m_frameQueue.dequeue();
    }

    // enqueue the delayed frames for later processing
    while(!delayed.isEmpty()){
        m_frameQueue.enqueue(delayed.dequeue());
    }

    if(disconnect){
        disconnectFromHost();
    }
}
