#include "WindowRuleApplicator.hpp"
#include "WindowRule.hpp"
#include "../Engine.hpp"
#include "../utils/SetUtils.hpp"
#include "../../view/Window.hpp"
#include "../../types/OverridableVar.hpp"
#include "../../../event/EventBus.hpp"

#include <hyprutils/string/String.hpp>
#include <hyprutils/string/VarList.hpp>
#include <hyprutils/string/VarList2.hpp>
#include <tuple>

using namespace Hyprutils::String;

using namespace Desktop;
using namespace Desktop::Rule;

namespace {
    template <typename T, typename TEffect>
    void resetRuleProp(std::pair<Desktop::Types::COverridableVar<T>, std::underlying_type_t<Desktop::Rule::eRuleProperty>>& prop,
                       std::underlying_type_t<Desktop::Rule::eRuleProperty> props, Desktop::Types::eOverridePriority prio,
                       std::unordered_set<Desktop::Rule::CWindowRuleEffectContainer::storageType>& effectsNuked, TEffect&& effect) {
        auto& [value, propMask] = prop;

        if (!(propMask & props))
            return;

        if (prio == Desktop::Types::PRIORITY_WINDOW_RULE) {
            effectsNuked.emplace(effect());
            propMask &= ~props;
        }

        value.unset(prio);
    }
}

CWindowRuleApplicator::CWindowRuleApplicator(PHLWINDOW w) : m_window(w) {
    ;
}

std::unordered_set<CWindowRuleEffectContainer::storageType> CWindowRuleApplicator::resetProps(std::underlying_type_t<eRuleProperty> props, Types::eOverridePriority prio) {
    std::unordered_set<CWindowRuleEffectContainer::storageType> effectsNuked;

    std::apply([&](auto&&... prop) { (resetRuleProp(prop.first.get(), props, prio, effectsNuked, prop.second), ...); },
               std::make_tuple(
                   std::pair{std::ref(m_alpha), [this] { return alphaEffect(); }}, std::pair{std::ref(m_alphaInactive), [this] { return alphaInactiveEffect(); }},
                   std::pair{std::ref(m_alphaFullscreen), [this] { return alphaFullscreenEffect(); }}, std::pair{std::ref(m_allowsInput), [this] { return allowsInputEffect(); }},
                   std::pair{std::ref(m_decorate), [this] { return decorateEffect(); }}, std::pair{std::ref(m_focusOnActivate), [this] { return focusOnActivateEffect(); }},
                   std::pair{std::ref(m_keepAspectRatio), [this] { return keepAspectRatioEffect(); }},
                   std::pair{std::ref(m_nearestNeighbor), [this] { return nearestNeighborEffect(); }}, std::pair{std::ref(m_noAnim), [this] { return noAnimEffect(); }},
                   std::pair{std::ref(m_noBlur), [this] { return noBlurEffect(); }}, std::pair{std::ref(m_noDim), [this] { return noDimEffect(); }},
                   std::pair{std::ref(m_noFocus), [this] { return noFocusEffect(); }}, std::pair{std::ref(m_noMaxSize), [this] { return noMaxSizeEffect(); }},
                   std::pair{std::ref(m_noShadow), [this] { return noShadowEffect(); }}, std::pair{std::ref(m_noShortcutsInhibit), [this] { return noShortcutsInhibitEffect(); }},
                   std::pair{std::ref(m_opaque), [this] { return opaqueEffect(); }}, std::pair{std::ref(m_dimAround), [this] { return dimAroundEffect(); }},
                   std::pair{std::ref(m_RGBX), [this] { return RGBXEffect(); }}, std::pair{std::ref(m_syncFullscreen), [this] { return syncFullscreenEffect(); }},
                   std::pair{std::ref(m_tearing), [this] { return tearingEffect(); }}, std::pair{std::ref(m_xray), [this] { return xrayEffect(); }},
                   std::pair{std::ref(m_renderUnfocused), [this] { return renderUnfocusedEffect(); }},
                   std::pair{std::ref(m_noFollowMouse), [this] { return noFollowMouseEffect(); }}, std::pair{std::ref(m_noScreenShare), [this] { return noScreenShareEffect(); }},
                   std::pair{std::ref(m_noVRR), [this] { return noVRREffect(); }}, std::pair{std::ref(m_persistentSize), [this] { return persistentSizeEffect(); }},
                   std::pair{std::ref(m_stayFocused), [this] { return stayFocusedEffect(); }}, std::pair{std::ref(m_idleInhibitMode), [this] { return idleInhibitModeEffect(); }},
                   std::pair{std::ref(m_borderSize), [this] { return borderSizeEffect(); }}, std::pair{std::ref(m_rounding), [this] { return roundingEffect(); }},
                   std::pair{std::ref(m_roundingPower), [this] { return roundingPowerEffect(); }}, std::pair{std::ref(m_scrollMouse), [this] { return scrollMouseEffect(); }},
                   std::pair{std::ref(m_scrollTouchpad), [this] { return scrollTouchpadEffect(); }},
                   std::pair{std::ref(m_animationStyle), [this] { return animationStyleEffect(); }}, std::pair{std::ref(m_maxSize), [this] { return maxSizeEffect(); }},
                   std::pair{std::ref(m_minSize), [this] { return minSizeEffect(); }}, std::pair{std::ref(m_activeBorderColor), [this] { return activeBorderColorEffect(); }},
                   std::pair{std::ref(m_inactiveBorderColor), [this] { return inactiveBorderColorEffect(); }}));

    if (prio == Types::PRIORITY_WINDOW_RULE) {
        std::erase_if(m_dynamicTags, [props, this](const auto& el) {
            const bool REMOVE = el.second & props;

            if (REMOVE)
                m_tagKeeper.removeDynamicTag(el.first);

            return REMOVE;
        });

        std::erase_if(m_otherProps.props, [props](const auto& el) { return !el.second || el.second->propMask & props; });
    }

    return effectsNuked;
}

CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const SP<CWindowRule>& rule) {
    SRuleResult result;

    for (const auto& [key, effect] : rule->effects()) {
        switch (key) {
            default: {
                if (key <= WINDOW_RULE_EFFECT_LAST_STATIC) {
                    Log::logger->log(Log::TRACE, "CWindowRuleApplicator::applyDynamicRule: Skipping effect {}, not dynamic", sc<std::underlying_type_t<eWindowRuleEffect>>(key));
                    break;
                }

                // custom type, add to our vec
                if (!m_otherProps.props.contains(key)) {
                    m_otherProps.props.emplace(key,
                                               makeUnique<SCustomPropContainer>(SCustomPropContainer{
                                                   .idx      = key,
                                                   .propMask = rule->getPropertiesMask(),
                                                   .effect   = effect,
                                               }));
                } else {
                    auto& e = m_otherProps.props[key];
                    e->propMask |= rule->getPropertiesMask();
                    e->effect = effect;
                }

                break;
            }

            case WINDOW_RULE_EFFECT_NONE: {
                Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: BUG THIS: WINDOW_RULE_EFFECT_NONE??");
                break;
            }
            case WINDOW_RULE_EFFECT_ROUNDING: {
                try {
                    m_rounding.first.set(std::stoull(effect), Types::PRIORITY_WINDOW_RULE);
                    m_rounding.second |= rule->getPropertiesMask();
                } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid rounding {}", effect); }
                break;
            }
            case WINDOW_RULE_EFFECT_ROUNDING_POWER: {
                try {
                    m_roundingPower.first.set(std::clamp(std::stof(effect), 1.F, 10.F), Types::PRIORITY_WINDOW_RULE);
                    m_roundingPower.second |= rule->getPropertiesMask();
                } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid rounding_power {}", effect); }
                break;
            }
            case WINDOW_RULE_EFFECT_PERSISTENT_SIZE: {
                m_persistentSize.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_persistentSize.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_ANIMATION: {
                m_animationStyle.first.set(effect, Types::PRIORITY_WINDOW_RULE);
                m_animationStyle.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_BORDER_COLOR: {
                try {
                    // Each vector will only get used if it has at least one color
                    Config::CGradientValueData activeBorderGradient   = {};
                    Config::CGradientValueData inactiveBorderGradient = {};
                    bool                       active                 = true;
                    CVarList                   colorsAndAngles        = CVarList(trim(effect), 0, 's', true);

                    // Basic form has only two colors, everything else can be parsed as a gradient
                    if (colorsAndAngles.size() == 2 && !colorsAndAngles[1].contains("deg")) {
                        m_activeBorderColor.first =
                            Types::COverridableVar(Config::CGradientValueData(CHyprColor(configStringToInt(colorsAndAngles[0]).value_or(0))), Types::PRIORITY_WINDOW_RULE);
                        m_inactiveBorderColor.first =
                            Types::COverridableVar(Config::CGradientValueData(CHyprColor(configStringToInt(colorsAndAngles[1]).value_or(0))), Types::PRIORITY_WINDOW_RULE);
                        m_activeBorderColor.second |= rule->getPropertiesMask();
                        m_inactiveBorderColor.second |= rule->getPropertiesMask();
                        break;
                    }

                    for (auto const& token : colorsAndAngles) {
                        // The first angle, or an explicit "0deg", splits the two gradients
                        if (active && token.contains("deg")) {
                            activeBorderGradient.m_angle = std::stoi(token.substr(0, token.size() - 3)) * (PI / 180.0);
                            active                       = false;
                        } else if (token.contains("deg"))
                            inactiveBorderGradient.m_angle = std::stoi(token.substr(0, token.size() - 3)) * (PI / 180.0);
                        else if (active)
                            activeBorderGradient.m_colors.emplace_back(configStringToInt(token).value_or(0));
                        else
                            inactiveBorderGradient.m_colors.emplace_back(configStringToInt(token).value_or(0));
                    }

                    activeBorderGradient.updateColorsOk();

                    // Includes sanity checks for the number of colors in each gradient
                    if (activeBorderGradient.m_colors.size() > 10 || inactiveBorderGradient.m_colors.size() > 10)
                        Log::logger->log(Log::WARN, "Bordercolor rule \"{}\" has more than 10 colors in one gradient, ignoring", effect);
                    else if (activeBorderGradient.m_colors.empty())
                        Log::logger->log(Log::WARN, "Bordercolor rule \"{}\" has no colors, ignoring", effect);
                    else if (inactiveBorderGradient.m_colors.empty())
                        m_activeBorderColor.first = Types::COverridableVar(activeBorderGradient, Types::PRIORITY_WINDOW_RULE);
                    else {
                        m_activeBorderColor.first   = Types::COverridableVar(activeBorderGradient, Types::PRIORITY_WINDOW_RULE);
                        m_inactiveBorderColor.first = Types::COverridableVar(inactiveBorderGradient, Types::PRIORITY_WINDOW_RULE);
                    }
                } catch (std::exception& e) { Log::logger->log(Log::ERR, "BorderColor rule \"{}\" failed with: {}", effect, e.what()); }
                m_activeBorderColor.second   = rule->getPropertiesMask();
                m_inactiveBorderColor.second = rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_IDLE_INHIBIT: {
                if (effect == "none")
                    m_idleInhibitMode.first.set(IDLEINHIBIT_NONE, Types::PRIORITY_WINDOW_RULE);
                else if (effect == "always")
                    m_idleInhibitMode.first.set(IDLEINHIBIT_ALWAYS, Types::PRIORITY_WINDOW_RULE);
                else if (effect == "focus")
                    m_idleInhibitMode.first.set(IDLEINHIBIT_FOCUS, Types::PRIORITY_WINDOW_RULE);
                else if (effect == "fullscreen")
                    m_idleInhibitMode.first.set(IDLEINHIBIT_FULLSCREEN, Types::PRIORITY_WINDOW_RULE);
                else
                    Log::logger->log(Log::ERR, "Rule idleinhibit: unknown mode {}", effect);
                m_idleInhibitMode.second = rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_OPACITY: {
                try {
                    CVarList2 vars(std::string{effect}, 0, ' ');

                    int       opacityIDX = 0;

                    for (const auto& r : vars) {
                        if (r == "opacity")
                            continue;

                        if (r == "override") {
                            if (opacityIDX == 1)
                                m_alpha.first = Types::COverridableVar(Types::SAlphaValue{.alpha = m_alpha.first.value().alpha, .overridden = true}, Types::PRIORITY_WINDOW_RULE);
                            else if (opacityIDX == 2)
                                m_alphaInactive.first =
                                    Types::COverridableVar(Types::SAlphaValue{.alpha = m_alphaInactive.first.value().alpha, .overridden = true}, Types::PRIORITY_WINDOW_RULE);
                            else if (opacityIDX == 3)
                                m_alphaFullscreen.first =
                                    Types::COverridableVar(Types::SAlphaValue{.alpha = m_alphaFullscreen.first.value().alpha, .overridden = true}, Types::PRIORITY_WINDOW_RULE);
                        } else {
                            if (opacityIDX == 0)
                                m_alpha.first = Types::COverridableVar(Types::SAlphaValue{.alpha = std::stof(std::string{r}), .overridden = false}, Types::PRIORITY_WINDOW_RULE);
                            else if (opacityIDX == 1)
                                m_alphaInactive.first =
                                    Types::COverridableVar(Types::SAlphaValue{.alpha = std::stof(std::string{r}), .overridden = false}, Types::PRIORITY_WINDOW_RULE);
                            else if (opacityIDX == 2)
                                m_alphaFullscreen.first =
                                    Types::COverridableVar(Types::SAlphaValue{.alpha = std::stof(std::string{r}), .overridden = false}, Types::PRIORITY_WINDOW_RULE);
                            else
                                throw std::runtime_error("more than 3 alpha values");

                            opacityIDX++;
                        }
                    }

                    if (opacityIDX == 1) {
                        m_alphaInactive.first   = m_alpha.first;
                        m_alphaFullscreen.first = m_alpha.first;
                    }
                } catch (std::exception& e) { Log::logger->log(Log::ERR, "Opacity rule \"{}\" failed with: {}", effect, e.what()); }
                m_alpha.second           = rule->getPropertiesMask();
                m_alphaInactive.second   = rule->getPropertiesMask();
                m_alphaFullscreen.second = rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_TAG: {
                m_dynamicTags.emplace_back(std::make_pair<>(effect, rule->getPropertiesMask()));
                m_tagKeeper.applyTag(effect, true);
                result.tagsChanged = true;
                break;
            }
            case WINDOW_RULE_EFFECT_MAX_SIZE: {
                try {
                    static auto PCLAMP_TILED = CConfigValue<Config::INTEGER>("misc:size_limits_tiled");

                    if (!m_window)
                        break;

                    const auto VEC = m_window->calculateExpression(effect);
                    if (!VEC) {
                        Log::logger->log(Log::ERR, "failed to parse {} as an expression", effect);
                        break;
                    }
                    if (VEC->x < 1 || VEC->y < 1) {
                        Log::logger->log(Log::ERR, "Invalid size for maxsize");
                        break;
                    }

                    m_maxSize.first = Types::COverridableVar(*VEC, Types::PRIORITY_WINDOW_RULE);

                    if (*PCLAMP_TILED || m_window->m_isFloating)
                        m_window->clampWindowSize(std::nullopt, m_maxSize.first.value());
                } catch (std::exception& e) { Log::logger->log(Log::ERR, "maxsize rule \"{}\" failed with: {}", effect, e.what()); }
                m_maxSize.second = rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_MIN_SIZE: {
                try {
                    static auto PCLAMP_TILED = CConfigValue<Config::INTEGER>("misc:size_limits_tiled");

                    if (!m_window)
                        break;

                    const auto VEC = m_window->calculateExpression(effect);
                    if (!VEC) {
                        Log::logger->log(Log::ERR, "failed to parse {} as an expression", effect);
                        break;
                    }

                    if (VEC->x < 1 || VEC->y < 1) {
                        Log::logger->log(Log::ERR, "Invalid size for maxsize");
                        break;
                    }

                    m_minSize.first = Types::COverridableVar(*VEC, Types::PRIORITY_WINDOW_RULE);
                    if (*PCLAMP_TILED || m_window->m_isFloating)
                        m_window->clampWindowSize(m_minSize.first.value(), std::nullopt);
                } catch (std::exception& e) { Log::logger->log(Log::ERR, "minsize rule \"{}\" failed with: {}", effect, e.what()); }
                m_minSize.second = rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_BORDER_SIZE: {
                try {
                    auto oldBorderSize = m_borderSize.first.valueOrDefault();
                    m_borderSize.first.set(std::stoi(effect), Types::PRIORITY_WINDOW_RULE);
                    m_borderSize.second |= rule->getPropertiesMask();
                    if (oldBorderSize != m_borderSize.first.valueOrDefault())
                        result.needsRelayout = true;
                } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid border_size {}", effect); }
                break;
            }
            case WINDOW_RULE_EFFECT_ALLOWS_INPUT: {
                m_allowsInput.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_allowsInput.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_DIM_AROUND: {
                m_dimAround.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_dimAround.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_DECORATE: {
                m_decorate.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_decorate.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_FOCUS_ON_ACTIVATE: {
                m_focusOnActivate.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_focusOnActivate.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_KEEP_ASPECT_RATIO: {
                m_keepAspectRatio.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_keepAspectRatio.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NEAREST_NEIGHBOR: {
                m_nearestNeighbor.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_nearestNeighbor.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_ANIM: {
                m_noAnim.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noAnim.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_BLUR: {
                m_noBlur.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noBlur.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_DIM: {
                m_noDim.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noDim.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_FOCUS: {
                m_noFocus.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noFocus.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_FOLLOW_MOUSE: {
                m_noFollowMouse.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noFollowMouse.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_MAX_SIZE: {
                m_noMaxSize.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noMaxSize.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_SHADOW: {
                m_noShadow.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noShadow.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_SHORTCUTS_INHIBIT: {
                m_noShortcutsInhibit.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noShortcutsInhibit.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_OPAQUE: {
                m_opaque.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_opaque.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_FORCE_RGBX: {
                m_RGBX.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_RGBX.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_SYNC_FULLSCREEN: {
                m_syncFullscreen.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_syncFullscreen.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_IMMEDIATE: {
                m_tearing.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_tearing.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_XRAY: {
                m_xray.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_xray.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_RENDER_UNFOCUSED: {
                m_renderUnfocused.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_renderUnfocused.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_SCREEN_SHARE: {
                m_noScreenShare.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noScreenShare.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_NO_VRR: {
                m_noVRR.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_noVRR.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_STAY_FOCUSED: {
                m_stayFocused.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE);
                m_stayFocused.second |= rule->getPropertiesMask();
                break;
            }
            case WINDOW_RULE_EFFECT_SCROLL_MOUSE: {
                try {
                    m_scrollMouse.first.set(std::clamp(std::stof(effect), 0.01F, 10.F), Types::PRIORITY_WINDOW_RULE);
                    m_scrollMouse.second |= rule->getPropertiesMask();
                } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid scroll_mouse {}", effect); }
                break;
            }
            case WINDOW_RULE_EFFECT_SCROLL_TOUCHPAD: {
                try {
                    m_scrollTouchpad.first.set(std::clamp(std::stof(effect), 0.01F, 10.F), Types::PRIORITY_WINDOW_RULE);
                    m_scrollTouchpad.second |= rule->getPropertiesMask();
                } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid scroll_touchpad {}", effect); }
                break;
            }
        }
    }
    return result;
}

CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyStaticRule(const SP<CWindowRule>& rule) {
    for (const auto& [key, effect] : rule->effects()) {
        switch (key) {
            default: {
                Log::logger->log(Log::TRACE, "CWindowRuleApplicator::applyStaticRule: Skipping effect {}, not static", sc<std::underlying_type_t<eWindowRuleEffect>>(key));
                break;
            }

            case WINDOW_RULE_EFFECT_FLOAT: {
                static_.floating = truthy(effect);
                break;
            }
            case WINDOW_RULE_EFFECT_TILE: {
                static_.floating = !truthy(effect);
                break;
            }
            case WINDOW_RULE_EFFECT_FULLSCREEN: {
                static_.fullscreen = truthy(effect);
                break;
            }
            case WINDOW_RULE_EFFECT_MAXIMIZE: {
                static_.maximize = truthy(effect);
                break;
            }
            case WINDOW_RULE_EFFECT_FULLSCREENSTATE: {
                CVarList2 vars(std::string{effect}, 0, 's');
                try {
                    static_.fullscreenStateInternal = std::stoi(std::string{vars[0]});
                    if (!vars[1].empty())
                        static_.fullscreenStateClient = std::stoi(std::string{vars[1]});
                } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyStaticRule: invalid fullscreen state {}", effect); }
                break;
            }
            case WINDOW_RULE_EFFECT_MOVE: {
                static_.center   = std::nullopt;
                static_.position = effect;
                break;
            }
            case WINDOW_RULE_EFFECT_SIZE: {
                static_.size = effect;
                break;
            }
            case WINDOW_RULE_EFFECT_CENTER: {
                static_.position.clear();
                static_.center = truthy(effect);
                break;
            }
            case WINDOW_RULE_EFFECT_PSEUDO: {
                static_.pseudo = truthy(effect);
                break;
            }
            case WINDOW_RULE_EFFECT_MONITOR: {
                static_.monitor = effect;
                break;
            }
            case WINDOW_RULE_EFFECT_WORKSPACE: {
                static_.workspace = effect;
                break;
            }
            case WINDOW_RULE_EFFECT_NOINITIALFOCUS: {
                static_.noInitialFocus = truthy(effect);
                break;
            }
            case WINDOW_RULE_EFFECT_PIN: {
                static_.pin = truthy(effect);
                break;
            }
            case WINDOW_RULE_EFFECT_GROUP: {
                static_.group = effect;
                break;
            }
            case WINDOW_RULE_EFFECT_SUPPRESSEVENT: {
                CVarList2 varlist(std::string{effect}, 0, 's');
                for (const auto& e : varlist) {
                    static_.suppressEvent.emplace_back(e);
                }
                break;
            }
            case WINDOW_RULE_EFFECT_CONTENT: {
                static_.content = NContentType::fromString(effect);
                break;
            }
            case WINDOW_RULE_EFFECT_NOCLOSEFOR: {
                try {
                    static_.noCloseFor = std::stoi(effect);
                } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyStaticRule: invalid no close for {}", effect); }
                break;
            }
            case WINDOW_RULE_EFFECT_SCROLLING_WIDTH: {
                try {
                    static_.scrollingWidth = std::stof(effect);
                } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyStaticRule: invalid scrolling width {}", effect); }
                break;
            }
        }
    }

    return SRuleResult{};
}

//
bool CWindowRuleApplicator::readStaticRules(bool preRead) {
    if (!m_window)
        return false;

    static_ = {};

    std::vector<SP<CWindowRule>> execRules;
    bool                         tagsWereChanged = false;

    for (const auto& r : ruleEngine()->rules()) {
        if (r->type() != RULE_TYPE_WINDOW)
            continue;

        auto wr = reinterpretPointerCast<CWindowRule>(r);

        if (!wr->matches(m_window.lock(), true))
            continue;

        if (wr->isExecRule()) {
            execRules.emplace_back(wr);
            continue;
        }

        applyStaticRule(wr);
        const auto RES  = applyDynamicRule(wr);
        tagsWereChanged = tagsWereChanged || RES.tagsChanged;
    }

    // set a recheck for some props people might wanna use for static rules.
    if (tagsWereChanged)
        propsToRecheck |= RULE_PROP_TAG;
    if (static_.content != NContentType::CONTENT_TYPE_NONE)
        propsToRecheck |= RULE_PROP_CONTENT;

    for (const auto& wr : execRules) {
        applyStaticRule(wr);
        applyDynamicRule(wr);
        if (!preRead)
            ruleEngine()->unregisterRule(wr);
    }
    return (propsToRecheck != RULE_PROP_NONE);
}

void CWindowRuleApplicator::recheckStaticRules() {
    for (const auto& r : ruleEngine()->rules()) {
        if (r->type() != RULE_TYPE_WINDOW)
            continue;

        if (!(r->getPropertiesMask() & propsToRecheck))
            continue;

        auto wr = reinterpretPointerCast<CWindowRule>(r);

        if (!wr->matches(m_window.lock(), true))
            continue;

        applyStaticRule(wr);
    }
}

void CWindowRuleApplicator::propertiesChanged(std::underlying_type_t<eRuleProperty> props) {
    if (!m_window || !m_window->m_isMapped || m_window->isHidden())
        return;

    bool                                                        needsRelayout         = false;
    std::unordered_set<CWindowRuleEffectContainer::storageType> effectsNeedingRecheck = resetProps(props);

    for (const auto& r : ruleEngine()->rules()) {
        if (r->type() != RULE_TYPE_WINDOW)
            continue;

        const auto WR = reinterpretPointerCast<CWindowRule>(r);

        if (!(WR->getPropertiesMask() & props) && !setsIntersect(WR->effectsSet(), effectsNeedingRecheck))
            continue;

        if (!WR->matches(m_window.lock()))
            continue;

        const auto RES = applyDynamicRule(WR);
        needsRelayout  = needsRelayout || RES.needsRelayout;
    }

    m_window->updateWindowData();
    m_window->updateWindowDecos();
    m_window->updateDecorationValues();

    if (needsRelayout)
        g_pDecorationPositioner->forceRecalcFor(m_window.lock());

    // for plugins
    Event::bus()->m_events.window.updateRules.emit(m_window.lock());
}
