#include "LuaBindingsInternal.hpp"

#include "Check.hpp"

#include "../objects/LuaLayerSurface.hpp"
#include "../objects/LuaMonitor.hpp"
#include "../objects/LuaWindow.hpp"
#include "../objects/LuaWorkspace.hpp"

#include "../../../desktop/history/WindowHistoryTracker.hpp"
#include "../../../desktop/history/WorkspaceHistoryTracker.hpp"
#include "../../../desktop/rule/windowRule/WindowRuleEffectContainer.hpp"
#include "../../../desktop/view/LayerSurface.hpp"
#include "../../../desktop/view/Window.hpp"
#include "../../../managers/input/InputManager.hpp"
#include "../../../state/MonitorState.hpp"
#include "../../../state/WorkspaceState.hpp"

using namespace Config;
using namespace Config::Lua;
using namespace Config::Lua::Bindings;

namespace {
    struct SWindowQuery {
        std::optional<PHLMONITOR>   monitor;
        std::optional<PHLWORKSPACE> workspace;
        std::optional<bool>         floating;
        std::optional<bool>         mapped;
        std::optional<std::string>  className;
        std::optional<std::string>  title;
        std::optional<std::string>  tag;
    };

    struct SLayerQuery {
        std::optional<PHLMONITOR>  monitor;
        std::optional<std::string> namespace_;
    };
}

static bool windowMatchesQuery(const PHLWINDOW& w, const SWindowQuery& query) {
    if (query.monitor) {
        const auto mon = w->m_monitor.lock();
        if (mon != *query.monitor)
            return false;
    }

    if (query.workspace && w->m_workspace != *query.workspace)
        return false;

    if (query.floating && w->m_isFloating != *query.floating)
        return false;

    if (query.mapped && w->m_isMapped != *query.mapped)
        return false;

    if (query.className && w->m_class != *query.className)
        return false;

    if (query.title && w->m_title != *query.title)
        return false;

    if (query.tag) {
        if (!w->m_ruleApplicator || !w->m_ruleApplicator->m_tagKeeper.isTagged(*query.tag, true))
            return false;
    }

    return true;
}

static void pushWindowsMatchingQuery(lua_State* L, const SWindowQuery& query) {
    lua_newtable(L);

    int i = 1;
    for (const auto& w : Desktop::windowState()->windows()) {
        if (!windowMatchesQuery(w, query))
            continue;

        Objects::CLuaWindow::push(L, w);
        lua_rawseti(L, -2, i++);
    }
}

static void parseWindowQueryFromTable(lua_State* L, int idx, const char* fnName, SWindowQuery& query) {
    query.monitor   = Internal::tableOptMonitor(L, idx, "monitor", fnName);
    query.workspace = Internal::tableOptWorkspace(L, idx, "workspace", fnName);

    if (const auto floating = Internal::tableOptBool(L, idx, "floating"); floating.has_value())
        query.floating = floating;

    if (const auto mapped = Internal::tableOptBool(L, idx, "mapped"); mapped.has_value())
        query.mapped = mapped;

    query.className = Internal::tableOptStr(L, idx, "class");
    query.title     = Internal::tableOptStr(L, idx, "title");
    query.tag       = Internal::tableOptStr(L, idx, "tag");
}

static void parseLayerQueryFromTable(lua_State* L, int idx, const char* fnName, SLayerQuery& query) {
    query.monitor    = Internal::tableOptMonitor(L, idx, "monitor", fnName);
    query.namespace_ = Internal::tableOptStr(L, idx, "namespace");
}

static PHLMONITOR monitorFromOptionalArg(lua_State* L, int idx, const char* fnName) {
    if (lua_gettop(L) < idx || lua_isnil(L, idx))
        return Desktop::focusState()->monitor();

    return Internal::monitorFromLuaSelectorOrObject(L, idx, fnName);
}

static int hlGetWindows(lua_State* L) {
    SWindowQuery query;
    query.mapped = true;

    if (lua_gettop(L) >= 1 && !lua_isnil(L, 1)) {
        if (!lua_istable(L, 1))
            return Internal::configError(L, "hl.get_windows: expected no args or a table of filters");

        parseWindowQueryFromTable(L, 1, "hl.get_windows", query);
    }

    pushWindowsMatchingQuery(L, query);
    return 1;
}

static int hlGetActiveWindow(lua_State* L) {
    auto PWINDOW = Desktop::focusState()->window();
    if (!PWINDOW) {
        lua_pushnil(L);
        return 1;
    }

    Config::Lua::Objects::CLuaWindow::push(L, PWINDOW);
    return 1;
}

