//
//  RunnerCharacterNode.swift
//  DashSmash
//
//  Optional animated stick-figure player. The collision bounds are still
//  handled by the parent `playerNode` rect — this node is purely visual.
//
//  States:
//    .running    — leg/arm cycle synced to the beat, slight torso bob.
//    .crouching  — brief pre-jump compress as the legs load.
//    .airborne   — arms windmill clockwise; legs tuck up under the body.
//    .landing    — knees bend on impact, then ease back into the run.
//
//  Transitions are driven by `update(dt:onGround:vy:secondsPerBeat:)` so the
//  scene doesn't have to know anything about animation timing — it just
//  reports physics state each frame.
//

import SpriteKit
import UIKit

final class RunnerCharacterNode: SKNode {

    private enum State {
        case running
        case crouching      // pre-jump (brief)
        case airborne
        case landing        // knee-bend on impact (brief)
    }

    // Body sized to fit roughly inside a 44×44 player rect. Origins are at
    // the joint (hip/shoulder) so rotating each limb pivots correctly.
    private let head = SKShapeNode(circleOfRadius: 5)
    private let torsoNode = SKNode()
    private let torso = SKShapeNode(rectOf: CGSize(width: 8, height: 14), cornerRadius: 2)
    private let leftArm = SKShapeNode()
    private let rightArm = SKShapeNode()
    private let leftLeg = SKShapeNode()
    private let rightLeg = SKShapeNode()

    // Vertical pivot offsets relative to playerNode center (collision rect
    // centered at (0,0), size 44×44 so floor edge is at -22).
    private let hipY: CGFloat = -2
    private let shoulderY: CGFloat = 8
    private let headY: CGFloat = 16

    private var state: State = .running
    private var stateElapsed: TimeInterval = 0
    private var runPhase: Double = 0
    private var airSpin: CGFloat = 0
    private var lastOnGround: Bool = true

    init(tintColor: UIColor) {
        super.init()
        buildLimbs(tint: tintColor)
        applyRunningPose()
    }

    required init?(coder: NSCoder) { fatalError() }

    private func buildLimbs(tint: UIColor) {
        head.fillColor = .white
        head.strokeColor = tint
        head.lineWidth = 1.5
        head.position = CGPoint(x: 0, y: headY)
        addChild(head)

        torso.fillColor = .white
        torso.strokeColor = tint
        torso.lineWidth = 1.5
        torsoNode.position = CGPoint(x: 0, y: (shoulderY + hipY) / 2)
        torsoNode.addChild(torso)
        addChild(torsoNode)

        configureLimb(leftArm, length: 10, color: tint)
        leftArm.position = CGPoint(x: -3, y: shoulderY)
        addChild(leftArm)

        configureLimb(rightArm, length: 10, color: tint)
        rightArm.position = CGPoint(x: 3, y: shoulderY)
        addChild(rightArm)

        configureLimb(leftLeg, length: 12, color: tint)
        leftLeg.position = CGPoint(x: -3, y: hipY)
        addChild(leftLeg)

        configureLimb(rightLeg, length: 12, color: tint)
        rightLeg.position = CGPoint(x: 3, y: hipY)
        addChild(rightLeg)
    }

    /// Limbs render as line segments anchored at (0,0) extending down −length.
    /// Rotating zRotation swings the limb around its joint.
    private func configureLimb(_ node: SKShapeNode, length: CGFloat, color: UIColor) {
        let path = CGMutablePath()
        path.move(to: .zero)
        path.addLine(to: CGPoint(x: 0, y: -length))
        node.path = path
        node.strokeColor = color
        node.lineWidth = 3
        node.lineCap = .round
    }

    // MARK: - Frame update

