#include "Workspace.hpp"
#include "view/Group.hpp"
#include "view/LayerSurface.hpp"
#include "state/FocusState.hpp"
#include "../Compositor.hpp"
#include "../config/shared/parserUtils/ParserUtils.hpp"
#include "../config/shared/animation/AnimationTree.hpp"
#include "../config/shared/workspace/WorkspaceRuleManager.hpp"
#include "../config/supplementary/executor/Executor.hpp"
#include "managers/animation/AnimationManager.hpp"
#include "../managers/EventManager.hpp"
#include "../output/Monitor.hpp"
#include "../state/MonitorState.hpp"
#include "../state/WorkspacePlacementController.hpp"
#include "../state/WorkspaceState.hpp"
#include "../layout/algorithm/Algorithm.hpp"
#include "../layout/space/Space.hpp"
#include "../layout/target/Target.hpp"
#include "../layout/supplementary/WorkspaceAlgoMatcher.hpp"
#include "../event/EventBus.hpp"

#include <hyprutils/animation/AnimatedVariable.hpp>
#include <hyprutils/string/String.hpp>
using namespace Hyprutils::String;
using namespace Desktop::View;

PHLWORKSPACE CWorkspace::create(WORKSPACEID id, PHLMONITOR monitor, std::string name, bool special, bool isEmpty) {
    PHLWORKSPACE workspace = makeShared<CWorkspace>(id, monitor, name, special, isEmpty);
    workspace->init(workspace);
    State::workspaceState()->add(workspace);
    return workspace;
}

CWorkspace::CWorkspace(WORKSPACEID id, PHLMONITOR monitor, std::string name, bool special, bool isEmpty) :
    m_id(id), m_name(name), m_monitor(monitor), m_isSpecialWorkspace(special), m_wasCreatedEmpty(isEmpty) {
    ;
}

void CWorkspace::init(PHLWORKSPACE self) {
    m_self = self;

    g_pAnimationManager->createAnimation(
        Vector2D(0, 0), m_renderOffset, Config::animationTree()->getAnimationPropertyConfig(m_isSpecialWorkspace ? "specialWorkspaceIn" : "workspacesIn"), self, AVARDAMAGE_ENTIRE);
    g_pAnimationManager->createAnimation(1.f, m_alpha, Config::animationTree()->getAnimationPropertyConfig(m_isSpecialWorkspace ? "specialWorkspaceIn" : "workspacesIn"), self,
                                         AVARDAMAGE_ENTIRE);

    const auto RULEFORTHIS = Config::workspaceRuleMgr()->getWorkspaceRuleFor(self).value_or(Config::CWorkspaceRule{});
    if (RULEFORTHIS.m_defaultName.has_value())
        m_name = RULEFORTHIS.m_defaultName.value();
    if (RULEFORTHIS.m_animationStyle.has_value())
        m_animationStyle = RULEFORTHIS.m_animationStyle.value();

    m_focusedWindowHook = Event::bus()->m_events.window.close.listen([this](PHLWINDOW pWindow) {
        if (pWindow == m_lastFocusedWindow.lock())
            m_lastFocusedWindow.reset();
    });

    m_space = Layout::CSpace::create(m_self.lock());
    m_space->setAlgorithmProvider(Layout::Supplementary::algoMatcher()->createAlgorithmForWorkspace(m_self.lock()));

    m_inert = false;

    const auto WORKSPACERULE = Config::workspaceRuleMgr()->getWorkspaceRuleFor(self).value_or(Config::CWorkspaceRule{});
    setPersistent(WORKSPACERULE.m_isPersistent.value_or(false));

    if (self->m_wasCreatedEmpty)
        if (auto cmd = WORKSPACERULE.m_onCreatedEmptyRunCmd)
            Config::Supplementary::executor()->spawnWithRules(*cmd, self);

    g_pEventManager->postEvent({.event = "createworkspace", .data = m_name});
    g_pEventManager->postEvent({.event = "createworkspacev2", .data = std::format("{},{}", m_id, m_name)});
    Event::bus()->m_events.workspace.created.emit(self);
}

