// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2010 Wes Hardaker <hardaker@users.sourceforge.net>
//

#include "AprsGatherer.h"

#include "MarbleDebug.h"

#include <QIODevice>
#include <QMutex>
#include <QRegExp>

using namespace Marble;

AprsGatherer::AprsGatherer(AprsSource *from, QMap<QString, AprsObject *> *objects, QMutex *mutex, QString *filter)
    : m_source(from)
    , m_socket(nullptr)
    , m_filter(filter)
    , m_running(true)
    , m_dumpOutput(false)
    , m_seenFrom(GeoAprsCoordinates::FromNowhere)
    , m_sourceName()
    , m_mutex(mutex)
    , m_objects(objects)
{
    m_sourceName = from->sourceName();
    initMicETables();
}

AprsGatherer::AprsGatherer(QIODevice *from, QMap<QString, AprsObject *> *objects, QMutex *mutex, QString *filter)
    : m_source(nullptr)
    , m_socket(from)
    , m_filter(filter)
    , m_running(true)
    , m_dumpOutput(false)
    , m_seenFrom(GeoAprsCoordinates::FromNowhere)
    , m_sourceName(QStringLiteral("unknown"))
    , m_mutex(mutex)
    , m_objects(objects)
{
    initMicETables();
}

void AprsGatherer::run()
{
    char buf[4096];
    qint64 linelength;
    // one particular APRS packet sender can add data after the : ( sigh )
    QRegExp matcher(
        QStringLiteral("^([0-9A-Z]+-*[0-9A-Z]*)>([^:]*):([!=@\\/])([0-9][0-9][0-9][0-9][0-9][0-9]|)([hz\\/"
                       "]|)([0-9][0-9])([0-9][0-9]\\.[0-9][0-9])([NS])(.)([0-9][0-9][0-9])([0-9][0-9]\\.[0-9][0-9])([EW])(.)"));

    // mic-e formatted
    // 1: src
    // 2: dst
    // 3: routes
    // 4: longitude x 3
    // 5: speed and course x3
    // 6: symbol and symbol ID
    // 7: status text
    QRegExp mic_e_matcher(QStringLiteral("^([0-9A-Z]+-*[0-9A-Z]*)>([^,:]*),*([^:]*):['`](...)(...)(..)(.*)"));

    // If a source can directly receive a signal (as opposed to
    // through a relay like the internet) will return true.  This
    // prevents accidentally coloring signals heard over some sources
    // as heard directly where it's never possible (such as over the
    // internet).
    Q_ASSERT(m_source);
    if (!m_source) {
        return;
    }
    bool canDoDirect = m_source->canDoDirect();

    while (m_running) {
        if (m_socket && !m_socket->isOpen()) {
            // connection closed; attempt to reopen
            mDebug() << "aprs: socket closed; attempting to reopen";
            delete m_socket;
            m_socket = nullptr;
        }

        if (!m_socket)
            m_socket = m_source->openSocket();

        if (!m_socket) {
            mDebug() << "aprs: failed to open socket from " << m_sourceName.toLocal8Bit().data();
            sleep(5);
            continue;
        }

        // wait for data to read in
        if (m_socket->bytesAvailable() <= 0)
            // wait no longer than 1s
            if (!m_socket->waitForReadyRead(1000))
                continue; // continue to loop again on "not ready"

        // Read the line to parse
        linelength = m_socket->readLine(buf, sizeof(buf));

        // if we got 0 or less bytes this is probably an odd case; ask
        // the source what to do.
        m_source->checkReadReturn(linelength, &m_socket, this);

        if (linelength <= 0) {
            // don't go into an infinite untimed loop of failed sockets
            sleep(2);
            continue;
        }

        if (m_socket && m_filter != nullptr) {
            QMutexLocker locker(m_mutex);
            if (m_filter->length() > 0) {
                m_socket->write(m_filter->toLocal8Bit().data(), m_filter->length());
            }
        }

        // Parse the results
        QString line(QString::fromUtf8(buf));

        // Dump it out if we wanted it dumped
        if (m_dumpOutput)
            mDebug() << "aprs: " << m_sourceName.toLocal8Bit().data() << ": " << line;

        if (matcher.indexIn(line) != -1) {
            QString callSign = matcher.cap(1);
            qreal latitude = matcher.cap(6).toFloat() + (matcher.cap(7).toFloat() / 60);
            if (matcher.cap(8) == QLatin1StringView("S"))
                latitude = -latitude;

            qreal longitude = matcher.cap(10).toFloat() + (matcher.cap(11).toFloat() / 60);
            if (matcher.cap(12) == QLatin1StringView("W"))
                longitude = -longitude;

            addObject(callSign, latitude, longitude, canDoDirect, QString(matcher.cap(2)), QChar(matcher.cap(9)[0]), QChar(matcher.cap(13)[0]));
        } else if (mic_e_matcher.indexIn(line) != -1) {
            // MIC-E formatted compressed packet
            QString myCall = mic_e_matcher.cap(1);
            QString dstCall = mic_e_matcher.cap(2);

            qreal latitude =
                // hours
                m_dstCallDigits[dstCall[0]] * 10 + m_dstCallDigits[dstCall[1]] +

                // minutes
                (qreal(m_dstCallDigits[dstCall[2]] * 10 + m_dstCallDigits[dstCall[3]]) + qreal(m_dstCallDigits[dstCall[4]]) / 10.0
                 + qreal(m_dstCallDigits[dstCall[5]]) / 100)
                    / 60.0;

            if (m_dstCallSouthEast[dstCall[4]])
                latitude = -latitude;

            qreal longitude = calculateLongitude(QString(mic_e_matcher.cap(4)), m_dstCallLongitudeOffset[dstCall[4]], m_dstCallSouthEast[dstCall[5]]);

            //           mDebug() << "  MIC-E: " << line.toLocal8Bit().data();
            //           mDebug() << "    lat: " << latitude;
            //           mDebug() << "    lon: " << longitude;

            addObject(myCall, latitude, longitude, canDoDirect, QString(mic_e_matcher.cap(3)), QChar(mic_e_matcher.cap(6)[1]), QChar(mic_e_matcher.cap(6)[0]));
        } else {
            mDebug() << "aprs: UNPARSED: " << line;
        }

        // If the filter should be changed, send it out the socket
        if (m_filter != nullptr) {
            QMutexLocker locker(m_mutex);
            if (m_filter->length() > 0) {
                m_socket->write(m_filter->toLocal8Bit().data(), m_filter->length());
            }
        }
    }
}

