#include "Scrolling.hpp"

#include <algorithm>

#include <hyprland/src/Compositor.hpp>
#include <hyprland/src/desktop/state/FocusState.hpp>
#include <hyprland/src/helpers/time/Timer.hpp>
#include <hyprland/src/managers/input/InputManager.hpp>
#include <hyprland/src/managers/eventLoop/EventLoopManager.hpp>
#include <hyprland/src/config/ConfigManager.hpp>
#include <hyprland/src/config/ConfigValue.hpp>
#include <hyprland/src/render/Renderer.hpp>

#include <hyprutils/string/ConstVarList.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::String;
using namespace Hyprutils::Utils;

constexpr float MIN_COLUMN_WIDTH = 0.05F;
constexpr float MAX_COLUMN_WIDTH = 1.F;
constexpr float MIN_ROW_HEIGHT   = 0.1F;
constexpr float MAX_ROW_HEIGHT   = 1.F;

//
void SColumnData::add(PHLWINDOW w) {
    for (auto& wd : windowDatas) {
        wd->windowSize *= (float)windowDatas.size() / (float)(windowDatas.size() + 1);
    }

    windowDatas.emplace_back(makeShared<SScrollingWindowData>(w, self.lock(), 1.F / (float)(windowDatas.size() + 1)));
}

void SColumnData::add(PHLWINDOW w, int after) {
    for (auto& wd : windowDatas) {
        wd->windowSize *= (float)windowDatas.size() / (float)(windowDatas.size() + 1);
    }

    windowDatas.insert(windowDatas.begin() + after + 1, makeShared<SScrollingWindowData>(w, self.lock(), 1.F / (float)(windowDatas.size() + 1)));
}

void SColumnData::add(SP<SScrollingWindowData> w) {
    for (auto& wd : windowDatas) {
        wd->windowSize *= (float)windowDatas.size() / (float)(windowDatas.size() + 1);
    }

    windowDatas.emplace_back(w);
    w->column     = self;
    w->windowSize = 1.F / (float)(windowDatas.size());
}

void SColumnData::add(SP<SScrollingWindowData> w, int after) {
    for (auto& wd : windowDatas) {
        wd->windowSize *= (float)windowDatas.size() / (float)(windowDatas.size() + 1);
    }

    windowDatas.insert(windowDatas.begin() + after + 1, w);
    w->column     = self;
    w->windowSize = 1.F / (float)(windowDatas.size());
}

size_t SColumnData::idx(PHLWINDOW w) {
    for (size_t i = 0; i < windowDatas.size(); ++i) {
        if (windowDatas[i]->window == w)
            return i;
    }
    return 0;
}

size_t SColumnData::idxForHeight(float y) {
    for (size_t i = 0; i < windowDatas.size(); ++i) {
        if (windowDatas[i]->window->m_position.y < y)
            continue;
        return i - 1;
    }
    return windowDatas.size() - 1;
}

void SColumnData::remove(PHLWINDOW w) {
    const auto SIZE_BEFORE = windowDatas.size();
    std::erase_if(windowDatas, [&w](const auto& e) { return e->window == w; });

    if (SIZE_BEFORE == windowDatas.size() && SIZE_BEFORE > 0)
        return;

    float newMaxSize = 0.F;
    for (auto& wd : windowDatas) {
        newMaxSize += wd->windowSize;
    }

    for (auto& wd : windowDatas) {
        wd->windowSize *= 1.F / newMaxSize;
    }

    if (windowDatas.empty() && workspace)
        workspace->remove(self.lock());
}

void SColumnData::up(SP<SScrollingWindowData> w) {
    for (size_t i = 1; i < windowDatas.size(); ++i) {
        if (windowDatas[i] != w)
            continue;

        std::swap(windowDatas[i], windowDatas[i - 1]);
        break;
    }
}

void SColumnData::down(SP<SScrollingWindowData> w) {
    for (size_t i = 0; i < windowDatas.size() - 1; ++i) {
        if (windowDatas[i] != w)
            continue;

        std::swap(windowDatas[i], windowDatas[i + 1]);
        break;
    }
}

SP<SScrollingWindowData> SColumnData::next(SP<SScrollingWindowData> w) {
    for (size_t i = 0; i < windowDatas.size() - 1; ++i) {
        if (windowDatas[i] != w)
            continue;

        return windowDatas[i + 1];
    }

    return nullptr;
}

SP<SScrollingWindowData> SColumnData::prev(SP<SScrollingWindowData> w) {
    for (size_t i = 1; i < windowDatas.size(); ++i) {
        if (windowDatas[i] != w)
            continue;

        return windowDatas[i - 1];
    }

    return nullptr;
}

bool SColumnData::has(PHLWINDOW w) {
    return std::ranges::find_if(windowDatas, [w](const auto& e) { return e->window == w; }) != windowDatas.end();
}

SP<SColumnData> SWorkspaceData::add() {
    static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("plugin:hyprscrolling:column_width");
    auto              col       = columns.emplace_back(makeShared<SColumnData>(self.lock()));
    col->self                   = col;
    col->columnWidth            = *PCOLWIDTH;
    return col;
}

SP<SColumnData> SWorkspaceData::add(int after) {
    static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("plugin:hyprscrolling:column_width");
    auto              col       = makeShared<SColumnData>(self.lock());
    col->self                   = col;
    col->columnWidth            = *PCOLWIDTH;
    columns.insert(columns.begin() + after + 1, col);
    return col;
}

int64_t SWorkspaceData::idx(SP<SColumnData> c) {
    for (size_t i = 0; i < columns.size(); ++i) {
        if (columns[i] == c)
            return i;
    }

    return -1;
}

void SWorkspaceData::remove(SP<SColumnData> c) {
    std::erase(columns, c);
}

SP<SColumnData> SWorkspaceData::next(SP<SColumnData> c) {
    for (size_t i = 0; i < columns.size(); ++i) {
        if (columns[i] != c)
            continue;

        if (i == columns.size() - 1)
            return nullptr;

        return columns[i + 1];
    }

    return nullptr;
}

SP<SColumnData> SWorkspaceData::prev(SP<SColumnData> c) {
    for (size_t i = 0; i < columns.size(); ++i) {
        if (columns[i] != c)
            continue;

        if (i == 0)
            return nullptr;

        return columns[i - 1];
    }

    return nullptr;
}

void SWorkspaceData::centerCol(SP<SColumnData> c) {
    if (!c)
        return;

    static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");

    PHLMONITOR        PMONITOR    = workspace->m_monitor.lock();
    double            currentLeft = 0;
    const auto        USABLE      = layout->usableAreaFor(PMONITOR);

    for (const auto& COL : columns) {
        const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;

        if (COL != c)
            currentLeft += ITEM_WIDTH;
        else {
            leftOffset = currentLeft - (USABLE.w - ITEM_WIDTH) / 2.F;
            return;
        }
    }
}

