#include "MonocleAlgorithm.hpp"

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

#include "../../../../managers/fullscreen/handler/FullscreenHandler.hpp"

#include "../../../../config/ConfigValue.hpp"
#include "../../../../desktop/state/FocusState.hpp"
#include "../../../../desktop/history/WindowHistoryTracker.hpp"
#include "../../../../output/Monitor.hpp"
#include "../../../../state/MonitorState.hpp"
#include "../../../../event/EventBus.hpp"

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

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

CMonocleAlgorithm::CMonocleAlgorithm() {
    // hook into focus changes to bring focused window to front
    m_focusCallback = Event::bus()->m_events.window.active.listen([this](PHLWINDOW pWindow, Desktop::eFocusReason reason) {
        if (!pWindow)
            return;

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

        const auto TARGET = pWindow->layoutTarget();
        if (!TARGET)
            return;

        focusTargetUpdate(TARGET);
    });
}

CMonocleAlgorithm::~CMonocleAlgorithm() {
    // unhide all windows before destruction
    for (const auto& data : m_targetDatas) {
        const auto TARGET = data->target.lock();
        if (!TARGET)
            continue;

        const auto WINDOW = TARGET->window();
        if (WINDOW) {
            WINDOW->setInputBlocked(Desktop::View::INPUT_BLOCK_MONOCLE_INACTIVE, false);
            *WINDOW->alpha(Desktop::View::WINDOW_ALPHA_LAYOUT) = 1.F;
        }
    }

    m_focusCallback.reset();
}

SP<SMonocleTargetData> CMonocleAlgorithm::dataFor(SP<ITarget> t) {
    for (auto& data : m_targetDatas) {
        if (data->target.lock() == t)
            return data;
    }
    return nullptr;
}

void CMonocleAlgorithm::newTarget(SP<ITarget> target) {
    const auto DATA = m_targetDatas.emplace_back(makeShared<SMonocleTargetData>(target));

    m_currentVisibleIndex = m_targetDatas.size() - 1;

    recalculate();
}

void CMonocleAlgorithm::movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint) {
    newTarget(target);
}

void CMonocleAlgorithm::removeTarget(SP<ITarget> target) {
    auto it = std::ranges::find_if(m_targetDatas, [target](const auto& data) { return data->target.lock() == target; });

    if (it == m_targetDatas.end())
        return;

    // unhide window when removing from monocle layout
    const auto WINDOW = target->window();
    if (WINDOW) {
        WINDOW->setInputBlocked(Desktop::View::INPUT_BLOCK_MONOCLE_INACTIVE, false);
        *WINDOW->alpha(Desktop::View::WINDOW_ALPHA_LAYOUT) = 1.F;
    }

    const auto INDEX = std::distance(m_targetDatas.begin(), it);
    m_targetDatas.erase(it);

    if (m_targetDatas.empty()) {
        m_currentVisibleIndex = 0;
        return;
    }

    // try to use the last window in history if we can
    for (const auto& historyWindow : Desktop::History::windowTracker()->historyForWorkspace(m_parent->space()->workspace()) | std::views::reverse) {
        auto it = std::ranges::find_if(m_targetDatas, [&historyWindow](const auto& d) { return d->target == historyWindow->layoutTarget(); });

        if (it == m_targetDatas.end())
            continue;

        // we found a historical target, use that first
        m_currentVisibleIndex = std::distance(m_targetDatas.begin(), it);

        recalculate();

        return;
    }

    // if we didn't find history, fall back to last

    if (m_currentVisibleIndex >= (int)m_targetDatas.size())
        m_currentVisibleIndex = m_targetDatas.size() - 1;
    else if (INDEX <= m_currentVisibleIndex && m_currentVisibleIndex > 0)
        m_currentVisibleIndex--;

    recalculate();
}

void CMonocleAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
    // monocle layout doesn't support manual resizing, all windows are fullscreen
}

void CMonocleAlgorithm::recalculate(eRecalculateReason reason) {
    if (m_targetDatas.empty() || !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;
    }

    for (size_t i = 0; i < m_targetDatas.size(); ++i) {

        if (!m_targetDatas[i] || !m_targetDatas[i]->target || !m_targetDatas[i]->target->window())
            continue;

        const auto& DATA   = m_targetDatas[i];
        const auto  TARGET = DATA->target.lock();
        const auto  WINDOW = TARGET->window();
        const auto  SPACE  = m_parent->space();

        // Adjust visibility before setting pos so workspace rules like w[tv1] get the correct number of visible windows
        const bool SHOULD_BE_VISIBLE = ((int)i == m_currentVisibleIndex);
        WINDOW->setInputBlocked(Desktop::View::INPUT_BLOCK_MONOCLE_INACTIVE, !SHOULD_BE_VISIBLE);
        *WINDOW->alpha(Desktop::View::WINDOW_ALPHA_LAYOUT) = SHOULD_BE_VISIBLE ? 1.F : 0.F;

        // Need to update work area for the above change to take affect on work area dimensions
        SPACE->recheckWorkArea();
        const auto WORK_AREA = m_parent->space()->workArea();
        DATA->layoutBox      = WORK_AREA;
        TARGET->setPositionGlobal(WORK_AREA);
    }
}

