-- Server: setzt die Masse authoritative, entgegnet Client-Commands und macht einen sanften Fallback-Tick

pcall(function() require "MSW_MassUtil" end)
if not MSW_MassUtil then return end
if MSW_MassUtil.isPayloadDampingEnabled and not MSW_MassUtil.isPayloadDampingEnabled() then return end
if isClient and isClient() then return end

local CHUNK_SIZE = (getChunkSizeInSquares and getChunkSizeInSquares()) or 10
local lastChunkByPlayer = {}
local MASS_UPDATE_MAX_SPEED_KMH = 5
local pendingVehicleIds = {}
local acceptedMassStateByVehicleId = {}

local function getVehicleKey(vehicle)
    if not vehicle then return nil end
    return (vehicle.getId and vehicle:getId()) or tostring(vehicle)
end

local function getAcceptedMassState(vehicle)
    local key = getVehicleKey(vehicle)
    if not key then return nil end
    local state = acceptedMassStateByVehicleId[key]
    if not state then
        state = {}
        acceptedMassStateByVehicleId[key] = state
    end
    return state
end

local function setPending(vehicle, pending)
    local key = getVehicleKey(vehicle)
    if not key then return end
    pendingVehicleIds[key] = pending and true or nil
end

local function isPending(vehicle)
    local key = getVehicleKey(vehicle)
    return key and pendingVehicleIds[key] == true or false
end

local function isVehiclePhysicallyActive(vehicle)
    if not vehicle or not vehicle.getCurrentSpeedKmHour then return false end
    local speed = math.abs(vehicle:getCurrentSpeedKmHour() or 0)
    if speed > MASS_UPDATE_MAX_SPEED_KMH then
        return true
    end
    if vehicle.isEngineRunning and vehicle:isEngineRunning() then
        return true
    end
    return false
end

local function hasDriver(vehicle)
    if not vehicle or not vehicle.getDriver then return false end
    return vehicle:getDriver() ~= nil
end

local function clusterHasDriver(vehicle)
    if not vehicle then return false end
    if hasDriver(vehicle) then return true end

    local towing = vehicle.getVehicleTowing and vehicle:getVehicleTowing() or nil
    if hasDriver(towing) then return true end

    local towedBy = vehicle.getVehicleTowedBy and vehicle:getVehicleTowedBy() or nil
    if hasDriver(towedBy) then return true end

    return false
end

local function hasTowLink(vehicle)
    if not vehicle then return false end
    local towing = vehicle.getVehicleTowing and vehicle:getVehicleTowing() or nil
    local towedBy = vehicle.getVehicleTowedBy and vehicle:getVehicleTowedBy() or nil
    return towing ~= nil or towedBy ~= nil
end

local function getVehicleScriptName(vehicle)
    if not vehicle or not vehicle.getScript then
        return nil
    end

    local script = vehicle:getScript()
    if not script then
        return nil
    end

    if script.getFullName then
        return script:getFullName()
    end
    if script.getName then
        return script:getName()
    end
    return nil
end

local function isTrailerPayloadTarget(vehicle)
    local scriptName = getVehicleScriptName(vehicle)
    if type(scriptName) ~= "string" then
        return false
    end

    return string.find(string.lower(scriptName), "semitrailer", 1, true) ~= nil
end

local function isMassUpdateLocked(vehicle)
    local towing = vehicle and vehicle.getVehicleTowing and vehicle:getVehicleTowing() or nil
    local towedBy = vehicle and vehicle.getVehicleTowedBy and vehicle:getVehicleTowedBy() or nil
    local linked = towing ~= nil or towedBy ~= nil

    if isVehiclePhysicallyActive(vehicle) then
        if not linked and hasDriver(vehicle) then
            return false
        end
        return true
    end

    if isVehiclePhysicallyActive(towing) then
        return true
    end

    if isVehiclePhysicallyActive(towedBy) then
        return true
    end

    if linked and clusterHasDriver(vehicle) then
        return true
    end

    return false
end

local function clusterHasPending(vehicle)
    if not vehicle then return false end
    if isPending(vehicle) then return true end

    local towing = vehicle.getVehicleTowing and vehicle:getVehicleTowing() or nil
    if isPending(towing) then return true end

    local towedBy = vehicle.getVehicleTowedBy and vehicle:getVehicleTowedBy() or nil
    if isPending(towedBy) then return true end

    return false
end

local function getFactor()
    local f = 0.65
    if SandboxVars and SandboxVars.MSW and SandboxVars.MSW.PayloadFactor ~= nil then
        local v = tonumber(SandboxVars.MSW.PayloadFactor)
        if v and v >= 0 and v <= 1 then f = v end
    end
    return f
end

local function getValidatedDesiredMass(vehicle, factor)
    local desired = MSW_MassUtil.desiredMass(vehicle, factor)
    local _, _, payloadTotal = MSW_MassUtil.computePayload(vehicle)
    local state = getAcceptedMassState(vehicle)
    if not state then
        return desired
    end

    local acceptedDesired = desired
    if state.desired ~= nil and desired > state.desired + 0.5 then
        local lastFactor = state.factor
        local factorChanged = lastFactor == nil or math.abs(lastFactor - factor) > 0.0001
        local payloadIncreased = state.payloadTotal == nil or payloadTotal > state.payloadTotal + 0.01
        if not factorChanged and not payloadIncreased then
            acceptedDesired = state.desired
        end
    end

    state.desired = acceptedDesired
    state.payloadTotal = payloadTotal
    state.factor = factor
    return acceptedDesired
