ManureSteamVehicles = {}
ManureSteamVehicles.modDirectory = g_currentModDirectory

ManureSteamVehicles.CONFIG = {
    customParticleFile = "effects/manureSteamVehicleParticle.i3d",
    particleTypes = {"smoke_chimney"},
    scanInterval = 500,
    maxEmitters = 12,
    minFillLevel = 500,
    maxDistance = 120,
    missingScanTolerance = 8,
    heightOffset = 0.15,
}

local function manureSteamVehiclesInfo(text, ...)
    Logging.info("[ManureSteamVehicles] " .. string.format(text, ...))
end

function ManureSteamVehicles.new()
    local self = {}

    setmetatable(self, {__index = ManureSteamVehicles})

    self.scanTimer = 0
    self.pool = {}
    self.activeByKey = {}
    self.rootNode = createTransformGroup("ManureSteamVehicles")
    self.particleTypeName = nil

    link(getRootNode(), self.rootNode)

    return self
end

function ManureSteamVehicles:delete()
    for _, emitter in ipairs(self.pool) do
        if emitter.particleSystem ~= nil then
            ParticleUtil.deleteParticleSystem(emitter.particleSystem)
        end
    end

    self.pool = {}
    self.activeByKey = {}

    if self.rootNode ~= nil and self.rootNode ~= 0 and entityExists(self.rootNode) then
        delete(self.rootNode)
    end

    self.rootNode = nil
end

function ManureSteamVehicles:update(dt)
    if g_currentMission == nil then
        return
    end

    self.scanTimer = self.scanTimer - dt

    if self.scanTimer <= 0 then
        self.scanTimer = ManureSteamVehicles.CONFIG.scanInterval
        self:scan()
    end

    self:updateActiveEmitterPositions()
end

function ManureSteamVehicles:getReferencePosition()
    local player = g_localPlayer

    if player ~= nil then
        local vehicle = player:getCurrentVehicle()

        if vehicle ~= nil and vehicle.rootNode ~= nil then
            return getWorldTranslation(vehicle.rootNode)
        end

        if player.rootNode ~= nil and player.rootNode ~= 0 then
            return getWorldTranslation(player.rootNode)
        end
    end

    if g_currentMission.controlledVehicle ~= nil and g_currentMission.controlledVehicle.rootNode ~= nil then
        return getWorldTranslation(g_currentMission.controlledVehicle.rootNode)
    end

    return nil
end

function ManureSteamVehicles:getSourceParticleSystem()
    if self.particleTypeName ~= nil then
        return g_particleSystemManager:getParticleSystem(self.particleTypeName)
    end

    for _, particleTypeName in ipairs(ManureSteamVehicles.CONFIG.particleTypes) do
        local sourceParticleSystem = g_particleSystemManager:getParticleSystem(particleTypeName)

        if sourceParticleSystem ~= nil and sourceParticleSystem.shape ~= nil then
            self.particleTypeName = particleTypeName
            manureSteamVehiclesInfo("Using particle system '%s'", particleTypeName)

            return sourceParticleSystem
        end
    end

    return nil
end

function ManureSteamVehicles:createEmitter()
    local particleSystem = {}
    local particleLoaded = self:loadCustomParticleSystem(particleSystem)

    if not particleLoaded then
        local sourceParticleSystem = self:getSourceParticleSystem()

        if sourceParticleSystem == nil or sourceParticleSystem.shape == nil then
            return nil
        end

        local shape = clone(sourceParticleSystem.shape, true, false, true)

        ParticleUtil.loadParticleSystemFromNode(shape, particleSystem, false, true, sourceParticleSystem.forceFullLifespan)
    end

    if not particleSystem.isValid then
        return nil
    end

    ParticleUtil.setEmittingState(particleSystem, false)

    local emitter = {
        particleSystem = particleSystem,
        isActive = false,
        key = nil,
    }

    table.insert(self.pool, emitter)

    return emitter
end

