/****************************************************************************
 *
 *   (c) 2019 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
 *
 * QGroundControl is licensed according to the terms in the file
 * COPYING.md in the root of the source code directory.
 *
 ****************************************************************************/

#include "PairingManager.h"
#include "SettingsManager.h"
#include "MicrohardManager.h"
#include "QGCApplication.h"
#include "QGCCorePlugin.h"

#include <QSettings>
#include <QJsonObject>
#include <QStandardPaths>
#include <QMutexLocker>

QGC_LOGGING_CATEGORY(PairingManagerLog, "PairingManagerLog")

static const char* jsonFileName = "pairing.json";

//-----------------------------------------------------------------------------
static QString
random_string(uint length)
{
    auto randchar = []() -> char
    {
        const char charset[] =
            "0123456789"
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            "abcdefghijklmnopqrstuvwxyz";
        const uint max_index = (sizeof(charset) - 1);
        return charset[static_cast<uint>(rand()) % max_index];
    };
    std::string str(length, 0);
    std::generate_n(str.begin(), length, randchar);
    return QString::fromStdString(str);
}

//-----------------------------------------------------------------------------
PairingManager::PairingManager(QGCApplication* app, QGCToolbox* toolbox)
    : QGCTool(app, toolbox)
    , _aes("J6+KuWh9K2!hG(F'", 0x368de30e8ec063ce)
{
    _jsonFileName = QDir::temp().filePath(jsonFileName);
    connect(this, &PairingManager::parsePairingJson, this, &PairingManager::_parsePairingJson);
    connect(this, &PairingManager::setPairingStatus, this, &PairingManager::_setPairingStatus);
    connect(this, &PairingManager::startUpload, this, &PairingManager::_startUpload);
}

//-----------------------------------------------------------------------------
PairingManager::~PairingManager()
{
}

//-----------------------------------------------------------------------------

void
PairingManager::setToolbox(QGCToolbox *toolbox)
{
    QGCTool::setToolbox(toolbox);
    _updatePairedDeviceNameList();
    emit pairedListChanged();
}

//-----------------------------------------------------------------------------
void
PairingManager::_pairingCompleted(QString name)
{
    _writeJson(_jsonDoc, _pairingCacheFile(name));
    _remotePairingMap["NM"] = name;
    _lastPaired = name;
    _updatePairedDeviceNameList();
    emit pairedListChanged();
    emit pairedVehicleChanged();
    //_app->informationMessageBoxOnMainThread("", tr("Paired with %1").arg(name));
    setPairingStatus(PairingSuccess, tr("Pairing Successfull"));
}

//-----------------------------------------------------------------------------
void
PairingManager::_connectionCompleted(QString /*name*/)
{
    //QString pwd = _remotePairingMap["PWD"].toString();
    //_toolbox->microhardManager()->switchToConnectionEncryptionKey(pwd);
    //_app->informationMessageBoxOnMainThread("", tr("Connected to %1").arg(name));
    setPairingStatus(PairingConnected, tr("Connection Successfull"));
}

//-----------------------------------------------------------------------------
void
PairingManager::_startUpload(QString pairURL, QJsonDocument jsonDoc)
{
    QMutexLocker lock(&_uploadMutex);
    if (_uploadManager != nullptr) {
        return;
    }
    _uploadManager = new QNetworkAccessManager(this);

    QString str = jsonDoc.toJson(QJsonDocument::JsonFormat::Compact);
    qCDebug(PairingManagerLog) << "Starting upload to: " << pairURL << " " << str;
    _uploadData = QString::fromStdString(_aes.encrypt(str.toStdString()));
    _uploadURL = pairURL;
    _startUploadRequest();
}

//-----------------------------------------------------------------------------
void
PairingManager::_startUploadRequest()
{
    QNetworkRequest req;
    req.setUrl(QUrl(_uploadURL));
    req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
    QNetworkReply *reply = _uploadManager->post(req, _uploadData.toUtf8());
    connect(reply, &QNetworkReply::finished, this, &PairingManager::_uploadFinished);
}

