/*****************************************************************************
 *
 * 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> // must be first!

#include "MainWindow.h"
#include "ConnectDialog.h"
#include "ReplaceDialog.h"
#include "DataModel.h"
#include "DataSource.h"
#include "SourceDelegate.h"
#include "PropertyModel.h"
#include "PropertyDelegate.h"
#include "Property.h"
#include "TabDialog.h"
#include "StyleDialog.h"
#include "AboutDialog.h"
#include "MessageDialog.h"
#include "WidgetContainer.h"

// manually load plugins
#include "plugins/BarPlugin.h"
#include "plugins/CheckBoxPlugin.h"
#include "plugins/DialPlugin.h"
#include "plugins/DigitalPlugin.h"
#include "plugins/DoubleSpinBoxPlugin.h"
#include "plugins/GraphPlugin.h"
#include "plugins/LedPlugin.h"
#include "plugins/PushButtonPlugin.h"
#include "plugins/QLabelPlugin.h"
#include "plugins/RadioButtonPlugin.h"
#include "plugins/SvgPlugin.h"
#include "plugins/TextPlugin.h"
#include "plugins/TouchEditPlugin.h"
#include "plugins/XYGraphPlugin.h"

#include "TabPage.h"

#include <QDebug>
#include <QElapsedTimer>
#include <QFileDialog>
#include <QJsonDocument>
#include <QJsonArray>
#include <QKeyEvent>
#include <QSettings>
#include <QClipboard>
#include <QMimeData>
#include <QScrollArea>

#include <stdexcept>

#include <sys/types.h> // socketpair()
#include <sys/socket.h>
#include <signal.h> // SIGINT, SIGTERM

#ifdef TM_MODEL_TEST
#include "modeltest.h"
#endif

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

int multiply(int a, int b)
{
    return a * b;
}

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

MainWindow *MainWindow::singleton = NULL;

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

static QColor color[] = {
    QColor( 38, 139, 210), // blue
    QColor(220,  50,  47), // red
    QColor(133, 153,   0), // green
    QColor(181, 137,   0), // yellow
    QColor(211,  54, 130)  // magenta
};

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

MainWindow::MainWindow(
        const QString &fileName,
        bool newView,
        bool autoConnect,
        QWidget *parent):
    QMainWindow(parent),
    dataModel(new DataModel(this)),
    sourceDelegate(new SourceDelegate(dataModel)),
    propertyDelegate(new PropertyDelegate()),
    editMode(false),
    gridStep(10),
    restore(false),
    propertyNode(NULL),
    pythonStdOutNotifier(NULL),
    pythonStdErrNotifier(NULL),
    autoConnect(autoConnect),
    pythonHistoryIndex(0)
{
    if (singleton) {
        throw(std::runtime_error("Only one instance allowed!"));
    }
    singleton = this;

    setupUi(this);

    setWindowIcon(QPixmap(":/images/testmanager.svg"));

#ifdef TM_MODEL_TEST
    qDebug() << "DataModel test enabled.";
    new ModelTest(dataModel, this);
#endif
    sourceTree->setModel(dataModel);
    sourceTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
    sourceTree->header()->resizeSection(1, 100);
    sourceTree->setItemDelegateForColumn(0, sourceDelegate);
    sourceTree->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(sourceTree, SIGNAL(customContextMenuRequested(const QPoint &)),
            this, SLOT(sourceTreeCustomContextMenu(const QPoint &)));
    connect(dataModel, SIGNAL(connectionEstablished()),
            this, SLOT(connectDataSlots()));
    connect(dataModel,
            SIGNAL(connectionStateChanged(DataModel::ConnectionState)),
            this, SLOT(connectionStateChanged(DataModel::ConnectionState)));
    connect(dataModel, SIGNAL(statsChanged()),
            this, SLOT(statsChanged()));
    connectionStateChanged(DataModel::NoSources);

    propertyModel = new PropertyModel();
#ifdef TM_MODEL_TEST
    qDebug() << "PropertyModel test enabled.";
    new ModelTest(propertyModel, this);
#endif
    propertyTree->setModel(propertyModel);
    propertyTree->header()->resizeSection(0, 192);
    propertyTree->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(propertyTree, SIGNAL(customContextMenuRequested(const QPoint &)),
            this, SLOT(propertyTreeCustomContextMenu(const QPoint &)));

    connect(propertyModel,
            SIGNAL(rowsInserted(const QModelIndex &, int, int)),
            this, SLOT(expandProperties()));

    propertyTree->setItemDelegateForColumn(1, propertyDelegate);
    propertyTree->updateBrightness();

    tableViewScriptVariables->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(tableViewScriptVariables,
            SIGNAL(customContextMenuRequested(const QPoint &)),
            this, SLOT(scriptVariablesCustomContextMenu(const QPoint &)));

    // fill status bar
    labelIn = new QLabel(this);
    labelIn->setAlignment(Qt::AlignRight);
    statusBar()->addWidget(labelIn);
    labelOut = new QLabel(this);
    labelOut->setAlignment(Qt::AlignRight);
    statusBar()->addWidget(labelOut);
    QFontMetrics fm(labelIn->font());
    auto num = QLocale().toString(10000.0, 'f', 1);
    auto minSize(fm.boundingRect(tr("%1 KB/s").arg(num)).size());
    labelIn->setMinimumSize(minSize);
    labelOut->setMinimumSize(minSize);
    statusMessageIcon = new QLabel(this);
    statusMessageIcon->setMinimumSize(QSize(16, 16));
    statusBar()->addWidget(statusMessageIcon);
    statusMessageText = new QLabel(this);
    statusBar()->addWidget(statusMessageText, 2);

    initPython();

    WidgetContainer::registerPythonType();
    ScriptVariable::registerPythonType();

    // fill list with available widgets
    Plugin *plugin;
    plugin = new BarPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new CheckBoxPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new RadioButtonPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new DialPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new DigitalPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new DoubleSpinBoxPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new GraphPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new LedPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new PushButtonPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new QLabelPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new SvgPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new TextPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new TouchEditPlugin();
    pluginMap.insert(plugin->type(), plugin);
    plugin = new XYGraphPlugin();
    pluginMap.insert(plugin->type(), plugin);

    on_actionNew_triggered();

    updateWindowTitle();

#if QT_VERSION >= 0x050600
    QList<QDockWidget *> docks;
    docks
        << dockWidgetSources
        << dockWidgetProperties;
    QList<int> sizes;
    sizes << 256 << 350;
    resizeDocks(docks, sizes, Qt::Horizontal);
#endif

    QSettings settings;
    restore = settings.value("restore", restore).toBool();
    recentFiles = settings.value("recentFiles").toStringList();
    restoreGeometry(settings.value("geometry").toByteArray());
    restoreState(settings.value("windowState").toByteArray());

    for (int i = 0; i < MaxRecentFiles; ++i) {
        recentFileActions[i] = new QAction(this);
        recentFileActions[i]->setVisible(false);
        connect(recentFileActions[i], SIGNAL(triggered()),
                this, SLOT(openRecentFile()));
        menuRecentFiles->addAction(recentFileActions[i]);
    }

    updateRecentFileActions();

    menuWindows->addAction(dockWidgetSources->toggleViewAction());
    menuWindows->addAction(dockWidgetProperties->toggleViewAction());
    menuWindows->addAction(dockWidgetShell->toggleViewAction());
    menuWindows->addAction(dockWidgetScript->toggleViewAction());
    menuWindows->addAction(toolBar->toggleViewAction());

#if 0
    pythonBenchmark();
#endif

    QString fileToLoad;

    if (newView) {
        // pass
    }
    else if (!fileName.isEmpty()) {
        fileToLoad = fileName;
    }
    else if (restore && recentFiles.size() > 0) {
        fileToLoad = recentFiles.front();
    }

    if (!fileToLoad.isEmpty()) {
        loadLayout(fileToLoad);

        if (autoConnect) {
            dataModel->connectAll();
        }
    }

    updateEditMode();

    connect(tabWidget->tabBar(), SIGNAL(tabCloseRequested(int)),
                this, SLOT(tabCloseRequested(int)));

    QClipboard *clipboard(QApplication::clipboard());
    connect(clipboard, SIGNAL(changed(QClipboard::Mode)),
            this, SLOT(clipboardChanged(QClipboard::Mode)));

    QFont f(textEditScript->font());
    f.setStyleHint(QFont::Monospace);
    textEditScript->setFont(f);
}

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

MainWindow::~MainWindow()
{
    clearTabs();

    sourceTree->setModel(nullptr);
    delete dataModel;
    delete propertyModel;

    delete propertyDelegate;
    delete sourceDelegate;

    auto map = std::move(pluginMap);
    for (const auto& plugin : map)
        delete plugin;

    Py_Finalize();
}

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

PyObject *MainWindow::pythonMethodTabCount(PyObject *, PyObject *args)
{
    if (!PyArg_ParseTuple(args, ":tabCount")) {
        return NULL;
    }
    return PyLong_FromLong(singleton->tabWidget->count());
}

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

PyObject *MainWindow::pythonMethodMaximize(PyObject *, PyObject *args)
{
    bool max(true);

    if (!PyArg_ParseTuple(args, "|b:maximize", &max)) {
        return NULL;
    }
    Qt::WindowStates states(singleton->windowState());
    if (max) {
        states |= Qt::WindowMaximized;
    }
    else {
        states &= ~Qt::WindowMaximized;
    }
    singleton->setWindowState(states);
    return Py_None;
}

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

QSet<WidgetContainer *> MainWindow::containers(Filter filter) const
{
    auto all(tabWidget->findChildren<WidgetContainer *>().toSet());

    if (filter == Selected) {
        QSet<WidgetContainer *> selectedContainers;
        foreach (WidgetContainer *container, all) {
            if (container->isSelected()) {
                selectedContainers.insert(container);
            }
        }
        return selectedContainers;
    }
    else {
        return all;
    }
}

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

QSet<WidgetContainer *> MainWindow::containers(const QUrl &url) const
{
    QSet<QUrl> urls;
    urls << url;
    return containers(urls);
}

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

QSet<WidgetContainer *> MainWindow::containers(const QSet<QUrl> &urls) const
{
    auto all(tabWidget->findChildren<WidgetContainer *>().toSet());

    QSet<WidgetContainer *> ret;
    foreach (WidgetContainer *container, all) {
        foreach (const QUrl &url, urls) {
            if (container->uses(url)) {
                ret.insert(container);
            }
        }
    }
    return ret;
}

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

void MainWindow::deselectAll() const
{
    foreach (WidgetContainer *container, containers(MainWindow::All)) {
        container->deselect();
    }
}

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

void MainWindow::selectionChanged()
{
    auto selected = propertyModel->getContainers();
    actionCopy->setEnabled(selected.size() > 0);
}

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

QJsonArray MainWindow::checkPaste()
{
    QClipboard *clipboard = QGuiApplication::clipboard();
    const QMimeData *mimeData(clipboard->mimeData(QClipboard::Clipboard));

    if (not mimeData->hasText()) {
        return QJsonArray();
    }

    QString jsonStr(mimeData->text());
    QByteArray ba = jsonStr.toUtf8();

    QJsonParseError err;
    QJsonDocument doc(QJsonDocument::fromJson(ba, &err));
    if (err.error != QJsonParseError::NoError) {
        return QJsonArray();
    }

    return doc.array();
}

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

void MainWindow::signalRaised(int signum)
{
    switch (signum) {
        case SIGINT:
        case SIGTERM:
            qDebug() << "Closing application.";
            close();
            break;
    }
}

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

void MainWindow::clearTabs()
{
    for (int i = tabWidget->count(); i > 0; i--) {
        delete tabWidget->widget(i - 1);
        tabWidget->removeTab(i - 1);
    }
}

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

static PyMethodDef pythonModuleMethods[] = {
    {
        "tabCount", // ml_name
        MainWindow::pythonMethodTabCount, // ml_meth
        METH_VARARGS, // ml_flags
        "Return the number of tab pages." // ml_doc
    },
    {
        "maximize", // ml_name
        MainWindow::pythonMethodMaximize, // ml_meth
        METH_VARARGS, // ml_flags
        "Maximize the window." // ml_doc
    },
    {NULL, NULL, 0, NULL}
};

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

static PyModuleDef pythonModuleDef = {
    PyModuleDef_HEAD_INIT, // m_base
    "tm", // m_name
    NULL, // m_doc
    -1, // m_size: global state
    pythonModuleMethods, // methods
    NULL, // slots
    NULL, // m_traverse
    NULL, // m_clear
    NULL // m_free
};

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

static PyObject *initPythonModule(void)
{
    return PyModule_Create(&pythonModuleDef);
}

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

void MainWindow::initPython()
{
    // argument must be a wide-character string with static storage
    // older Pythons take the version without const, so avoid a warning
    Py_SetProgramName((wchar_t *) L"testmanager");

    PyImport_AppendInittab("tm", &initPythonModule);

    Py_Initialize();

    PyObject *sys = PyImport_ImportModule("sys");
    if (!sys) {
        qFatal("Failed to import sys module.");
        return;
    }

    // ----- redirect stdout -----

    if (::socketpair(AF_UNIX, SOCK_STREAM, 0, pythonStdOut)) {
        qWarning() << "Couldn't create stdout socket pair:"
            << strerror(errno);
        return;
    }

    pythonStdOutNotifier =
        new QSocketNotifier(pythonStdOut[1], QSocketNotifier::Read, this);
    connect(pythonStdOutNotifier, SIGNAL(activated(int)),
            this, SLOT(pythonStdOutDataAvailable(int)));

    PyObject *out = PyFile_FromFd(pythonStdOut[0], NULL, "w", 1, NULL, NULL,
            NULL, 1);
    if (!out) {
        qFatal("Failed to create python file from stdout socket.");
        return;
    }

    if (PyObject_SetAttrString(sys, "stdout", out)) {
        qFatal("Failed to replace python stdout.");
        return;
    }

    // ----- redirect stderr -----

    if (::socketpair(AF_UNIX, SOCK_STREAM, 0, pythonStdErr)) {
        qWarning() << "Couldn't create stderr socket pair:"
            << strerror(errno);
        return;
    }

    pythonStdErrNotifier =
        new QSocketNotifier(pythonStdErr[1], QSocketNotifier::Read, this);
    connect(pythonStdErrNotifier, SIGNAL(activated(int)),
            this, SLOT(pythonStdErrDataAvailable(int)));

    PyObject *err = PyFile_FromFd(pythonStdErr[0], NULL, "w", 1, NULL, NULL,
            NULL, 1);
    if (!err) {
        qFatal("Failed to create python file from stderr socket.");
        return;
    }

    if (PyObject_SetAttrString(sys, "stderr", err)) {
        qFatal("Failed to replace python stderr.");
        return;
    }

    QString banner(QString("Python %1 on %2\n")
            .arg(Py_GetVersion())
            .arg(Py_GetPlatform()));
    textEditPython->setTextColor(color[0]);
    textEditPython->append(banner);

#if 0
    ::write(pythonStdOut[0], "stdout\n", 7);
    ::write(pythonStdErr[0], "stderr\n", 7);
#endif
}

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

void MainWindow::loadLayout(const QString &path)
{
    QFile file(path);
    if (!file.open(QIODevice::ReadOnly)) {
        QString msg(tr("Failed to open %1.").arg(path));
        statusBar()->showMessage(msg, 5000);
        qWarning() << msg;
        return;
    }

    QByteArray ba = file.readAll();
    file.close();

    QJsonParseError err;
    QJsonDocument doc(QJsonDocument::fromJson(ba, &err));

    if (err.error != QJsonParseError::NoError) {
        qCritical() << "Layout parsing error (" << err.error
            << ") at offset " << err.offset << ": "
            << err.errorString();
        return;
    }

    filePath = path;
    QFileInfo fi(filePath);
    QString searchPath(fi.absoluteDir().path());
    QDir::setSearchPaths("layout", QStringList(searchPath));

    QJsonObject layoutObject(doc.object());
    QJsonArray tabArray(layoutObject["tabs"].toArray());

    clearTabs();

    foreach (QJsonValue tabValue, tabArray) {
        QJsonObject tabObject(tabValue.toObject());
        QString label(tabObject["name"].toString());
        appendTab(label, tabObject);
    }

    if (layoutObject.contains("styleSheet")) {
        tabWidget->setStyleSheet(layoutObject["styleSheet"].toString());
    }
    else {
        tabWidget->setStyleSheet(QString());
    }

    QJsonArray dataSourceArray(layoutObject["dataSources"].toArray());
    dataModel->read(dataSourceArray);

    updateWindowTitle();
    addRecentFile(path);

    connectDataSlots();
}

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

void MainWindow::saveLayout()
{
    QJsonArray tabArray;

    for (int i = 0; i < tabWidget->count(); i++) {
        QScrollArea *scrollArea =
            static_cast<QScrollArea *>(tabWidget->widget(i));
        TabPage *tab = static_cast<TabPage *>(scrollArea->widget());
        QJsonObject tabObject;
        tabObject["name"] = tabWidget->tabText(i);
        tab->write(tabObject);
        tabArray.append(tabObject);
    }

    QJsonObject layoutObject;
    layoutObject["version"] = 1;
    layoutObject["tabs"] = tabArray;

    if (!tabWidget->styleSheet().isEmpty()) {
        layoutObject["styleSheet"] = tabWidget->styleSheet();
    }

    QJsonArray dataSourceArray;
    dataModel->write(dataSourceArray);
    if (!dataSourceArray.isEmpty()) {
        layoutObject["dataSources"] = dataSourceArray;
    }

    QJsonDocument saveDoc(layoutObject);

    QFile saveFile(filePath);
    if (!saveFile.open(QIODevice::WriteOnly)) {
        statusBar()->showMessage(tr("Failed to open %1")
                .arg(filePath), 2000);
        return;
    }

    saveFile.write(saveDoc.toJson());
    addRecentFile(filePath);
}

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

void MainWindow::updateWindowTitle()
{
    if (filePath.isEmpty()) {
        setWindowFilePath("");
        setWindowTitle(QCoreApplication::applicationName());
    }
    else {
        setWindowTitle("");
        setWindowFilePath(filePath);
    }
}

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

void MainWindow::pythonBenchmark()
{
    // add local directory to python path
    PyObject *sysPath = PySys_GetObject("path");
    PyObject *dirString = PyUnicode_FromString(".");
    PyList_Append(sysPath, dirString);

    PyObject *module = PyImport_ImportModule("mult");
    if (!module) {
        PyErr_Print();
        qWarning() << "Failed to import module.";
        return;
    }

    PyObject *func = PyObject_GetAttrString(module, "multiply");
    if (!func || !PyCallable_Check(func)) {
        if (PyErr_Occurred()) {
            PyErr_Print();
        }
        qWarning() << "Failed to find func.";
        Py_DECREF(module);
        return;
    }

    PyObject *args = PyTuple_New(2);
    for (int i = 0; i < 2; ++i) {
        PyObject *value = PyLong_FromLong(i + 3);
        if (!value) {
            Py_DECREF(args);
            Py_DECREF(func);
            Py_DECREF(module);
            qWarning() << "Cannot convert argument.";
            return;
        }
        /* pValue reference
         * stolen here: */
        PyTuple_SetItem(args, i, value);
        Py_DECREF(value);
    }


    PyObject *call = NULL;

    QElapsedTimer timer;
    timer.start();

    for (int i = 0; i < 1000000; i++) {
        if (call) {
            Py_DECREF(call);
        }
        call = PyObject_CallObject(func, args);
    }

    qDebug() << "The slow operation took"
        << timer.elapsed() << "milliseconds";

    Py_DECREF(args);

    if (call) {
        qDebug()  << "Result of call:" << PyLong_AsLong(call);
        Py_DECREF(call);
    }
    else {
        Py_DECREF(func);
        Py_DECREF(module);
        PyErr_Print();
        qWarning() << "Call failed";
        return;
    }

    Py_DECREF(func);
    Py_DECREF(module);

    timer.start();

    int (*ref)(int, int) = &multiply;

    for (int i = 0; i < 1000000; i++) {
        int res = ref(3, 4);
        res = res;
    }

    qDebug() << "The fast operation took"
        << timer.elapsed() << "milliseconds";
}

