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

#pragma once

#include "colors.h"
#include "type.h"
#include "../layout.h"
#include "../schema/element_styles.h"
#include "../schema/color_var_resolver.h"
#include "../schema/ui_schema.h"
#include "../effects/theme_effects.h"
#include "../effects/low_spec.h"
#include "../effects/imgui_acrylic.h"
#include "../theme.h"
#include "../../util/noise_texture.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include "imgui_internal.h"
#include <algorithm>
#include <unordered_map>
#include <cmath>
#include <string>

namespace dragonx {
namespace ui {
namespace material {

// Scale the alpha channel of an ImU32 color by a float factor.
inline ImU32 ScaleAlpha(ImU32 col, float scale) {
    int a = static_cast<int>(((col >> IM_COL32_A_SHIFT) & 0xFF) * scale);
    if (a > 255) a = 255;
    return (col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(a) << IM_COL32_A_SHIFT);
}

// ============================================================================
// Text Drop Shadow
// ============================================================================
// Draw text with a subtle dark shadow behind it for readability on
// translucent / glassy surfaces.  The shadow is a 1px offset copy of
// the text in a near-black colour.  When the OS backdrop (DWM Acrylic)
// is inactive, the shadow is skipped since opaque backgrounds already
// provide enough contrast.

inline void DrawTextShadow(ImDrawList* dl, ImFont* font, float fontSize,
                           const ImVec2& pos, ImU32 col, const char* text,
                           float offsetX = 1.0f, float offsetY = 1.0f,
                           ImU32 shadowCol = 0)
{
    if (IsBackdropActive()) {
        if (!shadowCol) {
            static uint32_t s_gen = 0;
            static ImU32 s_shadowCol = 0;
            uint32_t g = schema::UI().generation();
            if (g != s_gen) { s_gen = g; s_shadowCol = schema::UI().resolveColor("var(--text-shadow)", IM_COL32(0, 0, 0, 120)); }
            shadowCol = s_shadowCol;
        }
        dl->AddText(font, fontSize,
                    ImVec2(pos.x + offsetX, pos.y + offsetY),
                    shadowCol, text);
    }
    dl->AddText(font, fontSize, pos, col, text);
}

// Convenience overload that uses the current default font.
inline void DrawTextShadow(ImDrawList* dl, const ImVec2& pos, ImU32 col,
                           const char* text,
                           float offsetX = 1.0f, float offsetY = 1.0f,
                           ImU32 shadowCol = 0)
{
    if (IsBackdropActive()) {
        if (!shadowCol) {
            static uint32_t s_gen = 0;
            static ImU32 s_shadowCol = 0;
            uint32_t g = schema::UI().generation();
            if (g != s_gen) { s_gen = g; s_shadowCol = schema::UI().resolveColor("var(--text-shadow)", IM_COL32(0, 0, 0, 120)); }
            shadowCol = s_shadowCol;
        }
        dl->AddText(ImVec2(pos.x + offsetX, pos.y + offsetY),
                    shadowCol, text);
    }
    dl->AddText(pos, col, text);
}

// ============================================================================
// Modal-Aware Hover Check
// ============================================================================
// Drop-in replacement for ImGui::IsMouseHoveringRect that also respects
// modal popup blocking.  The raw ImGui helper is a pure geometric test
// and will return true even when a modal popup covers the rect, which
// causes background elements to show hover highlights through dialogs.

inline int& OverlayDialogActiveFrame()
{
    static int s_frame = -1;
    return s_frame;
}

inline void MarkOverlayDialogActive()
{
    OverlayDialogActiveFrame() = ImGui::GetFrameCount();
}

inline bool IsCurrentWindowOverlayDialog()
{
    ImGuiWindow* window = ImGui::GetCurrentWindow();
    for (ImGuiWindow* node = window; node; node = node->ParentWindow) {
        if (node->Name && strcmp(node->Name, "##OverlayScrim") == 0)
            return true;
    }
    return false;
}

inline bool IsOverlayDialogBlockingInput()
{
    int activeFrame = OverlayDialogActiveFrame();
    int currentFrame = ImGui::GetFrameCount();
    return activeFrame == currentFrame || activeFrame == (currentFrame - 1);
}

inline bool IsRectHovered(const ImVec2& r_min, const ImVec2& r_max, bool clip = true)
{
    if (!ImGui::IsMouseHoveringRect(r_min, r_max, clip))
        return false;
    if (IsOverlayDialogBlockingInput() && !IsCurrentWindowOverlayDialog())
        return false;
    // If a modal popup is open and it is not the current window, treat
    // the content as non-hoverable (same logic ImGui uses internally
    // inside IsWindowContentHoverable for modal blocking).
    //
    // We cannot rely solely on GetTopMostAndVisiblePopupModal() because
    // it checks Active, which is only set when BeginPopupModal() is
    // called in the current frame.  Content tabs render BEFORE their
    // associated dialogs, so the modal's Active flag is still false at
    // this point.  Checking WasActive (set from the previous frame)
    // covers this render-order gap.
    ImGuiContext& g = *ImGui::GetCurrentContext();
    for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) {
        ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window;
        if (popup && (popup->Flags & ImGuiWindowFlags_Modal)) {
            if ((popup->Active || popup->WasActive) && !popup->Hidden) {
                if (popup != ImGui::GetCurrentWindow())
                    return false;
            }
        }
    }
    return true;
}

// ============================================================================
// Tactile Button Overlay
// ============================================================================
// Adds a subtle top-edge highlight and bottom-edge shadow to a button rect,
// creating the illusion of a raised physical surface.  On active (pressed),
// the highlight/shadow swap to create an inset "pushed" feel.
// Call this AFTER ImGui::Button() using GetItemRectMin()/GetItemRectMax().

inline void DrawTactileOverlay(ImDrawList* dl, const ImVec2& bMin,
                               const ImVec2& bMax, float rounding,
                               bool active = false)
{
    float h = bMax.y - bMin.y;
    float edgeH = std::min(h * 0.38f, 6.0f);  // highlight/shadow strip height

    // AddRectFilledMultiColor does not support rounding, so clip to the
    // button's rounded rect to prevent sharp corners from poking out.
    if (rounding > 0.0f) {
        float inset = rounding * 0.29f;  // enough to hide corners
        dl->PushClipRect(
            ImVec2(bMin.x + inset, bMin.y + inset),
            ImVec2(bMax.x - inset, bMax.y - inset), true);
    }

    ImU32 tHi = schema::UI().resolveColor("var(--tactile-top)", IM_COL32(255, 255, 255, 18));
    ImU32 tHiT = tHi & ~IM_COL32_A_MASK;  // transparent version

    if (!active) {
        // Raised: bright top edge, dark bottom edge
        // Top highlight
        dl->AddRectFilledMultiColor(
            bMin, ImVec2(bMax.x, bMin.y + edgeH),
            tHi, tHi, tHiT, tHiT);
        // Bottom shadow
        dl->AddRectFilledMultiColor(
            ImVec2(bMin.x, bMax.y - edgeH), bMax,
            IM_COL32(0, 0, 0, 0),          // top-left
            IM_COL32(0, 0, 0, 0),          // top-right
            IM_COL32(0, 0, 0, 22),         // bottom-right
            IM_COL32(0, 0, 0, 22));        // bottom-left
    } else {
        // Pressed: dark top edge, bright bottom edge (inset)
        // Top shadow
        dl->AddRectFilledMultiColor(
            bMin, ImVec2(bMax.x, bMin.y + edgeH),
            IM_COL32(0, 0, 0, 20),
            IM_COL32(0, 0, 0, 20),
            IM_COL32(0, 0, 0, 0),
            IM_COL32(0, 0, 0, 0));
        // Bottom highlight (dimmer when pressed)
        ImU32 pHi = ScaleAlpha(tHi, 0.55f);
        ImU32 pHiT = pHi & ~IM_COL32_A_MASK;
        dl->AddRectFilledMultiColor(
            ImVec2(bMin.x, bMax.y - edgeH), bMax,
            pHiT, pHiT, pHi, pHi);
    }

    if (rounding > 0.0f)
        dl->PopClipRect();
}

// Convenience: call right after any ImGui::Button() / SmallButton() call
// to add tactile depth.  Uses the last item rect automatically.
inline void ApplyTactile(ImDrawList* dl = nullptr)
{
    if (!dl) dl = ImGui::GetWindowDrawList();
    ImVec2 bMin = ImGui::GetItemRectMin();
    ImVec2 bMax = ImGui::GetItemRectMax();
    float rounding = ImGui::GetStyle().FrameRounding;
    bool active = ImGui::IsItemActive();
    DrawTactileOverlay(dl, bMin, bMax, rounding, active);
}

// ── Button font tier helper ─────────────────────────────────────────────
// Resolves an int tier (0=sm, 1=md/default, 2=lg) to the matching ImFont*.
// Passing -1 or any out-of-range value returns the default button font.

inline ImFont* resolveButtonFont(int tier)
{
    switch (tier) {
        case 0: return Type().buttonSm();
        case 2: return Type().buttonLg();
        default: return Type().button();   // 1 or any other value
    }
}

// Resolve per-button font: if perButton >= 0 use it, else fall back to sectionDefault
inline ImFont* resolveButtonFont(int perButton, int sectionDefault)
{
    return resolveButtonFont(perButton >= 0 ? perButton : sectionDefault);
}

// ── Tactile wrappers ────────────────────────────────────────────────────
// Drop-in replacements for ImGui::Button / SmallButton that automatically
// add the tactile highlight overlay after rendering.

inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0), ImFont* font = nullptr)
{
    // Draw button with glass-card styling: translucent fill + border + tactile overlay
    ImDrawList* dl = ImGui::GetWindowDrawList();
    ImFont* useFont = font ? font : Type().button();
    
    // For icon fonts, use InvisibleButton + manual centered text rendering
    // to ensure perfect centering (ImGui::Button alignment can be off for icons)
    bool isIconFont = font && (font == Type().iconSmall() || font == Type().iconMed() || 
                               font == Type().iconLarge() || font == Type().iconXL());
    
    bool pressed;
    if (isIconFont && size.x > 0 && size.y > 0) {
        pressed = ImGui::InvisibleButton(label, size);
    } else {
        ImGui::PushFont(useFont);
        pressed = ImGui::Button(label, size);
        ImGui::PopFont();
    }
    // Glass overlay on the button rect
    ImVec2 bMin = ImGui::GetItemRectMin();
    ImVec2 bMax = ImGui::GetItemRectMax();
    
    // For icon fonts, manually draw centered icon after getting button rect
    if (isIconFont && size.x > 0 && size.y > 0) {
        ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label);
        ImVec2 textPos(bMin.x + (size.x - textSz.x) * 0.5f, bMin.y + (size.y - textSz.y) * 0.5f);
        dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label);
    }
    
    float rounding = ImGui::GetStyle().FrameRounding;
    bool active = ImGui::IsItemActive();
    bool hovered = ImGui::IsItemHovered();
    // Frosted glass highlight — subtle fill on top
    if (!active) {
        ImU32 col = hovered
            ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
            : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
        dl->AddRectFilled(bMin, bMax, col, rounding);
    }
    // Rim light
    ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
    dl->AddRect(bMin, bMax,
                active ? ScaleAlpha(rim, 0.6f) : rim,
                rounding, 0, 1.0f);
    // Tactile depth
    DrawTactileOverlay(dl, bMin, bMax, rounding, active);
    return pressed;
}

