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

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-plugins.
*
* nymea-plugins 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, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-plugins 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "integrationpluginnanoleaf.h"
#include "plugininfo.h"

#include <network/zeroconf/zeroconfservicebrowser.h>
#include <platform/platformzeroconfcontroller.h>

#include <QHash>
#include <QDebug>

IntegrationPluginNanoleaf::IntegrationPluginNanoleaf()
{

}

void IntegrationPluginNanoleaf::init()
{
    m_zeroconfBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_nanoleafapi._tcp");
}

void IntegrationPluginNanoleaf::discoverThings(ThingDiscoveryInfo *info)
{
    QStringList serialNumbers;
    foreach (const ZeroConfServiceEntry &entry, m_zeroconfBrowser->serviceEntries()) {

        QHostAddress address = QHostAddress(entry.hostAddress().toString());
        ThingDescriptor descriptor(lightPanelsThingClassId, entry.name(), address.toString());
        ParamList params;

        QString serialNo = entry.txt("id");
        QString model = entry.txt("md");
        QString firmwareVersion = entry.txt("srcvers");

        if (serialNumbers.contains(serialNo))
            continue; //To avoid duplicated devices

        Thing *existingThing = myThings().findByParams(ParamList() << Param(lightPanelsThingSerialNoParamTypeId, serialNo));
        if (existingThing) {
            //For thing rediscovery
            descriptor.setThingId(existingThing->id());
        }

        serialNumbers.append(serialNo);
        qCDebug(dcNanoleaf()) << "Have device" << entry.name() << serialNo << model << firmwareVersion;
        params << Param(lightPanelsThingModelParamTypeId, model);
        params << Param(lightPanelsThingSerialNoParamTypeId, serialNo);
        params << Param(lightPanelsThingFirmwareVersionParamTypeId, firmwareVersion);
        descriptor.setParams(params);

        info->addThingDescriptor(descriptor);
    }
    info->finish(Thing::ThingErrorNoError);
}

void IntegrationPluginNanoleaf::startPairing(ThingPairingInfo *info)
{
    info->finish(Thing::ThingErrorNoError, tr("On the Nanoleaf controller, hold the on-off button for 5-7 seconds until the LED starts flashing."));
}

void IntegrationPluginNanoleaf::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
{
    Q_UNUSED(username)
    Q_UNUSED(secret)
    QString serialNumber =  info->params().paramValue(lightPanelsThingSerialNoParamTypeId).toString();
    QHostAddress address = getHostAddress(serialNumber);
    if (address.isNull()) {
        qCWarning(dcNanoleaf()) << "Could not find any device with serial number" << serialNumber;
        return info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Cloud not find device."));
    }
    uint port = getPort(serialNumber);
    qCDebug(dcNanoleaf()) << "ConfirmPairing: Creating Nanoleaf connection with address" << address << "and port" << port;
    Nanoleaf *nanoleaf = createNanoleafConnection(address, port);
    nanoleaf->addUser(); //push button pairing
    m_unfinishedNanoleafConnections.insert(info->thingId(), nanoleaf);
    m_unfinishedPairing.insert(nanoleaf, info);
    connect(info, &ThingPairingInfo::aborted, this, [info, this] {
        Nanoleaf *nanoleaf = m_unfinishedNanoleafConnections.take(info->thingId());
        m_unfinishedPairing.remove(nanoleaf);
        nanoleaf->deleteLater();
    });
}

void IntegrationPluginNanoleaf::setupThing(ThingSetupInfo *info)
{
    Thing *thing = info->thing();
    if(thing->thingClassId() == lightPanelsThingClassId) {

        QString thingSerialNo = thing->paramValue(lightPanelsThingSerialNoParamTypeId).toString();
        qCDebug(dcNanoleaf()) << "Setting up Nanoleaf light panel with serial number:" << thingSerialNo;

        pluginStorage()->beginGroup(thing->id().toString());
        QString token = pluginStorage()->value("authToken").toString();
        pluginStorage()->endGroup();

        Nanoleaf *nanoleaf;
        if (m_unfinishedNanoleafConnections.contains(thing->id())) {
            // This setupDevice is called after a discovery
            nanoleaf = m_unfinishedNanoleafConnections.take(thing->id());
            m_nanoleafConnections.insert(thing->id(), nanoleaf);
            return info->finish(Thing::ThingErrorNoError);
        } else {
            // This setupDevice is called after a (re)start, with an already added thing
            QString serialNumber = thing->paramValue(lightPanelsThingSerialNoParamTypeId).toString();
            QHostAddress address = getHostAddress(serialNumber);
            if (address.isNull()) {
                qCWarning(dcNanoleaf()) << "Could not find any device with serial number" << serialNumber;
                return info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Cloud not find device."));
            }
            int port = getPort(serialNumber);
            qCDebug(dcNanoleaf()) << "SetupThing: Creating Nanoleaf connection with address" << address << "and port" << port;
            nanoleaf = createNanoleafConnection(address, port);
            nanoleaf->setAuthToken(token);
            nanoleaf->getControllerInfo(); //This is just to check if the thing is available

            m_nanoleafConnections.insert(thing->id(), nanoleaf);
            m_asyncDeviceSetup.insert(nanoleaf, info);
            connect(info, &ThingSetupInfo::aborted, this, [nanoleaf, this](){m_asyncDeviceSetup.remove(nanoleaf);});
            return;
        }
    }
}

