--[[
    AttachImplementsManually

    Changes the attacher joints behavior to allow attaching implements manually.

	@author: 		BayernGamers
	@date: 			31.05.2025
	@version:		1.0

	History:		v1.0 @31.05.2025 - initial implementation in FS25
                    ------------------------------------------------------------------------------------------------------

	
	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/utils/ExtendedSpecializationUtil.lua", g_currentModDirectory))
source(Utils.getFilename("scripts/AttachImplementsManuallySettingsManager.lua", g_currentModDirectory))
source(Utils.getFilename("scripts/events/AttachImplementEvent.lua", g_currentModDirectory))
source(Utils.getFilename("scripts/utils/ConnectionHoseUtil.lua", g_currentModDirectory))

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

AttachImplementsManually = {}
AttachImplementsManually.MOD_DIRECTORY = g_currentModDirectory
AttachImplementsManually.MOD_NAME = g_currentModName
AttachImplementsManually.MAX_ATTACH_DISTANCE_SQ = 4 -- 2m

function AttachImplementsManually.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(AttacherJoints, specializations)
end

function AttachImplementsManually.initSpecialization()
    local schema = Vehicle.xmlSchema

    schema:setXMLSpecializationType("AttachImplementsManually")
    schema:register(XMLValueType.NODE_INDEX, "vehicle.attachImplementsManually.trigger#rootNode", "Attach implements manually trigger root node")
    schema:register(XMLValueType.VECTOR_TRANS, "vehicle.attachImplementsManually.trigger#offset", "Offset of the attach implements manually trigger in relation to the rootNode", "0 0 0")
    schema:register(XMLValueType.VECTOR_SCALE, "vehicle.attachImplementsManually.trigger#size", "Scale of the attach implements manually trigger", "1 1 1")

    schema:register(XMLValueType.STRING, "vehicle.attachImplementsManually.defaultFrontloaderAttacherType#type", "Default frontloader attacher type configuration for vehicles with frontloader attacher joints", "MANUAL")
    schema:register(XMLValueType.BOOL, "vehicle.attachImplementsManually.defaultFrontloaderAttacherType#allowSelection", "Allow the default frontloader attacher type configuration to be selected by the player in the configuration menu", true)
    schema:register(XMLValueType.STRING_LIST, "vehicle.attachImplementsManually.defaultFrontloaderAttacherType#allowedTypes", "Allowed frontloader attacher type configurations for vehicles with frontloader attacher joints. If empty, all types are allowed")
    schema:setXMLSpecializationType()

    g_vehicleConfigurationManager:addConfigurationType("frontloaderAttacherType", g_i18n:getText("configuration_frontloaderAttacherType"), nil, VehicleConfigurationItem)
end

function AttachImplementsManually.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onPreLoad", AttachImplementsManually)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", AttachImplementsManually)
    SpecializationUtil.registerEventListener(vehicleType, "onLoadFinished", AttachImplementsManually)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdate", AttachImplementsManually)
    SpecializationUtil.registerEventListener(vehicleType, "onRegisterExternalActionEvents", AttachImplementsManually)
    SpecializationUtil.registerEventListener(vehicleType, "onDelete", AttachImplementsManually)

    SpecializationUtil.registerEventListener(vehicleType, "onPreDetachImplement", AttachImplementsManually)
    SpecializationUtil.registerEventListener(vehicleType, "onPostDetachImplement", AttachImplementsManually)

    ExtendedSpecializationUtil.registerOverwrittenEventListener(vehicleType, "onRegisterActionEvents", AttacherJoints, AttachImplementsManually)
    ExtendedSpecializationUtil.registerOverwrittenEventListener(vehicleType, "onLightsTypesMaskChanged", AttacherJoints, AttachImplementsManually)
    ExtendedSpecializationUtil.registerOverwrittenEventListener(vehicleType, "onTurnLightStateChanged", AttacherJoints, AttachImplementsManually)
    ExtendedSpecializationUtil.registerOverwrittenEventListener(vehicleType, "onBrakeLightsVisibilityChanged", AttacherJoints, AttachImplementsManually)
    ExtendedSpecializationUtil.registerOverwrittenEventListener(vehicleType, "onReverseLightsVisibilityChanged", AttacherJoints, AttachImplementsManually)
    ExtendedSpecializationUtil.registerOverwrittenEventListener(vehicleType, "onBeaconLightsVisibilityChanged", AttacherJoints, AttachImplementsManually)
end

function AttachImplementsManually.registerOverwrittenFunctions(vehicleType)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "getCanToggleAttach", AttachImplementsManually.getCanToggleAttach)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "attachImplement", AttachImplementsManually.attachImplement)
end

function AttachImplementsManually.registerFunctions(vehicleType)
    SpecializationUtil.registerFunction(vehicleType, "updateAttachableInfo", AttachImplementsManually.updateAttachableInfo)
    SpecializationUtil.registerFunction(vehicleType, "updateIsPlayerInRange", AttachImplementsManually.updateIsPlayerInRange)
    SpecializationUtil.registerFunction(vehicleType, "updateClosestVehicleInRange", AttachImplementsManually.updateClosestVehicleInRange)
    SpecializationUtil.registerFunction(vehicleType, "getIsElectricConnected", AttachImplementsManually.getIsElectricConnected)
end

function AttachImplementsManually:onPreLoad(savegame)
    self.spec_attachImplementsManually = {}
end

function AttachImplementsManually:onLoad(savegame)
    local spec = self.spec_attachImplementsManually

    spec.isNexatWidespanVehicle = false
    spec.externalControlTrigger = nil
    spec.closestVehicleInPlayerRange = nil
    spec.actionEventAttachData = nil
    spec.modSettingsManager = AttachImplementsManuallySettingsManager.getInstance()
    spec.maxDistanceSq = AttachImplementsManually.MAX_ATTACH_DISTANCE_SQ
    spec.isDetachingImplement = false
    spec.doUpdateIsPlayerInRange = false
    spec.playerRangeUpdateDt = 0
    spec.closestVehicleUpdateDt = 0
    spec.actionEventUpdateDt = 0

    if self.xmlFile.filename:contains("nexatPack") then
        spec.isNexatWidespanVehicle = true
    end
end

