#include "nodeobject.h"
using Xybrid::UI::NodeObject;
using Xybrid::UI::PortObject;
using Xybrid::UI::PortConnectionObject;
using Xybrid::Data::Node;
using Xybrid::Data::Port;

#include "fileops.h"

#include "ui/patchboard/gadgetscene.h"

#include "util/strings.h"

#include <cmath>

#include <QDebug>
#include <QTimer>
#include <QPainter>
#include <QGraphicsScene>
#include <QStyleOptionGraphicsItem>
#include <QGraphicsSceneHoverEvent>
#include <QTextDocument>
#include <QTextCharFormat>
#include <QTextCursor>
#include <QMenu>
#include <QMessageBox>
#include <QInputDialog>


namespace {
    const QColor tcolor[] {
        QColor(239, 179, 59), // Audio
        QColor(163, 95, 191), // Command
        QColor(95, 191, 163), // MIDI
        QColor(127, 127, 255), // Parameter
    };

    const constexpr qreal edgePad = 3;
}

void PortObject::connectTo(PortObject* o) {
    if (!o) return;
    if (connections.find(o) != connections.end()) return;
    if (port->type == o->port->type) return;

    PortObject* in;
    PortObject* out;
    if (port->type == Port::Input) { in = this; out = o; }
    else { out = this; in = o; }

    if (out->port->connect(in->port)) {
        /*auto* pc =*/ new PortConnectionObject(in, out);
    }

}

void PortObject::setHighlighted(bool h, bool hideLabel) {
    highlighted = h;

    auto gs = static_cast<GadgetScene*>(scene());
    if (h && !hideLabel) {
        auto c = tcolor[port->dataType()];
        auto txt = qs("%1 %2").arg(Util::enumName(port->dataType()).toLower()).arg(Util::hex(port->index));
        if (!port->name.isEmpty()) txt = qs("%1 (%2)").arg(port->name).arg(txt);
        double side = port->type == Port::Input ? -1.0 : 1.0;
        gs->toolTip(this, txt, {side, 0}, c);
    } else gs->toolTip(this, { }, { }, { });

    update();
}

PortObject::PortObject(const std::shared_ptr<Data::Port>& p) {
    port = p;
    p->obj = this;
    setAcceptHoverEvents(true);
    setAcceptedMouseButtons(Qt::LeftButton);
    setFlag(QGraphicsItem::ItemSendsScenePositionChanges);

    for (auto c : port->connections) {
        if (auto cc = c.lock(); cc && cc->obj) connectTo(cc->obj);
    }

    setCursor(Qt::CursorShape::CrossCursor);
}

PortObject::~PortObject() {
    while (connections.begin() != connections.end()) delete connections.begin()->second;
}

void PortObject::mousePressEvent(QGraphicsSceneMouseEvent*) {
    //setCursor(Qt::CursorShape::CrossCursor);
    setHighlighted(true, true);
    dragLine.reset(new QGraphicsLineItem());
    dragLine->setPen(QPen(tcolor[port->dataType()].lighter(125), 1.5));
    dragLine->setLine(QLineF(scenePos(), scenePos()));
    dragLine->setZValue(100);
    scene()->addItem(dragLine.get());
}

void PortObject::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
    //unsetCursor();
    dragLine.reset();

    auto* i = scene()->itemAt(e->scenePos(), QTransform());
    if (i && i->type() == PortObject::Type) {
        auto* p = static_cast<PortObject*>(i);
        //qDebug() << "connection:" << port->connect(p->port);
        connectTo(p);
    }
}

void PortObject::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
    if (dragLine) dragLine->setLine(QLineF(scenePos(), e->scenePos()));
    update();
}

void PortObject::hoverEnterEvent(QGraphicsSceneHoverEvent*) {
    setHighlighted(true);
}

void PortObject::hoverLeaveEvent(QGraphicsSceneHoverEvent*) {
    setHighlighted(false);
}

void PortObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
    auto* m = new QMenu();
    m->addAction("Disconnect All", this, [this] {
        while (connections.begin() != connections.end()) {
            auto* c = connections.begin()->second;
            c->in->port->disconnect(c->out->port);
            delete c;
        }
    });//->setEnabled(this->connections.size() != 0);
    m->setAttribute(Qt::WA_DeleteOnClose);
    m->popup(e->screenPos());
}

