#include "PropertyPanel.h"
#include "../utils/Logger.h"
#include "../utils/FileDialog.h"
#include "../core/commands/TransformCommand.h"
#include "../core/commands/AttributeSetCommand.h"

#include <pxr/usd/sdf/listOp.h>
#include <pxr/usd/sdf/reference.h>
#include <pxr/usd/sdf/payload.h>
#include <pxr/usd/sdf/schema.h>
#include <pxr/base/vt/dictionary.h>
#include <pxr/base/tf/stringUtils.h>

#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/attribute.h>
#include <pxr/usd/usd/relationship.h>
#include <pxr/usd/usd/variantSets.h>
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdGeom/xformable.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdGeom/xformOp.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h>
#include <pxr/base/gf/matrix3d.h>
#include <pxr/base/gf/matrix3f.h>
#include <pxr/base/gf/matrix2d.h>
#include <pxr/base/gf/matrix2f.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/base/gf/half.h>
#include <pxr/base/gf/vec2f.h>
#include <pxr/base/gf/vec2d.h>
#include <pxr/base/gf/vec2h.h>
#include <pxr/base/gf/vec2i.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/vec3h.h>
#include <pxr/base/gf/vec3i.h>
#include <pxr/base/gf/vec4f.h>
#include <pxr/base/gf/vec4d.h>
#include <pxr/base/gf/vec4h.h>
#include <pxr/base/gf/vec4i.h>
#include <pxr/base/gf/quatd.h>
#include <pxr/base/gf/quatf.h>
#include <pxr/base/gf/quath.h>
#include <pxr/base/vt/value.h>
#include <pxr/base/vt/array.h>
#include <pxr/base/tf/token.h>
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include <pxr/usd/usdShade/tokens.h>
#include <pxr/usd/usd/schemaRegistry.h>
#include <pxr/usd/usd/primDefinition.h>
#include <memory>
#include <sstream>
#include <unordered_map>
#include <vector>
#include <set>

PXR_NAMESPACE_USING_DIRECTIVE

namespace UsdLayerManager {

// ---------------------------------------------------------------------------
// Colours
// ---------------------------------------------------------------------------
static const ImVec4 kColorX       { 0.86f, 0.30f, 0.30f, 1.f };  // X axis – red
static const ImVec4 kColorY       { 0.40f, 0.80f, 0.30f, 1.f };  // Y axis – green
static const ImVec4 kColorZ       { 0.28f, 0.56f, 0.96f, 1.f };  // Z axis – blue
static const ImVec4 kColorLabel   { 0.80f, 0.80f, 0.80f, 1.f };  // row labels
static const ImVec4 kColorKeyOn   { 1.00f, 0.65f, 0.10f, 1.f };  // key exists at editTime
static const ImVec4 kColorKeyOff  { 0.35f, 0.35f, 0.35f, 1.f };  // varying, no sample here

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

void PropertyPanel::SetPropertyManager(PropertyManager* manager) {
    m_propertyManager = manager;
}

void PropertyPanel::SetStage(pxr::UsdStageRefPtr stage) {
    m_stage = stage;
    m_selectedPrimPath.clear();
    m_needsRead = true;
}

void PropertyPanel::SetSelectedPrimPath(const std::string& path) {
    if (m_selectedPrimPath != path) {
        m_selectedPrimPath = path;
        m_needsRead = true;
    }
}

void PropertyPanel::SetTimeCodes(pxr::UsdTimeCode displayTime,
                                 pxr::UsdTimeCode editTime) {
    m_displayTime = displayTime;
    m_editTime    = editTime;
    m_needsRead   = true;
}

// ---------------------------------------------------------------------------
// USD read
// ---------------------------------------------------------------------------
void PropertyPanel::ReadTransform() {
    m_needsRead    = false;
    m_hasXform     = false;
    m_isXformable  = false;
    m_xformFallback = false;
    m_primType.clear();

    if (!m_stage || m_selectedPrimPath.empty()) return;

    UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath));
    if (!prim) return;

    m_primType    = prim.GetTypeName().GetString();
    m_isXformable = prim.IsA<UsdGeomXformable>();

    UsdGeomXformCommonAPI xformAPI(prim);
    if (!xformAPI) return;

    GfVec3d translation;
    GfVec3f rotation, scale, pivot;
    UsdGeomXformCommonAPI::RotationOrder rotOrder;

    if (xformAPI.GetXformVectors(
            &translation, &rotation, &scale, &pivot, &rotOrder,
            m_displayTime))
    {
        m_translate = GfVec3f(
            static_cast<float>(translation[0]),
            static_cast<float>(translation[1]),
            static_cast<float>(translation[2]));
        m_rotate   = rotation;
        m_scale    = scale;
        m_pivot    = pivot;
        m_rotOrder = rotOrder;
    }
    else
    {
        // XformCommonAPI can't decompose the op stack (e.g. single
        // xformOp:transform from a reference).  Fall back to decomposing
        // the local transformation matrix directly so the panel still
        // shows meaningful values.
        UsdGeomXformable xformable(prim);
        GfMatrix4d localMatrix;
        bool resetsXformStack = false;
        if (!xformable.GetLocalTransformation(
                &localMatrix, &resetsXformStack, m_displayTime))
        {
            localMatrix = GfMatrix4d(1); // identity fallback
        }

        // --- Translation ---
        GfVec3d t = localMatrix.ExtractTranslation();
        m_translate = GfVec3f(
            static_cast<float>(t[0]),
            static_cast<float>(t[1]),
            static_cast<float>(t[2]));

        // --- Scale: length of each row of the upper 3x3 ---
        double sx = GfVec3d(localMatrix[0][0], localMatrix[0][1], localMatrix[0][2]).GetLength();
        double sy = GfVec3d(localMatrix[1][0], localMatrix[1][1], localMatrix[1][2]).GetLength();
        double sz = GfVec3d(localMatrix[2][0], localMatrix[2][1], localMatrix[2][2]).GetLength();
        m_scale = GfVec3f(
            static_cast<float>(sx > 1e-9 ? sx : 1.0),
            static_cast<float>(sy > 1e-9 ? sy : 1.0),
            static_cast<float>(sz > 1e-9 ? sz : 1.0));

        // --- Rotation: remove scale, then extract Euler XYZ (degrees) ---
        GfMatrix4d rotMat = localMatrix;
        if (sx > 1e-9) { rotMat[0][0] /= sx; rotMat[0][1] /= sx; rotMat[0][2] /= sx; }
        if (sy > 1e-9) { rotMat[1][0] /= sy; rotMat[1][1] /= sy; rotMat[1][2] /= sy; }
        if (sz > 1e-9) { rotMat[2][0] /= sz; rotMat[2][1] /= sz; rotMat[2][2] /= sz; }

        GfRotation rot = rotMat.ExtractRotation();
        GfVec3d eulerDeg = rot.Decompose(
            GfVec3d::XAxis(), GfVec3d::YAxis(), GfVec3d::ZAxis());
        m_rotate = GfVec3f(
            static_cast<float>(eulerDeg[0]),
            static_cast<float>(eulerDeg[1]),
            static_cast<float>(eulerDeg[2]));
        m_rotOrder = UsdGeomXformCommonAPI::RotationOrderXYZ;
        m_pivot    = GfVec3f(0.f, 0.f, 0.f);

        m_xformFallback = true;
    }
    m_hasXform = true;
}

// ---------------------------------------------------------------------------
// USD write helpers
// ---------------------------------------------------------------------------

void PropertyPanel::EnsureCommonAPILayout() {
    // Called when the prim's xform ops are not XformCommonAPI-compatible
    // (m_xformFallback == true).  We author the standard three ops
    // (translate / rotateXYZ / scale) plus an explicit xformOpOrder in the
    // current edit layer, shadowing the reference's incompatible op stack.
    // After this call m_xformFallback is cleared and subsequent writes use
    // the normal XformCommonAPI path.

    if (!m_stage || m_selectedPrimPath.empty()) return;
    UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath));
    if (!prim) return;

    UsdGeomXformable xformable(prim);
    if (!xformable) return;

    UsdEditContext ec(m_stage, m_stage->GetEditTarget());

    // Inspect the *composed* op order to see which common ops already exist.
    bool resetsXformStack = false;
    auto orderedOps = xformable.GetOrderedXformOps(&resetsXformStack);

    UsdGeomXformOp translateOp, rotateOp, scaleOp;
    for (auto& op : orderedOps) {
        if (op.IsInverseOp()) continue;
        switch (op.GetOpType()) {
            case UsdGeomXformOp::TypeTranslate:
                // Only accept the bare "xformOp:translate" (no suffix/namespace)
                if (!translateOp) translateOp = op;
                break;
            case UsdGeomXformOp::TypeRotateXYZ:
                if (!rotateOp) rotateOp = op;
                break;
            case UsdGeomXformOp::TypeScale:
                if (!scaleOp) scaleOp = op;
                break;
            default:
                break;
        }
    }

    // Create any ops that are missing.  AddXxxOp fails when an op of the
    // same name is already in the composed xformOpOrder, so we only call it
    // when we did not find the op above.
    if (!translateOp) translateOp = xformable.AddTranslateOp();
    if (!rotateOp)    rotateOp    = xformable.AddRotateXYZOp();
    if (!scaleOp)     scaleOp     = xformable.AddScaleOp();

    if (!translateOp || !rotateOp || !scaleOp) {
        LOG_ERROR("PropertyPanel::EnsureCommonAPILayout: failed to create xform ops");
        return;
    }

    // Override the xformOpOrder in the edit layer to exactly these three ops.
    // This shadows the reference's incompatible order (e.g. ["xformOp:transform"]).
    std::vector<UsdGeomXformOp> commonOps = { translateOp, rotateOp, scaleOp };
    xformable.SetXformOpOrder(commonOps, resetsXformStack);

    // Write the decomposed TRS values we already cached.
    translateOp.Set(GfVec3d(m_translate[0], m_translate[1], m_translate[2]));
    rotateOp.Set(m_rotate);
    scaleOp.Set(m_scale);

    m_xformFallback = false;
    m_rotOrder      = UsdGeomXformCommonAPI::RotationOrderXYZ;
}

