#include "node.h"
using namespace Xybrid::Data;

#include "data/project.h"
#include "data/graph.h"
#include "data/porttypes.h"

#include "ui/patchboard/nodeobject.h"

#include "config/pluginregistry.h"
using namespace Xybrid::Config;

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

#include "util/strings.h"
#include "util/ycombinator.h"

#include "uisocket.h"

#include <algorithm>

#include <QDebug>
#include <QThread>
#include <QMetaEnum>
#include <QCborValue>
#include <QCborMap>
#include <QCborArray>

#define qs QStringLiteral

Port::~Port() {
    // clean up others' connection lists
    while (connections.size() > 0) {
        if (auto cc = connections.back().lock(); cc) cc->cleanConnections();
        connections.pop_back();
    }
}

Port::Port(const Port &) : std::enable_shared_from_this<Port>() { }

bool Port::canConnectTo(DataType d) const {
    return d == dataType();
}

bool Port::connect(std::shared_ptr<Port> p) {
    if (!p) return false; // no blank pointers pls
    // actual processing is always done on the input port, since that's where any limits are
    if (type == Output) return p->type == Input && p->connect(shared_from_this());
    if (!canConnectTo(p->dataType())) return false; // can't hook up to an incompatible data type
    for (auto c : connections) if (c.lock() == p) return true; // I guess report success if already connected?
    if (singleInput() && connections.size() > 0) return false; // reject multiple connections on single-input ports
    if (auto o = owner.lock(), po = p->owner.lock(); !o || !po || po->dependsOn(o)) return false; // no dependency loops!
    // actually hook up
    connections.emplace_back(p);
    p->connections.emplace_back(shared_from_this());
    if (auto o = owner.lock(); o) {
        audioEngine->invalidateQueue(o->project);
        o->onPortConnected(type, dataType(), index, p);
    }
    return true;
}

void Port::disconnect(std::shared_ptr<Port> p) {
    if (!p) return;
    auto t = shared_from_this();
    connections.erase(std::remove_if(connections.begin(), connections.end(), [p](auto w) { return w.lock() == p; }), connections.end());
    p->connections.erase(std::remove_if(p->connections.begin(), p->connections.end(), [t](auto w) { return w.lock() == t; }), p->connections.end());
    if (auto o = owner.lock(); o) {
        audioEngine->invalidateQueue(o->project);
        o->onPortDisconnected(type, dataType(), index, p);
    }
}

void Port::cleanConnections() {
    bool needNotify = false;
    connections.erase(std::remove_if(connections.begin(), connections.end(), [&needNotify](auto w) { if (!w.lock()) { needNotify = true; return true; } return false; }), connections.end());
    if (auto o = owner.lock(); o && needNotify) o->onPortDisconnected(type, dataType(), index, std::weak_ptr<Port>());
}

std::shared_ptr<Port> Port::makePort(DataType dt) {
    if (dt == Audio) return std::make_shared<AudioPort>();
    if (dt == Command) return std::make_shared<CommandPort>();
    // fallback
    return std::make_shared<Port>();
}

QCborMap Node::toCbor() const {
    QCborMap m;

    if (plugin) m[qs("id")] = plugin->id;
    if (!name.isEmpty()) m[qs("name")] = name;
    m[qs("x")] = x;
    m[qs("y")] = y;
    saveData(m);

    return m;
}

std::shared_ptr<Node> Node::fromCbor(const QCborMap& m, std::shared_ptr<Graph> parent) {
    auto ch = PluginRegistry::createInstance(m.value("id").toString());
    ch->parentTo(parent);
    ch->name = m.value("name").toString();
    ch->x = static_cast<int>(m.value("x").toInteger());
    ch->y = static_cast<int>(m.value("y").toInteger());
    ch->loadData(m);
    return ch;
}
std::shared_ptr<Node> Node::fromCbor(const QCborValue& m, std::shared_ptr<Graph> parent) { return fromCbor(m.toMap(), parent); }

