local M = {}
M.type = "auxiliary"
M.relevantDevice = nil
M.defaultOrder = 1000

local constants = {
    -- Conversion rates
    mpsToMph = 2.23694,
    hpToKw = 0.7457,
    torqueToHpCoef = 7120.54016,
    kWhToJ = 3600000,
    sToH = 1.0 / 3600,
    avToRPM = 9.549296596425384,
    rpmToAV = 1.0 / 9.549296596425384,
}

-- lib requires
local mathutil = require("mathutil")
local latch = require("latch")
local pid = require("pid")
local debugutil = require("debugutil")("hybrid", true)
local engineutil = require("engineUtil")

local min = math.min
local max = math.max
local abs = math.abs
local maprange = mathutil.maprange
local clamp = mathutil.clamp
local blend = mathutil.blend
local round = mathutil.round
local sign = mathutil.sign
local fnum = debugutil.formatNumber

local engine = nil
local gearbox = nil
local battery = nil
local clutch = nil

local sounds = {
    mg1Whine = nil,
    mg2Whine = nil,
    motorRamp = nil
}

-- Calculated/initial values
local initialWheelBrakeSplit = {}
local torqueData = {
    maxElectricTorque = 0,
    maxEngineTorque = 0,
    maxCombinedTorque = 0,
    engineIdleTorque = 0,
    engineMaxTorqueRpm = 0,
    engineTorqueCurve = {},
}

-- Quick access upvalues
local engineOn = false
local watertemp = 0
local speedMph = 0
local batteryLevel = 1
local totalPowerOutKw = 0
local throttle = 0
local throttleAggressiveness = 0
local gear = "P"

-- Other state variables/upvalues
local engineDemandReason = "none"
local state = "init"
local nextState = nil
local lastState = nil
local engineDemandThrottle = false
local timeSinceEngineStart = 5
local timeSinceEngineCanShutOff = 0
local timeSinceAppliedBrakes = 0
local ecvtDriveMode = 0
local startingTime = 0
local isInChargeup = false
local lastThrottle = 0
local chargeThrottle = 0
local disableRegen = false
local timeSinceAbsHit = 0
local timeSinceInit = 0
local mg2TorqueAdjustment = 0
local engineDeload = 0
local desiredRegen = 0
local availableMG1Power = 0
local availableMG2Power = 0
local targetEngineRpm = 0

local function getProportionalBatteryRatio(minLevelBias, maxLevelBias)
    minLevelBias = minLevelBias or 0
    maxLevelBias = maxLevelBias or 0

    local minPowerLevelThreshold = blend(M.minBatteryPowerLevel, M.maxBatteryPowerLevel, minLevelBias)
    local maxPowerLevelThreshold = blend(M.minBatteryPowerLevel, M.maxBatteryPowerLevel, 1 - maxLevelBias)

    return maprange(batteryLevel, minPowerLevelThreshold, maxPowerLevelThreshold)
end

local function calculateTorqueStatistics()
    -- Calculate some torque performance statistics
        
    -- Raw max torques from gearbox and engine
    local maxElectricOnlyTorque = gearbox:getMaxMG2Torque(true)
    local maxElectricTorqueWithEngine = maxElectricOnlyTorque * M.maxPowerSplitRatio
    local maxSunGearTorque = gearbox:getMaxMG1Torque(true)
    local maxEngineTorque = engine.torqueData.maxTorque
    
    -- Figure out compound torques
    local maxEngineTorqueToCarrier = maxEngineTorque / gearbox.inputGearRatio
    local maxEngineTorqueToSun = abs(gearbox:getSunTorqueFromCarrierTorque(maxEngineTorqueToCarrier))
    local maxUsableEngineTorqueToSun = min(maxEngineTorqueToSun, maxSunGearTorque)
    local maxEngineTorqueToRing = abs(gearbox:getRingTorqueFromSunTorque(maxUsableEngineTorqueToSun))
    
    torqueData.maxElectricTorque = maxElectricOnlyTorque
    torqueData.maxEngineTorque = maxEngineTorqueToRing
    torqueData.maxCombinedTorque = maxEngineTorqueToRing + maxElectricTorqueWithEngine
    
    -- Figure out engine torque characteristics
    local curve = engine.torqueData.curves[engine.torqueData.finalCurveName]
    local idleTorque = curve.torque[M.engineMinIdleRpm]
    
    torqueData.engineIdleTorque = idleTorque
    torqueData.engineMaxTorqueRpm = engine.torqueData.maxTorqueRPM
    torqueData.engineTorqueCurve = curve.torque
end

local function isEngineDemandedOn()
    local inputThrottle = electrics.values.throttleOverride or input.throttle
    local minRunTime
    
    if watertemp < 57 then
        minRunTime = M.engineMinRunTime * 2
    else
        minRunTime = M.engineMinRunTime
    end

    if state == "chargemode" then
        engineDemandReason = "charge mode"
        return true
    end
    if not M.enableAutoStop then
        engineDemandReason = "auto stop disabled"
        return true
    end
    if watertemp < 30 then
        engineDemandReason = "temp"
        return true
    end -- engine must be warmed up
    
    if gear == "R" and batteryLevel >= blend(M.minBatteryPowerLevel, M.maxBatteryPowerLevel, 0.2) and timeSinceEngineStart >= minRunTime then
        -- After this point, if in R and battery >= 0.4, force engine off
        engineDemandReason = "none (forced off due to reverse)"
        return false
    end
    
    if speedMph > M.maxEvOnlySpeed then
        engineDemandReason = "speed"
        return true
    end -- too fast
    if obj:getDirectionVector().z > M.maxEvOnlyPitch then
        engineDemandReason = "vehicle pitch"
        return true
    end -- pitched too high up (hill start)
    if batteryLevel < M.minBatteryPowerLevel or isInChargeup then
        engineDemandReason = "battery level"
        isInChargeup = true
        return true
    end -- battery too low
    if engineDemandThrottle or throttleAggressiveness > M.maxEvOnlyThrottleAggressiveness then
        engineDemandReason = "throttle"
        return true
    end
    
    local brakeInput = max(input.brake, desiredRegen)
    
    if engineOn and (
        not (electrics.values.parkingbrake == 1 or brakeInput >= M.autoStopBrakeThreshold) or
        timeSinceAppliedBrakes < M.autoStopBrakeTime
    ) then
        -- this check only applies when not in P
        if gear ~= "P" then
            engineDemandReason = "brakes"
            return true
        end
    end

    -- Easy checks are done; dynamic checks now

    -- This is the percentage of the max EV-only kW output that's available to us,
    -- based on battery level
    local batteryWorkingRatio = getProportionalBatteryRatio(0, 0.35)
    local availableEvOnlyPower = batteryWorkingRatio * M.maxEvOnlyPower
    local curPower = gearbox.motorGenerator2Power

    if engineOn then
        if inputThrottle > 0.01 then
            engineDemandReason = "accelerating"
            return true
        end -- once engine is on, must coast again to shut off
    else
        if inputThrottle > 0.01 and curPower > availableEvOnlyPower then
            engineDemandReason = "accelerating"
            return true
        end
    end

    -- Put this last so it doesn't override other reasons
    if timeSinceEngineStart < minRunTime then
        engineDemandReason = "time since start"
        return true
    end -- engine has to run for at least 4 seconds

    engineDemandReason = "none"
    return false -- by default, the engine is off
end