void SWorkspaceData::fitCol(SP<SColumnData> c) {
    if (!c)
        return;

    static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");

    PHLMONITOR        PMONITOR    = workspace->m_monitor.lock();
    double            currentLeft = 0;
    const auto        USABLE      = layout->usableAreaFor(PMONITOR);

    for (const auto& COL : columns) {
        const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;

        if (COL != c)
            currentLeft += ITEM_WIDTH;
        else {
            leftOffset = std::clamp((double)leftOffset, currentLeft - USABLE.w + ITEM_WIDTH, currentLeft);
            return;
        }
    }
}

void SWorkspaceData::centerOrFitCol(SP<SColumnData> c) {
    if (!c)
        return;

    static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:focus_fit_method");

    if (*PFITMETHOD == 1)
        fitCol(c);
    else
        centerCol(c);
}

SP<SColumnData> SWorkspaceData::atCenter() {
    static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");

    PHLMONITOR        PMONITOR    = workspace->m_monitor.lock();
    double            currentLeft = leftOffset;
    const auto        USABLE      = layout->usableAreaFor(PMONITOR);

    for (const auto& COL : columns) {
        const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;

        currentLeft += ITEM_WIDTH;

        if (currentLeft >= PMONITOR->m_size.x / 2.0 - 2)
            return COL;
    }

    return nullptr;
}

void SWorkspaceData::recalculate(bool forceInstant) {
    static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");

    if (!workspace) {
        Log::logger->log(Log::ERR, "[scroller] broken internal state on workspace data");
        return;
    }

    const auto   MAX_WIDTH = maxWidth();

    PHLMONITOR   PMONITOR = workspace->m_monitor.lock();

    const CBox   USABLE = layout->usableAreaFor(PMONITOR);

    double       currentLeft = 0;
    const double cameraLeft  = MAX_WIDTH < USABLE.w ? std::round((MAX_WIDTH - USABLE.w) / 2.0) : leftOffset; // layout pixels
    const auto   workAreaPos = layout->workAreaOnWorkspace(PMONITOR->m_activeWorkspace).pos();

    for (size_t i = 0; i < columns.size(); ++i) {
        const auto&  COL        = columns[i];
        double       currentTop = 0.0;
        const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;

        for (const auto& WINDOW : COL->windowDatas) {
            WINDOW->layoutBox = CBox{currentLeft, currentTop, ITEM_WIDTH, WINDOW->windowSize * USABLE.h}.translate(workAreaPos + Vector2D{-cameraLeft, 0.0});

            currentTop += WINDOW->windowSize * USABLE.h;

            layout->applyNodeDataToWindow(WINDOW, forceInstant, i != columns.size() - 1, i != 0);
        }

        currentLeft += ITEM_WIDTH;
        if (currentLeft == USABLE.width)
            currentLeft++; // avoid ffm from "grabbing" the window on the right
    }
}

double SWorkspaceData::maxWidth() {
    static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");

    PHLMONITOR        PMONITOR    = workspace->m_monitor.lock();
    double            currentLeft = 0;
    const auto        USABLE      = layout->usableAreaFor(PMONITOR);

    for (const auto& COL : columns) {
        const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;

        currentLeft += ITEM_WIDTH;
    }

    return currentLeft;
}

bool SWorkspaceData::visible(SP<SColumnData> c) {
    const auto USABLE    = layout->usableAreaFor(workspace->m_monitor.lock());
    float      totalLeft = 0;
    for (const auto& col : columns) {
        if (col == c) {
            const float colLeft   = totalLeft;
            const float colRight  = totalLeft + col->columnWidth * USABLE.w;
            const float viewLeft  = leftOffset;
            const float viewRight = leftOffset + USABLE.w;
            return colLeft < viewRight && viewLeft < colRight;
        }

        totalLeft += col->columnWidth * USABLE.w;
    }

    return false;
}

void CScrollingLayout::applyNodeDataToWindow(SP<SScrollingWindowData> data, bool force, bool hasWindowsRight, bool hasWindowsLeft) {
    PHLMONITOR   PMONITOR;
    PHLWORKSPACE PWORKSPACE;

    if (!data || !data->column || !data->column->workspace) {
        if (!data->overrideWorkspace) {
            Log::logger->log(Log::ERR, "[scroller] broken internal state on workspace (1)");
            return;
        }

        PMONITOR   = data->overrideWorkspace->m_monitor.lock();
        PWORKSPACE = data->overrideWorkspace.lock();
    } else {
        PMONITOR   = data->column->workspace->workspace->m_monitor.lock();
        PWORKSPACE = data->column->workspace->workspace.lock();
    }

    if (!PMONITOR || !PWORKSPACE) {
        Log::logger->log(Log::ERR, "[scroller] broken internal state on workspace (2)");
        return;
    }

    // for gaps outer
    const auto WORKAREA      = workAreaOnWorkspace(PWORKSPACE);
    const bool DISPLAYLEFT   = !hasWindowsLeft;
    const bool DISPLAYRIGHT  = !hasWindowsRight;
    const bool DISPLAYTOP    = STICKS(data->layoutBox.y, WORKAREA.y);
    const bool DISPLAYBOTTOM = STICKS(data->layoutBox.y + data->layoutBox.h, WORKAREA.y + WORKAREA.h);

    const auto PWINDOW = data->window.lock();
    // get specific gaps and rules for this workspace,
    // if user specified them in config
    const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(PWORKSPACE);

    if (!validMapped(PWINDOW)) {
        Log::logger->log(Log::ERR, "Node {} holding invalid {}!!", (uintptr_t)data.get(), PWINDOW);
        onWindowRemovedTiling(PWINDOW);
        return;
    }

    if (PWINDOW->isFullscreen() && !data->ignoreFullscreenChecks)
        return;

    PWINDOW->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT);
    PWINDOW->updateWindowData();

    static auto PGAPSINDATA = CConfigValue<Hyprlang::CUSTOMTYPE>("general:gaps_in");
    auto* const PGAPSIN     = (CCssGapData*)(PGAPSINDATA.ptr())->getData();

    auto        gapsIn  = WORKSPACERULE.gapsIn.value_or(*PGAPSIN);
    CBox        nodeBox = data->layoutBox;
    nodeBox.round();

    PWINDOW->m_size     = nodeBox.size();
    PWINDOW->m_position = nodeBox.pos();

    PWINDOW->updateWindowDecos();

    auto       calcPos  = PWINDOW->m_position;
    auto       calcSize = PWINDOW->m_size;

    const auto OFFSETTOPLEFT = Vector2D((double)(DISPLAYLEFT ? 0 : gapsIn.m_left), (double)(DISPLAYTOP ? 0 : gapsIn.m_top));

    const auto OFFSETBOTTOMRIGHT = Vector2D((double)(DISPLAYRIGHT ? 0 : gapsIn.m_right), (double)(DISPLAYBOTTOM ? 0 : gapsIn.m_bottom));

    calcPos  = calcPos + OFFSETTOPLEFT;
    calcSize = calcSize - OFFSETTOPLEFT - OFFSETBOTTOMRIGHT;

    if (PWINDOW->m_isPseudotiled) {
        // Calculate pseudo
        float scale = 1;

        // adjust if doesnt fit
        if (PWINDOW->m_pseudoSize.x > calcSize.x || PWINDOW->m_pseudoSize.y > calcSize.y) {
            if (PWINDOW->m_pseudoSize.x > calcSize.x) {
                scale = calcSize.x / PWINDOW->m_pseudoSize.x;
            }

            if (PWINDOW->m_pseudoSize.y * scale > calcSize.y) {
                scale = calcSize.y / PWINDOW->m_pseudoSize.y;
            }

            auto DELTA = calcSize - PWINDOW->m_pseudoSize * scale;
            calcSize   = PWINDOW->m_pseudoSize * scale;
            calcPos    = calcPos + DELTA / 2.f; // center
        } else {
            auto DELTA = calcSize - PWINDOW->m_pseudoSize;
            calcPos    = calcPos + DELTA / 2.f; // center
            calcSize   = PWINDOW->m_pseudoSize;
        }
    }

    const auto RESERVED = PWINDOW->getFullWindowReservedArea();
    calcPos             = calcPos + RESERVED.topLeft;
    calcSize            = calcSize - (RESERVED.topLeft + RESERVED.bottomRight);

    if (PWINDOW->onSpecialWorkspace() && !PWINDOW->isFullscreen()) {
        // if special, we adjust the coords a bit
        static auto PSCALEFACTOR = CConfigValue<Hyprlang::FLOAT>("dwindle:special_scale_factor");

        CBox        wb = {calcPos + (calcSize - calcSize * *PSCALEFACTOR) / 2.f, calcSize * *PSCALEFACTOR};
        wb.round(); // avoid rounding mess

        *PWINDOW->m_realPosition = wb.pos();
        *PWINDOW->m_realSize     = wb.size();
    } else {
        CBox wb = {calcPos, calcSize};
        wb.round(); // avoid rounding mess

        *PWINDOW->m_realSize     = wb.size();
        *PWINDOW->m_realPosition = wb.pos();
    }

    if (force) {
        g_pHyprRenderer->damageWindow(PWINDOW);

        PWINDOW->m_realPosition->warp();
        PWINDOW->m_realSize->warp();

        g_pHyprRenderer->damageWindow(PWINDOW);
    }

    PWINDOW->updateWindowDecos();
}