inline bool TactileSmallButton(const char* label, ImFont* font = nullptr)
{
    ImDrawList* dl = ImGui::GetWindowDrawList();
    ImGui::PushFont(font ? font : Type().button());
    bool pressed = ImGui::SmallButton(label);
    ImGui::PopFont();
    ImVec2 bMin = ImGui::GetItemRectMin();
    ImVec2 bMax = ImGui::GetItemRectMax();
    float rounding = ImGui::GetStyle().FrameRounding;
    bool active = ImGui::IsItemActive();
    bool hovered = ImGui::IsItemHovered();
    if (!active) {
        ImU32 col = hovered
            ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
            : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
        dl->AddRectFilled(bMin, bMax, col, rounding);
    }
    ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
    dl->AddRect(bMin, bMax,
                active ? ScaleAlpha(rim, 0.6f) : rim,
                rounding, 0, 1.0f);
    DrawTactileOverlay(dl, bMin, bMax, rounding, active);
    return pressed;
}

// ============================================================================
// Glass Panel  (glassmorphism card)
// ============================================================================
// Draws a frosted-glass style panel: a semi-transparent fill with a
// subtle light border.  Used for card/panel containers that sit above
// the blurred background.  When the backdrop is inactive the fill is
// simply the normal surface colour (fully opaque).

