/*****************************************************************************
 *
 * Testmanager - Graphical Automation and Visualisation Tool
 *
 * Copyright (C) 2018  Florian Pose <fp@igh.de>
 *
 * This file is part of Testmanager.
 *
 * Testmanager 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.
 *
 * Testmanager 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 Testmanager. If not, see <http://www.gnu.org/licenses/>.
 *
 ****************************************************************************/

#include "DataSource.h"
#include "DataModel.h"
#include "MessageWidget.h"
#include "MainWindow.h"

#include <QDebug>
#include <QErrorMessage>
#include <QStringList>
#include <QIcon>
#include <QJsonObject>
#include <QDir>
#include <QDockWidget>
#include <QMainWindow>

#include <pdcom5/Exception.h>
#include <pdcom5/Variable.h>
#include <QtPdCom1/FutureWatchers.h>
#include <QtPdCom1/LoginManager.h>

#ifdef __WIN32__
#include <windows.h> // GetUserName(), gethostname()
#include <lmcons.h> // UNLEN
#else
#include <unistd.h> // getlogin()
#endif

using QtPdCom::LoginManager;


/****************************************************************************/

LoginManager* DataSource::createLoginManager(const QUrl& url)
{
    auto ans = new LoginManager(url.host(), this);

    // if login was not successful and <connected> hasn't arrived yet,
    // we need to make a new connection to enable login again
    connect(ans, &LoginManager::loginFailed, this,
            [this]() {
                this->lastLoginSuccessful_ = false;
                if (!this->isConnected()) {
                    QTimer::singleShot(0, this,
                            &DataSource::disconnectFromHost);
                }
            });

    connect(ans, &LoginManager::loginSuccessful, this,
            [this]() {
                this->lastLoginSuccessful_ = true;
            }
    );

    return ans;
}

/****************************************************************************/

DataSource::DataSource(DataModel *dataModel, const QUrl &url,
        const QUrl &connUrl):
    DataNode(dataModel, this),
    process(new QtPdCom::Process()),
    url(url),
    connectUrl(connUrl.isEmpty() ? url : connUrl),
    name(connectUrl.toString()),
    connectionDesired(false),
    bytesOut(0),
    bytesIn(0),
    outRate(0),
    inRate(0),
    messageDockWidget(NULL),
    loginManager(createLoginManager(url))
{
    connect(process.get(), &QtPdCom::Process::disconnected,
            this, &DataSource::socketDisconnected);
    connect(process.get(), &QtPdCom::Process::error,
            this, &DataSource::socketError);
    connect(process.get(), &QtPdCom::Process::processConnected, this, [this]() {
        process->list("", this, &DataSource::onListReplyReceived);
    });

    connect(&connectTimer, SIGNAL(timeout()),
            this, SLOT(connectTimeout()));
    process->setApplicationName(tr("TestManager"));

    messageModel.setIcon(QtPdCom::Message::Critical,
            QIcon(":/images/dialog-error.svg"));
    messageModel.setIcon(QtPdCom::Message::Error,
            QIcon(":/images/dialog-error.svg"));
    messageModel.setIcon(QtPdCom::Message::Warning,
            QIcon(":/images/dialog-warning.svg"));
    messageModel.connect(process.get());

    connectTimer.setInterval(10000); // ms
    connectTimer.setSingleShot(true);
}

/****************************************************************************/

DataSource::~DataSource()
{
    disconnectFromHost();
    if (messageDockWidget) {
        delete messageDockWidget;
    }
}

/****************************************************************************/

void DataSource::connectToHost()
{
    connectionDesired = true;
    process->setLoginManager(loginManager);
    process->connectToHost(connectUrl.host(), connectUrl.port());
    dataModel->notify(this, 0, 1);
}

/****************************************************************************/

void DataSource::disconnectFromHost()
{
    connectionDesired = false;
    lastLoginSuccessful_ = false;
    dataModel->notify(this, 0, 1);
    process->disconnectFromHost();
}

/****************************************************************************/

void DataSource::setUrl(const QUrl &newUrl)
{
    url = newUrl;
    if (loginManager) {
        auto lm2 = loginManager;
        loginManager = nullptr;
        lm2->deleteLater();
    }
    loginManager = createLoginManager(url);
    lastLoginSuccessful_ = false;
}

/****************************************************************************/

DataNode *DataSource::findDataNode(const QUrl &varUrl)
{
    if (!url.isParentOf(varUrl)) {
        return NULL;
    }

    DataNode *currentNode = this, *childNode;
    QStringList sections = varUrl.path().split("/");

    if (sections.empty()) {
        return NULL;
    }

    if (sections.first() == "") {
        sections.takeFirst();
    }

    while (sections.size() > 0) {
        QString name = sections.takeFirst();
        if (!(childNode = currentNode->findChild(name))) {
            return NULL;
        }
        currentNode = childNode;
    }

    return currentNode;
}

/****************************************************************************/

void DataSource::logout()
{
    lastLoginSuccessful_ = false;
    if (loginManager) {
        loginManager->logout();
        loginManager->clearCredentials();
    }
}

void DataSource::filter(const QRegExp &re)
{
    if (re.isEmpty()) {
        showAll(true);
    }
    else {
        showAll(false);
        applyFilter(re);
    }

    updateDisplay();
}

/****************************************************************************/