//-----------------------------------------------------------------------------
void
PairingManager::_stopUpload()
{
    QMutexLocker lock(&_uploadMutex);
    if (_uploadManager != nullptr) {
        delete _uploadManager;
        _uploadManager = nullptr;
    }
}

//-----------------------------------------------------------------------------
void
PairingManager::_uploadFinished()
{
    QMutexLocker lock(&_uploadMutex);
    QNetworkReply* reply = qobject_cast<QNetworkReply*>(QObject::sender());
    if (reply) {
        if (_uploadManager != nullptr) {
            if (reply->error() == QNetworkReply::NoError) {
                qCDebug(PairingManagerLog) << "Upload finished.";
                QByteArray bytes = reply->readAll();
                QString str = QString::fromUtf8(bytes.data(), bytes.size());
                qCDebug(PairingManagerLog) << "Reply: " << str;
                auto a = str.split(QRegExp("\\s+"));
                if (a[0] == "Accepted" && a.length() > 1) {
                    _pairingCompleted(a[1]);
                } else if (a[0] == "Connected" && a.length() > 1) {
                    _connectionCompleted(a[1]);
                } else if (a[0] == "Connection" && a.length() > 1) {
                    setPairingStatus(PairingConnectionRejected, tr("Connection Rejected"));
                    qCDebug(PairingManagerLog) << "Connection error: " << str;
                } else {
                    setPairingStatus(PairingRejected, tr("Pairing Rejected"));
                    qCDebug(PairingManagerLog) << "Pairing error: " << str;
                }
                _uploadManager->deleteLater();
                _uploadManager = nullptr;
            } else {
                if(++_pairRetryCount > 3) {
                    qCDebug(PairingManagerLog) << "Giving up";
                    setPairingStatus(PairingError, tr("No Response From Vehicle"));
                    _uploadManager->deleteLater();
                    _uploadManager = nullptr;
                } else {
                    qCDebug(PairingManagerLog) << "Upload error: " + reply->errorString();
                    _startUploadRequest();
                }
            }
        }
    }
}

//-----------------------------------------------------------------------------
void
PairingManager::_parsePairingJsonFile()
{
    QFile file(_jsonFileName);
    file.open(QIODevice::ReadOnly | QIODevice::Text);
    QString json = file.readAll();
    file.remove();
    file.close();

    jsonReceived(json);
}

//-----------------------------------------------------------------------------
void
PairingManager::connectToPairedDevice(QString name)
{
    setPairingStatus(PairingConnecting, tr("Connecting to %1").arg(name));
    QFile file(_pairingCacheFile(name));
    file.open(QIODevice::ReadOnly | QIODevice::Text);
    QString json = file.readAll();
    jsonReceived(json);
}

//-----------------------------------------------------------------------------
void
PairingManager::removePairedDevice(QString name)
{
    QFile file(_pairingCacheFile(name));
    file.remove();
    _updatePairedDeviceNameList();
    emit pairedListChanged();
}

//-----------------------------------------------------------------------------
void
PairingManager::_updatePairedDeviceNameList()
{
    _deviceList.clear();
    QDirIterator it(_pairingCacheDir().absolutePath(), QDir::Files);
    while (it.hasNext()) {
        QFileInfo fileInfo(it.next());
        _deviceList.append(fileInfo.fileName());
        qCDebug(PairingManagerLog) << "Listing: " << fileInfo.fileName();
    }
}

//-----------------------------------------------------------------------------
QString
PairingManager::_assumeMicrohardPairingJson()
{
    QJsonDocument json;
    QJsonObject   jsonObject;

    jsonObject.insert("LT", "MH");
    jsonObject.insert("IP", "192.168.168.10");
    jsonObject.insert("AIP", _toolbox->microhardManager()->remoteIPAddr());
    jsonObject.insert("CU",  _toolbox->microhardManager()->configUserName());
    jsonObject.insert("CP",  _toolbox->microhardManager()->configPassword());
    jsonObject.insert("EK",  _toolbox->microhardManager()->encryptionKey());
    json.setObject(jsonObject);

    return QString(json.toJson(QJsonDocument::Compact));
}