function AttachImplementsManually:onLoadFinished(savegame)
    local spec = self.spec_attachImplementsManually
    local spec_attacherJoints = self.spec_attacherJoints
    local spec_externalVehicleControl = self.spec_externalVehicleControl

    if spec_externalVehicleControl ~= nil and spec_attacherJoints ~= nil and spec_attacherJoints.attacherJoints ~= nil and #spec_attacherJoints.attacherJoints > 0 then
        if spec_externalVehicleControl.triggers == nil then
            spec_externalVehicleControl.triggers = {}
        end

        local node, sharedLoadRequestId, failedReason =  g_i3DManager:loadSharedI3DFile(AttachImplementsManually.MOD_DIRECTORY .. "i3d/detectionTrigger.i3d", false, false)

        if failedReason ~= 0 or node == nil then
            log:printError("Failed to load trigger. Error Id: " .. failedReason)
            printCallstack()

            if sharedLoadRequestId ~= nil then
                g_i3DManager:releaseSharedI3DFile(sharedLoadRequestId)
            end

            return
        end

        local triggerNode = getChildAt(node, 0)

        if triggerNode == nil then
            log:printError("Failed to resolve trigger node for vehicle: " .. tostring(self:getName()))
            delete(node)

            if sharedLoadRequestId ~= nil then
                g_i3DManager:releaseSharedI3DFile(sharedLoadRequestId)
            end

            return
        end

        unlink(triggerNode)
        delete(node)

        if sharedLoadRequestId ~= nil then
            g_i3DManager:releaseSharedI3DFile(sharedLoadRequestId)
        end

        local rootNode = self.rootNode

        local length = self.xmlFile:getValue("vehicle.base.size#length", self.size.length or 1)
        local width = self.xmlFile:getValue("vehicle.base.size#width", self.size.width or 1)
        local height = self.xmlFile:getValue("vehicle.base.size#height", self.size.height or 1)

        local lengthOffset = self.xmlFile:getValue("vehicle.base.size#lengthOffset", self.size.lengthOffset or 0)
        local widthOffset = self.xmlFile:getValue("vehicle.base.size#widthOffset", self.size.widthOffset or 0)
        local heightOffset = self.xmlFile:getValue("vehicle.base.size#heightOffset", self.size.heightOffset or 0)

        local calculatedwidth = width * 1.3
        local calculatedLength = length * 1.2

        if width < 2 then
            calculatedwidth = width * 2.3
        end

        -- Enable modders to customize the trigger size and position via XML
        if self.xmlFile:hasProperty("vehicle.attachImplementsManually.trigger") then
            local xmlRootNode = self.xmlFile:getValue("vehicle.attachImplementsManually.trigger#rootNode", nil, self.components, self.i3dMappings)
            local offsetX, offsetY, offsetZ = self.xmlFile:getValue("vehicle.attachImplementsManually.trigger#offset", nil)
            local scaleX, scaleY, scaleZ = self.xmlFile:getValue("vehicle.attachImplementsManually.trigger#size", nil)

            if xmlRootNode ~= nil then
                rootNode = xmlRootNode
            end 

            if offsetX ~= nil and offsetY ~= nil and offsetZ ~= nil then
                widthOffset = offsetX
                heightOffset = offsetY
                lengthOffset = offsetZ
            end

            if scaleX ~= nil and scaleY ~= nil and scaleZ ~= nil then
                calculatedwidth = scaleX
                height = scaleY
                calculatedLength = scaleZ
            end
        end

        local rootTransform = createTransformGroup("attachImplementsManuallyTriggerRoot")
        link(rootNode, rootTransform)
        setScale(rootTransform, calculatedwidth, height, calculatedLength)
        setTranslation(rootTransform, widthOffset, heightOffset, lengthOffset)
        setRotation(rootTransform, 0, 0, 0)
        link(rootTransform, triggerNode)
        setTranslation(triggerNode, 0, 0, 0)
        setScale(triggerNode, 1, 1, 1)
        setRotation(triggerNode, 0, 0, 0)

        local externalControlTrigger = {}

        externalControlTrigger.node = triggerNode
        externalControlTrigger.vehicle = self
        externalControlTrigger.isPlayerInRange = false
        externalControlTrigger.controlFunctions = {}
        externalControlTrigger.callbackId = addTrigger(externalControlTrigger.node, "onExternalVehicleControlTriggerCallback", externalControlTrigger, true, AttachImplementsManually.onExternalVehicleControlTriggerCallback)
        externalControlTrigger.size = {width = calculatedwidth, height = height, length = calculatedLength}

        local names = {
            "attachImplementManually",
            --"attachPTOManually",
            --"attachHosesManually",
        }
        local funcKey = ""

        for _, name in ipairs(names) do
            SpecializationUtil.raiseEvent(self, "onRegisterExternalActionEvents", externalControlTrigger, name, self.xmlFile, funcKey)
        end

        externalControlTrigger.activatable = ExternalVehicleControlActivatable.new(self, externalControlTrigger)
        table.insert(spec_externalVehicleControl.triggers, externalControlTrigger)

        spec.externalControlTrigger = externalControlTrigger
    end
end

function AttachImplementsManually:onUpdate(dt, isActivbeForInput, isActiveForInputIgnoreSelection, isSelected)
    local spec = self.spec_attachImplementsManually
    local externalControlTrigger = spec.externalControlTrigger
    local isPlayerInRange = externalControlTrigger ~= nil and externalControlTrigger.isPlayerInRange
    local isActive = self.getIsActive ~= nil and self:getIsActive()

    if not self.isClient then
        return
    end

    if not isActive and not spec.doUpdateIsPlayerInRange and not isPlayerInRange then
        return
    end

    spec.playerRangeUpdateDt = spec.playerRangeUpdateDt + dt
    spec.closestVehicleUpdateDt = spec.closestVehicleUpdateDt + dt
    spec.actionEventUpdateDt = spec.actionEventUpdateDt + dt

    if spec.playerRangeUpdateDt >= 100 then
        spec.playerRangeUpdateDt = 0
        self:updateIsPlayerInRange()
    end

    if spec.closestVehicleUpdateDt >= 50 then
        spec.closestVehicleUpdateDt = 0
        self:updateClosestVehicleInRange()
    end

    if spec.actionEventUpdateDt >= 50 then
        spec.actionEventUpdateDt = 0
        AttachImplementsManually.updateActionEvents(self)
    end
end

function AttachImplementsManually:onRegisterActionEvents(isActiveForInput, isActiveForInputIgnoreSelection)
    if self.isClient then
        local spec = self.spec_attacherJoints
        self:clearActionEventsTable(spec.actionEvents)

        -- ignore vehicle selection on 'getIsActiveForInput', so we can select the target vehicle and attach or lower it
        if isActiveForInputIgnoreSelection then
            if #spec.attacherJoints > 0 then
                -- only display lower and attach action if selected implement is direct child of vehicle, not sub child
                local selectedImplement = self:getSelectedImplement()
                if selectedImplement ~= nil and selectedImplement.object ~= self then
                    for _, attachedImplement in pairs(spec.attachedImplements) do
                        if attachedImplement == selectedImplement then
                            -- custom registration of the action event. This allows us to overwritte it in the implement (e.g in Foldable)
                            selectedImplement.object:registerLoweringActionEvent(spec.actionEvents, InputAction.LOWER_IMPLEMENT, selectedImplement.object, AttacherJoints.actionEventLowerImplement, false, true, false, true, nil, nil, true)
                        end
                    end
                end

                local _, actionEventId = self:addPoweredActionEvent(spec.actionEvents, InputAction.LOWER_ALL_IMPLEMENTS, self, AttacherJoints.actionEventLowerAllImplements, false, true, false, true, nil, nil, true)
                g_inputBinding:setActionEventTextVisibility(actionEventId, false)
            end

            if self:getSelectedVehicle() == self then
                local state, _ = self:registerSelfLoweringActionEvent(spec.actionEvents, InputAction.LOWER_IMPLEMENT, self, AttacherJoints.actionEventLowerImplement, false, true, false, true, nil, nil, true)

                -- if the selected attacher vehicle can not be lowered and we got only one implement that can be lowered
                -- we add the lowering action for the first implement
                if state == nil or not state then
                    if #spec.attachedImplements == 1 then
                        local firstImplement = spec.attachedImplements[1]
                        if firstImplement ~= nil then
                            firstImplement.object:registerLoweringActionEvent(spec.actionEvents, InputAction.LOWER_IMPLEMENT, firstImplement.object, AttacherJoints.actionEventLowerImplement, false, true, false, true, nil, nil, true)
                        end
                    end
                end
            end

            local _, actionEventId = self:addActionEvent(spec.actionEvents, InputAction.ATTACH, self, AttachImplementsManually.actionEventAttachNoPlayerRequired, false, true, false, true, nil, nil, true)
            g_inputBinding:setActionEventTextPriority(actionEventId, GS_PRIO_VERY_HIGH)
            g_inputBinding:setActionEventActive(actionEventId, false)

            _, actionEventId = self:addActionEvent(spec.actionEvents, InputAction.DETACH, self, AttachImplementsManually.actionEventAttachNoPlayerRequired, false, true, false, true, nil, nil, true)
            g_inputBinding:setActionEventTextVisibility(actionEventId, false)
            g_inputBinding:setActionEventActive(actionEventId, false)

            AttacherJoints.updateActionEvents(self)
        end
    end
