// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3

#include "theme_effects.h"
#include "low_spec.h"
#include "../schema/ui_schema.h"
#include "../layout.h"
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include "../../util/logger.h"

namespace dragonx {
namespace ui {
namespace effects {

namespace {

// Colors are packed little-endian RGBA (R at bit 0, G at 8, B at 16, A at 24) — the
// same layout IM_COL32 produces. unpackRGB pulls out the three color channels (alpha is
// handled separately at each site).
struct RGB { int r, g, b; };
static inline RGB unpackRGB(ImU32 c) {
    return RGB{ (int)(c & 0xFF), (int)((c >> 8) & 0xFF), (int)((c >> 16) & 0xFF) };
}

// Scales a 0..1 base alpha by the background opacity into a clamped 0..255 int.
static inline int scaledAlpha(float base, float bgOpacity) {
    return std::clamp((int)(base * bgOpacity * 255.0f), 0, 255);
}

} // namespace

// ============================================================================
// Singleton
// ============================================================================

ThemeEffects& ThemeEffects::instance() {
    static ThemeEffects s;
    return s;
}

// ============================================================================
// Frame update
// ============================================================================

void ThemeEffects::beginFrame() {
    time_ = (float)ImGui::GetTime();
    ImGuiViewport* vp = ImGui::GetMainViewport();
    vpMinY_ = vp->WorkPos.y;
    vpMaxY_ = vp->WorkPos.y + vp->WorkSize.y;
}

// ============================================================================
// Load config from [effects] section via UISchema
// ============================================================================

void ThemeEffects::loadFromTheme() {
    auto& S = schema::UI();
    const float dp = Layout::dpiScale();
    auto eff = [&](const char* name) {
        return S.drawElement("effects", name);
    };

    // ---- Hue Cycle ----
    hue_cycle_.enabled = eff("hue-cycle-enabled").sizeOr(0.0f) > 0.5f;
    hue_cycle_.speed   = eff("hue-cycle-speed").sizeOr(0.1f);
    hue_cycle_.sat     = eff("hue-cycle-saturation").sizeOr(0.6f);
    hue_cycle_.val     = eff("hue-cycle-value").sizeOr(0.85f);
    hue_cycle_.range   = eff("hue-cycle-range").sizeOr(1.0f);
    hue_cycle_.offset  = eff("hue-cycle-offset").sizeOr(0.0f);

    // ---- Rainbow Border ----
    rainbow_border_.enabled = eff("rainbow-border-enabled").sizeOr(0.0f) > 0.5f;
    rainbow_border_.speed   = eff("rainbow-border-speed").sizeOr(0.05f);
    rainbow_border_.alpha   = eff("rainbow-border-alpha").sizeOr(0.25f);
    rainbow_border_.stops.clear();

    // Read color stops from extraColors (stored as stop-0, stop-1, etc.)
    for (int i = 0; i < 8; i++) {
        char key[32];
        snprintf(key, sizeof(key), "rainbow-border-stop-%d", i);
        auto elem = eff(key);
        if (!elem.color.empty()) {
            rainbow_border_.stops.push_back(parseHexColor(elem.color));
        } else if (elem.size > 0) {
            break; // no more stops
        }
    }
    // Fallback default stops
    if (rainbow_border_.enabled && rainbow_border_.stops.empty()) {
        rainbow_border_.stops.push_back(IM_COL32(255, 107, 157, 255));
        rainbow_border_.stops.push_back(IM_COL32(192, 132, 252, 255));
        rainbow_border_.stops.push_back(IM_COL32(103, 232, 249, 255));
        rainbow_border_.stops.push_back(IM_COL32(252, 165, 165, 255));
    }

    // ---- Shimmer ----
    shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
    shimmer_.speed   = eff("shimmer-speed").sizeOr(0.12f);
    shimmer_.width   = eff("shimmer-width").sizeOr(80.0f) * dp;
    shimmer_.alpha   = eff("shimmer-alpha").sizeOr(0.06f);
    shimmer_.angle   = eff("shimmer-angle").sizeOr(30.0f);
    // Shimmer color: read from the schema's color resolver
    auto shimmerColorElem = eff("shimmer-color");
    if (!shimmerColorElem.color.empty()) {
        shimmer_.color = S.resolveColor(shimmerColorElem.color, IM_COL32(255,255,255,255));
    } else {
        shimmer_.color = IM_COL32(255, 255, 255, 255);
    }

    // ---- Positional Hue ----
    positional_hue_.enabled  = eff("positional-hue-enabled").sizeOr(0.0f) > 0.5f;
    positional_hue_.strength = eff("positional-hue-strength").sizeOr(0.3f);
    auto topElem = eff("positional-hue-top");
    auto botElem = eff("positional-hue-bottom");
    if (!topElem.color.empty())
        positional_hue_.topColor = parseHexColorVec4(topElem.color);
    if (!botElem.color.empty())
        positional_hue_.bottomColor = parseHexColorVec4(botElem.color);

    // ---- Glow Pulse ----
    glow_pulse_.enabled  = eff("glow-pulse-enabled").sizeOr(0.0f) > 0.5f;
    glow_pulse_.speed    = eff("glow-pulse-speed").sizeOr(2.0f);
    glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
    glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f);
    glow_pulse_.radius   = eff("glow-pulse-radius").sizeOr(4.0f) * dp;
    auto glowColorElem = eff("glow-pulse-color");
    if (!glowColorElem.color.empty()) {
        glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255));
    } else {
        glow_pulse_.color = IM_COL32(255, 218, 0, 255);
    }