QCborMap Node::multiToCbor(std::vector<std::shared_ptr<Node>>& v) {
    QCborMap m;

    Sample::startExport();

    std::unordered_map<Node*, int> indices;
    { /* nodes */ } {
        QCborArray nm;
        int idx = 0;
        for (auto n : v) {
            if (n->isVolatile()) continue; // skip things with volatile locality (i/o ports etc.)
            indices[n.get()] = idx++;
            nm << n->toCbor();
        }
        m[qs("nodes")] = nm;
    }

    // exported samples
    if (auto v = Sample::finishExport(); !v.empty()) {
        QCborMap smp;
        for (auto s : v) smp[QCborValue(s->uuid)] = s->toCbor();
        m[qs("samples")] = smp;
    }

    { /* connections */ } {
        QCborArray cm;

        for (auto n : v) {
            if (n->isVolatile()) continue; // already skipped
            int idx = indices[n.get()];
            for (auto dt : n->outputs) {
                for (auto op : dt.second) {
                    auto o = op.second;
                    o->cleanConnections(); // let's just do some groundskeeping here
                    for (auto iw : o->connections) {
                        auto i = iw.lock();
                        if (auto in = indices.find(i->owner.lock().get()); in != indices.end()) { // only connections within the collection
                            QCborArray c;
                            c << idx;
                            c << Util::enumName(o->dataType());
                            c << o->index;
                            c << in->second;
                            c << Util::enumName(i->dataType());
                            c << i->index;
                            cm << c;
                        }
                    }
                }
            }
        }

        m[qs("connections")] = cm;
    }

    { /* center */ } {
        int count = 0;
        QPoint center;
        for (auto n : v) {
            if (n->isVolatile()) continue;
            center += QPoint(n->x, n->y);
            count++;
        }
        center /= count;
        m[qs("center")] = QCborArray({center.x(), center.y()});
    }

    return m;
}

std::vector<std::shared_ptr<Node>> Node::multiFromCbor(const QCborMap& m, std::shared_ptr<Graph> parent, QPoint cp) {
    std::vector<std::shared_ptr<Node>> v;

    QPoint center;

    { /* center point */ } {
        QCborArray c = m.value("center").toArray();
        center.setX(static_cast<int>(c.at(0).toInteger()));
        center.setY(static_cast<int>(c.at(1).toInteger()));
    }

    { /* exported samples */ } {
        QCborMap smp = m.value("samples").toMap();
        auto project = parent->project;
        for (auto it = smp.constBegin(), end = smp.constEnd(); it != end; ++it) {
            auto uuid = it.key().toUuid();
            if (project->samples.find(uuid) != project->samples.end()) continue; // we already have this; next
            auto s = Sample::fromCbor(it.value(), uuid);
            s->project = project;
            project->samples.insert(s->uuid, s);
        }
        emit project->socket->updatePatternLists();
    }

    { /* nodes */ } {
        QCborArray n = m.value("nodes").toArray();
        v.reserve(static_cast<size_t>(n.size()));
        for (auto nm : n) v.push_back(Node::fromCbor(nm, parent));
    }

    { /* connections */ } {
        QCborArray cn = m.value("connections").toArray();

        auto pmt = QMetaType::metaObjectForType(QMetaType::type("Xybrid::Data::Port"));
        auto dtm = pmt->enumerator(pmt->indexOfEnumerator("DataType"));

        for (auto cv : cn) {
            auto c = cv.toArray();
            if (c.empty()) continue;
            auto on = v[static_cast<size_t>(c[0].toInteger())];
            auto in = v[static_cast<size_t>(c[3].toInteger())];
            auto op = on->port(Port::Output, static_cast<Port::DataType>(dtm.keyToValue(c[1].toString().toStdString().c_str())), static_cast<uint8_t>(c[2].toInteger()));
            auto ip = in->port(Port::Input, static_cast<Port::DataType>(dtm.keyToValue(c[4].toString().toStdString().c_str())), static_cast<uint8_t>(c[5].toInteger()));
            if (op && ip) op->connect(ip);
        }
    }

    if (!cp.isNull()) { // offset and such
        QPoint off = cp - center;
        for (auto n : v) {
            n->x += off.x();
            n->y += off.y();
        }
    }

    return v;
}
std::vector<std::shared_ptr<Node>> Node::multiFromCbor(const QCborValue& m, std::shared_ptr<Graph> parent, QPoint cp) { return multiFromCbor(m.toMap(), parent, cp); }

void Node::parentTo(std::shared_ptr<Graph> graph) {
    auto t = shared_from_this(); // keep alive during reparenting
    if (auto p = parent.lock(); p) {
        onUnparent(p);
        p->children.erase(std::remove(p->children.begin(), p->children.end(), t), p->children.end());
    }
    parent = graph;
    if (graph) {
        project = graph->project;
        graph->children.push_back(t);
        onParent(graph);
    }
    audioEngine->invalidateQueue(project); // just to be safe
}

std::shared_ptr<Port> Node::port(Port::Type t, Port::DataType dt, uint8_t idx, bool addIfNeeded) {
    auto& m = t == Port::Input ? inputs : outputs;
    if (auto mdt = m.find(dt); mdt != m.end()) {
        if (auto it = mdt->second.find(idx); it != mdt->second.end()) return it->second;
    }
    return addIfNeeded ? addPort(t, dt, idx) : nullptr;
}

