function getRandomColor(h, s, l, a) {
    h = h ?? Math.random();
    s = s ?? Math.random();
    l = l ?? Math.random();
    a = a ?? 1.0;
    return Qt.hsla(h, s, l, a);
}

function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function getBgManaged(item) {
    let managed = null;
    if (item?.children) {
        for (let i in item.children) {
            const child = item.children[i];
            if (!child?.luisbocanegraPanelColorizerBgManaged) continue;
            managed = child;
        }
    }
    // console.error(item, "managed:", managed);
    return managed;
}

function getEffectItem(item) {
    let managed = null;
    if (item?.children) {
        for (let i in item.children) {
            const child = item.children[i];
            if (!child?.luisbocanegraPanelColorizerEffectManaged) continue;
            managed = child;
        }
    }
    // console.error(item, "managed:", managed);
    return managed;
}

function findTrayGridView(item) {
    if (!item?.children) return null;
    if (item instanceof GridView) {
        return item;
    }
    for (let i = 0; i < item.children.length; i++) {
        let result = findTrayGridView(item.children[i]);
        if (result) {
            return result;
        }
    }
    return null;
}

function findTrayExpandArrow(item) {
    if (item instanceof GridLayout) {
        for (let i in item.children) {
            const child = item.children[i];
            if (!(child instanceof GridView)) {
                return child;
            }
        }
    }
    return null;
}

function panelOpacity(panelElement, enabled, panelRealBgOpacity) {
    if (!panelElement) return;
    for (let i in panelElement.children) {
        const current = panelElement.children[i];

        if (
            current.imagePath &&
            current.imagePath.toString().includes("panel-background")
        ) {
            current.opacity = enabled ? panelRealBgOpacity : 1;
        }
    }
}

