/*****************************************************************************
 *
 * 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 <QStringList>
#include <QIcon>
#include <QJsonObject>
#include <QDir>
#include <QDockWidget>
#include <QMainWindow>

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

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

DataSource::DataSource(DataModel *dataModel, const QUrl &url,
        const QUrl &connUrl):
    DataNode(dataModel, this),
    socketValid(false),
    url(url),
    connectUrl(connUrl.isEmpty() ? url : connUrl),
    name(connectUrl.toString()),
    connectionDesired(false),
    bytesOut(0),
    bytesIn(0),
    outRate(0),
    inRate(0),
    messageDockWidget(NULL)
{
    connect(&socket, SIGNAL(connected()),
            this, SLOT(socketConnected()));
    connect(&socket, SIGNAL(disconnected()),
            this, SLOT(socketDisconnected()));
    connect(&socket, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(socketError()));
    connect(&socket, SIGNAL(readyRead()),
            this, SLOT(socketRead()));
    connect(&connectTimer, SIGNAL(timeout()),
            this, SLOT(connectTimeout()));

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

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

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

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

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

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

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

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

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

void DataSource::setUrl(const QUrl &newUrl)
{
    url = newUrl;
}

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

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::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) const
{
    if (!isConnected()) {
        flags &= ~Qt::ItemIsEnabled;
    }
}

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

void DataSource::updateStats()
{
    outRate = bytesOut;
    inRate = bytesIn;
    bytesOut = 0;
    bytesIn = 0;
    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 (Pd::MessageModel::Exception &e) {
            qWarning() << "Failed to load messages: " << e.msg;
        }

        messageModel.connect(this);
    }
}

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

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

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

void DataSource::connectMessages(QObject *obj)
{
    connect(&messageModel,
            SIGNAL(currentMessage(const Pd::Message *)),
            obj, SLOT(currentMessage(const Pd::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 connection established.
 *
 * This is called, when the pure socket connection was established and the
 * Process object can start using it.
 */
void DataSource::socketConnected()
{
    socketValid = true;
    socket.setSocketOption(QAbstractSocket::KeepAliveOption, 1);
}

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

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

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

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

void DataSource::socketError()
{
    socketValid = false;
    dataModel->notify(this, 0, 1);
    emit error();

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

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

/** The socket has new data to read.
 */
void DataSource::socketRead()
{
    QByteArray data;
    data = socket.readAll();
    bytesIn += data.length();

#if DEBUG_DATA
    qDebug() << "Read:" << data;
#endif

    try {
        newData(data.constData(), data.length());
    } catch (PdCom::Exception &e) {
        qWarning() << "PdCom::Exception: " << e.what();
        socketValid = false;
        reset();
        socket.disconnectFromHost();
        emit error();
    } catch (std::exception &e) {
        qWarning() << "std::exception: " << e.what();
        socketValid = false;
        reset();
        socket.disconnectFromHost();
        emit error();
    }
}

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

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

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

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

    QString path = QString::fromLocal8Bit(pv->path.c_str());
    sections = path.split("/", QString::SkipEmptyParts);

    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);
}

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

/** Called when the process object has new data to send.
 */
void DataSource::sendRequest()
{
#if DEBUG_DATA
    qDebug() << "sendRequest()";
#endif
    while (socketValid && (writeReady() > 0)) ;
}

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

/** Sends data via the socket.
 *
 * This is the implementation of the virtual PdCom::Process()
 * method to enable the Process object to send data to the process.
 */
int DataSource::sendData(const char *data, size_t length)
{
#if DEBUG_DATA
    qDebug() << "Writing:" << length << QByteArray(data, length);
#endif
    int ret = socket.write(data, length);

    if (ret == -1) {
        qWarning("write() failed.");
    } else {
        bytesOut += ret;
        if (ret < (int) length) {
            qWarning("write() incomplete.");
        }
    }

    return ret;
}

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

/** The process is connected and ready.
 *
 * This virtual function from PdCom::Process has to be overloaded to let
 * subclasses know about this event.
 */
void DataSource::sigConnected()
{
    for (PdCom::Process::VariableSet::const_iterator pv =
            getVariables().begin(); pv != getVariables().end(); pv++) {
        appendVariable(*pv);
    }

    sortChildren();
    updateDisplay();

    messageModel.connect(this);

    emit connected();
}

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

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

    try {
        PdCom::Process::reset();
    } catch (std::exception &e) {
        // do nothing
    }

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

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

/**
 */
bool DataSource::clientInteraction(
        const std::string &,
        const std::string &,
        const std::string &,
        std::list<ClientInteraction> &interactionList
        )
{
    std::list<ClientInteraction>::iterator it;

    for (it = interactionList.begin(); it != interactionList.end(); it++) {
        if (it->prompt == "Username") {
#ifdef __WIN32__
            TCHAR user[UNLEN + 1];
            DWORD userSize = UNLEN;
            if (GetUserName(user, &userSize)) {
                QString u = QString::fromWCharArray(user, userSize);
                it->response = u.toLocal8Bit().constData();
            }
#else // unix
            char *login = getlogin();
            if (login) {
                it->response = login;
            }
#endif
        } else if (it->prompt == "Hostname") {
            char hostname[256];
            if (!gethostname(hostname, sizeof(hostname))) {
                it->response = hostname;
            }
        } else if (it->prompt == "Application") {
            it->response = tr("TestManager").toLocal8Bit().constData();
        }
    }

    return true;
}

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