void PortObject::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) {
    QColor bg = tcolor[port->dataType()];

    QColor outline = bg.darker(200);
    if (highlighted) outline = bg.lighter(150);

    painter->setRenderHint(QPainter::RenderHint::Antialiasing);
    painter->setBrush(QBrush(bg));
    painter->setPen(QPen(QBrush(outline), 1));
    painter->drawEllipse(boundingRect());
}

QRectF PortObject::boundingRect() const {
    return QRectF(portSize * -.5, portSize * -.5, portSize, portSize);
}

NodeObject::NodeObject(const std::shared_ptr<Data::Node>& n) {
    node = n;
    node->obj = this;

    setFlag(QGraphicsItem::ItemIsMovable);
    setFlag(QGraphicsItem::ItemIsFocusable);
    setFlag(QGraphicsItem::ItemIsSelectable);
    setFlag(QGraphicsItem::ItemSendsScenePositionChanges);

    setPos(node->x, node->y);

    connect(this, &QGraphicsObject::xChanged, this, &NodeObject::onMoved);
    connect(this, &QGraphicsObject::yChanged, this, &NodeObject::onMoved);

    //setToolTip(QString::fromStdString(node->name));

    contents = new QGraphicsRectItem(this);
    contents->setPen(Qt::NoPen);
    contents->setBrush(Qt::NoBrush);

    createPorts();

    node->onGadgetCreated();

    emit finalized();
}

void NodeObject::setGadgetSize(QPointF p) {
    gadgetSize_ = p;
    updateGeometry();
}

void NodeObject::promptDelete() {
    QPointer<NodeObject> t = this;
    if (QMessageBox::warning(nullptr, "Are you sure?", QString("Remove node? (This cannot be undone!)"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) != QMessageBox::Yes) return;
    if (t) {
        t->node->parentTo(nullptr); // unparent node so it gets deleted
        delete t;
    }
}

void NodeObject::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
    QGraphicsObject::mouseMoveEvent(e);
    if (e->modifiers() & Qt::Modifier::SHIFT) {
        auto oPos = pos();
        // snap to grid (by center) if shift held
        constexpr qreal grid = 16.0;
        auto r = boundingRect();
        auto cx = r.width()/2.0;
        auto cy = r.height()/2.0;
        setPos(std::round((x()+cx)/grid)*grid-cx, std::round((y()+cy)/grid)*grid-cy);

        auto posDelta = pos() - oPos;
        for (auto itm : scene()->selectedItems()) {
            if (itm == this) continue;
            itm->moveBy(posDelta.x(), posDelta.y()); // apply snap to everything else, in relative terms
        }
    }
}

void NodeObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) {
    node->onDoubleClick();
}

void NodeObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
    if (!isSelected()) {
        for (auto* s : scene()->selectedItems()) s->setSelected(false);
        setSelected(true);
    }
    auto* m = new QMenu();
    if (canRename) {
        m->addAction("Rename...", this, [this] {
            bool ok = false;
            auto cn = node->name.isEmpty() ? node->pluginName() : QString("\"%1\"").arg(node->name);
            auto capt = QString("Rename %1:").arg(cn);
            auto n = QInputDialog::getText(nullptr, "Rename...", capt, QLineEdit::Normal, node->name, &ok);
            if (!ok) return; // canceled
            //setToolTip(n);
            node->name = n;
            node->onRename();
            updateGeometry();
            update();
        });
    }
    m->addAction("Export...", this, [this] {
        if (auto fileName = FileOps::showSaveAsDialog(nullptr, "Export node...", Config::Directories::presets, FileOps::Filter::node, "xyn"); !fileName.isEmpty()) {
            FileOps::saveNode(node, fileName);
        }
    });
    m->addSeparator();
    m->addAction("Delete node", this, &NodeObject::promptDelete);
    m->setAttribute(Qt::WA_DeleteOnClose);
    m->popup(e->screenPos());
}

void NodeObject::bringToTop(bool force) {
    for (auto o : collidingItems()) if (o->parentItem() == parentItem() && (force || !o->isSelected())) o->stackBefore(this);
}

