///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel                                                   //
// Copyright (C) 2015-2019, 2023 Edouard Griffiths, F4EXB <f4exb06@gmail.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 <QStandardPaths>
#include <QDir>
#include <QDateTime>

#include <array>
#include <algorithm>
#include <cmath>

#include "channel/channelapi.h"
#include "dsp/wavfilerecord.h"
#include "util/messagequeue.h"
#include "util/ft8message.h"
#include "util/maidenhead.h"
#include "maincore.h"
#include "SWGMapItem.h"

#include "ft8demodsettings.h"
#include "ft8demodworker.h"
#include "libldpc.h"
#include "osd.h"

FT8DemodWorker::FT8Callback::FT8Callback(
    const QDateTime& periodTS,
    qint64 baseFrequency,
    FT8::Packing& packing,
    const QString& name
) :
    m_packing(packing),
    m_periodTS(periodTS),
    m_name(name),
    m_validCallsigns(nullptr)
{
    m_msgReportFT8Messages = MsgReportFT8Messages::create();
    m_msgReportFT8Messages->setBaseFrequency(baseFrequency);
}

int FT8DemodWorker::FT8Callback::hcb(
    int *a91,
    float hz0,
    float off,
    const char *comment,
    float snr,
    int pass,
    int correct_bits
)
{
    std::string call1;
    std::string call2;
    std::string loc;
    std::string type;
    std::string msg = m_packing.unpack(a91, call1, call2, loc, type);

    cycle_mu.lock();

    if (cycle_already.count(msg) > 0)
    {
        // already decoded this message on this cycle
        cycle_mu.unlock();
        return 1; // 1 => already seen, don't subtract.
    }

    cycle_already[msg] = true;
    QString call2Str(call2.c_str());
    QString info(comment);

    if (m_validCallsigns && info.startsWith("OSD") && !m_validCallsigns->contains(call2Str))
    {
        cycle_mu.unlock();
        return 2; // leave without reporting. Set as new decode.
    }

    QList<FT8Message>& ft8Messages = m_msgReportFT8Messages->getFT8Messages();
    FT8Message baseMessage{
        m_periodTS,
        QString(type.c_str()),
        pass,
        (int) snr,
        correct_bits,
        off - 0.5f,
        hz0,
        QString(call1.c_str()).simplified(),
        call2Str,
        QString(loc.c_str()).simplified(),
        info
    };

    // DXpedition packs two messages in one with the two callees in the first call area separated by a semicolon
    if (type == "0.1")
    {
        QStringList callees = QString(call1.c_str()).simplified().split(";");

        for (int i = 0; i < 2; i++)
        {
            baseMessage.call1 = callees[i];

            if (i == 0) { // first is with RR73 greetings in locator area
                baseMessage.loc = "RR73";
            }

            ft8Messages.push_back(baseMessage);
        }
    }
    else
    {
        ft8Messages.push_back(baseMessage);
    }

    cycle_mu.unlock();

    return 2; // 2 => new decode, do subtract.
}

QString FT8DemodWorker::FT8Callback::get_name()
{
    return m_name;
}

FT8DemodWorker::FT8DemodWorker() :
    m_recordSamples(false),
    m_logMessages(false),
    m_enablePskReporter(false),
    m_nbDecoderThreads(6),
    m_decoderTimeBudget(0.5),
    m_decoderMode(FT8DemodSettings::DecoderModeFT8),
    m_useOSD(false),
    m_osdDepth(0),
    m_osdLDPCThreshold(70),
    m_verifyOSD(false),
    m_lowFreq(200),
    m_highFreq(3000),
    m_unsupportedModeWarningPending(true),
    m_invalidSequence(true),
    m_baseFrequency(0),
    m_guiReportingMessageQueue(nullptr),
    m_pskReportingMessageQueue(nullptr),
    m_channel(nullptr)
{
    QString relPath = "ft8/save";
    QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
    dir.mkpath(relPath);
    m_samplesPath = dir.absolutePath() + "/" + relPath;
    qDebug("FT8DemodWorker::FT8DemodWorker: samples path: %s", qPrintable(m_samplesPath));
    relPath = "ft8/logs";
    m_logsPath = dir.absolutePath() + "/" + relPath;
    qDebug("FT8DemodWorker::FT8DemodWorker: logs path: %s", qPrintable(m_logsPath));

    QDir samplesDir(m_samplesPath);
    if (!samplesDir.exists()) {
        samplesDir.mkpath(".");
    }

    QDir logsDir(m_logsPath);
    if (!logsDir.exists()) {
        logsDir.mkpath(".");
    }
}

FT8DemodWorker::~FT8DemodWorker()
{}

void FT8DemodWorker::setDecoderMode(FT8DemodSettings::DecoderMode decoderMode)
{
    m_decoderMode = decoderMode;
    m_unsupportedModeWarningPending = true;
}


