-- Author: U_BMP
-- Group: https://vk.com/biomodprod_utilit_fs
-- Date: 19.11.2025

MudPhysicsConsole = MudPhysicsConsole or {}
MudPhysicsConsole.name = (g_currentModName or "MudPhysics") .. ".Console"

local function mp()
    return _G["MudPhysics"]
end

local function clamp(v, a, b)
    if v < a then return a end
    if v > b then return b end
    return v
end

local function fmtNum(v)
    if v == nil then return "nil" end
    if type(v) == "number" then
        return string.format("%.4f", v)
    end
    return tostring(v)
end

local function fmtBool(v)
    return (v == true) and "true" or "false"
end

local function toLower(s)
    return (s ~= nil) and string.lower(tostring(s)) or nil
end

local function toNumber(s)
    if s == nil then return nil end
    return tonumber(s)
end

local function toBool(s)
    if s == nil then return nil end
    local v = toLower(s)
    if v == "on" or v == "true" or v == "1" or v == "yes" then return true end
    if v == "off" or v == "false" or v == "0" or v == "no" then return false end
    return nil
end

local function splitWords(str)
    local t = {}
    if str == nil then return t end
    for w in tostring(str):gmatch("%S+") do
        table.insert(t, w)
    end
    return t
end

local function setField(M, field, v, kind)
    if M == nil or field == nil then return false, "no mudphysics/field" end

    if kind == "bool" then
        local b = (type(v) == "boolean") and v or toBool(v)
        if b == nil then
            return false, "need bool (on/off/true/false/1/0)"
        end
        M[field] = b
        return true
    elseif kind == "num" then
        local n = (type(v) == "number") and v or toNumber(v)
        if n == nil then
            return false, "need number"
        end
        M[field] = n
        return true
    elseif kind == "int" then
        local n = (type(v) == "number") and v or toNumber(v)
        if n == nil then
            return false, "need integer"
        end
        M[field] = math.floor(n + 0.000001)
        return true
    elseif kind == "str" then
        if v == nil then return false, "need string" end
        M[field] = tostring(v)
        return true
    end

    local n = toNumber(v)
    if n ~= nil then
        M[field] = n
        return true
    end
    M[field] = v
    return true
end

-- =========================================================
-- SETTINGS
-- =========================================================