//-----------------------------------------------------------------------------
void
PairingManager::_parsePairingJson(QString jsonEnc)
{
    QString json = QString::fromStdString(_aes.decrypt(jsonEnc.toStdString()));
    if (json == "") {
        json = jsonEnc;
    }
    qCDebug(PairingManagerLog) << "Parsing JSON: " << json;

    _jsonDoc = QJsonDocument::fromJson(json.toUtf8());

    if (_jsonDoc.isNull()) {
        setPairingStatus(PairingError, tr("Invalid Pairing File"));
        qCDebug(PairingManagerLog) << "Failed to create Pairing JSON doc.";
        return;
    }
    if (!_jsonDoc.isObject()) {
        setPairingStatus(PairingError, tr("Error Parsing Pairing File"));
        qCDebug(PairingManagerLog) << "Pairing JSON is not an object.";
        return;
    }

    QJsonObject jsonObj = _jsonDoc.object();

    if (jsonObj.isEmpty()) {
        setPairingStatus(PairingError, tr("Error Parsing Pairing File"));
        qCDebug(PairingManagerLog) << "Pairing JSON object is empty.";
        return;
    }

    _remotePairingMap = jsonObj.toVariantMap();
    QString linkType  = _remotePairingMap["LT"].toString();
    QString pport     = _remotePairingMap["PP"].toString();
    if (pport.length()==0) {
        pport = "29351";
    }

    if (linkType.length()==0) {
        setPairingStatus(PairingError, tr("Error Parsing Pairing File"));
        qCDebug(PairingManagerLog) << "Pairing JSON is malformed.";
        return;
    }

    _toolbox->microhardManager()->switchToPairingEncryptionKey();

    QString pairURL = "http://" + _remotePairingMap["IP"].toString() + ":" + pport;
    bool connecting = jsonObj.contains("PWD");
    QJsonDocument jsonDoc;

    if (!connecting) {
        pairURL +=  + "/pair";
        QString pwd = random_string(8);
        // TODO generate certificates
        QString cert1 = "";
        QString cert2 = "";
        jsonObj.insert("PWD", pwd);
        jsonObj.insert("CERT1", cert1);
        jsonObj.insert("CERT2", cert2);
        _jsonDoc.setObject(jsonObj);
        if (linkType == "ZT") {
            jsonDoc = _createZeroTierPairingJson(cert1);
        } else if (linkType == "MH") {
            jsonDoc = _createMicrohardPairingJson(pwd, cert1);
        }
    } else {
        pairURL +=  + "/connect";
        QString cert2 = _remotePairingMap["CERT2"].toString();
        if (linkType == "ZT") {
            jsonDoc = _createZeroTierConnectJson(cert2);
        } else if (linkType == "MH") {
            jsonDoc = _createMicrohardConnectJson(cert2);
        }
    }

    if (linkType == "ZT") {
        _toolbox->settingsManager()->appSettings()->enableMicrohard()->setRawValue(false);
        _toolbox->settingsManager()->appSettings()->enableTaisync()->setRawValue(false);
        emit startUpload(pairURL, jsonDoc);
    } else if (linkType == "MH") {
        _toolbox->settingsManager()->appSettings()->enableMicrohard()->setRawValue(true);
        _toolbox->settingsManager()->appSettings()->enableTaisync()->setRawValue(false);
        if (_remotePairingMap.contains("AIP")) {
            _toolbox->microhardManager()->setRemoteIPAddr(_remotePairingMap["AIP"].toString());
        }
        if (_remotePairingMap.contains("CU")) {
            _toolbox->microhardManager()->setConfigUserName(_remotePairingMap["CU"].toString());
        }
        if (_remotePairingMap.contains("CP")) {
            _toolbox->microhardManager()->setConfigPassword(_remotePairingMap["CP"].toString());
        }
        if (_remotePairingMap.contains("EK") && !connecting) {
            _toolbox->microhardManager()->setEncryptionKey(_remotePairingMap["EK"].toString());
        }
        _toolbox->microhardManager()->updateSettings();
        emit startUpload(pairURL, jsonDoc);
    }
}