end

function AttachImplementsManually:onRegisterExternalActionEvents(trigger, name, xmlFile, key)
    local spec = self.spec_attachImplementsManually
    local spec_attacherJoints = self.spec_attacherJoints

    if name == "attachImplementManually" then
        self:registerExternalActionEvent(trigger, name, AttachImplementsManually.externalActionEventRegister, AttachImplementsManually.externalActionEventUpdate)
    end
end

function AttachImplementsManually:onDelete()
    local spec = self.spec_externalVehicleControl
    if spec ~= nil and spec.triggers ~= nil then
        for _, trigger in ipairs(spec.triggers) do
            if trigger.node ~= nil then
                removeTrigger(trigger.node, trigger.callbackId)
            end

            g_currentMission.activatableObjectsSystem:removeActivatable(trigger.activatable)
        end
    end
end

function AttachImplementsManually:onPreDetachImplement()
    local spec = self.spec_attachImplementsManually
    local spec_attachConnectionHosesManually = self.spec_attachConnectionHosesManually
    local spec_attachPowerTakeOffsManually = self.spec_attachPowerTakeOffsManually

    if spec ~= nil then
        spec.isDetachingImplement = true
    end

    if spec_attachConnectionHosesManually ~= nil and spec_attachConnectionHosesManually.actionEventAttachHosesData ~= nil then
        local actionEventId = spec_attachConnectionHosesManually.actionEventAttachHosesData.actionEventId

        if actionEventId ~= nil then
            g_inputBinding:setActionEventActive(actionEventId, false)
        end
    end

    if spec_attachPowerTakeOffsManually ~= nil and spec_attachPowerTakeOffsManually.actionEventAttachPTOData ~= nil then
        local actionEventId = spec_attachPowerTakeOffsManually.actionEventAttachPTOData.actionEventId

        if actionEventId ~= nil then
            g_inputBinding:setActionEventActive(actionEventId, false)
        end
    end
end

function AttachImplementsManually:onPostDetachImplement()
    local spec = self.spec_attachImplementsManually

    if spec ~= nil then
        spec.isDetachingImplement = false
    end
end

function AttachImplementsManually:onLightsTypesMaskChanged(lightsTypesMask)
    local spec_attacherJoints = self.spec_attacherJoints

    for _, implement in pairs(spec_attacherJoints.attachedImplements) do
        local vehicle = implement.object

        if vehicle ~= nil and vehicle.setLightsTypesMask ~= nil then
            if self:getIsElectricConnected(vehicle) then
                vehicle:setLightsTypesMask(lightsTypesMask, true, true)
            else
                vehicle:setLightsTypesMask(0, true, true)
            end
        end
    end
end

function AttachImplementsManually:onTurnLightStateChanged(state)
    local spec = self.spec_attacherJoints
    for _, implement in pairs(spec.attachedImplements) do
        local vehicle = implement.object
        if vehicle ~= nil and vehicle.setTurnLightState ~= nil then
            if self:getIsElectricConnected(vehicle) then
                vehicle:setTurnLightState(state, true, true)
            else
                vehicle:setTurnLightState(false, true, true)
            end
        end
    end
end

function AttachImplementsManually:onBrakeLightsVisibilityChanged(visibility)
    local spec = self.spec_attacherJoints
    for _, implement in pairs(spec.attachedImplements) do
        local vehicle = implement.object
        if vehicle ~= nil and vehicle.setBrakeLightsVisibility ~= nil then
            if self:getIsElectricConnected(vehicle) then
                vehicle:setBrakeLightsVisibility(visibility)
            else
                vehicle:setBrakeLightsVisibility(false)
            end
        end
    end
end

function AttachImplementsManually:onReverseLightsVisibilityChanged(visibility)
    local spec = self.spec_attacherJoints
    for _, implement in pairs(spec.attachedImplements) do
        local vehicle = implement.object
        if vehicle ~= nil and vehicle.setReverseLightsVisibility ~= nil then
            if self:getIsElectricConnected(vehicle) then
                vehicle:setReverseLightsVisibility(visibility)
            else
                vehicle:setReverseLightsVisibility(false)
            end
        end
    end
end

function AttachImplementsManually:onBeaconLightsVisibilityChanged(visibility)
    local spec = self.spec_attacherJoints
    for _, implement in pairs(spec.attachedImplements) do
        local vehicle = implement.object
        if vehicle ~= nil and vehicle.setBeaconLightsVisibility ~= nil then
            if self:getIsElectricConnected(vehicle) then
                vehicle:setBeaconLightsVisibility(visibility, true, true)
            else
                vehicle:setBeaconLightsVisibility(false, true, true)
            end
        end
    end
end

function AttachImplementsManually.onExternalVehicleControlTriggerCallback(trigger, triggerId, otherId, onEnter, onLeave, onStay)
    if g_localPlayer ~= nil and otherId == g_localPlayer.rootNode then
        if trigger.vehicle ~= nil and trigger.vehicle.spec_attachImplementsManually ~= nil then
            local spec = trigger.vehicle.spec_attachImplementsManually

            if onEnter then
                spec.doUpdateIsPlayerInRange = true
                --trigger.isPlayerInRange = true
                trigger.vehicle:raiseActive()

                trigger.activatable:updateText()
                g_currentMission.activatableObjectsSystem:addActivatable(trigger.activatable)
            else
                spec.doUpdateIsPlayerInRange = false
                --trigger.isPlayerInRange = false
                g_currentMission.activatableObjectsSystem:removeActivatable(trigger.activatable)
            end
        end
    end
end

function AttachImplementsManually.externalActionEventRegister(data, vehicle)
    local spec = vehicle.spec_attachImplementsManually
    local spec_attacherJoints = vehicle.spec_attacherJoints

    local function createVehicleActionEvent(func)
        return function (...)
            return func(vehicle, ...)
        end
    end

    if spec.externalControlTrigger.isPlayerInRange then
        local actionEvent = createVehicleActionEvent(AttachImplementsManually.actionEventAttach)
        local _, actionEventId = g_inputBinding:registerActionEvent(InputAction.ATTACH, data, actionEvent, false, true, false, true)
        data.actionEventId = actionEventId
        g_inputBinding:setActionEventText(data.actionEventId, spec_attacherJoints.texts.actionAttach)
        g_inputBinding:setActionEventTextPriority(data.actionEventId, GS_PRIO_VERY_HIGH)
        g_inputBinding:setActionEventActive(data.actionEventId, false)

        spec.actionEventAttachData = data
    end
end