local SETTINGS = {
    core = {
        enabled = { field="enabled", kind="bool" },
        debug   = { field="debug",   kind="bool" },
    },

    wet = {
        threshold = { field="wetnessThreshold",     kind="num" },
        rainMin   = { field="rainForcesWetnessMin", kind="num" },
        rainScale = { field="rainScaleThreshold",   kind="num" },
    },

    layers = {
        min      = { field="mudMinToAffect",      kind="num" },
        dirtMult = { field="dirtLayerEffectMult", kind="num" },
    },

    sink = {
        ["in"]   = { field="sinkInSpeed",     kind="num" },
        out      = { field="sinkOutSpeed",    kind="num" },
        stuck    = { field="stuckThreshold",  kind="num" },
    },

    speed = {
        mud   = { field="maxSpeedMudKph",    kind="num" },
        stuck = { field="maxSpeedStuckKph",  kind="num" },
    },

    wheel = {
        enable   = { field="extraWheelSinkEnable",   kind="bool" },
        max      = { field="extraWheelSinkMax",      kind="num" },
        inSpd    = { field="extraWheelSinkSpeedIn",  kind="num" },
        outSpd   = { field="extraWheelSinkSpeedOut", kind="num" },
        relMax   = { field="extraWheelSinkMaxRel",   kind="num" },
        relBias  = { field="radiusMinFactorRelBias", kind="num" },
    },

    radius = {
        min   = { field="radiusMinFactor",    kind="num" },
        inSpd = { field="radiusSinkInSpeed",  kind="num" },
        outSpd= { field="radiusSinkOutSpeed", kind="num" },
    },

    freeze = {
        on     = { field="freezeRadiusWhenStopped", kind="bool" },
        speed  = { field="freezeStopSpeedKph",      kind="num" },
        slip   = { field="freezeStopSlip",          kind="num" },
        settle = { field="freezeSettleSeconds",     kind="num" },
        eps    = { field="freezeRadiusEps",         kind="num" },
    },

    relief = {
        chance   = { field="reliefChancePerSec",  kind="num" },
        strength = { field="reliefStrength",      kind="num" },
        brakeSec = { field="reliefBrakeSeconds",  kind="num" },
        brakeMul = { field="reliefBrakeMult",     kind="num" },
    },

    var = {
        strength = { field="mudVarStrength", kind="num" },
        cell     = { field="mudVarCell",     kind="num" },
        bobAmp   = { field="mudBobAmp",      kind="num" },
        bobFreq  = { field="mudBobFreq",     kind="num" },
    },

    motor = {
        on     = { field="motorLoadEnable",    kind="bool" },
        sink   = { field="motorLoadFromSink",  kind="num" },
        slip   = { field="motorLoadFromSlip",  kind="num" },
        max    = { field="motorLoadMaxMult",   kind="num" },
        minMud = { field="motorLoadMinEffMud", kind="num" },
    },

    brake = {
        on     = { field="wheelBrakeEnable",    kind="bool" },
        base   = { field="wheelBrakeBase",      kind="num" },
        sink   = { field="wheelBrakeFromSink",  kind="num" },
        slip   = { field="wheelBrakeFromSlip",  kind="num" },
        ratio  = { field="wheelBrakeRatio",     kind="num" },
        max    = { field="wheelBrakeMaxRatio",  kind="num" },
        minMud = { field="wheelBrakeMinEffMud", kind="num" },
    },

    dirt = {
        on       = { field="dirtEnable",        kind="bool" },
        minMud   = { field="dirtMinEffMud",     kind="num" },
        wetMin   = { field="dirtWetnessMin",    kind="num" },
        body     = { field="dirtBodyPerSec",    kind="num" },
        wheel    = { field="dirtWheelPerSec",   kind="num" },
        speedKph = { field="dirtSpeedBoostKph", kind="num" },
        max      = { field="dirtMax",           kind="num" },
    },

    fx = {
        on    = { field="particlesEnable", kind="bool" },
        emit  = { field="emitMultWetMud",  kind="num" },
        size  = { field="sizeMultWetMud",  kind="num" },
        speed = { field="speedMultWetMud", kind="num" },
    },

    fx2 = {
        on        = { field="extraParticlesEnable",     kind="bool" },
        onlyWet   = { field="extraParticleOnlyWetMud",  kind="bool" },
        offsetY   = { field="extraParticleOffsetY",     kind="num" },
        clipDist  = { field="extraParticleClipDist",    kind="num" },
        maxCount  = { field="extraParticleMaxCount",    kind="int" },
        minSpeed  = { field="extraParticleMinSpeedKph", kind="num" },

        burstOn     = { field="extraParticleBurstEnable",        kind="bool" },
        moveMax     = { field="extraParticleOffsetMoveMax",      kind="num" },
        moveFull    = { field="extraParticleOffsetMoveKphFull",  kind="num" },
        slipMin     = { field="extraParticleBurstSlipMin",       kind="num" },
        burstChance = { field="extraParticleBurstChancePerSec",  kind="num" },
        burstY      = { field="extraParticleBurstYOffset",       kind="num" },
        burstTmin   = { field="extraParticleBurstTimeMin",       kind="num" },
        burstTmax   = { field="extraParticleBurstTimeMax",       kind="num" },
        burstFwd    = { field="extraParticleBurstForwardMul",    kind="num" },
        burstUp     = { field="extraParticleBurstUpMul",         kind="num" },

        fullFwd   = { field="extraParticleOffsetMoveKphFullFwd",     kind="num" },
        fullRev   = { field="extraParticleOffsetMoveKphFullRev",     kind="num" },
        deadzone  = { field="extraParticleOffsetMoveDeadzoneKph",    kind="num" },
        revMul    = { field="extraParticleOffsetMoveReverseMul",     kind="num" },
        minFwd    = { field="extraParticleOffsetMoveMinFwd",         kind="num" },
        minRev    = { field="extraParticleOffsetMoveMinRev",         kind="num" },
        maxFwd    = { field="extraParticleOffsetMoveMaxFwd",         kind="num" },
        maxRev    = { field="extraParticleOffsetMoveMaxRev",         kind="num" },
    },

    pocket = {
        enterChance = { field="deepPocketChanceOnEnter",       kind="num" },
        durMin      = { field="deepPocketDurationMin",        kind="num" },
        durMax      = { field="deepPocketDurationMax",        kind="num" },
        boost       = { field="deepPocketTargetBoost",        kind="num" },
        inMul       = { field="deepPocketInSpeedMul",         kind="num" },
        biasEnter   = { field="deepPocketBiasByPatch",        kind="num" },

        inMudChance = { field="deepPocketChancePerSecInMud",  kind="num" },
        cdMin       = { field="deepPocketCooldownMin",        kind="num" },
        cdMax       = { field="deepPocketCooldownMax",        kind="num" },
        slipMin     = { field="deepPocketStruggleSlip",       kind="num" },
        sinkMin     = { field="deepPocketStruggleSink",       kind="num" },
        biasInMud   = { field="deepPocketBiasByPatchInMud",   kind="num" },
    },

    log = {
        enter      = { field="logEnterMud",           kind="bool" },
        enterCdMs  = { field="logEnterMudCooldownMs", kind="int" },
        intervalMs = { field="debugPrintIntervalMs",  kind="int" },
    },
}

