#include "MasterAlgorithm.hpp"

#include "../../Algorithm.hpp"
#include "../../../space/Space.hpp"
#include "../../../target/WindowTarget.hpp"

#include "../../../../config/ConfigValue.hpp"
#include "../../../../config/shared/actions/ConfigActions.hpp"
#include "../../../../config/shared/workspace/WorkspaceRuleManager.hpp"
#include "../../../../desktop/state/FocusState.hpp"
#include "../../../../desktop/state/WindowState.hpp"
#include "../../../../output/Monitor.hpp"
#include "../../../../pointer/PointerController.hpp"
#include "../../../../Compositor.hpp"
#include "../../../../render/Renderer.hpp"
#include "../../../../state/MonitorState.hpp"
#include "../../../../managers/fullscreen/FullscreenController.hpp"
#include "../../../../managers/fullscreen/handler/FullscreenHandler.hpp"

#include <hyprutils/utils/ScopeGuard.hpp>
#include <hyprutils/string/VarList2.hpp>

using namespace Layout;
using namespace Layout::Tiled;
using namespace Hyprutils::String;

// Center-master slaves alternate left/right columns starting on the fallback side, so an odd
// count puts the extra slave there: left by default, right when extraToRight. calculateWorkspace
// and resizeTarget must both split via this or they disagree for odd counts. Returns {left, right}.
static std::pair<int, int> centerSlaveColumns(int slaveCount, bool extraToRight) {
    const int LARGER  = 1 + (slaveCount - 1) / 2; // ceil, holds the odd one
    const int SMALLER = slaveCount - LARGER;
    return extraToRight ? std::pair{SMALLER, LARGER} : std::pair{LARGER, SMALLER};
}

void CMasterAlgorithm::newTarget(SP<ITarget> target) {
    addTarget(target, true);
}

void CMasterAlgorithm::movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint) {
    addTarget(target, false);
}

void CMasterAlgorithm::addTarget(SP<ITarget> target, bool firstMap) {
    static auto PNEWONACTIVE      = CConfigValue<std::string>("master:new_on_active");
    static auto PNEWONTOP         = CConfigValue<Config::INTEGER>("master:new_on_top");
    static auto PNEWSTATUS        = CConfigValue<std::string>("master:new_status");
    static auto PDROPATCURSOR     = CConfigValue<Config::INTEGER>("master:drop_at_cursor");
    static auto PSLAVECOUNTCENTER = CConfigValue<Config::INTEGER>("master:slave_count_for_center_master");

    const auto  PWORKSPACE = m_parent->space()->workspace();
    const auto  PMONITOR   = PWORKSPACE->m_monitor;

    bool        dragOntoMaster = false;

    const bool  DRAGMOVE = g_layoutManager->dragController()->mode() == MBIND_MOVE;

    const bool  CENTERED         = getDynamicOrientation() == ORIENTATION_CENTER && getNodesNo() - getMastersNo() >= *PSLAVECOUNTCENTER;
    const bool  CENTERCURSORDROP = *PDROPATCURSOR && DRAGMOVE && CENTERED;

    if (g_layoutManager->dragController()->wasDraggingWindow() && (getNodesNo() > 1 || !*PDROPATCURSOR) && !CENTERCURSORDROP) {
        if (const auto n = getClosestNode(g_pInputManager->getMouseCoordsInternal()); n && n->isMaster)
            dragOntoMaster = true;
    }

    const bool BNEWBEFOREACTIVE = *PNEWONACTIVE == "before";
    const bool BNEWISMASTER     = dragOntoMaster || *PNEWSTATUS == "master";

    const auto PNODE = [&]() -> SP<SMasterNodeData> {
        if (*PNEWONACTIVE != "none" && !BNEWISMASTER) {
            const auto pLastNode = getNodeFromWindow(Desktop::focusState()->window());
            if (pLastNode && !(pLastNode->isMaster && (getMastersNo() == 1 || *PNEWSTATUS == "slave"))) {
                auto it = std::ranges::find(m_masterNodesData, pLastNode);
                if (!BNEWBEFOREACTIVE)
                    ++it;
                return *m_masterNodesData.emplace(it, makeShared<SMasterNodeData>());
            }
        }
        return *PNEWONTOP ? *m_masterNodesData.emplace(m_masterNodesData.begin(), makeShared<SMasterNodeData>()) : m_masterNodesData.emplace_back(makeShared<SMasterNodeData>());
    }();

    PNODE->pTarget = target;

    const auto   WINDOWSONWORKSPACE = getNodesNo();
    static auto  PMFACT             = CConfigValue<Config::FLOAT>("master:mfact");
    float        lastSplitPercent   = *PMFACT;

    auto         OPENINGON = isWindowTiled(Desktop::focusState()->window()) && Desktop::focusState()->window()->m_workspace == PWORKSPACE ?
        getNodeFromWindow(Desktop::focusState()->window()) :
        getMasterNode();

    const auto   MOUSECOORDS = g_pInputManager->getMouseCoordsInternal();
    eOrientation orientation = getDynamicOrientation();
    const auto   NODEIT      = std::ranges::find(m_masterNodesData, PNODE);

    bool         forceDropAsMaster = false;
    // if dragging window to move, drop it at the cursor position instead of bottom/top of stack
    if (*PDROPATCURSOR && DRAGMOVE) {
        if (CENTERED) {
            if (const auto PMASTER = getMasterNode(); PMASTER) {
                const CBox MBOX = PMASTER->pTarget->position();
                if (MOUSECOORDS.x >= MBOX.x && MOUSECOORDS.x <= MBOX.x + MBOX.w)
                    forceDropAsMaster = true;
                else {
                    static auto CMFALLBACK = CConfigValue<std::string>("master:center_master_fallback");

                    const bool  DROPRIGHT      = MOUSECOORDS.x > MBOX.middle().x;
                    const bool  FIRSTSIDERIGHT = *CMFALLBACK == "right";

                    auto&       v = m_masterNodesData;
                    std::erase(v, PNODE);

                    int slot     = 0;
                    int slavesNo = 0;
                    for (auto const& nd : v) {
                        if (nd->isMaster)
                            continue;
                        const bool ndRight = (slavesNo % 2 == 0) == FIRSTSIDERIGHT;
                        if (ndRight == DROPRIGHT && MOUSECOORDS.y > nd->pTarget->position().middle().y)
                            ++slot;
                        ++slavesNo;
                    }

                    const bool TARGETFIRSTSIDE = DROPRIGHT == FIRSTSIDERIGHT;
                    const int  TOTALSLAVES     = slavesNo + 1;
                    const int  TARGETCAP       = TARGETFIRSTSIDE ? (TOTALSLAVES + 1) / 2 : TOTALSLAVES / 2;
                    if (TARGETCAP > 0)
                        slot = std::min(slot, TARGETCAP - 1);
                    const int   INSERTSLAVEPOS = TARGETCAP == 0 ? 0 : (TARGETFIRSTSIDE ? 2 * slot : 2 * slot + 1);

                    std::size_t insertIndex = v.size();
                    int         counted     = 0;
                    for (std::size_t i = 0; i < v.size(); ++i) {
                        if (counted == INSERTSLAVEPOS) {
                            insertIndex = i;
                            break;
                        }
                        if (!v[i]->isMaster)
                            ++counted;
                    }

                    v.insert(v.begin() + sc<std::ptrdiff_t>(insertIndex), PNODE);
                }
            }
        } else if (WINDOWSONWORKSPACE > 2) {
            auto&             v = m_masterNodesData;

            const std::size_t srcIndex = sc<std::size_t>(std::distance(v.begin(), NODEIT));

            for (std::size_t i = 0; i < v.size(); ++i) {
                const CBox box = v[i]->pTarget->position();
                if (!box.containsPoint(MOUSECOORDS))
                    continue;

                std::size_t insertIndex = i;

                switch (orientation) {
                    case ORIENTATION_LEFT:
                    case ORIENTATION_RIGHT:
                        if (MOUSECOORDS.y > box.middle().y)
                            ++insertIndex; // insert after
                        break;

                    case ORIENTATION_TOP:
                    case ORIENTATION_BOTTOM:
                        if (MOUSECOORDS.x > box.middle().x)
                            ++insertIndex; // insert after
                        break;

                    case ORIENTATION_CENTER: break;

                    default: UNREACHABLE();
                }

                if (insertIndex > srcIndex)
                    --insertIndex;

                if (insertIndex == srcIndex)
                    break;

                auto node = std::move(v[srcIndex]);
                v.erase(v.begin() + sc<std::ptrdiff_t>(srcIndex));
                v.insert(v.begin() + sc<std::ptrdiff_t>(insertIndex), std::move(node));

                break;
            }
        } else if (WINDOWSONWORKSPACE == 2) {
            // when dropping as the second tiled window in the workspace,
            // make it the master only if the cursor is on the master side of the screen
            for (auto const& nd : m_masterNodesData) {
                if (nd->isMaster) {
                    const auto MIDDLE = nd->pTarget->position().middle();
                    switch (orientation) {
                        case ORIENTATION_LEFT:
                        case ORIENTATION_CENTER:
                            if (MOUSECOORDS.x < MIDDLE.x)
                                forceDropAsMaster = true;
                            break;
                        case ORIENTATION_RIGHT:
                            if (MOUSECOORDS.x > MIDDLE.x)
                                forceDropAsMaster = true;
                            break;
                        case ORIENTATION_TOP:
                            if (MOUSECOORDS.y < MIDDLE.y)
                                forceDropAsMaster = true;
                            break;
                        case ORIENTATION_BOTTOM:
                            if (MOUSECOORDS.y > MIDDLE.y)
                                forceDropAsMaster = true;
                            break;
                        default: UNREACHABLE();
                    }
                    break;
                }
            }
        }
    }

    if (BNEWISMASTER                                                                 //
        || WINDOWSONWORKSPACE == 1                                                   //
        || (WINDOWSONWORKSPACE > 2 && !firstMap && OPENINGON && OPENINGON->isMaster) //
        || forceDropAsMaster                                                         //
        || (*PNEWSTATUS == "inherit" && OPENINGON && OPENINGON->isMaster && g_layoutManager->dragController()->mode() != MBIND_MOVE)) {

        if (BNEWBEFOREACTIVE) {
            for (auto& nd : m_masterNodesData | std::views::reverse) {
                if (nd->isMaster) {
                    nd->isMaster     = false;
                    lastSplitPercent = nd->percMaster;
                    break;
                }
            }
        } else {
            for (auto& nd : m_masterNodesData) {
                if (nd->isMaster) {
                    nd->isMaster     = false;
                    lastSplitPercent = nd->percMaster;
                    break;
                }
            }
        }

        PNODE->isMaster   = true;
        PNODE->percMaster = lastSplitPercent;

        // first, check if it isn't too big.
        if (const auto MAXSIZE = target->maxSize().value_or(Math::VECTOR2D_MAX); MAXSIZE.x < PMONITOR->m_size.x * lastSplitPercent || MAXSIZE.y < PMONITOR->m_size.y) {
            // we can't continue. make it floating.
            m_parent->setFloating(target, true, true);
            std::erase(m_masterNodesData, PNODE);
            return;
        }
    } else {
        PNODE->isMaster   = false;
        PNODE->percMaster = lastSplitPercent;

        // first, check if it isn't too big.
        if (const auto MAXSIZE = target->maxSize().value_or(Math::VECTOR2D_MAX);
            MAXSIZE.x < PMONITOR->m_size.x * (1 - lastSplitPercent) || MAXSIZE.y < PMONITOR->m_size.y * (1.f / (WINDOWSONWORKSPACE - 1))) {
            // we can't continue. make it floating.
            m_parent->setFloating(target, true);
            std::erase(m_masterNodesData, PNODE);
            return;
        }
    }

    // recalc
    calculateWorkspace();
}

