--[[
Copyright (C) GtX (Andy), 2019

Author: GtX | Andy
Date: 05.10.2019
Revision: FS22-01

Contact:
https://forum.giants-software.com
https://github.com/GtX-Andy

Note:
Idea based on FS15/FS17 PressureWasher.lua concept by Ifko[nator]

Important:
Free for use in mods (FS22 Only) - no permission needed.
No modifications may be made to this script, including conversion to other game versions without written permission from GtX | Andy

Frei verwendbar (Nur LS22) - keine erlaubnis nötig
Ohne schriftliche Genehmigung von GtX | Andy dürfen keine Änderungen an diesem Skript vorgenommen werden, einschließlich der Konvertierung in andere Spielversionen
]]

PressureWasherVehicle = {}

PressureWasherVehicle.MOD_NAME = g_currentModName
PressureWasherVehicle.BASE_DIRECTORY = g_currentModDirectory
PressureWasherVehicle.SPEC_NAME = string.format("spec_%s.pressureWasherVehicle", g_currentModName)

PressureWasherVehicle.STATE_OFF = 0
PressureWasherVehicle.STATE_MOVING = 1
PressureWasherVehicle.STATE_DEACTIVATE = 2
PressureWasherVehicle.STATE_ACTIVE = 3
PressureWasherVehicle.NUM_STATES = 4

PressureWasherVehicle.MAX_MOVE_DISTANCE = 1
PressureWasherVehicle.DEFAULT_WASH_MULTIPLIER = 1

PressureWasherVehicle.INFO_HUD_RED = {1, 0, 0, 1}

source(PressureWasherVehicle.BASE_DIRECTORY .. "Scripts/handTools/PressureWasherVehicleLance.lua")
source(PressureWasherVehicle.BASE_DIRECTORY .. "Scripts/events/PressureWasherVehiclePumpEvent.lua")
source(PressureWasherVehicle.BASE_DIRECTORY .. "Scripts/events/PressureWasherVehicleStateEvent.lua")

local function removeUnusedEventListener(vehicle, name, specClass)
    local eventListeners = vehicle.eventListeners[name]

    if eventListeners ~= nil then
        for i = #eventListeners, 1, -1 do
            if specClass.className ~= nil and specClass.className == eventListeners[i].className then
                table.remove(eventListeners, i)
            end
        end
    end
end

function PressureWasherVehicle.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(FillUnit, specializations)
end

function PressureWasherVehicle.initSpecialization()
    if g_configurationManager:getConfigurationDescByName("pressureWasherLance") == nil then
        g_configurationManager:addConfigurationType("pressureWasherLance", g_i18n:getText("configuration_extension"), "pressureWasher", nil, nil, nil, ConfigurationUtil.SELECTOR_MULTIOPTION)
    end

    if g_configurationManager:getConfigurationDescByName("pressureWasherConsumer") == nil then
        g_configurationManager:addConfigurationType("pressureWasherConsumer", g_i18n:getText("configuration_consumer"), "pressureWasher", nil, nil, nil, ConfigurationUtil.SELECTOR_MULTIOPTION)
    end

    local schema = Vehicle.xmlSchema

    schema:setXMLSpecializationType("PressureWasherVehicle")

    schema:register(XMLValueType.NODE_INDEX, "vehicle.pressureWasher.interactionTrigger#node", "Interaction trigger node", nil, true)
    schema:register(XMLValueType.STRING, "vehicle.pressureWasher.lance#filename", "Lance XML filename. Must be using hand tool type 'pressureWasherVehicleLance'")
    schema:register(XMLValueType.NODE_INDEX, "vehicle.pressureWasher.lance#node", "Lance node")

    local lanceConfigurationKey = "vehicle.pressureWasher.pressureWasherLanceConfigurations.pressureWasherLanceConfiguration(?)"

    schema:register(XMLValueType.FLOAT, lanceConfigurationKey .. "#washMultiplier", "Lance wash multiplier, overwrites the 'washMultiplier' given in the lance XML if > 0", 0)
    schema:register(XMLValueType.FLOAT, lanceConfigurationKey .. "#hoseLength", "Hose length", 15)

    ObjectChangeUtil.registerObjectChangeXMLPaths(schema, lanceConfigurationKey)

    schema:register(XMLValueType.NODE_INDEX, "vehicle.pressureWasher.hoseConnection#node", "Connection point to calculate the hose length used and dynamic connection", "0>")

    schema:register(XMLValueType.INT, "vehicle.pressureWasher.pump#maxPressure", "Maximum pump pressure (PSI)", 4000)
    schema:register(XMLValueType.BOOL, "vehicle.pressureWasher.pump#canAdjustPressure", "Allows supported lance to adjust pressure", false)

    local consumerConfigurationKey = "vehicle.pressureWasher.pressureWasherConsumerConfigurations.pressureWasherConsumerConfiguration(?)"

    schema:register(XMLValueType.FLOAT, consumerConfigurationKey .. ".consumer(?)#fillUnitIndex", "Consumer fill unit index", 1)
    schema:register(XMLValueType.STRING, consumerConfigurationKey .. ".consumer(?)#fillType", "Fill type name")
    schema:register(XMLValueType.FLOAT, consumerConfigurationKey .. ".consumer(?)#usage", "Usage in l/h", 1)
    schema:register(XMLValueType.FLOAT, consumerConfigurationKey .. ".consumer(?)#washMultiplierIncrease", "Extra value applied when fill type is available. Excludes 'Water' & 'Diesel'", 0.01)

    schema:register(XMLValueType.BOOL, "vehicle.pressureWasher#drawFirstFillText", "Display message to remind players to fill required fill types that are empty", true)

    ObjectChangeUtil.registerObjectChangeXMLPaths(schema, consumerConfigurationKey)

    AnimationManager.registerAnimationNodesXMLPaths(schema, "vehicle.pressureWasher.animationNodes")

    SoundManager.registerSampleXMLPaths(schema, "vehicle.pressureWasher.sounds", "start(?)")
    SoundManager.registerSampleXMLPaths(schema, "vehicle.pressureWasher.sounds", "stop(?)")
    SoundManager.registerSampleXMLPaths(schema, "vehicle.pressureWasher.sounds", "work(?)")

    schema:register(XMLValueType.NODE_INDEX, "vehicle.pressureWasher.exhaustEffect#node", "Effect link node")
    schema:register(XMLValueType.STRING, "vehicle.pressureWasher.exhaustEffect#filename", "Effect i3d filename")
    schema:register(XMLValueType.VECTOR_4, "vehicle.pressureWasher.exhaustEffect#minLoadColor", "Min. load colour", "0 0 0 1")
    schema:register(XMLValueType.VECTOR_4, "vehicle.pressureWasher.exhaustEffect#maxLoadColor", "Max. load colour", "0.0384 0.0359 0.0627 2.0")
    schema:register(XMLValueType.FLOAT, "vehicle.pressureWasher.exhaustEffect#minLoadScale", "Min. load scale", 0.25)
    schema:register(XMLValueType.FLOAT, "vehicle.pressureWasher.exhaustEffect#maxLoadScale", "Max. load scale", 0.95)
    schema:register(XMLValueType.FLOAT, "vehicle.pressureWasher.exhaustEffect#upFactor", "Defines how far the effect goes up in the air in meter", 0.75)

    schema:register(XMLValueType.STRING, "vehicle.pressureWasher.turnedAnimation#name", "Turned animation name (Animation played while activating and deactivating)")
    schema:register(XMLValueType.FLOAT, "vehicle.pressureWasher.turnedAnimation#turnOnSpeedScale", "Turn on speed scale", 1)
    schema:register(XMLValueType.FLOAT, "vehicle.pressureWasher.turnedAnimation#turnOffSpeedScale", "Turn off speed scale", "Inversed turnOnSpeedScale")

    schema:register(XMLValueType.FLOAT, "vehicle.pressureWasher.startDuration", "Duration pump motor takes to start", "Start sample time")
    schema:register(XMLValueType.FLOAT, "vehicle.pressureWasher.dashboards#delayedStartDuration", "Allows dashboard groups to have a delayed start time", 0)

    Dashboard.registerDashboardXMLPaths(schema, "vehicle.pressureWasher.dashboards", "time | operatingTime | waterPressure | waterFlowRate")

    schema:register(XMLValueType.BOOL, Dashboard.GROUP_XML_KEY .. "#isPressureWasherRunning", "Is running")
    schema:register(XMLValueType.BOOL, Dashboard.GROUP_XML_KEY .. "#isPressureWasherStarting", "Is starting")
    schema:register(XMLValueType.BOOL, Dashboard.GROUP_XML_KEY .. "#isPressureWasherDelayedStart", "Is delayed start")

    schema:setXMLSpecializationType()

    local schemaSavegame = Vehicle.xmlSchemaSavegame
    local savegameKey = string.format("vehicles.vehicle(?).%s.pressureWasherVehicle", PressureWasherVehicle.MOD_NAME)

    schemaSavegame:register(XMLValueType.FLOAT, savegameKey .. "#pumpWorkMultiplier", "Pump work multiplier", 1)