    // ---- Edge Trace ----
    edge_trace_.enabled   = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
    edge_trace_.speed     = eff("edge-trace-speed").sizeOr(0.3f);
    edge_trace_.length    = eff("edge-trace-length").sizeOr(0.20f);
    edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f) * dp;
    edge_trace_.alpha     = eff("edge-trace-alpha").sizeOr(0.6f);
    auto edgeColorElem = eff("edge-trace-color");
    if (!edgeColorElem.color.empty()) {
        edge_trace_.color = S.resolveColor(edgeColorElem.color, IM_COL32(255, 255, 255, 255));
    } else {
        edge_trace_.color = IM_COL32(255, 255, 255, 255);
    }

    // ---- Ember Rise ----
    ember_rise_.enabled      = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
    ember_rise_.count        = (int)eff("ember-rise-count").sizeOr(8.0f);
    ember_rise_.speed        = eff("ember-rise-speed").sizeOr(0.4f);
    ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f) * dp;
    ember_rise_.alpha        = eff("ember-rise-alpha").sizeOr(0.5f);
    auto emberColorElem = eff("ember-rise-color");
    if (!emberColorElem.color.empty()) {
        ember_rise_.color = S.resolveColor(emberColorElem.color, IM_COL32(255, 120, 20, 255));
    } else {
        ember_rise_.color = IM_COL32(255, 120, 20, 255);
    }

    // ---- Gradient Border Shift ----
    gradient_border_.enabled   = eff("gradient-border-enabled").sizeOr(0.0f) > 0.5f;
    // Opt-in: also draw the shifting border on every glass panel (not just the
    // active nav button). Off by default so themes that only want the button
    // accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
    gradient_border_.panels    = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
    gradient_border_.speed     = eff("gradient-border-speed").sizeOr(0.15f);
    gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f) * dp;
    gradient_border_.alpha     = eff("gradient-border-alpha").sizeOr(0.6f);
    auto gbColorA = eff("gradient-border-color-a");
    if (!gbColorA.color.empty()) {
        gradient_border_.colorA = S.resolveColor(gbColorA.color, IM_COL32(206, 147, 216, 255));
    }
    auto gbColorB = eff("gradient-border-color-b");
    if (!gbColorB.color.empty()) {
        gradient_border_.colorB = S.resolveColor(gbColorB.color, IM_COL32(26, 35, 126, 255));
    }

    // ---- Specular Glare ----
    specular_glare_.enabled   = eff("specular-glare-enabled").sizeOr(0.0f) > 0.5f;
    specular_glare_.speed     = eff("specular-glare-speed").sizeOr(0.02f);
    specular_glare_.intensity = eff("specular-glare-intensity").sizeOr(0.06f);
    specular_glare_.radius    = eff("specular-glare-radius").sizeOr(0.5f);
    specular_glare_.count     = (int)eff("specular-glare-count").sizeOr(2.0f);
    auto glareColorElem = eff("specular-glare-color");
    if (!glareColorElem.color.empty()) {
        specular_glare_.color = S.resolveColor(glareColorElem.color, IM_COL32(255, 255, 255, 255));
    } else {
        specular_glare_.color = IM_COL32(255, 255, 255, 255);
    }

    // ---- Sandstorm ----
    sandstorm_.enabled      = eff("sandstorm-enabled").sizeOr(0.0f) > 0.5f;
    sandstorm_.count        = (int)eff("sandstorm-count").sizeOr(80.0f);
    sandstorm_.speed        = eff("sandstorm-speed").sizeOr(0.35f);
    sandstorm_.windAngle    = eff("sandstorm-wind-angle").sizeOr(15.0f);
    sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f) * dp;
    sandstorm_.alpha        = eff("sandstorm-alpha").sizeOr(0.35f);
    sandstorm_.gustSpeed    = eff("sandstorm-gust-speed").sizeOr(0.07f);
    sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f);
    sandstorm_.streakLength = eff("sandstorm-streak-length").sizeOr(3.0f);
    auto sandColorElem = eff("sandstorm-color");
    if (!sandColorElem.color.empty()) {
        sandstorm_.color = S.resolveColor(sandColorElem.color, IM_COL32(200, 160, 96, 255));
    } else {
        sandstorm_.color = IM_COL32(200, 160, 96, 255);
    }

    // ---- Viewport Overlay (shader-like post-processing) ----
    viewport_overlay_.colorWashEnabled = eff("viewport-wash-enabled").sizeOr(0.0f) > 0.5f;
    viewport_overlay_.washAlpha        = eff("viewport-wash-alpha").sizeOr(0.05f);
    viewport_overlay_.washRotateSpeed  = eff("viewport-wash-rotate").sizeOr(0.0f);
    viewport_overlay_.washPulseSpeed   = eff("viewport-wash-pulse").sizeOr(0.0f);
    viewport_overlay_.washPulseDepth   = eff("viewport-wash-pulse-depth").sizeOr(0.0f);
    auto washTL = eff("viewport-wash-tl");
    auto washTR = eff("viewport-wash-tr");
    auto washBL = eff("viewport-wash-bl");
    auto washBR = eff("viewport-wash-br");
    if (!washTL.color.empty()) viewport_overlay_.cornerTL = S.resolveColor(washTL.color, IM_COL32(255,100,0,255));
    if (!washTR.color.empty()) viewport_overlay_.cornerTR = S.resolveColor(washTR.color, IM_COL32(68,34,0,255));
    if (!washBL.color.empty()) viewport_overlay_.cornerBL = S.resolveColor(washBL.color, IM_COL32(68,17,0,255));
    if (!washBR.color.empty()) viewport_overlay_.cornerBR = S.resolveColor(washBR.color, IM_COL32(255,153,0,255));

    viewport_overlay_.vignetteEnabled  = eff("viewport-vignette-enabled").sizeOr(0.0f) > 0.5f;
    viewport_overlay_.vignetteRadius   = eff("viewport-vignette-radius").sizeOr(0.35f);
    viewport_overlay_.vignetteAlpha    = eff("viewport-vignette-alpha").sizeOr(0.30f);
    auto vigColorElem = eff("viewport-vignette-color");
    if (!vigColorElem.color.empty()) {
        viewport_overlay_.vignetteColor = S.resolveColor(vigColorElem.color, IM_COL32(0,0,0,255));
    } else {
        viewport_overlay_.vignetteColor = IM_COL32(0,0,0,255);
    }

    DEBUG_LOGF("[ThemeEffects] Loaded — hue:%d rainbow:%d shimmer:%d pos_hue:%d glow:%d edge:%d ember:%d glare:%d sand:%d wash:%d vignette:%d\n",
           hue_cycle_.enabled, rainbow_border_.enabled, shimmer_.enabled,
           positional_hue_.enabled, glow_pulse_.enabled,
           edge_trace_.enabled, ember_rise_.enabled, specular_glare_.enabled,
           sandstorm_.enabled,
           viewport_overlay_.colorWashEnabled, viewport_overlay_.vignetteEnabled);
}

// ============================================================================
// Hue-Cycling Accents
// ============================================================================

ImU32 ThemeEffects::getAccentColor(float phaseOffset) const {
    if (!enabled_ || !hue_cycle_.enabled) return IM_COL32(255, 218, 0, 255);

    float hue = std::fmod(hue_cycle_.offset + time_ * hue_cycle_.speed + phaseOffset, 1.0f);
    if (hue < 0.0f) hue += 1.0f;
    // Clamp to configured range
    hue = hue_cycle_.offset + std::fmod(hue, std::max(0.01f, hue_cycle_.range));
    hue = std::fmod(hue, 1.0f);

    float r, g, b;
    ImGui::ColorConvertHSVtoRGB(hue, hue_cycle_.sat, hue_cycle_.val, r, g, b);
    return IM_COL32((int)(r * 255), (int)(g * 255), (int)(b * 255), 255);
}