void CMasterAlgorithm::removeTarget(SP<ITarget> target) {
    const auto  MASTERSLEFT = getMastersNo();
    static auto SMALLSPLIT  = CConfigValue<Config::INTEGER>("master:allow_small_split");

    const auto  PNODE = getNodeFromTarget(target);

    if (!PNODE || !target)
        return;

    if (Fullscreen::controller()->isFullscreen(target->window()))
        Fullscreen::controller()->setFullscreenMode(target->window(), Fullscreen::FSMODE_NONE);

    if (PNODE->isMaster && (MASTERSLEFT <= 1 || *SMALLSPLIT == 1)) {
        // find a new master from top of the list
        for (auto& nd : m_masterNodesData) {
            if (!nd->isMaster) {
                nd->isMaster   = true;
                nd->percMaster = PNODE->percMaster;
                break;
            }
        }
    }

    std::erase(m_masterNodesData, PNODE);

    if (getMastersNo() == getNodesNo() && MASTERSLEFT > 1) {
        for (auto& nd : m_masterNodesData | std::views::reverse) {
            nd->isMaster = false;
            break;
        }
    }
    // BUGFIX: correct bug where closing one master in a stack of 2 would leave
    // the screen half bare, and make it difficult to select remaining window
    if (getNodesNo() == 1) {
        for (auto& nd : m_masterNodesData) {
            if (!nd->isMaster) {
                nd->isMaster = true;
                break;
            }
        }
    }

    calculateWorkspace();
}

void CMasterAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
    const auto PNODE = getNodeFromTarget(target);

    if (!PNODE)
        return;

    const auto   PMONITOR            = m_parent->space()->workspace()->m_monitor;
    static auto  SLAVECOUNTFORCENTER = CConfigValue<Config::INTEGER>("master:slave_count_for_center_master");
    static auto  PSMARTRESIZING      = CConfigValue<Config::INTEGER>("master:smart_resizing");

    const auto   WORKAREA      = PMONITOR->logicalBoxMinusReserved();
    const bool   DISPLAYBOTTOM = STICKS(PNODE->position.y + PNODE->size.y, WORKAREA.y + WORKAREA.h);
    const bool   DISPLAYRIGHT  = STICKS(PNODE->position.x + PNODE->size.x, WORKAREA.x + WORKAREA.w);
    const bool   DISPLAYTOP    = STICKS(PNODE->position.y, WORKAREA.y);
    const bool   DISPLAYLEFT   = STICKS(PNODE->position.x, WORKAREA.x);

    const bool   LEFT = edgeLeft(corner);
    const bool   TOP  = edgeTop(corner);
    const bool   NONE = corner == CORNER_NONE;

    const auto   MASTERS      = getMastersNo();
    const auto   WINDOWS      = getNodesNo();
    const auto   STACKWINDOWS = WINDOWS - MASTERS;

    eOrientation orientation = getDynamicOrientation();
    bool         centered    = orientation == ORIENTATION_CENTER && (STACKWINDOWS >= *SLAVECOUNTFORCENTER);
    double       delta       = 0;

    if (getNodesNo() == 1 && !centered)
        return;

    m_forceWarps = true;

    switch (orientation) {
        case ORIENTATION_LEFT: delta = Δ.x / PMONITOR->m_size.x; break;
        case ORIENTATION_RIGHT: delta = -Δ.x / PMONITOR->m_size.x; break;
        case ORIENTATION_BOTTOM: delta = -Δ.y / PMONITOR->m_size.y; break;
        case ORIENTATION_TOP: delta = Δ.y / PMONITOR->m_size.y; break;
        case ORIENTATION_CENTER:
            delta = Δ.x / PMONITOR->m_size.x;
            if (STACKWINDOWS >= *SLAVECOUNTFORCENTER) {
                if (!NONE || !PNODE->isMaster)
                    delta *= 2;
                if ((!PNODE->isMaster && DISPLAYLEFT) || (PNODE->isMaster && LEFT && *PSMARTRESIZING))
                    delta = -delta;
            }
            break;
        default: UNREACHABLE();
    }

    for (auto& n : m_masterNodesData) {
        if (n->isMaster)
            n->percMaster = std::clamp(n->percMaster + delta, 0.05, 0.95);
    }

    // check the up/down resize
    const bool isStackVertical = orientation == ORIENTATION_LEFT || orientation == ORIENTATION_RIGHT || orientation == ORIENTATION_CENTER;

    const auto RESIZEDELTA = isStackVertical ? Δ.y : Δ.x;

    auto       nodesInSameColumn = PNODE->isMaster ? MASTERS : STACKWINDOWS;
    if (orientation == ORIENTATION_CENTER && !PNODE->isMaster) {
        static auto CMFALLBACK   = CConfigValue<std::string>("master:center_master_fallback");
        const bool  EXTRATORIGHT = *CMFALLBACK == "right";

        // slaves alternate columns from the fallback side, so even slave index == fallback side
        const auto NODEIT     = std::ranges::find(m_masterNodesData, PNODE);
        const int  SLAVEINDEX = std::count_if(m_masterNodesData.begin(), NODEIT, [](const auto& n) { return !n->isMaster; });
        const bool ONLEFT     = (SLAVEINDEX % 2 == 0) != EXTRATORIGHT;

        const auto [SLAVESLEFT, SLAVESRIGHT] = centerSlaveColumns(STACKWINDOWS, EXTRATORIGHT);
        nodesInSameColumn                    = ONLEFT ? SLAVESLEFT : SLAVESRIGHT;
    }

    const auto SIZE = isStackVertical ? WORKAREA.h / nodesInSameColumn : WORKAREA.w / nodesInSameColumn;

    if (RESIZEDELTA != 0 && nodesInSameColumn > 1) {
        if (!*PSMARTRESIZING) {
            PNODE->percSize = std::clamp(PNODE->percSize + RESIZEDELTA / SIZE, 0.05, 1.95);
        } else {
            const auto  NODEIT    = std::ranges::find(m_masterNodesData, PNODE);
            const auto  REVNODEIT = std::ranges::find(m_masterNodesData | std::views::reverse, PNODE);

            const float totalSize       = isStackVertical ? WORKAREA.h : WORKAREA.w;
            const float minSize         = totalSize / nodesInSameColumn * 0.2;
            const bool  resizePrevNodes = isStackVertical ? (TOP || DISPLAYBOTTOM) && !DISPLAYTOP : (LEFT || DISPLAYRIGHT) && !DISPLAYLEFT;

            int         nodesLeft = 0;
            float       sizeLeft  = 0;
            int         nodeCount = 0;
            // check the sizes of all the nodes to be resized for later calculation
            auto checkNodesLeft = [&sizeLeft, &nodesLeft, orientation, isStackVertical, &nodeCount, PNODE](auto it) {
                if (it->isMaster != PNODE->isMaster)
                    return;
                nodeCount++;
                if (!it->isMaster && orientation == ORIENTATION_CENTER && nodeCount % 2 == 1)
                    return;
                sizeLeft += isStackVertical ? it->size.y : it->size.x;
                nodesLeft++;
            };
            float resizeDiff;
            if (resizePrevNodes) {
                std::for_each(std::next(REVNODEIT), m_masterNodesData.rend(), checkNodesLeft);
                resizeDiff = -RESIZEDELTA;
            } else {
                std::for_each(std::next(NODEIT), m_masterNodesData.end(), checkNodesLeft);
                resizeDiff = RESIZEDELTA;
            }

            const float nodeSize        = isStackVertical ? PNODE->size.y : PNODE->size.x;
            const float maxSizeIncrease = sizeLeft - nodesLeft * minSize;
            const float maxSizeDecrease = minSize - nodeSize;

            // leaves enough room for the other nodes
            resizeDiff = std::clamp(resizeDiff, maxSizeDecrease, maxSizeIncrease);
            PNODE->percSize += resizeDiff / SIZE;

            // resize the other nodes
            nodeCount            = 0;
            auto resizeNodesLeft = [maxSizeIncrease, resizeDiff, minSize, orientation, isStackVertical, SIZE, &nodeCount, nodesLeft, PNODE](auto& it) {
                if (it->isMaster != PNODE->isMaster)
                    return;
                nodeCount++;
                // if center orientation, only resize when on the same side
                if (!it->isMaster && orientation == ORIENTATION_CENTER && nodeCount % 2 == 1)
                    return;
                const float size               = isStackVertical ? it->size.y : it->size.x;
                const float resizeDeltaForEach = maxSizeIncrease != 0 ? resizeDiff * (size - minSize) / maxSizeIncrease : resizeDiff / nodesLeft;
                it->percSize -= resizeDeltaForEach / SIZE;
            };
            if (resizePrevNodes)
                std::for_each(std::next(REVNODEIT), m_masterNodesData.rend(), resizeNodesLeft);
            else
                std::for_each(std::next(NODEIT), m_masterNodesData.end(), resizeNodesLeft);
        }
    }

    recalculate();

    m_forceWarps = false;
}

void CMasterAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
    auto nodeA = getNodeFromTarget(a);
    auto nodeB = getNodeFromTarget(b);

    if (nodeA)
        nodeA->pTarget = b;
    if (nodeB)
        nodeB->pTarget = a;
}

void CMasterAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
    static auto PMONITORFALLBACK = CConfigValue<Config::INTEGER>("binds:window_direction_monitor_fallback");

    const auto  PWINDOW2 = Desktop::windowState()->query().inDirection(t->window(), dir);

    if (!t->window())
        return;

    PHLWORKSPACE targetWs;

    if (!PWINDOW2 && t->space() && t->space()->workspace()) {
        // try to find a monitor in dir
        const auto PMONINDIR = State::monitorState()->query().relativeTo(t->space()->workspace()->m_monitor.lock()).inDirection(dir).run();
        if (PMONINDIR)
            targetWs = PMONINDIR->m_activeWorkspace;
    } else
        targetWs = PWINDOW2->m_workspace;

    if (!targetWs)
        return;

    t->window()->setAnimationsToMove();

    if (t->window()->m_workspace != targetWs) {
        if (!*PMONITORFALLBACK)
            return; // noop

        t->assignToSpace(targetWs->m_space, focalPointForDir(t, dir));
    } else if (PWINDOW2) {
        // if same monitor, switch windows
        g_layoutManager->switchTargets(t, PWINDOW2->layoutTarget());
        if (silent)
            Desktop::focusState()->fullWindowFocus(PWINDOW2, Desktop::FOCUS_REASON_KEYBIND);

        recalculate();
    }
}