//-----------------------------------------------------------------------------
QString
PairingManager::_getLocalIPInNetwork(QString remoteIP, int num)
{
    QStringList pieces = remoteIP.split(".");
    QString ipPrefix = "";
    for (int i = 0; i<num && i<pieces.length(); i++) {
        ipPrefix += pieces[i] + ".";
    }

    const QHostAddress &localhost = QHostAddress(QHostAddress::LocalHost);
    for (const QHostAddress &address: QNetworkInterface::allAddresses()) {
        if (address.protocol() == QAbstractSocket::IPv4Protocol && address != localhost) {
            if (address.toString().startsWith(ipPrefix)) {
                return address.toString();
            }
        }
    }

    return "";
}

//-----------------------------------------------------------------------------
QDir
PairingManager::_pairingCacheDir()
{
    const QString spath(QFileInfo(QSettings().fileName()).dir().absolutePath());
    QDir dir = spath + QDir::separator() + "PairingCache";
    if (!dir.exists()) {
        dir.mkpath(".");
    }

    return dir;
}

//-----------------------------------------------------------------------------
QString
PairingManager::_pairingCacheFile(QString uavName)
{
    return _pairingCacheDir().filePath(uavName);
}

//-----------------------------------------------------------------------------
void
PairingManager::_writeJson(QJsonDocument &jsonDoc, QString fileName)
{
    QString val = jsonDoc.toJson(QJsonDocument::JsonFormat::Compact);
    qCDebug(PairingManagerLog) << "Write json " << val;
    QString enc = QString::fromStdString(_aes.encrypt(val.toStdString()));

    QFile file(fileName);
    file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate);
    file.write(enc.toUtf8());
    file.close();
}

//-----------------------------------------------------------------------------
QJsonDocument
PairingManager::_createZeroTierPairingJson(QString cert1)
{
    QString localIP = _getLocalIPInNetwork(_remotePairingMap["IP"].toString(), 2);

    QJsonObject jsonObj;
    jsonObj.insert("LT", "ZT");
    jsonObj.insert("IP", localIP);
    jsonObj.insert("P", 14550);
    jsonObj.insert("CERT1", cert1);
    return QJsonDocument(jsonObj);
}

//-----------------------------------------------------------------------------
QJsonDocument
PairingManager::_createMicrohardPairingJson(QString pwd, QString cert1)
{
    QString localIP = _getLocalIPInNetwork(_remotePairingMap["IP"].toString(), 3);

    QJsonObject jsonObj;
    jsonObj.insert("LT", "MH");
    jsonObj.insert("IP", localIP);
    jsonObj.insert("P", 14550);
    jsonObj.insert("PWD", pwd);
    jsonObj.insert("CERT1", cert1);
    return QJsonDocument(jsonObj);
}

//-----------------------------------------------------------------------------
QJsonDocument
PairingManager::_createZeroTierConnectJson(QString cert2)
{
    QString localIP = _getLocalIPInNetwork(_remotePairingMap["IP"].toString(), 2);

    QJsonObject jsonObj;
    jsonObj.insert("LT", "ZT");
    jsonObj.insert("IP", localIP);
    jsonObj.insert("P", 14550);
    jsonObj.insert("CERT2", cert2);
    return QJsonDocument(jsonObj);
}

//-----------------------------------------------------------------------------
QJsonDocument
PairingManager::_createMicrohardConnectJson(QString cert2)
{
    QString localIP = _getLocalIPInNetwork(_remotePairingMap["IP"].toString(), 3);

    QJsonObject jsonObj;
    jsonObj.insert("LT", "MH");
    jsonObj.insert("IP", localIP);
    jsonObj.insert("P", 14550);
    jsonObj.insert("CERT2", cert2);
    return QJsonDocument(jsonObj);
}

