#include "ViewportTile.h"
#include "../utils/Logger.h"

#include <pxr/usd/usdGeom/camera.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdGeom/xformable.h>
#include <pxr/usd/usdGeom/bboxCache.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/base/gf/bbox3d.h>
#include <pxr/base/gf/frustum.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/imaging/cameraUtil/conformWindow.h>
#include <imgui.h>

#include <algorithm>
#include <cmath>
#include <map>
#include <string>

namespace UsdLayerManager {

// ---------------------------------------------------------------------------
ViewportTile::ViewportTile() = default;
ViewportTile::~ViewportTile() = default;

// ---------------------------------------------------------------------------
void ViewportTile::SetCommandHistory(CommandHistory* h)
{
    m_commandHistory = h;
    // The TransformManipulator's command history is set by the container
    // (ViewportPanel), not by individual tiles.
}

void ViewportTile::SetTimeCodes(pxr::UsdTimeCode displayTime,
                                pxr::UsdTimeCode editTime)
{
    m_displayTime = displayTime;
    m_editTime    = editTime;
    m_renderer.SetCurrentTimeCode(displayTime);
    m_renderer.SetForceRefresh(true);
}

void ViewportTile::SetStage(pxr::UsdStageRefPtr stage)
{
    m_stage = stage;
    m_renderer.SetStage(stage);
    m_camera.SetStage(stage);
    m_selectedCameraIndex     = 0;
    m_cameraListDirty         = true;
    m_hasLastGfCamera         = false;
    m_hasSavedFreeCameraState = false;
    m_isDrivingUsdCamPrim     = false;
    m_drivenUsdCamPath        = pxr::SdfPath();
    m_camera.SwitchToFreeCamera();

    if (stage) {
        pxr::TfTokenVector purposes = {
            pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
        pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true);
        pxr::GfBBox3d stageBBox = bboxCache.ComputeWorldBound(stage->GetPseudoRoot());
        if (!stageBBox.GetRange().IsEmpty())
            m_camera.FrameSelection(stageBBox, 1.1);
    }
}

// ---------------------------------------------------------------------------
void ViewportTile::SetSelectedPaths(const pxr::SdfPathVector& paths,
                                    const std::string&         primaryPath)
{
    m_selectedSdfPaths = paths;
    m_selectedPrimPath = primaryPath;
    m_renderer.SetSelectedPaths(paths);
}

// ---------------------------------------------------------------------------
void ViewportTile::FrameScene()
{
    if (!m_stage) return;
    pxr::TfTokenVector purposes = {
        pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
    pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true);
    pxr::GfBBox3d stageBBox = bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot());
    if (!stageBBox.GetRange().IsEmpty()) {
        InitCameraNavigation();
        m_camera.FrameSelection(stageBBox, 1.1);
    }
}

// ---------------------------------------------------------------------------
ViewportTileSettings ViewportTile::GetSettings() const
{
    ViewportTileSettings s;
    s.renderDelegate   = m_renderer.GetCurrentRendererId().GetString();
    s.showGrid         = m_renderer.ShowGrid();
    s.showCameraGuide  = m_renderer.ShowCameraGuide();
    s.aov              = m_renderer.GetCurrentAov().GetString();
    s.showGuides       = m_renderer.ShowGuides();
    s.showProxy        = m_renderer.ShowProxy();
    s.showRender       = m_renderer.ShowRender();
    s.aaEnabled        = m_renderer.GetAAEnabled();
    const pxr::GfVec3f& bg = m_renderer.GetBackgroundColor();
    s.bgColorR = bg[0]; s.bgColorG = bg[1]; s.bgColorB = bg[2];
    s.bboxMode         = static_cast<int>(m_renderer.GetBBoxMode());
    s.ambientLightOnly = m_renderer.GetAmbientLightOnly();
    s.domeLightEnabled = m_renderer.GetDomeLightEnabled();
    s.shadingMode      = static_cast<int>(m_renderer.GetShadingMode());
    s.cullStyle        = static_cast<int>(m_renderer.GetCullStyle());
    return s;
}

void ViewportTile::ApplySettings(const ViewportTileSettings& s)
{
    if (!s.renderDelegate.empty())
        m_renderer.SetRendererPlugin(pxr::TfToken(s.renderDelegate));
    m_renderer.SetShowGrid(s.showGrid);
    m_renderer.SetShowCameraGuide(s.showCameraGuide);
    if (!s.aov.empty())
        m_renderer.SetCurrentAov(pxr::TfToken(s.aov));
    m_renderer.SetShowGuides(s.showGuides);
    m_renderer.SetShowProxy(s.showProxy);
    m_renderer.SetShowRender(s.showRender);
    m_renderer.SetAAEnabled(s.aaEnabled);
    m_renderer.SetBackgroundColor(pxr::GfVec3f(s.bgColorR, s.bgColorG, s.bgColorB));
    m_renderer.SetBBoxMode(static_cast<BBoxMode>(s.bboxMode));
    m_renderer.SetAmbientLightOnly(s.ambientLightOnly);
    m_renderer.SetDomeLightEnabled(s.domeLightEnabled);
    m_renderer.SetShadingMode(static_cast<ShadingMode>(s.shadingMode));
    m_renderer.SetCullStyle(static_cast<CullStyle>(s.cullStyle));
}

// ---------------------------------------------------------------------------
void ViewportTile::RefreshCameraList()
{
    m_cameraPaths.clear();
    if (m_stage) {
        for (pxr::UsdPrim prim : m_stage->Traverse())
            if (prim.IsA<pxr::UsdGeomCamera>())
                m_cameraPaths.push_back(prim.GetPath());
    }
    m_cameraListDirty = false;
}

void ViewportTile::TrySwitchToFreeCamera()
{
    if (m_camera.GetMode() != ViewportCamera::CameraMode::UsdCamera) return;
    m_selectedCameraIndex = 0;
    m_isDrivingUsdCamPrim = false;
    if (m_hasSavedFreeCameraState)
        m_camera.SwitchToFreeCamera(&m_savedFreeCameraState);
    else
        m_camera.SwitchToFreeCamera(m_hasLastGfCamera ? &m_lastComputedGfCamera : nullptr);
}

void ViewportTile::InitCameraNavigation()
{
    if (m_camera.GetMode() != ViewportCamera::CameraMode::UsdCamera) return;
    pxr::SdfPath camPath = m_camera.GetUsdCameraPath();
    if (!m_stage || camPath.IsEmpty()) { TrySwitchToFreeCamera(); return; }
    pxr::UsdPrim prim = m_stage->GetPrimAtPath(camPath);
    if (!prim || !prim.IsA<pxr::UsdGeomCamera>()) { TrySwitchToFreeCamera(); return; }
    pxr::UsdGeomCamera usdCam(prim);
    pxr::GfCamera gfCam = usdCam.GetCamera(m_displayTime);
    m_camera.SwitchToFreeCamera(&gfCam);

    // SwitchToFreeCamera calls PullFromCameraTransform, which extracts orbital
    // theta/phi via Euler decomposition and uses GetFocusDistance() as m_dist.
    // Both are wrong for a USD camera prim: the focus distance is a DOF attribute
    // (defaults to 5 units) and the Euler decomposition produces gimbal-affected
    // angles when the camera has roll or an unusual orientation.
    // Fix: compute orbit center from the scene bbox, then call
    // InitOrbitFromEyeAndCenter to derive theta/phi from geometry with zero roll.
    {
        pxr::TfTokenVector purposes = {
            pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
        pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true);
        pxr::GfBBox3d stageBBox =
            bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot());
        if (!stageBBox.GetRange().IsEmpty()) {
            pxr::GfVec3d sceneCenter = stageBBox.ComputeCentroid();
            pxr::GfVec3d camEye      = gfCam.GetFrustum().GetPosition();
            pxr::GfVec3d viewDir     = gfCam.GetFrustum().ComputeViewDirection();

            // Project scene centre onto the view ray for the orbit distance;
            // keeps the camera at its authored position.
            double dist = pxr::GfDot(sceneCenter - camEye, viewDir);
            if (dist > 0.01)
                m_camera.InitOrbitFromEyeAndCenter(
                    camEye, camEye + dist * viewDir, dist);
        }
    }

    m_isDrivingUsdCamPrim = true;
    m_drivenUsdCamPath    = camPath;
}

