-- =========================================================
-- Author: U_BMP
-- Date: 22.01.2026
-- =========================================================

BMP_SoilDryingTuner = {}
BMP_SoilDryingTuner.name = g_currentModName or "BMP_SoilDryingTuner"
BMP_SoilDryingTuner.path = g_currentModDirectory or ""

addModEventListener(BMP_SoilDryingTuner)

-- =========================================================
-- CONFIG
-- =========================================================
-- базовый множитель (всегда)
-- 1.0 = vanilla
-- 5.0 = сохнет в 5 раз медленнее
BMP_SoilDryingTuner.enabled = true
BMP_SoilDryingTuner.dryDurationMul = 5.0

-- как быстро намокает (обычно 1.0)
BMP_SoilDryingTuner.wetDurationMul = 2.5

-- -------------------------
-- Dynamic drying settings
-- -------------------------
BMP_SoilDryingTuner.enableDynamic = true

-- Температура (°C): ниже coldTemp -> сильнее замедляем, выше warmTemp -> почти без доп. замедления
BMP_SoilDryingTuner.coldTemp = 2
BMP_SoilDryingTuner.warmTemp = 18
-- во сколько раз дополнительно замедлять высыхание при coldTemp и ниже
BMP_SoilDryingTuner.coldDryExtraMul = 2.5

-- Облачность (0..1): оценка по облакам (приблизительно)
BMP_SoilDryingTuner.cloudyFrom = 0.35
BMP_SoilDryingTuner.cloudyTo   = 0.90
-- во сколько раз дополнительно замедлять высыхание при сильной облачности
BMP_SoilDryingTuner.cloudDryExtraMul = 2.0

-- "multiply" или "max"
BMP_SoilDryingTuner.combineMode = "multiply"

-- страховка
BMP_SoilDryingTuner.maxTotalExtraMul = 8.0

-- -------------------------
-- Optimization settings
-- -------------------------
-- Порог включения динамики: 1%
BMP_SoilDryingTuner.dynamicWetnessThreshold = 0.01

-- Когда wetness == 0, делаем редкий опрос, чтобы поймать момент намокания (мс)
BMP_SoilDryingTuner.idlePollIntervalMs = 1500

-- Когда wetness > 0, пересчитываем temp/cloud не чаще чем раз в N мс
BMP_SoilDryingTuner.activeRecalcIntervalMs = 400

-- Не обновлять duration если изменение меньше этого (мс)
BMP_SoilDryingTuner.minDurationDeltaMs = 50

-- Debug
BMP_SoilDryingTuner.debug = false

-- =========================================================
-- INTERNAL
-- =========================================================
BMP_SoilDryingTuner._applied = false
BMP_SoilDryingTuner._baseDry = nil
BMP_SoilDryingTuner._baseWet = nil
BMP_SoilDryingTuner._lastAppliedDry = nil

BMP_SoilDryingTuner._dbgTimer = 0
BMP_SoilDryingTuner._idlePollTimer = 0
BMP_SoilDryingTuner._activeRecalcTimer = 0
BMP_SoilDryingTuner._dynamicActive = true

BMP_SoilDryingTuner._cachedExtraMul = 1.0
BMP_SoilDryingTuner._cachedTemp = nil
BMP_SoilDryingTuner._cachedCloud = 0

function BMP_SoilDryingTuner:_captureBase(weather)
    if weather == nil then
        return false
    end

    if self._baseDry == nil then
        self._baseDry = tonumber(weather.__bmpSoilDryingBaseDry) or tonumber(weather.groundWetnessDryDuration) or 1800000
        weather.__bmpSoilDryingBaseDry = self._baseDry
    end

    if self._baseWet == nil then
        self._baseWet = tonumber(weather.__bmpSoilDryingBaseWet) or tonumber(weather.groundWetnessWetDuration) or 300000
        weather.__bmpSoilDryingBaseWet = self._baseWet
    end

    return true
end

function BMP_SoilDryingTuner:_restoreVanilla(weather)
    if weather == nil then
        return
    end

    self:_captureBase(weather)

    if self._baseDry ~= nil then
        weather.groundWetnessDryDuration = self._baseDry
    end
    if self._baseWet ~= nil then
        weather.groundWetnessWetDuration = self._baseWet
    end

    weather.__bmpSoilDryingPatched = false
    weather.__bmpSoilDryingDynamic = false

    self._lastAppliedDry = self._baseDry
    self._cachedExtraMul = 1.0
    self._cachedTemp = nil
    self._cachedCloud = 0
    self._dynamicActive = false
end

