--[[

Game:    Farming Simulator 19
Script:  ExtendedExhaust
Author:  ThundR
Created: 12/15/2020
Version: 1.0.0.3

Copyright ©2020, Todd Hundersmarck (ThundR)
    - Inspired by FS17 "RollColl" script by modelleicher
    - Complete rewrite

Notes:
    - Adds exhaust features
    - Adds dynamic exhaust particleSystem effects

ChangeLog v1.0.0.3:
    - Adjusted motor load calculations
    - Allow setting certain initial values via vehicle xml
    - Added speed damping to particleSystem rpmInfo section
    - Added throttle setting to rpmInfo section
    - Added a "roll coal" vs. normal exhaust option
    - Added optional debug messages
    - Updated instructions

ChangeLog v1.0.0.2:
    - Added minLoad and maxLoad to "loadInfo" (see vehicleEntries.xml)
    - Slightly increased opacity of default exhaust particle (darker)
    - Slightly increased dispersion width of default exhaust particle
    - Updated instructions

ChangeLog v1.0.0.1:
    - Fixed issue with objectChange not hiding smoke particles properly

--]]

-- luacheck: ignore 111 112 113 212 631

local g_modName = g_currentModName
local g_specName = "spec_"..g_modName..".extendedExhaust"
local showDebug = false

ExtendedExhaust = {}

-- <<[Local Functions]>> --------------------------------------------------------------------------

-- <<msg>>
local function msg(text, ...)
    printf(">> ExtendedExhaust: "..text, ...)
end

-- <<dbgMsg>>
local function dbgMsg(text, ...)
    if showDebug then
        if text == "" then
            print(text)
        else
            printf(">> "..text, ...)
        end
    end
end

-- <<[Initialization]>> ---------------------------------------------------------------------------

-- <<prerequisitesPresent>>
function ExtendedExhaust.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(Motorized, specializations)
end

-- <<registerOverwrittenFunctions>>
function ExtendedExhaust.registerOverwrittenFunctions(vehicleType)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "loadExhaustEffects", ExtendedExhaust.loadExhaustEffects)
end

-- <<registerEventListeners>>
function ExtendedExhaust.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onStartMotor", ExtendedExhaust)
    SpecializationUtil.registerEventListener(vehicleType, "onStopMotor", ExtendedExhaust)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", ExtendedExhaust)
    SpecializationUtil.registerEventListener(vehicleType, "onDelete", ExtendedExhaust)
end

-- <<[Event Functions]>> --------------------------------------------------------------------------