void CScrollingLayout::onEnable() {
    static const auto PCONFWIDTHS = CConfigValue<Hyprlang::STRING>("plugin:hyprscrolling:explicit_column_widths");

    m_configCallback = g_pHookSystem->hookDynamic("configReloaded", [this](void* hk, SCallbackInfo& info, std::any param) {
        // bitch ass
        m_config.configuredWidths.clear();

        CConstVarList widths(*PCONFWIDTHS, 0, ',');
        for (auto& w : widths) {
            try {
                m_config.configuredWidths.emplace_back(std::stof(std::string{w}));
            } catch (...) { Log::logger->log(Log::ERR, "scrolling: Failed to parse width {} as float", w); }
        }
        if (m_config.configuredWidths.empty())
            m_config.configuredWidths = {0.333, 0.5, 0.667, 1.0};
    });

    m_focusCallback = g_pHookSystem->hookDynamic("activeWindow", [this](void* hk, SCallbackInfo& info, std::any param) {
        const auto PWINDOW = std::any_cast<PHLWINDOW>(param);

        if (!PWINDOW)
            return;

        static const auto PFOLLOW_FOCUS = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:follow_focus");

        if (!*PFOLLOW_FOCUS)
            return;

        if (!PWINDOW->m_workspace->isVisible())
            return;

        const auto DATA       = dataFor(PWINDOW->m_workspace);
        const auto WINDOWDATA = dataFor(PWINDOW);

        if (!DATA || !WINDOWDATA)
            return;

        static const auto PFOLLOW_DEBOUNCE_MS = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:follow_debounce_ms");
        static CTimer     debounceTimer;
        if (debounceTimer.getMillis() < *PFOLLOW_DEBOUNCE_MS)
            return;
        static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:focus_fit_method");
        if (*PFITMETHOD == 1)
            DATA->fitCol(WINDOWDATA->column.lock());
        else
            DATA->centerCol(WINDOWDATA->column.lock());
        DATA->recalculate();
        debounceTimer.reset();
    });

    for (auto const& w : g_pCompositor->m_windows) {
        if (w->m_isFloating || !w->m_isMapped || w->isHidden())
            continue;

        onWindowCreatedTiling(w);
    }
}

void CScrollingLayout::onDisable() {
    m_workspaceDatas.clear();
    m_configCallback.reset();
}

void CScrollingLayout::onWindowCreatedTiling(PHLWINDOW window, eDirection direction) {
    if (m_columnMoveState.isMovingColumn && window->m_workspace->m_id == m_columnMoveState.targetWorkspaceID)
        return;

    auto workspaceData = dataFor(window->m_workspace);

    if (!workspaceData) {
        Log::logger->log(Log::DEBUG, "[scrolling] No workspace data yet, creating");
        workspaceData       = m_workspaceDatas.emplace_back(makeShared<SWorkspaceData>(window->m_workspace, this));
        workspaceData->self = workspaceData;
    }

    auto droppingOn = Desktop::focusState()->window();

    if (droppingOn == window)
        droppingOn = g_pCompositor->vectorToWindowUnified(g_pInputManager->getMouseCoordsInternal(), Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS);

    SP<SScrollingWindowData> droppingData   = droppingOn ? dataFor(droppingOn) : nullptr;
    SP<SColumnData>          droppingColumn = droppingData ? droppingData->column.lock() : nullptr;

    Log::logger->log(Log::DEBUG, "[scrolling] new window {:x}, droppingColumn: {:x}, columns before: {}", (uintptr_t)window.get(), (uintptr_t)droppingColumn.get(),
                     workspaceData->columns.size());

    if (!droppingColumn) {
        auto col = workspaceData->add();
        col->add(window);
        workspaceData->fitCol(col);
    } else {
        if (window->m_draggingTiled) {
            if (droppingOn) {
                const auto IDX = droppingColumn->idx(droppingOn);
                const auto TOP = droppingOn->getWindowIdealBoundingBoxIgnoreReserved().middle().y > g_pInputManager->getMouseCoordsInternal().y;
                droppingColumn->add(window, TOP ? (IDX == 0 ? -1 : IDX - 1) : (IDX));
            } else
                droppingColumn->add(window);
            workspaceData->fitCol(droppingColumn);
        } else {
            auto idx = workspaceData->idx(droppingColumn);
            auto col = idx == -1 ? workspaceData->add() : workspaceData->add(idx);
            col->add(window);
            workspaceData->fitCol(col);
        }
    }

    workspaceData->recalculate();
}

