-- Owns wheel dirt and wheelMudMeshes while a wheel is managed by Mud System.
-- GIANTS' stock wheel-mud callback uses a hard tire-dirt threshold, which makes
-- a mesh blink when another system adds small amounts around that threshold.

MudWheelVisualSystem = {}
MudWheelVisualSystem.states = setmetatable({}, { __mode = "k" })
MudWheelVisualSystem.hooksInstalled = false

MudWheelVisualSystem.tireRateMul = 1.85
MudWheelVisualSystem.meshExposureStartSec = 2.20
MudWheelVisualSystem.meshExposureFullSec = 8.50
MudWheelVisualSystem.meshRisePerSec = 0.22
MudWheelVisualSystem.contactHoldMs = 180
MudWheelVisualSystem.roadTireCleanPerSec = 0.015
MudWheelVisualSystem.roadMeshShedPerSec = 0.105
MudWheelVisualSystem.rainTireCleanPerSec = 0.004
MudWheelVisualSystem.rainMeshCleanPerSec = 0.014
MudWheelVisualSystem.waterTireCleanPerSec = 0.30
MudWheelVisualSystem.waterMeshCleanPerSec = 0.65

local function mwvClamp(value, minValue, maxValue)
    return math.max(minValue, math.min(maxValue, value or 0))
end

local function mwvSmoothstep(value)
    value = mwvClamp(value, 0, 1)
    return value * value * (3 - 2 * value)
end

local function mwvApproach(current, target, maxStep)
    if current < target then
        return math.min(target, current + maxStep)
    end
    return math.max(target, current - maxStep)
end

local function mwvGetSpeedKph(vehicle)
    if vehicle == nil then
        return 0
    end
    if vehicle.getLastSpeed ~= nil then
        -- FS25 already returns km/h here.
        return math.abs(vehicle:getLastSpeed() or 0)
    end
    return math.abs(vehicle.lastSpeed or 0) * 3600
end

function MudWheelVisualSystem:getWheelState(vehicle, wheel, wheelIndex)
    local vehicleState = self.states[vehicle]
    if vehicleState == nil then
        vehicleState = { wheels = {} }
        self.states[vehicle] = vehicleState
    end

    if wheelIndex == nil then
        for _, existingState in pairs(vehicleState.wheels) do
            if existingState.wheel == wheel then
                return existingState
            end
        end
    end

    local key = wheelIndex or wheel
    local state = vehicleState.wheels[key]
    if state == nil then
        state = {
            wheel = wheel,
            wheelIndex = wheelIndex,
            exposure = 0,
            tireAmount = nil,
            meshAmount = nil,
            lastContactMs = -1000000,
            rainScale = 0,
            particleChannel = 0,
            shedFxActive = false,
            nextShedFxMs = 0,
            nextShedBurstMs = 0,
            shedBurstUntilMs = 0
        }
        vehicleState.wheels[key] = state
    else
        state.wheel = wheel
        state.wheelIndex = wheelIndex or state.wheelIndex
    end

    return state
end

function MudWheelVisualSystem:getWashableNodes(vehicle, wheel)
    if vehicle == nil or wheel == nil
        or vehicle.getWashableNodeByCustomIndex == nil then
        return nil, nil
    end

    local tireNode = vehicle:getWashableNodeByCustomIndex(wheel)
    local meshNode = nil
    if wheel.wheelMudMeshes ~= nil then
        meshNode = vehicle:getWashableNodeByCustomIndex(wheel.wheelMudMeshes)
    end
    if meshNode == tireNode then
        meshNode = nil
    end
    return tireNode, meshNode
end