/********************** HACK: QTBUG-16507 workaround ************************/

void MainWindow::showEvent(QShowEvent *event)
{
    QMainWindow::showEvent(event);
    QString filePath = windowFilePath();
    if (!filePath.isEmpty()) {
        setWindowFilePath(filePath + "x");
        setWindowFilePath(filePath);
    }
}

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

void MainWindow::closeEvent(QCloseEvent *event)
{
    QSettings settings;

    settings.setValue("restore", restore);
    settings.setValue("recentFiles", recentFiles);
    settings.setValue("geometry", saveGeometry());
    settings.setValue("windowState", saveState());

    event->accept();
}

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

void MainWindow::addRecentFile(const QString &path)
{
    QDir cur = QDir::currentPath();
    QString absPath = cur.absoluteFilePath(path);
    absPath = QDir::cleanPath(absPath);
    recentFiles.removeAll(absPath);
    recentFiles.prepend(absPath);
    updateRecentFileActions();
}

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

void MainWindow::updateRecentFileActions()
{
    int numRecentFiles = qMin(recentFiles.size(), (int) MaxRecentFiles);

    for (int i = 0; i < numRecentFiles; ++i) {
        QString text = QString("&%1 %2").arg(i + 1).arg(recentFiles[i]);
        recentFileActions[i]->setText(text);
        recentFileActions[i]->setData(recentFiles[i]);
        recentFileActions[i]->setVisible(true);
    }

    for (int j = numRecentFiles; j < MaxRecentFiles; ++j) {
        recentFileActions[j]->setVisible(false);
    }

    menuRecentFiles->setEnabled(numRecentFiles > 0);
}

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