local GROUP_ORDER = {
    "core","wet","layers","sink","speed","wheel","radius","freeze","relief","var","motor","brake","dirt","fx","fx2","pocket","log"
}

-- =========================================================
-- CASE-INSENSITIVE PARAM RESOLVER
-- =========================================================

local function resolveParamEntry(groupTable, paramAnyCase)
    if groupTable == nil or paramAnyCase == nil then
        return nil, nil
    end

    local e = groupTable[paramAnyCase]
    if e ~= nil then
        return e, paramAnyCase
    end

    local want = toLower(paramAnyCase)
    for k, v in pairs(groupTable) do
        if toLower(k) == want then
            return v, k
        end
    end

    return nil, nil
end

-- =========================================================
-- HELP / STATUS
-- =========================================================

local function printGroupList()
    print("------------------------------------------------------------")
    print("[MudPhysicsConsole] Groups:")
    for _, g in ipairs(GROUP_ORDER) do
        print("  " .. g)
    end
    print("Use: gsMud <group> list")
    print("------------------------------------------------------------")
end

local function printGroupParams(group)
    local g = SETTINGS[group]
    if g == nil then
        print(string.format("[MudPhysicsConsole] Unknown group '%s'", tostring(group)))
        printGroupList()
        return
    end
    print("------------------------------------------------------------")
    print(string.format("[MudPhysicsConsole] Params for group '%s':", group))
    local keys = {}
    for k, _ in pairs(g) do table.insert(keys, k) end
    table.sort(keys)
    for _, k in ipairs(keys) do
        local e = g[k]
        print(string.format("  %-12s -> %s (%s)", k, tostring(e.field), tostring(e.kind)))
    end
    print(string.format("Example: gsMud %s %s <value>", group, keys[1] or "param"))
    print("------------------------------------------------------------")
end