function MudWheelVisualSystem:reportMudContact(
    vehicle, wheel, wheelIndex, dtS, wheelAdd, contactIntensity, dirtMax, particleChannel
)
    if vehicle == nil or vehicle.__mudSystemDeleting == true
        or wheel == nil or vehicle.isServer ~= true
        or vehicle.setNodeDirtAmount == nil then
        return false
    end

    local tireNode, meshNode = self:getWashableNodes(vehicle, wheel)
    if tireNode == nil and meshNode == nil then
        return false
    end

    local state = self:getWheelState(vehicle, wheel, wheelIndex)
    wheel.__mudsysWheelVisualManaged = true
    state.particleChannel = particleChannel or state.particleChannel or 0
    state.shedFxActive = false

    dirtMax = math.max(0.01, dirtMax or 1)
    dtS = math.max(0, dtS or 0)
    wheelAdd = math.max(0, wheelAdd or 0)
    local intensity = mwvClamp(contactIntensity or 0, 0, 1)

    local tireActual = tireNode ~= nil and (tireNode.dirtAmount or 0) or 0
    local meshActual = meshNode ~= nil and (meshNode.dirtAmount or 0) or 0
    state.tireAmount = math.max(state.tireAmount or tireActual, tireActual)
    state.meshAmount = math.max(state.meshAmount or meshActual, meshActual)

    state.tireAmount = mwvClamp(
        state.tireAmount + wheelAdd * (self.tireRateMul or 1.85),
        0,
        dirtMax
    )
    state.exposure = math.min(
        (self.meshExposureFullSec or 8.5) + 4,
        (state.exposure or 0) + dtS * math.max(0.12, intensity)
    )

    local exposureStart = self.meshExposureStartSec or 2.2
    local exposureFull = math.max(exposureStart + 0.1, self.meshExposureFullSec or 8.5)
    local exposureRamp = mwvSmoothstep(
        ((state.exposure or 0) - exposureStart) / (exposureFull - exposureStart)
    )
    local tireSupport = mwvSmoothstep((state.tireAmount - 0.18) / 0.72)
    local meshTarget = dirtMax * exposureRamp * tireSupport
    local meshRise = (self.meshRisePerSec or 0.22) * (0.35 + intensity * 0.65) * dtS
    state.meshAmount = mwvApproach(state.meshAmount, meshTarget, meshRise)
    state.lastContactMs = g_time or 0

    if tireNode ~= nil then
        vehicle:setNodeDirtAmount(tireNode, state.tireAmount, true)
    end
    if meshNode ~= nil then
        vehicle:setNodeDirtAmount(meshNode, state.meshAmount, true)
    end

    return true
end

function MudWheelVisualSystem:setShedParticles(vehicle, state, active, nowMs)
    local wheel = state.wheel
    local physics = wheel ~= nil and wheel.physics or nil
    if wheel == nil or physics == nil then
        return
    end

    local channel = state.particleChannel or 0
    if active then
        physics.__mudsysShedParticlesUntil = nowMs + 300
        physics.__mudsysShedParticleChannel = channel
    else
        physics.__mudsysShedParticlesUntil = nil
        physics.__mudsysShedParticleChannel = nil
    end

    local missionInfo = g_currentMission ~= nil and g_currentMission.missionDynamicInfo or nil
    local isMultiplayer = missionInfo ~= nil and missionInfo.isMultiplayer == true
    if isMultiplayer or g_client == nil then
        state.shedFxActive = active
        return
    end

    if MudExtraParticleEvent == nil or channel == 0 then
        state.shedFxActive = false
        return
    end

    if active and nowMs >= (state.nextShedFxMs or 0) then
        local x, y, z = nil, nil, nil
        if channel == MudExtraParticleEvent.CHANNEL_FIELD
            and FieldGroundMudPhysics ~= nil
            and FieldGroundMudPhysics.getWheelContactPos ~= nil then
            x, y, z = FieldGroundMudPhysics:getWheelContactPos(wheel)
        elseif MudPhysics ~= nil and MudPhysics.getWheelContactPos ~= nil then
            x, y, z = MudPhysics:getWheelContactPos(wheel)
        end

        if x ~= nil and z ~= nil then
            if channel == MudExtraParticleEvent.CHANNEL_FIELD
                and FieldGroundMudPhysics ~= nil
                and FieldGroundMudPhysics.applyNetworkExtraParticles ~= nil then
                FieldGroundMudPhysics:applyNetworkExtraParticles(
                    vehicle, state.wheelIndex, true, x, y or 0, z, true
                )
            elseif channel == MudExtraParticleEvent.CHANNEL_MUD
                and MudPhysics ~= nil
                and MudPhysics.applyNetworkExtraParticles ~= nil then
                MudPhysics:applyNetworkExtraParticles(
                    vehicle, state.wheelIndex, true, x, y or 0, z, true
                )
            end
        end
        state.nextShedFxMs = nowMs + 250
    elseif not active and state.shedFxActive == true then
        if channel == MudExtraParticleEvent.CHANNEL_FIELD
            and FieldGroundMudPhysics ~= nil
            and FieldGroundMudPhysics.applyNetworkExtraParticles ~= nil then
            FieldGroundMudPhysics:applyNetworkExtraParticles(
                vehicle, state.wheelIndex, false, 0, 0, 0, false
            )
        elseif channel == MudExtraParticleEvent.CHANNEL_MUD
            and MudPhysics ~= nil
            and MudPhysics.applyNetworkExtraParticles ~= nil then
            MudPhysics:applyNetworkExtraParticles(
                vehicle, state.wheelIndex, false, 0, 0, 0, false
            )
        end
    end
    state.shedFxActive = active
