#pragma once

#include <stdint.h>

#include <limits>
#include <memory>
#include <list>
#include <vector>
#include <string>
#include <array>
#include <QString>

namespace Xybrid::Data {
    class Project;
    struct TimeSignature {
        int beatsPerMeasure = 4;
        int rowsPerBeat = 4;
        int ticksPerRow = 6;

        TimeSignature() = default;
        TimeSignature(int b, int r, int t) : beatsPerMeasure(b), rowsPerBeat(r), ticksPerRow(t) {}
        constexpr int rowsPerMeasure() const { return beatsPerMeasure * rowsPerBeat; }

        constexpr bool operator==(const TimeSignature& o) const { return beatsPerMeasure == o.beatsPerMeasure && rowsPerBeat == o.rowsPerBeat && ticksPerRow == o.ticksPerRow; }
        constexpr bool operator!=(const TimeSignature& o) const { return !(*this == o); }
    };
    class Pattern : public std::enable_shared_from_this<Pattern> {
    public:
        class Row { // with std::unique_ptr<std::vector>, each Row is 12 bytes inline on 64-bit (8 bytes on 32)
        public:
            static std::array<unsigned char, 2> fallbackParam;

            int16_t port = -1;
            int16_t note = -1;
            std::unique_ptr<std::vector<std::array<unsigned char, 2>>> params = nullptr; // empty by default

            Row() = default;
            Row(const Row&) noexcept;
            Row(Row&&) = default;
            Row(int16_t p, int16_t n) : Row() {
                port = p;
                note = n;
            }

            Row& operator=(const Row&) noexcept;

            bool isEmpty() const { return numParams() == 0 && port == -1 && note == -1; }

            size_t numParams() const {
                if (!this->params) return 0;
                return this->params->size();
            }

            std::array<unsigned char, 2>& param(size_t index) {
                if (!this->params || this->params->size() <= index) return fallbackParam;
                return this->params->at(index);
            }

            void addParam(char c = '.', unsigned char v = 0) {
                std::array<unsigned char, 2> p {static_cast<unsigned char>(c), v};
                if (!this->params) this->params.reset(new std::vector<std::array<unsigned char, 2>>({p}));
                else params->push_back(p);
            }

            void insertParam(size_t index, char c = ' ', unsigned char v = 0) {
                std::array<unsigned char, 2> p {static_cast<unsigned char>(c), v};
                if (!this->params) this->params.reset(new std::vector<std::array<unsigned char, 2>>({p}));
                else {
                    if (index > this->params->size()) index = this->params->size();
                    params->insert(this->params->begin() + static_cast<ptrdiff_t>(index), p);
                }
            }

            /// Sets parameter at given index, adding struts before if necessary
            void setParam(size_t index, char c = ' ', unsigned char v = 0) {
                if (!this->params) this->params.reset(new std::vector<std::array<unsigned char, 2>>());
                if (params->size() <= index) params->resize(index+1, {' ', 0});
                (*params)[index] = {static_cast<unsigned char>(c), v};
            }

            void removeParam(size_t index) {
                if (!this->params || this->params->size() <= index) return; // invalid index
                if (this->params->size() == 1) { // deallocate vector entirely if empty
                    this->params.reset(nullptr);
                    return;
                }
                this->params->erase(this->params->begin() + static_cast<ptrdiff_t>(index));
            }

        };

        class Channel {
        public:
            QString name;
            std::vector<Row> rows;

            Channel() = default;
            Channel(const Channel&) = default;
            Channel(int numRows, QString name = "");
        };
    private:
        static Row fallbackRow;
        static Channel fallbackChannel;

    public:
        // raw pointer is fine for now, since a project will never be destroyed without explicitly orphaning patterns
        // (and probably deleting them since basically the only reason one would be kept alive is if it's open in the pattern editor,
        // which would then immediately update with a pattern from opening a new project, or the window would close)
        Project* project;
        size_t index; // index in project's pattern list

        QString name;

        int rows = 64;
        double tempo = 0; // don't set playback tempo
        TimeSignature time;

        int fold = 0;

        std::vector<Channel> channels;

        Pattern();
        Pattern(const Pattern&) = default;
        Pattern(int rows, int channels = 0);

        void setLength(int rows);
        void addChannel(int at = -1);
        void deleteChannel(int at);

        size_t numChannels() const { return channels.size(); }

        bool valid() const;
        bool validFor(const Project*) const;
        bool validFor(const std::shared_ptr<Project>&) const;

        Channel& channel(int channel);
        Row& rowAt(int channel, int row);
    };
}

