#include "patchboardscene.h"
using Xybrid::UI::PatchboardScene;

#include <cmath>

#include <QDebug>
#include <QScrollBar>
#include <QGraphicsItem>
#include <QMainWindow>
#include <QKeyEvent>
#include <QMenu>
#include <QTimer>

#include <QShortcut>
#include <QGuiApplication>
#include <QClipboard>
#include <QMimeData>
#include <QMessageBox>

#include <QCborValue>
#include <QCborMap>
#include <QCborArray>

#include <QGraphicsSceneContextMenuEvent>

#include "data/graph.h"
#include "data/project.h"
using namespace Xybrid::Data;
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "fileops.h"

#include "audio/audioengine.h"
using namespace Xybrid::Audio;

#include "util/keys.h"

#include "ui/patchboard/nodeobject.h"

#include "config/colorscheme.h"

QShortcut* PatchboardScene::shortcut(QKeySequence s) {
    auto c = new QShortcut(s, view);
    connect(this, &QObject::destroyed, c, [c] {delete c;});
    return c;
}

PatchboardScene::PatchboardScene(QGraphicsView* parent, const std::shared_ptr<Xybrid::Data::Graph>& g) : GadgetScene(parent) {
    graph = g;

    connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, this, &PatchboardScene::queueResize);
    connect(view->verticalScrollBar(), &QScrollBar::valueChanged, this, &PatchboardScene::queueResize);
    connect(view->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &PatchboardScene::queueResize);
    connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, &PatchboardScene::queueResize);
    connect(this, &QGraphicsScene::changed, this, &PatchboardScene::queueResize);

    auto copy = [this] {
        auto sel = this->selectedItems();
        if (sel.empty()) return; // nope

        auto* clip = QGuiApplication::clipboard();
        auto* data = new QMimeData();

        std::vector<std::shared_ptr<Node>> v;
        v.reserve(static_cast<size_t>(sel.size()));
        QRectF r;
        for (auto s : sel) {
            auto n = static_cast<NodeObject*>(s)->getNode();
            if (n->isVolatile()) continue;
            v.push_back(n);
            r = r.united(s->sceneBoundingRect());
        }

        auto cbor = Node::multiToCbor(v);

        { /* override center point */ } {
            auto c = r.center().toPoint();
            cbor[QString("center")] = QCborArray({c.x(), c.y()});
        }

        data->setData("xybrid-internal/x-graph-copy", cbor.toCborValue().toCbor());
        clip->setMimeData(data);
    };
    connect(shortcut(QKeySequence(QKeySequence::StandardKey::Delete)), &QShortcut::activated, this, [this] {
        auto sel = this->selectedItems();
        if (sel.empty()) return;
        if (QMessageBox::warning(nullptr, "Are you sure?", QString("Remove %1 node(s)? (This cannot be undone!)").arg(sel.size()), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) != QMessageBox::Yes) return;
        for (auto n : sel) {
            if (!n) continue;
            static_cast<NodeObject*>(n)->getNode()->parentTo(nullptr);
            delete n;
        }
    });
    connect(shortcut(QKeySequence(QKeySequence::StandardKey::Cut)), &QShortcut::activated, this, [this, copy] {
        auto sel = this->selectedItems();
        if (sel.empty()) return;
        copy();
        if (QMessageBox::warning(nullptr, "Are you sure?", QString("Remove %1 node(s)? (This cannot be undone!)").arg(sel.size()), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) != QMessageBox::Yes) return;
        for (auto n : sel) {
            if (!n) continue;
            static_cast<NodeObject*>(n)->getNode()->parentTo(nullptr);
            delete n;
        }
    });
    connect(shortcut(QKeySequence(QKeySequence::StandardKey::Copy)), &QShortcut::activated, this, [copy] { copy(); });
    connect(shortcut(QKeySequence(QKeySequence::StandardKey::Paste)), &QShortcut::activated, this, [this] {
        const auto* clip = QGuiApplication::clipboard();
        const auto* data = clip->mimeData();

        if (!data->hasFormat("xybrid-internal/x-graph-copy")) return; // no pattern data

        auto center = view->mapToScene(view->viewport()->visibleRegion().boundingRect()).boundingRect().center().toPoint();

        auto v = Node::multiFromCbor(QCborValue::fromCbor(data->data("xybrid-internal/x-graph-copy")), graph, center);

        setSelectionArea(QPainterPath()); // deselect all
        for (auto n : v) { // and select pasted objects
            auto o = new NodeObject(n);
            addItem(o);
            o->setSelected(true);
        }
    });

    refresh();
}

