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

#include "nodelib/commandreader.h"

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

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

#include "util/strings.h"

#include <cmath>
#include <array>

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

namespace {
    bool _ = PluginRegistry::enqueueRegistration([] {
        auto i = std::make_shared<PluginInfo>();
        i->id = "plug:thicc";
        i->displayName = "THiCC";
        i->category = "Instrument";
        i->createInstance = []{ return std::make_shared<Thicc>(); };
        PluginRegistry::registerPlugin(i);
    });

    [[maybe_unused]] inline double wrap(double d) {
        while (true) {
            if (d > 1.0) d = (d - 2.0) * -1; //d-=2.0;
            else if (d < -1.0) d = (d + 2.0) * -1;
            else return d;
        }
    }
    [[maybe_unused]] inline double lerp(double a, double b, double p) {
        return b * p + a * (1.0 - p);
    }

    // silence qtcreator warnings about gcc optimize attributes
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wattributes"

    // polyBLEP algorithm (pulse antialiasing), slightly modified from https://www.kvraudio.com/forum/viewtopic.php?t=375517
    [[gnu::optimize("O3")]] double polyblep(double t, double dt) {
        // 0 <= t < 1
        if (t < dt) {
            t /= dt;
            return t+t - t*t - 1.0;
        }
        // -1 < t < 0
        else if (t > 1.0 - dt) {
            t = (t - 1.0) / dt;
            return t*t + t+t + 1.0;
        }
        // 0 otherwise
        return 0.0;
    }

    [[gnu::optimize("O3")]] double push(double in, double mod, double factor) {
        double s = in < 0 ? -1 : 1;
        in *= s;
        //if (mod < 0) mod = 1.0/-mod;
        //else mod += 1.0;
        mod = mod < 0 ? lerp(1.0, 1.0/factor, -mod) : lerp(1.0, factor, mod);
        return std::pow(in, mod)*s;
    }

    [[gnu::optimize("O3")]] double oscSaw(double phase, double delta, double mod) {
        phase = std::fmod(phase + 0.5, 1.0);
        double d = phase;// * 0.2 + (std::floor(phase*7.0) / 7.0 + (0.5/7.0)) * 0.8;
        d = d * 2.0 - 1.0;
        d = push(d, -mod, 5);
        d -= polyblep(phase, delta);
        return d;
    }

    [[gnu::optimize("O3")]] double oscSine(double phase, double, double mod) { return push(std::sin(phase*PI*2), -mod, 5); }

    [[gnu::optimize("O3")]] double oscPulse(double phase, double delta, double mod) {
        double duty = (mod+1.0)/2.0;
        double d = 1.0;
        if (std::fmod(phase, 1.0) >= duty) d = -1.0;
        d += polyblep(std::fmod(phase, 1.0), delta);
        d -= polyblep(std::fmod(phase + (1.0 - duty), 1.0), delta);
        return d;
    }


#pragma GCC diagnostic pop

    // for clang on freebsd (and possibly other non-apple llvm sources) it seems we need to specify more.
    // wave function list(s)
    const constexpr std::array<double(*)(double,double,double),3> waveFunc = {
        &oscSaw,
        &oscSine,
        &oscPulse,
    };
    const std::array<QString,3> waveName = {
        qs("saw"),
        qs("sine"),
        qs("pulse"),
    };
}

Thicc::Thicc() { }

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

    core.onNoteOn = [this](Note& note) {
        //qDebug() << "note on";
        note.adsr = adsr.normalized();
        note.scratch[0] = static_cast<double>(QRandomGenerator::global()->generate() % 573000);
    };

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

    core.processNote = [this](Note& note, AudioPort* p) {
        double freq;

        auto osc = waveFunc[static_cast<size_t>(wave)];

        int vc = voices;
        double vf = static_cast<double>(vc);
        double spr = std::pow(SEMI, detune/(vf/2.0));

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

            double n = note.effectiveNote();
            freq = 440.0 * std::pow(SEMI, n - (45+12*2));
            auto si = note.scratch[0];
            double delta = smpTime * freq;
            note.scratch[0] += delta;

            double o = 0;
            for (int i = 0; i < vc; i++) {
                auto ii = static_cast<double>(i);
                double pc = ii / vf; // phase coefficient
                double dc = ii - ((vf-1.0)/2.0); // detune coefficient
                double dm = std::pow(spr, dc);
                double sg = i % 2 == 0 ? 1.0 : -1.0;
                //o += (std::fmod(pc + si * dm, 1.0) * 2.0 - 1.0) * sg;
                o += osc(pc + si * dm, delta * dm, mod) * sg;

            }

            o /= 1.0 + ((vf - 1.0)/5.0);

            AudioFrame out = o;
            (*p)[i] += out.gainBalance(0, note.pan) * note.ampMult();
        }
    };
}

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

void Thicc::saveData(QCborMap& m) const {
    m[qs("adsr")] = adsr;

    m[qs("wave")] = wave;
    m[qs("voices")] = voices;
    m[qs("mod")] = mod;
    m[qs("detune")] = detune;
}

void Thicc::loadData(const QCborMap& m) {
    adsr = m.value("adsr");

    wave = static_cast<int>(m.value("wave").toInteger(wave));
    voices = static_cast<int>(m.value("voices").toInteger(voices));
    mod = m.value("mod").toDouble(mod);
    detune = m.value("detune").toDouble(detune);
}

void Thicc::onGadgetCreated() {
    auto wn = [](size_t i) { if (i >= waveName.size()) return QString::number(i); return waveName[i]; };

    auto l = new LayoutGadget(obj);

    (new KnobGadget(l))->bind(wave)->setLabel(qs("Wave"))->setTextFunc(wn)->setRange(0, waveFunc.size()-1, 1, KnobGadget::BigStep);
    (new KnobGadget(l))->bind(mod)->setLabel(qs("W. Mod"))->setRange(-1.0, 1.0, 0.01);
    l->addSpacer();
    (new KnobGadget(l))->bind(voices)->setLabel(qs("Voices"))->setRange(1, 16, 1, KnobGadget::BigStep)->setDefault(1);
    (new KnobGadget(l))->bind(detune)->setLabel(qs("Detune"))->setRange(0.0, 1.0, 0.001);
    l->addSpacer();
    KnobGadget::autoCreate(l, adsr);
}
