#include "2x03.h"
using Xybrid::Instruments::I2x03;
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 <QDebug>
#include <QCborMap>
#include <QCborValue>
#include <QCborArray>

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

    std::unordered_map<int8_t, QString> waveNames = [] {
        std::unordered_map<int8_t, QString> m;

        m[-1] = "keep";
        m[0] = "p50";
        m[1] = "p25";
        m[2] = "p12.5";
        m[3] = "tri";
        m[4] = "saw";
        m[5] = "pwm.l";
        m[6] = "pwm.g";

        return m;
    }();

    // 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 oscPulse(double phase, double delta, double duty = 0.5) {
        //double duty = 0.5 + std::cos(time * 2.5) * (1 - 0.125*2) * 0.5;
        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;
    }

    [[gnu::optimize("O3")]] double oscTri(double phase) {
        phase = std::fmod(phase + 0.75, 1.0);
        phase = phase * 0.2 + (std::floor(phase*32.0) / 32.0) * 0.8;
        return std::abs(phase*2.0 - 1.0)*2.0 - 1.0;
    }

    [[gnu::optimize("O3")]] double oscSaw(double phase, double delta) {
        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 -= polyblep(phase, delta);
        return d;
    }

#pragma GCC diagnostic pop
}

I2x03::I2x03() {

}

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

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

    core.processNote = [this](Note& note, AudioPort* p) {
        double freq;// = 440.0 * std::pow(SEMI, enote - (45+12*2));

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

            double smp = 0.0;
            double n = note.effectiveNote() + detune;
            int8_t wave = this->wave;
            if (note.time < blipTime) {
                if (blipWave != -1) wave = blipWave;
                n += blipNote;
            }
            freq = 440.0 * std::pow(SEMI, n - (45+12*2));

            switch(wave) {
            case 0:
                smp = oscPulse(note.scratch[0], smpTime*freq, 0.5);
                break;
            case 1:
                smp = oscPulse(note.scratch[0], smpTime*freq, 0.25);
                break;
            case 2:
                smp = oscPulse(note.scratch[0], smpTime*freq, 0.125);
                break;
            case 3:
                smp = oscTri(note.scratch[0]);
                break;
            case 4:
                smp = oscSaw(note.scratch[0], smpTime*freq);
                break;
            case 5:
                smp = oscPulse(note.scratch[0], smpTime*freq, 0.5 + std::sin(PI*2 * (note.time / pwmTime + pwmPhase)) * pwmDepth * 0.5);
                break;
            case 6:
                smp = oscPulse(note.scratch[0], smpTime*freq, 0.5 + std::sin(PI*2 * ((core.globalTime() + smpTime * i) / pwmTime + pwmPhase)) * pwmDepth * 0.5);
                break;
            default:
                break;
            }
            smp *= note.ampMult();
            note.scratch[0] += smpTime * freq;
            auto pn = panSignal(smp, note.pan);
            p->bufL[i] += pn.first;
            p->bufR[i] += pn.second;
            //p->bufL[i] += static_cast<float>(smp);
            //p->bufR[i] += static_cast<float>(smp);
        }
    };
}

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

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

    m[qs("blipTime")] = blipTime;
    m[qs("blipWave")] = blipWave;
    m[qs("blipNote")] = blipNote;

    m[qs("pwmDepth")] = pwmDepth;
    m[qs("pwmTime")] = pwmTime;
    m[qs("pwmPhase")] = pwmPhase;
}

void I2x03::loadData(const QCborMap& m) {
    wave = static_cast<int8_t>(m.value("wave").toInteger(wave));
    detune = m.value("detune").toDouble(detune);
    adsr = m.value("adsr");

    blipTime = m.value("blipTime").toDouble(blipTime);
    blipWave = static_cast<int8_t>(m.value("blipWave").toInteger(blipWave));
    blipNote = static_cast<int>(m.value("blipNote").toInteger(blipNote));

    pwmDepth = m.value("pwmDepth").toDouble(pwmDepth);
    pwmTime = m.value("pwmTime").toDouble(pwmTime);
    pwmPhase = m.value("pwmPhase").toDouble(pwmPhase);
}

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

    auto wavetxt = [](double inp) {
        if (auto f = waveNames.find(static_cast<int8_t>(inp)); f != waveNames.end()) return f->second;
        return QString("?");
    };
    auto percenttxt = [](double d) {
        return QString("%1%").arg(d*100, 0);
    };

    auto ol = new LayoutGadget(obj, true);
    auto l1 = new LayoutGadget(ol);
    auto l2 = new LayoutGadget(ol);

    // first line
    (new KnobGadget(l1))->bind(wave)->setLabel(qs("Wave"))->setTextFunc(wavetxt)->setRange(0, 6, 1, KnobGadget::BigStep);
    (new KnobGadget(l1))->bind(detune)->setLabel(qs("Detune"))->setRange(-1.0, 1.0, 0.01);
    l1->addSpacer();
    KnobGadget::autoCreate(l1, adsr);

    // blip group
    (new KnobGadget(l2))->bind(blipTime)->setLabel(qs("Blip"))->setRange(0.0, 0.1, 0.001);
    (new KnobGadget(l2))->bind(blipWave)->setLabel(qs("Wave"))->setTextFunc(wavetxt)->setRange(-1, 4, 1, KnobGadget::BigStep)->setDefault(-1);
    (new KnobGadget(l2))->bind(blipNote)->setLabel(qs("Note"))->setRange(-12, 12, 1);

    l2->addSpacer();

    // pwm group
    (new KnobGadget(l2))->bind(pwmDepth)->setLabel(qs("PWM"))->setTextFunc(percenttxt)->setRange(0.0, 1.0, 0.01)->setDefault(0.75);
    (new KnobGadget(l2))->bind(pwmTime)->setLabel(qs("Time"))->setRange(0.01, 5.0, 0.01)->setDefault(3.0);
    (new KnobGadget(l2))->bind(pwmPhase)->setLabel(qs("Phase"))->setRange(0.0, 1.0, 0.01);

    //
}