pxr::GfVec3d ViewportTile::ComputeGizmoPivot() const
{
    if (m_selectedSdfPaths.empty() || !m_stage) return pxr::GfVec3d(0.0);
    pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_selectedSdfPaths.front());
    if (!prim) return pxr::GfVec3d(0.0);

    pxr::UsdGeomXformCache xformCache(m_displayTime);

    // Primary: use XformCommonAPI to read the authored translate and pivot op.
    // The manipulator must sit at the pivot's world-space position:
    //   worldPivot = parentToWorld.Transform(translate + pivot)
    // NOT at the bounding-box center (geometry centroid) and NOT at
    // worldMatrix.ExtractTranslation() which folds in the pivot-inverse op
    // and gives the geometry origin, not the authored pivot point.
    pxr::UsdGeomXformCommonAPI api(prim);
    pxr::GfVec3d translate;
    pxr::GfVec3f rotation, scale, pivot;
    pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder;
    if (api.GetXformVectors(&translate, &rotation, &scale, &pivot,
                            &rotOrder, m_displayTime))
    {
        pxr::GfVec3d localPivot = translate + pxr::GfVec3d(pivot[0], pivot[1], pivot[2]);
        pxr::UsdPrim parent = prim.GetParent();
        if (parent && !parent.IsPseudoRoot()) {
            pxr::GfMatrix4d p2w = xformCache.GetLocalToWorldTransform(parent);
            return p2w.Transform(localPivot);
        }
        return localPivot; // root-level prim: parent space == world space
    }

    // Fallback for incompatible op stacks (e.g. xformOp:transform matrix ops):
    // use the prim's world-space origin from the local-to-world matrix.
    pxr::GfMatrix4d worldXform = xformCache.GetLocalToWorldTransform(prim);
    return worldXform.ExtractTranslation();
}

// ---------------------------------------------------------------------------
// BuildOrthoCamera
// ---------------------------------------------------------------------------
// Camera-to-world rotation matrices (row-major GfMatrix4d: rows are cam axes).
// Right-hand rule verified: cross(cam_X, cam_Y) == cam_Z for every entry.
//
// Y-up (isZUp=false)
//  Top    cam_X=(1,0,0)  cam_Y=(0,0,-1) cam_Z=(0,1,0)  eye=c+(0,+d,0)
//  Bottom cam_X=(1,0,0)  cam_Y=(0,0,+1) cam_Z=(0,-1,0) eye=c+(0,-d,0)
//  Front  cam_X=(1,0,0)  cam_Y=(0,1,0)  cam_Z=(0,0,+1) eye=c+(0,0,+d)
//  Back   cam_X=(-1,0,0) cam_Y=(0,1,0)  cam_Z=(0,0,-1) eye=c+(0,0,-d)
//  Left   cam_X=(0,0,+1) cam_Y=(0,1,0)  cam_Z=(-1,0,0) eye=c+(-d,0,0)
//  Right  cam_X=(0,0,-1) cam_Y=(0,1,0)  cam_Z=(+1,0,0) eye=c+(+d,0,0)
//
// Z-up (isZUp=true)
//  Top    cam_X=(1,0,0)  cam_Y=(0,1,0)  cam_Z=(0,0,+1) eye=c+(0,0,+d)
//  Bottom cam_X=(1,0,0)  cam_Y=(0,-1,0) cam_Z=(0,0,-1) eye=c+(0,0,-d)
//  Front  cam_X=(1,0,0)  cam_Y=(0,0,+1) cam_Z=(0,-1,0) eye=c+(0,-d,0)
//  Back   cam_X=(-1,0,0) cam_Y=(0,0,+1) cam_Z=(0,+1,0) eye=c+(0,+d,0)
//  Left   cam_X=(0,-1,0) cam_Y=(0,0,+1) cam_Z=(-1,0,0) eye=c+(-d,0,0)
//  Right  cam_X=(0,+1,0) cam_Y=(0,0,+1) cam_Z=(+1,0,0) eye=c+(+d,0,0)
// ---------------------------------------------------------------------------
pxr::GfCamera ViewportTile::BuildOrthoCamera() const
{
    const bool   zUp  = m_camera.IsZUp();
    const double d    = m_camera.GetDist();
    const pxr::GfVec3d c = m_camera.GetFocalPoint();
    const double aspect  = (m_viewWidth > 0 && m_viewHeight > 0)
        ? static_cast<double>(m_viewWidth) / static_cast<double>(m_viewHeight)
        : 1.0;

    pxr::GfCamera cam;

    // Visible vertical world span = d*2.  Scales with zoom/framing naturally.
    cam.SetOrthographicFromAspectRatioAndSize(
        static_cast<float>(aspect),
        static_cast<float>(d * 2.0),
        pxr::GfCamera::FOVVertical);

    // Large symmetric range: ortho cameras may need to see geometry behind
    // the nominal camera position (e.g., geometry above a Top-view camera).
    cam.SetClippingRange(pxr::GfRange1f(
        static_cast<float>(-d * 10.0),
        static_cast<float>( d * 100.0)));

    // Build camera-to-world matrix from three cam-space axes + eye position.
    auto M = [](
        double r00, double r01, double r02,
        double r10, double r11, double r12,
        double r20, double r21, double r22,
        const pxr::GfVec3d& e) -> pxr::GfMatrix4d
    {
        return pxr::GfMatrix4d(
            r00, r01, r02, 0,
            r10, r11, r12, 0,
            r20, r21, r22, 0,
            e[0], e[1], e[2], 1);
    };

    pxr::GfMatrix4d xform(1.0);

    if (!zUp) {
        switch (m_orthoView) {
        case OrthoView::Top:    xform = M( 1,0, 0,  0,0,-1,  0, 1, 0,  c+pxr::GfVec3d( 0, d, 0)); break;
        case OrthoView::Bottom: xform = M( 1,0, 0,  0,0, 1,  0,-1, 0,  c+pxr::GfVec3d( 0,-d, 0)); break;
        case OrthoView::Front:  xform = M( 1,0, 0,  0,1, 0,  0, 0, 1,  c+pxr::GfVec3d( 0, 0, d)); break;
        case OrthoView::Back:   xform = M(-1,0, 0,  0,1, 0,  0, 0,-1,  c+pxr::GfVec3d( 0, 0,-d)); break;
        case OrthoView::Left:   xform = M( 0,0, 1,  0,1, 0, -1, 0, 0,  c+pxr::GfVec3d(-d, 0, 0)); break;
        case OrthoView::Right:  xform = M( 0,0,-1,  0,1, 0,  1, 0, 0,  c+pxr::GfVec3d( d, 0, 0)); break;
        default: break;
        }
    } else {
        switch (m_orthoView) {
        case OrthoView::Top:    xform = M( 1,0, 0,  0, 1, 0,  0, 0, 1,  c+pxr::GfVec3d( 0, 0, d)); break;
        case OrthoView::Bottom: xform = M( 1,0, 0,  0,-1, 0,  0, 0,-1,  c+pxr::GfVec3d( 0, 0,-d)); break;
        case OrthoView::Front:  xform = M( 1,0, 0,  0, 0, 1,  0,-1, 0,  c+pxr::GfVec3d( 0,-d, 0)); break;
        case OrthoView::Back:   xform = M(-1,0, 0,  0, 0, 1,  0, 1, 0,  c+pxr::GfVec3d( 0, d, 0)); break;
        case OrthoView::Left:   xform = M( 0,-1,0,  0, 0, 1, -1, 0, 0,  c+pxr::GfVec3d(-d, 0, 0)); break;
        case OrthoView::Right:  xform = M( 0, 1,0,  0, 0, 1,  1, 0, 0,  c+pxr::GfVec3d( d, 0, 0)); break;
        default: break;
        }
    }

    cam.SetTransform(xform);
    return cam;
}

