--
-- TerraTrac for FS25
-- based on working tracked excavator setup
--

TerraTrac = {}
TerraTrac.modDirectory = g_currentModDirectory

local function rotateOnAxis(node, delta, axis)
    if axis == 2 then
        rotate(node, 0, delta, 0)
    elseif axis == 3 then
        rotate(node, 0, 0, delta)
    else
        rotate(node, delta, 0, 0)
    end
end

local function tryGetAnimTrack(node, clipName)
    if node == nil or clipName == nil then
        return nil, nil
    end

    local charId = getAnimCharacterSet(node)
    if charId ~= nil and charId > 0 then
        local clipIndex = getAnimClipIndex(charId, clipName)
        if clipIndex ~= nil and clipIndex ~= -1 then
            return charId, clipIndex
        end
    end

    return nil, nil
end

local function findAnimNodeWithClip(node, clipName, path)
    if node == nil then
        return nil, nil, nil, nil
    end

    local charId, clipIndex = tryGetAnimTrack(node, clipName)
    if charId ~= nil then
        return node, charId, clipIndex, path
    end

    local numChildren = getNumOfChildren(node)
    for i = 0, numChildren - 1 do
        local childNode = getChildAt(node, i)
        local childPath = string.format("%s|%d", path, i)
        local foundNode, foundCharId, foundClipIndex, foundPath = findAnimNodeWithClip(childNode, clipName, childPath)
        if foundNode ~= nil then
            return foundNode, foundCharId, foundClipIndex, foundPath
        end
    end

    return nil, nil, nil, nil
end

function TerraTrac.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(Motorized, specializations)
end

function TerraTrac.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", TerraTrac)
    SpecializationUtil.registerEventListener(vehicleType, "onPostLoad", TerraTrac)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdate", TerraTrac)
    SpecializationUtil.registerEventListener(vehicleType, "onLeaveVehicle", TerraTrac)
end

