//
//  CoreIKBridge.swift
//  HelloVisionOS - Niall George Horn - June 2026
//
//  ==========================================================================
//  What this file does, in plain English
//  ==========================================================================
//  Info: 
//  TL;DR : It's the meat and potatoes
// 
//  This is the single bridge between the Swift app and CoreIK. 
//  CoreIK == `CoreIK.framework` — the production body-IK solver that animates the
//  Persona avatar on Apple Vision Pro.
//
//  Apple never intended third-party code to call CoreIK directly: the
//  framework is loaded only by a few blessed Apple clients (or so they thought), 
//  its symbols are present in the binary but not in the public exports trie, and 
//  its C++ mangling changes between builds. So `dlsym()` does not work.
// ==========================================================================
//  What does work:
//    1. `dlopen()` the framework to get it mapped into our process
//    2. Read the ASLR slide for that image
//    3. For each private function we need, take the byte offset that
//       `nm --demangle CoreIK` prints, add the ASLR slide, and cast the
//       resulting address to a C function pointer
//    4. Call `_CoreIKSolverFusionEnableReplayMode` on a freshly-created
//       solver — this is the "no hardware, please" switch that Apple's
//       own engineers use to develop the solver offline, 
//       when they are totally working 5 days in office for sure. 
//    5. Per frame, hand the solver fabricated head and wrist positions
//       (the slider values in our UI, or values played back from a
//       recording) via `SetVioPoseV2` and `SetSourcePoseV2`
//       Vio stands for Visual Inertial Odometry (but ofc not used with synthetic data). 
//    6. Call `SolveSources` — Apple's production IK solver runs and
//       returns 91 joint transforms, which we hand to the renderer
// ==========================================================================
//  The solver has no way to tell the inputs are fake. It runs the same
//  code it runs on a Vision Pro. This is the whole point of the demo:
//  Apple built the switch that lets this work, and shipped it inside
//  the framework.
// ==========================================================================
//  All offsets below are verified against build 23N301 of CoreIK —
//  the version bundled in the visionOS 26.2 simulator runtime.
//  Of course parts of this could be wrong, note most development was done against canonical 26.2 runtime. 
// 

import Foundation
import simd
import MachO
import os.log

private let logger = Logger(subsystem: "RottenOS.HelloVisionOS", category: "CoreIK")

// Joint output type

struct JointTransform {
    var name: String
    var localPosition: SIMD3<Float>
    var localRotation: SIMD4<Float>
    var worldPosition: SIMD3<Float>
    var confidence: Float
}

// Parent hierarchy (91 joints, Apple Persona skeleton)