// ---------------------------------------------------------------------------
// ResolveCamera
// ---------------------------------------------------------------------------
pxr::GfCamera ViewportTile::ResolveCamera()
{
    double aspect = static_cast<double>(m_viewWidth) /
                    static_cast<double>(m_viewHeight);
    pxr::GfCamera gfCamera;

    // -- Orthographic preset (short-circuit) ---------------------------------
    if (m_orthoView != OrthoView::None) {
        gfCamera = BuildOrthoCamera();
        m_lastComputedGfCamera = gfCamera;
        m_hasLastGfCamera      = true;
        m_renderer.SetCameraStateFromGfCamera(gfCamera);
        return gfCamera;
    }

    if (m_camera.GetMode() == ViewportCamera::CameraMode::Free ||
        m_isDrivingUsdCamPrim)
    {
        m_camera.SetAspectRatio(aspect);
        pxr::GfBBox3d emptyBBox;
        gfCamera = m_camera.ComputeGfCamera(emptyBBox, false);
        pxr::CameraUtilConformWindow(
            &gfCamera, pxr::CameraUtilMatchVertically, aspect);

        m_lastComputedGfCamera = gfCamera;
        m_hasLastGfCamera      = true;
        m_renderer.SetCameraStateFromGfCamera(gfCamera);

        if (m_isDrivingUsdCamPrim) {
            pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_drivenUsdCamPath);
            if (prim && prim.IsA<pxr::UsdGeomCamera>()) {
                // Author the full camera-to-world matrix as a single transform
                // op rather than decomposing to XYZ-euler translate/rotate.
                // The euler round-trip (Decompose(X,Y,Z) -> rotateXYZ op) is
                // lossy: the read-back orientation differs from what was
                // written, which made the view jump every time a drag finished
                // and the mode flipped back to reading the prim.  A matrix op
                // round-trips exactly through UsdGeomCamera::GetCamera().
                pxr::UsdGeomXformable xformable(prim);
                bool resets = false;
                std::vector<pxr::UsdGeomXformOp> ops =
                    xformable.GetOrderedXformOps(&resets);
                pxr::UsdGeomXformOp matrixOp;
                if (ops.size() == 1 &&
                    ops[0].GetOpType() == pxr::UsdGeomXformOp::TypeTransform)
                    matrixOp = ops[0];
                else
                    matrixOp = xformable.MakeMatrixXform();
                if (matrixOp)
                    matrixOp.Set(gfCamera.GetTransform(), m_editTime);
            }
            if (!m_isOrbiting && !m_isPanning && !m_isDollying) {
                m_isDrivingUsdCamPrim = false;
                m_camera.SetUsdCamera(m_drivenUsdCamPath);
                m_renderer.SetCameraPath(m_drivenUsdCamPath);
            }
        }
    }
    else
    {
        pxr::SdfPath camPath = m_camera.GetUsdCameraPath();
        pxr::UsdPrim camPrim = m_stage->GetPrimAtPath(camPath);
        if (camPrim && camPrim.IsA<pxr::UsdGeomCamera>()) {
            pxr::UsdGeomCamera usdCam(camPrim);
            gfCamera = usdCam.GetCamera(m_displayTime);
            pxr::CameraUtilConformWindow(
                &gfCamera, pxr::CameraUtilMatchVertically, aspect);
            m_lastComputedGfCamera = gfCamera;
            m_hasLastGfCamera      = true;
            m_renderer.SetCameraStateFromGfCamera(gfCamera);
        }
    }

    return gfCamera;
}