void MainWindow::expandChildren(
        const QModelIndex &index,
        QTreeView *view,
        int depth
        )
{
    if (!depth or !view->model()) {
        return;
    }

    if (!view->isExpanded(index)) {
        view->expand(index);
    }

    if (depth > 0) {
        depth--;
    }

    int rowCount = 0;

    rowCount = view->model()->rowCount(index);

    for (int i = 0; i < rowCount; i++) {
        const QModelIndex &child = view->model()->index(i, 0, index);
        expandChildren(child, view, depth);
    }
}

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

void MainWindow::updateEditMode()
{
    dockWidgetProperties->setVisible(editMode);
    tabWidget->setTabsClosable(editMode);
    tabWidget->setMovable(editMode);
    actionAddTab->setEnabled(editMode);

    foreach (TabPage *tab, tabWidget->findChildren<TabPage *>()) {
        tab->editModeChanged();
    }

    updatePaste();
}

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

void MainWindow::updatePaste()
{
    QJsonArray array = checkPaste();
    actionPaste->setEnabled(array.size() > 0 && editMode &&
            tabWidget->currentIndex() != -1);
    actionPaste->setText(tr("Paste %Ln widgets", "", array.size()));
}

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

void MainWindow::replaceUrl(const QUrl &oldUrl, const QUrl &newUrl)
{
    foreach (TabPage *tab, tabWidget->findChildren<TabPage *>()) {
        tab->replaceUrl(oldUrl, newUrl);
    }
}

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