let kPersonaParent: [Int] = [
    -1,  // 0  root
     0,  // 1  hips_joint
     1,  // 2  left_upLeg_joint
     2,  // 3  left_leg_joint
     3,  // 4  left_foot_joint
     4,  // 5  left_toes_joint
     5,  // 6  left_toesEnd_joint
     1,  // 7  right_upLeg_joint
     7,  // 8  right_leg_joint
     8,  // 9  right_foot_joint
     9,  // 10 right_toes_joint
    10,  // 11 right_toesEnd_joint
     1,  // 12 spine_1_joint
    12,  // 13 spine_2_joint
    13,  // 14 spine_3_joint
    14,  // 15 spine_4_joint
    15,  // 16 spine_5_joint
    16,  // 17 spine_6_joint
    17,  // 18 spine_7_joint
    18,  // 19 neck_1_joint
    19,  // 20 neck_2_joint
    20,  // 21 neck_3_joint
    21,  // 22 neck_4_joint
    22,  // 23 head_joint
    23,  // 24 nose_joint
    23,  // 25 jaw_joint
    23,  // 26 chin_joint
    23,  // 27 left_eye_joint
    27,  // 28 left_eyeball_joint
    27,  // 29 left_eyeUpperLid_joint
    27,  // 30 left_eyeLowerLid_joint
    23,  // 31 right_eye_joint
    31,  // 32 right_eyeball_joint
    31,  // 33 right_eyeUpperLid_joint
    31,  // 34 right_eyeLowerLid_joint
    18,  // 35 left_shoulder_1_joint
    35,  // 36 left_arm_joint
    36,  // 37 left_forearm_joint
    37,  // 38 left_hand_joint
    38,  // 39 left_handPinkyStart
    39,  // 40 left_handPinky_1
    40,  // 41 left_handPinky_2
    41,  // 42 left_handPinky_3
    42,  // 43 left_handPinkyEnd
    38,  // 44 left_handRingStart
    44,  // 45 left_handRing_1
    45,  // 46 left_handRing_2
    46,  // 47 left_handRing_3
    47,  // 48 left_handRingEnd
    38,  // 49 left_handMidStart
    49,  // 50 left_handMid_1
    50,  // 51 left_handMid_2
    51,  // 52 left_handMid_3
    52,  // 53 left_handMidEnd
    38,  // 54 left_handIndexStart
    54,  // 55 left_handIndex_1
    55,  // 56 left_handIndex_2
    56,  // 57 left_handIndex_3
    57,  // 58 left_handIndexEnd
    38,  // 59 left_handThumbStart
    59,  // 60 left_handThumb_1
    60,  // 61 left_handThumb_2
    61,  // 62 left_handThumbEnd
    18,  // 63 right_shoulder_1_joint
    63,  // 64 right_arm_joint
    64,  // 65 right_forearm_joint
    65,  // 66 right_hand_joint
    66,  // 67 right_handPinkyStart
    67,  // 68 right_handPinky_1
    68,  // 69 right_handPinky_2
    69,  // 70 right_handPinky_3
    70,  // 71 right_handPinkyEnd
    66,  // 72 right_handRingStart
    72,  // 73 right_handRing_1
    73,  // 74 right_handRing_2
    74,  // 75 right_handRing_3
    75,  // 76 right_handRingEnd
    66,  // 77 right_handMidStart
    77,  // 78 right_handMid_1
    78,  // 79 right_handMid_2
    79,  // 80 right_handMid_3
    80,  // 81 right_handMidEnd
    66,  // 82 right_handIndexStart
    82,  // 83 right_handIndex_1
    83,  // 84 right_handIndex_2
    84,  // 85 right_handIndex_3
    85,  // 86 right_handIndexEnd
    66,  // 87 right_handThumbStart
    87,  // 88 right_handThumb_1
    88,  // 89 right_handThumb_2
    89,  // 90 right_handThumbEnd
]

// Per-build nm offsets
//
// CoreIK's private C entry points are not in the framework's exports trie, so
// dlsym() returns NULL. We instead take the byte offset that `nm --demangle`
// reports for each symbol, add the runtime ASLR slide, and cast the result to
// a C function pointer.
//
// Offsets are specific to the exact CoreIK binary — every new simulator build
// gets its own entry. `setup.sh` records which build was staged at
// /tmp/CoreIK.framework/BUILD; at load time we read that file and pick the
// matching entry here. To add a new build:
//   1. ./deploy/setup.sh --version <new-os-version>
//   2. ./deploy/derive_offsets.sh /tmp/CoreIK.framework/CoreIK
//   3. Paste the Swift block it prints here, inside kCoreIKOffsets.

private struct CoreIKOffsets {
    let CreateSolver:   UInt
    let EnableReplay:   UInt
    let SetVioPose:     UInt
    let SrcGroupSize:   UInt
    let SrcGroupName:   UInt
    let SetSourceV2:    UInt
    let SolveSources:   UInt
    let GetPoseSize:    UInt
    let GetJointName:   UInt
    let Destroy:        UInt
}

