//
// ExerciseMotion.swift
// Workouts
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//

import Foundation

/// Codable mirror of the Exercise Library's anatomical 3D rig data (see
/// `Exercise Library/SYSTEM.md`).
///
/// A **skeleton profile** is a table of bone lengths (plus real shoulder/pelvis widths
/// and feet); a **motion script** is key frames of anatomical joint angles measured in
/// degrees from the neutral standing pose (flexion forward, abduction away from the
/// midline, rotation external — spine/neck rotation is turn-right positive), a pelvis
/// `root` anchor with trunk orientation, an orthographic camera yaw, optional IK pins
/// for planted hands/feet, and hold/tween timings. The app bundles verbatim copies
/// exported by `render.py --export` into `Resources/ExerciseMotions/` — `skeleton.json`
/// plus one `<Exercise Name>.motion.json` per library entry.

/// The figure build the exercise animation renders. A presentation-only choice of
/// skeleton profile (bone proportions) — the motion scripts are identical across builds,
/// so switching never touches an exercise's animation, only its dimensions. The raw value
/// is the `skeleton.json` profile key; persisted via `@AppStorage("figureProfile")`.
enum FigureProfile: String, CaseIterable {
    case neutral
    case feminine = "female"
    case masculine = "male"

    var displayName: String {
        switch self {
        case .neutral: "Neutral"
        case .feminine: "Feminine"
        case .masculine: "Masculine"
        }
    }
}

/// Bone lengths for one figure profile (`neutral`, `female`, or `male`; the app picks
/// one per the `figureProfile` setting — see `FigureProfile`).
struct SkeletonProfile: Codable {
    let headR: Double
    let neck: Double
    let spine1: Double
    let spine2: Double
    let upperArm: Double
    let foreArm: Double
    let thigh: Double
    let shin: Double
    let foot: Double
    /// Half the shoulder / pelvis width — the lateral offset of each limb's attachment
    /// from the spine, giving the figure real depth face-on.
    let shoulderHalf: Double
    let hipHalf: Double
    /// Profile-view readability nudge `[dx, dy]` for the far member of a limb pair,
    /// scaled by how side-on the view is (vanishes face-on).
    let farOffset: [Double]
}

/// The skeleton file: named profiles plus per-joint ROM (unused by the renderer).
struct Skeleton: Codable {
    let profiles: [String: SkeletonProfile]
}

/// A single joint value: a bare number (shorthand for `{"flexion": n}`) or a dict of
/// named degrees of freedom. Ball joints read `flexion`/`abduction`/`rotation`, spine
/// segments `flexion`/`lateral`/`rotation`, hinges only `flexion`; each accessor
/// returns 0 for a DoF the value omits (matching the reference's `_full`).
enum JointValue: Codable {
    case scalar(Double)
    case object(flexion: Double?, abduction: Double?, rotation: Double?, lateral: Double?)

    private enum CodingKeys: String, CodingKey { case flexion, abduction, rotation, lateral }

    init(from decoder: Decoder) throws {
        if let single = try? decoder.singleValueContainer(), let value = try? single.decode(Double.self) {
            self = .scalar(value)
            return
        }
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self = .object(flexion: try container.decodeIfPresent(Double.self, forKey: .flexion),
                       abduction: try container.decodeIfPresent(Double.self, forKey: .abduction),
                       rotation: try container.decodeIfPresent(Double.self, forKey: .rotation),
                       lateral: try container.decodeIfPresent(Double.self, forKey: .lateral))
    }

    func encode(to encoder: Encoder) throws {
        switch self {
        case .scalar(let value):
            var container = encoder.singleValueContainer()
            try container.encode(value)
        case .object(let flexion, let abduction, let rotation, let lateral):
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encodeIfPresent(flexion, forKey: .flexion)
            try container.encodeIfPresent(abduction, forKey: .abduction)
            try container.encodeIfPresent(rotation, forKey: .rotation)
            try container.encodeIfPresent(lateral, forKey: .lateral)
        }
    }