local function updateValues(dt)
    if not gearbox or not engine or not battery then
        return
    end
    
    local throttleAggressivenessInst = (input.throttle - lastThrottle) / dt
    
    lastThrottle = input.throttle
    engineOn = not (engine.isStalled or engine.ignitionCoef < 0.01 or electrics.values.rpm < engine.idleRPM / 2)
    watertemp = electrics.values.watertemp
    speedMph = electrics.values.wheelspeed * constants.mpsToMph
    batteryLevel = battery.remainingRatio
    gear = electrics.values.gear
    mg2TorqueAdjustment = 0
    
    -- Rolling average
    throttleAggressiveness = throttleAggressiveness + (throttleAggressivenessInst - throttleAggressiveness) / 10
    
    local throttleRate = maprange(
        abs(throttleAggressiveness),
        M.minThrottleAggressivenessRamp,
        M.maxThrottleAggressivenessRamp,
        M.minThrottleAggressivenessFactor,
        M.maxThrottleAggressivenessFactor
    )
    
    debugutil.putValue("Throttle Rate", throttleRate)
    
    throttle = electrics.values.throttleOverride or M.throttleSmoother:get(input.throttle, dt * throttleRate)

    if engineOn then
        timeSinceEngineStart = timeSinceEngineStart + dt
    end

    local brakeInput = max(input.brake, desiredRegen)
    
    if M.autoStopBrakeThreshold > 0 and (brakeInput >= M.autoStopBrakeThreshold or electrics.values.parkingbrake == 1) and speedMph <= M.maxEvOnlySpeed then
        timeSinceAppliedBrakes = timeSinceAppliedBrakes + dt
    else
        timeSinceAppliedBrakes = 0
    end

    if isInChargeup and batteryLevel >= blend(M.minBatteryPowerLevel, M.maxBatteryPowerLevel, 0.2) then
        isInChargeup = false
    end
end

local function updateState(dt)
    local engineDemandedOn = isEngineDemandedOn()

    debugutil.putValue("Engine demanded on", tostring(engineDemandedOn))
    debugutil.putValue("Engine on reason", engineDemandReason)
    debugutil.putValue("State", state)

    -- keep things like daytime-running-lights on
    electrics.values.running = 1

    -- prevent engine starter from working (since the eCVT starts it)
    engine:deactivateStarter()
    engine.starterDisabled = true

    if state == "init" then
        timeSinceInit = timeSinceInit + dt

        if timeSinceInit >= 1 then
            if engineOn then
                nextState = "running"
            else
                nextState = "stopped"
            end
        end
    elseif state == "evmode" then
        engine.ignitionCoef = 0 -- Kill engine
        engine.isDisabled = true -- Prevent the user from manually restarting the engine
        electrics.values.checkengine = false -- Keep this from coming on (usually tied to engine.isDisabled)
        gearbox:setInputDisengaged(true)
        
        if electrics.values.chargeMode == 1 then
            if gear == "P" then
                electrics.values.evMode = 0
            else
                gui.message({txt = "Charge mode can only be enabled in P gear"}, 2, "vehicle.hybrid.chargeMode")
                electrics.values.chargeMode = 0
            end
        end

        if electrics.values.evMode ~= 1 then
            if M.forceEvMode then
                electrics.values.evMode = 1
            else
                gui.message({txt = "EV mode disabled"}, 2, "vehicle.hybrid.evMode")
                gearbox:setInputDisengaged(false)
                nextState = "init"
            end
        end
    elseif state == "running" then
        if M.maxEngineDeload > 0 then
            engine.ignitionCoef = 1 - M.engineDeloadMaxIgnitionCut * (engineDeload / M.maxEngineDeload)
        else
            engine.ignitionCoef = 1
        end
        
        if not engineOn then
            -- engine died?

            timeSinceInit = 0
            nextState = "init"
        elseif electrics.values.evMode == 1 then
            gui.message({txt = "EV mode enabled"}, 2, "vehicle.hybrid.evMode")
            nextState = "evmode"
        elseif electrics.values.chargeMode == 1 then
            if gear == "P" then
                gui.message({txt = "Charge mode enabled"}, 2, "vehicle.hybrid.chargeMode")
                chargeThrottle = 0
                nextState = "chargemode"
            else
                gui.message({txt = "Charge mode can only be enabled in P gear"}, 2, "vehicle.hybrid.chargeMode")
                electrics.values.chargeMode = 0
            end
        else
            if engineDemandedOn then
                timeSinceEngineCanShutOff = 0
            else
                if gear ~= "N" then -- engine keeps state in N
                    timeSinceEngineCanShutOff = timeSinceEngineCanShutOff + dt

                    if timeSinceEngineCanShutOff > 1 then
                        engine.ignitionCoef = 0 -- Kill engine
                        engine.isDisabled = true -- Prevent the user from manually restarting the engine
                        electrics.values.checkengine = false -- Keep this from coming on (usually tied to engine.isDisabled)

                        nextState = "stopped"
                    end
                end
            end
        end
    elseif state == "chargemode" then
        local inverseRelativePowerLevel = maprange(batteryLevel, 0.85, 0.9, 1.0, 0.0)
        local maxFastChargePower = M.maxFastChargePower or gearbox.maxMG1Power
        local curGenOutput = -min(totalPowerOutKw, 0)
        local targGenOutput = inverseRelativePowerLevel * maxFastChargePower

        if lastState ~= state then
            M.chargeThrottlePid:reset()
        end
        
        chargeThrottle = clamp(M.chargeThrottlePid:get(targGenOutput, curGenOutput, dt), 0, 1)

        if gear ~= "P" or electrics.values.evMode == 1 or batteryLevel >= 0.898 then
            electrics.values.chargeMode = 0
        end

        if electrics.values.chargeMode ~= 1 then
            gui.message({txt = "Charge mode disabled"}, 2, "vehicle.hybrid.chargeMode")
            nextState = "init"
        end
    elseif state == "stopped" then
        timeSinceEngineCanShutOff = 0
        engine.ignitionCoef = 0
        electrics.values.checkengine = false -- Keep this from coming on (usually tied to engine.isDisabled)
        electrics.values.iceThrottle = 0
        
        if electrics.values.chargeMode == 1 then
            if gear == "P" then
                engineDemandedOn = true
            else
                gui.message({txt = "Charge mode can only be enabled in P gear"}, 2, "vehicle.hybrid.chargeMode")
                electrics.values.chargeMode = 0
            end
        end

        if electrics.values.evMode == 1 then
            gui.message({txt = "EV mode enabled"}, 2, "vehicle.hybrid.evMode")
            nextState = "evmode"
        elseif gear ~= "N" and engineDemandedOn then
            engine.isDisabled = false -- allow engine to restart

            if electrics.values.rpm < engine.idleRPM then
                nextState = "startup"
                timeSinceEngineStart = 0
                startingTime = 0
            else
                engine.ignitionCoef = 1 -- engine already spinning, engage ignition
                nextState = "running"
            end
        end
    elseif state == "startup" then
        local actualRPM = electrics.values.rpm
        local rpmError = targetEngineRpm - actualRPM
        local targetStarterTorque = maprange(rpmError, 0, 200, 0, M.engineStarterTorque)
        
        engine.ignitionCoef = maprange(rpmError, 500, 300, 0, 1)
        electrics.values.iceThrottle = 0
        M.engineThrottleSmoother:reset()
        startingTime = startingTime + dt
        
        if engineOn then
            M.driveThrottlePid:setILock(false)
            
            local targetIceThrottle = M.driveThrottlePid:get(targetEngineRpm, actualRPM, dt)
            
            electrics.values.iceThrottle = clamp(targetIceThrottle, 0, 1)
            engine.ignitionCoef = 1
            
            if timeSinceEngineStart > M.engineStartupIdleTime then
                targetStarterTorque = 0
                nextState = "running"
            end
        else
            if startingTime > 3 then
                -- Something's not right; retry at init
                
                targetStarterTorque = 0
                timeSinceInit = 0
                nextState = "init"
            end
        end
        
        local starterTorque = M.engineStarterSmoother:get(targetStarterTorque, dt)
        local starterCoef
        
        gearbox:setMG1PowerOverride(maprange(batteryLevel, 0.05, 0.1, 0, gearbox.maxMG1Power))
        gearbox:setMG1TorqueDirection(1)
        
        if gearbox.sunGearAV < (-gearbox.generatorMinFullLoadAV * 5) then
            starterCoef = -starterTorque
        else
            starterCoef = starterTorque
        end 
        
        gearbox:setRequestedMG1Torque(starterCoef)
    end
    
    lastState = state
end