struct GlassPanelSpec {
    float rounding      = 6.0f;
    int   fillAlpha     = 18;        // fill brightness (white, 0-255)
    int   borderAlpha   = 30;        // border brightness (white, 0-255)
    float borderWidth   = 1.0f;
};

inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
                           const ImVec2& pMax,
                           const GlassPanelSpec& spec = GlassPanelSpec())
{
    if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode()) {
        // --- Cached color lookups (invalidated on theme change) ---
        // These 3 resolveColor() calls do string parsing + map lookup
        // each time.  Cache them per-frame using the schema generation
        // counter so they resolve at most once per theme load.
        static uint32_t s_gen = 0;
        static ImU32 s_glassFill = 0;
        static ImU32 s_glassNoiseTint = 0;
        static ImU32 s_glassBorder = 0;
        uint32_t curGen = schema::UI().generation();
        if (curGen != s_gen) {
            s_gen = curGen;
            s_glassFill      = schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 18));
            s_glassNoiseTint = schema::UI().resolveColor("var(--glass-noise-tint)", IM_COL32(255, 255, 255, 10));
            s_glassBorder    = schema::UI().resolveColor("var(--glass-border)", IM_COL32(255, 255, 255, 30));
        }

        float uiOp = effects::ImGuiAcrylic::GetUIOpacity();
        bool useAcrylic = effects::ImGuiAcrylic::IsEnabled()
                       && effects::ImGuiAcrylic::IsAvailable();

        // Glass / acrylic layer — only rendered when not fully opaque
        // (skip the blur pass at 100% for performance)
        if (uiOp < 0.99f) {
            if (useAcrylic) {
                const auto& acrylicTheme = GetCurrentAcrylicTheme();
                effects::ImGuiAcrylic::DrawAcrylicRect(dl, pMin, pMax,
                                                       acrylicTheme.card, spec.rounding);
            } else {
                // Lightweight fake-glass: translucent fill
                ImU32 fill = (spec.fillAlpha == 18) ? s_glassFill : ScaleAlpha(s_glassFill, spec.fillAlpha / 18.0f);
                dl->AddRectFilled(pMin, pMax, fill, spec.rounding);
            }
        }

        // Surface overlay — provides smooth transition from glass to opaque.
        // At uiOp=1.0 this fully covers the panel (opaque card).
        // As uiOp decreases, glass/blur progressively shows through.
        dl->AddRectFilled(pMin, pMax,
            WithAlphaF(GetElevatedSurface(GetCurrentColorTheme(), 1), uiOp),
            spec.rounding);

        // Noise grain overlay — drawn OVER the surface overlay so card
        // opacity doesn't hide it.  Gives cards a tactile paper feel.
        // Noise tint is cached per-generation, opacity checked each panel.
        {
            float noiseMul = dragonx::ui::effects::ImGuiAcrylic::GetNoiseOpacity();
            if (noiseMul > 0.0f) {
                // Tint base color changes only on theme reload; opacity slider may change per-frame
                static uint32_t s_noiseGen = 0;
                static uint8_t  s_baseAlpha = 0;
                if (curGen != s_noiseGen) {
                    s_noiseGen = curGen;
                    s_baseAlpha = (s_glassNoiseTint >> IM_COL32_A_SHIFT) & 0xFF;
                }
                uint8_t scaledAlpha = static_cast<uint8_t>(std::min(255.0f, s_baseAlpha * noiseMul));
                ImU32 noiseTint = (s_glassNoiseTint & ~(0xFFu << IM_COL32_A_SHIFT)) | (scaledAlpha << IM_COL32_A_SHIFT);
                float inset = spec.rounding * 0.3f;
                ImVec2 noiseMin(pMin.x + inset, pMin.y + inset);
                ImVec2 noiseMax(pMax.x - inset, pMax.y - inset);
                // Image rect matches clip bounds exactly — no PushClipRect needed
                dragonx::util::DrawTiledNoiseRect(dl, noiseMin, noiseMax, noiseTint);
            }
        }

        // Border — fades with UI opacity
        ImU32 border = (spec.borderAlpha == 30) ? s_glassBorder : ScaleAlpha(s_glassBorder, spec.borderAlpha / 30.0f);
        if (uiOp < 0.99f) border = ScaleAlpha(border, uiOp);
        dl->AddRect(pMin, pMax, border,
                    spec.rounding, 0, spec.borderWidth);

        // Theme visual effects drawn on ForegroundDrawList so they
        // render above card content (text, values, etc.), not below.
        auto& fx = effects::ThemeEffects::instance();
        if (fx.hasAnyPanelEffect()) {
            ImDrawList* fxDl = ImGui::GetForegroundDrawList();
            if (fx.hasRainbowBorder()) {
                fx.drawRainbowBorder(fxDl, pMin, pMax, spec.rounding, spec.borderWidth);
            }
            if (fx.hasShimmer()) {
                fx.drawShimmer(fxDl, pMin, pMax, spec.rounding);
            }
            if (fx.hasSpecularGlare()) {
                fx.drawSpecularGlare(fxDl, pMin, pMax, spec.rounding);
            }
            // Per-panel theme effects: edge trace + ember rise
            fx.drawPanelEffects(fxDl, pMin, pMax, spec.rounding);
        }
    } else {
        // Low-spec opaque fallback
        dl->AddRectFilled(pMin, pMax,
            GetElevatedSurface(GetCurrentColorTheme(), 1), spec.rounding);
    }
}