void CMasterAlgorithm::recalculate(eRecalculateReason reason) {
    if (!m_parent || !m_parent->space())
        return;
    // Avoid further pos recalc if in fullscreen
    if (Fullscreen::controller()->hasFullscreen(m_parent->space()->workspace(), true)) {
        m_defaultFullscreenHandler->syncTargetSizeAndPosition();
        return;
    }

    calculateWorkspace();
}

Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) {
    auto switchToWindow = [&](SP<ITarget> target) {
        if (!target || !validMapped(target->window()))
            return;

        Desktop::focusState()->fullWindowFocus(target->window(), Desktop::FOCUS_REASON_KEYBIND);
        Pointer::pointerController()->warpTo(target->position().middle());

        g_pInputManager->m_forcedFocus = target->window();
        g_pInputManager->simulateMouseMovement();
        g_pInputManager->m_forcedFocus.reset();
    };

    CVarList2  vars(std::string{sv}, 0, 's');

    const auto invalidArg = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); };
    const auto noTarget   = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::NO_TARGET); };
    const auto stateErr   = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::INVALID_STATE); };

    if (vars.size() < 1 || vars[0].empty()) {
        Log::logger->log(Log::ERR, "layoutmsg called without params");
        return invalidArg("layoutmsg without params");
    }

    auto command = vars[0];

    // swapwithmaster <master | child | auto> <ignoremaster>
    // first message argument can have the following values:
    // * master - keep the focus at the new master
    // * child - keep the focus at the new child
    // * auto (default) - swap the focus (keep the focus of the previously selected window)
    // * ignoremaster - ignore if master is focused

    const auto PWINDOW = Desktop::focusState()->window();

    if (command == "swapwithmaster") {
        if (!PWINDOW)
            return noTarget("No focused window");

        if (!isWindowTiled(PWINDOW))
            return stateErr("focused window isn't tiled");

        const auto PMASTER = getMasterNode();

        if (!PMASTER)
            return stateErr("no master node");

        const auto NEWCHILD = PMASTER->pTarget.lock();
        if (!NEWCHILD)
            return stateErr("master target expired");

        const bool IGNORE_IF_MASTER = vars.size() >= 2 && std::ranges::any_of(vars, [](const auto& e) { return e == "ignoremaster"; });

        if (NEWCHILD != PWINDOW->layoutTarget()) {
            const auto& NEWMASTER       = PWINDOW->layoutTarget();
            const bool  newFocusToChild = vars.size() >= 2 && vars[1] == "child";
            g_layoutManager->switchTargets(NEWMASTER, NEWCHILD);
            const auto NEWFOCUS = newFocusToChild ? NEWCHILD : NEWMASTER;
            switchToWindow(NEWFOCUS);
        } else if (!IGNORE_IF_MASTER) {
            for (auto const& n : m_masterNodesData) {
                if (!n->isMaster) {
                    const auto NEWMASTER = n->pTarget.lock();
                    g_layoutManager->switchTargets(NEWMASTER, NEWCHILD);
                    const bool newFocusToMaster = vars.size() >= 2 && vars[1] == "master";
                    const auto NEWFOCUS         = newFocusToMaster ? NEWMASTER : NEWCHILD;
                    switchToWindow(NEWFOCUS);
                    break;
                }
            }
        }

        return {};
    }
    // focusmaster <master | previous | auto>
    // first message argument can have the following values:
    // * master - keep the focus at the new master, even if it was focused before
    // * previous - focus window which was previously switched from using `focusmaster previous` command, otherwise fallback to `auto`
    // * auto (default) - swap the focus with the first child, if the current focus was master, otherwise focus master
    else if (command == "focusmaster") {
        if (!PWINDOW)
            return noTarget("no focused window");

        const auto PMASTER = getMasterNode();

        if (!PMASTER)
            return stateErr("no master");

        const auto& ARG = vars[1]; // returns empty string if out of bounds

        const auto  TARGET = PMASTER->pTarget.lock();
        if (!TARGET)
            return stateErr("master target expired");

        if (TARGET != PWINDOW->layoutTarget()) {
            switchToWindow(TARGET);
            // save previously focused window (only for `previous` mode)
            if (ARG == "previous")
                m_workspaceData.focusMasterPrev = PWINDOW->layoutTarget();
            return {};
        }

        const auto focusAuto = [&]() {
            // focus first non-master window
            for (auto const& n : m_masterNodesData) {
                if (!n->isMaster) {
                    switchToWindow(n->pTarget.lock());
                    break;
                }
            }
        };

        if (ARG == "master")
            return {};
        // switch to previously saved window
        else if (ARG == "previous") {
            const auto PREVWINDOW = m_workspaceData.focusMasterPrev.lock();
            const bool VALID      = PREVWINDOW && getNodeFromWindow(PREVWINDOW->window()) && (PWINDOW != PREVWINDOW->window());
            VALID ? switchToWindow(PREVWINDOW) : focusAuto();
        } else
            focusAuto();
    } else if (command == "cyclenext") {
        if (!PWINDOW)
            return noTarget("no window");

        const bool NOLOOP      = vars.size() >= 2 && vars[1] == "noloop";
        const auto PNEXTWINDOW = getNextTarget(PWINDOW->layoutTarget(), true, !NOLOOP);
        switchToWindow(PNEXTWINDOW);
    } else if (command == "cycleprev") {
        if (!PWINDOW)
            return noTarget("no window");

        const bool NOLOOP      = vars.size() >= 2 && vars[1] == "noloop";
        const auto PPREVWINDOW = getNextTarget(PWINDOW->layoutTarget(), false, !NOLOOP);
        switchToWindow(PPREVWINDOW);
    } else if (command == "swapnext") {
        if (!validMapped(PWINDOW))
            return noTarget("no window");

        if (PWINDOW->layoutTarget()->floating()) {
            Config::Actions::swapNext(true);
            return {};
        }

        const bool NOLOOP            = vars.size() >= 2 && vars[1] == "noloop";
        const auto PWINDOWTOSWAPWITH = getNextTarget(PWINDOW->layoutTarget(), true, !NOLOOP);

        if (PWINDOWTOSWAPWITH) {
            Fullscreen::controller()->setFullscreenMode(PWINDOW, Fullscreen::FSMODE_NONE);
            g_layoutManager->switchTargets(PWINDOW->layoutTarget(), PWINDOWTOSWAPWITH);
            switchToWindow(PWINDOW->layoutTarget());
        }
    } else if (command == "swapprev") {
        if (!validMapped(PWINDOW))
            return noTarget("no window");

        if (PWINDOW->layoutTarget()->floating()) {
            Config::Actions::swapNext(false);
            return {};
        }

        const bool NOLOOP            = vars.size() >= 2 && vars[1] == "noloop";
        const auto PWINDOWTOSWAPWITH = getNextTarget(PWINDOW->layoutTarget(), false, !NOLOOP);

        if (PWINDOWTOSWAPWITH) {
            Fullscreen::controller()->setFullscreenMode(PWINDOW, Fullscreen::FSMODE_NONE);
            g_layoutManager->switchTargets(PWINDOW->layoutTarget(), PWINDOWTOSWAPWITH);
            switchToWindow(PWINDOW->layoutTarget());
        }
    } else if (command == "addmaster") {
        if (!validMapped(PWINDOW))
            return noTarget("no window");

        if (PWINDOW->layoutTarget()->floating())
            return stateErr("window is floating");

        const auto  PNODE = getNodeFromTarget(PWINDOW->layoutTarget());

        const auto  WINDOWS    = getNodesNo();
        const auto  MASTERS    = getMastersNo();
        static auto SMALLSPLIT = CConfigValue<Config::INTEGER>("master:allow_small_split");

        if (MASTERS + 2 > WINDOWS && *SMALLSPLIT == 0)
            return stateErr("nothing to do");

        Fullscreen::controller()->setFullscreenMode(PWINDOW, Fullscreen::FSMODE_NONE);

        if (!PNODE || PNODE->isMaster) {
            // first non-master node
            for (auto& n : m_masterNodesData) {
                if (!n->isMaster) {
                    n->isMaster = true;
                    break;
                }
            }
        } else {
            PNODE->isMaster = true;
        }

        calculateWorkspace();

    } else if (command == "removemaster") {

        if (!validMapped(PWINDOW))
            return noTarget("no window");

        if (PWINDOW->layoutTarget()->floating())
            return stateErr("window isnt tiled");

        const auto PNODE = getNodeFromTarget(PWINDOW->layoutTarget());

        const auto WINDOWS = getNodesNo();
        const auto MASTERS = getMastersNo();

        if (WINDOWS < 2 || MASTERS < 2)
            return stateErr("nothing to do");

        Fullscreen::controller()->setFullscreenMode(PWINDOW, Fullscreen::FSMODE_NONE);

        if (!PNODE || !PNODE->isMaster) {
            // first non-master node
            for (auto& nd : m_masterNodesData | std::views::reverse) {
                if (nd->isMaster) {
                    nd->isMaster = false;
                    break;
                }
            }
        } else {
            PNODE->isMaster = false;
        }

        calculateWorkspace();
    } else if (command == "orientationleft" || command == "orientationright" || command == "orientationtop" || command == "orientationbottom" || command == "orientationcenter") {
        if (!PWINDOW)
            return noTarget("no window");

        Fullscreen::controller()->setFullscreenMode(PWINDOW, Fullscreen::FSMODE_NONE);

        if (command == "orientationleft")
            m_workspaceData.explicitOrientation = ORIENTATION_LEFT;
        else if (command == "orientationright")
            m_workspaceData.explicitOrientation = ORIENTATION_RIGHT;
        else if (command == "orientationtop")
            m_workspaceData.explicitOrientation = ORIENTATION_TOP;
        else if (command == "orientationbottom")
            m_workspaceData.explicitOrientation = ORIENTATION_BOTTOM;
        else if (command == "orientationcenter")
            m_workspaceData.explicitOrientation = ORIENTATION_CENTER;

        calculateWorkspace();
    } else if (command == "orientationnext") {
        runOrientationCycle(nullptr, 1);
    } else if (command == "orientationprev") {
        runOrientationCycle(nullptr, -1);
    } else if (command == "orientationcycle") {
        runOrientationCycle(&vars, 1);
    } else if (command == "mfact") {

        if (!PWINDOW)
            return noTarget("no window");

        const bool exact = vars[1] == "exact";

        float      ratio = 0.F;

        try {
            ratio = std::stof(std::string{exact ? vars[2] : vars[1]});
        } catch (...) { return invalidArg("bad ratio"); }

        const auto PMASTER = getMasterNode();
        if (!PMASTER)
            return stateErr("no master node");

        float newRatio      = exact ? ratio : PMASTER->percMaster + ratio;
        PMASTER->percMaster = std::clamp(newRatio, 0.05f, 0.95f);

        recalculate();
    } else if (command == "rollnext") {
        const auto PNODE = getNodeFromWindow(PWINDOW);

        if (!PNODE)
            return noTarget("window couldnt be found");

        const auto OLDMASTER = PNODE->isMaster ? PNODE : getMasterNode();
        if (!OLDMASTER)
            return stateErr("no old master");

        auto        oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER);

        SP<ITarget> newFocus;

        for (auto& nd : m_masterNodesData) {
            if (!nd->isMaster) {
                const auto& newMaster = nd;
                newMaster->isMaster   = true;
                newFocus              = newMaster->pTarget.lock();

                auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster);

                if (newMasterIt < oldMasterIt)
                    std::ranges::rotate(newMasterIt, std::next(newMasterIt), oldMasterIt);
                else if (newMasterIt > oldMasterIt)
                    std::ranges::rotate(oldMasterIt, newMasterIt, std::next(newMasterIt));

                OLDMASTER->isMaster = false;

                oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER);
                if (oldMasterIt != m_masterNodesData.end())
                    std::ranges::rotate(oldMasterIt, std::next(oldMasterIt), m_masterNodesData.end());

                break;
            }
        }

        calculateWorkspace();
        if (newFocus)
            switchToWindow(newFocus);
    } else if (command == "rollprev") {
        const auto PNODE = getNodeFromWindow(PWINDOW);

        if (!PNODE)
            return noTarget("window couldnt be found");

        const auto OLDMASTER = PNODE->isMaster ? PNODE : getMasterNode();
        if (!OLDMASTER)
            return stateErr("no old master");

        auto        oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER);

        SP<ITarget> newFocus;

        for (auto& nd : m_masterNodesData | std::views::reverse) {
            if (!nd->isMaster) {
                const auto& newMaster = nd;
                newMaster->isMaster   = true;
                newFocus              = newMaster->pTarget.lock();

                auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster);

                if (newMasterIt < oldMasterIt)
                    std::ranges::rotate(newMasterIt, std::next(newMasterIt), oldMasterIt);
                else if (newMasterIt > oldMasterIt)
                    std::ranges::rotate(oldMasterIt, newMasterIt, std::next(newMasterIt));

                OLDMASTER->isMaster = false;

                oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER);
                if (oldMasterIt != m_masterNodesData.begin())
                    std::ranges::rotate(m_masterNodesData.begin(), oldMasterIt, std::next(oldMasterIt));

                break;
            }
        }

        calculateWorkspace();
        if (newFocus)
            switchToWindow(newFocus);
    } else
        return Config::configError(std::format("Unknown master layoutmsg: {}", sv), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT);

    return {};
}