std::shared_ptr<Port> Node::addPort(Port::Type t, Port::DataType dt, uint8_t idx, bool allowUpdate) {
    auto& m = t == Port::Input ? inputs : outputs;
    m.try_emplace(dt);
    auto mdt = m.find(dt);
    if (mdt->second.find(idx) == mdt->second.end()) {
        auto p = Port::makePort(dt);
        p->owner = shared_from_this();
        p->type = t;
        mdt->second.insert({idx, p});
        p->index = idx;
        if (allowUpdate && obj) obj->updatePorts();
        return p;
    }
    return nullptr;
}

void Node::removePort(Port::Type t, Port::DataType dt, uint8_t idx, bool allowUpdate) {
    auto& m = t == Port::Input ? inputs : outputs;
    if (auto mdt = m.find(dt); mdt != m.end()) {
        mdt->second.erase(idx);
        if (allowUpdate && obj) obj->updatePorts();
    }
}

bool Node::movePort(Port::Type t, Port::DataType dt, uint8_t idx, uint8_t nidx, bool allowUpdate) {
    auto p = port(t, dt, idx);
    if (!p) return false;
    auto& m = t == Port::Input ? inputs : outputs;
    auto mdt = m.find(dt);
    if (mdt->second.find(nidx) == mdt->second.end()) {
        mdt->second.erase(idx);
        mdt->second.insert({nidx, p});
        p->index = nidx;
        if (allowUpdate && obj) obj->updatePorts();
        return true;
    }
    return false;
}

void Node::collapsePorts(Port::Type t, Port::DataType dt) {
    auto& m = t == Port::Input ? inputs : outputs;
    auto mdt = m.find(dt);
    if (mdt == m.end()) return; // nothing there
    auto& mm = mdt->second;
    uint8_t maxIdx = 0;
    for (auto p : mm) maxIdx = std::max(maxIdx, p.first);
    uint8_t firstUnused = 255;
    for (uint8_t i = 0; i <= maxIdx; i++) {
        if (auto pi = mm.find(i); pi != mm.end()) {
            if (firstUnused < i) {
                pi->second->index = firstUnused;
                mm.insert({firstUnused++, pi->second});
                mm.erase(i);
            }
        } else if (firstUnused > i) firstUnused = i;
    }
    if (obj) obj->updatePorts();
}

std::unordered_set<std::shared_ptr<Node>> Node::dependencies() const {
    std::unordered_set<std::shared_ptr<Node>> set;
    for (auto& t : inputs) {
        for (auto& p : t.second) {
            for (auto& c : p.second->connections) {
                // if connection still exists, *and its owner* still exists...
                if (auto cp = c.lock(); cp) if (auto n = cp->owner.lock(); n) set.insert(n);
            }
        }
    }
    return set;
}

bool Node::dependsOn(std::shared_ptr<Node> o) {
    auto deps = dependencies();
    for (auto d : deps) {
        if (d == o) return true;
        if (d->dependsOn(o)) return true;
    }
    return false;
}

bool Node::try_process(bool checkDependencies) {
    size_t tick_this = audioEngine->curTickId();
    if (tick_last == tick_this) return true; // already processed

    if (checkDependencies) { // check if dependencies are done
        auto checkInput = Util::yCombinator([tick_this](auto checkInput, std::shared_ptr<Port> p) -> bool {
            for (auto c : p->connections) { // check each connection; if node valid...
                if (auto cp = c.lock(); cp) {
                    if (auto n = cp->owner.lock(); n) {
                        if (n->tick_last != tick_this) return false; // if node itself not yet processed, check failed
                        if (auto cpp = cp->passthroughTo.lock(); cpp) { // if valid passthrough...
                            if (auto np = cpp->owner.lock(); np && (np->tick_last != tick_this || !checkInput(cpp))) {
                                return false; // check itself
                            }
                        }
                    }
                }
            }

            return true;
        });

        for (auto t : inputs) for (auto p : t.second) if (!checkInput(p.second)) return false;

        /*for (auto& t : inputs) {
            for (auto& p : t.second) {
                for (auto& c : p.second->connections) {
                    // if connection still exists, *and its owner* still exists...
                    if (auto cp = c.lock(); cp) {
                        if (auto n = cp->owner.lock(); n) {
                            if (auto cpp = cp->passthroughTo.lock(); cpp) { // passthrough...
                                if (auto np = cpp->owner.lock(); np && np->tick_last != tick_this) return false;
                            }
                            if (n->tick_last != tick_this) return false;
                        }
                    }
                }
            }
        }*/

    }

    process();

    tick_last = tick_this;
    return true;
}

QString Node::pluginName() const { if (!plugin) return qs("(unknown plugin)"); return plugin->displayName; }