end

local function applyMassServer(vehicle, mass)
    if not vehicle or not vehicle.setMass then return end
    mass = math.floor((tonumber(mass) or 0) + 0.5)
    pcall(function() vehicle:setMass(mass) end)
end

local function updateOne(vehicle, factor)
    if not vehicle then return end
    if not MSW_MassUtil.isTargetVehicle(vehicle) then return end
    if isTrailerPayloadTarget(vehicle) and not hasTowLink(vehicle) then
        setPending(vehicle, false)
        return
    end
    local desired = getValidatedDesiredMass(vehicle, factor)
    local okCur, cur = pcall(function() return vehicle:getMass() end)
    local needsRefresh = (not okCur or not cur) or math.abs(cur - desired) > 0.5
    if not needsRefresh then
        setPending(vehicle, false)
        return
    end
    local lowersMass = okCur and cur and desired < cur - 0.5
    if not lowersMass and isMassUpdateLocked(vehicle) then
        setPending(vehicle, true)
        return
    end
    applyMassServer(vehicle, desired)
    setPending(vehicle, false)
end

local function updateVehicleCluster(vehicle, factor)
    if not vehicle then return end

    updateOne(vehicle, factor)

    local towing = vehicle.getVehicleTowing and vehicle:getVehicleTowing() or nil
    if towing then
        updateOne(towing, factor)
    end

    local towedBy = vehicle.getVehicleTowedBy and vehicle:getVehicleTowedBy() or nil
    if towedBy then
        updateOne(towedBy, factor)
    end
end

local function getChunkCoords(square)
    if not square then return nil end

    return math.floor(square:getX() / CHUNK_SIZE), math.floor(square:getY() / CHUNK_SIZE), square:getZ()
end

local function updateChunksAroundSquare(square, factor)
    local cell = getCell()
    if not cell or not square then return end

    local chunkX, chunkY, z = getChunkCoords(square)
    if not chunkX or not chunkY then return end

    local seen = {}
    for dx = -1, 1 do
        for dy = -1, 1 do
            local originX = (chunkX + dx) * CHUNK_SIZE
            local originY = (chunkY + dy) * CHUNK_SIZE

            for tileX = 0, CHUNK_SIZE - 1 do
                for tileY = 0, CHUNK_SIZE - 1 do
                    local gridSquare = cell:getGridSquare(originX + tileX, originY + tileY, z)
                    if gridSquare then
                        local vehicle = gridSquare:getVehicleContainer()
                        if vehicle then
                            local vehicleKey = (vehicle.getId and vehicle:getId()) or tostring(vehicle)
                            if not seen[vehicleKey] then
                                seen[vehicleKey] = true
                                updateVehicleCluster(vehicle, factor)
                            end
                        end
                    end
                end
            end
        end
    end
end

-- Client-Command zum Setzen der Masse (redundant zu Tick, aber schnell)
local function OnClientCommand(module, command, playerObj, args)
    if module ~= 'msw_mass' or command ~= 'setMass' then return end
    if not args or not args.veh or not args.mass then return end
    local v = getVehicleById(args.veh)
    if not v then return end
    if not MSW_MassUtil.isTargetVehicle(v) then return end
    applyMassServer(v, getValidatedDesiredMass(v, getFactor()))
end
Events.OnClientCommand.Add(OnClientCommand)

Events.OnPlayerMove.Add(function(player)
    if not player then return end

    local currentVehicle = player:getVehicle()
    if currentVehicle and clusterHasPending(currentVehicle) then
        updateVehicleCluster(currentVehicle, getFactor())
    end

    local square = player:getSquare()
    if not square then return end

    local chunkX, chunkY, z = getChunkCoords(square)
    local playerKey = (player.getOnlineID and player:getOnlineID()) or (player.getPlayerNum and player:getPlayerNum()) or tostring(player)
    local last = lastChunkByPlayer[playerKey]
    if last and last.x == chunkX and last.y == chunkY and last.z == z then
        return
    end

    lastChunkByPlayer[playerKey] = { x = chunkX, y = chunkY, z = z }

    local factor = getFactor()
    updateChunksAroundSquare(square, factor)
    updateVehicleCluster(player:getVehicle(), factor)
end)

-- Leichter Fallback-Tick (seltener, um Last gering zu halten)
local tick = 0
local pendingTick = 0
Events.OnTick.Add(function()
    tick = (tick + 1) % 90 -- ~0.33x/Sek.
    pendingTick = (pendingTick + 1) % 10

    local factor = getFactor()
    if pendingTick == 0 then
        local n = getNumActivePlayers and getNumActivePlayers() or 1
        for i = 0, n - 1 do
            local pl = getSpecificPlayer and getSpecificPlayer(i) or nil
            if pl then
                local vehicle = pl:getVehicle()
                if vehicle and (clusterHasPending(vehicle) or not isMassUpdateLocked(vehicle)) then
                    updateVehicleCluster(vehicle, factor)
                end
            end
        end
    end

    if tick ~= 0 then return end
    local n = getNumActivePlayers and getNumActivePlayers() or 1
    for i = 0, n - 1 do
        local pl = getSpecificPlayer and getSpecificPlayer(i) or nil
        if pl then
            updateVehicleCluster(pl:getVehicle(), factor)
        end
    end
end)