function dumpProps(obj) {
    console.error("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
    console.error(obj);
    for (var k of Object.keys(obj)) {
        const val = obj[k];
        console.log(k + "=" + val + "\n");
    }
}

function toggleTransparency(containmentItem, nativePanelBackgroundEnabled) {
    if (!containmentItem) return;
    containmentItem.Plasmoid.backgroundHints = !nativePanelBackgroundEnabled
        ? PlasmaCore.Types.NoBackground
        : PlasmaCore.Types.DefaultBackground;
}

function findWidgets(panelLayout, panelWidgets) {
    if (!panelLayout) return panelWidgets;
    // console.log("Updating panel widgets list");
    for (let i in panelLayout.children) {
        const child = panelLayout.children[i];
        // name may not be available while gragging into the panel and
        // other situations
        if (!child.applet?.plasmoid?.pluginName) continue;
        // Utils.dumpProps(child.applet.plasmoid)
        const id = child.applet.plasmoid.id;
        const name = child.applet.plasmoid.pluginName;
        const title = child.applet.plasmoid.title;
        const icon = child.applet.plasmoid.icon;
        if (panelWidgets.find((item) => item.id === id)) continue;
        // console.error(name, title, icon)
        panelWidgets.push({
            id: id,
            name: name,
            title: title,
            icon: icon,
            inTray: false,
        });
    }
    return panelWidgets;
}
function findWidgetsTray(grid, panelWidgets) {
    if (!grid) return panelWidgets;
    if (grid instanceof GridView) {
        for (let i = 0; i < grid.count; i++) {
            const item = grid.itemAtIndex(i);
            if (!item) continue;
            for (let j in item.children) {
                if (!item.children[j].model) continue;
                const model = item.children[j].model;
                // in contrast with regular panel applets, an id is not given to notifier items,
                // but since there should be only a single instance of StatusNotifier per app,
                // model.Id _should_ be enough for any sane implementation of tray icon.
                // Also, since plasma 6.5.something (maybe earlier) widgets in the system tray
                // no longer use a consistent applet.plasmoid.id
                // So we set -1 to ignore the value when matching for overrides or color fix
                // App tray icons
                if (model.itemType === "StatusNotifier") {
                    const name = model.Id;
                    const title = model.Title || model.ToolTipTitle;
                    const icon = model.IconName;
                    if (
                        panelWidgets
                            .filter((item) => item.inTray)
                            .find((item) => item.name === name)
                    )
                        continue;
                    // console.error(name, title, icon)
                    panelWidgets.push({
                        id: -1,
                        name: name,
                        title: title,
                        icon: icon,
                        inTray: true,
                    });
                }
                // normal plasmoids in tray
                if (model.itemType === "Plasmoid") {
                    const applet = model.applet ?? null;
                    const name = applet?.plasmoid.pluginName ?? "";
                    const title = applet?.plasmoid.title ?? "";
                    const icon = applet?.plasmoid.icon ?? "";
                    if (
                        panelWidgets
                            .filter((item) => item.inTray)
                            .find((item) => item.name === name)
                    )
                        continue;
                    // console.error(name, title, icon)
                    panelWidgets.push({
                        id: -1,
                        name: name,
                        title: title,
                        icon: icon,
                        inTray: true,
                    });
                }
            }
        }
    }
    // find the expand tray arrow
    if (grid instanceof GridLayout) {
        for (let i in grid.children) {
            const item = grid.children[i];
            if (!(item instanceof GridView)) {
                const name = "org.kde.plasma.systemtray.expand";
                if (panelWidgets.find((item) => item.name === name)) continue;
                const title = item.subText || "Show hidden icons";
                panelWidgets.push({
                    id: -1,
                    name: name,
                    title: title,
                    icon: "arrow-down",
                    inTray: true,
                });
            }
        }
    }
    return panelWidgets;
}

function getSystemTrayState(applet, plasmaVersion) {
    if (!applet) {
        return null;
    }
    let systemTrayState = null;
    if (plasmaVersion.isGreaterThan("6.3.5")) {
        systemTrayState = "systemTrayState" in applet && applet.systemTrayState;
    } else {
        systemTrayState =
            "internalSystray" in applet && applet.internalSystray.systemTrayState;
    }
    return systemTrayState;
}

function getWidgetProperties(
    item,
    pcTypes,
    hovered,
    plasmaVersion,
    inTray,
    panelColorizer,
    logIconChanges,
) {
    let name = "";
    let id = -1;
    let expanded = false;
    let needsAttention = false;
    let busy = false;
    let trayIconHash = "";
    let title = "";
    if (item.applet?.plasmoid?.pluginName) {
        const applet = item.applet;
        name = applet.plasmoid.pluginName;
        title = applet.plasmoid.title;
        id = inTray ? -1 : (applet.plasmoid.id ?? -1);
        if (applet.compactRepresentationItem !== null) {
            expanded = applet.expanded;
        } else {
            expanded = getSystemTrayState(applet, plasmaVersion)?.expanded ?? false;
        }
        needsAttention =
            pcTypes.NeedsAttentionStatus === applet.plasmoid.status ?? null;
        busy = applet.plasmoid.busy;
    } else {
        for (let i in item.children) {
            if (!item?.children[i]?.model) continue;
            const model = item.children[i].model;
            if (model.itemType === "StatusNotifier") {
                name = model.Id;
                title = model.Title || model.ToolTipTitle;
                needsAttention = pcTypes.NeedsAttentionStatus === model.status;
                let iconSource;
                if (model.status === PlasmaCore.Types.NeedsAttentionStatus) {
                    if (model.AttentionIcon) {
                        iconSource = model.AttentionIcon;
                    }
                    if (model.AttentionIconName) {
                        iconSource = model.AttentionIconName;
                    }
                } else {
                    iconSource = model.Icon || model.IconName;
                }
                if (panelColorizer && iconSource) {
                    if (typeof panelColorizer.getIconHash !== "function") {
                        console.error(
                            "getIconHash is not a method of",
                            panelColorizer,
                            "please update/rebuild the plugin using latest source",
                        );
                    } else {
                        try {
                            trayIconHash = panelColorizer.getIconHash(
                                iconSource,
                                logIconChanges,
                            );
                        } catch (e) {
                            console.error(e.message, "\n", e.stack);
                        }
                    }
                }
                break;
            } else if (model.itemType === "Plasmoid") {
                const applet = model.applet;
                name = applet.plasmoid.pluginName ?? "";
                title = applet?.plasmoid.title ?? "";
                id = inTray ? -1 : (applet.plasmoid.id ?? -1);
                expanded = applet.compactRepresentationItem !== null && applet.expanded;
                needsAttention =
                    pcTypes.NeedsAttentionStatus === applet.plasmoid.status;
                busy = applet.plasmoid.busy;
                break;
            }
        }
    }
    return {
        name,
        title,
        id,
        expanded,
        needsAttention,
        busy,
        hovered,
        trayIconHash,
    };
}

var themeScopes = [
    "View",
    "Window",
    "Button",
    "Selection",
    "Tooltip",
    "Complementary",
    "Header",
];

function getWidgetAsocIdx(id, name, config) {
    // console.log("getWidgetAsocIdx()")
    return config.findIndex((item) => item.id == id && item.name == name);
}

function getCustomCfg(widgetName, widgetId, configurationOverrides) {
    if (!widgetId) return null;
    var custom = {};
    configurationOverrides.associations = clearOldWidgetConfig(
        configurationOverrides.associations,
    );
    let asocIndex = getWidgetAsocIdx(
        widgetId,
        widgetName,
        configurationOverrides.associations,
    );
    if (asocIndex !== -1) {
        const overrideNames =
            configurationOverrides.associations[asocIndex].presets;

        for (let overrideName of overrideNames) {
            if (!(overrideName in configurationOverrides.overrides)) continue;
            const current = configurationOverrides.overrides[overrideName];
            custom = getEffectiveSettings(current, custom);
        }
    }
    if (Object.keys(custom).length !== 0) {
        return custom;
    }
    return null;
}

function getGlobalSettings(itemType) {
    let globalSettings = {};
    if (itemType === Enums.ItemType.PanelBgItem) {
        globalSettings = panelSettings;
    } else if (
        itemType === Enums.ItemType.TrayItem ||
        itemType === Enums.ItemType.TrayArrow
    ) {
        globalSettings = trayWidgetSettings;
    } else {
        globalSettings = widgetSettings;
    }
    return globalSettings;
}

function getEffectiveSettings(customSettings, globalSettings) {
    let effectiveSettings = JSON.parse(JSON.stringify(customSettings));
    for (var key in customSettings) {
        if (
            typeof customSettings[key] === "object" &&
            customSettings[key] !== null &&
            globalSettings.hasOwnProperty(key)
        ) {
            effectiveSettings[key] = getEffectiveSettings(
                customSettings[key],
                globalSettings[key],
            );
        }
        if (customSettings[key].hasOwnProperty("enabled")) {
            if (!customSettings[key].enabled && globalSettings.hasOwnProperty(key)) {
                effectiveSettings[key] = globalSettings[key];
            }
        }
    }
    return effectiveSettings;
}

function getElementStateConfig(config, state) {
    if (!config) {
        return null;
    }
    let stateConfig = config.normal;

    if (state.busy && config.busy?.enabled) {
        stateConfig = Utils.getEffectiveSettings(config.busy, stateConfig);
    }

    if (state.needsAttention && config.needsAttention?.enabled) {
        stateConfig = Utils.getEffectiveSettings(
            config.needsAttention,
            stateConfig,
        );
    }

    if (state.hovered && config.hovered?.enabled) {
        stateConfig = Utils.getEffectiveSettings(config.hovered, stateConfig);
    }

    if (state.expanded && config.expanded?.enabled) {
        stateConfig = Utils.getEffectiveSettings(config.expanded, stateConfig);
    }
    return stateConfig;
}

function getItemCfg(
    itemType,
    widgetName,
    widgetId,
    config,
    globalOverrides,
    busy,
    needsAttention,
    hovered,
    expanded,
) {
    let output = { override: false };
    let state = { busy, needsAttention, hovered, expanded };
    let globalOverrideConfig = getCustomCfg(
        widgetName,
        widgetId,
        globalOverrides,
    );
    let globalOverride = getElementStateConfig(globalOverrideConfig, state);
    let presetOverride = getElementStateConfig(
        getCustomCfg(widgetName, widgetId, config.configurationOverrides),
        state,
    );

    output.settings = getElementStateConfig(getGlobalSettings(itemType), state);

    if (presetOverride) {
        output.settings = getEffectiveSettings(presetOverride, output.settings);
    }

    if (globalOverride) {
        if (globalOverrideConfig.disabledFallback) {
            output.settings = getEffectiveSettings(globalOverride, output.settings);
        } else {
            output.settings = globalOverride;
        }
    }
    return output;
}

function scaleSaturation(color, saturation) {
    return Qt.hsla(color.hslHue, saturation, color.hslLightness, color.a);
}

function scaleLightness(color, lightness) {
    return Qt.hsla(color.hslHue, color.hslSaturation, lightness, color.a);
}

function hexToRgb(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result
        ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16),
        }
        : null;
}

