/***************************************************************************
 *   This file is part of awesome-widgets                                  *
 *                                                                         *
 *   awesome-widgets 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.                       *
 *                                                                         *
 *   awesome-widgets 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 awesome-widgets. If not, see http://www.gnu.org/licenses/  *
 ***************************************************************************/

#include "awkeys.h"

#include <QDBusConnection>
#include <QDBusError>
#include <QRegularExpression>
#include <QTimer>

#include "awdataaggregator.h"
#include "awdataengineaggregator.h"
#include "awdbusadaptor.h"
#include "awdebug.h"
#include "awkeycache.h"
#include "awkeyoperations.h"
#include "awkeysaggregator.h"
#include "awpatternfunctions.h"
#include "graphicalitem.h"


AWKeys::AWKeys(QObject *_parent)
    : QObject(_parent)
{
    qSetMessagePattern(AWDebug::LOG_FORMAT);
    qCDebug(LOG_AW) << __PRETTY_FUNCTION__;
    for (auto &metadata : AWDebug::getBuildData())
        qCDebug(LOG_AW) << metadata;

    m_aggregator = new AWKeysAggregator(this);
    m_dataAggregator = new AWDataAggregator(this);
    m_dataEngineAggregator = AWDataEngineAggregator::instance(this);
    m_keyOperator = new AWKeyOperations(this);

    m_timer = new QTimer(this);
    m_timer->setSingleShot(false);

    createDBusInterface();

    // update key data if required
    connect(m_keyOperator, &AWKeyOperations::updateKeys, this, &AWKeys::reinitKeys);
    connect(m_timer, &QTimer::timeout, this, &AWKeys::updateTextData);

    // transfer signal from AWDataAggregator object to QML ui
    connect(m_dataAggregator, &AWDataAggregator::toolTipPainted,
            [this](const QString &_tooltip) { emit(needToolTipToBeUpdated(_tooltip)); });

    connect(this, &AWKeys::dropSourceFromDataengine, this,
            [this](const QString &_source) { m_dataEngineAggregator->dropSourceForClient(this, _source); });
    connect(m_dataEngineAggregator, &AWDataEngineAggregator::dataUpdated, this, &AWKeys::dataUpdated);
    // transfer signal from dataengine to update source list
    connect(m_dataEngineAggregator, &AWDataEngineAggregator::deviceAdded, m_keyOperator, &AWKeyOperations::addDevice);
}


AWKeys::~AWKeys()
{
    qCDebug(LOG_AW) << __PRETTY_FUNCTION__;

    m_dataEngineAggregator->unregisterClient(this);

    m_timer->stop();
    // delete dbus session
    auto id = reinterpret_cast<qlonglong>(this);
    QDBusConnection::sessionBus().unregisterObject(QString("/%1").arg(id));
}


void AWKeys::initDataAggregator(const QVariantMap &_tooltipParams)
{
    qCDebug(LOG_AW) << "Tooltip parameters" << _tooltipParams;

    // store parameters to generate m_requiredKeys
    m_tooltipParams = _tooltipParams;
    m_dataAggregator->setParameters(m_tooltipParams);
}


void AWKeys::initKeys(const QString &_currentPattern, const int _interval, const bool _optimize)
{
    qCDebug(LOG_AW) << "Pattern" << _currentPattern << "with interval" << _interval << "with optimization" << _optimize;

    // init
    m_optimize = _optimize;
    // child objects
    m_aggregator->initFormatters();
    m_keyOperator->setPattern(_currentPattern);
    m_keyOperator->updateCache();
    m_dataEngineAggregator->loadSources();

    // timer
    m_timer->setInterval(_interval);
    m_timer->start();
}


void AWKeys::setAggregatorProperty(const QString &_key, const QVariant &_value)
{
    qCDebug(LOG_AW) << "Key" << _key << "with value" << _value;

    m_aggregator->setProperty(_key.toUtf8().constData(), _value);
}


void AWKeys::setWrapNewLines(const bool _wrap)
{
    qCDebug(LOG_AW) << "Is wrapping enabled" << _wrap;

    m_wrapNewLines = _wrap;
}


void AWKeys::updateCache()
{
    return m_keyOperator->updateCache();
}


