--[[
    AgriBumper

    This script adds a AgriBumper Configuration to the vehicle.

	@author: 		BayernGamers
	@date: 			18.03.2025
	@version:		1.0

	History:		v1.0 @18.03.2025 - initial implementation in FS 25
                    ------------------------------------------------------------------------------------------------------
	
	License:        Terms:
                        Usage:
                            Feel free to use this work as-is as long as you adhere to the following terms:
						Attribution:
							You must give appropriate credit to the original author when using this work.
						No Derivatives:
							You may not alter, transform, or build upon this work in any way.
						Usage: 
							The work may be used for personal and commercial purposes, provided it is not modified or adapted.
						Additional Clause:
							This script may not be converted, adapted, or incorporated into any other game versions or platforms except by GIANTS Software.
]]
source(Utils.getFilename("scripts/utils/Logger.lua", g_currentModDirectory))
source(Utils.getFilename("scripts/VehiclePart.lua", g_currentModDirectory))
source(Utils.getFilename("scripts/ExtendedVehicleConfigurationDataObjectChange.lua", g_currentModDirectory))
source(Utils.getFilename("scripts/VehicleConfigurationItemColorExtension.lua", g_currentModDirectory))
source(Utils.getFilename("scripts/DefaultVehiclesConfigurations.lua", g_currentModDirectory))

local log = Logger.new(true, Logger.DEBUG_LEVELS.HIGH, "AgriBumper.lua")

AgriBumper = {}
AgriBumper.MOD_DIR = g_currentModDirectory
AgriBumper.MOD_NAME = g_currentModName
AgriBumper.AGRIBUMPER_CONFIG_XML_KEY = "vehicle.agriBumperConfigurations.agriBumperConfiguration(?)"
AgriBumper.AGRIBUMPER_BUMPER_CONFIG_XML_KEY = AgriBumper.AGRIBUMPER_CONFIG_XML_KEY .. ".agriBumper"
AgriBumper.AGRIBUMPER_ATTACHER_JOINT_OVERRIDE_XML_KEY = AgriBumper.AGRIBUMPER_BUMPER_CONFIG_XML_KEY .. ".attacherJointOverride"
AgriBumper.SAVEGAME_XML_KEY_PARTIAL = "FS25_agriBumper.agriBumper"
AgriBumper.SAVEGAME_XML_KEY_FULL = "vehicles.vehicle(?)." .. AgriBumper.SAVEGAME_XML_KEY_PARTIAL
AgriBumper.TOOLBOX_MOUNT_MAX_RETRIES = 120
AgriBumper.TOOLBOX_MOUNT_MAX_AGE = 12000

AgriBumper.defaultVehicles = {}
AgriBumper.DEFAULT_VEHICLE_BASE_PATH = "defaultVehicles.vehicle(?)"
AgriBumper.DEFAULT_VEHICLE_XML = Utils.getFilename("xml/defaultVehicles.xml", AgriBumper.MOD_DIR)
AgriBumper.DEFAULT_VEHICLE_SCHEMA = XMLSchema.new("agriBumperDefaultVehicles")

function AgriBumper.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(Drivable, specializations) and SpecializationUtil.hasSpecialization(Lights, specializations) and SpecializationUtil.hasSpecialization(AttacherJoints, specializations)
end

function AgriBumper.initSpecialization()
    g_vehicleConfigurationManager:addConfigurationType("agriBumper", g_i18n:getText("configuration_agriBumper"), nil, VehicleConfigurationItem)
    g_vehicleConfigurationManager:addConfigurationType("agriBumperColor", g_i18n:getText("configuration_agriBumperColor"), nil, VehicleConfigurationItemColor)
    g_vehicleConfigurationManager:addConfigurationType("agriBumperColor2", g_i18n:getText("configuration_agriBumperColor2"), nil, VehicleConfigurationItemColor)
    g_vehicleConfigurationManager:addConfigurationType("agriBumperColor3", g_i18n:getText("configuration_agriBumperColor3"), nil, VehicleConfigurationItemColor)

    local schema = Vehicle.xmlSchema
    schema:setXMLSpecializationType("AgriBumper")
    
    AgriBumper.registerAgriBumperXMLPaths(schema, AgriBumper.AGRIBUMPER_BUMPER_CONFIG_XML_KEY)
    AgriBumper.registerSavegameXMLPaths()
    AgriBumper.registerAttacherJointOverrideXMLPaths(schema, AgriBumper.AGRIBUMPER_ATTACHER_JOINT_OVERRIDE_XML_KEY)
    ObjectChangeUtil.registerObjectChangeXMLPaths(schema, AgriBumper.AGRIBUMPER_CONFIG_XML_KEY)

    schema:setXMLSpecializationType()

    DefaultVehiclesConfigurations.initSchema(AgriBumper.DEFAULT_VEHICLE_SCHEMA)
end

function AgriBumper.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onReadStream", AgriBumper)
    SpecializationUtil.registerEventListener(vehicleType, "onWriteStream", AgriBumper)
	SpecializationUtil.registerEventListener(vehicleType, "onPreLoad", AgriBumper)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", AgriBumper)
    SpecializationUtil.registerEventListener(vehicleType, "onLoadFinished", AgriBumper)
    SpecializationUtil.registerEventListener(vehicleType, "onPostLoad", AgriBumper)
    SpecializationUtil.registerEventListener(vehicleType, "onDelete", AgriBumper)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", AgriBumper)
    SpecializationUtil.registerEventListener(vehicleType, "onPreAttachImplement", AgriBumper)
    SpecializationUtil.registerEventListener(vehicleType, "onPostDetachImplement", AgriBumper)
end

