#include "instrumentcore.h"
using namespace Xybrid::NodeLib;
using Note = InstrumentCore::Note;
using Tween = InstrumentCore::Tween;

#include "nodelib/commandreader.h"

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

#include "audio/audioengine.h"
using namespace Xybrid::Audio;

#include <QDebug>

Note::Note(InstrumentCore* core, uint16_t id) {
    this->core = core;
    this->id = id;
}

InstrumentCore::~InstrumentCore() {
    release();
}

void InstrumentCore::release() {
    if (onDeleteNote) for (auto n : activeNotes) onDeleteNote(n.second); // make sure any plugin-side cleanup is done
    activeNotes.clear();
    activeTweens.clear();
}

void InstrumentCore::reset() {
    release();
    activeNotes.reserve(16+1);
    activeTweens.reserve(16*2+1);

    smpTime = 1.0 / audioEngine->curSampleRate();
    time = 0;

    volume = 1.0;
}

void InstrumentCore::process(Node* n) {
    if (!n) return;
    auto i = std::static_pointer_cast<CommandPort>(n->port(Port::Input, Port::Command, 0));
    auto o = std::static_pointer_cast<AudioPort>(n->port(Port::Output, Port::Audio, 0));
    if (!i) return;
    process(i.get(), o.get());
}

namespace {
    double adsrVol(const ADSR& adsr, uint8_t phase, double time) {
        switch(phase) {
        case 0: {
            if (adsr.a == 0) return 1.0;
            double a = 1.0 - std::clamp(time / adsr.a, 0.0, 1.0);
            a *= a;
            a *= a;
            return 1.0 - a;
        }
        case 1: {
            if (adsr.d == 0) return adsr.s;
            double sp = (1.0 - std::clamp(time / adsr.d, 0.0, 1.0));
            sp *= sp;
            return adsr.s + sp * (1.0 - adsr.s);
        }
        case 2: {
            if (adsr.r == 0) return 0.0;
            return (1.0 - std::clamp(time / adsr.r, 0.0, 1.0)) * adsr.s;
        }
        default:
            return 0.0;
        }
    }

    struct NoteIntern {
        bool markedForDeletion = false;

        decltype(InstrumentCore::activeTweens.equal_range(0)) tweenSet;
    };
}

void InstrumentCore::process(CommandPort* i, AudioPort* o) {
    // first, parse through commands
    auto cr = CommandReader(i);
    while (++cr) {
        uint16_t id = cr.noteId();
        int16_t n = cr.note();
        Note* notePtr = nullptr;
        if (n > -1) {
            auto sc = activeNotes.try_emplace(id, this, id);
            auto& note = sc.first->second;
            notePtr = &note;
            //auto& note = sc.first->second;
            if (!sc.second) {
                removeTween(note, &note.note); // stop any note-value tweens
                if (note.adsrPhase == 2) { note = Note(this, id); goto forceRetrigger; } // reinstantiate on replace
                if (cr.numParams() > 0 && cr.param(0) == 't') {
                    startTween(note, &note.note, n, 0, cr.val(0));
                } else note.note = n;
                if (onNoteLegato) onNoteLegato(note);
            } else {
            forceRetrigger:
                note.note = n;
                note.time = note.adsrTime = -smpTime; // compensate for first-advance
                if (onNoteOn) onNoteOn(note);
            }
        } else { // existing note
            if (auto ni = activeNotes.find(id); ni != activeNotes.end()) {
                auto& note = ni->second;
                notePtr = &note;
                if (n < -1) { // note off
                    note.adsr.s = adsrVol(note.adsr, note.adsrPhase, note.adsrTime);
                    note.adsrPhase = 2;
                    note.adsrTime = -smpTime;
                    if (n == -3) note.adsr.r = shortStep;
                    if (onNoteOff) onNoteOff(note, n == -3);
                }
            }
        }

        // time for params
        auto pr = ParamReader(cr);
        while (++pr) {
            // param filter first
            if (paramFilter && paramFilter(notePtr, pr)) continue;

            auto p = pr.param();
            auto v = pr.val();
            if (p == 't' || p == ',') continue;

            // TODO: paramFilter

            if (notePtr) { // try local params before globals
                auto& note = *notePtr;

                // local param hook
                if (auto pp = localParam.find(p); pp != localParam.end() && pp->second(note, pr)) continue;

                // built-in locals
                switch(p) {
                case 'v': {
                    double vol = (1.0*v) / 255.0;
                    auto t = pr.tween();
                    if (t <= 0) {
                        removeTween(note, &note.volume);
                        note.volume = vol;
                    } else startTween(note, &note.volume, vol, shortStep, t);
                    continue;
                }
                case 'p': {
                    double pan = std::clamp((1.0*static_cast<int8_t>(v)) / 127.0, -1.0, 1.0);
                    auto t = pr.tween();
                    if (t <= 0) {
                        removeTween(note, &note.pan);
                        note.pan = pan;
                    } else startTween(note, &note.pan, pan, shortStep, t);
                    continue;
                }
                case 'g': // g/G - glissando
                    [[fallthrough]];
                case 'G': {
                    double nd = v;
                    if (p == 'g') nd *= -1.0;
                    auto t = pr.tween();
                    if (t <= 0) {
                        removeTween(note, &note.note);
                        note.note += nd;
                    } else startTween(note, &note.note, note.note + nd, shortStep, t);
                    continue;
                }
                case 'r': { // vibrato
                    auto v2 = pr.next();
                    Tween* t = findTween(note, &note.noteAdd);
                    if (!t) {
                        if (v == 0) break; // don't bother with a stopped tween
                        t = &(startTween(note, &note.noteAdd, 0.25, 0, v));
                        t->flags |= 2;
                    } else if (v == 0 && v2 == -1) v2 = 0; // 00 with no second param to stop
                    if (v > 0) t->ticksLeft = v;
                    if (v2 != -1) t->valEnd = (1.0/16.0)*v2;
                    continue;
                }
                default:
                    break;
                }
            }

            // global param hook
            if (auto pp = globalParam.find(p); pp != globalParam.end() && pp->second(pr)) continue;

            // built-in globals
            switch(p) {
            case 'V': {
                volume = (1.0*v) / 255.0;
                continue;
            }
            default:
                break;
            }
        }
    }

    // then do the thing
    if (o) o->pull();

    double tickTime = smpTime * audioEngine->curTickSize();

    if (processNote) {
        for (auto p = activeNotes.begin(); p != activeNotes.end(); ) {
            auto& note = p->second;
            NoteIntern n;
            note.intern = &n;

            n.tweenSet = activeTweens.equal_range(note.id);
            for (auto it = n.tweenSet.first; it != n.tweenSet.second; ++it) it->second.startTick(note, tickTime);

            processNote(note, o);
            if (n.markedForDeletion) {
                if (onDeleteNote) onDeleteNote(note);
                activeTweens.erase(note.id);
                p = activeNotes.erase(p);
                continue;
            }
            // stuff
            for (auto it = n.tweenSet.first; it != n.tweenSet.second; ) {
                if (it->second.flags & 1) it = activeTweens.erase(it);
                else ++it;
            }

            note.intern = nullptr;
            ++p;
        }
    }
    time += tickTime;
}