end

function PressureWasherVehicle.registerOverwrittenFunctions(vehicleType)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "loadDashboardGroupFromXML", PressureWasherVehicle.loadDashboardGroupFromXML)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "getIsInUse", PressureWasherVehicle.getIsInUse)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "getIsDashboardGroupActive", PressureWasherVehicle.getIsDashboardGroupActive)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "getDrawFirstFillText", PressureWasherVehicle.getDrawFirstFillText)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "getIsOperating", PressureWasherVehicle.getIsOperating)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "showInfo", PressureWasherVehicle.showInfo)
end

function PressureWasherVehicle.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", PressureWasherVehicle)
    SpecializationUtil.registerEventListener(vehicleType, "onPostLoad", PressureWasherVehicle)
    SpecializationUtil.registerEventListener(vehicleType, "onDelete", PressureWasherVehicle)
    SpecializationUtil.registerEventListener(vehicleType, "onReadStream", PressureWasherVehicle)
    SpecializationUtil.registerEventListener(vehicleType, "onWriteStream", PressureWasherVehicle)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdate", PressureWasherVehicle)
    SpecializationUtil.registerEventListener(vehicleType, "onFillUnitFillLevelChanged", PressureWasherVehicle)
end

function PressureWasherVehicle.registerFunctions(vehicleType)
    SpecializationUtil.registerFunction(vehicleType, "updatePressureWasherConsumers", PressureWasherVehicle.updatePressureWasherConsumers)
    SpecializationUtil.registerFunction(vehicleType, "setPressureWasherState", PressureWasherVehicle.setPressureWasherState)
    SpecializationUtil.registerFunction(vehicleType, "setPressureWasherIsWashing", PressureWasherVehicle.setPressureWasherIsWashing)
    SpecializationUtil.registerFunction(vehicleType, "adjustPumpWorkMultiplier", PressureWasherVehicle.adjustPumpWorkMultiplier)
    SpecializationUtil.registerFunction(vehicleType, "getCanPressureWasherOperate", PressureWasherVehicle.getCanPressureWasherOperate)
    SpecializationUtil.registerFunction(vehicleType, "getFormattedPressureWasherOperatingTime", PressureWasherVehicle.getFormattedPressureWasherOperatingTime)
    SpecializationUtil.registerFunction(vehicleType, "getIsPressureWasherActive", PressureWasherVehicle.getIsPressureWasherActive)
    SpecializationUtil.registerFunction(vehicleType, "getIsPressureWasherUser", PressureWasherVehicle.getIsPressureWasherUser)
    SpecializationUtil.registerFunction(vehicleType, "getPumpWorkMultiplier", PressureWasherVehicle.getPumpWorkMultiplier)
    SpecializationUtil.registerFunction(vehicleType, "onPressureWasherUserDeleted", PressureWasherVehicle.onPressureWasherUserDeleted)
    SpecializationUtil.registerFunction(vehicleType, "onPressureWasherLanceEquipped", PressureWasherVehicle.onPressureWasherLanceEquipped)
    SpecializationUtil.registerFunction(vehicleType, "onPressureWasherExhaustI3DLoaded", PressureWasherVehicle.onPressureWasherExhaustI3DLoaded)
    SpecializationUtil.registerFunction(vehicleType, "getPressureWasherUserName", PressureWasherVehicle.getPressureWasherUserName)
    SpecializationUtil.registerFunction(vehicleType, "getPressureWasherEngineLoad", PressureWasherVehicle.getPressureWasherEngineLoad)