void MainWindow::appendTab(const QString &label, const QJsonObject &config)
{
    QScrollArea *scrollArea(new QScrollArea(this));
    TabPage *tab = new TabPage(this, scrollArea);
    scrollArea->setWidget(tab);
    scrollArea->setWidgetResizable(true);
    tab->read(config);
    tabWidget->addTab(scrollArea, label);
}

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

QSet<WidgetContainer *> MainWindow::usingSelected() const
{
    QSet<QUrl> urls;

    foreach (QModelIndex index,
            sourceTree->selectionModel()->selectedIndexes()) {
        auto dataNode(dataModel->getDataNode(index));
        if (dataNode) {
            urls.insert(dataNode->nodeUrl());
        }
    }

    return containers(urls);
}

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

void MainWindow::on_actionNew_triggered()
{
    filePath = QString();
    QDir::setSearchPaths("layout", QStringList());
    updateWindowTitle();

    clearTabs();
    tabWidget->setStyleSheet(QString());

    on_actionAddTab_triggered();
}

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

void MainWindow::on_actionLoad_triggered()
{
    QFileDialog dialog(this);
    dialog.setAcceptMode(QFileDialog::AcceptOpen);

    QStringList filters;
    filters
        << tr("Testmanager Layouts (*.tml)")
        << tr("Any files (*)");

    dialog.setNameFilters(filters);
    dialog.setDefaultSuffix("tml");

    int ret = dialog.exec();

    if (ret != QDialog::Accepted) {
        return;
    }

    QString path = dialog.selectedFiles()[0];
    loadLayout(path);
}

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

