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

#include <list>
#include <map>
#include <unordered_map>

#include <QMenu>
#include <QHash>

#include "data/graph.h"
using namespace Xybrid::Data;

#include "nodes/gadget/ioport.h"
using Xybrid::Gadgets::IOPort;

#include "util/strings.h"

namespace {
    typedef std::list<std::function<void()>> fqueue; // typedef so QtCreator's auto indent doesn't completely break :|
    fqueue& regQueue() {
        static fqueue q;
        return q;
    }
    bool& initialized() { static bool b = false; return b; }

    QHash<QString, std::shared_ptr<PluginInfo>> plugins;

    QString priorityCategories[] {
        "Gadget", "Instrument", "Sampler", "Effect"
    };
}

bool PluginRegistry::enqueueRegistration(std::function<void ()> f) {
    auto& queue = regQueue();
    queue.push_back(f);
    if (initialized()) f();
    return true;
}

void PluginRegistry::init() {
    if (initialized()) return;

    for (auto& f : regQueue()) f();
    initialized() = true;
}

void PluginRegistry::registerPlugin(std::shared_ptr<PluginInfo> pi) {
    if (pi->id.isEmpty()) return;
    if (plugins.find(pi->id) != plugins.end()) return;
    plugins[pi->id] = pi;
    // there might be a better way to do this?
    for (auto& id : pi->oldIds) plugins[id] = pi;
}

std::shared_ptr<Node> PluginRegistry::createInstance(const QString& id) {
    auto f = plugins.find(id);
    if (f == plugins.end()) return nullptr;
    auto n = f.value()->createInstance();
    n->plugin = f.value();
    n->init();
    return n;
}

void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::shared_ptr<Node>)> f, Graph* g) {
    std::map<QString, std::map<QString, std::shared_ptr<PluginInfo>>> cm; // category map
    cm.try_emplace(""); // force empty category
    for (auto i : plugins) {
        if (i->hidden) continue;
        cm.try_emplace(i->category);
        cm[i->category][i->displayName] = i;
    }

    // I/O ports
    if (g) {
        auto* mio = m->addMenu("I/O Port");
        //auto* mi = mio->addMenu("Input");
        //auto* mo = mio->addMenu("Output");
        Port::DataType d[] {Port::Audio, Port::Command};

        for (auto dt : d) {
            auto* mi = mio->addMenu(QString("&%1 In").arg(Util::enumName(dt)));
            auto* mo = mio->addMenu(QString("&%1 Out").arg(Util::enumName(dt)));
            //mi->setStyleSheet("QMenu { menu-scrollable: 1; }");
            //mi->setFixedHeight(256);

            for (int ih = 0; ih < 16; ih++) {
                QString n = QString::number(ih, 16).toUpper();
                auto* mis = mi->addMenu(QString(u8"%1​0-%1​F").arg(n));
                auto* mos = mo->addMenu(QString(u8"%1​0-%1​F").arg(n));
                for (int il = 0; il < 16; il++) {
                    int i = ih*16+il;
                    QString nn = Util::hex(i);
                    mis->addAction(nn, [f, dt, i] {
                        auto n = std::static_pointer_cast<IOPort>(createInstance("ioport"));
                        n->setPort(Port::Input, dt, static_cast<uint8_t>(i));
                        f(n);
                    })->setEnabled(!g->port(Port::Input, dt, static_cast<uint8_t>(i)));
                    mos->addAction(nn, [f, dt, i] {
                        auto n = std::static_pointer_cast<IOPort>(createInstance("ioport"));
                        n->setPort(Port::Output, dt, static_cast<uint8_t>(i));
                        f(n);
                    })->setEnabled(!g->port(Port::Output, dt, static_cast<uint8_t>(i)));
                }

            }
            //
        }

        m->addSeparator();
    }

    // populate priorities into menu
    for (auto& pc : priorityCategories) {
        if (auto c = cm.find(pc); c != cm.end()) {
            auto* ccm = m->addMenu(c->first);
            for (auto& i : c->second) {
                ccm->addAction(i.second->displayName, [f, pi = i.second] {
                    auto n = pi->createInstance();
                    n->plugin = pi;
                    n->init();
                    f(n);
                });
            }
            cm.erase(pc);
        }
    }

    // then any other category
    for (auto& c : cm) {
        if (c.first.isEmpty() || c.second.empty()) continue;
        auto* ccm = m->addMenu(c.first);
        for (auto& i : c.second) {
            ccm->addAction(i.second->displayName, [f, pi = i.second] {
                auto n = pi->createInstance();
                n->plugin = pi;
                n->init();
                f(n);
            });
        }
    }

    m->addSeparator();

    for (auto& i : cm[""]) m->addAction(i.second->displayName, [f, pi = i.second] {
        auto n = pi->createInstance();
        n->plugin = pi;
        n->init();
        f(n);
    });

}