void PropertyPanel::CaptureEditStart() {
    m_editStartTranslate = m_translate;
    m_editStartRotate    = m_rotate;
    m_editStartScale     = m_scale;
    // Keyframe presence must be sampled before the first live write so the
    // undo command knows whether this edit created the sample at m_editTime.
    m_editStartSamples = TransformCommand::QuerySamplePresence(
        m_stage, SdfPath(m_selectedPrimPath), m_rotOrder, m_editTime);
}

void PropertyPanel::WriteTranslate() {
    if (!m_stage || m_selectedPrimPath.empty()) return;
    UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath));
    if (!prim) return;
    if (m_xformFallback) { EnsureCommonAPILayout(); return; }
    UsdGeomXformCommonAPI api(prim);
    if (!api) return;
    api.SetTranslate(
        GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
        m_editTime);
}

void PropertyPanel::WriteRotate() {
    if (!m_stage || m_selectedPrimPath.empty()) return;
    UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath));
    if (!prim) return;
    if (m_xformFallback) { EnsureCommonAPILayout(); return; }
    UsdGeomXformCommonAPI api(prim);
    if (!api) return;
    api.SetRotate(m_rotate, m_rotOrder, m_editTime);
}

void PropertyPanel::WriteScale() {
    if (!m_stage || m_selectedPrimPath.empty()) return;
    UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath));
    if (!prim) return;
    if (m_xformFallback) { EnsureCommonAPILayout(); return; }
    UsdGeomXformCommonAPI api(prim);
    if (!api) return;
    api.SetScale(m_scale, m_editTime);
}

// ---------------------------------------------------------------------------
// Transform section — Maya Channel Box grid
//
//   |            |   X   |   Y   |   Z   |
//   | Translate  | [   ] | [   ] | [   ] |
//   | Rotate     | [   ] | [   ] | [   ] |
//   | Scale      | [   ] | [   ] | [   ] |
// ---------------------------------------------------------------------------
void PropertyPanel::RenderTransformSection() {
    // 4 columns: label | X | Y | Z
    // Borders on the outer frame and between cells for the channel-box look.
    constexpr ImGuiTableFlags kTableFlags =
        ImGuiTableFlags_BordersOuter |
        ImGuiTableFlags_BordersInnerV |
        ImGuiTableFlags_SizingStretchSame |
        ImGuiTableFlags_NoHostExtendX;

    constexpr float kLabelColW = 72.f;   // width of the row-label column

    if (!ImGui::BeginTable("##channelbox", 4, kTableFlags)) return;

    // Column setup
    ImGui::TableSetupColumn("##attr", ImGuiTableColumnFlags_WidthFixed, kLabelColW);
    ImGui::TableSetupColumn("##X",    ImGuiTableColumnFlags_WidthStretch);
    ImGui::TableSetupColumn("##Y",    ImGuiTableColumnFlags_WidthStretch);
    ImGui::TableSetupColumn("##Z",    ImGuiTableColumnFlags_WidthStretch);

    // ---- Header row: empty | X | Y | Z ----
    ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
    ImGui::TableSetColumnIndex(0); // empty label cell
    ImGui::TableSetColumnIndex(1);
    ImGui::TableHeader("##hx"); // needed to close the header cell
    ImGui::SameLine(0, 0);
    float cellW = ImGui::GetContentRegionAvail().x;
    ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (cellW - ImGui::CalcTextSize("X").x) * 0.5f);
    ImGui::TextColored(kColorX, "X");

    ImGui::TableSetColumnIndex(2);
    ImGui::TableHeader("##hy");
    ImGui::SameLine(0, 0);
    cellW = ImGui::GetContentRegionAvail().x;
    ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (cellW - ImGui::CalcTextSize("Y").x) * 0.5f);
    ImGui::TextColored(kColorY, "Y");

    ImGui::TableSetColumnIndex(3);
    ImGui::TableHeader("##hz");
    ImGui::SameLine(0, 0);
    cellW = ImGui::GetContentRegionAvail().x;
    ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (cellW - ImGui::CalcTextSize("Z").x) * 0.5f);
    ImGui::TextColored(kColorZ, "Z");

    // ---- Translate row ----
    ImGui::TableNextRow();
    ImGui::TableSetColumnIndex(0);
    ImGui::TextColored(kColorLabel, "Translate");
    ImGui::TableSetColumnIndex(1);
    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::DragFloat("##tx", &m_translate[0], 0.01f, 0.f, 0.f, "%.3f")) WriteTranslate();
    bool anyActive = ImGui::IsItemActive();
    if (ImGui::IsItemActivated()) CaptureEditStart();
    if (ImGui::IsItemDeactivatedAfterEdit()) {
        if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) {
            auto editLayer = m_stage->GetEditTarget().GetLayer();
            m_commandHistory->Push(std::make_unique<TransformCommand>(
                m_stage, pxr::SdfPath(m_selectedPrimPath), editLayer,
                pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]),
                m_editStartRotate, m_editStartScale,
                pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
                m_rotate, m_scale, m_rotOrder,
                m_editTime, m_editStartSamples, "Translate"));
        }
    }
    ImGui::TableSetColumnIndex(2);
    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::DragFloat("##ty", &m_translate[1], 0.01f, 0.f, 0.f, "%.3f")) WriteTranslate();
    anyActive = anyActive || ImGui::IsItemActive();
    if (ImGui::IsItemActivated()) CaptureEditStart();
    if (ImGui::IsItemDeactivatedAfterEdit()) {
        if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) {
            auto editLayer = m_stage->GetEditTarget().GetLayer();
            m_commandHistory->Push(std::make_unique<TransformCommand>(
                m_stage, pxr::SdfPath(m_selectedPrimPath), editLayer,
                pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]),
                m_editStartRotate, m_editStartScale,
                pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
                m_rotate, m_scale, m_rotOrder,
                m_editTime, m_editStartSamples, "Translate"));
        }
    }
    ImGui::TableSetColumnIndex(3);
    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::DragFloat("##tz", &m_translate[2], 0.01f, 0.f, 0.f, "%.3f")) WriteTranslate();
    anyActive = anyActive || ImGui::IsItemActive();
    if (ImGui::IsItemActivated()) CaptureEditStart();
    if (ImGui::IsItemDeactivatedAfterEdit()) {
        if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) {
            auto editLayer = m_stage->GetEditTarget().GetLayer();
            m_commandHistory->Push(std::make_unique<TransformCommand>(
                m_stage, pxr::SdfPath(m_selectedPrimPath), editLayer,
                pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]),
                m_editStartRotate, m_editStartScale,
                pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
                m_rotate, m_scale, m_rotOrder,
                m_editTime, m_editStartSamples, "Translate"));
        }
    }

    // ---- Rotate row ----
    ImGui::TableNextRow();
    ImGui::TableSetColumnIndex(0);
    ImGui::TextColored(kColorLabel, "Rotate");
    ImGui::TableSetColumnIndex(1);
    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::DragFloat("##rx", &m_rotate[0], 0.1f, 0.f, 0.f, "%.3f")) WriteRotate();
    anyActive = anyActive || ImGui::IsItemActive();
    if (ImGui::IsItemActivated()) CaptureEditStart();
    if (ImGui::IsItemDeactivatedAfterEdit()) {
        if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) {
            auto editLayer = m_stage->GetEditTarget().GetLayer();
            m_commandHistory->Push(std::make_unique<TransformCommand>(
                m_stage, pxr::SdfPath(m_selectedPrimPath), editLayer,
                pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]),
                m_editStartRotate, m_editStartScale,
                pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
                m_rotate, m_scale, m_rotOrder,
                m_editTime, m_editStartSamples, "Rotate"));
        }
    }
    ImGui::TableSetColumnIndex(2);
    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::DragFloat("##ry", &m_rotate[1], 0.1f, 0.f, 0.f, "%.3f")) WriteRotate();
    anyActive = anyActive || ImGui::IsItemActive();
    if (ImGui::IsItemActivated()) CaptureEditStart();
    if (ImGui::IsItemDeactivatedAfterEdit()) {
        if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) {
            auto editLayer = m_stage->GetEditTarget().GetLayer();
            m_commandHistory->Push(std::make_unique<TransformCommand>(
                m_stage, pxr::SdfPath(m_selectedPrimPath), editLayer,
                pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]),
                m_editStartRotate, m_editStartScale,
                pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
                m_rotate, m_scale, m_rotOrder,
                m_editTime, m_editStartSamples, "Rotate"));
        }
    }
    ImGui::TableSetColumnIndex(3);
    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::DragFloat("##rz", &m_rotate[2], 0.1f, 0.f, 0.f, "%.3f")) WriteRotate();
    anyActive = anyActive || ImGui::IsItemActive();
    if (ImGui::IsItemActivated()) CaptureEditStart();
    if (ImGui::IsItemDeactivatedAfterEdit()) {
        if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) {
            auto editLayer = m_stage->GetEditTarget().GetLayer();
            m_commandHistory->Push(std::make_unique<TransformCommand>(
                m_stage, pxr::SdfPath(m_selectedPrimPath), editLayer,
                pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]),
                m_editStartRotate, m_editStartScale,
                pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
                m_rotate, m_scale, m_rotOrder,
                m_editTime, m_editStartSamples, "Rotate"));
        }
    }

    // ---- Scale row ----
    ImGui::TableNextRow();
    ImGui::TableSetColumnIndex(0);
    ImGui::TextColored(kColorLabel, "Scale");
    ImGui::TableSetColumnIndex(1);
    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::DragFloat("##sx", &m_scale[0], 0.005f, 0.f, 0.f, "%.3f")) WriteScale();
    anyActive = anyActive || ImGui::IsItemActive();
    if (ImGui::IsItemActivated()) CaptureEditStart();
    if (ImGui::IsItemDeactivatedAfterEdit()) {
        if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) {
            auto editLayer = m_stage->GetEditTarget().GetLayer();
            m_commandHistory->Push(std::make_unique<TransformCommand>(
                m_stage, pxr::SdfPath(m_selectedPrimPath), editLayer,
                pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]),
                m_editStartRotate, m_editStartScale,
                pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
                m_rotate, m_scale, m_rotOrder,
                m_editTime, m_editStartSamples, "Scale"));
        }
    }
    ImGui::TableSetColumnIndex(2);
    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::DragFloat("##sy", &m_scale[1], 0.005f, 0.f, 0.f, "%.3f")) WriteScale();
    anyActive = anyActive || ImGui::IsItemActive();
    if (ImGui::IsItemActivated()) CaptureEditStart();
    if (ImGui::IsItemDeactivatedAfterEdit()) {
        if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) {
            auto editLayer = m_stage->GetEditTarget().GetLayer();
            m_commandHistory->Push(std::make_unique<TransformCommand>(
                m_stage, pxr::SdfPath(m_selectedPrimPath), editLayer,
                pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]),
                m_editStartRotate, m_editStartScale,
                pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
                m_rotate, m_scale, m_rotOrder,
                m_editTime, m_editStartSamples, "Scale"));
        }
    }
    ImGui::TableSetColumnIndex(3);
    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::DragFloat("##sz", &m_scale[2], 0.005f, 0.f, 0.f, "%.3f")) WriteScale();
    anyActive = anyActive || ImGui::IsItemActive();
    if (ImGui::IsItemActivated()) CaptureEditStart();
    if (ImGui::IsItemDeactivatedAfterEdit()) {
        if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) {
            auto editLayer = m_stage->GetEditTarget().GetLayer();
            m_commandHistory->Push(std::make_unique<TransformCommand>(
                m_stage, pxr::SdfPath(m_selectedPrimPath), editLayer,
                pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]),
                m_editStartRotate, m_editStartScale,
                pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]),
                m_rotate, m_scale, m_rotOrder,
                m_editTime, m_editStartSamples, "Scale"));
        }
    }

    // ---- Pivot row (read-only) ----
    ImGui::TableNextRow();
    ImGui::TableSetColumnIndex(0);
    ImGui::TextColored(kColorLabel, "Pivot");
    for (int i = 0; i < 3; ++i) {
        ImGui::TableSetColumnIndex(i + 1);
        ImGui::SetNextItemWidth(-FLT_MIN);
        // Use InputFloat with ReadOnly so the value is visible but not editable.
        // Pivot changes require a dedicated pivot-editing mode (not yet supported).
        char id[8]; snprintf(id, sizeof(id), "##pv%d", i);
        float v = m_pivot[i];
        ImGui::PushStyleColor(ImGuiCol_FrameBg,
            ImVec4(0.15f, 0.15f, 0.15f, 1.f)); // dimmer background signals read-only
        ImGui::InputFloat(id, &v, 0.f, 0.f, "%.3f", ImGuiInputTextFlags_ReadOnly);
        ImGui::PopStyleColor();
    }

    ImGui::EndTable();

    // Record whether any field was active this frame so Render() can decide
    // whether to re-read USD values next frame.
    m_isAnyFieldActive = anyActive;
}

