-- Shared, deterministic terramechanics approximation for the two mud systems.
--
-- The game does not expose Bekker/Wong soil parameters per terrain layer, so the
-- model uses the values we do have: wet-soil intensity, sinkage and wheel slip.
-- It separates compaction, bulldozing and slip excavation instead of treating
-- mud as an instant speed-limit switch.

MudTerramechanicsModel = MudTerramechanicsModel or {}

local Model = MudTerramechanicsModel

local function clamp(value, minimum, maximum)
    if value < minimum then return minimum end
    if value > maximum then return maximum end
    return value
end

local function smoothstep(edge0, edge1, value)
    if edge0 == edge1 then
        return value >= edge1 and 1 or 0
    end
    local t = clamp((value-edge0) / (edge1-edge0), 0, 1)
    return t*t*(3-2*t)
end

local function expSmoothing(previous, target, dtS, seconds)
    seconds = math.max(0.001, tonumber(seconds) or 0.001)
    local alpha = 1-math.exp(-math.max(0, dtS or 0) / seconds)
    return previous+(target-previous)*alpha
end

function Model:updateSoilState(state, key, intensity, sink, slip, dtS, enterSec, exitSec)
    intensity = clamp(tonumber(intensity) or 0, 0, 1)
    sink = clamp(tonumber(sink) or 0, 0, 1)
    slip = math.max(0, tonumber(slip) or 0)

    local soil = smoothstep(0.035, 0.92, intensity)
    local sinkLoad = sink^1.35
    -- Useful traction is normally reached at modest slip. Beyond that, extra
    -- wheel speed mostly excavates soil and deepens the rut.
    local slipLoad = smoothstep(0.04, 0.32, slip)
    local compaction = soil*(0.18+0.62*sinkLoad)
    local bulldozing = soil*sinkLoad*(0.08+0.12*smoothstep(2, 18, state.__tmSpeedKph or 0))
    local excavation = soil*slipLoad*(0.10+0.30*sinkLoad)
    local rawSeverity = clamp(compaction+bulldozing+excavation, 0, 1)

    local previous = clamp(tonumber(state[key]) or 0, 0, 1)
    local response = rawSeverity > previous and enterSec or exitSec
    local severity = expSmoothing(previous, rawSeverity, dtS, response)
    if math.abs(severity-rawSeverity) < 0.0005 then
        severity = rawSeverity
    end
    state[key] = severity

    return {
        severity=severity,
        rawSeverity=rawSeverity,
        soil=soil,
        sink=sink,
        sinkLoad=sinkLoad,
        slip=slip,
        slipLoad=slipLoad,
        compaction=compaction,
        bulldozing=bulldozing,
        excavation=excavation
    }
end

function Model:getSoftSpeedLimit(naturalKph, mudKph, stuckKph, soilState, stuckThreshold)
    naturalKph = math.max(1, tonumber(naturalKph) or tonumber(mudKph) or 20)
    mudKph = math.max(1, tonumber(mudKph) or naturalKph)
    stuckKph = math.max(1, math.min(mudKph, tonumber(stuckKph) or mudKph))

    local severity = clamp(soilState ~= nil and soilState.severity or 0, 0, 1)
    local sink = clamp(soilState ~= nil and soilState.sink or 0, 0, 1)
    local soil = clamp(soilState ~= nil and soilState.soil or 0, 0, 1)
    local mudBlend = smoothstep(0.05, 1, severity)^1.25
    local threshold = clamp(tonumber(stuckThreshold) or 0.85, 0.25, 0.99)
    local deepStart = math.max(0.35, threshold*0.72)
    local deepBlend = smoothstep(deepStart, 1, sink)*soil

    local limit = naturalKph+(math.min(naturalKph, mudKph)-naturalKph)*mudBlend
    limit = limit+(math.min(limit, stuckKph)-limit)*(deepBlend^1.35)
    return math.max(stuckKph, math.min(naturalKph, limit)), mudBlend, deepBlend
end

function Model:getMotorLoadTarget(soilState, fromSink, fromSlip, supportMul, configuredMax, stuck)
    if soilState == nil then return 1 end

    local sinkScale = clamp((tonumber(fromSink) or 0)/4.0, 0, 2.5)
    local slipScale = clamp((tonumber(fromSlip) or 0)/2.0, 0, 2.5)
    local sink = soilState.sinkLoad or 0
    local slip = soilState.slipLoad or 0
    local soil = soilState.soil or 0

    -- Convert estimated power lost at the tyre/soil interface to an equivalent
    -- engine load multiplier.  A 50% loss therefore becomes 2.0, not an
    -- arbitrary near-stall multiplier of 10+.
    local loss = soil*(
        0.07
        + 0.34*sink*sinkScale
        + 0.20*slip*(0.35+0.65*sink)*slipScale
    )
    loss = loss+0.12*(soilState.severity or 0)
    if stuck then
        loss = loss+0.07*soil
    end
    loss = clamp(loss*math.max(0.05, tonumber(supportMul) or 1), 0, 0.72)

    local target = 1/math.max(0.28, 1-loss)
    local physicalMaximum = math.min(4.0, math.max(1, tonumber(configuredMax) or 4.0))
    return clamp(target, 1, physicalMaximum)
end

function Model:smoothMotorLoad(state, key, target, dtS, enterSec, exitSec)
    target = math.max(1, tonumber(target) or 1)
    local previous = math.max(1, tonumber(state[key]) or 1)
    local response = target > previous and enterSec or exitSec
    local value = expSmoothing(previous, target, dtS, response)
    if math.abs(value-target) < 0.002 then value = target end
    state[key] = value
    return value
end

function Model:getResistanceRatio(soilState, speedKph, base, fromSink, fromSlip, ratio, multiplier, maximum, localSlip)
    if soilState == nil then return 0 end

    local speedFactor = 0.58+0.42*smoothstep(1.5, 18, math.max(0, speedKph or 0))
    local sink = soilState.sinkLoad or 0
    local slip = localSlip ~= nil
        and smoothstep(0.04, 0.32, math.max(0, tonumber(localSlip) or 0))
        or (soilState.slipLoad or 0)
    local soil = soilState.soil or 0
    local resistance = soil*(
        (tonumber(base) or 0)*0.32
        + (tonumber(fromSink) or 0)*sink
        + (tonumber(fromSlip) or 0)*slip*(0.25+0.75*sink)
    )
    resistance = resistance*speedFactor*math.max(0, tonumber(multiplier) or 1)
    return clamp((tonumber(ratio) or 0)*resistance, 0, tonumber(maximum) or 2.5)
end

_G.__MudTerramechanicsModel = Model