QStringList AWKeys::dictKeys(const bool _sorted, const QString &_regexp) const
{
    qCDebug(LOG_AW) << "Should be sorted" << _sorted << "and filter applied" << _regexp;

    // check if functions asked
    if (_regexp == "functions")
        return QString(STATIC_FUNCTIONS).split(',');
    // check if user defined keys asked
    if (_regexp == "userdefined")
        return m_keyOperator->userKeys();

    QStringList allKeys = m_keyOperator->dictKeys();
    // sort if required
    if (_sorted)
        allKeys.sort();

    return allKeys.filter(QRegularExpression(_regexp));
}


QString AWKeys::infoByKey(const QString &_key) const
{
    qCDebug(LOG_AW) << "Requested info for key" << _key;

    return m_keyOperator->infoByKey(_key);
}


// HACK this method requires to define tag value from bar from UI interface
QString AWKeys::valueByKey(const QString &_key) const
{
    qCDebug(LOG_AW) << "Requested value for key" << _key;

    auto realKey = _key.startsWith("bar") ? m_keyOperator->infoByKey(_key) : _key;

    return m_aggregator->formatter(m_values[realKey], realKey, true);
}


void AWKeys::editItem(const QString &_type)
{
    qCDebug(LOG_AW) << "Item type" << _type;

    return m_keyOperator->editItem(_type);
}


void AWKeys::dataUpdated(const QHash<QString, KSysGuard::SensorInfo> &_sensors, const KSysGuard::SensorDataList &_data)
{
    qCDebug(LOG_AW) << "Update data for" << _data.count() << "items";

    for (auto &data : _data) {
        if (!_sensors.contains(data.sensorProperty))
            continue;
        auto sensor = _sensors[data.sensorProperty];
        setDataBySource(data.sensorProperty, sensor, data.payload);
    }
}


void AWKeys::reinitKeys(const QStringList &_currentKeys)
{
    qCDebug(LOG_AW) << "Update found keys by using list" << _currentKeys;

    // append lists
    m_foundBars = AWPatternFunctions::findKeys(m_keyOperator->pattern(), _currentKeys, true);
    m_foundKeys = AWPatternFunctions::findKeys(m_keyOperator->pattern(), _currentKeys, false);
    m_foundLambdas = AWPatternFunctions::findLambdas(m_keyOperator->pattern());
    // generate list of required keys for bars
    QStringList barKeys;
    for (auto &bar : m_foundBars) {
        auto item = m_keyOperator->giByKey(bar);
        if (item->isCustom())
            item->setUsedKeys(AWPatternFunctions::findKeys(item->bar(), _currentKeys, false));
        else
            item->setUsedKeys(QStringList() << item->bar());
        barKeys.append(item->usedKeys());
    }
    // get required keys
    m_requiredKeys = AWKeyCache::getRequiredKeys(m_foundKeys, barKeys, m_tooltipParams,
                                                 m_keyOperator->requiredUserKeys(), _currentKeys);

    // set key data to m_aggregator
    m_aggregator->setDevices(m_keyOperator->devices());
}


void AWKeys::updateTextData()
{
    // do not do it in parallel to avoid race condition
    calculateValues();
    auto text = parsePattern(m_keyOperator->pattern());
    // update tooltip values under lock
    m_dataAggregator->dataUpdate(m_values);

    emit(needTextToBeUpdated(text));
}


