#include "audioengine.h"
#include "data/project.h"
using namespace Xybrid::Audio;
using namespace Xybrid::Data;
#include "data/graph.h"
#include "data/porttypes.h"

#include "mainwindow.h"
#include "uisocket.h"

#include <algorithm>
#include <cmath>

#include <QDebug>
#include <QThread>

// zero-initialize
AudioEngine* Xybrid::Audio::audioEngine = nullptr;

void AudioEngine::init() {
    if (audioEngine) return; // already set up

    // instantiate singleton
    QThread* thread = new QThread;
    audioEngine = new AudioEngine(nullptr);
    audioEngine->moveToThread(thread);
    audioEngine->thread = thread;

    // hook up signals
    // ...

    // and off to the races
    thread->start();
    //thread->setPriority(QThread::TimeCriticalPriority);
    QMetaObject::invokeMethod(audioEngine, &AudioEngine::postInit, Qt::QueuedConnection);
}
void AudioEngine::postInit() {
    open(QIODevice::ReadOnly);

    // set up buffer for per-tick allocation
    tickBuf = std::make_unique<size_t[]>(tickBufSize/sizeof(size_t)); // aligned to size_t
    tickBufPtr = tickBuf.get();
    tickBufEnd = tickBufPtr+tickBufSize;

    buf.reserve(1024); // 1kb isn't much to make sure it's super unlikely to have to reallocate
    chTrack.reserve(256);
    noteEndQueue.reserve(256);
    nameTrack.reserve(64+1); // +1 to make extra sure it doesn't rehash later
}

void* AudioEngine::tickAlloc(size_t size) {
    if (auto r = size % sizeof(size_t); r != 0) size += sizeof(size_t) - r; // pad
    auto n = tickBufPtr.fetch_add(static_cast<ptrdiff_t>(size));
    if (n + size > tickBufEnd) qWarning() << "Tick buffer overrun!";
    return n;
}

AudioEngine::AudioEngine(QObject *parent) : QIODevice(parent) { }

void AudioEngine::initAudio(bool startNow) {
    if (!output) {
        const QAudioDeviceInfo& deviceInfo = QAudioDeviceInfo::defaultOutputDevice();

        QAudioFormat format;
        format.setSampleRate(48000);
        format.setChannelCount(2);
        format.setSampleSize(16);
        format.setCodec("audio/pcm");
        format.setByteOrder(QAudioFormat::LittleEndian);
        format.setSampleType(QAudioFormat::SignedInt);

        if (!deviceInfo.isFormatSupported(format)) {
            qWarning() << "Default format not supported - trying to use nearest";
            format = deviceInfo.nearestFormat(format);
        }
        sampleRate = format.sampleRate();

        output.reset(new QAudioOutput(deviceInfo, format));
        output->setObjectName("Xybrid"); // if Qt ever implements naming the stream this way, WE'LL BE READY
        output->setBufferSize(static_cast<int>(sampleRate*4*100.0/1000.0)); // 100ms
    }

    if (startNow) output->start();
}

void AudioEngine::deinitAudio() {
    if (output) {
        output->stop();
        output.reset();
    }
}

void AudioEngine::play(std::shared_ptr<Project> p) {
    QMetaObject::invokeMethod(this, [this, p]() {
        if (!p) return; // nope
        project = p;

        // stop and reset, then init playback
        queueValid = false;
        queue.clear();
        portLastNoteId.fill(0);
        project->rootGraph->reset();

        initAudio();
        for (auto& b : buffer) {
            b.clear();
            b.reserve(static_cast<size_t>(sampleRate/4));
        }

        seqPos = -1;
        tempo = project->tempo;
        tickAcc = 0;

        output->start(this);

        //tickId = 0; // actually, no reason to reset this

        mode = Playing;
        emit this->playbackModeChanged();
    }, Qt::QueuedConnection);
}