void FT8DemodWorker::processFT8(int16_t *buffer, int frameSampleCount, FT8Callback& ft8Callback, const int *hints)
{
    m_ft8Decoder.getParams().nthreads = m_nbDecoderThreads;
    m_ft8Decoder.getParams().use_osd = m_useOSD ? 1 : 0;
    m_ft8Decoder.getParams().osd_depth = m_osdDepth;
    m_ft8Decoder.getParams().osd_ldpc_thresh = m_osdLDPCThreshold;
    std::vector<float> samples(frameSampleCount);

    std::transform(
        buffer,
        buffer + frameSampleCount,
        samples.begin(),
        [](const int16_t& s) -> float { return s / 32768.0f; }
    );

    m_ft8Decoder.entry(
        samples.data(),
        samples.size(),
        0.5 * FT8DemodSettings::m_ft8SampleRate,
        FT8DemodSettings::m_ft8SampleRate,
        m_lowFreq,
        m_highFreq,
        hints,
        hints,
        m_decoderTimeBudget,
        m_decoderTimeBudget,
        &ft8Callback,
        0,
        (struct FT8::cdecode *) nullptr
    );

    m_ft8Decoder.wait(m_decoderTimeBudget + 1.0); // add one second to budget to force quit threads
}

void FT8DemodWorker::processFT4(int16_t *buffer, int frameSampleCount, FT8Callback& ft8Callback, const int *hints)
{
    std::vector<float> samples(frameSampleCount);
    std::transform(
        buffer,
        buffer + frameSampleCount,
        samples.begin(),
        [](const int16_t& s) -> float { return s / 32768.0f; }
    );

    m_ft4Decoder.getParams().nthreads = m_nbDecoderThreads;
    m_ft4Decoder.getParams().ldpc_iters = m_ft8Decoder.getParams().ldpc_iters;
    m_ft4Decoder.getParams().use_osd = m_useOSD ? 1 : 0;
    m_ft4Decoder.getParams().osd_depth = m_osdDepth;
    m_ft4Decoder.getParams().osd_ldpc_thresh = m_osdLDPCThreshold;
    m_ft4Decoder.entry(
        samples.data(),
        samples.size(),
        0.5 * FT8DemodSettings::m_ft8SampleRate,
        FT8DemodSettings::m_ft8SampleRate,
        m_lowFreq,
        m_highFreq,
        hints,
        hints,
        m_decoderTimeBudget,
        m_decoderTimeBudget,
        &ft8Callback,
        0,
        (struct FT8::cdecode *) nullptr
    );

    m_ft4Decoder.wait(m_decoderTimeBudget + 1.0);
}