// ---------------------------------------------------------------------------
// Helper: property display name  (namespace:basename, like usdtweak GetDisplayName)
// ---------------------------------------------------------------------------
static std::string GetPropDisplayName(const pxr::UsdProperty& prop) {
    const std::string ns = prop.GetNamespace().GetString();
    const std::string bn = prop.GetBaseName().GetString();
    return ns.empty() ? bn : (ns + ":" + bn);
}

// ---------------------------------------------------------------------------
// Helper: check whether an attribute has an exact time sample at t
// ---------------------------------------------------------------------------
static bool HasExactTimeSample(const pxr::UsdAttribute& attr, double t) {
    double lo = 0.0, hi = 0.0;
    bool has = false;
    attr.GetBracketingTimeSamples(t, &lo, &hi, &has);
    return has && (lo == t) && (hi == t);
}

// ---------------------------------------------------------------------------
// Helper: small "(k)" key button for animatable (Varying) attributes.
// Orange = exact time sample at editTime; dim gray = no sample at this frame.
// Click = Set(currentValue, editTime) with undo.
// ---------------------------------------------------------------------------
static void DrawKeyButton(pxr::UsdAttribute attr,
                          pxr::UsdTimeCode  editTime,
                          UsdLayerManager::CommandHistory* cmdHistory)
{
    using namespace pxr;
    double t = editTime.GetValue();
    bool exactSample = HasExactTimeSample(attr, t);

    ImVec4 col = exactSample ? kColorKeyOn : kColorKeyOff;
    ImGui::PushStyleColor(ImGuiCol_Text,   col);
    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f));
    bool clicked = ImGui::SmallButton("(k)");
    ImGui::PopStyleColor(2);

    if (ImGui::IsItemHovered())
        ImGui::SetTooltip("Set key at frame %.3g", t);

    if (!clicked || !cmdHistory) return;

    VtValue val;
    if (!attr.Get(&val, editTime)) return;

    std::function<void()> executeFn = [attr, val, editTime]() mutable {
        attr.Set(val, editTime);
    };
    std::function<void()> undoFn;
    if (exactSample) {
        // overwrite: undo restores the same value that was there before
        VtValue prev;
        attr.Get(&prev, editTime);
        undoFn = [attr, prev, editTime]() mutable { attr.Set(prev, editTime); };
    } else {
        undoFn = [attr, editTime]() mutable { attr.ClearAtTime(editTime.GetValue()); };
    }

    cmdHistory->Push(std::make_unique<AttributeSetCommand>(
        "Set Key", std::move(executeFn), std::move(undoFn)));
}

// ---------------------------------------------------------------------------
// Helper: mini-button  (SmallButton coloured by authored state, like usdtweak)
// Opens a popup context with "Copy path" on left-click.
// Returns true when the button was clicked (popup opened).
// ---------------------------------------------------------------------------
static void DrawPropertyMiniButton(const char* label, bool authored,
                                   const char* pathForCopy,
                                   const char* typeHint = nullptr)
{
    ImVec4 btnColor = authored
        ? ImVec4(1.f, 0.85f, 0.4f, 1.f)   // yellow  – authored at edit target
        : ImVec4(0.5f, 0.5f, 0.5f, 1.f);  // grey    – inherited / no opinion

    ImGui::PushStyleColor(ImGuiCol_Text,   btnColor);
    ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f));
    ImGui::AlignTextToFramePadding();
    ImGui::SmallButton(label);
    ImGui::PopStyleColor(2);

    if (ImGui::IsItemHovered()) {
        if (typeHint)
            ImGui::SetTooltip("%s\n%s", pathForCopy, typeHint);
        else
            ImGui::SetTooltip("%s", pathForCopy);
    }
    if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonLeft)) {
        if (ImGui::MenuItem("Copy path"))
            ImGui::SetClipboardText(pathForCopy);
        ImGui::EndPopup();
    }
}

