--[[
    Helicopter Flight System for FS25
    Copyright (C) Lambo Mods, 2025
    
    Controls:
    W/S         - Pitch forward/backward
    A/D         - Strafe left/right  
    Q/R         - Yaw rotation
    Left Mouse  - Increase collective (climb)
    Right Mouse - Decrease collective (descend)
    Left Shift  - Boost mode
    Left Ctrl   - Toggle altitude hold
    I           - Toggle HUD (interior view only)
]]
Helicopter = {}

Helicopter.MOD_NAME = "Helicopter"

-- Local safety wrappers (critical for stability!)
local function safeLinearVelocity(node)
    local vx, vy, vz = getLinearVelocity(node)
    return tonumber(vx) or 0, tonumber(vy) or 0, tonumber(vz) or 0
end

local function safeAngularVelocity(node)
    local avx, avy, avz = getAngularVelocity(node)
    return tonumber(avx) or 0, tonumber(avy) or 0, tonumber(avz) or 0
end

function Helicopter.prerequisitesPresent(specializations)
    return true
end

function Helicopter.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", Helicopter)
    SpecializationUtil.registerEventListener(vehicleType, "onStartMotor", Helicopter)
    SpecializationUtil.registerEventListener(vehicleType, "onStopMotor", Helicopter)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdate", Helicopter)
    SpecializationUtil.registerEventListener(vehicleType, "onDraw", Helicopter)
end

function Helicopter:onLoad(savegame)
    -- Input state
    self.heliInputForward = false
    self.heliInputBack = false
    self.heliInputLeft = false
    self.heliInputRight = false
    self.heliInputYawLeft = false
    self.heliInputYawRight = false
    self.heliInputClimb = false
    self.heliInputDescend = false
    self.heliInputBoost = false
    
    self.lastInputState = {}
    
    -- Flight state
    self.collectivePitch = 0
    self.verticalThrust = 0
    self.engineRunning = false
    self.engineStarting = false
    self.engineStartTime = 0
    self.engineStartDurationSec = 3.5
    
    -- Altitude hold (auto-hover)
    self.altitudeHoldActive = false
    self.altitudeHoldTarget = nil
    self.altitudeHoldIntegral = 0
    
    -- HUD
    self.hudDisplayMode = 1  -- 1=interior with features, 2=basic exterior, 3=off
    self.hudToggleCooldown = 0
    self.hudToggleMode = 1
    self.lastHudToggleKey = false
    self.hudInteriorMode = "ON"
    
    -- HUD smoothing and animation
    self.hudFadeTimer = 0
    self.hudLastDrawTime = 0
    self.hudSmoothedVspd = 0
    self.hudLastPitchRaw = 0
    self.hudLastRollRaw = 0
    self.hudDisplayRpm = 0
    
    -- HUD configuration (can be customized)
    self.hudInteriorScale = 1.0
    self.hudInteriorLineSpacing = 0.008
    self.hudInteriorBaseYOffset = -0.004
    self.hudInteriorNdcOffset = {x = 0, y = 0}
    self.hudRollSlantMode = true
    self.hudRollSlantScale = 1.0
    self.hudRollSpreadMode = true
    self.hudRollSpreadMaxDeg = 60
    self.hudRollSpreadGain = 0.25
    self.hudHeliDist = 1800
    self.hudHeliRight = 0.0
    self.hudHeliUp = 0.0
    
    -- RPM simulation
    self.currentRPM = 0
    self.targetRPM = 2800
    
    -- Ground yaw ramping
    self.yawGroundRamp = 0
    
    -- Yaw control
    self.yawInputSmoothed = 0
    
    -- Strafe smoothing
    self.strafeInput = 0
    self.strafeVelocity = 0
    
    -- Force smoothing
    self.prevForceX = 0
    
    -- Wheel animation
    self.wheelPositionOffset = 0
    self.wheelTargetOffset = 0
    
    -- Fuel tracking
    self.fuelTickAccumulator = 0
    
    -- Configuration - easily tunable
    self.config = {
        -- Mass and lift
        maxSupportedMass = 12000,
        baseHoverMultiplier = 0.82,
        maxThrustHeadroom = 1.12,
        maxClimbSpeed = 5.1,
        maxClimbAccel = 18.0,
        
        -- Collective
        collectiveMin = -16,
        collectiveMax = 32,
        collectiveRampUp = 50,
        collectiveRampDown = 90,
        collectiveReturn = 24,
        collectiveSmoothing = 0.09,
        
        -- Movement
        forwardSpeed = 200,
        strafeSpeed = 180,
        boostMultiplier = 1.2,
        
        -- Strafe dynamics
        strafeDeadzone = 0.18,
        strafeExponent = 1.8,
        strafeAccel = 3.5,
        strafeDecel = 5.0,
        strafeAccelNearZero = 1.2,
        strafeDecelNearZero = 2.5,
        strafeCrossingBrake = 0.55,
        strafeSmoothing = 0.08,
        
        -- Yaw
        yawAccel = 5.0,
        yawReturn = 2.2,
        yawSmoothing = 0.08,
        
        -- Altitude hold PID
        altHoldKp = 180,
        altHoldKd = 55,
        altHoldKi = 30,
        altHoldIntegralMax = 5,
        
        -- RPM
        rpmIdle = 2800,
        rpmHigh = 3200,
        rpmLow = 2250,
        rpmRampUp = 600,
        rpmRampDown = 600,
        
        -- Fuel consumption
        fuelTickRate = 1.0,
        fuelIdleRate = 0.25,
        fuelRpmScale = 0.45,
        fuelLoadScale = 0.40,
        
        -- Damping
        linearDampFlight = 0.22,
        linearDampHover = 0.34,
        linearDampFuelOut = 0.85,
        angularDampFlight = 0.02,
        angularDampHover = 0.05,
        angularDampFuelOut = 0.10,
        
        -- Ground mode
        groundAltitude = 1.0,
        groundVerticalSpeed = 0.8,
        groundLinearDamp = 0.65,
        groundAngularDampYaw = 0.45,
        groundAngularDampNoYaw = 0.10,
        groundStickForce = 1.08,
        groundYawTorque = 55,
        
        -- Wheels
        wheelOffsetGround = -0.037,
        wheelOffsetAir = 0.034,
        wheelOffsetFuelOut = 0.07,
        wheelSmoothTime = 0.65,
        wheelFrontScale = 0.5,
    }
    
    -- Set center of mass
    if self.components and self.components[1] then
        setCenterOfMass(self.components[1].node, 0, -0.8, 0)
    end
    
    -- Initialize wheel tracking
    self.wheelNodes = {}
    self.wheelSetupComplete = false
