/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project 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
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "nymeacore.h"
#include "loggingcategories.h"
#include "platform/platform.h"
#include "jsonrpc/jsonrpcserverimplementation.h"
#include "ruleengine/ruleengine.h"
#include "nymeasettings.h"
#include "tagging/tagsstorage.h"
#include "platform/platform.h"
#include "experiences/experiencemanager.h"
#include "platform/platformsystemcontroller.h"

#include "scriptengine/scriptengine.h"
#include "jsonrpc/scriptshandler.h"

#include "integrations/thingmanagerimplementation.h"
#include "integrations/thing.h"
#include "integrations/thingactioninfo.h"
#include "integrations/browseractioninfo.h"
#include "integrations/browseritemactioninfo.h"
#include "cloud/cloudmanager.h"
#include "cloud/cloudnotifications.h"
#include "cloud/cloudtransport.h"

#include "zigbee/zigbeemanager.h"

#include "hardware/modbus/modbusrtumanager.h"
#include "hardware/serialport/serialportmonitor.h"

#include <networkmanager.h>

#include <QDir>
#include <QCoreApplication>

NYMEA_LOGGING_CATEGORY(dcCore, "Core")

namespace nymeaserver {

NymeaCore* NymeaCore::s_instance = nullptr;

/*! Returns a pointer to the single \l{NymeaCore} instance. */
NymeaCore *NymeaCore::instance()
{
    if (!s_instance) {
        s_instance = new NymeaCore();
    }
    return s_instance;
}

/*! Constructs NymeaCore with the given \a parent. This is private.
    Use \l{NymeaCore::instance()} to access the single instance.*/
NymeaCore::NymeaCore(QObject *parent) :
    QObject(parent)
{
}

void NymeaCore::init(const QStringList &additionalInterfaces) {
    qCDebug(dcCore()) << "Initializing NymeaCore";

    qCDebug(dcPlatform()) << "Loading platform abstraction";
    m_platform = new Platform(this);

    qCDebug(dcCore()) << "Loading nymea configurations" << NymeaSettings(NymeaSettings::SettingsRoleGlobal).fileName();
    m_configuration = new NymeaConfiguration(this);

    qCDebug(dcCore()) << "Creating Time Manager";
    // Migration path: nymea < 0.18 doesn't use system time zone but stores its own time zone in the config
    // For migration, let's set the system's time zone to the config now to upgrade to the system time zone based nymea >= 0.18
    if (QTimeZone(m_configuration->timeZone()).isValid()) {
        if (m_platform->systemController()->setTimeZone(QTimeZone(m_configuration->timeZone()))) {
            m_configuration->setTimeZone("");
        }
    }
    m_timeManager = new TimeManager(this);

    qCDebug(dcCore()) << "Creating User Manager";
    m_userManager = new UserManager(NymeaSettings::settingsPath() + "/user-db.sqlite", this);

    qCDebug(dcCore) << "Creating Server Manager";
    m_serverManager = new ServerManager(m_platform, m_configuration, additionalInterfaces, this);

    qCDebug(dcCore()) << "Create Zigbee Manager";
    m_zigbeeManager = new ZigbeeManager(this);

    qCDebug(dcCore()) << "Create Serial Port Monitor";
    m_serialPortMonitor = new SerialPortMonitor(this);

    qCDebug(dcCore()) << "Create Modbus RTU Manager";
    m_modbusRtuManager = new ModbusRtuManager(m_serialPortMonitor, this);

    qCDebug(dcCore) << "Creating Hardware Manager";
    m_hardwareManager = new HardwareManagerImplementation(m_platform, m_serverManager->mqttBroker(), m_zigbeeManager, m_modbusRtuManager, this);

    qCDebug(dcCore) << "Creating Thing Manager (locale:" << m_configuration->locale() << ")";
    m_thingManager = new ThingManagerImplementation(m_hardwareManager, m_configuration->locale(), this);

    qCDebug(dcCore) << "Creating Rule Engine";
    m_ruleEngine = new RuleEngine(this);

    qCDebug(dcCore) << "Creating Log Engine";
    m_logger = new LogEngine(m_configuration->logDBDriver(), m_configuration->logDBName(), m_configuration->logDBHost(), m_configuration->logDBUser(), m_configuration->logDBPassword(), m_configuration->logDBMaxEntries(), this);
    m_logger->setThingManager(m_thingManager);

    qCDebug(dcCore()) << "Creating Script Engine";
    m_scriptEngine = new ScriptEngine(m_thingManager, this);
    m_serverManager->jsonServer()->registerHandler(new ScriptsHandler(m_scriptEngine, m_scriptEngine));

    qCDebug(dcCore()) << "Creating Tags Storage";
    m_tagsStorage = new TagsStorage(m_thingManager, m_ruleEngine, this);

    qCDebug(dcCore) << "Creating Network Manager";
    m_networkManager = new NetworkManager(this);
    m_networkManager->start();

    qCDebug(dcCore) << "Creating Debug Server Handler";
    m_debugServerHandler = new DebugServerHandler(this);

    qCDebug(dcCore) << "Creating Cloud Manager";
    m_cloudManager = new CloudManager(m_configuration, m_networkManager, this);

    qCDebug(dcCore()) << "Loading experiences";
    m_experienceManager = new ExperienceManager(m_thingManager, m_serverManager->jsonServer(), this);


    CloudNotifications *cloudNotifications = m_cloudManager->createNotificationsPlugin();
    m_thingManager->registerStaticPlugin(cloudNotifications);

    CloudTransport *cloudTransport = m_cloudManager->createTransportInterface();
    m_serverManager->jsonServer()->registerTransportInterface(cloudTransport, false);

    connect(m_configuration, &NymeaConfiguration::serverNameChanged, m_serverManager, &ServerManager::setServerName);

    connect(m_thingManager, &ThingManagerImplementation::pluginConfigChanged, this, &NymeaCore::pluginConfigChanged);
    connect(m_thingManager, &ThingManagerImplementation::eventTriggered, this, &NymeaCore::gotEvent);
    connect(m_thingManager, &ThingManagerImplementation::thingStateChanged, this, &NymeaCore::thingStateChanged);
    connect(m_thingManager, &ThingManagerImplementation::thingAdded, this, &NymeaCore::thingAdded);
    connect(m_thingManager, &ThingManagerImplementation::thingChanged, this, &NymeaCore::thingChanged);
    connect(m_thingManager, &ThingManagerImplementation::thingSettingChanged, this, &NymeaCore::thingSettingChanged);
    connect(m_thingManager, &ThingManagerImplementation::thingRemoved, this, &NymeaCore::thingRemoved);
    connect(m_thingManager, &ThingManagerImplementation::thingDisappeared, this, &NymeaCore::onThingDisappeared);
    connect(m_thingManager, &ThingManagerImplementation::loaded, this, &NymeaCore::thingManagerLoaded);

    connect(m_ruleEngine, &RuleEngine::ruleAdded, this, &NymeaCore::ruleAdded);
    connect(m_ruleEngine, &RuleEngine::ruleRemoved, this, &NymeaCore::ruleRemoved);
    connect(m_ruleEngine, &RuleEngine::ruleConfigurationChanged, this, &NymeaCore::ruleConfigurationChanged);

    connect(m_timeManager, &TimeManager::dateTimeChanged, this, &NymeaCore::onDateTimeChanged);

    m_logger->logSystemEvent(m_timeManager->currentDateTime(), true);
}

/*! Destructor of the \l{NymeaCore}. */
NymeaCore::~NymeaCore()
{
    qCDebug(dcCore()) << "Shutting down NymeaCore";
    m_logger->logSystemEvent(m_timeManager->currentDateTime(), false);

    // Disconnect all signals/slots, we're going down now
    m_timeManager->disconnect(this);
    m_thingManager->disconnect(this);
    m_ruleEngine->disconnect(this);

    // Then stop magic from happening
    qCDebug(dcCore) << "Shutting down \"Rule Engine\"";
    delete m_ruleEngine;

    // Next, ThingManager, so plugins don't access any resources any more.
    qCDebug(dcCore) << "Shutting down \"Thing Manager\"";
    delete m_thingManager;

    // Destroy resources used by things
    qCDebug(dcCore) << "Shutting down \"Server Manager\"";
    delete m_serverManager;
    qCDebug(dcCore) << "Shutting down \"CloudManager\"";
    delete m_cloudManager;
    qCDebug(dcCore()) << "Shutting down \"Hardware Manager\"";
    delete m_hardwareManager;

    // Now go ahead and clean up stuff.
    qCDebug(dcCore) << "Shutting down \"Log Engine\"";
    delete m_logger;

    qCDebug(dcCore) << "Done shutting down NymeaCore";
}

void NymeaCore::destroy()
{
    if (s_instance) {
        delete s_instance;
    }

    s_instance = nullptr;
}

QPair<Thing::ThingError, QList<RuleId> > NymeaCore::removeConfiguredThing(const ThingId &thingId, const QHash<RuleId, RuleEngine::RemovePolicy> &removePolicyList)
{
    Thing *thing = m_thingManager->findConfiguredThing(thingId);

    if (!thing) {
        return QPair<Thing::ThingError, QList<RuleId> > (Thing::ThingErrorThingNotFound, QList<RuleId>());
    }

    // Check if this is a child
    if (!thing->parentId().isNull() && thing->autoCreated()) {
        qCWarning(dcThingManager) << "Thing is an autocreated child of" << thing->parentId().toString() << ". Please remove the parent.";
        return QPair<Thing::ThingError, QList<RuleId> > (Thing::ThingErrorThingIsChild, QList<RuleId>());
    }

    // FIXME: Let's remove this for now. It will come back with more fine grained control, presumably introducing a RemoveMethod flag in the DeviceClass
//    if (thing->autoCreated()) {
//        qCWarning(dcThingManager) << "This thing has been auto-created and cannot be deleted manually.";
//        return QPair<Thing::ThingError, QList<RuleId> >(Thing::ThingErrorCreationMethodNotSupported, {});
//    }

    // Check if this thing has childs
    QList<Thing *> thingsToRemove;
    thingsToRemove.append(thing);
    QList<Thing *> childs = m_thingManager->findChilds(thingId);
    if (!childs.isEmpty()) {
        foreach (Thing *child, childs) {
            thingsToRemove.append(child);
        }
    }

    // check things
    QList<RuleId> offendingRules;
    qCDebug(dcThingManager) << "Things to remove:";
    foreach (Thing *d, thingsToRemove) {
        qCDebug(dcThingManager) << " -> " << d->name() << d->id().toString();

        // Check if thing is in a rule
        foreach (const RuleId &ruleId, m_ruleEngine->findRules(d->id())) {
            qCDebug(dcThingManager) << "      -> in rule:" << ruleId.toString();
            if (!offendingRules.contains(ruleId)) {
                offendingRules.append(ruleId);
            }
        }
    }

    // check each offending rule if there is a corresponding remove policy
    QHash<RuleId, RuleEngine::RemovePolicy> toBeChanged;
    QList<RuleId> unhandledRules;
    foreach (const RuleId &ruleId, offendingRules) {
        bool found = false;
        foreach (const RuleId &policyRuleId, removePolicyList.keys()) {
            if (ruleId == policyRuleId) {
                found = true;
                toBeChanged.insert(ruleId, removePolicyList.value(ruleId));
                break;
            }
        }
        if (!found)
            unhandledRules.append(ruleId);

    }

    if (!unhandledRules.isEmpty()) {
        qCWarning(dcThingManager) << "There are unhandled rules which depend on this thing:\n" << unhandledRules;
        return QPair<Thing::ThingError, QList<RuleId> > (Thing::ThingErrorThingInRule, unhandledRules);
    }

    // Update the rules...
    foreach (const RuleId &ruleId, toBeChanged.keys()) {
        if (toBeChanged.value(ruleId) == RuleEngine::RemovePolicyCascade) {
            m_ruleEngine->removeRule(ruleId);
        } else if (toBeChanged.value(ruleId) == RuleEngine::RemovePolicyUpdate){
            foreach (Thing *thing, thingsToRemove) {
                m_ruleEngine->removeThingFromRule(ruleId, thing->id());
            }
        }
    }

    // remove the childs
    foreach (Thing *d, childs) {
        Thing::ThingError removeError = m_thingManager->removeConfiguredThing(d->id());
        if (removeError == Thing::ThingErrorNoError) {
            m_logger->removeThingLogs(d->id());
        }
    }

    // delete the things
    Thing::ThingError removeError = m_thingManager->removeConfiguredThing(thingId);
    if (removeError == Thing::ThingErrorNoError) {
        m_logger->removeThingLogs(thingId);
    }

    return QPair<Thing::ThingError, QList<RuleId> > (Thing::ThingErrorNoError, QList<RuleId>());
}


Thing::ThingError NymeaCore::removeConfiguredThing(const ThingId &thingId, const RuleEngine::RemovePolicy &removePolicy)
{
    Thing *thing = m_thingManager->findConfiguredThing(thingId);

    if (!thing) {
        return Thing::ThingErrorThingNotFound;
    }

    // Check if this is a child
    if (!thing->parentId().isNull() && thing->autoCreated()) {
        qCWarning(dcThingManager) << "Thing is an autocreated child of" << thing->parentId().toString() << ". Please remove the parent.";
        return Thing::ThingErrorThingIsChild;
    }

    // FIXME: Let's remove this for now. It will come back with more fine grained control, presumably introducing a RemoveMethod flag in the DeviceClass
//    if (thing->autoCreated()) {
//        qCWarning(dcThingManager) << "This thing has been auto-created and cannot be deleted manually.";
//        return Thing::ThingErrorCreationMethodNotSupported;
//    }

    // Check if this thing has childs
    QList<Thing *> thingsToRemove;
    thingsToRemove.append(thing);
    QList<Thing *> childs = m_thingManager->findChilds(thingId);
    if (!childs.isEmpty()) {
        foreach (Thing *child, childs) {
            thingsToRemove.append(child);
        }
    }

    // check things
    QList<RuleId> offendingRules;
    qCDebug(dcThingManager) << "Things to remove:";
    foreach (Thing *d, thingsToRemove) {
        qCDebug(dcThingManager) << " -> " << d->name() << d->id().toString();

        // Check if thing is in a rule
        foreach (const RuleId &ruleId, m_ruleEngine->findRules(d->id())) {
            qCDebug(dcThingManager) << "      -> in rule:" << ruleId.toString();
            if (!offendingRules.contains(ruleId)) {
                offendingRules.append(ruleId);
            }
        }
    }

    // apply removepolicy for foreach rule
    foreach (const RuleId &ruleId, offendingRules) {
        if (removePolicy == RuleEngine::RemovePolicyCascade) {
            m_ruleEngine->removeRule(ruleId);
        } else if (removePolicy == RuleEngine::RemovePolicyUpdate){
            foreach (Thing *thing, thingsToRemove) {
                m_ruleEngine->removeThingFromRule(ruleId, thing->id());
            }
        }
    }

    // remove the childs
    foreach (Thing *d, childs) {
        Thing::ThingError removeError = m_thingManager->removeConfiguredThing(d->id());
        if (removeError == Thing::ThingErrorNoError) {
            m_logger->removeThingLogs(d->id());
        }
    }

    // delete the things
    Thing::ThingError removeError = m_thingManager->removeConfiguredThing(thingId);
    if (removeError == Thing::ThingErrorNoError) {
        m_logger->removeThingLogs(thingId);
    }

    return removeError;
}

BrowserActionInfo* NymeaCore::executeBrowserItem(const BrowserAction &browserAction)
{
    BrowserActionInfo *info = m_thingManager->executeBrowserItem(browserAction);
    connect(info, &BrowserActionInfo::finished, info->thing(), [this, info](){
        m_logger->logBrowserAction(info->browserAction(), info->status() == Thing::ThingErrorNoError ? Logging::LoggingLevelInfo : Logging::LoggingLevelAlert, info->status());
    });
    return info;
}

BrowserItemActionInfo *NymeaCore::executeBrowserItemAction(const BrowserItemAction &browserItemAction)
{
    BrowserItemActionInfo *info = m_thingManager->executeBrowserItemAction(browserItemAction);
    connect(info, &BrowserItemActionInfo::finished, info->thing(), [this, info](){
        m_logger->logBrowserItemAction(info->browserItemAction(), info->status() == Thing::ThingErrorNoError ? Logging::LoggingLevelInfo : Logging::LoggingLevelAlert, info->status());
    });
    return info;
}

/*! Execute the given \a ruleActions. */
void NymeaCore::executeRuleActions(const QList<RuleAction> ruleActions)
{
    QList<Action> actions;
    QList<BrowserAction> browserActions;
    foreach (const RuleAction &ruleAction, ruleActions) {
        if (ruleAction.type() == RuleAction::TypeThing) {
            Thing *thing = m_thingManager->findConfiguredThing(ruleAction.thingId());
            if (!thing) {
                qCWarning(dcRuleEngine()) << "Unable to find thing" << ruleAction.thingId() << "for rule action" << ruleAction;
                continue;
            }
            ActionTypeId actionTypeId = ruleAction.actionTypeId();
            ParamList params;
            bool ok = true;
            foreach (const RuleActionParam &ruleActionParam, ruleAction.ruleActionParams()) {
                if (ruleActionParam.isValueBased()) {
                    params.append(Param(ruleActionParam.paramTypeId(), ruleActionParam.value()));
                } else if (ruleActionParam.isStateBased()) {
                    Thing *stateThing = m_thingManager->findConfiguredThing(ruleActionParam.stateThingId());
                    if (!stateThing) {
                        qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action";
                        ok = false;
                        break;
                    }
                    ThingClass stateThingClass = m_thingManager->findThingClass(stateThing->thingClassId());
                    if (!stateThingClass.hasStateType(ruleActionParam.stateTypeId())) {
                        qCWarning(dcRuleEngine()) << "Device" << thing->name() << thing->id() << "does not have a state type" << ruleActionParam.stateTypeId();
                        ok = false;
                        break;
                    }
                    params.append(Param(ruleActionParam.paramTypeId(), stateThing->stateValue(ruleActionParam.stateTypeId())));
                }
            }
            if (!ok) {
                qCWarning(dcRuleEngine()) << "Not executing rule action";
                continue;
            }
            Action action(actionTypeId, thing->id(), Action::TriggeredByRule);
            action.setParams(params);
            actions.append(action);
        } else if (ruleAction.type() == RuleAction::TypeBrowser) {
            Thing *thing = m_thingManager->findConfiguredThing(ruleAction.thingId());
            if (!thing) {
                qCWarning(dcRuleEngine()) << "Unable to find thing" << ruleAction.thingId() << "for rule action" << ruleAction;
                continue;
            }
            BrowserAction browserAction(ruleAction.thingId(), ruleAction.browserItemId());
            browserActions.append(browserAction);
        } else {
            Things things = m_thingManager->findConfiguredThings(ruleAction.interface());
            foreach (Thing* thing, things) {
                ThingClass thingClass = m_thingManager->findThingClass(thing->thingClassId());
                ActionType actionType = thingClass.actionTypes().findByName(ruleAction.interfaceAction());
                if (actionType.id().isNull()) {
                    qCWarning(dcRuleEngine()) << "Error creating Action. The given ThingClass does not implement action:" << ruleAction.interfaceAction();
                    continue;
                }

                ParamList params;
                bool ok = true;
                foreach (const RuleActionParam &ruleActionParam, ruleAction.ruleActionParams()) {
                    ParamType paramType = actionType.paramTypes().findByName(ruleActionParam.paramName());
                    if (paramType.id().isNull()) {
                        qCWarning(dcRuleEngine()) << "Error creating Action. The given ActionType does not have a parameter:" << ruleActionParam.paramName();
                        ok = false;
                        continue;
                    }
                    if (ruleActionParam.isValueBased()) {
                        params.append(Param(paramType.id(), ruleActionParam.value()));
                    } else if (ruleActionParam.isStateBased()) {
                        Thing *stateThing = m_thingManager->findConfiguredThing(ruleActionParam.stateThingId());
                        if (!stateThing) {
                            qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action";
                            ok = false;
                            break;
                        }
                        ThingClass stateThingClass = m_thingManager->findThingClass(stateThing->thingClassId());
                        if (!stateThingClass.hasStateType(ruleActionParam.stateTypeId())) {
                            qCWarning(dcRuleEngine()) << "Thing" << thing->name() << thing->id() << "does not have a state type" << ruleActionParam.stateTypeId();
                            ok = false;
                            break;
                        }
                        params.append(Param(paramType.id(), stateThing->stateValue(ruleActionParam.stateTypeId())));
                    }
                }
                if (!ok) {
                    qCWarning(dcRuleEngine()) << "Not executing rule action";
                    continue;
                }

                Action action = Action(actionType.id(), thing->id(), Action::TriggeredByRule);
                action.setParams(params);
                actions.append(action);
            }
        }
    }

    foreach (const Action &action, actions) {
        qCDebug(dcRuleEngine) << "Executing action" << action.actionTypeId() << action.params();
        ThingActionInfo *info = m_thingManager->executeAction(action);
        connect(info, &ThingActionInfo::finished, this, [info](){
            if (info->status() != Thing::ThingErrorNoError) {
                qCWarning(dcRuleEngine) << "Error executing action:" << info->status() << info->displayMessage();
            }
        });
    }

    foreach (const BrowserAction &browserAction, browserActions) {
        BrowserActionInfo *info = executeBrowserItem(browserAction);
        connect(info, &BrowserActionInfo::finished, this, [info](){
            if (info->status() != Thing::ThingErrorNoError) {
                qCWarning(dcRuleEngine) << "Error executing browser action:" << info->status();
            }
        });
    }
}

/*! Calls the metheod RuleEngine::removeRule(\a id).
 *  \sa RuleEngine, */
RuleEngine::RuleError NymeaCore::removeRule(const RuleId &id)
{
    RuleEngine::RuleError removeError = m_ruleEngine->removeRule(id);
    if (removeError == RuleEngine::RuleErrorNoError)
        m_logger->removeRuleLogs(id);

    return removeError;
}

NymeaConfiguration *NymeaCore::configuration() const
{
    return m_configuration;
}

ThingManager *NymeaCore::thingManager() const
{
    return m_thingManager;
}

RuleEngine *NymeaCore::ruleEngine() const
{
    return m_ruleEngine;
}

ScriptEngine *NymeaCore::scriptEngine() const
{
    return m_scriptEngine;
}

TimeManager *NymeaCore::timeManager() const
{
    return m_timeManager;
}

ServerManager *NymeaCore::serverManager() const
{
    return m_serverManager;
}

QStringList NymeaCore::getAvailableLanguages()
{
    qCDebug(dcCore()) << "Loading translations from" << NymeaSettings::translationsPath();

    QStringList searchPaths;
    searchPaths << QCoreApplication::applicationDirPath() + "/../translations";
    searchPaths << NymeaSettings::translationsPath();

    QStringList translationFiles;
    foreach (const QString &path, searchPaths) {
        QDir translationDirectory(path);
        translationDirectory.setNameFilters(QStringList() << "*.qm");
        translationFiles = translationDirectory.entryList();
        qCDebug(dcTranslations()) << translationFiles.count() << "translations in" << path;
        if (translationFiles.count() > 0) {
            break;
        }
    }

    QStringList availableLanguages;
    foreach (QString translationFile, translationFiles) {
        if (!translationFile.startsWith("nymead-"))
            continue;

        QString language = translationFile.remove("nymead-").remove(".qm");
        QLocale languageLocale(language);
        availableLanguages.append(languageLocale.name());
    }

    return availableLanguages;
}

QStringList NymeaCore::loggingFilters()
{
    return nymeaLoggingCategories();
}

QStringList NymeaCore::loggingFiltersPlugins()
{
    QStringList loggingFiltersPlugins;
    foreach (const QJsonObject &pluginMetadata, ThingManagerImplementation::pluginsMetadata()) {
        QString pluginName = pluginMetadata.value("name").toString();
        loggingFiltersPlugins << pluginName.left(1).toUpper() + pluginName.mid(1);
    }
    return loggingFiltersPlugins;
}

BluetoothServer *NymeaCore::bluetoothServer() const
{
    return m_serverManager->bluetoothServer();
}

NetworkManager *NymeaCore::networkManager() const
{
    return m_networkManager;
}

UserManager *NymeaCore::userManager() const
{
    return m_userManager;
}

CloudManager *NymeaCore::cloudManager() const
{
    return m_cloudManager;
}

DebugServerHandler *NymeaCore::debugServerHandler() const
{
    return m_debugServerHandler;
}

TagsStorage *NymeaCore::tagsStorage() const
{
    return m_tagsStorage;
}

Platform *NymeaCore::platform() const
{
    return m_platform;
}

ZigbeeManager *NymeaCore::zigbeeManager() const
{
    return m_zigbeeManager;
}

ModbusRtuManager *NymeaCore::modbusRtuManager() const
{
    return m_modbusRtuManager;
}

void NymeaCore::gotEvent(const Event &event)
{
    emit eventTriggered(event);

    QList<RuleAction> actions;
    QList<RuleAction> eventBasedActions;
    foreach (const Rule &rule, m_ruleEngine->evaluateEvent(event)) {
        if (m_executingRules.contains(rule.id())) {
            qCWarning(dcRuleEngine()) << "WARNING: Loop detected in rule execution for rule" << rule.id() << rule.name();
            break;
        }
        m_executingRules.append(rule.id());

        // Event based
        if (!rule.eventDescriptors().isEmpty()) {
            m_logger->logRuleTriggered(rule);
            QList<RuleAction> tmp;
            if (rule.statesActive() && rule.timeActive()) {
                qCDebug(dcRuleEngineDebug()) << "Executing actions";
                tmp = rule.actions();
            } else {
                qCDebug(dcRuleEngineDebug()) << "Executing exitActions";
                tmp = rule.exitActions();
            }
            // check if we have an event based action or a normal action
            foreach (const RuleAction &action, tmp) {
                if (action.isEventBased()) {
                    eventBasedActions.append(action);
                } else {
                    actions.append(action);
                }
            }
        } else {
            // State based rule
            m_logger->logRuleActiveChanged(rule);
            emit ruleActiveChanged(rule);
            if (rule.active()) {
                actions.append(rule.actions());
            } else {
                actions.append(rule.exitActions());
            }
        }
    }

    // Set action params, depending on the event value
    foreach (RuleAction ruleAction, eventBasedActions) {
        RuleActionParams newParams;
        foreach (RuleActionParam ruleActionParam, ruleAction.ruleActionParams()) {
            // if this event param should be taken over in this action
            if (event.eventTypeId() == ruleActionParam.eventTypeId()) {
                QVariant eventValue = event.params().paramValue(ruleActionParam.eventParamTypeId());

                // TODO: limits / scale calculation -> actionValue = eventValue * x
                //       something like a EventParamDescriptor

                ruleActionParam.setValue(eventValue);
                qCDebug(dcRuleEngine) << "Using param value from event:" << ruleActionParam.value();
            }
            newParams.append(ruleActionParam);
        }
        ruleAction.setRuleActionParams(newParams);
        actions.append(ruleAction);
    }

    executeRuleActions(actions);
    m_executingRules.clear();
}

void NymeaCore::onDateTimeChanged(const QDateTime &dateTime)
{
    QList<RuleAction> actions;
    foreach (const Rule &rule, m_ruleEngine->evaluateTime(dateTime)) {
        // TimeEvent based
        if (!rule.timeDescriptor().timeEventItems().isEmpty()) {
            m_logger->logRuleTriggered(rule);
            if (rule.statesActive() && rule.timeActive()) {
                actions.append(rule.actions());
            } else {
                actions.append(rule.exitActions());
            }
        } else {
            // Calendar based rule
            m_logger->logRuleActiveChanged(rule);
            emit ruleActiveChanged(rule);
            if (rule.active()) {
                actions.append(rule.actions());
            } else {
                actions.append(rule.exitActions());
            }
        }
    }
    executeRuleActions(actions);
}

LogEngine* NymeaCore::logEngine() const
{
    return m_logger;
}

JsonRPCServerImplementation *NymeaCore::jsonRPCServer() const
{
    return m_serverManager->jsonServer();
}

void NymeaCore::onThingDisappeared(const ThingId &thingId)
{
    Thing *thing = m_thingManager->findConfiguredThing(thingId);
    if (!thing) {
        return;
    }

    // Check if this thing has childs
    Things thingsToRemove;
    thingsToRemove.append(thing);
    QList<Thing *> childs = m_thingManager->findChilds(thingId);
    if (!childs.isEmpty()) {
        foreach (Thing *child, childs) {
            thingsToRemove.append(child);
        }
    }

    // check things
    QList<RuleId> offendingRules;
    qCDebug(dcThingManager) << "Thing to remove:";
    foreach (Thing *d, thingsToRemove) {
        qCDebug(dcThingManager) << " -> " << d->name() << d->id().toString();

        // Check if thing is in a rule
        foreach (const RuleId &ruleId, m_ruleEngine->findRules(d->id())) {
            qCDebug(dcThingManager) << "      -> in rule:" << ruleId.toString();
            if (!offendingRules.contains(ruleId)) {
                offendingRules.append(ruleId);
            }
        }
    }

    // update involved rules
    foreach (const RuleId &ruleId, offendingRules) {
        foreach (Thing *thing, thingsToRemove) {
            m_ruleEngine->removeThingFromRule(ruleId, thing->id());
        }
    }

    // remove the child devices
    foreach (Thing *d, childs) {
        Thing::ThingError removeError = m_thingManager->removeConfiguredThing(d->id());
        if (removeError == Thing::ThingErrorNoError) {
            m_logger->removeThingLogs(d->id());
        }
    }

    // delete the thing
    Thing::ThingError removeError = m_thingManager->removeConfiguredThing(thingId);
    if (removeError == Thing::ThingErrorNoError) {
        m_logger->removeThingLogs(thingId);
    }
}

void NymeaCore::thingManagerLoaded()
{
    m_ruleEngine->init();
    // Evaluate rules on current time
    onDateTimeChanged(m_timeManager->currentDateTime());

    // Tell hardare resources we're done with loading stuff...
    m_hardwareManager->thingsLoaded();

    emit initialized();

    // Do some houskeeping...
    qCDebug(dcCore()) << "Starting housekeeping...";
    QDateTime startTime = QDateTime::currentDateTime();
    ThingsFetchJob *job = m_logger->fetchThings();
    connect(job, &ThingsFetchJob::finished, m_thingManager, [this, job, startTime](){
        foreach (const ThingId &thingId, job->results()) {
            if (!m_thingManager->findConfiguredThing(thingId)) {
                qCDebug(dcCore()) << "Cleaning stale thing entries from log DB for thing id" << thingId;
                m_logger->removeThingLogs(thingId);
            }
        }
        qCDebug(dcCore()) << "Housekeeping done in" << startTime.msecsTo(QDateTime::currentDateTime()) << "ms.";
    });

    foreach (const ThingId &thingId, m_ruleEngine->thingsInRules()) {
        if (!m_thingManager->findConfiguredThing(thingId)) {
            qCDebug(dcCore()) << "Cleaning stale rule entries for thing id" << thingId;
            foreach (const RuleId &ruleId, m_ruleEngine->findRules(thingId)) {
                m_ruleEngine->removeThingFromRule(ruleId, thingId);
            }
        }
    }

    foreach (const Tag &tag, m_tagsStorage->tags()) {
        if (!tag.ruleId().isNull() && !m_ruleEngine->findRule(tag.ruleId()).isValid()) {
            qCDebug(dcCore()) << "Cleaning up stale rule tag" << tag;
            m_tagsStorage->removeTag(tag);
        }
        if (!tag.thingId().isNull() && !m_thingManager->findConfiguredThing(tag.thingId())) {
            qCDebug(dcCore()) << "Cleaning up stale thing tag" << tag.tagId();
            m_tagsStorage->removeTag(tag);
        }
    }    
}

}