std::optional<Vector2D> CMasterAlgorithm::predictSizeForNewTarget() {
    static auto PNEWSTATUS = CConfigValue<std::string>("master:new_status");

    const auto  MONITOR = m_parent->space()->workspace()->m_monitor;

    if (!MONITOR)
        return std::nullopt;

    const int NODES = getNodesNo();

    if (NODES <= 0)
        return Desktop::focusState()->monitor()->m_size;

    const auto MASTER = getMasterNode();
    if (!MASTER) // wtf
        return std::nullopt;

    if (*PNEWSTATUS == "master") {
        return MASTER->size;
    } else {
        const auto SLAVES = NODES - getMastersNo();

        // TODO: make this better
        if (SLAVES == 0)
            return Vector2D{MONITOR->m_size.x / 2.F, MONITOR->m_size.y};
        else
            return Vector2D{MONITOR->m_size.x - MASTER->size.x, MONITOR->m_size.y / (SLAVES + 1)};
    }

    return std::nullopt;
}

void CMasterAlgorithm::buildOrientationCycleVectorFromVars(std::vector<eOrientation>& cycle, Hyprutils::String::CVarList2* vars) {
    for (size_t i = 1; i < vars->size(); ++i) {
        if ((*vars)[i] == "top") {
            cycle.emplace_back(ORIENTATION_TOP);
        } else if ((*vars)[i] == "right") {
            cycle.emplace_back(ORIENTATION_RIGHT);
        } else if ((*vars)[i] == "bottom") {
            cycle.emplace_back(ORIENTATION_BOTTOM);
        } else if ((*vars)[i] == "left") {
            cycle.emplace_back(ORIENTATION_LEFT);
        } else if ((*vars)[i] == "center") {
            cycle.emplace_back(ORIENTATION_CENTER);
        }
    }
}

void CMasterAlgorithm::buildOrientationCycleVectorFromEOperation(std::vector<eOrientation>& cycle) {
    for (int i = 0; i <= ORIENTATION_CENTER; ++i) {
        cycle.push_back(sc<eOrientation>(i));
    }
}

eOrientation CMasterAlgorithm::defaultOrientation() {
    static auto PORIENT = CConfigValue<std::string>("master:orientation");

    const auto  WORKSPACERULE = Config::workspaceRuleMgr()->getWorkspaceRuleFor(m_parent->space()->workspace());
    std::string orientationString;
    if (WORKSPACERULE && WORKSPACERULE->m_layoutopts.contains("orientation"))
        orientationString = WORKSPACERULE->m_layoutopts.at("orientation");
    else
        orientationString = *PORIENT;

    eOrientation orientation = ORIENTATION_LEFT;
    // override if workspace rule is set
    if (!orientationString.empty()) {
        if (orientationString == "top")
            orientation = ORIENTATION_TOP;
        else if (orientationString == "right")
            orientation = ORIENTATION_RIGHT;
        else if (orientationString == "bottom")
            orientation = ORIENTATION_BOTTOM;
        else if (orientationString == "center")
            orientation = ORIENTATION_CENTER;
        else
            orientation = ORIENTATION_LEFT;
    }

    return orientation;
}

void CMasterAlgorithm::runOrientationCycle(Hyprutils::String::CVarList2* vars, int next) {
    std::vector<eOrientation> cycle;
    if (vars != nullptr)
        buildOrientationCycleVectorFromVars(cycle, vars);

    if (cycle.empty())
        buildOrientationCycleVectorFromEOperation(cycle);

    const auto PWINDOW = Desktop::focusState()->window();

    if (!PWINDOW)
        return;

    Fullscreen::controller()->setFullscreenMode(PWINDOW, Fullscreen::FSMODE_NONE);

    int nextOrPrev = 0;
    for (size_t i = 0; i < cycle.size(); ++i) {
        if (m_workspaceData.explicitOrientation.value_or(defaultOrientation()) == cycle[i]) {
            nextOrPrev = i + next;
            break;
        }
    }

    if (nextOrPrev >= sc<int>(cycle.size()))
        nextOrPrev = nextOrPrev % sc<int>(cycle.size());
    else if (nextOrPrev < 0)
        nextOrPrev = cycle.size() + (nextOrPrev % sc<int>(cycle.size()));

    m_workspaceData.explicitOrientation = cycle.at(nextOrPrev);
    calculateWorkspace();
}

eOrientation CMasterAlgorithm::getDynamicOrientation() {
    return m_workspaceData.explicitOrientation.value_or(defaultOrientation());
}

int CMasterAlgorithm::getNodesNo() {
    return m_masterNodesData.size();
}

SP<SMasterNodeData> CMasterAlgorithm::getNodeFromWindow(PHLWINDOW x) {
    return x ? getNodeFromTarget(x->layoutTarget()) : nullptr;
}

SP<SMasterNodeData> CMasterAlgorithm::getNodeFromTarget(SP<ITarget> x) const {
    for (const auto& n : m_masterNodesData) {
        if (n->pTarget == x)
            return n;
    }

    return nullptr;
}

SP<SMasterNodeData> CMasterAlgorithm::getMasterNode() {
    for (const auto& n : m_masterNodesData) {
        if (n->isMaster)
            return n;
    }

    return nullptr;
}

