#include "PluginEditor.h"
#include "BuildInfo.h"
#include <BinaryData.h>

namespace
{
    // Returns a sensible default directory for preset files. On Linux,
    // File::getSpecialLocation (userHomeDirectory) can resolve to "/" when the
    // home directory can't be determined, so fall back through $HOME, the user
    // documents directory, and finally the current working directory.
    juce::File getDefaultPresetDirectory()
    {
        auto test = [] (const juce::File& f) { return f.isDirectory() && f.createDirectory().wasOk(); };

        if (auto* homeEnv = std::getenv ("HOME"))
            if (test (juce::File (homeEnv)))
                return juce::File (homeEnv);

        if (test (juce::File::getSpecialLocation (juce::File::userHomeDirectory)))
            return juce::File::getSpecialLocation (juce::File::userHomeDirectory);

        if (test (juce::File::getSpecialLocation (juce::File::userDocumentsDirectory)))
            return juce::File::getSpecialLocation (juce::File::userDocumentsDirectory);

        return juce::File::getCurrentWorkingDirectory();
    }
}

KnobLookAndFeel::KnobLookAndFeel() {
    knobOverlay = juce::ImageCache::getFromMemory(BinaryData::metalknob_png,
                                                    BinaryData::metalknob_pngSize);
}

void KnobLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height,
                                        float sliderPos, float rotaryStartAngle,
                                        float rotaryEndAngle, juce::Slider& slider) {
    auto bounds = juce::Rectangle<int>(x, y, width, height).toFloat();

    float labelH = 20.0f;
    auto dialBounds = bounds.withTrimmedTop(labelH);

    auto radius = juce::jmin(dialBounds.getWidth(), dialBounds.getHeight()) / 2.0f - 4.0f;
    auto centreX = dialBounds.getCentreX();
    auto centreY = dialBounds.getCentreY();
    auto rx = centreX - radius;
    auto ry = centreY - radius;
    auto rw = radius * 2.0f;
    auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);

    // Blurred drop shadow
    juce::DropShadow knobShadow(juce::Colour(0x55000000), 10, juce::Point<int>(0, 5));
    juce::Path shadowPath;
    shadowPath.addEllipse(juce::Rectangle<float>(rx, ry, rw, rw));
    knobShadow.drawForPath(g, shadowPath);

    // Outer bevel (dark bottom-right, light top-left)
    auto bevelPath = juce::Path();
    bevelPath.addEllipse(rx, ry, rw, rw);

    // Metal knob overlay (clipped to circle, behind gradient)
    if (knobOverlay.isValid()) {
        juce::Graphics::ScopedSaveState state(g);
        juce::Path circlePath;
        circlePath.addEllipse(rx, ry, rw, rw);
        g.reduceClipRegion(circlePath);
        g.setOpacity(0.55f);
        g.drawImageWithin(knobOverlay, (int)rx, (int)ry, (int)rw, (int)rw, juce::RectanglePlacement::stretchToFit);
        g.setOpacity(1.0f);
    }

    // Knob body gradient — light top-left to dark bottom-right for 3D bulge
    juce::ColourGradient bodyGrad(juce::Colour(0xa3191919), rx, ry,
                                   juce::Colour(0xd31f1f1f), rx + rw, ry + rw, true);
    g.setGradientFill(bodyGrad);
    g.fillEllipse(rx, ry, rw, rw);

    // Soft inner shadow ring (bottom-right dark edge, subtle)
    // g.setColour(juce::Colour(0x18000000));
    // auto shadowArc = juce::Path();
    // float innerR = radius - 2.0f;
    // shadowArc.addCentredArc(centreX, centreY, innerR, innerR, 0.0f, 0.5f, 2.7f, true);
    // g.strokePath(shadowArc, juce::PathStrokeType(1.5f));

    // Outer rim
    // g.setColour(juce::Colour(0xff555555));
    // g.drawEllipse(rx, ry, rw, rw, 1.5f);

    // Pointer line with glow — green (left) → yellow (mid) → red (right)
    auto indicatorColour = juce::Colour::fromHSV(0.02f + 0.30f * (1.0f - sliderPos), 0.55f, 0.98f, 1.0f);
    auto glowColour = juce::Colour::fromHSV(0.02f + 0.30f * (1.0f - sliderPos), 0.55f, 0.98f, 0.4f);

    g.setColour(glowColour);
    juce::Path pointerGlow;
    pointerGlow.addRoundedRectangle(-2.5f, -radius + 9, 5.0f, radius * 0.5f, 2.0f);
    g.fillPath(pointerGlow, juce::AffineTransform::rotation(angle).translated(centreX, centreY));

    g.setColour(indicatorColour);
    juce::Path pointer;
    pointer.addRoundedRectangle(-1.5f, -radius + 9, 3.0f, radius * 0.5f, 1.5f);
    g.fillPath(pointer, juce::AffineTransform::rotation(angle).translated(centreX, centreY));

    // Label above knob
    auto name = slider.getName();
    if (name.isNotEmpty()) {
        g.setColour(juce::Colour(0xff999999));
        g.setFont(juce::Font(juce::FontOptions(13.0f).withStyle("Bold")));
        g.drawText(name, bounds.getX(), bounds.getY(), bounds.getWidth(), labelH,
                   juce::Justification::centredBottom);
    }
}

void ComboBoxLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height,
                                       bool isButtonDown, int /*buttonX*/, int /*buttonY*/,
                                       int /*buttonW*/, int /*buttonH*/, juce::ComboBox& /*box*/) {
    auto bounds = juce::Rectangle<int>(0, 0, width, height).toFloat();

    // Drop shadow
    g.setColour(juce::Colour(0x40000000));
    g.fillRoundedRectangle(bounds.getX() + 1.5f, bounds.getY() + 2.0f,
                           bounds.getWidth(), bounds.getHeight(), 4.0f);

    // Body gradient — spotlight at 88° from horizontal (near-vertical, subtle tilt)
    float spotlightAngle = 88.0f * juce::MathConstants<float>::pi / 180.0f;
    float sdx = std::cos(spotlightAngle);
    float sdy = std::sin(spotlightAngle);
    float scx = bounds.getCentreX();
    float scy = bounds.getCentreY();
    float slen = std::hypot(bounds.getWidth(), bounds.getHeight()) * 0.5f;
    juce::ColourGradient bodyGrad(juce::Colour(0xb3404040), scx - sdx * slen, scy - sdy * slen,
                                  juce::Colour(0xb31a1a1a), scx + sdx * slen, scy + sdy * slen, true);
    g.setGradientFill(bodyGrad);
    g.fillRoundedRectangle(bounds, 4.0f);

    // Top highlight edge
    g.setColour(juce::Colour(0x30ffffff));
    g.drawRoundedRectangle(bounds.getX() + 0.5f, bounds.getY() + 0.5f,
                           bounds.getWidth() - 1.0f, bounds.getHeight() - 1.0f, 4.0f, 1.0f);

    // Bottom shadow edge
    g.setColour(juce::Colour(0x30000000));
    g.drawLine(bounds.getX() + 4.0f, bounds.getBottom() - 0.5f,
               bounds.getRight() - 4.0f, bounds.getBottom() - 0.5f, 1.0f);

    // Outline
    g.setColour(juce::Colour(0xff555555));
    g.drawRoundedRectangle(bounds, 4.0f, 1.0f);

    // Arrow (gold triangle on right)
    float arrowSize = 6.0f;
    float arrowX = bounds.getRight() - 16.0f;
    float arrowY = bounds.getCentreY();

    juce::Path arrow;
    arrow.addTriangle(arrowX, arrowY - arrowSize / 2,
                      arrowX + arrowSize, arrowY,
                      arrowX, arrowY + arrowSize / 2);
    g.setColour(juce::Colour(0xffccaa44));
    g.fillPath(arrow);
}

void ComboBoxLookAndFeel::drawPopupMenuItem(juce::Graphics& g, const juce::Rectangle<int>& area,
                                            bool isSeparator, bool /*isActive*/,
                                            bool isHighlighted, bool isTicked,
                                            bool /*hasSubMenu*/, const juce::String& text,
                                            const juce::String& /*shortcutKeyText*/,
                                            const juce::Drawable* /*icon*/,
                                            const juce::Colour* /*textColour*/) {
    if (isSeparator) {
        g.setColour(juce::Colour(0xff444444));
        g.drawLine(area.getX() + 8.0f, area.getCentreY(),
                   area.getRight() - 8.0f, area.getCentreY(), 1.0f);
        return;
    }

    // Persistent highlight for the currently selected (ticked) entry or section
    if (isTicked && !isHighlighted) {
        g.setColour(juce::Colour(0xffccaa44).withAlpha(0.3f));
        g.fillRoundedRectangle(area.reduced(2).toFloat(), 3.0f);
    }

    if (isHighlighted) {
        g.setColour(juce::Colour(0xffccaa44));
        g.fillRoundedRectangle(area.reduced(2).toFloat(), 3.0f);
    }

    g.setColour(isHighlighted ? juce::Colour(0xff1a1a1a)
                              : (isTicked ? juce::Colour(0xffccaa44) : juce::Colour(0xffcccccc)));
    g.setFont(juce::Font(juce::FontOptions(13.0f)));
    g.drawText(text, area.reduced(8, 0), juce::Justification::centredLeft, true);
}

// --- VU Meter ---
void VuMeter::paint(juce::Graphics& g) {
    auto bounds = getLocalBounds().toFloat().reduced(0.5f);

    juce::ColourGradient bgGrad(juce::Colour(0xBB222222), bounds.getCentreX(), bounds.getY(),
                                 juce::Colour(0xBB111111), bounds.getCentreX(), bounds.getBottom(), false);
    g.setGradientFill(bgGrad);
    g.fillRoundedRectangle(bounds, 9.0f);

    float level = processor.getRmsLevel();
    float clamped = juce::jlimit(0.0f, 1.0f, level);
    float barH = bounds.getHeight() * clamped;
    auto barBounds = juce::Rectangle<float>(bounds.getX(), bounds.getBottom() - barH, bounds.getWidth(), barH);
    auto innerBar = barBounds.reduced(1.0f);

    {
        juce::Graphics::ScopedSaveState saved(g);
        juce::Path clipPath;
        clipPath.addRoundedRectangle(bounds, 9.0f);
        g.reduceClipRegion(clipPath);

        juce::ColourGradient grad(juce::Colour(0xffc59c07).withAlpha(0.7f), 0.0f, bounds.getBottom(),
                                   juce::Colour(0xffcc2200).withAlpha(0.7f), 0.0f, bounds.getY(), false);
        g.setGradientFill(grad);
        g.fillRoundedRectangle(innerBar, 2.0f);
    }

    g.setColour(juce::Colour(0xff333333));
    g.drawRoundedRectangle(bounds, 9.0f, 1.0f);

    g.setColour(juce::Colour(0xffccaa44));
    g.setFont(juce::Font(juce::FontOptions(13.0f).withStyle("Bold")));
    g.drawText("VU", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred);
}

