#include "UsdSceneRenderer.h"
#include "../utils/Logger.h"
#include "../utils/GLExt.h"

#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdGeom/bboxCache.h>
#include <pxr/usd/usdGeom/camera.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdLux/lightAPI.h>
#include <pxr/usd/usdLux/sphereLight.h>
#include <pxr/usd/usdLux/rectLight.h>
#include <pxr/usd/usdLux/diskLight.h>
#include <pxr/usd/usdLux/distantLight.h>
#include <pxr/usd/usdLux/domeLight.h>
#include <pxr/usd/usdLux/cylinderLight.h>
#include <pxr/imaging/glf/contextCaps.h>
#include <pxr/imaging/cameraUtil/conformWindow.h>
#include <pxr/imaging/cameraUtil/framing.h>
#include <pxr/base/gf/range2f.h>
#include <pxr/base/gf/rect2i.h>
#include <pxr/base/gf/frustum.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/base/tf/diagnosticMgr.h>
#include <pxr/base/tf/error.h>
#include <pxr/base/tf/warning.h>
#include <pxr/base/tf/status.h>

#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <exception>
#include <unordered_set>

namespace UsdLayerManager {

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

static std::string Vec3fStr(const pxr::GfVec3f& v) {
    return "(" + std::to_string(v[0]) + ", " + std::to_string(v[1]) + ", "
               + std::to_string(v[2]) + ")";
}

// Forwards USD's TfError / TF_WARN / TF_STATUS messages into the app log.
// Without this, Hydra diagnostics (e.g. the reason HdxColorCorrectionTask
// fails to build an OCIO processor) only go to a stderr console the GUI
// never shows.  Installed once, process-wide, from InitRenderer().
namespace {
class UsdDiagnosticLogger : public pxr::TfDiagnosticMgr::Delegate {
public:
    void IssueError(pxr::TfError const& err) override {
        LOG_ERROR("[USD] " + err.GetCommentary());
    }
    void IssueFatalError(pxr::TfCallContext const&,
                         std::string const& msg) override {
        LOG_ERROR("[USD fatal] " + msg);
    }
    void IssueStatus(pxr::TfStatus const& status) override {
        LOG_INFO("[USD] " + status.GetCommentary());
    }
    void IssueWarning(pxr::TfWarning const& warning) override {
        LOG_WARNING("[USD] " + warning.GetCommentary());
    }
};

void InstallUsdDiagnosticLogger() {
    static UsdDiagnosticLogger s_logger;   // process-lifetime; never removed
    static bool s_installed = false;
    if (s_installed) return;
    s_installed = true;
    pxr::TfDiagnosticMgr::GetInstance().AddDelegate(&s_logger);
}
} // namespace

/// Port of stageView._ComputeCameraFraming():
/// Converts a Y-up integer viewport rect into a CameraUtilFraming whose
/// display/data windows are expressed in the Y-down coordinate system that
/// CameraUtilFraming / OpenEXR use.
static pxr::CameraUtilFraming ComputeCameraFraming(
    int x, int y, int w, int h,
    int renderBufferWidth, int renderBufferHeight)
{
    // Flip Y: viewport is Y-up, display/data windows are Y-down.
    float dy  = static_cast<float>(renderBufferHeight - y - h);
    float dy2 = static_cast<float>(renderBufferHeight - y);

    pxr::GfRange2f displayWindow(
        pxr::GfVec2f(static_cast<float>(x), dy),
        pxr::GfVec2f(static_cast<float>(x + w), dy2));

    // dataWindow: integer rect, same area but Y-flipped.
    pxr::GfRect2i renderBufferRect(
        pxr::GfVec2i(0, 0),
        renderBufferWidth, renderBufferHeight);
    pxr::GfRect2i dataWindow = renderBufferRect.GetIntersection(
        pxr::GfRect2i(
            pxr::GfVec2i(x, static_cast<int>(dy)),
            w, h));

    return pxr::CameraUtilFraming(displayWindow, dataWindow);
}

static GLuint CompileShader(const char* source, GLenum type) {
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &source, nullptr);
    glCompileShader(shader);
    GLint ok = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char log[512];
        glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
        LOG_ERROR("Shader compile error: " + std::string(log));
        glDeleteShader(shader);
        return 0;
    }
    return shader;
}

static GLuint LinkProgram(GLuint vs, GLuint fs) {
    GLuint prog = glCreateProgram();
    glAttachShader(prog, vs);
    glAttachShader(prog, fs);
    glLinkProgram(prog);
    glDeleteShader(vs);
    glDeleteShader(fs);
    GLint ok = 0;
    glGetProgramiv(prog, GL_LINK_STATUS, &ok);
    if (!ok) {
        char log[512];
        glGetProgramInfoLog(prog, sizeof(log), nullptr, log);
        LOG_ERROR("Program link error: " + std::string(log));
        glDeleteProgram(prog);
        return 0;
    }
    return prog;
}

// ---------------------------------------------------------------------------
// GLSL sources
// ---------------------------------------------------------------------------

// The grid reuses the same axis shader (kAxisVS / kAxisFS) — no separate
// grid program is needed.  Line geometry is built in RebuildGridVBO().

// Axis shader (stageView.DrawAxis port)
// VS receives 3D position; uniform MVP scales and projects.
static const char* kAxisVS = R"(#version 130
in vec3 position;
uniform mat4 mvpMatrix;
void main() { gl_Position = vec4(position, 1.0) * mvpMatrix; }
)";

static const char* kAxisFS = R"(#version 130
uniform vec4 color;
out vec4 outColor;
void main() { outColor = color; }
)";

// ===========================================================================
// Constructor / Destructor
// ===========================================================================

UsdSceneRenderer::UsdSceneRenderer()
    : m_showGrid(true)
    , m_showCameraGuide(false)
    , m_showGuides(false)
    , m_showProxy(true)
    , m_showRender(false)
    , m_currentAov(pxr::TfToken("color"))
    , m_aaEnabled(false)
    , m_backgroundColor(0.15f, 0.15f, 0.15f)
    , m_shadingMode(ShadingMode::SmoothShaded)
    , m_cullStyle(CullStyle::Nothing)   // matches usdview; see ViewportTileSettings::cullStyle
    , m_colorCorrectionMode(ColorCorrectionMode::sRGB)
    , m_ambientLightOnly(true)
    , m_domeLightEnabled(false)
    , m_stageIsZup(false)
    , m_defaultMaterialAmbient(0.2f)
    , m_defaultMaterialSpecular(0.1f)
    , m_rendererInitialized(false)
    , m_forceRefresh(false)
    , m_useCameraPath(false)
    , m_diagFrameCount(0)
    , m_gridVAO(0)
    , m_gridVBO(0)
    , m_gridMinorFirst(0),  m_gridMinorCount(0)
    , m_gridMajorFirst(0),  m_gridMajorCount(0)
    , m_gridAxisAFirst(0),  m_gridAxisACount(0)
    , m_gridAxisBFirst(0),  m_gridAxisBCount(0)
    , m_gridHalfSize(50.0f)
    , m_axisVAO(0)
    , m_axisVBO(0)
    , m_axisProgram(0)
    , m_axisUniformMVP(-1)
    , m_axisUniformColor(-1)
    , m_bboxMode(BBoxMode::None)
    , m_bboxColor(1.0f, 1.0f, 1.0f, 1.0f)
    , m_bboxVAO(0)
    , m_bboxVBO(0)
    , m_bboxProgram(0)
    , m_bboxUniformMVP(-1)
    , m_bboxUniformColor(-1)
{
    m_viewMatrix.SetIdentity();
    m_projMatrix.SetIdentity();
}

UsdSceneRenderer::~UsdSceneRenderer() {
    DestroyGridResources();
    DestroyAxisResources();
    DestroyBBoxResources();
    DestroyCamWireResources();
    DestroyLightWireResources();
}

// ===========================================================================
// Stage
// ===========================================================================

void UsdSceneRenderer::SetStage(pxr::UsdStageRefPtr stage) {
    m_stage = stage;
    m_rendererInitialized = false;
    m_useCameraPath = false;
    m_cameraPath    = pxr::SdfPath();
    m_clipPlanes.clear();
    m_cameraCacheDirty = true;
    DestroyGridResources();
    DestroyAxisResources();
    DestroyBBoxResources();
    DestroyCamWireResources();
    DestroyLightWireResources();

    // Determine stage up-axis for dome light rotation (mirrors stageView._stageIsZup)
    if (stage) {
        pxr::TfToken upAxis = pxr::UsdGeomGetStageUpAxis(stage);
        m_stageIsZup = (upAxis == pxr::UsdGeomTokens->z);
    } else {
        m_stageIsZup = false;
    }

    // Rebuild grid geometry for the new up-axis (if GL resources are ready)
    RebuildGridVBO();
}

// ===========================================================================
// Renderer Initialization (lazy, deferred to first Render())
// ===========================================================================

