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

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

#include "config/pluginregistry.h"

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

#include <algorithm>

#include <QDebug>

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

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);
    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);
}

void Port::cleanConnections() {
    connections.erase(std::remove_if(connections.begin(), connections.end(), [](auto w) { return !w.lock(); }), connections.end());
}

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>();
}

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) {
    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;
        return p;
    }
    return nullptr;
}

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

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
        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;
                        }
                    }
                }
            }
        }
    }

    /*auto qd = qDebug() << "processing" << QString::fromStdString(pluginName());
    if (!name.empty()) qd << "named" << QString::fromStdString(name);
    if (auto p = parent.lock(); p && !p->name.empty()) qd << "within" << QString::fromStdString(p->name);*/
    process();

    tick_last = tick_this;
    return true;
}

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