function AttachImplementsManually.externalActionEventUpdate(data, vehicle)
    local spec = vehicle.spec_attachImplementsManually
    local spec_attacherJoints = vehicle.spec_attacherJoints

    if spec.modSettingsManager:getEnableAttachImplementsManually() and not AttachImplementsManually.getIsManualAttachForNexatDisabled(vehicle) then
        if spec.externalControlTrigger ~= nil and spec.externalControlTrigger.isPlayerInRange then
            if data ~= nil then
                local visible = false

                if spec_attacherJoints ~= nil and vehicle.updateAttachableInfo ~= nil then
                    vehicle:updateAttachableInfo()
                end

                local info = spec_attacherJoints.attachableInfo

                if vehicle:getCanToggleAttach() then
                    if info.warning ~= nil then
                        local isNexatOverlappingTriggersWarning = info.warning == spec_attacherJoints.texts.warningToolNotCompatible and vehicle.xmlFile.filename:contains("nexatPack")
                        if not isNexatOverlappingTriggersWarning then
                            g_currentMission:showBlinkingWarning(info.warning, 500)
                        end
                    end

                    local text = spec_attacherJoints.texts.actionAttach
                    local prio = GS_PRIO_VERY_LOW

                    --local selectedVehicle = self:getSelectedVehicle()
                    local selectedVehicle = spec.closestVehicleInPlayerRange

                    local spec_attachPowerTakeOffsManually = vehicle.spec_attachPowerTakeOffsManually
                    local spec_attachConnectionHosesManually = vehicle.spec_attachConnectionHosesManually
                    local implementIndex = vehicle:getImplementIndexByObject(selectedVehicle)
                    local jointDescIndex = vehicle:getAttacherJointIndexFromImplementIndex(implementIndex)
                    local isPTOAttached = false
                    local areHosesAttached = false
                    local allowDirectAttach = false

                    if spec_attachPowerTakeOffsManually ~= nil and spec_attachPowerTakeOffsManually.attachedPTOs ~= nil then
                        isPTOAttached = spec_attachPowerTakeOffsManually.attachedPTOs[jointDescIndex] ~= nil
                    end

                    if spec_attachConnectionHosesManually ~= nil and spec_attachConnectionHosesManually.attachedHoses ~= nil then
                        areHosesAttached = spec_attachConnectionHosesManually.attachedHoses[jointDescIndex] ~= nil
                    end

                    if selectedVehicle ~= nil then
                        --local info = {}

                        if selectedVehicle.getAttacherVehicle ~= nil then
                            local attacherVehicle = selectedVehicle:getAttacherVehicle()
                            if attacherVehicle ~= nil and attacherVehicle.getImplementIndexByObject ~= nil and attacherVehicle.getAttacherJointIndexFromImplementIndex ~= nil then
                                local implementIndex = attacherVehicle:getImplementIndexByObject(selectedVehicle)
                                local jointDescIndex = attacherVehicle:getAttacherJointIndexFromImplementIndex(implementIndex)

                                info = {
                                    attacherVehicle = attacherVehicle,
                                    attachable = selectedVehicle,
                                    attacherVehicleJointDescIndex = jointDescIndex,
                                    attachableJointDescIndex = selectedVehicle:getActiveInputAttacherJointDescIndex()
                                }

                                if (SpecializationUtil.hasSpecialization(Cutter, selectedVehicle.specializations) and not SpecializationUtil.hasSpecialization(Combine, selectedVehicle.specializations) and AttacherJointUtil.getUsesCutterAttacher(info))
                                    or AttacherJointUtil.getUsesFrontloaderToolAttacher(info) or AttacherJointUtil.getAllowsAttachFromInside(info) or AttacherJointUtil.getUsesHookLiftAttacher(info) or AttacherJointUtil.getUsesBigBagAttacher(info)
                                then
                                    allowDirectAttach = true
                                end
                            end
                        end

                    elseif info.attacherVehicle ~= nil and info.attachable ~= nil then
                        if (SpecializationUtil.hasSpecialization(Cutter, info.attachable.specializations) and not SpecializationUtil.hasSpecialization(Combine, info.attachable.specializations) and AttacherJointUtil.getUsesCutterAttacher(info))
                                    or AttacherJointUtil.getUsesFrontloaderToolAttacher(info) or AttacherJointUtil.getAllowsAttachFromInside(info) or AttacherJointUtil.getUsesHookLiftAttacher(info) or AttacherJointUtil.getUsesBigBagAttacher(info)
                        then
                            allowDirectAttach = true
                        end
                    end

                    if not isPTOAttached and not areHosesAttached and not allowDirectAttach then
                        if selectedVehicle ~= nil and not selectedVehicle.isDeleted and selectedVehicle.isDetachAllowed ~= nil and selectedVehicle:isDetachAllowed() then
                            if selectedVehicle:getAttacherVehicle() ~= nil then
                                visible = true
                                text = spec_attacherJoints.texts.actionDetach
                                prio = GS_PRIO_VERY_HIGH
                                if g_inputBinding:getActionEventActive(data.actionEventId) then
                                    g_currentMission:showDetachContext(selectedVehicle)
                                end
                            end

                        elseif info.attacherVehicle ~= nil and info.attachable ~= nil and info.attacherVehicle ~= info.attachable then
                            if g_currentMission.accessHandler:canFarmAccess(vehicle:getActiveFarm(), info.attachable) then
                                visible = true
                                text = spec_attacherJoints.texts.actionAttach

                                if g_inputBinding:getActionEventActive(data.actionEventId) then
                                    g_currentMission:showAttachContext(info.attachable)
                                end
                                prio = GS_PRIO_VERY_HIGH
                            else
                                spec_attacherJoints.showAttachNotAllowedText = 100
                            end
                        end

                        g_inputBinding:setActionEventText(data.actionEventId, text)
                        g_inputBinding:setActionEventTextPriority(data.actionEventId, prio)
                        g_inputBinding:setActionEventTextVisibility(data.actionEventId, true)
                    else
                        visible = true
                        g_inputBinding:setActionEventTextVisibility(data.actionEventId, false)
                    end
                end

                g_inputBinding:setActionEventActive(data.actionEventId, visible)
            end
        end
    else
        if data ~= nil then
            g_inputBinding:setActionEventActive(data.actionEventId, false)
        end
    end
end