end

function MudWheelVisualSystem:clearVehicle(vehicle)
    if vehicle == nil then
        return
    end

    local vehicleState = self.states[vehicle]
    if vehicleState ~= nil and vehicleState.wheels ~= nil then
        for _, state in pairs(vehicleState.wheels) do
            if state.wheel ~= nil then
                state.wheel.__mudsysWheelVisualManaged = nil
                if state.wheel.physics ~= nil then
                    state.wheel.physics.__mudsysShedParticlesUntil = nil
                    state.wheel.physics.__mudsysShedParticleChannel = nil
                end
            end
        end
    end
    self.states[vehicle] = nil
end

function MudWheelVisualSystem:updateWheelAwayFromMud(vehicle, state, dtS, nowMs)
    local wheel = state.wheel
    if wheel == nil or (nowMs - (state.lastContactMs or -1000000)) <= (self.contactHoldMs or 180) then
        return
    end

    local tireNode, meshNode = self:getWashableNodes(vehicle, wheel)
    if tireNode == nil and meshNode == nil then
        return
    end

    local tireActual = tireNode ~= nil and (tireNode.dirtAmount or 0) or 0
    local meshActual = meshNode ~= nil and (meshNode.dirtAmount or 0) or 0
    -- Accept material external changes (hand washing, workshop cleaning,
    -- savegame sync), but keep sub-frame cleanup fractions accumulated in our
    -- state until they are large enough to send to the washable node.
    if state.tireAmount == nil or math.abs(tireActual - state.tireAmount) > 0.02 then
        state.tireAmount = tireActual
    end
    if state.meshAmount == nil or math.abs(meshActual - state.meshAmount) > 0.02 then
        state.meshAmount = meshActual
    end

    local speed01 = mwvClamp(mwvGetSpeedKph(vehicle) / 45, 0, 1)
    local physics = wheel.physics
    local inWater = physics ~= nil and physics.hasWaterContact == true
    local rain = mwvClamp(state.rainScale or 0, 0, 1)

    local tireClean = 0
    local meshClean = 0
    local hasMudChunks = (state.meshAmount or 0) > 0.015
    if inWater then
        meshClean = (self.waterMeshCleanPerSec or 0.65) * dtS
        if not hasMudChunks then
            tireClean = (self.waterTireCleanPerSec or 0.30) * dtS
        end
        state.exposure = math.max(0, (state.exposure or 0) - dtS * 2.5)
    else
        if hasMudChunks then
            meshClean = (
                (self.roadMeshShedPerSec or 0.105) * speed01
                + (self.rainMeshCleanPerSec or 0.014) * rain
            ) * dtS
        else
            tireClean = (
                (self.roadTireCleanPerSec or 0.015) * speed01
                + (self.rainTireCleanPerSec or 0.004) * rain
            ) * dtS
            -- Quietly remove the thin residue after visible chunks are gone.
            meshClean = (
                (self.roadMeshShedPerSec or 0.105) * speed01
                + (self.rainMeshCleanPerSec or 0.014) * rain
            ) * dtS
        end
        state.exposure = math.max(0, (state.exposure or 0) - dtS * (0.03 + speed01 * 0.05))
    end

    local canShedFx = not inWater and hasMudChunks and speed01 > 0.03
    if canShedFx and nowMs >= (state.nextShedBurstMs or 0) then
        state.shedBurstUntilMs = nowMs + 140 + math.random() * 160
        state.nextShedBurstMs = nowMs + 450 + math.random() * 750
    end
    local shedFx = canShedFx and nowMs <= (state.shedBurstUntilMs or 0)
    self:setShedParticles(vehicle, state, shedFx, nowMs)

    state.tireAmount = math.max(0, (state.tireAmount or 0) - tireClean)
    state.meshAmount = math.max(0, (state.meshAmount or 0) - meshClean)

    if tireNode ~= nil and math.abs(tireActual - state.tireAmount) > 0.0005 then
        vehicle:setNodeDirtAmount(tireNode, state.tireAmount, true)
    end
    if meshNode ~= nil and math.abs(meshActual - state.meshAmount) > 0.0005 then
        vehicle:setNodeDirtAmount(meshNode, state.meshAmount, true)
    end