function ManureSteamVehicles:loadCustomParticleSystem(particleSystem)
    local filename = ManureSteamVehicles.CONFIG.customParticleFile

    if filename == nil or filename == "" then
        return false
    end

    filename = Utils.getFilename(filename, ManureSteamVehicles.modDirectory)

    local i3dNode = g_i3DManager:loadI3DFile(filename, false, false)

    if i3dNode == nil or i3dNode == 0 then
        Logging.warning("[ManureSteamVehicles] Could not load custom particle file '%s'", filename)
        return false
    end

    local particleNode = i3dNode

    if getNumOfChildren(i3dNode) > 0 then
        particleNode = getChildAt(i3dNode, 0)
    end

    if particleNode ~= i3dNode then
        link(getRootNode(), particleNode)
    end

    ParticleUtil.loadParticleSystemFromNode(particleNode, particleSystem, false, true, false, filename)

    if particleNode ~= i3dNode and entityExists(i3dNode) then
        delete(i3dNode)
    end

    return particleSystem.isValid == true
end

function ManureSteamVehicles:getFreeEmitter()
    for _, emitter in ipairs(self.pool) do
        if not emitter.isActive then
            return emitter
        end
    end

    if #self.pool < ManureSteamVehicles.CONFIG.maxEmitters then
        return self:createEmitter()
    end

    return nil
end

function ManureSteamVehicles:setEmitterPosition(emitter, x, y, z)
    local particleSystem = emitter.particleSystem
    local node = particleSystem.emitterShape

    if node == nil or node == 0 or not entityExists(node) then
        node = particleSystem.shape
    end

    if node ~= nil and node ~= 0 and entityExists(node) then
        setWorldTranslation(node, x, y, z)
    end
end

function ManureSteamVehicles:activateEmitter(emitter, key, x, y, z, node)
    emitter.key = key
    emitter.isActive = true
    emitter.missedScans = 0
    emitter.node = node
    self.activeByKey[key] = emitter

    self:setEmitterPosition(emitter, x, y, z)
    ParticleUtil.setEmittingState(emitter.particleSystem, true)
end

function ManureSteamVehicles:updateEmitter(emitter, x, y, z, node)
    emitter.missedScans = 0
    emitter.node = node
    self:setEmitterPosition(emitter, x, y, z)
end

function ManureSteamVehicles:deactivateEmitter(emitter)
    if emitter == nil or not emitter.isActive then
        return
    end

    ParticleUtil.setEmittingState(emitter.particleSystem, false)

    if emitter.key ~= nil then
        self.activeByKey[emitter.key] = nil
    end

    emitter.key = nil
    emitter.node = nil
    emitter.isActive = false
end

function ManureSteamVehicles:getFillUnitNode(vehicle, fillUnitIndex)
    local node = nil

    if vehicle.getFillUnitExactFillRootNode ~= nil then
        node = vehicle:getFillUnitExactFillRootNode(fillUnitIndex)
    end

    if node == nil or node == 0 or not entityExists(node) then
        if vehicle.getFillUnitRootNode ~= nil then
            node = vehicle:getFillUnitRootNode(fillUnitIndex)
        end
    end

    if (node == nil or node == 0 or not entityExists(node)) and vehicle.rootNode ~= nil then
        node = vehicle.rootNode
    end

    if node == nil or node == 0 or not entityExists(node) then
        return nil
    end

    return node
end

function ManureSteamVehicles:getFillUnitPosition(vehicle, fillUnitIndex)
    local node = self:getFillUnitNode(vehicle, fillUnitIndex)

    if node == nil then
        return nil
    end

    local x, y, z = getWorldTranslation(node)

    return x, y + ManureSteamVehicles.CONFIG.heightOffset, z, node
end

function ManureSteamVehicles:updateActiveEmitterPositions()
    for _, emitter in pairs(self.activeByKey) do
        local node = emitter.node

        if node ~= nil and node ~= 0 and entityExists(node) then
            local x, y, z = getWorldTranslation(node)

            self:setEmitterPosition(emitter, x, y + ManureSteamVehicles.CONFIG.heightOffset, z)
        end
    end