void CScrollingLayout::onWindowRemovedTiling(PHLWINDOW window) {
    const auto DATA = dataFor(window);

    if (!DATA)
        return;

    const auto WS = DATA->column->workspace.lock();

    if (!WS->next(DATA->column.lock())) {
        // move the view if this is the last column
        const auto USABLE = usableAreaFor(window->m_monitor.lock());
        WS->leftOffset -= USABLE.w * DATA->column->columnWidth;
    }

    DATA->column->remove(window);

    WS->recalculate();

    if (!DATA->column) {
        // column got removed, let's ensure we don't leave any cringe extra space
        const auto USABLE = usableAreaFor(window->m_monitor.lock());
        WS->leftOffset    = std::clamp((double)WS->leftOffset, 0.0, std::max(WS->maxWidth() - USABLE.w, 1.0));
    }
}

bool CScrollingLayout::isWindowTiled(PHLWINDOW window) {
    const auto DATA = dataFor(window);

    return DATA;
}

void CScrollingLayout::recalculateMonitor(const MONITORID& id) {
    const auto PMONITOR = g_pCompositor->getMonitorFromID(id);
    if (!PMONITOR || !PMONITOR->m_activeWorkspace)
        return;

    const auto DATA = dataFor(PMONITOR->m_activeWorkspace);

    if (!DATA)
        return;

    DATA->recalculate();
}

void CScrollingLayout::recalculateWindow(PHLWINDOW window) {
    if (!window->m_workspace)
        return;

    const auto DATA = dataFor(window->m_workspace);

    if (!DATA)
        return;

    DATA->recalculate();
}

void CScrollingLayout::onBeginDragWindow() {
    IHyprLayout::onBeginDragWindow();
}