CWorkspace::~CWorkspace() {
    Log::logger->log(Log::DEBUG, "Destroying workspace ID {}", m_id);

    if (g_pEventManager) {
        g_pEventManager->postEvent({.event = "destroyworkspace", .data = m_name});
        g_pEventManager->postEvent({.event = "destroyworkspacev2", .data = std::format("{},{}", m_id, m_name)});
    }

    Event::bus()->m_events.workspace.removed.emit(m_self);

    m_events.destroy.emit();
}

PHLWINDOW CWorkspace::getLastFocusedWindow() {
    if (!validMapped(m_lastFocusedWindow) || m_lastFocusedWindow->workspaceID() != m_id)
        return nullptr;

    return m_lastFocusedWindow.lock();
}

PHLWINDOW CWorkspace::getFocusCandidate() {
    auto pWindow = getLastFocusedWindow();

    if (!pWindow)
        pWindow = getTopLeftWindow();

    if (!pWindow)
        pWindow = getFirstWindow();

    return pWindow;
}

std::string CWorkspace::getConfigName() {
    if (m_isSpecialWorkspace) {
        return m_name;
    }

    if (m_id > 0)
        return std::to_string(m_id);

    return "name:" + m_name;
}

bool CWorkspace::matchesStaticSelector(const std::string& selector_) {
    auto selector = trim(selector_);

    if (selector.empty())
        return true;

    if (isNumber(selector)) {
        const auto& [wsid, wsname, isAutoID] = getWorkspaceIDNameFromString(selector);

        if (wsid == WORKSPACE_INVALID)
            return false;

        return wsid == m_id;

    } else if (selector.starts_with("name:")) {
        return m_name == selector.substr(5);
    } else if (selector.starts_with("special")) {
        return m_name == selector;
    } else {
        // parse selector

        for (size_t i = 0; i < selector.length(); ++i) {
            const char& cur = selector[i];
            if (std::isspace(cur))
                continue;

            // Allowed selectors:
            // r - range: r[1-5]
            // s - special: s[true]
            // n - named: n[true] or n[s:string] or n[e:string]
            // m - monitor: m[monitor_selector]
            // w - windowCount: w[1-4] or w[1], optional flag t or f for tiled or floating and
            //                  flag p to count only pinned windows, e.g. w[p1-2], w[pg4]
            //                  flag g to count groups instead of windows, e.g. w[t1-2], w[fg4]
            //                  flag v will count only visible windows
            // f - fullscreen state : f[-1], f[0], f[1], or f[2] for different fullscreen states
            //                        -1: no fullscreen, 0: fullscreen, 1: maximized, 2: fullscreen without sending fs state to window

            const auto  CLOSING_BRACKET = selector.find_first_of(']', i);
            std::string prop            = selector.substr(i, CLOSING_BRACKET == std::string::npos ? std::string::npos : CLOSING_BRACKET + 1 - i);
            i                           = std::min(CLOSING_BRACKET, std::string::npos - 1);

            if (cur == 'r') {
                WORKSPACEID from = 0, to = 0;
                if (!prop.starts_with("r[") || !prop.ends_with("]")) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                prop = prop.substr(2, prop.length() - 3);

                if (!prop.contains("-")) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                const auto DASHPOS = prop.find('-');
                const auto LHS = prop.substr(0, DASHPOS), RHS = prop.substr(DASHPOS + 1);

                if (!isNumber(LHS) || !isNumber(RHS)) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                try {
                    from = std::stoll(LHS);
                    to   = std::stoll(RHS);
                } catch (std::exception& e) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                if (to < from || to < 1 || from < 1) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                if (std::clamp(m_id, from, to) != m_id)
                    return false;
                continue;
            }

            if (cur == 's') {
                if (!prop.starts_with("s[") || !prop.ends_with("]")) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                prop = prop.substr(2, prop.length() - 3);

                const auto SHOULDBESPECIAL = Config::ParserUtils::parseInt(prop);

                if (SHOULDBESPECIAL && sc<bool>(*SHOULDBESPECIAL) != m_isSpecialWorkspace)
                    return false;
                continue;
            }

            if (cur == 'm') {
                if (!prop.starts_with("m[") || !prop.ends_with("]")) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                prop = prop.substr(2, prop.length() - 3);

                const auto PMONITOR = State::monitorState()->query().relativeTo(Desktop::focusState()->monitor()).configString(prop).run();

                if (!(PMONITOR ? PMONITOR == m_monitor : false))
                    return false;
                continue;
            }

            if (cur == 'n') {
                if (!prop.starts_with("n[") || !prop.ends_with("]")) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                prop = prop.substr(2, prop.length() - 3);

                if (prop.starts_with("s:") && !m_name.starts_with(prop.substr(2)))
                    return false;
                if (prop.starts_with("e:") && !m_name.ends_with(prop.substr(2)))
                    return false;

                const auto WANTSNAMED = Config::ParserUtils::parseInt(prop);

                if (WANTSNAMED && *WANTSNAMED != (m_id <= -1337))
                    return false;
                continue;
            }

            if (cur == 'w') {
                WORKSPACEID from = 0, to = 0;
                if (!prop.starts_with("w[") || !prop.ends_with("]")) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                prop = prop.substr(2, prop.length() - 3);

                int  wantsOnlyTiled    = -1;
                int  wantsOnlyPinned   = false;
                bool wantsCountGroup   = false;
                bool wantsCountVisible = false;

                int  flagCount = 0;
                for (auto const& flag : prop) {
                    if (flag == 't' && wantsOnlyTiled == -1) {
                        wantsOnlyTiled = 1;
                        flagCount++;
                    } else if (flag == 'f' && wantsOnlyTiled == -1) {
                        wantsOnlyTiled = 0;
                        flagCount++;
                    } else if (flag == 'p' && !wantsOnlyPinned) {
                        wantsOnlyPinned = true;
                        flagCount++;
                    } else if (flag == 'g' && !wantsCountGroup) {
                        wantsCountGroup = true;
                        flagCount++;
                    } else if (flag == 'v' && !wantsCountVisible) {
                        wantsCountVisible = true;
                        flagCount++;
                    } else {
                        break;
                    }
                }
                prop = prop.substr(flagCount);

                if (!prop.contains("-")) {
                    // try single

                    if (!isNumber(prop)) {
                        Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                        return false;
                    }

                    try {
                        from = std::stoll(prop);
                    } catch (std::exception& e) {
                        Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                        return false;
                    }

                    int count;
                    if (wantsCountGroup)
                        count = getGroups(wantsOnlyTiled == -1 ? std::nullopt : std::optional<bool>(sc<bool>(wantsOnlyTiled)),
                                          wantsOnlyPinned ? std::optional<bool>(wantsOnlyPinned) : std::nullopt,
                                          wantsCountVisible ? std::optional<bool>(wantsCountVisible) : std::nullopt);
                    else
                        count = getWindowCount(wantsOnlyTiled == -1 ? std::nullopt : std::optional<bool>(sc<bool>(wantsOnlyTiled)),
                                               wantsOnlyPinned ? std::optional<bool>(wantsOnlyPinned) : std::nullopt,
                                               wantsCountVisible ? std::optional<bool>(wantsCountVisible) : std::nullopt);

                    if (count != from)
                        return false;
                    continue;
                }

                const auto DASHPOS = prop.find('-');
                const auto LHS = prop.substr(0, DASHPOS), RHS = prop.substr(DASHPOS + 1);

                if (!isNumber(LHS) || !isNumber(RHS)) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                try {
                    from = std::stoll(LHS);
                    to   = std::stoll(RHS);
                } catch (std::exception& e) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                if (to < from || to < 1 || from < 1) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                WORKSPACEID count;
                if (wantsCountGroup)
                    count =
                        getGroups(wantsOnlyTiled == -1 ? std::nullopt : std::optional<bool>(sc<bool>(wantsOnlyTiled)),
                                  wantsOnlyPinned ? std::optional<bool>(wantsOnlyPinned) : std::nullopt, wantsCountVisible ? std::optional<bool>(wantsCountVisible) : std::nullopt);
                else
                    count = getWindowCount(wantsOnlyTiled == -1 ? std::nullopt : std::optional<bool>(sc<bool>(wantsOnlyTiled)),
                                           wantsOnlyPinned ? std::optional<bool>(wantsOnlyPinned) : std::nullopt,
                                           wantsCountVisible ? std::optional<bool>(wantsCountVisible) : std::nullopt);

                if (std::clamp(count, from, to) != count)
                    return false;
                continue;
            }

            if (cur == 'f') {
                if (!prop.starts_with("f[") || !prop.ends_with("]")) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                prop        = prop.substr(2, prop.length() - 3);
                int FSSTATE = -1;
                try {
                    FSSTATE = std::stoi(prop);
                } catch (std::exception& e) {
                    Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
                    return false;
                }

                switch (FSSTATE) {
                    case -1: // no fullscreen
                        if (m_hasFullscreenWindow)
                            return false;
                        break;
                    case 0: // fullscreen full
                        if (!m_hasFullscreenWindow || m_fullscreenMode != FSMODE_FULLSCREEN)
                            return false;
                        break;
                    case 1: // maximized
                        if (!m_hasFullscreenWindow || m_fullscreenMode != FSMODE_MAXIMIZED)
                            return false;
                        break;
                    default: break;
                }
                continue;
            }

            Log::logger->log(Log::DEBUG, "Invalid selector {}", selector);
            return false;
        }

        return true;
    }

    UNREACHABLE();
    return false;
}