// --- Waveform Display (with semi-transparent spectrum overlay) ---
void WaveformDisplay::paint(juce::Graphics& g) {
    auto bounds = getLocalBounds().toFloat().reduced(0.5f);

    juce::ColourGradient bgGrad(juce::Colour(0xBB222222), bounds.getCentreX(), bounds.getY(),
                                 juce::Colour(0xBB111111), bounds.getCentreX(), bounds.getBottom(), false);
    g.setGradientFill(bgGrad);
    g.fillRoundedRectangle(bounds, 9.0f);

    g.setColour(juce::Colour(0xff333333));
    g.drawRoundedRectangle(bounds, 9.0f, 1.0f);

    // ---- Spectrum overlay (half-transparent, drawn behind the waveform) ----
    if (fft != nullptr) {
        std::array<float, ChromaFlockProcessor::fftSize * 2> fftData{};
        int writePos = processor.fftWritePos.load(std::memory_order_acquire);
        int fftSize = ChromaFlockProcessor::fftSize;

        for (int i = 0; i < fftSize; ++i) {
            int idx = (writePos + i) % fftSize;
            fftData[i] = processor.fftInput[idx];
        }

        for (int i = 0; i < fftSize; ++i) {
            float window = 0.5f - 0.5f * std::cos(2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(fftSize));
            fftData[i] *= window;
        }

        fft->performFrequencyOnlyForwardTransform(fftData.data());

        int numPoints = 128;
        float w = bounds.getWidth();
        float h = bounds.getHeight() - 22.0f;
        float bottom = bounds.getBottom() - 2.0f;
        int maxBin = fftSize / 4;
        float sampleRate = static_cast<float>(processor.getSampleRate());
        float binHz = sampleRate / static_cast<float>(fftSize);
        const float dbFloor = -48.0f;

        // Log-frequency mapping so the low end (sub bass) spreads across the
        // display instead of bunching up in the leftmost ~10%.
        const float fLow = 20.0f;
        const float fHigh = static_cast<float>(maxBin) * binHz;
        const float logRange = std::log(fHigh / fLow);

        if (specSmooth.size() != static_cast<size_t>(numPoints + 1))
            specSmooth.assign(numPoints + 1, 0.0f);

        std::vector<float> mags(numPoints + 1);
        for (int i = 0; i <= numPoints; ++i) {
            float t = static_cast<float>(i) / static_cast<float>(numPoints);
            float tN = juce::jmin(t + 1.0f / static_cast<float>(numPoints), 1.0f);
            float fL = fLow * std::exp(logRange * t);
            float fN = fLow * std::exp(logRange * tN);
            int binStart = juce::jmax(1, static_cast<int>(fL / binHz));
            int binEnd = static_cast<int>(fN / binHz) + 1;
            if (binEnd <= binStart) binEnd = binStart + 1;
            if (binEnd > maxBin) binEnd = maxBin;

            float mag = 0.0f;
            int count = 0;
            for (int b = binStart; b < binEnd; ++b) {
                mag += fftData[b];
                ++count;
            }
            mag = count > 0 ? mag / static_cast<float>(count) : 0.0f;

            // dB scale: 0 dB reference ≈ full-scale sine peak, floor at dbFloor.
            float lin = mag / static_cast<float>(fftSize) * 4.0f;
            float db = 20.0f * std::log10(lin + 1.0e-6f);
            mags[i] = juce::jlimit(0.0f, 1.0f, (db - dbFloor) / -dbFloor);
        }

        // Spatial smoothing between adjacent points (rolling-hill look).
        std::vector<float> blurred = mags;
        for (int pass = 0; pass < 2; ++pass) {
            for (int i = 0; i <= numPoints; ++i) {
                float a = mags[static_cast<size_t>(juce::jmax(0, i - 1))];
                float c = mags[static_cast<size_t>(juce::jmin(numPoints, i + 1))];
                blurred[static_cast<size_t>(i)] = (a + 2.0f * mags[static_cast<size_t>(i)] + c) * 0.25f;
            }
            mags = blurred;
        }

        // Time smoothing (EMA) so the curve glides instead of jumping.
        // Asymmetric: fast attack, slower fall — a released note's spectrum
        // decays away instead of being held up.
        const float emaUp = 0.7f;
        const float emaDown = 0.6f;
        for (int i = 0; i <= numPoints; ++i) {
            float& s = specSmooth[static_cast<size_t>(i)];
            float m = mags[static_cast<size_t>(i)];
            float coeff = m > s ? emaUp : emaDown;
            s = coeff * s + (1.0f - coeff) * m;
        }

        juce::Path specPath;
        specPath.startNewSubPath(bounds.getX(), bottom);
        for (int i = 0; i <= numPoints; ++i) {
            float x = bounds.getX() + (static_cast<float>(i) / static_cast<float>(numPoints)) * w;
            float y = bottom - specSmooth[static_cast<size_t>(i)] * h;
            specPath.lineTo(x, y);
        }
        specPath.lineTo(bounds.getRight(), bottom);
        specPath.closeSubPath();

        {
            juce::Graphics::ScopedSaveState saved(g);
            juce::Path clipPath;
            clipPath.addRoundedRectangle(bounds, 9.0f);
            g.reduceClipRegion(clipPath);

            juce::ColourGradient specGrad(juce::Colour(0xff7b94b5).withAlpha(0.5f), 0.0f, bottom,
                                           juce::Colour(0xff2a3a4a).withAlpha(0.4f), 0.0f, bottom - h, false);
            g.setGradientFill(specGrad);
            g.fillPath(specPath);

            g.setColour(juce::Colour(0xff9db8d8).withAlpha(0.55f));
            g.strokePath(specPath, juce::PathStrokeType(1.2f));
        }
    }

    g.setColour(juce::Colour(0xff333333));
    g.drawLine(bounds.getX(), bounds.getCentreY(), bounds.getRight(), bounds.getCentreY(), 1.0f);

    int writePos = processor.scopeWritePos.load(std::memory_order_acquire);
    int bufSize = ChromaFlockProcessor::scopeBufferSize;
    float w = bounds.getWidth();
    float h = bounds.getHeight();
    float midY = bounds.getY() + h * 0.5f;

    juce::Path wavePath;
    bool started = false;
    for (int i = 0; i < bufSize; ++i) {
        int idx = (writePos + i) % bufSize;
        float x = bounds.getX() + (static_cast<float>(i) / static_cast<float>(bufSize)) * w;
        float sample = processor.scopeBuffer[idx];
        float y = midY - sample * h * 0.45f;

        if (!started) {
            wavePath.startNewSubPath(x, y);
            started = true;
        } else {
            wavePath.lineTo(x, y);
        }
    }

    juce::Path filledPath(wavePath);
    filledPath.lineTo(bounds.getRight(), midY);
    filledPath.lineTo(bounds.getX(), midY);
    filledPath.closeSubPath();

    {
        juce::Graphics::ScopedSaveState saved(g);
        juce::Path clipPath;
        clipPath.addRoundedRectangle(bounds, 9.0f);
        g.reduceClipRegion(clipPath);

        juce::ColourGradient waveGrad(juce::Colour(0xff8fa35a).withAlpha(0.5f), 0.0f, bounds.getY(),
                                        juce::Colour(0xff2e3a18).withAlpha(0.4f), 0.0f, bounds.getBottom(), false);
        g.setGradientFill(waveGrad);
        g.fillPath(filledPath);

        g.setColour(juce::Colour(0xff8fa35a).withAlpha(0.9f));
        g.strokePath(wavePath, juce::PathStrokeType(1.5f));
    }

    g.setColour(juce::Colour(0xffccaa44));
    g.setFont(juce::Font(juce::FontOptions(13.0f).withStyle("Bold")));
    g.drawText("WAVE", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred);
}

// --- Patch LCD (amber dot-matrix) ---
namespace {
    struct Glyph { char c; unsigned char rows[7]; };
    // 5x7 font; each byte is a row, bits 4..0 are columns left->right.
    const Glyph font5x7[] = {
        {' ', {0x00,0x00,0x00,0x00,0x00,0x00,0x00}},
        {'-', {0x00,0x00,0x00,0x1F,0x00,0x00,0x00}},
        {'A', {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}},
        {'B', {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E}},
        {'C', {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}},
        {'D', {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E}},
        {'E', {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}},
        {'F', {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10}},
        {'G', {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}},
        {'H', {0x11,0x11,0x11,0x1F,0x11,0x11,0x11}},
        {'I', {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}},
        {'J', {0x07,0x02,0x02,0x02,0x02,0x12,0x0C}},
        {'K', {0x11,0x12,0x14,0x18,0x14,0x12,0x11}},
        {'L', {0x10,0x10,0x10,0x10,0x10,0x10,0x1F}},
        {'M', {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}},
        {'N', {0x11,0x11,0x19,0x15,0x13,0x11,0x11}},
        {'O', {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}},
        {'P', {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10}},
        {'Q', {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}},
        {'R', {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11}},
        {'S', {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}},
        {'T', {0x1F,0x04,0x04,0x04,0x04,0x04,0x04}},
        {'U', {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}},
        {'V', {0x11,0x11,0x11,0x11,0x11,0x0A,0x04}},
        {'W', {0x11,0x11,0x11,0x15,0x15,0x1B,0x11}},
        {'X', {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11}},
        {'Y', {0x11,0x11,0x0A,0x04,0x04,0x04,0x04}},
        {'Z', {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F}},
        {'0', {0x0E,0x13,0x15,0x15,0x19,0x11,0x0E}},
        {'1', {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}},
        {'2', {0x0E,0x11,0x01,0x06,0x08,0x10,0x1F}},
        {'3', {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E}},
        {'4', {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}},
        {'5', {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}},
        {'6', {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}},
        {'7', {0x1F,0x01,0x02,0x04,0x08,0x08,0x08}},
        {'8', {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}},
        {'9', {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}},
    };

    const unsigned char* getFontGlyph(char c) {
        for (const auto& g : font5x7)
            if (g.c == c) return g.rows;
        return font5x7[0].rows; // space fallback
    }
}

