--[[
    AutoAttachTrailers.lua

    Changes the attacher joints behavior to automatically attach attachables with jointType "trailer" when the vehicle is close enough.

	@author: 		BayernGamers
	@date: 			08.06.2025
	@version:		1.0

	History:		v1.0 @08.06.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/AttachImplementsManuallySettingsManager.lua", g_currentModDirectory))
source(Utils.getFilename("scripts/events/AttachImplementEvent.lua", g_currentModDirectory))

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

AutoAttachTrailers = {}
AutoAttachTrailers.MOD_DIRECTORY = g_currentModDirectory
AutoAttachTrailers.MOD_NAME = g_currentModName
AutoAttachTrailers.AUTO_ATTACH_DISTANCE = 0.1
AutoAttachTrailers.AUTO_ATTACH_RESET_DISTANCE = 0.75

AutoAttachTrailers.AUTO_ATTACH_DISTANCE_SEMITRAILER = 0.5
AutoAttachTrailers.AUTO_ATTACH_RESET_DISTANCE_SEMITRAILER = 1.5

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

function AutoAttachTrailers.initSpecialization()
    local schemaSavegame = Vehicle.xmlSchemaSavegame

    schemaSavegame:register(XMLValueType.STRING, "vehicles.vehicle(?).FS25_attachImplementsManually.autoAttachTrailers.attachable(?)#uniqueId", "Unique ID of the vehicle to auto attach trailers to", nil)
    schemaSavegame:register(XMLValueType.INT, "vehicles.vehicle(?).FS25_attachImplementsManually.autoAttachTrailers.attachable(?)#attacherJointIndex", "Attacher joint index to auto attach trailers to", nil)
    schemaSavegame:register(XMLValueType.INT, "vehicles.vehicle(?).FS25_attachImplementsManually.autoAttachTrailers.attachable(?)#inputJointDescIndex", "Input attacher joint index of the trailer to auto attach", nil)
end

function AutoAttachTrailers.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onPreLoad", AutoAttachTrailers)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", AutoAttachTrailers)
    SpecializationUtil.registerEventListener(vehicleType, "onPostLoad", AutoAttachTrailers)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", AutoAttachTrailers)
    SpecializationUtil.registerEventListener(vehicleType, "onReadStream", AutoAttachTrailers)
    SpecializationUtil.registerEventListener(vehicleType, "onWriteStream", AutoAttachTrailers)

    SpecializationUtil.registerEventListener(vehicleType, "onPreDetachImplement", AutoAttachTrailers)
end

function AutoAttachTrailers.registerFunctions(vehicleType)
    SpecializationUtil.registerFunction(vehicleType, "getIsTrailerAttacher", AutoAttachTrailers.getIsTrailerAttacher)
    SpecializationUtil.registerFunction(vehicleType, "getIsHookliftAttacher", AutoAttachTrailers.getIsHookliftAttacher)
    SpecializationUtil.registerFunction(vehicleType, "getIsSemitrailerAttacher", AutoAttachTrailers.getIsSemitrailerAttacher)
    SpecializationUtil.registerFunction(vehicleType, "getIsFrontloaderAttacher", AutoAttachTrailers.getIsFrontloaderAttacher)
    SpecializationUtil.registerFunction(vehicleType, "getIsAttachableFrontloaderAttacher", AutoAttachTrailers.getIsAttachableFrontloaderAttacher)

    SpecializationUtil.registerFunction(vehicleType, "getHasFrontloaderAutoLockAttacher", AutoAttachTrailers.getHasFrontloaderAutoLockAttacher)

    SpecializationUtil.registerFunction(vehicleType, "loadDisAllowAutoAttachFromXMLFile", AutoAttachTrailers.loadDisAllowAutoAttachFromXMLFile)

    SpecializationUtil.registerFunction(vehicleType, "getHasAutoAttachVehicleNearby", AutoAttachTrailers.getHasAutoAttachVehicleNearby)
end

function AutoAttachTrailers:onPreLoad(savegame)
    self.spec_autoAttachTrailers = {}
end

function AutoAttachTrailers:onLoad(savegame)
    local spec = self.spec_autoAttachTrailers
    
    spec.modSettingsManager = AttachImplementsManuallySettingsManager.getInstance()
    spec.disallowAutoAttachUntilReset = {}
    spec.allowsAutoAttach = false
    self:loadDisAllowAutoAttachFromXMLFile(savegame)
end

function AutoAttachTrailers:onPostLoad(savegame)
    local spec = self.spec_autoAttachTrailers

    for _, attacherJoint in pairs(self.spec_attacherJoints.attacherJoints) do
        local isTrailerAttacher = self:getIsTrailerAttacher(attacherJoint, nil, true)
        local isHookliftAttacher = self:getIsHookliftAttacher(attacherJoint, nil, true)
        local isSemitrailerAttacher = self:getIsSemitrailerAttacher(attacherJoint, nil, true)
        local isFrontloaderAttacher = self:getIsFrontloaderAttacher(attacherJoint, nil, true)
        local isAttachableFrontloaderAttacher = self:getIsAttachableFrontloaderAttacher(attacherJoint, nil, true)

        if isTrailerAttacher or isHookliftAttacher or isSemitrailerAttacher or isFrontloaderAttacher or isAttachableFrontloaderAttacher then
            spec.allowsAutoAttach = true
        end
    end
end

function AutoAttachTrailers:saveToXMLFile(xmlFile, key, usedModNames)
    local spec = self.spec_autoAttachTrailers

    local index = 0
    for uniqueId, attacherJointIndices in pairs(spec.disallowAutoAttachUntilReset) do
        local subKey = string.format("%s.attachable(%d)", key, index)

        xmlFile:setValue(subKey .. "#uniqueId", uniqueId)
        xmlFile:setValue(subKey .. "#attacherJointIndex", attacherJointIndices.jointDescIndex)
        xmlFile:setValue(subKey .. "#inputJointDescIndex", attacherJointIndices.inputJointDescIndex)

        index = index + 1
    end
end

function AutoAttachTrailers:loadDisAllowAutoAttachFromXMLFile(savegame)
    local spec = self.spec_autoAttachTrailers
    local disallowAutoAttachUntilReset = {}

    if savegame ~= nil and savegame.xmlFile ~= nil then
        local xmlFile = savegame.xmlFile
        local key = savegame.key .. ".FS25_attachImplementsManually.autoAttachTrailers.attachable"

        xmlFile:iterate(key, function(index, subKey)
            local uniqueId = xmlFile:getValue(subKey .. "#uniqueId")
            local attacherJointIndex = xmlFile:getValue(subKey .. "#attacherJointIndex")
            local inputJointDescIndex = xmlFile:getValue(subKey .. "#inputJointDescIndex")

            if uniqueId ~= nil and attacherJointIndex ~= nil and inputJointDescIndex ~= nil then
                disallowAutoAttachUntilReset[uniqueId] = {
                    jointDescIndex = attacherJointIndex,
                    inputJointDescIndex = inputJointDescIndex
                }
            end
        end)
    end

    spec.disallowAutoAttachUntilReset = disallowAutoAttachUntilReset
end

function AutoAttachTrailers:onUpdateTick(dt)
    local spec = self.spec_autoAttachTrailers
    local hasAutoAttachVehicleNearby = spec.allowsAutoAttach
    local isActive = self.getIsActive ~= nil and self:getIsActive()

    if not isActive then
        return
    end

    if not hasAutoAttachVehicleNearby then
        hasAutoAttachVehicleNearby = self:getHasAutoAttachVehicleNearby() == true
    end

    if hasAutoAttachVehicleNearby then
        if self:getCanToggleAttach() then
            local info = self.spec_attacherJoints.attachableInfo

            if info ~= nil and info.attachable ~= nil then
                local uniqueId = info.attachable:getUniqueId()
                local attachable = info.attachable
                local attacherVehicle = info.attacherVehicle
                local attachableJointDescIndex = info.attachableJointDescIndex
                local attacherVehicleJointDescIndex = info.attacherVehicleJointDescIndex

                if attacherVehicle == self then
                    local attacherJoint = attacherVehicle:getAttacherJointByJointDescIndex(attacherVehicleJointDescIndex)
                    local attachableJoint = attachable:getInputAttacherJointByJointDescIndex(attachableJointDescIndex)

                    local isTrailerAttacher = self:getIsTrailerAttacher(attacherJoint, attachableJoint)
                    local isHookliftAttacher = self:getIsHookliftAttacher(attacherJoint, attachableJoint)
                    local isSemitrailerAttacher = self:getIsSemitrailerAttacher(attacherJoint, attachableJoint)
                    local isFrontloaderAttacher = self:getIsFrontloaderAttacher(attacherJoint, attachableJoint) and self:getHasFrontloaderAutoLockAttacher()
                    local isAttachableFrontloaderAttacher = self:getIsAttachableFrontloaderAttacher(attacherJoint, attachableJoint)
                    local isAutoAttachEnabledImplement = attachableJoint.autoAttach

                    if isTrailerAttacher or isHookliftAttacher or isSemitrailerAttacher or isFrontloaderAttacher or isAttachableFrontloaderAttacher or isAutoAttachEnabledImplement then
                        local jointNode = attacherJoint.jointTransform
                        local attachableJointNode = attachableJoint.node

                        if (attacherVehicle.getIsBeingDeleted ~= nil and attacherVehicle:getIsBeingDeleted()) or (attachable.getIsBeingDeleted ~= nil and attachable:getIsBeingDeleted()) then
                            return
                        end

                        local jointPosX, _, jointPosZ = getWorldTranslation(jointNode)
                        local attachableJointPosX, _, attachableJointPosZ = getWorldTranslation(attachableJointNode)

                        if jointPosX == nil or jointPosZ == nil or attachableJointPosX == nil or attachableJointPosZ == nil then
                            return
                        end
                        
                        if isFrontloaderAttacher and not spec.modSettingsManager:getEnableAutoAttachFrontloaderTools() then 
                            return
                        end

                        if isAttachableFrontloaderAttacher and not spec.modSettingsManager:getEnableAutoAttachFrontloader() then
                            return
                        end

                        local distance = MathUtil.vector2Length(jointPosX - attachableJointPosX, jointPosZ - attachableJointPosZ)
                        local minDistance = AutoAttachTrailers.AUTO_ATTACH_DISTANCE

                        if isSemitrailerAttacher then
                            minDistance = AutoAttachTrailers.AUTO_ATTACH_DISTANCE_SEMITRAILER
                        end

                        if isAutoAttachEnabledImplement then
                            minDistance = attachableJoint.autoAttachDistance
                        end

                        if not spec.disallowAutoAttachUntilReset[uniqueId] then
                            if distance <= minDistance 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
                                        AttachImplementEvent.sendEvent(self, attachable, attacherVehicleJointDescIndex, attachableJointDescIndex, true, false)
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end

        if next(spec.disallowAutoAttachUntilReset) ~= nil then
            for uniqueId, indices in pairs(spec.disallowAutoAttachUntilReset) do
                local vehicle = g_currentMission.vehicleSystem:getVehicleByUniqueId(uniqueId)

                if vehicle == nil then
                    spec.disallowAutoAttachUntilReset[uniqueId] = nil
                else
                    local jointDescIndex = indices.jointDescIndex
                    local inputJointDescIndex = indices.inputJointDescIndex

                    local attacherJoint = self:getAttacherJointByJointDescIndex(jointDescIndex)
                    local attachableJoint = vehicle:getInputAttacherJointByJointDescIndex(inputJointDescIndex)

                    local isSemitrailerAttacher = self:getIsSemitrailerAttacher(attacherJoint, attachableJoint)

                    local minResetDistance = AutoAttachTrailers.AUTO_ATTACH_RESET_DISTANCE

                    if isSemitrailerAttacher then
                        minResetDistance = AutoAttachTrailers.AUTO_ATTACH_RESET_DISTANCE_SEMITRAILER
                    end

                    if attacherJoint ~= nil and attachableJoint ~= nil then
                        local jointNode = attacherJoint.jointTransform
                        local attachableJointNode = attachableJoint.node

                        local jointPosX, _, jointPosZ = getWorldTranslation(jointNode)
                        local attachableJointPosX, _, attachableJointPosZ = getWorldTranslation(attachableJointNode)
                        local distance = MathUtil.vector2Length(jointPosX - attachableJointPosX, jointPosZ - attachableJointPosZ)

                        if distance > minResetDistance then
                            spec.disallowAutoAttachUntilReset[uniqueId] = nil
                        end
                    else
                        spec.disallowAutoAttachUntilReset[uniqueId] = nil
                    end
                end
            end
        end
    end
end

function AutoAttachTrailers:onWriteStream(streamId, connection)
    local spec = self.spec_autoAttachTrailers

    local count = 0
    for _, _ in pairs(spec.disallowAutoAttachUntilReset) do
        count = count + 1
    end
    streamWriteInt32(streamId, count)

    for uniqueId, indices in pairs(spec.disallowAutoAttachUntilReset) do
        streamWriteString(streamId, uniqueId)
        streamWriteInt8(streamId, indices.jointDescIndex)
        streamWriteInt8(streamId, indices.inputJointDescIndex)
    end
end

function AutoAttachTrailers:onReadStream(streamId, connection)
    local spec = self.spec_autoAttachTrailers

    local count = streamReadInt32(streamId)

    if count > 0 then
        for i = 1, count do
            local uniqueId = streamReadString(streamId)
            local jointDescIndex = streamReadInt8(streamId)
            local inputJointDescIndex = streamReadInt8(streamId)

            spec.disallowAutoAttachUntilReset[uniqueId] = {
                jointDescIndex = jointDescIndex,
                inputJointDescIndex = inputJointDescIndex
            }
        end
    end
end

function AutoAttachTrailers:onPreDetachImplement(implement)
    local spec = self.spec_autoAttachTrailers

    if implement ~= nil then
        local detachedVehicle = implement.object

        if detachedVehicle ~= nil then
            local uniqueId = detachedVehicle:getUniqueId()
            if spec.disallowAutoAttachUntilReset[uniqueId] == nil then
                local implementIndex = self:getImplementIndexByObject(detachedVehicle)
                local jointDescIndex = self:getAttacherJointIndexFromImplementIndex(implementIndex)
                local inputJointDescIndex = detachedVehicle.spec_attachable.inputAttacherJointDescIndex

                spec.disallowAutoAttachUntilReset[uniqueId] = {
                    jointDescIndex = jointDescIndex,
                    inputJointDescIndex = inputJointDescIndex
                }
            end
        end
    end
end

function AutoAttachTrailers:getIsTrailerAttacher(attacherJoint, attachableJoint, ignoreAttachableJointType)
    if ignoreAttachableJointType == true then
        if attacherJoint == nil then
            return false
        end

        return attacherJoint.jointType == AttacherJoints.JOINTTYPE_TRAILER
    end

    if attacherJoint == nil or attachableJoint == nil then
        return false
    end

    return attacherJoint.jointType == AttacherJoints.JOINTTYPE_TRAILER and attachableJoint.jointType == AttacherJoints.JOINTTYPE_TRAILER
end

function AutoAttachTrailers:getIsHookliftAttacher(attacherJoint, attachableJoint, ignoreAttachableJointType)
    if ignoreAttachableJointType == true then
        if attacherJoint == nil then
            return false
        end

        return attacherJoint.jointType == AttacherJoints.JOINTTYPE_HOOKLIFT
    end

    if attacherJoint == nil or attachableJoint == nil then
        return false
    end

    return attacherJoint.jointType == AttacherJoints.JOINTTYPE_HOOKLIFT and attachableJoint.jointType == AttacherJoints.JOINTTYPE_HOOKLIFT
end

function AutoAttachTrailers:getIsSemitrailerAttacher(attacherJoint, attachableJoint, ignoreAttachableJointType)
    if ignoreAttachableJointType == true then
        if attacherJoint == nil then
            return false
        end

        return attacherJoint.jointType == AttacherJoints.JOINTTYPE_SEMITRAILER or attacherJoint.jointType == AttacherJoints.JOINTTYPE_SEMITRAILERCAR
    end

    if attacherJoint == nil or attachableJoint == nil then
        return false
    end

    return (attacherJoint.jointType == AttacherJoints.JOINTTYPE_SEMITRAILER and attachableJoint.jointType == AttacherJoints.JOINTTYPE_SEMITRAILER) or
           (attacherJoint.jointType == AttacherJoints.JOINTTYPE_SEMITRAILERCAR and attachableJoint.jointType == AttacherJoints.JOINTTYPE_SEMITRAILERCAR)
end

function AutoAttachTrailers:getIsFrontloaderAttacher(attacherJoint, attachableJoint, ignoreAttachableJointType)
    if ignoreAttachableJointType == true then
        if attacherJoint == nil then
            return false
        end

        return (attacherJoint.jointType == AttacherJoints.JOINTTYPE_FRONTLOADER) or
               (attacherJoint.jointType == AttacherJoints.JOINTTYPE_TELEHANDLER) or
               (attacherJoint.jointType == AttacherJoints.JOINTTYPE_WHEELLOADER) or
               (attacherJoint.jointType == AttacherJoints.JOINTTYPE_SKIDSTEER) or
               (g_attachImplementsManuallyManager.customFrontloaderJointTypeIndices[attacherJoint.jointType] ~= nil)
    end

    if attacherJoint == nil or attachableJoint == nil then
        return false
    end

    return (attacherJoint.jointType == AttacherJoints.JOINTTYPE_FRONTLOADER and attachableJoint.jointType == AttacherJoints.JOINTTYPE_FRONTLOADER) or
           (attacherJoint.jointType == AttacherJoints.JOINTTYPE_TELEHANDLER and attachableJoint.jointType == AttacherJoints.JOINTTYPE_TELEHANDLER) or
           (attacherJoint.jointType == AttacherJoints.JOINTTYPE_WHEELLOADER and attachableJoint.jointType == AttacherJoints.JOINTTYPE_WHEELLOADER) or
           (attacherJoint.jointType == AttacherJoints.JOINTTYPE_SKIDSTEER and attachableJoint.jointType == AttacherJoints.JOINTTYPE_SKIDSTEER) or
           (g_attachImplementsManuallyManager.customFrontloaderJointTypeIndices[attacherJoint.jointType] ~= nil and g_attachImplementsManuallyManager.customFrontloaderJointTypeIndices[attachableJoint.jointType] ~= nil and attacherJoint.jointType == attachableJoint.jointType)
end

function AutoAttachTrailers:getHasFrontloaderAutoLockAttacher()
    local frontLoaderAttacherTypeConfig = self.configurations["frontloaderAttacherType"]

    if frontLoaderAttacherTypeConfig ~= nil then
        if frontLoaderAttacherTypeConfig >= 2 then
            return true
        end
    end

    return false
end

function AutoAttachTrailers:getHasAutoAttachVehicleNearby()
    local spec = self.spec_autoAttachTrailers
    local spec_attacherJoints = self.spec_attacherJoints

    local info = spec_attacherJoints.attachableInfo
    if info ~= nil and info.attachable ~= nil then
        local attachable = info.attachable
        local attacherVehicle = info.attacherVehicle
        local attachableJointDescIndex = info.attachableJointDescIndex
        local attacherVehicleJointDescIndex = info.attacherVehicleJointDescIndex

        if attacherVehicle == self then
            local attacherJoint = attacherVehicle:getAttacherJointByJointDescIndex(attacherVehicleJointDescIndex)
            local attachableJoint = attachable:getInputAttacherJointByJointDescIndex(attachableJointDescIndex)

            return attachableJoint ~= nil and attachableJoint.autoAttach == true
        end
    end

    return false
end

function AutoAttachTrailers:getIsAttachableFrontloaderAttacher(attacherJoint, attachableJoint, ignoreAttachableJointType)
    if ignoreAttachableJointType == true then
        if attacherJoint == nil then
            return false
        end

        return attacherJoint.jointType == AttacherJoints.JOINTTYPE_ATTACHABLEFRONTLOADER
    end

    if attacherJoint == nil or attachableJoint == nil then
        return false
    end

    return attacherJoint.jointType == AttacherJoints.JOINTTYPE_ATTACHABLEFRONTLOADER and attachableJoint.jointType == AttacherJoints.JOINTTYPE_ATTACHABLEFRONTLOADER
end