local function updateBattery(dt)
    local mg1Kw = gearbox.motorGenerator1Power
    local mg2Kw = gearbox.motorGenerator2Power
    
    -- If negative (generating power), multiply by generator efficiency (power loss from motors to battery)
    -- If positive (consumint power), divide by generator efficiency (power loss from battery to motors)
    if mg1Kw < 0 then
        mg1Kw = mg1Kw * M.regenEfficiency
    else
        mg1Kw = mg1Kw / M.regenEfficiency
    end
    
    if mg2Kw < 0 then
        mg2Kw = mg2Kw * M.regenEfficiency
    else
        mg2Kw = mg2Kw / M.regenEfficiency
    end
    
    totalPowerOutKw = M.idlePowerDraw + mg1Kw + mg2Kw

    local totalPowerOutKj = totalPowerOutKw * dt

    debugutil.putValue("Battery power (kW)", totalPowerOutKw)
    debugutil.putValue("Generator power (kW)", mg1Kw)

    battery.storedEnergy = mathutil.clamp(battery.storedEnergy - totalPowerOutKj * 1000, 0, battery.initialStoredEnergy)
end

local function updateSounds(dt)
    -- Update sounds
    local mg1Power = abs(gearbox.motorGenerator1Power)
    local mg2Power = abs(gearbox.motorGenerator2Power)
    local mg1AV = abs(gearbox.sunGearAV)
    local mg2AV = abs(gearbox.ringGearAV)
    local mg1InverterPitch = maprange(mg1AV, M.inverterPitchMinSpeed, M.inverterPitchMaxSpeed, M.inverterMinPitch, M.inverterMaxPitch)
    local mg1InverterEffort = mg1Power / M.inverterMaxVolumeKw
    local mg1InverterVolume = M.inverterBaseVolume * clamp(mg1InverterEffort, 0.0, 1.0)
    local mg2InverterPitch = maprange(mg2AV, M.inverterPitchMinSpeed, M.inverterPitchMaxSpeed, M.inverterMinPitch, M.inverterMaxPitch)
    local mg2InverterEffort = mg2Power / M.inverterMaxVolumeKw
    local mg2InverterVolume = M.inverterBaseVolume * clamp(mg2InverterEffort, 0.0, 1.0)

    obj:setVolumePitch(sounds.mg1Whine, mg1InverterVolume, M.mg1PitchSmoother:get(mg1InverterPitch, dt))
    obj:setVolumePitch(sounds.mg2Whine, mg2InverterVolume, M.mg2PitchSmoother:get(mg2InverterPitch, dt))

    local motorPitch =
        maprange(
        speedMph,
        M.motorPitchMinSpeed,
        M.motorPitchMaxSpeed,
        M.motorMinPitch,
        M.motorMaxPitch,
        false
    )
    local motorVolume =
        maprange(
        speedMph,
        M.motorVolumeMinSpeed,
        M.motorVolumeMaxSpeed,
        0,
        M.motorBaseVolume,
        false
    )

    obj:setVolumePitch(sounds.motorRamp, motorVolume, motorPitch)
end

local function updateGUI(dt)
    local maxKw
    
    if battery and battery.jbeamData and (battery.jbeamData.maxPowerOutput or battery.jbeamData.maxPowerInput) then
        local maxOutput = battery.jbeamData.maxPowerOutput or 0
        local maxInput = battery.jbeamData.maxPowerInput or 0
        
        maxKw = max(maxOutput, maxInput)
    else
        maxKw = gearbox and max(gearbox.maxMG1Power, gearbox.maxMG2Power) or 0
    end
    
    local str = {
        soc = batteryLevel,
        kw = totalPowerOutKw,
        maxKw = maxKw,
        minSoc = state == "evmode" and 0.15 or M.minBatteryPowerLevel,
        maxSoc = M.maxBatteryPowerLevel
    }

    gui.send("hybrid", str)
    debugutil.sendStreams()
end