local function log(fmt, ...)
    if BMP_SoilDryingTuner.debug then
        print(string.format("[BMP_SoilDryingTuner] " .. fmt, ...))
    end
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 lerp(a, b, t)
    return a + (b - a) * t
end

local function invLerp(a, b, v)
    if a == b then
        return 0
    end
    return (v - a) / (b - a)
end

local function getWeather()
    if g_currentMission ~= nil
        and g_currentMission.environment ~= nil
        and g_currentMission.environment.weather ~= nil
    then
        return g_currentMission.environment.weather
    end
    return nil
end

local function getGroundWetness(weather)
    if weather ~= nil and weather.getGroundWetness ~= nil then
        return weather:getGroundWetness()
    end
    return nil
end

local function getTemperature(weather)
    if weather ~= nil and weather.getCurrentTemperature ~= nil then
        return weather:getCurrentTemperature()
    end
    return nil
end

-- Облачность 0..1 (proxy)
local function getCloudiness(weather)
    if weather == nil or weather.cloudUpdater == nil or weather.cloudUpdater.currentClouds == nil then
        return 0
    end

    local c = weather.cloudUpdater.currentClouds
    local precip = tonumber(c.precipitation) or 0
    local cirrus = tonumber(c.cirrusCoverage) or 0
    local dens   = tonumber(c.densityScale) or 0

    local densN = clamp(dens * 35.0, 0, 1)

    local cloudiness = math.max(precip, densN, clamp(cirrus * 1.5, 0, 1))
    return clamp(cloudiness, 0, 1)
end

function BMP_SoilDryingTuner:_applyOnce()
    if self._applied then
        return
    end

    local weather = getWeather()
    if weather == nil then
        return
    end

    if not self:_captureBase(weather) then
        return
    end

    if self.enabled ~= true then
        self:_restoreVanilla(weather)
        self._applied = true
        log("Disabled: using vanilla Weather durations.")
        return
    end

    if weather.__bmpSoilDryingPatched == true then
        self._applied = true
        log("Already patched (flag on Weather).")
        return
    end

    local newDry = math.max(1, self._baseDry * (self.dryDurationMul or 1.0))
    local newWet = math.max(1, self._baseWet * (self.wetDurationMul or 1.0))

    weather.groundWetnessDryDuration = newDry
    weather.groundWetnessWetDuration = newWet

    weather.__bmpSoilDryingPatched = true
    weather.__bmpSoilDryingDynamic = true

    self._lastAppliedDry = newDry
    self._applied = true

    log("Patched Weather: baseDry=%s baseWet=%s => dry=%s (mul=%.3f) wet=%s (mul=%.3f)",
        tostring(self._baseDry), tostring(self._baseWet),
        tostring(newDry), self.dryDurationMul or 1.0,
        tostring(newWet), self.wetDurationMul or 1.0
    )
end

function BMP_SoilDryingTuner:_computeExtraMul(weather)
    if not self.enableDynamic then
        return 1.0, nil, 0
    end

    local temp = getTemperature(weather)
    local cloud = getCloudiness(weather)

    local tempMul = 1.0
    if temp ~= nil then
        local t = clamp(invLerp(self.warmTemp, self.coldTemp, temp), 0, 1)
        tempMul = lerp(1.0, self.coldDryExtraMul or 2.0, t)
    end

    local cloudMul = 1.0
    do
        local t = clamp(invLerp(self.cloudyFrom, self.cloudyTo, cloud), 0, 1)
        cloudMul = lerp(1.0, self.cloudDryExtraMul or 1.5, t)
    end

    local extra
    if self.combineMode == "max" then
        extra = math.max(tempMul, cloudMul)
    else
        extra = tempMul * cloudMul
    end

    extra = clamp(extra, 1.0, self.maxTotalExtraMul or 8.0)
    return extra, temp, cloud
end

function BMP_SoilDryingTuner:_setDryDuration(weather, targetDry)
    if weather == nil then return end

    targetDry = math.max(1, targetDry)

    if self._lastAppliedDry == nil then
        weather.groundWetnessDryDuration = targetDry
        self._lastAppliedDry = targetDry
        return
    end

    local delta = math.abs(targetDry - self._lastAppliedDry)
    if delta < (self.minDurationDeltaMs or 50) then
        return
    end

    weather.groundWetnessDryDuration = targetDry
    self._lastAppliedDry = targetDry
end

function BMP_SoilDryingTuner:_applyBaseOnly(weather)
    if weather == nil or self._baseDry == nil then
        return
    end
    local baseScaled = self._baseDry * (self.dryDurationMul or 1.0)
    self:_setDryDuration(weather, baseScaled)
