#include "PropertyPanel.h"
#include "../utils/Logger.h"
#include "../core/commands/TransformCommand.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 <memory>
#include <sstream>

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

// ---------------------------------------------------------------------------
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: 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();
}

// ---------------------------------------------------------------------------
// 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();
}

// ---------------------------------------------------------------------------
// 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) {
    // Exact flags used by usdtweak – no borders, alternating row background
    constexpr ImGuiTableFlags kFlags =
        ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;

    if (!ImGui::BeginTable("##properties", 3, kFlags)) return;

    ImGui::TableSetupColumn("##dot",  ImGuiTableColumnFlags_WidthFixed,    20.f);
    ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthFixed,   140.f);
    ImGui::TableSetupColumn("##val",  ImGuiTableColumnFlags_WidthStretch);
    // No TableHeadersRow – matches usdtweak "no header" intent

    const pxr::UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget();
    const float rowH = ImGui::GetFrameHeight();
    int uid = 0;

    // ---- Attributes (mirrors usdtweak attribute loop) ----
    for (auto& attr : prim.GetAttributes()) {
        ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);

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

        // Col 1 – display name  (namespace:basename)
        ImGui::TableSetColumnIndex(1);
        ImGui::AlignTextToFramePadding();
        ImGui::TextUnformatted(GetPropDisplayName(attr).c_str());

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

        // Check for allowedTokens first → Combo  (mirrors DrawTfToken in usdtweak)
        pxr::VtValue allowedTokens;
        attr.GetMetadata(pxr::TfToken("allowedTokens"), &allowedTokens);

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

        if (!allowedTokens.IsEmpty() &&
            allowedTokens.IsHolding<pxr::VtArray<pxr::TfToken>>())
        {
            const auto& tokens = allowedTokens.UncheckedGet<pxr::VtArray<pxr::TfToken>>();
            std::string cur = (hasVal && val.IsHolding<pxr::TfToken>())
                ? val.UncheckedGet<pxr::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) {
            // Type-aware widget dispatch  (mirrors DrawVtValue in usdtweak)
            pxr::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 (mirrors usdtweak relationship loop) ----
    for (const auto& rel : prim.GetRelationships()) {
        ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);

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

        // Col 1 – display name, coloured by authored state  (mirrors DrawUsdRelationshipDisplayName)
        ImGui::TableSetColumnIndex(1);
        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());

        // Col 2 – target list: single row when collapsed, expand for multiple
        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();
}

// ---------------------------------------------------------------------------
// 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 – variant sets → transform → all properties
    ImGui::BeginChild("##body");

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

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

    RenderPropertiesTable(prim);

    ImGui::EndChild();
}

} // namespace UsdLayerManager