void AudioEngine::stop() {
    QMetaObject::invokeMethod(this, [this]() {
        project = nullptr;
        queueValid = false;
        queue.clear();
        deinitAudio();
        mode = Stopped;
        emit this->playbackModeChanged();
    }, Qt::QueuedConnection);
}

void AudioEngine::buildQueue() {
    queue.clear();
    // stuff
    std::deque<std::shared_ptr<Node>> q1, q2;
    auto* qCurrent = &q1;
    auto* qNext = &q2;

    if (auto p = project->rootGraph->port(Port::Output, Port::Audio, 0); p)
        if (auto pt = p->passthroughTo.lock(); pt)
            if (auto ptn = pt->owner.lock(); ptn)
                qCurrent->push_back(ptn);

    // T_ODO: make this not process things the weird way around
    // oh, it's working properly... it just processing subgraph before its internally-connected *inputs*
    while (!qCurrent->empty()) {
        // ... this could be made more efficient with some redundancy checking, but whatever
        for (auto n : *qCurrent) {
            queue.push_front(n); // add to actual queue
            for (auto p1 : n->inputs) { // data types...
                for (auto p2 : p1.second) { // ports...
                    for (auto p3 : p2.second->connections) { // connected ports!
                        auto pc = p3.lock();
                        if (!pc) continue;
                        auto pcn = pc->owner.lock();
                        if (!pcn) continue;
                        qNext->push_back(pcn);
                        if (auto pp = pc->passthroughTo.lock(); pp) {
                            // if it has a passthrough, also place passthrough's owner after (before)
                            if (auto ppp = pp->owner.lock(); ppp) qNext->push_back(ppp);
                        }

                    }
                }
            }
        }

        qCurrent->clear();
        std::swap(qCurrent, qNext);
    }

    queueValid = true;
}

qint64 AudioEngine::readData(char *data, qint64 maxlen) {
    const constexpr qint64 smp = 2;
    const constexpr qint64 stride = smp*2;
    qint64 sr = maxlen;

    while (sr >= stride) {
        if (bufPos >= buffer[0].size()) {
            nextTick(); // process next tick when end of buffer reached
            if (sr < maxlen) break; // if not the start of the buffer, yield so previewing works
        }
        //if (bufPos >= buffer[0].size()) break; // if held up still, let the event loop run another cycle

        // convert non-interleaved floating point into interleaved int16
        int16_t* l = reinterpret_cast<int16_t*>(data);
        int16_t* r = reinterpret_cast<int16_t*>(data+smp);
        *l = static_cast<int16_t>(buffer[0][bufPos] * 32767);
        *r = static_cast<int16_t>(buffer[1][bufPos] * 32767);

        bufPos++;
        data += stride;
        sr -= stride;
    }

    return maxlen - sr;
}