private let kCoreIKOffsets: [String: CoreIKOffsets] = [

    // visionOS 26.2 — the build this demo was originally verified against
    "23N301": CoreIKOffsets(
        CreateSolver:  0x0009ac80,
        EnableReplay:  0x0009ae94,
        SetVioPose:    0x0009b144,
        SrcGroupSize:  0x0009b174,
        SrcGroupName:  0x0009b1bc,
        SetSourceV2:   0x0009b2ac,
        SolveSources:  0x0009b3fc,
        GetPoseSize:   0x0009ae14,
        GetJointName:  0x0009ae1c,
        Destroy:       0x0009b05c
    ),

    // visionOS 26.4.1 — verified 2026-04-21. All 10 symbols present and
    // callable; offsets shift by ~3.6 KB vs 23N301 but no renames.
    "23O249a": CoreIKOffsets(
        CreateSolver:  0x00099e94,
        EnableReplay:  0x0009a0a4,
        SetVioPose:    0x0009a354,
        SrcGroupSize:  0x0009a384,
        SrcGroupName:  0x0009a3cc,
        SetSourceV2:   0x0009a4bc,
        SolveSources:  0x0009a60c,
        GetPoseSize:   0x0009a024,
        GetJointName:  0x0009a02c,
        Destroy:       0x0009a26c
    ),

    "23O470": CoreIKOffsets(  // visionOS 26.5 — offsets identical to 23O249a (26.4.1)
        CreateSolver:  0x00099e94,
        EnableReplay:  0x0009a0a4,
        SetVioPose:    0x0009a354,
        SrcGroupSize:  0x0009a384,
        SrcGroupName:  0x0009a3cc,
        SetSourceV2:   0x0009a4bc,
        SolveSources:  0x0009a60c,
        GetPoseSize:   0x0009a024,
        GetJointName:  0x0009a02c,
        Destroy:       0x0009a26c
    ),

    // Add more builds here after running deploy/derive_offsets.sh against
    // /tmp/CoreIK.framework/CoreIK for each newly-installed simulator.
    // VisionOS 27 will totally have working Body Animation and Tracking for sure...

]

/// Read the build tag written by deploy/setup.sh. Returns nil if the file
/// isn't present — that means setup wasn't run and we have no idea which
/// binary we're about to talk to.
private func readStagedBuildTag() -> String? {
    let path = "/tmp/CoreIK.framework/BUILD"
    guard let data = try? String(contentsOfFile: path, encoding: .utf8) else { return nil }
    let t = data.trimmingCharacters(in: .whitespacesAndNewlines)
    return t.isEmpty ? nil : t
}

// FIKTransform layout (32 bytes)

private struct FIKTransformRaw {
    var tx, ty, tz, tp: Float  // translation first (confirmed by output analysis)
    var rx, ry, rz, rw: Float  // rotation quaternion second
}

// The actual CoreIKBridge class 

@MainActor
@Observable
class CoreIKBridge {

    var isLoaded = false
    var statusMessage = "Not loaded"
    var jointCount = 0

    private var handle: UnsafeMutableRawPointer? = nil
    private var solver: UnsafeMutableRawPointer? = nil
    private var slide: Int = 0
    private var timestamp: Double = 1.0

    // C function pointer typedefs.
    // Swift finds a way to make loading C PFN's ugly af.
    //
    // Each flat C entry point below is a thin wrapper over a demangled C++ method
    // in the CoreIK binary — the unstripped symbol table shows both, e.g.:
    //   000000000009a60c T _CoreIKSolverFusionSolveSources                         (the C stub we call)
    //   00000000000a8174 t CoreIKSolverFusion::solveSources(                       (the C++ it wraps)
    //                          FIK::Fusion::OutputFormat,
    //                          FIK::IKArrayView<FIK::Transform>,
    //                          FIK::IKArrayView<float>)
    // (offsets shown for build 23O470; they shift per build, the mapping does not.)
    //
    // Sanity Check / Reminder: Three RE notes the C++ signatures clear up:
    //   - arg 0 (the solver pointer) is the C++ `this`. These flat C names are
    //     extern "C" shims over non-static member functions, so every call passes
    //     the solver as the implicit receiver — that's why EnableReplayMode, which
    //     "returns void", still takes the solver. (The two Get* readers are the
    //     exception: static table lookups, no solver arg.)
    //   - SolveSources' 2nd arg is an OutputFormat enum (1 = local-space), NOT a
    //     "solver mode". We pass 1.
    //   - There is also a `_CoreIKSolverFusionSolve` that looks right but writes
    //     nothing (routes to solveWithTime). SolveSources is the one that fills
    //     the output buffer. See README "Finding the Functions" for the full table.