// ============================================================================
// Rainbow Gradient Border
// ============================================================================

ImU32 ThemeEffects::sampleGradient(float t) const {
    if (rainbow_border_.stops.empty()) return IM_COL32(255,255,255,128);
    int n = (int)rainbow_border_.stops.size();
    if (n == 1) return rainbow_border_.stops[0];

    t = std::fmod(t, 1.0f);
    if (t < 0.0f) t += 1.0f;

    float segment = t * n;
    int idx = (int)segment;
    float frac = segment - idx;
    int next = (idx + 1) % n;
    idx = idx % n;

    ImU32 c1 = rainbow_border_.stops[idx];
    ImU32 c2 = rainbow_border_.stops[next];

    // Lerp RGBA components
    int r = (int)((c1 & 0xFF) + frac * (((int)(c2 & 0xFF)) - (int)(c1 & 0xFF)));
    int g = (int)(((c1 >> 8) & 0xFF) + frac * (((int)((c2 >> 8) & 0xFF)) - (int)((c1 >> 8) & 0xFF)));
    int b = (int)(((c1 >> 16) & 0xFF) + frac * (((int)((c2 >> 16) & 0xFF)) - (int)((c1 >> 16) & 0xFF)));
    int a = (int)(rainbow_border_.alpha * bgOpacity_ * 255.0f);

    return IM_COL32(r, g, b, a);
}

void ThemeEffects::drawRainbowBorder(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
                                      float rounding, float thickness) const {
    if (!enabled_ || !rainbow_border_.enabled || rainbow_border_.stops.empty()) return;

    float t = time_ * rainbow_border_.speed;

    // Draw the rainbow border as line segments around the rounded perimeter.
    // Each segment gets a color sampled from the gradient at its position,
    // giving a smoothly rotating hue-shift that follows rounded corners.
    //
    // We need enough segments so that the curved corners are smooth.
    // Each corner arc ≈ π*r/2 pixels.  We want ~1 segment per 2-3px of arc
    // to look smooth, plus the straight edges.
    float w = pMax.x - pMin.x;
    float h = pMax.y - pMin.y;
    float r = std::min(rounding, std::min(w, h) * 0.5f);
    float cornerSegs = (r > 0.5f) ? std::ceil(r * 1.5f) : 0.0f;  // segments per corner arc
    int segments = std::max(48, (int)(4 * cornerSegs + 2 * (w + h) / 8.0f));
    segments = std::min(segments, 256);  // cap for perf

    ImVec2 prev = perimeterPoint(pMin, pMax, 0.0f, rounding);
    for (int i = 1; i <= segments; i++) {
        float frac = (float)i / (float)segments;
        ImVec2 pt = perimeterPoint(pMin, pMax, frac, rounding);

        // Sample gradient at the midpoint of this segment for smooth color
        float midFrac = (frac - 0.5f / segments);
        ImU32 c = sampleGradient(t + midFrac);

        dl->AddLine(prev, pt, c, thickness);
        prev = pt;
    }
}

// ============================================================================
// Shimmer Sweep
// ============================================================================

void ThemeEffects::drawShimmer(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
                                float rounding) const {
    if (!enabled_ || !shimmer_.enabled) return;

    float w = pMax.x - pMin.x;
    float h = pMax.y - pMin.y;
    if (w <= 0 || h <= 0) return;

    // Sweep position: band moves left to right across the panel
    float period = 1.0f / std::max(0.01f, shimmer_.speed);
    float totalTravel = w + shimmer_.width * 2.0f;  // band enters and exits
    float pos = std::fmod(time_ / period, 1.0f) * totalTravel - shimmer_.width;

    // Band bounds (horizontal, ignoring angle for simplicity in this version)
    float bandLeft = pMin.x + pos;
    float bandRight = bandLeft + shimmer_.width;

    // Clip to panel
    float clLeft = std::max(bandLeft, pMin.x);
    float clRight = std::min(bandRight, pMax.x);
    if (clLeft >= clRight) return;

    float mid = (clLeft + clRight) * 0.5f;
    int peakA = scaledAlpha(shimmer_.alpha, bgOpacity_);

    // Extract shimmer color RGB (ignore alpha, use shimmer_.alpha)
    RGB s = unpackRGB(shimmer_.color);

    ImU32 clear = IM_COL32(s.r, s.g, s.b, 0);
    ImU32 peak  = IM_COL32(s.r, s.g, s.b, peakA);

    // Inset clip rect by corner rounding to prevent shimmer bleeding into rounded corners
    float cr = std::min(rounding, std::min(w, h) * 0.5f) * 0.3f;
    dl->PushClipRect(ImVec2(pMin.x + cr, pMin.y + cr),
                     ImVec2(pMax.x - cr, pMax.y - cr), true);

    // Left half: transparent → peak
    if (clLeft < mid) {
        dl->AddRectFilledMultiColor(
            ImVec2(clLeft, pMin.y), ImVec2(mid, pMax.y),
            clear, peak, peak, clear);
    }
    // Right half: peak → transparent
    if (mid < clRight) {
        dl->AddRectFilledMultiColor(
            ImVec2(mid, pMin.y), ImVec2(clRight, pMax.y),
            peak, clear, clear, peak);
    }

    dl->PopClipRect();
}

// ============================================================================
// Specular Glare — polished-surface highlights (obsidian / glass look)
// ============================================================================