function TerraTrac:onLoad(savegame)
    self.spec_terraTrac = {}
    local spec = self.spec_terraTrac

    spec.indexTracRight = I3DUtil.indexToObject(self.components, self.xmlFile:getString("vehicle.TracWheelsRight#index"))
    spec.charIdAnimR = I3DUtil.indexToObject(self.components, self.xmlFile:getString("vehicle.TracWheelsRight#node"))
    spec.clipIndexAnimR = self.xmlFile:getString("vehicle.TracWheelsRight#animationClip")
    spec.spedR = Utils.getNoNil(self.xmlFile:getFloat("vehicle.TracWheelsRight#sped"), 18.5)
    spec.TracWheelsRight = {}

    local i = 0
    while true do
        local baseName = string.format("vehicle.TracWheelsRight.rotatingPart(%d)", i)
        local index = self.xmlFile:getString(baseName .. "#index")
        if index == nil then
            break
        end

        local node = I3DUtil.indexToObject(self.components, index)
        if node ~= nil then
            local entry = {}
            entry.node = node
            entry.speedScale = self.xmlFile:getFloat(baseName .. "#speedScale")
            entry.rotAxis = Utils.getNoNil(self.xmlFile:getInt(baseName .. "#rotAxis"), 1)
            if entry.speedScale == nil then
                entry.speedScale = 1 / Utils.getNoNil(self.xmlFile:getFloat(baseName .. "#radius"), 1)
            end
            table.insert(spec.TracWheelsRight, entry)
        end

        i = i + 1
    end

    spec.indexTracLeft = I3DUtil.indexToObject(self.components, self.xmlFile:getString("vehicle.TracWheelsLeft#index"))
    spec.charIdAnimL = I3DUtil.indexToObject(self.components, self.xmlFile:getString("vehicle.TracWheelsLeft#node"))
    spec.clipIndexAnimL = self.xmlFile:getString("vehicle.TracWheelsLeft#animationClip")
    spec.spedL = Utils.getNoNil(self.xmlFile:getFloat("vehicle.TracWheelsLeft#sped"), 18.5)
    spec.TracWheelsLeft = {}

    i = 0
    while true do
        local baseName = string.format("vehicle.TracWheelsLeft.rotatingPart(%d)", i)
        local index = self.xmlFile:getString(baseName .. "#index")
        if index == nil then
            break
        end

        local node = I3DUtil.indexToObject(self.components, index)
        if node ~= nil then
            local entry = {}
            entry.node = node
            entry.speedScale = self.xmlFile:getFloat(baseName .. "#speedScale")
            entry.rotAxis = Utils.getNoNil(self.xmlFile:getInt(baseName .. "#rotAxis"), 1)
            if entry.speedScale == nil then
                entry.speedScale = 1 / Utils.getNoNil(self.xmlFile:getFloat(baseName .. "#radius"), 1)
            end
            table.insert(spec.TracWheelsLeft, entry)
        end

        i = i + 1
    end

    spec.animCharIdL, spec.animClipIndexL = tryGetAnimTrack(spec.charIdAnimL, spec.clipIndexAnimL)
    spec.animCharIdR, spec.animClipIndexR = tryGetAnimTrack(spec.charIdAnimR, spec.clipIndexAnimR)
    local resolvedPathL = "xml"
    local resolvedPathR = "xml"

    -- Search only under the side node to avoid resolving the right side to the left track clip.
    if spec.animCharIdL == nil and spec.charIdAnimL ~= nil and spec.clipIndexAnimL ~= nil then
        local foundNodeL, foundCharIdL, foundClipIndexL, foundPathL =
            findAnimNodeWithClip(spec.charIdAnimL, spec.clipIndexAnimL, "left")
        if foundNodeL ~= nil then
            spec.charIdAnimL = foundNodeL
            spec.animCharIdL = foundCharIdL
            spec.animClipIndexL = foundClipIndexL
            resolvedPathL = foundPathL
        end
    end

    if spec.animCharIdR == nil and spec.charIdAnimR ~= nil and spec.clipIndexAnimR ~= nil then
        local foundNodeR, foundCharIdR, foundClipIndexR, foundPathR =
            findAnimNodeWithClip(spec.charIdAnimR, spec.clipIndexAnimR, "right")
        if foundNodeR ~= nil then
            spec.charIdAnimR = foundNodeR
            spec.animCharIdR = foundCharIdR
            spec.animClipIndexR = foundClipIndexR
            resolvedPathR = foundPathR
        end
    end

    if spec.animCharIdL == nil then
        spec.fallbackRotateNodeL = spec.charIdAnimL
    end
    if spec.animCharIdR == nil then
        spec.fallbackRotateNodeR = spec.charIdAnimR
    end

    if spec.animCharIdL ~= nil and spec.animClipIndexL ~= nil then
        assignAnimTrackClip(spec.animCharIdL, 0, spec.animClipIndexL)
        setAnimTrackLoopState(spec.animCharIdL, 0, true)
    end

    if spec.animCharIdR ~= nil and spec.animClipIndexR ~= nil then
        assignAnimTrackClip(spec.animCharIdR, 0, spec.animClipIndexR)
        setAnimTrackLoopState(spec.animCharIdR, 0, true)
    end

    spec.isValid = spec.indexTracLeft ~= nil or spec.indexTracRight ~= nil

    Logging.info(
        "[WNET145TerraTrac] left=%s right=%s leftParts=%d rightParts=%d clipL=%s clipR=%s charL=%s clipIdL=%s charR=%s clipIdR=%s pathL=%s pathR=%s fbL=%s fbR=%s",
        tostring(spec.indexTracLeft ~= nil),
        tostring(spec.indexTracRight ~= nil),
        #spec.TracWheelsLeft,
        #spec.TracWheelsRight,
        tostring(spec.clipIndexAnimL),
        tostring(spec.clipIndexAnimR),
        tostring(spec.animCharIdL),
        tostring(spec.animClipIndexL),
        tostring(spec.animCharIdR),
        tostring(spec.animClipIndexR),
        tostring(resolvedPathL),
        tostring(resolvedPathR),
        tostring(spec.fallbackRotateNodeL ~= nil),
        tostring(spec.fallbackRotateNodeR ~= nil)
    )
end

function TerraTrac:onPostLoad(savegame)
end

