TractorTerrainDynamicsCompatibility = {}

TractorTerrainDynamicsCompatibility.NOTICE_VERSION = 2
TractorTerrainDynamicsCompatibility.NOTICE_FILENAME =
    "mudSystemPhysicsNotices.xml"
TractorTerrainDynamicsCompatibility.MOD_NAMES = {
    "FS25_TractorTerrainDynamics",
    "TractorTerrainDynamics"
}
TractorTerrainDynamicsCompatibility.scanIntervalMs = 1000
TractorTerrainDynamicsCompatibility.active = false
TractorTerrainDynamicsCompatibility.wrappedVehicles = setmetatable({}, {__mode="k"})

local function ttdcGetText(key, fallback)
    if g_i18n ~= nil and g_i18n.hasText ~= nil and g_i18n:hasText(key) then
        return g_i18n:getText(key)
    end

    if g_i18n ~= nil and g_i18n.getText ~= nil then
        local value = g_i18n:getText(key)
        if value ~= nil and value ~= key then return value end
    end

    return fallback or key
end

local function ttdcValidRadius(value)
    return type(value) == "number" and value == value and value > 0.05
end

local function ttdcGetOriginalRadius(wheel, physics)
    local result = nil
    local function consider(value)
        if ttdcValidRadius(value) then
            result = result == nil and value or math.max(result, value)
        end
    end

    consider(physics.__mpOrigRadius)
    consider(physics.__fgOrigRadius)
    consider(physics.__tpOrigRadius)
    consider(physics.__punctureOrigRadius)
    consider(physics.__ttdOrigRadius)
    consider(physics.__uytOrigRadius)
    consider(physics.radiusOriginal)
    consider(wheel.ttdOriginalRadius)
    consider(physics.radius)
    return result
end

local function ttdcClampRadius(value, originalRadius)
    if not ttdcValidRadius(value) then return originalRadius end
    if not ttdcValidRadius(originalRadius) then return value end
    return math.max(0.06, math.min(originalRadius, value))
end

local function ttdcClamp01(value)
    value = tonumber(value) or 0
    return math.max(0, math.min(1, value))
end

local function ttdcVehicleDeleting(vehicle)
    return vehicle == nil
        or vehicle.__mudSystemDeleting == true
        or vehicle.isDeleting == true
        or vehicle.isDeleted == true
end

local function ttdcHasPhysicalWheels(vehicle)
    local wheels = vehicle ~= nil and vehicle.spec_wheels ~= nil
        and vehicle.spec_wheels.wheels or nil
    return wheels ~= nil and next(wheels) ~= nil
end

function TractorTerrainDynamicsCompatibility:isTTDVehicleActive(vehicle)
    return self.active == true and not ttdcVehicleDeleting(vehicle)
        and ttdcHasPhysicalWheels(vehicle)
        and vehicle.spec_tractorTerrainDynamics ~= nil
        and vehicle.spec_tractorTerrainDynamics.isActive == true
        and type(vehicle.updateTTD) == "function"
end

function TractorTerrainDynamicsCompatibility:getActiveMudState(vehicle)
    local coordinator = _G.__MudDriveCoordinator
    if coordinator ~= nil and coordinator.getTerrainDynamicsState ~= nil then
        return coordinator:getTerrainDynamicsState(vehicle)
    end
    return nil
end

function TractorTerrainDynamicsCompatibility:isHandlingVehicleDynamics(vehicle)
    if not self:isTTDVehicleActive(vehicle) then return false end
    local spec = vehicle.spec_tractorTerrainDynamics
    if spec.currentTTDState == "field" then return true end
    return self:getActiveMudState(vehicle) ~= nil
end

function TractorTerrainDynamicsCompatibility:getMudSystemWetness(vehicle, dt)
    local wetness = 0
    local environment = g_currentMission ~= nil and g_currentMission.environment or nil
    local weather = environment ~= nil and environment.weather or nil
    if weather ~= nil and weather.getGroundWetness ~= nil then
        local ok, value = pcall(weather.getGroundWetness, weather)
        if ok then wetness = ttdcClamp01(value) end
    end

    if MudPhysics ~= nil and MudPhysics.isHardWinterFrozen ~= nil
        and MudPhysics:isHardWinterFrozen() then
        return 0
    end

    if FieldLocalWetness ~= nil and FieldLocalWetness.enabled == true
        and FieldLocalWetness.getVehicleWetness ~= nil and vehicle ~= nil then
        wetness = FieldLocalWetness:getVehicleWetness(vehicle, wetness, dt)
    end
    return ttdcClamp01(wetness)