void UsdSceneRenderer::InitRenderer() {
    if (m_rendererInitialized || !m_stage) return;

    LOG_INFO("UsdSceneRenderer::InitRenderer - initializing...");

    // Route USD/Hydra diagnostics (incl. OCIO failures) to the app log.
    InstallUsdDiagnosticLogger();

    pxr::GlfContextCaps::InitInstance();

    pxr::UsdImagingGLEngine::Parameters params;
    params.rootPath    = m_stage->GetPseudoRoot().GetPath();
    params.excludedPaths = {};
    m_renderer = std::make_shared<pxr::UsdImagingGLEngine>(params);
    if (!m_renderer) {
        LOG_ERROR("Failed to create UsdImagingGLEngine");
        return;
    }

    // Plugin selection: prefer HdStorm / GL-based renderers
    auto plugins = pxr::UsdImagingGLEngine::GetRendererPlugins();
    LOG_INFO("Available renderer plugins: " + std::to_string(plugins.size()));
    bool hasCycles = false;
    for (const auto& p : plugins) {
        LOG_INFO("  " + std::string(p.GetText()) + " -> "
                 + pxr::UsdImagingGLEngine::GetRendererDisplayName(p));
        if (std::string(p.GetText()) == "HdCyclesPlugin") hasCycles = true;
    }
    if (!hasCycles)
        LOG_WARNING("HdCyclesPlugin not available (see startup log for DLL load errors).");

    // Apply the requested plugin. m_currentRendererPlugin is set from global
    // preferences before InitRenderer runs; always honour it rather than
    // checking whether the engine already chose something by default.
    if (!m_currentRendererPlugin.IsEmpty()) {
        pxr::TfToken cur = m_renderer->GetCurrentRendererId();
        if (cur != m_currentRendererPlugin)
            m_renderer->SetRendererPlugin(m_currentRendererPlugin);
    } else if (!plugins.empty()) {
        // Fallback heuristic: prefer Storm/GL
        pxr::TfToken best;
        for (const auto& p : plugins) {
            std::string n(p.GetText());
            if (n.find("Storm") != std::string::npos ||
                n.find("GL")    != std::string::npos) { best = p; break; }
        }
        if (best.IsEmpty()) best = plugins[0];
        m_renderer->SetRendererPlugin(best);
    }
    LOG_INFO("Selected renderer: " + std::string(m_renderer->GetCurrentRendererId().GetText()));

    // Enable AOV (matches stageView._handleRendererChanged; uses saved m_currentAov)
    m_renderer->SetRendererAov(m_currentAov);

    // Selection highlight color (usdview default: yellow)
    m_renderer->SetSelectionColor(pxr::GfVec4f(1.0f, 1.0f, 0.0f, 1.0f));

    // Remember which plugin is active
    m_currentRendererPlugin = m_renderer->GetCurrentRendererId();

    m_rendererInitialized = true;
    LOG_INFO("UsdSceneRenderer initialized.");

    InitGridResources();
    InitAxisResources();
    InitBBoxResources();
    InitCamWireResources();
    InitLightWireResources();
}

// ===========================================================================
// Render delegate
// ===========================================================================

/*static*/ std::vector<pxr::TfToken> UsdSceneRenderer::GetRendererPlugins() {
    return pxr::UsdImagingGLEngine::GetRendererPlugins();
}

pxr::TfToken UsdSceneRenderer::GetCurrentRendererId() const {
    if (m_renderer) return m_renderer->GetCurrentRendererId();
    return m_currentRendererPlugin;
}

/*static*/ std::string UsdSceneRenderer::GetRendererDisplayName(const pxr::TfToken& pluginId) {
    return pxr::UsdImagingGLEngine::GetRendererDisplayName(pluginId);
}

bool UsdSceneRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
    m_currentRendererPlugin = pluginId;

    if (!m_renderer) {
        // Will be picked up on first Render() call via InitRenderer()
        return true;
    }

    bool ok = m_renderer->SetRendererPlugin(pluginId);
    if (ok) {
        // Reset AOV to "color" on every delegate switch (mirrors stageView._handleRendererChanged)
        m_currentAov = pxr::TfToken("color");
        m_renderer->SetRendererAov(m_currentAov);
        LOG_INFO("Render delegate switched to: " + std::string(pluginId.GetText()));
    } else {
        LOG_ERROR("Failed to switch render delegate to: " + std::string(pluginId.GetText()));
    }
    return ok;
}

pxr::TfTokenVector UsdSceneRenderer::GetRendererAovs() const
{
    if (!m_renderer) return {};
    return m_renderer->GetRendererAovs();
}

bool UsdSceneRenderer::SetCurrentAov(const pxr::TfToken& aov)
{
    if (!m_renderer) return false;
    if (m_renderer->SetRendererAov(aov)) {
        m_currentAov = aov;
        return true;
    }
    return false;
}

pxr::UsdImagingGLRendererSettingsList UsdSceneRenderer::GetRendererSettingsList() const
{
    if (!m_renderer) return {};
    return m_renderer->GetRendererSettingsList();
}

pxr::VtValue UsdSceneRenderer::GetRendererSetting(const pxr::TfToken& id) const
{
    if (!m_renderer) return {};
    return m_renderer->GetRendererSetting(id);
}

void UsdSceneRenderer::SetRendererSetting(const pxr::TfToken& id, const pxr::VtValue& v)
{
    if (m_renderer) m_renderer->SetRendererSetting(id, v);
}

// ===========================================================================
// Camera State
// ===========================================================================

void UsdSceneRenderer::SetCameraState(
    const pxr::GfMatrix4d& viewMatrix,
    const pxr::GfMatrix4d& projMatrix)
{
    m_viewMatrix    = viewMatrix;
    m_projMatrix    = projMatrix;
    m_useCameraPath = false;
    m_clipPlanes.clear();
}

void UsdSceneRenderer::SetCameraStateFromGfCamera(const pxr::GfCamera& gfCamera)
{
    pxr::GfFrustum frustum = gfCamera.GetFrustum();
    m_viewMatrix    = frustum.ComputeViewMatrix();
    m_projMatrix    = frustum.ComputeProjectionMatrix();
    m_useCameraPath = false;
    m_cameraFrustum    = frustum;
    m_hasCameraFrustum = true;

    // Extract clip planes from GfCamera (same as stageView's renderParams.clipPlanes)
    m_clipPlanes.clear();
    for (const auto& p : gfCamera.GetClippingPlanes()) {
        m_clipPlanes.emplace_back(
            static_cast<double>(p[0]), static_cast<double>(p[1]),
            static_cast<double>(p[2]), static_cast<double>(p[3]));
    }
}

void UsdSceneRenderer::SetCameraPath(const pxr::SdfPath& cameraPath)
{
    m_cameraPath    = cameraPath;
    m_useCameraPath = true;
}

// ===========================================================================
// Selection
// ===========================================================================

void UsdSceneRenderer::ClearSelected()
{
    if (m_renderer) m_renderer->ClearSelected();
}

void UsdSceneRenderer::AddSelected(const pxr::SdfPath& path, int instanceIndex)
{
    if (m_renderer) m_renderer->AddSelected(path, instanceIndex);
}

void UsdSceneRenderer::SetSelectedPaths(const pxr::SdfPathVector& paths)
{
    if (m_renderer) m_renderer->SetSelected(paths);
}

// ===========================================================================
// Bounds
// ===========================================================================

pxr::GfRange3d UsdSceneRenderer::ComputeStageBounds()
{
    if (!m_stage) return {};
    pxr::TfTokenVector purposes = {
        pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
    pxr::UsdGeomBBoxCache bboxCache(m_currentTime, purposes, true);
    return bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot()).ComputeAlignedRange();
}

// ===========================================================================
// Picking (stageView.pick / computePickFrustum port)
// ===========================================================================

bool UsdSceneRenderer::PickObject(
    int mouseX, int mouseY,
    int viewWidth, int viewHeight,
    pxr::GfVec3d*  outHitPoint,
    pxr::SdfPath*  outHitPrimPath)
{
    if (!m_renderer || !m_stage) return false;
    if (!m_hasCameraFrustum) return false;

    // Normalize mouse to NDC [-1, 1]; Y is flipped (screen Y-down → NDC Y-up)
    double nx = (static_cast<double>(mouseX) / static_cast<double>(viewWidth))  * 2.0 - 1.0;
    double ny = 1.0 - (static_cast<double>(mouseY) / static_cast<double>(viewHeight)) * 2.0;
    pxr::GfVec2d point(nx, ny);
    // Pick window: one pixel in NDC
    pxr::GfVec2d size(1.0 / static_cast<double>(viewWidth),
                      1.0 / static_cast<double>(viewHeight));

    // Build narrow pick frustum from the stored camera frustum
    // (mirrors stageView.computePickFrustum / GfFrustum.ComputeNarrowedFrustum)
    pxr::GfFrustum pickFrustum = m_cameraFrustum.ComputeNarrowedFrustum(point, size);

    pxr::UsdImagingGLRenderParams pickParams;
    pickParams.drawMode   = pxr::UsdImagingGLDrawMode::DRAW_GEOM_ONLY; // no shading: faster pick pass
    pickParams.showGuides = false;
    pickParams.showProxy  = true;
    pickParams.showRender = false;
    pickParams.enableSampleAlphaToCoverage = false;
    pickParams.enableLighting              = false;

    pxr::GfVec3d hitPoint, hitNormal;
    pxr::SdfPath hitPrimPath;

    bool hit = m_renderer->TestIntersection(
        pickFrustum.ComputeViewMatrix(),
        pickFrustum.ComputeProjectionMatrix(),
        m_stage->GetPseudoRoot(),
        pickParams,
        &hitPoint, &hitNormal, &hitPrimPath);

    if (hit) {
        if (outHitPoint)    *outHitPoint    = hitPoint;
        if (outHitPrimPath) *outHitPrimPath = hitPrimPath;
        LOG_INFO("PickObject hit: " + hitPrimPath.GetString());
    }
    return hit;
}

bool UsdSceneRenderer::PickObjectsInRect(
    int x0, int y0, int x1, int y1,
    int viewWidth, int viewHeight,
    pxr::SdfPathVector* outHitPaths)
{
    if (!m_renderer || !m_stage || !m_hasCameraFrustum) return false;
    if (!outHitPaths) return false;
    outHitPaths->clear();

    // Clamp rect to viewport bounds
    x0 = std::max(0, std::min(x0, viewWidth  - 1));
    x1 = std::max(0, std::min(x1, viewWidth  - 1));
    y0 = std::max(0, std::min(y0, viewHeight - 1));
    y1 = std::max(0, std::min(y1, viewHeight - 1));
    if (x0 > x1) std::swap(x0, x1);
    if (y0 > y1) std::swap(y0, y1);
    if (x0 == x1 || y0 == y1) return false;

    // Convert rect to NDC [-1, 1]; Y is flipped (screen Y-down → NDC Y-up)
    double ndcCenterX = ((static_cast<double>(x0 + x1) * 0.5) / viewWidth)  * 2.0 - 1.0;
    double ndcCenterY = 1.0 - ((static_cast<double>(y0 + y1) * 0.5) / viewHeight) * 2.0;
    double ndcSizeX   =  static_cast<double>(x1 - x0) / viewWidth;
    double ndcSizeY   =  static_cast<double>(y1 - y0) / viewHeight;

    pxr::GfVec2d center(ndcCenterX, ndcCenterY);
    pxr::GfVec2d size  (ndcSizeX,   ndcSizeY);

    // Narrow the stored camera frustum to the selection rect
    pxr::GfFrustum pickFrustum = m_cameraFrustum.ComputeNarrowedFrustum(center, size);

    pxr::UsdImagingGLRenderParams pickParams;
    pickParams.drawMode   = pxr::UsdImagingGLDrawMode::DRAW_GEOM_ONLY; // no shading: faster pick pass
    pickParams.showGuides = false;
    pickParams.showProxy  = true;
    pickParams.showRender = false;
    pickParams.enableSampleAlphaToCoverage = false;
    pickParams.enableLighting              = false;

    // resolveUnique: single pick-buffer render pass, returns all unique VISIBLE prims.
    // Much faster than resolveDeep (which does a full deep-selection traversal).
    pxr::UsdImagingGLEngine::PickParams pp;
    pp.resolveMode = pxr::TfToken("resolveUnique");

    pxr::UsdImagingGLEngine::IntersectionResultVector results;
    bool hit = m_renderer->TestIntersection(
        pp,
        pickFrustum.ComputeViewMatrix(),
        pickFrustum.ComputeProjectionMatrix(),
        m_stage->GetPseudoRoot(),
        pickParams,
        &results);

    if (hit) {
        // Deduplicate by prim path (a prim may appear multiple times for
        // different instances or mesh subsets within the rect).
        std::unordered_set<std::string> seen;
        for (const auto& r : results) {
            const std::string& s = r.hitPrimPath.GetString();
            if (!s.empty() && seen.insert(s).second) {
                outHitPaths->push_back(r.hitPrimPath);
            }
        }
        LOG_INFO("PickObjectsInRect: " + std::to_string(outHitPaths->size()) + " prims selected");
    }
    return !outHitPaths->empty();
}