void IntegrationPluginNanoleaf::postSetupThing(Thing *thing)
{
    if (thing->thingClassId() == lightPanelsThingClassId) {
        Nanoleaf *nanoleaf = m_nanoleafConnections.value(thing->id());
        if (!nanoleaf)
            return;
         nanoleaf->getControllerInfo();
         nanoleaf->registerForEvents();
    }

    if(!m_pluginTimer) {
        m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(5);
        connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() {
            foreach (Nanoleaf *nanoleaf, m_nanoleafConnections) {
                nanoleaf->getControllerInfo();
            }
        });
    }
}


void IntegrationPluginNanoleaf::thingRemoved(Thing *thing)
{
    if(thing->thingClassId() == lightPanelsThingClassId) {
        Nanoleaf *nanoleaf = m_nanoleafConnections.take(thing->id());
        nanoleaf->deleteLater();
    }

    if (myThings().isEmpty()) {
        hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
        m_pluginTimer = nullptr;
    }
}


void IntegrationPluginNanoleaf::executeAction(ThingActionInfo *info)
{
    Thing *thing = info->thing();
    Action action = info->action();

    if (thing->thingClassId() == lightPanelsThingClassId) {
        Nanoleaf *nanoleaf = m_nanoleafConnections.value(thing->id());
        if (!nanoleaf) {
            return info->finish(Thing::ThingErrorHardwareFailure);
        }

        if (action.actionTypeId() == lightPanelsPowerActionTypeId) {
            bool power = action.param(lightPanelsPowerActionPowerParamTypeId).value().toBool();
            QUuid requestId = nanoleaf->setPower(power);
            connect(info, &ThingActionInfo::aborted, this, [requestId, this](){ m_asyncActions.remove(requestId); });
            m_asyncActions.insert(requestId, info);

        } else if (action.actionTypeId() == lightPanelsBrightnessActionTypeId) {
            int brightness = action.param(lightPanelsBrightnessActionBrightnessParamTypeId).value().toInt();
            QUuid requestId = nanoleaf->setBrightness(brightness);
            connect(info, &ThingActionInfo::aborted, this, [requestId, this](){ m_asyncActions.remove(requestId); });
            m_asyncActions.insert(requestId, info);

        } else if (action.actionTypeId() == lightPanelsColorActionTypeId) {
            QColor color(action.param(lightPanelsColorActionColorParamTypeId).value().toString());
            QUuid requestId = nanoleaf->setColor(color);
            connect(info, &ThingActionInfo::aborted, this, [requestId, this](){ m_asyncActions.remove(requestId); });
            m_asyncActions.insert(requestId, info);

        } else if (action.actionTypeId() == lightPanelsColorTemperatureActionTypeId) {
            int colorTemperature = action.param(lightPanelsColorTemperatureActionColorTemperatureParamTypeId).value().toInt();
            QUuid requestId = nanoleaf->setMired(colorTemperature);
            connect(info, &ThingActionInfo::aborted, this, [requestId, this](){ m_asyncActions.remove(requestId); });
            m_asyncActions.insert(requestId, info);
        } else if (action.actionTypeId() == lightPanelsAlertActionTypeId) {
            QUuid requestId = nanoleaf->identify();
            connect(info, &ThingActionInfo::aborted, this, [requestId, this](){ m_asyncActions.remove(requestId); });
            m_asyncActions.insert(requestId, info);
        }
    }
}

void IntegrationPluginNanoleaf::browseThing(BrowseResult *result)
{
    Thing *thing = result->thing();
    Nanoleaf *nanoleaf = m_nanoleafConnections.value(thing->id());
    nanoleaf->getEffects();
    m_asyncBrowseResults.insert(nanoleaf, result);
    connect(result, &BrowseResult::aborted, this, [nanoleaf, this]{m_asyncBrowseResults.remove(nanoleaf);});
}

void IntegrationPluginNanoleaf::browserItem(BrowserItemResult *result)
{
    Q_UNUSED(result)
    qCDebug(dcNanoleaf()) << "BrowserItem called";
}