function AgriBumper.registerFunctions(vehicleType)
    SpecializationUtil.registerFunction(vehicleType, "loadAgribumperFromXMLFile", AgriBumper.loadAgribumperFromXMLFile)
    SpecializationUtil.registerFunction(vehicleType, "getIsDefaultAgriBumperVehicle", AgriBumper.getIsDefaultAgriBumperVehicle)
    SpecializationUtil.registerFunction(vehicleType, "loadDefaultAgriBumperVehicles", AgriBumper.loadDefaultAgriBumperVehicles)
    SpecializationUtil.registerFunction(vehicleType, "postLoadDefaultAgriBumperVehicles", AgriBumper.postLoadDefaultAgriBumperVehicles)
    SpecializationUtil.registerFunction(vehicleType, "loadDisableCollisions", AgriBumper.loadDisableCollisions)
    SpecializationUtil.registerFunction(vehicleType, "loadAttacherJointOverrides", AgriBumper.loadAttacherJointOverrides)
    SpecializationUtil.registerFunction(vehicleType, "loadClearPTOs", AgriBumper.loadClearPTOs)
    SpecializationUtil.registerFunction(vehicleType, "loadToolBox", AgriBumper.loadToolBox)
    SpecializationUtil.registerFunction(vehicleType, "setLeasedToolBox", AgriBumper.setLeasedToolBox)
    SpecializationUtil.registerFunction(vehicleType, "onToolBoxLoaded", AgriBumper.onToolBoxLoaded)
	SpecializationUtil.registerFunction(vehicleType, "onToolBoxDelete", AgriBumper.onToolBoxDelete)
end

function AgriBumper.registerAgriBumperXMLPaths(schema, baseName)
    schema:register(XMLValueType.STRING, baseName .. "#linkNode")
    schema:register(XMLValueType.STRING, baseName .. "#filename")

    schema:register(XMLValueType.INT, baseName .. ".disableCollisions#attacherJointIndex", "Attacher joint which disables the collisions", 2)
    schema:register(XMLValueType.STRING, baseName .. ".disableCollisions#onConfigurationActive", "Configuration which disables the collisions", nil)
    schema:register(XMLValueType.VECTOR_N, baseName .. ".disableCollisions#configurationIndices", "Configuration indices which disables the collisions")
    schema:register(XMLValueType.NODE_INDEX , baseName .. ".disableCollisions.disableCollision(?)#node", "Node index of the collision to disable", nil)

    schema:register(XMLValueType.NODE_INDEX , baseName .. ".clearPTOs.clearPTO(?)#node", "Node index of the PTO to clear", nil)
    schema:register(XMLValueType.NODE_INDEX , baseName .. ".clearPTOs.clearPTO(?)#outputNode", "Node index of the PTO output node", nil)

    schema:register(XMLValueType.NODE_INDEX, baseName .. ".toolBox#lockPositionNode", "Node index of the lock position node", nil)
    schema:register(XMLValueType.STRING, baseName .. ".toolBox#xmlFilename", "XML filename of the lock position node", "toolBox/toolBox.xml")
end

function AgriBumper.registerSavegameXMLPaths()
    local schema = Vehicle.xmlSchemaSavegame

    schema:register(XMLValueType.BOOL, AgriBumper.SAVEGAME_XML_KEY_FULL .. "#toolBoxAlreadyBought", "ToolBox already bought", false)
    schema:register(XMLValueType.STRING, AgriBumper.SAVEGAME_XML_KEY_FULL .. "#toolBoxId", "Toolbox unique vehicle id", nil)
end

function AgriBumper.registerAttacherJointOverrideXMLPaths(schema, baseName)
    schema:register(XMLValueType.STRING, baseName .. "#node")
    schema:register(XMLValueType.VECTOR_ROT, baseName .. ".bottomArm#startRotation")
end

function AgriBumper:saveToXMLFile(xmlFile, key, usedModNames)
    local spec = self.spec_agriBumper

    xmlFile:setValue(key .. "#toolBoxAlreadyBought", spec.toolBoxAlreadyBought)
    if spec.leasedToolBox ~= nil then
        xmlFile:setValue(key .. "#toolBoxId", spec.leasedToolBox:getUniqueId())
    end
end

function AgriBumper:onReadStream(streamId, connection)
    local spec = self.spec_agriBumper

    spec.toolBoxAlreadyBought = streamReadBool(streamId)
end

function AgriBumper:onWriteStream(streamId, connection)
    local spec = self.spec_agriBumper

    streamWriteBool(streamId, spec.toolBoxAlreadyBought)
end

function AgriBumper:onPreLoad(savegame)
    log:printDevInfo("PreLoading AgriBumper specialization...", Logger.DEBUG_LEVELS.HIGH)
    self.spec_agriBumper = {}

    local spec = self.spec_agriBumper
    spec.toolBoxToDelete = nil
    spec.toolBoxAlreadyBought = false
    spec.leasedToolBoxId = nil
    spec.leasedToolBox = nil
    spec.defaultVehicleIndex = nil
    spec.defaultVehicleKey = nil
    spec.defaultVehicleFilename = nil
    spec.pendingToolBoxMounts = {}
    spec.toolBoxDeleteListenerVehicle = nil

    if savegame ~= nil then
        local baseKey = string.format("%s.%s", savegame.key, AgriBumper.SAVEGAME_XML_KEY_PARTIAL)

        spec.toolBoxAlreadyBought = savegame.xmlFile:getValue(baseKey .. "#toolBoxAlreadyBought", false)
        spec.leasedToolBoxId = savegame.xmlFile:getValue(baseKey .. "#toolBoxId", nil)
    end
end