void AprsGatherer::shutDown()
{
    m_running = false;
}

void AprsGatherer::addObject(const QString &callSign,
                             qreal latitude,
                             qreal longitude,
                             bool canDoDirect,
                             const QString &routePath,
                             const QChar &symbolTable,
                             const QChar &symbolCode)
{
    QMutexLocker locker(m_mutex);

    GeoAprsCoordinates location(longitude, latitude, m_seenFrom);
    if (canDoDirect) {
        if (!routePath.contains(QLatin1Char('*'))) {
            location.addSeenFrom(GeoAprsCoordinates::Directly);
        }
    }

    if (m_objects->contains(callSign)) {
        // we already have one for this callSign; just add the new
        // history item.
        (*m_objects)[callSign]->setLocation(location);
    } else {
        auto foundObject = new AprsObject(location, callSign);
        foundObject->setPixmapId(m_pixmaps[QPair<QChar, QChar>(symbolTable, symbolCode)]);
        (*m_objects)[callSign] = foundObject;
        mDebug() << "aprs:  new: " << callSign.toLocal8Bit().data();
    }
}

qreal AprsGatherer::calculateLongitude(const QString &threeBytes, int offset, bool isEast)
{
    // otherwise known as "fun with funky encoding"
    qreal hours = threeBytes[0].toLatin1() - 28 + offset;
    if (180 <= hours && hours <= 189)
        hours -= 80;
    if (190 <= hours && hours <= 199)
        hours -= 190;

    hours += (qreal((threeBytes[1].toLatin1() - 28) % 60) + (qreal(threeBytes[2].toLatin1() - 28)) / 100) / 60.0;

    if (!isEast)
        hours = -hours;
    return hours;
}

void AprsGatherer::setDumpOutput(bool to)
{
    m_dumpOutput = to;
}

bool AprsGatherer::dumpOutput() const
{
    return m_dumpOutput;
}

void AprsGatherer::setSeenFrom(GeoAprsCoordinates::SeenFrom to)
{
    m_seenFrom = to;
}

GeoAprsCoordinates::SeenFrom AprsGatherer::seenFrom()
{
    return m_seenFrom;
}

// gets around parent QThread protected sleep as our objects need a sleep
void AprsGatherer::sleepFor(int seconds)
{
    sleep(seconds);
}

#include "moc_AprsGatherer.cpp"
