#include "beatpad.h"
using Xybrid::Instruments::BeatPad;
using namespace Xybrid::NodeLib;
using Note = InstrumentCore::Note;
using namespace Xybrid::Data;

#include "nodelib/commandreader.h"
#include "nodelib/resampler.h"

#include "data/project.h"
#include "data/sample.h"

#include "data/porttypes.h"
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;

#include "ui/waveformpreviewwidget.h"

#include "ui/patchboard/nodeobject.h"
#include "ui/gadgets/knobgadget.h"
#include "ui/gadgets/selectorgadget.h"
#include "ui/gadgets/sampleselectorgadget.h"
using namespace Xybrid::UI;

#include "ui/patchboard/nodeuiscene.h"
#include "uisocket.h"

#include "util/strings.h"

#include <cmath>

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

#include <QMenu>
#include <QGraphicsProxyWidget>

#define qs QStringLiteral

namespace {
    bool _ = PluginRegistry::enqueueRegistration([] {
        auto i = std::make_shared<PluginInfo>();
        i->id = "plug:beatpad";
        i->displayName = "BeatPad";
        i->category = "Sampler";
        //i->hidden = true;
        i->createInstance = []{ return std::make_shared<BeatPad>(); };
        PluginRegistry::registerPlugin(i);
        //inf = i;
    });

}

BeatPad::BeatPad() {

}

void BeatPad::init() {
    addPort(Port::Input, Port::Command, 0);
    addPort(Port::Output, Port::Audio, 0);

    core.onNoteOn = [this](Note& note) {
        auto& data = *reinterpret_cast<NoteData*>(&note.scratch);
        new (&data) NoteData(); // construct in-place

        // look up config for note
        if (auto f = cfg.find(static_cast<int16_t>(note.note)); f != cfg.end()) {
            data.config = f->second;

            note.adsr = {0.0, 0.0, 1.0, shortStep}; // minimal tweening
            //
        } else core.deleteNote(note); // cancel note if no config found
    };

    core.onDeleteNote = [](Note& note) {
        auto& data = *reinterpret_cast<NoteData*>(&note.scratch);
        data.~NoteData(); // destroy
    };

    /*core.globalParam['Q'] = [](const ParamReader& pr) {
        qDebug() << "global recieved" << pr.param() << pr.val();
        return true;
    };*/

    core.processNote = [this](Note& note, AudioPort* p) {
        auto& data = *reinterpret_cast<NoteData*>(&note.scratch);
        if (!data.config) return core.deleteNote(note);
        auto smp = data.config->smp.lock();
        if (!smp) return core.deleteNote(note);

        double rate = static_cast<double>(smp->sampleRate) / static_cast<double>(audioEngine->curSampleRate());
        auto start = data.config->start;
        if (start < 0) start = 0;
        auto end = data.config->end;
        if (end < 0) end = smp->length();
        else end = std::min(end, static_cast<decltype(end)>(smp->length()));

        size_t ts = p->size;
        for (size_t i = 0; i < ts; i++) {
            core.advanceNote(note);

            // actual sample pos
            double sp = static_cast<double>(start) + data.sampleTime * rate;

            double ip = std::floor(sp);
            double fp = sp - ip;
            size_t lutIndex = static_cast<size_t>(fp*NodeLib::LUT_STEPS) % NodeLib::LUT_STEPS;
            auto& pt = NodeLib::resamplerLUT[lutIndex];
            AudioFrame out(0.0);
            auto ii = static_cast<ptrdiff_t>(ip) - 3;
            for (size_t i = 0; i < 8; i++) {
                auto si = ii+static_cast<ptrdiff_t>(i);
                if (si >= start && si < static_cast<ptrdiff_t>(end)) out += (*smp)[static_cast<size_t>(si)] * pt[i];
            }

            // stuff

            (*p)[i] += out.gainBalance(0, note.pan) * note.ampMult();
            data.sampleTime += 1;
        }
    };
}

void BeatPad::reset() { core.reset(); }
void BeatPad::release() { core.release(); }
void BeatPad::process() { core.process(this); }

void BeatPad::saveData(QCborMap& m) const {
    QCborMap cm;
    for (auto c : cfg) {
        if (auto smp = c.second->smp.lock(); smp) {
            QCborMap e;
            e[qs("sample")] = QCborValue(smp->uuid);
            e[qs("start")] = static_cast<qint64>(c.second->start);
            e[qs("end")] = static_cast<qint64>(c.second->end);
            cm[c.first] = e;
            smp->markForExport();
        }
    }
    m[qs("notecfg")] = cm;
}