// ---------------------------------------------------------------------------
// HandleInput
// ---------------------------------------------------------------------------
void ViewportTile::HandleInput(bool isFocused, TransformManipulator& manipulator,
                               bool dividerActive)
{
    ImGuiIO& io       = ImGui::GetIO();
    ImVec2   mousePos = ImGui::GetMousePos();
    // IsWindowHovered scopes to this child window
    bool     hovered  = ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
    bool     altHeld  = io.KeyAlt;

    // Track click for focus update ??any mouse button press in this tile
    // makes it the focused (active) viewport.
    if (hovered && (ImGui::IsMouseClicked(ImGuiMouseButton_Left)   ||
                    ImGui::IsMouseClicked(ImGuiMouseButton_Middle)  ||
                    ImGui::IsMouseClicked(ImGuiMouseButton_Right)))
        m_wasClickedThisFrame = true;

    // --- Manipulator input (only in focused tile) ---
    bool manipConsumed = false;
    if (isFocused &&
        manipulator.GetMode() != ManipulatorMode::Select &&
        !m_selectedSdfPaths.empty() && m_stage &&
        m_viewWidth > 0 && m_viewHeight > 0 && m_hasLastGfCamera)
    {
        pxr::GfVec3d    pivot     = ComputeGizmoPivot();
        pxr::GfVec3d    camEye    = m_lastComputedGfCamera.GetFrustum().GetPosition();
        pxr::GfFrustum  frustum   = m_lastComputedGfCamera.GetFrustum();
        pxr::GfMatrix4d viewProj  =
            frustum.ComputeViewMatrix() * frustum.ComputeProjectionMatrix();

        manipConsumed = manipulator.HandleInput(
            viewProj, pivot, camEye,
            m_imageScreenPos,
            m_viewWidth, m_viewHeight,
            hovered);
    }

    // --- Start camera drags ---
    if (hovered && !manipConsumed) {
        if (altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
            if (m_orthoView == OrthoView::None) {
                // Perspective free camera: orbit
                InitCameraNavigation();
                m_isOrbiting = true;
            } else {
                // Ortho view: Alt+LMB pans (same as Alt+MMB in Maya)
                m_isPanning = true;
            }
            m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
        }
        if (altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) {
            InitCameraNavigation();
            m_isPanning = true;
            m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
        }
        if (altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
            InitCameraNavigation();
            m_isDollying = true;
            m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
        }
        if (!altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
            !dividerActive &&   // don't start rect-select while a split handle is active
            m_stage && m_viewWidth > 0 && m_viewHeight > 0)
        {
            m_rectAnchor      = mousePos;
            m_rectCurrent     = mousePos;
            m_rectDragStarted = true;
            m_isRectSelecting = false;
        }
    }

    // --- Stop Alt-based camera drags when Alt is released ---
    // Alt+LMB / Alt+MMB / Alt+RMB navigation should end as soon as Alt is
    // released, even if the mouse button is still held.
    if (!altHeld) {
        m_isOrbiting = false;
        m_isPanning  = false;
        m_isDollying = false;
    }

    // --- Process camera drags ---
    if (m_isOrbiting) {
        float dx = mousePos.x - m_lastMouseX;
        float dy = mousePos.y - m_lastMouseY;
        m_camera.Tumble(0.25 * dx, 0.25 * dy);
        m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
    }
    if (m_isPanning) {
        float dx = mousePos.x - m_lastMouseX;
        float dy = mousePos.y - m_lastMouseY;

        if (m_orthoView != OrthoView::None) {
            // Ortho pan: derive right/up from the ortho camera matrix so the
            // movement direction always matches the view, regardless of whatever
            // stale theta/phi the underlying free-camera orbital state carries.
            // Scale: visible world height = dist*2, so 1 pixel = (dist*2)/viewH.
            pxr::GfCamera   orthoCam = BuildOrthoCamera();
            pxr::GfMatrix4d xform    = orthoCam.GetTransform();
            // row0 = cam_X (screen right), row1 = cam_Y (screen up) in world space
            pxr::GfVec3d screenRight(xform[0][0], xform[0][1], xform[0][2]);
            pxr::GfVec3d screenUp   (xform[1][0], xform[1][1], xform[1][2]);

            double factor = m_camera.GetDist() * 2.0
                          / static_cast<double>(std::max(m_viewHeight, 1));

            pxr::GfVec3d newCenter = m_camera.GetFocalPoint()
                                   - screenRight * (static_cast<double>(dx) * factor)
                                   + screenUp    * (static_cast<double>(dy) * factor);
            m_camera.SetFocalPoint(newCenter);
        } else {
            double factor = m_camera.ComputePixelsToWorldFactor(
                static_cast<double>(std::max(m_viewHeight, 1)));
            m_camera.Truck(static_cast<double>(-dx) * factor,
                           static_cast<double>( dy) * factor);
        }
        m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
    }
    if (m_isDollying) {
        float  dx        = mousePos.x - m_lastMouseX;
        float  dy        = mousePos.y - m_lastMouseY;
        double zoomDelta = -0.002 * (dx + dy);
        m_camera.AdjustDistance(1.0 + zoomDelta);
        m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
    }

    // --- Update rect selection while LMB held ---
    if (m_rectDragStarted && !altHeld &&
        ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
        !m_isOrbiting && !m_isPanning && !m_isDollying &&
        !manipulator.IsDragging())
    {
        m_rectCurrent = mousePos;
        float dx = m_rectCurrent.x - m_rectAnchor.x;
        float dy = m_rectCurrent.y - m_rectAnchor.y;
        if (std::sqrt(dx * dx + dy * dy) > kRectDragThreshold)
            m_isRectSelecting = true;
    }

    // --- LMB released ---
    if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
        m_isOrbiting = false;

        if (m_rectDragStarted && !altHeld &&
            m_stage && m_viewWidth > 0 && m_viewHeight > 0)
        {
            if (hovered) {
                int ax = static_cast<int>(m_rectAnchor.x  - m_imageScreenPos.x);
                int ay = static_cast<int>(m_rectAnchor.y  - m_imageScreenPos.y);
                int cx = static_cast<int>(m_rectCurrent.x - m_imageScreenPos.x);
                int cy = static_cast<int>(m_rectCurrent.y - m_imageScreenPos.y);

                if (m_isRectSelecting) {
                    pxr::SdfPathVector hitPaths;
                    if (m_renderer.PickObjectsInRect(
                            ax, ay, cx, cy,
                            m_viewWidth, m_viewHeight, &hitPaths))
                    {
                        if (io.KeyShift) {
                            for (const auto& hp : hitPaths)
                                if (std::find(m_selectedSdfPaths.begin(),
                                              m_selectedSdfPaths.end(), hp)
                                    == m_selectedSdfPaths.end())
                                    m_selectedSdfPaths.push_back(hp);
                        } else {
                            m_selectedSdfPaths = hitPaths;
                        }
                        m_selectedPrimPath = m_selectedSdfPaths.empty()
                            ? "" : m_selectedSdfPaths.front().GetString();
                        m_renderer.SetSelectedPaths(m_selectedSdfPaths);

                        if (OnPrimsPickedRect) {
                            std::vector<std::string> pathStrs;
                            pathStrs.reserve(m_selectedSdfPaths.size());
                            for (const auto& p : m_selectedSdfPaths)
                                pathStrs.push_back(p.GetString());
                            OnPrimsPickedRect(pathStrs);
                        }
                    } else if (!io.KeyShift) {
                        m_renderer.ClearSelected();
                        m_selectedPrimPath.clear();
                        m_selectedSdfPaths.clear();
                        if (OnPrimsPickedRect) OnPrimsPickedRect({});
                    }
                } else {
                    // Single click pick
                    if (cx >= 0 && cy >= 0 && cx < m_viewWidth && cy < m_viewHeight) {
                        pxr::SdfPath camPickPath;
                        bool hitCamera = false;
                        if (m_hasLastGfCamera) {
                            pxr::GfFrustum  fr  = m_lastComputedGfCamera.GetFrustum();
                            pxr::GfMatrix4d vp  = fr.ComputeViewMatrix()
                                                * fr.ComputeProjectionMatrix();
                            hitCamera = m_renderer.PickCameraAtPoint(
                                m_stage, mousePos.x, mousePos.y, vp,
                                m_imageScreenPos.x, m_imageScreenPos.y,
                                m_viewWidth, m_viewHeight,
                                m_camera.GetDist(), &camPickPath);
                        }

                        pxr::SdfPath hitPath  = hitCamera ? camPickPath : pxr::SdfPath();
                        bool         hitGeom  = false;
                        pxr::GfVec3d hitPoint;
                        if (!hitCamera)
                            hitGeom = m_renderer.PickObject(
                                cx, cy, m_viewWidth, m_viewHeight, &hitPoint, &hitPath);

                        if (hitCamera || hitGeom) {
                            if (io.KeyShift) {
                                auto it = std::find(m_selectedSdfPaths.begin(),
                                                    m_selectedSdfPaths.end(), hitPath);
                                if (it != m_selectedSdfPaths.end())
                                    m_selectedSdfPaths.erase(it);
                                else
                                    m_selectedSdfPaths.push_back(hitPath);
                                m_selectedPrimPath = m_selectedSdfPaths.empty()
                                    ? "" : m_selectedSdfPaths.back().GetString();
                                m_renderer.ClearSelected();
                                for (const auto& p : m_selectedSdfPaths)
                                    m_renderer.AddSelected(p);
                                if (OnPrimsPickedRect) {
                                    std::vector<std::string> pathStrs;
                                    pathStrs.reserve(m_selectedSdfPaths.size());
                                    for (const auto& p : m_selectedSdfPaths)
                                        pathStrs.push_back(p.GetString());
                                    OnPrimsPickedRect(pathStrs);
                                }
                            } else {
                                m_selectedPrimPath = hitPath.GetString();
                                m_selectedSdfPaths = { hitPath };
                                m_renderer.ClearSelected();
                                m_renderer.AddSelected(hitPath);
                                if (OnPrimPicked) OnPrimPicked(m_selectedPrimPath);
                            }
                        } else {
                            if (!io.KeyShift) {
                                m_renderer.ClearSelected();
                                m_selectedPrimPath.clear();
                                m_selectedSdfPaths.clear();
                                if (OnPrimPicked) OnPrimPicked("");
                            }
                        }
                    }
                }
            }
        }
        m_rectDragStarted = false;
        m_isRectSelecting = false;
    }
    if (ImGui::IsMouseReleased(ImGuiMouseButton_Middle)) m_isPanning  = false;
    if (ImGui::IsMouseReleased(ImGuiMouseButton_Right))  m_isDollying = false;

    // --- Mouse wheel zoom ---
    if (hovered && io.MouseWheel != 0.0f) {
        InitCameraNavigation();
        double delta = std::max(-0.5, std::min(0.5,
            static_cast<double>(io.MouseWheel) * 0.12));
        m_camera.AdjustDistance(1.0 - delta);
    }

    // --- Keyboard shortcuts ---
    // All shortcuts are gated on `hovered` (mouse is over this tile) so that
    // exactly one viewport responds per key press.  This matches standard DCC
    // behaviour: the viewport under the cursor is the "active" one for both
    // mouse and keyboard interaction.
    if (!io.WantTextInput && hovered) {
        if (ImGui::IsKeyPressed(ImGuiKey_F)) {
            if (!m_selectedSdfPaths.empty() && m_stage) {
                pxr::TfTokenVector purposes = {
                    pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
                pxr::UsdGeomBBoxCache bboxCache(
                    m_displayTime, purposes, true);
                pxr::GfRange3d combined;
                for (const auto& path : m_selectedSdfPaths) {
                    pxr::UsdPrim prim = m_stage->GetPrimAtPath(path);
                    if (!prim) continue;
                    pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim);
                    combined.UnionWith(bbox.ComputeAlignedRange());
                }
                if (combined.IsEmpty()) {
                    pxr::UsdGeomXformCache xformCache(m_displayTime);
                    for (const auto& path : m_selectedSdfPaths) {
                        pxr::UsdPrim prim = m_stage->GetPrimAtPath(path);
                        if (!prim) continue;
                        pxr::GfMatrix4d worldXform =
                            xformCache.GetLocalToWorldTransform(prim);
                        pxr::GfVec3d pos = worldXform.ExtractTranslation();
                        combined.UnionWith(pos - pxr::GfVec3d(1.0));
                        combined.UnionWith(pos + pxr::GfVec3d(1.0));
                    }
                }
                if (!combined.IsEmpty()) {
                    InitCameraNavigation();
                    m_camera.FrameSelection(pxr::GfBBox3d(combined), 1.1);
                    m_renderer.SetForceRefresh(true);
                }
            }
        }

        if (ImGui::IsKeyPressed(ImGuiKey_A)) {
            FrameScene();
            m_renderer.SetForceRefresh(true);
        }
        // Q/W/E/R tool mode and World/Object space are handled globally by
        // ViewportPanel so they always work regardless of which tile is hovered.
    }
}

// ---------------------------------------------------------------------------
// DrawSelectionRect
// ---------------------------------------------------------------------------
void ViewportTile::DrawSelectionRect()
{
    if (!m_isRectSelecting || !m_rectDragStarted) return;

    float vpMinX = m_imageScreenPos.x;
    float vpMinY = m_imageScreenPos.y;
    float vpMaxX = vpMinX + static_cast<float>(m_viewWidth);
    float vpMaxY = vpMinY + static_cast<float>(m_viewHeight);

    ImVec2 drawMin(
        std::max(vpMinX, std::min(m_rectAnchor.x,   m_rectCurrent.x)),
        std::max(vpMinY, std::min(m_rectAnchor.y,   m_rectCurrent.y)));
    ImVec2 drawMax(
        std::min(vpMaxX, std::max(m_rectAnchor.x,   m_rectCurrent.x)),
        std::min(vpMaxY, std::max(m_rectAnchor.y,   m_rectCurrent.y)));

    if (drawMin.x >= drawMax.x || drawMin.y >= drawMax.y) return;

    ImDrawList* dl = ImGui::GetWindowDrawList();
    dl->AddRectFilled(drawMin, drawMax, IM_COL32(100, 160, 255,  40));
    dl->AddRect      (drawMin, drawMax, IM_COL32(100, 160, 255, 220), 0.0f, 0, 1.5f);
}