SP<ITarget> CMonocleAlgorithm::getNextCandidate(SP<ITarget> old) {
    if (m_targetDatas.empty())
        return nullptr;

    auto it = std::ranges::find_if(m_targetDatas, [old](const auto& data) { return data->target.lock() == old; });

    if (it == m_targetDatas.end()) {
        if (m_currentVisibleIndex >= 0 && m_currentVisibleIndex < (int)m_targetDatas.size())
            return m_targetDatas[m_currentVisibleIndex]->target.lock();
        return nullptr;
    }

    auto next = std::next(it);
    if (next == m_targetDatas.end())
        next = m_targetDatas.begin();

    return next->get()->target.lock();
}

Config::ErrorResult CMonocleAlgorithm::layoutMsg(const std::string_view& sv) {
    CVarList2 vars(std::string{sv}, 0, 's');

    if (vars.size() < 1)
        return Config::configError("layoutmsg requires at least 1 argument", Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT);

    const auto COMMAND = vars[0];

    if (COMMAND == "cyclenext") {
        cycleNext();
        return {};
    } else if (COMMAND == "cycleprev") {
        cyclePrev();
        return {};
    }

    return Config::configError(std::format("Unknown monocle layoutmsg: {}", COMMAND), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT);
}

std::optional<Vector2D> CMonocleAlgorithm::predictSizeForNewTarget() {
    const auto WORK_AREA = m_parent->space()->workArea();
    return WORK_AREA.size();
}

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

    if (nodeA)
        nodeA->target = b;
    if (nodeB)
        nodeB->target = a;

    recalculate();
}

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

    if (!*PMONITORFALLBACK)
        return; // noop

    // try to find a monitor in the specified direction, thats the logical thing
    if (!t || !t->space() || !t->space()->workspace())
        return;

    const auto PMONITOR  = t->space()->workspace()->m_monitor.lock();
    const auto PMONINDIR = State::monitorState()->query().relativeTo(PMONITOR).inDirection(dir).run();

    // if we found a monitor, move the window there
    if (PMONINDIR && PMONINDIR != PMONITOR) {
        const auto TARGETWS = PMONINDIR->m_activeWorkspace;

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

        t->assignToSpace(TARGETWS->m_space, focalPointForDir(t, dir));
    }
}

void CMonocleAlgorithm::cycleNext() {
    if (m_targetDatas.empty())
        return;

    m_currentVisibleIndex = (m_currentVisibleIndex + 1) % m_targetDatas.size();
    updateVisible();
}

void CMonocleAlgorithm::cyclePrev() {
    if (m_targetDatas.empty())
        return;

    m_currentVisibleIndex--;
    if (m_currentVisibleIndex < 0)
        m_currentVisibleIndex = m_targetDatas.size() - 1;
    updateVisible();
}

void CMonocleAlgorithm::focusTargetUpdate(SP<ITarget> target) {
    auto it = std::ranges::find_if(m_targetDatas, [target](const auto& data) { return data->target.lock() == target; });

    if (it == m_targetDatas.end())
        return;

    const auto NEW_INDEX = std::distance(m_targetDatas.begin(), it);

    if (m_currentVisibleIndex != NEW_INDEX) {
        m_currentVisibleIndex = NEW_INDEX;
        updateVisible();
    }
}

void CMonocleAlgorithm::updateVisible() {
    recalculate();

    const auto VISIBLE_TARGET = getVisibleTarget();
    if (!VISIBLE_TARGET)
        return;

    const auto WINDOW = VISIBLE_TARGET->window();
    if (!WINDOW)
        return;

    Desktop::focusState()->fullWindowFocus(WINDOW, Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE);
}

SP<ITarget> CMonocleAlgorithm::getVisibleTarget() {
    if (m_currentVisibleIndex < 0 || m_currentVisibleIndex >= (int)m_targetDatas.size())
        return nullptr;

    return m_targetDatas[m_currentVisibleIndex]->target.lock();
}