// ===========================================================================
// Render
// ===========================================================================

void UsdSceneRenderer::Render(int width, int height)
{
    if (!m_stage || width <= 0 || height <= 0) return;
    m_lastRenderWidth  = width;
    m_lastRenderHeight = height;

    InitRenderer();
    if (!m_renderer) return;

    // (Re)create draw target when it doesn't exist or when the AA/MSAA
    // preference has changed.  With AA enabled we request a multisampled
    // FBO so Hydra and all overlay geometry benefit from hardware MSAA.
    const bool wantMSAA = m_aaEnabled;
    if (m_drawTarget && m_drawTarget->HasMSAA() != wantMSAA) {
        if (m_drawTarget->IsBound()) m_drawTarget->Unbind();
        m_drawTarget.Reset();
        LOG_INFO("DrawTarget recreated: AA toggled (" +
                 std::string(wantMSAA ? "MSAA on" : "MSAA off") + ")");
    }
    if (!m_drawTarget) {
        m_drawTarget = pxr::GlfDrawTarget::New(pxr::GfVec2i(width, height), wantMSAA);
        if (!m_drawTarget) { LOG_ERROR("Failed to create GlfDrawTarget"); return; }
        m_drawTarget->Bind();
        // RGBA16F: Hydra's internal render buffers carry linear HDR values;
        // RGBA16F on the draw target preserves precision in the corrected output.
        m_drawTarget->AddAttachment("color", GL_RGBA, GL_FLOAT, GL_RGBA16F);
        m_drawTarget->AddAttachment("depth",
            GL_DEPTH_COMPONENT, GL_FLOAT, GL_DEPTH_COMPONENT32F);
        m_drawTarget->Unbind();
        LOG_INFO("DrawTarget created: fboId="
            + std::to_string(m_drawTarget->GetFramebufferId())
            + " msaa=" + std::string(wantMSAA ? "yes" : "no")
            + " size=" + std::to_string(width) + "x" + std::to_string(height));
    }

    // Bind first, then resize if needed.
    // GlfDrawTarget::SetSize requires the FBO to be bound (asserts otherwise).
    m_drawTarget->Bind();
    pxr::GfVec2i desiredSize(width, height);
    if (m_drawTarget->GetSize() != desiredSize) {
        m_drawTarget->SetSize(desiredSize);
    }

    // Clear
    glViewport(0, 0, width, height);
    glClearColor(m_backgroundColor[0], m_backgroundColor[1],
                 m_backgroundColor[2], 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // --- Camera state ---
    if (m_useCameraPath && !m_cameraPath.IsEmpty()) {
        m_renderer->SetCameraPath(m_cameraPath);
    } else {
        m_renderer->SetCameraState(m_viewMatrix, m_projMatrix);
    }

    // --- Use SetRenderBufferSize + SetFraming instead of deprecated SetRenderViewport ---
    // (mirrors stageView.paintGL: renderer.SetRenderBufferSize + renderer.SetFraming)
    m_renderer->SetRenderBufferSize(pxr::GfVec2i(width, height));
    m_renderer->SetFraming(ComputeCameraFraming(0, 0, width, height, width, height));
    m_renderer->SetOverrideWindowPolicy(pxr::CameraUtilMatchVertically);

    // --- Lighting: mirrors stageView.py paintGL lighting setup ---
    // stageView uses two optional lights controlled by viewSettings:
    //   ambientLightOnly  (default True)  → camera headlight (point at cam pos)
    //   domeLightEnabled  (default False) → dome/IBL light
    // sceneAmbient and material values from viewSettingsDataModel.py defaults.

    pxr::GfVec4f sceneAmbient(0.01f, 0.01f, 0.01f, 1.0f);

    pxr::GlfSimpleMaterial material;
    float kA = m_defaultMaterialAmbient;   // 0.2
    float kS = m_defaultMaterialSpecular;  // 0.1
    material.SetAmbient (pxr::GfVec4f(kA, kA, kA, 1.0f));
    material.SetSpecular(pxr::GfVec4f(kS, kS, kS, 1.0f));
    material.SetShininess(32.0f);

    pxr::GlfSimpleLightVector lights;

    // Camera headlight: point light (w=1) positioned at the camera world-origin,
    // transformed by the view-inverse so it tracks the camera each frame.
    // (stageView.py: l.position = cam_pos + (1,); l.transform = frustum.ComputeViewInverse())
    //
    // The headlight is the *default* fill used only while the stage has no
    // authored lights.  Once the scene contains real UsdLux lights, suppress it
    // so the scene is lit purely by those lights (Hydra evaluates them via
    // enableSceneLights) -- mirrors usdview's "use scene lights when present".
    const bool stageHasLights = StageHasAuthoredLights();
    if (m_ambientLightOnly && !stageHasLights) {
        pxr::GfMatrix4d viewInverse = m_viewMatrix.GetInverse();
        pxr::GfVec3d camPos = viewInverse.ExtractTranslation();

        pxr::GlfSimpleLight camLight;
        camLight.SetAmbient (pxr::GfVec4f(0.0f, 0.0f, 0.0f, 0.0f));
        camLight.SetDiffuse (pxr::GfVec4f(1.0f, 1.0f, 1.0f, 1.0f));
        camLight.SetSpecular(pxr::GfVec4f(1.0f, 1.0f, 1.0f, 1.0f));
        camLight.SetPosition(pxr::GfVec4f(
            static_cast<float>(camPos[0]),
            static_cast<float>(camPos[1]),
            static_cast<float>(camPos[2]),
            1.0f));                        // w=1 → point light
        camLight.SetTransform(viewInverse);
        lights.push_back(camLight);
    }

    // Dome light (IBL): isDomeLight=true, Z-up stages need a 90° X-axis rotation.
    // (stageView.py: l.isDomeLight = True; if stageIsZup: l.transform = rot90X)
    if (m_domeLightEnabled) {
        pxr::GlfSimpleLight domeLight;
        domeLight.SetIsDomeLight(true);
        if (m_stageIsZup) {
            pxr::GfMatrix4d rot;
            rot.SetRotate(pxr::GfRotation(pxr::GfVec3d::XAxis(), 90.0));
            domeLight.SetTransform(rot);
        }
        lights.push_back(domeLight);
    }

    m_renderer->SetLightingState(lights, material, sceneAmbient);

    // --- Render params (matches stageView.renderSinglePass) ---
    m_renderParams = pxr::UsdImagingGLRenderParams();
    m_renderParams.frame                    = m_currentTime;
    m_renderParams.complexity               = m_complexity;
    // Apply shading mode → drawMode + enableLighting
    switch (m_shadingMode) {
        case ShadingMode::FlatShaded:
            m_renderParams.drawMode     = pxr::UsdImagingGLDrawMode::DRAW_SHADED_FLAT;
            m_renderParams.enableLighting = true;  break;
        case ShadingMode::WireframeOnSurface:
            m_renderParams.drawMode     = pxr::UsdImagingGLDrawMode::DRAW_WIREFRAME_ON_SURFACE;
            m_renderParams.enableLighting = true;  break;
        case ShadingMode::Wireframe:
            m_renderParams.drawMode     = pxr::UsdImagingGLDrawMode::DRAW_WIREFRAME;
            m_renderParams.enableLighting = false; break;
        case ShadingMode::Unlit:
            m_renderParams.drawMode     = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
            m_renderParams.enableLighting = false; break;
        default: // SmoothShaded
            m_renderParams.drawMode     = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
            m_renderParams.enableLighting = true;  break;
    }
    m_renderParams.showGuides               = m_showGuides;
    m_renderParams.showProxy                = m_showProxy;
    m_renderParams.showRender               = m_showRender;
    m_renderParams.enableSampleAlphaToCoverage = true;
    m_renderParams.gammaCorrectColors       = false;
    m_renderParams.cullStyle                = static_cast<pxr::UsdImagingGLCullStyle>(m_cullStyle);
    m_renderParams.enableSceneMaterials     = true;
    m_renderParams.enableSceneLights        = true;
    m_renderParams.highlight                = true;
    m_renderParams.clearColor               = pxr::GfVec4f(
        m_backgroundColor[0], m_backgroundColor[1], m_backgroundColor[2], 1.0f);
    m_renderParams.forceRefresh             = m_forceRefresh;
    m_renderParams.clipPlanes               = m_clipPlanes;

    // Color correction — stageView.py approach: pass the actual mode + OCIO
    // params to HdxColorCorrectionTask via RenderParams and SetColorCorrection-
    // Settings (mirrors stageView.renderSinglePass).
    pxr::TfToken ccToken("disabled");
    if (m_colorCorrectionMode == ColorCorrectionMode::sRGB)
        ccToken = pxr::TfToken("sRGB");
    else if (m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO)
        ccToken = pxr::TfToken("openColorIO");

    m_renderParams.colorCorrectionMode = ccToken;
    if (m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO) {
        m_renderParams.ocioDisplay    = pxr::TfToken(m_ocioDisplay);
        m_renderParams.ocioView       = pxr::TfToken(m_ocioView);
        m_renderParams.ocioColorSpace = pxr::TfToken(m_ocioColorSpace);
        m_renderParams.ocioLook       = pxr::TfToken(m_ocioLook);
    }
    m_renderer->SetColorCorrectionSettings(
        ccToken,
        pxr::TfToken(m_ocioDisplay),
        pxr::TfToken(m_ocioView),
        pxr::TfToken(m_ocioColorSpace),
        pxr::TfToken(m_ocioLook));

    // Guard the Hydra render: if it throws, the m_drawTarget->Unbind() below
    // would be skipped, permanently unbalancing the GlfDrawTarget bind stack
    // and blacking out every later frame.
    try {
        m_renderer->Render(m_stage->GetPseudoRoot(), m_renderParams);
    } catch (const std::exception& e) {
        LOG_ERROR("Hydra render failed: " + std::string(e.what()));
    }
    m_forceRefresh = false;

    // --- Optional grid overlay ---
    if (m_showGrid) {
        // When MSAA is active, render the grid into the MSAA FBO so it is
        // also multisampled and resolved together with the Hydra output.
        GLuint gridFbo = m_drawTarget->HasMSAA()
            ? m_drawTarget->GetFramebufferMSId()
            : m_drawTarget->GetFramebufferId();
        glBindFramebuffer(GL_FRAMEBUFFER, gridFbo);
        glViewport(0, 0, width, height);
        RenderGrid(width, height);
    }

    // --- Diagnostic logging ---
    if (m_diagFrameCount < 3) {
        auto att = m_drawTarget->GetAttachment("color");
        if (att) {
            LOG_INFO("Frame " + std::to_string(m_diagFrameCount)
                + ": texId=" + std::to_string(att->GetGlTextureName())
                + " fboId=" + std::to_string(m_drawTarget->GetFramebufferId())
                + " size=" + std::to_string(width) + "x" + std::to_string(height)
                + " bg=" + Vec3fStr(m_backgroundColor));
        }
        ++m_diagFrameCount;
    }

    m_drawTarget->Unbind();
}

// ===========================================================================
// Output
// ===========================================================================

uint32_t UsdSceneRenderer::GetColorTextureID()
{
    if (!m_drawTarget) return 0;
    // Resolve MSAA → regular texture before the caller samples it.
    // Called after all overlay draws (axis, bboxes, camera wireframes) so
    // every layer of MSAA-rendered content is included in the resolve.
    m_drawTarget->Resolve();
    auto att = m_drawTarget->GetAttachment("color");
    return att ? static_cast<uint32_t>(att->GetGlTextureName()) : 0;
}

bool UsdSceneRenderer::CaptureFrame(std::vector<uint8_t>& outRGBA)
{
    if (!m_drawTarget || m_lastRenderWidth <= 0 || m_lastRenderHeight <= 0)
        return false;

    int w = m_lastRenderWidth;
    int h = m_lastRenderHeight;

    // Resolve MSAA so we read from the non-multisampled colour attachment.
    m_drawTarget->Resolve();

    outRGBA.resize(static_cast<size_t>(w) * h * 4);

    // Save/restore the read-framebuffer binding so we don't upset ImGui.
    GLint prevFbo = 0;
    glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevFbo);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, m_drawTarget->GetFramebufferId());
    glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, outRGBA.data());
    glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(prevFbo));

    // OpenGL reads bottom-up; flip to top-down.
    int stride = w * 4;
    std::vector<uint8_t> row(stride);
    for (int y = 0; y < h / 2; ++y) {
        uint8_t* top = outRGBA.data() + y * stride;
        uint8_t* bot = outRGBA.data() + (h - 1 - y) * stride;
        std::copy(top, top + stride, row.data());
        std::copy(bot, bot + stride, top);
        std::copy(row.data(), row.data() + stride, bot);
    }

    return true;
}

