-- Optional runtime integration with FS25_useYourTyres.
-- The external mod is never edited and Mud System Physics remains standalone.

UseYourTyresCompatibility = {}
UseYourTyresCompatibility.NOTICE_VERSION = 1
UseYourTyresCompatibility.NOTICE_FILENAME = "mudSystemPhysicsNotices.xml"
UseYourTyresCompatibility.MOD_NAMES = {
    "FS25_useYourTyres",
    "FS25_UseYourTyres",
    "useYourTyres"
}
UseYourTyresCompatibility.riskStartWear = 0.20
UseYourTyresCompatibility.riskMaximumMultiplier = 10.0
UseYourTyresCompatibility.riskExponent = 2.20
UseYourTyresCompatibility.active = false

local function uytcClamp(value, minimum, maximum)
    value = tonumber(value) or minimum
    return math.max(minimum, math.min(maximum, value))
end

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

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

local function uytcGetText(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

function UseYourTyresCompatibility:getModEnvironment()
    -- GIANTS gives every script mod its own environment table. Symbols exported
    -- by another mod therefore live under _G[modName], not directly in our _G.
    for _, modName in ipairs(self.MOD_NAMES) do
        local environment = _G[modName]
        if type(environment) == "table"
            and type(rawget(environment, "UseYourTyres")) == "table" then
            return environment, modName
        end
    end

    -- Keeps compatibility with development builds that expose the API globally.
    if type(rawget(_G, "UseYourTyres")) == "table" then
        return _G, "runtime"
    end
    return nil, nil
end

function UseYourTyresCompatibility:getAPI()
    local environment = self:getModEnvironment()
    return environment ~= nil and rawget(environment, "UseYourTyres") or nil
end

function UseYourTyresCompatibility:isModActive()
    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

    local _, detectedAs = self:getModEnvironment()
    if detectedAs ~= nil then
        return true, detectedAs
    end
    return false, nil
end

function UseYourTyresCompatibility:getWheelWear(vehicle, wheel)
    if self.active ~= true or wheel == nil then return nil end
    vehicle = vehicle or wheel.vehicle
    if uytcVehicleDeleting(vehicle) or vehicle.uytHasTyres ~= true
        or vehicle.uytUnsupported == true then return nil end

    local api = self:getAPI()
    if api ~= nil and type(api.getWearAmount) == "function" then
        local ok, wear = pcall(api.getWearAmount, wheel)
        if ok and type(wear) == "number" then
            return uytcClamp(wear, 0, 1)
        end
    end

    -- Compatibility fallback for future versions retaining the synchronized
    -- travelled-distance fields but renaming the public helper.
    local travelled = tonumber(wheel.uytTravelledDist)
    local usedMax = api ~= nil and tonumber(api.USED_MAX_M) or nil
    local distanceMultiplier = api ~= nil and tonumber(api.tyreDistanceMultiplier) or nil
    if travelled ~= nil and usedMax ~= nil and usedMax > 0
        and distanceMultiplier ~= nil and distanceMultiplier > 0 then
        return uytcClamp(travelled/(usedMax*distanceMultiplier), 0, 1)
    end
    return nil
end

function UseYourTyresCompatibility:getPunctureRiskMultiplier(vehicle, wheel)
    local wear = self:getWheelWear(vehicle, wheel)
    if wear == nil then return 1, nil end

    local startWear = uytcClamp(self.riskStartWear or 0.20, 0, 0.95)
    local normalized = uytcClamp((wear-startWear)/math.max(0.01, 1-startWear), 0, 1)
    local maximum = math.max(1, tonumber(self.riskMaximumMultiplier) or 10)
    local exponent = math.max(0.25, tonumber(self.riskExponent) or 2.20)
    return 1 + (maximum-1)*(normalized^exponent), wear
end

function UseYourTyresCompatibility:getConsoleVehicle(componentIndex)
    if PunctureSystem ~= nil and PunctureSystem.getConsoleVehicle ~= nil then
        return PunctureSystem:getConsoleVehicle(componentIndex)
    end

    local mission = g_currentMission
    local vehicle = mission ~= nil
        and (mission.controlledVehicle or mission.currentVehicle) or nil
    if vehicle ~= nil and vehicle.spec_wheels ~= nil then return vehicle end
    return nil
end

function UseYourTyresCompatibility:refreshWheelAfterWearChange(vehicle, wheel)
    local api = self:getAPI()
    if api ~= nil and type(api.updateWheelRadius) == "function" then
        for _, visualWheel in ipairs(wheel.visualWheels or {}) do
            for _, visualPart in ipairs(visualWheel.visualParts or {}) do
                if visualPart.uytEnabled == true then
                    pcall(api.updateWheelRadius, visualPart, false, vehicle)
                end
            end
        end
    end

    if wheel.physics ~= nil then
        wheel.physics.isFrictionDirty = true
    end
    self:updateWheelRadiusChannel(wheel)
end

function UseYourTyresCompatibility:consoleCommandTyreWear(wearArg, wheelArg, componentIndex)
    if self.active ~= true then
        return "[MudSystemPhysics] FS25_useYourTyres is not active"
    end
    local api = self:getAPI()
    if api == nil then
        return "[MudSystemPhysics] FS25_useYourTyres API is unavailable; reload the savegame"
    end
    if g_server == nil then
        return "[MudSystemPhysics] gsMudTyreWear is server-only"
    end

    local wearPercent = tonumber(wearArg)
    if wearPercent == nil or wearPercent < 0 or wearPercent > 100 then
        return "Usage: gsMudTyreWear <0-100> <wheel 1..N|all> [component 1..N]"
    end

    local vehicle = self:getConsoleVehicle(componentIndex)
    local wheels = vehicle ~= nil and vehicle.spec_wheels ~= nil
        and vehicle.spec_wheels.wheels or nil
    if wheels == nil or #wheels == 0 then
        return "[MudSystemPhysics] No wheeled vehicle selected"
    end
    if vehicle.uytHasTyres ~= true or vehicle.uytUnsupported == true then
        return "[MudSystemPhysics] Use Your Tyres does not support the selected vehicle"
    end

    local selected = {}
    local wheelSelector = string.lower(tostring(wheelArg or "all"))
    if wheelSelector == "all" then
        for wheelIndex, wheel in ipairs(wheels) do
            if wheel ~= nil and wheel.physics ~= nil then
                selected[#selected+1] = {index=wheelIndex, wheel=wheel}
            end
        end
    else
        local wheelIndex = math.floor(tonumber(wheelArg) or 0)
        local wheel = wheels[wheelIndex]
        if wheelIndex < 1 or wheel == nil or wheel.physics == nil then
            return string.format(
                "Usage: gsMudTyreWear <0-100> <wheel 1-%d|all> [component 1..N]",
                #wheels
            )
        end
        selected[1] = {index=wheelIndex, wheel=wheel}
    end

    if #selected == 0 then
        return "[MudSystemPhysics] The selected vehicle has no physical wheels"
    end

    local usedMax = tonumber(api.USED_MAX_M) or 240000
    local distanceMultiplier = math.max(0.00001, tonumber(api.tyreDistanceMultiplier) or 1)
    local travelledDistance = usedMax*distanceMultiplier*(wearPercent/100)

    for _, item in ipairs(selected) do
        item.wheel.uytTravelledDist = travelledDistance
        self:refreshWheelAfterWearChange(vehicle, item.wheel)
    end

    if type(api.saveWheelTravelsToConfigurationData) == "function" then
        pcall(api.saveWheelTravelsToConfigurationData, vehicle)
    end
    if wearPercent < 85 then vehicle.uytWearReported = nil end

    local vehicleName = vehicle.getName ~= nil and vehicle:getName()
        or vehicle.typeName or "vehicle"
    local targetText = wheelSelector == "all"
        and string.format("all %d wheels", #selected)
        or string.format("wheel %d", selected[1].index)
    local riskMultiplier = self:getPunctureRiskMultiplier(vehicle, selected[1].wheel)
    return string.format(
        "[MudSystemPhysics] %s: tyre wear set to %.0f%% for %s (puncture risk x%.2f)",
        tostring(vehicleName), wearPercent, targetText, riskMultiplier or 1
    )
end

function UseYourTyresCompatibility:updateWheelRadiusChannel(wheel)
    if self.active ~= true or wheel == nil or wheel.physics == nil then return end
    local physics = wheel.physics
    local vehicle = wheel.vehicle
    if uytcVehicleDeleting(vehicle) then return end

    if vehicle.uytHasTyres ~= true or vehicle.uytUnsupported == true then
        if physics.__uytDesiredRadius ~= nil then
            physics.__uytDesiredRadius = nil
            physics.__uytOrigRadius = nil
            if _G.__MudRadiusCombiner ~= nil and _G.__MudRadiusCombiner.apply ~= nil then
                _G.__MudRadiusCombiner.apply(physics, 0.001)
            end
        end
        return
    end

    local wear = self:getWheelWear(vehicle, wheel)
    if wear == nil then return end
    local originalRadius = nil
    local desiredRadius = nil

    for _, visualWheel in ipairs(wheel.visualWheels or {}) do
        for _, visualPart in ipairs(visualWheel.visualParts or {}) do
            local maxPhysical = tonumber(visualPart.uytMaxPhysRadius)
            local maxUsage = tonumber(visualPart.uytMaxUsage)
            if visualPart.uytEnabled == true and uytcValidRadius(maxPhysical)
                and type(maxUsage) == "number" and maxUsage >= 0 then
                originalRadius = originalRadius == nil
                    and maxPhysical or math.max(originalRadius, maxPhysical)
                local candidate = math.max(0.06, maxPhysical-wear*maxUsage)
                desiredRadius = desiredRadius == nil
                    and candidate or math.min(desiredRadius, candidate)
            end
        end
    end

    if not uytcValidRadius(originalRadius) or not uytcValidRadius(desiredRadius) then
        return
    end

    physics.__uytOrigRadius = originalRadius
    physics.__uytDesiredRadius = math.min(originalRadius, desiredRadius)
    if _G.__MudRadiusCombiner ~= nil and _G.__MudRadiusCombiner.apply ~= nil then
        _G.__MudRadiusCombiner.apply(physics, 0.001)
    end
end

function UseYourTyresCompatibility:releaseVehicle(vehicle)
    if vehicle == nil then return end
    local wheels = vehicle.spec_wheels ~= nil and vehicle.spec_wheels.wheels or nil
    for _, wheel in ipairs(wheels or {}) do
        local physics = wheel ~= nil and wheel.physics or nil
        if physics ~= nil then
            physics.__uytDesiredRadius = nil
            physics.__uytOrigRadius = nil
        end
    end
end

function UseYourTyresCompatibility:installWheelHook()
    if self._wheelHookInstalled == true or Wheel == nil or Wheel.update == nil
        or Utils == nil or Utils.appendedFunction == nil then return end

    self._originalWheelUpdate = Wheel.update
    local compatibility = self
    self._wheelUpdateWrapper = Utils.appendedFunction(
        Wheel.update,
        function(wheel, dt)
            if compatibility.active == true then
                compatibility:updateWheelRadiusChannel(wheel)
            end
        end
    )
    Wheel.update = self._wheelUpdateWrapper
    self._wheelHookInstalled = true
end

function UseYourTyresCompatibility:installReplacementHook()
    local environment = self:getModEnvironment()
    local eventClass = environment ~= nil and rawget(environment, "UytReplaceEvent") or nil
    if self._replacementHookInstalled == true or eventClass == nil
        or eventClass.run == nil or Utils == nil
        or Utils.appendedFunction == nil then return end

    self._replacementEventClass = eventClass
    self._originalReplacementRun = eventClass.run
    local compatibility = self
    self._replacementRunWrapper = Utils.appendedFunction(
        eventClass.run,
        function(event, connection)
            if compatibility.active ~= true or g_server == nil
                or event == nil or event.vehicle == nil
                or PunctureSystem == nil or PunctureStateEvent == nil then return end

            local wheels = event.vehicle.spec_wheels ~= nil
                and event.vehicle.spec_wheels.wheels or nil
            if wheels == nil then return end
            for wheelIndex, _ in ipairs(wheels) do
                if PunctureSystem:getWheelState(event.vehicle, wheelIndex) ~= nil then
                    PunctureStateEvent.send(
                        event.vehicle, wheelIndex,
                        PunctureSystem.STAGE_NONE, true,
                        0, 0, 0, 0
                    )
                end
            end
        end
    )
    eventClass.run = self._replacementRunWrapper
    self._replacementHookInstalled = true
end

function UseYourTyresCompatibility: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 UseYourTyresCompatibility: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.useYourTyresCompatibility#version"
    ) or 0
    delete(xmlFile)
    return version
end

function UseYourTyresCompatibility:markShown()
    local path = self:getNoticePath()
    if path == nil then return end
    local xmlFile = fileExists(path)
        and loadXMLFile("MudSystemPhysicsNotices", path)
        or createXMLFile("MudSystemPhysicsNotices", path, "mudSystemPhysicsNotices")
    if xmlFile == 0 then return end
    setXMLInt(
        xmlFile,
        "mudSystemPhysicsNotices.useYourTyresCompatibility#version",
        self.NOTICE_VERSION
    )
    saveXMLFile(xmlFile)
    delete(xmlFile)
end

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

function UseYourTyresCompatibility:showDialog(onComplete)
    local text = uytcGetText(
        "mudsys_uyt_compat_text",
        "Use Your Tyres was detected. Compatibility is active: when wheel punctures are enabled, the puncture risk of each tire now increases with its individual wear. Use Your Tyres remains responsible for wear, replacement and tire visuals; wheel-radius changes are merged with mud, pressure and puncture deformation. Both mods also work independently."
    )
    local okText = uytcGetText("mudsys_uyt_compat_ok", "OK")
    local function closeNotice()
        UseYourTyresCompatibility: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 UseYourTyresCompatibility:loadMap()
    local active, detectedAs = self:isModActive()
    self.active = active == true
    _G.__MudUseYourTyresCompatibility = self.active and self or nil
    if not self.active then return end

    self:installWheelHook()
    self:installReplacementHook()
    addConsoleCommand(
        "gsMudTyreWear",
        "Set Use Your Tyres wear: gsMudTyreWear <0-100> <wheel|all> [component]",
        "consoleCommandTyreWear",
        self
    )
    self._consoleRegistered = true
    print(string.format(
        "[MudSystemPhysics] Use Your Tyres compatibility active (%s, API=%s)",
        tostring(detectedAs), self:getAPI() ~= nil and "ready" or "missing"
    ))

    if not self:isClient() or self:getShownVersion() >= self.NOTICE_VERSION then return end
    if MudSystemDialogQueue ~= nil and MudSystemDialogQueue.enqueue ~= nil then
        MudSystemDialogQueue:enqueue(
            "useYourTyresCompatibility",
            70,
            function(onComplete)
                return UseYourTyresCompatibility:showDialog(onComplete)
            end
        )
    else
        self:showDialog(nil)
    end
end

function UseYourTyresCompatibility:deleteMap()
    if self._consoleRegistered == true then
        removeConsoleCommand("gsMudTyreWear")
        self._consoleRegistered = false
    end
    if self._wheelHookInstalled == true and Wheel ~= nil
        and Wheel.update == self._wheelUpdateWrapper then
        Wheel.update = self._originalWheelUpdate
    end
    self._wheelHookInstalled = false
    self._wheelUpdateWrapper = nil
    self._originalWheelUpdate = nil

    local eventClass = self._replacementEventClass
    if self._replacementHookInstalled == true and eventClass ~= nil
        and eventClass.run == self._replacementRunWrapper then
        eventClass.run = self._originalReplacementRun
    end
    self._replacementHookInstalled = false
    self._replacementEventClass = nil
    self._replacementRunWrapper = nil
    self._originalReplacementRun = nil

    local vehicles = g_currentMission ~= nil and g_currentMission.vehicles or nil
    if vehicles ~= nil then
        for _, vehicle in pairs(vehicles) do
            local wheels = vehicle ~= nil and vehicle.spec_wheels ~= nil
                and vehicle.spec_wheels.wheels or nil
            for _, wheel in ipairs(wheels or {}) do
                local physics = wheel ~= nil and wheel.physics or nil
                if physics ~= nil then
                    physics.__uytDesiredRadius = nil
                    physics.__uytOrigRadius = nil
                end
            end
        end
    end
    self.active = false
    _G.__MudUseYourTyresCompatibility = nil
end

addModEventListener(UseYourTyresCompatibility)