function TerraTrac:onUpdate(dt, vehicle)
    local spec = self.spec_terraTrac
    if spec == nil or not spec.isValid or self.spec_motorized == nil then
        return
    end

    local motorStarted = true
    if self.getIsMotorStarted ~= nil then
        motorStarted = self:getIsMotorStarted()
    elseif self.spec_motorized.isMotorStarted ~= nil then
        motorStarted = self.spec_motorized.isMotorStarted
    end

    if not motorStarted then
        return
    end

    local fallbackDirection = Utils.getNoNil(spec.lastDirection, 1)
    if self.movingDirection ~= nil and math.abs(self.movingDirection) > 1.0E-4 then
        fallbackDirection = self.movingDirection < 0 and -1 or 1
    end

    if spec.indexTracLeft ~= nil then
        local newX1, newY1, newZ1 = getWorldTranslation(spec.indexTracLeft)
        if spec.lastTranslationL == nil then
            spec.lastTranslationL = {newX1, newY1, newZ1}
        end

        local dx1, dy1, dz1 = worldDirectionToLocal(
            spec.indexTracLeft,
            newX1 - spec.lastTranslationL[1],
            newY1 - spec.lastTranslationL[2],
            newZ1 - spec.lastTranslationL[3]
        )

        local signedDeltaL = math.abs(dz1) >= math.abs(dx1) and dz1 or dx1
        local movingDirectionL = 0
        if signedDeltaL < -1.0E-4 then
            movingDirectionL = -1
        elseif signedDeltaL > 1.0E-4 then
            movingDirectionL = 1
        else
            movingDirectionL = Utils.getNoNil(spec.lastDirectionL, fallbackDirection)
        end

        local lastMovedDistanceL = MathUtil.vector3Length(dx1, dy1, dz1)
        if lastMovedDistanceL <= 1.0E-6 then
            lastMovedDistanceL = 0
        end

        spec.lastTranslationL[1] = newX1
        spec.lastTranslationL[2] = newY1
        spec.lastTranslationL[3] = newZ1
        spec.lastDirectionL = movingDirectionL
        spec.lastDirection = movingDirectionL

        if spec.animCharIdL ~= nil and spec.animClipIndexL ~= nil then
            setAnimTrackSpeedScale(spec.animCharIdL, 0, spec.spedL * lastMovedDistanceL * movingDirectionL)
            enableAnimTrack(spec.animCharIdL, 0)
        elseif spec.fallbackRotateNodeL ~= nil then
            rotateOnAxis(spec.fallbackRotateNodeL, spec.spedL * lastMovedDistanceL * movingDirectionL, 1)
        end

        for _, v in pairs(spec.TracWheelsLeft) do
            rotateOnAxis(v.node, v.speedScale * lastMovedDistanceL * movingDirectionL, v.rotAxis)
        end
    end

    if spec.indexTracRight ~= nil then
        local newX2, newY2, newZ2 = getWorldTranslation(spec.indexTracRight)
        if spec.lastTranslationR == nil then
            spec.lastTranslationR = {newX2, newY2, newZ2}
        end

        local dx2, dy2, dz2 = worldDirectionToLocal(
            spec.indexTracRight,
            newX2 - spec.lastTranslationR[1],
            newY2 - spec.lastTranslationR[2],
            newZ2 - spec.lastTranslationR[3]
        )

        local signedDeltaR = math.abs(dz2) >= math.abs(dx2) and dz2 or dx2
        local movingDirectionR = 0
        if signedDeltaR < -1.0E-4 then
            movingDirectionR = -1
        elseif signedDeltaR > 1.0E-4 then
            movingDirectionR = 1
        else
            movingDirectionR = Utils.getNoNil(spec.lastDirectionR, fallbackDirection)
        end

        local lastMovedDistanceR = MathUtil.vector3Length(dx2, dy2, dz2)
        if lastMovedDistanceR <= 1.0E-6 then
            lastMovedDistanceR = 0
        end

        spec.lastTranslationR[1] = newX2
        spec.lastTranslationR[2] = newY2
        spec.lastTranslationR[3] = newZ2
        spec.lastDirectionR = movingDirectionR
        spec.lastDirection = movingDirectionR

        if spec.animCharIdR ~= nil and spec.animClipIndexR ~= nil then
            setAnimTrackSpeedScale(spec.animCharIdR, 0, spec.spedR * lastMovedDistanceR * movingDirectionR)
            enableAnimTrack(spec.animCharIdR, 0)
        elseif spec.fallbackRotateNodeR ~= nil then
            rotateOnAxis(spec.fallbackRotateNodeR, spec.spedR * lastMovedDistanceR * movingDirectionR, 1)
        end

        for _, v in pairs(spec.TracWheelsRight) do
            rotateOnAxis(v.node, v.speedScale * lastMovedDistanceR * movingDirectionR, v.rotAxis)
        end
    end
end

function TerraTrac:onLeaveVehicle()
    local spec = self.spec_terraTrac
    if spec ~= nil then
        spec.lastTranslationL = nil
        spec.lastTranslationR = nil
        spec.lastDirectionL = nil
        spec.lastDirectionR = nil
        spec.lastDirection = nil
    end
end