void ThemeEffects::drawSpecularGlare(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
                                      float rounding) const {
    if (!enabled_ || !specular_glare_.enabled || reduced_transparency_) return;

    float w = pMax.x - pMin.x;
    float h = pMax.y - pMin.y;
    if (w <= 20 || h <= 10) return; // skip tiny panels

    float minDim = std::min(w, h);
    float glareR = minDim * specular_glare_.radius;

    RGB c = unpackRGB(specular_glare_.color);

    // Per-panel seed from position — unique drift per panel
    float seed = pMin.x * 0.0073f + pMin.y * 0.0137f;

    // Clip to panel with rounding inset
    float cr = std::min(rounding, minDim * 0.5f) * 0.3f;
    dl->PushClipRect(ImVec2(pMin.x + cr, pMin.y + cr),
                     ImVec2(pMax.x - cr, pMax.y - cr), true);

    for (int s = 0; s < specular_glare_.count; s++) {
        float phaseSeed = seed + s * 3.14159265f;

        // Slow Lissajous drift — each spot follows a unique orbit
        float freqX = 2.0f * 3.14159265f * specular_glare_.speed;
        float freqY = freqX * 1.618f; // golden ratio offset → non-repeating path
        float driftX = std::sin(time_ * freqX + phaseSeed) * 0.3f + 0.5f;
        float driftY = std::cos(time_ * freqY + phaseSeed * 1.3f) * 0.25f + 0.4f;

        float cx = pMin.x + w * driftX;
        float cy = pMin.y + h * driftY;

        // Concentric circles with Gaussian falloff for a soft, blurred glow.
        // More rings = smoother gradient, more segments = rounder circles.
        const int rings = 20;
        for (int r = rings; r >= 1; r--) {
            float frac = (float)r / (float)rings; // 1.0 → outer, 0.05 → innermost
            float ringRadius = glareR * frac;
            // Gaussian falloff: exp(-frac² * 4) — soft blur-like appearance
            float alphaMul = std::exp(-frac * frac * 4.0f);
            int a = (int)(specular_glare_.intensity * alphaMul * bgOpacity_ * 255.0f);
            if (a <= 0) continue;

            dl->AddCircleFilled(ImVec2(cx, cy), ringRadius,
                                IM_COL32(c.r, c.g, c.b, a), 32);
        }
    }

    dl->PopClipRect();
}

// ============================================================================
// Positional Hue Tinting
// ============================================================================

ImU32 ThemeEffects::getPositionalTint(float screenY) const {
    if (!enabled_ || !positional_hue_.enabled) return IM_COL32(255,255,255,255);

    float t = 0.5f;
    float range = vpMaxY_ - vpMinY_;
    if (range > 0) {
        t = std::clamp((screenY - vpMinY_) / range, 0.0f, 1.0f);
    }

    // Lerp between top and bottom colors
    float r = positional_hue_.topColor.x + t * (positional_hue_.bottomColor.x - positional_hue_.topColor.x);
    float g = positional_hue_.topColor.y + t * (positional_hue_.bottomColor.y - positional_hue_.topColor.y);
    float b = positional_hue_.topColor.z + t * (positional_hue_.bottomColor.z - positional_hue_.topColor.z);

    return IM_COL32((int)(r * 255), (int)(g * 255), (int)(b * 255), 255);
}

ImU32 ThemeEffects::tintByPosition(ImU32 baseColor, float screenY) const {
    if (!enabled_ || !positional_hue_.enabled || positional_hue_.strength <= 0.0f) return baseColor;

    ImU32 tint = getPositionalTint(screenY);
    float s = positional_hue_.strength;
    float inv = 1.0f - s;

    RGB base = unpackRGB(baseColor);
    int bA = (baseColor >> 24) & 0xFF;

    RGB t = unpackRGB(tint);

    int r = (int)(base.r * inv + t.r * s);
    int g = (int)(base.g * inv + t.g * s);
    int b = (int)(base.b * inv + t.b * s);

    return IM_COL32(r, g, b, bA);
}

// ============================================================================
// Glow Pulse
// ============================================================================

void ThemeEffects::drawGlowPulse(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
                                  float rounding) const {
    if (!enabled_ || !glow_pulse_.enabled) return;

    // Sinusoidal oscillation
    float phase = std::sin(time_ * glow_pulse_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
    float alpha = glow_pulse_.minAlpha + phase * (glow_pulse_.maxAlpha - glow_pulse_.minAlpha);
    if (alpha <= 0.001f) return;

    int a = scaledAlpha(alpha, bgOpacity_);
    RGB c = unpackRGB(glow_pulse_.color);

    // Border-only glow: concentric outlines with smooth Gaussian falloff.
    // Use many fine sub-rings (4× the pixel radius) to avoid visible
    // stepping as the overall alpha pulses.
    float maxExpand = glow_pulse_.radius;
    int rings = std::max(4, (int)(maxExpand * 4.0f));
    for (int i = 1; i <= rings; i++) {
        float expand = maxExpand * (float)i / (float)rings;
        float frac = (float)i / (float)rings;  // 0→1 from inner to outer
        // Gaussian-like falloff: smooth and continuous
        float falloff = std::exp(-frac * frac * 3.0f);
        int ringAlpha = (int)(a * falloff);
        if (ringAlpha <= 0) continue;

        float thickness = std::max(0.5f, 1.5f * (1.0f - frac));
        dl->AddRect(
            ImVec2(pMin.x - expand, pMin.y - expand),
            ImVec2(pMax.x + expand, pMax.y + expand),
            IM_COL32(c.r, c.g, c.b, ringAlpha),
            rounding + expand * 0.5f, 0, thickness);
    }
}

// ============================================================================
// Gradient Border Shift — border color oscillates between two gem colors
// ============================================================================

void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
                                            float rounding, float phaseOffset,
                                            float alphaMul) const {
    if (!enabled_ || !gradient_border_.enabled) return;

    // Smooth sinusoidal oscillation between color A and color B.
    // phaseOffset shifts where in the cycle this element sits so a wall of
    // panels reads like veins at different depths rather than one pulse.
    float phase = std::sin((time_ * gradient_border_.speed + phaseOffset)
                           * 2.0f * 3.14159265f) * 0.5f + 0.5f;

    // Extract RGBA from both colors and lerp
    RGB ca = unpackRGB(gradient_border_.colorA);
    RGB cb = unpackRGB(gradient_border_.colorB);

    int r = ca.r + (int)((cb.r - ca.r) * phase);
    int g = ca.g + (int)((cb.g - ca.g) * phase);
    int b = ca.b + (int)((cb.b - ca.b) * phase);
    int a = scaledAlpha(gradient_border_.alpha * alphaMul, bgOpacity_);

    // Draw the shifting border
    dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a),
                rounding, 0, gradient_border_.thickness);

    // Subtle outer glow ring at lower alpha for depth
    float expand = gradient_border_.thickness;
    int glowA = a / 3;
    if (glowA > 0) {
        dl->AddRect(
            ImVec2(pMin.x - expand, pMin.y - expand),
            ImVec2(pMax.x + expand, pMax.y + expand),
            IM_COL32(r, g, b, glowA),
            rounding + expand * 0.5f, 0, 1.0f);
    }
}

// ============================================================================
// Edge Trace — a bright segment that traces the border perimeter
// ============================================================================