    var flexion: Double {
        switch self {
        case .scalar(let value): value
        case .object(let flexion, _, _, _): flexion ?? 0
        }
    }
    var abduction: Double { if case .object(_, let abduction, _, _) = self { return abduction ?? 0 }; return 0 }
    var rotation: Double { if case .object(_, _, let rotation, _) = self { return rotation ?? 0 }; return 0 }
    var lateral: Double { if case .object(_, _, _, let lateral) = self { return lateral ?? 0 }; return 0 }
}

/// The pelvis anchor plus trunk orientation. `pos` is `[x, y]` in 320×180 canvas
/// coordinates; `yaw`/`pitch`/`roll` are the trunk's facing, forward bow, and side-lean
/// (all optional, degrees).
struct RootValue: Codable {
    let pos: [Double]
    let yaw: Double?
    let pitch: Double?
    let roll: Double?
}

/// The orthographic camera: `yaw` 0 is the classic side view, 90 face-on.
struct MotionCamera: Codable {
    let yaw: Double?
    /// Camera elevation override; nil uses the standard slightly-raised viewpoint.
    let pitch: Double?
    /// Presentation-only zoom about the ground-center anchor (nil = 1). Standing
    /// motions author full-size anatomy — taller than the canvas — and zoom out
    /// to fit; the solver and fixtures stay in full-size canvas units.
    let zoom: Double?
}

/// A prop's joint reference: one joint (`"hand_r"`, `"knee_l"`, `"elbow_r"`, …)
/// or the midpoint of two (`["foot_r", "foot_l"]`, `["knee_r", "foot_r"]`).
/// Extremity keys match the pin keys; elbows/knees are the mid joints.
enum PropJointRef: Codable {
    case single(String)
    case midpoint([String])

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let name = try? container.decode(String.self) {
            self = .single(name)
        } else {
            self = .midpoint(try container.decode([String].self))
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .single(let name): try container.encode(name)
        case .midpoint(let names): try container.encode(names)
        }
    }

    var names: [String] {
        switch self {
        case .single(let name): [name]
        case .midpoint(let names): names
        }
    }
}

/// One static shape of a `scene` prop, authored in canvas coordinates in the
/// authored view, with world-space 3D form for the orbiting presentation.
struct PropSceneShape: Codable {
    let kind: String          // "line" | "circle"
    /// line: polyline points, `[x, y]` or `[x, y, z]`; `w` is the stroke width.
    let pts: [[Double]]?
    let w: Double?
    /// circle: center + radius; `fill` false draws an outline.
    let c: [Double]?
    let r: Double?
    let fill: Bool?
    /// The shape's depth plane (+ toward the camera in the authored view).
    let z: Double?
    /// Extrusion half-width: a line with `depth` is a slab (a seat, a platform)
    /// that opens into a swept quad as the camera orbits.
    let depth: Double?
    /// Optional palette override: `"prop"` for the darker attached-item gray.
    let color: String?
}

/// One equipment prop. `type` selects the flavor; the other fields apply per type
/// (mirroring the reference renderer's `resolve_props`).
struct MotionProp: Codable {
    let type: String          // "scene" | "cable" | "bar" | "dumbbell" | "pad" | "roller"
    /// scene: the static shapes.
    let shapes: [PropSceneShape]?
    /// cable: fixed anchor `[x, y]` or `[x, y, z]` → moving joint `to`.
    let from: [Double]?
    let to: PropJointRef?
    /// bar/dumbbell/pad: the joint(s) the item is centered on.
    let at: PropJointRef?
    /// Fixed world angle (degrees, y-up). Default: bars are horizontal;
    /// dumbbells/pads sit perpendicular to the lower bone.
    let angle: Double?
    /// True 3D axis, superseding `angle`: `"z"` marks a cross-body rod
    /// (barbell, pull-up bar) whose projection foreshortens naturally —
    /// end-on in a profile view, full span face-on.
    let axis: String?
    let halfLen: Double?
    let w: Double?
    /// End-disc radius (dumbbell plates default 4.5; bars none).
    let plateR: Double?
    /// roller: which side of the lower bone the disc presses (+1/−1), its radius,
    /// and how far back along the bone from the joint it sits.
    let side: Double?
    let r: Double?
    let back: Double?
}