// ===========================================================================
// Axis Overlay (stageView.DrawAxis port)
// ===========================================================================

void UsdSceneRenderer::InitAxisResources()
{
    if (m_axisProgram != 0) return;

    GLuint vs = CompileShader(kAxisVS, GL_VERTEX_SHADER);
    GLuint fs = CompileShader(kAxisFS, GL_FRAGMENT_SHADER);
    if (!vs || !fs) { glDeleteShader(vs); glDeleteShader(fs); return; }
    m_axisProgram = LinkProgram(vs, fs);
    if (!m_axisProgram) return;

    m_axisUniformMVP   = glGetUniformLocation(m_axisProgram, "mvpMatrix");
    m_axisUniformColor = glGetUniformLocation(m_axisProgram, "color");

    // 3 line segments: X=(1,0,0), Y=(0,1,0), Z=(0,0,1) from origin (0,0,0)
    float axisVerts[] = {
        1,0,0,  0,0,0,
        0,1,0,  0,0,0,
        0,0,1,  0,0,0
    };

    glGenVertexArrays(1, &m_axisVAO);
    glGenBuffers(1, &m_axisVBO);
    glBindVertexArray(m_axisVAO);
    glBindBuffer(GL_ARRAY_BUFFER, m_axisVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(axisVerts), axisVerts, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
    glBindVertexArray(0);
    LOG_INFO("Axis resources initialized.");
}

void UsdSceneRenderer::DestroyAxisResources()
{
    if (m_axisVAO)     { glDeleteVertexArrays(1, &m_axisVAO);  m_axisVAO = 0; }
    if (m_axisVBO)     { glDeleteBuffers(1, &m_axisVBO);        m_axisVBO = 0; }
    if (m_axisProgram) { glDeleteProgram(m_axisProgram);        m_axisProgram = 0; }
}

void UsdSceneRenderer::DrawAxis(
    const pxr::GfMatrix4d& viewProjMatrix, double cameraDist)
{
    if (!m_axisProgram || !m_axisVAO) return;
    if (!m_drawTarget) return;

    // Overlays are called after Render() has unbound the FBO.
    // Re-bind so we draw into the offscreen texture, not the default framebuffer.
    pxr::GfVec2i sz = m_drawTarget->GetSize();
    m_drawTarget->Bind();
    glViewport(0, 0, sz[0], sz[1]);

    // Scale the gizmo to stay roughly fixed in screen space (stageView: dist/20)
    pxr::GfMatrix4f mvp =
        pxr::GfMatrix4f(1.0f).SetScale(static_cast<float>(cameraDist / 20.0))
        * pxr::GfMatrix4f(viewProjMatrix);

    glUseProgram(m_axisProgram);
    glBindVertexArray(m_axisVAO);

    glUniformMatrix4fv(m_axisUniformMVP, 1, GL_TRUE, mvp.GetArray());

    GLboolean prevDepthMask;
    glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
    glDepthMask(GL_FALSE);
    GLboolean prevDepthTest  = glIsEnabled(GL_DEPTH_TEST);
    GLboolean prevLineSmooth = glIsEnabled(GL_LINE_SMOOTH);
    glEnable(GL_DEPTH_TEST);
    if (m_aaEnabled) { glEnable(GL_LINE_SMOOTH); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); }

    // X axis: red
    glUniform4f(m_axisUniformColor, 1, 0, 0, 1);
    glDrawArrays(GL_LINES, 0, 2);
    // Y axis: green
    glUniform4f(m_axisUniformColor, 0, 1, 0, 1);
    glDrawArrays(GL_LINES, 2, 2);
    // Z axis: blue
    glUniform4f(m_axisUniformColor, 0, 0, 1, 1);
    glDrawArrays(GL_LINES, 4, 2);

    glDepthMask(prevDepthMask);
    if (!prevDepthTest)  glDisable(GL_DEPTH_TEST);
    if (prevLineSmooth)  glEnable(GL_LINE_SMOOTH);
    else                 glDisable(GL_LINE_SMOOTH);

    glBindVertexArray(0);
    glUseProgram(0);
    m_drawTarget->Unbind();
}

// ===========================================================================
// Grid Overlay
// ===========================================================================

void UsdSceneRenderer::InitGridResources()
{
    if (m_gridVAO != 0) return;

    // Grid reuses the axis shader program — both use vec3 position + uniform MVP/color.
    // The VAO only needs a position attribute.
    glGenVertexArrays(1, &m_gridVAO);
    glGenBuffers(1, &m_gridVBO);
    glBindVertexArray(m_gridVAO);
    glBindBuffer(GL_ARRAY_BUFFER, m_gridVBO);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
    glBindVertexArray(0);

    LOG_INFO("Grid VAO/VBO created.");
    RebuildGridVBO();
}