end

function ManureSteamVehicles:getSteamKey(vehicle, fillUnitIndex)
    if vehicle.getUniqueId ~= nil then
        local uniqueId = vehicle:getUniqueId()

        if uniqueId ~= nil then
            return string.format("%s:%s", uniqueId, fillUnitIndex)
        end
    end

    return string.format("%s:%s", tostring(vehicle.rootNode), fillUnitIndex)
end

function ManureSteamVehicles:scan()
    if FillType == nil or FillType.MANURE == nil or g_particleSystemManager == nil then
        return
    end

    local vehicleSystem = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil

    if vehicleSystem == nil or vehicleSystem.vehicles == nil then
        return
    end

    local refX, _, refZ = self:getReferencePosition()
    local maxDistanceSq = ManureSteamVehicles.CONFIG.maxDistance * ManureSteamVehicles.CONFIG.maxDistance
    local wantedKeys = {}

    for _, vehicle in ipairs(vehicleSystem.vehicles) do
        if vehicle ~= nil and vehicle.getFillUnits ~= nil and vehicle.getFillUnitFillLevel ~= nil and vehicle.getFillUnitFillType ~= nil then
            local fillUnits = vehicle:getFillUnits()

            if fillUnits ~= nil then
                for fillUnitIndex, _ in ipairs(fillUnits) do
                    local fillType = vehicle:getFillUnitFillType(fillUnitIndex)
                    local fillLevel = vehicle:getFillUnitFillLevel(fillUnitIndex)

                    if fillType == FillType.MANURE and fillLevel ~= nil and fillLevel >= ManureSteamVehicles.CONFIG.minFillLevel then
                        local x, y, z, node = self:getFillUnitPosition(vehicle, fillUnitIndex)
                        local isInRange = true

                        if x ~= nil and refX ~= nil then
                            local dx = x - refX
                            local dz = z - refZ
                            isInRange = dx * dx + dz * dz <= maxDistanceSq
                        end

                        if x ~= nil and isInRange then
                            local key = self:getSteamKey(vehicle, fillUnitIndex)
                            local emitter = self.activeByKey[key]
                            wantedKeys[key] = true

                            if emitter ~= nil then
                                self:updateEmitter(emitter, x, y, z, node)
                            else
                                emitter = self:getFreeEmitter()

                                if emitter ~= nil then
                                    self:activateEmitter(emitter, key, x, y, z, node)
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    local staleEmitters = {}

    for key, emitter in pairs(self.activeByKey) do
        if not wantedKeys[key] then
            emitter.missedScans = (emitter.missedScans or 0) + 1

            if emitter.missedScans >= ManureSteamVehicles.CONFIG.missingScanTolerance then
                table.insert(staleEmitters, emitter)
            end
        end
    end

    for _, emitter in ipairs(staleEmitters) do
        self:deactivateEmitter(emitter)
    end
end

function ManureSteamVehicles.install()
    if Mission00 == nil or Utils == nil then
        return
    end

    Mission00.onStartMission = Utils.appendedFunction(Mission00.onStartMission, function(mission)
        if g_client == nil then
            return
        end

        if ManureSteamVehicles.instance == nil then
            ManureSteamVehicles.instance = ManureSteamVehicles.new()
            mission:addUpdateable(ManureSteamVehicles.instance, "ManureSteamVehicles")
            manureSteamVehiclesInfo("Started")
        end
    end)

    Mission00.delete = Utils.prependedFunction(Mission00.delete, function(mission)
        if ManureSteamVehicles.instance ~= nil then
            mission:removeUpdateable("ManureSteamVehicles")
            ManureSteamVehicles.instance:delete()
            ManureSteamVehicles.instance = nil
        end
    end)
end

ManureSteamVehicles.install()