local function statusShort()
    local M = mp()
    if M == nil then
        print("[MudPhysicsConsole] MudPhysics not loaded yet")
        return
    end
    local n = (M.mudLayerIds ~= nil and #M.mudLayerIds or 0)
    print(string.format(
        "[MudPhysics] enabled=%s debug=%s mudLayers=%d | wetTh=%s rainMin=%s | mudMin=%s | sinkIn=%s sinkOut=%s stuckTh=%s | maxMud=%s maxStuck=%s | fx2.maxRev=%s | enterLog=%s",
        fmtBool(M.enabled), fmtBool(M.debug), n,
        fmtNum(M.wetnessThreshold), fmtNum(M.rainForcesWetnessMin),
        fmtNum(M.mudMinToAffect),
        fmtNum(M.sinkInSpeed), fmtNum(M.sinkOutSpeed), fmtNum(M.stuckThreshold),
        fmtNum(M.maxSpeedMudKph), fmtNum(M.maxSpeedStuckKph),
        fmtNum(M.extraParticleOffsetMoveMaxRev),
        fmtBool(M.logEnterMud)
    ))
end

local function helpText()
    print("------------------------------------------------------------")
    print("[MudPhysicsConsole] Commands:")
    print("  gsMud                         -> status (short)")
    print("  gsMud help                    -> help")
    print("  gsMud status                  -> status (short)")
    print("  gsMud groups                  -> list groups")
    print("  gsMud on|off                  -> enable/disable MudPhysics")
    print("  gsMud debug                   -> toggle MudPhysics.debug")
    print("  gsMud refresh                 -> rebuild mud layer cache")
    print("")
    print("  gsMud <group> list            -> list params in group")
    print("  gsMud <group> <param>         -> show current value")
    print("  gsMud <group> <param> <value> -> set value")
    print("")
    print("Example:")
    print("  gsMud fx2 maxRev 1.22   (also works: maxrev / MAXREV / MaxRev)")
    print("------------------------------------------------------------")
    printGroupList()
end

-- =========================================================
-- APPLY ROUTING
-- =========================================================

local function showValue(M, group, param)
    local g = SETTINGS[group]
    if g == nil then
        print(string.format("[MudPhysicsConsole] Unknown group '%s'", tostring(group)))
        printGroupList()
        return
    end

    local e, canonicalKey = resolveParamEntry(g, param)
    if e == nil then
        print(string.format("[MudPhysicsConsole] Unknown param '%s' in group '%s'", tostring(param), tostring(group)))
        printGroupParams(group)
        return
    end

    local v = M[e.field]
    if e.kind == "bool" then
        print(string.format("[MudPhysics] %s.%s = %s", group, canonicalKey, fmtBool(v)))
    else
        print(string.format("[MudPhysics] %s.%s = %s", group, canonicalKey, fmtNum(v)))
    end
end

local function applyValue(M, group, param, value)
    local g = SETTINGS[group]
    if g == nil then
        print(string.format("[MudPhysicsConsole] Unknown group '%s'", tostring(group)))
        printGroupList()
        return
    end

    local e, canonicalKey = resolveParamEntry(g, param)
    if e == nil then
        print(string.format("[MudPhysicsConsole] Unknown param '%s' in group '%s'", tostring(param), tostring(group)))
        printGroupParams(group)
        return
    end

    local ok, err = setField(M, e.field, value, e.kind)
    if not ok then
        print(string.format("[MudPhysicsConsole] Can't set %s.%s (%s): %s", group, canonicalKey, tostring(e.field), tostring(err)))
        return
    end

    local f = e.field
    if f == "wetnessThreshold" or f == "rainForcesWetnessMin" or f == "rainScaleThreshold"
        or f == "mudMinToAffect" or f == "dirtLayerEffectMult"
        or f == "stuckThreshold"
        or f == "reliefChancePerSec" or f == "reliefStrength"
        or f == "mudVarStrength" or f == "mudBobAmp"
        or f == "dirtMinEffMud" or f == "dirtWetnessMin" or f == "dirtMax"
        or f == "wheelBrakeRatio" or f == "wheelBrakeMaxRatio" or f == "wheelBrakeMinEffMud"
        or f == "motorLoadMinEffMud"
        or f == "extraWheelSinkMaxRel" then
        if type(M[f]) == "number" then
            M[f] = clamp(M[f], 0, 1)
        end
    end

    print(string.format("[MudPhysics] set %s.%s = %s", group, canonicalKey,
        (e.kind == "bool") and fmtBool(M[e.field]) or fmtNum(M[e.field])
    ))
end

-- =========================================================
-- MAIN CONSOLE HANDLER
-- =========================================================

function MudPhysicsConsole:consoleCommandMud(...)
    local M = mp()

    local args = {}
    local va = { ... }
    for i = 1, #va do
        local part = va[i]
        if part ~= nil then
            local words = splitWords(part)
            for _, w in ipairs(words) do
                table.insert(args, w)
            end
        end
    end

    if #args == 0 then
        statusShort()
        return
    end

    local a1 = toLower(args[1])

    if a1 == "help" or a1 == "?" then
        helpText(); return
    elseif a1 == "groups" then
        printGroupList(); return
    end

    if M == nil then
        print("[MudPhysicsConsole] MudPhysics not loaded yet")
        return
    end

    if a1 == "on" then
        M.enabled = true; statusShort(); return
    elseif a1 == "off" then
        M.enabled = false; statusShort(); return
    elseif a1 == "debug" then
        M.debug = not M.debug; statusShort(); return
    elseif a1 == "refresh" then
        if M.refreshMudLayerCache ~= nil then M:refreshMudLayerCache() end
        statusShort(); return
    elseif a1 == "status" then
        statusShort(); return
    end

    local group = a1
    local a2 = args[2]

    if a2 == nil then
        printGroupParams(group)
        return
    end

    if toLower(a2) == "list" then
        printGroupParams(group)
        return
    end

    local param = a2
    local value = args[3]

    if value == nil then
        showValue(M, group, param)
    else
        applyValue(M, group, param, value)
    end
end

-- =========================================================
-- Registration
-- =========================================================

function MudPhysicsConsole.registerCommands()
    if MudPhysicsConsole._registered then
        return
    end

    addConsoleCommand("gsMud", "MudPhysics live tuning (type 'gsMud help')", "consoleCommandMud", MudPhysicsConsole)
    MudPhysicsConsole._registered = true
    print("[MudPhysicsConsole] Registered console command: gsMud")
end

MudPhysicsConsole.registerCommands()
addModEventListener(MudPhysicsConsole)

function MudPhysicsConsole:loadMap()
    MudPhysicsConsole.registerCommands()
end


-- =========================================================
-- FieldGroundMudPhysics live tuning (groundProfiles + globals)
-- Console command: gsMudField
--
-- Examples:
--   gsMudField help
--   gsMudField list
--   gsMudField StubbleTillage              -> show all keys for that profile
--   gsMudField StubbleTillage mud          -> show one key
--   gsMudField StubbleTillage mud 0.50     -> set one key (number/bool/string)
--   gsMudField global radiusMinFactor 0.60 -> set global setting
-- =========================================================

local function fgGetProfiles()
    if FieldGroundMudPhysics ~= nil and FieldGroundMudPhysics.groundProfiles ~= nil then
        return FieldGroundMudPhysics.groundProfiles
    end
    return nil
end

local function fgFindProfileByName(name)
    local profiles = fgGetProfiles()
    if profiles == nil or name == nil then return nil, nil end

    local needle = string.lower(tostring(name))
    for gt, prof in pairs(profiles) do
        if prof ~= nil then
            local n = prof.name or ""
            if string.lower(tostring(n)) == needle then
                return prof, gt
            end
        end
    end
    return nil, nil
end

local function fgFormatAny(v)
    if v == nil then return "nil" end
    if type(v) == "boolean" then
        return v and "true" or "false"
    end
    if type(v) == "number" then
        return string.format("%.5g", v)
    end
    return tostring(v)
end

local function fgPrintProfile(prof, gt)
    if prof == nil then return end
    print(string.format("[gsMudField] Profile %s (groundType=%s):", tostring(prof.name or "?"), tostring(gt)))

    local keyOrder = {
        "mud", "wetMul", "sinkMul", "brakeMul", "motorMul",
        "radiusMinFactor", "dirtMul",
        "fxExtra", "permaStuck"
    }

    local printed = {}
    for _, k in ipairs(keyOrder) do
        if prof[k] ~= nil then
            printed[k] = true
            print(string.format("  %s = %s", k, fgFormatAny(prof[k])))
        end
    end

    for k, v in pairs(prof) do
        if k ~= "name" and not printed[k] then
            print(string.format("  %s = %s", tostring(k), fgFormatAny(v)))
        end
    end
end

local function fgSetAny(tbl, key, valueStr)
    if tbl == nil or key == nil then return end
    local cur = tbl[key]
    local newVal = nil

    if type(parseAnyValue) == "function" then
        newVal = parseAnyValue(valueStr, cur)
    else
        local n = tonumber(valueStr)
        if n ~= nil then newVal = n
        else
            local s = string.lower(tostring(valueStr))
            if s == "true" or s == "1" or s == "yes" or s == "on" then newVal = true
            elseif s == "false" or s == "0" or s == "no" or s == "off" then newVal = false
            else newVal = tostring(valueStr) end
        end
    end

    tbl[key] = newVal
    print(string.format("[gsMudField] set %s = %s (was %s)", tostring(key), fgFormatAny(newVal), fgFormatAny(cur)))
end

function MudPhysicsConsole:consoleCommandMudField(...)
    local raw = { ... }
    local args = {}
    for i = 1, #raw do
        if raw[i] ~= nil then
            for _, w in ipairs(splitWords(tostring(raw[i]))) do
                table.insert(args, w)
            end
        end
    end

    if #args == 0 or toLower(args[1]) == "help" or args[1] == "?" then
        return table.concat({
            "Usage:",
            "  gsMudField list",
            "  gsMudField <ProfileName>                 -> show profile",
            "  gsMudField <ProfileName> <key>           -> show key",
            "  gsMudField <ProfileName> <key> <value>   -> set key",
            "  gsMudField global <key> <value>          -> set global FieldGroundMudPhysics.<key>",
            "",
            "Example:",
            "  gsMudField Plowed radiusMinFactor 0.22",
        }, "\n")
    end

    local cmd1 = args[1]

    if toLower(cmd1) == "list" then
        local list = {}
        if FieldGroundMudPhysics ~= nil and FieldGroundMudPhysics.groundProfiles ~= nil then
            for gt, prof in pairs(FieldGroundMudPhysics.groundProfiles) do
                if type(prof) == "table" and prof.name ~= nil then
                    table.insert(list, string.format("%s (groundType=%s)", tostring(prof.name), tostring(gt)))
                end
            end
            table.sort(list)
        end
        return "[gsMudField] Profiles:\n  " .. table.concat(list, "\n  ")
    end

    if toLower(cmd1) == "global" then
        if FieldGroundMudPhysics == nil then
            return "[gsMudField] FieldGroundMudPhysics not loaded"
        end
        local key = args[2]
        local value = args[3]
        if key == nil then
            return "Usage: gsMudField global <key> <value>"
        end
        if value == nil then
            return string.format("[gsMudField] global %s = %s", tostring(key), fgFormatAny(FieldGroundMudPhysics[key]))
        end

        fgSetAny(FieldGroundMudPhysics, key, value)

        if FieldGroundMudPhysics.onSettingsChanged ~= nil then
            FieldGroundMudPhysics:onSettingsChanged()
        end

        return "ok"
    end

    local profileName = cmd1
    local key = args[2]
    local value = args[3]

    local prof, gt = fgFindProfileByName(profileName)
    if prof == nil then
        return string.format("[gsMudField] Profile '%s' not found. Use: gsMudField list", tostring(profileName))
    end

    if key ~= nil and value ~= nil then
        fgSetAny(prof, key, value)

        if FieldGroundMudPhysics ~= nil then
            if FieldGroundMudPhysics.onSettingsChanged ~= nil then
                FieldGroundMudPhysics:onSettingsChanged()
            elseif g_currentMission ~= nil and g_currentMission.vehicles ~= nil and FieldGroundMudPhysics.resetVehicleRuntime ~= nil then
                for _, v in pairs(g_currentMission.vehicles) do
                    if v ~= nil then
                        FieldGroundMudPhysics:resetVehicleRuntime(v, "gsMudField")
                    end
                end
            end
        end

        fgPrintProfile(prof, gt)
        print(string.format("[gsMudField] Set %s.%s = %s", tostring(prof.name or profileName), tostring(key), fgFormatAny(prof[key])))
        return "ok"
    end

    if key ~= nil and value == nil then
        return string.format("[gsMudField] %s.%s = %s", tostring(prof.name or profileName), tostring(key), fgFormatAny(prof[key]))
    end

    fgPrintProfile(prof, gt)
    return "ok"
end

local _oldRegisterCommands = MudPhysicsConsole.registerCommands
function MudPhysicsConsole.registerCommands()
    if _oldRegisterCommands ~= nil then _oldRegisterCommands() end
    if MudPhysicsConsole._registeredField then return end
    addConsoleCommand("gsMudField", "FieldGroundMudPhysics live tuning (type 'gsMudField help')", "consoleCommandMudField", MudPhysicsConsole)
    MudPhysicsConsole._registeredField = true
    print("[MudPhysicsConsole] Registered console command: gsMudField")
end