local function updateGFX(dt)
    updateValues(dt)

    local iceThrottle = 0.0
    
    if gearbox and engine and battery and clutch then
        calculateTorqueStatistics()
        
        -- Once we know we have all the required components for running,
        -- we can start to perform our (very complex and abundant) hybrid
        -- calculations
        
        -- Calculate some rudimentary locals
        local actualSpeedSign = mathutil.sign(electrics.values.avgWheelAV)
        local desiredSpeedSign = gear == "R" and -1 or 1
        
        -- Pull in some measurements on the gearbox
        local gearboxMaxMG1Torque = gearbox:getMaxMG1Torque()
        local gearboxMaxMG2Torque = gearbox:getMaxMG2Torque()
        local gearboxMaxMG1TorqueUnchecked = gearbox:getMaxMG1Torque(true)
        local gearboxMaxMG2TorqueUnchecked = gearbox:getMaxMG2Torque(true)
        local gearboxMG1AV = abs(gearbox.sunGearAV)
        local gearboxMG2AV = abs(gearbox.ringGearAV)
        
        -- Calculate the coefficient of battery charge left (more power from the battery
        -- can be used at low state-of-charge in EV mode)
        local batteryWorkingRatio
        
        if state == "evmode" then
            batteryWorkingRatio = maprange(batteryLevel, 0.15, M.minBatteryPowerLevel)
        else
            batteryWorkingRatio = getProportionalBatteryRatio()
        end
        
        -- Calculate battery power limits
        local maxBatteryPower = ((battery.jbeamData.maxPowerOutput or 1e5) - M.idlePowerDraw)
        local availableBatteryPowerCoef = maprange(batteryWorkingRatio, 0, M.relativeBatteryLevelForMaxPower)
        local availableBatteryPower = availableBatteryPowerCoef * maxBatteryPower
        
        -- If there is a maximum power input on the battery, we can't exceed it while regenerating
        -- Instead, we have to figure out the maximum regen coefficient that we can apply without
        -- exceeding the battery's max power input
        local maxRegenPercentOfMaxMG2Power = 1
        
        if battery.jbeamData.maxPowerInput then
            local gearboxMaxRegenKw = mathutil.motorTorqueToKw(gearboxMaxMG2Torque, gearboxMG2AV)
            local batteryPowerInputCoef = maprange(batteryLevel, 0.9, 0.95, 1, 0)
            local batteryPowerInputAvailable = battery.jbeamData.maxPowerInput * batteryPowerInputCoef
            
            -- Calculate what percentage of regen we can use, taking into account
            -- the maximum amount of power the battery can currently accept, minus
            -- however much power the engine is putting into the battery
            maxRegenPercentOfMaxMG2Power = (batteryPowerInputAvailable) / (gearboxMaxRegenKw * M.regenEfficiency)
        end
        
        local maxRegen = not disableRegen and min(M.maxRegenCoef, maxRegenPercentOfMaxMG2Power) or 0
        
        -- Calculate some throttle input variables
        local throttleOffset = max(maxRegen * (gear == "L" and 0.25 or M.evCoastRegen), 0.0001)
        local posThrottle = math.pow( maprange(throttle, throttleOffset, 1), 1 / M.throttleCurveExponent )
        local negThrottle = maprange(throttle, throttleOffset, 0)
        local requestedThrottle = posThrottle
        local creepThrottle = 0
        local requestedRegen = 0

        -- Fill in some debug values
        debugutil.putValue("Battery SOC % (usable)", math.floor(getProportionalBatteryRatio() * 100))
        debugutil.putValue("Throttle % EV thresh.", math.floor(throttle * 100 / M.maxEvOnlyThrottle))

        if input.brake > 0 then
            -- Updating while brake pedal is applied
            
            if gear == "N" or gear == "P" then
                -- No regen in N or P
                
                for i = 0, wheels.wheelCount - 1, 1 do
                    local wheel = wheels.wheels[i]

                    if wheel then
                        -- Reset brake split coef
                        wheel.brakeInputSplit = initialWheelBrakeSplit[i][1]
                        wheel.brakeSplitCoef = initialWheelBrakeSplit[i][2]
                    end
                end

                debugutil.putValue("Friction % of full brakes", math.floor(input.brake * 100))
            else
                -- Blend friction/regen brakes in any other gear
                
                local maxBrakeTorque = 0.0
                local absActive = false

                for i = 0, wheels.wheelCount - 1, 1 do
                    local wheel = wheels.wheels[i]

                    if wheel then
                        -- Find total braking torque
                        if wheel.brakeTorque > maxBrakeTorque then
                            maxBrakeTorque = wheel.brakeTorque
                        end

                        -- Determine if ABS active
                        if wheel.absActive then
                            absActive = true
                        end
                    end
                end

                if disableRegen then
                    if not absActive then
                        timeSinceAbsHit = timeSinceAbsHit + dt

                        if timeSinceAbsHit > 1.0 then
                            disableRegen = false
                            timeSinceAbsHit = 0
                        end
                    end
                else
                    if absActive then
                        timeSinceAbsHit = timeSinceAbsHit + dt

                        if timeSinceAbsHit > 0.5 then
                            disableRegen = true
                            timeSinceAbsHit = 0
                        end
                    else
                        timeSinceAbsHit = 0
                    end
                end

                -- Combine regen input from brake pedal and throttle pedal (one-pedal driving)
                local regenBrakePedal = maprange(input.brake, 0, M.regenPercentOfBrakingForce)
                local regenAccelPedal = gear == "L" and negThrottle or 0
                
                desiredRegen = clamp(regenBrakePedal + regenAccelPedal, 0, 1) * maprange(speedMph, 0, M.minRegenFadeSpeed)
                requestedRegen = maxRegen * desiredRegen

                -- Calculate amount of friction brakes needed to cover brake demand
                local actualRegenCoef = requestedRegen < 0.01 and 0 or clamp((gearbox.motorGenerator2Torque * actualSpeedSign) / (-requestedRegen * gearboxMaxMG2Torque), 0, 1)
                local actualEffectiveRegen = actualRegenCoef * maxRegen * M.regenPercentOfBrakingForce
                local brakesCoveredByRegen = maprange(input.brake + (regenAccelPedal * M.regenPercentOfBrakingForce), actualEffectiveRegen, 1, 1, 0)
                local neededFrictionBrake = maprange(input.brake + (regenAccelPedal * M.regenPercentOfBrakingForce), brakesCoveredByRegen, 1)

                debugutil.putValue("Max Regen", maxRegen)
                debugutil.putValue("Regen % of full brakes", math.floor(actualEffectiveRegen * 100))
                debugutil.putValue("Regen % of full regen", math.floor(actualRegenCoef * 100))
                debugutil.putValue("Friction % of full brakes", math.floor(neededFrictionBrake * 100))

                -- Apply more friction brakes on wheels without regen to balance them
                for i = 0, wheels.wheelCount - 1, 1 do
                    local wd = wheels.wheels[i]

                    wd.brakeInputSplit = 1 - brakesCoveredByRegen

                    if wd.isPropulsed then
                        -- Propulsed wheels are affected by regenerative braking

                        wd.brakeSplitCoef = initialWheelBrakeSplit[i][2] * max(neededFrictionBrake - actualRegenCoef, 0)
                    else
                        -- Unpropulsed wheels are not

                        wd.brakeSplitCoef = initialWheelBrakeSplit[i][2] * neededFrictionBrake
                    end
                end
            end
        else
            disableRegen = false

            -- Regenerative braking when letting off accelerator (tiny bit in D, full in L)
            local zeroThrottleRegen = gear == "L" and 1 or M.evCoastRegen
            
            desiredRegen = negThrottle * zeroThrottleRegen * maprange(speedMph, 0, M.minRegenFadeSpeed)
            requestedRegen = maxRegen * desiredRegen
        end

        if input.brake < 0.25 and electrics.values.parkingbrake < 1 and gear ~= "P" and gear ~= "N" then
            -- apply creep throttle
            
            local positiveSpeedCreep = maprange(speedMph, 0, 5, M.evCreepThrottle, 0)
            local hillAssistCreep = maprange(speedMph * actualSpeedSign * desiredSpeedSign, 0, -2, 0, M.maxHillAssistThrottle)
            
            creepThrottle = max(positiveSpeedCreep, hillAssistCreep)
            requestedThrottle = min(requestedThrottle + creepThrottle, 1)
        end
        
        local mg1Dir = 1
        local mg1Torque = 0.0
        local mg2Torque = 0.0
        local requestedChargePower = 0
        
        if gear == "N" then
            iceThrottle = throttle
        elseif state == "chargemode" then
            iceThrottle = chargeThrottle
            
            local chargeTargetRpm = M.fastChargeRpm or M.engineMaxRpm
            local desiredGenPowerCoef = maprange(electrics.values.rpm, chargeTargetRpm - M.engineLoadRpmTolerance, chargeTargetRpm)
            
            mg1Torque = -desiredGenPowerCoef * gearboxMaxMG1TorqueUnchecked
        else
            -- TOO MANY CALCULATIONS, SEND HELP
            
            -- Throttle-related variables
            local normalizedThrottle = requestedThrottle
            local requestedTorque = 0
            local requestedElectricTorque = 0
            local requestedEngineTorque = 0
            local targetEngineRpmCoef = 0
            local engineLoad = 0
            
            -- Determine which drive mode we should be in based on whether or not MG1 can provide generation drag
            local modeZeroLatchTest = gearbox.sunGearAV > gearbox.generatorMinFullLoadAV * 3
            local modeOneLatchTest = gearbox.sunGearAV < gearbox.generatorMinFullLoadAV * 2
            
            if M.latchDriveModeOne:test(modeOneLatchTest, dt) then
                if ecvtDriveMode == 0 then
                    M.latchDriveModeZero:reset()
                    ecvtDriveMode = 1
                end
            end
            if M.latchDriveModeZero:test(modeZeroLatchTest, dt) then
                if ecvtDriveMode == 1 then
                    M.latchDriveModeOne:reset()
                    ecvtDriveMode = 0
                end
            end
            
            -- Prevent gearbox or vehicle from going overspeed
            local mg1OverspeedCoef = 0
            local mg2OverspeedCoef = 0
            local vehicleOverspeedCoef = 0
            local maxVehicleSpeed = gear == "R" and M.maxVehicleSpeedReverse or M.maxVehicleSpeed
            local maxVehicleSpeedFalloff = gear == "R" and M.maxVehicleSpeedFalloffReverse or M.maxVehicleSpeedFalloff
            
            -- Limit vehicle speed to spec (with falloff)
            if type(maxVehicleSpeed) == "number" then
                vehicleOverspeedCoef = maprange(speedMph, maxVehicleSpeed, maxVehicleSpeed + maxVehicleSpeedFalloff)
            end
            
            -- Prevent sun gear (MG1) from overspeeding in either direction
            if gearbox.sunGearAV >= 0 then
                mg1OverspeedCoef = maprange(gearbox.sunGearAV, gearbox.maxMG1AV, gearbox.maxMG1AV + 100)
            else
                mg1OverspeedCoef = maprange(gearbox.sunGearAV, gearbox.minMG1AV, gearbox.minMG1AV - 100)
            end
            
            -- Prevent ring gear (MG2) from overspeeding in either direction
            if gearbox.ringGearAV >= 0 then
                mg2OverspeedCoef = maprange(gearbox.ringGearAV, gearbox.maxMG2AV, gearbox.maxMG2AV + 100)
            else
                mg2OverspeedCoef = maprange(gearbox.ringGearAV, gearbox.minMG2AV, gearbox.minMG2AV - 100)
            end
            
            local overspeedCoef = M.throttleLimiterSmoother:get(mathutil.maxall(
                mg1OverspeedCoef,
                mg2OverspeedCoef,
                vehicleOverspeedCoef
            ), dt)
            
            normalizedThrottle = min(normalizedThrottle, 1 - overspeedCoef)
            
            -- Calculate RPM range for engine based on required sun gear speeds
            local minRpm = max(M.engineMinIdleRpm, (gearbox:getMinCarrierAV() / gearbox.inputGearRatio) * constants.avToRPM)
            local maxRpm = min(M.engineMaxRpm, (gearbox:getMaxCarrierAV() / gearbox.inputGearRatio) * constants.avToRPM)
            local actualRpm = round(electrics.values.rpm)
            
            -- Figure out max possible engine torque at its current RPM, as well as the torque it's currently
            -- applying on\the different transmission components
            local achievableEngineTorque = engineutil.getAchievableEngineTorque(engine)
            local actualEngineTorque = min(achievableEngineTorque, abs(engine.outputTorque1))
            local actualEngineTorqueOnCarrier = actualEngineTorque / gearbox.inputGearRatio
            local actualEngineTorqueOnRing = abs(gearbox:getRingTorqueFromCarrierTorque(actualEngineTorqueOnCarrier))
            local actualEngineTorqueOnSun = abs(gearbox:getSunTorqueFromCarrierTorque(actualEngineTorqueOnCarrier))
            
            maxRpm = max(minRpm, maxRpm)
            
            local isInDriveGear = gear ~= "P" and gear ~= "N"
            
            -- Calculate requested charging torque now so it can be referenced later
            if state == "running" then
                -- If the engine is running and the battery is low, we need to charge it with extra generation torque
                local maxChargePower = ecvtDriveMode == 0 and M.maxModeZeroChargePower or M.maxModeOneChargePower
                local chargePowerCoefRegen = 1 - desiredRegen
                local chargePowerCoefAccel = maprange(normalizedThrottle, 1, 0)
                local targetChargePower = chargePowerCoefRegen * chargePowerCoefAccel * maprange(batteryWorkingRatio, 0.5, 0, 0, maxChargePower)
                
                if isInChargeup then
                    targetChargePower = max(targetChargePower, M.chargeupMinPower)
                end
                
                requestedChargePower = targetChargePower
            end
            
            if isInDriveGear then
                -- Update while in a driving gear (anything other than Park or Neutral)
                
                -- Figure out some general values about what the electric subsystem can currently provide
                -- We re-calculate the maximum MG2 torque here so we can exclude power needed for charging
                local maxMG2Torque = gearbox:getMaxMG2Torque(availableMG2Power - requestedChargePower)
                local availableEvTorque = min(maxMG2Torque, torqueData.maxElectricTorque)
                
                -- Map throttle to total capable torque output
                requestedTorque = normalizedThrottle * torqueData.maxCombinedTorque
                
                -- Calculate power split from battery level and distribute torque
                local maxPowerSplit = availableEvTorque / max(1e-20, requestedTorque)
                local fullEvPower = state ~= "running" or gear == "R" or posThrottle < creepThrottle
                local powerSplit = fullEvPower and 1 or min(maxPowerSplit, maprange(batteryWorkingRatio, 0, 1, 0, M.maxPowerSplitRatio))
                local splitEngine = 1 - powerSplit
                local splitEV = powerSplit
                
                requestedEngineTorque = splitEngine * requestedTorque
                requestedElectricTorque = splitEV * requestedTorque
                targetEngineRpmCoef = maprange(requestedEngineTorque, 0, torqueData.maxEngineTorque)
                
                -- Figure out if engine needs to be on due to throttle/torque demands
                if gear ~= "R" then
                    engineDemandThrottle = throttle > M.maxEvOnlyThrottle 
                        or requestedEngineTorque > 1e-20
                        or (input.brake >= 0.25 and throttle > 0.02)
                end
                
                if state == "running" then
                    -- While the engine is working on revving up, the main drive motor should compensate for it
                    -- so that the driver feels a more immediate acceleration response.
                    -- It's okay if we do this even if the battery is low (it's a short period of time it will be drawing power)
                    local immediateMG2CompensationNeeded = max(requestedEngineTorque - actualEngineTorqueOnRing, 0)
                    
                    requestedElectricTorque = min(availableEvTorque, requestedElectricTorque + immediateMG2CompensationNeeded)
                end
                
                if input.brake > 0.1 or electrics.values.parkingbrake > 0 then
                    requestedElectricTorque = 0
                end
                
                debugutil.putValue("Available EV Torque", fnum(availableEvTorque))
            else
                requestedTorque = 0
                requestedElectricTorque = 0
                requestedEngineTorque = maprange(normalizedThrottle, M.evCreepThrottle, 1, 0, M.maxParkEngineTorqueCoef * torqueData.maxEngineTorque)
                targetEngineRpmCoef = normalizedThrottle
                engineDemandThrottle = throttle > 0.02
            end
            
            if state == "startup" then
                local minTargetRpm = minRpm
                local maxTargetRpm = maprange(normalizedThrottle, 0, M.evCreepThrottle, minTargetRpm, maxRpm)
                
                targetEngineRpm = M.engineTargetRpmSmoother:get(maprange(targetEngineRpmCoef, 0, 1, minTargetRpm, maxTargetRpm), dt)
            elseif state == "running" then
                -- The torque of MG1 has to match the torque that the engine is currently producing
                -- It also has to be biased to allow the engine to reach its target RPM faster
                
                -- Calculate base engine load
                local requestedChargeTorqueEngine
                
                if ecvtDriveMode == 0 then
                    local maxMG1RegenPower = gearbox:getMaxMG1RegenTorque()
                    local maxMG2RegenPower = gear == "R" and 0 or gearbox:getMaxMG2RegenTorque()
                    
                    -- MG2 is easier to generate power from if it's spinning fast enough, so it gets prioritized
                    -- We bias the maximum a bit so that it's not trying to use 100% MG2 at low speeds
                    local requestedChargePowerRing = min(requestedChargePower, max(0, maxMG2RegenPower / 2))
                    local requestedChargePowerSun = min(maxMG1RegenPower, requestedChargePower - requestedChargePowerRing)
                    local requestedChargeTorqueRing = mathutil.motorKwToTorque(requestedChargePowerRing, gearboxMG2AV)
                    local requestedChargeTorqueSun = mathutil.motorKwToTorque(requestedChargePowerSun, gearboxMG1AV)
                    
                    if gear == "R" then
                        requestedElectricTorque = requestedElectricTorque + requestedChargeTorqueRing
                    else
                        requestedElectricTorque = requestedElectricTorque - requestedChargeTorqueRing
                    end
                    
                    requestedChargeTorqueEngine = abs(gearbox:getCarrierTorqueFromSunTorque(requestedChargeTorqueSun))
                else
                    -- Pre-calculate this, even though the rest of the calculations are quite a ways down
                    local requestedChargeTorqueRing = mathutil.motorKwToTorque(requestedChargePower, gearboxMG2AV)
                    
                    requestedChargeTorqueEngine = abs(gearbox:getCarrierTorqueFromRingTorque(requestedChargeTorqueRing))
                end
                
                requestedEngineTorque = requestedEngineTorque + requestedChargeTorqueEngine
                    
                if isInDriveGear then
                    targetEngineRpmCoef = targetEngineRpmCoef + maprange(requestedChargeTorqueEngine, 0, torqueData.maxEngineTorque)
                end
                
                local achievableEngineTorqueSun = abs(gearbox:getSunTorqueFromCarrierTorque(achievableEngineTorque / gearbox.inputGearRatio))
                local requestedEngineTorqueSun = abs(gearbox:getSunTorqueFromRingTorque(requestedEngineTorque))
                local targetMG1Torque = min(achievableEngineTorqueSun, requestedEngineTorqueSun)
                local minRpmForTorque = engineutil.getBestRpmForTorque(engine, requestedEngineTorque)
                local minTargetRpm = max(minRpm, minRpmForTorque)
                local maxTargetRpm = maprange(normalizedThrottle, 0, M.evCreepThrottle, minTargetRpm, maxRpm)
                
                targetEngineRpm = M.engineTargetRpmSmoother:get(maprange(targetEngineRpmCoef, 0, 1, minTargetRpm, maxTargetRpm), dt)
                
                -- Calculate RPM load bias
                local rpmError = maprange(actualRpm, targetEngineRpm - M.engineLoadRpmTolerance, targetEngineRpm + M.engineLoadRpmTolerance, -1, 1)
                local loadBiasCoef = rpmError * (rpmError > 0 and M.engineMaxPositiveLoadBias or M.engineMaxNegativeLoadBias)
                local loadBiasTorque = loadBiasCoef * gearbox:getMaxMG1Torque(loadBiasCoef < 0)
                
                engineLoad = M.engineLoadSmoother:get(targetMG1Torque, dt) + loadBiasTorque
                
                -- Calculate engine deloading
                local availableEngineDeloadBattery = maprange(batteryWorkingRatio, 0, 0.1, 0, 1)
                local availableEngineDeloadRegen = maprange(requestedRegen, 0, 1, 0, 1)
                local availableEngineDeload = M.maxEngineDeload * mathutil.maxall(availableEngineDeloadBattery, availableEngineDeloadRegen)
                
                if requestedChargePower > 0 then
                    availableEngineDeload = 0
                end
                
                -- Apply engine deloading
                engineDeload = maprange(desiredRegen, 0, 0.05, 0, availableEngineDeload)
                engineLoad = engineLoad - engineDeload
                
                -- Limit engine load based on certain criteria
                local maxEngineLoadRpm = maprange(actualRpm, engine.idleRPM - 200, minTargetRpm)
                local maxEngineLoadStall = maprange(actualRpm, engine.idleRPM - 400, engine.idleRPM - 300)
                local maxEngineLoadOverspeed = maprange(mathutil.maxall(mg1OverspeedCoef, mg2OverspeedCoef, vehicleOverspeedCoef), 1, 0)
                local maxEngineLoadBattery = ecvtDriveMode == 1 and 1 or maprange(batteryLevel, 1, 0.98)
                
                engineLoad = engineLoad * mathutil.minall(maxEngineLoadRpm, maxEngineLoadStall, maxEngineLoadOverspeed, maxEngineLoadBattery)
                
                -- Engine throttle is controlled by the desired output torque of the engine
                -- Engine RPM is controlled by the throttle pedal
                -- Actual load on the engine is biased to achieve the desired RPM
                local targetIceThrottle = M.driveThrottlePid:get(targetEngineRpm, actualRpm, dt)
                
                M.driveThrottlePid:setILock(false)
                iceThrottle = clamp(targetIceThrottle, 0, 1)
            
                debugutil.putValue("Target MG1 Torque", targetMG1Torque)
                debugutil.putValue("Min RPM", minRpm)
                debugutil.putValue("Max RPM", maxRpm)
                debugutil.putValue("Target RPM", targetEngineRpm)
                debugutil.putValue("Actual RPM", actualRpm)
                debugutil.putValue("RPM Error", rpmError)
                debugutil.putValue("Load Bias", loadBiasTorque)
                debugutil.putValue("Target ICE Throttle", targetIceThrottle)
                debugutil.putValue("Actual ICE Throttle", electrics.values.iceThrottle)
                debugutil.putValue("Actual Engine Torque", fnum(actualEngineTorque) .. " (" .. fnum(actualEngineTorqueOnSun) .. " / " .. fnum(actualEngineTorqueOnRing) .. ")")
                debugutil.putValue("Achievable Engine Torque", fnum(achievableEngineTorque) .. " (" .. fnum(achievableEngineTorqueSun) .. ")")
            else
                M.driveThrottlePid:reset()
                M.driveThrottlePid:setILock(true)
                M.engineThrottleSmoother:reset()
                
                -- To shut down engine faster, we apply engine load proportional to the engine RPM (but only if MG1 is spinning positive)
                local engineLoadShutdown = maprange(actualRpm, 10, engine.idleRPM, 0, M.engineSpooldownTorque)
                
                if gearbox.sunGearAV > gearbox.generatorMinFullLoadAV then
                    engineLoad = engineLoadShutdown
                else
                    engineLoad = 0
                end
                
                iceThrottle = 0
                engineDeload = 0
                targetEngineRpm = minRpm
            end
            
            -- Calculate actual torques
            local actualTorque = abs(gearbox.outputTorque1) / gearbox.finalGearRatio
            local actualTorqueEV = abs(gearbox.motorGenerator2Torque)
            local actualTorqueICE = abs(gearbox:getRingTorqueFromCarrierTorque(actualEngineTorqueOnCarrier))
            
            debugutil.putValue("Requested Torque", fnum(requestedTorque) .. " (" .. fnum(requestedElectricTorque) .. " / " .. fnum(requestedEngineTorque) .. ")")
            debugutil.putValue("Requested Charge Power", requestedChargePower)
            debugutil.putValue("Actual Torque", fnum(actualTorque) .. " (" .. fnum(actualTorqueEV) .. " / " .. fnum(actualTorqueICE) .. ")")
            debugutil.putValue("Available MG1 Power", fnum(availableMG1Power))
            debugutil.putValue("Available MG2 Power", fnum(availableMG2Power))
            debugutil.putValue("ICE Throttle", iceThrottle)
            debugutil.putValue("Requested Regen", requestedRegen)
            debugutil.putValue("Engine Load", engineLoad)
            debugutil.putValue("Engine Deload", engineDeload)
            
            if state ~= "running" then
                ecvtDriveMode = 0
                M.latchDriveModeZero:reset()
                M.latchDriveModeOne:reset()
            end
            
            -- Determine actual requested torque from requested regen coef
            local regenTorque = requestedRegen * gearboxMaxMG2TorqueUnchecked
            
            -- Determine MG2 adjustment/compensation values
            if state == "startup" and isInDriveGear then
                local starterTorqueMG2Compensation = gearbox:getRingTorqueFromSunTorque(gearbox.motorGenerator1Torque)
                
                mg2TorqueAdjustment = starterTorqueMG2Compensation
            end
            
            if ecvtDriveMode == 0 then
                -- engine load can be achieved using gen drag on MG1
                
                mg1Torque = -engineLoad
                mg1Dir = 1
                mg2Torque = requestedElectricTorque - regenTorque + mg2TorqueAdjustment
            else
                -- MG1 spinning too slow (or backwards) for gen drag; use negative power instead
                -- in this case, we have to regen off of MG2 to provide power for MG1
                -- (thus we have to limit MG1 to what MG2 is able to counteract)
                
                local engineLoadPower = mathutil.motorTorqueToKw(max(0, engineLoad), gearboxMG1AV)
                local regenTorqueForEngineLoad = mathutil.motorKwToTorque(engineLoadPower, gearboxMG2AV)
                local regenTorqueForCharge = mathutil.motorKwToTorque(requestedChargePower, gearboxMG2AV)
                
                mg1Torque = abs(engineLoad)
                mg1Dir = -sign(engineLoad)
                mg2Torque = requestedElectricTorque - (regenTorque + regenTorqueForEngineLoad + regenTorqueForCharge) + mg2TorqueAdjustment
            end
        end
        
        if state ~= "evmode" then
            gearbox:setInputDisengaged(gear == "N")
        end
        
        debugutil.putValue("Input Clutch", fnum(electrics.values[clutch.electricsClutchRatioName]))
        
        if electrics.values[clutch.electricsClutchRatioName] < 0.001 then
            -- ICE is unclutched from transmission and drag is applied to MG1 when necessary
            -- to prevent overspeed
            local dragCoef
            
            if gearbox.sunGearAV >= 0 then
                dragCoef = maprange(gearbox.sunGearAV, gearbox.maxMG1AV - 200, gearbox.maxMG1AV - 100)
            else
                dragCoef = maprange(gearbox.sunGearAV, gearbox.minMG1AV + 200, gearbox.minMG1AV + 100)
            end
            
            
            gearbox:setMG1TorqueDirection(1)
            gearbox:setRequestedMG1Torque(dragCoef * -mathutil.sign(gearbox.sunGearAV) * M.neutralSunGearDragTorque)
        else
            gearbox:setMG1TorqueDirection(mg1Dir)
            gearbox:setRequestedMG1Torque(mg1Torque)
        end
        
        gearbox:setRequestedMG2Torque(mg2Torque)
        
        -- Calculate motor power restrictions
        local mg1GeneratedPower = max(-gearbox.motorGenerator1Power, 0)
        local mg2GeneratedPower = max(-gearbox.motorGenerator2Power, 0)
        
        availableMG1Power = min(gearbox.maxMG1Power, availableBatteryPower + mg2GeneratedPower)
        
        if ecvtDriveMode == 0 then
            availableMG2Power = min(gearbox.maxMG2Power, availableBatteryPower + mg1GeneratedPower)
        else
            availableMG2Power = min(gearbox.maxMG2Power, availableBatteryPower)
        end
        
        -- Limit the amount of power MG1 and MG2 are allowed to consume (so they don't overdraw the battery)
        gearbox:setMG1PowerOverride(M.mg1MaxPowerSmoother:get(availableMG1Power, dt))
        gearbox:setMG2PowerOverride(M.mg2MaxPowerSmoother:get(availableMG2Power, dt))
        
        updateState(dt)
        updateBattery(dt)
    end
    
    electrics.values.iceThrottle = M.engineThrottleSmoother:get(iceThrottle, dt)

    if battery then
        debugutil.putValue("Battery SOC %", tostring(battery.remainingRatio * 100))
    end

    debugutil.putValue("Input Throttle", (throttle or 0))
    debugutil.putValue("ICE Throttle (True)", electrics.values.iceThrottle)
    debugutil.putValue("Throttle Aggr.", throttleAggressiveness)

    if nextState then
        state = nextState
    end

    debugutil.putValue("ICE Torque", engine.outputTorque1)
    debugutil.putValue("ICE HP", mathutil.motorTorqueToKw(engine.outputTorque1, engine.outputAV1) * 1.34102)
    debugutil.putValue("Charge Throttle", chargeThrottle)
    debugutil.putValue("MG2 Adjustment", mg2TorqueAdjustment)

    updateGUI(dt)
    updateSounds(dt)
end

local function init(jbeamData)
    M.jbeamData = jbeamData
    
    M.startEvMode = jbeamData.startEvMode == true
    M.forceEvMode = jbeamData.forceEvMode == true
    
    M.maxEvOnlyPower = jbeamData.maxEvOnlyPower or 30 -- kW
    M.maxEvOnlySpeed = jbeamData.maxEvOnlySpeed or 38 -- MPH
    M.maxEvOnlyThrottle = jbeamData.maxEvOnlyThrottle or 0.65 -- ratio
    M.maxEvOnlyThrottleAggressiveness = jbeamData.maxEvOnlyThrottleAggressiveness or 20
    M.maxEvOnlyPitch = jbeamData.maxEvOnlyPitch or math.pi -- 180 degrees pitch by default; will never happen (effectively disabled)
    M.neutralSunGearDragTorque = jbeamData.neutralSunGearDragTorque or 10
    
    M.minRegenFadeSpeed = jbeamData.minRegenFadeSpeed or 8 -- MPH
    M.maxRegenFadeSpeed = jbeamData.maxRegenFadeSpeed or 38 -- MPH
    
    M.idlePowerDraw = jbeamData.idlePowerDraw or 1 -- kW
    M.minBatteryPowerLevel = 0.25 -- ratio; low end of max-kW slope
    M.maxBatteryPowerLevel = 0.7 -- ratio; high end of max-kW slope
    M.relativeBatteryLevelForMaxPower = jbeamData.relativeBatteryLevelForMaxPower or 0.4 -- %
    M.maxFastChargePower = jbeamData.maxFastChargePower or 40 -- kW
    M.fastChargeRpm = jbeamData.fastChargeRpm
    
    M.enableAutoStop = jbeamData.enableAutoStop ~= false
    M.autoStopBrakeThreshold = jbeamData.autoStopBrakeThreshold or 0
    M.autoStopBrakeTime = jbeamData.autoStopBrakeTime or 0
    M.engineMinRunTime = jbeamData.engineMinRunTime or 4 -- seconds
    
    M.engineMaxRpm = jbeamData.engineMaxRpm
    M.engineStartupIdleTime = jbeamData.engineStartupIdleTime or 0.1
    M.engineStarterTorque = jbeamData.engineStarterTorque or 60
    M.engineSpooldownTorque = jbeamData.engineSpooldownTorque or 50
    M.maxModeZeroChargePower = jbeamData.maxModeZeroChargePower or 25
    M.maxModeOneChargePower = jbeamData.maxModeOneChargePower or 40
    M.chargeupMinPower = jbeamData.chargeupMinPower or 10
    M.engineMinIdleRpm = jbeamData.engineMinIdleRpm
    M.engineLoadRpmTolerance = jbeamData.engineLoadRpmTolerance or 1000
    M.engineMaxPositiveLoadBias = jbeamData.engineMaxPositiveLoadBias or 0.4
    M.engineMaxNegativeLoadBias = jbeamData.engineMaxNegativeLoadBias or 0.85
    M.maxPowerSplitRatio = jbeamData.maxPowerSplitRatio or 0.85
    M.maxEngineDeload = jbeamData.maxEngineDeload or 0.01
    M.engineDeloadMaxIgnitionCut = jbeamData.engineDeloadMaxIgnitionCut or 0.4
    M.maxParkEngineTorqueCoef = jbeamData.maxParkEngineTorqueCoef or 0.5
    M.maxVehicleSpeed = jbeamData.maxVehicleSpeed
    M.maxVehicleSpeedReverse = jbeamData.maxVehicleSpeedReverse or 20
    M.maxVehicleSpeedFalloff = jbeamData.maxVehicleSpeedFalloff or 5
    M.maxVehicleSpeedFalloffReverse = jbeamData.maxVehicleSpeedFalloffReverse or M.maxVehicleSpeedFalloff
    
    M.minThrottleAggressivenessRamp = jbeamData.minThrottleAggressivenessRamp or 1.5
    M.maxThrottleAggressivenessRamp = jbeamData.maxThrottleAggressivenessRamp or 8
    M.minThrottleAggressivenessFactor = jbeamData.minThrottleAggressivenessFactor or 1
    M.maxThrottleAggressivenessFactor = jbeamData.maxThrottleAggressivenessFactor or 10
    M.throttleCurveExponent = jbeamData.throttleCurveExponent or 1.1
    
    M.maxRegenCoef = jbeamData.maxRegenCoef or 0.95
    M.regenEfficiency = jbeamData.regenEfficiency or 0.93
    M.regenPercentOfBrakingForce = jbeamData.regenPercentOfBrakingForce or 0.2
    
    M.evCreepThrottle = jbeamData.evCreepThrottle or 0.18
    M.evCoastRegen = jbeamData.evCoastRegen or 0.05
    M.maxHillAssistThrottle = jbeamData.maxHillAssistThrottle or 0.25
    
    M.inverterMaxVolumeKw = jbeamData.inverterMaxVolumeKw or 50
    M.inverterBaseVolume = jbeamData.inverterBaseVolume or 0.65
    M.inverterPitchMinSpeed = jbeamData.inverterPitchMinSpeed or 1
    M.inverterPitchMaxSpeed = jbeamData.inverterPitchMaxSpeed or 1100
    M.inverterMinPitch = jbeamData.inverterMinPitch or 0.01
    M.inverterMaxPitch = jbeamData.inverterMaxPitch or 2.0
    M.motorBaseVolume = jbeamData.motorBaseVolume or 0.45
    M.motorVolumeMinSpeed = jbeamData.motorVolumeMinSpeed or 0.2
    M.motorVolumeMaxSpeed = jbeamData.motorVolumeMaxSpeed or 50
    M.motorPitchMinSpeed = jbeamData.motorPitchMinSpeed or 0
    M.motorPitchMaxSpeed = jbeamData.motorPitchMaxSpeed or 95
    M.motorMinPitch = jbeamData.motorMinPitch or 0.3
    M.motorMaxPitch = jbeamData.motorMaxPitch or 3.4
    
    M.chargeThrottlePid = pid.newPid(
        jbeamData.chargeThrottlePidPFactor or 2, 
        jbeamData.chargeThrottlePidIFactor or 5
    ):setScale(1 / 1000):clampOutput(0, 1)
    M.driveThrottlePid = pid.newPid(
        jbeamData.throttlePidPFactor or 1.7,
        jbeamData.throttlePidIFactor or 0.45,
        jbeamData.throttlePidDFactor or 0.08
    ):setScale(1 / (jbeamData.throttlePidRPMScale or 1000)):clampOutput(0, 1):setDynamicILock(0.6, false):setName("throttle")--:enableDebug()

    -- Lots of temporal smoothers!
    M.throttleSmoother = newTemporalSmoothingNonLinear(
        jbeamData.throttleSmoothingIn or 7.25, 
        jbeamData.throttleSmoothingOut or 7.25)
    M.throttleLimiterSmoother = newTemporalSmoothingNonLinear(jbeamData.throttleLimitSmoothing or 0.95)
    M.engineThrottleSmoother = newTemporalSmoothingNonLinear(jbeamData.engineThrottleSmoothing or 2)
    M.engineLoadSmoother = newTemporalSmoothingNonLinear(
        jbeamData.engineLoadSmoothingIn or 7.5,
        jbeamData.engineLoadSmoothingOut or 12.0
    )
    M.chargeTorqueSmoother = newTemporalSmoothingNonLinear(
        jbeamData.chargeTorqueSmoothingIn or 2.5,
        jbeamData.chargeTorqueSmoothingOut or 1.0
    )
    M.engineTargetRpmSmoother = newTemporalSmoothingNonLinear(
        jbeamData.engineTargetRpmSmoothingIn or 4.5,
        jbeamData.engineTargetRpmSmoothingOut or 8.5
    )
    M.engineStarterSmoother = newTemporalSmoothingNonLinear(
        jbeamData.engineStarterSmoothingIn or 7,
        jbeamData.engineStarterSmoothingOut or 4)
    M.mg1MaxPowerSmoother = newTemporalSmoothingNonLinear(jbeamData.motorPowerSmoothing or 20)
    M.mg2MaxPowerSmoother = newTemporalSmoothingNonLinear(jbeamData.motorPowerSmoothing or 20)
        
    -- Sound smoothers
    M.mg1PitchSmoother = newTemporalSmoothingNonLinear(6)
    M.mg2PitchSmoother = newTemporalSmoothingNonLinear(6)
    
    -- Latches
    M.latchDriveModeZero = latch.newTimedLatch(1.0)
    M.latchDriveModeOne  = latch.newTimedLatch(0.25, true)

    -- Pre-fill debug table with empty strings to set up proper order
    debugutil.clearValues()
    debugutil.putValue("Engine demanded on", "")
    debugutil.putValue("Engine on reason", "")
    debugutil.putValue("State", "")
    debugutil.putValue("Battery power (kW)", "")
    debugutil.putValue("Generator power (kW)", "")
    debugutil.putValue("Battery SOC %", "")
    debugutil.putValue("Battery SOC % (usable)", "")
    debugutil.putValue("Max Regen", "")
    debugutil.putValue("Friction % of full brakes", "")
    debugutil.putValue("Regen % of full brakes", "")
    debugutil.putValue("Regen % of full regen", "")
    debugutil.putValue("Requested Regen", "")
    debugutil.putValue("Charge Throttle", "")
    debugutil.putValue("Input Throttle", "")
    debugutil.putValue("Throttle Aggr.", "")
    debugutil.putValue("ICE Throttle", "")
    debugutil.putValue("Throttle Rate", "")
    debugutil.putValue("Throttle % EV thresh.", "")
    debugutil.putValue("Requested Torque", "")
    debugutil.putValue("Requested Charge Power", "")
    debugutil.putValue("Actual Torque", "")
    debugutil.putValue("Actual Engine Torque", "")
    debugutil.putValue("Achievable Engine Torque", "")
    debugutil.putValue("Available EV Torque", "")
    debugutil.putValue("Target MG1 Torque", "")
    debugutil.putValue("Min RPM", "")
    debugutil.putValue("Max RPM", "")
    debugutil.putValue("Target RPM", "")
    debugutil.putValue("Actual RPM", "")
    debugutil.putValue("RPM Error", "")
    debugutil.putValue("Load Bias", "")
    debugutil.putValue("Target ICE Throttle", "")
    debugutil.putValue("Actual ICE Throttle", "")
    debugutil.putValue("Engine Load", "")
    debugutil.putValue("ICE Throttle (True)", "")
    debugutil.putValue("ICE Torque", "")
    debugutil.putValue("ICE HP", "")
    debugutil.putValue("MG2 Adjustment", "")
end

local function reset()
    timeSinceInit = 0
    state = "init"
    nextState = nil
    
    M.chargeThrottlePid:reset()
    M.driveThrottlePid:reset()
    M.latchDriveModeZero:reset()
    M.latchDriveModeOne:reset()

    init(M.jbeamData)
    M.initSecondStage()
end

local function initSounds()
    sounds.mg1Whine =
        obj:createSFXSource(
        "vehicles/hybrid_common/sounds/inverter.ogg",
        "AudioDefaultLoop3D",
        "mg1Whine",
        engine.engineNodeID
    )
    
    sounds.mg2Whine =
        obj:createSFXSource(
        "vehicles/hybrid_common/sounds/inverter.ogg",
        "AudioDefaultLoop3D",
        "mg2Whine",
        engine.engineNodeID
    )

    sounds.motorRamp =
        obj:createSFXSource(
        "vehicles/hybrid_common/sounds/motor_ramp.ogg",
        "AudioDefaultLoop3D",
        "motorRamp",
        engine.engineNodeID
    )
end

local function initSecondStage()
    local engineTorqueData
    
    engine = powertrain.getDevice("mainEngine")
    gearbox = powertrain.getDevice("gearbox")
    clutch = powertrain.getDevice("clutch")
    battery = energyStorage.getStorage("tractionBattery")

    if not gearbox or gearbox.type ~= "ecvtGearbox" then
        gearbox = nil
    end

    if battery then
        M.minBatteryPowerLevel = battery.jbeamData.minWorkingSoc or M.minBatteryPowerLevel
        M.maxBatteryPowerLevel = battery.jbeamData.maxWorkingSoc or M.maxBatteryPowerLevel

        if not M.maxFastChargePower then
            M.maxFastChargePower = battery.jbeamData.maxPowerInput
        end
    end

    if engine then
        engineTorqueData = engine:getTorqueData()
        
        if not M.jbeamData.engineMaxRpm then
            M.engineMaxRpm = engineTorqueData.maxPowerRPM
        end
        
        if not M.engineMinIdleRpm then
            M.engineMinIdleRpm = engine.idleRPM + 200
        end
    end
    
    if engine and gearbox then
        calculateTorqueStatistics()
        
        debugutil.log("I", "Max Electric Torque: " .. fnum(torqueData.maxElectricTorque))
        debugutil.log("I", "Max Usable Engine Torque: " .. fnum(torqueData.maxEngineTorque))
        debugutil.log("I", "Max Optimal Total Torque: " .. fnum(torqueData.maxCombinedTorque))
        debugutil.log("I", "Max Engine Idle Torque: " .. fnum(torqueData.engineIdleTorque))
        debugutil.log("I", "Engine Max Torque RPM: " .. fnum(torqueData.engineMaxTorqueRpm))
    end

    for i = 0, wheels.wheelCount - 1, 1 do
        if not initialWheelBrakeSplit[i] then
            initialWheelBrakeSplit[i] = {wheels.wheels[i].brakeInputSplit, wheels.wheels[i].brakeSplitCoef}
        end
    end

    if M.startEvMode then
        electrics.values.evMode = 1
    end
end

M.updateGFX = updateGFX
M.init = init
M.initSounds = initSounds
M.initSecondStage = initSecondStage
M.reset = reset

return M