end

function PressureWasherVehicle:onLoad(savegame)
    self.spec_pressureWasherVehicle = self[PressureWasherVehicle.SPEC_NAME]

    if self.spec_pressureWasherVehicle == nil then
        Logging.error("[%s] Specialization with name 'pressureWasherVehicle' was not found in modDesc!", PressureWasherVehicle.MOD_NAME)
    end

    local spec = self.spec_pressureWasherVehicle
    local xmlFile = self.xmlFile

    spec.isActive = false
    spec.isWashing = false

    spec.activatePosition = nil
    spec.currentUser = nil
    spec.engineLoad = 0

    spec.interactionTrigger = xmlFile:getValue("vehicle.pressureWasher.interactionTrigger#node", nil, self.components, self.i3dMappings)

    if spec.interactionTrigger ~= nil then
        if not CollisionFlag.getHasFlagSet(spec.interactionTrigger, CollisionFlag.TRIGGER_PLAYER) then
            Logging.xmlError(xmlFile, "Missing player collision mask bit '%d' for interaction trigger.", CollisionFlag.getBit(CollisionFlag.TRIGGER_PLAYER))
        else
            spec.activatable = PressureWasherVehicleActivatable.new(self, spec.interactionTrigger)
        end
    else
        Logging.xmlError(xmlFile, "Missing required interaction trigger.")
    end

    spec.handtoolFilename = xmlFile:getValue("vehicle.pressureWasher.lance#filename")

    if spec.handtoolFilename ~= nil then
        spec.handtoolFilename = Utils.getFilename(spec.handtoolFilename, self.baseDirectory)

        if not fileExists(spec.handtoolFilename) then
            Logging.xmlError(xmlFile, "Hand tool with path '%s' could not be found.", spec.handtoolFilename)
            spec.handtoolFilename = ""
        end
    else
        Logging.xmlError(xmlFile, "Missing filename at 'vehicle.pressureWasher.handtool#filename'.")
        spec.handtoolFilename = ""
    end

    spec.lanceNode = xmlFile:getValue("vehicle.pressureWasher.lance#node", nil, self.components, self.i3dMappings)

    local lanceConfigurationId = Utils.getNoNil(self.configurations["pressureWasherLance"], 1)
    local lanceConfigurationKey = string.format("vehicle.pressureWasher.pressureWasherLanceConfigurations.pressureWasherLanceConfiguration(%d)", lanceConfigurationId - 1)

    spec.lanceWashMultiplier = xmlFile:getValue(lanceConfigurationKey .. "#washMultiplier", 0)
    spec.hoseLength = xmlFile:getValue(lanceConfigurationKey .. "#hoseLength", 15)

    ObjectChangeUtil.updateObjectChanges(xmlFile, "vehicle.pressureWasher.pressureWasherLanceConfigurations.pressureWasherLanceConfiguration", lanceConfigurationId , self.components, self)

    spec.hoseConnectionNode = xmlFile:getValue("vehicle.pressureWasher.hoseConnection#node", "0>", self.components, self.i3dMappings)

    spec.pump = {
        maxPressure = xmlFile:getValue("vehicle.pressureWasher.pump#maxPressure", 4000),
        canAdjustPressure = xmlFile:getValue("vehicle.pressureWasher.pump#canAdjustPressure", false),
        pressure = 0,
        maxFlowRate = 0,
        flowRate = 0,
        workMultiplier = 1
    }

    spec.consumers = {}
    spec.consumersByFillTypeIndex = {}
    spec.consumersByFillTypeName = {}

    -- Consumers are optional. Diesel and Water if used will be required for operation.
    -- All other fillTypes are treated as additives and will speed up wash times using 'washMultiplierIncrease' only if fill level > 0

    local consumerConfigurationId = Utils.getNoNil(self.configurations["pressureWasherConsumer"], 1)
    local consumerConfigurationKey = string.format("vehicle.pressureWasher.pressureWasherConsumerConfigurations.pressureWasherConsumerConfiguration(%d)", consumerConfigurationId - 1)

    ObjectChangeUtil.updateObjectChanges(xmlFile, "vehicle.pressureWasher.pressureWasherConsumerConfigurations.pressureWasherConsumerConfiguration", consumerConfigurationId , self.components, self)

    xmlFile:iterate(consumerConfigurationKey .. ".consumer", function (_, consumerKey)
        local fillUnitIndex = xmlFile:getValue(consumerKey .. "#fillUnitIndex", 1)
        local fillUnit = self:getFillUnitByIndex(fillUnitIndex)

        if fillUnit ~= nil and fillUnit.capacity > 0 then
            local fillTypeName = xmlFile:getValue(consumerKey .. "#fillType", "consumer"):upper()
            local fillType = g_fillTypeManager:getFillTypeByName(fillTypeName)

            if fillType ~= nil then
                if spec.consumersByFillTypeName[fillTypeName] == nil then
                    local fillTypeIndex = fillType.index

                    local consumer = {
                        fillLevel = 0,
                        isRequired = true,
                        title = fillType.title,
                        washMultiplierIncrease = 0,
                        fillTypeIndex = fillTypeIndex,
                        fillUnitIndex = fillUnitIndex,
                        fillTypeName = fillTypeName,
                        isDiesel = fillTypeIndex == FillType.DIESEL,
                        isWater = fillTypeIndex == FillType.WATER
                    }

                    consumer.unitText = fillUnit.unitText or g_i18n:getVolumeUnit()
                    consumer.usagePerMS = math.max(xmlFile:getValue(consumerKey .. "#usage", 1000), 1) / (60 * 60 * 1000)

                    if not consumer.isDiesel then
                        if consumer.isWater then
                            spec.pump.maxFlowRate = consumer.usagePerMS * (60 * 1000)
                        else
                            consumer.isRequired = false
                            consumer.washMultiplierIncrease = math.max(xmlFile:getValue(consumerKey .. "#washMultiplierIncrease", 0.1), 0.01)
                        end
                    end

                    table.insert(spec.consumers, consumer)

                    spec.consumersByFillTypeName[fillTypeName] = consumer
                    spec.consumersByFillTypeIndex[fillTypeIndex] = consumer
                else
                    Logging.xmlWarning(xmlFile, "Consumer with fillType '%s' already in use!", fillTypeName)
                end
            else
                Logging.xmlWarning(xmlFile, "Unknown fillType '%s' for consumer '%s'", fillTypeName, consumerKey)
            end
        else
            Logging.xmlError(xmlFile, "FillUnit '%d' not defined or capacity is 0 at '%s'!", fillUnitIndex, consumerKey)
        end
    end)

    spec.numberOfConsumer = #spec.consumers
    spec.canDrawFirstFillText = xmlFile:getValue("vehicle.pressureWasher#drawFirstFillText", true)

    if self.isClient then
        spec.animationNodes = g_animationManager:loadAnimations(self.xmlFile, "vehicle.pressureWasher.animationNodes", self.components, self, self.i3dMappings)

        spec.samples = {
            start = g_soundManager:loadSampleFromXML(xmlFile, "vehicle.pressureWasher.sounds", "start", self.baseDirectory, self.components, 1, AudioGroup.VEHICLE, self.i3dMappings, self),
            stop = g_soundManager:loadSampleFromXML(xmlFile, "vehicle.pressureWasher.sounds", "stop", self.baseDirectory, self.components, 1, AudioGroup.VEHICLE, self.i3dMappings, self),
            work = g_soundManager:loadSamplesFromXML(xmlFile, "vehicle.pressureWasher.sounds", "work", self.baseDirectory, self.components, 0, AudioGroup.VEHICLE, self.i3dMappings, self)
        }

        local exhaustKey = "vehicle.pressureWasher.exhaustEffect"
        local filename = xmlFile:getValue(exhaustKey .. "#filename")
        local linkNode = xmlFile:getValue(exhaustKey .. "#node", nil, self.components, self.i3dMappings)

        if filename ~= nil and linkNode ~= nil then
            filename = Utils.getFilename(filename, self.baseDirectory)

            local arguments = {
                xmlFile = xmlFile,
                key = exhaustKey,
                linkNode = linkNode,
                filename = filename
            }

            local sharedLoadRequestId = self:loadSubSharedI3DFile(filename, false, false, self.onPressureWasherExhaustI3DLoaded, self, arguments)

            if spec.sharedLoadRequestIds == nil then
                spec.sharedLoadRequestIds = {}
            end

            if sharedLoadRequestId ~= nil then
                table.insert(spec.sharedLoadRequestIds, sharedLoadRequestId)
            end
        end

        local allowsAnimations = SpecializationUtil.hasSpecialization(AnimatedVehicle, self.specializations)

        if allowsAnimations then
            local turnOnAnimation = xmlFile:getValue("vehicle.pressureWasher.turnedAnimation#name")

            if turnOnAnimation ~= nil then
                spec.turnOnAnimation = {
                    name = turnOnAnimation,
                    turnOnSpeedScale = xmlFile:getValue("vehicle.pressureWasher.turnedAnimation#turnOnSpeedScale", 1)
                }

                spec.turnOnAnimation.turnOffSpeedScale = xmlFile:getValue("vehicle.pressureWasher.turnedAnimation#turnOffSpeedScale", -spec.turnOnAnimation.turnOnSpeedScale)
            end
        end
    end

    spec.startTime = 0
    spec.delayedStartTime = 0

    if spec.samples ~= nil and spec.samples.start ~= nil then
        spec.startDuration = spec.samples.start.duration
    end

    spec.startDuration = xmlFile:getValue("vehicle.pressureWasher.startDuration", spec.startDuration or 0)
    spec.delayedStartDuration = xmlFile:getValue("vehicle.pressureWasher.dashboards#delayedStartDuration", 0) * 1000

    spec.texts = {
        fillWarning = g_i18n:getText("info_firstFillTheTool") .. " (%s)",
        inUse = g_i18n:getText("shop_messageIsInUse") .. " (%s)!",
        secureVehicle = g_i18n:getText("info_pwSecureVehicle", self.customEnvironment),
        hoseLength = g_i18n:getText("configuration_hoseLenght", self.customEnvironment)
    }

    -- Dashboards (These will only update when pressure washer is active. It is best to have this invisible otherwise.)
    if self.loadDashboardsFromXML ~= nil then
        self:loadDashboardsFromXML(xmlFile, "vehicle.pressureWasher.dashboards", {
            valueFunc = "getEnvironmentTime",
            valueTypeToLoad = "time",
            valueObject = g_currentMission.environment
        })

        self:loadDashboardsFromXML(xmlFile, "vehicle.pressureWasher.dashboards", {
            valueFunc = "getFormattedPressureWasherOperatingTime",
            valueTypeToLoad = "operatingTime",
            valueObject = self
        })

        self:loadDashboardsFromXML(xmlFile, "vehicle.pressureWasher.dashboards", {
            valueFunc = "pressure",
            valueTypeToLoad = "waterPressure",
            maxFunc = "maxPressure",
            minFunc = 0,
            valueObject = spec.pump
        })

        if spec.numberOfConsumer > 0 then
            self:loadDashboardsFromXML(xmlFile, "vehicle.pressureWasher.dashboards", {
                valueFunc = "flowRate",
                valueTypeToLoad = "waterFlowRate",
                maxFunc = "maxFlowRate",
                minFunc = 0,
                valueObject = spec.pump
            })
        end
    end
