/*****************************************************************************
 *
 * Testmanager - Graphical Automation and Visualisation Tool
 *
 * Copyright (C) 2019  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 <Python.h>

#include "ScriptVariable.h"
#include <pdcom5/Exception.h>
#include <pdcom5/Subscriber.h>
#include <pdcom5/Subscription.h>

#include "ScriptVariableModel.h"
#include "WidgetContainer.h"
#include "TabPage.h"
#include "MainWindow.h"
#include "DataNode.h"
#include "DataModel.h"

#include <QJsonObject>
#include <QDebug>

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

typedef struct {
    PyObject_HEAD
    ScriptVariable *ptr;
} PyScriptVariable;

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

static PyObject *getValue(PyObject *self, PyObject * /* args */)
{
    ScriptVariable *var(((PyScriptVariable *) self)->ptr);
    return Py_BuildValue("d", var->getValue());
}

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

static PyMethodDef pyScriptVariableMethods[] = {
    { "getValue", getValue, METH_NOARGS,
        "Query the current value" },
    {NULL, NULL, 0, NULL}
};

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

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"

static PyTypeObject pyScriptVariableType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "ScriptVariable",              /* tp_name */
    sizeof(PyScriptVariable),      /* tp_basicsize */
    0,                             /* tp_itemsize */
    0,                             /* tp_dealloc */
    0,                             /* tp_print */
    0,                             /* tp_getattr */
    0,                             /* tp_setattr */
    0,                             /* tp_reserved */
    0,                             /* tp_repr */
    0,                             /* tp_as_number */
    0,                             /* tp_as_sequence */
    0,                             /* tp_as_mapping */
    0,                             /* tp_hash */
    0,                             /* tp_call */
    0,                             /* tp_str */
    0,                             /* tp_getattro */
    0,                             /* tp_setattro */
    0,                             /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT,            /* tp_flags */
    "Script variables",            /* tp_doc */
    0,                             /* tp_traverse */
    0,                             /* tp_clear */
    0,                             /* tp_richcompare */
    0,                             /* tp_weaklistoffset */
    0,                             /* tp_iter */
    0,                             /* tp_iternext */
    pyScriptVariableMethods,       /* tp_methods */
    0,                             /* tp_members */
    0,                             /* tp_getset */
    0,                             /* tp_base */
    0,                             /* tp_dict */
    0,                             /* tp_descr_get */
    0,                             /* tp_descr_set */
    0,                             /* tp_dictoffset */
    0,                             /* tp_init */
    0,                             /* tp_alloc */
    0,                             /* tp_new */
};

#pragma GCC diagnostic pop

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

void ScriptVariable::registerPythonType()
{
    if (PyType_Ready(&pyScriptVariableType) < 0) {
        qWarning() << "Failed to finalize Python type ScriptVariable!";
    }
}

/*****************************************************************************
 * public
 ****************************************************************************/


struct ScriptVariable::Subscription : PdCom::Subscriber
{
    ScriptVariable *parent_;
    PdCom::Subscription subscription_;

    Subscription(ScriptVariable& parent, PdCom::Transmission t, PdCom::Variable const& var)
        : PdCom::Subscriber(t), parent_(&parent), subscription_(*this, var)
    {}

