#pragma once

#include <memory>
#include <list>
#include <vector>
#include <deque>
#include <unordered_map>
#include <atomic>
#include <array>
#include <QIODevice>
#include <QAudioOutput>
#include <QSemaphore>
#include <QWaitCondition>

class QThread;
namespace Xybrid::Data {
    class Project;
    class Pattern;
    class Node;
}
namespace Xybrid::Audio {
    class AudioWorkerCore {
    protected:
        __attribute__((always_inline)) inline void processQueue();
    };

    class AudioWorker;
    template <typename T> struct PointerCompare {
        bool operator()(T* a, T* b) const { return *a == *b; }
        //size_t operator()(T* a) const { return std::hash<T>()(*a); }
        size_t operator()(T* a) const { return qHash(*a); }
    };
    struct NoteInfo {
        bool valid = false;
        uint8_t port = 0;
        uint16_t noteId = 0;
        NoteInfo() = default;
        NoteInfo(uint8_t p, uint16_t nId) { valid = true; port = p; noteId = nId; }
    };
    class AudioEngine : public QIODevice, private AudioWorkerCore {
        friend class AudioWorker;
        friend class AudioWorkerCore;
        Q_OBJECT
        explicit AudioEngine(QObject *parent = nullptr);
    public:
        enum PlaybackMode : uint8_t {
            Stopped, // stopped
            Playing, // playing track
            Paused, // paused during playback
            Previewing, // instrument live preview
            Rendering, // rendering to file
        };
    private:
        QThread* thread;
        std::unique_ptr<QAudioOutput> output;
        int sampleRate = 48000;

        std::vector<float> buffer[2];
        size_t bufPos = 0;

        static const constexpr size_t tickBufSize = (1024*1024*5); // 5mb should be enough
        std::unique_ptr<size_t[]> tickBuf;
        std::atomic<size_t*> tickBufPtr;
        size_t* tickBufEnd;

        PlaybackMode mode = Stopped;
        size_t tickId = 0;
        std::shared_ptr<Data::Project> project;

        std::vector<AudioWorker*> workers;
        QSemaphore wsem;

        std::vector<std::shared_ptr<Data::Node>> queue;
        std::atomic_size_t queueIndex;
        bool queueValid;
        void buildQueue();

        std::array<uint16_t, 256> portLastNoteId;
        std::vector<NoteInfo> chTrack;
        std::vector<NoteInfo> noteEndQueue;
        std::unordered_map<QString*, NoteInfo, PointerCompare<QString>, PointerCompare<QString>> nameTrack;
        std::vector<uint8_t> buf; /// preallocated buffer for building commands

        uint8_t previewPort_ = 0;
        uint16_t previewNote_ = 0;
        Data::Node* previewNode = nullptr;

        // playback timing and position
        double tempo = 140.0;
        int seqPos;
        int curRow;
        int curTick;
        double tickAcc; // accumulator for tick remainder

        void postInit();
        void initAudio(bool startNow = false);
        void deinitAudio();
        Data::Pattern* findPattern(int = 0);
        void nextTick();
        void processNodes();
    public:
        static void init();
        inline constexpr PlaybackMode playbackMode() const { return mode; }
        inline constexpr const std::shared_ptr<Data::Project>& playingProject() const { return project; }
        void play(std::shared_ptr<Data::Project>, int fromPos = -1);
        void stop();
        uint16_t preview(std::shared_ptr<Data::Project>, int16_t port, int16_t note, uint16_t nId = 0, Data::Node* node = nullptr);
        void render(std::shared_ptr<Data::Project>, QString);
        inline uint8_t previewPort() const { return previewPort_; }

        inline void invalidateQueue(Data::Project* p) { if (p == project.get()) queueValid = false; }

        void* tickAlloc(size_t size);
        inline size_t curTickId() const { return tickId; }
        inline size_t curTickSize() const { return buffer[0].size(); }
        inline int curSampleRate() const { return sampleRate; }
        inline double curTempo() const { return tempo; }

        // QIODevice functions
        qint64 readData(char* data, qint64 maxlen) override;
        qint64 writeData(const char*, qint64) override { return 0; }
        qint64 bytesAvailable() const override { return 0; } // not actually used by QAudioOutput

    signals:
        void playbackModeChanged();

    public slots:
    };

    class AudioWorker : public QObject, private AudioWorkerCore {
        friend class AudioEngine;
        Q_OBJECT
        explicit AudioWorker(QObject* parent = nullptr);

        QThread* thread;
        QSemaphore signal;

        void invoke();
        [[noreturn]] void processLoop();
    };

    extern AudioEngine* audioEngine;
}