function AgriBumper:onLoad(savegame)
    log:printDevInfo("Loading AgriBumper specialization...", Logger.DEBUG_LEVELS.HIGH)
    local spec = self.spec_agriBumper
    spec.disableCollisions = {}
    spec.clearPTOs = {}

    local isDefaultVehicle, defaultVehicleIndex, defaultVehicleFilename = self:getIsDefaultAgriBumperVehicle()
    spec.defaultVehicleIndex = defaultVehicleIndex
    spec.defaultVehicleFilename = defaultVehicleFilename
    spec.defaultVehicleKey = nil

    if defaultVehicleIndex ~= nil and defaultVehicleIndex > 0 then
        spec.defaultVehicleKey = AgriBumper.DEFAULT_VEHICLE_BASE_PATH:sub(1, -4) .. string.format("(%d)", defaultVehicleIndex - 1)
    end

    if isDefaultVehicle then
        self:loadDefaultAgriBumperVehicles(defaultVehicleIndex)
    else
        AgriBumper.defaultVehicles = {}
    end

    if self.configurations["agriBumper"] ~= nil then
        local agriBumperConfigs = string.format("vehicle.agriBumperConfigurations.agriBumperConfiguration(%d).agriBumper", self.configurations["agriBumper"] - 1)

        local linkNodePath = agriBumperConfigs .. "#linkNode"
        local filenamePath = agriBumperConfigs .. "#filename"

        if self.xmlFile:hasProperty(linkNodePath) then
            local linkNodeIndexOrMapping = self.xmlFile:getValue(linkNodePath, nil)
            local filename = self.xmlFile:getValue(filenamePath, "agriBumper/agriBumper.xml")
            log:printDevInfo("XML linkNodeIndexOrMapping: " .. tostring(linkNodeIndexOrMapping), Logger.DEBUG_LEVELS.HIGH)

            local linkNode = I3DUtil.indexToObject(self.components, linkNodeIndexOrMapping, self.i3dMappings)

            local function getIndexPathForNode(node, components)
                local componentIndex, componentNode
                for i, comp in ipairs(components) do
                    if I3DUtil.getIsLinkedToNode(comp.node, node) then
                        componentIndex = i - 1
                        componentNode = comp.node
                        break
                    end
                end
                if not componentIndex or not componentNode then
                    return nil
                end

                local indexPath = I3DUtil.getRelativeNodePathIndices(node, componentNode)
                return string.format("%d>%s", componentIndex, indexPath)
            end

            local linkNodeIndex = getIndexPathForNode(linkNode, self.components)
            log:printDevInfo("AgriBumper linkNodeIndex: " .. tostring(linkNodeIndex), Logger.DEBUG_LEVELS.HIGH)

            if linkNodeIndex ~= nil then
                self:loadAgribumperFromXMLFile(filename, linkNodeIndex)
                ObjectChangeUtil.updateObjectChanges(self.xmlFile, "vehicle.agriBumperConfigurations.agriBumperConfiguration", self.configurations["agriBumper"], self.components, self)
            end
        end
    end     
end

function AgriBumper:onLoadFinished(savegame)

    local spec = self.spec_agriBumper

    if spec.defaultVehicleKey ~= nil and self.configurations["agriBumper"] ~= nil then
        log:printDevInfo("Finishing loading of default vehicle: " .. tostring(spec.defaultVehicleFilename), Logger.DEBUG_LEVELS.HIGH)

        local defaultVehiclesXML = XMLFile.loadIfExists("agriBumperDefaultVehicles", AgriBumper.DEFAULT_VEHICLE_XML, AgriBumper.DEFAULT_VEHICLE_SCHEMA)
        if defaultVehiclesXML ~= nil then
            log:printDevInfo("Found agriBumper configuration: " .. tostring(self.configurations["agriBumper"]), Logger.DEBUG_LEVELS.HIGH)
            local agriBumperConfigs = string.format("%s.agriBumperConfigurations.agriBumperConfiguration(%d).agriBumper", spec.defaultVehicleKey, self.configurations["agriBumper"] - 1)

            if savegame == nil or not spec.toolBoxAlreadyBought then
                self:loadToolBox(defaultVehiclesXML, agriBumperConfigs .. ".toolBox")
            end

            self:setLeasedToolBox()
            defaultVehiclesXML:delete()
            return
        end
    end
    
    if self.configurations["agriBumper"] ~= nil then
        log:printDevInfo("Finishing loading of vehicle: " .. self.xmlFile.filename, Logger.DEBUG_LEVELS.HIGH)
        local agriBumperConfigs = string.format("vehicle.agriBumperConfigurations.agriBumperConfiguration(%d).agriBumper", self.configurations["agriBumper"] - 1)

        if savegame == nil or not spec.toolBoxAlreadyBought then
            self:loadToolBox(self.xmlFile, agriBumperConfigs .. ".toolBox")
        end

        self:setLeasedToolBox()
    end
end

function AgriBumper:onPostLoad(savegame)
    log:printDevInfo("PostLoading AgriBumper specialization...", Logger.DEBUG_LEVELS.HIGH)
    local spec = self.spec_agriBumper
    spec.clearPTOs = {}

    self:postLoadDefaultAgriBumperVehicles()

    if self.configurations["agriBumper"] ~= nil then
        local agriBumperConfigs = string.format("vehicle.agriBumperConfigurations.agriBumperConfiguration(%d).agriBumper", self.configurations["agriBumper"] - 1)

        local linkNodePath = agriBumperConfigs .. "#linkNode"

        if self.xmlFile:hasProperty(linkNodePath) then
            local linkNodeIndex = self.xmlFile:getValue(linkNodePath, nil)

            if linkNodeIndex ~= nil then
                self:loadDisableCollisions(self.xmlFile, agriBumperConfigs .. ".disableCollisions")
                self:loadClearPTOs(self.xmlFile, agriBumperConfigs .. ".clearPTOs")
                self:loadAttacherJointOverrides(self.xmlFile, agriBumperConfigs .. ".attacherJointOverride")

                if self.spec_agriBumper.agriBumper ~= nil and self.spec_agriBumper.agriBumper.loadDynamicMountAttacher ~= nil then
                    self.spec_agriBumper.agriBumper:loadDynamicMountAttacher(self)
                end
            end
        end
    end 
end

function AgriBumper:onDelete()
    log:printDevInfo("Deleting AgriBumper specialization...", Logger.DEBUG_LEVELS.HIGH)
    local spec = self.spec_agriBumper

    if spec ~= nil then
        log:printDevInfo("ToolBoxToDelete == LeasedToolBox: " .. tostring(spec.toolBoxToDelete == spec.leasedToolBox), Logger.DEBUG_LEVELS.HIGH)
        log:printDevInfo("ToolBoxToDelete: " .. tostring(spec.toolBoxToDelete), Logger.DEBUG_LEVELS.HIGH)
        log:printDevInfo("LeasedToolBox: " .. tostring(spec.leasedToolBox), Logger.DEBUG_LEVELS.HIGH)

        AgriBumper.clearToolBoxDeleteListener(self)

        if spec.toolBoxToDelete ~= nil and not spec.toolBoxToDelete.isDeleted then
            local dynamicMountObject = nil

            if spec.toolBoxToDelete.getDynamicMountObject ~= nil then
                dynamicMountObject = spec.toolBoxToDelete:getDynamicMountObject()
            end

            if dynamicMountObject == nil or dynamicMountObject == self then
                spec.toolBoxToDelete:delete()
            else
                log:printWarning("Skipping ToolBox delete because it is mounted to a different object.", Logger.DEBUG_LEVELS.HIGH)
            end
        end

        spec.toolBoxToDelete = nil

        spec.pendingToolBoxMounts = {}

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