ImVec2 ThemeEffects::perimeterPoint(ImVec2 pMin, ImVec2 pMax, float t, float rounding) const {
    float w = pMax.x - pMin.x;
    float h = pMax.y - pMin.y;
    if (w <= 0 || h <= 0) return pMin;

    // Clamp rounding to half of the smaller dimension
    float r = std::min(rounding, std::min(w, h) * 0.5f);
    if (r < 0.5f) r = 0.0f; // treat tiny rounding as none

    t = std::fmod(t, 1.0f);
    if (t < 0.0f) t += 1.0f;

    if (r <= 0.0f) {
        // Original rectangular path
        float perimeter = 2.0f * (w + h);
        float d = t * perimeter;
        if (d < w)           return ImVec2(pMin.x + d, pMin.y);
        else if (d < w + h)  return ImVec2(pMax.x, pMin.y + (d - w));
        else if (d < 2*w+h)  return ImVec2(pMax.x - (d - w - h), pMax.y);
        else                 return ImVec2(pMin.x, pMax.y - (d - 2*w - h));
    }

    // Rounded rect perimeter:
    // Segments (clockwise starting from top-left of top edge):
    //   1. Top edge:    (x+r, y)       → (x+w-r, y)       len = w - 2r
    //   2. TR arc:      center (x+w-r, y+r)  -90° → 0°    len = πr/2
    //   3. Right edge:  (x+w, y+r)     → (x+w, y+h-r)     len = h - 2r
    //   4. BR arc:      center (x+w-r, y+h-r)  0° → 90°   len = πr/2
    //   5. Bottom edge: (x+w-r, y+h)   → (x+r, y+h)       len = w - 2r
    //   6. BL arc:      center (x+r, y+h-r)  90° → 180°   len = πr/2
    //   7. Left edge:   (x, y+h-r)     → (x, y+r)         len = h - 2r
    //   8. TL arc:      center (x+r, y+r)  180° → 270°    len = πr/2
    const float PI = 3.14159265f;
    float arcLen = PI * r * 0.5f;
    float edgeW = w - 2.0f * r;
    float edgeH = h - 2.0f * r;
    float perimeter = 2.0f * edgeW + 2.0f * edgeH + 4.0f * arcLen;
    float d = t * perimeter;

    // Segment boundaries (cumulative)
    float s1 = edgeW;                        // top edge
    float s2 = s1 + arcLen;                  // TR arc
    float s3 = s2 + edgeH;                   // right edge
    float s4 = s3 + arcLen;                  // BR arc
    float s5 = s4 + edgeW;                   // bottom edge
    float s6 = s5 + arcLen;                  // BL arc
    float s7 = s6 + edgeH;                   // left edge
    // s8 = s7 + arcLen = perimeter          // TL arc

    if (d < s1) {
        // Top edge
        return ImVec2(pMin.x + r + d, pMin.y);
    } else if (d < s2) {
        // TR arc: center (pMax.x - r, pMin.y + r), angle -90° to 0°
        float frac = (d - s1) / arcLen;
        float angle = -PI * 0.5f + frac * PI * 0.5f;
        return ImVec2(pMax.x - r + r * std::cos(angle),
                      pMin.y + r + r * std::sin(angle));
    } else if (d < s3) {
        // Right edge
        return ImVec2(pMax.x, pMin.y + r + (d - s2));
    } else if (d < s4) {
        // BR arc: center (pMax.x - r, pMax.y - r), angle 0° to 90°
        float frac = (d - s3) / arcLen;
        float angle = frac * PI * 0.5f;
        return ImVec2(pMax.x - r + r * std::cos(angle),
                      pMax.y - r + r * std::sin(angle));
    } else if (d < s5) {
        // Bottom edge (right to left)
        return ImVec2(pMax.x - r - (d - s4), pMax.y);
    } else if (d < s6) {
        // BL arc: center (pMin.x + r, pMax.y - r), angle 90° to 180°
        float frac = (d - s5) / arcLen;
        float angle = PI * 0.5f + frac * PI * 0.5f;
        return ImVec2(pMin.x + r + r * std::cos(angle),
                      pMax.y - r + r * std::sin(angle));
    } else if (d < s7) {
        // Left edge (bottom to top)
        return ImVec2(pMin.x, pMax.y - r - (d - s6));
    } else {
        // TL arc: center (pMin.x + r, pMin.y + r), angle 180° to 270°
        float frac = (d - s7) / arcLen;
        float angle = PI + frac * PI * 0.5f;
        return ImVec2(pMin.x + r + r * std::cos(angle),
                      pMin.y + r + r * std::sin(angle));
    }
}

void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
                                  float rounding) const {
    if (!enabled_ || !edge_trace_.enabled) return;

    float headPos = std::fmod(time_ * edge_trace_.speed, 1.0f);
    float traceLen = edge_trace_.length;

    RGB c = unpackRGB(edge_trace_.color);

    // Draw the trace as connected line segments with fading alpha.
    // Use enough segments to make curved corners smooth.
    float w = pMax.x - pMin.x;
    float h = pMax.y - pMin.y;
    float r = std::min(rounding, std::min(w, h) * 0.5f);
    float cornerSegs = (r > 0.5f) ? std::ceil(r * 1.5f) : 0.0f;
    // Scale by trace length — we only draw a fraction of the perimeter
    int segments = std::max(24, (int)((4 * cornerSegs + 2 * (w + h) / 8.0f) * traceLen + 8));
    segments = std::min(segments, 128);

    ImVec2 prev = perimeterPoint(pMin, pMax, headPos, rounding);
    for (int i = 1; i <= segments; i++) {
        float frac = (float)i / (float)segments; // 0→1 from head to tail
        float pos = headPos - frac * traceLen;
        if (pos < 0.0f) pos += 1.0f;

        ImVec2 pt = perimeterPoint(pMin, pMax, pos, rounding);

        // Alpha fades from head (1.0) to tail (0.0), with a cubic falloff
        float aFrac = 1.0f - frac;
        aFrac = aFrac * aFrac; // quadratic falloff for natural fade
        int a = (int)(edge_trace_.alpha * aFrac * bgOpacity_ * 255.0f);
        if (a > 0) {
            dl->AddLine(prev, pt, IM_COL32(c.r, c.g, c.b, a), edge_trace_.thickness);
        }
        prev = pt;
    }

    // Bright dot at the head
    int headA = scaledAlpha(edge_trace_.alpha, bgOpacity_);
    ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
    dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.5f,
                        IM_COL32(c.r, c.g, c.b, headA), 8);
}

// ============================================================================
// Ember Rise — fire particles that drift upward from the element
// ============================================================================