void MainWindow::openRecentFile()
{
    QAction *action = qobject_cast<QAction *>(sender());
    if (!action) {
        return;
    }

    QString path = action->data().toString();
    loadLayout(path);
}

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

void MainWindow::on_actionSave_triggered()
{
    if (filePath.isEmpty()) {
        on_actionSaveAs_triggered();
        return;
    }

    saveLayout();
}

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

void MainWindow::on_actionSaveAs_triggered()
{
    QFileDialog dialog(this);
    dialog.setAcceptMode(QFileDialog::AcceptSave);

    QStringList filters;
    filters
        << tr("Testmanager Layouts (*.tml)")
        << tr("Any files (*)");

    dialog.setNameFilters(filters);
    dialog.setDefaultSuffix("tml");

    int ret = dialog.exec();

    if (ret != QDialog::Accepted) {
        return;
    }

    filePath = dialog.selectedFiles()[0];
    QFileInfo fi(filePath);
    QString searchPath(fi.absoluteDir().path());
    QDir::setSearchPaths("layout", QStringList(searchPath));
    updateWindowTitle();

    saveLayout();
}

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

void MainWindow::on_actionClose_triggered()
{
    close();
}

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

void MainWindow::on_actionCopy_triggered()
{
    auto selected(propertyModel->getContainers());
    QJsonArray array;
    foreach (WidgetContainer *container, selected) {
        QJsonObject obj;
        container->write(obj);
        array.append(obj);
    }

    QJsonDocument doc(array);
    QByteArray json = doc.toJson(QJsonDocument::Indented);
    QString jsonStr = QString::fromUtf8(json);
    QClipboard *clipboard = QGuiApplication::clipboard();
    clipboard->setText(jsonStr);
}

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