void PatchLCD::paint(juce::Graphics& g) {
    auto b = getLocalBounds();

    // Dark LCD panel
    g.setColour(juce::Colour(0xcc1a0e00));
    g.fillRoundedRectangle(b.toFloat(), 3.0f);
    g.setColour(juce::Colour(0xff5a3a00));
    g.drawRoundedRectangle(b.toFloat(), 3.0f, 1.0f);

    const int pitch = 3;
    const int glyphW = 5, glyphH = 7, glyphGap = 1;

    // Faint "off" LED grid
    g.setColour(juce::Colour(0xff1a1000));
    for (int y = pitch / 2; y < b.getHeight(); y += pitch)
        for (int x = pitch / 2; x < b.getWidth(); x += pitch)
            g.fillRect(static_cast<float>(x) - 0.5f, static_cast<float>(y) - 0.5f, 1.0f, 1.0f);

    int y0 = (b.getHeight() - glyphH * pitch) / 2;
    int cx = 5;
    int cy = y0;
    if (cy < 0) cy = 0;

    juce::String u = text.toUpperCase();
    for (auto chr : u) {
        const unsigned char* g7 = getFontGlyph(chr);
        for (int row = 0; row < glyphH; ++row) {
            unsigned char bits = g7[row];
            for (int col = 0; col < glyphW; ++col) {
                if (bits & (1 << (glyphW - 1 - col))) {
                    float px = static_cast<float>(cx + col * pitch + pitch / 2);
                    float py = static_cast<float>(cy + row * pitch + pitch / 2);
                    g.setColour(juce::Colour(0xff3a2000));
                    g.fillEllipse(px - pitch * 0.5f, py - pitch * 0.5f, pitch, pitch);
                    g.setColour(juce::Colour(0xffc59c07));
                    g.fillEllipse(px - pitch * 0.4f, py - pitch * 0.4f, pitch * 0.8f, pitch * 0.8f);
                }
            }
        }
        cx += (glyphW + glyphGap) * pitch;
        if (cx > b.getWidth()) break;
    }
}

// --- MIDI Signal LED ---
void MidiLed::paint(juce::Graphics& g) {
    auto b = getLocalBounds().toFloat();
    auto centre = b.getCentre();
    float radius = juce::jmin(b.getWidth(), b.getHeight()) * 0.5f;

    bool active = processor.isAnyNoteActive();

    if (active) {
        g.setColour(juce::Colour(0xffc59c07).withAlpha(0.2f));
        g.fillEllipse(centre.x - radius * 1.8f, centre.y - radius * 1.8f, radius * 3.6f, radius * 3.6f);
    }

    g.setColour(juce::Colour(0xff1a1200));
    g.fillEllipse(centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f);

    if (active)
        g.setColour(juce::Colour(0xffc59c07));
    else
        g.setColour(juce::Colour(0xff3a2a06));

    g.fillEllipse(centre.x - radius * 0.75f, centre.y - radius * 0.75f, radius * 1.5f, radius * 1.5f);
}

// --- Clickable state LED (on/off, direction) ---
LedToggle::LedToggle(juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId,
                     const juce::String& onText, const juce::String& offText)
    : apvts(apvts), paramId(paramId), onText(onText), offText(offText) {
    startTimerHz(30);
    setMouseCursor(juce::MouseCursor::PointingHandCursor);
}

void LedToggle::timerCallback() {
    bool v = apvts.getRawParameterValue(paramId)->load() > 0.5f;
    if (v != lit) {
        lit = v;
        repaint();
        if (onChange)
            onChange();
    }
}

void LedToggle::mouseDown(const juce::MouseEvent&) {
    auto* param = apvts.getParameter(paramId);
    float newVal = param->getValue() > 0.5f ? 0.0f : 1.0f;
    param->beginChangeGesture();
    param->setValueNotifyingHost(newVal);
    param->endChangeGesture();
}

void LedToggle::mouseEnter(const juce::MouseEvent&) {
    setMouseCursor(juce::MouseCursor::PointingHandCursor);
}

void LedToggle::paint(juce::Graphics& g) {
    auto b = getLocalBounds().toFloat();
    float ledD = juce::jmin(22.0f, b.getHeight() - 8.0f);
    float radius = ledD * 0.5f;
    auto centre = juce::Point<float>(b.getX() + radius + 4.0f, b.getCentreY());

    if (lit) {
        float auraR = radius * 1.9f;
        float maxAura = juce::jmin(centre.x - b.getX(), b.getRight() - centre.x,
                                   centre.y - b.getY(), b.getBottom() - centre.y) - 1.0f;
        auraR = juce::jmin(auraR, maxAura);
        g.setColour(juce::Colour(0xffccaa44).withAlpha(0.2f));
        g.fillEllipse(centre.x - auraR, centre.y - auraR, auraR * 2.0f, auraR * 2.0f);
    }

    g.setColour(juce::Colour(0xff1a1200));
    g.fillEllipse(centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f);
    g.setColour(juce::Colour(0xff555555));
    g.drawEllipse(centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f, 1.5f);

    g.setColour(lit ? juce::Colour(0xffeebb55) : juce::Colour(0xff3a2a06));
    g.fillEllipse(centre.x - radius * 0.78f, centre.y - radius * 0.78f, radius * 1.56f, radius * 1.56f);

    g.setFont(juce::Font(juce::FontOptions(10.0f).withStyle("Bold")));
    g.setColour(lit ? juce::Colour(0xffeebb55) : juce::Colour(0xff666666));
    g.drawText(lit ? onText : offText,
               juce::Rectangle<float>(b.getX() + ledD + 12.0f, b.getY(),
                                      b.getWidth() - ledD - 12.0f, b.getHeight()),
               juce::Justification::centredLeft);
}

MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
    : processorRef(p), vuMeter(p), waveformDisplay(p), pianoRoll(p), midiLed(p),
      arpEnabledLed(p.apvts, "arpEnabled", "ON", "OFF"),
      arpDirectionLed(p.apvts, "arpDirection", "ON", "OFF"),
      delayPingPongLed(p.apvts, "delayPingPong", "PINGPONG", "OFF"),
      delayInvertLed(p.apvts, "delayInvert", "INVERT", "OFF"),
      delayFlattenLed(p.apvts, "delayFlatten", "FLATTEN", "OFF"),
      reverbOnLed(p.apvts, "reverbOn", "ON", "OFF"),
      lfo1SyncLed(p.apvts, "lfo1Sync", "SYNC ON", "SYNC OFF"),
      lfo2SyncLed(p.apvts, "lfo2Sync", "SYNC ON", "SYNC OFF"),
      delaySyncLed(p.apvts, "delaySync", "SYNC ON", "SYNC OFF") {

    auto setupParam = [&](juce::Slider& knob, std::unique_ptr<SliderAttachment>& attach,
                          const juce::String& paramId, const juce::String& name) {
        setupKnob(knob, name);
        attach = std::make_unique<SliderAttachment>(processorRef.apvts, paramId, knob);
    };

    auto setupCB = [&](juce::ComboBox& box, std::unique_ptr<ComboBoxAttachment>& attach,
                       const juce::String& paramId, const juce::StringArray& items) {
        setupCombo(box);
        box.addItemList(items, 1);
        attach = std::make_unique<ComboBoxAttachment>(processorRef.apvts, paramId, box);
    };

    setupLabel(osc1Label, "OSC 1");
    setupCB(osc1WaveBox, osc1WaveAttach, "osc1Wave", {"Sine", "Saw", "Square", "Triangle", "Noise",
        "Pulse 25%", "Pulse 12%", "Rev Saw", "Full Rect", "Half Rect",
        "Stair 4", "Stair 8", "Soft Sine", "Sinc", "Exp Pulse", "Double Sine", "SuperSaw"});
    setupParam(osc1OctKnob, osc1OctAttach, "osc1Oct", "OCT");
    setupParam(osc1SemiKnob, osc1SemiAttach, "osc1Semi", "SEMI");
    setupParam(osc1FineKnob, osc1FineAttach, "osc1Fine", "FINE");
    setupParam(osc1LevelKnob, osc1LevelAttach, "osc1Level", "LEVEL");

    setupLabel(osc2Label, "OSC 2");
    setupCB(osc2WaveBox, osc2WaveAttach, "osc2Wave", {"Sine", "Saw", "Square", "Triangle", "Noise",
        "Pulse 25%", "Pulse 12%", "Rev Saw", "Full Rect", "Half Rect",
        "Stair 4", "Stair 8", "Soft Sine", "Sinc", "Exp Pulse", "Double Sine", "SuperSaw"});
    setupParam(osc2OctKnob, osc2OctAttach, "osc2Oct", "OCT");
    setupParam(osc2SemiKnob, osc2SemiAttach, "osc2Semi", "SEMI");
    setupParam(osc2FineKnob, osc2FineAttach, "osc2Fine", "FINE");
    setupParam(osc2LevelKnob, osc2LevelAttach, "osc2Level", "LEVEL");
    setupParam(phaseOffsetKnob, phaseOffsetAttach, "phaseOffset", "PHASE");

    setupLabel(filterLabel, "FILTER");
    // Display order differs from the parameter's index order: LP 48dB is shown
    // directly under LP 24dB. itemIds are (paramIndex + 1) so the underlying
    // choice index (and therefore saved state / presets) stays meaningful.
    setupCombo(filterTypeBox);
    filterTypeBox.addItem("LP 12dB", 1);
    filterTypeBox.addItem("LP 24dB", 2);
    filterTypeBox.addItem("LP 48dB", 9);
    filterTypeBox.addItem("Band Pass", 3);
    filterTypeBox.addItem("High Pass", 4);
    filterTypeBox.addItem("Notch", 5);
    filterTypeBox.addItem("Band Pass 24", 6);
    filterTypeBox.addItem("High Pass 24", 7);
    filterTypeBox.addItem("Notch 24", 8);
    filterTypeAttach = std::make_unique<IndexedComboBoxAttachment>(processorRef.apvts, "filterType", filterTypeBox);
    setupParam(filterCutoffKnob, filterCutoffAttach, "filterCutoff", "CUTOFF");
    setupParam(filterResKnob, filterResAttach, "filterRes", "RES");
    setupParam(filterEnvAmtKnob, filterEnvAmtAttach, "filterEnvAmt", "ENV AMT");
    setupParam(keyTrackKnob, keyTrackAttach, "keyTrack", "KEY TRK");
    setupParam(velToCutKnob, velToCutAttach, "velToCut", "VEL CUT");

    setupLabel(envLabel, "AMP ENV");
    setupParam(envAttackKnob, envAttackAttach, "envAttack", "ATTACK");
    setupParam(envDecayKnob, envDecayAttach, "envDecay", "DECAY");
    setupParam(envSustainKnob, envSustainAttach, "envSustain", "SUSTAIN");
    setupParam(envReleaseKnob, envReleaseAttach, "envRelease", "RELEASE");

    setupLabel(fEnvLabel, "FILTER ENV");
    setupParam(fEnvAttackKnob, fEnvAttackAttach, "fEnvAttack", "ATTACK");
    setupParam(fEnvDecayKnob, fEnvDecayAttach, "fEnvDecay", "DECAY");
    setupParam(fEnvSustainKnob, fEnvSustainAttach, "fEnvSustain", "SUSTAIN");
    setupParam(fEnvReleaseKnob, fEnvReleaseAttach, "fEnvRelease", "RELEASE");

    setupLabel(globalLabel, "MASTER");
    setupParam(panKnob, panAttach, "pan", "PAN");
    setupParam(driveKnob, driveAttach, "drive", "DRIVE");
    setupParam(masterKnob, masterAttach, "masterLevel", "LEVEL");
    setupParam(pbRangeKnob, pbRangeAttach, "pitchBendRange", "PB RANGE");

    // FX
    setupCB(distTypeBox, distTypeAttach, "distType", {"Soft Clip", "Hard Clip", "Foldback", "Overdrive"});
    setupParam(distAmountKnob, distAmountAttach, "distAmount", "AMOUNT");
    setupParam(distSymmetryKnob, distSymmetryAttach, "distSymmetry", "SYMMETRY");
    setupParam(distToneKnob, distToneAttach, "distTone", "TONE");
    setupParam(compThresholdKnob, compThresholdAttach, "compThreshold", "THRESH");
    setupParam(compRatioKnob, compRatioAttach, "compRatio", "RATIO");
    setupParam(compAttackKnob, compAttackAttach, "compAttack", "ATTACK");
    setupParam(compReleaseKnob, compReleaseAttach, "compRelease", "RELEASE");
    setupParam(compMakeupKnob, compMakeupAttach, "compMakeup", "MAKEUP");
    setupParam(autoPanRateKnob, autoPanRateAttach, "autoPanRate", "RATE");
    setupParam(autoPanDepthKnob, autoPanDepthAttach, "autoPanDepth", "DEPTH");
    setupParam(autoPanPhaseKnob, autoPanPhaseAttach, "autoPanPhase", "PHASE");
    setupParam(limiterThresholdKnob, limiterThresholdAttach, "limiterThreshold", "THRESH");
    setupParam(limiterCeilingKnob, limiterCeilingAttach, "limiterCeiling", "CEILING");
    setupParam(limiterReleaseKnob, limiterReleaseAttach, "limiterRelease", "RELEASE");

    // LFO
    setupKnob(lfo1RateKnob, "RATE");
    setupParam(lfo1DepthKnob, lfo1DepthAttach, "lfo1Depth", "DEPTH");
    setupCB(lfo1ShapeBox, lfo1ShapeAttach, "lfo1Shape", {"Sine", "Triangle", "Saw", "Square"});
    setupCB(lfo1DestBox, lfo1DestAttach, "lfo1Dest", {"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"});
    addAndMakeVisible(lfo1SyncLed);
    lfo1SyncLed.onChange = [this]() {
        setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());
    };
    setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());

    setupKnob(lfo2RateKnob, "RATE");
    setupParam(lfo2DepthKnob, lfo2DepthAttach, "lfo2Depth", "DEPTH");
    setupCB(lfo2ShapeBox, lfo2ShapeAttach, "lfo2Shape", {"Sine", "Triangle", "Saw", "Square"});
    setupCB(lfo2DestBox, lfo2DestAttach, "lfo2Dest", {"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"});
    addAndMakeVisible(lfo2SyncLed);
    lfo2SyncLed.onChange = [this]() {
        setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());
    };
    setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());

    // FX2
    setupKnob(delayTimeKnob, "TIME");
    addAndMakeVisible(delaySyncLed);
    delaySyncLed.onChange = [this]() {
        setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
    };
    setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
    setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK");
    setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX");
    addAndMakeVisible(delayPingPongLed);
    addAndMakeVisible(delayInvertLed);
    addAndMakeVisible(delayFlattenLed);
    addAndMakeVisible(reverbOnLed);
    setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE");
    setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP");
    setupParam(reverbMixKnob, reverbMixAttach, "reverbMix", "MIX");

    // Bottom status bar: git build info + actual compilation time
    addAndMakeVisible(statusBar);
    statusBar.setColour(juce::Label::textColourId, juce::Colour(0xff888888));
    statusBar.setFont(juce::Font(juce::FontOptions(11.0f)));
    statusBar.setText(juce::String(getBuildInfoString()), juce::dontSendNotification);

    // Preset section: title button + dot-matrix LCD + prev/next
    presetButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xcc2a2a2a));
    presetButton.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffccaa44));
    presetButton.setLookAndFeel(&comboLaf);
    presetButton.onClick = [this]() { showPresetMenu(); };
    addAndMakeVisible(presetButton);

    addAndMakeVisible(patchLCD);
    addAndMakeVisible(midiLed);
    patchLCD.onClick = [this]() { showFileMenu(); };

    auto setupArrow = [&](juce::TextButton& btn) {
        btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xcc2a2a2a));
        btn.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffccaa44));
        btn.setLookAndFeel(&comboLaf);
        addAndMakeVisible(btn);
    };
    setupArrow(prevPresetButton);
    setupArrow(nextPresetButton);

    prevPresetButton.onClick = [this]() {
        auto& presets = processorRef.presetManager.getPresets();
        if (presets.empty()) return;
        currentPresetIndex = (currentPresetIndex - 1 + static_cast<int>(presets.size())) % static_cast<int>(presets.size());
        applyCurrentPreset();
    };
    nextPresetButton.onClick = [this]() {
        auto& presets = processorRef.presetManager.getPresets();
        if (presets.empty()) return;
        currentPresetIndex = (currentPresetIndex + 1) % static_cast<int>(presets.size());
        applyCurrentPreset();
    };

    randomizeButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xcc2a2a2a));
    randomizeButton.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffccaa44));
    randomizeButton.setLookAndFeel(&comboLaf);
    randomizeButton.onClick = [this]() { randomizeParameters(); };
    addAndMakeVisible(randomizeButton);

    auto& presets = processorRef.presetManager.getPresets();
    if (!presets.empty()) {
        // Restore the preset name from the saved state so re-opening the
        // plugin shows the patch the user last selected instead of defaulting
        // to "Zero State". The parameter values themselves are already
        // restored by the host via setStateInformation.
        currentPresetIndex = -1;
        auto savedName = processorRef.apvts.state.getProperty("presetName", juce::String()).toString();
        if (savedName.isNotEmpty()) {
            for (size_t i = 0; i < presets.size(); ++i) {
                if (presets[i].name == savedName) {
                    currentPresetIndex = static_cast<int>(i);
                    break;
                }
            }
            patchLCD.setText(savedName);
        } else {
            currentPresetIndex = 0;
            patchLCD.setText(presets[currentPresetIndex].name);
        }
    }

    scaleLabel.setText("SCALE", juce::dontSendNotification);
    scaleLabel.setFont(juce::Font(juce::FontOptions(13.0f).withStyle("Bold")));
    scaleLabel.setColour(juce::Label::textColourId, juce::Colour(0xff888888));
    scaleLabel.setJustificationType(juce::Justification::centredRight);
    addAndMakeVisible(scaleLabel);

    midiLabel.setText("MIDI IN", juce::dontSendNotification);
    midiLabel.setFont(juce::Font(juce::FontOptions(13.0f).withStyle("Bold")));
    midiLabel.setColour(juce::Label::textColourId, juce::Colour(0xff888888));
    midiLabel.setJustificationType(juce::Justification::centredLeft);
    addAndMakeVisible(midiLabel);

    auto setupTransposeLabel = [&](juce::Label& label, const juce::String& text) {
        label.setText(text, juce::dontSendNotification);
        label.setFont(juce::Font(juce::FontOptions(13.0f).withStyle("Bold")));
        label.setColour(juce::Label::textColourId, juce::Colour(0xff888888));
        label.setJustificationType(juce::Justification::centredLeft);
        addAndMakeVisible(label);
    };
    setupTransposeLabel(octaveTransposeLabel, "OCT");
    setupTransposeLabel(semitoneTransposeLabel, "SEMI");

    setupCombo(octaveTransposeBox);
    octaveTransposeBox.addItemList(juce::StringArray{"-3", "-2", "-1", "0", "+1", "+2", "+3"}, 1);
    octaveTransposeAttach = std::make_unique<ComboBoxAttachment>(
        processorRef.apvts, "octaveTranspose", octaveTransposeBox);

    setupCombo(semitoneTransposeBox);
    semitoneTransposeBox.addItemList(
        juce::StringArray{getSemitoneChoices()}, 1);
    semitoneTransposeAttach = std::make_unique<ComboBoxAttachment>(
        processorRef.apvts, "semitoneTranspose", semitoneTransposeBox);

    addAndMakeVisible(vuMeter);
    addAndMakeVisible(waveformDisplay);
    addAndMakeVisible(pianoRoll);

    // Arpeggiator section
    auto setupArpLabel = [&](juce::Label& label, const juce::String& text) {
        label.setText(text, juce::dontSendNotification);
        label.setFont(juce::Font(juce::FontOptions(11.0f).withStyle("Bold")));
        label.setColour(juce::Label::textColourId, juce::Colour(0xff888888));
        label.setJustificationType(juce::Justification::centred);
        addAndMakeVisible(label);
    };
    setupArpLabel(arpEnabledLabel, "ON/OFF");
    setupArpLabel(arpPatternLabel, "PATTERN");
    setupArpLabel(arpOctavesLabel, "OCTAVES");
    setupArpLabel(arpDirectionLabel, "INVERT");
    setupArpLabel(arpRateLabel, "RATE");

    addAndMakeVisible(arpEnabledLed);
    addAndMakeVisible(arpDirectionLed);

    setupCB(arpPatternBox, arpPatternAttach, "arpPattern",
            {"Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Random",
             "As Played", "Chord", "Up & Down X", "Down & Up X", "Random Once", "Octave Up",
             "Octave Down", "Pinky Up", "Pinky Down", "Up / Major", "Down / Major",
             "Up & Down / Minor", "Stab 1", "Stab 2", "Stab 3", "Stab 4", "Stab 5"});
    setupCB(arpOctavesBox, arpOctavesAttach, "arpOctaves", {"1", "2", "3"});
    setupCB(arpRateBox, arpRateAttach, "arpRate", {"1/32", "1/16", "1/8", "1/4", "1/2", "1", "2"});

    setupCombo(uiScaleBox);
    uiScaleBox.addItem("100%", 1);
    uiScaleBox.addItem("125%", 2);
    uiScaleBox.addItem("150%", 3);
    uiScaleBox.addItem("200%", 4);
    uiScaleBox.setSelectedId(1, juce::dontSendNotification);
    uiScaleBox.onChange = [this]() {
        static const float scales[] = {1.0f, 1.25f, 1.5f, 2.0f};
        int idx = uiScaleBox.getSelectedId() - 1;
        if (idx >= 0 && idx < 4 && onScaleChanged)
            onScaleChanged(scales[idx]);
    };
}