function AgriBumper:onPreAttachImplement(object, inputJointDescIndex, jointDescIndex)
    local spec = self.spec_agriBumper
    local disableCollisions = spec.disableCollisions[jointDescIndex]

    if disableCollisions ~= nil and #disableCollisions > 0 then
        log:printDevInfo("Disabling collisions for joint index: " .. tostring(jointDescIndex), Logger.DEBUG_LEVELS.HIGH)
        for _, collisionNode in ipairs(disableCollisions) do
            if collisionNode ~= nil then
                unlink(collisionNode.node)
                link(self.components[1].node, collisionNode.node)
                setTranslation(collisionNode.node, 0, 0, 0)
                setRotation(collisionNode.node, 0, 0, 0)
                setScale(collisionNode.node, 0.001, 0.001, 0.001)
            end
        end
    end

    local clearPTOs = spec.clearPTOs[jointDescIndex]
    if clearPTOs ~= nil and #clearPTOs > 0 then
        log:printDevInfo("Clearing PTOs for joint index: " .. tostring(jointDescIndex), Logger.DEBUG_LEVELS.HIGH)
        for _, clearPTO in pairs(clearPTOs) do
            if clearPTO ~= nil and clearPTO.node ~= nil then
                setVisibility(clearPTO.node, false)
            end
        end
    end
end

function AgriBumper:onPostDetachImplement(implementIndex)
    local spec = self.spec_agriBumper
    local object = nil

	if self.getObjectFromImplementIndex ~= nil then
		object = self:getObjectFromImplementIndex(implementIndex)
	end

    if object ~= nil then
        local attachedImplements = self:getAttachedImplements()

        if attachedImplements[implementIndex] ~= nil then
            local implement = attachedImplements[implementIndex]
            local jointIndex = implement.jointDescIndex

            local disableCollisions = spec.disableCollisions[jointIndex]
            if disableCollisions ~= nil and #disableCollisions > 0 then
                log:printDevInfo("Enabling collisions for joint index: " .. tostring(jointIndex), Logger.DEBUG_LEVELS.HIGH)
                for _, collisionNode in ipairs(disableCollisions) do
                    if collisionNode ~= nil then
                        unlink(collisionNode.node)
                        link(collisionNode.parent, collisionNode.node)
                        setTranslation(collisionNode.node, unpack(collisionNode.translation))
                        setRotation(collisionNode.node, unpack(collisionNode.rotation))
                        setScale(collisionNode.node, unpack(collisionNode.scale))
                    end
                end
            end

            local clearPTOs = spec.clearPTOs[jointIndex]
            if clearPTOs ~= nil and #clearPTOs > 0 then
                log:printDevInfo("Clearing PTOs for joint index: " .. tostring(jointIndex), Logger.DEBUG_LEVELS.HIGH)
                for _, clearPTO in pairs(clearPTOs) do
                    if clearPTO ~= nil and clearPTO.node ~= nil then
                        setVisibility(clearPTO.node, true)
                    end
                end
            end
        end
    end
end

