// SPDX-License-Identifier: LGPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea.
*
* nymea is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* nymea 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "scriptintegrationplugin.h"

#include <QQmlEngine>
#include <QDir>
#include <QJsonDocument>

#include "loggingcategories.h"
#include <plugintimer.h>

ScriptIntegrationPlugin::ScriptIntegrationPlugin(QObject *parent) : IntegrationPlugin(parent)
{

}

bool ScriptIntegrationPlugin::loadScript(const QString &fileName)
{

    QFileInfo fi(fileName);
    QString metaDataFileName = fi.absoluteDir().path() + '/' + fi.baseName() + ".json";

    QFile metaDataFile(metaDataFileName);
    if (!metaDataFile.open(QFile::ReadOnly)) {
        qCWarning(dcThingManager()) << "Failed to open plugin metadata at" << metaDataFileName;
        return false;
    }
    QJsonParseError error;
    QByteArray data = metaDataFile.readAll();
    QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
    metaDataFile.close();

    if (error.error != QJsonParseError::NoError) {
        int errorOffset = error.offset;
        int newLineIndex = data.indexOf("\n");
        int lineIndex = 1;
        while (newLineIndex > 0 && errorOffset > newLineIndex) {
            data.remove(0, newLineIndex + 2);
            errorOffset -= (newLineIndex + 2);
            newLineIndex = data.indexOf("\n");
            lineIndex++;
        }
        if (newLineIndex >= 0) {
            data = data.left(newLineIndex);
        }
        QString spacer;
        for (int i = 0; i < errorOffset; i++) {
            spacer += ' ';
        }
        QDebug dbg = qWarning(dcThingManager()).nospace().noquote();
        dbg << metaDataFileName << ":" << lineIndex << ":" << errorOffset + 2 << ": error: JSON parsing failed: " << error.errorString() << ": " << data.trimmed() << '\n';
        dbg << data << '\n';
        dbg << spacer << "^";
        return false;
    }
    setMetaData(QJsonObject::fromVariantMap(jsonDoc.toVariant().toMap()));

    m_engine = new QQmlEngine(this);
    m_engine->installExtensions(QJSEngine::AllExtensions);

    QJSValue thingMetaObject = m_engine->newQMetaObject(&Thing::staticMetaObject);
    m_engine->globalObject().setProperty("Thing", thingMetaObject);

    m_pluginImport = m_engine->importModule(fileName);
    if (m_pluginImport.isError()) {
        qCWarning(dcThingManager()) << "Error loading plugin module" << m_pluginImport.errorType() << m_pluginImport.toString();
        return false;
    }

    return true;
}

void ScriptIntegrationPlugin::init()
{
    //Couldn't find an non-qml way to register abstract classes in the JS engine as qRegisterMetatype doesn't deal so well with abstract classes
    qmlRegisterUncreatableType<PluginTimerManager>("nymea", 1, 0, "PluginTimerManager", "Get it from hardwareManager");
    qmlRegisterUncreatableType<PluginTimer>("nymea", 1, 0, "PluginTimer", "Get it from PluginTimerManager");

    QJSValue hardwareManagerObject = m_engine->newQObject(hardwareManager());
    m_engine->globalObject().setProperty("hardwareManager", hardwareManagerObject);

    if (!m_pluginImport.hasOwnProperty("init")) {
        IntegrationPlugin::init();
        return;
    }
    QJSValue initFunction = m_pluginImport.property("init");
    QJSValue result = initFunction.call();
    if (result.isError()) {
        qCWarning(dcThingManager()) << "Error calling init in JS plugin:" << result.toString();
        return;
    }
}

void ScriptIntegrationPlugin::discoverThings(ThingDiscoveryInfo *info)
{
    if (!m_pluginImport.hasOwnProperty("discoverThings")) {
        IntegrationPlugin::discoverThings(info);
        return;
    }

    ScriptThingDiscoveryInfo *scriptInfo = new ScriptThingDiscoveryInfo(info);
    QJSValue jsInfo = m_engine->newQObject(scriptInfo);

    QJSValue discoverFunction = m_pluginImport.property("discoverThings");
    QJSValue ret = discoverFunction.call({jsInfo});
    if (ret.isError()) {
        qCWarning(dcThingManager()) << "discoverThings script failed to execute:\n" << ret.toString();
    }
}

