/*****************************************************************************
 *
 * Testmanager - Graphical Automation and Visualisation Tool
 *
 * Copyright (C) 2018 - 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 "DataSource.h"
#include "WidgetContainer.h"

#include "MainWindow.h"
#include "TabPage.h"
#include "HandleWidget.h"
#include "Plugin.h"
#include "PropertyModel.h"
#include "DataModel.h"
#include "DataSlot.h"
#include "SlotDialog.h"
#include "StyleDialog.h"
#include "DataNode.h"

#include <QMenu>
#include <QAction>
#include <QContextMenuEvent>
#include <QMimeData>
#include <QPainter>
#include <QJsonObject>
#include <QJsonArray>
#include <QStatusBar>
#include <QDebug>
#include <QMetaProperty>

#define MIN_SIZE (21)

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

typedef struct {
    PyObject_HEAD
    WidgetContainer *ptr;
} PyWidgetContainer;

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

static PyObject *setEnabled(PyObject *self, PyObject *args)
{
    WidgetContainer *container(((PyWidgetContainer *) self)->ptr);
    bool enable;
    if (!PyArg_ParseTuple(args, "b", &enable)) {
        qWarning() << __func__ << "invalid argument!";
        return Py_BuildValue("");
    }

    container->getWidget()->setEnabled(enable);

    return Py_BuildValue("");
}

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

static PyMethodDef pyWidgetContainerMethods[] = {
    { "setEnabled", setEnabled, METH_VARARGS,
        "Set the enable state of the widget" },
    {NULL, NULL, 0, NULL}
};

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

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

static PyTypeObject pyWidgetContainerType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "WidgetContainer",             /* tp_name */
    sizeof(PyWidgetContainer),     /* 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 */
    "Widget Container",            /* tp_doc */
    0,                             /* tp_traverse */
    0,                             /* tp_clear */
    0,                             /* tp_richcompare */
    0,                             /* tp_weaklistoffset */
    0,                             /* tp_iter */
    0,                             /* tp_iternext */
    pyWidgetContainerMethods,      /* 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 WidgetContainer::registerPythonType()
{
    if (PyType_Ready(&pyWidgetContainerType) < 0) {
        qWarning() << "Failed to finalize Python type WidgetContainer!";
    }
}

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

WidgetContainer::WidgetContainer(
        TabPage *tabPage,
        Plugin *plugin
        ):
    QWidget(tabPage),
    tabPage(tabPage),
    plugin(plugin),
    widget(plugin->create(this)),
    handleWidget(new HandleWidget(this)),
    privateData(plugin->createPrivateData(widget)),
    slotModel(tabPage->getMainWindow()->getDataModel()),
    pyObject((PyObject *) PyObject_New(PyWidgetContainer,
                &pyWidgetContainerType)),
    dragging(false),
    selected(false),
    scriptVariables(this),
    scriptModule(nullptr),
    scriptUpdate(nullptr),
    scriptLabel(this),
    scriptWarning(false),
    legend(this, tabPage),
    showLegend(false)
{
    ((PyWidgetContainer *) pyObject)->ptr = this;

    setAcceptDrops(true);

    plugin->initProperties(this);

    widget->show();
    resize(widget->size());

    handleWidget->raise();
    updateHandleWidget();

    scriptLabel.raise();
    scriptLabel.hide();

    legend.resize(51, MIN_SIZE);
    updateLegend();
}

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

WidgetContainer::~WidgetContainer()
{
    auto dataModel(tabPage->getMainWindow()->getDataModel());

    deselect();

    plugin->clearVariables(widget, privateData);

    auto urls(slotModel.getUrls()); // save URLs for notification below
    slotModel.clear();
    dataModel->notifyUnsubscribed(urls);

    auto scriptUrls(scriptVariables.getUrls()); // save URLs for below
    scriptVariables.clear(); // be sure to clear first -> callback!
    dataModel->notifyUnsubscribed(scriptUrls);

    Py_XDECREF(scriptUpdate);
    scriptUpdate = nullptr;
    Py_XDECREF(scriptModule);
    scriptModule = nullptr;

    plugin->deletePrivateData(widget, privateData);

    legend.deleteLater();
}

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

void WidgetContainer::adjustLeft(int dx)
{
    if (width() - dx < MIN_SIZE) {
        dx = width() - MIN_SIZE;
    }

    setGeometry(x() + dx, y(), width() - dx, height());
    tabPage->updateMinimumSize();
}

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

void WidgetContainer::adjustTop(int dy)
{
    if (height() - dy < MIN_SIZE) {
        dy = height() - MIN_SIZE;
    }

    setGeometry(x(), y() + dy, width(), height() - dy);
    tabPage->updateMinimumSize();
}

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

void WidgetContainer::adjustWidth(int dw)
{
    if (width() + dw < MIN_SIZE) {
        dw = MIN_SIZE - width();
    }

    resize(width() + dw, height());
    tabPage->updateMinimumSize();
}

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

void WidgetContainer::adjustHeight(int dh)
{
    if (height() + dh < MIN_SIZE) {
        dh = MIN_SIZE - height();
    }

    resize(width(), height() + dh);
    tabPage->updateMinimumSize();
}

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

void WidgetContainer::moveSelected(const QPoint &dp)
{
    foreach(WidgetContainer *container,
            tabPage->getMainWindow()->containers(MainWindow::Selected)) {
        container->move(container->pos() + dp);
    }
    tabPage->updateMinimumSize();
}

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

void WidgetContainer::editModeChanged()
{
    updateHandleWidget();
    updateScriptLabel();
    legend.editModeChanged();
}

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

void WidgetContainer::read(const QJsonObject &obj)
{
    QJsonObject geo(obj["geometry"].toObject());

    int x = geo["x"].toDouble(); // older versions do not support toInt()
    int y = geo["y"].toDouble();
    move(x, y);

    int width = geo["width"].toDouble();
    int height = geo["height"].toDouble();
    resize(width, height);

    slotModel.clear();
    if (obj.contains("slots")) {
        slotModel.fromJson(obj["slots"]);
    }

    if (obj.contains("properties")) {
        QJsonObject propertyObject(obj["properties"].toObject());
        foreach (QString keyStr, propertyObject.keys()) {
            QByteArray key(keyStr.toLocal8Bit());
            QJsonValue jsonVal(propertyObject[key]);
#if QT_VERSION >= 0x050200
            QVariant value(jsonVal.toVariant());
#else
            QVariant value;
            if (jsonVal.isDouble()) {
                value = jsonVal.toDouble();
            }
            else if (jsonVal.isBool()) {
                value = jsonVal.toBool();
            }
            else if (jsonVal.isString()) {
                value = jsonVal.toString();
            }
            else {
                qWarning() << jsonVal << "not convertible.";
            }
#endif
            setProperty(key, value);
        }
    }

    if (obj.contains("private")) {
        QJsonObject privateObject(obj["private"].toObject());
        plugin->readPrivateData(privateObject, widget, privateData);
    }

    if (obj.contains("legend")) {
        QJsonObject legendObject(obj["legend"].toObject());
        legendOffset.setX(legendObject["x"].toDouble());
        legendOffset.setY(legendObject["y"].toDouble());
        legend.resize(legendObject["width"].toDouble(),
                legendObject["height"].toDouble());
        legend.setAlignment(
                (Qt::AlignmentFlag) legendObject["alignment"].toDouble());
        showLegend = legendObject["show"].toBool();
    }
    updateLegend();
    legend.update();

    scriptVariables.clear();
    if (obj.contains("scriptVariables")) {
        scriptVariables.fromJson(obj["scriptVariables"].toArray());
    }


    if (plugin->usesVariables() == Plugin::UsesVariables::ProcessOnly)
    {
        if (obj.contains("process") && obj["process"].isArray()) {
            const auto obj_arr = obj["process"].toArray();
            if (obj_arr.size() == 1 && obj_arr[0].isObject()) {
                const auto o = obj_arr[0].toObject();
                if (o.contains("url")) {
                    auto dataModel(tabPage->getMainWindow()->getDataModel());
                    const auto url = QUrl(o["url"].toString()).adjusted(QUrl::RemovePassword |
                                                                        QUrl::RemovePath | QUrl::RemoveQuery |
                                                                        QUrl::RemoveFragment);
                    auto *ds = dataModel->getDataSource(url);
                    if (!ds) {
                        ds = new DataSource(dataModel, url);
                        dataModel->append(ds);
                    }
                    plugin->setDataSource(widget, privateData, ds);
                }
            }
        }
    }

    script = obj["script"].toString();
    updateScript();
}

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

void WidgetContainer::write(QJsonObject &obj) const
{
    obj["type"] = plugin->type();

    QJsonObject geo;
    geo["x"] = x();
    geo["y"] = y();
    geo["width"] = width();
    geo["height"] = height();
    obj["geometry"] = geo;

    QJsonObject propertiesObject;

    foreach (QByteArray propName, properties.keys()) {
        int idx = widget->metaObject()->indexOfProperty(propName);
        QMetaProperty metaProperty(widget->metaObject()->property(idx));
        QString valueStr;
        QVariant value(widget->property(propName));
        if (metaProperty.isEnumType()) {
            QMetaEnum e(metaProperty.enumerator());
            QByteArray keys(e.valueToKeys(value.toInt()));
            valueStr = QString::fromLocal8Bit(keys);
        }
        else {
            // TODO more types natively
            valueStr = value.toString();
        }
        propertiesObject[propName] = valueStr;
    }

    if (!propertiesObject.empty()) {
        obj["properties"] = propertiesObject;
    }

    // slot model always returns an array
    QJsonArray slotsArray(slotModel.toJson().toArray());
    if (!slotsArray.empty()) {
        obj["slots"] = slotsArray;
    }

    QJsonObject privateObject(plugin->writePrivateData(widget, privateData));
    if (!privateObject.empty()) {
        obj["private"] = privateObject;
    }

    if (showLegend or legendOffset != QPoint()) {
        QJsonObject legendObject;
        legendObject["x"] = legendOffset.x();
        legendObject["y"] = legendOffset.y();
        legendObject["width"] = legend.width();
        legendObject["height"] = legend.height();
        legendObject["alignment"] = legend.getAlignment();
        legendObject["show"] = showLegend;
        obj["legend"] = legendObject;
    }

    QJsonArray scriptVarArray(scriptVariables.toJson());
    if (!scriptVarArray.empty()) {
        obj["scriptVariables"] = scriptVarArray;
    }

    if (not script.isEmpty()) {
        obj["script"] = script;
    }

    if (plugin->usesVariables() == Plugin::UsesVariables::ProcessOnly)
    {
        QJsonObject o;
        o["url"] = plugin->getDataSource(widget, privateData);
        obj["process"] = QJsonArray({o});
    }
}

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

void WidgetContainer::connectDataSlots()
{
    auto dataModel(tabPage->getMainWindow()->getDataModel());

    // no variables, that are now connected, should be disconnected
    // afterwards, thus unsubscription notification can be omitted
    plugin->clearVariables(widget, privateData);

    slotModel.appendDataSources(dataModel);
    plugin->connectDataSlots(widget, privateData, &slotModel, dataModel);
    scriptVariables.connectVariables();
}

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

void WidgetContainer::replaceUrl(const QUrl &oldUrl, const QUrl &newUrl)
{
    slotModel.replaceSlotUrls(oldUrl, newUrl);
    scriptVariables.replaceUrl(oldUrl, newUrl);
    legend.update();
}

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

bool WidgetContainer::uses(const QUrl &url) const
{
    return slotModel.slotsUse(url) or scriptVariables.uses(url);
}

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

void WidgetContainer::select()
{
    if (!selected) {
        selected = true;
        handleWidget->update();
        MainWindow *mainWindow(tabPage->getMainWindow());
        mainWindow->getPropertyModel()->addContainer(this);
        mainWindow->selectionChanged();
        mainWindow->tableViewScriptVariables->setModel(&scriptVariables);

        // disconnect all other widgets from the script editor
        disconnect(mainWindow->textEditScript, SIGNAL(textChanged()),
                nullptr, nullptr);

        mainWindow->textEditScript->setPlainText(script);
        mainWindow->textEditScript->setEnabled(true);
        connect(mainWindow->textEditScript, SIGNAL(textChanged()),
                    this, SLOT(scriptChanged()));
    }
}

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

void WidgetContainer::deselect()
{
    if (selected) {
        selected = false;
        handleWidget->update();
        MainWindow *mainWindow(tabPage->getMainWindow());
        mainWindow->getPropertyModel()->removeContainer(this);
        mainWindow->selectionChanged();

        // Keep in mind, that the deselected widget may be not the last widget
        // that was selected.
        // This disconnects the script editor from all widgets, so that one
        // widget has to be re-selected to continue editing. This could be
        // solved better.
        mainWindow->tableViewScriptVariables->setModel(nullptr);
        disconnect(mainWindow->textEditScript, SIGNAL(textChanged()),
                nullptr, nullptr);
        mainWindow->textEditScript->setPlainText(QString());
        mainWindow->textEditScript->setEnabled(false);
    }
}

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

bool WidgetContainer::setProperty(const QByteArray &propName,
        const QVariant &value)
{
    if (!widget->property(propName).isValid()) {
        // property does not exist
        QString propNameStr(QString::fromLocal8Bit(propName));
        tabPage->getMainWindow()->statusBar()->showMessage(
                tr("Property %1 does not exist.").arg(propNameStr), 2000);
        return false;
    }

    if (!widget->setProperty(propName, value)) {
        QString propNameStr(QString::fromLocal8Bit(propName));
        tabPage->getMainWindow()->statusBar()->showMessage(
                tr("Failed to set property %1 to %2")
                .arg(propNameStr).arg(value.toString()), 2000);
        return false;
    }

    properties[propName] = value;
    return true;
}

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

void WidgetContainer::resetProperty(const QByteArray &propName)
{
    properties.remove(propName);
    widget->setProperty(propName, QVariant());
}

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

const WidgetContainer *WidgetContainer::duplicateSelected() const
{
    const WidgetContainer *ret(this);

    MainWindow *mainWindow(tabPage->getMainWindow());
    auto selectedContainers(mainWindow->containers(MainWindow::Selected));

    mainWindow->deselectAll();

    foreach (WidgetContainer *selectedContainer, selectedContainers) {

        QWidget *parentWidget =
            dynamic_cast<QWidget *>(selectedContainer->parent());
        if (!parentWidget) {
            continue;
        }
        QJsonObject obj;
        selectedContainer->write(obj);

        WidgetContainer *container = new WidgetContainer(tabPage,
                selectedContainer->plugin);
        container->read(obj);
        container->show();
        container->raise();
        container->legend.raise();
        container->select();
        container->connectDataSlots();

        if (selectedContainer == this) {
            ret = container;
        }
    }

    return ret;
}

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

void WidgetContainer::openEditor()
{
    plugin->openEditor(widget, privateData);
}

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

void WidgetContainer::updateScript()
{
    Py_XDECREF(scriptModule);
    scriptModule = nullptr;
    Py_XDECREF(scriptUpdate);
    scriptUpdate = nullptr;

    if (script.isEmpty()) {
        scriptWarning = false;
        scriptLabel.setToolTip("");
        updateScriptLabel();
        return;
    }

    scriptModule = PyModule_New("WidgetContainer");
    PyModule_AddStringConstant(scriptModule, "__file__", "");
    PyObject *locals = PyModule_GetDict(scriptModule); // borrowed reference
    PyObject *builtins = PyEval_GetBuiltins();  // borrowed reference
    PyDict_SetItemString(locals, "__builtins__", builtins);

    PyDict_SetItemString(locals, "widget", pyObject);
    scriptVariables.appendToPythonDict(locals);

    QByteArray ba(script.toLocal8Bit());
    PyObject *result = PyRun_String(ba.constData(), Py_file_input,
            locals, locals);
    if (result == NULL) {
        PyObject *ptype, *pvalue, *ptraceback;
        PyErr_Fetch(&ptype, &pvalue, &ptraceback);
        PyErr_NormalizeException(&ptype, &pvalue, &ptraceback);
        QString msg;

        if (PyErr_GivenExceptionMatches(pvalue, PyExc_SyntaxError)) {
            PyObject *message(PyObject_GetAttrString(pvalue, "msg"));
            PyObject *lineNo(PyObject_GetAttrString(pvalue, "lineno"));
            msg = tr("Syntax error in line %1: %2")
                .arg(PyLong_AsLong(lineNo))
                .arg(PyUnicode_AsUTF8(message));
            qWarning() << msg;
            Py_DECREF(message);
            Py_DECREF(lineNo);
        }
        else {
            PyObject *v = PyObject_Repr(pvalue);
            if (v) {
                msg = PyUnicode_AsUTF8(v);
            }
            Py_XDECREF(v);
        }

        Py_DECREF(ptype);
        Py_XDECREF(pvalue);
        Py_XDECREF(ptraceback);
        scriptWarning = true;
        scriptLabel.setToolTip(msg);
        updateScriptLabel();
        return;
    }

    Py_DECREF(result);

    scriptUpdate = PyObject_GetAttrString(scriptModule, "update");
    if (PyErr_Occurred()) {
        PyErr_Print();
        qWarning() << "Failed to find update function.";
        Py_DECREF(scriptModule);
        scriptModule = nullptr;
        scriptWarning = true;
        scriptLabel.setToolTip(tr("Callable update() not found."));
        updateScriptLabel();
        return;
    }

    if (not PyCallable_Check(scriptUpdate)) {
        qWarning() << "update() is not callable.";
        Py_DECREF(scriptUpdate);
        scriptUpdate = nullptr;
        Py_DECREF(scriptModule);
        scriptModule = nullptr;
        scriptWarning = true;
        scriptLabel.setToolTip(tr("update() is not callable."));
        updateScriptLabel();
        return;
    }

    scriptWarning = false;
    scriptLabel.setToolTip("");
    updateScriptLabel();
}

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

void WidgetContainer::executeScript()
{
    if (not scriptUpdate) {
        return;
    }

    PyObject *args = PyTuple_New(0);
    PyObject *call = PyObject_CallObject(scriptUpdate, args);
    Py_DECREF(args);

    if (PyErr_Occurred()) {
        PyObject *ptype, *pvalue, *ptraceback;
        PyErr_Fetch(&ptype, &pvalue, &ptraceback);
        PyErr_NormalizeException(&ptype, &pvalue, &ptraceback);
        QString msg;

        PyObject *v = PyObject_Repr(pvalue);
        if (v) {
            msg = PyUnicode_AsUTF8(v);
        }
        Py_XDECREF(v);

        scriptLabel.setToolTip(msg);
        scriptWarning = true;
        updateScriptLabel();

        Py_DECREF(ptype);
        Py_XDECREF(pvalue);
        Py_XDECREF(ptraceback);

        Py_DECREF(scriptUpdate);
        scriptUpdate = nullptr;
        Py_DECREF(scriptModule);
        scriptModule = nullptr;
    }
    if (call) {
        Py_DECREF(call);
    }
}

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

void WidgetContainer::scriptVariablesChanged()
{
    updateScriptLabel();
}

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

void WidgetContainer::setLegend(bool show)
{
    showLegend = show;
    updateLegend();
}

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

void WidgetContainer::legendMoved()
{
    legendOffset = legend.pos() - pos();
}

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

void WidgetContainer::contextMenuEvent(QContextMenuEvent *event)
{
    QMenu menu;

    auto selectedContainers(tabPage->getMainWindow()->containers(
                    MainWindow::Selected));
    bool canAlign = selectedContainers.size() > 1;

    QAction *action;
    QMenu *subMenu;

    action = new QAction(this);
    action->setText(tr("Copy %Ln widget(s)", "",
                selectedContainers.size()));
    connect(action, SIGNAL(triggered()),
            tabPage->getMainWindow(), SLOT(on_actionCopy_triggered()));
    action->setEnabled(selectedContainers.size() > 0);
    action->setShortcut(Qt::CTRL + Qt::Key_C);
    action->setIcon(QIcon(":/images/edit-copy.svg"));
    menu.addAction(action);

    action = new QAction(this);
    action->setText(tr("Remove %Ln widget(s)", "",
                selectedContainers.size()));
    connect(action, SIGNAL(triggered()), this, SLOT(actionDelete()));
    action->setEnabled(selectedContainers.size() > 0);
    action->setShortcut(Qt::Key_Delete);
    action->setIcon(QIcon(":/images/list-remove.svg"));
    menu.addAction(action);

    menu.addSeparator();

    action = new QAction(this);
    action->setText(tr("Process data connections..."));
    connect(action, SIGNAL(triggered()), this, SLOT(actionSlots()));
    action->setIcon(QIcon(":/images/network-transmit-receive.svg"));
    menu.addAction(action);

    subMenu = new QMenu(this);
    subMenu->setTitle(tr("Jump to variable"));
    subMenu->setIcon(QIcon(":/images/go-jump.svg"));
    menu.addMenu(subMenu);

    QList<const DataSlot *> slotList(slotModel.getDataSlots());
    subMenu->setEnabled(!slotList.isEmpty());

    foreach (const DataSlot *dataSlot, slotList) {
        action = new QAction(this);
        action->setText(dataSlot->url.toString());
        action->setIcon(dataSlot->getIcon(
                    tabPage->getMainWindow()->getDataModel()));
        action->setData(dataSlot->url);
        connect(action, SIGNAL(triggered()), this, SLOT(actionJump()));
        subMenu->addAction(action);
    }

    action = new QAction(this);
    action->setText(tr("Clear process data connections of %Ln widget(s)",
                "", selectedContainers.size()));
    connect(action, SIGNAL(triggered()), this, SLOT(actionDisconnect()));
    action->setIcon(QIcon(":/images/edit-clear.svg"));
    menu.addAction(action);

    menu.addSeparator();

    action = new QAction(this);
    action->setText(tr("Align left"));
    action->setIcon(QIcon(":/images/align-horizontal-left.svg"));
    action->setEnabled(canAlign);
    connect(action, SIGNAL(triggered()), this, SLOT(actionAlignLeft()));
    menu.addAction(action);

    action = new QAction(this);
    action->setText(tr("Align center"));
    action->setIcon(QIcon(":/images/align-horizontal-center.svg"));
    action->setEnabled(canAlign);
    connect(action, SIGNAL(triggered()), this, SLOT(actionAlignCenter()));
    menu.addAction(action);

    action = new QAction(this);
    action->setText(tr("Align right"));
    action->setIcon(QIcon(":/images/align-horizontal-right.svg"));
    action->setEnabled(canAlign);
    connect(action, SIGNAL(triggered()), this, SLOT(actionAlignRight()));
    menu.addAction(action);

    menu.addSeparator();

    action = new QAction(this);
    action->setText(tr("Bring to front"));
    action->setShortcut(Qt::Key_Home);
    connect(action, SIGNAL(triggered()), this, SLOT(actionBringToFront()));
    action->setIcon(QIcon(":/images/go-up.svg"));
    menu.addAction(action);

    action = new QAction(this);
    action->setText(tr("Send to back"));
    action->setShortcut(Qt::Key_End);
    connect(action, SIGNAL(triggered()), this, SLOT(actionSendToBack()));
    action->setIcon(QIcon(":/images/go-down.svg"));
    menu.addAction(action);

    menu.addSeparator();

    action = new QAction(this);
    action->setText(tr("Edit stylesheet of %Ln widget(s)...", "",
                selectedContainers.size()));
    action->setIcon(QIcon(":/images/stylesheet.svg"));
    connect(action, SIGNAL(triggered()), this, SLOT(actionStylesheet()));
    menu.addAction(action);

    action = new QAction(this);
    action->setText(tr("Edit script..."));
    action->setIcon(QIcon(":/images/python-script.svg"));
    connect(action, SIGNAL(triggered()), this, SLOT(actionScript()));
    menu.addAction(action);

    action = new QAction(this);
    action->setText(tr("Show legend"));
    action->setCheckable(true);
    action->setChecked(showLegend);
    connect(action, SIGNAL(changed()), this, SLOT(actionLegend()));
    menu.addAction(action);

    menu.exec(event->globalPos());
}

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

void WidgetContainer::dragEnterEvent(QDragEnterEvent *event)
{
    dragging = true;
    event->accept();
}

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

void WidgetContainer::dragMoveEvent(QDragMoveEvent *event)
{
    if (dropAcceptable(event)) {
        handleWidget->setHighlight(HandleWidget::DropAccepted);
        updateHandleWidget();
        event->accept();
    }
    else {
        handleWidget->setHighlight(HandleWidget::DropDenied);
        updateHandleWidget();
        event->ignore();
    }
}

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

void WidgetContainer::dragLeaveEvent(QDragLeaveEvent *event)
{
    event->accept();
    dragging = false;
    handleWidget->setHighlight(HandleWidget::Standard);
    updateHandleWidget();
    tabPage->getMainWindow()->statusBar()->clearMessage();
}

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

void WidgetContainer::dropEvent(QDropEvent *event)
{
    dragging = false;
    handleWidget->setHighlight(HandleWidget::Standard);
    updateHandleWidget();

    auto mainWindow(tabPage->getMainWindow());
    auto dataModel(mainWindow->getDataModel());

    bool accept = dropAcceptable(event);
    mainWindow->statusBar()->clearMessage();
    if (!accept) {
        return;
    }

    if (plugin->usesVariables() == Plugin::UsesVariables::ProcessOnly) {
        auto *ds = dataModel->getDataSource(event->mimeData()->urls().at(0));
        plugin->setDataSource(widget, privateData, ds);
        legend.update();
        event->accept();
        return;
    }

    int capacity = plugin->getSlotCapacity();
    int numUrls = event->mimeData()->urls().size();
    int freeSlots = plugin->getFreeSlots(slotModel);

    if (freeSlots == 0 and numUrls == capacity) {
        // replace
        auto urls(slotModel.getUrls()); // save URLs for notification below
        plugin->clearVariables(widget, privateData);
        slotModel.clear();
        dataModel->notifyUnsubscribed(urls);
    }

    foreach (QUrl url, event->mimeData()->urls()) {
        DataNode *dataNode(dataModel->findDataNode(url));
        if (!dataNode) {
            mainWindow->statusBar()->showMessage(tr("Url not found: %1")
                    .arg(url.toString()), 2000);
            continue;
        }

        plugin->appendVariable(widget, privateData, &slotModel, dataNode);
        // TODO status bar message on failure
    }

    legend.update();
    event->accept();
}

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

void WidgetContainer::moveEvent(QMoveEvent *)
{
    legend.move(pos() + legendOffset);
}

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

void WidgetContainer::resizeEvent(QResizeEvent *event)
{
    handleWidget->resize(event->size());
    widget->resize(event->size());

    QSize labelSize(16, 16);
    QRect rect(
            contentsRect().bottomLeft() + QPoint(5, - labelSize.height()),
            labelSize);
    scriptLabel.setGeometry(rect);
}

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

void WidgetContainer::updateHandleWidget()
{
    if (tabPage->getMainWindow()->getEditMode() || dragging) {
        handleWidget->show();
    }
    else {
        handleWidget->hide();
    }
}

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

void WidgetContainer::updateScriptLabel()
{
    bool noScript(script.isEmpty() and scriptVariables.isEmpty());

    if (noScript) {
        scriptLabel.hide();
        return;
    }

    if (scriptWarning) {
        scriptLabel.setPixmap(QPixmap(":/images/python-warning.svg"));
        scriptLabel.show();
        return;
    }

    if (tabPage->getMainWindow()->getEditMode()) {
        scriptLabel.setPixmap(QPixmap(":/images/python.svg"));
        scriptLabel.show();
    }
    else {
        scriptLabel.hide();
    }
}

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

bool WidgetContainer::dropAcceptable(const QDropEvent *event) const
{
    MainWindow *mainWindow(tabPage->getMainWindow());

    if (!event->mimeData()->hasUrls()) {
        mainWindow->statusBar()->showMessage(
                tr("Process variables required."));
        return false;
    }

    if (plugin->usesVariables() == Plugin::UsesVariables::No) {
        mainWindow->statusBar()->showMessage(
                tr("No variables allowed for this widget"));
        return false;
    } else if (plugin->usesVariables() == Plugin::UsesVariables::ProcessOnly) {
        if (event->mimeData()->urls().size() != 1 or
                    !event->mimeData()->urls()[0].path().isEmpty()) {
            mainWindow->statusBar()->showMessage(
                    tr("Exactly one data source must be selected"));
            return false;
        }
        return true;
    } else {
        for (const auto& url : event->mimeData()->urls()) {
            if (url.path().isEmpty() || url.path() == "/") {
                mainWindow->statusBar()->showMessage(
                    tr("Only variables allowed for this widget"));
                return false;
            }
        }
    }

    int capacity = plugin->getSlotCapacity();
    if (capacity == -1) {
        return true; // unlimited
    }

    int numUrls = event->mimeData()->urls().size();
    int freeSlots = plugin->getFreeSlots(slotModel);

    if (freeSlots == 0 and numUrls == capacity) {
        // replace
        if (numUrls > 1) {
            mainWindow->statusBar()->showMessage(
                    tr("Replace %Ln process variables.", "", numUrls));
        }
        else {
            mainWindow->statusBar()->showMessage(tr("Replace %1.")
                    .arg(event->mimeData()->urls().front().toString()));
        }
        return true;
    }

    if (numUrls > freeSlots) {
        mainWindow->statusBar()->showMessage(
                tr("%Ln slots required and %1 free", "", numUrls)
                .arg(freeSlots));
        return false;
    }

    if (numUrls > 1) {
        mainWindow->statusBar()->showMessage(
                tr("Connect %Ln process variables.", "", numUrls));
    }
    else {
        mainWindow->statusBar()->showMessage(tr("Connect %1.")
                .arg(event->mimeData()->urls().front().toString()));
    }

    return true;
}

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

void WidgetContainer::updateLegend()
{
    legend.move(pos() + legendOffset);
    legend.setVisible(showLegend);
}

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

void WidgetContainer::actionDelete()
{
    MainWindow *mainWindow(tabPage->getMainWindow());
    foreach (WidgetContainer *container,
            mainWindow->containers(MainWindow::Selected)) {
        mainWindow->getPropertyModel()->removeContainer(container);
        container->deleteLater();
    }
}

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

void WidgetContainer::actionDisconnect()
{
    MainWindow *mainWindow(tabPage->getMainWindow());
    foreach (WidgetContainer *container,
            mainWindow->containers(MainWindow::Selected)) {
        auto urls(container->slotModel.getUrls()); // save URLs for later
        container->plugin->clearVariables(container->widget, privateData);
        container->slotModel.clear();
        mainWindow->getDataModel()->notifyUnsubscribed(urls);
        container->legend.update();
    }
}

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

void WidgetContainer::actionAlignLeft()
{
    foreach (WidgetContainer *container,
            tabPage->getMainWindow()->containers(MainWindow::Selected)) {
        container->move(x(), container->y());
    }
    tabPage->updateMinimumSize();
}

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

void WidgetContainer::actionAlignCenter()
{
    int center = x() + width() / 2;
    foreach (WidgetContainer *container,
            tabPage->getMainWindow()->containers(MainWindow::Selected)) {
        if (container->width()) {
            container->move(center - container->width() / 2, container->y());
        }
    }
    tabPage->updateMinimumSize();
}

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

void WidgetContainer::actionAlignRight()
{
    foreach (WidgetContainer *container,
            tabPage->getMainWindow()->containers(MainWindow::Selected)) {
        container->move(x() + width() - container->width(), container->y());
    }
    tabPage->updateMinimumSize();
}

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

void WidgetContainer::actionSendToBack()
{
    foreach (WidgetContainer *container,
            tabPage->getMainWindow()->containers(MainWindow::Selected)) {
        container->lower();
    }
}

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

void WidgetContainer::actionBringToFront()
{
    foreach (WidgetContainer *container,
            tabPage->getMainWindow()->containers(MainWindow::Selected)) {
        container->raise();
    }
}

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

void WidgetContainer::actionSlots()
{
    auto dataModel(tabPage->getMainWindow()->getDataModel());
    auto urls(slotModel.getUrls()); // save URLs for notification below

    SlotDialog dialog(tabPage->getMainWindow(), &slotModel, plugin, this);

    dialog.exec();

    // FIXME update variable connections instead of re-connecting all

    plugin->clearVariables(widget, privateData);
    dataModel->notifyUnsubscribed(urls);

    plugin->connectDataSlots(widget, privateData, &slotModel, dataModel);
    legend.update();
}

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

void WidgetContainer::actionStylesheet()
{
    StyleDialog dialog(widget);
    if (dialog.exec() == QDialog::Accepted) {
        foreach (WidgetContainer *container,
                tabPage->getMainWindow()->containers(MainWindow::Selected)) {
            container->setProperty("styleSheet", widget->styleSheet());
        }
    }
}

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

void WidgetContainer::actionJump()
{
    QAction *action = dynamic_cast<QAction *>(sender());
    if (!action) {
        return;
    }

    MainWindow *mainWindow(tabPage->getMainWindow());
    QUrl url(action->data().toUrl());
    QModelIndex index(mainWindow->getDataModel()->indexFromUrl(url));
    mainWindow->sourceTree->selectionModel()->setCurrentIndex(index,
            QItemSelectionModel::SelectCurrent);
}

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

void WidgetContainer::actionScript()
{
    QAction *action = dynamic_cast<QAction *>(sender());
    if (!action) {
        return;
    }

    MainWindow *mainWindow(tabPage->getMainWindow());
    mainWindow->tableViewScriptVariables->setModel(&scriptVariables);
    mainWindow->dockWidgetScript->setVisible(true);
}

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

void WidgetContainer::actionLegend()
{
    QAction *action = dynamic_cast<QAction *>(sender());
    if (!action) {
        return;
    }

    foreach (WidgetContainer *container,
            tabPage->getMainWindow()->containers(MainWindow::Selected)) {
        container->setLegend(action->isChecked());
    }
}

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

void WidgetContainer::scriptChanged()
{
    MainWindow *mainWindow(tabPage->getMainWindow());
    script = mainWindow->textEditScript->toPlainText().trimmed();
    updateScript();
}

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