void UsdSceneRenderer::RebuildGridVBO()
{
    // Can only rebuild once the VAO/VBO have been created.
    if (m_gridVAO == 0 || m_gridVBO == 0) return;

    // -------------------------------------------------------------------------
    // Build all grid line vertices grouped by category so we can draw each
    // group with a different colour in a single draw call.
    //
    //  Group A — minor lines  (1-unit spacing, excluding multiples of 10 and 0)
    //  Group B — major lines  (10-unit spacing, excluding 0)
    //  Group C — "A"-axis     (the axis along direction a, at b = 0  → red   )
    //  Group D — "B"-axis     (the axis along direction b, at a = 0  → blue/green)
    //
    // For Y-up: ground plane = XZ (Y=0), a = X axis, b = Z axis.
    // For Z-up: ground plane = XY (Z=0), a = X axis, b = Y axis.
    // -------------------------------------------------------------------------

    const float H   = m_gridHalfSize;          // half-extent (default 50)
    const int   N   = static_cast<int>(H);     // integer half-extent (e.g. 50)
    const int   maj = 10;                      // major-line interval

    // Helper: given a 2-D coordinate pair (a, b) on the ground plane,
    // return the 3-D world position based on the current up-axis.
    auto toWorld = [&](float a, float b) -> std::array<float,3> {
        if (m_stageIsZup)
            return { a, b, 0.0f };   // XY plane at Z=0
        else
            return { a, 0.0f, b };   // XZ plane at Y=0
    };

    std::vector<float> minor_verts, major_verts, axisA_verts, axisB_verts;

    // Lines parallel to the A-axis (at fixed b values):
    for (int bi = -N; bi <= N; ++bi) {
        float b = static_cast<float>(bi);
        auto  p0 = toWorld(-H, b);
        auto  p1 = toWorld( H, b);

        if (bi == 0) {
            // A-axis (red)
            axisA_verts.insert(axisA_verts.end(), p0.begin(), p0.end());
            axisA_verts.insert(axisA_verts.end(), p1.begin(), p1.end());
        } else if (bi % maj == 0) {
            // Major line
            major_verts.insert(major_verts.end(), p0.begin(), p0.end());
            major_verts.insert(major_verts.end(), p1.begin(), p1.end());
        } else {
            // Minor line
            minor_verts.insert(minor_verts.end(), p0.begin(), p0.end());
            minor_verts.insert(minor_verts.end(), p1.begin(), p1.end());
        }
    }

    // Lines parallel to the B-axis (at fixed a values):
    for (int ai = -N; ai <= N; ++ai) {
        float a = static_cast<float>(ai);
        auto  p0 = toWorld(a, -H);
        auto  p1 = toWorld(a,  H);

        if (ai == 0) {
            // B-axis (blue / green)
            axisB_verts.insert(axisB_verts.end(), p0.begin(), p0.end());
            axisB_verts.insert(axisB_verts.end(), p1.begin(), p1.end());
        } else if (ai % maj == 0) {
            major_verts.insert(major_verts.end(), p0.begin(), p0.end());
            major_verts.insert(major_verts.end(), p1.begin(), p1.end());
        } else {
            minor_verts.insert(minor_verts.end(), p0.begin(), p0.end());
            minor_verts.insert(minor_verts.end(), p1.begin(), p1.end());
        }
    }

    // Pack into one contiguous buffer: [minor | major | axisA | axisB]
    std::vector<float> all;
    all.reserve(minor_verts.size() + major_verts.size() +
                axisA_verts.size() + axisB_verts.size());

    auto floatsToVerts = [](size_t f) { return static_cast<GLint>(f / 3); };

    m_gridMinorFirst = 0;
    m_gridMinorCount = floatsToVerts(minor_verts.size());
    all.insert(all.end(), minor_verts.begin(), minor_verts.end());

    m_gridMajorFirst = m_gridMinorFirst + m_gridMinorCount;
    m_gridMajorCount = floatsToVerts(major_verts.size());
    all.insert(all.end(), major_verts.begin(), major_verts.end());

    m_gridAxisAFirst = m_gridMajorFirst + m_gridMajorCount;
    m_gridAxisACount = floatsToVerts(axisA_verts.size());
    all.insert(all.end(), axisA_verts.begin(), axisA_verts.end());

    m_gridAxisBFirst = m_gridAxisAFirst + m_gridAxisACount;
    m_gridAxisBCount = floatsToVerts(axisB_verts.size());
    all.insert(all.end(), axisB_verts.begin(), axisB_verts.end());

    glBindBuffer(GL_ARRAY_BUFFER, m_gridVBO);
    glBufferData(GL_ARRAY_BUFFER,
                 static_cast<GLsizeiptr>(all.size() * sizeof(float)),
                 all.data(), GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    LOG_INFO("Grid VBO rebuilt: "
        + std::to_string(m_gridMinorCount / 2) + " minor lines, "
        + std::to_string(m_gridMajorCount / 2) + " major lines, "
        + std::string(m_stageIsZup ? "Z-up" : "Y-up"));
}

void UsdSceneRenderer::DestroyGridResources()
{
    if (m_gridVAO) { glDeleteVertexArrays(1, &m_gridVAO); m_gridVAO = 0; }
    if (m_gridVBO) { glDeleteBuffers(1, &m_gridVBO);       m_gridVBO = 0; }
    // No separate grid program — the axis program is destroyed in DestroyAxisResources().
}

void UsdSceneRenderer::RenderGrid(int /*width*/, int /*height*/)
{
    if (!m_gridVAO || !m_axisProgram) return;

    // Grid reuses the axis shader.
    // MVP = view * proj  (no extra scale — grid is already in world units).
    pxr::GfMatrix4f mvp(m_viewMatrix * m_projMatrix);

    // Helper: enable/disable GL_LINE_SMOOTH depending on the AA setting.
    auto setLineAA = [&](bool enable) {
        if (enable) {
            glEnable(GL_LINE_SMOOTH);
            glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
        } else {
            glDisable(GL_LINE_SMOOTH);
        }
    };

    // Save relevant GL state.
    GLboolean prevDepthMask;
    GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
    GLboolean prevBlend     = glIsEnabled(GL_BLEND);
    GLboolean prevLineSmooth= glIsEnabled(GL_LINE_SMOOTH);
    GLint     prevDepthFunc, prevBlendSrc, prevBlendDst;
    glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
    glGetIntegerv(GL_DEPTH_FUNC,      &prevDepthFunc);
    glGetIntegerv(GL_BLEND_SRC_ALPHA, &prevBlendSrc);
    glGetIntegerv(GL_BLEND_DST_ALPHA, &prevBlendDst);

    glDepthMask(GL_FALSE);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    setLineAA(m_aaEnabled);

    glUseProgram(m_axisProgram);
    glUniformMatrix4fv(m_axisUniformMVP, 1, GL_TRUE, mvp.GetArray());
    glBindVertexArray(m_gridVAO);

    // --- Minor lines — dark grey ---
    glLineWidth(1.0f);
    glUniform4f(m_axisUniformColor, 0.35f, 0.35f, 0.35f, 1.0f);
    if (m_gridMinorCount > 0)
        glDrawArrays(GL_LINES, m_gridMinorFirst, m_gridMinorCount);

    // --- Major lines — medium grey ---
    glUniform4f(m_axisUniformColor, 0.52f, 0.52f, 0.52f, 1.0f);
    if (m_gridMajorCount > 0)
        glDrawArrays(GL_LINES, m_gridMajorFirst, m_gridMajorCount);

    // --- A-axis (at b=0): X axis — red ---
    glLineWidth(m_aaEnabled ? 1.5f : 1.0f);
    glUniform4f(m_axisUniformColor, 0.62f, 0.28f, 0.28f, 1.0f);
    if (m_gridAxisACount > 0)
        glDrawArrays(GL_LINES, m_gridAxisAFirst, m_gridAxisACount);

    // --- B-axis (at a=0): Z-axis (blue) for Y-up, Y-axis (green) for Z-up ---
    if (m_stageIsZup)
        glUniform4f(m_axisUniformColor, 0.28f, 0.55f, 0.28f, 1.0f);   // green (Y)
    else
        glUniform4f(m_axisUniformColor, 0.28f, 0.28f, 0.62f, 1.0f);   // blue  (Z)
    if (m_gridAxisBCount > 0)
        glDrawArrays(GL_LINES, m_gridAxisBFirst, m_gridAxisBCount);

    glBindVertexArray(0);
    glUseProgram(0);

    // Restore GL state.
    glLineWidth(1.0f);
    glDepthMask(prevDepthMask);
    glDepthFunc(static_cast<GLenum>(prevDepthFunc));
    glBlendFunc(static_cast<GLenum>(prevBlendSrc),
                static_cast<GLenum>(prevBlendDst));
    if (!prevDepthTest)  glDisable(GL_DEPTH_TEST);
    if (!prevBlend)      glDisable(GL_BLEND);
    if (prevLineSmooth)  glEnable(GL_LINE_SMOOTH);
    else                 glDisable(GL_LINE_SMOOTH);
}

// ===========================================================================
// Bounding Box Overlay
// ===========================================================================

// Reuse the same simple line shader as the axis gizmo.
static const char* kBBoxVS = R"(#version 130
in vec3 position;
uniform mat4 mvpMatrix;
void main() { gl_Position = vec4(position, 1.0) * mvpMatrix; }
)";

static const char* kBBoxFS = R"(#version 130
uniform vec4 color;
out vec4 outColor;
void main() { outColor = color; }
)";