function AttachImplementsManually:actionEventAttach(actionName, actionName, inputValue, callbackState, isAnalog)
    local spec = self.spec_attachImplementsManually
    local spec_attacherJoints = self.spec_attacherJoints
    if spec_attacherJoints ~= nil and self.updateAttachableInfo ~= nil then
        self:updateAttachableInfo()
    end

    local info = self.spec_attacherJoints.attachableInfo
    if info.attachable ~= nil and info.attacherVehicle ~= info.attachable then
        -- attach
        if info.attachable.getActiveInputAttacherJointDescIndex ~= nil then
            local inputAttacherJoint = info.attachable:getInputAttacherJointByJointDescIndex(info.attachableJointDescIndex)

            if SpecializationUtil.hasSpecialization(Cutter, info.attachable.specializations) and not SpecializationUtil.hasSpecialization(Combine, info.attachable.specializations) and AttacherJointUtil.getUsesCutterAttacher(info) then
                g_currentMission:showBlinkingWarning(g_i18n:getText("warning_attachCutterFromMainVehicle"), 2000)
                return
            end

            --[[
            if inputAttacherJoint ~= nil and inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_FRONTLOADER or inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_TELEHANDLER or inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_WHEELLOADER then
                g_currentMission:showBlinkingWarning(g_i18n:getText("warning_attachFrontloaderFromMainVehicle"), 2000)
                return
            end
            ]]

            if AttacherJointUtil.getAllowsAttachFromInside(info) then
                g_currentMission:showBlinkingWarning(g_i18n:getText("warning_attachImplementFromMainVehicle"), 2000)
                return
            end

            if AttacherJointUtil.getUsesFrontloaderToolAttacher(info) then
                g_currentMission:showBlinkingWarning(g_i18n:getText("warning_attachFrontloaderFromMainVehicle"), 2000)
                return
            end

            if inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_BIGBAG then
                g_currentMission:showBlinkingWarning(g_i18n:getText("warning_attachBigBagFromMainVehicle"), 2000)
                return
            end

            if inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_HOOKLIFT then
                g_currentMission:showBlinkingWarning(g_i18n:getText("warning_attachHookliftFromMainVehicle"), 2000)
                return
            end
        end

        local attachAllowed, warning = info.attachable:isAttachAllowed(self:getActiveFarm(), info.attacherVehicle)
        if attachAllowed then
            if self.isServer then
                self:attachImplementFromInfo(info)
            else
                g_client:getServerConnection():sendEvent(VehicleAttachRequestEvent.new(info))
            end
        else
            if warning ~= nil then
                g_currentMission:showBlinkingWarning(warning, 2000)
            end
        end
    else
        local object = spec.closestVehicleInPlayerRange
        -- detach
        if object ~= nil and object ~= self and object.isDetachAllowed ~= nil then
            if object.getActiveInputAttacherJointDescIndex ~= nil then
                local inputAttacherJointDescIndex = object:getActiveInputAttacherJointDescIndex()
                local inputAttacherJoint = object:getInputAttacherJointByJointDescIndex(inputAttacherJointDescIndex)
                local attacherVehicle = object:getAttacherVehicle()
                local implementIndex = attacherVehicle:getImplementIndexByObject(object)
                local jointDescIndex = attacherVehicle:getAttacherJointIndexFromImplementIndex(implementIndex)

                --[[
                if inputAttacherJoint ~= nil and inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_FRONTLOADER or inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_TELEHANDLER or inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_WHEELLOADER then
                    g_currentMission:showBlinkingWarning(g_i18n:getText("warning_detachFrontloaderFromMainVehicle"), 2000)
                    return
                end
                ]]

                local info = {
                    attacherVehicle = attacherVehicle,
                    attachable = object,
                    attacherVehicleJointDescIndex = jointDescIndex,
                    attachableJointDescIndex = inputAttacherJointDescIndex
                }
                
                if SpecializationUtil.hasSpecialization(Cutter, object.specializations) and not SpecializationUtil.hasSpecialization(Combine, object.specializations) and AttacherJointUtil.getUsesCutterAttacher(info) then
                    g_currentMission:showBlinkingWarning(g_i18n:getText("warning_detachCutterFromMainVehicle"), 2000)
                    return
                end

                if AttacherJointUtil.getAllowsAttachFromInside(info) then
                    g_currentMission:showBlinkingWarning(g_i18n:getText("warning_detachImplementFromMainVehicle"), 2000)
                    return
                end

                if AttacherJointUtil.getUsesFrontloaderToolAttacher(info) then
                    g_currentMission:showBlinkingWarning(g_i18n:getText("warning_detachFrontloaderFromMainVehicle"), 2000)
                    return
                end

                if inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_BIGBAG then
                    g_currentMission:showBlinkingWarning(g_i18n:getText("warning_detachBigBagFromMainVehicle"), 2000)
                    return
                end

                if inputAttacherJoint.jointType == AttacherJoints.JOINTTYPE_HOOKLIFT then
                    g_currentMission:showBlinkingWarning(g_i18n:getText("warning_detachHookliftFromMainVehicle"), 2000)
                    return
                end
            end

            local detachAllowed, warning, showWarning = object:isDetachAllowed()
            if detachAllowed then
                AttachImplementsManually.onPreDetachImplement(self)
                object:startDetachProcess()
            elseif showWarning == nil or showWarning then
                g_currentMission:showBlinkingWarning(warning or self.spec_attacherJoints.texts.detachNotAllowed, 2000)
            end
        end
    end
end

function AttachImplementsManually:actionEventAttachNoPlayerRequired(actionName, actionName, inputValue, callbackState, isAnalog)
    local spec = self.spec_attachImplementsManually
    local spec_attacherJoints = self.spec_attacherJoints

    local info = self.spec_attacherJoints.attachableInfo
    if info.attachable ~= nil then
        -- attach
        local attachAllowed, warning = info.attachable:isAttachAllowed(self:getActiveFarm(), info.attacherVehicle)
        if attachAllowed then
            if self.isServer then
                self:attachImplementFromInfo(info)
            else
                g_client:getServerConnection():sendEvent(VehicleAttachRequestEvent.new(info))

                if not spec.modSettingsManager:getEnableAttachImplementsManually() or AttachImplementsManually.getIsManualAttachForNexatDisabled(self) then
                    log:printDevInfo("info.attachableJointDescIndex: " .. tostring(info.attachableJointDescIndex), Logger.DEBUG_LEVELS.HIGH)

                    for key, value in pairs(info) do
                        log:printDevInfo("info." .. tostring(key) .. ": " .. tostring(value), Logger.DEBUG_LEVELS.HIGH)
                    end

                    AttachImplementEvent.sendEvent(info.attacherVehicle, info.attachable, info.attacherVehicleJointDescIndex, info.attachableJointDescIndex, true, false)
                end
            end
        else
            if warning ~= nil then
                g_currentMission:showBlinkingWarning(warning, 2000)
            end
        end
    else
        -- detach
        local object = self:getSelectedVehicle()
        --local object = spec.closestVehicleInPlayerRange
        if object ~= nil and object ~= self and object.isDetachAllowed ~= nil then
            local attacherVehicle = object:getAttacherVehicle()
            if attacherVehicle ~= nil then
                local detachAllowed, warning, showWarning = object:isDetachAllowed()
                if detachAllowed then
                    if not self.isServer and (not spec.modSettingsManager:getEnableAttachImplementsManually() or AttachImplementsManually.getIsManualAttachForNexatDisabled(self)) then
                        log:printDevInfo("self: " .. tostring(self:getName()), Logger.DEBUG_LEVELS.HIGH)

                        local implementIndex = attacherVehicle:getImplementIndexByObject(object)

                        log:printDevInfo("implementIndex: " .. tostring(implementIndex), Logger.DEBUG_LEVELS.HIGH)

                        local jointDescIndex = attacherVehicle:getAttacherJointIndexFromImplementIndex(implementIndex)

                        log:printDevInfo("jointDescIndex: " .. tostring(jointDescIndex), Logger.DEBUG_LEVELS.HIGH)

                        local inputJointDescIndex = object.spec_attachable.inputAttacherJointDescIndex

                        AttachImplementEvent.sendEvent(attacherVehicle, object, jointDescIndex, inputJointDescIndex, false, false)
                    end

                    object:startDetachProcess()
                elseif showWarning == nil or showWarning then
                    g_currentMission:showBlinkingWarning(warning or self.spec_attacherJoints.texts.detachNotAllowed, 2000)
                end
            else
                log:printError("Attacher vehicle not found for object: " .. tostring(object:getName()))
                printCallstack()
            end
        end
    end