    func update(dt: TimeInterval, onGround: Bool, vy: CGFloat, secondsPerBeat: Double) {
        stateElapsed += dt

        // Detect ground-state transitions to switch into crouching/landing.
        if lastOnGround && !onGround && state != .crouching {
            transition(to: .crouching)
        } else if !lastOnGround && onGround {
            transition(to: .landing)
        }
        lastOnGround = onGround

        switch state {
        case .crouching:
            if stateElapsed > 0.08 || !onGround {
                transition(to: .airborne)
            } else {
                applyCrouchPose(progress: stateElapsed / 0.08)
            }
        case .airborne:
            airSpin += CGFloat(dt) * .pi * 4   // ~2 full rotations / sec
            applyAirbornePose(spin: airSpin, vy: vy)
        case .landing:
            if stateElapsed > 0.18 {
                transition(to: .running)
            } else {
                applyLandingPose(progress: stateElapsed / 0.18)
            }
        case .running:
            // Stride period: one full cycle per beat keeps the run visually
            // locked to the music. Clamp for very slow / very fast tempos.
            let cycle = max(0.35, min(0.75, secondsPerBeat))
            runPhase += dt / cycle
            applyRunningPose()
        }
    }

    private func transition(to next: State) {
        state = next
        stateElapsed = 0
        if next != .airborne { airSpin = 0 }
    }

    // MARK: - Poses

    private func applyRunningPose() {
        // Legs alternate forward/back; arms swing opposite. Slight torso
        // bob in sync with the stride for visual weight.
        let theta = CGFloat(runPhase * .pi * 2)
        let swing: CGFloat = .pi / 4    // ±45° at the joint
        leftLeg.zRotation = sin(theta) * swing
        rightLeg.zRotation = -sin(theta) * swing
        leftArm.zRotation = -sin(theta) * swing * 0.8
        rightArm.zRotation = sin(theta) * swing * 0.8
        let bob = abs(sin(theta * 2)) * 1.2
        torsoNode.position.y = (shoulderY + hipY) / 2 - bob
        head.position.y = headY - bob
        // Reset crouch scale
        torsoNode.yScale = 1
    }

    private func applyCrouchPose(progress: Double) {
        // Knees fold in to load the jump; torso lowers a touch.
        let p = CGFloat(max(0, min(1, progress)))
        leftLeg.zRotation = p * .pi / 5
        rightLeg.zRotation = -p * .pi / 5
        leftArm.zRotation = -.pi / 8
        rightArm.zRotation = .pi / 8
        torsoNode.yScale = 1 - p * 0.18
        torsoNode.position.y = (shoulderY + hipY) / 2 - p * 4
        head.position.y = headY - p * 4
    }

    private func applyAirbornePose(spin: CGFloat, vy: CGFloat) {
        // Arms windmill clockwise — both rotate together, offset 180° so it
        // looks like alternating sweeps.
        leftArm.zRotation = -spin
        rightArm.zRotation = -spin + .pi
        // Legs tuck up; bend more at the peak (low |vy|) than at takeoff.
        let tuck = CGFloat(0.5)
        leftLeg.zRotation = -.pi / 6 + tuck * 0.4
        rightLeg.zRotation = .pi / 6 - tuck * 0.4
        torsoNode.yScale = 1
        torsoNode.position.y = (shoulderY + hipY) / 2
        head.position.y = headY
        _ = vy  // currently unused — reserved for a stretch-on-rise pose
    }

    private func applyLandingPose(progress: Double) {
        // Knees absorb the impact: deep bend at p=0, easing to neutral at p=1.
        let p = CGFloat(max(0, min(1, progress)))
        let bend = (1 - p) * .pi / 3.5
        leftLeg.zRotation = bend
        rightLeg.zRotation = -bend
        leftArm.zRotation = (1 - p) * .pi / 6
        rightArm.zRotation = -(1 - p) * .pi / 6
        torsoNode.yScale = 1 - (1 - p) * 0.22
        torsoNode.position.y = (shoulderY + hipY) / 2 - (1 - p) * 5
        head.position.y = headY - (1 - p) * 5
    }
}