end

function MudWheelVisualSystem:update(dt)
    if g_server == nil then
        return
    end

    local dtS = math.max(0, (dt or 0) * 0.001)
    local nowMs = g_time or 0
    for vehicle, vehicleState in pairs(self.states) do
        if vehicle == nil or vehicle.__mudSystemDeleting == true
            or vehicle.spec_wheels == nil then
            self:clearVehicle(vehicle)
        elseif vehicle.isServer == true and vehicleState.wheels ~= nil then
            for _, state in pairs(vehicleState.wheels) do
                self:updateWheelAwayFromMud(vehicle, state, dtS, nowMs)
            end
        end
    end
end

function MudWheelVisualSystem:installHooks()
    if self.hooksInstalled or Wheels == nil or Utils == nil
        or Utils.overwrittenFunction == nil then
        return
    end

    if Wheels.updateWheelDirtAmount ~= nil then
        Wheels.updateWheelDirtAmount = Utils.overwrittenFunction(
            Wheels.updateWheelDirtAmount,
            function(vehicle, superFunc, nodeData, dt, a, rainScale, b, temperature)
                local changeDirt, changeWetness = superFunc(
                    vehicle, nodeData, dt, a, rainScale, b, temperature
                )
                local wheel = nodeData ~= nil and nodeData.wheel or nil
                if wheel ~= nil and wheel.__mudsysWheelVisualManaged == true then
                    local state = MudWheelVisualSystem:getWheelState(vehicle, wheel, nil)
                    state.rainScale = rainScale or 0
                    return 0, changeWetness
                end
                return changeDirt, changeWetness
            end
        )
    end

    if Wheels.updateWheelMudAmount ~= nil then
        Wheels.updateWheelMudAmount = Utils.overwrittenFunction(
            Wheels.updateWheelMudAmount,
            function(vehicle, superFunc, nodeData, dt, a, rainScale, b, temperature)
                local changeDirt, changeWetness = superFunc(
                    vehicle, nodeData, dt, a, rainScale, b, temperature
                )
                local wheel = nodeData ~= nil and nodeData.wheel or nil
                if wheel ~= nil and wheel.__mudsysWheelVisualManaged == true then
                    local state = MudWheelVisualSystem:getWheelState(vehicle, wheel, nil)
                    state.rainScale = rainScale or 0
                    return 0, changeWetness
                end
                return changeDirt, changeWetness
            end
        )
    end

    self.hooksInstalled = true
    print("[MudSystemPhysics] realistic wheel mud visual hooks installed")
end

function MudWheelVisualSystem:loadMap()
    self.states = setmetatable({}, { __mode = "k" })
    self:installHooks()
end

function MudWheelVisualSystem:deleteMap()
    self.states = setmetatable({}, { __mode = "k" })
end

addModEventListener(MudWheelVisualSystem)