void CMasterAlgorithm::calculateWorkspace() {
    const auto PMASTERNODE = getMasterNode();

    if (!PMASTERNODE)
        return;

    Hyprutils::Utils::CScopeGuard x([this] {
        g_pHyprRenderer->damageMonitor(m_parent->space()->workspace()->m_monitor.lock());

        if (!m_forceWarps)
            return;

        for (const auto& n : m_masterNodesData) {
            n->pTarget->warpPositionSize();
        }
    });

    eOrientation                  orientation         = getDynamicOrientation();
    bool                          centerMasterWindow  = false;
    static auto                   SLAVECOUNTFORCENTER = CConfigValue<Config::INTEGER>("master:slave_count_for_center_master");
    static auto                   CMFALLBACK          = CConfigValue<std::string>("master:center_master_fallback");
    static auto                   PIGNORERESERVED     = CConfigValue<Config::INTEGER>("master:center_ignores_reserved");
    static auto                   PSMARTRESIZING      = CConfigValue<Config::INTEGER>("master:smart_resizing");

    const auto                    MASTERS          = getMastersNo();
    const auto                    WINDOWS          = getNodesNo();
    const auto                    STACKWINDOWS     = WINDOWS - MASTERS;
    const auto                    WORKAREA         = m_parent->space()->workArea();
    const auto                    PMONITOR         = m_parent->space()->workspace()->m_monitor;
    const auto                    reservedLeft     = PMONITOR ? PMONITOR->m_reservedArea.left() : 0;
    const auto                    reservedRight    = PMONITOR ? PMONITOR->m_reservedArea.right() : 0;
    const auto                    UNRESERVED_WIDTH = WORKAREA.width + reservedLeft + reservedRight;

    if (orientation == ORIENTATION_CENTER) {
        if (STACKWINDOWS >= *SLAVECOUNTFORCENTER)
            centerMasterWindow = true;
        else {
            if (*CMFALLBACK == "left")
                orientation = ORIENTATION_LEFT;
            else if (*CMFALLBACK == "right")
                orientation = ORIENTATION_RIGHT;
            else if (*CMFALLBACK == "top")
                orientation = ORIENTATION_TOP;
            else if (*CMFALLBACK == "bottom")
                orientation = ORIENTATION_BOTTOM;
            else
                orientation = ORIENTATION_LEFT;
        }
    }

    const float totalSize             = (orientation == ORIENTATION_TOP || orientation == ORIENTATION_BOTTOM) ? WORKAREA.w : WORKAREA.h;
    const float masterAverageSize     = totalSize / MASTERS;
    const float slaveAverageSize      = totalSize / STACKWINDOWS;
    float       masterAccumulatedSize = 0;
    float       slaveAccumulatedSize  = 0;

    if (*PSMARTRESIZING) {
        // check the total width and height so that later
        // if larger/smaller than screen size them down/up
        for (auto const& nd : m_masterNodesData) {
            if (nd->isMaster)
                masterAccumulatedSize += totalSize / MASTERS * nd->percSize;
            else
                slaveAccumulatedSize += totalSize / STACKWINDOWS * nd->percSize;
        }
    }

    // compute placement of master window(s)
    if (WINDOWS == 1 && !centerMasterWindow) {
        static auto PALWAYSKEEPPOSITION = CConfigValue<Config::INTEGER>("master:always_keep_position");
        if (*PALWAYSKEEPPOSITION) {
            const float WIDTH = WORKAREA.w * PMASTERNODE->percMaster;
            float       nextX = 0;

            if (orientation == ORIENTATION_RIGHT)
                nextX = WORKAREA.w - WIDTH;
            else if (orientation == ORIENTATION_CENTER)
                nextX = (WORKAREA.w - WIDTH) / 2;

            PMASTERNODE->size     = Vector2D(WIDTH, WORKAREA.h);
            PMASTERNODE->position = WORKAREA.pos() + Vector2D(nextX, 0.0);
        } else {
            PMASTERNODE->size     = WORKAREA.size();
            PMASTERNODE->position = WORKAREA.pos();
        }

        PMASTERNODE->pTarget->setPositionGlobal({PMASTERNODE->position, PMASTERNODE->size});
        return;
    } else if (orientation == ORIENTATION_TOP || orientation == ORIENTATION_BOTTOM) {
        const float HEIGHT      = STACKWINDOWS != 0 ? WORKAREA.h * PMASTERNODE->percMaster : WORKAREA.h;
        float       widthLeft   = WORKAREA.w;
        int         mastersLeft = MASTERS;
        float       nextX       = 0;
        float       nextY       = 0;

        if (orientation == ORIENTATION_BOTTOM)
            nextY = WORKAREA.h - HEIGHT;

        for (auto& nd : m_masterNodesData) {
            if (!nd->isMaster)
                continue;

            float WIDTH = mastersLeft > 1 ? widthLeft / mastersLeft * nd->percSize : widthLeft;
            if (WIDTH > widthLeft * 0.9f && mastersLeft > 1)
                WIDTH = widthLeft * 0.9f;

            if (*PSMARTRESIZING) {
                nd->percSize *= WORKAREA.w / masterAccumulatedSize;
                WIDTH = masterAverageSize * nd->percSize;
            }

            nd->size     = Vector2D(WIDTH, HEIGHT);
            nd->position = WORKAREA.pos() + Vector2D(nextX, nextY);
            nd->pTarget->setPositionGlobal({nd->position, nd->size});

            mastersLeft--;
            widthLeft -= WIDTH;
            nextX += WIDTH;
        }
    } else { // orientation left, right or center
        const float TOTAL_WIDTH = *PIGNORERESERVED && centerMasterWindow ? UNRESERVED_WIDTH : WORKAREA.w;
        float       WIDTH       = TOTAL_WIDTH;
        float       heightLeft  = WORKAREA.h;
        int         mastersLeft = MASTERS;
        float       nextX       = 0;
        float       nextY       = 0;

        if (STACKWINDOWS > 0 || centerMasterWindow)
            WIDTH *= PMASTERNODE->percMaster;

        if (orientation == ORIENTATION_RIGHT)
            nextX = WORKAREA.w - WIDTH;
        else if (centerMasterWindow)
            nextX += (TOTAL_WIDTH - WIDTH) / 2;

        for (auto& nd : m_masterNodesData) {
            if (!nd->isMaster)
                continue;

            float HEIGHT = mastersLeft > 1 ? heightLeft / mastersLeft * nd->percSize : heightLeft;
            if (HEIGHT > heightLeft * 0.9f && mastersLeft > 1)
                HEIGHT = heightLeft * 0.9f;

            if (*PSMARTRESIZING) {
                nd->percSize *= WORKAREA.h / masterAccumulatedSize;
                HEIGHT = masterAverageSize * nd->percSize;
            }

            nd->size     = Vector2D(WIDTH, HEIGHT);
            nd->position = (*PIGNORERESERVED && centerMasterWindow ? WORKAREA.pos() - Vector2D(reservedLeft, 0.0) : WORKAREA.pos()) + Vector2D(nextX, nextY);
            nd->pTarget->setPositionGlobal({nd->position, nd->size});

            mastersLeft--;
            heightLeft -= HEIGHT;
            nextY += HEIGHT;
        }
    }

    if (STACKWINDOWS == 0)
        return;

    // compute placement of slave window(s)
    int slavesLeft = STACKWINDOWS;
    if (orientation == ORIENTATION_TOP || orientation == ORIENTATION_BOTTOM) {
        const float HEIGHT    = WORKAREA.h - PMASTERNODE->size.y;
        float       widthLeft = WORKAREA.w;
        float       nextX     = 0;
        float       nextY     = 0;

        if (orientation == ORIENTATION_TOP)
            nextY = PMASTERNODE->size.y;

        for (auto& nd : m_masterNodesData) {
            if (nd->isMaster)
                continue;

            float WIDTH = slavesLeft > 1 ? widthLeft / slavesLeft * nd->percSize : widthLeft;
            if (WIDTH > widthLeft * 0.9f && slavesLeft > 1)
                WIDTH = widthLeft * 0.9f;

            if (*PSMARTRESIZING) {
                nd->percSize *= WORKAREA.w / slaveAccumulatedSize;
                WIDTH = slaveAverageSize * nd->percSize;
            }

            nd->size     = Vector2D(WIDTH, HEIGHT);
            nd->position = WORKAREA.pos() + Vector2D(nextX, nextY);
            nd->pTarget->setPositionGlobal({nd->position, nd->size});

            slavesLeft--;
            widthLeft -= WIDTH;
            nextX += WIDTH;
        }
    } else if (orientation == ORIENTATION_LEFT || orientation == ORIENTATION_RIGHT) {
        const float WIDTH      = WORKAREA.w - PMASTERNODE->size.x;
        float       heightLeft = WORKAREA.h;
        float       nextY      = 0;
        float       nextX      = 0;

        if (orientation == ORIENTATION_LEFT)
            nextX = PMASTERNODE->size.x;

        for (auto& nd : m_masterNodesData) {
            if (nd->isMaster)
                continue;

            float HEIGHT = slavesLeft > 1 ? heightLeft / slavesLeft * nd->percSize : heightLeft;
            if (HEIGHT > heightLeft * 0.9f && slavesLeft > 1)
                HEIGHT = heightLeft * 0.9f;

            if (*PSMARTRESIZING) {
                nd->percSize *= WORKAREA.h / slaveAccumulatedSize;
                HEIGHT = slaveAverageSize * nd->percSize;
            }

            nd->size     = Vector2D(WIDTH, HEIGHT);
            nd->position = WORKAREA.pos() + Vector2D(nextX, nextY);
            nd->pTarget->setPositionGlobal({nd->position, nd->size});

            slavesLeft--;
            heightLeft -= HEIGHT;
            nextY += HEIGHT;
        }
    } else { // slaves for centered master window(s)
        const float WIDTH               = ((*PIGNORERESERVED ? UNRESERVED_WIDTH : WORKAREA.w) - PMASTERNODE->size.x) / 2.0;
        float       heightLeft          = 0;
        float       heightLeftL         = WORKAREA.h;
        float       heightLeftR         = WORKAREA.h;
        float       nextX               = 0;
        float       nextY               = 0;
        float       nextYL              = 0;
        float       nextYR              = 0;
        bool        onRight             = *CMFALLBACK == "right";
        auto [slavesLeftL, slavesLeftR] = centerSlaveColumns(slavesLeft, onRight);

        const float slaveAverageHeightL     = WORKAREA.h / slavesLeftL;
        const float slaveAverageHeightR     = WORKAREA.h / slavesLeftR;
        float       slaveAccumulatedHeightL = 0;
        float       slaveAccumulatedHeightR = 0;

        if (*PSMARTRESIZING) {
            for (auto const& nd : m_masterNodesData) {
                if (nd->isMaster)
                    continue;

                if (onRight)
                    slaveAccumulatedHeightR += slaveAverageHeightR * nd->percSize;
                else
                    slaveAccumulatedHeightL += slaveAverageHeightL * nd->percSize;

                onRight = !onRight;
            }

            onRight = *CMFALLBACK == "right";
        }

        for (auto& nd : m_masterNodesData) {
            if (nd->isMaster)
                continue;

            if (onRight) {
                nextX      = WIDTH + PMASTERNODE->size.x - (*PIGNORERESERVED ? reservedLeft : 0);
                nextY      = nextYR;
                heightLeft = heightLeftR;
                slavesLeft = slavesLeftR;
            } else {
                nextX      = 0;
                nextY      = nextYL;
                heightLeft = heightLeftL;
                slavesLeft = slavesLeftL;
            }

            float HEIGHT = slavesLeft > 1 ? heightLeft / slavesLeft * nd->percSize : heightLeft;
            if (HEIGHT > heightLeft * 0.9f && slavesLeft > 1)
                HEIGHT = heightLeft * 0.9f;

            if (*PSMARTRESIZING) {
                if (onRight) {
                    nd->percSize *= WORKAREA.h / slaveAccumulatedHeightR;
                    HEIGHT = slaveAverageHeightR * nd->percSize;
                } else {
                    nd->percSize *= WORKAREA.h / slaveAccumulatedHeightL;
                    HEIGHT = slaveAverageHeightL * nd->percSize;
                }
            }

            nd->size     = Vector2D(*PIGNORERESERVED ? (WIDTH - (onRight ? reservedRight : reservedLeft)) : WIDTH, HEIGHT);
            nd->position = WORKAREA.pos() + Vector2D(nextX, nextY);
            nd->pTarget->setPositionGlobal({nd->position, nd->size});

            if (onRight) {
                heightLeftR -= HEIGHT;
                nextYR += HEIGHT;
                slavesLeftR--;
            } else {
                heightLeftL -= HEIGHT;
                nextYL += HEIGHT;
                slavesLeftL--;
            }

            onRight = !onRight;
        }
    }
}