void BeatPad::loadData(const QCborMap& m) {
    for (auto ce : m.value("notecfg").toMap()) {
        auto cm = ce.second.toMap();
        auto id = cm.value("sample").toUuid();
        if (auto f = project->samples.find(id); f != project->samples.end()) {
            auto c = std::make_shared<NoteConfig>();
            c->smp = f.value();
            c->start = cm.value("start").toInteger(-1);
            c->end = cm.value("end").toInteger(-1);
            cfg[static_cast<int16_t>(ce.first.toInteger())] = c;
        }
    }
}

void BeatPad::onGadgetCreated() {
    if (!obj) return;

    //
}

void BeatPad::onDoubleClick() { emit project->socket->openNodeUI(this); }
void BeatPad::initUI(NodeUIScene* scene) {
    // define and create state management struct
    // (lambda captures will end up managing the lifetime of this thing)
    struct UIState {
        std::shared_ptr<NoteConfig> cfg;
        int16_t note = -1;

        std::function<void(int16_t)> selectNote;
        std::function<void(std::shared_ptr<Sample>)> setSample;

        //UIState() { qDebug() << "constructing ui"; }
        //~UIState() { qDebug() << "deconstructing ui"; }
    };
    auto state = scene->makeStateObject<UIState>();

    // set up gadgets
    auto noteSelector = new SelectorGadget();
    scene->addItem(noteSelector);
    noteSelector->setPos(0, 0);
    noteSelector->setWidth(320);

    noteSelector->fGetList = [=] {
        std::vector<SelectorGadget::Entry> v;
        v.reserve(cfg.size());
        for (int16_t i = 0; i < 12*8; i++) {
            if (auto f = cfg.find(i); f != cfg.end()) {
                auto c = f->second;
                QString n;
                if (auto smp = c->smp.lock(); smp) n = smp->name.section('/', -1, -1);
                v.push_back({ f->first, qs("%1 %2").arg(Util::noteName(i)).arg(n) });
            }
        }
        return v;
    };
    noteSelector->fEditMenu = [=](QMenu* m) {
        auto first = m->actions().at(0);
        auto mAdd = new QMenu("Add...", m);
        m->insertMenu(first, mAdd);
        m->insertSeparator(first);
        for (int16_t oct = 0; oct < 8; oct++) {
            auto mo = mAdd->addMenu(qs("Octave %1").arg(oct));
            for (int16_t i = 0; i < 12; i++) {
                int16_t n = oct*12+i;
                if (auto f = cfg.find(n); f != cfg.end()) mo->addAction(Util::noteName(n))->setDisabled(true);
                else mo->addAction(Util::noteName(n), [=] { state->selectNote(n); });
            }
        }
    };

    auto sampleSelector = new SampleSelectorGadget(project);
    scene->addItem(sampleSelector);
    sampleSelector->setPos(0, 28);
    sampleSelector->setSize(320, 96);

    // create functions now that all UI elements exist to be referenced
    state->selectNote = [=](int16_t n) {
        if (auto f = cfg.find(n); f != cfg.end()) state->cfg = f->second;
        else state->cfg = std::make_shared<NoteConfig>();
        state->note = n;
        auto smp = state->cfg->smp.lock();
        sampleSelector->setSample(smp);

        noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n)).arg(smp ? smp->name.section('/', -1, -1) : "")}, false);
    };
    state->setSample = [=](std::shared_ptr<Sample> smp) {
        state->cfg->smp = smp;
        auto n = state->note;
        if (smp) cfg[n] = state->cfg;
        else if (auto f = cfg.find(n); f != cfg.end()) cfg.erase(f);

        noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n)).arg(smp ? smp->name : "")}, false);
    };

    // hook up relevantsignals
    QObject::connect(scene, &NodeUIScene::notePreview, [=](int16_t note) { state->selectNote(note); });
    QObject::connect(noteSelector, &SelectorGadget::onSelect, [=](auto e) { state->selectNote(e.first); });
    QObject::connect(sampleSelector, &SampleSelectorGadget::sampleSelected, [=](auto smp) { state->setSample(smp); });

    // and lastly, select C-5 by default
    state->selectNote(60);
}