    private typealias CreateFn       = @convention(c) (Int32, UnsafeMutablePointer<Float>, UnsafeMutablePointer<UInt8>, Int) -> UnsafeMutableRawPointer?
    private typealias EnableReplayFn = @convention(c) (UnsafeMutableRawPointer) -> Void
    private typealias SetVioPoseFn   = @convention(c) (UnsafeMutableRawPointer, Float, Float, Float, Float, Float, Float, Float, Float) -> Void
    private typealias GroupSizeFn    = @convention(c) (UnsafeMutableRawPointer, Int32) -> Int32
    private typealias GroupNameFn    = @convention(c) (UnsafeMutableRawPointer, Int32) -> UnsafePointer<CChar>?
    private typealias SetSourceFn    = @convention(c) (UnsafeMutableRawPointer, Int32, UnsafeMutableRawPointer, UnsafeMutableRawPointer, Int32, Float, Double) -> Void
    private typealias SolveFn        = @convention(c) (UnsafeMutableRawPointer, Int32, UnsafeMutableRawPointer, UnsafeMutableRawPointer, Int32) -> Double
    private typealias GetSizeFn      = @convention(c) () -> Int32
    private typealias GetNameFn      = @convention(c) (UnsafeMutableRawPointer, Int32) -> UnsafePointer<CChar>?
    private typealias DestroyFn      = @convention(c) (UnsafeMutableRawPointer) -> Void

    private var fnCreate:        CreateFn?
    private var fnEnableReplay:  EnableReplayFn?
    private var fnSetVio:        SetVioPoseFn?
    private var fnSrcGroupSize:  GroupSizeFn?
    private var fnSrcGroupName:  GroupNameFn?
    private var fnSetSrc:        SetSourceFn?
    private var fnSolve:         SolveFn?
    private var fnGetName:       GetNameFn?
    private var fnDestroy:       DestroyFn?
    private var didLogSolveTiming = false

    // ============ Load CoreIK ============