end

function PressureWasherVehicle:onPostLoad(savegame)
    local spec = self.spec_pressureWasherVehicle

    if savegame ~= nil and not savegame.resetVehicles then
        local savegameKey = string.format("%s.%s.pressureWasherVehicle", savegame.key, PressureWasherVehicle.MOD_NAME)
        local workMultiplier = savegame.xmlFile:getValue(savegameKey .. "#pumpWorkMultiplier", 0)

        if workMultiplier > 0 then
            self:adjustPumpWorkMultiplier(workMultiplier, false)
        end
    end

    if spec == nil or spec.activatable == nil or spec.handtoolFilename == "" then
        removeUnusedEventListener(self, "onReadStream", PressureWasherVehicle)
        removeUnusedEventListener(self, "onWriteStream", PressureWasherVehicle)
        removeUnusedEventListener(self, "onUpdate", PressureWasherVehicle)
        removeUnusedEventListener(self, "onFillUnitFillLevelChanged", PressureWasherVehicle)
    end
end

function PressureWasherVehicle:onDelete()
    local spec = self.spec_pressureWasherVehicle

    self:setPressureWasherState(PressureWasherVehicle.STATE_OFF)

    if spec.activatable ~= nil then
        spec.activatable:delete()
        spec.activatable = nil
    end

    if self.isClient then
        if spec.animationNodes ~= nil then
            g_animationManager:deleteAnimations(spec.animationNodes)
            spec.animationNodes = nil
        end

        if spec.samples ~= nil then
            g_soundManager:deleteSample(spec.samples.start)
            g_soundManager:deleteSample(spec.samples.stop)
            g_soundManager:deleteSamples(spec.samples.work)
        end

        if spec.sharedLoadRequestIds ~= nil then
            for _, sharedLoadRequestId in ipairs(spec.sharedLoadRequestIds) do
                g_i3DManager:releaseSharedI3DFile(sharedLoadRequestId)
            end

            spec.sharedLoadRequestIds = nil
        end
    end