static int hlGetWindow(lua_State* L) {
    const auto PWINDOW = Internal::windowFromLuaSelectorOrObject(L, 1, "hl.get_window");
    if (!PWINDOW) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaWindow::push(L, PWINDOW);
    return 1;
}

static int hlGetUrgentWindow(lua_State* L) {
    const auto PWINDOW = Desktop::viewState()->query().urgent().runWindow();
    if (!PWINDOW) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaWindow::push(L, PWINDOW);
    return 1;
}

static int hlGetWorkspaces(lua_State* L) {
    lua_newtable(L);
    int i = 1;
    for (const auto& wsRef : State::workspaceState()->workspaces()) {
        const auto ws = wsRef.lock();
        if (!ws || ws->inert())
            continue;
        Objects::CLuaWorkspace::push(L, ws);
        lua_rawseti(L, -2, i++);
    }
    return 1;
}

static int hlGetWorkspace(lua_State* L) {
    const auto PWORKSPACE = Internal::workspaceFromLuaSelectorOrObject(L, 1, "hl.get_workspace");
    if (!PWORKSPACE) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaWorkspace::push(L, PWORKSPACE);
    return 1;
}

static int hlGetActiveWorkspace(lua_State* L) {
    const auto PMONITOR = monitorFromOptionalArg(L, 1, "hl.get_active_workspace");
    if (!PMONITOR || !PMONITOR->m_activeWorkspace) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaWorkspace::push(L, PMONITOR->m_activeWorkspace);
    return 1;
}

static int hlGetActiveSpecialWorkspace(lua_State* L) {
    const auto PMONITOR = monitorFromOptionalArg(L, 1, "hl.get_active_special_workspace");
    if (!PMONITOR || !PMONITOR->m_activeSpecialWorkspace) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaWorkspace::push(L, PMONITOR->m_activeSpecialWorkspace);
    return 1;
}

static int hlGetMonitors(lua_State* L) {
    lua_newtable(L);
    int i = 1;
    for (const auto& mon : State::monitorState()->monitors()) {
        Objects::CLuaMonitor::push(L, mon);
        lua_rawseti(L, -2, i++);
    }
    return 1;
}

static int hlGetMonitor(lua_State* L) {
    const auto PMONITOR = Internal::monitorFromLuaSelectorOrObject(L, 1, "hl.get_monitor");
    if (!PMONITOR) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaMonitor::push(L, PMONITOR);
    return 1;
}

static int hlGetActiveMonitor(lua_State* L) {
    const auto PMONITOR = Desktop::focusState()->monitor();
    if (!PMONITOR) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaMonitor::push(L, PMONITOR);
    return 1;
}

static int hlGetMonitorAt(lua_State* L) {
    double x = 0;
    double y = 0;

    if (lua_istable(L, 1)) {
        const auto tx = Internal::tableOptNum(L, 1, "x");
        const auto ty = Internal::tableOptNum(L, 1, "y");
        if (!tx || !ty)
            return Internal::configError(L, "hl.get_monitor_at: expected a table { x, y }");

        x = *tx;
        y = *ty;
    } else {
        const auto tx = Check::number(L, 1);
        if (!tx)
            return Internal::configError(L, std::format("get_monitor_at: bad argument 1: {}", tx.error()));

        const auto ty = Check::number(L, 2);
        if (!ty)
            return Internal::configError(L, std::format("get_monitor_at: bad argument 2: {}", ty.error()));

        x = *tx;
        y = *ty;
    }

    const auto PMONITOR = State::monitorState()->query().vec(Vector2D{x, y}).run();
    if (!PMONITOR) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaMonitor::push(L, PMONITOR);
    return 1;
}

static int hlGetMonitorAtCursor(lua_State* L) {
    const auto PMONITOR = State::monitorState()->query().vec(g_pInputManager->getMouseCoordsInternal()).run();
    if (!PMONITOR) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaMonitor::push(L, PMONITOR);
    return 1;
}

static int hlGetCursorPos(lua_State* L) {
    if (!g_pInputManager) {
        lua_pushnil(L);
        return 1;
    }

    const auto pos = g_pInputManager->getMouseCoordsInternal();

    lua_newtable(L);
    lua_pushnumber(L, pos.x);
    lua_setfield(L, -2, "x");
    lua_pushnumber(L, pos.y);
    lua_setfield(L, -2, "y");
    return 1;
}