void MainContentComponent::setupLabel(juce::Label& label, const juce::String& text) {
    label.setText(text, juce::dontSendNotification);
    label.setFont(juce::Font(juce::FontOptions(13.0f).withStyle("Bold")));
    label.setColour(juce::Label::textColourId, juce::Colour(0xffccaa44));
    label.setJustificationType(juce::Justification::centred);
    addAndMakeVisible(label);
}

void MainContentComponent::setupKnob(juce::Slider& knob, const juce::String& name) {
    knob.setSliderStyle(juce::Slider::RotaryVerticalDrag);
    knob.setRotaryParameters(juce::MathConstants<float>::pi * 1.2f,
                             juce::MathConstants<float>::pi * 2.8f,
                             true);
    knob.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 55, 16);
    knob.setLookAndFeel(&knobLaf);
    knob.setName(name);
    knob.setColour(juce::Slider::textBoxTextColourId, juce::Colour(0xffcccccc));
    knob.setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xff1a1a1a));
    knob.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack);
    addAndMakeVisible(knob);
}

void MainContentComponent::setupCombo(juce::ComboBox& combo) {
    combo.setEditableText(false);
    combo.setLookAndFeel(&comboLaf);
    combo.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2a2a2a));
    combo.setColour(juce::ComboBox::textColourId, juce::Colour(0xffcccccc));
    combo.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff555555));
    combo.setColour(juce::ComboBox::arrowColourId, juce::Colour(0xffccaa44));
    combo.setColour(juce::PopupMenu::backgroundColourId, juce::Colour(0xff2a2a2a));
    combo.setColour(juce::PopupMenu::textColourId, juce::Colour(0xffcccccc));
    combo.setColour(juce::PopupMenu::highlightedBackgroundColourId, juce::Colour(0xffccaa44));
    combo.setColour(juce::PopupMenu::highlightedTextColourId, juce::Colour(0xff1a1a1a));
    addAndMakeVisible(combo);
}

void MainContentComponent::setupSyncKnob(juce::Slider& knob,
                                         std::unique_ptr<SliderAttachment>& rateAttach,
                                         std::unique_ptr<SliderAttachment>& beatAttach,
                                         const juce::String& rateId,
                                         const juce::String& beatId,
                                         bool syncOn) {
    rateAttach.reset();
    beatAttach.reset();
    if (syncOn)
        beatAttach = std::make_unique<SliderAttachment>(processorRef.apvts, beatId, knob);
    else
        rateAttach = std::make_unique<SliderAttachment>(processorRef.apvts, rateId, knob);
}

void MainContentComponent::showPresetMenu() {
    juce::PopupMenu menu;
    std::vector<juce::String> menuOrder;

    auto categories = processorRef.presetManager.getCategories();

    juce::String currentName;
    juce::String currentCategory;
    auto& allPresets = processorRef.presetManager.getPresets();
    if (currentPresetIndex >= 0 && currentPresetIndex < static_cast<int>(allPresets.size())) {
        currentName = allPresets[currentPresetIndex].name;
        currentCategory = allPresets[currentPresetIndex].category;
    }

    for (auto& cat : categories) {
        juce::PopupMenu subMenu;
        auto presets = processorRef.presetManager.getPresetsInCategory(cat);
        for (auto& preset : presets) {
            menuOrder.push_back(preset.name);
            bool isCurrent = (preset.name == currentName);
            subMenu.addItem(static_cast<int>(menuOrder.size()), preset.name, true, isCurrent);
        }
        // Tick the section that contains the active patch (cleared when randomize -> index -1)
        menu.addSubMenu(cat, subMenu, true, juce::Image(), cat == currentCategory);
    }

    menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&presetButton),
        [this, menuOrder](int result) {
            if (result > 0 && result <= static_cast<int>(menuOrder.size())) {
                auto name = menuOrder[static_cast<size_t>(result) - 1];
                auto& presets = processorRef.presetManager.getPresets();
                for (int i = 0; i < static_cast<int>(presets.size()); ++i) {
                    if (presets[i].name == name) {
                        currentPresetIndex = i;
                        break;
                    }
                }
                applyCurrentPreset();
            }
        });
}

void MainContentComponent::showFileMenu() {
    juce::PopupMenu menu;
    menu.addItem(1, "Load Preset...");
    menu.addItem(2, "Save Preset...");
    menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&patchLCD),
        [this](int result) {
            if (result == 1) loadPresetFile();
            else if (result == 2) savePresetFile();
        });
}

static const char* kCflTag = "ChromaFlockPatch";
#include "Version.h"
static const juce::String kCflVersion = CHROMAFLOCK_VERSION;

juce::String MainContentComponent::currentPresetName() const {
    auto& presets = processorRef.presetManager.getPresets();
    if (currentPresetIndex >= 0 && currentPresetIndex < static_cast<int>(presets.size()))
        return presets[currentPresetIndex].name;
    return patchLCD.getText();
}

void MainContentComponent::savePresetFile() {
    auto* chooser = new juce::FileChooser("Save ChromaFlock Preset",
        getDefaultPresetDirectory()
            .getChildFile("chromaflock-" + currentPresetName().replaceCharacter(' ', '-') + ".cfl"),
        "*.cfl");

        chooser->launchAsync(juce::FileBrowserComponent::saveMode
                             | juce::FileBrowserComponent::canSelectFiles
                             | juce::FileBrowserComponent::warnAboutOverwriting,
            [this, chooser](const juce::FileChooser& c) {
                std::unique_ptr<juce::FileChooser> deleter(chooser);
                auto file = c.getResult();
                if (file == juce::File())
                    return;

                // Always force the .cfl extension (macOS native panel does not
                // reliably append it) and lowercase the filename.
                juce::String name = file.getFileNameWithoutExtension();
                if (name.toLowerCase() != name)
                    file = file.getParentDirectory().getChildFile(name.toLowerCase());
                file = file.withFileExtension(".cfl");

                auto state = processorRef.apvts.copyState();
                std::unique_ptr<juce::XmlElement> presetXml(state.createXml());

                std::unique_ptr<juce::XmlElement> root(new juce::XmlElement(kCflTag));
                root->setAttribute("version", kCflVersion);
                root->setAttribute("generator", "ChromaFlock");
                root->addChildElement(presetXml.release());

                if (!root->writeTo(file))
                    juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon,
                        "Save Failed", "Could not write preset file:\n" + file.getFullPathName(), this);
                else
                    patchLCD.setText(file.getFileNameWithoutExtension());
            });
}

void MainContentComponent::loadPresetFile() {
    auto* chooser = new juce::FileChooser("Load ChromaFlock Preset",
        getDefaultPresetDirectory(),
        "*.cfl");

    chooser->launchAsync(juce::FileBrowserComponent::openMode
                         | juce::FileBrowserComponent::canSelectFiles,
        [this, chooser](const juce::FileChooser& c) {
            std::unique_ptr<juce::FileChooser> deleter(chooser);
            auto file = c.getResult();
            if (file == juce::File() || !file.existsAsFile())
                return;

            std::unique_ptr<juce::XmlElement> root(juce::XmlDocument::parse(file));
            if (root == nullptr || !root->hasTagName(kCflTag)) {
                juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon,
                    "Invalid File", "This is not a valid ChromaFlock (.cfl) preset file.", this);
                return;
            }

            auto fileVersion = root->getStringAttribute("version", "");
            if (fileVersion != kCflVersion) {
                juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon,
                    "Version Mismatch",
                    "This preset was saved with ChromaFlock " + (fileVersion.isEmpty() ? "<unknown>" : fileVersion) +
                    ", but you are running ChromaFlock " + kCflVersion +
                    ".\nThe preset may not sound as intended.", this);
            }

            auto* presetXml = root->getChildByName(processorRef.apvts.state.getType());
            if (presetXml == nullptr) {
                juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon,
                    "Invalid File", "The preset file does not contain ChromaFlock parameter data.", this);
                return;
            }

            processorRef.fadeOutActiveVoices(0.25f);
            processorRef.apvts.replaceState(juce::ValueTree::fromXml(*presetXml));

            currentPresetIndex = -1;
            patchLCD.setText(file.getFileNameWithoutExtension());
            processorRef.apvts.state.setProperty("presetName", file.getFileNameWithoutExtension(), nullptr);

            setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());
            setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());
            setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
        });
}

void MainContentComponent::applyCurrentPreset() {
    auto& presets = processorRef.presetManager.getPresets();
    if (presets.empty()) return;
    currentPresetIndex = juce::jlimit(0, static_cast<int>(presets.size()) - 1, currentPresetIndex);
    processorRef.fadeOutActiveVoices(0.25f);
    processorRef.presetManager.applyPreset(presets[currentPresetIndex].name, processorRef.apvts);
    patchLCD.setText(presets[currentPresetIndex].name);
    processorRef.apvts.state.setProperty("presetName", presets[currentPresetIndex].name, nullptr);

    // The LedToggle updates itself via its timer, so re-sync the rate/beat
    // knob attachments to match.
    setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());
    setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());
    setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
}

void MainContentComponent::randomizeParameters() {
    juce::Random rand;
    processorRef.fadeOutActiveVoices(0.25f);
    currentPresetIndex = -1;
    for (auto* p : processorRef.getParameters()) {
        auto* rap = dynamic_cast<juce::RangedAudioParameter*>(p);
        if (rap == nullptr)
            continue;
        const auto& id = rap->paramID;
        if (id == "octaveTranspose" || id == "semitoneTranspose")
            continue;
        float v = rand.nextFloat();
        if (id == "masterLevel")
            v = 0.4f + 0.6f * v;
        rap->setValueNotifyingHost(v);
    }

    patchLCD.setText("RANDOM");
    processorRef.apvts.state.setProperty("presetName", "RANDOM", nullptr);

    // Re-sync the rate/beat knob attachments to match the randomized sync states.
    setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());
    setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());
    setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
}

