#include "delay.h"
using Xybrid::Effects::Delay;
using namespace Xybrid::Data;

#include "nodelib/basics.h"
using namespace Xybrid::NodeLib;

#include "data/audioframe.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/togglegadget.h"
#include "ui/gadgets/knobgadget.h"
using namespace Xybrid::UI;

#include <cmath>

#include <QCborMap>

#define qs QStringLiteral

namespace {
    bool _ = PluginRegistry::enqueueRegistration([] {
        auto i = std::make_shared<PluginInfo>();
        i->id = "gadget:delay";
        i->displayName = "Delay";
        i->category = "Effect";
        i->createInstance = []{ return std::make_shared<Delay>(); };
        PluginRegistry::registerPlugin(i);
    });
}

Delay::Delay() { }

void Delay::init() {
    addPort(Port::Input, Port::Audio, 0);
    addPort(Port::Output, Port::Audio, 0);
}

void Delay::reset() {
    release();
    auto sr = audioEngine->curSampleRate();
    buf.setCapacity(sr * 5); // five seconds of buffer
}

void Delay::release() {
    buf.clear();
    buf.setCapacity(0);
}

void Delay::process() {
    // precalculate actual multipliers from volume knobs
    auto delayMult = std::pow(amount, 2);
    auto fbMult = std::pow(feedback, 2);

    // calculate number of frames ahead to write to the buffer
    int frames = static_cast<int>(delayTime * static_cast<double>(audioEngine->curSampleRate()) * (timeInBeats ? (60.0 / audioEngine->curTempo()) : 1.0));

    // enlarge buffer if too small
    if (auto mc = frames+1; buf.capacity() < mc) buf.setCapacity(mc);
    while (buf.size() <= frames) buf.append({0.0, 0.0}); // pack to fit

    auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
    auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
    in->pull();
    out->pull();

    size_t ts = audioEngine->curTickSize();
    for (size_t f = 0; f < ts; f++) {
        AudioFrame fCurrent = (*in)[f];
        auto fOut = buf.takeFirst();
        if (!buf.areIndexesValid()) buf.normalizeIndexes(); // make sure we can actually reach the point we need
        int i = frames + buf.firstIndex();
        buf[i] += (fCurrent * delayMult) + (fOut * fbMult);

        (*out)[f] = fCurrent + fOut;
    }
}

void Delay::saveData(QCborMap& m) const {
    m[qs("time")] = QCborValue(delayTime);
    m[qs("inBeats")] = QCborValue(timeInBeats);
    m[qs("amount")] = QCborValue(amount);
    m[qs("feedback")] = QCborValue(feedback);
}

void Delay::loadData(const QCborMap& m) {
    delayTime = m.value("time").toDouble(delayTime);
    timeInBeats = m.value("inBeats").toBool(timeInBeats);
    amount = m.value("amount").toDouble(amount);
    feedback = m.value("feedback").toDouble(feedback);
}

void Delay::onGadgetCreated() {
    if (!obj) return;
    auto l = new LayoutGadget(obj);

    (new KnobGadget(l))->bind(delayTime)->setLabel(qs("Time"))->setRange(0.0, 5.0, 0.001)->setDefault(0.5);
    (new ToggleGadget(l))->bind(timeInBeats)->setColor({191, 127, 255})->setToolTip(qs("BPM-relative"));
    l->addSpacer();
    (new KnobGadget(l))->bind(amount)->setLabel(qs("Amount"))->setDefault(0.5);
    l->addSpacer();
    (new KnobGadget(l))->bind(feedback)->setLabel(qs("Feedback"))->setDefault(0.0);
}