void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
    if (!enabled_ || !ember_rise_.enabled) return;

    const float dp = Layout::dpiScale();
    float w = pMax.x - pMin.x;
    float h = pMax.y - pMin.y;
    if (w <= 0 || h <= 0) return;

    RGB c = unpackRGB(ember_rise_.color);

    for (int i = 0; i < ember_rise_.count; i++) {
        // Golden-ratio spacing gives evenly distributed phases
        float phase = std::fmod(time_ * ember_rise_.speed + i * 0.618033988f, 1.0f);

        // Deterministic pseudo-random x position per particle
        // Simple hash: sin of large prime multiples
        float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f;
        float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f * dp; // gentle sway

        float x = pMin.x + w * xHash + xDrift;
        float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top

        // Fade in at bottom (phase 0→0.2), full in middle, fade out at top (0.7→1.0)
        float aFrac;
        if (phase < 0.15f) {
            aFrac = phase / 0.15f;
        } else if (phase > 0.7f) {
            aFrac = (1.0f - phase) / 0.3f;
        } else {
            aFrac = 1.0f;
        }

        // Size decreases as particle rises (embers shrink)
        float size = ember_rise_.particleSize * (1.0f - phase * 0.4f);

        int a = (int)(ember_rise_.alpha * aFrac * bgOpacity_ * 255.0f);

        // Warm core
        dl->AddCircleFilled(ImVec2(x, y), size,
                            IM_COL32(c.r, c.g, c.b, a), 6);
        // Softer outer glow
        if (a > 30) {
            dl->AddCircleFilled(ImVec2(x, y), size * 2.0f,
                                IM_COL32(c.r, c.g, c.b, a / 4), 6);
        }
    }
}

// ============================================================================
// Viewport-wide ambient ember particles (fire theme atmosphere)
// ============================================================================

void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
    if (!enabled_ || !ember_rise_.enabled) return;

    const float dp = Layout::dpiScale();
    ImGuiViewport* vp = ImGui::GetMainViewport();
    float vpW = vp->WorkSize.x;
    float vpH = vp->WorkSize.y;
    float vpX = vp->WorkPos.x;
    float vpY = vp->WorkPos.y;
    if (vpW <= 0 || vpH <= 0) return;

    RGB c = unpackRGB(ember_rise_.color);

    // Viewport embers: more particles, spread across the whole screen
    int vpCount = ember_rise_.count * 3;
    float vpAlpha = ember_rise_.alpha * 0.4f * bgOpacity_; // softer than panel embers

    for (int i = 0; i < vpCount; i++) {
        float phase = std::fmod(time_ * ember_rise_.speed * 0.6f + i * 0.618033988f, 1.0f);

        // Deterministic x position spread across viewport
        float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
        xHash = xHash - (int)xHash; // fractional part
        if (xHash < 0) xHash += 1.0f;
        float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f * dp;

        float x = vpX + vpW * xHash + xDrift;
        float y = vpY + vpH * (1.0f - phase); // rise from bottom to top

        // Fade in/out
        float aFrac;
        if (phase < 0.1f) aFrac = phase / 0.1f;
        else if (phase > 0.75f) aFrac = (1.0f - phase) / 0.25f;
        else aFrac = 1.0f;

        float size = ember_rise_.particleSize * 0.8f * (1.0f - phase * 0.3f);
        int a = (int)(vpAlpha * aFrac * 255.0f);
        if (a <= 0) continue;

        dl->AddCircleFilled(ImVec2(x, y), size,
                            IM_COL32(c.r, c.g, c.b, a), 6);
        if (a > 20) {
            dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
                                IM_COL32(c.r, c.g, c.b, a / 5), 6);
        }
    }
}

// ============================================================================
// Sandstorm — wind-driven sand/dust particles blowing across the viewport
// ============================================================================

void ThemeEffects::drawSandstorm(ImDrawList* dl) const {
    if (!enabled_ || !sandstorm_.enabled || effects::isLowSpecMode()) return;

    ImGuiViewport* vp = ImGui::GetMainViewport();
    float vpW = vp->WorkSize.x;
    float vpH = vp->WorkSize.y;
    float vpX = vp->WorkPos.x;
    float vpY = vp->WorkPos.y;
    if (vpW <= 0 || vpH <= 0) return;

    RGB c = unpackRGB(sandstorm_.color);

    // Wind direction vector from angle (degrees from horizontal)
    float windRad = sandstorm_.windAngle * 3.14159265f / 180.0f;
    float windDx = std::cos(windRad);  // horizontal component
    float windDy = std::sin(windRad);  // vertical component (positive = downward)

    // Global gust modulation: smoothly varies wind speed
    float gust = 1.0f + sandstorm_.gustStrength *
        std::sin(time_ * sandstorm_.gustSpeed * 2.0f * 3.14159265f);

    // Diagonal traversal distance (how far a particle travels across viewport)
    float traversal = vpW + vpH * std::abs(windDy / std::max(0.01f, windDx));

    for (int i = 0; i < sandstorm_.count; i++) {
        // Deterministic pseudo-random properties per particle
        float hash1 = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
        hash1 = hash1 - (int)hash1;
        if (hash1 < 0) hash1 += 1.0f;

        float hash2 = std::sin((float)(i + 1) * 269.5f) * 27183.3291f;
        hash2 = hash2 - (int)hash2;
        if (hash2 < 0) hash2 += 1.0f;

        float hash3 = std::sin((float)(i + 1) * 419.3f) * 15731.7927f;
        hash3 = hash3 - (int)hash3;
        if (hash3 < 0) hash3 += 1.0f;

        // Per-particle speed variation (0.5x to 1.5x base speed)
        float speedMul = 0.5f + hash1;
        // Per-particle size variation (0.4x to 1.6x base)
        float size = sandstorm_.particleSize * (0.4f + hash2 * 1.2f);
        // Per-particle alpha variation (0.5x to 1.0x)
        float alphaMul = 0.5f + hash3 * 0.5f;

        // Phase: horizontal progress across viewport (0→1)
        // Different particles have staggered starts via golden ratio
        float phase = std::fmod(
            time_ * sandstorm_.speed * speedMul * gust + i * 0.618033988f, 1.0f);

        // Y position: distributed across viewport with turbulent drift
        float yBase = vpY + vpH * hash2;
        float yTurb = std::sin(time_ * 0.8f + i * 3.7f) * vpH * 0.04f;
        // Additional gust-correlated vertical shift
        float yGust = std::sin(time_ * sandstorm_.gustSpeed * 6.28318f + i * 1.3f) * vpH * 0.02f;

        // Position: particle blows right-to-left with downward drift
        float x = vpX + vpW * (1.0f - phase) + phase * traversal * (1.0f - windDx) * 0.1f;
        float y = yBase + yTurb + yGust + phase * vpH * windDy * 0.3f;

        // Wrap Y if it goes out of bounds
        if (y < vpY) y += vpH;
        if (y > vpY + vpH) y -= vpH;

        // Fade in at entry edge (phase 0→0.1), fade out at exit (0.85→1.0)
        float aFrac;
        if (phase < 0.1f) aFrac = phase / 0.1f;
        else if (phase > 0.85f) aFrac = (1.0f - phase) / 0.15f;
        else aFrac = 1.0f;

        int a = (int)(sandstorm_.alpha * alphaMul * aFrac * bgOpacity_ * 255.0f);
        if (a <= 0) continue;

        // Fast particles draw as motion-blurred streaks
        float particleSpeed = sandstorm_.speed * speedMul * gust;
        float streakLen = sandstorm_.streakLength * particleSpeed * size;

        if (streakLen > size * 1.5f) {
            // Draw as a short line (motion streak)
            float sx = -windDx * streakLen;
            float sy = -windDy * streakLen * 0.3f;
            dl->AddLine(
                ImVec2(x, y),
                ImVec2(x + sx, y + sy),
                IM_COL32(c.r, c.g, c.b, a),
                size * 0.7f);
            // Bright head dot
            dl->AddCircleFilled(ImVec2(x, y), size * 0.5f,
                                IM_COL32(c.r, c.g, c.b, std::min(255, a * 3 / 2)), 5);
        } else {
            // Small/slow particles: simple circle
            dl->AddCircleFilled(ImVec2(x, y), size,
                                IM_COL32(c.r, c.g, c.b, a), 6);
        }

        // Occasional larger dust puff (every ~8th particle, double size, half alpha)
        if (i % 8 == 0 && a > 15) {
            dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
                                IM_COL32(c.r, c.g, c.b, a / 4), 8);
        }
    }
}