end

function BMP_SoilDryingTuner:onSettingsChanged()
    local weather = getWeather()
    if weather == nil then
        return
    end

    self:_captureBase(weather)

    if self.enabled ~= true then
        self:_restoreVanilla(weather)
        self._applied = true
        return
    end

    weather.__bmpSoilDryingPatched = true
    weather.__bmpSoilDryingDynamic = self.enableDynamic == true
    self._applied = true

    local newWet = math.max(1, self._baseWet * (self.wetDurationMul or 1.0))
    weather.groundWetnessWetDuration = newWet

    if self.enableDynamic then
        local extraMul, temp, cloud = self:_computeExtraMul(weather)
        self._cachedExtraMul = extraMul or 1.0
        self._cachedTemp = temp
        self._cachedCloud = cloud or 0
        self:_applyDynamicCached(weather, 0)
    else
        self._cachedExtraMul = 1.0
        self._cachedTemp = getTemperature(weather)
        self._cachedCloud = getCloudiness(weather)
        self:_applyBaseOnly(weather)
    end
end

function BMP_SoilDryingTuner:_applyDynamicCached(weather, dt)
    if weather == nil or self._baseDry == nil then
        return
    end

    local baseScaled = self._baseDry * (self.dryDurationMul or 1.0)
    local targetDry = baseScaled * (self._cachedExtraMul or 1.0)
    self:_setDryDuration(weather, targetDry)

    if self.debug then
        self._dbgTimer = (self._dbgTimer or 0) + (dt or 0)
        if self._dbgTimer > 5000 then
            self._dbgTimer = 0
            log("Dynamic(cached): temp=%sC cloud=%.2f extraMul=%.2f => dryDuration=%s",
                self._cachedTemp ~= nil and string.format("%.1f", self._cachedTemp) or "nil",
                self._cachedCloud or 0,
                self._cachedExtraMul or 1.0,
                tostring(targetDry)
            )
        end
    end
end

-- =========================================================
-- LIFECYCLE
-- =========================================================
function BMP_SoilDryingTuner:loadMap()
    self:_applyOnce()
end

function BMP_SoilDryingTuner:update(dt)
    if not self._applied then
        self:_applyOnce()
        return
    end

    local weather = getWeather()
    if weather == nil then
        return
    end

    if self.enabled ~= true then
        self:_restoreVanilla(weather)
        return
    end

    if weather.__bmpSoilDryingPatched ~= true then
        self._applied = false
        self:_applyOnce()
        return
    end

    if not self._dynamicActive then
        self._idlePollTimer = (self._idlePollTimer or 0) + (dt or 0)
        if self._idlePollTimer < (self.idlePollIntervalMs or 1500) then
            return
        end
        self._idlePollTimer = 0
    end

    local wetness = getGroundWetness(weather)
    if wetness == nil then
        self._dynamicActive = true
        wetness = 1
    end

    if wetness <= 0 then
        if self._dynamicActive then
            self._dynamicActive = false
            self._dbgTimer = 0
            log("Wetness=0 -> dynamic OFF (idle poll every %d ms)", self.idlePollIntervalMs or 1500)
        end
        self:_applyBaseOnly(weather)
        return
    end

    local threshold = self.dynamicWetnessThreshold or 0.01

    if wetness < threshold then
        self:_applyBaseOnly(weather)
        return
    end

    if not self._dynamicActive then
        self._dynamicActive = true
        self._dbgTimer = 0
        self._activeRecalcTimer = 0
        log("Wetness=%.3f -> dynamic ON", wetness)
    end

    self._activeRecalcTimer = (self._activeRecalcTimer or 0) + (dt or 0)
    if self._activeRecalcTimer >= (self.activeRecalcIntervalMs or 400) then
        self._activeRecalcTimer = 0
        local extraMul, temp, cloud = self:_computeExtraMul(weather)
        self._cachedExtraMul = extraMul or 1.0
        self._cachedTemp = temp
        self._cachedCloud = cloud or 0
    end

    self:_applyDynamicCached(weather, dt)
end

function BMP_SoilDryingTuner:deleteMap()
    self._applied = false
    self._baseDry = nil
    self._baseWet = nil
    self._lastAppliedDry = nil

    self._dbgTimer = 0
    self._idlePollTimer = 0
    self._activeRecalcTimer = 0
    self._dynamicActive = true

    self._cachedExtraMul = 1.0
    self._cachedTemp = nil
    self._cachedCloud = 0
end