    func load() {
        // Dump from notes: Path priority:
        // 1. /tmp/CoreIK.framework/CoreIK — space-free, staged by deploy/setup.sh.
        //    This is the canonical path. Using /tmp avoids CoreIK's internal
        //    CFURLCopyPath bug where the space in "xrOS 26.2.simruntime" gets
        //    URL-encoded to "xrOS%2026.2.simruntime" before std::ifstream::open
        //    receives it, causing silent rig-load failures. 
        //.   Note that this is expanded to ther simulator images. 
        // 2. /System/Library/PrivateFrameworks — mapped into the sim's VFS at
        //    runtime; works as a fallback inside sim processes.
        // (We deliberately don't hunt for the runtime on the host — setup.sh
        // is the one step users are expected to run once per reboot, and it
        // also writes the BUILD tag we need below for offset lookup.)
        let tmpPath = "/tmp/CoreIK.framework/CoreIK"
        let simInternalPath = "/System/Library/PrivateFrameworks/CoreIK.framework/CoreIK"

        let simPath: String
        if FileManager.default.fileExists(atPath: tmpPath) {
            simPath = tmpPath
        } else if FileManager.default.fileExists(atPath: simInternalPath) {
            simPath = simInternalPath
        } else {
            statusMessage = "CoreIK not found. Run deploy/setup.sh first."
            logger.error("neither /tmp nor sim-internal CoreIK path exists — setup.sh not run?")
            return
        }

        // Figure out which build we're about to call into, and pick matching
        // offsets from the per-build table. If setup.sh wrote a BUILD file,
        // use it; otherwise fall back to assuming the historical 23N301.
        let buildTag = readStagedBuildTag() ?? "23N301"
        guard let offsets = kCoreIKOffsets[buildTag] else {
            statusMessage = "Unknown CoreIK build: \(buildTag). "
                          + "Run ./deploy/derive_offsets.sh to add it."
            logger.error("no offsets for build \(buildTag) — regenerate with derive_offsets.sh")
            return
        }
        logger.info("CoreIK build: \(buildTag) (offsets found)")

        // DIAGNOSTIC: scan dyld image list BEFORE dlopen to detect pre-loading
        // If CoreIK is already present, CF already has com.apple.CoreIK registered
        // at whatever path dyld used (likely the space-containing runtime path).
        let imageCountBefore = _dyld_image_count()
        var preDlopenCoreIKPath: String? = nil
        for i in 0..<imageCountBefore {
            if let raw = _dyld_get_image_name(i) {
                let n = String(cString: raw)
                if n.contains("/CoreIK") && !n.contains("/CoreIKDebugging") && !n.contains("/CoreIKDemo") {
                    preDlopenCoreIKPath = n
                    break
                }
            }
        }
        let preDlopenDiag = preDlopenCoreIKPath != nil
            ? "CoreIK PRE-LOADED before dlopen at: \(preDlopenCoreIKPath!)"
            : "CoreIK NOT in dyld image list before dlopen (our load will be first)"
        logger.info("\(preDlopenDiag)")
        let docsURLPre = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
        if let url = docsURLPre?.appendingPathComponent("pre_dlopen_diag.txt") {
            try? preDlopenDiag.write(to: url, atomically: true, encoding: .utf8)
        }

        logger.info("CoreIKBridge.load() — path: \(simPath)")
        guard let h = dlopen(simPath, RTLD_LAZY | RTLD_GLOBAL) else {
            let err = String(cString: dlerror())
            statusMessage = "dlopen failed: \(err)"
            logger.error("dlopen failed: \(err)")
            return
        }
        handle = h
        logger.info("dlopen OK — path used: \(simPath)")

        // Find CoreIK base address by scanning dyld image list.
        // Use _dyld_get_image_header() to get the actual mach_header pointer — this IS
        // the real load address. nm offsets are relative to the binary's __TEXT vmaddr (0
        // for PIC simulator frameworks). The actual function address = header_ptr + nm_offset.
        // We store the base as slide (Int) so the sym() helper still works.
        let n = _dyld_image_count()
        var foundSlide = false
        for i in 0..<n {
            guard let rawName = _dyld_get_image_name(i) else { continue }
            let name = String(cString: rawName)
            // Match /CoreIK at a word boundary (not /CoreIKDebugging, not /CoreIKDemo etc.)
            if let range = name.range(of: "/CoreIK") {
                let afterIdx = range.upperBound
                let isWordBoundary = afterIdx == name.endIndex ||
                    name[afterIdx] == "." || name[afterIdx] == "_"
                if isWordBoundary {
                    // _dyld_get_image_header returns the actual mach_header pointer in memory.
                    // For PIC frameworks (__TEXT vmaddr == 0), this pointer IS the base.
                    // Reported slide can be 0 inside simulator app processes, so derive base
                    // directly from the header pointer instead.
                    if let hdr = _dyld_get_image_header(i) {
                        let vmaddrSlide = _dyld_get_image_vmaddr_slide(i)
                        let headerBase = UInt(bitPattern: hdr)
                        // If slide is non-zero, use header base; else also use header base
                        // (both should give the same result for PIC binaries).
                        // slide stored as Int for bitPattern cast in sym()
                        slide = Int(bitPattern: headerBase)
                        logger.info("CoreIK found: name=\(name), headerBase=0x\(String(headerBase, radix:16)), vmaddrSlide=0x\(String(UInt(bitPattern: vmaddrSlide), radix:16))")
                        foundSlide = true
                    }
                    break
                }
            }
        }

        if !foundSlide {
            statusMessage = "CoreIK not found in dyld image list"
            logger.error("CoreIK slide not found in dyld image list")
            return
        }
        logger.info("Using base 0x\(String(UInt(bitPattern: self.slide), radix: 16)) for function resolution")
        CoreIKEventLog.shared.setBase(UInt(bitPattern: slide), path: simPath)

        // Resolve function pointers via nm offset + slide
        func sym<T>(_ off: UInt) -> T {
            let addr = off &+ UInt(bitPattern: slide)
            guard let ptr = UnsafeMutableRawPointer(bitPattern: addr) else {
                // addr resolves to 0 only if the offset table is wrong for this build.
                // Fail loudly naming the offending offset, rather than tripping a
                // cryptic force-unwrap trap with no context.
                fatalError("CoreIK symbol resolved to a null address (offset 0x\(String(off, radix: 16)) + slide). Offset table is wrong for this build — re-run deploy/derive_offsets.sh.")
            }
            return unsafeBitCast(ptr, to: T.self)
        }

        fnCreate        = sym(offsets.CreateSolver)
        fnEnableReplay  = sym(offsets.EnableReplay)
        fnSetVio        = sym(offsets.SetVioPose)
        fnSrcGroupSize  = sym(offsets.SrcGroupSize)
        fnSrcGroupName  = sym(offsets.SrcGroupName)
        fnSetSrc        = sym(offsets.SetSourceV2)
        fnSolve         = sym(offsets.SolveSources)
        fnGetName       = sym(offsets.GetJointName)
        fnDestroy       = sym(offsets.Destroy)

        // Query joint count (static function, no solver arg)
        let getSizeFn: GetSizeFn = sym(offsets.GetPoseSize)
        jointCount = Int(getSizeFn())

        // Create solver with default bone lengths
        var boneLengths = [Float](repeating: 0.1, count: jointCount)
        boneLengths[2] = 0.42; boneLengths[7] = 0.42   // thighs
        boneLengths[3] = 0.42; boneLengths[8] = 0.42   // shins
        boneLengths[36] = 0.28; boneLengths[64] = 0.28  // upper arms
        boneLengths[37] = 0.25; boneLengths[65] = 0.25  // forearms

        var enrollStates = [UInt8](repeating: 0, count: jointCount)

        solver = boneLengths.withUnsafeMutableBufferPointer { blPtr in
            enrollStates.withUnsafeMutableBufferPointer { esPtr in
                fnCreate?(0, blPtr.baseAddress!, esPtr.baseAddress!, jointCount)
            }
        }

        CoreIKEventLog.shared.logCreate(jointCount: jointCount, result: solver)
        if let s = solver {
            // Switch solver to replay mode — accepts synthetic inputs directly
            // instead of waiting for hardware data from arkitd via XPC.
            // Without this call the solver ignores setWristPose/setHeadPose
            // and holds the default T-pose regardless of our inputs.
            fnEnableReplay?(s)
            logger.info("enableReplayMode called — solver will accept synthetic inputs")
            CoreIKEventLog.shared.logIntercept(
                original: "_CoreIKSolverFusionEnableReplayMode(solver)",
                patched:  "→ solver now accepts synthetic inputs without hardware"
            )
        }
        if solver != nil {
            isLoaded = true
            statusMessage = "Solver active (replay mode) | Joints: \(jointCount) | slide: \(String(format: "0x%x", slide))"
            logger.info("Solver created OK — joints: \(self.jointCount), slide: 0x\(String(self.slide, radix: 16))")
        } else {
            statusMessage = "Solver creation failed — .ikn rig files missing?"
            logger.error("Solver creation FAILED — .ikn files not found by CFBundle")
        }

        // Write status to Documents dir for host-side verification
        let diagMsg = """
            CoreIKBridge.load() result
            status: \(statusMessage)
            isLoaded: \(isLoaded)
            jointCount: \(jointCount)
            slide: 0x\(String(format: "%x", slide))
            pathUsed: \(simPath)
            """
        let docsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
        if let url = docsURL?.appendingPathComponent("coreik_status.txt") {
            try? diagMsg.write(to: url, atomically: true, encoding: .utf8)
            logger.info("Status written to \(url.path)")
        }
        // Also try /tmp directly (shared with host on macOS simulator)
        try? diagMsg.write(toFile: "/tmp/coreik_bridge_status.txt", atomically: true, encoding: .utf8)
    }