end

function AttachImplementsManually:getCanToggleAttach(superFunc)
    if SpecializationUtil.hasSpecialization(AttachImplementsManually, self.specializations) then
        return true
    end

    return superFunc(self)
end

function AttachImplementsManually:attachImplement(superFunc, object, inputJointDescIndex, jointDescIndex, noEventSend, index, startLowered, noSmoothAttach, loadFromSavegame)
    local spec = self.spec_attachImplementsManually
    local spec_attacherJoints = self.spec_attacherJoints
    local connectedInputsToReset = {}
    
    local attacherJoint = spec_attacherJoints.attacherJoints[jointDescIndex]
    local info = {
        attacherVehicle = self,
        attachable = object,
        attacherVehicleJointDescIndex = jointDescIndex,
        attachableJointDescIndex = inputJointDescIndex
    }

    local enableFix = spec.modSettingsManager:getEnableAttachImplementsManually() and not AttacherJointUtil.getAllowsAttachFromInside(info) and not AttachImplementsManually.getIsManualAttachForNexatDisabled(self)

    if enableFix then
        if attacherJoint ~= nil then
            local transNodeMinY = attacherJoint.transNodeMinY
            local transNodeMaxY = attacherJoint.transNodeMaxY

            if self.getOutputPowerTakeOffsByJointDescIndex ~= nil and transNodeMinY ~= transNodeMaxY then
                local outputPowerTakeOffs = self:getOutputPowerTakeOffsByJointDescIndex(jointDescIndex)
                for _, outputPowerTakeOff in ipairs(outputPowerTakeOffs) do
                    if object.getInputPowerTakeOffsByJointDescIndexAndName ~= nil then
                        local inputPowerTakeOffs = object:getInputPowerTakeOffsByJointDescIndexAndName(inputJointDescIndex, outputPowerTakeOff.ptoName)
                        for _, inputPowerTakeOff in ipairs(inputPowerTakeOffs) do
                            outputPowerTakeOff.connectedInput = inputPowerTakeOff
                            table.insert(connectedInputsToReset, {
                                outputPowerTakeOff = outputPowerTakeOff,
                                jointDescIndex = jointDescIndex
                            })
                        end
                    end
                end
            end
        end
    end

    superFunc(self, object, inputJointDescIndex, jointDescIndex, noEventSend, index, startLowered, noSmoothAttach, loadFromSavegame)

    if enableFix then
        local spec_attachPowerTakeOffsManually = self.spec_attachPowerTakeOffsManually

        for _, resetData in ipairs(connectedInputsToReset) do
            local isPTOManuallyAttached = spec_attachPowerTakeOffsManually ~= nil
                and spec_attachPowerTakeOffsManually.attachedPTOs ~= nil
                and spec_attachPowerTakeOffsManually.attachedPTOs[resetData.jointDescIndex] ~= nil

            -- Keep real PTO links created during post-attach (e.g. after join sync).
            if not isPTOManuallyAttached then
                resetData.outputPowerTakeOff.connectedInput = nil
            end
        end
    end
end

function AttachImplementsManually:updateAttachableInfo()
    local spec = self.spec_attachImplementsManually
    local spec_attacherJoints = self.spec_attacherJoints

    local info = spec_attacherJoints.attachableInfo

    if self.isClient then
        if self:getCanToggleAttach() then
            local maxDistanceSq = spec.maxDistanceSq or AttachImplementsManually.MAX_ATTACH_DISTANCE_SQ
            AttachImplementsManually.updateVehiclesInAttachRange(self, maxDistanceSq, AttacherJoints.MAX_ATTACH_ANGLE, true)
        else
            info.attacherVehicle, info.attacherVehicleJointDescIndex, info.attachable, info.attachableJointDescIndex = nil, nil, nil, nil
        end
    end
end

function AttachImplementsManually:updateActionEvents()
    local spec = self.spec_attachImplementsManually

    if spec.externalControlTrigger ~= nil and spec.externalControlTrigger.isPlayerInRange and spec.actionEventAttachData ~= nil then
        AttachImplementsManually.externalActionEventUpdate(spec.actionEventAttachData, self)
    end

    local selectedVehicle = self:getSelectedVehicle()
    if selectedVehicle ~= nil then
        local info = {}

        if selectedVehicle.getAttacherVehicle ~= nil then
            local attacherVehicle = selectedVehicle:getAttacherVehicle()
            if attacherVehicle ~= nil and attacherVehicle.getImplementIndexByObject ~= nil and attacherVehicle.getAttacherJointIndexFromImplementIndex ~= nil then
                local implementIndex = attacherVehicle:getImplementIndexByObject(selectedVehicle)
                local jointDescIndex = attacherVehicle:getAttacherJointIndexFromImplementIndex(implementIndex)

                info = {
                    attacherVehicle = attacherVehicle,
                    attachable = selectedVehicle,
                    attacherVehicleJointDescIndex = jointDescIndex,
                    attachableJointDescIndex = selectedVehicle:getActiveInputAttacherJointDescIndex()
                }
            end
        end

        if (SpecializationUtil.hasSpecialization(Cutter, selectedVehicle.specializations) and not SpecializationUtil.hasSpecialization(Combine, selectedVehicle.specializations) and AttacherJointUtil.getUsesCutterAttacher(info))
            or AttacherJointUtil.getUsesFrontloaderToolAttacher(info) or not spec.modSettingsManager:getEnableAttachImplementsManually() or AttachImplementsManually.getIsManualAttachForNexatDisabled(self)
            or AttacherJointUtil.getAllowsAttachFromInside(info) or AttacherJointUtil.getUsesHookLiftAttacher(info) or AttacherJointUtil.getUsesBigBagAttacher(info)
        then
            local actionEventAttach = self.spec_attacherJoints.actionEvents[InputAction.ATTACH]
            if actionEventAttach ~= nil then
                g_inputBinding:setActionEventActive(actionEventAttach.actionEventId, true)
            end

            local actionEventDetach = self.spec_attacherJoints.actionEvents[InputAction.DETACH]
            if actionEventDetach ~= nil then
                g_inputBinding:setActionEventActive(actionEventDetach.actionEventId, true)
            end
        else
            local actionEventAttach = self.spec_attacherJoints.actionEvents[InputAction.ATTACH]
            if actionEventAttach ~= nil then
                g_inputBinding:setActionEventActive(actionEventAttach.actionEventId, false)
            end

            local actionEventDetach = self.spec_attacherJoints.actionEvents[InputAction.DETACH]
            if actionEventDetach ~= nil then
                g_inputBinding:setActionEventActive(actionEventDetach.actionEventId, false)
            end
        end
    end

    local info = self.spec_attacherJoints.attachableInfo
    if info.attacherVehicle ~= nil then
        if g_currentMission.accessHandler:canFarmAccess(self:getActiveFarm(), info.attachable) then

            if (SpecializationUtil.hasSpecialization(Cutter, info.attachable.specializations) and not SpecializationUtil.hasSpecialization(Combine, info.attachable.specializations) and AttacherJointUtil.getUsesCutterAttacher(info))
                or AttacherJointUtil.getUsesFrontloaderToolAttacher(info) or not spec.modSettingsManager:getEnableAttachImplementsManually() or AttachImplementsManually.getIsManualAttachForNexatDisabled(self)
                or AttacherJointUtil.getAllowsAttachFromInside(info) or AttacherJointUtil.getUsesHookLiftAttacher(info) or AttacherJointUtil.getUsesBigBagAttacher(info)
            then
                local actionEventAttach = self.spec_attacherJoints.actionEvents[InputAction.ATTACH]
                if actionEventAttach ~= nil then
                    g_inputBinding:setActionEventActive(actionEventAttach.actionEventId, true)
                end
            else
                local actionEventAttach = self.spec_attacherJoints.actionEvents[InputAction.ATTACH]
                if actionEventAttach ~= nil then
                    g_inputBinding:setActionEventActive(actionEventAttach.actionEventId, false)
                end
            end
        end
    end