-- <<onUpdateTick>>
function ExtendedExhaust:onUpdateTick(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
    local spec = self[g_specName]
    local motorizedSpec = self.spec_motorized
    local drivableSpec = self.spec_drivable

    if self.isClient then
        if self:getIsMotorStarted() then
            if spec.particleSystems ~= nil then
                local isMotorStarting = false
                if spec.psMotorStartTime > 0 then
                    spec.psMotorStartTime = math.max(0, spec.psMotorStartTime - dt)
                    isMotorStarting = true
                end

                if isMotorStarting then
                    for _, ps in ipairs(spec.particleSystems) do
                        local startInfo = ps.startInfo
                        local defaultInfo = ps.defaultInfo

                        if startInfo.emitScale > 0 then
                            ParticleUtil.setEmittingState(ps, true)

                            local emitScale = defaultInfo.emitScale * startInfo.emitScale
                            ParticleUtil.setEmitCountScale(ps, emitScale)
                            ParticleUtil.setParticleSystemSpeed(ps, startInfo.emitSpeed)
                            ParticleUtil.setParticleSystemTangentSpeed(ps, startInfo.tangent)
                        else
                            ParticleUtil.setEmittingState(ps, false)
                        end
                    end
                else
                    local motor = motorizedSpec.motor
                    local rpmPct = MathUtil.clamp(motor:getLastRealMotorRpm() / motor:getMaxRpm(), 0, 1)
                    local loadPct = motorizedSpec.smoothedLoadPercentage
                    local speedPct = 0 -- set below
                    local acceleration = 1 -- set below
                    local isReverse = false

                    -- get speed related values (if applicable)
                    if drivableSpec ~= nil then
                        acceleration = self:getAccelerationAxis()

                        local currentSpeed = self:getLastSpeed(true) -- get attacher vehicle (if applicable)
                        local maxSpeed = motor:getMaximumForwardSpeed() * 3.6

                        if self:getDrivingDirection() < 0 then
                            maxSpeed = motor:getMaximumBackwardSpeed() * 3.6
                            isReverse = true
                        end

                        if maxSpeed > 0 then
                            speedPct = MathUtil.clamp(currentSpeed / maxSpeed, 0, 1)
                        end
                    end

                    for i, ps in ipairs(spec.particleSystems) do
                        local defaultInfo = ps.defaultInfo
                        local loadInfo = ps.loadInfo
                        local rpmInfo = ps.rpmInfo

                        local rpmSpeedDamping = rpmInfo.speedDampingForward
                        if isReverse then
                            rpmSpeedDamping = rpmInfo.speedDampingReverse
                        end
                        local rpmSpeedFactor = 1 - (speedPct * rpmSpeedDamping)

                        local psRpmPct = MathUtil.clamp(rpmPct * rpmSpeedFactor, 0, 1)
                        local psLoadPct = loadPct

                        if rpmInfo.useThrottle then -- mainly for "roll coal" effect
                            psRpmPct = MathUtil.clamp(psRpmPct * acceleration, 0, 1)

                            if rpmInfo.emitScale == 0 then
                                local loadRpmOffset = (1 - psLoadPct) * psRpmPct
                                psLoadPct = MathUtil.clamp(psLoadPct + loadRpmOffset, 0, 1)
                            end
                        end

                        dbgMsg("Exhaust #%s ***********************************", i)
                        dbgMsg("Rpm Speed Factor: %s", rpmSpeedFactor)
                        dbgMsg("Rpm Percent: %s", rpmPct)
                        dbgMsg("Adjusted Rpm Percent: %s", psRpmPct)
                        dbgMsg("")

                        dbgMsg("Load Percent: %s", loadPct)
                        dbgMsg("Adjusted Load Percent: %s", psLoadPct)
                        dbgMsg("")

                        if (loadInfo.emitScale > 0 and psLoadPct >= loadInfo.emitMin)
                        or (rpmInfo.emitScale > 0 and psRpmPct >= rpmInfo.emitMin)
                        then
                            local loadFactor = 0
                            if loadInfo.emitScale > 0 then
                                loadFactor = math.max(0, psLoadPct - loadInfo.emitMin) / (loadInfo.emitMax - loadInfo.emitMin)
                            end

                            local rpmFactor = 0
                            if rpmInfo.emitScale > 0 then
                                rpmFactor = math.max(0, psRpmPct - rpmInfo.emitMin) / (rpmInfo.emitMax - rpmInfo.emitMin)
                            end

                            local loadEmitScale = MathUtil.lerp(1, loadInfo.emitScale, loadFactor)
                            local rpmEmitScale = MathUtil.lerp(1, rpmInfo.emitScale, rpmFactor)
                            local emitScale = math.max(0, loadEmitScale, rpmEmitScale) -- whichever is higher

                            dbgMsg("Emit Scale: %s", emitScale)
                            dbgMsg("")

                            ParticleUtil.setEmittingState(ps, true)
                            ParticleUtil.setEmitCountScale(ps, defaultInfo.emitScale * emitScale)
                            ParticleUtil.setParticleSystemSpeed(ps, defaultInfo.speed)
                            ParticleUtil.setParticleSystemTangentSpeed(ps, defaultInfo.tangent)
                        else
                            ParticleUtil.setEmittingState(ps, false)
                        end
                    end
                end
            end
        end
    end
end

-- <<onStartMotor>>
function ExtendedExhaust:onStartMotor()
    local spec = self[g_specName]

    if self.isClient then
        if spec.particleSystems ~= nil then
            spec.psMotorStartTime = spec.psMotorStartDuration
            for _, ps in ipairs(spec.particleSystems) do
                ParticleUtil.setEmittingState(ps, true)
            end
        end
    end
end

-- <<onStopMotor>>
function ExtendedExhaust:onStopMotor()
    local spec = self[g_specName]

    if self.isClient then
        if spec.particleSystems ~= nil then
            for _, ps in ipairs(spec.particleSystems) do
                ParticleUtil.setEmittingState(ps, false)
            end
        end
    end
end

-- <<onDelete>>
function ExtendedExhaust:onDelete()
    local spec = self[g_specName]
    if self.isClient then
        if spec.particleSystems ~= nil then
            ParticleUtil.deleteParticleSystems(spec.particleSystems)
        end
    end
end

-- <<[Specialization Overrides]>> -----------------------------------------------------------------

-- <<loadExhaustEffects>>
function ExtendedExhaust:loadExhaustEffects(superFunc, xmlFile)
    local spec = self[g_specName]
    superFunc(self, xmlFile)

    local exhaustKey = "vehicle.motorized.extendedExhaust"

    spec.psMotorStartDuration = Utils.getNoNil(getXMLFloat(xmlFile, exhaustKey.."#motorStartDuration"), 0)
    spec.psMotorStartTime = 0
    spec.particleSystems = {}

    local idx = 0
    while true do
        local psKey = string.format("%s.particleSystems.particleSystem(%d)", exhaustKey, idx)
        if not hasXMLProperty(xmlFile, psKey) then
            break
        end

        local psNode = I3DUtil.indexToObject(self.components, getXMLString(xmlFile, psKey.."#node"))

        -- check visibility
        local nodeIsVisible = true
        while true do
            if Utils.getNoNil(psNode, 0) == 0 or not nodeIsVisible then
                break
            end

            nodeIsVisible = getVisibility(psNode)
            psNode = getParent(psNode)
        end

        if nodeIsVisible then
            local ps1 = {}
            ParticleUtil.loadParticleSystem(xmlFile, ps1, psKey, self.components, false, nil, self.baseDirectory)

            local psScale = getXMLFloat(xmlFile, psKey.."#scale")
            local psGain = getXMLFloat(xmlFile, psKey.."#gain")
            local psSpeed = getXMLFloat(xmlFile, psKey.."#speed")
            local psNormal = getXMLFloat(xmlFile, psKey.."#normal")
            local psTangent = getXMLFloat(xmlFile, psKey.."#tangent")
            local psLifespan = getXMLFloat(xmlFile, psKey.."#lifespan")
            local psEmitScale = math.max(0, Utils.getNoNil(getXMLFloat(xmlFile, psKey.."#emitScale"), 1))

            ps1.defaultInfo = {
                scaleX = math.max(0, Utils.getNoNil(psScale, ParticleUtil.getParticleSystemSpriteScaleX(ps1))),
                scaleY = math.max(0, Utils.getNoNil(psScale, ParticleUtil.getParticleSystemSpriteScaleY(ps1))),
                scaleXGain = math.max(0, Utils.getNoNil(psGain, ParticleUtil.getParticleSystemSpriteScaleXGain(ps1))),
                scaleYGain = math.max(0, Utils.getNoNil(psGain, ParticleUtil.getParticleSystemSpriteScaleXGain(ps1))),
                speed = math.max(0, Utils.getNoNil(psSpeed, ParticleUtil.getParticleSystemSpeed(ps1))),
                normal = MathUtil.clamp(Utils.getNoNil(psNormal, ParticleUtil.getParticleSystemNormalSpeed(ps1)), 0, 1),
                tangent = MathUtil.clamp(Utils.getNoNil(psTangent, ParticleUtil.getParticleSystemTangentSpeed(ps1)), 0, 1),
                lifespan = math.max(0, Utils.getNoNil(psLifespan, ps1.originalLifespan)),
                emitScale = psEmitScale
            }

            local rpmInfo = {
                emitMin     = MathUtil.clamp(Utils.getNoNil(getXMLFloat(xmlFile, psKey..".rpmInfo#emitMin"), 0), 0, 1),
                emitMax     = MathUtil.clamp(Utils.getNoNil(getXMLFloat(xmlFile, psKey..".rpmInfo#emitMax"), 1), 0, 1),
                emitScale   = math.max(0, Utils.getNoNil(getXMLFloat(xmlFile, psKey..".rpmInfo#emitScale"), 0)),
                useThrottle = Utils.getNoNil(getXMLBool(xmlFile, psKey..".rpmInfo#useThrottle"), false),
                speedDampingForward = math.max(0, Utils.getNoNil(getXMLFloat(xmlFile, psKey..".rpmInfo.speedDamping#forward"), 0)),
                speedDampingReverse = math.max(0, Utils.getNoNil(getXMLFloat(xmlFile, psKey..".rpmInfo.speedDamping#reverse"), 0))
            }

            -- validate minimum and maximum rpm
            if rpmInfo.emitScale == 0 then
                rpmInfo.emitMin = 1
                rpmInfo.emitMax = 1
            elseif rpmInfo.emitMin >= rpmInfo.emitMax then
                msg("Minimum rpm cannot be equal or greater than maximum rpm. Using default values!")
                rpmInfo.emitMin = 0
                rpmInfo.emitMax = 1
            end

            ps1.rpmInfo = rpmInfo

            local loadInfo = {
                emitMin   = MathUtil.clamp(Utils.getNoNil(getXMLFloat(xmlFile, psKey..".loadInfo#emitMin"), 0), 0, 1),
                emitMax   = MathUtil.clamp(Utils.getNoNil(getXMLFloat(xmlFile, psKey..".loadInfo#emitMax"), 1), 0, 1),
                emitScale = math.max(0, Utils.getNoNil(getXMLFloat(xmlFile, psKey..".loadInfo#emitScale"), 0)),
            }

            -- validate minimum and maximum load
            if loadInfo.emitScale == 0 then
                loadInfo.emitMin = 1
                loadInfo.emitMax = 1
            elseif loadInfo.emitMin >= loadInfo.emitMax then
                msg("Minimum load cannot be equal or greater than maximum load. Using default values!")
                loadInfo.emitMin = 0
                loadInfo.emitMax = 1
            end

            ps1.loadInfo = loadInfo

            local startInfo = {
                emitScale = math.max(0, Utils.getNoNil(getXMLFloat(xmlFile, psKey..".startInfo#emitScale"), 0)),
                emitSpeed = getXMLFloat(xmlFile, psKey..".startInfo#emitSpeed"),
                tangent = getXMLFloat(xmlFile, psKey..".startInfo#tangent")
            }

            ps1.startInfo = startInfo
            ps1.startInfo.emitSpeed = math.max(0, Utils.getNoNil(startInfo.emitSpeed, ps1.defaultInfo.speed))
            ps1.startInfo.tangent = math.max(0, Utils.getNoNil(startInfo.tangent, ps1.defaultInfo.tangent))

            ParticleUtil.setParticleSystemSpriteScaleX(ps1, ps1.defaultInfo.scaleX)
            ParticleUtil.setParticleSystemSpriteScaleY(ps1, ps1.defaultInfo.scaleY)
            ParticleUtil.setParticleSystemSpriteScaleXGain(ps1, ps1.defaultInfo.scaleXGain)
            ParticleUtil.setParticleSystemSpriteScaleYGain(ps1, ps1.defaultInfo.scaleYGain)
            ParticleUtil.setParticleSystemSpeed(ps1, ps1.defaultInfo.speed)
            ParticleUtil.setParticleSystemNormalSpeed(ps1, ps1.defaultInfo.normal)
            ParticleUtil.setParticleSystemTangentSpeed(ps1, ps1.defaultInfo.tangent)

            ps1.originalLifespan = ps1.defaultInfo.lifespan
            ParticleUtil.setParticleLifespan(ps1, ps1.originalLifespan)

            table.insert(spec.particleSystems, ps1)
        end

        idx = idx + 1
    end

    if #spec.particleSystems == 0 then
        spec.particleSystems = nil
    end
end