static int hlGetLastWindow(lua_State* L) {
    const auto  current     = Desktop::focusState()->window();
    const auto& fullHistory = Desktop::History::windowTracker()->fullHistory();

    for (auto it = fullHistory.rbegin(); it != fullHistory.rend(); ++it) {
        const auto candidate = it->lock();
        if (!candidate || !candidate->m_isMapped)
            continue;

        if (current && candidate == current)
            continue;

        Objects::CLuaWindow::push(L, candidate);
        return 1;
    }

    lua_pushnil(L);
    return 1;
}

static int hlGetLastWorkspace(lua_State* L) {
    const bool hadMonitorArg = lua_gettop(L) >= 1 && !lua_isnil(L, 1);
    const auto PMONITOR      = hadMonitorArg ? Internal::monitorFromLuaSelectorOrObject(L, 1, "hl.get_last_workspace") : Desktop::focusState()->monitor();
    if (!PMONITOR || !PMONITOR->m_activeWorkspace) {
        lua_pushnil(L);
        return 1;
    }

    const auto current = PMONITOR->m_activeWorkspace;

    auto previous = hadMonitorArg ? Desktop::History::workspaceTracker()->previousWorkspace(current, PMONITOR) : Desktop::History::workspaceTracker()->previousWorkspace(current);

    auto ws = previous.workspace.lock();
    if ((!ws || ws->inert()) && previous.id != WORKSPACE_INVALID)
        ws = State::workspaceState()->query().id(previous.id).run();

    if (!ws || ws->inert()) {
        lua_pushnil(L);
        return 1;
    }

    Objects::CLuaWorkspace::push(L, ws);
    return 1;
}

static int hlGetLayers(lua_State* L) {
    SLayerQuery query;

    if (lua_gettop(L) >= 1 && !lua_isnil(L, 1)) {
        if (!lua_istable(L, 1))
            return Internal::configError(L, "hl.get_layers: expected no args or a table of filters");

        parseLayerQueryFromTable(L, 1, "hl.get_layers", query);
    }

    lua_newtable(L);
    int i = 1;
    for (const auto& mon : State::monitorState()->monitors()) {
        if (query.monitor && mon != *query.monitor)
            continue;

        for (const auto& level : mon->m_layerSurfaceLayers) {
            for (const auto& lsRef : level) {
                const auto ls = lsRef.lock();
                if (!ls)
                    continue;

                if (query.namespace_ && ls->m_namespace != *query.namespace_)
                    continue;

                Objects::CLuaLayerSurface::push(L, ls);
                lua_rawseti(L, -2, i++);
            }
        }
    }
    return 1;
}

static int hlGetWorkspaceWindows(lua_State* L) {
    const auto   ws = Internal::workspaceFromLuaSelectorOrObject(L, 1, "hl.get_workspace_windows");

    SWindowQuery query;
    query.workspace = ws;
    query.mapped    = true;

    pushWindowsMatchingQuery(L, query);
    return 1;
}

static int hlGetCurrentSubmap(lua_State* L) {
    lua_pushstring(L, Config::Actions::state()->m_currentSubmap.c_str());
    return 1;
}

void Internal::registerQueryBindings(lua_State* L) {
    Internal::setFn(L, "get_windows", hlGetWindows);
    Internal::setFn(L, "get_window", hlGetWindow);
    Internal::setFn(L, "get_active_window", hlGetActiveWindow);
    Internal::setFn(L, "get_urgent_window", hlGetUrgentWindow);
    Internal::setFn(L, "get_workspaces", hlGetWorkspaces);
    Internal::setFn(L, "get_workspace", hlGetWorkspace);
    Internal::setFn(L, "get_active_workspace", hlGetActiveWorkspace);
    Internal::setFn(L, "get_active_special_workspace", hlGetActiveSpecialWorkspace);
    Internal::setFn(L, "get_monitors", hlGetMonitors);
    Internal::setFn(L, "get_monitor", hlGetMonitor);
    Internal::setFn(L, "get_active_monitor", hlGetActiveMonitor);
    Internal::setFn(L, "get_monitor_at", hlGetMonitorAt);
    Internal::setFn(L, "get_monitor_at_cursor", hlGetMonitorAtCursor);
    Internal::setFn(L, "get_layers", hlGetLayers);
    Internal::setFn(L, "get_workspace_windows", hlGetWorkspaceWindows);
    Internal::setFn(L, "get_cursor_pos", hlGetCursorPos);
    Internal::setFn(L, "get_last_window", hlGetLastWindow);
    Internal::setFn(L, "get_last_workspace", hlGetLastWorkspace);
    Internal::setFn(L, "get_current_submap", hlGetCurrentSubmap);
}