end

function TractorTerrainDynamicsCompatibility:getMudSystemWetnessPercent(vehicle, dt)
    local wetness = self:getMudSystemWetness(vehicle, dt)
    return wetness, math.floor(wetness * 100 + 0.5)
end

function TractorTerrainDynamicsCompatibility:injectMudSystemWetness(vehicle, dt)
    if not self:isTTDVehicleActive(vehicle) then return end

    local spec = vehicle.spec_tractorTerrainDynamics
    spec.groundData = spec.groundData or {}
    local wetness = self:getMudSystemWetness(vehicle, dt)
    spec.groundData.moisture = wetness
    spec.groundData.__mudSystemOverride = true

    if self:getActiveMudState(vehicle) ~= nil then
        spec.groundData.onField = true
    end

    if vehicle.spec_tirePressure ~= nil
        and vehicle.spec_tirePressure.ttdGroundData ~= nil then
        vehicle.spec_tirePressure.ttdGroundData.moisture = wetness
        vehicle.spec_tirePressure.ttdGroundData.onField = spec.groundData.onField
    end
end

function TractorTerrainDynamicsCompatibility:isTractorTerrainDynamicsActive()
    for _, modName in ipairs(self.MOD_NAMES) do
        if g_modIsLoaded ~= nil and g_modIsLoaded[modName] then
            return true, modName
        end
    end

    local mission = g_currentMission
    local dynamicInfo = (mission ~= nil and mission.missionDynamicInfo)
        or (g_mpLoadingScreen ~= nil and g_mpLoadingScreen.missionDynamicInfo)
    local mods = dynamicInfo ~= nil and dynamicInfo.mods or nil
    if mods ~= nil then
        for _, modInfo in pairs(mods) do
            for _, modName in ipairs(self.MOD_NAMES) do
                if modInfo ~= nil and modInfo.modName == modName then
                    return true, modName
                end
            end
        end
    end

    if rawget(_G, "TractorTerrainDynamics") ~= nil
        or rawget(_G, "g_TTD_Shared") ~= nil
        or rawget(_G, "TTD_Shared") ~= nil then
        return true, "runtime"
    end

    return false, nil
end

function TractorTerrainDynamicsCompatibility:getNoticePath()
    local missionInfo = g_currentMission ~= nil
        and g_currentMission.missionInfo or nil
    local savegameDirectory = missionInfo ~= nil
        and missionInfo.savegameDirectory or nil
    if savegameDirectory == nil or savegameDirectory == "" then return nil end
    return Utils.getFilename(self.NOTICE_FILENAME, savegameDirectory)
end

function TractorTerrainDynamicsCompatibility:getShownVersion()
    local path = self:getNoticePath()
    if path == nil or not fileExists(path) then return 0 end

    local xmlFile = loadXMLFile("MudSystemPhysicsNotices", path)
    if xmlFile == 0 then return 0 end
    local version = getXMLInt(
        xmlFile,
        "mudSystemPhysicsNotices.tractorTerrainDynamicsCompatibility#version"
    ) or 0
    delete(xmlFile)
    return version
end

function TractorTerrainDynamicsCompatibility:markShown()
    local path = self:getNoticePath()
    if path == nil then
        Logging.warning(
            "[MudSystemPhysics] Could not resolve savegame directory for TTD notice"
        )
        return
    end

    local xmlFile = fileExists(path)
        and loadXMLFile("MudSystemPhysicsNotices", path)
        or createXMLFile(
            "MudSystemPhysicsNotices",
            path,
            "mudSystemPhysicsNotices"
        )
    if xmlFile == 0 then
        Logging.warning(
            "[MudSystemPhysics] Could not save TTD compatibility notice state"
        )
        return
    end

    setXMLInt(
        xmlFile,
        "mudSystemPhysicsNotices.tractorTerrainDynamicsCompatibility#version",
        self.NOTICE_VERSION
    )
    saveXMLFile(xmlFile)
    delete(xmlFile)
end

function TractorTerrainDynamicsCompatibility:isClient()
    local mission = g_currentMission
    if mission ~= nil and mission.getIsClient ~= nil then
        return mission:getIsClient()
    end
    return g_gui ~= nil
end