void MainWindow::clipboardChanged(QClipboard::Mode mode)
{
    if (mode != QClipboard::Clipboard) {
        return;
    }

    updatePaste();
}

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

void MainWindow::on_actionPaste_triggered()
{
    QJsonArray array = checkPaste();
    if (array.size() == 0) {
        return;
    }

    int tabIdx = tabWidget->currentIndex();
    if (tabIdx == -1) {
        return;
    }

    QScrollArea *scrollArea =
        static_cast<QScrollArea *>(tabWidget->widget(tabIdx));
    TabPage *tab = static_cast<TabPage *>(scrollArea->widget());

    deselectAll();

    tab->addContainerArray(array, true /* select */);

    if (not editMode) {
        editMode = true;
        updateEditMode();
    }
}

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

void MainWindow::on_actionConnectAll_triggered()
{
    dataModel->connectAll();
}

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

void MainWindow::on_actionConnect_triggered()
{
    ConnectDialog dialog(dataModel);

    if (dialog.exec() != QDialog::Accepted) {
        return;
    }

    DataSource *source = dialog.adoptSource();
    dataModel->append(source);
    connectDataSlots();
}

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

void MainWindow::on_actionEditMode_toggled(bool checked)
{
    editMode = checked;
    updateEditMode();
}

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

void MainWindow::on_actionGlobalStyleSheet_triggered()
{
    StyleDialog dialog(tabWidget);
    dialog.exec();
}

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

void MainWindow::on_actionAddTab_triggered()
{
    appendTab(tr("New Tab"));
}

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

void MainWindow::on_actionAboutTestManager_triggered()
{
    AboutDialog dialog(this);
    dialog.exec();
}

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

void MainWindow::on_tabWidget_tabBarDoubleClicked(int index)
{
    if (!editMode) {
        return;
    }

    TabDialog dialog(tabWidget, index, this);
    dialog.exec();
}

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

void MainWindow::on_lineEditFilter_textChanged(const QString &text)
{
    if (text.isEmpty()) {
        dataModel->filter(QRegExp());
        return;
    }

    QRegExp re(text, Qt::CaseInsensitive);

    if (re.isValid()) {
        lineEditFilter->setStyleSheet(QString());
        lineEditFilter->setToolTip(QString());
        dataModel->filter(re);
    }
    else {
        lineEditFilter->setStyleSheet("color: red;");
        lineEditFilter->setToolTip(re.errorString());
    }
}

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

void MainWindow::on_toolButtonClearFilter_clicked()
{
    lineEditFilter->setText(QString());
}

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

