#include "MonitorZoomController.hpp"

#include <hyprlang.hpp>
#include "../config/ConfigValue.hpp"
#include "../managers/input/InputManager.hpp"
#include "../render/OpenGL.hpp"
#include "desktop/DesktopTypes.hpp"
#include "render/Renderer.hpp"

using namespace Monitor;

void CMonitorZoomController::pinAnchor(const Vector2D& anchor) {
    m_pinnedAnchor = anchor;
    m_anchorPinned = true;
}

void CMonitorZoomController::clearAnchor() {
    m_anchorPinned = false;
}

bool CMonitorZoomController::shouldDamageEntire(float zoomLevel) {
    const bool CHANGED      = zoomLevel != m_lastRenderedZoomLevel;
    m_lastRenderedZoomLevel = zoomLevel;
    return zoomLevel != 1.F || CHANGED;
}

Vector2D CMonitorZoomController::getAnchor(const PHLMONITORREF& monitor) {
    if (m_anchorPinned)
        return m_pinnedAnchor;

    return g_pInputManager->getMouseCoordsInternal() - monitor->m_position;
}

void CMonitorZoomController::zoomWithDetachedCamera(CBox& result, const Render::SRenderData& m_renderData) {
    static auto PZOOMRIGID = CConfigValue<Config::INTEGER>("cursor:zoom_rigid");

    const auto  m      = m_renderData.pMonitor;
    auto        monbox = CBox(0, 0, m->m_size.x, m->m_size.y);
    const auto  ZOOM   = g_pHyprRenderer->m_renderData.mouseZoomFactor;
    const auto  MOUSE  = getAnchor(m);

    if (m_lastZoomLevel != ZOOM) {
        if (m_resetCameraState) {
            m_resetCameraState = false;
            m_camera           = CBox(0, 0, m->m_size.x, m->m_size.y);
            m_lastZoomLevel    = 1.0f;
        }
        const CBox old = m_camera;

        // mouse normalized inside screen (0..1)
        const float mx = MOUSE.x / m->m_size.x;
        const float my = MOUSE.y / m->m_size.y;
        // world-space point under the cursor before zoom
        const float mouseWorldX = old.x + (mx * old.w);
        const float mouseWorldY = old.y + (my * old.h);

        const auto  CAMERAW = monbox.w / ZOOM;
        const auto  CAMERAH = monbox.h / ZOOM;

        // compute new top-left so the same world point stays under the cursor
        const float newX = mouseWorldX - (mx * CAMERAW);
        const float newY = mouseWorldY - (my * CAMERAH);

        m_camera = CBox(newX, newY, CAMERAW, CAMERAH);
        // Detect if this zoom would've caused jerk to keep mouse in view and disable edges if so
        if (!m_camera.copy().scaleFromCenter(.9).containsPoint(MOUSE))
            m_padCamEdges = false;
        m_lastZoomLevel = ZOOM;
    }

    // Keep mouse inside cameraview
    auto smallerbox = m_camera;
    // Prevent zoom step from causing us to jerk to keep mouse in padded camera view,
    // but let us switch to the padded camera once the mouse moves into the safe area
    if (!m_padCamEdges)
        if (smallerbox.copy().scaleFromCenter(.9).containsPoint(MOUSE))
            m_padCamEdges = true;
    if (m_padCamEdges)
        smallerbox.scaleFromCenter(.9);
    // when rigid, lock the camera at the zoom anchor instead of panning to follow the cursor to its edges
    if (!*PZOOMRIGID && !smallerbox.containsPoint(MOUSE)) {
        if (MOUSE.x < smallerbox.x)
            m_camera.x -= smallerbox.x - MOUSE.x;
        if (MOUSE.y < smallerbox.y)
            m_camera.y -= smallerbox.y - MOUSE.y;
        if (MOUSE.y > smallerbox.y + smallerbox.h)
            m_camera.y += MOUSE.y - (smallerbox.y + smallerbox.h);
        if (MOUSE.x > smallerbox.x + smallerbox.w)
            m_camera.x += MOUSE.x - (smallerbox.x + smallerbox.w);
    }

    auto z = ZOOM * m->m_scale;
    monbox.scale(z).translate(-m_camera.pos() * z);

    result = monbox;
}

void CMonitorZoomController::applyZoomTransform(CBox& monbox, const Render::SRenderData& m_renderData) {
    static auto PZOOMRIGID          = CConfigValue<Config::INTEGER>("cursor:zoom_rigid");
    static auto PZOOMDETACHEDCAMERA = CConfigValue<Config::INTEGER>("cursor:zoom_detached_camera");
    const auto  ZOOM                = g_pHyprRenderer->m_renderData.mouseZoomFactor;

    if (ZOOM == 1.F)
        return;

    const auto m        = m_renderData.pMonitor;
    const auto ORIGINAL = monbox;
    const auto INITANIM = m->m_zoomAnimProgress->value() != 1.0;

    if (*PZOOMDETACHEDCAMERA && !INITANIM)
        zoomWithDetachedCamera(monbox, m_renderData);
    else {
        const auto ZOOMCENTER = g_pHyprRenderer->m_renderData.mouseZoomUseMouse ? getAnchor(m) * m->m_scale : m->m_transformedSize / 2.f;

        monbox.translate(-ZOOMCENTER).scale(ZOOM).translate(*PZOOMRIGID ? m->m_transformedSize / 2.0 : ZOOMCENTER);
    }

    monbox.x = std::min(monbox.x, 0.0);
    monbox.y = std::min(monbox.y, 0.0);
    if (monbox.x + monbox.width < ORIGINAL.w)
        monbox.x = ORIGINAL.w - monbox.width;
    if (monbox.y + monbox.height < ORIGINAL.h)
        monbox.y = ORIGINAL.h - monbox.height;
}