void MainContentComponent::paint(juce::Graphics& g) {
    g.fillAll(juce::Colour(0xff1a1a1a));

    // Draw background image tiled
    if (!bgImageLoaded) {
        bgImage = juce::ImageFileFormat::loadFrom(
            BinaryData::bg_png, BinaryData::bg_pngSize);
        bgImageLoaded = true;
    }
    if (bgImage.isValid()) {
        g.setOpacity(0.35f);
        g.drawImage(bgImage,
                    juce::Rectangle<float>(0.0f, 0.0f, static_cast<float>(getWidth()),
                                           static_cast<float>(getHeight())),
                    juce::RectanglePlacement::stretchToFit);
        g.setOpacity(1.0f);
    }

    // Header section: no boxed background (border removed)

    // Load logo
    if (!logoLoaded) {
        auto img = juce::ImageFileFormat::loadFrom(
            BinaryData::logo_png, BinaryData::logo_pngSize);
        if (img.isValid()) {
            juce::Image::BitmapData bd(img, juce::Image::BitmapData::readOnly);
            int minX = bd.width, maxX = 0, minY = bd.height, maxY = 0;
            bool found = false;
            for (int y = 0; y < bd.height; ++y) {
                for (int x = 0; x < bd.width; ++x) {
                    int a = bd.data[static_cast<size_t>(y) * bd.lineStride + x * bd.pixelStride + 3];
                    if (a > 10) {
                        found = true;
                        if (x < minX) minX = x;
                        if (x > maxX) maxX = x;
                        if (y < minY) minY = y;
                        if (y > maxY) maxY = y;
                    }
                }
            }
            logoImage = found ? img.getClippedImage(juce::Rectangle<int>(minX, minY, maxX - minX + 1, maxY - minY + 1))
                              : img;
        }
        logoLoaded = true;
    }
    if (logoImage.isValid()) {
        int logoW = 352;
        int logoH = logoImage.getWidth() > 0
            ? static_cast<int>(logoW * static_cast<double>(logoImage.getHeight()) / logoImage.getWidth())
            : logoW;
        int logoX = (getWidth() - logoW) / 2;
        int logoY = (80 - logoH) / 2 - 2;
        auto logoArea = juce::Rectangle<int>(logoX, logoY, logoW, logoH);
        juce::Graphics::ScopedSaveState saved(g);
        g.setOpacity(1.0f);
        g.drawImage(logoImage, logoArea.toFloat(), juce::RectanglePlacement::centred);
    }

    auto drawSection = [&](int x, int y, int w, int h) {
        auto rect = juce::Rectangle<float>(static_cast<float>(x), static_cast<float>(y),
                                           static_cast<float>(w), static_cast<float>(h));
        // Soft glow behind section
        auto glow = rect.expanded(6.0f);
        g.setColour(juce::Colour(0xff222222).withAlpha(0.25f));
        g.fillRoundedRectangle(glow, 12.0f);
        // 3D fill gradient: slight light top, dark bottom
        juce::ColourGradient fillGrad(juce::Colour(0xBB222222), rect.getCentreX(), rect.getY(),
                                       juce::Colour(0xBB111111), rect.getCentreX(), rect.getBottom(), false);
        g.setGradientFill(fillGrad);
        g.fillRoundedRectangle(rect, 9.0f);
        // Border
        g.setColour(juce::Colour(0xff333333));
        g.drawRoundedRectangle(rect, 9.0f, 1.0f);
    };

    drawSection(10, 88, 498, 200);
    drawSection(518, 88, 624, 200);
    drawSection(1152, 88, 498, 200);
    drawSection(10, 298, 563, 160);
    drawSection(583, 298, 502, 160);
    drawSection(1095, 298, 555, 160);
    drawSection(10, 892, 1640, 136);

    // FX sub-sections
    auto drawSubSection = [&](int x, int y, int w, int h, const juce::String& title, int titlePadY = 2) {
        auto rect = juce::Rectangle<float>(static_cast<float>(x), static_cast<float>(y),
                                           static_cast<float>(w), static_cast<float>(h));
        auto glow = rect.expanded(6.0f);
        g.setColour(juce::Colour(0xff222222).withAlpha(0.25f));
        g.fillRoundedRectangle(glow, 12.0f);
        juce::ColourGradient fillGrad(juce::Colour(0xBB222222), rect.getCentreX(), rect.getY(),
                                       juce::Colour(0xBB111111), rect.getCentreX(), rect.getBottom(), false);
        g.setGradientFill(fillGrad);
        g.fillRoundedRectangle(rect, 9.0f);
        g.setColour(juce::Colour(0xff333333));
        g.drawRoundedRectangle(rect, 9.0f, 1.0f);
        g.setColour(juce::Colour(0xffccaa44));
        g.setFont(juce::Font(juce::FontOptions(11.0f).withStyle("Bold")));
        g.drawText(title, x + 6, y + titlePadY, w - 12, 14, juce::Justification::centred);
    };

    drawSubSection(10, 468, 510, 115, "DISTORTION", 6);
    drawSubSection(528, 468, 468, 115, "COMPRESSION", 6);
    drawSubSection(1004, 468, 268, 115, "PANNING", 6);
    drawSubSection(1280, 468, 370, 115, "LIMITER", 6);

    // LFO sub-sections
    drawSubSection(10, 594, 464, 150, "LFO 1", 4);
    drawSubSection(480, 594, 464, 150, "LFO 2", 4);

    // FX2 sub-sections
    drawSubSection(950, 594, 368, 150, "DELAY", 4);
    drawSubSection(1324, 594, 326, 150, "REVERB", 4);

    // Arpeggiator section (replaces the removed spectrum analyzer)
    drawSubSection(860, 754, 790, 128, "ARPEGGIATOR", 6);

    // Status bar divider
    g.setColour(juce::Colour(0xff333333));
    g.drawHorizontalLine(1034, 10.0f, 1650.0f);
}

