pragma Singleton
pragma ComponentBehavior: Bound

import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.DankCommon.Common as DankCommon
import qs.Services
import "StockThemes.js" as StockThemes

Singleton {
    id: root
    readonly property var log: Log.scoped("Theme")

    readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()) + "/DankMaterialShell"
    readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
    readonly property string defaultFontFamily: "Inter Variable"
    readonly property string defaultMonoFontFamily: "Fira Code"

    readonly property real popupDistance: {
        if (typeof SettingsData === "undefined")
            return 4;
        const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
        if (!defaultBar)
            return 4;
        const useAuto = defaultBar.popupGapsAuto ?? true;
        const manualValue = defaultBar.popupGapsManual ?? 4;
        const spacing = defaultBar.spacing ?? 4;
        return useAuto ? Math.max(4, spacing) : manualValue;
    }

    property string currentTheme: "purple"
    property string currentThemeCategory: "generic"
    property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false
    property bool colorsFileLoadFailed: false

    readonly property string dynamic: "dynamic"
    readonly property string custom: "custom"

    readonly property string homeDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation))
    readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
    readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "")
    readonly property string wallpaperPath: {
        if (typeof SessionData === "undefined")
            return "";

        var monitors = SessionData.monitorWallpapers;
        if (SessionData.perMonitorWallpaper) {
            var screens = Quickshell.screens;
            if (screens.length > 0) {
                var s = screens[0];
                return monitors[s.name] || (s.model ? monitors[s.model] : "") || SessionData.wallpaperPath;
            }
        }

        return SessionData.wallpaperPath;
    }
    readonly property string rawWallpaperPath: {
        if (typeof SessionData === "undefined")
            return "";

        var monitors = SessionData.monitorWallpapers;
        if (SessionData.perMonitorWallpaper) {
            var screens = Quickshell.screens;
            if (screens.length > 0) {
                var targetMonitor = (typeof SettingsData !== "undefined" && SettingsData.matugenTargetMonitor && SettingsData.matugenTargetMonitor !== "") ? SettingsData.matugenTargetMonitor : screens[0].name;

                var targetMonitorExists = false;
                for (var i = 0; i < screens.length; i++) {
                    if (screens[i].name === targetMonitor) {
                        targetMonitorExists = true;
                        break;
                    }
                }

                if (!targetMonitorExists)
                    targetMonitor = screens[0].name;

                var s = null;
                for (var j = 0; j < screens.length; j++) {
                    if (screens[j].name === targetMonitor) {
                        s = screens[j];
                        break;
                    }
                }

                if (s)
                    return monitors[s.name] || (s.model ? monitors[s.model] : "") || SessionData.wallpaperPath;
                return monitors[targetMonitor] || SessionData.wallpaperPath;
            }
        }

        return SessionData.wallpaperPath;
    }

    property bool matugenAvailable: false
    property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
    property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
    property var workerRunning: false
    property var pendingThemeRequest: null

    signal matugenCompleted(string mode, string result)
    property var matugenColors: ({})
    property var _pendingGenerateParams: null
    property int _colorsRetryCount: 0
    property double _lastGenerateMs: 0

    property bool blurLayersActive: false
    property bool matugenToastSuppressed: false

    signal screenTransitionNeeded
    signal themeGenerationStarting

    readonly property var dank16: {
        const raw = matugenColors?.dank16;
        if (!raw)
            return null;

        const dark = {};
        const light = {};
        const def = {};

        for (let i = 0; i < 16; i++) {
            const key = "color" + i;
            const c = raw[key];
            if (!c)
                continue;
            dark[key] = c.dark;
            light[key] = c.light;
            def[key] = c.default;
        }

        return {
            dark,
            light,
            "default": def
        };
    }
    property var customThemeData: null
    property var customThemeRawData: null
    readonly property var currentThemeVariants: customThemeRawData?.variants || null
    readonly property string currentThemeId: customThemeRawData?.id || ""

    Component.onCompleted: {
        Quickshell.execDetached(["mkdir", "-p", stateDir]);
        // shellDir may be an embedded-UI extraction, which is read-only and
        // unexecutable (dankgo shellapp/shellfs makeReadOnly chmods 0444)
        Quickshell.execDetached(["bash", shellDir + "/scripts/gtk.sh", configDir, "assets", "", shellDir]);
        Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
            matugenAvailable = (code === 0) && !envDisableMatugen;

            if (!matugenAvailable) {
                return;
            }

            if (colorsFileLoadFailed && currentTheme === dynamic && rawWallpaperPath) {
                log.info("Matugen now available, regenerating colors for dynamic theme");
                const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
                const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
                const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
                if (rawWallpaperPath.startsWith("#")) {
                    setDesiredTheme("hex", rawWallpaperPath, isLight, iconTheme, selectedMatugenType);
                } else {
                    setDesiredTheme("image", rawWallpaperPath, isLight, iconTheme, selectedMatugenType);
                }
                return;
            }

            const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
            const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";

            if (currentTheme === dynamic) {
                if (rawWallpaperPath) {
                    const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
                    if (rawWallpaperPath.startsWith("#")) {
                        setDesiredTheme("hex", rawWallpaperPath, isLight, iconTheme, selectedMatugenType);
                    } else {
                        setDesiredTheme("image", rawWallpaperPath, isLight, iconTheme, selectedMatugenType);
                    }
                }
            } else if (currentTheme !== "custom") {
                const darkTheme = StockThemes.getThemeByName(currentTheme, false);
                const lightTheme = StockThemes.getThemeByName(currentTheme, true);
                if (darkTheme && darkTheme.primary) {
                    const stockColors = buildMatugenColorsFromTheme(darkTheme, lightTheme);
                    const themeData = isLight ? lightTheme : darkTheme;
                    setDesiredTheme("hex", themeData.primary, isLight, iconTheme, themeData.matugen_type, stockColors);
                }
            }
        }, 0);
        if (typeof SessionData !== "undefined") {
            SessionData.isLightModeChanged.connect(root.onLightModeChanged);
        }

        if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
            switchTheme(SettingsData.currentThemeName, false, false);
            const currentIsLight = (typeof SessionData !== "undefined") ? SessionData.isLightMode : false;
            SettingsData.updateCosmicThemeMode(currentIsLight);
        }
    }

    function getMatugenColor(path, fallback) {
        const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";
        let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode];
        for (const part of path.split(".")) {
            if (!cur || typeof cur !== "object" || !(part in cur))
                return fallback;
            cur = cur[part];
        }
        return cur || fallback;
    }

    readonly property var currentThemeData: {
        if (currentTheme === "custom") {
            return customThemeData || StockThemes.getThemeByName("purple", isLightMode);
        } else if (currentTheme === dynamic) {
            return {
                "primary": getMatugenColor("primary", "#42a5f5"),
                "primaryText": getMatugenColor("on_primary", "#ffffff"),
                "primaryContainer": getMatugenColor("primary_container", "#1976d2"),
                "secondary": getMatugenColor("secondary", "#8ab4f8"),
                "secondaryContainer": getMatugenColor("secondary_container", getMatugenColor("surface_container_high", "#292b2f")),
                "tertiary": getMatugenColor("tertiary", "#efb8c8"),
                "tertiaryContainer": getMatugenColor("tertiary_container", getMatugenColor("surface_container_high", "#292b2f")),
                "surface": getMatugenColor("surface", "#1a1c1e"),
                "surfaceText": getMatugenColor("on_background", "#e3e8ef"),
                "surfaceVariant": getMatugenColor("surface_variant", "#44464f"),
                "surfaceVariantText": getMatugenColor("on_surface_variant", "#c4c7c5"),
                "surfaceTint": getMatugenColor("surface_tint", "#8ab4f8"),
                "background": getMatugenColor("background", "#1a1c1e"),
                "backgroundText": getMatugenColor("on_background", "#e3e8ef"),
                "outline": getMatugenColor("outline", "#8e918f"),
                "surfaceContainerLowest": getMatugenColor("surface_container_lowest", "#0e1013"),
                "surfaceContainerLow": getMatugenColor("surface_container_low", "#181a1d"),
                "surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
                "surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
                "surfaceContainerHighest": getMatugenColor("surface_container_highest", "#343740"),
                "error": "#F2B8B5",
                "warning": "#FF9800",
                "info": "#2196F3",
                "success": "#4CAF50"
            };
        } else {
            return StockThemes.getThemeByName(currentTheme, isLightMode);
        }
    }

    readonly property var availableMatugenSchemes: [({
                "value": "scheme-tonal-spot",
                "label": I18n.tr("Tonal Spot", "matugen color scheme option"),
                "description": I18n.tr("Balanced palette with focused accents (default).")
            }), ({
                "value": "scheme-vibrant",
                "label": I18n.tr("Vibrant", "matugen color scheme option"),
                "description": I18n.tr("Lively palette with saturated accents.")
            }), ({
                "value": "scheme-content",
                "label": I18n.tr("Content", "matugen color scheme option"),
                "description": I18n.tr("Derives colors that closely match the underlying image.")
            }), ({
                "value": "scheme-expressive",
                "label": I18n.tr("Expressive", "matugen color scheme option"),
                "description": I18n.tr("Vibrant palette with playful saturation.")
            }), ({
                "value": "scheme-fidelity",
                "label": I18n.tr("Fidelity", "matugen color scheme option"),
                "description": I18n.tr("High-fidelity palette that preserves source hues.")
            }), ({
                "value": "scheme-fruit-salad",
                "label": I18n.tr("Fruit Salad", "matugen color scheme option"),
                "description": I18n.tr("Colorful mix of bright contrasting accents.")
            }), ({
                "value": "scheme-monochrome",
                "label": I18n.tr("Monochrome", "matugen color scheme option"),
                "description": I18n.tr("Minimal palette built around a single hue.")
            }), ({
                "value": "scheme-neutral",
                "label": I18n.tr("Neutral", "matugen color scheme option"),
                "description": I18n.tr("Muted palette with subdued, calming tones.")
            }), ({
                "value": "scheme-rainbow",
                "label": I18n.tr("Rainbow", "matugen color scheme option"),
                "description": I18n.tr("Diverse palette spanning the full spectrum.")
            })]

    function getMatugenScheme(value) {
        const schemes = availableMatugenSchemes;
        for (var i = 0; i < schemes.length; i++) {
            if (schemes[i].value === value)
                return schemes[i];
        }
        return schemes[0];
    }

    property color primary: currentThemeData.primary
    property color primaryText: currentThemeData.primaryText
    property color secondary: currentThemeData.secondary
    property color tertiary: currentThemeData.tertiary || currentThemeData.secondary
    property color surface: currentThemeData.surface
    property color surfaceText: currentThemeData.surfaceText
    property color surfaceVariant: currentThemeData.surfaceVariant
    property color surfaceVariantText: currentThemeData.surfaceVariantText
    property color surfaceTint: currentThemeData.surfaceTint
    property color background: currentThemeData.background
    property color backgroundText: currentThemeData.backgroundText
    property color outline: currentThemeData.outline
    property color outlineVariant: currentThemeData.outlineVariant || withAlpha(outline, 0.6)
    property color surfaceContainerLowest: currentThemeData.surfaceContainerLowest || blend(surfaceContainer, surface, 1.2)
    property color surfaceContainerLow: currentThemeData.surfaceContainerLow || blend(surface, surfaceContainer, 0.667)
    property color surfaceContainer: currentThemeData.surfaceContainer
    property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
    property color surfaceContainerHighest: currentThemeData.surfaceContainerHighest || surfaceContainerHigh
    property color primaryContainer: currentThemeData.primaryContainer || blend(surfaceContainerHigh, primary, 0.45)
    property color secondaryContainer: currentThemeData.secondaryContainer || blend(surfaceContainerHigh, secondary, 0.35)
    property color tertiaryContainer: currentThemeData.tertiaryContainer || blend(surfaceContainerHigh, tertiary, 0.35)

    property color onSurface: surfaceText
    property color onSurfaceVariant: surfaceVariantText
    property color onPrimary: primaryText
    property color onSurface_12: withAlpha(onSurface, 0.12)
    property color onSurface_38: withAlpha(onSurface, 0.38)
    property color onSurfaceVariant_30: withAlpha(onSurfaceVariant, 0.30)

    property color error: currentThemeData.error || "#F2B8B5"
    property color warning: currentThemeData.warning || "#FF9800"
    property color info: currentThemeData.info || "#2196F3"
    property color tempWarning: "#ff9933"
    property color tempDanger: "#ff5555"
    property color success: currentThemeData.success || "#4CAF50"

    property color primaryHover: withAlpha(primary, 0.12)
    property color primaryHoverLight: withAlpha(primary, transparentBlurLayers ? 0.12 : 0.08)
    property color primaryPressed: withAlpha(primary, transparentBlurLayers ? 0.24 : 0.16)
    property color primarySelected: withAlpha(primary, 0.3)
    property color primaryBackground: withAlpha(primary, 0.04)

    property color secondaryHover: withAlpha(secondary, 0.08)

    property color surfaceHover: withAlpha(surfaceVariant, 0.08)
    property color surfacePressed: withAlpha(surfaceVariant, 0.12)
    property color surfaceSelected: withAlpha(surfaceVariant, 0.15)
    property color surfaceLight: withAlpha(surfaceVariant, transparentBlurLayers ? 0.3 : 0.1)
    property color surfaceVariantAlpha: withAlpha(surfaceVariant, 0.2)

    readonly property bool foregroundLayers: typeof SettingsData === "undefined" || (SettingsData.blurForegroundLayers ?? true)
    readonly property bool blurForegroundLayers: blurLayersActive && foregroundLayers
    readonly property bool transparentBlurLayers: blurLayersActive && !foregroundLayers
    readonly property bool notificationForegroundLayers: typeof SettingsData === "undefined" || (SettingsData.notificationForegroundLayers ?? true)
    readonly property color readableSurface: withAlpha(surfaceContainer, popupTransparency)
    readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
    readonly property color floatingSurface: foregroundLayers ? readableSurface : withAlpha(readableSurface, 0)
    readonly property color floatingSurfaceHigh: foregroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0)
    readonly property color nestedSurface: floatingSurfaceHigh
    readonly property color notificationFloatingSurface: notificationForegroundLayers ? readableSurface : withAlpha(readableSurface, 0)
    readonly property color notificationFloatingSurfaceHigh: notificationForegroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0)
    readonly property color notificationNestedSurface: notificationFloatingSurfaceHigh
    readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0.12 : (SettingsData.blurLayerOutlineOpacity ?? 0.12)))
    readonly property real layerOutlineOpacity: blurLayerOutlineOpacity
    readonly property int layerOutlineWidth: layerOutlineOpacity > 0 ? 1 : 0
    property color surfaceTextHover: withAlpha(surfaceText, 0.08)
    property color surfaceTextAlpha: withAlpha(surfaceText, 0.3)

    function roleColor(mode) {
        switch (mode) {
        case "primary":
        case "pri":
            return primary;
        case "primaryContainer":
            return primaryContainer;
        case "secondary":
        case "sec":
            return secondary;
        case "secondaryContainer":
            return secondaryContainer;
        case "tertiary":
        case "ter":
            return tertiary;
        case "tertiaryContainer":
            return tertiaryContainer;
        case "surfaceText":
            return surfaceText;
        case "surfaceVariant":
            return surfaceVariant;
        case "s":
            return surface;
        case "scll":
            return surfaceContainerLowest;
        case "scl":
            return surfaceContainerLow;
        case "sc":
            return surfaceContainer;
        case "sch":
            return surfaceContainerHigh;
        case "schh":
            return surfaceContainerHighest;
        case "sth":
            return surfaceTextHover;
        case "error":
        case "err":
            return error;
        default:
            return withAlpha(surface, 0);
        }
    }
    property color surfaceTextLight: withAlpha(surfaceText, 0.06)
    property color surfaceTextSecondary: withAlpha(surfaceText, 0.6)
    property color surfaceTextMedium: withAlpha(surfaceText, 0.7)

    property color outlineButton: withAlpha(outline, 0.5)
    property color outlineLight: withAlpha(outline, Math.min(1, layerOutlineOpacity * 0.625))
    property color outlineMedium: withAlpha(outline, layerOutlineOpacity)
    property color outlineStrong: withAlpha(outline, Math.min(1, layerOutlineOpacity * 1.5))
    property color outlineHeavy: withAlpha(outline, 0.2)

    property color errorHover: withAlpha(error, 0.12)
    property color errorPressed: withAlpha(error, 0.16)
    property color errorSelected: withAlpha(error, 0.3)
    property color warningHover: withAlpha(warning, 0.12)

    readonly property color ccTileActiveBg: {
        switch (SettingsData.controlCenterTileColorMode) {
        case "primaryContainer":
            return primaryContainer;
        case "secondary":
            return secondary;
        case "surfaceVariant":
            return surfaceVariant;
        default:
            return primary;
        }
    }

    readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (foregroundLayers ? withAlpha(surfaceContainerHigh, blurLayersActive ? Math.min(popupTransparency, 0.24) : popupTransparency) : withAlpha(surfaceContainer, 0))
    readonly property color ccPillInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.08) : nestedSurface
    readonly property color ccPillInactiveHoverBg: transparentBlurLayers ? withAlpha(primary, 0.10) : primaryPressed
    readonly property color ccSliderTrackColor: transparentBlurLayers ? surfaceText : surfaceContainerHigh
    readonly property real ccSliderTrackOpacity: transparentBlurLayers ? 0.18 : popupTransparency

    readonly property color ccTileActiveText: {
        switch (SettingsData.controlCenterTileColorMode) {
        case "primaryContainer":
            return primary;
        case "secondary":
            return surfaceText;
        case "surfaceVariant":
            return surfaceText;
        default:
            return primaryText;
        }
    }

    readonly property color ccTileInactiveIcon: {
        switch (SettingsData.controlCenterTileColorMode) {
        case "primaryContainer":
            return primary;
        case "secondary":
            return secondary;
        case "surfaceVariant":
            return surfaceText;
        default:
            return primary;
        }
    }

    readonly property color ccTileRing: {
        switch (SettingsData.controlCenterTileColorMode) {
        case "primaryContainer":
            return withAlpha(primary, 0.22);
        case "secondary":
            return withAlpha(surfaceText, 0.22);
        case "surfaceVariant":
            return withAlpha(surfaceText, 0.22);
        default:
            return withAlpha(primaryText, 0.22);
        }
    }

    readonly property color buttonBg: {
        switch (SettingsData.buttonColorMode) {
        case "primaryContainer":
            return primaryContainer;
        case "secondary":
            return secondary;
        case "surfaceVariant":
            return surfaceVariant;
        default:
            return primary;
        }
    }

    readonly property color buttonText: {
        switch (SettingsData.buttonColorMode) {
        case "primaryContainer":
            return primary;
        case "secondary":
            return surfaceText;
        case "surfaceVariant":
            return surfaceText;
        default:
            return primaryText;
        }
    }

    readonly property color buttonHover: {
        switch (SettingsData.buttonColorMode) {
        case "primaryContainer":
            return withAlpha(primary, 0.12);
        case "secondary":
            return withAlpha(surfaceText, 0.12);
        case "surfaceVariant":
            return withAlpha(surfaceText, 0.12);
        default:
            return primaryHover;
        }
    }

    readonly property color buttonPressed: {
        switch (SettingsData.buttonColorMode) {
        case "primaryContainer":
            return withAlpha(primary, 0.16);
        case "secondary":
            return withAlpha(surfaceText, 0.16);
        case "surfaceVariant":
            return withAlpha(surfaceText, 0.16);
        default:
            return primaryPressed;
        }
    }

    property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
    property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)

    readonly property bool elevationEnabled: typeof SettingsData !== "undefined" && (SettingsData.m3ElevationEnabled ?? true)
    readonly property real elevationBlurMax: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? Math.min(128, Math.max(32, SettingsData.m3ElevationIntensity * 2)) : 64

    readonly property real _elevMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? SettingsData.m3ElevationIntensity / 12 : 1
    readonly property real _opMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationOpacity !== undefined ? SettingsData.m3ElevationOpacity / 60 : 1
    function normalizeElevationDirection(direction) {
        switch (direction) {
        case "top":
        case "topLeft":
        case "topRight":
        case "bottom":
        case "bottomLeft":
        case "bottomRight":
        case "left":
        case "right":
        case "autoBar":
            return direction;
        default:
            return "top";
        }
    }

    readonly property string elevationLightDirection: {
        if (typeof SettingsData === "undefined" || !SettingsData.m3ElevationLightDirection)
            return "top";
        switch (SettingsData.m3ElevationLightDirection) {
        case "autoBar":
        case "top":
        case "topLeft":
        case "topRight":
        case "bottom":
            return SettingsData.m3ElevationLightDirection;
        default:
            return "top";
        }
    }
    readonly property real _elevDiagRatio: 0.55
    readonly property string _globalElevationDirForTokens: {
        const normalized = normalizeElevationDirection(elevationLightDirection);
        return normalized === "autoBar" ? "top" : normalized;
    }
    readonly property real _elevDirX: {
        switch (_globalElevationDirForTokens) {
        case "topLeft":
        case "bottomLeft":
        case "left":
            return 1;
        case "topRight":
        case "bottomRight":
        case "right":
            return -1;
        default:
            return 0;
        }
    }
    readonly property real _elevDirY: {
        switch (_globalElevationDirForTokens) {
        case "bottom":
        case "bottomLeft":
        case "bottomRight":
            return -1;
        case "left":
        case "right":
            return 0;
        default:
            return 1;
        }
    }
    readonly property real _elevDirXScale: (_globalElevationDirForTokens === "left" || _globalElevationDirForTokens === "right") ? 1 : _elevDiagRatio

    readonly property var elevationLevel1: ({
            blurPx: 4 * _elevMult,
            offsetX: 1 * _elevMult * _elevDirXScale * _elevDirX,
            offsetY: 1 * _elevMult * _elevDirY,
            spreadPx: 0,
            alpha: 0.2 * _opMult
        })
    readonly property var elevationLevel2: ({
            blurPx: 8 * _elevMult,
            offsetX: 4 * _elevMult * _elevDirXScale * _elevDirX,
            offsetY: 4 * _elevMult * _elevDirY,
            spreadPx: 0,
            alpha: 0.25 * _opMult
        })
    readonly property var elevationLevel3: ({
            blurPx: 12 * _elevMult,
            offsetX: 6 * _elevMult * _elevDirXScale * _elevDirX,
            offsetY: 6 * _elevMult * _elevDirY,
            spreadPx: 0,
            alpha: 0.3 * _opMult
        })
    readonly property var elevationLevel4: ({
            blurPx: 16 * _elevMult,
            offsetX: 8 * _elevMult * _elevDirXScale * _elevDirX,
            offsetY: 8 * _elevMult * _elevDirY,
            spreadPx: 0,
            alpha: 0.3 * _opMult
        })
    readonly property var elevationLevel5: ({
            blurPx: 20 * _elevMult,
            offsetX: 10 * _elevMult * _elevDirXScale * _elevDirX,
            offsetY: 10 * _elevMult * _elevDirY,
            spreadPx: 0,
            alpha: 0.3 * _opMult
        })

    function elevationOffsetMagnitude(level, fallback, direction) {
        if (!level) {
            return fallback !== undefined ? Math.abs(fallback) : 0;
        }
        const yMag = Math.abs(level.offsetY !== undefined ? level.offsetY : 0);
        if (yMag > 0)
            return yMag;
        const xMag = Math.abs(level.offsetX !== undefined ? level.offsetX : 0);
        if (xMag > 0) {
            if (direction === "left" || direction === "right")
                return xMag;
            return xMag / _elevDiagRatio;
        }
        return fallback !== undefined ? Math.abs(fallback) : 0;
    }

    function elevationOffsetXFor(level, direction, fallback) {
        const dir = normalizeElevationDirection(direction || elevationLightDirection);
        const mag = elevationOffsetMagnitude(level, fallback, dir);
        switch (dir) {
        case "topLeft":
        case "bottomLeft":
            return mag * _elevDiagRatio;
        case "topRight":
        case "bottomRight":
            return -mag * _elevDiagRatio;
        case "left":
            return mag;
        case "right":
            return -mag;
        default:
            return 0;
        }
    }

    function elevationOffsetYFor(level, direction, fallback) {
        const dir = normalizeElevationDirection(direction || elevationLightDirection);
        const mag = elevationOffsetMagnitude(level, fallback, dir);
        switch (dir) {
        case "bottom":
        case "bottomLeft":
        case "bottomRight":
            return -mag;
        case "left":
        case "right":
            return 0;
        default:
            return mag;
        }
    }

    function elevationOffsetX(level, fallback) {
        return elevationOffsetXFor(level, elevationLightDirection, fallback);
    }

    function elevationOffsetY(level, fallback) {
        return elevationOffsetYFor(level, elevationLightDirection, fallback);
    }

    function elevationRenderPadding(level, direction, fallbackOffset, extraPadding, minPadding) {
        const dir = direction !== undefined ? direction : elevationLightDirection;
        const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0;
        const spread = (level && level.spreadPx !== undefined) ? Math.max(0, level.spreadPx) : 0;
        const fallback = fallbackOffset !== undefined ? fallbackOffset : 0;
        const extra = extraPadding !== undefined ? extraPadding : 8;
        const minPad = minPadding !== undefined ? minPadding : 16;
        const offsetX = Math.abs(elevationOffsetXFor(level, dir, fallback));
        const offsetY = Math.abs(elevationOffsetYFor(level, dir, fallback));
        return Math.max(minPad, blur + spread + Math.max(offsetX, offsetY) + extra);
    }

    function elevationShadowColor(level) {
        const alpha = (level && level.alpha !== undefined) ? level.alpha : 0.3;
        let r = 0;
        let g = 0;
        let b = 0;

        if (typeof SettingsData !== "undefined") {
            const mode = SettingsData.m3ElevationColorMode || "default";
            if (mode === "default") {
                r = 0;
                g = 0;
                b = 0;
            } else if (mode === "text") {
                r = surfaceText.r;
                g = surfaceText.g;
                b = surfaceText.b;
            } else if (mode === "primary") {
                r = primary.r;
                g = primary.g;
                b = primary.b;
            } else if (mode === "surfaceVariant") {
                r = surfaceVariant.r;
                g = surfaceVariant.g;
                b = surfaceVariant.b;
            } else if (mode === "custom" && SettingsData.m3ElevationCustomColor) {
                const c = Qt.color(SettingsData.m3ElevationCustomColor);
                r = c.r;
                g = c.g;
                b = c.b;
            }
        }
        return Qt.rgba(r, g, b, alpha);
    }
    function elevationAmbient(level) {
        const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0;
        const alpha = ((level && level.alpha !== undefined) ? level.alpha : 0.3) * 0.5;
        return {
            blurPx: blur * 1.75,
            spreadPx: 1,
            alpha: alpha
        };
    }

    readonly property var animationDurations: [
        {
            "shorter": 0,
            "short": 0,
            "medium": 0,
            "long": 0,
            "extraLong": 0
        },
        {
            "shorter": 50,
            "short": 75,
            "medium": 150,
            "long": 250,
            "extraLong": 500
        },
        {
            "shorter": 100,
            "short": 150,
            "medium": 300,
            "long": 500,
            "extraLong": 1000
        },
        {
            "shorter": 150,
            "short": 225,
            "medium": 450,
            "long": 750,
            "extraLong": 1500
        },
        {
            "shorter": 200,
            "short": 300,
            "medium": 600,
            "long": 1000,
            "extraLong": 2000
        }
    ]

    readonly property int currentAnimationSpeed: typeof SettingsData !== "undefined" ? SettingsData.animationSpeed : SettingsData.AnimationSpeed.Short
    readonly property var currentDurations: animationDurations[currentAnimationSpeed] || animationDurations[SettingsData.AnimationSpeed.Short]

    readonly property int shorterDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.shorter
    readonly property int shortDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.short
    readonly property bool snapListModelChanges: shortDuration <= 0
    readonly property int mediumDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.medium
    readonly property int longDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.long
    readonly property int extraLongDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.extraLong
    readonly property int standardEasing: Easing.OutCubic
    readonly property int emphasizedEasing: Easing.OutQuart

    readonly property var expressiveCurves: {
        "emphasized": [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1],
        "emphasizedAccel": [0.3, 0, 0.8, 0.15, 1, 1],
        "emphasizedDecel": [0.05, 0.7, 0.1, 1, 1, 1],
        "standard": [0.2, 0, 0, 1, 1, 1],
        "standardAccel": [0.3, 0, 1, 1, 1, 1],
        "standardDecel": [0, 0, 0, 1, 1, 1],
        "expressiveFastSpatial": [0.42, 1.67, 0.21, 0.9, 1, 1],
        "expressiveDefaultSpatial": [0.38, 1.21, 0.22, 1, 1, 1],
        "expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
    }

    // Theme is the canonical access point for animation variant state. The
    // aliases below forward to AnimVariants.qml so consumers don't need two
    // imports. ~200 call sites read through Theme.variantEnterCurve /
    // Theme.isConnectedEffect / etc. — do NOT migrate to AnimVariants directly.
    readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
    readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
    readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
    readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
    readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
    readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
    readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
    readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
    readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
    readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
    readonly property bool isDepthEffect: AnimVariants.isDepthEffect
    readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect
    readonly property real connectedCornerRadius: {
        if (typeof SettingsData === "undefined")
            return 12;
        return FrameTransitionState.effectiveConnectedFrameModeActive ? SettingsData.frameRounding : cornerRadius;
    }
    readonly property color connectedSurfaceColor: {
        if (typeof SettingsData === "undefined")
            return withAlpha(surfaceContainer, popupTransparency);
        return isConnectedEffect ? withAlpha(SettingsData.effectiveFrameColor, SettingsData.frameOpacity) : withAlpha(surfaceContainer, popupTransparency);
    }
    readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
    readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined") ? true : (!isConnectedEffect || SettingsData.frameBlurEnabled)
    readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
    readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
    function variantDuration(baseDuration, entering) {
        return AnimVariants.variantDuration(baseDuration, entering);
    }
    function variantExitCleanupPadding() {
        return AnimVariants.variantExitCleanupPadding();
    }
    function variantCloseInterval(baseDuration) {
        return AnimVariants.variantCloseInterval(baseDuration);
    }

    readonly property var animationPresetDurations: {
        "none": 0,
        "short": 250,
        "medium": 500,
        "long": 750
    }

    readonly property int currentAnimationBaseDuration: {
        if (typeof SettingsData === "undefined")
            return 500;

        if (SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) {
            return SettingsData.customAnimationDuration;
        }

        const presetMap = [0, 250, 500, 750];
        return presetMap[SettingsData.animationSpeed] !== undefined ? presetMap[SettingsData.animationSpeed] : 500;
    }

    readonly property var expressiveDurations: {
        if (typeof SettingsData === "undefined") {
            return {
                "fast": 200,
                "normal": 400,
                "large": 600,
                "extraLarge": 1000,
                "expressiveFastSpatial": 350,
                "expressiveDefaultSpatial": 500,
                "expressiveEffects": 200
            };
        }

        const baseDuration = currentAnimationBaseDuration;
        return {
            "fast": baseDuration * 0.4,
            "normal": baseDuration * 0.8,
            "large": baseDuration * 1.2,
            "extraLarge": baseDuration * 2.0,
            "expressiveFastSpatial": baseDuration * 0.7,
            "expressiveDefaultSpatial": baseDuration,
            "expressiveEffects": baseDuration * 0.4
        };
    }

    readonly property int notificationAnimationBaseDuration: {
        if (typeof SettingsData === "undefined")
            return 200;
        if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.None)
            return 0;
        if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.Custom)
            return SettingsData.notificationCustomAnimationDuration;
        const presetMap = [0, 200, 400, 600];
        return presetMap[SettingsData.notificationAnimationSpeed] ?? 200;
    }

    readonly property int notificationEnterDuration: {
        const base = notificationAnimationBaseDuration;
        return base === 0 ? 0 : Math.round(base * 0.875);
    }

    readonly property int notificationExitDuration: {
        const base = notificationAnimationBaseDuration;
        return base === 0 ? 0 : Math.round(base * 0.75);
    }

    readonly property int notificationExpandDuration: {
        const base = notificationAnimationBaseDuration;
        return base === 0 ? 0 : Math.round(base * 1.0);
    }

    readonly property int notificationCollapseDuration: {
        const base = notificationAnimationBaseDuration;
        return base === 0 ? 0 : Math.round(base * 0.85);
    }

    readonly property int notificationInlineExpandDuration: notificationAnimationBaseDuration === 0 ? 0 : 185
    readonly property int notificationInlineCollapseDuration: notificationAnimationBaseDuration === 0 ? 0 : 150

    readonly property real notificationIconSizeNormal: 56
    readonly property real notificationIconSizeCompact: 48
    readonly property real notificationExpandedIconSizeNormal: 48
    readonly property real notificationExpandedIconSizeCompact: 40
    readonly property real notificationActionMinWidth: 48
    readonly property real notificationButtonCornerRadius: cornerRadius / 2
    readonly property real notificationHoverRevealMargin: spacingXL
    readonly property real notificationContentSpacing: spacingXS
    readonly property real notificationCardPadding: spacingM
    readonly property real notificationCardPaddingCompact: spacingS

    readonly property real stateLayerHover: 0.08
    readonly property real stateLayerFocus: 0.12
    readonly property real stateLayerPressed: 0.12
    readonly property real stateLayerDrag: 0.16

    readonly property int popoutAnimationDuration: {
        if (typeof SettingsData === "undefined")
            return 150;
        if (SettingsData.syncComponentAnimationSpeeds) {
            return Math.min(currentAnimationBaseDuration, 1000);
        }
        const presetMap = [0, 150, 300, 500];
        if (SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom)
            return SettingsData.popoutCustomAnimationDuration;
        return presetMap[SettingsData.popoutAnimationSpeed] ?? 150;
    }

    readonly property int modalAnimationDuration: {
        if (typeof SettingsData === "undefined")
            return 150;
        if (SettingsData.syncComponentAnimationSpeeds) {
            return Math.min(currentAnimationBaseDuration, 1000);
        }
        const presetMap = [0, 150, 300, 500];
        if (SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom)
            return SettingsData.modalCustomAnimationDuration;
        return presetMap[SettingsData.modalAnimationSpeed] ?? 150;
    }

    property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12

    property string fontFamily: typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DankCommon.Fonts.sans

    property string monoFontFamily: typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DankCommon.Fonts.mono

    function resolvedFontFamily(family) {
        if (family === defaultFontFamily)
            return DankCommon.Fonts.sans;
        return family;
    }

    function resolvedMonoFontFamily(family) {
        if (family === defaultMonoFontFamily)
            return DankCommon.Fonts.mono;
        return family;
    }

    property int fontWeight: typeof SettingsData !== "undefined" ? SettingsData.fontWeight : Font.Normal

    property real fontScale: typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0

    property real spacingXXS: 2
    property real spacingXS: 4
    property real spacingS: 8
    property real spacingM: 12
    property real spacingL: 16
    property real spacingXL: 24
    property real fontSizeSmall: Math.round(fontScale * 12)
    property real fontSizeMedium: Math.round(fontScale * 14)
    property real fontSizeLarge: Math.round(fontScale * 16)
    property real fontSizeXLarge: Math.round(fontScale * 20)
    property real barHeight: 48
    property real iconSize: 24
    property real iconSizeSmall: 16
    property real iconSizeLarge: 32

    property real panelTransparency: 0.85
    property real popupTransparency: {
        if (typeof SettingsData === "undefined")
            return 1.0;
        if (isConnectedEffect)
            return SettingsData.frameOpacity !== undefined ? SettingsData.frameOpacity : 1.0;
        return SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0;
    }

    function screenTransition() {
        screenTransitionNeeded();
    }

    function switchTheme(themeName, savePrefs = true, enableTransition = true) {
        if (enableTransition) {
            screenTransition();
            themeTransitionTimer.themeName = themeName;
            themeTransitionTimer.savePrefs = savePrefs;
            themeTransitionTimer.restart();
            return;
        }

        if (themeName === dynamic) {
            currentTheme = dynamic;
            if (currentThemeCategory !== "registry")
                currentThemeCategory = dynamic;
        } else if (themeName === custom) {
            currentTheme = custom;
            if (currentThemeCategory !== "registry")
                currentThemeCategory = custom;
            if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
                loadCustomThemeFromFile(SettingsData.customThemeFile);
            }
        } else if (themeName === "" && currentThemeCategory === "registry") {
            // Registry category selected but no theme chosen yet
        } else {
            currentTheme = themeName;
            if (currentThemeCategory !== "registry") {
                currentThemeCategory = "generic";
            }
        }
        if (savePrefs && typeof SettingsData !== "undefined") {
            SettingsData.set("currentThemeCategory", currentThemeCategory);
            SettingsData.set("currentThemeName", currentTheme);
        }

        generateSystemThemesFromCurrentTheme();
    }

    function setLightMode(light, savePrefs = true, enableTransition = false) {
        if (enableTransition) {
            screenTransition();
            lightModeTransitionTimer.lightMode = light;
            lightModeTransitionTimer.savePrefs = savePrefs;
            lightModeTransitionTimer.restart();
            return;
        }

        if (savePrefs && typeof SessionData !== "undefined") {
            SessionData.setLightMode(light);
        }

        if (typeof SettingsData !== "undefined") {
            SettingsData.updateCosmicThemeMode(light);
        }
        generateSystemThemesFromCurrentTheme();
    }

    function toggleLightMode(savePrefs = true) {
        setLightMode(!isLightMode, savePrefs, true);
    }

    function getThemeColors(themeName) {
        if (themeName === "custom" && customThemeData) {
            return customThemeData;
        }
        return StockThemes.getThemeByName(themeName, isLightMode);
    }

    function switchThemeCategory(category, defaultTheme) {
        screenTransition();
        themeCategoryTransitionTimer.category = category;
        themeCategoryTransitionTimer.defaultTheme = defaultTheme;
        themeCategoryTransitionTimer.restart();
    }

    function loadCustomTheme(themeData) {
        customThemeRawData = themeData;
        const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";

        var baseColors = {};
        if (themeData.dark || themeData.light) {
            baseColors = themeData[colorMode] || themeData.dark || themeData.light || {};
        } else {
            baseColors = themeData;
        }

        if (themeData.variants) {
            const themeId = themeData.id || "";

            if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
                const defaults = themeData.variants.defaults || {};
                const modeDefaults = defaults[colorMode] || defaults.dark || {};
                const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
                var flavorId = stored.flavor || modeDefaults.flavor || "";
                const accentId = stored.accent || modeDefaults.accent || "";
                var flavor = findVariant(themeData.variants.flavors, flavorId);
                if (flavor) {
                    const hasCurrentModeColors = flavor[colorMode] && (flavor[colorMode].primary || flavor[colorMode].surface);
                    if (!hasCurrentModeColors) {
                        flavorId = modeDefaults.flavor || "";
                        flavor = findVariant(themeData.variants.flavors, flavorId);
                    }
                }
                const accent = findAccent(themeData.variants.accents, accentId);
                if (flavor) {
                    const flavorColors = flavor[colorMode] || flavor.dark || flavor.light || {};
                    baseColors = mergeColors(baseColors, flavorColors);
                }
                if (accent && flavor) {
                    const accentColors = accent[flavor.id] || {};
                    baseColors = mergeColors(baseColors, accentColors);
                }
                customThemeData = baseColors;
                generateSystemThemesFromCurrentTheme();
                return;
            }

            if (themeData.variants.options && themeData.variants.options.length > 0) {
                const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
                const variant = findVariant(themeData.variants.options, selectedVariantId);
                if (variant) {
                    const variantColors = variant[colorMode] || variant.dark || variant.light || {};
                    customThemeData = mergeColors(baseColors, variantColors);
                    generateSystemThemesFromCurrentTheme();
                    return;
                }
            }
        }

        customThemeData = baseColors;
        generateSystemThemesFromCurrentTheme();
    }

    function findVariant(options, variantId) {
        if (!variantId || !options)
            return null;
        for (var i = 0; i < options.length; i++) {
            if (options[i].id === variantId)
                return options[i];
        }
        return options[0] || null;
    }

    function findAccent(accents, accentId) {
        if (!accentId || !accents)
            return null;
        for (var i = 0; i < accents.length; i++) {
            if (accents[i].id === accentId)
                return accents[i];
        }
        return accents[0] || null;
    }

    function mergeColors(base, overlay) {
        var result = JSON.parse(JSON.stringify(base));
        for (var key in overlay) {
            if (overlay[key])
                result[key] = overlay[key];
        }
        return result;
    }

    function loadCustomThemeFromFile(filePath) {
        customThemeFileView.path = Paths.expandTilde(filePath);
    }

    function reloadCustomThemeVariant() {
        if (currentTheme !== "custom" || !customThemeRawData)
            return;
        loadCustomTheme(customThemeRawData);
    }

    property alias availableThemeNames: root._availableThemeNames
    readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
    property string currentThemeName: currentTheme

    property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency

    property bool widgetBackgroundHasAlpha: {
        const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch";
        return colorMode === "sth" || colorMode === "custom";
    }

    function safeColor(value, fallback) {
        try {
            if (value === undefined || value === null || value === "")
                return fallback;
            return Qt.color(value);
        } catch (e) {
            return fallback;
        }
    }

    readonly property color widgetBackgroundCustomBaseColor: safeColor(typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundCustomColor : "#6750A4", primaryContainer)
    readonly property real widgetBackgroundCustomStrength: Math.max(0, Math.min(1, typeof SettingsData !== "undefined" ? (SettingsData.widgetBackgroundCustomStrength ?? 0.4) : 0.4))

    property var widgetBaseBackgroundColor: {
        const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch";
        switch (colorMode) {
        case "s":
            return surface;
        case "sc":
            return surfaceContainer;
        case "sch":
            return surfaceContainerHigh;
        case "primaryContainer":
            return primaryContainer;
        case "secondaryContainer":
            return secondaryContainer;
        case "tertiaryContainer":
            return tertiaryContainer;
        case "custom":
            return blend(surfaceContainerHigh, widgetBackgroundCustomBaseColor, widgetBackgroundCustomStrength);
        case "sth":
        default:
            return surfaceTextHover;
        }
    }

    property color widgetBaseHoverColor: {
        const blended = blend(widgetBaseBackgroundColor, primary, 0.1);
        return withAlpha(blended, Math.max(0.3, blended.a));
    }

    property color widgetIconColor: {
        if (typeof SettingsData === "undefined") {
            return surfaceText;
        }

        switch (SettingsData.widgetColorMode) {
        case "colorful":
            return surfaceText;
        case "default":
        default:
            return surfaceText;
        }
    }

    property color widgetTextColor: {
        if (typeof SettingsData === "undefined") {
            return surfaceText;
        }

        switch (SettingsData.widgetColorMode) {
        case "colorful":
            return primary;
        case "default":
        default:
            return surfaceText;
        }
    }

    function barIconSize(barThickness, offset, maximizeIcon, iconScale) {
        const defaultOffset = offset !== undefined ? offset : -6;
        const size = (maximizeIcon ?? false) ? iconSizeLarge : iconSize;
        const s = iconScale !== undefined ? iconScale : 1.0;

        return Math.round((barThickness / 48) * (size + defaultOffset) * s);
    }

    function barTextSize(barThickness, fontScale, maximizeText) {
        const scale = barThickness / 48;
        const dankBarScale = fontScale !== undefined ? fontScale : 1.0;
        const maxScale = (maximizeText ?? false) ? 1.5 : 1.0;
        if (scale <= 0.75)
            return Math.round(fontSizeSmall * 0.9 * dankBarScale * maxScale);
        if (scale >= 1.25)
            return Math.round(fontSizeMedium * dankBarScale * maxScale);
        return Math.round(fontSizeSmall * dankBarScale * maxScale);
    }

    function getBatteryIcon(level, isCharging, batteryAvailable) {
        if (!batteryAvailable)
            return "battery_std";

        if (isCharging) {
            if (level >= 90)
                return "battery_charging_full";
            if (level >= 80)
                return "battery_charging_90";
            if (level >= 60)
                return "battery_charging_80";
            if (level >= 50)
                return "battery_charging_60";
            if (level >= 30)
                return "battery_charging_50";
            if (level >= 20)
                return "battery_charging_30";
            return "battery_charging_20";
        } else {
            if (level >= 95)
                return "battery_full";
            if (level >= 85)
                return "battery_6_bar";
            if (level >= 70)
                return "battery_5_bar";
            if (level >= 55)
                return "battery_4_bar";
            if (level >= 40)
                return "battery_3_bar";
            if (level >= 25)
                return "battery_2_bar";
            if (level >= 10)
                return "battery_1_bar";
            return "battery_alert";
        }
    }

    function getPowerProfileIcon(profile) {
        switch (profile) {
        case 0:
            return "battery_saver";
        case 1:
            return "battery_std";
        case 2:
            return "flash_on";
        default:
            return "settings";
        }
    }

    function getPowerProfileLabel(profile) {
        switch (profile) {
        case 0:
            return I18n.tr("Power Saver", "power profile option");
        case 1:
            return I18n.tr("Balanced", "power profile option");
        case 2:
            return I18n.tr("Performance", "power profile option");
        default:
            return I18n.tr("Unknown", "power profile option");
        }
    }

    function onLightModeChanged() {
        if (currentTheme === "custom" && customThemeFileView.path) {
            customThemeFileView.reload();
        }
    }

    function setDesiredTheme(kind, value, isLight, iconTheme, matugenType, stockColors) {
        if (!matugenAvailable) {
            log.warn("matugen not available or disabled - cannot set system theme");
            return;
        }

        if (workerRunning) {
            log.info("Worker already running, queueing request");
            pendingThemeRequest = {
                kind,
                value,
                isLight,
                iconTheme,
                matugenType,
                stockColors
            };
            return;
        }

        log.info("Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", stockColors ? "(stock colors)" : "(dynamic)");

        themeGenerationStarting();

        const desired = {
            "kind": kind,
            "value": value,
            "mode": isLight ? "light" : "dark",
            "iconTheme": iconTheme || "System Default",
            "matugenType": matugenType || "scheme-tonal-spot",
            "runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
        };

        log.debug("Starting matugen worker");
        workerRunning = true;

        const args = ["dms", "matugen", "queue", "--state-dir", stateDir, "--shell-dir", shellDir, "--config-dir", configDir, "--kind", desired.kind, "--value", desired.value, "--mode", desired.mode, "--icon-theme", desired.iconTheme, "--matugen-type", desired.matugenType,];

        if (!desired.runUserTemplates) {
            args.push("--run-user-templates=false");
        }
        if (stockColors) {
            args.push("--stock-colors", JSON.stringify(stockColors));
        }
        if (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal) {
            args.push("--sync-mode-with-portal");
        }
        if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
            args.push("--terminals-always-dark");
        }
        if (typeof SettingsData !== "undefined" && SettingsData.matugenContrast !== 0) {
            args.push("--contrast", SettingsData.matugenContrast.toString());
        }

        if (typeof SettingsData !== "undefined") {
            const skipTemplates = [];
            if (!SettingsData.runDmsMatugenTemplates) {
                skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "vencord", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
            } else {
                if (!SettingsData.matugenTemplateGtk)
                    skipTemplates.push("gtk");
                if (!SettingsData.matugenTemplateNiri)
                    skipTemplates.push("niri");
                if (!SettingsData.matugenTemplateHyprland)
                    skipTemplates.push("hyprland");
                if (!SettingsData.matugenTemplateMangowc)
                    skipTemplates.push("mangowc");
                if (!SettingsData.matugenTemplateQt5ct)
                    skipTemplates.push("qt5ct");
                if (!SettingsData.matugenTemplateQt6ct)
                    skipTemplates.push("qt6ct");
                if (!SettingsData.matugenTemplateFirefox)
                    skipTemplates.push("firefox");
                if (!SettingsData.matugenTemplatePywalfox)
                    skipTemplates.push("pywalfox");
                if (!SettingsData.matugenTemplateZenBrowser)
                    skipTemplates.push("zenbrowser");
                if (!SettingsData.matugenTemplateVesktop)
                    skipTemplates.push("vesktop");
                if (!SettingsData.matugenTemplateVencord)
                    skipTemplates.push("vencord");
                if (!SettingsData.matugenTemplateEquibop)
                    skipTemplates.push("equibop");
                if (!SettingsData.matugenTemplateGhostty)
                    skipTemplates.push("ghostty");
                if (!SettingsData.matugenTemplateKitty)
                    skipTemplates.push("kitty");
                if (!SettingsData.matugenTemplateFoot)
                    skipTemplates.push("foot");
                if (!SettingsData.matugenTemplateNeovim)
                    skipTemplates.push("nvim");
                if (!SettingsData.matugenTemplateAlacritty)
                    skipTemplates.push("alacritty");
                if (!SettingsData.matugenTemplateWezterm)
                    skipTemplates.push("wezterm");
                if (!SettingsData.matugenTemplateDgop)
                    skipTemplates.push("dgop");
                if (!SettingsData.matugenTemplateKcolorscheme)
                    skipTemplates.push("kcolorscheme");
                if (!SettingsData.matugenTemplateVscode)
                    skipTemplates.push("vscode");
                if (!SettingsData.matugenTemplateEmacs)
                    skipTemplates.push("emacs");
                if (!SettingsData.matugenTemplateZed)
                    skipTemplates.push("zed");
            }
            if (skipTemplates.length > 0) {
                args.push("--skip-templates", skipTemplates.join(","));
            }
        }

        systemThemeGenerator.command = args;
        systemThemeGenerator.running = true;
    }

    function generateSystemThemesFromCurrentTheme() {
        if (!matugenAvailable)
            return;

        _lastGenerateMs = Date.now();
        _pendingGenerateParams = true;
        _themeGenerateDebounce.restart();
    }

    function _executeThemeGeneration() {
        if (!_pendingGenerateParams)
            return;
        _pendingGenerateParams = null;

        const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
        const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";

        if (currentTheme === dynamic) {
            if (!rawWallpaperPath) {
                log.warn("Auto theme has no wallpaper - skipping matugen");
                return;
            }
            const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
            const kind = rawWallpaperPath.startsWith("#") ? "hex" : "image";
            setDesiredTheme(kind, rawWallpaperPath, isLight, iconTheme, selectedMatugenType, null);
            return;
        }

        let darkTheme, lightTheme;
        if (currentTheme === "custom") {
            if (customThemeRawData && (customThemeRawData.dark || customThemeRawData.light)) {
                darkTheme = customThemeRawData.dark || customThemeRawData.light;
                lightTheme = customThemeRawData.light || customThemeRawData.dark;

                if (customThemeRawData.variants) {
                    const themeId = customThemeRawData.id || "";

                    if (customThemeRawData.variants.type === "multi" && customThemeRawData.variants.flavors && customThemeRawData.variants.accents) {
                        const defaults = customThemeRawData.variants.defaults || {};
                        const darkDefaults = defaults.dark || {};
                        const lightDefaults = defaults.light || defaults.dark || {};
                        const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
                        const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
                        const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
                        const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
                        const darkAccentId = storedDark.accent || darkDefaults.accent || "";
                        const lightAccentId = storedLight.accent || lightDefaults.accent || "";
                        const darkFlavor = findVariant(customThemeRawData.variants.flavors, darkFlavorId);
                        const lightFlavor = findVariant(customThemeRawData.variants.flavors, lightFlavorId);
                        const darkAccent = findAccent(customThemeRawData.variants.accents, darkAccentId);
                        const lightAccent = findAccent(customThemeRawData.variants.accents, lightAccentId);
                        if (darkFlavor) {
                            darkTheme = mergeColors(darkTheme, darkFlavor.dark || {});
                            if (darkAccent)
                                darkTheme = mergeColors(darkTheme, darkAccent[darkFlavor.id] || {});
                        }
                        if (lightFlavor) {
                            lightTheme = mergeColors(lightTheme, lightFlavor.light || {});
                            if (lightAccent)
                                lightTheme = mergeColors(lightTheme, lightAccent[lightFlavor.id] || {});
                        }
                    } else if (customThemeRawData.variants.options) {
                        const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
                        const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
                        if (variant) {
                            darkTheme = mergeColors(darkTheme, variant.dark || {});
                            lightTheme = mergeColors(lightTheme, variant.light || {});
                        }
                    }
                }
            } else {
                darkTheme = customThemeData;
                lightTheme = customThemeData;
            }
        } else {
            darkTheme = StockThemes.getThemeByName(currentTheme, false);
            lightTheme = StockThemes.getThemeByName(currentTheme, true);
        }

        if (!darkTheme || !darkTheme.primary) {
            log.warn("Theme data not available for:", currentTheme);
            return;
        }

        const stockColors = buildMatugenColorsFromTheme(darkTheme, lightTheme);
        const themeData = isLight ? lightTheme : darkTheme;
        setDesiredTheme("hex", themeData.primary, isLight, iconTheme, themeData.matugen_type, stockColors);
    }

    function buildMatugenColorsFromTheme(darkTheme, lightTheme) {
        const colors = {};
        const isLight = SessionData !== "undefined" && SessionData.isLightMode;

        function addColor(matugenKey, darkVal, lightVal) {
            if (!darkVal && !lightVal)
                return;
            colors[matugenKey] = {
                "dark": {
                    "color": String(darkVal || lightVal)
                },
                "light": {
                    "color": String(lightVal || darkVal)
                },
                "default": {
                    "color": String((isLight && lightVal) ? lightVal : darkVal)
                }
            };
        }

        function get(theme, key, fallback) {
            return theme[key] || fallback;
        }

        addColor("primary", darkTheme.primary, lightTheme.primary);
        addColor("on_primary", darkTheme.primaryText, lightTheme.primaryText);
        addColor("primary_container", darkTheme.primaryContainer, lightTheme.primaryContainer);
        addColor("on_primary_container", darkTheme.primaryContainerText || darkTheme.surfaceText, lightTheme.primaryContainerText || lightTheme.surfaceText);
        addColor("secondary", darkTheme.secondary, lightTheme.secondary);
        addColor("on_secondary", darkTheme.secondaryText || darkTheme.primaryText, lightTheme.secondaryText || lightTheme.primaryText);
        addColor("secondary_container", darkTheme.secondaryContainer || darkTheme.surfaceContainerHigh, lightTheme.secondaryContainer || lightTheme.surfaceContainerHigh);
        addColor("on_secondary_container", darkTheme.secondaryContainerText || darkTheme.surfaceText, lightTheme.secondaryContainerText || lightTheme.surfaceText);
        addColor("tertiary", darkTheme.tertiary || darkTheme.secondary, lightTheme.tertiary || lightTheme.secondary);
        addColor("on_tertiary", darkTheme.tertiaryText || darkTheme.secondaryText || darkTheme.primaryText, lightTheme.tertiaryText || lightTheme.secondaryText || lightTheme.primaryText);
        addColor("tertiary_container", darkTheme.tertiaryContainer || darkTheme.secondaryContainer || darkTheme.surfaceContainerHigh, lightTheme.tertiaryContainer || lightTheme.secondaryContainer || lightTheme.surfaceContainerHigh);
        addColor("on_tertiary_container", darkTheme.tertiaryContainerText || darkTheme.surfaceText, lightTheme.tertiaryContainerText || lightTheme.surfaceText);
        addColor("error", darkTheme.error || "#F2B8B5", lightTheme.error || "#B3261E");
        addColor("on_error", darkTheme.errorText || "#601410", lightTheme.errorText || "#FFFFFF");
        addColor("error_container", darkTheme.errorContainer || "#8C1D18", lightTheme.errorContainer || "#F9DEDC");
        addColor("on_error_container", darkTheme.errorContainerText || "#F9DEDC", lightTheme.errorContainerText || "#410E0B");
        addColor("surface", darkTheme.surface, lightTheme.surface);
        addColor("on_surface", darkTheme.surfaceText, lightTheme.surfaceText);
        addColor("surface_variant", darkTheme.surfaceVariant, lightTheme.surfaceVariant);
        addColor("on_surface_variant", darkTheme.surfaceVariantText, lightTheme.surfaceVariantText);
        addColor("surface_tint", darkTheme.surfaceTint, lightTheme.surfaceTint);
        addColor("background", darkTheme.background, lightTheme.background);
        addColor("on_background", darkTheme.backgroundText, lightTheme.backgroundText);
        addColor("outline", darkTheme.outline, lightTheme.outline);
        addColor("outline_variant", darkTheme.outlineVariant || darkTheme.surfaceVariant, lightTheme.outlineVariant || lightTheme.surfaceVariant);
        addColor("surface_container", darkTheme.surfaceContainer, lightTheme.surfaceContainer);
        addColor("surface_container_high", darkTheme.surfaceContainerHigh, lightTheme.surfaceContainerHigh);
        addColor("surface_container_highest", darkTheme.surfaceContainerHighest || darkTheme.surfaceContainerHigh, lightTheme.surfaceContainerHighest || lightTheme.surfaceContainerHigh);
        addColor("surface_container_low", darkTheme.surfaceContainerLow || darkTheme.surface, lightTheme.surfaceContainerLow || lightTheme.surface);
        addColor("surface_container_lowest", darkTheme.surfaceContainerLowest || darkTheme.background, lightTheme.surfaceContainerLowest || lightTheme.background);
        addColor("surface_bright", darkTheme.surfaceBright || darkTheme.surfaceContainerHighest || darkTheme.surfaceContainerHigh, lightTheme.surfaceBright || lightTheme.surface);
        addColor("surface_dim", darkTheme.surfaceDim || darkTheme.background, lightTheme.surfaceDim || lightTheme.surfaceContainer);
        addColor("inverse_surface", darkTheme.inverseSurface || lightTheme.surface, lightTheme.inverseSurface || darkTheme.surface);
        addColor("inverse_on_surface", darkTheme.inverseOnSurface || lightTheme.surfaceText, lightTheme.inverseOnSurface || darkTheme.surfaceText);
        addColor("inverse_primary", darkTheme.inversePrimary || lightTheme.primary, lightTheme.inversePrimary || darkTheme.primary);
        addColor("scrim", darkTheme.scrim || "#000000", lightTheme.scrim || "#000000");
        addColor("shadow", darkTheme.shadow || "#000000", lightTheme.shadow || "#000000");
        addColor("source_color", darkTheme.primary, lightTheme.primary);
        addColor("primary_fixed", darkTheme.primaryFixed || darkTheme.primaryContainer, lightTheme.primaryFixed || lightTheme.primaryContainer);
        addColor("primary_fixed_dim", darkTheme.primaryFixedDim || darkTheme.primary, lightTheme.primaryFixedDim || lightTheme.primary);
        addColor("on_primary_fixed", darkTheme.onPrimaryFixed || darkTheme.primaryText, lightTheme.onPrimaryFixed || lightTheme.primaryText);
        addColor("on_primary_fixed_variant", darkTheme.onPrimaryFixedVariant || darkTheme.primaryText, lightTheme.onPrimaryFixedVariant || lightTheme.primaryText);
        addColor("secondary_fixed", darkTheme.secondaryFixed || darkTheme.secondary, lightTheme.secondaryFixed || lightTheme.secondary);
        addColor("secondary_fixed_dim", darkTheme.secondaryFixedDim || darkTheme.secondary, lightTheme.secondaryFixedDim || lightTheme.secondary);
        addColor("on_secondary_fixed", darkTheme.onSecondaryFixed || darkTheme.primaryText, lightTheme.onSecondaryFixed || lightTheme.primaryText);
        addColor("on_secondary_fixed_variant", darkTheme.onSecondaryFixedVariant || darkTheme.primaryText, lightTheme.onSecondaryFixedVariant || lightTheme.primaryText);
        addColor("tertiary_fixed", darkTheme.tertiaryFixed || darkTheme.tertiary || darkTheme.secondary, lightTheme.tertiaryFixed || lightTheme.tertiary || lightTheme.secondary);
        addColor("tertiary_fixed_dim", darkTheme.tertiaryFixedDim || darkTheme.tertiary || darkTheme.secondary, lightTheme.tertiaryFixedDim || lightTheme.tertiary || lightTheme.secondary);
        addColor("on_tertiary_fixed", darkTheme.onTertiaryFixed || darkTheme.primaryText, lightTheme.onTertiaryFixed || lightTheme.primaryText);
        addColor("on_tertiary_fixed_variant", darkTheme.onTertiaryFixedVariant || darkTheme.primaryText, lightTheme.onTertiaryFixedVariant || lightTheme.primaryText);

        return colors;
    }

    function refreshGtkTheme() {
        const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
        const theme = isLight ? "adw-gtk3" : "adw-gtk3-dark";
        const schema = "org.gnome.desktop.interface";
        const key = "gtk-theme";

        const makeCmd = (tool, schema, val) => {
            if (tool === "gsettings") {
                return `gsettings set ${schema} ${key} '' && gsettings set ${schema} ${key} ${val}`;
            } else {
                const dconfPath = `/${schema.replace(/\./g, "/")}`;
                return `dconf write ${dconfPath}/${key} "''" && dconf write ${dconfPath}/${key} "'${val}'"`;
            }
        };

        Proc.runCommand("gtkRefresher", ["sh", "-c", makeCmd("gsettings", schema, theme)], (output, exitCode) => {
            if (exitCode !== 0) {
                Proc.runCommand("gtkRefreshFallback", ["sh", "-c", makeCmd("dconf", schema, theme)], (output, exitCode) => {
                    if (exitCode !== 0) {
                        log.warn("Failed to refresh gtk-theme");
                    }
                });
            }
        });
    }

    function patchGtk3colors() {
        const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
        Proc.runCommand("gtk3Patcher", ["bash", shellDir + "/scripts/gtk.sh", configDir, "patch", isLight, shellDir], (output, exitCode) => {
            switch (exitCode) {
            case 0:
                refreshGtkTheme();
                break;
            case 2:
                break;
            default:
                log.warn(`Failed to patch GTK3 colors: ${output}`);
            }
        });
    }

    function applyGtkColors() {
        if (!matugenAvailable) {
            if (typeof ToastService !== "undefined") {
                ToastService.showError(I18n.tr("matugen not available or disabled - cannot apply %1 colors").arg("GTK"));
            }
            return;
        }

        const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
        Proc.runCommand("gtkApplier", ["bash", shellDir + "/scripts/gtk.sh", configDir, "apply", isLight, shellDir], (output, exitCode) => {
            if (exitCode === 0) {
                if (typeof ToastService !== "undefined" && !root.matugenToastSuppressed) {
                    ToastService.showInfo(I18n.tr("GTK colors applied successfully"));
                }
            } else {
                if (typeof ToastService !== "undefined") {
                    ToastService.showError(I18n.tr("Failed to apply %1 colors").arg("GTK"));
                }
            }
        });
    }

    function applyQtColors() {
        if (!matugenAvailable) {
            if (typeof ToastService !== "undefined") {
                ToastService.showError(I18n.tr("matugen not available or disabled - cannot apply %1 colors").arg("Qt"));
            }
            return;
        }

        Proc.runCommand("qtApplier", ["bash", shellDir + "/scripts/qt.sh", configDir], (output, exitCode) => {
            if (exitCode === 0) {
                if (typeof ToastService !== "undefined") {
                    ToastService.showInfo(I18n.tr("Qt colors applied successfully"));
                }
            } else {
                if (typeof ToastService !== "undefined") {
                    ToastService.showError(I18n.tr("Failed to apply %1 colors").arg("Qt"));
                }
            }
        });
    }

    function withAlpha(c, a) {
        if (!c || c.r === undefined)
            return Qt.rgba(0, 0, 0, 0);
        return Qt.rgba(c.r, c.g, c.b, a);
    }

    function blendAlpha(c, a) {
        if (!c || c.r === undefined)
            return Qt.rgba(0, 0, 0, 0);
        return Qt.rgba(c.r, c.g, c.b, c.a * a);
    }

    function hoverTint(base) {
        const factor = 1.2;
        return isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
    }

    function blend(c1, c2, r) {
        return Qt.rgba(c1.r * (1 - r) + c2.r * r, c1.g * (1 - r) + c2.g * r, c1.b * (1 - r) + c2.b * r, c1.a * (1 - r) + c2.a * r);
    }

    function getFillMode(modeName) {
        switch (modeName) {
        case "Stretch":
            return Image.Stretch;
        case "Fit":
        case "PreserveAspectFit":
            return Image.PreserveAspectFit;
        case "Fill":
        case "PreserveAspectCrop":
            return Image.PreserveAspectCrop;
        case "Tile":
            return Image.Tile;
        case "TileVertically":
            return Image.TileVertically;
        case "TileHorizontally":
            return Image.TileHorizontally;
        case "Pad":
            return Image.Pad;
        default:
            return Image.PreserveAspectCrop;
        }
    }

    // Returns numeric fillMode value for shader use (matches shader calculateUV logic)
    function getShaderFillMode(modeName) {
        switch (modeName) {
        case "Stretch": return 0;
        case "Fit":
        case "PreserveAspectFit": return 1;
        case "Fill":
        case "PreserveAspectCrop": return 2;
        case "Tile": return 3;
        case "TileVertically": return 4;
        case "TileHorizontally": return 5;
        case "Pad": return 6;
        case "Scrolling": return 7;
        default: return 2;
        }
    }

    function snap(value, dpr) {
        const s = dpr || 1;
        return Math.round(value * s) / s;
    }

    function px(value, dpr) {
        const s = dpr || 1;
        return Math.round(value * s) / s;
    }

    function hairline(dpr) {
        return 1 / (dpr || 1);
    }

    function invertHex(hex) {
        hex = hex.replace('#', '');

        if (!/^[0-9A-Fa-f]{6}$/.test(hex)) {
            return hex;
        }

        const r = parseInt(hex.substr(0, 2), 16);
        const g = parseInt(hex.substr(2, 2), 16);
        const b = parseInt(hex.substr(4, 2), 16);

        const invR = (255 - r).toString(16).padStart(2, '0');
        const invG = (255 - g).toString(16).padStart(2, '0');
        const invB = (255 - b).toString(16).padStart(2, '0');

        return `#${invR}${invG}${invB}`;
    }

    property var baseLogoColor: {
        if (typeof SettingsData === "undefined")
            return "";
        const colorOverride = SettingsData.launcherLogoColorOverride;
        if (!colorOverride || colorOverride === "")
            return "";
        if (colorOverride === "primary")
            return primary;
        if (colorOverride === "surface")
            return surfaceText;
        return colorOverride;
    }

    property var effectiveLogoColor: {
        if (typeof SettingsData === "undefined")
            return "";

        const colorOverride = SettingsData.launcherLogoColorOverride;
        if (!colorOverride || colorOverride === "")
            return "";

        if (colorOverride === "primary")
            return primary;
        if (colorOverride === "surface")
            return surfaceText;

        if (!SettingsData.launcherLogoColorInvertOnMode) {
            return colorOverride;
        }

        if (isLightMode) {
            return invertHex(colorOverride);
        }

        return colorOverride;
    }

    Process {
        id: systemThemeGenerator
        running: false
        stdout: SplitParser {
            onRead: data => log.info("Theme worker:", data)
        }
        stderr: SplitParser {
            onRead: data => log.warn("Theme worker:", data)
        }

        onExited: exitCode => {
            workerRunning = false;
            const currentMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";

            switch (exitCode) {
            case 0:
                log.info("Matugen worker completed successfully");
                root.matugenCompleted(currentMode, "success");
                break;
            case 2:
                log.debug("Matugen worker completed with code 2 (no changes needed)");
                root.matugenCompleted(currentMode, "no-changes");
                break;
            default:
                if (typeof ToastService !== "undefined") {
                    ToastService.showError(I18n.tr("Theme worker failed (%1)").arg(exitCode));
                }
                log.warn("Matugen worker failed with exit code:", exitCode);
                root.matugenCompleted(currentMode, "error");
            }

            if (!pendingThemeRequest) {
                if (SettingsData.matugenTemplateGtk)
                    patchGtk3colors();
                return;
            }

            const req = pendingThemeRequest;
            pendingThemeRequest = null;
            log.info("Processing queued theme request");
            setDesiredTheme(req.kind, req.value, req.isLight, req.iconTheme, req.matugenType, req.stockColors);
        }
    }

    FileView {
        id: customThemeFileView
        blockLoading: false
        watchChanges: currentTheme === "custom"

        function parseAndLoadTheme() {
            try {
                var themeData = JSON.parse(customThemeFileView.text());
                loadCustomTheme(themeData);
            } catch (e) {
                ToastService.showError(I18n.tr("Invalid JSON format: %1").arg(e.message));
            }
        }

        onLoaded: {
            parseAndLoadTheme();
        }

        onFileChanged: {
            customThemeFileView.reload();
        }

        onLoadFailed: function (error) {
            if (typeof ToastService !== "undefined") {
                ToastService.showError(I18n.tr("Failed to read theme file: %1").arg(error));
            }
        }
    }

    FileView {
        id: dynamicColorsFileView
        path: stateDir + "/dms-colors.json"
        blockLoading: false
        watchChanges: true

        function parseAndLoadColors() {
            try {
                const colorsText = dynamicColorsFileView.text();
                if (colorsText) {
                    root.matugenColors = JSON.parse(colorsText);
                    if (typeof ToastService !== "undefined") {
                        ToastService.clearWallpaperError();
                    }
                }
            } catch (e) {
                log.error("Failed to parse dynamic colors:", e);
                if (typeof ToastService !== "undefined") {
                    ToastService.wallpaperErrorStatus = "error";
                    ToastService.showError(I18n.tr("Dynamic colors parse error: %1").arg(e.message));
                }
            }
        }

        onLoaded: {
            _colorsRetryCount = 0;
            if (currentTheme === dynamic)
                colorsFileLoadFailed = false;
            parseAndLoadColors();
        }

        onFileChanged: {
            dynamicColorsFileView.reload();
        }

        onLoadFailed: function (error) {
            if (currentTheme !== dynamic)
                return;

            if (workerRunning) {
                colorsReloadRetry.restart();
                return;
            }

            if (_colorsRetryCount < 3) {
                _colorsRetryCount++;
                colorsReloadRetry.restart();
                return;
            }

            colorsFileLoadFailed = true;
            const stale = Date.now() - _lastGenerateMs > 5000;
            if (matugenAvailable && rawWallpaperPath && stale) {
                log.debug("Dynamic colors unrecoverable, regenerating");
                generateSystemThemesFromCurrentTheme();
            }
        }

        onPathChanged: {
            colorsFileLoadFailed = false;
        }
    }

    Timer {
        id: colorsReloadRetry
        interval: 150
        repeat: false
        onTriggered: dynamicColorsFileView.reload()
    }

    IpcHandler {
        target: "theme"

        function toggle(): string {
            root.toggleLightMode();
            return root.isLightMode ? "dark" : "light";
        }

        function light(): string {
            root.setLightMode(true, true, true);
            return "light";
        }

        function dark(): string {
            root.setLightMode(false, true, true);
            return "dark";
        }

        function getMode(): string {
            return root.isLightMode ? "light" : "dark";
        }
    }

    Timer {
        id: _themeGenerateDebounce
        interval: 100
        repeat: false
        onTriggered: root._executeThemeGeneration()
    }

    // These timers are for screen transitions, since sometimes QML still beats the niri call
    Timer {
        id: themeTransitionTimer
        interval: 50
        repeat: false
        property string themeName: ""
        property bool savePrefs: true
        onTriggered: root.switchTheme(themeName, savePrefs, false)
    }

    Timer {
        id: lightModeTransitionTimer
        interval: 100
        repeat: false
        property bool lightMode: false
        property bool savePrefs: true
        onTriggered: root.setLightMode(lightMode, savePrefs, false)
    }

    Timer {
        id: themeCategoryTransitionTimer
        interval: 50
        repeat: false
        property string category: ""
        property string defaultTheme: ""
        onTriggered: {
            root.currentThemeCategory = category;
            root.switchTheme(defaultTheme, true, false);
        }
    }
}