SP<ITarget> CMasterAlgorithm::getNextCandidate(SP<ITarget> old) {
    const auto FOCUS_MASTER = CConfigValue<Config::BOOL>("master:focus_master_on_close");
    const auto MIDDLE       = old->position().middle();

    if (const auto NODE = getMasterNode(); *FOCUS_MASTER && NODE)
        return NODE->pTarget.lock();

    if (const auto NODE = getClosestNode(MIDDLE); NODE)
        return NODE->pTarget.lock();

    if (const auto NODE = getMasterNode(); NODE)
        return NODE->pTarget.lock();

    return nullptr;
}

SP<ITarget> CMasterAlgorithm::getNextTarget(SP<ITarget> t, bool next, bool loop) {
    if (!t || t->floating())
        return nullptr;

    const auto PNODE = getNodeFromTarget(t);
    if (!PNODE)
        return nullptr;

    auto nodes = m_masterNodesData;
    if (!next)
        std::ranges::reverse(nodes);

    const auto NODEIT = std::ranges::find(nodes, PNODE);

    const bool ISMASTER = PNODE->isMaster;

    auto       CANDIDATE = std::find_if(NODEIT, nodes.end(), [&](const auto& other) { return other != PNODE && ISMASTER == other->isMaster; });
    if (CANDIDATE == nodes.end())
        CANDIDATE = std::ranges::find_if(nodes, [&](const auto& other) { return other != PNODE && ISMASTER != other->isMaster; });

    if (CANDIDATE != nodes.end() && !loop) {
        if ((*CANDIDATE)->isMaster && next)
            return nullptr;
        if (!(*CANDIDATE)->isMaster && ISMASTER && !next)
            return nullptr;
    }

    return CANDIDATE == nodes.end() ? nullptr : (*CANDIDATE)->pTarget.lock();
}

int CMasterAlgorithm::getMastersNo() {
    return std::ranges::count_if(m_masterNodesData, [](const auto& n) { return n->isMaster; });
}

bool CMasterAlgorithm::isWindowTiled(PHLWINDOW x) {
    return x && !x->layoutTarget()->floating();
}

SP<SMasterNodeData> CMasterAlgorithm::getClosestNode(const Vector2D& point) {
    SP<SMasterNodeData> res         = nullptr;
    double              distClosest = -1;
    for (auto& n : m_masterNodesData) {
        if (n->pTarget && Desktop::View::validMapped(n->pTarget->window())) {
            auto distAnother = vecToRectDistanceSquared(point, n->position, n->position + n->size);
            if (!res || distAnother < distClosest) {
                res         = n;
                distClosest = distAnother;
            }
        }
    }
    return res;
}