// ---------------------------------------------------------------------------
// RenderContextMenu
// ---------------------------------------------------------------------------
void ViewportTile::RenderContextMenu(int tileIndex)
{
    if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) &&
        ImGui::IsMouseReleased(ImGuiMouseButton_Right) &&
        !ImGui::GetIO().KeyAlt)
    {
        std::string popupId = "VPCtxMenu_" + std::to_string(tileIndex);
        ImGui::OpenPopup(popupId.c_str());
    }

    std::string popupId = "VPCtxMenu_" + std::to_string(tileIndex);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
    if (ImGui::BeginPopup(popupId.c_str())) {
        bool showGrid = m_renderer.ShowGrid();
        if (ImGui::MenuItem("Show Grid", nullptr, &showGrid))
            m_renderer.SetShowGrid(showGrid);

        bool showGuide = m_renderer.ShowCameraGuide();
        if (ImGui::MenuItem("Camera Guide", nullptr, &showGuide))
            m_renderer.SetShowCameraGuide(showGuide);

        ImGui::Separator();

        bool ambientOnly = m_renderer.GetAmbientLightOnly();
        if (ImGui::MenuItem("Camera Light", nullptr, &ambientOnly))
            m_renderer.SetAmbientLightOnly(ambientOnly);

        bool domeLight = m_renderer.GetDomeLightEnabled();
        if (ImGui::MenuItem("Dome Light", nullptr, &domeLight))
            m_renderer.SetDomeLightEnabled(domeLight);

        ImGui::Separator();

        if (ImGui::BeginMenu("Bounding Box")) {
            BBoxMode cur = m_renderer.GetBBoxMode();
            if (ImGui::MenuItem("None",         nullptr, cur == BBoxMode::None))
                m_renderer.SetBBoxMode(BBoxMode::None);
            if (ImGui::MenuItem("Per Object",   nullptr, cur == BBoxMode::PerObject))
                m_renderer.SetBBoxMode(BBoxMode::PerObject);
            if (ImGui::MenuItem("All Selection",nullptr, cur == BBoxMode::AllSelection))
                m_renderer.SetBBoxMode(BBoxMode::AllSelection);
            ImGui::EndMenu();
        }

        ImGui::Separator();

        if (ImGui::BeginMenu("Background")) {
            auto& bg = m_renderer.GetBackgroundColor();
            if (ImGui::MenuItem("Dark Gray", nullptr,
                    bg == pxr::GfVec3f(0.15f, 0.15f, 0.15f)))
                m_renderer.SetBackgroundColor(pxr::GfVec3f(0.15f, 0.15f, 0.15f));
            if (ImGui::MenuItem("Black", nullptr,
                    bg == pxr::GfVec3f(0.0f, 0.0f, 0.0f)))
                m_renderer.SetBackgroundColor(pxr::GfVec3f(0.0f, 0.0f, 0.0f));
            if (ImGui::MenuItem("Light Gray", nullptr,
                    bg == pxr::GfVec3f(0.45f, 0.45f, 0.45f)))
                m_renderer.SetBackgroundColor(pxr::GfVec3f(0.45f, 0.45f, 0.45f));
            if (ImGui::MenuItem("Midnight Blue", nullptr,
                    bg == pxr::GfVec3f(0.1f, 0.1f, 0.2f)))
                m_renderer.SetBackgroundColor(pxr::GfVec3f(0.1f, 0.1f, 0.2f));
            ImGui::EndMenu();
        }

        ImGui::EndPopup();
    }
    ImGui::PopStyleVar();
}