function TractorTerrainDynamicsCompatibility:showDialog(onComplete)
    local text = ttdcGetText(
        "mudsys_ttd_compat_text",
        "Tractor Terrain Dynamics was detected. Deep compatibility mode is active: Mud System Physics supplies local wetness to TTD, while TTD handles terrain resistance, traction and transmission. Mud System Physics keeps local mud layers, real per-wheel load, tire pressure, punctures and the final speed ceiling. Duplicate resistance is disabled and wheel deformation is merged without applying sink twice. Both mods also work independently."
    )
    local okText = ttdcGetText("mudsys_ttd_compat_ok", "OK")

    local function closeNotice()
        TractorTerrainDynamicsCompatibility:markShown()
        if onComplete ~= nil then onComplete() end
    end

    if InfoDialog ~= nil and InfoDialog.show ~= nil
        and InfoDialog.INSTANCE ~= nil then
        InfoDialog.show(
            text,
            closeNotice,
            nil,
            DialogElement ~= nil and DialogElement.TYPE_INFO or nil,
            okText
        )
        return true
    end

    print("[MudSystemPhysics] " .. text)
    closeNotice()
    return false
end

function TractorTerrainDynamicsCompatibility:wrapVehicle(vehicle)
    if ttdcVehicleDeleting(vehicle) or not ttdcHasPhysicalWheels(vehicle)
        or vehicle.spec_tractorTerrainDynamics == nil
        or type(vehicle.updateTTD) ~= "function" then return false end

    local existing = self.wrappedVehicles[vehicle]
    if existing ~= nil and vehicle.updateTTD == existing.wrapper then
        -- Other specializations may restore individual registered functions
        -- after vehicle creation. Keep every part of the bridge installed,
        -- not only updateTTD.
        if existing.groundDataWrapper ~= nil
            and vehicle.getGroundData ~= existing.groundDataWrapper then
            vehicle.getGroundData = existing.groundDataWrapper
        end
        if existing.hudDataWrapper ~= nil
            and vehicle.getTTDDataForHUD ~= existing.hudDataWrapper then
            vehicle.getTTDDataForHUD = existing.hudDataWrapper
        end
        return true
    end

    local original = vehicle.updateTTD
    local originalGroundData = vehicle.getGroundData
    local originalHudData = vehicle.getTTDDataForHUD
    local compatibility = self
    local groundDataWrapper = nil
    if type(originalGroundData) == "function" then
        groundDataWrapper = function(currentVehicle, ...)
            if ttdcVehicleDeleting(currentVehicle) then return nil end
            compatibility:injectMudSystemWetness(currentVehicle, 0)
            local results = {originalGroundData(currentVehicle, ...)}
            compatibility:injectMudSystemWetness(currentVehicle, 0)
            return unpack(results)
        end
        vehicle.getGroundData = groundDataWrapper
    end
    local hudDataWrapper = nil
    if type(originalHudData) == "function" then
        hudDataWrapper = function(currentVehicle, ...)
            if ttdcVehicleDeleting(currentVehicle) then return nil end
            -- TTD derives other HUD values from moisture too, so inject before
            -- asking TTD to build its HUD data rather than replacing text only.
            compatibility:injectMudSystemWetness(currentVehicle, 0)
            local data = originalHudData(currentVehicle, ...)
            if compatibility.active == true and type(data) == "table" then
                local wetness, wetnessPercent =
                    compatibility:getMudSystemWetnessPercent(currentVehicle, 0)
                data.moisture = wetnessPercent

                local spec = currentVehicle.spec_tractorTerrainDynamics
                if spec ~= nil then
                    spec.groundData = spec.groundData or {}
                    spec.groundData.moisture = wetness
                    spec.groundData.__mudSystemOverride = true
                end
            end
            return data
        end
        vehicle.getTTDDataForHUD = hudDataWrapper
    end
    local wrapper
    wrapper = function(currentVehicle, dt, ...)
        if ttdcVehicleDeleting(currentVehicle) then return nil end
        if compatibility.active ~= true then
            return original(currentVehicle, dt, ...)
        end

        local forcedGroundState = nil
        local forcedAttacherState = nil
        local mudState = compatibility:getActiveMudState(currentVehicle)
        local ttdSpec = currentVehicle.spec_tractorTerrainDynamics
        if mudState ~= nil and (mudState.intensity or 0) > 0.001 and ttdSpec ~= nil
            and ttdSpec.currentTTDState ~= "field" then
            ttdSpec.ttdState = ttdSpec.ttdState or {}
            forcedGroundState = {
                currentTTDState=ttdSpec.currentTTDState,
                stateLastUpdate=ttdSpec.stateLastUpdate,
                current=ttdSpec.ttdState.current,
                lastChangeTime=ttdSpec.ttdState.lastChangeTime
            }
            ttdSpec.currentTTDState = "field"
            ttdSpec.stateLastUpdate = tonumber(g_time) or 0
            ttdSpec.ttdState.current = "field"
            ttdSpec.ttdState.lastChangeTime = tonumber(g_time) or 0

            local attacherSpec = currentVehicle.attacherVehicle ~= nil
                and currentVehicle.attacherVehicle.spec_tractorTerrainDynamics or nil
            if attacherSpec ~= nil then
                attacherSpec.ttdState = attacherSpec.ttdState or {}
                forcedAttacherState = {
                    spec=attacherSpec,
                    currentTTDState=attacherSpec.currentTTDState,
                    current=attacherSpec.ttdState.current,
                    lastChangeTime=attacherSpec.ttdState.lastChangeTime
                }
                attacherSpec.currentTTDState = "field"
                attacherSpec.ttdState.current = "field"
                attacherSpec.ttdState.lastChangeTime = tonumber(g_time) or 0
            end
        end

        local snapshots = {}
        local wheels = currentVehicle.spec_wheels ~= nil
            and currentVehicle.spec_wheels.wheels or nil
        if wheels ~= nil then
            for _, wheel in ipairs(wheels) do
                local physics = wheel ~= nil and wheel.physics or nil
                if physics ~= nil and ttdcValidRadius(physics.radius) then
                    local originalRadius = ttdcGetOriginalRadius(wheel, physics)
                    if ttdcValidRadius(originalRadius) then
                        physics.__ttdOrigRadius = originalRadius
                        wheel.ttdOriginalRadius = originalRadius
                    end

                    snapshots[#snapshots+1] = {
                        wheel=wheel,
                        physics=physics,
                        actualRadius=physics.radius,
                        originalRadius=originalRadius,
                        oldDesiredRadius=physics.__ttdDesiredRadius,
                        oldCurrentRadius=wheel.ttdCurrentRadius
                    }

                    local isolatedRadius = wheel.ttdCurrentRadius
                        or physics.__ttdDesiredRadius
                        or originalRadius
                        or physics.radius
                    physics.radius = ttdcClampRadius(
                        isolatedRadius,
                        originalRadius
                    )
                    -- TTD already checks this optional field when calculating
                    -- average sink. Clear it during its own update so it reads
                    -- the freshly simulated isolated radius instead of the
                    -- value from the previous 500 ms interval.
                    wheel.ttdCurrentRadius = nil
                end
            end
        end

        local results = {pcall(original, currentVehicle, dt, ...)}
        if forcedGroundState ~= nil then
            ttdSpec.currentTTDState = forcedGroundState.currentTTDState
            ttdSpec.stateLastUpdate = forcedGroundState.stateLastUpdate
            ttdSpec.ttdState.current = forcedGroundState.current
            ttdSpec.ttdState.lastChangeTime = forcedGroundState.lastChangeTime
        end
        if forcedAttacherState ~= nil then
            local attacherSpec = forcedAttacherState.spec
            attacherSpec.currentTTDState = forcedAttacherState.currentTTDState
            attacherSpec.ttdState.current = forcedAttacherState.current
            attacherSpec.ttdState.lastChangeTime = forcedAttacherState.lastChangeTime
        end
        local ttdEnabled = currentVehicle.spec_tractorTerrainDynamics ~= nil
            and currentVehicle.spec_tractorTerrainDynamics.isActive == true

        for _, snapshot in ipairs(snapshots) do
            local wheel = snapshot.wheel
            local physics = snapshot.physics
            if results[1] and ttdEnabled then
                local ttdRadius = ttdcClampRadius(
                    physics.radius,
                    snapshot.originalRadius
                )
                wheel.ttdCurrentRadius = ttdRadius
                physics.__ttdDesiredRadius = ttdRadius
            elseif not ttdEnabled then
                physics.__ttdDesiredRadius = nil
                wheel.ttdCurrentRadius = nil
            else
                physics.__ttdDesiredRadius = snapshot.oldDesiredRadius
                wheel.ttdCurrentRadius = snapshot.oldCurrentRadius
            end

            physics.radius = snapshot.actualRadius
            if _G.__MudRadiusCombiner ~= nil
                and _G.__MudRadiusCombiner.apply ~= nil then
                _G.__MudRadiusCombiner.apply(physics, 0.001)
            end
        end

        if not results[1] then error(results[2]) end
        return results[2]
    end

    self.wrappedVehicles[vehicle] = {
        original=original,
        wrapper=wrapper,
        originalGroundData=originalGroundData,
        groundDataWrapper=groundDataWrapper,
        originalHudData=originalHudData,
        hudDataWrapper=hudDataWrapper
    }
    vehicle.updateTTD = wrapper
    if self.vehicleBridgeLogShown ~= true then
        self.vehicleBridgeLogShown = true
        print("[MudSystemPhysics] TTD vehicle moisture bridge active")
    end
    return true