void PatchboardScene::drawBackground(QPainter* painter, const QRectF& rect) {
    painter->setBrush(QBrush(Config::colorScheme.patternBg));
    painter->setPen(QPen(Qt::PenStyle::NoPen));
    painter->drawRect(rect);

    const constexpr int step = 32; // grid size
    painter->setPen(QPen(QColor(127, 127, 127, 63), 1));
    for (auto y = std::floor(rect.top() / step) * step + .5; y < rect.bottom(); y += step)
        painter->drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y));
    for (auto x = std::floor(rect.left() / step) * step + .5; x < rect.right(); x += step)
        painter->drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()));
}

void PatchboardScene::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
    // only override if nothing inside picks it up
    QGraphicsScene::contextMenuEvent(e);
    if (e->isAccepted()) return;

    auto p = e->scenePos();
    auto* m = new QMenu();
    PluginRegistry::populatePluginMenu(m->addMenu("Add..."), [this, p](std::shared_ptr<Node> n) {
        n->x = static_cast<int>(p.x());
        n->y = static_cast<int>(p.y());
        n->parentTo(graph);

        addItem(new NodeObject(n));
    }, graph.get());
    m->addAction("Import...", [this, p] {
        if (auto fileName = FileOps::showOpenDialog(nullptr, "Import node...", Config::Directories::presets, FileOps::Filter::node); !fileName.isEmpty()) {
            auto n = FileOps::loadNode(fileName, graph);
            if (!n) return; // right, that can return null
            n->x = static_cast<int>(p.x());
            n->y = static_cast<int>(p.y());

            addItem(new NodeObject(n));
        }
    });

    m->setAttribute(Qt::WA_DeleteOnClose);
    m->popup(e->screenPos());
}

void PatchboardScene::keyPressEvent(QKeyEvent* e) {
    QGraphicsScene::keyPressEvent(e);
    if (!e->isAccepted() && !e->isAutoRepeat()) {
        auto note = Util::keyToNote(e->key());
        if (note >= 0) {
            if (e->modifiers() & Qt::Modifier::SHIFT) note += 24;
            startPreview(Util::unshiftedKey(e->key()), note);
        }
    }
}

void PatchboardScene::keyReleaseEvent(QKeyEvent* e) {
    QGraphicsScene::keyReleaseEvent(e);
    if (!e->isAccepted() && !e->isAutoRepeat()) stopPreview(Util::unshiftedKey(e->key()));
}

void PatchboardScene::startPreview(int key, int16_t note) {
    stopPreview(key); // end current preview first, if applicable
    auto p = graph->project->shared_from_this();
    bool subgraph = true && graph->parent.lock();
    previewKey[key] = {audioEngine->previewPort(), audioEngine->preview(p, -1, note, 0, subgraph ? graph.get() : nullptr)};

}

void PatchboardScene::stopPreview(int key) {
    if (auto k = previewKey.find(key); k != previewKey.end()) {
        auto p = graph->project->shared_from_this();
        bool subgraph = true && graph->parent.lock();
        //audioEngine->preview(p, k->second[0], k->second[1], false);
        audioEngine->preview(p, k->second.first, -2, k->second.second, subgraph ? graph.get() : nullptr);
        previewKey.erase(k);
    }
}

void PatchboardScene::queueResize() {
    if (!resizeQueued) {
        resizeQueued = true;
        QTimer::singleShot(1, this, &PatchboardScene::autoResize);
    }
}

void PatchboardScene::autoResize() {
    if (!view) return;
    auto vrect = view->mapToScene(view->viewport()->visibleRegion().boundingRect()).boundingRect();
    auto rect = itemsBoundingRect()
                .united(vrect)
                .united(QRectF(0, 0, 1, 1));
    rect.setTopLeft(QPointF(0, 0));
    setSceneRect(rect);
    graph->viewX = static_cast<int>(vrect.left());
    graph->viewY = static_cast<int>(vrect.top());
    resizeQueued = false;
}

void PatchboardScene::refresh() {
    // build scene from graph
    clear();

    for (auto n : graph->children) {
        auto* o = new NodeObject(n);
        addItem(o);
    }

    autoResize();
}