void NodeObject::createPorts() {
    auto* ipc = new QGraphicsLineItem(this);
    auto* opc = new QGraphicsLineItem(this);
    inputPortContainer.reset(ipc);
    outputPortContainer.reset(opc);
    updateGeometry();

    QPen p(QColor(95, 95, 95), 2.5);
    QPointF inc(0, PortObject::portSize + PortObject::portSpacing);

    QPointF cursor = QPointF(0, 0);
    for (auto mdt : node->inputs) {
        for (auto pp : mdt.second) {
            auto* p = new PortObject(pp.second);
            p->setParentItem(ipc);
            p->setPos(cursor);
            cursor += inc;
        }
    }
    ipc->setVisible(cursor.y() > 0);
    cursor -= inc;
    ipc->setLine(QLineF(QPointF(0, 0), cursor));
    ipc->setPen(p);

    cursor = QPointF(0, 0);
    for (auto mdt : node->outputs) {
        for (auto pp : mdt.second) {
            auto* p = new PortObject(pp.second);
            p->setParentItem(opc);
            p->setPos(cursor);
            cursor += inc;
        }
    }
    opc->setVisible(cursor.y() > 0);
    cursor -= inc;
    opc->setLine(QLineF(QPointF(0, 0), cursor));
    opc->setPen(p);
}

void NodeObject::updateGeometry() {
    contents->setRect(QRectF(QPointF(0, 0), gadgetSize_));
    if (showName) contents->setPos(edgePad, edgePad + nameSize());
    else contents->setPos(edgePad, edgePad);

    if (autoPositionPorts) {
        qreal pm = PortObject::portSize * .5 + PortObject::portSpacing;
        if (inputPortContainer) inputPortContainer->setPos(QPointF(-pm, PortObject::portSize));
        if (outputPortContainer) outputPortContainer->setPos(QPointF(boundingRect().width() + pm, PortObject::portSize));
    }
    emit postGeometryUpdate();
}

qreal NodeObject::nameSize() const {
    qreal n = 0;
    if (showName) {
        if (showPluginName) n++;
        if (!node->name.isEmpty()) n++;
    }
    if (n > 0) {
        n *= QFontMetrics(QFont()).height();
        n--; // decreased top padding
    }
    return n;
}

void NodeObject::onMoved() {
    if (x() < 0) setX(0);
    else setX(std::round(x()));
    if (y() < 0) setY(0);
    else setY(std::round(y()));
    node->x = static_cast<int>(x());
    node->y = static_cast<int>(y());

    if (isSelected()) {
        bringToTop();
    }

    if (auto s = scene(); s) s->update();
}

void NodeObject::focusInEvent(QFocusEvent *) {
    bringToTop(true);
}

void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget *) {
    if (customChrome) {
        node->drawCustomChrome(painter, opt);
        return;
    }
    QRectF r = boundingRect();

    QColor outline = QColor(31, 31, 31);
    if (opt->state & QStyle::State_Selected) outline = QColor(127, 127, 255);

    QLinearGradient fill(QPointF(0, 0), QPointF(0, r.height()));
    fill.setColorAt(0, QColor(95, 95, 95));
    fill.setColorAt(16.0/r.height(), QColor(63, 63, 63));
    fill.setColorAt(1.0 - (1.0 - 16.0/r.height()) / 2, QColor(55, 55, 55));
    fill.setColorAt(1, QColor(35, 35, 35));

    painter->setRenderHint(QPainter::RenderHint::Antialiasing);
    painter->setBrush(QBrush(fill));
    painter->setPen(QPen(QBrush(outline), 2));
    painter->drawRoundedRect(r, 8, 8);

    if (showName) {
        static QFont f = [] {
            QFont f("Arcon Rounded", 8);
            f.setStretch(QFont::SemiCondensed);
            return f;
        }();

        painter->setFont(f);
        QRectF tr = r - QMarginsF(edgePad, edgePad - 1, edgePad, 0);
        if (!node->name.isEmpty()) {
            painter->setPen(QColor(222, 222, 222));
            painter->drawText(tr, Qt::AlignLeft, node->name);
            tr -= QMarginsF(0, painter->fontMetrics().height(), 0, 0);
        }
        if (showPluginName) {
            painter->setPen(QColor(171, 171, 171));
            painter->drawText(tr, Qt::AlignLeft, node->pluginName());
        }
    }
}