end

function Helicopter:onStartMotor()
    self.engineStarting = true
    self.engineRunning = false
    self.engineStartTime = 0
    
    local node = self.components and self.components[1] and self.components[1].node
    if node then
        local _, y, _ = getWorldTranslation(node)
        self.altitudeZeroReference = y
    end
    
    -- Reset altitude hold state (critical for clean startup)
    self.altitudeHoldIntegral = 0
    self.altitudeHoldActive = false
    self.altitudeHoldTarget = nil
end

function Helicopter:onStopMotor()
    self.engineStarting = false
    self.engineRunning = false
    self.engineStartTime = 0
    self.altitudeHoldActive = false
    self.altitudeHoldIntegral = 0
end

function Helicopter:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
    local controlled = self:getIsControlled()
    
    if not controlled then
        if not (self.isServer and self.altitudeHoldActive and self.engineRunning) then
            return
        end
    end
    
    -- Engine startup sequence
    if self.engineStarting and not self.engineRunning then
        self.engineStartTime = self.engineStartTime + (dt / 1000)
        if self.engineStartTime >= self.engineStartDurationSec then
            self.engineStarting = false
            self.engineRunning = true
        end
    end
    
    -- Update RPM (inlined for stability)
    local dtSec = math.max(((type(dt)=="number" and dt or 16) / 1000), 0.001)
    
    if self.heliInputClimb then
        self.targetRPM = self.config.rpmHigh
    elseif self.heliInputDescend then
        self.targetRPM = self.config.rpmLow
    else
        self.targetRPM = self.config.rpmIdle
    end
    
    local rpmRampRate = (self.targetRPM > self.currentRPM) and self.config.rpmRampUp or self.config.rpmRampDown
    local maxRPMChange = rpmRampRate * dtSec
    local rpmDelta = self.targetRPM - self.currentRPM
    
    if rpmDelta > maxRPMChange then
        rpmDelta = maxRPMChange
    elseif rpmDelta < -maxRPMChange then
        rpmDelta = -maxRPMChange
    end
    
    self.currentRPM = self.currentRPM + rpmDelta
    
    -- Handle HUD toggle
    local hudToggleKey = Input.isKeyPressed(Input.KEY_i)
    if hudToggleKey and self.hudToggleCooldown <= 0 then
        self.hudDisplayMode = (self.hudDisplayMode % 3) + 1
        self.hudToggleCooldown = 300
    end
    if self.hudToggleCooldown > 0 then
        self.hudToggleCooldown = self.hudToggleCooldown - dt
    end
    
    -- Client input handling
    if self.isClient and self:getIsActiveForInput(true) then
        Helicopter._handleClientInput(self, dt)
    end
    
-- Server physics
    if not self.isServer then return end
    
    -- Fuel consumption
    Helicopter._consumeFuel(self, dt)
    
    Helicopter._applyFlightPhysics(self, dt)
end

function Helicopter._handleClientInput(self, dt)
    local outOfFuel = Helicopter._isFuelDepleted(self)
    local allowInput = self.engineRunning or outOfFuel
    
    -- Read inputs
    local fwd = allowInput and Input.isKeyPressed(Input.KEY_w) or false
    local back = allowInput and Input.isKeyPressed(Input.KEY_s) or false
    local left = allowInput and Input.isKeyPressed(Input.KEY_a) or false
    local right = allowInput and Input.isKeyPressed(Input.KEY_d) or false
    local yawL = allowInput and Input.isKeyPressed(Input.KEY_q) or false
    local yawR = allowInput and Input.isKeyPressed(Input.KEY_r) or false
    local boost = allowInput and (Input.isKeyPressed(Input.KEY_lshift) or Input.isKeyPressed(Input.KEY_rshift)) or false
    local climb = (allowInput and not outOfFuel) and Input.isMouseButtonPressed(Input.MOUSE_BUTTON_LEFT) or false
    local descend = (allowInput and not outOfFuel) and Input.isMouseButtonPressed(Input.MOUSE_BUTTON_RIGHT) or false
    
    -- Store boost state ALWAYS (used directly by physics, not networked)
    self.heliInputBoost = boost
    
    -- Out of fuel handling
    if outOfFuel and self.altitudeHoldActive then
        self.altitudeHoldActive = false
        self.altitudeHoldTarget = nil
    end
    
    -- Altitude hold toggle
    local ctrlKey = Input.isKeyPressed(Input.KEY_lctrl)
    if ctrlKey and not self.lastCtrlState then
        if self.altitudeHoldActive then
            self.altitudeHoldActive = false
            self.altitudeHoldTarget = nil
        else
            local node = self.components[1].node
            local _, yPos, _ = getWorldTranslation(node)
            self.altitudeHoldActive = true
            self.altitudeHoldTarget = yPos
            self.altitudeHoldIntegral = 0
        end
    end
    self.lastCtrlState = ctrlKey
    
    -- Cancel altitude hold on manual input
    if climb or descend then
        self.altitudeHoldActive = false
        self.altitudeHoldTarget = nil
    end
    
    -- Only send network event if inputs changed (critical optimization!)
    if fwd ~= self.lastInputState.fwd or back ~= self.lastInputState.back or
       left ~= self.lastInputState.left or right ~= self.lastInputState.right or
       yawL ~= self.lastInputState.yawL or yawR ~= self.lastInputState.yawR or
       climb ~= self.lastInputState.climb or descend ~= self.lastInputState.descend then
        
        -- Update last inputs
        self.lastInputState = {fwd=fwd, back=back, left=left, right=right, yawL=yawL, yawR=yawR, climb=climb, descend=descend}
        
        -- Store inputs locally
        self.heliInputForward = fwd
        self.heliInputBack = back
        self.heliInputLeft = left
        self.heliInputRight = right
        self.heliInputYawLeft = yawL
        self.heliInputYawRight = yawR
        self.heliInputClimb = climb
        self.heliInputDescend = descend
        
        -- Send to server via network event
        local connection = g_client and g_client.getServerConnection and g_client:getServerConnection() or nil
        if HelicopterNetworkEvent ~= nil and HelicopterNetworkEvent.new ~= nil and connection ~= nil then
            connection:sendEvent(HelicopterNetworkEvent.new(self, fwd, back, left, right, yawL, yawR, climb, descend))
        end
    end