function rgbToQtColor(rgb) {
    return Qt.rgba(rgb.r / 255, rgb.g / 255, rgb.b / 255, 1);
}

function fixArraysAsObjects(config) {
    for (var key in config) {
        // skip overrides because they are intentionally saved as key-value and
        // may be empty or intentionally named with numbers by the user
        if (key === "overrides") {
            continue;
        }
        if (typeof config[key] === "object" && config[key] !== null) {
            if (!Array.isArray(config[key])) {
                const isArrayInDisguise = Object.keys(config[key]).every(
                    (k) => !isNaN(Number(k)),
                );
                if (isArrayInDisguise) {
                    config[key] = Object.values(config[key]);
                } else {
                    fixArraysAsObjects(config[key]);
                }
            } else {
                fixArraysAsObjects(config[key]);
            }
        }
    }
}

function mergeConfigs(sourceConfig, newConfig) {
    fixArraysAsObjects(newConfig);
    for (var key in sourceConfig) {
        if (Array.isArray(sourceConfig[key])) {
            if (!newConfig.hasOwnProperty(key)) {
                newConfig[key] = sourceConfig[key].slice();
            }
        } else if (
            typeof sourceConfig[key] === "object" &&
            sourceConfig[key] !== null
        ) {
            if (!newConfig.hasOwnProperty(key)) {
                newConfig[key] = {};
            }
            mergeConfigs(sourceConfig[key], newConfig[key]);
        } else {
            if (!newConfig.hasOwnProperty(key)) {
                newConfig[key] = sourceConfig[key];
            }
        }
    }
    return newConfig;
}