// ---------------------------------------------------------------------------
// Helper: type-aware value widget  (mirrors DrawVtValue in usdtweak)
// Renders an appropriate ImGui widget for the VtValue type.
// Uses "##" label so the attribute name shown in col 1 acts as the label.
// Returns a non-empty VtValue if the user changed the value.
// ---------------------------------------------------------------------------
static pxr::VtValue DrawVtValueWidget(pxr::UsdAttribute& attr,
                                       const pxr::VtValue& val)
{
    using namespace pxr;

    // Color role → ColorEdit3/4 (scalar and array)
    if (attr.GetRoleName() == TfToken("Color")) {
        // ---- scalar ----
        if (val.IsHolding<GfVec3f>()) {
            GfVec3f c = val.UncheckedGet<GfVec3f>();
            if (ImGui::ColorEdit3("##col", c.data()))
                return VtValue(c);
            return VtValue();
        }
        if (val.IsHolding<GfVec4f>()) {
            GfVec4f c = val.UncheckedGet<GfVec4f>();
            if (ImGui::ColorEdit4("##col4", c.data()))
                return VtValue(c);
            return VtValue();
        }

        // ---- color3f[] (e.g. primvars:displayColor) ----
        if (val.IsHolding<VtArray<GfVec3f>>()) {
            VtArray<GfVec3f> arr = val.UncheckedGet<VtArray<GfVec3f>>();
            bool changed = false;
            if (arr.size() == 1) {
                GfVec3f c = arr[0];
                if (ImGui::ColorEdit3("##dc", c.data()))
                    { arr[0] = c; changed = true; }
            } else if (!arr.empty()) {
                // Collapsible: arrow + swatch preview on header line
                bool open = ImGui::TreeNodeEx("##dcarr",
                    ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth);
                ImGui::SameLine(0, 6);
                float sz = ImGui::GetTextLineHeight();
                size_t shown = std::min(arr.size(), (size_t)5);
                for (size_t i = 0; i < shown; ++i) {
                    if (i > 0) ImGui::SameLine(0, 2);
                    char sid[16]; snprintf(sid, sizeof(sid), "##sv%d", (int)i);
                    ImGui::ColorButton(sid,
                        ImVec4(arr[i][0], arr[i][1], arr[i][2], 1.f),
                        ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip,
                        ImVec2(sz, sz));
                }
                ImGui::SameLine(0, 4);
                ImGui::TextDisabled("[%zu]", arr.size());
                if (open) {
                    static constexpr size_t kEditLimit = 16;
                    size_t lim = std::min(arr.size(), kEditLimit);
                    for (size_t i = 0; i < lim; ++i) {
                        ImGui::PushID((int)i);
                        ImGui::Text("[%d]", (int)i);
                        ImGui::SameLine();
                        GfVec3f c = arr[i];
                        ImGui::SetNextItemWidth(-FLT_MIN);
                        if (ImGui::ColorEdit3("##ce", c.data()))
                            { arr[i] = c; changed = true; }
                        ImGui::PopID();
                    }
                    if (arr.size() > kEditLimit)
                        ImGui::TextDisabled("  ... %zu more", arr.size() - kEditLimit);
                    ImGui::TreePop();
                }
            }
            if (changed) return VtValue(arr);
            return VtValue();
        }

        // ---- color4f[] ----
        if (val.IsHolding<VtArray<GfVec4f>>()) {
            VtArray<GfVec4f> arr = val.UncheckedGet<VtArray<GfVec4f>>();
            bool changed = false;
            if (arr.size() == 1) {
                GfVec4f c = arr[0];
                if (ImGui::ColorEdit4("##dc4", c.data()))
                    { arr[0] = c; changed = true; }
            } else if (!arr.empty()) {
                bool open = ImGui::TreeNodeEx("##dcarr4",
                    ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth);
                ImGui::SameLine(0, 6);
                float sz = ImGui::GetTextLineHeight();
                size_t shown = std::min(arr.size(), (size_t)5);
                for (size_t i = 0; i < shown; ++i) {
                    if (i > 0) ImGui::SameLine(0, 2);
                    char sid[16]; snprintf(sid, sizeof(sid), "##sv4%d", (int)i);
                    ImGui::ColorButton(sid,
                        ImVec4(arr[i][0], arr[i][1], arr[i][2], arr[i][3]),
                        ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip,
                        ImVec2(sz, sz));
                }
                ImGui::SameLine(0, 4);
                ImGui::TextDisabled("[%zu]", arr.size());
                if (open) {
                    static constexpr size_t kEditLimit = 16;
                    size_t lim = std::min(arr.size(), kEditLimit);
                    for (size_t i = 0; i < lim; ++i) {
                        ImGui::PushID((int)i);
                        ImGui::Text("[%d]", (int)i);
                        ImGui::SameLine();
                        GfVec4f c = arr[i];
                        ImGui::SetNextItemWidth(-FLT_MIN);
                        if (ImGui::ColorEdit4("##ce4", c.data()))
                            { arr[i] = c; changed = true; }
                        ImGui::PopID();
                    }
                    if (arr.size() > kEditLimit)
                        ImGui::TextDisabled("  ... %zu more", arr.size() - kEditLimit);
                    ImGui::TreePop();
                }
            }
            if (changed) return VtValue(arr);
            return VtValue();
        }
    }

    // ---- Scalar types ----
    if (val.IsHolding<bool>()) {
        bool b = val.UncheckedGet<bool>();
        if (ImGui::Checkbox("##b", &b)) return VtValue(b);
        return VtValue();
    }
    if (val.IsHolding<int>()) {
        int v = val.UncheckedGet<int>();
        ImGui::InputInt("##i", &v);
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<unsigned int>()) {
        unsigned int v = val.UncheckedGet<unsigned int>();
        ImGui::InputScalar("##ui", ImGuiDataType_U32, &v);
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<int64_t>()) {
        int64_t v = val.UncheckedGet<int64_t>();
        ImGui::InputScalar("##i64", ImGuiDataType_S64, &v);
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<uint64_t>()) {
        uint64_t v = val.UncheckedGet<uint64_t>();
        ImGui::InputScalar("##u64", ImGuiDataType_U64, &v);
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<float>()) {
        float v = val.UncheckedGet<float>();
        ImGui::InputFloat("##f", &v, 0.f, 0.f, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<double>()) {
        double v = val.UncheckedGet<double>();
        ImGui::InputDouble("##d", &v, 0.0, 0.0, "%.6f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<GfHalf>()) {
        float v = static_cast<float>(val.UncheckedGet<GfHalf>());
        ImGui::InputFloat("##h", &v, 0.f, 0.f, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(GfHalf(v));
        return VtValue();
    }

    // ---- Vec2 ----
    if (val.IsHolding<GfVec2f>()) {
        GfVec2f v = val.UncheckedGet<GfVec2f>();
        ImGui::InputScalarN("##v2f", ImGuiDataType_Float,  v.data(), 2, nullptr, nullptr, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<GfVec2d>()) {
        GfVec2d v = val.UncheckedGet<GfVec2d>();
        ImGui::InputScalarN("##v2d", ImGuiDataType_Double, v.data(), 2, nullptr, nullptr, "%.6f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<GfVec2h>()) {
        GfVec2f v(val.UncheckedGet<GfVec2h>());
        ImGui::InputScalarN("##v2h", ImGuiDataType_Float,  v.data(), 2, nullptr, nullptr, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(GfVec2h(v));
        return VtValue();
    }
    if (val.IsHolding<GfVec2i>()) {
        GfVec2i v = val.UncheckedGet<GfVec2i>();
        ImGui::InputScalarN("##v2i", ImGuiDataType_S32,    v.data(), 2);
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }

    // ---- Vec3 ----
    if (val.IsHolding<GfVec3f>()) {
        GfVec3f v = val.UncheckedGet<GfVec3f>();
        ImGui::InputScalarN("##v3f", ImGuiDataType_Float,  v.data(), 3, nullptr, nullptr, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<GfVec3d>()) {
        GfVec3d v = val.UncheckedGet<GfVec3d>();
        ImGui::InputScalarN("##v3d", ImGuiDataType_Double, v.data(), 3, nullptr, nullptr, "%.6f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<GfVec3h>()) {
        GfVec3f v(val.UncheckedGet<GfVec3h>());
        ImGui::InputScalarN("##v3h", ImGuiDataType_Float,  v.data(), 3, nullptr, nullptr, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(GfVec3h(v));
        return VtValue();
    }
    if (val.IsHolding<GfVec3i>()) {
        GfVec3i v = val.UncheckedGet<GfVec3i>();
        ImGui::InputScalarN("##v3i", ImGuiDataType_S32,    v.data(), 3);
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }

    // ---- Vec4 ----
    if (val.IsHolding<GfVec4f>()) {
        GfVec4f v = val.UncheckedGet<GfVec4f>();
        ImGui::InputScalarN("##v4f", ImGuiDataType_Float,  v.data(), 4, nullptr, nullptr, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<GfVec4d>()) {
        GfVec4d v = val.UncheckedGet<GfVec4d>();
        ImGui::InputScalarN("##v4d", ImGuiDataType_Double, v.data(), 4, nullptr, nullptr, "%.6f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }
    if (val.IsHolding<GfVec4h>()) {
        GfVec4f v(val.UncheckedGet<GfVec4h>());
        ImGui::InputScalarN("##v4h", ImGuiDataType_Float,  v.data(), 4, nullptr, nullptr, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(GfVec4h(v));
        return VtValue();
    }
    if (val.IsHolding<GfVec4i>()) {
        GfVec4i v = val.UncheckedGet<GfVec4i>();
        ImGui::InputScalarN("##v4i", ImGuiDataType_S32,    v.data(), 4);
        if (ImGui::IsItemDeactivatedAfterEdit()) return VtValue(v);
        return VtValue();
    }

    // ---- Quaternions ----
    if (val.IsHolding<GfQuatf>()) {
        GfQuatf q = val.UncheckedGet<GfQuatf>();
        GfVec4f buf(q.GetReal(), q.GetImaginary()[0], q.GetImaginary()[1], q.GetImaginary()[2]);
        ImGui::InputScalarN("##qf", ImGuiDataType_Float,  buf.data(), 4, nullptr, nullptr, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit())
            return VtValue(GfQuatf(buf[0], GfVec3f(buf[1], buf[2], buf[3])));
        return VtValue();
    }
    if (val.IsHolding<GfQuatd>()) {
        GfQuatd q = val.UncheckedGet<GfQuatd>();
        GfVec4d buf(q.GetReal(), q.GetImaginary()[0], q.GetImaginary()[1], q.GetImaginary()[2]);
        ImGui::InputScalarN("##qd", ImGuiDataType_Double, buf.data(), 4, nullptr, nullptr, "%.6f");
        if (ImGui::IsItemDeactivatedAfterEdit())
            return VtValue(GfQuatd(buf[0], GfVec3d(buf[1], buf[2], buf[3])));
        return VtValue();
    }
    if (val.IsHolding<GfQuath>()) {
        GfQuath q  = val.UncheckedGet<GfQuath>();
        GfVec4f buf(q.GetReal(), q.GetImaginary()[0], q.GetImaginary()[1], q.GetImaginary()[2]);
        ImGui::InputScalarN("##qh", ImGuiDataType_Float,  buf.data(), 4, nullptr, nullptr, "%.4f");
        if (ImGui::IsItemDeactivatedAfterEdit())
            return VtValue(GfQuath(static_cast<GfHalf>(buf[0]),
                                   GfVec3h(buf[1], buf[2], buf[3])));
        return VtValue();
    }

    // ---- Matrices: collapsed single row, expand to edit ----
    // Render rows inside a TreeNode so the table row stays one line tall
    // when collapsed.  Expanded rows are user-initiated so taller is fine.
    auto drawMatrix = [](const char* id, ImGuiDataType dt, void* data,
                         int rows, int cols) -> bool {
        bool changed = false;
        char hdr[24]; snprintf(hdr, sizeof(hdr), "[%dx%d]##%s", rows, cols, id);
        bool open = ImGui::TreeNodeEx(hdr,
            ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth);
        if (open) {
            ImGui::PushID(id);
            for (int r = 0; r < rows; ++r) {
                char rowId[8]; snprintf(rowId, sizeof(rowId), "##r%d", r);
                ImGui::SetNextItemWidth(-FLT_MIN);
                ImGui::InputScalarN(rowId, dt,
                    static_cast<char*>(data) + r * cols * (dt == ImGuiDataType_Double ? 8 : 4),
                    cols, nullptr, nullptr, "%.4f");
                changed |= ImGui::IsItemDeactivatedAfterEdit();
            }
            ImGui::PopID();
            ImGui::TreePop();
        }
        return changed;
    };

    if (val.IsHolding<GfMatrix4d>()) {
        GfMatrix4d m = val.UncheckedGet<GfMatrix4d>();
        if (drawMatrix("m4d", ImGuiDataType_Double, m.data(), 4, 4)) return VtValue(m);
        return VtValue();
    }
    if (val.IsHolding<GfMatrix4f>()) {
        GfMatrix4f m = val.UncheckedGet<GfMatrix4f>();
        if (drawMatrix("m4f", ImGuiDataType_Float, m.data(), 4, 4)) return VtValue(m);
        return VtValue();
    }
    if (val.IsHolding<GfMatrix3d>()) {
        GfMatrix3d m = val.UncheckedGet<GfMatrix3d>();
        if (drawMatrix("m3d", ImGuiDataType_Double, m.data(), 3, 3)) return VtValue(m);
        return VtValue();
    }
    if (val.IsHolding<GfMatrix3f>()) {
        GfMatrix3f m = val.UncheckedGet<GfMatrix3f>();
        if (drawMatrix("m3f", ImGuiDataType_Float, m.data(), 3, 3)) return VtValue(m);
        return VtValue();
    }
    if (val.IsHolding<GfMatrix2d>()) {
        GfMatrix2d m = val.UncheckedGet<GfMatrix2d>();
        if (drawMatrix("m2d", ImGuiDataType_Double, m.data(), 2, 2)) return VtValue(m);
        return VtValue();
    }
    if (val.IsHolding<GfMatrix2f>()) {
        GfMatrix2f m = val.UncheckedGet<GfMatrix2f>();
        if (drawMatrix("m2f", ImGuiDataType_Float, m.data(), 2, 2)) return VtValue(m);
        return VtValue();
    }

    // ---- Token ----
    if (val.IsHolding<TfToken>()) {
        ImGui::AlignTextToFramePadding();
        ImGui::TextUnformatted(val.UncheckedGet<TfToken>().GetText());
        return VtValue();
    }

    // ---- String / AssetPath ----
    if (val.IsHolding<std::string>()) {
        ImGui::AlignTextToFramePadding();
        ImGui::TextUnformatted(val.UncheckedGet<std::string>().c_str());
        return VtValue();
    }
    if (val.IsHolding<SdfAssetPath>()) {
        ImGui::AlignTextToFramePadding();
        ImGui::TextUnformatted(val.UncheckedGet<SdfAssetPath>().GetAssetPath().c_str());
        return VtValue();
    }

    // ---- Token array: view + edit ----
    if (val.IsHolding<VtArray<TfToken>>()) {
        VtArray<TfToken> arr = val.UncheckedGet<VtArray<TfToken>>();
        bool changed = false;

        // Optional allowedTokens → show Combo per element instead of InputText.
        VtValue allowedMeta;
        attr.GetMetadata(TfToken("allowedTokens"), &allowedMeta);
        const VtArray<TfToken>* allowed =
            (!allowedMeta.IsEmpty() && allowedMeta.IsHolding<VtArray<TfToken>>())
            ? &allowedMeta.UncheckedGet<VtArray<TfToken>>()
            : nullptr;

        // Widget for one element (combo or free-text).
        auto drawOne = [&](int idx) {
            ImGui::PushID(idx);
            if (allowed) {
                std::string cur = arr[idx].GetString();
                ImGui::SetNextItemWidth(-FLT_MIN);
                if (ImGui::BeginCombo("##tc", cur.c_str())) {
                    for (const auto& tok : *allowed) {
                        bool sel = (tok == arr[idx]);
                        if (ImGui::Selectable(tok.GetText(), sel))
                            { arr[idx] = tok; changed = true; }
                        if (sel) ImGui::SetItemDefaultFocus();
                    }
                    ImGui::EndCombo();
                }
            } else {
                char buf[256];
                snprintf(buf, sizeof(buf), "%s", arr[idx].GetText());
                ImGui::SetNextItemWidth(-FLT_MIN);
                ImGui::InputText("##te", buf, sizeof(buf));
                if (ImGui::IsItemDeactivatedAfterEdit())
                    { arr[idx] = TfToken(buf); changed = true; }
            }
            ImGui::PopID();
        };

        if (arr.empty()) {
            ImGui::AlignTextToFramePadding();
            ImGui::TextDisabled("[empty]");
        } else if (arr.size() == 1) {
            drawOne(0);
        } else {
            bool open = ImGui::TreeNodeEx("##tarr",
                ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth);
            ImGui::SameLine(0, 6);
            ImGui::TextDisabled("[%zu tokens]", arr.size());
            if (open) {
                static constexpr size_t kLimit = 32;
                size_t lim = std::min(arr.size(), kLimit);
                for (size_t i = 0; i < lim; ++i) {
                    ImGui::Text("[%d]", (int)i);
                    ImGui::SameLine();
                    drawOne((int)i);
                }
                if (arr.size() > kLimit)
                    ImGui::TextDisabled("  ... %zu more", arr.size() - kLimit);
                ImGui::TreePop();
            }
        }
        if (changed) return VtValue(arr);
        return VtValue();
    }

    // ---- Large arrays: just show size ----
    if (val.IsArrayValued() && val.GetArraySize() > 5) {
        ImGui::AlignTextToFramePadding();
        ImGui::TextDisabled("[array  %zu]", val.GetArraySize());
        return VtValue();
    }

    // ---- Fallback: stream to string ----
    {
        std::ostringstream oss;
        oss << val;
        std::string s = oss.str();
        if (s.size() > 120) { s.resize(120); s += " ..."; }
        ImGui::AlignTextToFramePadding();
        ImGui::TextUnformatted(s.c_str());
    }
    return VtValue();
}

// ---------------------------------------------------------------------------
// Header child  (2 rows – mirrors DrawUsdPrimHeader in usdtweak)
// ---------------------------------------------------------------------------
void PropertyPanel::RenderPrimHeader(const pxr::UsdPrim& prim) {
    constexpr ImGuiTableFlags kFlags =
        ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;

    if (!ImGui::BeginTable("##primHeader", 2, kFlags)) return;
    ImGui::TableSetupColumn("##field", ImGuiTableColumnFlags_WidthFixed, 80.f);
    ImGui::TableSetupColumn("##value", ImGuiTableColumnFlags_WidthStretch);

    // Row 1 – Edit target
    ImGui::TableNextRow();
    ImGui::TableSetColumnIndex(0);
    ImGui::TextDisabled("Edit target");
    ImGui::TableSetColumnIndex(1);
    {
        const auto& et    = prim.GetStage()->GetEditTarget();
        const auto  tpath = et.MapToSpecPath(prim.GetPath());
        ImVec4 col = tpath == SdfPath()
            ? ImVec4(1.f, 0.3f, 0.3f, 1.f)
            : ImVec4(0.7f, 1.f, 0.7f, 1.f);
        ImGui::TextColored(col, "%s  %s",
            et.GetLayer()->GetDisplayName().c_str(),
            tpath.GetString().c_str());
    }

    // Row 2 – Type + breadcrumb path
    ImGui::TableNextRow();
    ImGui::TableSetColumnIndex(0);
    ImGui::TextColored(ImVec4(0.5f, 0.9f, 1.f, 1.f),
        "%s", m_primType.empty() ? "Prim" : m_primType.c_str());
    ImGui::TableSetColumnIndex(1);
    {
        const SdfPath primPath = prim.GetPrimPath();
        const auto prefixes    = primPath.GetPrefixes();
        ImGui::Text("/");
        for (int i = 0; i < static_cast<int>(prefixes.size()); ++i) {
            ImGui::SameLine(0, 0);
            ImGui::TextColored(ImVec4(0.85f, 0.85f, 0.85f, 1.f),
                "%s", prefixes[i].GetName().c_str());
            if (i < static_cast<int>(prefixes.size()) - 1) {
                ImGui::SameLine(0, 0);
                ImGui::TextDisabled(" / ");
            }
        }
    }
    ImGui::EndTable();
}

// ---------------------------------------------------------------------------
// References & Payloads – one row per list-op entry; clicking the asset path
// browses for a replacement file (undoable whole-list-op swap).
// ---------------------------------------------------------------------------
namespace {

pxr::SdfReference WithAssetPath(const pxr::SdfReference& r, const std::string& assetPath) {
    return pxr::SdfReference(assetPath, r.GetPrimPath(), r.GetLayerOffset(), r.GetCustomData());
}
pxr::SdfPayload WithAssetPath(const pxr::SdfPayload& p, const std::string& assetPath) {
    return pxr::SdfPayload(assetPath, p.GetPrimPath(), p.GetLayerOffset());
}

// Shared row renderer for SdfReferenceListOp / SdfPayloadListOp: one row per
// item in each list-op bucket, labelled with its op. On replace, the whole
// modified list op is authored to the edit target as one undoable command.
template <typename ListOpT>
void RenderArcListRows(const pxr::UsdPrim& prim, CommandHistory* history,
                       IconManager* icons,
                       const char* tag, const char* displayKind,
                       const pxr::TfToken& fieldKey, const ListOpT& listOp, int& uid)
{
    using ItemVector = typename ListOpT::ItemVector;
    struct Bucket { const char* opName; ItemVector items; };
    Bucket buckets[] = {
        {"explicit", listOp.GetExplicitItems()},
        {"add",      listOp.GetAddedItems()},
        {"prepend",  listOp.GetPrependedItems()},
        {"append",   listOp.GetAppendedItems()},
        {"delete",   listOp.GetDeletedItems()},
    };
    const float rowH = ImGui::GetFrameHeight();

    for (size_t b = 0; b < sizeof(buckets) / sizeof(buckets[0]); ++b) {
        for (size_t i = 0; i < buckets[b].items.size(); ++i) {
            const auto& item = buckets[b].items[i];
            std::string display = item.GetAssetPath();
            if (!item.GetPrimPath().IsEmpty())
                display += "  <" + item.GetPrimPath().GetString() + ">";

            ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
            ImGui::PushID(uid++);

            ImGui::TableSetColumnIndex(0);
            DrawPropertyMiniButton(tag, /*authored=*/true,
                                   item.GetAssetPath().c_str(), displayKind);

            ImGui::TableSetColumnIndex(1);
            ImGui::AlignTextToFramePadding();
            ImGui::TextDisabled("%s", buckets[b].opName);

            ImGui::TableSetColumnIndex(2);
            // Browse icon button first, then the path as plain text.
            bool clicked;
            if (icons) {
                clicked = ImGui::ImageButton("##browseArc",
                                             ImTextureRef(icons->Get(Icon::FolderOpen)),
                                             ImVec2(14.f, 14.f));
            } else {
                clicked = ImGui::SmallButton("...");
            }
            if (ImGui::IsItemHovered())
                ImGui::SetTooltip("Browse for a replacement file");
            ImGui::SameLine();
            ImGui::AlignTextToFramePadding();
            ImGui::TextUnformatted(display.empty() ? "(no asset)" : display.c_str());
            if (ImGui::IsItemHovered())
                ImGui::SetTooltip("%s (%s)\n%s", displayKind, buckets[b].opName,
                                  item.GetAssetPath().c_str());
            if (clicked) {
                // Anything USD can compose is a valid arc target: native USD
                // layers plus file formats with layer plugins (MaterialX,
                // Alembic).
                static const char* kArcFileFilter =
                    "Referenceable (*.usd;*.usda;*.usdc;*.usdz;*.mtlx;*.abc)\0"
                        "*.usd;*.usda;*.usdc;*.usdz;*.mtlx;*.abc\0"
                    "USD Files (*.usd;*.usda;*.usdc;*.usdz)\0*.usd;*.usda;*.usdc;*.usdz\0"
                    "MaterialX (*.mtlx)\0*.mtlx\0"
                    "Alembic (*.abc)\0*.abc\0"
                    "All Files (*.*)\0*.*\0";
                std::string picked = FileDialog::OpenFile(kArcFileFilter, "Select Reference File");
                if (!picked.empty() && picked != item.GetAssetPath()) {
                    ListOpT newOp = listOp;
                    ItemVector items = buckets[b].items;
                    items[i] = WithAssetPath(item, picked);
                    switch (b) {
                        case 0: newOp.SetExplicitItems(items);  break;
                        case 1: newOp.SetAddedItems(items);     break;
                        case 2: newOp.SetPrependedItems(items); break;
                        case 3: newOp.SetAppendedItems(items);  break;
                        case 4: newOp.SetDeletedItems(items);   break;
                    }
                    // Authors the whole (possibly multi-layer-composed) list
                    // op into the edit target — the pragmatic single-layer
                    // behaviour; undo restores the previous composed op.
                    if (history) {
                        pxr::UsdPrim p = prim;
                        ListOpT oldOp  = listOp;
                        history->Push(std::make_unique<AttributeSetCommand>(
                            std::string("Replace ") + displayKind + " Path",
                            [p, fieldKey, newOp]() mutable { p.SetMetadata(fieldKey, newOp); },
                            [p, fieldKey, oldOp]() mutable { p.SetMetadata(fieldKey, oldOp); }));
                    } else {
                        pxr::UsdPrim(prim).SetMetadata(fieldKey, newOp);
                    }
                }
            }
            ImGui::PopID();
        }
    }
}

} // namespace

void PropertyPanel::RenderCompositionArcsSection(const pxr::UsdPrim& prim) {
    if (!ImGui::CollapsingHeader("References & Payloads", ImGuiTreeNodeFlags_DefaultOpen))
        return;

    constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
    if (!ImGui::BeginTable("##compArcs", 3, kFlags)) return;
    ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, 20.f);
    ImGui::TableSetupColumn("##op",  ImGuiTableColumnFlags_WidthFixed, 60.f);
    ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch);

    int uid = 0;
    if (prim.HasAuthoredReferences()) {
        pxr::SdfReferenceListOp refOp;
        if (prim.GetMetadata(pxr::SdfFieldKeys->References, &refOp))
            RenderArcListRows(prim, m_commandHistory, m_iconManager, "(R)", "Reference",
                              pxr::SdfFieldKeys->References, refOp, uid);
    }
    if (prim.HasAuthoredPayloads()) {
        pxr::SdfPayloadListOp payloadOp;
        if (prim.GetMetadata(pxr::SdfFieldKeys->Payload, &payloadOp))
            RenderArcListRows(prim, m_commandHistory, m_iconManager, "(P)", "Payload",
                              pxr::SdfFieldKeys->Payload, payloadOp, uid);
    }
    ImGui::EndTable();
}

// ---------------------------------------------------------------------------
// Asset Info – read-only view of the composed assetInfo dictionary
// ---------------------------------------------------------------------------
void PropertyPanel::RenderAssetInfoSection(const pxr::UsdPrim& prim) {
    if (!ImGui::CollapsingHeader("Asset Info", ImGuiTreeNodeFlags_DefaultOpen))
        return;

    constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
    if (!ImGui::BeginTable("##assetInfo", 2, kFlags)) return;
    ImGui::TableSetupColumn("##key", ImGuiTableColumnFlags_WidthFixed, 110.f);
    ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch);

    const pxr::VtDictionary info = prim.GetAssetInfo();
    std::vector<std::string> keys;
    keys.reserve(info.size());
    for (const auto& entry : info)
        keys.push_back(entry.first);
    std::sort(keys.begin(), keys.end());

    for (const std::string& key : keys) {
        const pxr::VtValue& value = info.find(key)->second;
        ImGui::TableNextRow();
        ImGui::TableSetColumnIndex(0);
        ImGui::TextDisabled("%s", key.c_str());
        ImGui::TableSetColumnIndex(1);
        if (value.IsHolding<pxr::SdfAssetPath>()) {
            const auto& assetPath = value.UncheckedGet<pxr::SdfAssetPath>();
            ImGui::TextUnformatted(assetPath.GetAssetPath().c_str());
            if (ImGui::IsItemHovered() && !assetPath.GetResolvedPath().empty())
                ImGui::SetTooltip("resolved: %s", assetPath.GetResolvedPath().c_str());
        } else {
            const std::string text = pxr::TfStringify(value);
            ImGui::TextUnformatted(text.c_str());
            if (ImGui::IsItemHovered())
                ImGui::SetTooltip("%s", text.c_str());
        }
    }
    ImGui::EndTable();
}

// ---------------------------------------------------------------------------
// Variant Sets – no borders, no header  (mirrors DrawVariantSetsCombos in usdtweak)
// ---------------------------------------------------------------------------
void PropertyPanel::RenderVariantSetsSection(const pxr::UsdPrim& prim) {
    if (!prim.HasVariantSets()) return;

    // usdtweak uses SizingFixedFit | RowBg – no borders, no header
    constexpr ImGuiTableFlags kFlags =
        ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;

    if (!ImGui::BeginTable("##variantSets", 3, kFlags)) return;
    ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed,  20.f);
    ImGui::TableSetupColumn("##vsn", ImGuiTableColumnFlags_WidthFixed, 140.f);
    ImGui::TableSetupColumn("##vsc", ImGuiTableColumnFlags_WidthStretch);
    // No TableHeadersRow

    pxr::UsdVariantSets variantSets = prim.GetVariantSets();
    const auto& editTarget = prim.GetStage()->GetEditTarget();
    const SdfPath targetPath = editTarget.MapToSpecPath(prim.GetPath());
    auto editTargetPrimSpec = editTarget.GetLayer()->GetPrimAtPath(targetPath);

    for (const auto& name : variantSets.GetNames()) {
        ImGui::TableNextRow();

        ImGui::TableSetColumnIndex(0);
        {
            bool isAuthored = editTargetPrimSpec &&
                editTargetPrimSpec->GetVariantSelections().count(name) > 0;
            std::string path = prim.GetPath().GetString() + ".variantSets[" + name + "]";
            ImGui::PushID(name.c_str());
            DrawPropertyMiniButton("(v)", isAuthored, path.c_str());
            ImGui::PopID();
        }

        ImGui::TableSetColumnIndex(1);
        ImGui::TextUnformatted(name.c_str());

        ImGui::TableSetColumnIndex(2);
        ImGui::SetNextItemWidth(-FLT_MIN);
        pxr::UsdVariantSet varSet = variantSets.GetVariantSet(name);
        std::string current = varSet.GetVariantSelection();
        ImGui::PushID(name.c_str());
        if (ImGui::BeginCombo("##vs", current.c_str())) {
            for (const auto& variant : varSet.GetVariantNames()) {
                bool selected = (variant == current);
                if (ImGui::Selectable(variant.c_str(), selected))
                    varSet.SetVariantSelection(variant);
                if (selected) ImGui::SetItemDefaultFocus();
            }
            ImGui::EndCombo();
        }
        ImGui::PopID();
    }
    ImGui::EndTable();
}

// ---------------------------------------------------------------------------
// Material binding section
// ---------------------------------------------------------------------------
void PropertyPanel::RenderMaterialBindSection(const pxr::UsdPrim& prim) {
    using namespace pxr;

    if (!prim.HasAPI<UsdShadeMaterialBindingAPI>()) return;
    UsdShadeMaterialBindingAPI bindAPI(prim);

    if (!ImGui::CollapsingHeader("Material Binding", ImGuiTreeNodeFlags_DefaultOpen))
        return;

    constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
    if (!ImGui::BeginTable("##matbind", 2, kFlags)) return;
    ImGui::TableSetupColumn("##mf", ImGuiTableColumnFlags_WidthFixed,  80.f);
    ImGui::TableSetupColumn("##mv", ImGuiTableColumnFlags_WidthStretch);

    // Direct binding (allPurpose)
    {
        auto direct = bindAPI.GetDirectBinding();
        SdfPath matPath = direct.GetMaterialPath();

        ImGui::TableNextRow();
        ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Direct");
        ImGui::TableSetColumnIndex(1);
        if (!matPath.IsEmpty()) {
            bool authored = direct.GetBindingRel() && direct.GetBindingRel().IsAuthored();
            ImVec4 col = authored ? ImVec4(0.6f, 0.9f, 1.f, 1.f)
                                  : ImVec4(0.5f, 0.5f, 0.5f, 1.f);
            ImGui::TextColored(col, "%s", matPath.GetText());
            if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", matPath.GetText());
        } else {
            ImGui::TextDisabled("(none)");
        }
    }

    // Preview purpose direct binding (only show if different from allPurpose)
    {
        auto preview = bindAPI.GetDirectBinding(UsdShadeTokens->preview);
        SdfPath previewPath = preview.GetMaterialPath();
        if (!previewPath.IsEmpty()) {
            ImGui::TableNextRow();
            ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Preview");
            ImGui::TableSetColumnIndex(1);
            ImGui::TextColored(ImVec4(0.6f, 0.9f, 1.f, 1.f), "%s", previewPath.GetText());
        }
    }

    // Full purpose direct binding
    {
        auto full = bindAPI.GetDirectBinding(UsdShadeTokens->full);
        SdfPath fullPath = full.GetMaterialPath();
        if (!fullPath.IsEmpty()) {
            ImGui::TableNextRow();
            ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Full");
            ImGui::TableSetColumnIndex(1);
            ImGui::TextColored(ImVec4(0.6f, 0.9f, 1.f, 1.f), "%s", fullPath.GetText());
        }
    }

    // Resolved (computed) bound material
    {
        UsdShadeMaterial resolved = bindAPI.ComputeBoundMaterial();

        ImGui::TableNextRow();
        ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Resolved");
        ImGui::TableSetColumnIndex(1);
        if (resolved) {
            std::string matPathStr = resolved.GetPrim().GetPath().GetString();
            ImGui::TextColored(ImVec4(0.7f, 1.f, 0.7f, 1.f), "%s", matPathStr.c_str());
            if (OnEditMaterialRequested) {
                ImGui::SameLine();
                if (ImGui::SmallButton("Edit Material"))
                    OnEditMaterialRequested(matPathStr);
            }
        } else {
            ImGui::TextDisabled("(none)");
        }
    }

    ImGui::EndTable();
}

// ---------------------------------------------------------------------------
// Inner attribute + relationship table — one category group
// ---------------------------------------------------------------------------
void PropertyPanel::RenderAttrRelSubTable(
    const std::vector<pxr::UsdAttribute>&   attrs,
    const std::vector<pxr::UsdRelationship>& rels,
    const pxr::UsdEditTarget&               editTarget)
{
    using namespace pxr;

    if (attrs.empty() && rels.empty()) return;

    constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
    if (!ImGui::BeginTable("##propSubTable", 3, kFlags)) return;

    // Col 0 wider when the key button column is active
    float col0W = m_editTime.IsDefault() ? 20.f : 44.f;
    ImGui::TableSetupColumn("##dot",  ImGuiTableColumnFlags_WidthFixed,  col0W);
    ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthFixed,  140.f);
    ImGui::TableSetupColumn("##val",  ImGuiTableColumnFlags_WidthStretch);

    const float rowH = ImGui::GetFrameHeight();
    int uid = 0;

    // ── Attributes ──────────────────────────────────────────────────────────
    for (auto attr : attrs) {
        ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);

        // Col 0 – (a) mini-button + optional (k) key button
        ImGui::TableSetColumnIndex(0);
        ImGui::PushID(uid++);
        DrawPropertyMiniButton("(a)",
            attr.IsAuthoredAt(editTarget),
            attr.GetPath().GetString().c_str(),
            attr.GetTypeName().GetAsToken().GetText());
        ImGui::PopID();

        bool isVarying = (attr.GetVariability() == SdfVariabilityVarying);
        if (!m_editTime.IsDefault() && isVarying) {
            ImGui::SameLine(0, 2);
            ImGui::PushID(uid++);
            DrawKeyButton(attr, m_editTime, m_commandHistory);
            ImGui::PopID();
        }

        // Col 1 – display name + right-click context menu
        ImGui::TableSetColumnIndex(1);
        ImGui::PushID(static_cast<int>(attr.GetPath().GetHash()) ^ 0x100);
        ImGui::AlignTextToFramePadding();
        ImGui::TextUnformatted(GetPropDisplayName(attr).c_str());
        if (ImGui::BeginPopupContextItem("##attrCtx")) {
            if (ImGui::MenuItem("Copy attribute path"))
                ImGui::SetClipboardText(attr.GetPath().GetString().c_str());
            ImGui::Separator();
            bool authored = attr.IsAuthoredAt(editTarget);
            ImGui::BeginDisabled(!authored);
            if (ImGui::MenuItem("Clear Override")) {
                // Capture current authored opinions so undo can restore them
                std::vector<double> times;
                attr.GetTimeSamples(&times);
                std::vector<std::pair<double, VtValue>> samples;
                for (double t : times) {
                    VtValue v;
                    if (attr.Get(&v, UsdTimeCode(t)))
                        samples.emplace_back(t, v);
                }
                VtValue defVal;
                bool hasDefault = attr.Get(&defVal, UsdTimeCode::Default());

                if (m_commandHistory) {
                    m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
                        "Clear Override",
                        [attr]() mutable { attr.Clear(); },
                        [attr, samps = samples, hasDefault, defVal]() mutable {
                            if (hasDefault) attr.Set(defVal, UsdTimeCode::Default());
                            for (auto& [t, v] : samps)
                                attr.Set(v, UsdTimeCode(t));
                        }
                    ));
                } else {
                    attr.Clear();
                }
                ImGui::CloseCurrentPopup();
            }
            ImGui::EndDisabled();
            ImGui::EndPopup();
        }
        ImGui::PopID();

        // Col 2 – value widget
        ImGui::TableSetColumnIndex(2);
        ImGui::PushID(static_cast<int>(attr.GetPath().GetHash()));
        ImGui::PushItemWidth(-FLT_MIN);

        VtValue allowedTokens;
        attr.GetMetadata(TfToken("allowedTokens"), &allowedTokens);

        VtValue val;
        bool hasVal = attr.Get(&val, m_displayTime);

        if (!allowedTokens.IsEmpty() &&
            allowedTokens.IsHolding<VtArray<TfToken>>())
        {
            const auto& tokens = allowedTokens.UncheckedGet<VtArray<TfToken>>();
            std::string cur = (hasVal && val.IsHolding<TfToken>())
                ? val.UncheckedGet<TfToken>().GetString() : "";
            if (ImGui::BeginCombo("##tok", cur.c_str())) {
                for (const auto& tok : tokens) {
                    bool sel = (tok.GetString() == cur);
                    if (ImGui::Selectable(tok.GetText(), sel)) attr.Set(tok, m_editTime);
                    if (sel) ImGui::SetItemDefaultFocus();
                }
                ImGui::EndCombo();
            }
        } else if (hasVal) {
            VtValue modified = DrawVtValueWidget(attr, val);
            if (!modified.IsEmpty())
                attr.Set(modified, m_editTime);
        } else if (attr.HasAuthoredConnections()) {
            SdfPathVector conns;
            attr.GetConnections(&conns);
            if (!conns.empty()) {
                ImGui::AlignTextToFramePadding();
                ImGui::TextDisabled("-> %s", conns[0].GetString().c_str());
            }
        } else {
            ImGui::AlignTextToFramePadding();
            ImGui::TextDisabled("no value");
        }

        ImGui::PopItemWidth();
        ImGui::PopID();
    }

    // ── Relationships ────────────────────────────────────────────────────────
    for (const auto& rel : rels) {
        ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);

        // Col 0 – (r) mini-button (no key button for relationships)
        ImGui::TableSetColumnIndex(0);
        ImGui::PushID(uid++);
        DrawPropertyMiniButton("(r)",
            rel.IsAuthored(),
            rel.GetPath().GetString().c_str());
        ImGui::PopID();

        // Col 1 – display name + right-click context menu
        ImGui::TableSetColumnIndex(1);
        ImGui::PushID(static_cast<int>(rel.GetPath().GetHash()) ^ 0x100);
        ImVec4 relColor = rel.IsAuthored()
            ? ImVec4(0.6f, 0.9f, 1.f, 1.f)
            : ImVec4(0.5f, 0.5f, 0.5f, 1.f);
        ImGui::AlignTextToFramePadding();
        ImGui::TextColored(relColor, "%s", GetPropDisplayName(rel).c_str());
        if (ImGui::BeginPopupContextItem("##relCtx")) {
            if (ImGui::MenuItem("Copy path"))
                ImGui::SetClipboardText(rel.GetPath().GetString().c_str());
            ImGui::Separator();
            ImGui::BeginDisabled(!rel.IsAuthored());
            if (ImGui::MenuItem("Clear Override")) {
                SdfPathVector oldTargets;
                rel.GetTargets(&oldTargets);
                if (m_commandHistory) {
                    m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
                        "Clear Override",
                        [rel]() mutable { rel.ClearTargets(false); },
                        [rel, tgts = oldTargets]() mutable {
                            for (const auto& p : tgts) rel.AddTarget(p);
                        }
                    ));
                } else {
                    rel.ClearTargets(false);
                }
                ImGui::CloseCurrentPopup();
            }
            ImGui::EndDisabled();
            ImGui::EndPopup();
        }
        ImGui::PopID();

        // Col 2 – target list
        ImGui::TableSetColumnIndex(2);
        SdfPathVector targets;
        rel.GetTargets(&targets);
        if (targets.empty()) {
            ImGui::AlignTextToFramePadding();
            ImGui::TextDisabled("no targets");
        } else if (targets.size() == 1) {
            ImGui::AlignTextToFramePadding();
            ImGui::TextDisabled("%s", targets[0].GetString().c_str());
            if (ImGui::IsItemHovered())
                ImGui::SetTooltip("%s", targets[0].GetString().c_str());
        } else {
            ImGui::PushID(rel.GetPath().GetString().c_str());
            bool open = ImGui::TreeNodeEx("##rtarr",
                ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth);
            ImGui::SameLine(0, 6);
            ImGui::TextDisabled("[%zu targets]", targets.size());
            if (open) {
                for (const auto& path : targets) {
                    ImGui::PushID(path.GetString().c_str());
                    ImGui::TextDisabled("%s", path.GetString().c_str());
                    if (ImGui::IsItemHovered())
                        ImGui::SetTooltip("%s", path.GetString().c_str());
                    ImGui::PopID();
                }
                ImGui::TreePop();
            }
            ImGui::PopID();
        }
    }

    ImGui::EndTable();
}

// ---------------------------------------------------------------------------
// Unified properties table  (attributes + relationships)
//
// Mirrors ##DrawPropertyEditorTable in usdtweak DrawUsdPrimProperties:
//   • Flags: SizingFixedFit | RowBg  — NO borders, NO header row
//   • Col 0  (fixed ~20px) : SmallButton mini-button coloured by authored state
//   • Col 1  (fixed 140px) : property display name  (namespace:basename)
//   • Col 2  (stretch)     : type-aware value widget, PushItemWidth(-FLT_MIN)
// ---------------------------------------------------------------------------
void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
    using namespace pxr;

    // Build attr-name → category label map from schema registry
    std::unordered_map<std::string, std::string> attrToCategory;
    const UsdSchemaRegistry& reg = UsdSchemaRegistry::GetInstance();

    if (!m_primType.empty()) {
        if (const UsdPrimDefinition* typeDef =
                reg.FindConcretePrimDefinition(TfToken(m_primType))) {
            for (const TfToken& prop : typeDef->GetPropertyNames())
                attrToCategory.emplace(prop.GetString(), m_primType);
        }
    }

    for (const auto& schemaToken : prim.GetAppliedSchemas()) {
        std::string s = schemaToken.GetString();
        // Multi-apply tokens look like "CollectionAPI:name"; strip instance suffix
        auto pos = s.find(':');
        std::string baseName = (pos != std::string::npos) ? s.substr(0, pos) : s;

        if (const UsdPrimDefinition* apiDef =
                reg.FindAppliedAPIPrimDefinition(TfToken(baseName))) {
            for (const TfToken& prop : apiDef->GetPropertyNames())
                attrToCategory.emplace(prop.GetString(), baseName); // first writer wins
        }
    }

    // Bucket attrs and rels into ordered category groups
    struct PropGroup {
        std::vector<UsdAttribute>   attrs;
        std::vector<UsdRelationship> rels;
    };
    std::vector<std::string>              orderedCats;
    std::set<std::string>                 seenCats;
    std::unordered_map<std::string, PropGroup> groups;

    auto addCat = [&](const std::string& cat) {
        if (seenCats.insert(cat).second) orderedCats.push_back(cat);
    };
    if (!m_primType.empty()) addCat(m_primType);

    const std::string fallback = m_primType.empty() ? "Properties" : m_primType;

    for (auto attr : prim.GetAttributes()) {
        const std::string name = attr.GetName().GetString();
        if (TfStringStartsWith(name, "xformOp:")) continue; // shown in TRS section

        auto it = attrToCategory.find(name);
        std::string cat;
        if (it != attrToCategory.end()) {
            cat = it->second;
        } else {
            std::string ns = attr.GetNamespace().GetString();
            if (ns.empty()) {
                cat = fallback;
            } else {
                auto c = ns.find(':');
                cat = (c != std::string::npos) ? ns.substr(0, c) : ns;
            }
        }
        addCat(cat);
        groups[cat].attrs.push_back(attr);
    }

    for (const auto& rel : prim.GetRelationships()) {
        std::string ns = rel.GetNamespace().GetString();
        std::string cat;
        if (ns.empty()) {
            cat = fallback;
        } else {
            auto c = ns.find(':');
            cat = (c != std::string::npos) ? ns.substr(0, c) : ns;
        }
        addCat(cat);
        groups[cat].rels.push_back(rel);
    }

    // Sort: primType first (already inserted), remaining alphabetically
    if (orderedCats.size() > 1) {
        size_t firstSort = m_primType.empty() ? 0 : 1;
        std::sort(orderedCats.begin() + firstSort, orderedCats.end());
    }

    // Render each category under a collapsing header
    const UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget();

    for (const auto& cat : orderedCats) {
        auto& group = groups[cat];
        if (group.attrs.empty() && group.rels.empty()) continue;

        ImGui::PushID(cat.c_str());
        if (ImGui::CollapsingHeader(cat.c_str(), ImGuiTreeNodeFlags_DefaultOpen))
            RenderAttrRelSubTable(group.attrs, group.rels, editTarget);
        ImGui::PopID();
    }
}

// ---------------------------------------------------------------------------
// Main render entry point
// ---------------------------------------------------------------------------
void PropertyPanel::Render() {
    if (!m_stage) {
        ImGui::TextDisabled("No stage loaded");
        return;
    }
    if (m_selectedPrimPath.empty()) {
        ImGui::TextDisabled("No prim selected");
        return;
    }

    if (m_needsRead || !m_isAnyFieldActive)
        ReadTransform();

    UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath));
    if (!prim) return;

    // Fixed-height header child (2 rows)
    const float rowH    = ImGui::GetFrameHeightWithSpacing();
    const float headerH = rowH * 2.f + ImGui::GetStyle().WindowPadding.y * 2.f;
    ImGui::BeginChild("##header", ImVec2(-FLT_MIN, headerH), true);
    RenderPrimHeader(prim);
    ImGui::EndChild();

    ImGui::Separator();

    // Scrollable body – composition arcs → variant sets → transform → all properties
    ImGui::BeginChild("##body");

    if (prim.HasAuthoredReferences() || prim.HasAuthoredPayloads()) {
        RenderCompositionArcsSection(prim);
        ImGui::Separator();
    }

    if (!prim.GetAssetInfo().empty()) {
        RenderAssetInfoSection(prim);
        ImGui::Separator();
    }

    if (prim.HasVariantSets()) {
        RenderVariantSetsSection(prim);
        ImGui::Separator();
    }

    if (prim.HasAPI<pxr::UsdShadeMaterialBindingAPI>()) {
        RenderMaterialBindSection(prim);
        ImGui::Separator();
    }

    if (m_hasXform) {
        RenderTransformSection();
        ImGui::Separator();
    }

    RenderPropertiesTable(prim);

    ImGui::EndChild();
}

} // namespace UsdLayerManager