void CWorkspace::markInert() {
    m_inert   = true;
    m_id      = WORKSPACE_INVALID;
    m_visible = false;
    m_monitor.reset();
}

bool CWorkspace::inert() {
    return m_inert;
}

MONITORID CWorkspace::monitorID() {
    return m_monitor ? m_monitor->m_id : MONITOR_INVALID;
}

PHLWINDOW CWorkspace::getFullscreenWindow(bool includeLayoutHandledFullscreen) {
    for (auto const& w : Desktop::windowState()->windows()) {
        if (w->m_workspace == m_self && w->isFullscreen()) { // isFullscreen algo gets layout managed fullscreens
            if (!includeLayoutHandledFullscreen && w->m_target->layoutManagedFullscreen())
                continue;

            return w;
        }
    }

    return nullptr;
}

bool CWorkspace::hasFullscreen() {
    if (m_hasFullscreenWindow)
        return true;
    return m_space && m_space->algorithm() && m_space->algorithm()->layoutFullscreenCoversMonitor();
}

bool CWorkspace::isVisible() {
    return m_visible;
}

bool CWorkspace::isVisibleNotCovered() {
    const auto PMONITOR = m_monitor.lock();
    if (!PMONITOR)
        return false;

    if (PMONITOR->m_activeSpecialWorkspace)
        return PMONITOR->m_activeSpecialWorkspace->m_id == m_id;

    return PMONITOR->m_activeWorkspace->m_id == m_id;
}