void MainContentComponent::resized() {
    auto sectionY = 92;
    auto knobSize = 112;
    auto knobSpacing = 10;
    auto labelH = 18;
    auto comboH = 48;

    osc1Label.setBounds(10, sectionY, 498, labelH);
    osc1WaveBox.setBounds(20, sectionY + labelH + 4, 200, 28);
    int knobY1 = sectionY + labelH + comboH + 8;
    osc1OctKnob.setBounds(20, knobY1, knobSize, knobSize);
    osc1SemiKnob.setBounds(20 + knobSize + knobSpacing, knobY1, knobSize, knobSize);
    osc1FineKnob.setBounds(20 + (knobSize + knobSpacing) * 2, knobY1, knobSize, knobSize);
    osc1LevelKnob.setBounds(20 + (knobSize + knobSpacing) * 3, knobY1, knobSize, knobSize);

    osc2Label.setBounds(518, sectionY, 624, labelH);
    osc2WaveBox.setBounds(528, sectionY + labelH + 4, 200, 28);
    int knobY2 = sectionY + labelH + comboH + 8;
    osc2OctKnob.setBounds(528, knobY2, knobSize, knobSize);
    osc2SemiKnob.setBounds(528 + knobSize + knobSpacing, knobY2, knobSize, knobSize);
    osc2FineKnob.setBounds(528 + (knobSize + knobSpacing) * 2, knobY2, knobSize, knobSize);
    osc2LevelKnob.setBounds(528 + (knobSize + knobSpacing) * 3, knobY2, knobSize, knobSize);
    phaseOffsetKnob.setBounds(528 + (knobSize + knobSpacing) * 4, knobY2, knobSize, knobSize);

    filterLabel.setBounds(1152, sectionY, 498, labelH);
    filterTypeBox.setBounds(1430, 88, 220, 28);
    // FILTER holds 5 knobs on a single row (the per-note VEL RES knob was
    // removed); the type dropdown sits in the header row and the knobs span
    // the section width at the original large size.
    {
        int fSize = 89;
        int fGap = 8;
        int rowW = 5 * fSize + 4 * fGap;
        int fSx = 1152 + (498 - rowW) / 2;
        int fRowY = 124;
        filterCutoffKnob.setBounds(fSx, fRowY, fSize, fSize);
        filterResKnob.setBounds(fSx + (fSize + fGap), fRowY, fSize, fSize);
        filterEnvAmtKnob.setBounds(fSx + (fSize + fGap) * 2, fRowY, fSize, fSize);
        keyTrackKnob.setBounds(fSx + (fSize + fGap) * 3, fRowY, fSize, fSize);
        velToCutKnob.setBounds(fSx + (fSize + fGap) * 4, fRowY, fSize, fSize);
    }

    envLabel.setBounds(10, 302, 563, labelH);
    {
        int rowW = 4 * knobSize + 3 * knobSpacing;
        int sx = 10 + (563 - rowW) / 2;
        int knobYA = 312 + labelH + 6;
        envAttackKnob.setBounds(sx, knobYA, knobSize, knobSize);
        envDecayKnob.setBounds(sx + (knobSize + knobSpacing), knobYA, knobSize, knobSize);
        envSustainKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYA, knobSize, knobSize);
        envReleaseKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYA, knobSize, knobSize);
    }

    fEnvLabel.setBounds(583, 302, 502, labelH);
    {
        int rowW = 4 * knobSize + 3 * knobSpacing;
        int sx = 583 + (502 - rowW) / 2;
        int knobYFE = 312 + labelH + 6;
        fEnvAttackKnob.setBounds(sx, knobYFE, knobSize, knobSize);
        fEnvDecayKnob.setBounds(sx + (knobSize + knobSpacing), knobYFE, knobSize, knobSize);
        fEnvSustainKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYFE, knobSize, knobSize);
        fEnvReleaseKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYFE, knobSize, knobSize);
    }

    globalLabel.setBounds(1095, 302, 555, labelH);
    {
        int rowW = 4 * knobSize + 3 * knobSpacing;
        int sx = 1095 + (555 - rowW) / 2;
        int knobYM = 312 + labelH + 6;
        panKnob.setBounds(sx, knobYM, knobSize, knobSize);
        driveKnob.setBounds(sx + (knobSize + knobSpacing), knobYM, knobSize, knobSize);
        masterKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYM, knobSize, knobSize);
        pbRangeKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYM, knobSize, knobSize);
    }

    // --- FX SECTION ---
    int fxY = 440;
    int fxKnobSize = 80;
    int fxLabelH = 16;
    int fxKnobY = fxY + 54; // sub-section y(476) + titlePad(6) + titleH(14) + bottomPad(6) + 4

    fxLabel.setBounds(10, fxY + 8, 1640, fxLabelH);

    // Distortion sub-section (X=10, W=512) — dropdown fixed at x=17, 3 knobs fill remaining width
    distTypeBox.setBounds(17, fxKnobY + (fxKnobSize - 28) / 2, 140, 28);
    {
        int availLeft = 17 + 140;
        int availW = (10 + 512) - availLeft;
        int rowW = 3 * fxKnobSize + 2 * 6;
        int dx = availLeft + (availW - rowW) / 2;
        distAmountKnob.setBounds(dx, fxKnobY, fxKnobSize, fxKnobSize);
        distSymmetryKnob.setBounds(dx + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize);
        distToneKnob.setBounds(dx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize);
    }

    // Compression sub-section (X=528, W=470)
    {
        int rowW = 5 * fxKnobSize + 4 * 6;
        int sx = 528 + (470 - rowW) / 2;
        compThresholdKnob.setBounds(sx, fxKnobY, fxKnobSize, fxKnobSize);
        compRatioKnob.setBounds(sx + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize);
        compAttackKnob.setBounds(sx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize);
        compReleaseKnob.setBounds(sx + (fxKnobSize + 6) * 3, fxKnobY, fxKnobSize, fxKnobSize);
        compMakeupKnob.setBounds(sx + (fxKnobSize + 6) * 4, fxKnobY, fxKnobSize, fxKnobSize);
    }

    // Auto-Pan sub-section (X=1004, W=270)
    {
        int rowW = 3 * fxKnobSize + 2 * 6;
        int ax = 1004 + (270 - rowW) / 2;
        autoPanRateKnob.setBounds(ax, fxKnobY, fxKnobSize, fxKnobSize);
        autoPanDepthKnob.setBounds(ax + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize);
        autoPanPhaseKnob.setBounds(ax + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize);
    }

    // Limiter sub-section (X=1280, W=370)
    {
        int rowW = 3 * fxKnobSize + 2 * 6;
        int lx = 1280 + (370 - rowW) / 2;
        limiterThresholdKnob.setBounds(lx, fxKnobY, fxKnobSize, fxKnobSize);
        limiterCeilingKnob.setBounds(lx + (fxKnobSize + 6), fxKnobY, fxKnobSize, fxKnobSize);
        limiterReleaseKnob.setBounds(lx + (fxKnobSize + 6) * 2, fxKnobY, fxKnobSize, fxKnobSize);
    }

    // --- LFO SECTION ---
    int lfoY = 574;
    int lfoKnobSize = 80;
    int lfoKnobY = 594 + 4 + 14 + 6; // subSectionY + titlePad + titleH + bottomPad

    lfoLabel.setBounds(10, lfoY, 934, fxLabelH);
    fx2Label.setBounds(950, lfoY, 700, fxLabelH);

    // LFO 1 sub-section (X=10, Y=642, W=464, H=110)
    lfo1RateKnob.setBounds(22, lfoKnobY, lfoKnobSize, lfoKnobSize);
    lfo1DepthKnob.setBounds(132, lfoKnobY, lfoKnobSize, lfoKnobSize);
    lfo1ShapeBox.setBounds(242, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
    lfo1DestBox.setBounds(357, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
    lfo1SyncLed.setBounds(22, lfoKnobY + lfoKnobSize + 6, 200, 28);

    // LFO 2 sub-section (X=480, Y=642, W=464, H=110)
    lfo2RateKnob.setBounds(492, lfoKnobY, lfoKnobSize, lfoKnobSize);
    lfo2DepthKnob.setBounds(602, lfoKnobY, lfoKnobSize, lfoKnobSize);
    lfo2ShapeBox.setBounds(712, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
    lfo2DestBox.setBounds(827, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
    lfo2SyncLed.setBounds(492, lfoKnobY + lfoKnobSize + 6, 200, 28);

    // --- FX2 SECTION ---
    int fx2KnobY = lfoKnobY;

    // Delay sub-section: TIME, FBACK, MIX knobs + ping-pong toggle column + sync combo.
    // The ping-pong column (PINGPONG / INVERT / FLATTEN) is stacked on the right,
    // just below/under the ping-pong LED, with ping-pong itself nudged up a bit.
    int dx = 962;
    int dGap = lfoKnobSize + 8;
    delayTimeKnob.setBounds(dx, fx2KnobY, lfoKnobSize, lfoKnobSize);
    delayFbKnob.setBounds(dx + dGap, fx2KnobY, lfoKnobSize, lfoKnobSize);
    delayMixKnob.setBounds(dx + dGap * 2, fx2KnobY, lfoKnobSize, lfoKnobSize);

    int ppX = 1230;
    int ppW = 104;
    int ppH = 36;
    int ppGap = 4;
    int ppStep = ppH + ppGap;
    delayPingPongLed.setBounds(ppX, fx2KnobY, ppW, ppH);
    delayInvertLed.setBounds(ppX, fx2KnobY + ppStep, ppW, ppH);
    delayFlattenLed.setBounds(ppX, fx2KnobY + ppStep * 2, ppW, ppH);

    delaySyncLed.setBounds(dx, fx2KnobY + lfoKnobSize + 6, 250, 28);

    // Reverb sub-section: SIZE, DAMP, MIX (right edge aligned to 1650)
    {
        int rx = 1336;
        reverbSizeKnob.setBounds(rx, fx2KnobY, lfoKnobSize, lfoKnobSize);
        reverbDampKnob.setBounds(rx + (lfoKnobSize + 4), fx2KnobY, lfoKnobSize, lfoKnobSize);
        reverbMixKnob.setBounds(rx + (lfoKnobSize + 4) * 2, fx2KnobY, lfoKnobSize, lfoKnobSize);
        // On/off LED on the right, filling the remaining space before the box edge
        reverbOnLed.setBounds(rx + (lfoKnobSize + 4) * 3, fx2KnobY, 1650 - (rx + (lfoKnobSize + 4) * 3), lfoKnobSize);
    }

    // --- PRESET + SCALE (vertically centered within top section, y: 0..128) ---
    int headerH = 80;
    int btnH = 28, btnGap = 8;
    int presetGroupH = btnH * 2 + btnGap;
    int presetStartY = (headerH - presetGroupH) / 2;
    presetButton.setBounds(20, presetStartY, 90, btnH);
    randomizeButton.setBounds(20, presetStartY + btnH + btnGap, 90, btnH);

    int rowH = 28;
    int rowY = (headerH - rowH) / 2;
    int lcdX = 150;  // gap between the preset/random buttons and the patch LCD
    patchLCD.setBounds(lcdX, rowY, 360, rowH);
    prevPresetButton.setBounds(lcdX + 360 + 8, rowY, 28, rowH);
    nextPresetButton.setBounds(lcdX + 360 + 8 + 32, rowY, 28, rowH);

    int comboH2 = 28, labelH2 = 20;
    int comboY2 = (headerH - comboH2) / 2;
    int labelY2 = (headerH - labelH2) / 2;
    octaveTransposeLabel.setBounds(1112, labelY2, 44, labelH2);
    octaveTransposeBox.setBounds(1156, comboY2, 64, comboH2);
    semitoneTransposeLabel.setBounds(1226, labelY2, 46, labelH2);
    semitoneTransposeBox.setBounds(1272, comboY2, 74, comboH2);

    midiLabel.setBounds(1354, labelY2, 64, labelH2);
    midiLed.setBounds(1418, (headerH - 10) / 2, 10, 10);

    scaleLabel.setBounds(1499, labelY2, 58, labelH2);
    uiScaleBox.setBounds(1559, comboY2, 80, comboH2);

    // Visualizer area
    int vizY = 754;
    int vizH = 128;
    vuMeter.setBounds(10, vizY, 40, vizH);
    waveformDisplay.setBounds(60, vizY, 790, vizH);

    // Arpeggiator controls (right of the waveform display)
    {
        int arpX = 860, arpY = vizY, arpW = 790;
        int comboH = 28;
        int labelH = 16;
        int labelY = arpY + 26;
        int comboY = labelY + labelH + 8;

        int widths[] = {90, 170, 90, 90, 110};
        int gap = 22;
        int totalW = widths[0] + widths[1] + widths[2] + widths[3] + widths[4] + gap * 4;
        int x = arpX + (arpW - totalW) / 2;

        arpEnabledLabel.setBounds(x, labelY, widths[0], labelH);
        arpEnabledLed.setBounds(x, comboY, widths[0], comboH);
        x += widths[0] + gap;

        arpPatternLabel.setBounds(x, labelY, widths[1], labelH);
        arpPatternBox.setBounds(x, comboY, widths[1], comboH);
        x += widths[1] + gap;

        arpOctavesLabel.setBounds(x, labelY, widths[2], labelH);
        arpOctavesBox.setBounds(x, comboY, widths[2], comboH);
        x += widths[2] + gap;

        arpDirectionLabel.setBounds(x, labelY, widths[3], labelH);
        arpDirectionLed.setBounds(x, comboY, widths[3], comboH);
        x += widths[3] + gap;

        arpRateLabel.setBounds(x, labelY, widths[4], labelH);
        arpRateBox.setBounds(x, comboY, widths[4], comboH);
    }

    // Piano roll
    pianoRoll.setBounds(10, 892, 1640, 136);

    // Status bar (bottom strip)
    statusBar.setBounds(10, 1036, 1640, 20);
}

// ===== PianoRollComponent =====

int PianoRollComponent::midiNoteForKey(int index) const {
    // Map key index to MIDI note, skipping black keys in the index
    // White keys: C D E F G A B (indices 0,2,4,5,7,9,11 in chromatic scale)
    static const int whiteToChromatic[] = {0, 2, 4, 5, 7, 9, 11};
    int octave = index / 7;
    int noteInOctave = index % 7;
    return firstMidiNote + octave * 12 + whiteToChromatic[noteInOctave];
}

bool PianoRollComponent::isBlackKey(int midiNote) const {
    int noteInOctave = midiNote % 12;
    return noteInOctave == 1 || noteInOctave == 3 || noteInOctave == 6 ||
           noteInOctave == 8 || noteInOctave == 10;
}

int PianoRollComponent::keyAtPosition(juce::Point<int> pos) const {
    int pbRight = pitchBendWidth;
    int keysX = pbRight + 8;
    int keysWidth = getWidth() - keysX - 6;
    int numWhiteKeys = (numKeys * 7) / 12 + 1;
    int localWhiteKeyWidth = keysWidth / numWhiteKeys;

    // Check black keys first (they're on top)
    for (int i = 0; i < numKeys; ++i) {
        int note = firstMidiNote + i;
        if (!isBlackKey(note)) continue;

        int chroma = note % 12;
        int octave = (note - firstMidiNote) / 12;
        static const int blackToLeftWhite[] = { 0,0,1,1,2,3,3,4,4,5,5,6 };
        int leftWhite = blackToLeftWhite[chroma];
        int whiteIndexBefore = octave * 7 + leftWhite;

        float bx = static_cast<float>(keysX + (whiteIndexBefore + 1) * localWhiteKeyWidth) - static_cast<float>(blackKeyWidth) * 0.5f;
        auto blackRect = juce::Rectangle<int>(static_cast<int>(bx), 0, blackKeyWidth, blackKeyHeight);
        if (blackRect.contains(pos))
            return note;
    }

    // Check white keys
    for (int i = 0; i < numKeys; ++i) {
        int note = firstMidiNote + i;
        if (isBlackKey(note)) continue;

        int chroma = note % 12;
        int octave = (note - firstMidiNote) / 12;
        int whiteIndex = octave * 7;
        if (chroma == 2) whiteIndex += 1;
        else if (chroma == 4) whiteIndex += 2;
        else if (chroma == 5) whiteIndex += 3;
        else if (chroma == 7) whiteIndex += 4;
        else if (chroma == 9) whiteIndex += 5;
        else if (chroma == 11) whiteIndex += 6;

        auto whiteRect = juce::Rectangle<int>(keysX + whiteIndex * localWhiteKeyWidth, 0, localWhiteKeyWidth, whiteKeyHeight);
        if (whiteRect.contains(pos))
            return note;
    }

    return -1;
}

void PianoRollComponent::triggerNote(int note, bool on) {
    if (on)
        processor.noteOn(note, 0.8f);
    else
        processor.noteOff(note);
}

void PianoRollComponent::paint(juce::Graphics& g) {
    int pbRight = pitchBendWidth;
    int keysX = pbRight + 8;
    int keysWidth = getWidth() - keysX - 6;
    int numWhiteKeys = (numKeys * 7) / 12 + 1;
    whiteKeyWidth = keysWidth / numWhiteKeys;

    // Draw pitch bend strip background
    g.setColour(juce::Colour(0xff222222).withAlpha(0.7f));
    g.fillRect(10, 0, pbRight - 10, whiteKeyHeight);

    // Right edge border to separate from keys
    g.setColour(juce::Colour(0xff444444));
    g.drawVerticalLine(pbRight - 8, 0.0f, static_cast<float>(whiteKeyHeight));

    // Active bend area indicator (shaded region from center to current position)
    int pbCenter = whiteKeyHeight / 2;
    float pbNorm = static_cast<float>(currentPitchBend - 64) / 64.0f; // -1..+1
    int pbY = pbCenter - static_cast<int>(pbNorm * static_cast<float>(pbCenter - 8));

    g.setColour(juce::Colour(0x40ccaa44));
    if (pbY < pbCenter)
        g.fillRect(14, pbY, 18, pbCenter - pbY);
    else
        g.fillRect(14, pbCenter, 18, pbY - pbCenter);

    // Pitch bend center line
    g.setColour(juce::Colour(0xffccaa44));
    g.drawHorizontalLine(pbCenter, 12.0f, static_cast<float>(pbRight - 14));

    // Pitch bend indicator dot
    g.setColour(juce::Colour(0xffccaa44));
    g.fillEllipse(14, pbY - 5, 18, 10);

    // PB labels
    g.setColour(juce::Colour(0xff888888));
    g.setFont(9.0f);
    g.drawText("+", 14, 2, 18, 12, juce::Justification::centred);
    g.drawText("-", 14, whiteKeyHeight - 14, 18, 12, juce::Justification::centred);

    // Draw white keys first
    for (int i = 0; i < numKeys; ++i) {
        int note = firstMidiNote + i;
        if (isBlackKey(note)) continue;

        int chroma = note % 12;
        int octave = (note - firstMidiNote) / 12;
        int whiteIndex = octave * 7;
        if (chroma == 2) whiteIndex += 1;
        else if (chroma == 4) whiteIndex += 2;
        else if (chroma == 5) whiteIndex += 3;
        else if (chroma == 7) whiteIndex += 4;
        else if (chroma == 9) whiteIndex += 5;
        else if (chroma == 11) whiteIndex += 6;

        int kx = keysX + whiteIndex * whiteKeyWidth;
        auto rect = juce::Rectangle<int>(kx, 0, whiteKeyWidth - 1, whiteKeyHeight);

        bool pressed = processor.isNoteActive(note);
        if (pressed) {
            g.setColour(juce::Colour(0xffccaa44).withAlpha(0.7f));
        } else {
            juce::ColourGradient grad(juce::Colour(0xffe4e4e4).withAlpha(0.7f), static_cast<float>(kx), 0.0f,
                                      juce::Colour(0xffc8c8c8).withAlpha(0.7f), static_cast<float>(kx), static_cast<float>(whiteKeyHeight), false);
            g.setGradientFill(grad);
        }
        g.fillRect(rect);

        g.setColour(juce::Colour(0xff555555));
        g.drawRect(rect, 1);

        // Draw note name on C keys
        if (chroma == 0) {
            g.setColour(juce::Colour(0xff666666));
            g.setFont(9.0f);
            g.drawText("C" + juce::String(octave + 3), kx + 2, whiteKeyHeight - 14, whiteKeyWidth - 4, 12,
                       juce::Justification::centred);
        }
    }

    // Draw black keys on top
    for (int i = 0; i < numKeys; ++i) {
        int note = firstMidiNote + i;
        if (!isBlackKey(note)) continue;

        int chroma = note % 12;
        int octave = (note - firstMidiNote) / 12;
        // chroma → white key index to the LEFT of this black key
        static const int blackToLeftWhite[] = { 0,0,1,1,2,3,3,4,4,5,5,6 };
        int leftWhite = blackToLeftWhite[chroma];
        int whiteIndexBefore = octave * 7 + leftWhite;

        float bx = static_cast<float>(keysX + (whiteIndexBefore + 1) * whiteKeyWidth) - static_cast<float>(blackKeyWidth) * 0.5f;
        auto rect = juce::Rectangle<int>(static_cast<int>(bx), 0, blackKeyWidth, blackKeyHeight);

        bool pressed = processor.isNoteActive(note);
        if (pressed) {
            g.setColour(juce::Colour(0xff996600));
        } else {
            juce::ColourGradient grad(juce::Colour(0xff444444), static_cast<float>(bx), 0.0f,
                                      juce::Colour(0xff222222), static_cast<float>(bx), static_cast<float>(blackKeyHeight), false);
            g.setGradientFill(grad);
        }
        g.fillRect(rect);

        g.setColour(juce::Colour(0xff555555));
        g.drawRect(rect, 1);
    }
}

void PianoRollComponent::mouseDown(const juce::MouseEvent& e) {
    auto pos = e.getPosition();

    // Pitch bend strip: begin a drag that starts from center (0 bend)
    if (pos.x < pitchBendWidth) {
        pitchBendDragging = true;
        pitchBendGliding = false;
        pitchBendStartY = pos.y;
        currentPitchBend = 64;
        processor.pitchBendValue.store(0.0f);
        repaint();
        return;
    }

    int note = keyAtPosition(pos);
    if (note >= 0) {
        if (lastTriggeredNote >= 0 && lastTriggeredNote != note)
            triggerNote(lastTriggeredNote, false);
        lastTriggeredNote = note;
        triggerNote(note, true);
        repaint();
    }
}

void PianoRollComponent::mouseUp(const juce::MouseEvent& e) {
    // Reset pitch bend to center on release (only if it was actually dragged)
    if (pitchBendDragging) {
        pitchBendDragging = false;
        pitchBendGliding = true;
        pitchBendGlideStart = static_cast<float>(currentPitchBend - 64) / 64.0f;
        pitchBendGlideStartMs = juce::Time::currentTimeMillis();
        repaint();
    }

    if (lastTriggeredNote >= 0) {
        triggerNote(lastTriggeredNote, false);
        lastTriggeredNote = -1;
        repaint();
    }
}

void PianoRollComponent::timerCallback() {
    if (pitchBendGliding) {
        float t = static_cast<float>(juce::Time::currentTimeMillis() - pitchBendGlideStartMs)
                  / static_cast<float>(pitchBendGlideMs);
        if (t >= 1.0f) {
            pitchBendGliding = false;
            currentPitchBend = 64;
            processor.pitchBendValue.store(0.0f);
        } else {
            float pbNorm = pitchBendGlideStart * (1.0f - t);
            currentPitchBend = 64 + static_cast<int>(pbNorm * 64.0f);
            processor.pitchBendValue.store(pbNorm);
        }
        repaint();
    } else {
        repaint();
    }
}

void PianoRollComponent::mouseDrag(const juce::MouseEvent& e) {
    auto pos = e.getPosition();

    // Pitch bend drag — relative to where the drag started (starts at 0)
    if (pos.x < pitchBendWidth) {
        pitchBendDragging = true;
        pitchBendGliding = false;
        float pbNorm = 2.0f * static_cast<float>(pitchBendStartY - pos.y) / static_cast<float>(whiteKeyHeight);
        pbNorm = std::clamp(pbNorm, -1.0f, 1.0f);
        currentPitchBend = 64 + static_cast<int>(pbNorm * 64.0f);
        processor.pitchBendValue.store(pbNorm);
        repaint();
        return;
    }

    int note = keyAtPosition(pos);
    if (note >= 0 && note != lastTriggeredNote) {
        if (lastTriggeredNote >= 0)
            triggerNote(lastTriggeredNote, false);
        lastTriggeredNote = note;
        triggerNote(note, true);
        repaint();
    }
}

ChromaFlockEditor::ChromaFlockEditor(ChromaFlockProcessor& p)
    : AudioProcessorEditor(&p), content(p) {
    addAndMakeVisible(content);
    content.onScaleChanged = [this](float s) { setUIScale(s); };
    setSize(baseWidth, baseHeight);
    setResizable(false, false);
}

ChromaFlockEditor::~ChromaFlockEditor() {}

void ChromaFlockEditor::paint(juce::Graphics& g) {
    g.fillAll(juce::Colour(0xff1a1a1a));
}

void ChromaFlockEditor::resized() {
    content.setBounds(0, 0, baseWidth, baseHeight);
    content.setTransform(juce::AffineTransform::scale(currentScale, currentScale));
}

void ChromaFlockEditor::setUIScale(float newScale) {
    currentScale = newScale;
    setSize(static_cast<int>(baseWidth * newScale), static_cast<int>(baseHeight * newScale));
}