// ============================================================================
// Stat Card — reusable card with overline / value / subtitle + accent stripe
// ============================================================================

struct StatCardSpec {
    const char* overline    = nullptr;   // small label at top  (e.g. "LOCAL HASHRATE")
    const char* value       = nullptr;   // main value text     (e.g. "1.23 kH/s")
    const char* subtitle    = nullptr;   // optional small line (e.g. "3 blocks")
    ImU32       valueCol    = 0;         // value text colour (0 = OnSurface)
    ImU32       accentCol   = 0;         // left-stripe colour (0 = no stripe)
    bool        centered    = true;      // centre text horizontally
    bool        hovered     = false;     // draw hover glow border
};

/// Compute a generous stat-card height with breathing room.
inline float StatCardHeight(float vs, float minH = 56.0f) {
    float padV   = Layout::spacingLg() * 2;  // top + bottom
    float content = Type().overline()->LegacySize
                  + Layout::spacingMd()
                  + Type().subtitle1()->LegacySize;
    return std::max(minH, (content + padV) * std::max(vs, 0.85f));
}

inline void DrawStatCard(ImDrawList* dl,
                         const ImVec2& cMin, const ImVec2& cMax,
                         const StatCardSpec& card,
                         const GlassPanelSpec& glass = GlassPanelSpec())
{
    float cardW = cMax.x - cMin.x;
    float cardH = cMax.y - cMin.y;
    float rnd   = glass.rounding;

    // 1. Glass background
    DrawGlassPanel(dl, cMin, cMax, glass);

    // 2. Accent stripe (left edge, clipped to card rounded corners).
    //    Draw a full-height rounded rect with card rounding (left corners)
    //    and clip to stripe width so the shape follows the corner radius.
    if ((card.accentCol & IM_COL32_A_MASK) != 0) {
        float stripeW = 4.0f;
        dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
        dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
                          ImDrawFlags_RoundCornersLeft);
        dl->PopClipRect();
    }

    // 3. Compute content block height
    ImFont* ovFont  = Type().overline();
    ImFont* valFont = Type().subtitle1();
    ImFont* subFont = Type().caption();

    float blockH = 0;
    if (card.overline) blockH += ovFont->LegacySize;
    if (card.overline && card.value) blockH += Layout::spacingMd();
    if (card.value)   blockH += valFont->LegacySize;
    if (card.subtitle) blockH += Layout::spacingSm() + subFont->LegacySize;

    // 4. Vertically centre
    float topY = cMin.y + (cardH - blockH) * 0.5f;
    float padX = Layout::spacingLg();
    float cy = topY;

    // 5. Overline
    if (card.overline) {
        ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, card.overline);
        float tx = card.centered ? cMin.x + (cardW - sz.x) * 0.5f : cMin.x + padX;
        dl->AddText(ovFont, ovFont->LegacySize, ImVec2(tx, cy), OnSurfaceMedium(), card.overline);
        cy += ovFont->LegacySize + Layout::spacingMd();
    }

    // 6. Value (with text shadow)
    if (card.value) {
        ImU32 col = card.valueCol ? card.valueCol : OnSurface();
        ImVec2 sz = valFont->CalcTextSizeA(valFont->LegacySize, 10000, 0, card.value);
        float tx = card.centered ? cMin.x + (cardW - sz.x) * 0.5f : cMin.x + padX;
        DrawTextShadow(dl, valFont, valFont->LegacySize, ImVec2(tx, cy), col, card.value);
        cy += valFont->LegacySize;
    }

    // 7. Subtitle
    if (card.subtitle) {
        cy += Layout::spacingSm();
        ImVec2 sz = subFont->CalcTextSizeA(subFont->LegacySize, 10000, 0, card.subtitle);
        float tx = card.centered ? cMin.x + (cardW - sz.x) * 0.5f : cMin.x + padX;
        dl->AddText(subFont, subFont->LegacySize, ImVec2(tx, cy), OnSurfaceDisabled(), card.subtitle);
    }

    // 8. Hover glow
    if (card.hovered) {
        dl->AddRect(cMin, cMax,
                    schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 20)),
                    rnd, 0, 1.5f);
    }
}