int CWorkspace::getWindowCount(std::optional<bool> onlyTiled, std::optional<bool> onlyPinned, std::optional<bool> onlyVisible) {
    int no = 0;

    if (!m_space)
        return 0;

    for (auto const& t : m_space->targets()) {
        if (!t)
            continue;

        const auto visibilityFulfilled =
            t->window() && !t->window()->isHidden() && !t->window()->isInputBlocked(INPUT_BLOCK_GROUP_INACTIVE | INPUT_BLOCK_MONOCLE_INACTIVE | INPUT_BLOCK_BELOW_FULLSCREEN);

        if (onlyTiled.has_value() && t->floating() == onlyTiled.value())
            continue;
        if (onlyPinned.has_value() && (!t->window() || t->window()->m_pinned != onlyPinned.value()))
            continue;
        if (onlyVisible.has_value() && (!t->window() || visibilityFulfilled != onlyVisible.value()))
            continue;
        no++;
    }

    return no;
}

int CWorkspace::getGroups(std::optional<bool> onlyTiled, std::optional<bool> onlyPinned, std::optional<bool> onlyVisible) {
    int no = 0;
    for (auto const& g : Desktop::View::groups()) {
        const auto HEAD = g->head();

        const auto visibilityFulfilled =
            g->current() && !g->current()->isHidden() && !g->current()->isInputBlocked(INPUT_BLOCK_GROUP_INACTIVE | INPUT_BLOCK_MONOCLE_INACTIVE | INPUT_BLOCK_BELOW_FULLSCREEN);

        if (HEAD->workspaceID() != m_id || !HEAD->m_isMapped)
            continue;
        if (onlyTiled.has_value() && HEAD->m_isFloating == onlyTiled.value())
            continue;
        if (onlyPinned.has_value() && HEAD->m_pinned != onlyPinned.value())
            continue;
        if (onlyVisible.has_value() && visibilityFulfilled != onlyVisible.value())
            continue;
        no++;
    }
    return no;
}