void ScriptIntegrationPlugin::startPairing(ThingPairingInfo *info)
{
    if (!m_pluginImport.hasOwnProperty("startPairing")) {
        IntegrationPlugin::startPairing(info);
        return;
    }

    ScriptThingPairingInfo *scriptInfo = new ScriptThingPairingInfo(info);
    QJSValue jsInfo = m_engine->newQObject(scriptInfo);

    QJSValue startPairingFunction = m_pluginImport.property("startPairing");
    QJSValue ret = startPairingFunction.call({jsInfo});
    if (ret.isError()) {
        qCWarning(dcThingManager()) << "startPairing script failed to execute:\n" << ret.toString();
    }
}

void ScriptIntegrationPlugin::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
{
    if (!m_pluginImport.hasOwnProperty("confirmPairing")) {
        IntegrationPlugin::confirmPairing(info, username, secret);
        return;
    }

    ScriptThingPairingInfo *scriptInfo = new ScriptThingPairingInfo(info);
    QJSValue jsInfo = m_engine->newQObject(scriptInfo);

    QJSValue confirmPairingFunction = m_pluginImport.property("confirmPairing");
    QJSValue ret = confirmPairingFunction.call({jsInfo, username, secret});
    if (ret.isError()) {
        qCWarning(dcThingManager()) << "confirmPairing script failed to execute:\n" << ret.toString();
    }
}

void ScriptIntegrationPlugin::startMonitoringAutoThings()
{
    if (!m_pluginImport.hasOwnProperty("startMonitoringAutoThings")) {
        IntegrationPlugin::startMonitoringAutoThings();
        return;
    }

    QJSValue monitorFunction = m_pluginImport.property("startMonitoringAutoThings");
    QJSValue ret = monitorFunction.call();
    if (ret.isError()) {
        qCWarning(dcThingManager()) << "startMonitoringAutoThings failed to execute:\n" << ret.toString();
    }
}

void ScriptIntegrationPlugin::setupThing(ThingSetupInfo *info)
{
    if (!m_pluginImport.hasOwnProperty("setupThing")) {
        IntegrationPlugin::setupThing(info);
        return;
    }
    QJSValue setupFunction = m_pluginImport.property("setupThing");

    Thing *thing = info->thing();
    ScriptThing *scriptThing = new ScriptThing(thing);
    m_things.insert(thing, scriptThing);
    connect(thing, &Thing::destroyed, this, [this, thing](){
        m_things.remove(thing);
    });

    ScriptThingSetupInfo *scriptInfo = new ScriptThingSetupInfo(info, scriptThing);

    QJSValue jsInfo = m_engine->newQObject(scriptInfo);
    QJSValue ret = setupFunction.call({jsInfo});

    if (ret.errorType() != QJSValue::NoError) {
        qCWarning(dcThingManager()) << "setupThing script failed to execute:\n" << ret.toString();
    }
}

void ScriptIntegrationPlugin::postSetupThing(Thing *thing)
{
    if (!m_pluginImport.hasOwnProperty("postSetupThing")) {
        IntegrationPlugin::postSetupThing(thing);
        return;
    }
    QJSValue postSetupFunction = m_pluginImport.property("postSetupThing");

    QJSValue jsThing = m_engine->newQObject(m_things.value(thing));
    QJSValue ret = postSetupFunction.call({jsThing});
    if (ret.errorType() != QJSValue::NoError) {
        qCWarning(dcThingManager()) << "setupThing script failed to execute:\n" << ret.toString();
    }
}

void ScriptIntegrationPlugin::thingRemoved(Thing *thing)
{
    if (!m_pluginImport.hasOwnProperty("thingRemoved")) {
        IntegrationPlugin::thingRemoved(thing);
        return;
    }

    QJSValue jsThing = m_engine->newQObject(m_things.value(thing));

    QJSValue thingRemovedFunction = m_pluginImport.property("thingRemoved");
    QJSValue ret = thingRemovedFunction.call({jsThing});
    if (ret.isError()) {
        qCWarning(dcThingManager()) << "thingRemoved script failed to execute:\n" << ret.toString();
    }
}

void ScriptIntegrationPlugin::executeAction(ThingActionInfo *info)
{
    if (!m_pluginImport.hasOwnProperty("executeAction")) {
        IntegrationPlugin::executeAction(info);
        return;
    }

    ScriptThing *scriptThing = m_things.value(info->thing());
    QJSValue jsThing = m_engine->newQObject(scriptThing);

    ScriptThingActionInfo *scriptInfo = new ScriptThingActionInfo(info, scriptThing);
    QJSValue jsInfo = m_engine->newQObject(scriptInfo);

    QJSValue executeActionFunction = m_pluginImport.property("executeAction");
    QJSValue ret = executeActionFunction.call({jsInfo});
    if (ret.isError()) {
        qCWarning(dcThingManager()) << "executeAction script failed to execute:\n" << ret.toString();
    }
}