// ── Styled Button (font-only wrapper) ───────────────────────────────────
// Drop-in replacement for ImGui::Button that pushes the Button font style
// (from JSON config) without adding tactile visual effects.

inline bool StyledButton(const char* label, const ImVec2& size = ImVec2(0, 0), ImFont* font = nullptr)
{
    ImGui::PushFont(font ? font : Type().button());
    bool pressed = ImGui::Button(label, size);
    ImGui::PopFont();
    return pressed;
}

inline bool StyledSmallButton(const char* label, ImFont* font = nullptr)
{
    ImGui::PushFont(font ? font : Type().button());
    bool pressed = ImGui::SmallButton(label);
    ImGui::PopFont();
    return pressed;
}

// ============================================================================
// ButtonStyle-driven overloads (unified UI schema)
// ============================================================================
// These accept a ButtonStyle + ColorVarResolver to push font, colors,
// and size from the schema. Existing call sites are unaffected.

inline bool StyledButton(const char* label, const schema::ButtonStyle& style,
                         const schema::ColorVarResolver& colors)
{
    // Resolve font
    ImFont* font = nullptr;
    if (!style.font.empty()) {
        font = Type().resolveByName(style.font);
    }
    ImGui::PushFont(font ? font : Type().button());

    // Push color overrides
    int colorCount = 0;
    if (!style.colors.background.empty()) {
        ImGui::PushStyleColor(ImGuiCol_Button,
            colors.resolve(style.colors.background));
        colorCount++;
    }
    if (!style.colors.backgroundHover.empty()) {
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
            colors.resolve(style.colors.backgroundHover));
        colorCount++;
    }
    if (!style.colors.backgroundActive.empty()) {
        ImGui::PushStyleColor(ImGuiCol_ButtonActive,
            colors.resolve(style.colors.backgroundActive));
        colorCount++;
    }
    if (!style.colors.color.empty()) {
        ImGui::PushStyleColor(ImGuiCol_Text,
            colors.resolve(style.colors.color));
        colorCount++;
    }

    // Push style overrides
    int styleCount = 0;
    if (style.borderRadius >= 0) {
        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, style.borderRadius);
        styleCount++;
    }

    ImVec2 size(style.width > 0 ? style.width : 0,
                style.height > 0 ? style.height : 0);
    bool pressed = ImGui::Button(label, size);

    ImGui::PopStyleVar(styleCount);
    ImGui::PopStyleColor(colorCount);
    ImGui::PopFont();
    return pressed;
}

inline bool TactileButton(const char* label, const schema::ButtonStyle& style,
                          const schema::ColorVarResolver& colors)
{
    // Resolve font
    ImFont* font = nullptr;
    if (!style.font.empty()) {
        font = Type().resolveByName(style.font);
    }
    ImGui::PushFont(font ? font : Type().button());

    // Push color overrides
    int colorCount = 0;
    if (!style.colors.background.empty()) {
        ImGui::PushStyleColor(ImGuiCol_Button,
            colors.resolve(style.colors.background));
        colorCount++;
    }
    if (!style.colors.backgroundHover.empty()) {
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
            colors.resolve(style.colors.backgroundHover));
        colorCount++;
    }
    if (!style.colors.backgroundActive.empty()) {
        ImGui::PushStyleColor(ImGuiCol_ButtonActive,
            colors.resolve(style.colors.backgroundActive));
        colorCount++;
    }
    if (!style.colors.color.empty()) {
        ImGui::PushStyleColor(ImGuiCol_Text,
            colors.resolve(style.colors.color));
        colorCount++;
    }

    // Push style overrides
    int styleCount = 0;
    if (style.borderRadius >= 0) {
        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, style.borderRadius);
        styleCount++;
    }

    ImDrawList* dl = ImGui::GetWindowDrawList();
    ImVec2 size(style.width > 0 ? style.width : 0,
                style.height > 0 ? style.height : 0);
    bool pressed = ImGui::Button(label, size);

    // Glass overlay on the button rect
    ImVec2 bMin = ImGui::GetItemRectMin();
    ImVec2 bMax = ImGui::GetItemRectMax();
    float rounding = ImGui::GetStyle().FrameRounding;
    bool active = ImGui::IsItemActive();
    bool hovered = ImGui::IsItemHovered();
    if (!active) {
        ImU32 col = hovered
            ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
            : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
        dl->AddRectFilled(bMin, bMax, col, rounding);
    }
    ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
    dl->AddRect(bMin, bMax,
                active ? ScaleAlpha(rim, 0.6f) : rim,
                rounding, 0, 1.0f);
    DrawTactileOverlay(dl, bMin, bMax, rounding, active);

    ImGui::PopStyleVar(styleCount);
    ImGui::PopStyleColor(colorCount);
    ImGui::PopFont();
    return pressed;
}

} // namespace material
} // namespace ui
} // namespace dragonx