QVariant DataSource::nodeData(int role, int column)
{
    QVariant ret;

    switch (role) {
        case Qt::DisplayRole:
            switch (column) {
                case 0:
                    ret = name;
                    break;
                case 1:
                    {
                        QLocale loc;
                        ret = tr("%2 ⇄ %3 KB/s")
                            .arg(loc.toString(inRate / 1024.0, 'f', 1))
                            .arg(loc.toString(outRate / 1024.0, 'f', 1));
                    }
                    break;
            }
            break;

        case Qt::DecorationRole:
            switch (column) {
                case 0:
                    ret = QIcon(":/images/computer.svg");
                    break;
            }
            break;
    }

    return ret;
}

/****************************************************************************/

void DataSource::nodeFlags(Qt::ItemFlags &flags, int column) const
{
    if (!process->isConnected()) {
        flags &= ~Qt::ItemIsEnabled;
    }
    if (column == 0) {
        flags |= Qt::ItemIsDragEnabled;
    }
}

/****************************************************************************/

void DataSource::updateStats()
{
    outRate = process->getTxBytes() - bytesOut;
    inRate = process->getRxBytes() - bytesIn;
    bytesOut = process->getTxBytes();
    bytesIn = process->getRxBytes();
    dataModel->notify(this, 1, 1);
}

/****************************************************************************/

void DataSource::setMessagePath(const QString &path)
{
    if (path != messagePath) {
        messagePath = path;

        QString loadPath(messagePath);

        QStringList searchPaths(QDir::searchPaths("layout"));
        if (searchPaths.size()) {
            loadPath = QDir(searchPaths[0]).filePath(loadPath);
        }

        QString lang = QLocale().name().left(2);
        if (lang == "C") {
            lang = "en";
        }

        try {
            messageModel.load(loadPath, lang);
        }
        catch (QtPdCom::MessageModel::Exception &e) {
            qWarning() << "Failed to load messages: " << e.msg;
        }

        messageModel.connect(process.get());
    }
}

/****************************************************************************/

bool DataSource::hasMessages() const
{
    return !messagePath.isEmpty();
}

/****************************************************************************/

void DataSource::connectMessages(QObject *obj)
{
    connect(&messageModel,
            SIGNAL(currentMessage(const QtPdCom::Message *)),
            obj, SLOT(currentMessage(const QtPdCom::Message *)));
}

/****************************************************************************/

void DataSource::showMessages(QMainWindow *mainWindow)
{
    if (!messageDockWidget) {
        messageDockWidget = new QDockWidget(mainWindow);
        messageDockWidget->setObjectName("MessageDockWidget");
        MessageWidget *messageWidget =
            new MessageWidget(&messageModel, messageDockWidget);
        messageDockWidget->setWidget(messageWidget);
        mainWindow->addDockWidget(Qt::BottomDockWidgetArea,
                messageDockWidget);
    }

    messageDockWidget->show();
    messageDockWidget->raise();
}

/****************************************************************************/

void DataSource::read(const QJsonObject &json)
{
    if (json.contains("messagePath")) {
        setMessagePath(json["messagePath"].toString());
    }
}

/****************************************************************************/

void DataSource::write(QJsonObject &json) const
{
    if (!messagePath.isEmpty()) {
        json["messagePath"] = messagePath;
    }

    if (!json.isEmpty()) {
        json["url"] = url.toString();
    }
}

/*****************************************************************************
 * private slots
 ****************************************************************************/

/** Socket disconnected.
 *
 * The socket was closed and the process has to be told, that it is
 * disconnected.
 */
void DataSource::socketDisconnected()
{
    reset();
    dataModel->notify(this, 0, 1);
    lastLoginSuccessful_ = false;

    if (connectionDesired and dataModel->getMainWindow()->getAutoConnect()) {
        connectTimer.start();
    }
    emit disconnected();
}

void DataSource::socketError()
{
    lastLoginSuccessful_ = false;
    if (connectionDesired and dataModel->getMainWindow()->getAutoConnect()) {
        connectTimer.start();
    }
    emit error();
}

/****************************************************************************/

/** The re-connect timer has timed out.
 */
void DataSource::connectTimeout()
{
    connectToHost();
}

/*****************************************************************************
 * private
 ****************************************************************************/

void DataSource::appendVariable(PdCom::Variable const& pv)
{
    QStringList sections;
    DataNode *currentNode, *childNode;

    QString path = QString::fromLocal8Bit(pv.getPath().c_str());
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
    sections = path.split("/", Qt::SkipEmptyParts);
#else
    sections = path.split("/", QString::SkipEmptyParts);
#endif

    if (!sections.size()) {
        qWarning("Invalid variable path!");
        return;
    }

    currentNode = this;

    while (sections.size() > 0) {
        QString name = sections.takeFirst();
        childNode = currentNode->findChild(name);
        if (!childNode) {
            childNode = new DataNode(dataModel, this, currentNode, name);
        }
        currentNode = childNode;
    }

    currentNode->setVariable(pv);
}

/****************************************************************************/

void DataSource::onListReplyReceived(QtPdCom::VariableList vars)
{
    for (const auto& pv : vars.variables)
    {
        appendVariable(pv);
    }

    sortChildren();
    updateDisplay();


    emit variableTreeBuilt();
    process->callPendingCallbacks();
}

/****************************************************************************/

/** Resets the PdCom process.
 */
void DataSource::reset()
{
    clearVariables();


    dataModel->notify(this, 0, 1);
}

/****************************************************************************/

bool DataSource::isConnected() const
{
    return process->isConnected();
}

/****************************************************************************/

QString DataSource::errorString() const
{
    return process->getErrorString();
}

/****************************************************************************/

void DataSource::callPendingCallbacks()
{
    process->callPendingCallbacks();
}