function stringify(config) {
    return JSON.stringify(config, null, null);
}

function loadPreset(presetContent, item, ignoredConfigs, defaults, store) {
    for (let key in presetContent) {
        let val = presetContent[key];
        const cfgKey = "cfg_" + key;
        if (
            ignoredConfigs.some(function (k) {
                return key.includes(k);
            })
        )
            continue;
        if (key === "globalSettings") {
            val = val;
        }
        const valStr = stringify(val);
        if (store) {
            if (item[key]) item[key] = valStr;
        } else {
            if (item[cfgKey]) item[cfgKey] = valStr;
        }
        // }
    }
}

function getPresetName(panelState, presetAutoloading) {
    if (presetAutoloading.hasOwnProperty("enabled") && !presetAutoloading.enabled)
        return null;
    // loop until we find a the currently active 'true' panel state with a configured preset
    // normal is our fallback so does not need active state
    const priority = [
        "fullscreenWindow",
        "maximized",
        "touchingWindow",
        "activeWindow",
        "visibleWindows",
        "floating",
        "activity",
        "normal",
    ];
    let preset = null;
    for (let state of priority) {
        if ((panelState[state] || state === "normal") && presetAutoloading[state]) {
            if (state === "activity") {
                preset = presetAutoloading.activity[panelState[state]];
            } else {
                preset = presetAutoloading[state];
            }
            if (preset) break;
        }
    }
    return preset || presetAutoloading.normal;
}

function getGlobalPosition(rect, panelElement) {
    return rect.mapToItem(panelElement, 0, 0, rect.width, rect.height);
}

function setPanelModeScript(panelId, panelSettings) {
    var setPanelModeScript = `
var panel = panelById(${panelId});
if (${panelSettings.visibility.enabled}) {
  panel.hiding = "${panelSettings.visibility.value}"
}
if (${panelSettings.thickness.enabled}) {
  panel.height = ${panelSettings.thickness.value}
}
if (${panelSettings.lengthMode.enabled}) {
  panel.lengthMode = "${panelSettings.lengthMode.value}"
}
if (${panelSettings.position.enabled}) {
  panel.location = "${panelSettings.position.value}"
}
if (${panelSettings.floating.enabled}) {
  panel.floating = ${panelSettings.floating.value}
}
if (${panelSettings.alignment.enabled}) {
  panel.alignment = "${panelSettings.alignment.value}"
}
if (${panelSettings.opacity.enabled}) {
  panel.opacity = "${panelSettings.opacity.value}"
}
if (${panelSettings.screen.enabled}) {
  panel.screen = ${panelSettings.screen.value}
}
`;
    return setPanelModeScript;
}

function getForceFgWidgetConfig(id, name, config) {
    return config.find((item) => item.id == id && item.name == name);
}

function clearOldWidgetConfig(config) {
    if (Array.isArray(config)) {
        return config;
    } else return [];
}

function getWidgetConfigIdx(id, name, config) {
    // console.log("getWidgetConfigIdx()")
    return config.findIndex((item) => item.id == id && item.name == name);
}

function makeEven(n) {
    return n - (n % 2);
}

function parseValue(rawValue) {
    try {
        return JSON.parse(rawValue);
    } catch (e) {
        if (rawValue.toLowerCase() === "true") {
            return true;
        }
        if (rawValue.toLowerCase() === "false") {
            return false;
        }
        if (!isNaN(rawValue)) {
            return Number(rawValue);
        }

        return rawValue;
    }
}