namespace dragonx {
namespace ui {
namespace material {

// ============================================================================
// Scroll-edge clipping mask — CSS mask-image style vertex alpha fade.
// Call ApplyScrollEdgeMask after EndChild() to fade content at the
// top/bottom edges of a scrollable panel.  Works by walking vertices
// added during rendering and scaling their alpha based on distance
// to the panel edges.  No opaque overlay rectangles — content becomes
// truly transparent, revealing whatever is behind the panel.
//
// Usage pattern:
//   float scrollY = 0, scrollMaxY = 0;
//   int parentVtx = parentDL->VtxBuffer.Size;
//   ImGui::BeginChild(...);
//   ImDrawList* childDL = ImGui::GetWindowDrawList();
//   int childVtx = childDL->VtxBuffer.Size;
//   { ... scrollY = GetScrollY(); scrollMaxY = GetScrollMaxY(); ... }
//   ImGui::EndChild();
//   ApplyScrollEdgeMask(parentDL, parentVtx, childDL, childVtx,
//                       panelMin.y, panelMax.y, fadeZone, scrollY, scrollMaxY);
// ============================================================================
inline void ApplyScrollEdgeMask(ImDrawList* parentDL, int parentVtxStart,
                                ImDrawList* childDL,  int childVtxStart,
                                float topEdge, float bottomEdge,
                                float fadeZone,
                                float scrollY, float scrollMaxY)
{
    auto mask = [&](ImDrawList* dl, int startIdx) {
        for (int vi = startIdx; vi < dl->VtxBuffer.Size; vi++) {
            ImDrawVert& v = dl->VtxBuffer[vi];
            float alpha = 1.0f;

            // Top fade — only when scrolled down
            if (scrollY > 1.0f) {
                float d = v.pos.y - topEdge;
                if (d < fadeZone)
                    alpha = std::min(alpha, std::max(0.0f, d / fadeZone));
            }
            // Bottom fade — only when not at scroll bottom
            if (scrollMaxY > 0 && scrollY < scrollMaxY - 1.0f) {
                float d = bottomEdge - v.pos.y;
                if (d < fadeZone)
                    alpha = std::min(alpha, std::max(0.0f, d / fadeZone));
            }

            if (alpha < 1.0f) {
                int a = (v.col >> IM_COL32_A_SHIFT) & 0xFF;
                a = static_cast<int>(a * alpha);
                v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(a) << IM_COL32_A_SHIFT);
            }
        }
    };