// ---------------------------------------------------------------------------
// RenderCompactToolbar  -- all per-tile controls in one compact row
// ---------------------------------------------------------------------------
void ViewportTile::RenderCompactToolbar(int tileIndex)
{
    if (m_cameraListDirty) RefreshCameraList();

    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3, 3));

    // -- Camera selector -----------------------------------------------------
    ImGui::PushItemWidth(140.0f);

    // Helper: ortho view display name
    auto OrthoName = [](OrthoView v) -> const char* {
        switch (v) {
        case OrthoView::Top:    return "Top";
        case OrthoView::Bottom: return "Bottom";
        case OrthoView::Front:  return "Front";
        case OrthoView::Back:   return "Back";
        case OrthoView::Left:   return "Left";
        case OrthoView::Right:  return "Right";
        default:                return nullptr;
        }
    };

    // Determine display label: ortho view name > USD cam prim path > "Free Camera"
    const char* currentLabel = "Free Camera";
    if (m_orthoView != OrthoView::None) {
        currentLabel = OrthoName(m_orthoView);
    } else if (m_selectedCameraIndex > 0 &&
               static_cast<size_t>(m_selectedCameraIndex - 1) < m_cameraPaths.size()) {
        currentLabel = m_cameraPaths[m_selectedCameraIndex - 1].GetText();
    }

    std::string camComboId = "##Cam_" + std::to_string(tileIndex);
    if (ImGui::BeginCombo(camComboId.c_str(), currentLabel)) {
        RefreshCameraList();

        // -- Perspective free camera -----------------------------------------
        bool freeSel = (m_orthoView == OrthoView::None && m_selectedCameraIndex == 0);
        if (ImGui::Selectable("Free Camera", freeSel)) {
            m_orthoView           = OrthoView::None;
            m_selectedCameraIndex = 0;
            m_isDrivingUsdCamPrim = false;
            if (m_hasSavedFreeCameraState)
                m_camera.SwitchToFreeCamera(&m_savedFreeCameraState);
            else
                m_camera.SwitchToFreeCamera(
                    m_hasLastGfCamera ? &m_lastComputedGfCamera : nullptr);
        }

        // -- Orthographic presets -------------------------------------------
        ImGui::Separator();
        static const struct { OrthoView view; const char* label; } kOrtho[] = {
            { OrthoView::Top,    "Top"    },
            { OrthoView::Bottom, "Bottom" },
            { OrthoView::Front,  "Front"  },
            { OrthoView::Back,   "Back"   },
            { OrthoView::Left,   "Left"   },
            { OrthoView::Right,  "Right"  },
        };
        for (const auto& ov : kOrtho) {
            bool isSel = (m_orthoView == ov.view);
            if (ImGui::Selectable(ov.label, isSel)) {
                m_orthoView           = ov.view;
                m_selectedCameraIndex = 0;
                m_isDrivingUsdCamPrim = false;
                // Ensure free-camera mode so center/dist are maintained
                if (m_camera.GetMode() == ViewportCamera::CameraMode::UsdCamera) {
                    m_camera.SwitchToFreeCamera(
                        m_hasLastGfCamera ? &m_lastComputedGfCamera : nullptr);
                }
            }
            if (isSel) ImGui::SetItemDefaultFocus();
        }

        // -- USD camera prims -----------------------------------------------
        if (!m_cameraPaths.empty()) ImGui::Separator();
        for (size_t i = 0; i < m_cameraPaths.size(); ++i) {
            bool isSelected = (m_orthoView == OrthoView::None &&
                               m_selectedCameraIndex == static_cast<int>(i + 1));
            if (ImGui::Selectable(m_cameraPaths[i].GetText(), isSelected)) {
                if (m_camera.GetMode() == ViewportCamera::CameraMode::Free
                    && m_hasLastGfCamera)
                {
                    m_savedFreeCameraState    = m_lastComputedGfCamera;
                    m_hasSavedFreeCameraState = true;
                }
                m_orthoView           = OrthoView::None;
                m_selectedCameraIndex = static_cast<int>(i + 1);
                m_camera.SetUsdCamera(m_cameraPaths[i]);
                m_renderer.SetCameraPath(m_cameraPaths[i]);
            }
            if (isSelected) ImGui::SetItemDefaultFocus();
        }
        ImGui::EndCombo();
    }
    if (ImGui::IsItemHovered()) {
        if (m_orthoView != OrthoView::None)
            ImGui::SetTooltip("Orthographic: %s\n"
                              "Alt+LMB / Alt+MMB: Pan\n"
                              "Alt+RMB / Scroll: Zoom\n"
                              "F: Frame selection  A: Frame all",
                              OrthoName(m_orthoView));
        else
            ImGui::SetTooltip("Camera\n"
                              "Alt+LMB: Orbit  Alt+MMB: Pan  Alt+RMB: Dolly\n"
                              "Scroll: Zoom  F: Frame sel  A: Frame all");
    }
    ImGui::PopItemWidth();

    ImGui::SameLine();
    ImGui::TextDisabled("|");
    ImGui::SameLine();

    // -- Render delegate (global) --------------------------------------------
    {
        pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
        std::string  displayName = currentId.IsEmpty()
            ? "Rdr"
            : UsdSceneRenderer::GetRendererDisplayName(currentId);
        if (displayName.size() > 8) displayName = displayName.substr(0, 8);

        std::string rdrPopupId = "RdrPopup_" + std::to_string(tileIndex);
        ImGui::Button(displayName.c_str(), ImVec2(72.0f, 0.0f));
        if (ImGui::IsItemHovered())
            ImGui::SetTooltip("Render delegate");

        if (ImGui::BeginPopupContextItem(rdrPopupId.c_str(),
                ImGuiPopupFlags_MouseButtonLeft))
        {
            for (const auto& pluginId : UsdSceneRenderer::GetRendererPlugins()) {
                std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
                if (name.empty()) name = pluginId.GetString();
                bool selected = (pluginId == currentId);
                if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected)
                    m_renderer.SetRendererPlugin(pluginId);
            }
            ImGui::EndPopup();
        }
    }

    ImGui::SameLine();
    ImGui::TextDisabled("|");
    ImGui::SameLine();

    // -- View option toggles (icon buttons) ----------------------------------
    auto iconToggle = [this](const char* id, const char* fallback,
                             Icon iconEnum, bool active,
                             const char* tooltip) -> bool
    {
        const ImVec4 kActive  (0.26f, 0.59f, 0.98f, 1.00f);
        const ImVec4 kActiveHv(0.36f, 0.69f, 1.00f, 1.00f);
        if (active) {
            ImGui::PushStyleColor(ImGuiCol_Button,        kActive);
            ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kActiveHv);
        }
        bool clicked = false;
        if (m_iconManager) {
            ImTextureID tex = m_iconManager->Get(iconEnum);
            clicked = ImGui::ImageButton(id, ImTextureRef(tex), ImVec2(16.f, 16.f));
        } else {
            clicked = ImGui::Button(fallback);
        }
        if (active) ImGui::PopStyleColor(2);
        if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tooltip);
        return clicked;
    };

    std::string gridId = "##grd_" + std::to_string(tileIndex);
    std::string aaId   = "##aa_"  + std::to_string(tileIndex);

    bool showGrid = m_renderer.ShowGrid();
    if (iconToggle(gridId.c_str(), "Grd", Icon::Grid, showGrid, "Toggle grid"))
        m_renderer.SetShowGrid(!showGrid);

    ImGui::SameLine();

    bool aa = m_renderer.GetAAEnabled();
    if (iconToggle(aaId.c_str(), "AA", Icon::Antialias, aa, "Toggle anti-aliasing"))
        m_renderer.SetAAEnabled(!aa);

    ImGui::SameLine();
    ImGui::TextDisabled("|");
    ImGui::SameLine();

    // -- Gear / viewport options popup ---------------------------------------
    {
        std::string gearId      = "##gear_" + std::to_string(tileIndex);
        std::string gearPopupId = "GearPopup_" + std::to_string(tileIndex);

        if (m_iconManager) {
            ImTextureID tex = m_iconManager->Get(Icon::Settings);
            ImGui::ImageButton(gearId.c_str(), ImTextureRef(tex), ImVec2(16.f, 16.f));
        } else {
            ImGui::Button("*");
        }
        if (ImGui::IsItemHovered()) ImGui::SetTooltip("Viewport options");

        if (ImGui::BeginPopupContextItem(gearPopupId.c_str(),
                ImGuiPopupFlags_MouseButtonLeft))
        {
            // -- Shading mode ------------------------------------------------
            if (ImGui::BeginMenu("Shading")) {
                ShadingMode cur = m_renderer.GetShadingMode();
                auto shadingItem = [&](const char* label, ShadingMode mode) {
                    if (ImGui::MenuItem(label, nullptr, cur == mode))
                        m_renderer.SetShadingMode(mode);
                };
                shadingItem("Smooth Shaded",       ShadingMode::SmoothShaded);
                shadingItem("Flat Shaded",          ShadingMode::FlatShaded);
                shadingItem("Wireframe on Surface", ShadingMode::WireframeOnSurface);
                shadingItem("Wireframe",            ShadingMode::Wireframe);
                shadingItem("Unlit",                ShadingMode::Unlit);
                ImGui::EndMenu();
            }

            // -- Cull Style --------------------------------------------------
            if (ImGui::BeginMenu("Cull Style")) {
                CullStyle curCull = m_renderer.GetCullStyle();
                auto cullItem = [&](const char* label, CullStyle style) {
                    if (ImGui::MenuItem(label, nullptr, curCull == style))
                        m_renderer.SetCullStyle(style);
                };
                cullItem("Back Unless Double-Sided", CullStyle::BackUnlessDoubleSided);
                cullItem("Back",                     CullStyle::Back);
                cullItem("Front",                    CullStyle::Front);
                cullItem("Nothing",                  CullStyle::Nothing);
                cullItem("No Opinion",               CullStyle::NoOpinion);
                ImGui::EndMenu();
            }

            // -- AOV ---------------------------------------------------------
            {
                pxr::TfTokenVector aovs = m_renderer.GetRendererAovs();
                if (ImGui::BeginMenu("AOV", !aovs.empty())) {
                    pxr::TfToken currentAov = m_renderer.GetCurrentAov();
                    for (const auto& aov : aovs) {
                        bool checked = (aov == currentAov);
                        if (ImGui::MenuItem(aov.GetText(), nullptr, checked) && !checked)
                            m_renderer.SetCurrentAov(aov);
                    }
                    ImGui::EndMenu();
                }
            }

            // -- Lighting ----------------------------------------------------
            if (ImGui::BeginMenu("Lighting")) {
                bool ambient = m_renderer.GetAmbientLightOnly();
                if (ImGui::MenuItem("Camera Headlight", nullptr,  ambient))
                    m_renderer.SetAmbientLightOnly(true);
                if (ImGui::MenuItem("Scene Lights",     nullptr, !ambient))
                    m_renderer.SetAmbientLightOnly(false);
                ImGui::Separator();
                bool dome = m_renderer.GetDomeLightEnabled();
                if (ImGui::MenuItem("Dome Light", nullptr, dome))
                    m_renderer.SetDomeLightEnabled(!dome);
                ImGui::EndMenu();
            }

            ImGui::Separator();

            // -- Display toggles ---------------------------------------------
            {
                bool grid = m_renderer.ShowGrid();
                if (ImGui::MenuItem("Show Grid", nullptr, grid))
                    m_renderer.SetShowGrid(!grid);

                bool guide = m_renderer.ShowCameraGuide();
                if (ImGui::MenuItem("Camera Guide", nullptr, guide))
                    m_renderer.SetShowCameraGuide(!guide);

                if (ImGui::BeginMenu("Purpose")) {
                    bool purGuide = m_renderer.ShowGuides();
                    if (ImGui::MenuItem("Guide",  nullptr, purGuide))
                        m_renderer.SetShowGuides(!purGuide);
                    bool purProxy = m_renderer.ShowProxy();
                    if (ImGui::MenuItem("Proxy",  nullptr, purProxy))
                        m_renderer.SetShowProxy(!purProxy);
                    bool purRender = m_renderer.ShowRender();
                    if (ImGui::MenuItem("Render", nullptr, purRender))
                        m_renderer.SetShowRender(!purRender);
                    ImGui::EndMenu();
                }

                bool aaItem = m_renderer.GetAAEnabled();
                if (ImGui::MenuItem("Anti-Aliasing", nullptr, aaItem))
                    m_renderer.SetAAEnabled(!aaItem);
            }

            ImGui::Separator();

            // -- BBox --------------------------------------------------------
            if (ImGui::BeginMenu("Bounding Box")) {
                BBoxMode bbox = m_renderer.GetBBoxMode();
                if (ImGui::MenuItem("None",          nullptr, bbox == BBoxMode::None))
                    m_renderer.SetBBoxMode(BBoxMode::None);
                if (ImGui::MenuItem("Per Object",    nullptr, bbox == BBoxMode::PerObject))
                    m_renderer.SetBBoxMode(BBoxMode::PerObject);
                if (ImGui::MenuItem("All Selection", nullptr, bbox == BBoxMode::AllSelection))
                    m_renderer.SetBBoxMode(BBoxMode::AllSelection);
                ImGui::EndMenu();
            }

            // -- Background --------------------------------------------------
            if (ImGui::BeginMenu("Background")) {
                auto& bg = m_renderer.GetBackgroundColor();
                if (ImGui::MenuItem("Dark Gray",     nullptr, bg == pxr::GfVec3f(0.15f, 0.15f, 0.15f)))
                    m_renderer.SetBackgroundColor(pxr::GfVec3f(0.15f, 0.15f, 0.15f));
                if (ImGui::MenuItem("Black",         nullptr, bg == pxr::GfVec3f(0.0f, 0.0f, 0.0f)))
                    m_renderer.SetBackgroundColor(pxr::GfVec3f(0.0f, 0.0f, 0.0f));
                if (ImGui::MenuItem("Light Gray",    nullptr, bg == pxr::GfVec3f(0.45f, 0.45f, 0.45f)))
                    m_renderer.SetBackgroundColor(pxr::GfVec3f(0.45f, 0.45f, 0.45f));
                if (ImGui::MenuItem("Midnight Blue", nullptr, bg == pxr::GfVec3f(0.1f, 0.1f, 0.2f)))
                    m_renderer.SetBackgroundColor(pxr::GfVec3f(0.1f, 0.1f, 0.2f));
                ImGui::EndMenu();
            }

            ImGui::Separator();
            if (ImGui::MenuItem("Renderer Settings..."))
                m_showRendererSettings = true;

            ImGui::EndPopup();
        }
    }

    ImGui::PopStyleVar(); // ItemSpacing
}