/**
 * Edit an existing object property using dot and square brackets notation
 * Overrides objects if the new value is also an object
 * For array allows setting per index (appending if not consecutive) or replacing with a new array
 * @param {Object} object - The object to set the property on.
 * @param {string} path - The path to the property.
 * @param {string} value - The value to set.
 */
function editProperty(object, path, value) {
    console.log(`editing property path: '${path}' value: '${value}'`);
    value = parseValue(value);
    const keys = path.replace(/\[/g, ".").replace(/\]/g, "").split(".");
    let current = object;

    for (let i = 0; i < keys.length - 1; i++) {
        const key = keys[i];
        if (!current.hasOwnProperty(key)) return;
        current = current[key];
        if (typeof current !== "object" || current === null) return;
    }

    const lastKey = keys[keys.length - 1];

    // no new keys unless it's an array
    if (!current.hasOwnProperty(lastKey) && !Array.isArray(current)) return;

    if (Array.isArray(current)) {
        const index = parseInt(lastKey, 10);
        // add to array if it's the next index
        if (index === current.length) {
            current.push(value);
        } else if (index < current.length) {
            current[index] = value;
        } else if (Array.isArray(value)) {
            current[lastKey] = value;
        } else {
            return;
        }
    } else if (
        typeof current[lastKey] === "object" &&
        current[lastKey] !== null
    ) {
        // override only if the new value is an object
        if (typeof value === "object" && value !== null) {
            current[lastKey] = value;
        } else {
            return;
        }
    } else {
        current[lastKey] = value;
    }
}

// https://stackoverflow.com/questions/28507619/how-to-create-delay-function-in-qml
function delay(interval, callback, parentItem) {
    let timer = Qt.createQmlObject("import QtQuick; Timer {}", parentItem);
    timer.interval = interval;
    timer.repeat = false;
    timer.triggered.connect(callback);
    timer.triggered.connect(function release() {
        timer.triggered.disconnect(callback);
        timer.triggered.disconnect(release);
        timer.destroy();
    });
    timer.start();
}

function fixElementSettingsV3(elementSettings) {
    for (let key of ["panel", "widgets", "trayWidgets"]) {
        if (!("key" in elementSettings)) {
            continue;
        }
        const current = elementSettings[key];
        if ("normal" in current) {
            continue;
        }
        elementSettings[key] = {};
        elementSettings[key].normal = current;
    }
}

function fixConfigurationOverridesV3(configurationOverrides) {
    if (!configurationOverrides) {
        return;
    }
    if (Object.keys(configurationOverrides)) {
        Object.keys(configurationOverrides).forEach((key) => {
            if (!("normal" in configurationOverrides[key])) {
                const current = configurationOverrides[key];
                configurationOverrides[key] = {};
                configurationOverrides[key].normal = current;
                configurationOverrides[key].disabledFallback =
                    current.disabledFallback ?? true;
            }
        });
    }
}

function fixGlobalSettingsV3(globalSettings) {
    if (!globalSettings || !Object.keys.length) {
        return;
    }
    fixElementSettingsV3(globalSettings);
    let floatingDialogs = false;
    if (
        globalSettings?.panel?.normal?.floatingDialogs !== undefined &&
        "floatingDialogs" in globalSettings.panel.normal.floatingDialogs
    ) {
        floatingDialogs = globalSettings.panel.normal.floatingDialogs;
        delete globalSettings.panel.normal.floatingDialogs;
    }
    let nativePanelBackground = true;
    let nativePanelOpacity = 1.0;
    let nativePanelShadow = true;
    if (
        "nativePanelBackground" in globalSettings &&
        !("nativePanel" in globalSettings)
    ) {
        nativePanelBackground =
            globalSettings.nativePanelBackground?.enabled ?? true;
        nativePanelOpacity = globalSettings.nativePanelBackground?.opacity ?? 1.0;
        nativePanelShadow = globalSettings.nativePanelBackground?.shadow ?? true;
        globalSettings.nativePanel = {
            background: {},
        };
        globalSettings.nativePanel.background.enabled = nativePanelBackground;
        globalSettings.nativePanel.background.opacity = nativePanelOpacity;
        globalSettings.nativePanel.background.shadow = nativePanelShadow;
        globalSettings.nativePanel.floatingDialogs = floatingDialogs;
    }
    fixConfigurationOverridesV3(
        globalSettings.configurationOverrides?.overrides ?? null,
    );
}