end

function AttachImplementsManually.updateVehiclesInAttachRange(vehicle, maxDistanceSq, maxAngle, fullUpdate)
    local spec = vehicle.spec_attacherJoints

    if maxDistanceSq == nil then
        maxDistanceSq = AttachImplementsManually.MAX_ATTACH_DISTANCE_SQ
    end

    if vehicle.spec_attachImplementsManually ~= nil and vehicle.spec_attachImplementsManually.maxDistanceSq ~= nil then
        maxDistanceSq = vehicle.spec_attachImplementsManually.maxDistanceSq
    end

    if spec ~= nil then
        local attachableInfo = spec.attachableInfo
        local pendingInfo = spec.pendingAttachableInfo

        -- first, check if attached implements can attach something
        if vehicle.getAttachedImplements ~= nil then
            local implements = vehicle:getAttachedImplements()
            for _,implement in pairs(implements) do
                if implement.object ~= nil then
                    local attacherVehicle, attacherVehicleJointDescIndex, attachable, attachableJointDescIndex, warning = AttachImplementsManually.updateVehiclesInAttachRange(implement.object, maxDistanceSq, maxAngle, fullUpdate)
                    if attacherVehicle ~= nil then
                        attachableInfo.attacherVehicle, attachableInfo.attacherVehicleJointDescIndex, attachableInfo.attachable, attachableInfo.attachableJointDescIndex, attachableInfo.warning = attacherVehicle, attacherVehicleJointDescIndex, attachable, attachableJointDescIndex, warning
                        return attacherVehicle, attacherVehicleJointDescIndex, attachable, attachableJointDescIndex, warning
                    end
                end
            end
        end

        local numJoints = #g_currentMission.vehicleSystem.inputAttacherJoints
        local minUpdateJoints = math.max(math.floor(numJoints / 5), 1) -- update each joint at least every 5 frames
        local firstJoint = spec.lastInputAttacherCheckIndex % numJoints + 1
        local lastJoint = math.min(firstJoint + minUpdateJoints, numJoints)

        if fullUpdate then
            firstJoint = 1
            lastJoint = numJoints
        end

        spec.lastInputAttacherCheckIndex = lastJoint % numJoints

        for attacherJointIndex=1, #spec.attacherJoints do
            local attacherJoint = spec.attacherJoints[attacherJointIndex]

            if attacherJoint.jointIndex == 0 then
                if vehicle:getIsAttachingAllowed(attacherJoint) then
                    local x, y, z = getWorldTranslation(attacherJoint.jointTransform)

                    for i=firstJoint, lastJoint do
                        local jointInfo = g_currentMission.vehicleSystem.inputAttacherJoints[i]

                        if jointInfo.jointType == attacherJoint.jointType then
                            if jointInfo.vehicle:getIsInputAttacherActive(jointInfo.inputAttacherJoint) then
                                local distSq = MathUtil.vector2LengthSq(x-jointInfo.translation[1], z-jointInfo.translation[3])

                                if distSq < maxDistanceSq and distSq < pendingInfo.minDistance then

                                    if g_localPlayer ~= nil and g_localPlayer.rootNode ~= nil then
                                        local playerPosX, _, playerPosZ = getWorldTranslation(g_localPlayer.rootNode)
                                        local playerDistSq = MathUtil.vector2LengthSq(playerPosX - x, playerPosZ - z)

                                        if playerDistSq <= maxDistanceSq then
                                            local distY = y-jointInfo.translation[2]
                                            local distSqY = distY*distY

                                            if distSqY < maxDistanceSq*4 and distSqY < pendingInfo.minDistanceY then
                                                if jointInfo.vehicle:getActiveInputAttacherJointDescIndex() == nil or jointInfo.vehicle:getAllowMultipleAttachments() then
                                                    local compatibility, notAllowedWarning = AttacherJoints.getAttacherJointCompatibility(vehicle, attacherJoint, jointInfo.vehicle, jointInfo.inputAttacherJoint)
                                                    if compatibility then
                                                        local angleInRange
                                                        local attachAngleLimitAxis = jointInfo.inputAttacherJoint.attachAngleLimitAxis
                                                        if attachAngleLimitAxis == 1 then
                                                            local dx, _, _ = localDirectionToLocal(jointInfo.node, attacherJoint.jointTransform, 1, 0, 0)
                                                            angleInRange = dx > maxAngle
                                                        elseif attachAngleLimitAxis == 2 then
                                                            local _, dy, _ = localDirectionToLocal(jointInfo.node, attacherJoint.jointTransform, 0, 1, 0)
                                                            angleInRange = dy > maxAngle
                                                        else
                                                            local _, _, dz = localDirectionToLocal(jointInfo.node, attacherJoint.jointTransform, 0, 0, 1)
                                                            angleInRange = dz > maxAngle
                                                        end

                                                        if angleInRange then
                                                            pendingInfo.minDistance = distSq
                                                            pendingInfo.minDistanceY = distSqY
                                                            pendingInfo.attacherVehicle = vehicle
                                                            pendingInfo.attacherVehicleJointDescIndex = attacherJointIndex
                                                            pendingInfo.attachable = jointInfo.vehicle
                                                            pendingInfo.attachableJointDescIndex = jointInfo.jointIndex
                                                        end
                                                    else
                                                        pendingInfo.warning = pendingInfo.warning or notAllowedWarning
                                                    end
                                                end
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end

        if spec.lastInputAttacherCheckIndex == 0 or numJoints == 0 then
            attachableInfo.attacherVehicle = pendingInfo.attacherVehicle
            attachableInfo.attacherVehicleJointDescIndex = pendingInfo.attacherVehicleJointDescIndex
            attachableInfo.attachable = pendingInfo.attachable
            attachableInfo.attachableJointDescIndex = pendingInfo.attachableJointDescIndex
            attachableInfo.warning = pendingInfo.warning

            pendingInfo.minDistance = math.huge
            pendingInfo.minDistanceY = math.huge
            pendingInfo.attacherVehicle = nil
            pendingInfo.attacherVehicleJointDescIndex = nil
            pendingInfo.attachable = nil
            pendingInfo.attachableJointDescIndex = nil
            pendingInfo.warning = nil
        end

        return attachableInfo.attacherVehicle, attachableInfo.attacherVehicleJointDescIndex, attachableInfo.attachable, attachableInfo.attachableJointDescIndex, attachableInfo.warning
    end

    return nil, nil, nil, nil
end