void FT8DemodWorker::processBuffer(int16_t *buffer, QDateTime periodTS)
{
    const int frameSampleCount = FT8DemodSettings::getDecoderFrameSamples(m_decoderMode);
    const QString decoderMode = FT8DemodSettings::getDecoderModeString(m_decoderMode);

    qDebug("FT8DemodWorker::processBuffer: %6.3f %s mode:%s %d:%f [%d:%d]",
        m_baseFrequency / 1000000.0,
        qPrintable(periodTS.toString("yyyy-MM-dd HH:mm:ss")),
        qPrintable(decoderMode),
        m_nbDecoderThreads,
        m_decoderTimeBudget,
        m_lowFreq,
        m_highFreq
    );

    if (m_invalidSequence)
    {
        qDebug("FT8DemodWorker::processBuffer: invalid sequence");
        m_invalidSequence = false;
        return;
    }

    QString channelReference = "d0c0"; // default

    if (m_channel) {
        channelReference = tr("d%1c%2").arg(m_channel->getDeviceSetIndex()).arg(m_channel->getIndexInDeviceSet());
    }

    int hints[2] = { 2, 0 }; // CQ
    FT8Callback ft8Callback(periodTS, m_baseFrequency, m_packing, channelReference);
    ft8Callback.setValidCallsigns((m_useOSD && m_verifyOSD) ? &m_validCallsigns : nullptr);

    if (m_decoderMode == FT8DemodSettings::DecoderModeFT8) {
        processFT8(buffer, frameSampleCount, ft8Callback, hints);
    } else if (m_decoderMode == FT8DemodSettings::DecoderModeFT4) {
        processFT4(buffer, frameSampleCount, ft8Callback, hints);
    }
    else
    {
        if (m_unsupportedModeWarningPending)
        {
            qWarning("FT8DemodWorker::processBuffer: %s decoding is not supported", qPrintable(decoderMode));
            m_unsupportedModeWarningPending = false;
        }
    }

    qDebug("FT8DemodWorker::processBuffer: done: at %6.3f %d messages",
        m_baseFrequency / 1000000.0, (int)ft8Callback.getReportMessage()->getFT8Messages().size());

    if (m_guiReportingMessageQueue) {
        m_guiReportingMessageQueue->push(new MsgReportFT8Messages(*ft8Callback.getReportMessage()));
    }

    if (m_enablePskReporter && m_pskReportingMessageQueue) {
        m_pskReportingMessageQueue->push(new MsgReportFT8Messages(*ft8Callback.getReportMessage()));
    }

    QList<ObjectPipe*> mapPipes;
    MainCore::instance()->getMessagePipes().getMessagePipes((const QObject*) m_channel, "mapitems", mapPipes);
    const QList<FT8Message>& ft8Messages = ft8Callback.getReportMessage()->getFT8Messages();
    std::ofstream logFile;
    double baseFrequencyMHz = m_baseFrequency/1000000.0;

    for (const auto& ft8Message : ft8Messages)
    {
        if (m_logMessages)
        {
            if (!logFile.is_open())
            {
                QString logFileName(tr("%1_%2.txt").arg(periodTS.toString("yyyyMMdd")).arg(channelReference));
                QFileInfo lfi(QDir(m_logsPath), logFileName);
                QString logFilePath = lfi.absoluteFilePath();

                if (lfi.exists()) {
                    logFile.open(logFilePath.toStdString(), std::ios::app);
                } else {
                    logFile.open(logFilePath.toStdString());
                }
            }

            if (ft8Message.call1 == "UNK") {
                continue;
            }

            QString logMessage = QString("%1 %2 Rx %3 %4 %5 %6 %7 %8 %9")
                .arg(periodTS.toString("yyyyMMdd_HHmmss"))
                .arg(baseFrequencyMHz, 9, 'f', 3)
                .arg(decoderMode)
                .arg(ft8Message.snr, 6)
                .arg(ft8Message.dt, 4, 'f', 1)
                .arg(ft8Message.df, 4, 'f', 0)
                .arg(ft8Message.call1)
                .arg(ft8Message.call2)
                .arg(ft8Message.loc);
            logMessage.remove(0, 2);
            logFile << logMessage.toStdString() << std::endl;
        }
        else
        {
            if (logFile.is_open()) {
                logFile.close();
            }
        }

        if (mapPipes.size() > 0)
        {
            // If message contains a Maidenhead locator, display caller on Map feature
            float latitude, longitude;

            if ((ft8Message.loc.size() == 4) && (ft8Message.loc != "RR73") && Maidenhead::fromMaidenhead(ft8Message.loc, latitude, longitude))
            {
                QString text = QString("%1\nMode: %2\nFrequency: %3 Hz\nLocator: %4\nSNR: %5\nLast heard: %6")
                                    .arg(ft8Message.call2)
                                    .arg(decoderMode)
                                    .arg(baseFrequencyMHz*1000000 + ft8Message.df)
                                    .arg(ft8Message.loc)
                                    .arg(ft8Message.snr)
                                    .arg(periodTS.toString("dd MMM yyyy HH:mm:ss"));

                for (const auto& pipe : mapPipes)
                {
                    MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
                    SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
                    swgMapItem->setName(new QString(ft8Message.call2));
                    swgMapItem->setLatitude(latitude);
                    swgMapItem->setLongitude(longitude);
                    swgMapItem->setAltitude(0);
                    swgMapItem->setAltitudeReference(1); // CLAMP_TO_GROUND
                    swgMapItem->setPositionDateTime(new QString(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
                    swgMapItem->setImageRotation(0);
                    swgMapItem->setText(new QString(text));
                    swgMapItem->setImage(new QString("antenna.png"));
                    swgMapItem->setModel(new QString("antenna.glb"));
                    swgMapItem->setModelAltitudeOffset(0.0);
                    swgMapItem->setLabel(new QString(ft8Message.call2));
                    swgMapItem->setLabelAltitudeOffset(4.5);
                    swgMapItem->setFixedPosition(false);
                    swgMapItem->setOrientation(0);
                    swgMapItem->setHeading(0);
                    MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create((const QObject*) m_channel, swgMapItem);
                    messageQueue->push(msg);
                }
            }
        }

        if (m_verifyOSD && !ft8Message.decoderInfo.startsWith("OSD"))
        {
            if ((ft8Message.type == "1") || (ft8Message.type == "2"))
            {
                if (!ft8Message.call2.startsWith("<")) {
                    m_validCallsigns.insert(ft8Message.call2);
                }

                if (!ft8Message.call1.startsWith("CQ") && !ft8Message.call1.startsWith("<")) {
                    m_validCallsigns.insert(ft8Message.call1);
                }
            }
        }
    }

    delete ft8Callback.getReportMessage();

    if (m_recordSamples)
    {
        WavFileRecord *wavFileRecord = new WavFileRecord(FT8DemodSettings::m_ft8SampleRate);
        QFileInfo wfi(QDir(m_samplesPath), periodTS.toString("yyyyMMdd_HHmmss"));
        QString wpath = wfi.absoluteFilePath();
        qDebug("FT8DemodWorker::processBuffer: WAV file: %s.wav", qPrintable(wpath));
        wavFileRecord->setFileName(wpath);
        wavFileRecord->setFileBaseIsFileName(true);
        wavFileRecord->setMono(true);
        wavFileRecord->startRecording();
        wavFileRecord->writeMono(buffer, frameSampleCount);
        wavFileRecord->stopRecording();
        delete wavFileRecord;
    }
}