void MainWindow::sourceTreeCustomContextMenu(const QPoint &point)
{
    QMenu *menu = new QMenu;
    dataIndex = sourceTree->indexAt(point);
    int rows(dataModel->rowCount(dataIndex));
    DataSource *dataSource(dataModel->getDataSource(dataIndex));

    QAction *action;
    if (dataSource) {
        action = menu->addAction(tr("Connect"),
                this, SLOT(connectDataSource()));
        action->setEnabled(!dataSource->isConnected());
        action->setIcon(QIcon(":/images/network-transmit-receive.svg"));

        action = menu->addAction(tr("Disconnect"),
                this, SLOT(disconnectDataSource()));
        action->setIcon(QIcon(":/images/network-offline.svg"));
        action->setEnabled(dataSource->isConnected());

        action = menu->addAction(tr("Remove"),
                this, SLOT(removeDataSource()));
        action->setIcon(QIcon(":/images/list-remove.svg"));

        action = menu->addAction(tr("Replace..."),
                this, SLOT(replaceDataSource()));
        action->setIcon(QIcon(":/images/replace-datasource.svg"));

        menu->addSeparator(); // -------------------------------

        action = menu->addAction(tr("Show messages..."),
                this, SLOT(dataSourceShowMessages()));
        action->setEnabled(dataSource->hasMessages());
        action->setIcon(QIcon(":/images/messages.svg"));

        action = menu->addAction(tr("Configure messages..."),
                this, SLOT(dataSourceConfigMessages()));
        action->setIcon(QIcon(":/images/messages.svg"));

        menu->addSeparator(); // -------------------------------
    }

    action = menu->addAction(tr("Add datasource..."),
            this, SLOT(on_actionConnect_triggered()));
    action->setIcon(QIcon(":/images/list-add.svg"));

    if (dataIndex.isValid() and not dataSource) {
        menu->addSeparator(); // -------------------------------
        action = menu->addAction(tr("Copy variable URL"),
                this, SLOT(copyVariableUrl()));
        action->setIcon(QIcon(":/images/edit-copy.svg"));

        auto containers(usingSelected());
        action = menu->addAction(tr("Select %Ln subscriber(s)", "",
                    containers.size()),
                this, SLOT(selectSubscribers()));
        action->setEnabled(containers.size() > 0);
    }

    if (dataIndex.isValid()) {
        menu->addSeparator(); // -------------------------------
        action = menu->addAction(tr("Expand complete subtree"),
                this, SLOT(expandData()));
        action->setEnabled(rows > 0);
        action->setIcon(QIcon(":/images/view-fullscreen.svg"));
    }

    menu->exec(sourceTree->viewport()->mapToGlobal(point));
}

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

void MainWindow::connectDataSource()
{
    DataSource *dataSource(dataModel->getDataSource(dataIndex));
    if (!dataSource) {
        return;
    }

    if (!dataSource->isConnected()) {
        dataSource->connectToHost();
    }
}

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

void MainWindow::disconnectDataSource()
{
    DataSource *dataSource(dataModel->getDataSource(dataIndex));
    if (!dataSource) {
        return;
    }

    if (dataSource->isConnected()) {
        dataSource->disconnectFromHost();
    }
}

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

void MainWindow::removeDataSource()
{
    dataModel->remove(dataIndex);
}

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

void MainWindow::replaceDataSource()
{
    DataSource *oldSource(dataModel->getDataSource(dataIndex));
    if (not oldSource) {
        return;
    }

    ReplaceDialog dialog(dataModel, oldSource);

    if (dialog.exec() != QDialog::Accepted) {
        return;
    }

    DataSource *newSource = dialog.adoptSource();

    if (dialog.getMode() == ReplaceDialog::Permanent) {
        newSource->setUrl(newSource->getConnectUrl());
        replaceUrl(oldSource->getUrl(), newSource->getUrl());
    }

    dataModel->replace(oldSource, newSource); // oldSource invalid now!

    connectDataSlots();
}

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

void MainWindow::dataSourceShowMessages()
{
    DataSource *dataSource(dataModel->getDataSource(dataIndex));
    if (!dataSource) {
        return;
    }

    dataSource->showMessages(this);
}

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

void MainWindow::dataSourceConfigMessages()
{
    DataSource *dataSource(dataModel->getDataSource(dataIndex));
    if (!dataSource) {
        return;
    }

    MessageDialog dialog(dataSource, this);
    dialog.exec();
}

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

void MainWindow::copyVariableUrl()
{
    QSet<QUrl> seen;
    QList<QUrl> urls;

    foreach (QModelIndex index,
            sourceTree->selectionModel()->selectedIndexes()) {
        DataNode *dataNode(dataModel->getDataNode(index));
        if (dataNode) {
            QUrl url(dataNode->nodeUrl());
            if (not seen.contains(url)) {
                urls.append(url);
                seen.insert(url);
            }
        }
    }

    QClipboard *clipboard = QGuiApplication::clipboard();
    QMimeData *mimeData;

    mimeData = new QMimeData();
    mimeData->setUrls(urls);
    clipboard->setMimeData(mimeData, QClipboard::Clipboard);

    mimeData = new QMimeData();
    mimeData->setUrls(urls);
    clipboard->setMimeData(mimeData, QClipboard::Selection);
}

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

void MainWindow::selectSubscribers()
{
    deselectAll();
    foreach (WidgetContainer *container, usingSelected()) {
        container->select();
    }
}

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

void MainWindow::expandData()
{
    expandChildren(dataIndex, sourceTree, -1);
}

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

void MainWindow::expandProperties()
{
    expandChildren(propertyTree->rootIndex(), propertyTree, 2);
}

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

void MainWindow::propertyTreeCustomContextMenu(const QPoint &point)
{
    QMenu *menu = new QMenu;
    QModelIndex index = propertyTree->indexAt(point);

    if (index.isValid()) {
        propertyNode = (PropertyNode *) index.internalPointer();
    }
    else {
        propertyNode = NULL;
    }

    QAction *a = menu->addAction(tr("Reset"), this, SLOT(resetProperty()));
    //a->setIcon(QIcon(":/images/view-refresh.svg"));
    Property *property = dynamic_cast<Property *>(propertyNode);
    if (property) {
        a->setEnabled(property->isSet(propertyModel));
    }
    else {
        a->setEnabled(false);
    }

    menu->exec(propertyTree->viewport()->mapToGlobal(point));
}

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