// HACK this method is required since I could not define some values by using
// specified pattern. Usually they are values which depend on several others
void AWKeys::calculateValues()
{
    auto devices = m_keyOperator->devices();

    // up, down, upkb, downkb, upunits, downunits
    auto netIndex = devices.network.indexOf(m_values["netdev"].toString());
    m_values["down"] = m_values[QString("down%1").arg(netIndex)];
    m_values["downkb"] = m_values[QString("downkb%1").arg(netIndex)];
    m_values["downtot"] = m_values[QString("downtot%1").arg(netIndex)];
    m_values["downtotkb"] = m_values[QString("downtotkb%1").arg(netIndex)];
    m_values["downunits"] = m_values[QString("downunits%1").arg(netIndex)];
    m_values["up"] = m_values[QString("up%1").arg(netIndex)];
    m_values["upkb"] = m_values[QString("upkb%1").arg(netIndex)];
    m_values["uptot"] = m_values[QString("uptot%1").arg(netIndex)];
    m_values["uptotkb"] = m_values[QString("uptotkb%1").arg(netIndex)];
    m_values["upunits"] = m_values[QString("upunits%1").arg(netIndex)];

    // gpu memory keys
    m_values["gpufreemb"] = m_values["gputotmb"].toLongLong() - m_values["gpuusedmb"].toLongLong();
    m_values["gpufreegb"] = m_values["gputotgb"].toDouble() - m_values["gpuusedgb"].toDouble();
    m_values["gpumem"] = 100.0 * m_values["gpuusedmb"].toDouble() / m_values["gputotmb"].toDouble();
    for (auto i = 0; i < devices.gpu.count(); ++i) {
        m_values[QString("gpufreemb%1").arg(i)] = m_values[QString("gputotmb%1").arg(i)].toLongLong()
                                                  - m_values[QString("gpuusedmb%1").arg(i)].toLongLong();
        m_values[QString("gpufreegb%1").arg(i)]
            = m_values[QString("gputotgb%1").arg(i)].toDouble() - m_values[QString("gpuusedgb%1").arg(i)].toDouble();
        m_values[QString("gpumem%1").arg(i)] = 100.0 * m_values[QString("gpuusedmb%1").arg(i)].toDouble()
                                               / m_values[QString("gputotmb%1").arg(i)].toDouble();
    }

    // user defined keys
    for (auto &key : m_keyOperator->userKeys())
        m_values[key] = m_values[m_keyOperator->userKeySource(key)];

    // lambdas
    for (auto &key : m_foundLambdas)
        m_values[key] = AWPatternFunctions::expandLambdas(key, m_aggregator, m_values, m_foundKeys);
}


void AWKeys::createDBusInterface()
{
    // get this object id
    auto id = reinterpret_cast<qlonglong>(this);

    // create session
    auto instanceBus = QDBusConnection::sessionBus();
    // HACK we are going to use different services because it binds to
    // application
    if (instanceBus.registerService(QString("%1.i%2").arg(AWDBUS_SERVICE).arg(id))) {
        if (!instanceBus.registerObject(AWDBUS_PATH, new AWDBusAdaptor(this), QDBusConnection::ExportAllContents))
            qCWarning(LOG_AW) << "Could not register DBus object, last error" << instanceBus.lastError().message();
    } else {
        qCWarning(LOG_AW) << "Could not register DBus service, last error" << instanceBus.lastError().message();
    }

    // and same instance but for id independent service
    auto commonBus = QDBusConnection::sessionBus();
    if (commonBus.registerService(AWDBUS_SERVICE))
        commonBus.registerObject(AWDBUS_PATH, new AWDBusAdaptor(this), QDBusConnection::ExportAllContents);
}


QString AWKeys::parsePattern(QString _pattern) const
{
    // screen sign
    _pattern.replace("$$", QChar(0x1d));

    // lambdas
    for (auto &key : m_foundLambdas)
        _pattern.replace(QString("${{%1}}").arg(key), m_values[key].toString());

    // main keys
    for (auto &key : m_foundKeys)
        _pattern.replace(QString("$%1").arg(key), m_aggregator->formatter(m_values[key], key, true));

    // bars
    for (auto &bar : m_foundBars) {
        auto item = m_keyOperator->giByKey(bar);
        auto image = item->isCustom() ? item->image(AWPatternFunctions::expandLambdas(item->bar(), m_aggregator,
                                                                                      m_values, item->usedKeys()))
                                      : item->image(m_values[item->bar()]);
        _pattern.replace(QString("$%1").arg(bar), image);
    }

    // prepare strings
    _pattern.replace(QChar(0x1d), "$");
    if (m_wrapNewLines)
        _pattern.replace("\n", "<br>");

    return _pattern;
}


void AWKeys::setDataBySource(const QString &_source, const KSysGuard::SensorInfo &_sensor, const QVariant &_value)
{
    qCDebug(LOG_AW) << "Source" << _source << _sensor.name << "with data" << _value;

    // first list init
    auto tags = m_aggregator->keysFromSource(_source);
    if (tags.isEmpty())
        tags = m_aggregator->registerSource(_source, _sensor.unit, m_requiredKeys, m_optimize);

    // update data or drop source if there are no matches and exit
    if (tags.isEmpty()) {
        qCInfo(LOG_AW) << "Sensor" << _source << "not found";
        return emit(dropSourceFromDataengine(_source));
    }

    std::for_each(tags.cbegin(), tags.cend(), [this, _value](const QString &tag) { m_values[tag] = _value; });
}