end

function PressureWasherVehicle:onReadStream(streamId, connection)
    local workMultiplier = streamReadUInt8(streamId)

    if streamReadBool(streamId) then
        local currentUser = NetworkUtil.readNodeObject(streamId)

        self:setPressureWasherState(PressureWasherVehicle.STATE_ACTIVE, currentUser, true)
    end

    self:adjustPumpWorkMultiplier(workMultiplier / 10, false)
end

function PressureWasherVehicle:onWriteStream(streamId, connection)
    local spec = self.spec_pressureWasherVehicle
    local workMultiplier = spec.pump.workMultiplier or 1

    streamWriteUInt8(streamId, workMultiplier * 10)

    -- No need to send 'currentUser' as players are loaded after vehicles anyway???
    if streamWriteBool(streamId, spec.isActive) then
        NetworkUtil.writeNodeObject(streamId, spec.currentUser)
    end
end

function PressureWasherVehicle:saveToXMLFile(xmlFile, key, usedModNames)
    xmlFile:setValue(key .. "#pumpWorkMultiplier", self:getPumpWorkMultiplier())
end

function PressureWasherVehicle:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
    local spec = self.spec_pressureWasherVehicle

    if spec.isActive then
        if self.isServer then
            local movedDistance = 0

            if self.components[1] ~= nil and self.components[1].node ~= 0 then
                local x, y, z = getWorldTranslation(self.components[1].node)

                if spec.activatePosition == nil then
                    spec.activatePosition = {x, y, z}
                end

                movedDistance = MathUtil.vector3Length(spec.activatePosition[1] - x, spec.activatePosition[2] - y, spec.activatePosition[3] - z)
            end

            if movedDistance > PressureWasherVehicle.MAX_MOVE_DISTANCE then
                self:setPressureWasherState(PressureWasherVehicle.STATE_MOVING)
            end
        end

        if spec.currentUser ~= nil then
            spec.hoseLengthUsed = calcDistanceFrom(spec.currentUser.rootNode, spec.hoseConnectionNode)

            if spec.hoseLengthUsed >= spec.hoseLength then
                local distanceLeft = (spec.hoseLength + 4) - spec.hoseLengthUsed

                if distanceLeft > 0 then
                    if spec.currentUser == g_currentMission.player then
                        g_currentMission:showBlinkingWarning(string.format(g_i18n:getText("warning_hpwRangeRestriction"), distanceLeft), 100)
                    end
                elseif self.isServer then
                    self:setPressureWasherState(PressureWasherVehicle.STATE_OFF)
                end
            end
        end

        if spec.engineLoad < 1 and spec.startTime <= g_currentMission.time then
            spec.engineLoad = math.min(spec.engineLoad + 0.01, 1)
        end

        if spec.isWashing then
            spec.pump.flowRate = (spec.pump.maxFlowRate * spec.pump.workMultiplier) * spec.engineLoad
            spec.pump.pressure = (spec.pump.maxPressure * spec.pump.workMultiplier) * spec.engineLoad
        else
            spec.pump.flowRate = 0
            spec.pump.pressure = 0
        end

        if self.isClient then
            if spec.exhaustEffect ~= nil then
                local effect = spec.exhaustEffect
                local posX, posY, posZ = localToWorld(effect.effectNode, 0, 0.5, 0)

                if effect.lastPosition == nil then
                    effect.lastPosition = {posX, posY, posZ}
                end

                local vx = (posX - effect.lastPosition[1]) * 10
                local vy = (posY - effect.lastPosition[2]) * 10
                local vz = (posZ - effect.lastPosition[3]) * 10
                local ex, ey, ez = localToWorld(effect.effectNode, 0, 1, 0)
                vz = ez - vz
                vy = ey - vy + effect.upFactor
                vx = ex - vx

                local lx, ly, lz = worldToLocal(effect.effectNode, vx, vy, vz)
                local distance = MathUtil.vector2Length(lx, lz)
                lx, lz = MathUtil.vector2Normalize(lx, lz)
                ly = math.abs(math.max(ly, 0.01))

                local xFactor = math.atan(distance / ly) * (1.2 + 2 * ly)
                local yFactor = math.atan(distance / ly) * (1.2 + 2 * ly)
                local xRot = math.atan(lz / ly) * xFactor
                local zRot = -math.atan(lx / ly) * yFactor
                effect.xRot = effect.xRot * 0.95 + xRot * 0.05
                effect.zRot = effect.zRot * 0.95 + zRot * 0.05

                local engineLoad = spec.engineLoad
                local scale = MathUtil.lerp(effect.minRpmScale, effect.maxRpmScale, engineLoad)

                setShaderParameter(effect.effectNode, "param", effect.xRot, effect.zRot, 0, scale, false)

                local r = MathUtil.lerp(effect.minRpmColor[1], effect.maxRpmColor[1], engineLoad)
                local g = MathUtil.lerp(effect.minRpmColor[2], effect.maxRpmColor[2], engineLoad)
                local b = MathUtil.lerp(effect.minRpmColor[3], effect.maxRpmColor[3], engineLoad)
                local a = MathUtil.lerp(effect.minRpmColor[4], effect.maxRpmColor[4], engineLoad)

                setShaderParameter(effect.effectNode, "exhaustColor", r, g, b, a, false)

                effect.lastPosition[1] = posX
                effect.lastPosition[2] = posY
                effect.lastPosition[3] = posZ
            end
        end

        self:raiseActive()
    else
        if spec.engineLoad > 0 then
            spec.engineLoad = math.max(spec.engineLoad - 0.02, 0)
            self:raiseActive()
        end
    end