void UsdSceneRenderer::InitBBoxResources()
{
    if (m_bboxProgram != 0) return;

    GLuint vs = CompileShader(kBBoxVS, GL_VERTEX_SHADER);
    GLuint fs = CompileShader(kBBoxFS, GL_FRAGMENT_SHADER);
    if (!vs || !fs) { glDeleteShader(vs); glDeleteShader(fs); return; }
    m_bboxProgram = LinkProgram(vs, fs);
    if (!m_bboxProgram) return;

    m_bboxUniformMVP   = glGetUniformLocation(m_bboxProgram, "mvpMatrix");
    m_bboxUniformColor = glGetUniformLocation(m_bboxProgram, "color");

    // Allocate a VAO/VBO for 24 vertices (12 edges × 2 endpoints) — data uploaded dynamically.
    glGenVertexArrays(1, &m_bboxVAO);
    glGenBuffers(1, &m_bboxVBO);
    glBindVertexArray(m_bboxVAO);
    glBindBuffer(GL_ARRAY_BUFFER, m_bboxVBO);
    glBufferData(GL_ARRAY_BUFFER, 24 * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
    glBindVertexArray(0);
    LOG_INFO("BBox resources initialized.");
}

void UsdSceneRenderer::DestroyBBoxResources()
{
    if (m_bboxVAO)     { glDeleteVertexArrays(1, &m_bboxVAO);  m_bboxVAO = 0; }
    if (m_bboxVBO)     { glDeleteBuffers(1, &m_bboxVBO);        m_bboxVBO = 0; }
    if (m_bboxProgram) { glDeleteProgram(m_bboxProgram);        m_bboxProgram = 0; }
}

// ──────────────────────────────────────────────────────────────────────────────
// Camera Wireframe GL resources
// Uses the same simple line shader as the axis / bbox overlays.
// A separate VAO/VBO avoids any interaction with the bbox dynamic VBO.
// ──────────────────────────────────────────────────────────────────────────────
void UsdSceneRenderer::InitCamWireResources()
{
    if (m_camWireVAO != 0) return;
    if (!m_bboxProgram) return;  // reuse the already-compiled bbox shader program

    glGenVertexArrays(1, &m_camWireVAO);
    glGenBuffers(1, &m_camWireVBO);

    glBindVertexArray(m_camWireVAO);
    glBindBuffer(GL_ARRAY_BUFFER, m_camWireVBO);
    // Allocate an initial budget; DrawCameraWireframes reallocates per frame.
    glBufferData(GL_ARRAY_BUFFER, 256 * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
    glBindVertexArray(0);
    LOG_INFO("Camera wireframe GL resources initialized.");
}

void UsdSceneRenderer::DestroyCamWireResources()
{
    if (m_camWireVAO) { glDeleteVertexArrays(1, &m_camWireVAO); m_camWireVAO = 0; }
    if (m_camWireVBO) { glDeleteBuffers(1, &m_camWireVBO);       m_camWireVBO = 0; }
}

void UsdSceneRenderer::InitLightWireResources()
{
    if (m_lightWireVAO != 0) return;
    if (!m_bboxProgram) return;  // reuse the already-compiled bbox shader program

    glGenVertexArrays(1, &m_lightWireVAO);
    glGenBuffers(1, &m_lightWireVBO);

    glBindVertexArray(m_lightWireVAO);
    glBindBuffer(GL_ARRAY_BUFFER, m_lightWireVBO);
    // Sphere/cylinder gizmos use several circles; budget more than the camera VBO.
    glBufferData(GL_ARRAY_BUFFER, 1024 * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
    glBindVertexArray(0);
    LOG_INFO("Light wireframe GL resources initialized.");
}

void UsdSceneRenderer::DestroyLightWireResources()
{
    if (m_lightWireVAO) { glDeleteVertexArrays(1, &m_lightWireVAO); m_lightWireVAO = 0; }
    if (m_lightWireVBO) { glDeleteBuffers(1, &m_lightWireVBO);       m_lightWireVBO = 0; }
}

void UsdSceneRenderer::DrawBox(
    const pxr::GfRange3d&  range,
    const pxr::GfMatrix4f& mvp)
{
    if (range.IsEmpty()) return;

    pxr::GfVec3d mn = range.GetMin();
    pxr::GfVec3d mx = range.GetMax();

    // 8 corners of the AABB
    float verts[24][3] = {
        // Bottom face
        { (float)mn[0], (float)mn[1], (float)mn[2] }, { (float)mx[0], (float)mn[1], (float)mn[2] },
        { (float)mx[0], (float)mn[1], (float)mn[2] }, { (float)mx[0], (float)mn[1], (float)mx[2] },
        { (float)mx[0], (float)mn[1], (float)mx[2] }, { (float)mn[0], (float)mn[1], (float)mx[2] },
        { (float)mn[0], (float)mn[1], (float)mx[2] }, { (float)mn[0], (float)mn[1], (float)mn[2] },
        // Top face
        { (float)mn[0], (float)mx[1], (float)mn[2] }, { (float)mx[0], (float)mx[1], (float)mn[2] },
        { (float)mx[0], (float)mx[1], (float)mn[2] }, { (float)mx[0], (float)mx[1], (float)mx[2] },
        { (float)mx[0], (float)mx[1], (float)mx[2] }, { (float)mn[0], (float)mx[1], (float)mx[2] },
        { (float)mn[0], (float)mx[1], (float)mx[2] }, { (float)mn[0], (float)mx[1], (float)mn[2] },
        // Vertical pillars
        { (float)mn[0], (float)mn[1], (float)mn[2] }, { (float)mn[0], (float)mx[1], (float)mn[2] },
        { (float)mx[0], (float)mn[1], (float)mn[2] }, { (float)mx[0], (float)mx[1], (float)mn[2] },
        { (float)mx[0], (float)mn[1], (float)mx[2] }, { (float)mx[0], (float)mx[1], (float)mx[2] },
        { (float)mn[0], (float)mn[1], (float)mx[2] }, { (float)mn[0], (float)mx[1], (float)mx[2] },
    };

    glBindBuffer(GL_ARRAY_BUFFER, m_bboxVBO);
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts);

    glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, mvp.GetArray());
    glBindVertexArray(m_bboxVAO);
    glDrawArrays(GL_LINES, 0, 24);
    glBindVertexArray(0);
}

void UsdSceneRenderer::DrawBoundingBoxes(
    const pxr::SdfPathVector& selectedPaths,
    const pxr::GfMatrix4d&   viewProjMatrix)
{
    if (m_bboxMode == BBoxMode::None) return;
    if (!m_bboxProgram || !m_bboxVAO) return;
    if (!m_stage || selectedPaths.empty()) return;
    if (!m_drawTarget) return;

    // Re-bind the offscreen FBO so we draw into the texture, not the default framebuffer.
    pxr::GfVec2i sz = m_drawTarget->GetSize();
    m_drawTarget->Bind();
    glViewport(0, 0, sz[0], sz[1]);

    // Compute world bboxes via UsdGeomBBoxCache
    pxr::TfTokenVector purposes = {
        pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
    pxr::UsdGeomBBoxCache bboxCache(
        m_currentTime, purposes, /*useExtentsHint=*/true);

    // Save/restore GL state
    GLboolean prevDepthMask;
    glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
    GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
    GLboolean prevBlend     = glIsEnabled(GL_BLEND);

    glDepthMask(GL_FALSE);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glUseProgram(m_bboxProgram);
    glUniform4f(m_bboxUniformColor,
        m_bboxColor[0], m_bboxColor[1], m_bboxColor[2], m_bboxColor[3]);

    pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);

    if (m_bboxMode == BBoxMode::PerObject) {
        for (const auto& path : selectedPaths) {
            pxr::UsdPrim prim = m_stage->GetPrimAtPath(path);
            if (!prim) continue;
            pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim);
            DrawBox(bbox.ComputeAlignedRange(), vp);
        }
    } else {
        // AllSelection: one combined AABB around all selected prims
        pxr::GfRange3d combined;
        for (const auto& path : selectedPaths) {
            pxr::UsdPrim prim = m_stage->GetPrimAtPath(path);
            if (!prim) continue;
            pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim);
            combined.UnionWith(bbox.ComputeAlignedRange());
        }
        DrawBox(combined, vp);
    }

    glDepthMask(prevDepthMask);
    if (!prevDepthTest) glDisable(GL_DEPTH_TEST);
    if (!prevBlend)     glDisable(GL_BLEND);

    glUseProgram(0);
    m_drawTarget->Unbind();
}

// ===========================================================================
// Draw-target FBO helpers
// ===========================================================================

void UsdSceneRenderer::BindDrawTarget()
{
    if (!m_drawTarget) return;
    pxr::GfVec2i sz = m_drawTarget->GetSize();
    m_drawTarget->Bind();
    glViewport(0, 0, sz[0], sz[1]);
}

void UsdSceneRenderer::UnbindDrawTarget()
{
    if (m_drawTarget) m_drawTarget->Unbind();
}

// ===========================================================================
// Camera Wireframe helpers
// ===========================================================================

// Project a world-space point to absolute screen coords (imagePosX/Y + pixel offsets).
// Returns false if the point is behind the near plane (w ≤ 0).
bool UsdSceneRenderer::WorldToScreen(const pxr::GfVec3d&    world,
                                      const pxr::GfMatrix4d& vp,
                                      int viewW, int viewH,
                                      float imagePosX, float imagePosY,
                                      float& outX, float& outY)
{
    double cx = vp[0][0]*world[0] + vp[1][0]*world[1] + vp[2][0]*world[2] + vp[3][0];
    double cy = vp[0][1]*world[0] + vp[1][1]*world[1] + vp[2][1]*world[2] + vp[3][1];
    double cw = vp[0][3]*world[0] + vp[1][3]*world[1] + vp[2][3]*world[2] + vp[3][3];
    if (cw <= 0.0) return false;
    double invW = 1.0 / cw;
    outX = imagePosX + static_cast<float>(( cx * invW + 1.0) * 0.5 * viewW);
    outY = imagePosY + static_cast<float>((1.0 - cy * invW) * 0.5 * viewH);
    return true;
}

float UsdSceneRenderer::PointToSegmentDist(float px, float py,
                                            float ax, float ay,
                                            float bx, float by)
{
    float dx = bx - ax, dy = by - ay;
    float lenSq = dx*dx + dy*dy;
    if (lenSq < 1e-6f) {
        float ex = px - ax, ey = py - ay;
        return std::sqrt(ex*ex + ey*ey);
    }
    float t  = std::max(0.f, std::min(1.f, ((px-ax)*dx + (py-ay)*dy) / lenSq));
    float cx2 = ax + t*dx - px;
    float cy2 = ay + t*dy - py;
    return std::sqrt(cx2*cx2 + cy2*cy2);
}