/// A key frame of anatomical joint angles. A joint absent from the frame poses at
/// neutral (all zeros); `spine` is `[lower, upper]` segments; a bare number anywhere a
/// `JointValue` appears is its flexion.
struct MotionKeyFrame: Codable {
    /// Seconds held at this key frame (default 0.5).
    let hold: Double?
    /// Seconds animating to the *next* frame (default 0.6); the last frame tweens
    /// back to the first, looping.
    let tween: Double?
    /// Pelvis anchor plus trunk orientation.
    let root: RootValue
    /// The two chained spine segments (pelvis→mid, mid→neck).
    let spine: [JointValue]?
    /// Neck: flexion (+ rotation).
    let neck: JointValue?
    /// Extra gaze pitch layered on the neck (a bare number = flexion).
    let head: JointValue?
    /// IK targets for planted extremities, keyed `hand_r`/`hand_l`/`foot_r`/`foot_l`.
    /// A pin present in two consecutive key frames stays planted through the tween.
    let pins: [String: [Double]]?
    let shoulderR: JointValue?
    let shoulderL: JointValue?
    let elbowR: JointValue?
    let elbowL: JointValue?
    let hipR: JointValue?
    let hipL: JointValue?
    let kneeR: JointValue?
    let kneeL: JointValue?
    let ankleR: JointValue?
    let ankleL: JointValue?

    enum CodingKeys: String, CodingKey {
        case hold, tween, root, spine, neck, head, pins
        case shoulderR = "shoulder_r", shoulderL = "shoulder_l"
        case elbowR = "elbow_r", elbowL = "elbow_l"
        case hipR = "hip_r", hipL = "hip_l"
        case kneeR = "knee_r", kneeL = "knee_l"
        case ankleR = "ankle_r", ankleL = "ankle_l"
    }
}

/// One exercise's motion script: key frames plus the parts drawn in the accent color.
struct ExerciseMotion: Codable {
    let name: String
    /// 1-based frame used for the static visual (unused by the animated renderer).
    let primary: Int?
    /// Orthographic camera. Nil is the side view (`yaw` 0).
    let camera: MotionCamera?
    /// Parts (`arm_r`, `leg_l`, `spine`, …) drawn in the working accent color.
    let working: [String]?
    /// Equipment layer: scene shapes and cables behind the figure, joint-attached
    /// items (bar/dumbbell/pad) over the limbs. See SYSTEM.md "The props layer".
    let props: [MotionProp]?
    let frames: [MotionKeyFrame]
}

/// Finds and decodes the bundled rig resources for an exercise, by exact name match
/// against the exported `<Exercise Name>.motion.json` files.
enum ExerciseMotionLibrary {
    struct Resources {
        let motion: ExerciseMotion
        let profile: SkeletonProfile
    }

    /// Every exercise with a bundled motion script, sorted alphabetically. XcodeGen
    /// flattens resource groups, so `<Exercise Name>.motion.json` files land in the
    /// bundle root alongside `skeleton.json` — enumerate all json and filter on the
    /// compound suffix (which `skeleton.json` doesn't match).
    static let exerciseNames: [String] = {
        let urls = Bundle.main.urls(forResourcesWithExtension: "json", subdirectory: nil) ?? []
        return urls
            .map(\.lastPathComponent)
            .filter { $0.hasSuffix(".motion.json") }
            .map { String($0.dropLast(".motion.json".count)) }
            .sorted()
    }()

    /// The motion script plus the requested skeleton `profile` for `exerciseName`, or
    /// `nil` when no bundled motion matches (most exercises have none — the caller keeps
    /// its space empty). An unknown profile key falls back to `neutral`.
    static func resources(for exerciseName: String, profile: FigureProfile = .neutral) -> Resources? {
        guard
            let motionURL = Bundle.main.url(forResource: exerciseName, withExtension: "motion.json"),
            let skeletonURL = Bundle.main.url(forResource: "skeleton", withExtension: "json"),
            let motionData = try? Data(contentsOf: motionURL),
            let skeletonData = try? Data(contentsOf: skeletonURL),
            let motion = try? JSONDecoder().decode(ExerciseMotion.self, from: motionData),
            let skeleton = try? JSONDecoder().decode(Skeleton.self, from: skeletonData),
            let profile = skeleton.profiles[profile.rawValue] ?? skeleton.profiles[FigureProfile.neutral.rawValue]
        else { return nil }
        return Resources(motion: motion, profile: profile)
    }
}
