#pragma once
#include <vector>
#include <algorithm>
#include <cstdint>

// Tempo-synced arpeggiator. Tracks held notes and, on each clock step,
// produces the notes to play for the selected pattern / octave range /
// direction. Pure note logic — timing is driven from outside (the audio
// thread) via step().
class Arpeggiator {
public:
    enum Pattern {
        Up = 0,
        Down,
        UpDown,
        DownUp,
        Random,
        AsPlayed,
        Chord,
        UpDownX,
        DownUpX,
        RandomOnce,
        OctaveUp,
        OctaveDown,
        PinkyUp,
        PinkyDown,
        UpMajor,
        DownMajor,
        UpDownMinor,
        C3C4A,
        C3C4B,
        C3C4C,
        C3C4D,
        C3C4E,
        numPatterns
    };

    enum Direction {
        DirectionUp = 0,
        DirectionDown
    };

    struct NotePitch {
        int note;
        float velocity;
    };

    void setParameters(bool enabled, int pattern, int octaves, int direction,
                       double rateBeats, double bpm) {
        this->enabled = enabled;
        this->pattern = pattern < 0 ? Up : (pattern >= numPatterns ? Chord : pattern);
        this->octaves = octaves < 1 ? 1 : (octaves > 3 ? 3 : octaves);
        this->direction = (direction == DirectionDown) ? DirectionDown : DirectionUp;
        this->rateBeats = rateBeats;
        this->bpm = bpm > 0.0 ? bpm : 120.0;
    }

    bool isEnabled() const { return enabled; }

    double getTickSeconds() const {
        return rateBeats * 60.0 / bpm;
    }

    void noteOn(int note, float velocity) {
        if (note < 0 || note > 127) return;
        if (std::find(heldNotes.begin(), heldNotes.end(), note) != heldNotes.end())
            return;
        bool wasIdle = heldNotes.empty();
        heldNotes.push_back(note);
        velocities[note] = velocity;
        // The held set changed, so the pitch pool changed too. Restart the
        // sequence from the top so the pattern always starts from a known
        // note instead of continuing at stepIndex % newPoolSize.
        stepIndex = 0;
        randomOncePool.clear();
        randomOnceOrder.clear();
        // A fresh key press should sound immediately instead of waiting for
        // the next host-synced tick.
        if (wasIdle)
            needsImmediateStep = true;
    }

    void noteOff(int note) {
        auto it = std::find(heldNotes.begin(), heldNotes.end(), note);
        if (it != heldNotes.end()) {
            heldNotes.erase(it);
            stepIndex = 0;
            randomOncePool.clear();
            randomOnceOrder.clear();
        }
    }

    bool hasHeldNotes() const { return !heldNotes.empty(); }

    // Returns true if the next step should fire right now (a fresh key press
    // requested an immediate trigger) and clears the request.
    bool shouldTriggerNow() {
        bool v = needsImmediateStep;
        needsImmediateStep = false;
        return v;
    }

    // Clears held notes and step state. Notes that were sounding when reset
    // was called are returned so the caller can issue note-offs.
    void reset(std::vector<int>& notesToStop) {
        notesToStop.swap(currentNotes);
        heldNotes.clear();
        stepIndex = 0;
        needsImmediateStep = false;
        randomOncePool.clear();
        randomOnceOrder.clear();
    }

    // Advances one step. Notes that should stop go into `notesToStop`
    // (usually the note(s) from the previous step); notes that should start
    // go into `notesToPlay`.
    void step(std::vector<int>& notesToStop, std::vector<NotePitch>& notesToPlay) {
        notesToStop.clear();
        notesToPlay.clear();

        if (!enabled) return;

        if (heldNotes.empty()) {
            notesToStop = currentNotes;
            currentNotes.clear();
            stepIndex = 0;
            return;
        }

        std::vector<NotePitch> pool;
        buildPool(pool);

        if (pool.empty()) {
            notesToStop = currentNotes;
            currentNotes.clear();
            return;
        }

        if (pattern == Chord) {
            static const bool chordBlanks[] =
                {false, false, true, false, false, true, false, false,
                 true, false, false, true, false, true, false, true};
            bool blank = chordBlanks[stepIndex % 16];
            ++stepIndex;
            notesToStop = currentNotes;
            currentNotes.clear();
            if (blank)
                return;
            notesToPlay = pool;
            for (const auto& np : pool)
                currentNotes.push_back(np.note);
            return;
        }

        auto order = buildOrder(pool);
        if (order.empty()) {
            notesToStop = currentNotes;
            currentNotes.clear();
            return;
        }

        int idx = stepIndex % static_cast<int>(order.size());
        ++stepIndex;

        const NotePitch& np = order[idx];

        notesToStop = currentNotes;
        currentNotes.clear();
        if (np.note != restNote) {
            currentNotes.push_back(np.note);
            notesToPlay.push_back(np);
        }
    }

private:
    static constexpr int restNote = -1;