// ---------------------------------------------------------------------------
// RenderManipulatorOverlay  -- vertical Q/W/E/R + space buttons on the image
// ---------------------------------------------------------------------------
void ViewportTile::RenderManipulatorOverlay(TransformManipulator& manipulator)
{
    const float  kButtonSize = 32.0f;
    const float  kIconPad    =  4.0f;
    const float  kRounding   =  4.0f;
    const float  kPadX       = 10.0f;
    const float  kPadY       = 10.0f;
    const float  kSpacing    =  4.0f;
    const ImVec2 kBtnSz(kButtonSize, kButtonSize);

    ImVec2 origin(m_imageScreenPos.x + kPadX, m_imageScreenPos.y + kPadY);

    struct ToolInfo {
        ManipulatorMode mode;
        Icon            icon;
        const char*     fallbackLabel;
        const char*     tooltip;
        const char*     id;
    };
    static const ToolInfo tools[] = {
        { ManipulatorMode::Select, Icon::ToolSelect, "Q", "Select (Q)", "##ovl_q" },
        { ManipulatorMode::Move,   Icon::ToolMove,   "W", "Move (W)",   "##ovl_w" },
        { ManipulatorMode::Rotate, Icon::ToolRotate, "E", "Rotate (E)", "##ovl_e" },
        { ManipulatorMode::Scale,  Icon::ToolScale,  "R", "Scale (R)",  "##ovl_r" },
    };

    ManipulatorMode current = manipulator.GetMode();
    ImDrawList*     dl      = ImGui::GetWindowDrawList();

    for (int i = 0; i < 4; ++i) {
        const auto& t = tools[i];
        bool   active = (current == t.mode);
        ImVec2 btnMin(origin.x, origin.y + i * (kButtonSize + kSpacing));
        ImVec2 btnMax(btnMin.x + kButtonSize, btnMin.y + kButtonSize);

        ImGui::SetCursorScreenPos(btnMin);
        bool clicked = ImGui::InvisibleButton(t.id, kBtnSz);
        bool hovBtn  = ImGui::IsItemHovered();

        ImU32 bgCol = active  ? IM_COL32( 66, 150, 250, 230) :
                      hovBtn  ? IM_COL32( 64,  64,  64, 220) :
                                IM_COL32( 26,  26,  26, 178);
        dl->AddRectFilled(btnMin, btnMax, bgCol, kRounding);

        if (m_iconManager) {
            ImTextureID texId = m_iconManager->Get(t.icon);
            dl->AddImage(ImTextureRef(texId),
                         ImVec2(btnMin.x + kIconPad, btnMin.y + kIconPad),
                         ImVec2(btnMax.x - kIconPad, btnMax.y - kIconPad));
        } else {
            float tx = btnMin.x + (kButtonSize - ImGui::CalcTextSize(t.fallbackLabel).x) * 0.5f;
            float ty = btnMin.y + (kButtonSize - ImGui::GetTextLineHeight()) * 0.5f;
            dl->AddText(ImVec2(tx, ty), IM_COL32(255,255,255,255), t.fallbackLabel);
        }

        if (active)
            dl->AddRect(btnMin, btnMax, IM_COL32(100, 180, 255, 200), kRounding, 0, 1.5f);
        if (clicked)    manipulator.SetMode(t.mode);
        if (hovBtn)     ImGui::SetTooltip("%s", t.tooltip);
    }

    // World / Object space toggle
    float  spaceY  = origin.y + 4 * (kButtonSize + kSpacing) + 4.0f;
    ImVec2 spBtnMin(origin.x, spaceY);
    ImVec2 spBtnMax(spBtnMin.x + kButtonSize, spBtnMin.y + kButtonSize);
    bool   isWorld = (manipulator.GetTransformSpace() == TransformSpace::World);

    ImGui::SetCursorScreenPos(spBtnMin);
    bool spClicked = ImGui::InvisibleButton("##ovl_space", kBtnSz);
    bool spHovered = ImGui::IsItemHovered();

    ImU32 spBgCol = isWorld   ? IM_COL32( 66, 150, 250, 230) :
                   spHovered  ? IM_COL32( 64,  64,  64, 220) :
                                IM_COL32( 26,  26,  26, 178);
    dl->AddRectFilled(spBtnMin, spBtnMax, spBgCol, kRounding);

    if (m_iconManager) {
        ImTextureID spTex = m_iconManager->Get(isWorld ? Icon::WorldSpace : Icon::LocalSpace);
        dl->AddImage(ImTextureRef(spTex),
                     ImVec2(spBtnMin.x + kIconPad, spBtnMin.y + kIconPad),
                     ImVec2(spBtnMax.x - kIconPad, spBtnMax.y - kIconPad));
    } else {
        const char* spLabel = isWorld ? "W" : "O";
        float spTx = spBtnMin.x + (kButtonSize - ImGui::CalcTextSize(spLabel).x) * 0.5f;
        float spTy = spBtnMin.y + (kButtonSize - ImGui::GetTextLineHeight()) * 0.5f;
        dl->AddText(ImVec2(spTx, spTy), IM_COL32(255,255,255,255), spLabel);
    }
    if (isWorld)
        dl->AddRect(spBtnMin, spBtnMax, IM_COL32(100,180,255,200), kRounding, 0, 1.5f);
    if (spClicked)
        manipulator.SetTransformSpace(isWorld ? TransformSpace::Object : TransformSpace::World);
    if (spHovered)
        ImGui::SetTooltip(isWorld
            ? "World space\nClick to switch to Object space"
            : "Object space\nClick to switch to World space");
}

// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
void ViewportTile::Render(int tileIndex, ImVec2 pos, ImVec2 size,
                          bool isFocused, TransformManipulator& manipulator,
                          bool dividerActive)
{
    m_wasClickedThisFrame = false;
    m_wasHoveredThisFrame = false;

    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
    ImGui::SetCursorScreenPos(pos);

    std::string childId = "##vptile_" + std::to_string(tileIndex);
    ImGuiWindowFlags flags = ImGuiWindowFlags_NoScrollbar
                           | ImGuiWindowFlags_NoScrollWithMouse
                           | ImGuiWindowFlags_NoBackground;

    bool childOpen = ImGui::BeginChild(childId.c_str(), size, false, flags);
    ImGui::PopStyleVar(); // WindowPadding

    if (!childOpen) {
        ImGui::EndChild();
        return;
    }

    // Hover detection for the whole child (used by Space maximize in container)
    m_wasHoveredThisFrame = ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);

    // NOTE: Border is drawn AFTER EndChild (see below) so it is not clipped
    //       by this child window's scissor rect.

    // -- Compact toolbar ------------------------------------------------------
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4, 2));
    ImGui::SetCursorPos(ImVec2(4, 2));
    RenderCompactToolbar(tileIndex);
    ImGui::PopStyleVar();
    ImGui::Separator();

    // -- Scene image area -----------------------------------------------------
    ImVec2 avail = ImGui::GetContentRegionAvail();
    m_viewWidth  = static_cast<int>(avail.x);
    m_viewHeight = static_cast<int>(avail.y);

    if (m_viewWidth > 0 && m_viewHeight > 0 && m_stage) {
        m_imageScreenPos = ImGui::GetCursorScreenPos();

        HandleInput(isFocused, manipulator, dividerActive);

        pxr::GfCamera gfCamera = ResolveCamera();

        // Render scene into FBO
        m_renderer.Render(m_viewWidth, m_viewHeight);

        // Overlays into FBO
        if (m_hasLastGfCamera) {
            pxr::GfFrustum  frustum  = gfCamera.GetFrustum();
            pxr::GfMatrix4d viewProj =
                frustum.ComputeViewMatrix() * frustum.ComputeProjectionMatrix();
            m_renderer.DrawAxis(viewProj, m_camera.GetDist());
            m_renderer.DrawBoundingBoxes(m_selectedSdfPaths, viewProj);
            m_renderer.DrawCameraWireframes(
                m_stage, m_selectedSdfPaths,
                m_camera.GetUsdCameraPath(),
                viewProj, m_camera.GetDist());
            m_renderer.DrawLightWireframes(
                m_stage, m_selectedSdfPaths,
                viewProj, m_camera.GetDist());
        }

        // Display FBO texture (flip UV Y: OpenGL is bottom-up)
        uint32_t texID = m_renderer.GetColorTextureID();
        if (texID != 0) {
            ImGui::Image(
                ImTextureID(static_cast<ImU64>(texID)),
                ImVec2(static_cast<float>(m_viewWidth),
                       static_cast<float>(m_viewHeight)),
                ImVec2(0, 1), ImVec2(1, 0));
        } else {
            ImGui::TextColored(ImVec4(1,0,0,1),
                "Viewport: No texture (renderer not initialised)");
        }

        // -- 2-D ImGui overlays on top of the image --------------------------
        DrawSelectionRect();

        // Gizmo -- only in focused tile
        if (isFocused && m_hasLastGfCamera &&
            manipulator.GetMode() != ManipulatorMode::Select &&
            !m_selectedSdfPaths.empty())
        {
            pxr::GfFrustum  frustum  = gfCamera.GetFrustum();
            pxr::GfMatrix4d viewProj =
                frustum.ComputeViewMatrix() * frustum.ComputeProjectionMatrix();
            pxr::GfVec3d pivot  = ComputeGizmoPivot();
            pxr::GfVec3d camEye = frustum.GetPosition();

            manipulator.Render(
                ImGui::GetWindowDrawList(),
                viewProj, pivot, camEye,
                m_imageScreenPos,
                m_viewWidth, m_viewHeight);
        }

    } else {
        if (!m_stage)
            ImGui::TextDisabled("No stage loaded");
        else
            ImGui::TextDisabled("Viewport too small (%dx%d)", m_viewWidth, m_viewHeight);
    }

    // -- Focus / hover border -------------------------------------------------
    // Drawn as the LAST item inside the child window so it sits above the scene
    // texture in the draw list sequence.  Because it is inside the child window
    // it is automatically clipped to this tile's bounds and cannot bleed over
    // floating panels or menus positioned outside this tile.
    // A 1 px inset ensures the full 2 px focused line stays within the clip rect.
    {
        ImVec2 bMin(pos.x + 1.f, pos.y + 1.f);
        ImVec2 bMax(pos.x + size.x - 1.f, pos.y + size.y - 1.f);
        ImDrawList* dl = ImGui::GetWindowDrawList();

        if (isFocused) {
            dl->AddRect(bMin, bMax, IM_COL32(66, 150, 250, 230), 0.f, 0, 2.0f);
        } else if (m_wasHoveredThisFrame) {
            dl->AddRect(bMin, bMax, IM_COL32(220, 220, 220, 110), 0.f, 0, 1.0f);
        }
    }

    RenderContextMenu(tileIndex);
    ImGui::EndChild();

    if (m_showRendererSettings)
        RenderRendererSettingsDialog(tileIndex);
}