void IntegrationPluginNanoleaf::executeBrowserItem(BrowserActionInfo *info)
{
    Thing *thing = info->thing();
    Nanoleaf *nanoleaf = m_nanoleafConnections.value(thing->id());
    QUuid requestId = nanoleaf->setEffect(info->browserAction().itemId());
    m_asyncBrowserItem.insert(requestId, info);
    connect(info, &BrowserActionInfo::aborted, this, [requestId, this]{m_asyncBrowserItem.remove(requestId);});
}

Nanoleaf *IntegrationPluginNanoleaf::createNanoleafConnection(const QHostAddress &address, int port)
{
    Nanoleaf *nanoleaf = new Nanoleaf(hardwareManager()->networkManager(), address, port, this);
    connect(nanoleaf, &Nanoleaf::authTokenRecieved, this, &IntegrationPluginNanoleaf::onAuthTokenReceived);
    connect(nanoleaf, &Nanoleaf::authenticationStatusChanged, this, &IntegrationPluginNanoleaf::onAuthenticationStatusChanged);
    connect(nanoleaf, &Nanoleaf::requestExecuted, this, &IntegrationPluginNanoleaf::onRequestExecuted);
    connect(nanoleaf, &Nanoleaf::connectionChanged, this, &IntegrationPluginNanoleaf::onConnectionChanged);

    connect(nanoleaf, &Nanoleaf::controllerInfoReceived, this, &IntegrationPluginNanoleaf::onControllerInfoReceived);
    connect(nanoleaf, &Nanoleaf::brightnessReceived, this, &IntegrationPluginNanoleaf::onBrightnessReceived);
    connect(nanoleaf, &Nanoleaf::powerReceived, this, &IntegrationPluginNanoleaf::onPowerReceived);
    connect(nanoleaf, &Nanoleaf::colorModeReceived, this, &IntegrationPluginNanoleaf::onColorModeReceived);
    connect(nanoleaf, &Nanoleaf::saturationReceived, this, &IntegrationPluginNanoleaf::onSaturationReceived);
    connect(nanoleaf, &Nanoleaf::hueReceived, this, &IntegrationPluginNanoleaf::onHueReceived);
    connect(nanoleaf, &Nanoleaf::colorTemperatureReceived, this, &IntegrationPluginNanoleaf::onColorTemperatureReceived);
    connect(nanoleaf, &Nanoleaf::effectListReceived, this, &IntegrationPluginNanoleaf::onEffectListReceived);
    connect(nanoleaf, &Nanoleaf::selectedEffectReceived, this, &IntegrationPluginNanoleaf::onSelectedEffectReceived);
    return nanoleaf;
}

QHostAddress IntegrationPluginNanoleaf::getHostAddress(const QString &serialNumber)
{
    ZeroConfServiceEntry entry;
    foreach (const ZeroConfServiceEntry &e, m_zeroconfBrowser->serviceEntries()) {
        QString entrySerialNo = e.txt("id");

        if (serialNumber == entrySerialNo) {
            entry = e;
            break;
        }
    }
    return QHostAddress(entry.hostAddress());
}

uint IntegrationPluginNanoleaf::getPort(const QString &serialNumber)
{
    ZeroConfServiceEntry entry;
    foreach (const ZeroConfServiceEntry &e, m_zeroconfBrowser->serviceEntries()) {
        QString entrySerialNo = e.txt("id");
        if (serialNumber == entrySerialNo) {
            entry = e;
            break;
        }
    }
    return entry.port();
}

void IntegrationPluginNanoleaf::onAuthTokenReceived(const QString &token)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    if (m_unfinishedPairing.contains(nanoleaf)) {
        ThingPairingInfo *info = m_unfinishedPairing.take(nanoleaf);
        pluginStorage()->beginGroup(info->thingId().toString());
        pluginStorage()->setValue("authToken", token);
        pluginStorage()->endGroup();
        info->finish(Thing::ThingErrorNoError);
    }
}

void IntegrationPluginNanoleaf::onAuthenticationStatusChanged(bool authenticated)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    if (m_asyncDeviceSetup.contains(nanoleaf)) {
        ThingSetupInfo *info = m_asyncDeviceSetup.take(nanoleaf);
        if (authenticated) {
            info->finish(Thing::ThingErrorNoError);
        } else {
            info->finish(Thing::ThingErrorSetupFailed);
        }
    }
}

void IntegrationPluginNanoleaf::onRequestExecuted(QUuid requestId, bool success)
{
    if (m_asyncActions.contains(requestId)) {
        ThingActionInfo *info = m_asyncActions.take(requestId);
        if (success) {
            info->finish(Thing::ThingErrorNoError);
        } else {
            info->finish(Thing::ThingErrorHardwareNotAvailable);
        }
    }

    if (m_asyncBrowserItem.contains(requestId)) {
        BrowserActionInfo *info = m_asyncBrowserItem.take(requestId);
        if (success) {
            info->finish(Thing::ThingErrorNoError);
        } else {
            info->finish(Thing::ThingErrorHardwareNotAvailable);
        }
    }
}