    // Harmony patterns arpeggiate a third + fifth after each root: minor
    // (root, +3, +7) or major (root, +4, +7). Each is a separate step.
    bool hasHarmony() const {
        return pattern == Up || pattern == Down || pattern == UpDown || pattern == DownUp
            || pattern == UpMajor || pattern == DownMajor || pattern == UpDownMinor;
    }

    int harmonyThird() const {
        switch (pattern) {
            case Up: case Down: case UpDownMinor: return 3;
            default: return 4;
        }
    }
    void buildPool(std::vector<NotePitch>& pool) const {
        bool used[128] = {};
        auto addPitch = [&](int pitch, float velocity) {
            pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch);
            if (used[pitch]) return;
            used[pitch] = true;
            pool.push_back({pitch, velocity});
        };

        if (pattern == AsPlayed) {
            for (int n : heldNotes) {
                for (int k = 0; k < octaves; ++k) {
                    addPitch(n + 12 * k, velocities[n]);
                }
            }
        } else {
            std::vector<int> sorted(heldNotes.begin(), heldNotes.end());
            std::sort(sorted.begin(), sorted.end());
            for (int n : sorted) {
                for (int k = 0; k < octaves; ++k) {
                    addPitch(n + 12 * k, velocities[n]);
                }
            }
        }
    }

    std::vector<NotePitch> buildOrder(const std::vector<NotePitch>& pool) {
        std::vector<NotePitch> seq(pool);
        std::sort(seq.begin(), seq.end(),
                  [](const NotePitch& a, const NotePitch& b) { return a.note < b.note; });
        int n = static_cast<int>(seq.size());

        std::vector<NotePitch> order;
        auto push = [&](int i) { order.push_back(seq[i]); };

        switch (pattern) {
            case Down:
            case DownMajor:
                for (int i = n - 1; i >= 0; --i) push(i);
                break;
            case UpDown:
            case UpDownMinor:
                for (int i = 0; i < n; ++i) push(i);
                for (int i = n - 2; i >= 0; --i) push(i);
                break;
            case DownUp:
                for (int i = n - 1; i >= 0; --i) push(i);
                for (int i = 1; i < n; ++i) push(i);
                break;
            case UpDownX:
                for (int i = 0; i < n; ++i) push(i);
                for (int i = n - 2; i >= 1; --i) push(i);
                break;
            case DownUpX:
                for (int i = n - 1; i >= 0; --i) push(i);
                for (int i = 1; i < n - 1; ++i) push(i);
                break;
            case Random:
            case RandomOnce: {
                if (pattern == RandomOnce) {
                    std::vector<int> keys;
                    for (const auto& np : seq) keys.push_back(np.note);
                    if (keys != randomOncePool || stepIndex % n == 0) {
                        randomOncePool = keys;
                        randomOnceOrder = seq;
                        for (int i = n - 1; i > 0; --i) {
                            int j = static_cast<int>(randomState % static_cast<uint32_t>(i + 1));
                            std::swap(randomOnceOrder[i], randomOnceOrder[j]);
                            randomState = randomState * 1664525u + 1013904223u;
                        }
                    }
                    order = randomOnceOrder;
                    break;
                }
                order = seq;
                for (int i = n - 1; i > 0; --i) {
                    int j = static_cast<int>(randomState % static_cast<uint32_t>(i + 1));
                    std::swap(order[i], order[j]);
                    randomState = randomState * 1664525u + 1013904223u;
                }
                break;
            }
            case OctaveUp:
            case OctaveDown:
                for (const auto& np : seq) {
                    bool isRoot = true;
                    for (const auto& other : seq) {
                        if (other.note == np.note - 12) { isRoot = false; break; }
                    }
                    if (!isRoot) continue;
                    int step = (pattern == OctaveUp ? 12 : -12);
                    for (int k = 0; k < octaves; ++k) {
                        int pitch = np.note + step * k;
                        pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch);
                        auto it = std::find_if(seq.begin(), seq.end(),
                                               [&](const NotePitch& o) { return o.note == pitch; });
                        if (it != seq.end())
                            order.push_back(*it);
                        else
                            order.push_back({pitch, np.velocity});
                    }
                }
                break;
            case PinkyUp:
            case PinkyDown: {
                int anchor = heldNotes.empty() ? 48 : heldNotes.back();
                int up = anchor + 12;
                up = up > 127 ? 127 : up;
                float vel = heldNotes.empty() ? 0.9f : velocities[heldNotes.back()];
                NotePitch rest{restNote, vel};
                int lo = (direction == DirectionDown) ? up : anchor;
                int hi = (direction == DirectionDown) ? anchor : up;
                order.clear();
                if (pattern == PinkyUp) {
                    order = {{lo, vel}, rest, {hi, vel}, {lo, vel},
                             rest, {hi, vel}, {lo, vel}, {lo, vel}};
                } else {
                    order = {{hi, vel}, rest, {lo, vel}, {hi, vel},
                             rest, {lo, vel}, {hi, vel}, {hi, vel}};
                }
                break;
            }
            case AsPlayed:
                order = pool;
                break;
            case C3C4A:
            case C3C4B:
            case C3C4C:
            case C3C4D:
            case C3C4E: {
                int anchor = heldNotes.empty() ? 48 : heldNotes.back();
                int up = anchor + 12;
                up = up > 127 ? 127 : up;
                float vel = heldNotes.empty() ? 0.9f : velocities[heldNotes.back()];
                NotePitch rest{restNote, vel};
                // Invert swaps the lower/upper notes (C3 <-> C4) so the Stab
                // patterns play upside down when direction is Down.
                int lo = (direction == DirectionDown) ? up : anchor;
                int hi = (direction == DirectionDown) ? anchor : up;
                order.clear();
                if (pattern == C3C4A) {
                    order = {{lo, vel}, {lo, vel}, {hi, vel}, {lo, vel},
                             {hi, vel}, {lo, vel}, rest, {hi, vel}};
                } else if (pattern == C3C4B) {
                    order = {{hi, vel}, {lo, vel}, rest, {lo, vel},
                             {lo, vel}, {lo, vel}, rest, {lo, vel}};
                } else if (pattern == C3C4C) {
                    order = {{lo, vel}, {lo, vel}, rest, {hi, vel},
                             rest, {lo, vel}, rest, {lo, vel}};
                } else if (pattern == C3C4D) {
                    order = {{lo, vel}, {lo, vel}, {hi, vel}, {lo, vel},
                             {lo, vel}, {hi, vel}, {lo, vel}, {lo, vel},
                             {hi, vel}, {lo, vel}, {lo, vel}, {hi, vel},
                             {lo, vel}, {lo, vel}, {lo, vel}, {lo, vel}};
                } else {
                    order = {{lo, vel}, {lo, vel}, rest, {lo, vel},
                             {lo, vel}, rest, {lo, vel}, {lo, vel},
                             rest, {lo, vel}, {lo, vel}, rest,
                             {lo, vel}, {lo, vel}, {lo, vel}, {lo, vel}};
                }
                break;
            }
            case Chord:
            case Up:
            case UpMajor:
            default:
                for (int i = 0; i < n; ++i) push(i);
                break;
        }

        if (hasHarmony()) {
            std::vector<NotePitch> expanded;
            expanded.reserve(order.size() * 3);
            int third = harmonyThird();
            for (const auto& np : order) {
                expanded.push_back(np);
                auto pushClamped = [&](int pitch) {
                    pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch);
                    expanded.push_back({pitch, np.velocity});
                };
                pushClamped(np.note + third);
                pushClamped(np.note + 7);
            }
            order.swap(expanded);
        }

        if (direction == DirectionDown && pattern != Random && pattern != RandomOnce
            && pattern != AsPlayed && pattern != C3C4A && pattern != C3C4B
            && pattern != C3C4C && pattern != C3C4D && pattern != C3C4E
            && pattern != PinkyUp && pattern != PinkyDown)
            std::sort(order.begin(), order.end(),
                      [](const NotePitch& a, const NotePitch& b) { return a.note > b.note; });

        return order;
    }

    bool enabled = false;
    int pattern = Up;
    int octaves = 1;
    int direction = DirectionUp;
    double rateBeats = 0.5;
    double bpm = 120.0;

    std::vector<int> heldNotes;
    float velocities[128] = {};
    std::vector<int> currentNotes;
    int stepIndex = 0;
    bool needsImmediateStep = false;

    uint32_t randomState = 0x12345678u;
    std::vector<int> randomOncePool;
    std::vector<NotePitch> randomOnceOrder;
};