end

function PressureWasherVehicle:onFillUnitFillLevelChanged(fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, appliedDelta)
    local spec = self.spec_pressureWasherVehicle
    local consumer = spec.consumersByFillTypeIndex[fillTypeIndex]

    if consumer ~= nil then
        consumer.fillLevel = self:getFillUnitFillLevel(fillUnitIndex) or 0

        if consumer.isDiesel and consumer.fillLevel <= 0 then
            if spec.currentUser == g_currentMission.player then
                self:setPressureWasherState(PressureWasherVehicle.STATE_OFF)

                g_currentMission:showBlinkingWarning(string.format(spec.texts.fillWarning, consumer.title), 3000)
            end
        end
    end
end

function PressureWasherVehicle:updatePressureWasherConsumers(dt, isWashing, washMultiplier)
    washMultiplier = washMultiplier or PressureWasherVehicle.DEFAULT_WASH_MULTIPLIER

    if self.isServer then
        local spec = self.spec_pressureWasherVehicle

        if spec.numberOfConsumer > 0 then
            local pumpWorkMultiplier = spec.pump.workMultiplier
            local farmId = self:getOwnerFarmId()

            for _, consumer in ipairs (spec.consumers) do
                if consumer.isDiesel or isWashing then
                    local consumerUsage = consumer.usagePerMS * dt

                    if not consumer.isDiesel then
                        consumerUsage = consumerUsage * pumpWorkMultiplier

                        if consumer.washMultiplierIncrease > 0 and consumer.fillLevel > 0 then
                            washMultiplier = washMultiplier + consumer.washMultiplierIncrease
                        end
                    end

                    self:addFillUnitFillLevel(farmId, consumer.fillUnitIndex, -consumerUsage, consumer.fillTypeIndex, ToolType.UNDEFINED, nil)
                end
            end

            return washMultiplier * pumpWorkMultiplier
        end
    end

    return washMultiplier
end

function PressureWasherVehicle:setPressureWasherState(state, currentUser, noEventSend)
    local spec = self.spec_pressureWasherVehicle
    local isActive = false

    if state == PressureWasherVehicle.STATE_ACTIVE then
        isActive = true
    elseif state == PressureWasherVehicle.STATE_MOVING then
        if spec.currentUser == g_currentMission.player then
            g_currentMission:showBlinkingWarning(spec.texts.secureVehicle, 3000)
        end
    end

    if spec.isActive ~= isActive then
        PressureWasherVehicleStateEvent.sendEvent(self, state, currentUser, noEventSend)

        spec.isActive = isActive

        if spec.isActive then
            if currentUser ~= nil then
                spec.currentUser = currentUser

                spec.currentUser:equipHandtool(spec.handtoolFilename, true, true, self.onPressureWasherLanceEquipped, self)

                if self.isServer then
                    spec.ownerConnection = currentUser.networkInformation.creatorConnection
                    spec.currentUser:addDeleteListener(self, "onPressureWasherUserDeleted")
                end
            end

            spec.startTime = g_currentMission.time + spec.startDuration
            spec.delayedStartTime = spec.startTime + spec.delayedStartDuration
        else
            if spec.currentUser ~= nil then
                if state == PressureWasherVehicle.STATE_OFF or state == PressureWasherVehicle.STATE_MOVING then
                    spec.currentUser:equipHandtool("", true, noEventSend)
                end

                if self.isServer then
                    spec.ownerConnection = nil
                    spec.currentUser:removeDeleteListener(self, "onPressureWasherUserDeleted")
                end
            end

            spec.currentHandtool = nil

            spec.currentUser = nil
            spec.isWashing = false

            spec.pump.flowRate = 0
            spec.pump.pressure = 0

            spec.activatePosition = nil
        end

        if self.isClient then
            local samples = spec.samples

            if spec.lanceNode ~= nil then
                setVisibility(spec.lanceNode, not spec.isActive)
            end

            if spec.isActive then
                g_animationManager:startAnimations(spec.animationNodes)

                if spec.exhaustEffect ~= nil then
                    local color = spec.exhaustEffect.minRpmColor

                    setVisibility(spec.exhaustEffect.effectNode, true)
                    setShaderParameter(spec.exhaustEffect.effectNode, "param", spec.exhaustEffect.xRot, spec.exhaustEffect.zRot, 0, 0, false)
                    setShaderParameter(spec.exhaustEffect.effectNode, "exhaustColor", color[1], color[2], color[3], color[4], false)
                end

                if samples ~= nil then
                    g_soundManager:stopSample(samples.start)
                    g_soundManager:stopSamples(samples.work)
                    g_soundManager:stopSample(samples.stop)
                    g_soundManager:playSample(samples.start)

                    for i = 1, #samples.work do
                        g_soundManager:playSample(samples.work[i], 0, samples.start)
                    end
                end

                if spec.turnOnAnimation ~= nil and self.playAnimation ~= nil then
                    self:playAnimation(spec.turnOnAnimation.name, spec.turnOnAnimation.turnOnSpeedScale, self:getAnimationTime(spec.turnOnAnimation.name), true)
                end
            else
                g_animationManager:stopAnimations(spec.animationNodes)

                if spec.exhaustEffect ~= nil then
                    setVisibility(spec.exhaustEffect.effectNode, spec.isActive)
                end

                if samples ~= nil then
                    g_soundManager:stopSample(samples.start)
                    g_soundManager:stopSamples(samples.work)
                    g_soundManager:stopSample(samples.stop)
                    g_soundManager:playSample(samples.stop)
                end

                if spec.turnOnAnimation ~= nil and self.playAnimation ~= nil then
                    self:playAnimation(spec.turnOnAnimation.name, spec.turnOnAnimation.turnOffSpeedScale, self:getAnimationTime(spec.turnOnAnimation.name), true)
                end
            end
        end

        self:raiseActive()
    end