QRectF NodeObject::boundingRect() const {
    if (customChrome) return QRectF(QPointF(0, 0), gadgetSize_);
    if (gadgetSize_.isNull()) return QRectF(0, 0, 128+32, 36);// + QMarginsF(8, 8, 8, 8);
    if (showName) return QRectF(QPointF(), gadgetSize_ + QPointF(edgePad * 2, edgePad * 2 + nameSize()));
    return QRectF(QPointF(), gadgetSize_ + QPointF(edgePad * 2, edgePad * 2));
}

PortConnectionObject::PortConnectionObject(PortObject* in, PortObject* out) {
    this->in = in;
    this->out = out;

    // remove dupes
    if (in->connections[out]) delete in->connections[out];
    if (out->connections[in]) delete out->connections[in];
    // and hook up
    in->connections[out] = this;
    out->connections[in] = this;

    QTimer::singleShot(1, [this] { this->in->scene()->addItem(this); });
    setZValue(-100);
    setAcceptHoverEvents(true);
    //setFlag(QGraphicsItem::GraphicsItemFlag::)

    QTimer::singleShot(1, [this] {
        auto* op = static_cast<QGraphicsObject*>(this->out->parentItem()->parentItem());
        auto* ip = static_cast<QGraphicsObject*>(this->in->parentItem()->parentItem());
        connect(op, &QGraphicsObject::xChanged, this, &PortConnectionObject::updateEnds);
        connect(op, &QGraphicsObject::yChanged, this, &PortConnectionObject::updateEnds);
        connect(ip, &QGraphicsObject::xChanged, this, &PortConnectionObject::updateEnds);
        connect(ip, &QGraphicsObject::xChanged, this, &PortConnectionObject::updateEnds);

        updateEnds();
    });
}

void PortConnectionObject::updateEnds() {
    setPos((out->scenePos() + in->scenePos()) * .5);
    update();
}

PortConnectionObject::~PortConnectionObject() {
    in->connections.erase(out);
    out->connections.erase(in);
}

void PortConnectionObject::disconnect() {
    out->port->disconnect(in->port);
    delete this;
}

void PortConnectionObject::hoverEnterEvent(QGraphicsSceneHoverEvent*) {
    highlighted = true;
    update();
}

void PortConnectionObject::hoverLeaveEvent(QGraphicsSceneHoverEvent*) {
    highlighted = false;
    update();
}

void PortConnectionObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
    auto* m = new QMenu();
    m->addAction("Disconnect", this, [this] {
        disconnect();
    });
    m->popup(e->screenPos());
}

void PortConnectionObject::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) {
    QColor c = tcolor[in->port->dataType()];
    if (highlighted) c = c.lighter(150);

    painter->setPen(Qt::NoPen);
    painter->setBrush(QBrush(c));
    painter->drawPath(shape(2.5));
}

QRectF PortConnectionObject::boundingRect() const {
    return shape().boundingRect().normalized();//controlPointRect().normalized().united(QRectF(mapFromScene(out->scenePos()), mapFromScene(in->scenePos())));
}

QPainterPath PortConnectionObject::shape(qreal width) const {
    QPainterPath path;
    auto start = mapFromScene(out->scenePos());
    auto end = mapFromScene(in->scenePos());
    path.moveTo(start);
    //QPointF mod(std::max(std::max((end.x() - start.x()) * .64, (start.x() - end.x()) * .24), 64.0), 0);
    QPointF out(0, 0);
    //path.lineTo(start+out);
    QPointF mod(std::max((end.x() - start.x()) * .64, 96.0), 0);
    if (auto vdist = std::fabs(end.y() - start.y()), hdist = fabs(end.x() - start.x()) * 0.75; hdist < 96.0 && vdist < 96.0) {
        double p = std::max(vdist, hdist) / 96.0;
        mod = QPointF(mod.x() * p, 0);
    }
    path.cubicTo(start + out + mod, end - mod, end - out);
    //path.lineTo(end);

    if (width <= 0) return path;
    QPainterPathStroker qp;
    qp.setWidth(width);
    qp.setCapStyle(Qt::PenCapStyle::RoundCap);
    qp.setJoinStyle(Qt::PenJoinStyle::RoundJoin);
    auto p = qp.createStroke(path);
    return p;
}