PHLWINDOW CWorkspace::getFirstWindow() {
    for (auto const& w : Desktop::windowState()->windows()) {
        if (w->m_workspace == m_self && w->m_isMapped && w->acceptsInput())
            return w;
    }

    return nullptr;
}

PHLWINDOW CWorkspace::getTopLeftWindow() {
    const auto PMONITOR = m_monitor.lock();

    for (auto const& w : Desktop::windowState()->windows()) {
        if (w->m_workspace != m_self || !w->m_isMapped || !w->acceptsInput())
            continue;

        const auto WINDOWIDEALBB = w->getWindowIdealBoundingBoxIgnoreReserved();

        if (WINDOWIDEALBB.x <= PMONITOR->m_position.x + 1 && WINDOWIDEALBB.y <= PMONITOR->m_position.y + 1)
            return w;
    }
    return nullptr;
}

bool CWorkspace::hasUrgentWindow() {
    return std::ranges::any_of(Desktop::windowState()->windows(), [this](const auto& w) { return w->m_workspace == m_self && w->m_isMapped && w->m_isUrgent; });
}

void CWorkspace::updateWindowDecos() {
    for (auto const& w : Desktop::windowState()->windows()) {
        if (w->m_workspace != m_self)
            continue;

        w->updateWindowDecos();
    }
}

void CWorkspace::updateWindowData() {
    const auto WORKSPACERULE = Config::workspaceRuleMgr()->getWorkspaceRuleFor(m_self.lock());

    for (auto const& w : Desktop::windowState()->windows()) {
        if (w->m_workspace != m_self)
            continue;

        w->updateWindowData(WORKSPACERULE.value_or(Config::CWorkspaceRule{}));
    }
}

void CWorkspace::forceReportSizesToWindows() {
    for (auto const& w : Desktop::windowState()->windows()) {
        if (w->m_workspace != m_self || !w->m_isMapped || w->isHidden())
            continue;

        w->sendWindowSize(true);
    }
}

void CWorkspace::rename(const std::string& name) {
    if (State::workspaceState()->isSpecial(m_id))
        return;

    Log::logger->log(Log::DEBUG, "CWorkspace::rename: Renaming workspace {} to '{}'", m_id, name);
    m_name = name;

    const auto WORKSPACERULE = Config::workspaceRuleMgr()->getWorkspaceRuleFor(m_self.lock()).value_or(Config::CWorkspaceRule{});
    setPersistent(WORKSPACERULE.m_isPersistent.value_or(false));

    if (WORKSPACERULE.m_isPersistent.value_or(false))
        State::workspacePlacementController()->ensurePersistentWorkspacesPresent(
            std::vector<Config::CWorkspaceRule>{WORKSPACERULE}, m_self.lock(),
            [](PHLWORKSPACE ws, PHLMONITOR mon, bool noWarp) { State::workspacePlacementController()->moveWorkspaceToMonitor(ws, mon, noWarp); });

    g_pEventManager->postEvent({.event = "renameworkspace", .data = std::to_string(m_id) + "," + m_name});
    m_events.renamed.emit();
}

void CWorkspace::updateWindows() {
    m_hasFullscreenWindow = std::ranges::any_of(m_space->targets(), [](const auto& t) { return t && t->fullscreenMode() != FSMODE_NONE && !t->layoutManagedFullscreen(); });

    if (!m_hasFullscreenWindow)
        m_fullscreenMode = FSMODE_NONE;

    for (auto const& t : m_space->targets()) {
        if (t->window())
            t->window()->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_ON_WORKSPACE);
    }
}

void CWorkspace::setPersistent(bool persistent) {
    if (m_persistent == persistent)
        return;

    m_persistent = persistent;

    if (persistent)
        m_selfPersistent = m_self.lock();
    else
        m_selfPersistent.reset();
}

bool CWorkspace::isPersistent() {
    return m_persistent;
}

void CWorkspace::setNoMembersAboveFullscreen() {
    // make all windows and layers on the same workspace under the fullscreen window
    for (auto const& w : Desktop::windowState()->windows()) {
        if (w->m_workspace == m_self && !w->isFullscreen() && !w->m_pinned)
            w->m_createdOverFullscreen = false;
    }
    for (auto const& ls : Desktop::layerState()->layers()) {
        if (ls->m_monitor == m_monitor)
            ls->m_aboveFullscreen = false;
    }
}