// Build camera wireframe line segments in world space.
// Geometry: body box (12 edges) + frustum pyramid (4 lines to near quad) + up arrow (1 line).
void UsdSceneRenderer::BuildCameraWireframeLines(const pxr::GfCamera& gfCam,
                                                  double                scale,
                                                  std::vector<float>&   outVerts)
{
    // Camera transform: rows are camera axes in world space.
    // Column convention: cameraToWorld = camMat (USD row-vector).
    pxr::GfMatrix4d camToWorld = gfCam.GetTransform();

    // Camera origin in world space
    pxr::GfVec3d origin(camToWorld[3][0], camToWorld[3][1], camToWorld[3][2]);

    // Camera axes (columns of the rotation part, row-major: row i = axis i of camera)
    // In USD GfMatrix4d row-vector convention: world = local * M
    // So camera right = row 0, up = row 1, -forward = row 2
    pxr::GfVec3d right(  camToWorld[0][0], camToWorld[0][1], camToWorld[0][2]);
    pxr::GfVec3d up(     camToWorld[1][0], camToWorld[1][1], camToWorld[1][2]);
    pxr::GfVec3d forward(camToWorld[2][0], camToWorld[2][1], camToWorld[2][2]);
    // Note: USD cameras look down -Z in local space, so forward here is the +Z local = backward in view.
    // The camera shoots along -forward (local -Z).
    pxr::GfVec3d lookDir = -forward;  // world-space look direction

    // ---- Body box ----
    double bh = scale * 0.10;  // half-size
    // 8 corners of the body box in world space
    pxr::GfVec3d c[8];
    for (int xi = -1; xi <= 1; xi += 2)
    for (int yi = -1; yi <= 1; yi += 2)
    for (int zi = -1; zi <= 1; zi += 2) {
        int idx = ((xi+1)/2) | (((yi+1)/2) << 1) | (((zi+1)/2) << 2);
        c[idx] = origin + right*bh*xi + up*bh*yi + lookDir*bh*zi;
    }
    // 12 edges of the box: connect corners whose indices differ by exactly one bit
    static const int kEdges[12][2] = {
        {0,1},{2,3},{4,5},{6,7},  // X edges
        {0,2},{1,3},{4,6},{5,7},  // Y edges
        {0,4},{1,5},{2,6},{3,7}   // Z edges
    };
    auto push = [&](const pxr::GfVec3d& a, const pxr::GfVec3d& b) {
        outVerts.push_back(static_cast<float>(a[0]));
        outVerts.push_back(static_cast<float>(a[1]));
        outVerts.push_back(static_cast<float>(a[2]));
        outVerts.push_back(static_cast<float>(b[0]));
        outVerts.push_back(static_cast<float>(b[1]));
        outVerts.push_back(static_cast<float>(b[2]));
    };
    for (auto& e : kEdges) push(c[e[0]], c[e[1]]);

    // ---- Frustum pyramid (4 lines from origin to near quad corners) ----
    // Use perspective projection: half-widths at near depth proportional to aperture/focalLen
    double nearDepth = std::max(gfCam.GetClippingRange().GetMin(), 0.01f);
    // Cap display depth at scale so the pyramid isn't enormous
    double dispDepth = std::min(nearDepth * 3.0, scale * 1.5);
    dispDepth = std::max(dispDepth, scale * 0.3);

    double hApert = static_cast<double>(gfCam.GetHorizontalAperture()) * 0.5;
    double vApert = static_cast<double>(gfCam.GetVerticalAperture())   * 0.5;
    double focalL = static_cast<double>(gfCam.GetFocalLength());
    if (focalL < 1e-6) focalL = 50.0;  // fallback

    // Scale aperture to the display depth using similar triangles
    double hw = hApert / focalL * dispDepth;
    double hv = vApert / focalL * dispDepth;

    // 4 corners of the near quad at dispDepth along lookDir
    pxr::GfVec3d nearCentre = origin + lookDir * dispDepth;
    pxr::GfVec3d nBL = nearCentre - right*hw - up*hv;
    pxr::GfVec3d nBR = nearCentre + right*hw - up*hv;
    pxr::GfVec3d nTL = nearCentre - right*hw + up*hv;
    pxr::GfVec3d nTR = nearCentre + right*hw + up*hv;

    // Lines from origin to each corner (pyramid edges)
    push(origin, nBL);
    push(origin, nBR);
    push(origin, nTL);
    push(origin, nTR);
    // Near quad rectangle
    push(nBL, nBR);
    push(nBR, nTR);
    push(nTR, nTL);
    push(nTL, nBL);

    // ---- Up arrow ----
    pxr::GfVec3d arrowBase = origin + up * bh;
    pxr::GfVec3d arrowTip  = origin + up * (bh + scale * 0.18);
    push(arrowBase, arrowTip);
}

// Builds near quad + far quad + 4 connecting edges for the full GfFrustum.
// Mirrors stageView.py DrawCameraGuides: corners via GfFrustum::ComputeCorners(),
// line pairs via the same 24-index layout. Appends 24 XYZ float triples.
static void BuildCameraGuideLines(const pxr::GfCamera& gfCam,
                                   std::vector<float>&  outVerts)
{
    const std::vector<pxr::GfVec3d> c = gfCam.GetFrustum().ComputeCorners();
    // 0=LBN 1=RBN 2=LTN 3=RTN  4=LBF 5=RBF 6=LTF 7=RTF
    static const int kIdx[24] = {
        0,1, 1,3, 3,2, 2,0,   // near quad
        4,5, 5,7, 7,6, 6,4,   // far quad
        3,7, 0,4, 1,5, 2,6    // connecting edges
    };
    for (int i : kIdx) {
        outVerts.push_back(static_cast<float>(c[i][0]));
        outVerts.push_back(static_cast<float>(c[i][1]));
        outVerts.push_back(static_cast<float>(c[i][2]));
    }
}

// ===========================================================================
// DrawCameraWireframes
// ===========================================================================

void UsdSceneRenderer::DrawCameraWireframes(
    pxr::UsdStageRefPtr        stage,
    const pxr::SdfPathVector&  selectedPaths,
    const pxr::SdfPath&        activeCameraPath,
    const pxr::GfMatrix4d&     viewProjMatrix,
    double                     viewportCameraDist)
{
    if (!stage) return;
    if (!m_bboxProgram || !m_camWireVAO || !m_camWireVBO) return;
    if (!m_drawTarget) return;

    // Rebuild camera path list fresh each call.
    m_cachedCameraPaths.clear();
    for (const pxr::UsdPrim& prim : stage->Traverse()) {
        if (prim.IsA<pxr::UsdGeomCamera>())
            m_cachedCameraPaths.push_back(prim.GetPath());
    }
    if (m_cachedCameraPaths.empty()) return;

    double rawScale = viewportCameraDist * 0.12;
    double scale    = std::min(rawScale, 50.0);
    scale = std::max(scale, 0.5);    // minimum visible size

    // Build set of selected paths for O(1) lookup
    std::unordered_set<std::string> selectedSet;
    for (const auto& p : selectedPaths) selectedSet.insert(p.GetString());

    // Re-bind FBO
    pxr::GfVec2i sz = m_drawTarget->GetSize();
    m_drawTarget->Bind();
    glViewport(0, 0, sz[0], sz[1]);

    // Save GL state
    GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
    GLboolean prevDepthMask;
    glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
    GLboolean prevBlend = glIsEnabled(GL_BLEND);

    // Camera wireframes draw on top of everything (like the axis gizmo) so they
    // are always visible regardless of scene geometry at the camera location.
    glDisable(GL_DEPTH_TEST);
    glDepthMask(GL_FALSE);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    if (m_aaEnabled) glEnable(GL_LINE_SMOOTH);

    glUseProgram(m_bboxProgram);

    pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);

    std::vector<float> guideVerts;  // accumulates frustum oracle lines

    for (const auto& camPath : m_cachedCameraPaths) {
        pxr::UsdPrim prim = stage->GetPrimAtPath(camPath);
        if (!prim) continue;

        pxr::UsdGeomCamera usdCam(prim);
        pxr::GfCamera gfCam = usdCam.GetCamera(m_currentTime);

        // Determine colour
        bool isSelected = (selectedSet.count(camPath.GetString()) > 0);
        bool isActive   = (camPath == activeCameraPath && !activeCameraPath.IsEmpty());

        pxr::GfVec4f col;
        if      (isSelected) col = pxr::GfVec4f(1.0f, 0.75f, 0.10f, 1.0f);
        else if (isActive)   col = pxr::GfVec4f(0.2f, 0.90f, 1.00f, 1.0f);
        else                 col = pxr::GfVec4f(0.65f, 0.85f, 1.00f, 0.90f); // light blue

        glUniform4f(m_bboxUniformColor, col[0], col[1], col[2], col[3]);

        std::vector<float> verts;
        BuildCameraWireframeLines(gfCam, scale, verts);
        if (verts.empty()) continue;

        int vertCount = static_cast<int>(verts.size() / 3);

        // Upload to the dedicated camera wireframe VBO
        glBindBuffer(GL_ARRAY_BUFFER, m_camWireVBO);
        glBufferData(GL_ARRAY_BUFFER,
                     static_cast<GLsizeiptr>(verts.size() * sizeof(float)),
                     verts.data(), GL_DYNAMIC_DRAW);

        glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());

        glBindVertexArray(m_camWireVAO);
        glDrawArrays(GL_LINES, 0, vertCount);
        glBindVertexArray(0);

        // Accumulate full-frustum oracle lines for non-active cameras (stageView.DrawCameraGuides)
        if (m_showCameraGuide && !isActive)
            BuildCameraGuideLines(gfCam, guideVerts);
    }

    // Draw all camera guide frustum boxes in one call (oracle orange from stageView.py)
    if (m_showCameraGuide && !guideVerts.empty()) {
        glUniform4f(m_bboxUniformColor, 0.82745f, 0.39608f, 0.16471f, 1.0f);
        glBindBuffer(GL_ARRAY_BUFFER, m_camWireVBO);
        glBufferData(GL_ARRAY_BUFFER,
                     static_cast<GLsizeiptr>(guideVerts.size() * sizeof(float)),
                     guideVerts.data(), GL_DYNAMIC_DRAW);
        glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());
        glBindVertexArray(m_camWireVAO);
        glDrawArrays(GL_LINES, 0, static_cast<int>(guideVerts.size() / 3));
        glBindVertexArray(0);
    }

    // Restore GL state
    if (m_aaEnabled) glDisable(GL_LINE_SMOOTH);
    if (prevDepthTest)  glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
    glDepthMask(prevDepthMask);
    if (!prevBlend) glDisable(GL_BLEND);

    glUseProgram(0);
    m_drawTarget->Unbind();
}

// ===========================================================================
// BuildLightWireframeLines
//
// Geometry is built in the light's LOCAL space (using its schema attributes in
// local units) and every vertex is pushed through localToWorld, so position,
// orientation and scale are all handled uniformly.  USD lights emit along local
// -Z (rect/disk/distant); cylinder length runs along local X.
// ===========================================================================