void InstrumentCore::advanceNote(Note& n) {
    auto& ni = *reinterpret_cast<NoteIntern*>(n.intern);
    n.time += smpTime;
    n.adsrTime += smpTime;

    for (auto it = ni.tweenSet.first; it != ni.tweenSet.second; ++it) it->second.process(n, smpTime);

    if (n.adsrPhase == 0) {
        if (n.adsrTime > n.adsr.a) {
            n.adsrPhase++;
            n.adsrTime -= n.adsr.a;
        }
    }
    if (n.adsrPhase == 2) {
        if (n.adsrTime >= n.adsr.r) deleteNote(n);
    }

}

void InstrumentCore::deleteNote(Note& n) {
    if (n.intern != nullptr) {
        auto& ni = *reinterpret_cast<NoteIntern*>(n.intern);
        ni.markedForDeletion = true;
    } else {
        if (onDeleteNote) onDeleteNote(n);
        activeTweens.erase(n.id);
        activeNotes.erase(n.id);
    }
}

void InstrumentCore::removeTween(InstrumentCore::Note& n, double* op) {
    auto r = activeTweens.equal_range(n.id);
    for (auto it = r.first; it != r.second; ) {
        if (it->second.op == op) it = activeTweens.erase(it);
        else ++it;
    }
}
void InstrumentCore::removeTweens(InstrumentCore::Note& n) { activeTweens.erase(n.id); }

Tween* InstrumentCore::findTween(InstrumentCore::Note& n, double* op) {
    auto r = activeTweens.equal_range(n.id);
    for (auto it = r.first; it != r.second; ++it) {
        if (it->second.op == op) return &(it->second);
    }
    return nullptr;
}

Tween& InstrumentCore::startTween(InstrumentCore::Note& n, double* op, double val, double time, int16_t ticks) {
    // remove anything already operating on the same note
    removeTween(n, op);
    auto it = activeTweens.emplace(std::make_pair(n.id, Tween(n, op, val, time, ticks)));
    return it->second;
}

double Note::effectiveNote() const {
    return note + noteAdd;
}

double Note::ampMult() const {
    double a = adsrVol(adsr, adsrPhase, adsrTime) * volume * core->volume;
    return a*a; // most synthesizers use a curve of 40log(vol) dB... which simplifies to vol^2
}

Tween::Tween(InstrumentCore::Note& n, double* op, double val, double time, int16_t ticks) {
    this->noteId = n.id;
    this->op = op;
    valStart = *op;
    valEnd = val;
    if (ticks >= 0) ticksLeft = ticks;
    else {
        timeStart = n.time;
        timeEnd = timeStart + time;
    }
}

void Tween::startTick(Note& n, double tickTime) {
    if (ticksLeft >= 0) {
        if (flags & 2) { // sine mode
            timeEnd = tickTime * ticksLeft;
        } else {
            timeStart = n.time;
            timeEnd = timeStart + tickTime * ticksLeft;
            valStart = *op;
            ticksLeft--;
        }
    }
}

void Tween::process(Note& n, double smpTime) {
    if (flags & 1) return; // already done
    if (flags & 2) { // sine mode
        if (timeEnd > 0) timeStart += smpTime / timeEnd;
        *op = std::sin(timeStart * PI*2) * valEnd;
        if (valEnd == 0.0) flags |= 1; // stop when magnitude set to 0
        return;
    }
    if (timeEnd == timeStart) {
        *op = valEnd; // instant
        flags |= 1;
        return;
    }
    double p = std::clamp((n.time - timeStart) / (timeEnd - timeStart), 0.0, 1.0);
    *op = valStart * (1.0-p) + valEnd * p;
    if (n.time >= timeEnd) flags |= 1; // mark finished
}