function AgriBumper:onUpdateTick(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
    if self.isServer then
        AgriBumper.processPendingToolBoxMounts(self)

        local spec = self.spec_dynamicMountAttacher

        if spec ~= nil then
            for object, _ in pairs(spec.dynamicMountedObjects) do
                setJointFrame(object.dynamicMountJointIndex, 0, object.dynamicMountJointNode)
            end
        end
    end
end

function AgriBumper:getIsDefaultAgriBumperVehicle()
    local defaultVehiclesXML = XMLFile.loadIfExists("agriBumperDefaultVehicles", AgriBumper.DEFAULT_VEHICLE_XML, AgriBumper.DEFAULT_VEHICLE_SCHEMA)
    local isDefaultVehicle = false
    local foundIndex = nil
    local foundFilename = nil

    if defaultVehiclesXML ~= nil then
        defaultVehiclesXML:iterate(AgriBumper.DEFAULT_VEHICLE_BASE_PATH:sub(1, -4), function (index, key)
            local filename = defaultVehiclesXML:getString(key .. "#filename")
            filename = Utils.getFilename(filename)

            if self.xmlFile.filename:find(filename) ~= nil then
                isDefaultVehicle = true
                foundIndex = index
                foundFilename = filename
                return false
            end
        end)

        defaultVehiclesXML:delete()
    end

    return isDefaultVehicle, foundIndex, foundFilename
end

function AgriBumper:loadAgribumperFromXMLFile(filename, linkNodeIndex)
    -- TODO: Allow loading from a given BaseDirectory of the current vehicle
    local spec = self.spec_agriBumper

    local xmlFilePath = Utils.getFilename(filename, AgriBumper.MOD_DIR)
    local agriBumper = VehiclePart.new(xmlFilePath)

    if agriBumper ~= nil and agriBumper:getXMLFile() ~= nil then
        agriBumper:load(linkNodeIndex, self)
        spec.agriBumper = agriBumper
    end
end

function AgriBumper:loadDefaultAgriBumperVehicles(defaultVehicleIndex)
    log:printDevInfo("Loading default vehicles...", Logger.DEBUG_LEVELS.HIGH)
    log:printDevInfo("Self XML Filename: " .. self.xmlFile.filename, Logger.DEBUG_LEVELS.HIGH)
    AgriBumper.defaultVehicles = {}

    if defaultVehicleIndex == nil or defaultVehicleIndex < 1 then
        return
    end

    local defaultVehiclesXML = XMLFile.loadIfExists("agriBumperDefaultVehicles", AgriBumper.DEFAULT_VEHICLE_XML, AgriBumper.DEFAULT_VEHICLE_SCHEMA)

    if defaultVehiclesXML ~= nil then
        local key = AgriBumper.DEFAULT_VEHICLE_BASE_PATH:sub(1, -4) .. string.format("(%d)", defaultVehicleIndex - 1)
        local filename = defaultVehiclesXML:getString(key .. "#filename")
        filename = Utils.getFilename(filename)
        log:printDevInfo("Loading default vehicle: " .. tostring(filename), Logger.DEBUG_LEVELS.HIGH)

        table.insert(AgriBumper.defaultVehicles, {
            xmlFilePath = filename,
            key = key
        })

        if self.configurations["agriBumper"] ~= nil then
            log:printDevInfo("Found agriBumper configuration: " .. tostring(self.configurations["agriBumper"]), Logger.DEBUG_LEVELS.HIGH)
            local agriBumperConfigs = string.format(key .. ".agriBumperConfigurations.agriBumperConfiguration(%d).agriBumper", self.configurations["agriBumper"] - 1)
            
            local rootNodePath = agriBumperConfigs .. "#rootNode"
            local translationPath = agriBumperConfigs .. "#translation"
            local rotationPath = agriBumperConfigs .. "#rotation"
            local scalePath = agriBumperConfigs .. "#scale"
            local filenamePath = agriBumperConfigs .. "#filename"
            
            if defaultVehiclesXML:hasProperty(rootNodePath) and defaultVehiclesXML:hasProperty(translationPath) and defaultVehiclesXML:hasProperty(rotationPath) and defaultVehiclesXML:hasProperty(scalePath) then
                local rootNodeIndex = defaultVehiclesXML:getValue(rootNodePath, nil)
                local translation = defaultVehiclesXML:getValue(translationPath, "0 0 0", true)
                local rotation = defaultVehiclesXML:getValue(rotationPath, "0 0 0", true)
                local scale = defaultVehiclesXML:getValue(scalePath, "1 1 1", true)

                if rootNodeIndex ~= nil then
                    local id = "agriBumper"
                    local agriBumperTransform = createTransformGroup(id)
                    local rootNode = I3DUtil.indexToObject(self.components, rootNodeIndex, self.i3dMappings)

                    if rootNode ~= nil then
                        local verifiedRootNodeIndex = nil

                        if mappings ~= nil then
                            local mapping = mappings[rootNodeIndex]
                    
                            if mapping ~= nil then
                                if type(mapping) == "table" then
                                    log:printWarning("Multiple mappings found for rootNodeIndex: " .. rootNodeIndex)
                                else
                                    verifiedRootNodeIndex = mapping
                                end
                            end
                        end

                        if verifiedRootNodeIndex == nil then
                            verifiedRootNodeIndex = rootNodeIndex
                        end

                        link(rootNode, agriBumperTransform)
                        setTranslation(agriBumperTransform, unpack(translation))
                        setRotation(agriBumperTransform, unpack(rotation))
                        setScale(agriBumperTransform, unpack(scale))

                        local childIndex = getNumOfChildren(rootNode) - 1
                        local transformIndex = verifiedRootNodeIndex .. "|" .. tostring(childIndex)

                        local bumperFilename = defaultVehiclesXML:getValue(filenamePath, "agriBumper/agriBumper.xml")
                        self:loadAgribumperFromXMLFile(bumperFilename, transformIndex)
                        ObjectChangeUtil.updateObjectChanges(defaultVehiclesXML, key .. ".agriBumperConfigurations.agriBumperConfiguration", self.configurations["agriBumper"], self.components, self)
                    end
                end
            end
        end

        defaultVehiclesXML:delete()
    end
end

function AgriBumper:postLoadDefaultAgriBumperVehicles()
    log:printDevInfo("PostLoading default vehicles...", Logger.DEBUG_LEVELS.HIGH)
    log:printDevInfo("Self XML Filename: " .. self.xmlFile.filename, Logger.DEBUG_LEVELS.HIGH)
    local spec = self.spec_agriBumper
    local defaultVehicleKey = spec.defaultVehicleKey

    if defaultVehicleKey == nil or self.configurations["agriBumper"] == nil then
        return
    end

    log:printDevInfo("PostLoading default vehicle: " .. tostring(spec.defaultVehicleFilename), Logger.DEBUG_LEVELS.HIGH)
    local defaultVehiclesXML = XMLFile.loadIfExists("agriBumperDefaultVehicles", AgriBumper.DEFAULT_VEHICLE_XML, AgriBumper.DEFAULT_VEHICLE_SCHEMA)

    if defaultVehiclesXML ~= nil then
        log:printDevInfo("Found agriBumper configuration: " .. tostring(self.configurations["agriBumper"]), Logger.DEBUG_LEVELS.HIGH)
        local agriBumperConfigs = string.format(defaultVehicleKey .. ".agriBumperConfigurations.agriBumperConfiguration(%d).agriBumper", self.configurations["agriBumper"] - 1)

        local rootNodePath = agriBumperConfigs .. "#rootNode"
        local translationPath = agriBumperConfigs .. "#translation"
        local rotationPath = agriBumperConfigs .. "#rotation"
        local scalePath = agriBumperConfigs .. "#scale"

        if defaultVehiclesXML:hasProperty(rootNodePath) and defaultVehiclesXML:hasProperty(translationPath) and defaultVehiclesXML:hasProperty(rotationPath) and defaultVehiclesXML:hasProperty(scalePath) then
            self:loadDisableCollisions(defaultVehiclesXML, agriBumperConfigs .. ".disableCollisions")
            self:loadClearPTOs(defaultVehiclesXML, agriBumperConfigs .. ".clearPTOs")
            self:loadAttacherJointOverrides(defaultVehiclesXML, agriBumperConfigs .. ".attacherJointOverride")

            if self.spec_agriBumper.agriBumper ~= nil and self.spec_agriBumper.agriBumper.loadDynamicMountAttacher ~= nil then
                self.spec_agriBumper.agriBumper:loadDynamicMountAttacher(self)
            end
        end

        defaultVehiclesXML:delete()
    end
end

function AgriBumper:loadDisableCollisions(xmlFile, disableCollisionConfigsPath)
    local spec = self.spec_agriBumper

    if xmlFile:hasProperty(disableCollisionConfigsPath) then
        local attacherJointIndex = xmlFile:getValue(disableCollisionConfigsPath .. "#attacherJointIndex", 2)
        local attacherJoint = self.spec_attacherJoints.attacherJoints[attacherJointIndex]

        local onConfigurationActive = xmlFile:getValue(disableCollisionConfigsPath .. "#onConfigurationActive", nil)
        local configurationIndices = xmlFile:getValue(disableCollisionConfigsPath .. "#configurationIndices", nil, true)

        if onConfigurationActive ~= nil and g_vehicleConfigurationManager:getConfigurations()[onConfigurationActive] == nil then
            log:printXMLError(xmlFile, string.format("Configuration %s does not exist. Please check the given configuration name.", onConfigurationActive))

            for configName, _ in pairs(g_vehicleConfigurationManager:getConfigurations()) do
                log:printInfo("Available configurations are: " .. configName)
            end
        end

        if onConfigurationActive ~= nil and configurationIndices == nil then
            log:printXMLError(xmlFile, string.format("%s is not set. Please remove onConfigurationActive if not needed or set proper configurationIndices.", disableCollisionConfigsPath .. "#configurationIndices"))
        end

        if attacherJoint ~= nil or onConfigurationActive ~= nil then     
            local disableCollisionNodes = {}                           
            xmlFile:iterate(disableCollisionConfigsPath .. ".disableCollision", function (index, key)
                local node = xmlFile:getValue(key .. "#node", nil, self.components, self.i3dMappings)

                if node ~= nil then
                    local transX, transY, transZ = getTranslation(node)
                    local rotX, rotY, rotZ = getRotation(node)
                    local scaleX, scaleY, scaleZ = getScale(node)

                    local trans = {transX, transY, transZ}
                    local rot = {rotX, rotY, rotZ}
                    local scale = {scaleX, scaleY, scaleZ}

                    local collisionNode = {
                        node = node,
                        rigidBodyType = getRigidBodyType(node),
                        collisionFilterMask = getCollisionFilterMask(node),
                        collisionFilterGroup = getCollisionFilterGroup(node),
                        parent = getParent(node),
                        translation = trans,
                        rotation = rot,
                        scale = scale
                    }

                    table.insert(disableCollisionNodes, collisionNode)
                end
            end)

            if onConfigurationActive ~= nil then
                local configIndex = self.configurations[onConfigurationActive]
                if configIndex ~= nil then
                    for _, index in pairs(configurationIndices) do
                        if index == configIndex then
                            for _, collisionNode in ipairs(disableCollisionNodes) do
                                if collisionNode ~= nil then
                                    unlink(collisionNode.node)
                                    link(self.components[1].node, collisionNode.node)
                                    setTranslation(collisionNode.node, 0, 0, 0)
                                    setRotation(collisionNode.node, 0, 0, 0)
                                    setScale(collisionNode.node, 0.001, 0.001, 0.001)
                                end
                            end
    
                            disableCollisionNodes = {}
                            break
                        end
                    end
                end
            end

            spec.disableCollisions[attacherJointIndex] = disableCollisionNodes
        end
    end
end

function AgriBumper:loadAttacherJointOverrides(xmlFile, attacherJointOverrideConfigs)
    local attacherJointOverrideNodePath = attacherJointOverrideConfigs .. "#node"
    local bottomArmOverrideRotationPath = attacherJointOverrideConfigs .. ".bottomArm#startRotation"
    local attacherJointOverrideNodeIndex = xmlFile:getValue(attacherJointOverrideNodePath, nil)
    local attacherJointOverrideNode = I3DUtil.indexToObject(self.components, attacherJointOverrideNodeIndex, self.i3dMappings)
    local bottomArmOverrideRotation = xmlFile:getValue(bottomArmOverrideRotationPath, nil, true)

    if attacherJointOverrideNode ~= nil and bottomArmOverrideRotation ~= nil then
        local spec_attacherJoints = self.spec_attacherJoints

        if spec_attacherJoints ~= nil then
            local attacherJoint = nil
    
            for i, joint in ipairs(spec_attacherJoints.attacherJoints) do
                if joint.jointTransform == attacherJointOverrideNode then
                    attacherJoint = joint
                    break
                end
            end

            if attacherJoint ~= nil then
                local bottomArm = attacherJoint.bottomArm

                bottomArm.rotX, bottomArm.rotY, bottomArm.rotZ = unpack(bottomArmOverrideRotation)
                setRotation(bottomArm.rotationNode, bottomArm.rotX, bottomArm.rotY, bottomArm.rotZ)
            end
        end
    end
end

function AgriBumper:loadClearPTOs(xmlFile, clearPTOPath)
    local spec = self.spec_agriBumper
    local clearPTOs = {}

    xmlFile:iterate(clearPTOPath .. ".clearPTO", function (index, key)
        local node = xmlFile:getValue(key .. "#node", nil, self.components, self.i3dMappings)
        local outputNode = xmlFile:getValue(key .. "#outputNode", nil, self.components, self.i3dMappings)

        if node ~= nil and outputNode ~= nil then
            local spec_powerTakeOffs = self.spec_powerTakeOffs

            for i, pto in ipairs(spec_powerTakeOffs.outputPowerTakeOffs) do
                if pto.outputNode == outputNode then
                    local attacherJointIndices = pto.attacherJointIndices
                    for attacherJointIndex, _ in pairs(attacherJointIndices) do
                        if clearPTOs[attacherJointIndex] == nil then
                            clearPTOs[attacherJointIndex] = {}
                        end

                        local attacherJoint = self.spec_attacherJoints.attacherJoints[attacherJointIndex]
                        if attacherJoint ~= nil then
                            local clearPTO = {
                                node = node,
                                outputNode = outputNode
                            }

                            table.insert(clearPTOs[attacherJointIndex], clearPTO)
                        end
                    end
                end
            end
        end
    end)

    spec.clearPTOs = clearPTOs
end

function AgriBumper:loadToolBox(xmlFile, toolBoxPath)
    if self.isServer then
        if xmlFile ~= nil and toolBoxPath ~= nil then
            local lockPositionNode = xmlFile:getValue(toolBoxPath .. "#lockPositionNode", nil, self.components, self.i3dMappings)
            local xmlFilename = Utils.getFilename(xmlFile:getValue(toolBoxPath .. "#xmlFilename", "toolBox/toolBox.xml"), AgriBumper.MOD_DIR)

            if lockPositionNode ~= nil and xmlFilename ~= nil then
                local propertyState = self.propertyState == nil and VehiclePropertyState.NONE or self.propertyState
                local isShopPreview = propertyState == VehiclePropertyState.SHOP_CONFIG
                local isSaved = true

                if propertyState ~= VehiclePropertyState.OWNED and propertyState ~= VehiclePropertyState.LEASED then
                    isSaved = false
                end

                local args = {
                    lockPositionNode = lockPositionNode,
                }

                local vehicleLoadingData = VehicleLoadingData.new()
                vehicleLoadingData:setFilename(xmlFilename)
                vehicleLoadingData:setSpawnNode(lockPositionNode)
                vehicleLoadingData:setIgnoreShopOffset(true)
                vehicleLoadingData:setPropertyState(self.propertyState == nil and VehiclePropertyState.NONE or self.propertyState)
                vehicleLoadingData:setOwnerFarmId(self:getOwnerFarmId())
                vehicleLoadingData:setIsRegistered(not isShopPreview)
                vehicleLoadingData:setIsSaved(isSaved)

                log:printDevInfo("Loading ToolBox from XML file: " .. xmlFilename, Logger.DEBUG_LEVELS.HIGH)
                log:printDevInfo("------------------------------------------------", Logger.DEBUG_LEVELS.HIGH)
                for k, v in pairs(vehicleLoadingData) do
                    log:printDevInfo("Key: " .. tostring(k) .. ", Value: " .. tostring(v), Logger.DEBUG_LEVELS.HIGH)
                end
                log:printDevInfo("------------------------------------------------", Logger.DEBUG_LEVELS.HIGH)

                vehicleLoadingData:load(self.onToolBoxLoaded, self, args)
            end
        end
    end
end

function AgriBumper:setLeasedToolBox()
    local spec = self.spec_agriBumper

    if spec.leasedToolBoxId ~= nil and spec.leasedToolBoxId ~= "" then
        local leasedToolBox = g_currentMission.vehicleSystem:getVehicleByUniqueId(spec.leasedToolBoxId)
        if leasedToolBox ~= nil then
            log:printDevInfo("Leased ToolBox: " .. tostring(spec.leasedToolBoxId) .. " found in savegame.", Logger.DEBUG_LEVELS.HIGH)
            spec.leasedToolBox = leasedToolBox
            spec.toolBoxToDelete = leasedToolBox
        else
            log:printWarning("Leased ToolBox: " .. tostring(spec.leasedToolBoxId) .. " not found in savegame.", Logger.DEBUG_LEVELS.HIGH)
        end
    end
end

function AgriBumper:onToolBoxLoaded(vehicles, vehicleLoadState, args)
    log:printDevInfo("Called onToolBoxLoaded", Logger.DEBUG_LEVELS.HIGH)
    if self.isServer then
        if vehicles == nil then
            return
        end

        if self.isDeleted then
            for _, vehicle in ipairs(vehicles) do
                if vehicle ~= nil and not vehicle.isDeleted then
                    vehicle:delete()
                end
            end

            return
        end

        log:printDevInfo("Entered onToolBoxLoaded", Logger.DEBUG_LEVELS.HIGH)
        log:printDevInfo("Loaded vehicles: " .. tostring(#vehicles), Logger.DEBUG_LEVELS.HIGH)
        local spec = self.spec_agriBumper
        local lockPositionNode = args.lockPositionNode

        if #vehicles > 1 then
            log:printXMLError(args.xmlFile, "Multiple vehicles loaded. Only one vehicle is expected.")
            return
        end

        for _, vehicle in ipairs(vehicles) do
            if vehicle ~= nil then
                log:printDevInfo("Successfully loaded Vehicle: " .. vehicle.xmlFile.filename, Logger.DEBUG_LEVELS.HIGH)
                log:printDevInfo("Vehicle is already registered: " .. tostring(vehicle.isRegistered), Logger.DEBUG_LEVELS.HIGH)

                local isShopVehicle = self.propertyState == VehiclePropertyState.SHOP_CONFIG
                local isLeased = self.propertyState == VehiclePropertyState.LEASED
                if isLeased then
                    log:printDevInfo("Leased ToolBox: " .. vehicle.xmlFile.filename, Logger.DEBUG_LEVELS.HIGH)
                    spec.leasedToolBox = vehicle

                    if spec.toolBoxDeleteListenerVehicle ~= nil and spec.toolBoxDeleteListenerVehicle ~= vehicle then
                        AgriBumper.clearToolBoxDeleteListener(self)
                    end

                    spec.toolBoxToDelete = vehicle

                    if spec.toolBoxDeleteListenerVehicle ~= vehicle then
                        vehicle:addDeleteListener(self, "onToolBoxDelete")
                        spec.toolBoxDeleteListenerVehicle = vehicle
                    end
                elseif isShopVehicle then
                    -- Shop preview vehicles are recreated by the shop screen itself.
                    -- Keep a direct reference so this preview toolbox is cleaned when
                    -- the preview vehicle gets deleted on the next reconfigure.
                    AgriBumper.clearToolBoxDeleteListener(self)
                    spec.toolBoxToDelete = vehicle
                end

                if vehicle.getSupportsMountDynamic ~= nil and vehicle:getSupportsMountDynamic() and self.spec_dynamicMountAttacher ~= nil then
                    if isShopVehicle then
                        log:printDevInfo("Skipping dynamic ToolBox mount in SHOP_CONFIG preview.", Logger.DEBUG_LEVELS.HIGH)
                    else
                        local mounted, shouldRetry = AgriBumper.tryMountToolBox(self, vehicle, lockPositionNode)

                        if mounted then
                            log:printDevInfo("ToolBox mounted successfully in onToolBoxLoaded.", Logger.DEBUG_LEVELS.HIGH)
                        elseif shouldRetry then
                            AgriBumper.queueToolBoxMount(self, vehicle, lockPositionNode)
                        else
                            log:printWarning("ToolBox mount skipped (state not suitable for dynamic mount).", Logger.DEBUG_LEVELS.HIGH)
                        end
                    end
                end
            end
        end
    end
end

function AgriBumper.queueToolBoxMount(self, toolBoxVehicle, lockPositionNode)
    local spec = self.spec_agriBumper

    if spec == nil or toolBoxVehicle == nil then
        return
    end

    local hadPendingEntry = spec.pendingToolBoxMounts[toolBoxVehicle] ~= nil

    spec.pendingToolBoxMounts[toolBoxVehicle] = {
        lockPositionNode = lockPositionNode,
        createdAt = g_currentMission ~= nil and g_currentMission.time or 0,
        retries = 0
    }

    if not hadPendingEntry then
        log:printDevInfo("Queued ToolBox mount retry.", Logger.DEBUG_LEVELS.HIGH)
    end

    self:raiseActive()
end

function AgriBumper.clearToolBoxDeleteListener(self)
    local spec = self.spec_agriBumper

    if spec == nil then
        return
    end

    local listenerVehicle = spec.toolBoxDeleteListenerVehicle

    if listenerVehicle ~= nil and not listenerVehicle.isDeleted and not listenerVehicle.isDeleting then
        listenerVehicle:removeDeleteListener(self, "onToolBoxDelete")
    end

    spec.toolBoxDeleteListenerVehicle = nil
end

function AgriBumper.tryMountToolBox(self, toolBoxVehicle, lockPositionNode)
    local spec = self.spec_agriBumper

    if spec == nil or toolBoxVehicle == nil then
        return false, false
    end

    if toolBoxVehicle.isDeleted or toolBoxVehicle.isDeleting then
        return false, false
    end

    if toolBoxVehicle.getDynamicMountObject ~= nil and toolBoxVehicle:getDynamicMountObject() == self then
        spec.toolBoxAlreadyBought = true
        return true, false
    end

    local dynamicMountAttacher = self.spec_dynamicMountAttacher
    if dynamicMountAttacher == nil or dynamicMountAttacher.dynamicMountAttacherTrigger == nil then
        return false, true
    end

    if not self.isRegistered or not toolBoxVehicle.isRegistered then
        return false, true
    end

    if self.getIsSynchronized ~= nil and not self:getIsSynchronized() then
        return false, true
    end

    if toolBoxVehicle.getIsSynchronized ~= nil and not toolBoxVehicle:getIsSynchronized() then
        return false, true
    end

    local triggerData = dynamicMountAttacher.dynamicMountAttacherTrigger
    local componentNode = self:getParentComponent(lockPositionNode)
    local triggerNode = triggerData.triggerNode
    local dynamicMountType, forceLimit = self:getDynamicMountAttacherSettingsByNode(triggerNode)
    local jointNode = triggerData.jointNode
    local forceAcceleration = triggerData.forceAcceleration * forceLimit

    local mounted = toolBoxVehicle:mountDynamic(self, componentNode, jointNode, dynamicMountType, forceAcceleration)
    if mounted then
        spec.toolBoxAlreadyBought = true
        return true, false
    end

    return false, true
end

function AgriBumper.processPendingToolBoxMounts(self)
    local spec = self.spec_agriBumper

    if spec == nil or spec.pendingToolBoxMounts == nil or next(spec.pendingToolBoxMounts) == nil then
        return
    end

    local now = g_currentMission ~= nil and g_currentMission.time or 0

    for toolBoxVehicle, entry in pairs(spec.pendingToolBoxMounts) do
        if toolBoxVehicle == nil or toolBoxVehicle.isDeleted or toolBoxVehicle.isDeleting then
            spec.pendingToolBoxMounts[toolBoxVehicle] = nil
        else
            entry.retries = Utils.getNoNil(entry.retries, 0) + 1

            local mounted, shouldRetry = AgriBumper.tryMountToolBox(self, toolBoxVehicle, entry.lockPositionNode)
            if mounted or not shouldRetry then
                if mounted then
                    log:printDevInfo("ToolBox mounted from pending queue.", Logger.DEBUG_LEVELS.HIGH)
                end

                spec.pendingToolBoxMounts[toolBoxVehicle] = nil
            elseif entry.retries >= AgriBumper.TOOLBOX_MOUNT_MAX_RETRIES or now - entry.createdAt >= AgriBumper.TOOLBOX_MOUNT_MAX_AGE then
                -- Keep retrying for long-running shop/session transitions. If the vehicle is deleted,
                -- the pending entry is cleaned up by the checks above.
                entry.retries = 0
                entry.createdAt = now
            end
        end
    end

    if next(spec.pendingToolBoxMounts) ~= nil then
        self:raiseActive()
    end
end

function AgriBumper:onToolBoxDelete(vehicle)
    local spec = self.spec_agriBumper

    if spec == nil or vehicle == nil then
        return
    end

    log:printDevInfo("ToolBox deleted, cleaning references.", Logger.DEBUG_LEVELS.HIGH)

    if spec.pendingToolBoxMounts ~= nil then
        spec.pendingToolBoxMounts[vehicle] = nil
    end

    if spec.toolBoxToDelete == vehicle then
        spec.toolBoxToDelete = nil
    end

    if spec.leasedToolBox == vehicle then
        spec.leasedToolBox = nil
    end

    if spec.toolBoxDeleteListenerVehicle == vehicle then
        spec.toolBoxDeleteListenerVehicle = nil
    end
end

DynamicMountAttacher.onLoad = Utils.overwrittenFunction(DynamicMountAttacher.onLoad, function (self, superFunc, savegame)
    log:printDevInfo("DynamicMountAttacher onLoad called", Logger.DEBUG_LEVELS.HIGH)
    superFunc(self, savegame)
end)