// ---------------------------------------------------------------------------
// RenderRendererSettingsDialog
// ---------------------------------------------------------------------------
void ViewportTile::RenderRendererSettingsDialog(int tileIndex)
{
    using namespace pxr;
    std::string title = "Renderer Settings  [" +
        UsdSceneRenderer::GetRendererDisplayName(m_renderer.GetCurrentRendererId()) +
        "]###RendererSettingsDlg_" + std::to_string(tileIndex);

    ImGui::SetNextWindowSize(ImVec2(520, 460), ImGuiCond_FirstUseEver);
    // WindowPadding is captured by Begin(); pop it immediately after.
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.f, 12.f));
    bool open = ImGui::Begin(title.c_str(), &m_showRendererSettings,
                             ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking);
    ImGui::PopStyleVar();
    if (!open) {
        ImGui::End();
        return;
    }

    // ItemSpacing and FramePadding are read dynamically; push for content, pop before End.
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,  ImVec2(8.f, 6.f));
    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6.f, 4.f));

    // Cache the settings list per delegate — GetRendererSettingsList() calls TF_WARN
    // for every unsupported setting type, so querying it every frame floods the log.
    pxr::TfToken curId = m_renderer.GetCurrentRendererId();
    if (curId != m_cachedSettingsRendererId) {
        m_cachedRendererSettings   = m_renderer.GetRendererSettingsList();
        m_cachedSettingsRendererId = curId;
    }
    const auto& settings = m_cachedRendererSettings;
    if (settings.empty()) {
        ImVec2 sz = ImGui::GetContentRegionAvail();
        ImGui::SetCursorPos(ImVec2(sz.x * 0.5f - 120.f, sz.y * 0.5f - 8.f));
        ImGui::TextDisabled("This renderer exposes no settings.");
        ImGui::PopStyleVar(2);
        ImGui::End();
        return;
    }

    // Group settings by namespace prefix (split key at first ':').
    // Keys without ':' go into "General". std::map keeps alphabetical order.
    std::map<std::string, std::vector<const UsdImagingGLRendererSetting*>> groups;
    for (const auto& s : settings) {
        std::string k = s.key.GetString();
        auto colon = k.find(':');
        std::string cat = (colon != std::string::npos) ? k.substr(0, colon) : "General";
        if (!cat.empty()) cat[0] = static_cast<char>(toupper(static_cast<unsigned char>(cat[0])));
        groups[cat].push_back(&s);
    }

    bool singleGroup = (groups.size() == 1);
    bool tabBarOpen  = false;
    if (!singleGroup)
        tabBarOpen = ImGui::BeginTabBar("##rdrsettings_tabs");

    auto renderGroup = [&](const std::vector<const UsdImagingGLRendererSetting*>& items) {
        float tableH = ImGui::GetContentRegionAvail().y - 40.f;
        if (tableH < 20.f) tableH = 20.f;
        if (ImGui::BeginTable("##srows", 2,
            ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerH |
            ImGuiTableFlags_ScrollY, ImVec2(0, tableH)))
        {
            ImGui::TableSetupColumn("Name",  ImGuiTableColumnFlags_WidthStretch, 0.55f);
            ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch, 0.45f);
            for (const auto* sp : items) {
                const auto& s = *sp;
                VtValue cur = m_renderer.GetRendererSetting(s.key);
                ImGui::TableNextRow();
                ImGui::TableSetColumnIndex(0);
                ImGui::AlignTextToFramePadding();
                ImGui::TextUnformatted(s.name.c_str());
                ImGui::TableSetColumnIndex(1);
                ImGui::PushItemWidth(-1.f);
                std::string wid = "##" + s.key.GetString();
                if (s.type == UsdImagingGLRendererSetting::TYPE_FLAG) {
                    bool v = cur.GetWithDefault<bool>(false);
                    if (ImGui::Checkbox(wid.c_str(), &v))
                        m_renderer.SetRendererSetting(s.key, VtValue(v));
                } else if (s.type == UsdImagingGLRendererSetting::TYPE_INT) {
                    int v = cur.GetWithDefault<int>(0);
                    if (ImGui::DragInt(wid.c_str(), &v))
                        m_renderer.SetRendererSetting(s.key, VtValue(v));
                } else if (s.type == UsdImagingGLRendererSetting::TYPE_FLOAT) {
                    float v = cur.GetWithDefault<float>(0.f);
                    if (ImGui::DragFloat(wid.c_str(), &v, 0.01f))
                        m_renderer.SetRendererSetting(s.key, VtValue(v));
                } else if (s.type == UsdImagingGLRendererSetting::TYPE_STRING) {
                    std::string sv = cur.GetWithDefault<std::string>("");
                    char buf[256];
                    strncpy(buf, sv.c_str(), sizeof(buf) - 1);
                    buf[sizeof(buf) - 1] = '\0';
                    if (ImGui::InputText(wid.c_str(), buf, sizeof(buf)) &&
                        ImGui::IsItemDeactivatedAfterEdit())
                        m_renderer.SetRendererSetting(s.key, VtValue(std::string(buf)));
                }
                ImGui::PopItemWidth();
            }
            ImGui::EndTable();
        }
    };

    if (singleGroup) {
        renderGroup(groups.begin()->second);
    } else if (tabBarOpen) {
        // Non-"General" tabs first (alphabetical via std::map), then "General" last
        for (const auto& [cat, items] : groups) {
            if (cat == "General") continue;
            if (ImGui::BeginTabItem(cat.c_str())) {
                renderGroup(items);
                ImGui::EndTabItem();
            }
        }
        auto genIt = groups.find("General");
        if (genIt != groups.end() && ImGui::BeginTabItem("General")) {
            renderGroup(genIt->second);
            ImGui::EndTabItem();
        }
        ImGui::EndTabBar();
    }

    ImGui::Separator();
    ImGui::Spacing();
    if (ImGui::Button("Reset Defaults")) {
        for (const auto& s : settings)
            m_renderer.SetRendererSetting(s.key, s.defValue);
    }

    ImGui::PopStyleVar(2);
    ImGui::End();
}

} // namespace UsdLayerManager