void CScrollingLayout::resizeActiveWindow(const Vector2D& delta, eRectCorner corner, PHLWINDOW pWindow) {
    const auto PWINDOW  = pWindow ? pWindow : Desktop::focusState()->window();
    Vector2D   modDelta = delta;

    if (!validMapped(PWINDOW))
        return;

    const auto DATA = dataFor(PWINDOW);

    if (!DATA) {
        *PWINDOW->m_realSize = (PWINDOW->m_realSize->goal() + delta)
                                   .clamp(PWINDOW->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}),
                                          PWINDOW->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY}));
        PWINDOW->updateWindowDecos();
        return;
    }

    if (!DATA->column || !DATA->column->workspace || !DATA->column->workspace->workspace || !DATA->column->workspace->workspace->m_monitor)
        return;

    const auto USABLE        = usableAreaFor(DATA->column->workspace->workspace->m_monitor.lock());
    const auto DELTA_AS_PERC = delta / USABLE.size();

    const auto CURR_COLUMN = DATA->column.lock();
    const auto NEXT_COLUMN = DATA->column->workspace->next(CURR_COLUMN);
    const auto PREV_COLUMN = DATA->column->workspace->prev(CURR_COLUMN);

    switch (corner) {
        case CORNER_BOTTOMLEFT:
        case CORNER_TOPLEFT: {
            if (!PREV_COLUMN)
                break;

            PREV_COLUMN->columnWidth = std::clamp(PREV_COLUMN->columnWidth + (float)DELTA_AS_PERC.x, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
            CURR_COLUMN->columnWidth = std::clamp(CURR_COLUMN->columnWidth - (float)DELTA_AS_PERC.x, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
            break;
        }
        case CORNER_BOTTOMRIGHT:
        case CORNER_TOPRIGHT: {
            if (!NEXT_COLUMN)
                break;

            NEXT_COLUMN->columnWidth = std::clamp(NEXT_COLUMN->columnWidth - (float)DELTA_AS_PERC.x, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
            CURR_COLUMN->columnWidth = std::clamp(CURR_COLUMN->columnWidth + (float)DELTA_AS_PERC.x, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
            break;
        }

        default: break;
    }

    if (DATA->column->windowDatas.size() > 1) {
        const auto CURR_WD = DATA;
        const auto NEXT_WD = DATA->column->next(DATA);
        const auto PREV_WD = DATA->column->prev(DATA);
        if (corner == CORNER_NONE) {
            if (!PREV_WD)
                corner = CORNER_BOTTOMRIGHT;
            else {
                corner = CORNER_TOPRIGHT;
                modDelta.y *= -1.0f;
            }
        }

        switch (corner) {
            case CORNER_BOTTOMLEFT:
            case CORNER_BOTTOMRIGHT: {
                if (!NEXT_WD)
                    break;

                if (NEXT_WD->windowSize <= MIN_ROW_HEIGHT && delta.y >= 0)
                    break;

                float adjust = std::clamp((float)(delta.y / USABLE.h), (-CURR_WD->windowSize + MIN_ROW_HEIGHT), (NEXT_WD->windowSize - MIN_ROW_HEIGHT));

                NEXT_WD->windowSize = std::clamp(NEXT_WD->windowSize - adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
                CURR_WD->windowSize = std::clamp(CURR_WD->windowSize + adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
                break;
            }
            case CORNER_TOPLEFT:
            case CORNER_TOPRIGHT: {
                if (!PREV_WD)
                    break;

                if (PREV_WD->windowSize <= MIN_ROW_HEIGHT && modDelta.y <= 0 || CURR_WD->windowSize <= MIN_ROW_HEIGHT && delta.y >= 0)
                    break;

                float adjust = std::clamp((float)(modDelta.y / USABLE.h), -(PREV_WD->windowSize - MIN_ROW_HEIGHT), (CURR_WD->windowSize - MIN_ROW_HEIGHT));

                PREV_WD->windowSize = std::clamp(PREV_WD->windowSize + adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
                CURR_WD->windowSize = std::clamp(CURR_WD->windowSize - adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
                break;
            }

            default: break;
        }
    }

    DATA->column->workspace->recalculate(true);
}

void CScrollingLayout::fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE) {
    const auto PMONITOR   = pWindow->m_monitor.lock();
    const auto PWORKSPACE = pWindow->m_workspace;

    // save position and size if floating
    if (pWindow->m_isFloating && CURRENT_EFFECTIVE_MODE == FSMODE_NONE) {
        pWindow->m_lastFloatingSize     = pWindow->m_realSize->goal();
        pWindow->m_lastFloatingPosition = pWindow->m_realPosition->goal();
        pWindow->m_position             = pWindow->m_realPosition->goal();
        pWindow->m_size                 = pWindow->m_realSize->goal();
    }

    const auto PNODE = dataFor(pWindow);

    if (EFFECTIVE_MODE == FSMODE_NONE) {
        // if it got its fullscreen disabled, set back its node if it had one

        if (PNODE)
            applyNodeDataToWindow(PNODE, false, false, false);
        else {
            // get back its' dimensions from position and size
            *pWindow->m_realPosition = pWindow->m_lastFloatingPosition;
            *pWindow->m_realSize     = pWindow->m_lastFloatingSize;

            pWindow->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT);
            pWindow->updateWindowData();
        }
    } else {
        // apply new pos and size being monitors' box
        if (EFFECTIVE_MODE == FSMODE_FULLSCREEN) {
            *pWindow->m_realPosition = PMONITOR->m_position;
            *pWindow->m_realSize     = PMONITOR->m_size;
        } else {
            // This is a massive hack.
            // We make a fake "only" node and apply
            // To keep consistent with the settings without C+P code

            SP<SScrollingWindowData> fakeNode = makeShared<SScrollingWindowData>(pWindow, nullptr);
            fakeNode->window                  = pWindow;
            fakeNode->layoutBox               = workAreaOnWorkspace(PWORKSPACE);
            pWindow->m_size                   = fakeNode->layoutBox.size();
            fakeNode->ignoreFullscreenChecks  = true;
            fakeNode->overrideWorkspace       = pWindow->m_workspace;

            applyNodeDataToWindow(fakeNode, false, false, false);
        }
    }

    g_pCompositor->changeWindowZOrder(pWindow, true);
}

void CScrollingLayout::focusWindowUpdate(PHLWINDOW pWindow) {
    if (!validMapped(pWindow)) {
        Desktop::focusState()->fullWindowFocus(nullptr);
        return;
    }
    Desktop::focusState()->fullWindowFocus(pWindow);
    const auto WINDOWDATA = dataFor(pWindow);
    if (WINDOWDATA) {
        if (auto col = WINDOWDATA->column.lock())
            col->lastFocusedWindow = WINDOWDATA;
    }
}

SP<SScrollingWindowData> CScrollingLayout::findBestNeighbor(SP<SScrollingWindowData> pCurrent, SP<SColumnData> pTargetCol) {
    if (!pCurrent || !pTargetCol || pTargetCol->windowDatas.empty())
        return nullptr;

    const double                          currentTop    = pCurrent->layoutBox.y;
    const double                          currentBottom = pCurrent->layoutBox.y + pCurrent->layoutBox.h;
    std::vector<SP<SScrollingWindowData>> overlappingWindows;
    for (const auto& candidate : pTargetCol->windowDatas) {
        const double candidateTop    = candidate->layoutBox.y;
        const double candidateBottom = candidate->layoutBox.y + candidate->layoutBox.h;
        const bool   overlaps        = (candidateTop < currentBottom) && (candidateBottom > currentTop);

        if (overlaps)
            overlappingWindows.emplace_back(candidate);
    }
    if (!overlappingWindows.empty()) {
        auto lastFocused = pTargetCol->lastFocusedWindow.lock();

        if (lastFocused) {
            auto it = std::ranges::find(overlappingWindows, lastFocused);
            if (it != overlappingWindows.end())
                return lastFocused;
        }

        auto topmost = std::ranges::min_element(overlappingWindows, std::less<>{}, [](const SP<SScrollingWindowData>& w) { return w->layoutBox.y; });
        return *topmost;
    }
    if (!pTargetCol->windowDatas.empty())
        return pTargetCol->windowDatas.front();
    return nullptr;
}

std::any CScrollingLayout::layoutMessage(SLayoutMessageHeader header, std::string message) {
    static auto centerOrFit = [](const SP<SWorkspaceData> WS, const SP<SColumnData> COL) -> void {
        static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:focus_fit_method");
        if (*PFITMETHOD == 1)
            WS->fitCol(COL);
        else
            WS->centerCol(COL);
    };

    const auto ARGS = CVarList(message, 0, ' ');
    if (ARGS[0] == "move") {
        const auto DATA = currentWorkspaceData();
        if (!DATA)
            return {};

        if (ARGS[1] == "+col" || ARGS[1] == "col") {
            const auto WDATA = dataFor(Desktop::focusState()->window());
            if (!WDATA)
                return {};

            const auto COL = DATA->next(WDATA->column.lock());
            if (!COL) {
                // move to max
                DATA->leftOffset = DATA->maxWidth();
                DATA->recalculate();
                focusWindowUpdate(nullptr);
                return {};
            }

            centerOrFit(DATA, COL);
            DATA->recalculate();

            focusWindowUpdate(COL->windowDatas.front()->window.lock());
            g_pCompositor->warpCursorTo(COL->windowDatas.front()->window.lock()->middle());

            return {};
        } else if (ARGS[1] == "-col") {
            const auto WDATA = dataFor(Desktop::focusState()->window());
            if (!WDATA) {
                if (DATA->columns.size() > 0) {
                    DATA->centerCol(DATA->columns.back());
                    DATA->recalculate();
                    focusWindowUpdate((DATA->columns.back()->windowDatas.back())->window.lock());
                    g_pCompositor->warpCursorTo((DATA->columns.back()->windowDatas.back())->window.lock()->middle());
                }

                return {};
            }

            const auto COL = DATA->prev(WDATA->column.lock());
            if (!COL)
                return {};

            centerOrFit(DATA, COL);
            DATA->recalculate();

            focusWindowUpdate(COL->windowDatas.back()->window.lock());
            g_pCompositor->warpCursorTo(COL->windowDatas.front()->window.lock()->middle());

            return {};
        }

        const auto PLUSMINUS = getPlusMinusKeywordResult(ARGS[1], 0);

        if (!PLUSMINUS.has_value())
            return {};

        DATA->leftOffset -= *PLUSMINUS;
        DATA->recalculate();

        const auto ATCENTER = DATA->atCenter();

        focusWindowUpdate(ATCENTER ? (*ATCENTER->windowDatas.begin())->window.lock() : nullptr);
    } else if (ARGS[0] == "colresize") {
        const auto WDATA = dataFor(Desktop::focusState()->window());

        if (!WDATA)
            return {};

        if (ARGS[1] == "all") {
            float abs = 0;
            try {
                abs = std::stof(ARGS[2]);
            } catch (...) { return {}; }

            for (const auto& c : WDATA->column->workspace->columns) {
                c->columnWidth = abs;
            }

            WDATA->column->workspace->recalculate();
            return {};
        }

        CScopeGuard x([WDATA] {
            WDATA->column->columnWidth = std::clamp(WDATA->column->columnWidth, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
            WDATA->column->workspace->centerOrFitCol(WDATA->column.lock());
            WDATA->column->workspace->recalculate();
        });

        if (ARGS[1][0] == '+' || ARGS[1][0] == '-') {
            if (ARGS[1] == "+conf") {
                for (size_t i = 0; i < m_config.configuredWidths.size(); ++i) {
                    if (m_config.configuredWidths[i] > WDATA->column->columnWidth) {
                        WDATA->column->columnWidth = m_config.configuredWidths[i];
                        break;
                    }

                    if (i == m_config.configuredWidths.size() - 1)
                        WDATA->column->columnWidth = m_config.configuredWidths[0];
                }

                return {};
            } else if (ARGS[1] == "-conf") {
                for (size_t i = m_config.configuredWidths.size() - 1;; --i) {
                    if (m_config.configuredWidths[i] < WDATA->column->columnWidth) {
                        WDATA->column->columnWidth = m_config.configuredWidths[i];
                        break;
                    }

                    if (i == 0) {
                        WDATA->column->columnWidth = m_config.configuredWidths.back();
                        break;
                    }
                }

                return {};
            }

            const auto PLUSMINUS = getPlusMinusKeywordResult(ARGS[1], 0);

            if (!PLUSMINUS.has_value())
                return {};

            WDATA->column->columnWidth += *PLUSMINUS;
        } else {
            float abs = 0;
            try {
                abs = std::stof(ARGS[1]);
            } catch (...) { return {}; }

            WDATA->column->columnWidth = abs;
        }
    } else if (ARGS[0] == "movewindowto") {
        moveWindowTo(Desktop::focusState()->window(), ARGS[1], false);
    } else if (ARGS[0] == "fit") {

        if (ARGS[1] == "active") {
            // fit the current column to 1.F
            const auto WDATA    = dataFor(Desktop::focusState()->window());
            const auto WORKDATA = dataFor(Desktop::focusState()->window()->m_workspace);

            if (!WDATA || !WORKDATA || WORKDATA->columns.size() == 0)
                return {};

            const auto USABLE = usableAreaFor(WORKDATA->workspace->m_monitor.lock());

            WDATA->column->columnWidth = 1.F;

            WORKDATA->leftOffset = 0;
            for (size_t i = 0; i < WORKDATA->columns.size(); ++i) {
                if (WORKDATA->columns[i]->has(Desktop::focusState()->window()))
                    break;

                WORKDATA->leftOffset += USABLE.w * WORKDATA->columns[i]->columnWidth;
            }

            WDATA->column->workspace->recalculate();
        } else if (ARGS[1] == "all") {
            // fit all columns on screen
            const auto WDATA = dataFor(Desktop::focusState()->window()->m_workspace);

            if (!WDATA || WDATA->columns.size() == 0)
                return {};

            const size_t LEN = WDATA->columns.size();
            for (const auto& c : WDATA->columns) {
                c->columnWidth = 1.F / (float)LEN;
            }

            WDATA->recalculate();
        } else if (ARGS[1] == "toend") {
            // fit all columns on screen that start from the current and end on the last
            const auto WDATA = dataFor(Desktop::focusState()->window()->m_workspace);

            if (!WDATA || WDATA->columns.size() == 0)
                return {};

            bool   begun   = false;
            size_t foundAt = 0;
            for (size_t i = 0; i < WDATA->columns.size(); ++i) {
                if (!begun && !WDATA->columns[i]->has(Desktop::focusState()->window()))
                    continue;

                if (!begun) {
                    begun   = true;
                    foundAt = i;
                }

                WDATA->columns[i]->columnWidth = 1.F / (float)(WDATA->columns.size() - i);
            }

            if (!begun)
                return {};

            const auto USABLE = usableAreaFor(WDATA->workspace->m_monitor.lock());

            WDATA->leftOffset = 0;
            for (size_t i = 0; i < foundAt; ++i) {
                WDATA->leftOffset += USABLE.w * WDATA->columns[i]->columnWidth;
            }

            WDATA->recalculate();
        } else if (ARGS[1] == "tobeg") {
            // fit all columns on screen that start from the current and end on the last
            const auto WDATA = dataFor(Desktop::focusState()->window()->m_workspace);

            if (!WDATA || WDATA->columns.size() == 0)
                return {};

            bool   begun   = false;
            size_t foundAt = 0;
            for (int64_t i = (int64_t)WDATA->columns.size() - 1; i >= 0; --i) {
                if (!begun && !WDATA->columns[i]->has(Desktop::focusState()->window()))
                    continue;

                if (!begun) {
                    begun   = true;
                    foundAt = i;
                }

                WDATA->columns[i]->columnWidth = 1.F / (float)(foundAt + 1);
            }

            if (!begun)
                return {};

            WDATA->leftOffset = 0;

            WDATA->recalculate();
        } else if (ARGS[1] == "visible") {
            // fit all columns on screen that start from the current and end on the last
            const auto WDATA = dataFor(Desktop::focusState()->window()->m_workspace);

            if (!WDATA || WDATA->columns.size() == 0)
                return {};

            bool                         begun   = false;
            size_t                       foundAt = 0;
            std::vector<SP<SColumnData>> visible;
            for (size_t i = 0; i < WDATA->columns.size(); ++i) {
                if (!begun && !WDATA->visible(WDATA->columns[i]))
                    continue;

                if (!begun) {
                    begun   = true;
                    foundAt = i;
                }

                if (!WDATA->visible(WDATA->columns[i]))
                    break;

                visible.emplace_back(WDATA->columns[i]);
            }

            if (!begun)
                return {};

            WDATA->leftOffset = 0;

            if (foundAt != 0) {
                const auto USABLE = usableAreaFor(WDATA->workspace->m_monitor.lock());

                for (size_t i = 0; i < foundAt; ++i) {
                    WDATA->leftOffset += USABLE.w * WDATA->columns[i]->columnWidth;
                }
            }

            for (const auto& v : visible) {
                v->columnWidth = 1.F / (float)visible.size();
            }

            WDATA->recalculate();
        }
    } else if (ARGS[0] == "focus") {
        const auto        WDATA       = dataFor(Desktop::focusState()->window());
        static const auto PNOFALLBACK = CConfigValue<Hyprlang::INT>("general:no_focus_fallback");

        if (!WDATA || ARGS[1].empty())
            return {};

        switch (ARGS[1][0]) {
            case 'u':
            case 't': {
                auto PREV = WDATA->column->prev(WDATA);
                if (!PREV) {
                    if (*PNOFALLBACK)
                        break;
                    else
                        PREV = WDATA->column->windowDatas.back();
                }

                focusWindowUpdate(PREV->window.lock());
                g_pCompositor->warpCursorTo(PREV->window.lock()->middle());
                break;
            }

            case 'b':
            case 'd': {
                auto NEXT = WDATA->column->next(WDATA);
                if (!NEXT) {
                    if (*PNOFALLBACK)
                        break;
                    else
                        NEXT = WDATA->column->windowDatas.front();
                }

                focusWindowUpdate(NEXT->window.lock());
                g_pCompositor->warpCursorTo(NEXT->window.lock()->middle());
                break;
            }

            case 'l': {
                auto PREV = WDATA->column->workspace->prev(WDATA->column.lock());
                if (!PREV) {
                    if (*PNOFALLBACK) {
                        centerOrFit(WDATA->column->workspace.lock(), WDATA->column.lock());
                        WDATA->column->workspace->recalculate();
                        g_pCompositor->warpCursorTo(WDATA->window.lock()->middle());
                        break;
                    } else
                        PREV = WDATA->column->workspace->columns.back();
                }

                auto pTargetWindowData = findBestNeighbor(WDATA, PREV);
                if (pTargetWindowData) {
                    focusWindowUpdate(pTargetWindowData->window.lock());
                    centerOrFit(WDATA->column->workspace.lock(), PREV);
                    WDATA->column->workspace->recalculate();
                    g_pCompositor->warpCursorTo(pTargetWindowData->window.lock()->middle());
                }
                break;
            }

            case 'r': {
                auto NEXT = WDATA->column->workspace->next(WDATA->column.lock());
                if (!NEXT) {
                    if (*PNOFALLBACK) {
                        centerOrFit(WDATA->column->workspace.lock(), WDATA->column.lock());
                        WDATA->column->workspace->recalculate();
                        g_pCompositor->warpCursorTo(WDATA->window.lock()->middle());
                        break;
                    } else
                        NEXT = WDATA->column->workspace->columns.front();
                }

                auto pTargetWindowData = findBestNeighbor(WDATA, NEXT);
                if (pTargetWindowData) {
                    focusWindowUpdate(pTargetWindowData->window.lock());
                    centerOrFit(WDATA->column->workspace.lock(), NEXT);
                    WDATA->column->workspace->recalculate();
                    g_pCompositor->warpCursorTo(pTargetWindowData->window.lock()->middle());
                }
                break;
            }

            default: return {};
        }
    } else if (ARGS[0] == "promote") {
        const auto WDATA = dataFor(Desktop::focusState()->window());

        if (!WDATA)
            return {};

        auto idx = WDATA->column->workspace->idx(WDATA->column.lock());
        auto col = idx == -1 ? WDATA->column->workspace->add() : WDATA->column->workspace->add(idx);

        WDATA->column->remove(WDATA->window.lock());

        col->add(WDATA);

        WDATA->column->workspace->recalculate();
    } else if (ARGS[0] == "swapcol") {
        if (ARGS.size() < 2)
            return {};

        const auto WDATA = dataFor(Desktop::focusState()->window());
        if (!WDATA)
            return {};

        const auto CURRENT_COL = WDATA->column.lock();
        if (!CURRENT_COL)
            return {};

        const auto WS_DATA = CURRENT_COL->workspace.lock();
        if (!WS_DATA || WS_DATA->columns.size() < 2)
            return {};

        const int64_t current_idx = WS_DATA->idx(CURRENT_COL);
        const size_t  col_count   = WS_DATA->columns.size();

        if (current_idx == -1)
            return {};

        const std::string& direction  = ARGS[1];
        int64_t            target_idx = -1;

        if (direction == "l")
            target_idx = (current_idx == 0) ? (col_count - 1) : (current_idx - 1);
        else if (direction == "r")
            target_idx = (current_idx == (int64_t)col_count - 1) ? 0 : (current_idx + 1);
        else
            return {};

        std::swap(WS_DATA->columns[current_idx], WS_DATA->columns[target_idx]);
        WS_DATA->centerOrFitCol(CURRENT_COL);
        WS_DATA->recalculate();
    } else if (ARGS[0] == "movecoltoworkspace") {
        if (ARGS.size() < 2)
            return {};

        const auto WDATA = dataFor(Desktop::focusState()->window());
        if (!WDATA)
            return {};

        const auto CURRENT_COL = WDATA->column.lock();
        if (!CURRENT_COL)
            return {};

        const auto SOURCE_WS_DATA = CURRENT_COL->workspace.lock();
        if (!SOURCE_WS_DATA)
            return {};

        const auto PMONITOR = Desktop::focusState()->monitor();
        if (!PMONITOR)
            return {};

        PHLWORKSPACE       PWORKSPACE = nullptr;
        const std::string& arg        = ARGS[1];

        if (arg.starts_with("+") || arg.starts_with("-")) {
            try {
                const int offset             = std::stoi(arg);
                const int currentWorkspaceID = WDATA->window->m_workspace->m_id;
                const int targetWorkspaceID  = currentWorkspaceID + offset;

                if (targetWorkspaceID < 1)
                    return {};

                PWORKSPACE = g_pCompositor->getWorkspaceByID(targetWorkspaceID);
                if (!PWORKSPACE)
                    PWORKSPACE = g_pCompositor->createNewWorkspace(targetWorkspaceID, PMONITOR->m_id);
            } catch (...) { return {}; }
        } else if (arg == "special") {
            const int SPECIAL_WORKSPACE_ID = -99;
            PWORKSPACE                     = g_pCompositor->getWorkspaceByID(SPECIAL_WORKSPACE_ID);
            if (!PWORKSPACE)
                PWORKSPACE = g_pCompositor->createNewWorkspace(SPECIAL_WORKSPACE_ID, PMONITOR->m_id, "special");
        } else {
            PWORKSPACE = g_pCompositor->getWorkspaceByString(arg);
            if (!PWORKSPACE) {
                try {
                    const int workspaceID = std::stoi(arg);
                    PWORKSPACE            = g_pCompositor->getWorkspaceByID(workspaceID);
                    if (!PWORKSPACE)
                        PWORKSPACE = g_pCompositor->createNewWorkspace(workspaceID, PMONITOR->m_id);
                } catch (const std::invalid_argument&) { PWORKSPACE = g_pCompositor->createNewWorkspace(0, PMONITOR->m_id, arg); } catch (const std::out_of_range&) {
                    return {};
                }
            }
        }
        if (!PWORKSPACE)
            return {};

        if (PWORKSPACE == WDATA->window->m_workspace)
            return {};

        auto targetWorkspaceData = dataFor(PWORKSPACE);
        if (!targetWorkspaceData) {
            targetWorkspaceData       = m_workspaceDatas.emplace_back(makeShared<SWorkspaceData>(PWORKSPACE, this));
            targetWorkspaceData->self = targetWorkspaceData;
        }

        const auto NEW_COL = targetWorkspaceData->add();

        NEW_COL->columnWidth = CURRENT_COL->columnWidth;
        NEW_COL->windowDatas = CURRENT_COL->windowDatas;

        for (const auto& wd : NEW_COL->windowDatas) {
            wd->column = NEW_COL;
        }

        std::vector<PHLWINDOW> windowsToMove;
        for (const auto& wd : CURRENT_COL->windowDatas) {
            windowsToMove.push_back(wd->window.lock());
        }

        CURRENT_COL->windowDatas.clear();
        SOURCE_WS_DATA->remove(CURRENT_COL);

        CScopeGuard sg([this]() {
            m_columnMoveState.isMovingColumn    = false;
            m_columnMoveState.targetWorkspaceID = -1;

            for (auto& ws : m_workspaceDatas) {
                ws->recalculate();
            }
        });

        m_columnMoveState.isMovingColumn    = true;
        m_columnMoveState.targetWorkspaceID = PWORKSPACE->m_id;

        for (const auto& win : windowsToMove) {
            g_pCompositor->moveWindowToWorkspaceSafe(win, PWORKSPACE);
        }

        Desktop::focusState()->fullWindowFocus(windowsToMove.front());
        g_pCompositor->warpCursorTo(windowsToMove.front()->middle());
    } else if (ARGS[0] == "togglefit") {
        static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:focus_fit_method");
        auto&             fitMethod  = *PFITMETHOD.ptr();
        const int         toggled    = fitMethod ^ 1;

        fitMethod = toggled;

        const auto        focusedData = dataFor(Desktop::focusState()->window());
        static const auto PFSONONE    = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");

        for (const auto& ws : m_workspaceDatas) {
            if (!ws || ws->columns.empty())
                continue;

            const auto monitor = ws->workspace->m_monitor.lock();
            if (!monitor)
                continue;

            const auto USABLE = usableAreaFor(monitor);

            const auto focusedColumn = (focusedData && focusedData->column && focusedData->column->workspace.lock() == ws) ? focusedData->column.lock() : nullptr;

            const auto fallbackColumn = ws->atCenter();

            if (toggled == 1) {
                const auto columnToFit = focusedColumn ? focusedColumn : fallbackColumn;
                if (!columnToFit)
                    continue;

                double currentLeft = 0.0;
                for (const auto& col : ws->columns) {
                    const double itemWidth = *PFSONONE && ws->columns.size() == 1 ? USABLE.w : USABLE.w * col->columnWidth;

                    if (col == columnToFit) {
                        const double colLeft   = currentLeft;
                        const double colRight  = currentLeft + itemWidth;
                        const double scrollMax = std::max(ws->maxWidth() - USABLE.w, 0.0);
                        double       desiredOffset;

                        if (col == ws->columns.front())
                            desiredOffset = 0.0;
                        else
                            desiredOffset = std::clamp(colRight - USABLE.w, 0.0, scrollMax);

                        ws->leftOffset = desiredOffset;
                        break;
                    }

                    currentLeft += itemWidth;
                }
            } else {
                const auto columnToCenter = focusedColumn ? focusedColumn : fallbackColumn;
                if (!columnToCenter)
                    continue;

                ws->centerCol(columnToCenter);
            }

            ws->recalculate();
        }
    }
    return {};
}

SWindowRenderLayoutHints CScrollingLayout::requestRenderHints(PHLWINDOW a) {
    return {};
}

void CScrollingLayout::switchWindows(PHLWINDOW a, PHLWINDOW b) {
    const auto DATA1 = dataFor(a);
    const auto DATA2 = dataFor(b);

    std::swap(DATA1->window, DATA2->window);

    const auto WS1 = DATA1->column->workspace.lock();
    const auto WS2 = DATA2->column->workspace.lock();

    WS1->recalculate();

    if (WS1 == WS2)
        return;

    WS2->recalculate();
}

void CScrollingLayout::moveWindowTo(PHLWINDOW w, const std::string& dir, bool silent) {
    const auto DATA = dataFor(w);

    if (!DATA)
        return;

    const auto WS = DATA->column->workspace.lock();

    if (dir == "l") {
        const auto COL = WS->prev(DATA->column.lock());

        DATA->column->remove(w);

        if (!COL) {
            const auto NEWCOL = WS->add(-1);
            NEWCOL->add(DATA);
            WS->centerOrFitCol(NEWCOL);
        } else {
            if (COL->windowDatas.size() > 0)
                COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y));
            else
                COL->add(DATA);
            WS->centerOrFitCol(COL);
        }
    } else if (dir == "r") {
        const auto COL = WS->next(DATA->column.lock());

        DATA->column->remove(w);

        if (!COL) {
            // make a new one
            const auto NEWCOL = WS->add();
            NEWCOL->add(DATA);
            WS->centerOrFitCol(NEWCOL);
        } else {
            if (COL->windowDatas.size() > 0)
                COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y));
            else
                COL->add(DATA);
            WS->centerOrFitCol(COL);
        }

    } else if (dir == "t" || dir == "u")
        DATA->column->up(DATA);
    else if (dir == "b" || dir == "d")
        DATA->column->down(DATA);

    WS->recalculate();
    focusWindowUpdate(w);
    g_pCompositor->warpCursorTo(w->middle());
}

void CScrollingLayout::alterSplitRatio(PHLWINDOW, float, bool) {
    ;
}

std::string CScrollingLayout::getLayoutName() {
    return "scrolling";
}

void CScrollingLayout::replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to) {
    ;
}

Vector2D CScrollingLayout::predictSizeForNewWindowTiled() {
    return Vector2D{};
}

SP<SWorkspaceData> CScrollingLayout::dataFor(PHLWORKSPACE ws) {
    for (const auto& e : m_workspaceDatas) {
        if (e->workspace != ws)
            continue;

        return e;
    }

    return nullptr;
}

SP<SScrollingWindowData> CScrollingLayout::dataFor(PHLWINDOW w) {
    if (!w)
        return nullptr;

    for (const auto& e : m_workspaceDatas) {
        if (e->workspace != w->m_workspace)
            continue;

        for (const auto& c : e->columns) {
            for (const auto& d : c->windowDatas) {
                if (d->window != w)
                    continue;

                return d;
            }
        }
    }

    return nullptr;
}

SP<SWorkspaceData> CScrollingLayout::currentWorkspaceData() {
    if (!Desktop::focusState()->monitor() || !Desktop::focusState()->monitor()->m_activeWorkspace)
        return nullptr;

    // FIXME: special

    return dataFor(Desktop::focusState()->monitor()->m_activeWorkspace);
}

CBox CScrollingLayout::usableAreaFor(PHLMONITOR m) {
    return workAreaOnWorkspace(m->m_activeWorkspace).translate(-m->m_position);
}