// ============================================================================
// Panel-level effects (applied to every glass panel via DrawGlassPanel)
// ============================================================================

void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
                                     float rounding) const {
    if (!enabled_ || effects::isLowSpecMode()) return;

    // Gradient border on panels — a slow color-shifting outline that hugs the
    // panel's rounded corners (drawn via AddRect, so it follows the rounding
    // exactly — no polygonal corners). Position-based phase offset makes each
    // panel drift like a vein at a different depth; softer than the active
    // nav button (alphaMul 0.6) so a screenful of panels stays calm.
    if (gradient_border_.enabled && gradient_border_.panels) {
        float w = pMax.x - pMin.x;
        float h = pMax.y - pMin.y;
        if (w > 80 && h > 40) { // skip small panels
            float posKey = (pMin.x * 0.0073f + pMin.y * 0.0137f);
            posKey = posKey - (int)posKey; // fractional 0..1
            if (posKey < 0) posKey += 1.0f;
            drawGradientBorderShift(dl, pMin, pMax, rounding, posKey, 0.6f);
        }
    }

    // Edge trace on panels — use position-based phase offset so each
    // panel's tracer is at a different position around the border
    if (edge_trace_.enabled) {
        float w = pMax.x - pMin.x;
        float h = pMax.y - pMin.y;
        if (w > 80 && h > 40) { // skip small panels
            // Generate a consistent phase offset from panel position
            float posKey = (pMin.x * 0.0073f + pMin.y * 0.0137f);
            posKey = posKey - (int)posKey; // fractional 0..1
            if (posKey < 0) posKey += 1.0f;

            float headPos = std::fmod(time_ * edge_trace_.speed + posKey, 1.0f);
            float traceLen = edge_trace_.length;

            RGB c = unpackRGB(edge_trace_.color);

            // Panel edge trace is subtler than sidebar — 60% alpha
            float panelAlpha = edge_trace_.alpha * 0.6f * bgOpacity_;

            const int segments = 10;
            ImVec2 prev = perimeterPoint(pMin, pMax, headPos, rounding);
            for (int i = 1; i <= segments; i++) {
                float frac = (float)i / (float)segments;
                float pos = headPos - frac * traceLen;
                if (pos < 0.0f) pos += 1.0f;

                ImVec2 pt = perimeterPoint(pMin, pMax, pos, rounding);
                float aFrac = 1.0f - frac;
                aFrac = aFrac * aFrac;
                int a = (int)(panelAlpha * aFrac * 255.0f);
                if (a > 0) {
                    dl->AddLine(prev, pt, IM_COL32(c.r, c.g, c.b, a),
                                edge_trace_.thickness * 0.8f);
                }
                prev = pt;
            }

            // Head dot
            int headA = (int)(panelAlpha * 255.0f);
            ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
            dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.2f,
                                IM_COL32(c.r, c.g, c.b, headA), 6);
        }
    }

    // Ember rise on panels — sparse embers from larger panels only
    if (ember_rise_.enabled) {
        float w = pMax.x - pMin.x;
        float h = pMax.y - pMin.y;
        if (w > 120 && h > 60) { // only on large panels
            // Fewer particles per panel (scale by area relative to viewport)
            int panelCount = std::max(2, ember_rise_.count / 3);
            float panelAlpha = ember_rise_.alpha * 0.5f * bgOpacity_;

            RGB c = unpackRGB(ember_rise_.color);

            // Use panel position as seed for unique particle distribution
            float seed = pMin.x * 0.013f + pMin.y * 0.031f;

            dl->PushClipRect(
                ImVec2(pMin.x - 2, pMin.y - 4),
                ImVec2(pMax.x + 2, pMax.y + 2), true);

            for (int i = 0; i < panelCount; i++) {
                float phase = std::fmod(time_ * ember_rise_.speed + i * 0.618033988f + seed, 1.0f);
                float xHash = std::sin((float)(i + 1) * 127.1f + seed * 100.0f) * 0.5f + 0.5f;
                float xDrift = std::sin(time_ * 0.7f + i * 2.4f + seed) * 3.0f;

                float x = pMin.x + w * xHash + xDrift;
                float y = pMax.y - phase * (h + 6.0f);

                float aFrac;
                if (phase < 0.15f) aFrac = phase / 0.15f;
                else if (phase > 0.7f) aFrac = (1.0f - phase) / 0.3f;
                else aFrac = 1.0f;

                float size = ember_rise_.particleSize * 0.8f * (1.0f - phase * 0.4f);
                int a = (int)(panelAlpha * aFrac * 255.0f);
                if (a <= 0) continue;

                dl->AddCircleFilled(ImVec2(x, y), size,
                                    IM_COL32(c.r, c.g, c.b, a), 6);
            }

            dl->PopClipRect();
        }
    }
}

// ============================================================================
// Viewport Overlay — shader-like full-screen color wash + vignette
// ============================================================================