void MainWindow::resetProperty()
{
    if (!propertyNode) {
        return;
    }

    Property *property = dynamic_cast<Property *>(propertyNode);
    if (property) {
        property->reset(propertyModel);
    }
}

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

void MainWindow::scriptVariablesCustomContextMenu(const QPoint &point)
{
    ScriptVariableModel *model(dynamic_cast<ScriptVariableModel *>(
                tableViewScriptVariables->model()));
    if (not model) {
        return;
    }

    QMenu *menu = new QMenu;
    QModelIndex index(tableViewScriptVariables->indexAt(point));
    scriptVariable = model->getScriptVariable(index);

    QAction *action;
    action = menu->addAction(tr("Remove"),
            this, SLOT(removeScriptVariables()));
    action->setEnabled(scriptVariable);
    action->setIcon(QIcon(":/images/list-remove.svg"));

    menu->addSeparator(); // -------------------------------

    action = menu->addAction(tr("Clear list"),
            this, SLOT(clearScriptVariables()));
    action->setEnabled(not model->isEmpty());
    action->setIcon(QIcon(":/images/edit-clear.svg"));

    menu->exec(tableViewScriptVariables->viewport()->mapToGlobal(point));
}

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

void MainWindow::removeScriptVariables()
{
    if (!scriptVariable) {
        return;
    }

    ScriptVariableModel *model(dynamic_cast<ScriptVariableModel *>(
                tableViewScriptVariables->model()));
    if (not model) {
        return;
    }

    model->remove(scriptVariable);
}

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

void MainWindow::clearScriptVariables()
{
    ScriptVariableModel *model(dynamic_cast<ScriptVariableModel *>(
                tableViewScriptVariables->model()));
    if (not model) {
        return;
    }

    model->clear();
}

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

void MainWindow::tabCloseRequested(int index)
{
    tabWidget->widget(index)->deleteLater();
    tabWidget->removeTab(index);
}

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

void MainWindow::pythonStdOutDataAvailable(int)
{
    pythonStdOutNotifier->setEnabled(false);

    QByteArray data;
    data.reserve(1024);
    int ret = ::read(pythonStdOut[1], data.data(), data.capacity());
    if (ret == -1) {
        qWarning() << "Failed to read from python stdout:" << strerror(errno);
        return;
    }
    data.resize(ret);

    textEditPython->moveCursor(QTextCursor::End);
    textEditPython->setTextColor(color[0]);
    textEditPython->insertPlainText(data);
    textEditPython->moveCursor(QTextCursor::End);

    pythonStdOutNotifier->setEnabled(true);
}

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

void MainWindow::pythonStdErrDataAvailable(int)
{
    pythonStdErrNotifier->setEnabled(false);

    QByteArray data;
    data.reserve(1024);
    int ret = ::read(pythonStdErr[1], data.data(), data.capacity());
    if (ret == -1) {
        qWarning() << "Failed to read from python stderr:" << strerror(errno);
        return;
    }
    data.resize(ret);

    textEditPython->moveCursor(QTextCursor::End);
    textEditPython->setTextColor(color[1]);
    textEditPython->insertPlainText(data);
    textEditPython->moveCursor(QTextCursor::End);

    pythonStdErrNotifier->setEnabled(true);
}
void MainWindow::connectionStateChanged(DataModel::ConnectionState state)
{
    actionConnectAll->setEnabled(state == DataModel::NoneConnected ||
            state == DataModel::SomeConnected);
}

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

void MainWindow::connectDataSlots()
{
    foreach (TabPage *tab, tabWidget->findChildren<TabPage *>()) {
        tab->connectDataSlots();
    }
}

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

void MainWindow::statsChanged()
{
    QLocale loc;
    QString num;

    num = loc.toString(dataModel->getInRate() / 1024.0, 'f', 1);
    labelIn->setText(tr("%1 KB/s").arg(num));
    num = loc.toString(dataModel->getOutRate() / 1024.0, 'f', 1);
    labelOut->setText(tr("%1 KB/s").arg(num));
}

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

void MainWindow::currentMessage(
        const Pd::Message *msg
        )
{
    if (msg) {
        QIcon icon;
        switch (msg->getType()) {
            case Pd::Message::Critical:
            case Pd::Message::Error:
                icon = QIcon(":/images/dialog-error.svg");
                break;
            case Pd::Message::Warning:
                icon = QIcon(":/images/dialog-warning.svg");
                break;
            default:
                break;
        }
        statusMessageIcon->setPixmap(icon.pixmap(QSize(16, 16)));

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

        statusMessageText->setText(msg->getText(lang));
        statusMessageText->setToolTip(msg->getDescription(lang));
    } else {
        statusMessageIcon->setPixmap(QPixmap());
        statusMessageText->setText("");
        statusMessageText->setToolTip(QString());
    }
}

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