end

function TractorTerrainDynamicsCompatibility:releaseVehicle(vehicle)
    if vehicle == nil then return end

    local data = self.wrappedVehicles ~= nil and self.wrappedVehicles[vehicle] or nil
    if data ~= nil then
        if vehicle.updateTTD == data.wrapper then
            vehicle.updateTTD = data.original
        end
        if data.groundDataWrapper ~= nil
            and vehicle.getGroundData == data.groundDataWrapper then
            vehicle.getGroundData = data.originalGroundData
        end
        if data.hudDataWrapper ~= nil
            and vehicle.getTTDDataForHUD == data.hudDataWrapper then
            vehicle.getTTDDataForHUD = data.originalHudData
        end
        self.wrappedVehicles[vehicle] = nil
    end
    vehicle.__mudTransmissionDynamics = nil
end

function TractorTerrainDynamicsCompatibility:scanVehicles()
    local vehicles = g_currentMission ~= nil and g_currentMission.vehicles or nil
    if vehicles == nil then return end
    for _, vehicle in pairs(vehicles) do
        if self:wrapVehicle(vehicle) then
            self:injectMudSystemWetness(vehicle, 0)
        end
    end
end

function TractorTerrainDynamicsCompatibility:getControlledTTDVehicle()
    if g_localPlayer ~= nil and g_localPlayer.getCurrentVehicle ~= nil then
        local ok, vehicle = pcall(g_localPlayer.getCurrentVehicle, g_localPlayer)
        if ok and not ttdcVehicleDeleting(vehicle)
            and vehicle.spec_tractorTerrainDynamics ~= nil then
            return vehicle
        end
    end

    local vehicle = g_currentMission ~= nil and g_currentMission.controlledVehicle or nil
    if not ttdcVehicleDeleting(vehicle)
        and vehicle.spec_tractorTerrainDynamics ~= nil then
        return vehicle
    end
    return nil