void AudioEngine::nextTick() {
    bufPos = 0;

    if (mode == Paused) { // simplest case, just give a 100ms empty buffer
        buffer[0].clear();
        buffer[1].clear();
        buffer[0].resize(static_cast<size_t>(sampleRate/10));
        buffer[1].resize(static_cast<size_t>(sampleRate/10));
    } else if (mode == Previewing) {
        // NYI
        // reset raw buffer
        tickBufPtr = tickBuf.get();
        tickId++;
    } else if (mode == Playing) {
        // reset raw buffer
        tickBufPtr = tickBuf.get();
        tickId++;

        // empty out last tick
        buffer[0].clear();
        buffer[1].clear();

        if (!queueValid) buildQueue();

        Pattern* p = nullptr;
        Pattern* pOld = nullptr;
        auto setP = [&] {
            if (seqPos >= 0 && seqPos < static_cast<int>(project->sequence.size())) p = project->sequence[static_cast<size_t>(seqPos)];
            else p = nullptr;
        };
        setP();

        bool newRow = false;
        bool newPattern = false;
        auto advanceSeq = [&] {
            pOld = p;
            p = nullptr;
            int tries = 0;
            while (!p) {
                seqPos = (seqPos+1) % static_cast<int>(project->sequence.size());
                setP();
                if (++tries > 25) return; // either you have 25 separators in a row, or you have no patterns
            }
            curRow = 0;

            // set pattern things
            if (p->tempo > 0) tempo = p->tempo;

            newPattern = true;
        };
        auto advanceRow = [&] {
            curTick = 0;
            curRow++;
            if (!p || curRow >= p->rows) advanceSeq();
            MainWindow* w = project->socket->window;
            QMetaObject::invokeMethod(w, [this, w]{ w->playbackPosition(seqPos, curRow); }, Qt::QueuedConnection);

            // process global commands first
            for (int c = 0; c < static_cast<int>(p->numChannels()); c++) {
                if (auto& row = p->rowAt(c, curRow); row.port == -2 && row.params) {
                    for (auto p : *row.params) {
                        if (p[0] == 't' && p[1] > 0) tempo = p[1];
                    }
                }
            }

            newRow = true;

            // assemble command buffers

            noteEndQueue.clear();
            if (newPattern) { // notes on named channels carry over to their matching channel on the new pattern (if present); everything else is note-offed
                if (pOld) {
                    size_t cs = pOld->channels.size();
                    for (size_t c = 0; c < cs; c++) {
                        auto& ch = pOld->channels[c];
                        if (!chTrack[c].valid) continue; // skip notes that aren't actually playing
                        if (ch.name.empty()) noteEndQueue.push_back(chTrack[c]); // end notes in unnamed channels right away
                        else nameTrack[&ch.name] = chTrack[c]; // otherwise keep track for later
                    }
                }
                chTrack.clear(); // clear and prepare channel note tracking
                chTrack.resize(p->channels.size());
                if (nameTrack.size() > 0) { // if there were any
                    size_t cs = p->channels.size();
                    for (size_t c = 0; c < cs; c++) {
                        auto& ch = p->channels[c];
                        if (ch.name.empty()) continue;
                        if (auto nt = nameTrack.find(&ch.name); nt != nameTrack.end() && nt->second.valid) {
                            chTrack[c] = nt->second; // carry over
                            nt->second.valid = false; // and invalidate
                        }
                    }
                    // dump remainder into note end
                    for (auto nt : nameTrack) if (nt.second.valid) noteEndQueue.push_back(nt.second);
                }
                nameTrack.clear();
            }

            int chs = static_cast<int>(p->channels.size());
            for (int c = 0; c < chs; c++) {
                auto& ct = chTrack[static_cast<size_t>(c)];
                if (!ct.valid) continue; // no saved note
                auto& r = p->rowAt(c, curRow);
                if (r.note != -1 && r.port >= 0 && r.port != ct.port) { // if explicitly specified for a different port...
                    noteEndQueue.push_back(ct); // old note overwritten
                    ct.valid = false;
                }
            }

            auto& cpm = project->rootGraph->inputs[Port::Command];
            for (auto p_ : cpm) {
                auto* pt = static_cast<CommandPort*>(p_.second.get());
                //if (pt->passthroughTo.lock()->connections.empty()) continue; // port isn't hooked up to anything
                uint8_t idx = pt->index;
                buf.clear();

                for (auto& ne : noteEndQueue) {
                    if (ne.valid && ne.port == idx) {
                        size_t bi = buf.size();
                        buf.resize(bi+5, 0);
                        reinterpret_cast<uint16_t&>(buf[bi]) = ne.noteId; // trigger on note id...
                        reinterpret_cast<int16_t&>(buf[bi+2]) = -2; // note off
                    }
                }

                for (int c = 0; c < chs; c++) {
                    auto& r = p->rowAt(c, curRow);
                    auto& ct = chTrack[static_cast<size_t>(c)];
                    int16_t port = r.port;
                    if (port < 0 && ct.valid) port = ct.port; // assume last port used on channel if not specified
                    if (port != idx) continue;

                    NoteInfo rpl; // default initialization, invalid

                    if (r.note >= 0) {
                        if (ct.valid) rpl = ct; // replace
                        ct = NoteInfo(idx, portLastNoteId[idx]++);
                    } else if (r.note <= -2 && ct.valid) {
                        ct.valid = false; // invalidate it here but leave note id intact
                        // this condition will allow you to note-off the same note id multiple times but anything
                        // that takes offense to that is a bug anyway
                    }

                    size_t bi = buf.size();
                    buf.resize(bi+5, 0);
                    reinterpret_cast<uint16_t&>(buf[bi]) = ct.noteId; // either new note, or note-off on old one
                    reinterpret_cast<int16_t&>(buf[bi+2]) = r.note; // shove note into vector
                    auto& np = buf[bi+4]; // number of params

                    if (r.params) {
                        for (auto& p : *r.params) {
                            if (p[0] == ' ') continue; // ignore struts
                            buf.push_back(p[0]);
                            buf.push_back(p[1]);
                            np++;
                        }
                    }

                    if (rpl.valid) { // replacing old note on the same port and channel
                        bi = buf.size();
                        buf.resize(bi+5, 0);
                        reinterpret_cast<uint16_t&>(buf[bi]) = rpl.noteId; // trigger on note id...
                        reinterpret_cast<int16_t&>(buf[bi+2]) = -2; // note off
                    }
                }

                //qDebug() << "port" << idx << "data of size" << buf.size();
                pt->push(buf);
            }
        };

        curTick++;
        if (!p || curTick >= p->time.ticksPerRow) advanceRow();
        if (!p) return; // no patterns to be found, abort

        // (sample rate / seconds per beat) / ticks per beat
        double tickSize = (1.0 * sampleRate / (static_cast<double>(tempo)/60.0)) / (p->time.rowsPerBeat * p->time.ticksPerRow);
        tickSize += tickAcc; // add sample remainder from last tick
        double tickSf = std::floor(tickSize);
        tickAcc = tickSize - tickSf;
        size_t ts = static_cast<size_t>(tickSf);
        buffer[0].resize(ts);
        buffer[1].resize(ts);
        //qDebug() << "tick" << tickId << "contains"<<ts<<"samples";

        //qDebug() << "<tick start>";
        for (auto n : queue) if (!n->try_process()) qWarning() << "Dependency check failed in single threaded mode!";
        if (auto p = std::static_pointer_cast<AudioPort>(project->rootGraph->port(Port::Output, Port::Audio, 0)); p) {
            p->pull();
            size_t bufs = ts * sizeof(float);
            memcpy(buffer[0].data(), p->bufL, bufs);
            memcpy(buffer[1].data(), p->bufR, bufs);
            //p->bufL
        }
        //buffer[0].data()
        // test
        /*const double PI = std::atan(1)*4;
        const double SEMI = std::pow(2.0, 1.0/12.0);
        double time = 0;
        int note = curRow % 4;
        for (size_t i = 0; i < ts; i++) {

            buffer[0][i] = static_cast<float>(std::sin(time * PI*2 * 440 * std::pow(SEMI, -6 + note * 5)) * .25);
            buffer[1][i] = buffer[0][i];
            time += 1.0/sampleRate;
        }*/

    }

    // ...

    else { // old test code
        static double time = 0;
        const double PI = std::atan(1)*4;
        const double SEMI = std::pow(2.0, 1.0/12.0);

        for (size_t i = 0; i < buffer[0].size(); i++) {
            buffer[0][i] = static_cast<float>(std::sin(time * PI*2 * 440 * std::pow(SEMI, note - (45+12))) * .25);
            buffer[1][i] = buffer[0][i];
            //buffer[1][i] = static_cast<float>(std::sin(time * PI*2 * 440 * std::pow(SEMI, 3)) * .25);

            time += 1.0/sampleRate;
        }
    }
}