void IntegrationPluginNanoleaf::onConnectionChanged(bool connected)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    thing->setStateValue(lightPanelsConnectedStateTypeId, connected);
    if (!connected) {
        QTimer::singleShot(3000, this, [nanoleaf, thing, connected, this] {
           if (!connected) { //If after 3 seconds it is still not connected
               nanoleaf->setIpAddress(getHostAddress(thing->paramValue(lightPanelsThingSerialNoParamTypeId).toString()));
               nanoleaf->setPort(getPort(thing->paramValue(lightPanelsThingSerialNoParamTypeId).toString()));
               nanoleaf->getControllerInfo(); //Test connection
           }
        });
    }
}

void IntegrationPluginNanoleaf::onControllerInfoReceived(const Nanoleaf::ControllerInfo &controllerInfo)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    //qCDebug(dcNanoleaf()) << "Controller Info received" << controllerInfo.name << controllerInfo.firmwareVersion;
    thing->setParamValue(lightPanelsThingFirmwareVersionParamTypeId, controllerInfo.firmwareVersion);
}

void IntegrationPluginNanoleaf::onPowerReceived(bool power)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    //qCDebug(dcNanoleaf()) << "Power received" << power;
    thing->setStateValue(lightPanelsPowerStateTypeId, power);
}

void IntegrationPluginNanoleaf::onBrightnessReceived(int percentage)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    //qCDebug(dcNanoleaf()) << "Brightness received" << percentage;
    thing->setStateValue(lightPanelsBrightnessStateTypeId, percentage);
}

void IntegrationPluginNanoleaf::onColorReceived(QColor color)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    //qCDebug(dcNanoleaf()) << "Color received" << color.toRgb();
    thing->setStateValue(lightPanelsColorStateTypeId, color);
}

void IntegrationPluginNanoleaf::onColorModeReceived(Nanoleaf::ColorMode colorMode)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    switch (colorMode) {
    case Nanoleaf::ColorMode::ColorTemperatureMode:
        thing->setStateValue(lightPanelsColorModeStateTypeId, tr("Color temperature"));
        break;
    case Nanoleaf::ColorMode::HueSaturationMode:
        thing->setStateValue(lightPanelsColorModeStateTypeId, tr("Hue/Saturation"));
        break;
    case Nanoleaf::ColorMode::EffectMode:
        thing->setStateValue(lightPanelsColorModeStateTypeId, tr("Effect"));
        break;
    }
}

void IntegrationPluginNanoleaf::onHueReceived(int hue)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    //qCDebug(dcNanoleaf()) << "Hue received" << hue;
    QColor color = QColor(thing->stateValue(lightPanelsColorStateTypeId).toString());
    color.setHsv(hue, color.saturation(), color.value());
    thing->setStateValue(lightPanelsColorStateTypeId, color);
}

void IntegrationPluginNanoleaf::onSaturationReceived(int saturation)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    //qCDebug(dcNanoleaf()) << "Saturation received" << saturation;
    QColor color = QColor(thing->stateValue(lightPanelsColorStateTypeId).toString());
    color.setHsv(color.hue(), saturation, color.value());
    thing->setStateValue(lightPanelsColorStateTypeId, color);
}

void IntegrationPluginNanoleaf::onEffectListReceived(const QStringList &effects)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    //qCDebug(dcNanoleaf()) << "Effect list received" << effects;

    if (m_asyncBrowseResults.contains(nanoleaf)) {
        BrowseResult *result = m_asyncBrowseResults.take(nanoleaf);
        foreach (QString effect, effects) {
            BrowserItem item;
            item.setId(effect);
            item.setBrowsable(false);
            item.setExecutable(true);
            item.setDisplayName(effect);
            item.setDisabled(false);
            result->addItem(item);
        }
        result->finish(Thing::ThingErrorNoError);
    }
}

void IntegrationPluginNanoleaf::onColorTemperatureReceived(int kelvin)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    qCDebug(dcNanoleaf()) << "Color temperature received, Kelvin:" << kelvin << "Mired:" << (653-(kelvin/13));
    //NOTE: this is just a rough estimation of the mired value
    //Mired: 153 - 500
    //Kelvin: 1200-6500
    int mired = static_cast<int>(653-(kelvin/13));
    thing->setStateValue(lightPanelsColorTemperatureStateTypeId, mired);
}

void IntegrationPluginNanoleaf::onSelectedEffectReceived(const QString &effect)
{
    Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
    Thing *thing = myThings().findById(m_nanoleafConnections.key(nanoleaf));
    if (!thing)
        return;
    //qCDebug(dcNanoleaf()) << "Selected effect received" << effect;
    thing->setStateValue(lightPanelsEffectNameStateTypeId, QString(effect).remove('"').remove('*'));
}