void ThemeEffects::drawViewportOverlay(ImDrawList* dl) const {
    if (!enabled_ || effects::isLowSpecMode()) return;

    ImGuiViewport* vp = ImGui::GetMainViewport();
    ImVec2 vpPos  = vp->WorkPos;
    ImVec2 vpSize = vp->WorkSize;
    if (vpSize.x <= 0 || vpSize.y <= 0) return;

    ImVec2 vpMax = ImVec2(vpPos.x + vpSize.x, vpPos.y + vpSize.y);

    // === Color Wash: full-screen 4-corner animated gradient ===
    if (viewport_overlay_.colorWashEnabled) {
        ImU32 corners[4] = {
            viewport_overlay_.cornerTL,
            viewport_overlay_.cornerTR,
            viewport_overlay_.cornerBR,
            viewport_overlay_.cornerBL
        };

        // Rotation: smoothly shift which color appears at which corner
        if (viewport_overlay_.washRotateSpeed > 0.001f) {
            float shift = std::fmod(time_ * viewport_overlay_.washRotateSpeed, 1.0f);
            ImU32 originals[4] = { corners[0], corners[1], corners[2], corners[3] };

            for (int c = 0; c < 4; c++) {
                float pos = std::fmod(shift + c * 0.25f, 1.0f);
                float segment = pos * 4.0f;
                int idx  = ((int)segment) % 4;
                float frac = segment - (int)segment;
                int next = (idx + 1) % 4;

                int r1 = originals[idx] & 0xFF, g1 = (originals[idx]>>8)&0xFF, b1 = (originals[idx]>>16)&0xFF;
                int r2 = originals[next] & 0xFF, g2 = (originals[next]>>8)&0xFF, b2 = (originals[next]>>16)&0xFF;
                corners[c] = IM_COL32(
                    r1 + (int)(frac * (r2 - r1)),
                    g1 + (int)(frac * (g2 - g1)),
                    b1 + (int)(frac * (b2 - b1)),
                    255);
            }
        }

        // Calculate final alpha — keep it constant to avoid integer
        // quantisation stepping at low values (0.04–0.12 → only 10–30
        // integer levels).  Breathing is achieved by modulating the RGB
        // colour intensity instead, which has far more resolution.
        float alpha = viewport_overlay_.washAlpha * bgOpacity_;
        int a = std::clamp((int)(alpha * 255.0f + 0.5f), 0, 255);
        if (a > 0) {
            // Smooth breathing via colour modulation (not alpha)
            float colorMul = 1.0f;
            if (viewport_overlay_.washPulseSpeed > 0.001f) {
                float pulse = std::sin(time_ * viewport_overlay_.washPulseSpeed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
                colorMul = 1.0f - viewport_overlay_.washPulseDepth * (1.0f - pulse);
            }

            // Apply colour modulation to each corner while keeping alpha constant
            auto modCorner = [&](ImU32 c) -> ImU32 {
                int r = std::clamp((int)((c & 0xFF) * colorMul + 0.5f), 0, 255);
                int g = std::clamp((int)(((c >> 8) & 0xFF) * colorMul + 0.5f), 0, 255);
                int b = std::clamp((int)(((c >> 16) & 0xFF) * colorMul + 0.5f), 0, 255);
                return IM_COL32(r, g, b, a);
            };

            ImU32 c0 = modCorner(corners[0]);
            ImU32 c1 = modCorner(corners[1]);
            ImU32 c2 = modCorner(corners[2]);
            ImU32 c3 = modCorner(corners[3]);

            dl->AddRectFilledMultiColor(vpPos, vpMax, c0, c1, c2, c3);
        }
    }

    // === Vignette: edge darkening/tinting (4 gradient strips) ===
    // Overlapping strips naturally darken corners more than edges — cinematic look
    if (viewport_overlay_.vignetteEnabled) {
        RGB c = unpackRGB(viewport_overlay_.vignetteColor);
        int maxA = scaledAlpha(viewport_overlay_.vignetteAlpha, bgOpacity_);

        if (maxA > 0) {
            ImU32 edgeCol  = IM_COL32(c.r, c.g, c.b, maxA);
            ImU32 clearCol = IM_COL32(c.r, c.g, c.b, 0);

            float fadeW = vpSize.x * viewport_overlay_.vignetteRadius;
            float fadeH = vpSize.y * viewport_overlay_.vignetteRadius;

            // Top strip: dark at top edge, transparent at fade boundary
            dl->AddRectFilledMultiColor(
                vpPos,
                ImVec2(vpMax.x, vpPos.y + fadeH),
                edgeCol, edgeCol, clearCol, clearCol);

            // Bottom strip
            dl->AddRectFilledMultiColor(
                ImVec2(vpPos.x, vpMax.y - fadeH),
                vpMax,
                clearCol, clearCol, edgeCol, edgeCol);

            // Left strip
            dl->AddRectFilledMultiColor(
                vpPos,
                ImVec2(vpPos.x + fadeW, vpMax.y),
                edgeCol, clearCol, clearCol, edgeCol);

            // Right strip
            dl->AddRectFilledMultiColor(
                ImVec2(vpMax.x - fadeW, vpPos.y),
                vpMax,
                clearCol, edgeCol, edgeCol, clearCol);
        }
    }
}

// ============================================================================
// Hex color parsing helpers
// ============================================================================

ImU32 ThemeEffects::parseHexColor(const std::string& hex, ImU32 fallback) {
    if (hex.empty() || hex[0] != '#') return fallback;
    unsigned int val = 0;
    if (hex.size() == 7) { // #RRGGBB
        val = (unsigned int)strtoul(hex.c_str() + 1, nullptr, 16);
        int r = (val >> 16) & 0xFF;
        int g = (val >> 8) & 0xFF;
        int b = val & 0xFF;
        return IM_COL32(r, g, b, 255);
    } else if (hex.size() == 9) { // #RRGGBBAA
        val = (unsigned int)strtoul(hex.c_str() + 1, nullptr, 16);
        int r = (val >> 24) & 0xFF;
        int g = (val >> 16) & 0xFF;
        int b = (val >> 8) & 0xFF;
        int a = val & 0xFF;
        return IM_COL32(r, g, b, a);
    }
    return fallback;
}

ImVec4 ThemeEffects::parseHexColorVec4(const std::string& hex, ImVec4 fallback) {
    ImU32 c = parseHexColor(hex, 0);
    if (c == 0 && hex != "#000000") return fallback;
    float r = (c & 0xFF) / 255.0f;
    float g = ((c >> 8) & 0xFF) / 255.0f;
    float b = ((c >> 16) & 0xFF) / 255.0f;
    float a = ((c >> 24) & 0xFF) / 255.0f;
    return ImVec4(r, g, b, a);
}

} // namespace effects
} // namespace ui
} // namespace dragonx