end

function Helicopter._applyFlightPhysics(self, dt)
    local node = self.components[1].node
    if node == nil then return end  -- Critical safety check!
    
    local dtSec = math.max(((type(dt)=="number" and dt or 16) / 1000), 0.0005)
    local outOfFuel = Helicopter._isFuelDepleted(self)
    
    -- Critical: Clear climb/descend inputs if out of fuel (server-side)
    if outOfFuel then
        self.heliInputClimb = false
        self.heliInputDescend = false
        if self.altitudeHoldActive then
            self.altitudeHoldActive = false
            self.altitudeHoldTarget = nil
        end
    end
    
    -- Get current state
    local wx, wy, wz = getWorldTranslation(node)
    local groundY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, wx, 0, wz)
    local altitudeAGL = wy - groundY
    local vx, vy, vz = safeLinearVelocity(node)
    
    -- Check if grounded (taxi mode)
    local isTaxiMode = Helicopter._isGroundMode(self, altitudeAGL, vy)
    
    -- Apply taxi mode physics (but don't return - continue to flight physics!)
    if isTaxiMode then
        Helicopter._applyGroundPhysics(self, node, vx, vy, vz, dt)
    end
    
    -- Flight mode (applies even when on ground!)
    local boost = self.heliInputBoost and self.config.boostMultiplier or 1.0
    
    -- Forward/backward input
    local rawPitchInput = (self.heliInputForward and 1 or 0) - (self.heliInputBack and 1 or 0)
    
    -- Strafe input with smoothing
    local rawStrafeInput = (self.heliInputLeft and 1 or 0) - (self.heliInputRight and 1 or 0)
    Helicopter._updateStrafeSmoothing(self, rawStrafeInput, dtSec)
    
    -- Calculate movement forces
    local pitchForce = self.config.forwardSpeed * rawPitchInput * boost
    local strafeForce = self.config.strafeSpeed * self.strafeInput * boost
    
    -- Convert to world space
    local dxFwd, _, dzFwd = localDirectionToWorld(node, 1, 0, 0)
    local sinDir = (dxFwd or 0) / 15
    local cosDir = (dzFwd or 0) / -15
    
    local dxStrafe, _, dzStrafe = localDirectionToWorld(node, strafeForce * 0.125, 0, 0)
    
    -- Calculate raw forces
    local rawForceX = pitchForce * cosDir + (dxStrafe or 0)
    local forceZ = pitchForce * sinDir + (dzStrafe or 0)
    
    -- Smooth X force to reduce jitter (75% old, 25% new)
    local forceX = self.prevForceX * 0.75 + rawForceX * 0.25
    self.prevForceX = forceX
    
    -- Scale forces
    forceX = forceX * 1.25
    forceZ = forceZ * 1.25
    
    -- Altitude hold boost
    if self.altitudeHoldActive then
        forceX = forceX * 1.10
        forceZ = forceZ * 1.10
    end
    
    -- Collective (vertical) control
    Helicopter._updateCollective(self, dtSec)
    
    -- Calculate vertical force
    local mass = self:getTotalMass(true)
    local gravity = mass * -9.81
    local hoverForce = mass * 9.81 * self.config.baseHoverMultiplier
    
    -- Dampen collective when moving horizontally (prevents unwanted climb/descend)
    local moveActive = (rawPitchInput ~= 0) or (math.abs(self.strafeInput) > 0.02)
    local effectiveCollective = self.collectivePitch
    if moveActive and not (self.heliInputClimb or self.heliInputDescend) then
        effectiveCollective = math.max(-4, math.min(4, self.collectivePitch * 0.2))
    end
    
    self.verticalThrust = hoverForce + effectiveCollective
    
    -- Ground effect - extra lift near terrain
    if wy < 4 then
        self.verticalThrust = self.verticalThrust * (1 + (4 - wy) * 0.10)
    end
    
    -- Altitude hold
    if self.altitudeHoldActive and self.altitudeHoldTarget and not outOfFuel then
        local altError = self.altitudeHoldTarget - wy
        self.altitudeHoldIntegral = self.altitudeHoldIntegral + (altError * dtSec)
        self.altitudeHoldIntegral = math.max(-self.config.altHoldIntegralMax, 
                                             math.min(self.config.altHoldIntegralMax, self.altitudeHoldIntegral))
        
        local pidCorrection = self.config.altHoldKp * altError + 
                             self.config.altHoldKd * (-vy) + 
                             self.config.altHoldKi * self.altitudeHoldIntegral
        
        self.verticalThrust = self.verticalThrust + pidCorrection
    end
    
    -- Limit max thrust (absolute cap)
    local maxThrust = mass * 9.81 * self.config.maxThrustHeadroom
    if self.verticalThrust > maxThrust then
        self.verticalThrust = maxThrust
    end
    
    -- Limit climb speed - if going too fast up, clamp to hover force
    if vy > self.config.maxClimbSpeed and self.verticalThrust > hoverForce then
        self.verticalThrust = hoverForce
    end
    
    -- Limit climb acceleration - prevent excessive upward acceleration
    if self.verticalThrust > hoverForce then
        local netUpForce = self.verticalThrust - hoverForce
        local maxNetForce = mass * self.config.maxClimbAccel
        if netUpForce > maxNetForce then
            self.verticalThrust = hoverForce + maxNetForce
        end
    end
    
    -- Out of fuel handling
    if outOfFuel then
        self.verticalThrust = 0
        local maxDescent = -8
        if vy < maxDescent then
            setLinearVelocity(node, vx, maxDescent, vz)
        end
    end
    
    -- Calculate pitch offset for force application
    local pitchOffset = Helicopter._calculatePitchOffset(self, rawPitchInput, boost)
    
    -- Apply forces
    addForce(node, forceX, self.verticalThrust, forceZ, 0, 1.2, pitchOffset, true)
    
    -- Yaw control
    Helicopter._applyYawControl(self, dtSec)
    
    -- Set damping
    local linDamp = outOfFuel and self.config.linearDampFuelOut or
                   (self.altitudeHoldActive and self.config.linearDampHover or self.config.linearDampFlight)
    local angDamp = outOfFuel and self.config.angularDampFuelOut or
                   (self.altitudeHoldActive and self.config.angularDampHover or self.config.angularDampFlight)
    
    setLinearDamping(node, linDamp)
    setAngularDamping(node, angDamp)
end

function Helicopter._updateStrafeSmoothing(self, rawInput, dtSec)
    -- Apply deadzone and expo
    local processed = rawInput
    local absInput = math.abs(rawInput)
    
    if absInput <= self.config.strafeDeadzone then
        processed = 0
    else
        local normalized = (absInput - self.config.strafeDeadzone) / (1 - self.config.strafeDeadzone)
        normalized = math.pow(normalized, self.config.strafeExponent)
        processed = (rawInput >= 0 and 1 or -1) * normalized
    end
    
    -- Detect zero crossing
    local crossing = (processed ~= 0 and self.strafeVelocity ~= 0 and (processed * self.strafeVelocity) < 0)
    local nearCenter = math.abs(self.strafeVelocity) < 0.4
    
    -- Determine acceleration
    local accel = nearCenter and self.config.strafeAccelNearZero or self.config.strafeAccel
    local decel = nearCenter and self.config.strafeDecelNearZero or self.config.strafeDecel
    
    if crossing then
        accel = accel * self.config.strafeCrossingBrake
        decel = decel * self.config.strafeCrossingBrake
    end
    
    local rate
    if processed == 0 then
        rate = decel
    elseif math.abs(processed) > math.abs(self.strafeVelocity) then
        rate = accel
    else
        rate = decel
    end
    
    local maxStep = rate * dtSec
    local delta = processed - self.strafeVelocity
    
    if delta > maxStep then
        delta = maxStep
    elseif delta < -maxStep then
        delta = -maxStep
    end
    
    self.strafeVelocity = self.strafeVelocity + delta
    
    -- Final smoothing filter
    local alpha = 1 - math.exp(-dtSec / self.config.strafeSmoothing)
    self.strafeInput = self.strafeInput + (self.strafeVelocity - self.strafeInput) * alpha
end

function Helicopter._updateCollective(self, dtSec)
    -- Descend ramp - smoothly ramps descend rate from 20% to 100%
    self.descendRamp = self.descendRamp or 0
    if self.heliInputDescend then
        self.descendRamp = math.min(1, self.descendRamp + dtSec / 1.1)
    else
        self.descendRamp = math.max(0, self.descendRamp - dtSec / 1.2)
    end
    
    -- Update target directly (no intermediate variable)
    self.collectiveTarget = self.collectiveTarget or 0
    
    if self.heliInputClimb then
        self.collectiveTarget = math.min(self.collectiveTarget + self.config.collectiveRampUp * dtSec, self.config.collectiveMax)
    elseif self.heliInputDescend then
        local descendRate = self.config.collectiveRampDown * (0.2 + 0.8 * self.descendRamp)
        self.collectiveTarget = math.max(self.collectiveTarget - descendRate * dtSec, self.config.collectiveMin)
    else
        -- Return to neutral
        if self.collectiveTarget > 0 then
            self.collectiveTarget = math.max(0, self.collectiveTarget - self.config.collectiveReturn * dtSec)
        elseif self.collectiveTarget < 0 then
            self.collectiveTarget = math.min(0, self.collectiveTarget + self.config.collectiveReturn * dtSec)
        end
    end
    
    -- Apply single exponential smoothing (matches Mr Chow's approach)
    local alpha = 1 - math.exp(-dtSec / self.config.collectiveSmoothing)
    self.collectivePitch = self.collectivePitch + (self.collectiveTarget - self.collectivePitch) * alpha
end

function Helicopter._calculatePitchOffset(self, pitchInput, boost)
    local fwdOrBack = 0
    local lmb = self.heliInputClimb
    local rmb = self.heliInputDescend
    
    if pitchInput > 0 then
        if boost and lmb then fwdOrBack = 3.0
        elseif boost and rmb then fwdOrBack = 2.25
        elseif boost then fwdOrBack = 2.5
        elseif lmb then fwdOrBack = 1.75
        elseif rmb then fwdOrBack = 1.25
        else fwdOrBack = 1.5 end
    elseif pitchInput < 0 then
        if boost and lmb then fwdOrBack = -3.0
        elseif boost and rmb then fwdOrBack = -2.25
        elseif boost then fwdOrBack = -2.5
        elseif lmb then fwdOrBack = -1.75
        elseif rmb then fwdOrBack = -1.25
        else fwdOrBack = -1.5 end
    end
    
    -- Shift inversion and scaling logic
    if boost and pitchInput ~= 0 then
        fwdOrBack = -fwdOrBack
        fwdOrBack = fwdOrBack * 0.35
    end
    if boost then
        fwdOrBack = -fwdOrBack
    end
    
    return fwdOrBack
end

function Helicopter._applyYawControl(self, dtSec)
    local rawYaw = (self.heliInputYawLeft and 1 or 0) - (self.heliInputYawRight and 1 or 0)
    
    -- Initialize yaw target
    self.yawTarget = self.yawTarget or 0
    
    -- Altitude hold yaw behavior
    if self.altitudeHoldActive then
        if rawYaw ~= 0 then
            self.yawHold = rawYaw
        else
            self.yawHold = nil
        end
        self.yawTarget = self.yawHold or 0
    else
        -- Ramp yaw target with acceleration/return
        if rawYaw ~= 0 then
            self.yawTarget = math.max(-1, math.min(1, self.yawTarget + rawYaw * self.config.yawAccel * dtSec))
        else
            if self.yawTarget > 0 then
                self.yawTarget = math.max(0, self.yawTarget - self.config.yawReturn * dtSec)
            elseif self.yawTarget < 0 then
                self.yawTarget = math.min(0, self.yawTarget + self.config.yawReturn * dtSec)
            end
        end
    end
    
    -- Apply exponential smoothing to actual output
    local yawFollow = 0.08
    local yawAlpha = 1 - math.exp(-dtSec / yawFollow)
    self.yawPrev = self.yawPrev or 0
    local yTurn = self.yawPrev + (self.yawTarget - self.yawPrev) * yawAlpha
    self.yawPrev = yTurn
    yTurn = math.max(-1, math.min(1, yTurn))
    
    setAngularVelocity(self.components[1].node, 0, yTurn, 0)
end

function Helicopter._applyGroundPhysics(self, node, vx, vy, vz, dt)
    -- Prevent bounce
    if vy > 0 then
        setLinearVelocity(node, vx, 0, vz)
    end
    
    -- Apply stick force
    local mass = self:getTotalMass(true)
    local stickForce = -mass * 9.81 * self.config.groundStickForce
    addForce(node, 0, stickForce, 0, 0, 0, 0, true)
    
    -- Ground yaw with smooth ramping
    local rawYaw = (self.heliInputYawLeft and 1 or 0) - (self.heliInputYawRight and 1 or 0)
    if rawYaw ~= 0 then
        local rampRate = 1.5
        self.yawGroundRamp = self.yawGroundRamp + (rawYaw - self.yawGroundRamp) * rampRate * (dt / 1000)
        local yawTorque = self.yawGroundRamp * 50
        addTorque(node, 0, yawTorque, 0, true)
        setAngularDamping(node, self.config.groundAngularDampYaw)
    else
        -- Exponential decay when no input
        self.yawGroundRamp = self.yawGroundRamp * 0.92
        setAngularDamping(node, self.config.groundAngularDampNoYaw)
    end
    setLinearDamping(node, self.config.groundLinearDamp)
end

function Helicopter._isGroundMode(self, altitudeAGL, verticalVel)
    return (altitudeAGL < self.config.groundAltitude) and 
           (verticalVel < self.config.groundVerticalSpeed) and
           not self.heliInputClimb
end

function Helicopter._updateWheelAnimation(self, dt)
    if not self.wheelSetupComplete then
        Helicopter._setupWheelNodes(self)
    end
    
    local dtSec = ((type(dt)=="number" and dt or 16) / 1000)
    local grounded = Helicopter._isGroundMode(self, Helicopter._getAltitudeAGL(self), select(2, safeLinearVelocity(self.components[1].node)))
    local outOfFuel = Helicopter._isFuelDepleted(self)
    
    local target
    if outOfFuel then
        target = self.config.wheelOffsetFuelOut
    elseif grounded then
        target = self.config.wheelOffsetGround
    else
        target = self.config.wheelOffsetAir
    end
    
    -- Smooth transition
    local alpha = 1 - math.exp(-dtSec / self.config.wheelSmoothTime)
    self.wheelPositionOffset = self.wheelPositionOffset + (target - self.wheelPositionOffset) * alpha
    
    -- Apply to wheels
    for _, wheel in ipairs(self.wheelNodes) do
        local x, y, z = getTranslation(wheel.node)
        local delta = self.wheelPositionOffset - (wheel.currentOffset or 0)
        if math.abs(delta) > 0.0001 then
            local scale = wheel.isFront and self.config.wheelFrontScale or 1.0
            setTranslation(wheel.node, x, y + delta * scale, z)
            wheel.currentOffset = (wheel.currentOffset or 0) + delta * scale
        end
    end
end

function Helicopter._setupWheelNodes(self)
    self.wheelSetupComplete = true
    self.wheelNodes = self.wheelNodes or {}
    
    local wheelSpec = self.spec_wheels
    if not (wheelSpec and wheelSpec.wheels) then return end
    
    local rootNode = self.components[1].node
    
    for i, wheel in ipairs(wheelSpec.wheels) do
        local node = wheel.repr or wheel.driveNode
        if node then
            local wx, wy, wz = getWorldTranslation(node)
            local lx, ly, lz = worldToLocal(rootNode, wx, wy, wz)
            
            table.insert(self.wheelNodes, {
                node = node,
                currentOffset = 0,
                localZ = lz,
                isFront = false
            })
        end
    end
    
    -- Mark front wheels
    if #self.wheelNodes >= 2 then
        table.sort(self.wheelNodes, function(a, b) return a.localZ > b.localZ end)
        self.wheelNodes[1].isFront = true
    end
end

function Helicopter._consumeFuel(self, dt)
    if not self.engineRunning then return end
    
    local dtSec = (type(dt)=="number" and dt or 16) / 1000
    self.fuelTickAccumulator = (self.fuelTickAccumulator or 0) + dtSec
    
    while self.fuelTickAccumulator >= self.config.fuelTickRate do
        local fuelSpec = self.spec_motorized
        local fuelIdx = fuelSpec and fuelSpec.fuelFillUnitIndex
        
        if fuelIdx and self.getFillUnitFillLevel then
            local currentFuel = self:getFillUnitFillLevel(fuelIdx) or 0
            
            if currentFuel > 0 then
                -- Calculate consumption
                local rpmFraction = (self.currentRPM - self.config.rpmIdle) / (self.config.rpmHigh - self.config.rpmIdle)
                rpmFraction = math.max(0, math.min(1, rpmFraction))
                
                -- Pitch load from collective
                local pitchFraction = math.abs(self.collectivePitch) / self.config.collectiveMax
                pitchFraction = math.min(1, pitchFraction)
                
                -- Movement load from inputs (use actual magnitudes, not binary)
                local rawFwd = (self.heliInputForward and 1 or 0) - (self.heliInputBack and 1 or 0)
                local rawStr = (self.heliInputLeft and 1 or 0) - (self.heliInputRight and 1 or 0)
                local moveFraction = math.min(1, math.abs(rawFwd) * 1.0 + math.abs(rawStr) * 0.5)
                
                -- Combined load: 75% pitch, 25% movement
                local loadFraction = math.min(1, 0.75 * pitchFraction + 0.25 * moveFraction)
                
                local consumption = (self.config.fuelIdleRate + 
                                   self.config.fuelRpmScale * rpmFraction + 
                                   self.config.fuelLoadScale * loadFraction) * self.config.fuelTickRate
                
                local newLevel = math.max(0, currentFuel - consumption)
                
                -- Get farm ID and fuel type
                local farmId = (self.getOwnerFarmId and self:getOwnerFarmId()) or 1
                local fuelType = self:getFillUnitFillType(fuelIdx)
                if not fuelType and g_fillTypeManager then
                    fuelType = g_fillTypeManager:getFillTypeIndexByName("DIESEL") or g_fillTypeManager:getFillTypeIndexByName("diesel")
                end
                
                -- Try primary method (correct FS25 API signature)
                local changed = false
                if self.setFillUnitFillLevel and fuelType then
                    self:setFillUnitFillLevel(farmId, newLevel, fuelType, fuelIdx, true, false)
                    local after = self:getFillUnitFillLevel(fuelIdx) or currentFuel
                    if after < currentFuel then changed = true end
                end
                
                -- Fallback: try addFillUnitFillLevel
                if not changed and self.addFillUnitFillLevel and fuelType then
                    local delta = newLevel - currentFuel
                    self:addFillUnitFillLevel(farmId, delta, fuelType, nil, nil, fuelIdx, nil, nil, nil, true, false)
                    local after = self:getFillUnitFillLevel(fuelIdx) or currentFuel
                    if after < currentFuel then changed = true end
                end
                
                -- Last resort: direct table access
                if not changed and self.spec_fillUnit and self.spec_fillUnit.fillUnits and self.spec_fillUnit.fillUnits[fuelIdx] then
                    self.spec_fillUnit.fillUnits[fuelIdx].fillLevel = newLevel
                end
            end
        end
        
        self.fuelTickAccumulator = self.fuelTickAccumulator - self.config.fuelTickRate
    end
end

function Helicopter._isFuelDepleted(self)
    local fuelSpec = self.spec_motorized
    local fuelIdx = fuelSpec and fuelSpec.fuelFillUnitIndex
    
    if fuelIdx and self.getFillUnitFillLevel then
        local fuel = self:getFillUnitFillLevel(fuelIdx) or 0
        return fuel <= 0.05
    end
    return false
end

function Helicopter._getAltitudeAGL(self)
    local node = self.components and self.components[1] and self.components[1].node
    if not node then return 0 end
    
    local x, y, z = getWorldTranslation(node)
    local groundY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 0, z)
    return math.max(0, y - groundY)
end

function Helicopter:onDraw()
    if not self:getIsActiveForInput(true) then return end
    
    local node = self.components[1].node
    if not node then return end
    
    -- Check interior mode
    local isInterior = false
    if self.spec_enterable then
        local cam = self.spec_enterable.cameras and self.spec_enterable.cameras[self.spec_enterable.camIndex]
        isInterior = cam and cam.isInside
    end
    
    -- Handle HUD mode toggling
    if isInterior then
        if self.hudToggleMode < 1 or self.hudToggleMode > 3 then
            self.hudToggleMode = 1
        end
        if self.hudToggleMode == 3 then
            return
        elseif self.hudToggleMode == 1 then
            self.hudInteriorMode = "ON"
        elseif self.hudToggleMode == 2 then
            self.hudInteriorMode = "OFF"
        end
    else
        self.hudInteriorMode = "OFF"
        self.hudToggleMode = 2
    end
    
    -- Fade-in animation
    local currentTime = getTimeSec()
    self.hudLastDrawTime = self.hudLastDrawTime or currentTime
    local dtDraw = currentTime - self.hudLastDrawTime
    self.hudLastDrawTime = currentTime
    
    if self.engineRunning then
        self.hudFadeTimer = math.min(self.hudFadeTimer + dtDraw, 1.5)
    else
        self.hudFadeTimer = 0
        return
    end
    
    local fadeProgress = math.min(self.hudFadeTimer / 1.5, 1.0)
    
    -- Thick text rendering helper
    local function drawThickText(x, y, s, t)
        renderText(x, y, s, t)
        renderText(x + 0.0009, y, s, t)
        renderText(x - 0.0009, y, s, t)
        renderText(x, y + 0.0009, s, t)
        renderText(x, y - 0.0009, s, t)
    end
    
    -- Get telemetry
    local wx, wy, wz = getWorldTranslation(node)
    local groundY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, wx, 0, wz)
    local altitudeAGL = math.max(0, wy - groundY)
    local vx, vy, vz = safeLinearVelocity(node)
    
    -- Altitude display logic
    local altLabel, altDisplay
    if altitudeAGL < 50 then
        altLabel = "GL"
        altDisplay = altitudeAGL
    else
        altLabel = "SL"
        local seaLevel = 65  -- Can be customized
        altDisplay = math.max(0, wy - seaLevel)
    end
    
    local forwardSpeed = math.sqrt(vx * vx + vz * vz)
    
    -- Smooth vertical speed
    local vAlpha = 0.15
    self.hudSmoothedVspd = self.hudSmoothedVspd + (vy - self.hudSmoothedVspd) * vAlpha
    local verticalSpeed = self.hudSmoothedVspd
    
    -- Base colors
    local baseScale = 0.015
    local green = {0.2, 1.0, 0.2, fadeProgress}
    local red   = {1.0, 0.2, 0.2, fadeProgress}
    local amber = {1.0, 1.0, 0.3, fadeProgress}
    
    -- Taxi mode detection
    local isTaxiMode = Helicopter._isGroundMode(self, altitudeAGL, vy)
    local taxiBlinkAlpha = 0.35 + 0.65 * (0.5 + 0.5 * math.sin(getTimeSec() * 5.5))
    
    setTextBold(true)
    
    -- INTERIOR HUD (Advanced 3D projection)
    if isInterior and self.hudInteriorMode == "ON" then
        local hudScale = 0.8
        local scaleMult = self.hudInteriorScale
        local scale = baseScale * hudScale * scaleMult
        local lineSpace = self.hudInteriorLineSpacing
        local baseYShift = self.hudInteriorBaseYOffset
        
        -- Calculate pitch and roll
        local fX, fY, fZ = localDirectionToWorld(node, 0, 0, 1)
        local rX, rY, rZ = localDirectionToWorld(node, 1, 0, 0)
        local rawPitch = math.deg(math.atan2(fY, math.sqrt(fX * fX + fZ * fZ)))
        local rawRoll = math.deg(math.atan2(rY, math.sqrt(rX * rX + rZ * rZ)))
        
        -- Smooth pitch/roll for display
        local alpha = 0.1
        local pitch = self.hudLastPitchRaw + alpha * (rawPitch - self.hudLastPitchRaw)
        local roll = self.hudLastRollRaw + alpha * (rawRoll - self.hudLastRollRaw)
        self.hudLastPitchRaw = pitch
        self.hudLastRollRaw = roll
        
        -- Smooth RPM display
        local rpmSmoothAlpha = 0.15
        self.hudDisplayRpm = self.hudDisplayRpm + (self.currentRPM - self.hudDisplayRpm) * rpmSmoothAlpha
        local rpm = math.floor(self.hudDisplayRpm + 0.5)
        
        -- Fuel and battery
        local fuelSpec = self.spec_motorized
        local fuelIdx = fuelSpec and fuelSpec.fuelFillUnitIndex
        local fuelLiters = 0
        local fuelCap = 1
        if fuelIdx and self.getFillUnitFillLevel and self.getFillUnitCapacity then
            fuelLiters = self:getFillUnitFillLevel(fuelIdx) or 0
            fuelCap = self:getFillUnitCapacity(fuelIdx) or 1
        end
        local fuelPercent = (fuelCap > 0) and (fuelLiters / fuelCap) * 100 or 0
        local battPercent = (self.spec_electric and self.spec_electric.batteryLevel or 1) * 100
        
        -- Project HUD onto 3D space
        local px, py, pz
        local hx, hy, hz = getWorldTranslation(node)
        local fXh, fYh, fZh = localDirectionToWorld(node, 0, 0, 1)
        local rXh, rYh, rZh = localDirectionToWorld(node, 1, 0, 0)
        local uXh, uYh, uZh = localDirectionToWorld(node, 0, 1, 0)
        
        local dist = self.hudHeliDist
        local offR = self.hudHeliRight
        local offU = self.hudHeliUp
        
        local wx = hx + fXh * dist + rXh * offR + uXh * offU
        local wy = hy + fYh * dist + rYh * offR + uYh * offU
        local wz = hz + fZh * dist + rZh * offR + uZh * offU
        px, py, pz = project(wx, wy, wz)
        
        if px and pz > 0 then
            px = px + self.hudInteriorNdcOffset.x
            py = py + self.hudInteriorNdcOffset.y
            
            -- Roll slanting effect
            local rollRad = 0
            if self.hudRollSlantMode then
                rollRad = -math.rad(roll) * self.hudRollSlantScale
            end
            local ct, st = math.cos(rollRad), math.sin(rollRad)
            
            -- Roll spread effect
            local lineSpaceDyn = lineSpace
            if self.hudRollSpreadMode then
                local absRollDeg = math.abs(roll)
                local t = math.min(1, absRollDeg / self.hudRollSpreadMaxDeg)
                t = t * t * (3 - 2 * t)  -- Smooth step
                lineSpaceDyn = lineSpace * (1 + self.hudRollSpreadGain * t)
            end
            
            -- Position calculation helpers
            local function pos(dx, dy)
                return px + dx * ct - dy * st, py + dx * st + dy * ct
            end
            
            local function flatPos(dx, dy)
                return px + dx, py + dy
            end
            
            local function linePos(n)
                local dy = baseYShift - n * lineSpaceDyn
                return pos(0, dy)
            end
            
            -- HEADING TAPE
            setTextAlignment(RenderText.ALIGN_CENTER)
            setTextColor(table.unpack(green))
            
            local function norm360(h)
                h = h % 360
                if h < 0 then h = h + 360 end
                return h
            end
            
            local heading = norm360(math.deg(math.atan2(fX, fZ)) + 180)
            
            local function cardinal8(h)
                local dirs = {"N","NE","E","SE","S","SW","W","NW"}
                local idx = math.floor((h + 22.5) / 45) % 8 + 1
                return dirs[idx]
            end
            
            local hdgCard = cardinal8(heading)
            local baseN = -1.2
            local baseDy = baseYShift - baseN * lineSpaceDyn
            local headingScale = 1.0 * hudScale * scaleMult
            
            -- Heading display
            local lx, ly = flatPos(-0.024, baseDy + 0.010)
            renderText(lx, ly, 0.0085 * headingScale * 1.35, string.format("%03d° %s", math.floor(heading + 0.5), hdgCard))
            
            lx, ly = flatPos(-0.024, baseDy + 0.001)
            renderText(lx, ly, 0.0150 * headingScale, "|")
            
            -- Heading tape marks
            local stepDeg = 10
            local halfSpan = 30
            local unitWidth = 0.0074
            for d = -halfSpan, halfSpan, stepDeg do
                local dx = (-0.024) + (d / stepDeg) * unitWidth
                local lx1, ly1 = flatPos(dx, baseDy + 0.003)
                renderText(lx1, ly1, 0.0075 * headingScale, "|")
                local lx2, ly2 = flatPos(dx, baseDy - 0.002)
                renderText(lx2, ly2, 0.0082 * headingScale * 1.35, string.format("%03d", math.floor(norm360(heading + d) + 0.5)))
            end
            
            lx, ly = flatPos(-0.024, baseDy + 0.007)
            renderText(lx, ly, 0.0090 * headingScale, "▼")
            
            -- Main telemetry display
            setTextAlignment(RenderText.ALIGN_RIGHT)
            if isTaxiMode then setTextColor(table.unpack(amber)) else setTextColor(table.unpack(green)) end
            
            local x0, y0 = linePos(0.0)
            renderText(x0, y0, scale, string.format("ALT %s  %.0f m||", altLabel, altDisplay))
            
            local x1, y1 = linePos(1.0)
            renderText(x1, y1, scale, string.format("FSPD  %.0f m/s||", forwardSpeed))
            
            local x2, y2 = linePos(2.0)
            renderText(x2, y2, scale, string.format("VSPD  %.0f m/s||", verticalSpeed))
            
            local x3, y3 = linePos(3.0)
            renderText(x3, y3, scale, string.format("RPM          %d||", rpm))
            
            local x4, y4 = linePos(4.0)
            renderText(x4, y4, scale, string.format("PITCH    %.0f°||", pitch))
            
            local x5, y5 = linePos(5.0)
            renderText(x5, y5, scale, string.format("ROLL       %.0f°||", roll))
            
            local x6, y6 = linePos(6.0)
            renderText(x6, y6, scale, string.format("FUEL   %.0f L / %.0f %%||", fuelLiters, fuelPercent))
            
            local x7, y7 = linePos(7.0)
            renderText(x7, y7, scale, string.format("BATT     %.0f%%||", battPercent))
            
            local x8, y8 = linePos(8.0)
            renderText(x8, y8, scale, "[=======O=======]")
            
            -- Vertical speed indicator
            local baseN2 = 2.0
            local dy2 = baseYShift - baseN2 * lineSpaceDyn
            local lx2, ly2 = pos(-0.045, dy2)
            
            if verticalSpeed > 0.5 then
                setTextColor(table.unpack(red))
                setTextAlignment(RenderText.ALIGN_LEFT)
                renderText(lx2, ly2, scale, "↑")
            elseif verticalSpeed < -0.5 then
                setTextColor(table.unpack(red))
                setTextAlignment(RenderText.ALIGN_LEFT)
                renderText(lx2, ly2, scale, "↓")
            else
                setTextColor(0.8, 0.8, 0.8, fadeProgress)
                setTextAlignment(RenderText.ALIGN_LEFT)
                renderText(lx2, ly2 + 0.004, scale, "—")
            end
            
            setTextColor(table.unpack(green))
            setTextAlignment(RenderText.ALIGN_RIGHT)
            
            -- Warnings and indicators
            local baseN3 = 9.3
            local dy3 = baseYShift - baseN3 * lineSpaceDyn
            local leftShift, ahExtraL, rightNudge = 0.0020, 0.002, -0.015
            
            if self.altitudeHoldActive then
                local flashAlpha = 0.5 + 0.5 * math.sin(getTimeSec() * 8.0)
                setTextColor(1.0, 0.0, 0.0, flashAlpha)
                local lx3, ly3 = pos((-0.024 - ahExtraL - leftShift + rightNudge + 0.004), dy3 + 0.020)
                drawThickText(lx3, ly3, 0.010 * hudScale * scaleMult, "AH")
                setTextColor(table.unpack(green))
            end
            
            if fuelPercent < 25 then
                setTextColor(table.unpack(red))
                local lx3, ly3 = pos((-leftShift + rightNudge + 0.009), dy3 - 0.006)
                renderText(lx3, ly3, scale, "FUEL LOW")
            elseif fuelPercent < 45 then
                setTextColor(table.unpack(amber))
                local lx3, ly3 = pos((-leftShift + rightNudge + 0.009), dy3 - 0.006)
                renderText(lx3, ly3, scale, "FUEL WARN")
            end
            
            if altitudeAGL < 5 and vy < -5 then
                setTextColor(table.unpack(red))
                local lx3, ly3 = pos((-leftShift + rightNudge + 0.009), dy3 - 0.016)
                renderText(lx3, ly3, scale, "ALT WARNING")
            end
            
            if isTaxiMode then
                setTextColor(1.0, 1.0, 0.3, taxiBlinkAlpha)
                local lx3, ly3 = pos((-0.0015 - leftShift + rightNudge), dy3 + 0.002)
                renderText(lx3, ly3, scale, "TAXI")
                setTextColor(table.unpack(green))
            end
        end
    else
        -- EXTERIOR HUD (Simple)
        setTextAlignment(RenderText.ALIGN_LEFT)
        setTextColor(table.unpack(green))
        
        local baseX, baseY = 0.18, 0.2
        
        if isTaxiMode then
            setTextColor(1.0, 1.0, 0.3, taxiBlinkAlpha)
            renderText(baseX, baseY - 0.060, baseScale, "TAXI")
            setTextColor(table.unpack(green))
        end
        
        renderText(baseX, baseY, baseScale, string.format("ALT %s  %.0f m", altLabel, altDisplay))
        renderText(baseX, baseY - 0.015, baseScale, string.format("FSPD  %.0f m/s", forwardSpeed))
        renderText(baseX, baseY - 0.030, baseScale, string.format("VSPD  %.0f m/s", verticalSpeed))
        
        if self.altitudeHoldActive then
            local flashAlpha = 0.5 + 0.5 * math.sin(getTimeSec() * 8.0)
            setTextColor(1.0, 0.0, 0.0, flashAlpha)
            drawThickText(baseX, baseY - 0.045, 0.012, "AUTO HOVER")
            setTextColor(table.unpack(green))
        end
    end
end