void UsdSceneRenderer::BuildLightWireframeLines(const pxr::UsdPrim&    lightPrim,
                                                const pxr::GfMatrix4d& l2w,
                                                double                 scale,
                                                std::vector<float>&    outVerts)
{
    using pxr::GfVec3d;
    static constexpr int    kSeg   = 24;
    static constexpr double kTwoPi = 6.283185307179586;

    auto pushLine = [&](const GfVec3d& a, const GfVec3d& b) {
        GfVec3d wa = l2w.Transform(a);
        GfVec3d wb = l2w.Transform(b);
        outVerts.push_back((float)wa[0]); outVerts.push_back((float)wa[1]); outVerts.push_back((float)wa[2]);
        outVerts.push_back((float)wb[0]); outVerts.push_back((float)wb[1]); outVerts.push_back((float)wb[2]);
    };
    // Circle centred at c, spanned by unit axes u,v, radius r (local space).
    auto circle = [&](const GfVec3d& c, const GfVec3d& u, const GfVec3d& v, double r) {
        GfVec3d prev;
        for (int i = 0; i <= kSeg; ++i) {
            double a = kTwoPi * double(i) / double(kSeg);
            GfVec3d p = c + u * (r * std::cos(a)) + v * (r * std::sin(a));
            if (i > 0) pushLine(prev, p);
            prev = p;
        }
    };

    const GfVec3d X(1,0,0), Y(0,1,0), Z(0,0,1), O(0,0,0);
    const double  marker = scale * 0.5;  // display-scaled size for direction lines / markers

    if (lightPrim.IsA<pxr::UsdLuxRectLight>()) {
        float w = 1.0f, h = 1.0f;
        pxr::UsdLuxRectLight rl(lightPrim);
        rl.GetWidthAttr().Get(&w, m_currentTime);
        rl.GetHeightAttr().Get(&h, m_currentTime);
        double hw = w * 0.5, hh = h * 0.5;
        GfVec3d bl(-hw,-hh,0), br(hw,-hh,0), tr(hw,hh,0), tl(-hw,hh,0);
        pushLine(bl,br); pushLine(br,tr); pushLine(tr,tl); pushLine(tl,bl);
        pushLine(O, GfVec3d(0,0,-marker));  // emission direction (-Z)
    }
    else if (lightPrim.IsA<pxr::UsdLuxDiskLight>()) {
        float r = 0.5f; pxr::UsdLuxDiskLight(lightPrim).GetRadiusAttr().Get(&r, m_currentTime);
        circle(O, X, Y, r);
        pushLine(O, GfVec3d(0,0,-marker));
    }
    else if (lightPrim.IsA<pxr::UsdLuxCylinderLight>()) {
        float r = 0.5f, len = 1.0f;
        pxr::UsdLuxCylinderLight cl(lightPrim);
        cl.GetRadiusAttr().Get(&r, m_currentTime);
        cl.GetLengthAttr().Get(&len, m_currentTime);
        double hx = len * 0.5;
        circle(GfVec3d( hx,0,0), Y, Z, r);   // end caps (in local YZ)
        circle(GfVec3d(-hx,0,0), Y, Z, r);
        pushLine(GfVec3d(-hx, r,0), GfVec3d(hx, r,0));   // connecting lines
        pushLine(GfVec3d(-hx,-r,0), GfVec3d(hx,-r,0));
        pushLine(GfVec3d(-hx,0, r), GfVec3d(hx,0, r));
        pushLine(GfVec3d(-hx,0,-r), GfVec3d(hx,0,-r));
    }
    else if (lightPrim.IsA<pxr::UsdLuxDistantLight>()) {
        // Sun: small disc facing -Z plus parallel rays along -Z.
        double r = marker;
        circle(O, X, Y, r);
        const GfVec3d off[5] = { O, GfVec3d(r,0,0), GfVec3d(-r,0,0), GfVec3d(0,r,0), GfVec3d(0,-r,0) };
        for (const auto& o : off) pushLine(o, o + GfVec3d(0,0,-marker*2.0));
    }
    else if (lightPrim.IsA<pxr::UsdLuxDomeLight>()) {
        // Environment dome: 3 large display-scaled circles around the origin.
        double r = std::max(scale, 1.0);
        circle(O, X, Y, r); circle(O, X, Z, r); circle(O, Y, Z, r);
    }
    else if (lightPrim.IsA<pxr::UsdLuxSphereLight>()) {
        float r = 0.5f; pxr::UsdLuxSphereLight sl(lightPrim);
        sl.GetRadiusAttr().Get(&r, m_currentTime);
        bool pointish = false; sl.GetTreatAsPointAttr().Get(&pointish, m_currentTime);
        if (pointish || r < 1e-4f) {
            double s = marker;             // point light: axis cross + tiny disc
            pushLine(GfVec3d(-s,0,0), GfVec3d(s,0,0));
            pushLine(GfVec3d(0,-s,0), GfVec3d(0,s,0));
            pushLine(GfVec3d(0,0,-s), GfVec3d(0,0,s));
            circle(O, X, Y, s * 0.4);
        } else {
            circle(O, X, Y, r); circle(O, X, Z, r); circle(O, Y, Z, r);
        }
    }
    else {
        double s = marker;                 // unknown light: generic point marker
        pushLine(GfVec3d(-s,0,0), GfVec3d(s,0,0));
        pushLine(GfVec3d(0,-s,0), GfVec3d(0,s,0));
        pushLine(GfVec3d(0,0,-s), GfVec3d(0,0,s));
    }
}

// ===========================================================================
// StageHasAuthoredLights
// ===========================================================================

bool UsdSceneRenderer::StageHasAuthoredLights() const
{
    if (!m_stage) return false;
    for (const pxr::UsdPrim& prim : m_stage->Traverse()) {
        if (prim.HasAPI<pxr::UsdLuxLightAPI>())
            return true;
    }
    return false;
}

// ===========================================================================
// DrawLightWireframes  (mirrors DrawCameraWireframes)
// ===========================================================================

void UsdSceneRenderer::DrawLightWireframes(
    pxr::UsdStageRefPtr        stage,
    const pxr::SdfPathVector&  selectedPaths,
    const pxr::GfMatrix4d&     viewProjMatrix,
    double                     viewportCameraDist)
{
    if (!stage) return;
    if (!m_bboxProgram || !m_lightWireVAO || !m_lightWireVBO) return;
    if (!m_drawTarget) return;

    // Collect every light prim (anything carrying UsdLuxLightAPI).
    pxr::SdfPathVector lightPaths;
    for (const pxr::UsdPrim& prim : stage->Traverse()) {
        if (prim.HasAPI<pxr::UsdLuxLightAPI>())
            lightPaths.push_back(prim.GetPath());
    }
    if (lightPaths.empty()) return;

    double rawScale = viewportCameraDist * 0.12;
    double scale    = std::min(rawScale, 50.0);
    scale = std::max(scale, 0.5);

    std::unordered_set<std::string> selectedSet;
    for (const auto& p : selectedPaths) selectedSet.insert(p.GetString());

    pxr::GfVec2i sz = m_drawTarget->GetSize();
    m_drawTarget->Bind();
    glViewport(0, 0, sz[0], sz[1]);

    GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
    GLboolean prevDepthMask; glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
    GLboolean prevBlend = glIsEnabled(GL_BLEND);

    // Draw on top of everything (like camera wireframes) so lights stay visible.
    glDisable(GL_DEPTH_TEST);
    glDepthMask(GL_FALSE);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    if (m_aaEnabled) glEnable(GL_LINE_SMOOTH);

    glUseProgram(m_bboxProgram);
    pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);

    pxr::UsdGeomXformCache xformCache(m_currentTime);

    for (const auto& lpath : lightPaths) {
        pxr::UsdPrim prim = stage->GetPrimAtPath(lpath);
        if (!prim) continue;

        pxr::GfMatrix4d l2w = xformCache.GetLocalToWorldTransform(prim);

        bool isSelected = (selectedSet.count(lpath.GetString()) > 0);
        pxr::GfVec4f col = isSelected
            ? pxr::GfVec4f(1.00f, 0.75f, 0.10f, 1.00f)   // accent orange
            : pxr::GfVec4f(1.00f, 0.90f, 0.35f, 0.90f);  // warm yellow
        glUniform4f(m_bboxUniformColor, col[0], col[1], col[2], col[3]);

        std::vector<float> verts;
        BuildLightWireframeLines(prim, l2w, scale, verts);
        if (verts.empty()) continue;

        int vertCount = static_cast<int>(verts.size() / 3);
        glBindBuffer(GL_ARRAY_BUFFER, m_lightWireVBO);
        glBufferData(GL_ARRAY_BUFFER,
                     static_cast<GLsizeiptr>(verts.size() * sizeof(float)),
                     verts.data(), GL_DYNAMIC_DRAW);
        glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());
        glBindVertexArray(m_lightWireVAO);
        glDrawArrays(GL_LINES, 0, vertCount);
        glBindVertexArray(0);
    }

    if (m_aaEnabled) glDisable(GL_LINE_SMOOTH);
    if (prevDepthTest)  glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
    glDepthMask(prevDepthMask);
    if (!prevBlend) glDisable(GL_BLEND);

    glUseProgram(0);
    m_drawTarget->Unbind();
}

// ===========================================================================
// PickCameraAtPoint
// ===========================================================================

bool UsdSceneRenderer::PickCameraAtPoint(
    pxr::UsdStageRefPtr    stage,
    float mouseX, float mouseY,
    const pxr::GfMatrix4d& viewProjMatrix,
    float imagePosX, float imagePosY,
    int viewW, int viewH,
    double                 viewportCameraDist,
    pxr::SdfPath*          outCameraPath)
{
    if (!stage || !outCameraPath) return false;

    // Rebuild cache fresh (same logic as DrawCameraWireframes — always traverse
    // so newly created cameras are immediately pickable).
    m_cachedCameraPaths.clear();
    for (const pxr::UsdPrim& prim : stage->Traverse()) {
        if (prim.IsA<pxr::UsdGeomCamera>())
            m_cachedCameraPaths.push_back(prim.GetPath());
    }
    if (m_cachedCameraPaths.empty()) return false;

    static constexpr float kPickRadius = 10.0f;

    double rawScale = viewportCameraDist * 0.12;
    double scale    = std::min(rawScale, 50.0);
    scale = std::max(scale, 0.05);

    float  bestDist = kPickRadius;
    pxr::SdfPath bestPath;

    for (const auto& camPath : m_cachedCameraPaths) {
        pxr::UsdPrim prim = stage->GetPrimAtPath(camPath);
        if (!prim) continue;

        pxr::UsdGeomCamera usdCam(prim);
        pxr::GfCamera gfCam = usdCam.GetCamera(m_currentTime);

        std::vector<float> verts;
        BuildCameraWireframeLines(gfCam, scale, verts);

        // verts = interleaved XYZ pairs (2 verts per segment = 6 floats per segment)
        for (size_t i = 0; i + 5 < verts.size(); i += 6) {
            pxr::GfVec3d wa(verts[i],   verts[i+1], verts[i+2]);
            pxr::GfVec3d wb(verts[i+3], verts[i+4], verts[i+5]);

            float ax, ay, bx, by;
            bool okA = WorldToScreen(wa, viewProjMatrix, viewW, viewH, imagePosX, imagePosY, ax, ay);
            bool okB = WorldToScreen(wb, viewProjMatrix, viewW, viewH, imagePosX, imagePosY, bx, by);
            if (!okA && !okB) continue;
            if (!okA) { ax = bx; ay = by; }
            if (!okB) { bx = ax; by = ay; }

            float d = PointToSegmentDist(mouseX, mouseY, ax, ay, bx, by);
            if (d < bestDist) {
                bestDist = d;
                bestPath = camPath;
            }
        }
    }

    if (bestPath.IsEmpty()) return false;
    *outCameraPath = bestPath;
    return true;
}

} // namespace UsdLayerManager