    // Pose injection

    func setHeadPose(x: Float, y: Float, z: Float) {
        guard let s = solver, let fn = fnSetVio else { return }
        // C sig: (solver, rot.x, rot.y, rot.z, rot.w, tx, ty, tz, pad)
        fn(s, 0, 0, 0, 1,  x, y, z, 1)
        CoreIKEventLog.shared.logSetVioPose(qx: 0, qy: 0, qz: 0, qw: 1, tx: x, ty: y, tz: z)
    }

    func setWristPose(isLeft: Bool, x: Float, y: Float, z: Float) {
        guard let s = solver, let fn = fnSetSrc else { return }
        let group: Int32 = isLeft ? 2 : 3
        // poseSize for groups 2/3 = 27 (full 27-joint hand model).
        // Passing count=1 is silently ignored. Fill all 27 slots with the
        // wrist world position; the solver extracts the wrist anchor from this array.
        var xfms = [FIKTransformRaw](
            repeating: FIKTransformRaw(tx: x, ty: y, tz: z, tp: 1,
                                        rx: 0, ry: 0, rz: 0, rw: 1),
            count: 27)
        var confs = [Float](repeating: 1.0, count: 27)
        timestamp += 0.016
        xfms.withUnsafeMutableBufferPointer { xPtr in
            confs.withUnsafeMutableBufferPointer { cPtr in
                fn(s, group,
                   UnsafeMutableRawPointer(xPtr.baseAddress!),
                   UnsafeMutableRawPointer(cPtr.baseAddress!),
                   27, 1.0, timestamp)
            }
        }
        CoreIKEventLog.shared.logSetSource(group: group, tx: x, ty: y, tz: z, conf: 1.0)
    }