function AttachImplementsManually:updateIsPlayerInRange()
    local spec = self.spec_attachImplementsManually
    local spec_attacherJoints = self.spec_attacherJoints

    if spec.doUpdateIsPlayerInRange and g_localPlayer ~= nil and g_localPlayer.rootNode ~= nil and spec.externalControlTrigger ~= nil and spec.externalControlTrigger.node ~= nil then
        log:printDevInfo("Updating isPlayerInRange for vehicle: " .. tostring(self:getName()), Logger.DEBUG_LEVELS.LOW)
        local triggerNodeParent = getParent(spec.externalControlTrigger.node)
        local playerPosX, playerPosY, playerPosZ = getWorldTranslation(g_localPlayer.rootNode)
        local localX, localY, localZ = worldToLocal(triggerNodeParent, playerPosX, playerPosY, playerPosZ)

        local halfWidth = spec.externalControlTrigger.size.width * 0.5
        local halfHeight = spec.externalControlTrigger.size.height * 0.5
        local halfLength = spec.externalControlTrigger.size.length * 0.5

        if math.abs(localX) <= halfWidth and math.abs(localY) <= halfHeight and math.abs(localZ) <= halfLength then
            
            local info = spec_attacherJoints.attachableInfo

            if next(info) ~= nil then
                local attachableJointIndex = info.attachableJointDescIndex
                if info.attachable ~= nil and info.attachable.getInputAttacherJointByJointDescIndex ~= nil and info.attachable.getIsBeingDeleted ~= nil and not info.attachable:getIsBeingDeleted() then
                    local attachableJoint = info.attachable:getInputAttacherJointByJointDescIndex(attachableJointIndex)

                    if attachableJoint ~= nil then
                        local jointNode = attachableJoint.node
                        if jointNode ~= nil then
                            local jointX, jointY, jointZ = getWorldTranslation(jointNode)
                            local distanceSq = MathUtil.vector2LengthSq(playerPosX - jointX, playerPosZ - jointZ)
                            if distanceSq > spec.maxDistanceSq then
                                spec.externalControlTrigger.isPlayerInRange = false
                                return
                            end
                        end
                    end
                end
            end

            if self.getAttacherVehicle ~= nil and self:getAttacherVehicle() ~= nil then
                local attacherVehicle = self:getAttacherVehicle()
                if attacherVehicle.spec_attachImplementsManually ~= nil then
                    attacherVehicle:updateIsPlayerInRange()
                    spec.externalControlTrigger.isPlayerInRange = not attacherVehicle.spec_attachImplementsManually.externalControlTrigger.isPlayerInRange
                    return
                end
            end

            for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
                if vehicle ~= self and vehicle.spec_attacherJoints ~= nil and vehicle.spec_attacherJoints.attachableInfo ~= nil and next(vehicle.spec_attacherJoints.attachableInfo) ~= nil then
                    local attachableInfo = vehicle.spec_attacherJoints.attachableInfo
                    if attachableInfo.attachable == self and attachableInfo.attacherVehicle ~= nil and attachableInfo.attacherVehicle.spec_attachImplementsManually ~= nil then
                        attachableInfo.attacherVehicle:updateIsPlayerInRange()
                        spec.externalControlTrigger.isPlayerInRange = not attachableInfo.attacherVehicle.spec_attachImplementsManually.externalControlTrigger.isPlayerInRange
                        return
                    end
                end
            end

            spec.externalControlTrigger.isPlayerInRange = true
            return
        end
    end

    if spec.externalControlTrigger ~= nil then
        spec.externalControlTrigger.isPlayerInRange = false
    end
end

function AttachImplementsManually:updateClosestVehicleInRange()
    local spec = self.spec_attachImplementsManually
    local spec_attacherJoints = self.spec_attacherJoints
    local closestVehicle = nil

    if spec.externalControlTrigger ~= nil and spec.externalControlTrigger.isPlayerInRange and g_localPlayer ~= nil then
        local playerPosX, _, playerPosZ = getWorldTranslation(g_localPlayer.rootNode)
        local closestDistanceSq = spec.maxDistanceSq

        local attachedImplements = self:getAttachedImplements()

        for _, implement in pairs(attachedImplements) do
            if implement.object ~= nil and implement.object.rootNode ~= nil then
                local attachable = implement.object
                local vehiclePosX, _, vehiclePosZ = getWorldTranslation(attachable.rootNode)
                local inputAttacherJoints = attachable.spec_attachable ~= nil and attachable.spec_attachable.inputAttacherJoints or nil

                if inputAttacherJoints == nil or #inputAttacherJoints == 0 then
                    local distanceSq = MathUtil.vector2LengthSq(playerPosX - vehiclePosX, playerPosZ - vehiclePosZ)

                    if distanceSq < closestDistanceSq then
                        closestDistanceSq = distanceSq
                        closestVehicle = attachable
                    end
                else
                    for _, inputAttacherJoint in pairs(inputAttacherJoints) do
                        if inputAttacherJoint.node ~= nil then
                            vehiclePosX, _, vehiclePosZ = getWorldTranslation(inputAttacherJoint.node)
                        end

                        local distanceSq = MathUtil.vector2LengthSq(playerPosX - vehiclePosX, playerPosZ - vehiclePosZ)

                        if distanceSq < closestDistanceSq then
                            closestDistanceSq = distanceSq
                            closestVehicle = attachable
                        end
                    end
                end
            end
        end
    end

    spec.closestVehicleInPlayerRange = closestVehicle
end

function AttachImplementsManually.getIsElectricConnected(attacherVehicle, attachedVehicle)
    local isConnected = true
    local hasConnectionHoses = false
    if SpecializationUtil.hasSpecialization(ConnectionHoses, attacherVehicle.specializations) then
        local spec_connectionHoses = attacherVehicle.spec_connectionHoses
        local inputAttacherJointIndex = attachedVehicle:getActiveInputAttacherJointDescIndex()

        --hasConnectionHoses = #spec_connectionHoses.targetNodes > 0 or #spec_connectionHoses.toolConnectorHoses > 0 or #spec_connectionHoses.hoseNodes > 0 or #spec_connectionHoses.customHoseTargets > 0 or #spec_connectionHoses.customHoses > 0 or #spec_connectionHoses.hoseSkipNodes > 0
        hasConnectionHoses = ConnectionHoseUtil.getHasElecticInputConnectionHoses(attachedVehicle, inputAttacherJointIndex) and ConnectionHoseUtil.getHasAttacherVehicleElecticOutputHoses(attachedVehicle)
    end

    if hasConnectionHoses then
        if attacherVehicle.getAttacherVehicle ~= nil then
            local attacherVehicleParent = attacherVehicle:getAttacherVehicle()

            if attacherVehicleParent ~= nil then
                if attacherVehicleParent.getIsElectricConnected ~= nil then
                    if not attacherVehicleParent:getIsElectricConnected(attacherVehicle) then
                        return false
                    end
                else
                    return true
                end
            else
                return false
            end
        end

        local implementIndex = attacherVehicle:getImplementIndexByObject(attachedVehicle)
        local jointDescIndex = attacherVehicle:getAttacherJointIndexFromImplementIndex(implementIndex)

        if jointDescIndex ~= nil and attacherVehicle.spec_attachConnectionHosesManually ~= nil and attacherVehicle.spec_attachConnectionHosesManually.attachedHoses ~= nil then
            if attacherVehicle.spec_attachConnectionHosesManually.attachedHoses[jointDescIndex] == nil then
                return false
            end
        end
    end

    return true
end

function AttachImplementsManually:getIsManualAttachForNexatDisabled()
    local spec = self.spec_attachImplementsManually

    if spec ~= nil and spec.modSettingsManager ~= nil then
        return spec.isNexatWidespanVehicle
    end

    return false
end