end

function PressureWasherVehicle:adjustPumpWorkMultiplier(value, isInputValue)
    local spec = self.spec_pressureWasherVehicle

    if value ~= nil and value ~= 0 then
        if isInputValue then
            if spec.pump.canAdjustPressure then
                local workMultiplier = spec.pump.workMultiplier

                if value > 0 then
                    workMultiplier = math.min(workMultiplier + 0.1, 1.0)
                else
                    workMultiplier = math.max(workMultiplier - 0.1, 0.1)
                end

                if workMultiplier ~= spec.pump.workMultiplier then
                    spec.pump.workMultiplier = workMultiplier

                    if g_server ~= nil then
                        g_server:broadcastEvent(PressureWasherVehiclePumpEvent.new(self, workMultiplier), nil, nil, self)
                    else
                        g_client:getServerConnection():sendEvent(PressureWasherVehiclePumpEvent.new(self, workMultiplier))
                    end
                end
            end
        else
            spec.pump.workMultiplier = MathUtil.clamp(MathUtil.round(value, 1), 0.1, 1.0)
        end
    end

    return spec.pump.workMultiplier
end

function PressureWasherVehicle:setPressureWasherIsWashing(isWashing)
    self.spec_pressureWasherVehicle.isWashing = Utils.getNoNil(isWashing, false)
end

function PressureWasherVehicle:onPressureWasherLanceEquipped(handTool)
    local spec = self.spec_pressureWasherVehicle

    if handTool.setConnectedVehicle ~= nil then
        handTool:setConnectedVehicle(self, spec.lanceWashMultiplier, spec.pump.canAdjustPressure)
    end

    spec.currentHandtool = handTool
end

function PressureWasherVehicle:onPressureWasherUserDeleted(player)
    local spec = self.spec_pressureWasherVehicle

    if spec.isActive and spec.currentUser == player then
        self:setPressureWasherState(PressureWasherVehicle.STATE_OFF)
    end
end

function PressureWasherVehicle:onPressureWasherExhaustI3DLoaded(i3dNode, failedReason, args)
    if i3dNode ~= 0 then
        local node = getChildAt(i3dNode, 0)

        if getHasShaderParameter(node, "param") then
            local spec = self.spec_pressureWasherVehicle

            local xmlFile = args.xmlFile
            local key = args.key

            local exhaustEffect = {
                effectNode = node,
                node = args.linkNode,
                filename = args.filename
            }

            link(exhaustEffect.node, exhaustEffect.effectNode)
            setVisibility(exhaustEffect.effectNode, false)
            delete(i3dNode)

            exhaustEffect.minRpmColor = xmlFile:getValue(key .. "#minLoadColor", "0 0 0 1", true)
            exhaustEffect.maxRpmColor = xmlFile:getValue(key .. "#maxLoadColor", "0.0384 0.0359 0.0627 2.0", true)
            exhaustEffect.minRpmScale = xmlFile:getValue(key .. "#minLoadScale", 0.25)
            exhaustEffect.maxRpmScale = xmlFile:getValue(key .. "#maxLoadScale", 0.95)
            exhaustEffect.upFactor = xmlFile:getValue(key .. "#upFactor", 0.75)
            exhaustEffect.lastPosition = nil
            exhaustEffect.xRot = 0
            exhaustEffect.zRot = 0

            spec.exhaustEffect = exhaustEffect
        end
    end
end

function PressureWasherVehicle:loadDashboardGroupFromXML(superFunc, xmlFile, key, group)
    if not superFunc(self, xmlFile, key, group) then
        return false
    end

    group.isPressureWasherRunning = xmlFile:getValue(key .. "#isPressureWasherRunning")
    group.isPressureWasherStarting = xmlFile:getValue(key .. "#isPressureWasherStarting")
    group.isPressureWasherDelayedStart = xmlFile:getValue(key .. "#isPressureWasherDelayedStart")

    return true
end

function PressureWasherVehicle:showInfo(superFunc, box)
    local spec = self.spec_pressureWasherVehicle

    for _, consumer in ipairs (spec.consumers) do
        if consumer.fillLevel <= 0 then
            box:addLine(consumer.title, string.format("0 %s", consumer.unitText or "l"), consumer.isRequired, PressureWasherVehicle.INFO_HUD_RED)
        end
    end

    superFunc(self, box)
end

function PressureWasherVehicle:getIsInUse(superFunc, connection)
    local spec = self.spec_pressureWasherVehicle

    if spec ~= nil and spec.isActive and spec.ownerConnection ~= nil then
        if spec.ownerConnection ~= connection then
            return true
        end
    end

    return superFunc(self, connection)
end

function PressureWasherVehicle:getIsDashboardGroupActive(superFunc, group)
    local spec = self.spec_pressureWasherVehicle

    if spec ~= nil then
        if group.isPressureWasherRunning and group.isPressureWasherStarting then
            if not spec.isActive then
                return false
            end
        end

        if group.isPressureWasherStarting and not group.isPressureWasherRunning then
            if not spec.isActive or spec.startTime < g_currentMission.time then
                return false
            end
        end

        if group.isPressureWasherRunning and not group.isPressureWasherStarting then
            if not spec.isActive or spec.startTime > g_currentMission.time then
                return false
            end
        end

        if group.isPressureWasherDelayedStart then
            if not spec.isActive or spec.delayedStartTime < g_currentMission.time then
                return false
            end
        end
    end

    return superFunc(self, group)
end

function PressureWasherVehicle:getDrawFirstFillText(superFunc)
    local spec = self.spec_pressureWasherVehicle

    if self.isClient then
        if spec ~= nil and spec.canDrawFirstFillText and spec.numberOfConsumer > 0 then
            if self:getIsActiveForInput() and self:getIsSelected() then
                local consumer = spec.consumersByFillTypeName.DIESEL

                if consumer and consumer.fillLevel <= 0 then
                    return true
                end

                consumer = spec.consumersByFillTypeName.WATER

                if consumer and consumer.fillLevel <= 0 then
                    return true
                end
            end
        end
    end

    return false