    if (parentDL) mask(parentDL, parentVtxStart);
    if (childDL)  mask(childDL,  childVtxStart);
}

// ============================================================================
// Smooth scrolling — exponential decay interpolation for mouse wheel.
// Call immediately after BeginChild() on a child that was created with
// ImGuiWindowFlags_NoScrollWithMouse.  Captures wheel input and lerps
// scroll position each frame for a smooth feel.
//
// Usage:
//   ImGui::BeginChild("##List", size, false,
//       ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
//   ApplySmoothScroll();
//   ... render content ...
//   ImGui::EndChild();
// ============================================================================

/// Returns true when any smooth-scroll child is still interpolating toward
/// its target.  Checked by the idle-rendering logic in main.cpp to keep
/// rendering frames until the animation settles.
inline bool& SmoothScrollAnimating() {
    static bool sAnimating = false;
    return sAnimating;
}

inline void ApplySmoothScroll(float speed = 12.0f)
{
    struct ScrollState {
        float target = 0.0f;
        float current = 0.0f;
        bool  init = false;
    };
    static std::unordered_map<ImGuiID, ScrollState> sStates;

    ImGuiWindow* win = ImGui::GetCurrentWindow();
    if (!win) return;

    ScrollState& s = sStates[win->ID];
    float scrollMaxY = ImGui::GetScrollMaxY();

    if (!s.init) {
        s.target  = ImGui::GetScrollY();
        s.current = s.target;
        s.init    = true;
    }

    // If something external set scroll position (e.g. SetScrollHereY for
    // auto-scroll), sync our state so the next wheel-up starts from the
    // actual current position rather than an old remembered target.
    float actualY = ImGui::GetScrollY();
    if (std::abs(s.current - actualY) > 1.0f) {
        s.target  = actualY;
        s.current = actualY;
    }

    // Capture mouse wheel when the cursor is inside this child window.
    // Use a direct position check instead of IsWindowHovered() which can
    // return false when an ActiveId exists (focused input, held button) or
    // when tooltip/popup windows from row hovers interfere with hover
    // routing.  Still skip when a real popup (combo dropdown, context menu)
    // is open so the popup handles its own scrolling.
    {
        bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
        ImVec2 mpos = ImGui::GetIO().MousePos;
        ImVec2 wmin = win->Pos;
        ImVec2 wmax = ImVec2(wmin.x + win->Size.x, wmin.y + win->Size.y);
        bool inside = (mpos.x >= wmin.x && mpos.x < wmax.x &&
                       mpos.y >= wmin.y && mpos.y < wmax.y);
        if (!popupOpen && inside) {
            float wheel = ImGui::GetIO().MouseWheel;
            if (wheel != 0.0f) {
                float step = ImGui::GetTextLineHeightWithSpacing() * 3.0f;
                s.target -= wheel * step;
                s.target = ImClamp(s.target, 0.0f, scrollMaxY);
            }
        }
    }

    // Clamp target if scrollMax changed (e.g., content resized)
    if (s.target > scrollMaxY) s.target = scrollMaxY;

    // Exponential decay lerp
    float dt = ImGui::GetIO().DeltaTime;
    s.current += (s.target - s.current) * (1.0f - expf(-speed * dt));

    // Snap when close
    if (std::abs(s.current - s.target) < 0.5f)
        s.current = s.target;
    else
        SmoothScrollAnimating() = true;  // still interpolating — keep rendering

    ImGui::SetScrollY(s.current);
}

// ============================================================================
// Dialog Title Bar with Close Button
// ============================================================================
// Draws a custom title bar for modal popups with a title and close button.
// Returns true if the close button was clicked.

inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col = 0)
{
    bool closeClicked = false;
    ImDrawList* dl = ImGui::GetWindowDrawList();
    ImVec2 winPos = ImGui::GetWindowPos();
    float winWidth = ImGui::GetWindowWidth();
    float barHeight = 36.0f;
    
    // Get accent color from theme if not provided
    if (!accent_col) {
        accent_col = schema::UI().resolveColor("var(--primary)", IM_COL32(76, 175, 80, 255));
    }
    
    // Draw title bar background with subtle gradient
    ImVec2 barMin = winPos;
    ImVec2 barMax(winPos.x + winWidth, winPos.y + barHeight);
    
    // Slightly darker top edge
    ImU32 barTop = IM_COL32(0, 0, 0, 40);
    ImU32 barBot = IM_COL32(0, 0, 0, 20);
    dl->AddRectFilledMultiColor(barMin, barMax, barTop, barTop, barBot, barBot);
    
    // Accent line at bottom of title bar
    dl->AddLine(ImVec2(barMin.x, barMax.y), ImVec2(barMax.x, barMax.y), 
                ScaleAlpha(accent_col, 0.6f), 1.0f);
    
    // Title text
    ImFont* titleFont = Type().subtitle1();
    ImGui::PushFont(titleFont);
    ImVec2 titleSize = ImGui::CalcTextSize(title);
    float titleX = barMin.x + 16.0f;
    float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
    DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
    ImGui::PopFont();
    
    // Close button (X) on right side
    if (p_open) {
        float btnSize = 24.0f;
        float btnX = barMax.x - btnSize - 12.0f;
        float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
        ImVec2 btnMin(btnX, btnY);
        ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
        
        bool hovered = ImGui::IsMouseHoveringRect(btnMin, btnMax);
        bool held = false;
        if (hovered && ImGui::IsMouseClicked(0)) {
            held = true;
        }
        
        // Button background on hover
        if (hovered) {
            dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f);
        }
        
        // Draw X icon
        ImGui::PushFont(Type().iconSmall());
        ImVec2 iconSize = ImGui::CalcTextSize(ICON_MD_CLOSE);
        float iconX = btnX + (btnSize - iconSize.x) * 0.5f;
        float iconY = btnY + (btnSize - iconSize.y) * 0.5f;
        dl->AddText(ImVec2(iconX, iconY), 
                    hovered ? IM_COL32(255, 255, 255, 255) : OnSurfaceMedium(), 
                    ICON_MD_CLOSE);
        ImGui::PopFont();
        
        // Handle click
        if (hovered && ImGui::IsMouseReleased(0)) {
            *p_open = false;
            closeClicked = true;
        }
    }
    
    // Reserve space for title bar so content starts below it
    ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f);
    
    return closeClicked;
}

// ============================================================================
// Fullscreen Overlay Dialog
// ============================================================================
// Creates a fullscreen semi-transparent overlay with a centered card dialog.
// Similar to the shutdown screen pattern but for interactive dialogs.

// Per-dialog content height (keyed by the child's id) measured at the end of each frame, so the next
// frame can size the glass card to its content instead of a fixed viewport band. g_overlayCurrentKey
// carries the active dialog's key from BeginOverlayDialog to EndOverlayDialog (overlays don't nest).
inline std::unordered_map<std::string, float> g_overlayCardHeights;
inline std::string g_overlayCurrentKey;

inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f,
                               float cardBottomViewportRatio = 0.85f, const char* idSuffix = nullptr)
{
    MarkOverlayDialogActive();

    ImGuiViewport* vp = ImGui::GetMainViewport();
    ImVec2 vp_pos = vp->Pos;
    ImVec2 vp_size = vp->Size;

    // Dialog widths are authored as raw pixels, but the fonts/spacing inside scale with
    // Layout::dpiScale() (which includes the user's font-size setting). Scale the card by the same
    // factor so the content doesn't outgrow a fixed card and overflow/misalign at non-default
    // scales. No-op at the default scale (dpiScale() == 1). Clamped to the viewport so a large scale
    // can't push the card off-screen.
    cardWidth *= Layout::dpiScale();
    cardWidth = std::min(cardWidth, vp_size.x - 32.0f);

    // Fullscreen scrim overlay
    ImGui::SetNextWindowPos(vp_pos);
    ImGui::SetNextWindowSize(vp_size);
    ImGui::SetNextWindowFocus();
    ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.04f, 0.04f, 0.06f, scrimOpacity));
    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
    
    std::string scrimId = "##OverlayScrim";
    std::string childId = "##OverlayDialogContent";
    if (idSuffix && idSuffix[0] != '\0') {
        scrimId += "_";
        scrimId += idSuffix;
        childId += "_";
        childId += idSuffix;
    }

    bool opened = ImGui::Begin(scrimId.c_str(), nullptr,
        ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
        ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
        ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoNav |
        ImGuiWindowFlags_NoSavedSettings);
    
    if (!opened) {
        ImGui::End();
        ImGui::PopStyleVar(3);
        ImGui::PopStyleColor();
        return false;
    }
    
    ImDrawList* dl = ImGui::GetWindowDrawList();

    // Consume pointer input on the scrim so the overlay owns clicks and wheel
    // events even when the click lands outside the card content.
    ImGui::SetCursorScreenPos(vp_pos);
    ImGui::InvisibleButton("##OverlayInputBlocker", vp_size,
                           ImGuiButtonFlags_MouseButtonLeft |
                           ImGuiButtonFlags_MouseButtonRight |
                           ImGuiButtonFlags_MouseButtonMiddle);
    
    // Calculate card position (centered)
    float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
    float cardY = vp_pos.y + vp_size.y * 0.15f;

    // Size the card height to its content. The content child below is AutoResizeY, so a glass card
    // drawn to a fixed viewport ratio left a tall band of empty glass under short dialogs. Reuse the
    // height the child reported LAST frame (content is stable frame-to-frame, so no visible lag) and
    // fall back to the ratio on the first frame. Still capped at the ratio so a very tall dialog can't
    // run off-screen (its content spills/scrolls as before).
    g_overlayCurrentKey = childId;
    float ratioMaxY = vp_pos.y + vp_size.y * cardBottomViewportRatio;
    auto prevHeightIt = g_overlayCardHeights.find(childId);
    float cardBottomY = (prevHeightIt != g_overlayCardHeights.end() && prevHeightIt->second > 0.0f)
                            ? std::min(cardY + prevHeightIt->second, ratioMaxY)
                            : ratioMaxY;

    // Draw glass card background
    ImVec2 cardMin(cardX, cardY);
    ImVec2 cardMax(cardX + cardWidth, cardBottomY);
    
    // Card background with glass effect
    GlassPanelSpec cardGlass;
    cardGlass.rounding = 16.0f;
    cardGlass.fillAlpha = 35;
    cardGlass.borderAlpha = 50;
    cardGlass.borderWidth = 1.0f;
    DrawGlassPanel(dl, cardMin, cardMax, cardGlass);

    // Click outside the card dismisses the dialog — but NOT on the frame it first appears, otherwise
    // the very click that opened it (a button fired the same frame) is read as an outside-click and
    // the dialog flashes open then instantly closes.
    if (p_open && !ImGui::IsWindowAppearing() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
        !ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) {
        *p_open = false;
    }
    
    // Set up child region for card content
    ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
    ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 16.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28, 24));
    ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // Transparent - glass already drawn
    
    bool childVisible = ImGui::BeginChild(childId.c_str(),
        ImVec2(cardWidth, 0),  // 0 height = auto-size
        ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
        ImGuiWindowFlags_NoScrollbar);
    
    // Draw title bar with close button
    if (childVisible && title) {
        DrawDialogTitleBar(title, p_open);
    }
    
    return childVisible;
}

inline void EndOverlayDialog()
{
    ImGui::EndChild();
    // Remember the rendered card height (the child is the last item) so the next frame's
    // BeginOverlayDialog can size the glass to the content — kills the empty band under short dialogs.
    if (!g_overlayCurrentKey.empty()) {
        g_overlayCardHeights[g_overlayCurrentKey] = ImGui::GetItemRectSize().y;
    }
    ImGui::PopStyleColor();  // ChildBg
    ImGui::PopStyleVar(2);   // ChildRounding, WindowPadding (for child)
    
    ImGui::End();
    ImGui::PopStyleVar(3);   // WindowRounding, WindowBorderSize, WindowPadding (for scrim)
    ImGui::PopStyleColor();  // WindowBg scrim
}

inline void PlaceOverlayDialogActions(float totalWidth)
{
    float rowStartX = ImGui::GetCursorPosX();
    float contentW = ImGui::GetContentRegionAvail().x;
    ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalWidth) * 0.5f));
}

inline void BeginOverlayDialogFooter(float totalActionWidth, bool drawSeparator = true)
{
    ImGui::Spacing();
    if (drawSeparator) {
        ImGui::Separator();
        ImGui::Spacing();
    }
    PlaceOverlayDialogActions(totalActionWidth);
}

} // namespace material
} // namespace ui
} // namespace dragonx