//-----------------------------------------------------------------------------

QStringList
PairingManager::pairingLinkTypeStrings()
{
    //-- Must follow same order as enum LinkType in LinkConfiguration.h
    static QStringList list;
    int i = 0;
    if (!list.size()) {
#if defined QGC_ENABLE_QTNFC
        list += tr("NFC");
        _nfcIndex = i++;
#endif
#if defined QGC_GST_MICROHARD_ENABLED
        list += tr("Microhard");
        _microhardIndex = i++;
#endif
    }
    return list;
}

//-----------------------------------------------------------------------------
void
PairingManager::_setPairingStatus(PairingStatus status, QString statusStr)
{
    _status = status;
    _statusString = statusStr;
    emit pairingStatusChanged();
}

//-----------------------------------------------------------------------------
QString
PairingManager::pairingStatusStr() const
{
    return _statusString;
}

#if QGC_GST_MICROHARD_ENABLED
//-----------------------------------------------------------------------------
void
PairingManager::startMicrohardPairing()
{
    stopPairing();
    _pairRetryCount = 0;
    setPairingStatus(PairingActive, tr("Pairing..."));
    _parsePairingJson(_assumeMicrohardPairingJson());
}
#endif

//-----------------------------------------------------------------------------
void
PairingManager::stopPairing()
{
#if defined QGC_ENABLE_QTNFC
    pairingNFC.stop();
#endif
    _stopUpload();
    setPairingStatus(PairingIdle, "");
}

#if defined QGC_ENABLE_QTNFC
//-----------------------------------------------------------------------------
void
PairingManager::startNFCScan()
{
    stopPairing();
    setPairingStatus(PairingActive, tr("Pairing..."));
    pairingNFC.start();
}

#endif

//-----------------------------------------------------------------------------
#ifdef __android__
static const char kJniClassName[] {"org/mavlink/qgroundcontrol/QGCActivity"};

//-----------------------------------------------------------------------------
static void jniNFCTagReceived(JNIEnv *envA, jobject thizA, jstring messageA)
{
    Q_UNUSED(thizA);

    const char *stringL = envA->GetStringUTFChars(messageA, nullptr);
    QString ndef = QString::fromUtf8(stringL);
    envA->ReleaseStringUTFChars(messageA, stringL);
    if (envA->ExceptionCheck())
        envA->ExceptionClear();
    qCDebug(PairingManagerLog) << "NDEF Tag Received: " << ndef;
    qgcApp()->toolbox()->pairingManager()->jsonReceived(ndef);
}

//-----------------------------------------------------------------------------
void PairingManager::setNativeMethods(void)
{
    //  REGISTER THE C++ FUNCTION WITH JNI
    JNINativeMethod javaMethods[] {
        {"nativeNFCTagReceived", "(Ljava/lang/String;)V", reinterpret_cast<void *>(jniNFCTagReceived)}
    };

    QAndroidJniEnvironment jniEnv;
    if (jniEnv->ExceptionCheck()) {
        jniEnv->ExceptionDescribe();
        jniEnv->ExceptionClear();
    }

    jclass objectClass = jniEnv->FindClass(kJniClassName);
    if(!objectClass) {
        qWarning() << "Couldn't find class:" << kJniClassName;
        return;
    }

    jint val = jniEnv->RegisterNatives(objectClass, javaMethods, sizeof(javaMethods) / sizeof(javaMethods[0]));

    if (val < 0) {
        qWarning() << "Error registering methods: " << val;
    } else {
        qCDebug(PairingManagerLog) << "Native Functions Registered";
    }

    if (jniEnv->ExceptionCheck()) {
        jniEnv->ExceptionDescribe();
        jniEnv->ExceptionClear();
    }
}
#endif
//-----------------------------------------------------------------------------