end

function PressureWasherVehicle:getIsOperating(superFunc)
    return superFunc(self) or self:getIsPressureWasherActive()
end

function PressureWasherVehicle:getCanPressureWasherOperate(activatePressed, showWarning)
    local spec = self.spec_pressureWasherVehicle

    if activatePressed and spec.startTime < g_currentMission.time then
        for _, consumer in ipairs (spec.consumers) do
            if consumer.isRequired and consumer.fillLevel <= 0 then
                if showWarning and self:getIsPressureWasherUser() then
                    g_currentMission:showBlinkingWarning(string.format(spec.texts.fillWarning, consumer.title), 1500)
                end

                return false
            end
        end
    else
        return false
    end

    return true
end

function PressureWasherVehicle:getFormattedPressureWasherOperatingTime()
    local minutes = self.operatingTime / (1000 * 60)
    local hours = math.floor(minutes / 60)

    minutes = math.floor((minutes - hours * 60) / 6)
    local minutesString = string.format("%02d", minutes * 10)

    return tonumber(hours .. "." .. minutesString)
end

function PressureWasherVehicle:getIsPressureWasherActive()
    return self.spec_pressureWasherVehicle.isActive
end

function PressureWasherVehicle:getIsPressureWasherUser()
    return self.spec_pressureWasherVehicle.currentUser == g_currentMission.player
end

function PressureWasherVehicle:getPressureWasherUserName()
    local spec = self.spec_pressureWasherVehicle
    local user = nil

    if self.isServer then
        if spec.ownerConnection ~= nil then
            user = g_currentMission.userManager:getUserByConnection(spec.ownerConnection)
        end
    else
        if spec.currentUser ~= nil then
            user = g_currentMission.userManager:getUserByUserId(spec.currentUser.userId)
        end
    end

    if user == nil then
        return ""
    end

    return user:getNickname()
end

function PressureWasherVehicle:getPumpWorkMultiplier()
    return self.spec_pressureWasherVehicle.pump.workMultiplier or 1
end

function PressureWasherVehicle:getPressureWasherEngineLoad()
    return self.spec_pressureWasherVehicle.engineLoad or 0
end

function PressureWasherVehicle:updateDebugValues(values)
    local spec = self.spec_pressureWasherVehicle

    if spec ~= nil and spec.currentHandtool ~= nil then
        spec.currentHandtool:updateDebugValues(values)
    end
end

g_soundManager:registerModifierType("PRESSURE_WASHER_ENGINE_LOAD", PressureWasherVehicle.getPressureWasherEngineLoad)

PressureWasherVehicleActivatable = {}

local PressureWasherVehicleActivatable_mt = Class(PressureWasherVehicleActivatable)

function PressureWasherVehicleActivatable.new(vehicle, interactionTrigger)
    local self = setmetatable({}, PressureWasherVehicleActivatable_mt)

    self.vehicle = vehicle

    self.interactionTrigger = interactionTrigger
    addTrigger(self.interactionTrigger, "interactionTriggerCallback", self)

    self.isEnabled = true
    self.activateText = g_i18n:getText("typeDesc_highPressureWasher")

    self.turnOnText = string.format(g_i18n:getText("action_turnOnOBJECT"), self.activateText)
    self.turnOffText = string.format(g_i18n:getText("action_turnOffOBJECT"), self.activateText)

    self.detailsText = g_i18n:getText("ui_modHubDetails")

    return self
end

function PressureWasherVehicleActivatable:delete()
    self.isEnabled = false

    if self.interactionTrigger ~= nil then
        removeTrigger(self.interactionTrigger)
        self.interactionTrigger = nil
    end

    g_currentMission.activatableObjectsSystem:removeActivatable(self)
end

function PressureWasherVehicleActivatable:run()
    local spec = self.vehicle.spec_pressureWasherVehicle

    if spec.isActive then
        if self.vehicle:getIsPressureWasherUser() then
            self.vehicle:setPressureWasherState(PressureWasherVehicle.STATE_OFF)
        else
            g_currentMission:showBlinkingWarning(string.format(spec.texts.inUse, self.vehicle:getPressureWasherUserName()), 1500)
        end
    else
        g_currentMission.player:unequipHandtool()

        local dieselConsumer = spec.consumersByFillTypeName.DIESEL

        if dieselConsumer == nil or dieselConsumer.fillLevel > 0 then
            self.vehicle:setPressureWasherState(PressureWasherVehicle.STATE_ACTIVE, g_currentMission.player)
        else
            g_currentMission:showBlinkingWarning(string.format(spec.texts.fillWarning, dieselConsumer.title), 1500)
        end
    end

    self:updateActivateText()
end

function PressureWasherVehicleActivatable:updateActivateText()
    if self.vehicle:getIsPressureWasherActive() then
        if self.vehicle:getIsPressureWasherUser() then
            self.activateText = self.turnOffText
        else
            self.activateText = self.detailsText
        end
    else
        self.activateText = self.turnOnText
    end
end

function PressureWasherVehicleActivatable:getIsActivatable()
    if self.isEnabled and g_currentMission.controlPlayer then
        self:updateActivateText()

        return true
    end

    return false
end

function PressureWasherVehicleActivatable:getHasAccess(farmId)
    return g_currentMission.accessHandler:canFarmAccessOtherId(farmId, self.vehicle:getOwnerFarmId())
end

function PressureWasherVehicleActivatable:getDistance(x, y, z)
    if self.isEnabled and self.interactionTrigger ~= nil then
        local tx, ty, tz = getWorldTranslation(self.interactionTrigger)

        return MathUtil.vector3Length(x - tx, y - ty, z - tz)
    end

    return math.huge
end

function PressureWasherVehicleActivatable:interactionTriggerCallback(triggerId, otherId, onEnter, onLeave, onStay)
    if self.isEnabled and (onEnter or onLeave) then
        if g_currentMission.player ~= nil and otherId == g_currentMission.player.rootNode then
            if onEnter then
                g_currentMission.activatableObjectsSystem:addActivatable(self)
            else
                g_currentMission.activatableObjectsSystem:removeActivatable(self)
            end
        end
    end
end