    void stateChanged(PdCom::Subscription const &subscription) override
    {
        if (subscription.getState() == PdCom::Subscription::State::Invalid) {
            parent_->variable = {};
            subscription_ = {};

            if (parent_->dataNode) {
                parent_->dataNode->notifyUnsubscribed();
                parent_->dataNode = nullptr;
            }

            parent_->dataPresent = false;
            parent_-> model->notify(parent_);
        }else if (subscription.getState() == PdCom::Subscription::State::Active
                    and getTransmission() == PdCom::event_mode)
        {
            try {
                subscription_.poll();
            }
            catch (PdCom::Exception &e) {
                MainWindow *mainWindow(
                    parent_->model->getContainer()->getTabPage()->getMainWindow());
                QString msg(tr("Script variable poll failed: %1")
                        .arg(e.what()));
                mainWindow->statusBar()->showMessage(msg, 5000);
                qWarning() << msg;
                subscription_ = {};
                parent_->variable = {};
                parent_->dataNode->notifyUnsubscribed();
                parent_->dataNode = nullptr;
            }
        }
    }
    void newValues(std::chrono::nanoseconds time_ns) override
    {
        Q_UNUSED(time_ns);
        subscription_.getValue(parent_->value);
        parent_->dataPresent = true;
        parent_->model->notify(parent_);
        parent_->model->getContainer()->executeScript();
    }
};

ScriptVariable::ScriptVariable(ScriptVariableModel *model):
    period(0.0),
    model(model),
    dataNode(nullptr),
    dataPresent(false),
    value(0.0),
    pyObject((PyObject *) PyObject_New(PyScriptVariable,
                &pyScriptVariableType))
{
    ((PyScriptVariable *) pyObject)->ptr = this;
}

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

ScriptVariable::~ScriptVariable()
{
    subscription.reset();
    if (dataNode) {
        dataNode->notifyUnsubscribed();
    }

    PyObject_Del(pyObject);
}

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

QIcon ScriptVariable::getIcon() const
{
    if (dataNode) {
        return dataNode->getIcon();
    }
    else {
        return QIcon();
    }
}

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

void ScriptVariable::fromJson(const QJsonValue &value)
{
    QJsonObject obj(value.toObject());

    url = obj["url"].toString();
    period = std::chrono::duration<double>(obj["period"].toDouble());
    name = obj["name"].toString();
}

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

QJsonValue ScriptVariable::toJson() const
{
    QJsonObject obj;

    obj["url"] = url.toString();

    if (period.count()) {
        obj["period"] = period.count();
    }

    obj["name"] = name;

    return obj;
}

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

void ScriptVariable::replaceUrl(const QUrl &oldUrl, const QUrl &newUrl)
{
    QUrl dataSourceUrl(url.adjusted(QUrl::RemovePassword |
                QUrl::RemovePath | QUrl::RemoveQuery |
                QUrl::RemoveFragment));
    if (dataSourceUrl != oldUrl) {
        return;
    }

    url.setScheme(newUrl.scheme());
    url.setAuthority(newUrl.authority()); // user, pass, host, port
}

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

void ScriptVariable::connectVariables()
{
    subscription.reset();
    if (dataNode) {
        dataNode->notifyUnsubscribed();
        dataNode = nullptr;
    }

    MainWindow *mainWindow(
            model->getContainer()->getTabPage()->getMainWindow());
    DataModel *dataModel(mainWindow->getDataModel());
    dataNode = dataModel->findDataNode(url);

    if (dataNode) {
        variable = dataNode->getVariable();
        const auto transmission = period.count() == 0 ?
                QtPdCom::Transmission{QtPdCom::event_mode} : QtPdCom::Transmission{period};
        if (!variable.empty()) {
            try {
                subscription.reset(new Subscription(*this, transmission.toPdCom(), variable));
            }
            catch (PdCom::Exception &e) {
                QString msg(tr("Script variable subscription failed: %1")
                        .arg(e.what()));
                mainWindow->statusBar()->showMessage(msg, 5000);
                qWarning() << msg;
                variable = {};
                dataNode = nullptr;
                return;
            }
            dataNode->notifySubscribed(transmission);
        }
    }
    else {
        QString msg(tr("URL not found: %1").arg(url.toString()));
        mainWindow->statusBar()->showMessage(msg, 5000);
        qWarning() << msg;
    }
}

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

void ScriptVariable::appendToPythonDict(PyObject *dict) const
{
    if (name.isEmpty()) {
        return;
    }

    PyDict_SetItemString(dict, name.toUtf8().constData(), pyObject);
}

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