// a rudimentary way to parse gdbus GVariant into a valid js object
function parseGVariant(str) {
    str = gVariantTupleToArray(str);
    str = str
        .trim()
        .replace(/^\([']?/, "") // remove starting ( or ('
        .replace(/[']?[,]?\)$/, ""); // remove ending ,) or ',)

    // remove GVariant typing thingy e.g <(uint32 ...,)> or <@as ...> <...> <[...]>
    str = str
        .replace(/<[\(]?\s*(.+?)[,]?\s*[\)]?>/g, "$1")
        .replace(/@as |uint32 /g, "");

    if (str === "") return "";
    if (str === "true") return true;
    if (str === "false") return false;
    if (str === "null") return null;
    if (/^-?\d+(\.\d+)?$/.test(str)) return Number(str);

    // try to parse as array or dictionary
    if (/^[\[]?[\{]?.*[\]]?[\}]?$/.test(str)) {
        try {
            return JSON.parse(str.replace(/'null'/g, "null").replace(/'/g, '"'));
        } catch (e) {
            return str;
        }
    }
    return str.replace(/^['"]|['"]$/g, "").trim();
}

// convert GVariant tuples to arrays
function gVariantTupleToArray(str) {
    // convert all tuples like (..., ...) arrays [..., ...]
    return str.replace(/\(([^()]+?)\)/g, (_, inner) => {
        // only replace if it's NOT inside a JSON-style key or between quotes
        if (/^[^:][^']+,[^:][^']+$/.test(inner)) {
            return `[${inner}]`;
        }
        return `(${inner})`;
    });
}

function showWidgets(panelLayout, backgroundComponent, Plasmoid) {
    if (!panelLayout) return;
    // console.log("showWidgets()")
    for (var i in panelLayout.children) {
        const child = panelLayout.children[i];
        // name may not be available while dragging into the panel and
        // other situations
        if (!child.applet?.plasmoid?.pluginName) continue;
        const isTray =
            child.applet.plasmoid.pluginName === "org.kde.plasma.systemtray";
        const bgItem = getBgManaged(child);
        if (!bgItem) {
            const comp = backgroundComponent.createObject(child, {
                z: -1,
                target: child,
                itemType: Enums.ItemType.WidgetItem,
                targetIndex: i,
            });
            if (isTray) trayWidgetBgItem = comp;
        } else {
            bgItem.targetIndex = i;
        }
    }
}

function showTrayAreas(trayGridView, backgroundComponent) {
    if (trayGridView instanceof GridView) {
        let index = 0;
        for (let i = 0; i < trayGridView.count; i++) {
            const item = trayGridView.itemAtIndex(i);
            if (!item.visible) continue;
            const bgItem = getBgManaged(item);
            if (!bgItem) {
                backgroundComponent.createObject(item, {
                    z: -1,
                    target: item,
                    itemType: Enums.ItemType.TrayItem,
                    targetIndex: index,
                });
            } else {
                bgItem.targetIndex = index;
            }
            if (item.visible) {
                index++;
            }
        }

        for (let i in trayGridView.parent.children) {
            const item = trayGridView.parent.children[i];
            if (!(item instanceof GridView)) {
                if (!item.visible) continue;
                const bgItem = getBgManaged(item);
                if (!bgItem) {
                    backgroundComponent.createObject(item, {
                        z: -1,
                        target: item,
                        itemType: Enums.ItemType.TrayArrow,
                        targetIndex: index,
                    });
                } else {
                    bgItem.targetIndex = index;
                }
            }
        }
    }
}

function getColor(
    colorCfg,
    targetIndex,
    parentColor,
    itemType,
    kirigamiColorItem,
) {
    let newColor = "transparent";
    switch (colorCfg.sourceType) {
        case 0:
            newColor = rgbToQtColor(hexToRgb(colorCfg.custom));
            break;
        case 1:
            newColor = kirigamiColorItem.Kirigami.Theme[colorCfg.systemColor];
            break;
        case 2:
            const nextIndex = targetIndex % colorCfg.list.length;
            newColor = rgbToQtColor(hexToRgb(colorCfg.list[nextIndex]));
            break;
        case 3:
            newColor = getRandomColor();
            break;
        case 4:
            if (colorCfg.followColor === 0) {
                newColor = panelBgItem.color;
            } else if (colorCfg.followColor === 1) {
                newColor =
                    itemType === Enums.ItemType.TrayItem ||
                        itemType === Enums.ItemType.TrayArrow
                        ? trayWidgetBgItem.color
                        : parentColor;
            } else if (colorCfg.followColor === 2) {
                newColor = parentColor;
            }

            break;
        default:
            newColor = Qt.hsla(0, 0, 0, 0);
    }
    if (colorCfg.saturationEnabled) {
        newColor = scaleSaturation(newColor, colorCfg.saturationValue);
    }
    if (colorCfg.lightnessEnabled) {
        newColor = scaleLightness(newColor, colorCfg.lightnessValue);
    }
    newColor = Qt.hsla(
        newColor.hslHue,
        newColor.hslSaturation,
        newColor.hslLightness,
        colorCfg.alpha,
    );
    return newColor;
}

function applyFgColor(
    target,
    newColor,
    fgColorCfg,
    depth,
    wRecolorCfg,
    fgColorModified,
    fontCfg,
) {
    let count = 0;
    let maxDepth = depth;
    const forceMask = wRecolorCfg?.method?.mask ?? false;
    const forceEffect = wRecolorCfg?.method?.multiEffect ?? false;

    for (var i = 0; i < target.visibleChildren.length; i++) {
        var child = target.visibleChildren[i];
        let targetTypes = [Text, ToolButton, Label, Canvas, Kirigami.Icon];
        if (
            targetTypes.some(function (type) {
                return child instanceof type;
            })
        ) {
            // before trying to apply the foreground color, we need to know if we
            // have changed it in the first place, otherwise we have no easy way to
            // restore the original binding for widgets that do dynamic color
            // requires restarting plasma but is better than nothing
            if (fgColorModified && "color" in child) {
                child.color = newColor;
            }
            if (child.Kirigami?.Theme) {
                child.Kirigami.Theme.textColor = newColor;
                child.Kirigami.Theme.colorSet =
                    main.Kirigami.Theme[fgColorCfg.systemColorSet];
            }

            if ("isMask" in child && forceMask) {
                child.isMask = true;
            }

            if (child instanceof Text) {
                if (fontCfg && fontCfg.enabled) {
                    if (fontCfg.font.familyOverride) {
                        child.font.family = fontCfg.font.family;
                    }
                    if (fontCfg.font.italicOverride) {
                        child.font.italic = fontCfg.font.italic;
                    }
                    if (fontCfg.font.underlineOverride) {
                        child.font.underline = fontCfg.font.underline;
                    }
                    if (fontCfg.font.weightOverride) {
                        child.font.weight = fontCfg.font.weight;
                    }
                    if (fontCfg.font.pointSizeOverride) {
                        child.font.pixelSize = pointToPixel(fontCfg.font.pointSize);
                    }
                }
            }

            if (
                forceEffect &&
                [Canvas, Kirigami.Icon].some(function (type) {
                    return child instanceof type;
                })
            ) {
                const effectItem = getEffectItem(child.parent);
                if (!effectItem) {
                    main.colorEffectComponent.createObject(child.parent, {
                        target: child,
                        colorizationColor: newColor,
                    });
                } else {
                    effectItem.source = null;
                    effectItem.colorizationColor = newColor;
                    effectItem.source = child;
                }
            }
            count++;
            if (debug) {
                main.repaintDebugComponent.createObject(child);
            }
        }
        if (child.visibleChildren?.length ?? 0 > 0) {
            const result = applyFgColor(
                child,
                newColor,
                fgColorCfg,
                depth + 1,
                wRecolorCfg,
                fgColorModified,
                fontCfg,
            );
            count += result.count;
            if (result.depth > maxDepth) {
                maxDepth = result.depth;
            }
        }
    }
    return {
        count: count,
        depth: maxDepth,
    };
}

/**
 * Finds and returns an icon from the replacement rules if it matches the tray item properties
 * @param {Array} rules Replacement rules
 * @param {Object} trayItemProperties Tray item properties
 * @returns {String} Icon from rule or empty string
 */
function getTrayIconFromRules(rules, trayItemProperties) {
    const filtered = rules.filter((rule) => {
        return (
            rule.enabled && rule.hash !== "" && rule.match !== "" && rule.icon !== ""
        );
    });
    return (
        filtered.find((rule) => {
            const match = rule.hash || rule.match;
            if (match === trayItemProperties.trayIconHash) {
                return true;
            }
            const re = new RegExp(match);
            return (
                re.test(trayItemProperties.title) || re.test(trayItemProperties.name)
            );
        })?.icon ?? ""
    );
}


// https://invent.kde.org/plasma/plasma-workspace/-/blob/d5ca4251661b5adfdb9b87a4c932ed6971376cbe/applets/digital-clock/DigitalClock.qml#L133
function pointToPixel(pointSize) {
    return Math.round(pointSize / 72 * main.pixelsPerInch);
}

/**
 * Calculates types of each widget to create islands
 * @param {GridLayout} panelLayout - The panel's widgets layout
 * @param {Set<int>} noBgTracker - IDs of widgets with background disabled or blacklisted
 * @param {Set<int>} hiddenTracker - IDs of widgets that are currently hidden
 * @param {string} islandSeparatorWidget - Widget used as island separator
 * @param {boolean} islandsEnabled - Enable/disable island functionality
 * @param {boolean} islandSeparatorPairing - Treat separators as pairs (start and end) instead of sharing the separator between two islands
 * @returns {Array<{id: int, name: string, type: Enum.IslandSectionType}>} Widgets with their corresponding island section type
 */
function updateIslandWidgetTypes(panelLayout, noBgTracker, hiddenTracker, islandSeparatorWidget, islandsEnabled, islandSeparatorPairing) {
    let output = [];
    if (!panelLayout || !islandsEnabled) {
        return output;
    }

    const SectionType = Enum.IslandSectionType;

    const items = [];
    let totalSeparators = 0;

    for (let i = 0; i < panelLayout.children.length; i++) {
        const current = panelLayout.children[i];
        if (!current.applet?.plasmoid?.pluginName)
            continue;

        const currentName = current.applet.plasmoid.pluginName;
        const currentId = current.applet.plasmoid.id;
        const currentIsHidden = hiddenTracker.has(currentId);
        const currentIsBlank = noBgTracker.has(currentId) && !currentIsHidden;
        const currentIsSeparator = currentName === islandSeparatorWidget;

        if (currentIsSeparator) {
            totalSeparators++;
            items.push({
                type: "separator",
                id: currentId,
                name: currentName
            });
        } else if (currentIsBlank) {
            items.push({
                type: "blank",
                id: currentId,
                name: currentName
            });
        } else if (currentIsHidden) {
            items.push({
                type: "hidden",
                id: currentId,
                name: currentName
            });
        } else {
            items.push({
                type: "widget",
                id: currentId,
                name: currentName
            });
        }
    }

    function pushWidget(item, type) {
        output.push({
            id: item.id,
            name: item.name,
            type: type,
        });
    }

    // Pass 1: split into separator-delimited groups and mark active groups.
    const groups = [];
    let separatorsSeen = 0;
    let groupStart = 0;

    function pushGroup(end) {
        if (groupStart > end) {
            return;
        }
        const active = !islandSeparatorPairing
            ? true
            : (separatorsSeen % 2 === 1 && separatorsSeen < totalSeparators);
        groups.push({
            start: groupStart,
            end: end,
            active: active,
        });
    }

    for (let i = 0; i < items.length; i++) {
        if (items[i].type !== "separator") {
            continue;
        }

        pushGroup(i - 1);
        separatorsSeen++;
        groupStart = i + 1;
    }

    // Flush trailing non-separator group.
    pushGroup(items.length - 1);

    function pushWidgetChunk(start, end) {
        let firstWidgetIndex = -1;
        let lastWidgetIndex = -1;
        let widgetCount = 0;

        for (let i = start; i <= end; i++) {
            if (items[i].type === "widget") {
                if (firstWidgetIndex === -1) {
                    firstWidgetIndex = i;
                }
                lastWidgetIndex = i;
                widgetCount++;
            }
        }

        for (let i = start; i <= end; i++) {
            const current = items[i];
            if (current.type !== "widget" || widgetCount < 2) {
                pushWidget(current, SectionType.Default);
                continue;
            }

            if (i === firstWidgetIndex) {
                pushWidget(current, SectionType.Start);
            } else if (i === lastWidgetIndex) {
                pushWidget(current, SectionType.End);
            } else {
                pushWidget(current, SectionType.Middle);
            }
        }
    }

    // Pass 2: assign section type within each active group, splitting on blanks.
    for (const group of groups) {
        if (!group.active) {
            for (let i = group.start; i <= group.end; i++) {
                pushWidget(items[i], SectionType.Default);
            }
            continue;
        }

        let chunkStart = group.start;
        for (let i = group.start; i <= group.end + 1; i++) {
            const atGroupEnd = i === group.end + 1;
            const atBlank = !atGroupEnd && items[i].type === "blank";
            if (!atGroupEnd && !atBlank) {
                continue;
            }

            if (chunkStart <= i - 1) {
                pushWidgetChunk(chunkStart, i - 1);
            }
            if (atBlank) {
                pushWidget(items[i], SectionType.Default);
            }
            chunkStart = i + 1;
        }
    }

    return output;
}