end

function TractorTerrainDynamicsCompatibility:loadMap()
    self.wrappedVehicles = setmetatable({}, {__mode="k"})
    self.vehicleBridgeLogShown = false
    self._scanDelay = 0

    local active, detectedAs = self:isTractorTerrainDynamicsActive()
    self.active = active == true
    _G.__MudTTDCompatibility = self.active and self or nil
    if not self.active then return end

    print(string.format(
        "[MudSystemPhysics] Tractor Terrain Dynamics compatibility active (%s)",
        tostring(detectedAs)
    ))
    self:scanVehicles()

    if not self:isClient()
        or self:getShownVersion() >= self.NOTICE_VERSION then return end

    if MudSystemDialogQueue ~= nil and MudSystemDialogQueue.enqueue ~= nil then
        MudSystemDialogQueue:enqueue(
            "tractorTerrainDynamicsCompatibility",
            60,
            function(onComplete)
                return TractorTerrainDynamicsCompatibility:showDialog(onComplete)
            end
        )
    else
        self:showDialog(nil)
    end
end

function TractorTerrainDynamicsCompatibility:update(dt)
    if self.active ~= true then return end

    -- Keep the controlled vehicle authoritative every frame. Its HUD may be
    -- rendered between TTD's own 500 ms updates and the periodic full scan.
    local controlledVehicle = self:getControlledTTDVehicle()
    if controlledVehicle ~= nil then
        self:wrapVehicle(controlledVehicle)
        self:injectMudSystemWetness(controlledVehicle, dt or 0)
    end

    self._scanDelay = (self._scanDelay or 0)-(dt or 0)
    if self._scanDelay <= 0 then
        self._scanDelay = self.scanIntervalMs or 1000
        self:scanVehicles()
    end
end

function TractorTerrainDynamicsCompatibility:deleteMap()
    local vehicles = {}
    for vehicle in pairs(self.wrappedVehicles or {}) do
        vehicles[#vehicles+1] = vehicle
    end
    for _, vehicle in ipairs(vehicles) do
        self:releaseVehicle(vehicle)
    end
    self.wrappedVehicles = setmetatable({}, {__mode="k"})
    self.vehicleBridgeLogShown = false
    self.active = false
    _G.__MudTTDCompatibility = nil
end

addModEventListener(TractorTerrainDynamicsCompatibility)