    // The fun stuff: Solve 

    func solve() -> [JointTransform] {
        guard let s = solver, let fn = fnSolve, let nameFn = fnGetName else { return [] }

        var outputBuf = [FIKTransformRaw](
            repeating: FIKTransformRaw(tx:0,ty:0,tz:0,tp:0,rx:0,ry:0,rz:0,rw:0),
            count: jointCount)
        var confBuf = [Float](repeating: 0, count: jointCount)

        let solveStart = ProcessInfo.processInfo.systemUptime
        // `quality` is CoreIK's tracking-confidence metric, NOT a "did it solve" flag.
        // With synthetic slider inputs there's no real sensor groundtruth, so it reads
        // ~0.0 by design. Don't treat 0.0 as failure: the output buffer is still fully
        // written and the skeleton responds to input (that's the part that matters).
        let quality = outputBuf.withUnsafeMutableBufferPointer { oPtr in
            confBuf.withUnsafeMutableBufferPointer { cPtr in
                fn(s, 1,
                   UnsafeMutableRawPointer(oPtr.baseAddress!),
                   UnsafeMutableRawPointer(cPtr.baseAddress!),
                   Int32(jointCount))
            }
        }
        // One-time timing receipt: log the measured solve rate so the "solves/second"
        // figure is something you can check in the log, not a number asserted in prose.
        if !didLogSolveTiming {
            didLogSolveTiming = true
            let secs = ProcessInfo.processInfo.systemUptime - solveStart
            let perSec = secs > 0 ? Int(1.0 / secs) : 0
            logger.info("solve timing: \(String(format: "%.3f", secs * 1000)) ms/solve (~\(perSec) solves/sec)")
        }
        CoreIKEventLog.shared.logSolve(quality: quality, jointCount: jointCount)

        // Proper quaternion FK: rotate local translation by accumulated parent world rotation
        var worldX   = [Float](repeating: 0, count: jointCount)
        var worldY   = [Float](repeating: 0, count: jointCount)
        var worldZ   = [Float](repeating: 0, count: jointCount)
        var worldRot = [simd_quatf](repeating: simd_quatf(ix:0,iy:0,iz:0,r:1), count: jointCount)

        for i in 0..<jointCount {
            let p = kPersonaParent[i]
            let lq = simd_quatf(ix: outputBuf[i].rx, iy: outputBuf[i].ry,
                                iz: outputBuf[i].rz, r:  outputBuf[i].rw)
            if p < 0 {
                worldX[i]   = outputBuf[i].tx
                worldY[i]   = outputBuf[i].ty
                worldZ[i]   = outputBuf[i].tz
                worldRot[i] = lq
            } else {
                let localTrans = SIMD3<Float>(outputBuf[i].tx, outputBuf[i].ty, outputBuf[i].tz)
                let rotated    = worldRot[p].act(localTrans)
                worldX[i]   = worldX[p] + rotated.x
                worldY[i]   = worldY[p] + rotated.y
                worldZ[i]   = worldZ[p] + rotated.z
                worldRot[i] = (worldRot[p] * lq).normalized
            }
        }

        return (0..<jointCount).map { i in
            let name = nameFn(s, Int32(i)).map { String(cString: $0) } ?? "joint_\(i)"
            return JointTransform(
                name: name,
                localPosition: SIMD3(outputBuf[i].tx, outputBuf[i].ty, outputBuf[i].tz),
                localRotation: SIMD4(outputBuf[i].rx, outputBuf[i].ry, outputBuf[i].rz, outputBuf[i].rw),
                worldPosition: SIMD3(worldX[i], worldY[i], worldZ[i]),
                confidence: confBuf[i]
            )
        }
    }

    // Cleanup

    func unload() {
        if let s = solver { fnDestroy?(s) }
        CoreIKEventLog.shared.logDestroy()
        solver = nil
        if let h = handle { dlclose(h) }
        handle = nil
        isLoaded = false
        statusMessage = "Unloaded"
    }
}
