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

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

addModEventListener(MudWetnessMiniHUD)

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

MudWetnessMiniHUD.enabled = true
MudWetnessMiniHUD.debug   = false

MudWetnessMiniHUD.wetIconRel  = "gui/wet.dds"
MudWetnessMiniHUD.rainIconRel = "gui/rain.dds"

-- icon preset (1..3)
MudWetnessMiniHUD.iconPreset = 1
MudWetnessMiniHUD._appliedIconPreset = nil

MudWetnessMiniHUD.ICON_PRESETS = {
    [1] = { wet="gui/wet.dds",        rain="gui/rain.dds" },         -- standard
    [2] = { wet="gui/wet_rmg.dds",    rain="gui/rain_rmg.dds" },     -- RMG
    [3] = { wet="gui/wet_custom.dds", rain="gui/rain_custom.dds" }   -- custom
}

MudWetnessMiniHUD.iconSizePx     = 17
MudWetnessMiniHUD.textPadPx      = 5
MudWetnessMiniHUD.roundToPercent = true

-- прозрачность
MudWetnessMiniHUD.iconAlpha = 0.85
MudWetnessMiniHUD.textAlpha = 0.85

-- отступ относительно GameInfoDisplay
MudWetnessMiniHUD.gapAboveGameInfoPx = 6

-- ручные смещения (в пикселях)
-- offsetXPx: + -> влево,  - -> вправо
-- offsetYPx: + -> вверх,  - -> вниз
MudWetnessMiniHUD.offsetXPx = 15
MudWetnessMiniHUD.offsetYPx = 0
MudWetnessMiniHUD.dragging = false
MudWetnessMiniHUD.dragOffsetX = 0
MudWetnessMiniHUD.dragOffsetY = 0
MudWetnessMiniHUD.cursorModeActive = false
MudWetnessMiniHUD._actionEventId = nil
MudWetnessMiniHUD._inputLockedByCursor = false

-- фон
MudWetnessMiniHUD.bgEnable = true
MudWetnessMiniHUD.bgAlpha  = 0.45

-- внутренние отступы фона (в пикселях) вокруг всего блока
MudWetnessMiniHUD.bgPadXPx = 8
MudWetnessMiniHUD.bgPadYPx = 4

-- ширина закруглённых краёв (в пикселях), как у стандартного gameInfo (левый/правый край)
MudWetnessMiniHUD.bgCapWidthPx = 10

-- ---------------------------------------------------------
-- Таймер до следующих осадков (дождь/снег/град)
-- ---------------------------------------------------------
-- Должно включаться/выключаться через меню настроек:
MudWetnessMiniHUD.showNextPrecipTimer = true

-- дополнительное поведение:
MudWetnessMiniHUD.showNextPrecipTimerWhenRaining = false -- если true: показывать таймер даже если уже идут осадки
MudWetnessMiniHUD.nextPrecipTimerPrefix = " \226\134\146 " -- " → " в виде UTF-8 байтов

-- ключи l10n для единиц времени:
MudWetnessMiniHUD.showCurrentPrecipRemainingTimer = true
MudWetnessMiniHUD.currentPrecipRemainingPrefix = " \226\134\144 "
MudWetnessMiniHUD.l10nUnitHourKey = "mudhud_unit_h"
MudWetnessMiniHUD.l10nUnitMinKey  = "mudhud_unit_m"
MudWetnessMiniHUD.l10nPrecipRainKey = "mudhud_precip_rain"
MudWetnessMiniHUD.l10nPrecipSnowKey = "mudhud_precip_snow"
MudWetnessMiniHUD.l10nPrecipHailKey = "mudhud_precip_hail"
MudWetnessMiniHUD.l10nPrecipThunderKey = "mudhud_precip_thunder"

-- =========================================================
-- UTILS
-- =========================================================

local DAY_LENGTH_MS = 86400000

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 getTextSafe(key, fallback)
    if g_i18n ~= nil and key ~= nil and g_i18n.hasText ~= nil and g_i18n:hasText(key) then
        return g_i18n:getText(key)
    end
    return fallback or (key or "")
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 getWetAndRain()
    local weather = getWeather()
    if weather == nil then
        return 0, 0, 0, 0, 0
    end

    local wet = 0
    if weather.getGroundWetness ~= nil then
        wet = weather:getGroundWetness() or 0
    end

    local rain = 0
    if weather.getRainFallScale ~= nil then
        rain = weather:getRainFallScale() or 0
    end

    local snow = 0
    if weather.getSnowFallScale ~= nil then
        snow = weather:getSnowFallScale() or 0
    end

    local hail = 0
    if weather.getHailFallScale ~= nil then
        hail = weather:getHailFallScale() or 0
    end

    local precip = math.max(rain, snow, hail)

    return clamp(wet, 0, 1), clamp(precip, 0, 1), clamp(rain, 0, 1), clamp(snow, 0, 1), clamp(hail, 0, 1)
end

local function getUseLocalWetness()
    return FieldLocalWetness ~= nil and FieldLocalWetness.enabled == true
end

local function fmt01(v, asPercent)
    if asPercent then
        return string.format("%d%%", math.floor(v * 100 + 0.5))
    end
    return string.format("%.2f", v)
end

local function getControlledVehicle()
    -- TTD HUD uses g_localPlayer:getCurrentVehicle(). Use the same vehicle
    -- first; mission.hud.controlledVehicle may point at the currently selected
    -- implement and therefore sample different local-wetness cells.
    if g_localPlayer ~= nil and g_localPlayer.getCurrentVehicle ~= nil then
        local ok, vehicle = pcall(g_localPlayer.getCurrentVehicle, g_localPlayer)
        if ok and vehicle ~= nil then
            return vehicle
        end
    end
    if g_currentMission == nil then return nil end
    if g_currentMission.hud ~= nil and g_currentMission.hud.controlledVehicle ~= nil then
        return g_currentMission.hud.controlledVehicle
    end
    return g_currentMission.controlledVehicle
end

local function isHardWinterFrozen()
    return FieldGroundMudPhysics ~= nil
        and FieldGroundMudPhysics.isHardWinterFrozen ~= nil
        and FieldGroundMudPhysics:isHardWinterFrozen()
end

local function isValidNode(node)
    if node == nil or node == 0 then return false end
    if entityExists ~= nil then return entityExists(node) end
    return true
end

local function chooseWheelNode(wheel)
    if wheel == nil then return nil end

    local node = wheel.repr
    if isValidNode(node) then return node end
    node = wheel.driveNode
    if isValidNode(node) then return node end
    node = wheel.linkNode
    if isValidNode(node) then return node end
    if wheel.getFirstTireNode ~= nil then
        node = wheel:getFirstTireNode()
        if isValidNode(node) then return node end
    end
    node = wheel.wheelNode or wheel.node
    if isValidNode(node) then return node end

    return nil
end

local function getWheelContactWorldPos(wheel)
    local p = wheel ~= nil and wheel.physics or nil
    if p ~= nil and p.lastContactX ~= nil and p.lastContactY ~= nil and p.lastContactZ ~= nil then
        return p.lastContactX, p.lastContactY, p.lastContactZ
    end

    if FieldGroundMudPhysics ~= nil and FieldGroundMudPhysics.getWheelContactPos ~= nil then
        local x, y, z = FieldGroundMudPhysics:getWheelContactPos(wheel)
        if x ~= nil then return x, y, z end
    end

    local node = chooseWheelNode(wheel)
    if node ~= nil and getWorldTranslation ~= nil then
        return getWorldTranslation(node)
    end

    return nil, nil, nil
end

local function getVehicleRootNode(vehicle)
    if vehicle == nil then return nil end
    if vehicle.rootNode ~= nil and vehicle.rootNode ~= 0 then return vehicle.rootNode end
    if vehicle.components ~= nil and vehicle.components[1] ~= nil then
        local node = vehicle.components[1].node
        if node ~= nil and node ~= 0 then return node end
    end
    return nil
end

local function getPlayerXZ()
    if g_localPlayer == nil or getWorldTranslation == nil then return nil, nil end
    local node = g_localPlayer.rootNode or g_localPlayer.graphicsRootNode
    if node == nil or node == 0 then return nil, nil end
    local x, _, z = getWorldTranslation(node)
    return x, z
end

local function getProfileAt(x, z)
    if FieldGroundMudPhysics ~= nil and FieldGroundMudPhysics.getProfileAtWorldPos ~= nil and x ~= nil and z ~= nil then
        return FieldGroundMudPhysics:getProfileAtWorldPos(x, z)
    end
    return 0, nil, nil
end

local function getWheelProfile(wheel, x, z)
    local p = wheel ~= nil and wheel.physics or nil
    local densityType = p ~= nil and p.densityType or nil
    if FieldGroundMudPhysics ~= nil
        and densityType ~= nil
        and FieldGroundMudPhysics.getProfileByFieldGroundTypeValue ~= nil then
        local prof = FieldGroundMudPhysics:getProfileByFieldGroundTypeValue(densityType)
        if prof ~= nil then
            return clamp(prof.mud or 0, 0, 1), densityType, prof
        end
    end
    return getProfileAt(x, z)
end

local function getLocalWetnessHudValue(baseWetness)
    local fallback = clamp(baseWetness or 0, 0, 1)
    if not getUseLocalWetness() then
        return fallback
    end

    local vehicle = getControlledVehicle()
    if vehicle ~= nil and FieldLocalWetness.getVehicleWetness ~= nil then
        return clamp(
            FieldLocalWetness:getVehicleWetness(vehicle, fallback, nil),
            0,
            1
        )
    end

    -- On foot there is no wheel set to sample. Read the same persistent local
    -- moisture layer at the player's world position instead of falling back to
    -- the map-wide weather groundWetness value.
    local x, z = getPlayerXZ()
    if x ~= nil and z ~= nil then
        if FieldLocalWetness.getEffectiveWetnessAt ~= nil then
            local _, _, profile = getProfileAt(x, z)
            return clamp(
                FieldLocalWetness:getEffectiveWetnessAt(x, z, fallback, nil, profile),
                0,
                1
            )
        elseif FieldLocalWetness.getWetnessAt ~= nil then
            local wetness = FieldLocalWetness:getWetnessAt(x, z, nil)
            if wetness ~= nil then
                return clamp(wetness, 0, 1)
            end
        end
    end

    return fallback
end

local function getAuthoritativeWetnessHudValue(baseWetness)
    local fallback = clamp(baseWetness or 0, 0, 1)
    if isHardWinterFrozen() then return 0 end

    local compatibility = rawget(_G, "__MudTTDCompatibility")
    local vehicle = getControlledVehicle()
    if compatibility ~= nil and compatibility.active == true
        and vehicle ~= nil and vehicle.spec_tractorTerrainDynamics ~= nil
        and compatibility.getMudSystemWetnessPercent ~= nil then
        local wetness = compatibility:getMudSystemWetnessPercent(vehicle, 0)
        return clamp(wetness or fallback, 0, 1)
    end

    if getUseLocalWetness() then
        return getLocalWetnessHudValue(fallback)
    end
    return fallback
end

local function fmtMsToHM(ms, unitH, unitM)
    if ms == nil or ms == math.huge then
        return nil
    end

    ms = math.max(0, ms)
    local totalMinutes = math.floor(ms / 60000 + 0.5)
    local h = math.floor(totalMinutes / 60)
    local m = totalMinutes - h * 60

    unitH = unitH or "h"
    unitM = unitM or "m"

    if h > 0 then
        return string.format("%d%s %02d%s", h, unitH, m, unitM)
    end

    return string.format("%d%s", m, unitM)
end

local function isPrecipWeatherType(wt)
    if wt == nil or WeatherType == nil then
        return false
    end

    if WeatherType.RAIN ~= nil and wt == WeatherType.RAIN then
        return true
    end

    if WeatherType.SNOW ~= nil and wt == WeatherType.SNOW then
        return true
    end

    if WeatherType.HAIL ~= nil and wt == WeatherType.HAIL then
        return true
    end

    if WeatherType.THUNDER ~= nil and wt == WeatherType.THUNDER then
        return true
    end

    return false
end

local function getPrecipTypeText(wt)
    if wt == nil or WeatherType == nil then
        return nil
    end

    if WeatherType.RAIN ~= nil and wt == WeatherType.RAIN then
        return getTextSafe(MudWetnessMiniHUD.l10nPrecipRainKey, "Rain")
    end

    if WeatherType.SNOW ~= nil and wt == WeatherType.SNOW then
        return getTextSafe(MudWetnessMiniHUD.l10nPrecipSnowKey, "Snow")
    end

    if WeatherType.HAIL ~= nil and wt == WeatherType.HAIL then
        return getTextSafe(MudWetnessMiniHUD.l10nPrecipHailKey, "Hail")
    end

    if WeatherType.THUNDER ~= nil and wt == WeatherType.THUNDER then
        return getTextSafe(MudWetnessMiniHUD.l10nPrecipThunderKey, "Thunderstorm")
    end

    return nil
end

local function getTimeUntilNextPrecip()
    local weather = getWeather()
    if weather == nil or weather.forecastItems == nil or #weather.forecastItems == 0 then
        return math.huge, nil
    end

    local env = weather.owner
    if env == nil or env.currentMonotonicDay == nil or env.dayTime == nil then
        return math.huge, nil
    end

    local curDay  = env.currentMonotonicDay
    local curTime = env.dayTime

    for i = 1, #weather.forecastItems do
        local inst = weather.forecastItems[i]
        if inst ~= nil and inst.startDay ~= nil and inst.startDayTime ~= nil then
            local obj = nil
            if weather.getWeatherObjectByIndex ~= nil then
                obj = weather:getWeatherObjectByIndex(inst.season, inst.objectIndex)
            end

            local wt = obj ~= nil and obj.weatherType or nil
            if isPrecipWeatherType(wt) then
                local dt = (inst.startDay - curDay) * DAY_LENGTH_MS + (inst.startDayTime - curTime)
                if dt > 0 then
                    return dt, wt
                end
            end
        end
    end

    return math.huge, nil
end

local function getTimeUntilCurrentPrecipEnds()
    local weather = getWeather()
    if weather == nil or weather.forecastItems == nil or #weather.forecastItems == 0 then
        return math.huge, nil
    end

    local env = weather.owner
    if env == nil or env.currentMonotonicDay == nil or env.dayTime == nil or env.getDayAndDayTime == nil then
        return math.huge, nil
    end

    local inst = weather.forecastItems[1]
    if inst == nil or inst.startDay == nil or inst.startDayTime == nil or inst.duration == nil then
        return math.huge, nil
    end

    local obj = nil
    if weather.getWeatherObjectByIndex ~= nil then
        obj = weather:getWeatherObjectByIndex(inst.season, inst.objectIndex)
    end

    local wt = obj ~= nil and obj.weatherType or nil
    if not isPrecipWeatherType(wt) then
        return math.huge, nil
    end

    local endDay, endDayTime = env:getDayAndDayTime(inst.startDayTime + inst.duration, inst.startDay)
    if endDay == nil or endDayTime == nil then
        return math.huge, wt
    end

    local dt = (endDay - env.currentMonotonicDay) * DAY_LENGTH_MS + (endDayTime - env.dayTime)
    return math.max(0, dt), wt
end

-- =========================================================
-- LIFECYCLE
-- =========================================================

function MudWetnessMiniHUD:loadMap()
    self._appliedIconPreset = -1
    self.cursorModeActive = false
    self._actionEventId = nil
    self._inputLockedByCursor = false
    self:setIconPreset(self.iconPreset or 1, true, true)

    addConsoleCommand("gsMudWetMini", "MudWetnessMiniHUD on/off/toggle/debug", "consoleCommand", self)
end

function MudWetnessMiniHUD:deleteMap()
    removeConsoleCommand("gsMudWetMini")
    self:setCursorMode(false)
    self.cursorModeActive = false
    self._actionEventId = nil
    self._inputLockedByCursor = false
    self:_deleteOverlays()
end

function MudWetnessMiniHUD:consoleCommand(arg)
    if arg == "on" then
        self.enabled = true
    elseif arg == "off" then
        self.enabled = false
    elseif arg == "toggle" or arg == nil or arg == "" then
        self.enabled = not self.enabled
    elseif arg == "debug" then
        self.debug = not self.debug
    end

    local wet, precip, rain, snow, hail = getWetAndRain()
    return string.format(
        "MudWetnessMiniHUD enabled=%s debug=%s wet=%s precip=%s (rain=%s snow=%s hail=%s) timer=%s",
        tostring(self.enabled), tostring(self.debug),
        fmt01(wet, true), fmt01(precip, true),
        fmt01(rain, true), fmt01(snow, true), fmt01(hail, true),
        string.format("next=%s current=%s", tostring(self.showNextPrecipTimer), tostring(self.showCurrentPrecipRemainingTimer))
    )
end

-- =========================================================
-- OVERLAYS
-- =========================================================

function MudWetnessMiniHUD:_deleteOverlays()
    if self.wetIcon ~= nil then self.wetIcon:delete(); self.wetIcon = nil end
    if self.rainIcon ~= nil then self.rainIcon:delete(); self.rainIcon = nil end

    if self.bgLeft ~= nil then self.bgLeft:delete(); self.bgLeft = nil end
    if self.bgMiddle ~= nil then self.bgMiddle:delete(); self.bgMiddle = nil end
    if self.bgRight ~= nil then self.bgRight:delete(); self.bgRight = nil end

    if self.bgOverlay ~= nil then self.bgOverlay:delete(); self.bgOverlay = nil end
end

function MudWetnessMiniHUD:_rebuildOverlays()
    self:_deleteOverlays()

    self.wetIcon  = Overlay.new(self.wetIconPath,  0, 0, 0.01, 0.01)
    self.rainIcon = Overlay.new(self.rainIconPath, 0, 0, 0.01, 0.01)

    if self.bgEnable then
        if g_overlayManager ~= nil and g_overlayManager.createOverlay ~= nil then
            self.bgLeft   = g_overlayManager:createOverlay("gui.gameInfo_left",   0, 0, 0, 0)
            self.bgMiddle = g_overlayManager:createOverlay("gui.gameInfo_middle", 0, 0, 0, 0)
            self.bgRight  = g_overlayManager:createOverlay("gui.gameInfo_right",  0, 0, 0, 0)

            local a = self.bgAlpha or 0.45
            self.bgLeft:setColor(0, 0, 0, a)
            self.bgMiddle:setColor(0, 0, 0, a)
            self.bgRight:setColor(0, 0, 0, a)
        else
            self.bgOverlay = Overlay.new(nil, 0, 0, 0.01, 0.01)
            self.bgOverlay:setColor(0, 0, 0, self.bgAlpha or 0.45)
        end
    end
end

-- =========================================================
-- ICON PRESET
-- =========================================================

function MudWetnessMiniHUD:setIconPreset(presetIndex, silent, force)
    presetIndex = math.floor(tonumber(presetIndex) or 1)
    if presetIndex < 1 then presetIndex = 1 end
    if presetIndex > 3 then presetIndex = 3 end

    local preset = self.ICON_PRESETS[presetIndex] or self.ICON_PRESETS[1]

    if not force then
        if self.iconPreset == presetIndex
            and self.wetIconRel  == preset.wet
            and self.rainIconRel == preset.rain
            and self.wetIconPath ~= nil
            and self.rainIconPath ~= nil then
            return
        end
    end

    self.iconPreset = presetIndex

    self.wetIconRel  = preset.wet
    self.rainIconRel = preset.rain

    self.wetIconPath  = Utils.getFilename(self.wetIconRel,  self.path)
    self.rainIconPath = Utils.getFilename(self.rainIconRel, self.path)

    self:_rebuildOverlays()

    if self.debug and not silent then
        print(string.format("[MudWetnessMiniHUD] iconPreset=%d wet=%s rain=%s",
            presetIndex, tostring(self.wetIconRel), tostring(self.rainIconRel)))
    end
end

function MudWetnessMiniHUD:onSettingsChanged()
    self:setIconPreset(self.iconPreset, true, true)
end

function MudWetnessMiniHUD:update(dt)
    if self.dragging and (g_inputBinding == nil or g_inputBinding.getShowMouseCursor == nil or not g_inputBinding:getShowMouseCursor()) then
        self.dragging = false
    end

    if self.cursorModeActive and (g_inputBinding == nil or g_inputBinding.getShowMouseCursor == nil or not g_inputBinding:getShowMouseCursor()) then
        self:setCursorMode(false)
    end
end

function MudWetnessMiniHUD:getToggleMouseText()
    if self.cursorModeActive then
        return getTextSafe("input_MUDSYS_TOGGLE_MOUSE_MODE_OFF", "Hide HUD mouse cursor")
    end

    return getTextSafe("input_MUDSYS_TOGGLE_MOUSE_MODE", "Show HUD mouse cursor")
end

function MudWetnessMiniHUD:updateActionEvents()
    if self._actionEventId ~= nil and g_inputBinding ~= nil then
        g_inputBinding:setActionEventText(self._actionEventId, self:getToggleMouseText())
        g_inputBinding:setActionEventActive(self._actionEventId, true)
    end
end

function MudWetnessMiniHUD:registerActionEvents()
    if g_inputBinding == nil or self._actionEventId ~= nil then
        return
    end

    local _, actionEventId = g_inputBinding:registerActionEvent(InputAction.MUDSYS_TOGGLE_MOUSE_MODE, self, self.onPressToggleMouseMode, false, true, false, true)
    self._actionEventId = actionEventId
    self:updateActionEvents()
end

function MudWetnessMiniHUD:onPressToggleMouseMode()
    self:setCursorMode(not self.cursorModeActive)
end

function MudWetnessMiniHUD:setCursorMode(state)
    if g_inputBinding == nil or g_inputBinding.setShowMouseCursor == nil then
        return
    end

    state = state == true
    self.cursorModeActive = state

    local player = g_localPlayer
    local inputComponent = player ~= nil and player.inputComponent or nil

    if state then
        if inputComponent ~= nil and inputComponent.lock ~= nil and inputComponent.locked ~= true then
            inputComponent:lock()
            self._inputLockedByCursor = true
        else
            self._inputLockedByCursor = false
        end

        g_inputBinding:setShowMouseCursor(true, true)
    else
        self.dragging = false

        if self._inputLockedByCursor and inputComponent ~= nil and inputComponent.unlock ~= nil then
            inputComponent:unlock()
        end

        self._inputLockedByCursor = false
        g_inputBinding:setShowMouseCursor(false, true)
    end

    self:updateActionEvents()
end

function MudWetnessMiniHUD:getCanDragWithMouse()
    if not self.enabled or g_currentMission == nil then
        return false
    end

    if g_inputBinding == nil or g_inputBinding.getShowMouseCursor == nil then
        return false
    end

    return g_inputBinding:getShowMouseCursor() == true
end

function MudWetnessMiniHUD:_getLayoutMetrics()
    if not self.enabled or g_currentMission == nil or g_currentMission.hud == nil then
        return nil
    end

    local hud = g_currentMission.hud
    local gid = hud.gameInfoDisplay
    if gid == nil or gid.getPosition == nil then
        return nil
    end

    local posX, posY = gid:getPosition()

    local sW = gid.scalePixelToScreenWidth ~= nil
        and function(px) return gid:scalePixelToScreenWidth(px) end
        or function(px) return px / 1920 end

    local sH = gid.scalePixelToScreenHeight ~= nil
        and function(px) return gid:scalePixelToScreenHeight(px) end
        or function(px) return px / 1080 end

    local wet, precip = getWetAndRain()
    wet = getAuthoritativeWetnessHudValue(wet)
    local wetText = fmt01(wet, self.roundToPercent)
    local precipText = fmt01(precip, self.roundToPercent)
    local isPrecipNow = precip > 0.05

    if self.showCurrentPrecipRemainingTimer and isPrecipNow then
        local tMs, currentWt = getTimeUntilCurrentPrecipEnds()
        local unitH = getTextSafe(self.l10nUnitHourKey, "h")
        local unitM = getTextSafe(self.l10nUnitMinKey,  "m")
        local tStr = fmtMsToHM(tMs, unitH, unitM)
        if tStr ~= nil then
            local pfx = self.currentPrecipRemainingPrefix or " \226\134\144 "
            local precipTypeText = getPrecipTypeText(currentWt)
            if precipTypeText ~= nil and precipTypeText ~= "" then
                precipText = precipText .. pfx .. tStr .. " (" .. precipTypeText .. ")"
            else
                precipText = precipText .. pfx .. tStr
            end
        end
    end

    if self.showNextPrecipTimer then
        if (not isPrecipNow) or self.showNextPrecipTimerWhenRaining then
            local tMs, nextWt = getTimeUntilNextPrecip()
            local unitH = getTextSafe(self.l10nUnitHourKey, "h")
            local unitM = getTextSafe(self.l10nUnitMinKey,  "m")
            local tStr = fmtMsToHM(tMs, unitH, unitM)
            if tStr ~= nil then
                local pfx = self.nextPrecipTimerPrefix or " \226\134\146 "
                local precipTypeText = getPrecipTypeText(nextWt)
                if precipTypeText ~= nil and precipTypeText ~= "" then
                    precipText = precipText .. pfx .. tStr .. " (" .. precipTypeText .. ")"
                else
                    precipText = precipText .. pfx .. tStr
                end
            end
        end
    end

    local iconW = sW(self.iconSizePx)
    local iconH = sH(self.iconSizePx)
    local padX = sW(self.textPadPx)
    local gapX = sW(10)
    local gapY = sH(self.gapAboveGameInfoPx)
    local offX = sW(self.offsetXPx)
    local offY = sH(self.offsetYPx)
    local textSize = gid.clockTextSize or gid.moneyTextSize or sH(17)
    local wetW  = iconW + padX + getTextWidth(textSize, wetText)
    local rainW = iconW + padX + getTextWidth(textSize, precipText)
    local blockW = wetW + gapX + rainW
    local blockH = iconH
    local xRight = posX - offX
    local x0 = xRight - blockW
    local y0 = (posY + gapY) + offY

    return {
        gid = gid,
        posX = posX,
        posY = posY,
        sW = sW,
        sH = sH,
        sW1 = sW(1),
        sH1 = sH(1),
        gapY = gapY,
        x0 = x0,
        y0 = y0,
        blockW = blockW,
        blockH = blockH
    }
end

function MudWetnessMiniHUD:_isMouseOverHud(posX, posY, layout)
    return posX >= layout.x0 and posX <= layout.x0 + layout.blockW
        and posY >= layout.y0 and posY <= layout.y0 + layout.blockH
end

function MudWetnessMiniHUD:_applyDraggedPosition(newX0, newY0, layout, saveNow)
    local pxPerUnitX = layout.sW1 ~= 0 and layout.sW1 or (1 / 1920)
    local pxPerUnitY = layout.sH1 ~= 0 and layout.sH1 or (1 / 1080)

    local newOffsetXPx = math.floor(((layout.posX - (newX0 + layout.blockW)) / pxPerUnitX) + 0.5)
    local newOffsetYPx = math.floor(((newY0 - (layout.posY + layout.gapY)) / pxPerUnitY) + 0.5)

    self.offsetXPx = clamp(newOffsetXPx, -2600, 2600)
    self.offsetYPx = clamp(newOffsetYPx, -2400, 20)

    if saveNow and MudSystemSettings ~= nil and type(MudSystemSettings.writeSettings) == "function" then
        MudSystemSettings:writeSettings()
    end
end

function MudWetnessMiniHUD:mouseEvent(posX, posY, isDown, isUp, button)
    local layout = self:_getLayoutMetrics()
    if layout == nil then
        return
    end

    local canDrag = self:getCanDragWithMouse()

    if isDown then
        if button ~= Input.MOUSE_BUTTON_LEFT then
            return
        end

        if not self:_isMouseOverHud(posX, posY, layout) then
            return
        end

        if not canDrag then
            return
        end

        self.dragging = true
        self.dragOffsetX = posX - layout.x0
        self.dragOffsetY = posY - layout.y0
        return
    end

    if isUp then
        if self.dragging then
            self:_applyDraggedPosition(posX - self.dragOffsetX, posY - self.dragOffsetY, layout, true)
        end
        self.dragging = false
        return
    end

    if self.dragging then
        if not canDrag then
            self.dragging = false
            return
        end
        self:_applyDraggedPosition(posX - self.dragOffsetX, posY - self.dragOffsetY, layout, false)
    end
end

-- =========================================================
-- DRAW
-- =========================================================

function MudWetnessMiniHUD:draw()
    if not self.enabled or g_currentMission == nil or g_currentMission.hud == nil then
        return
    end

    local hud = g_currentMission.hud

    if hud.getIsVisible ~= nil then
        if not hud:getIsVisible() then return end
    elseif hud.isVisible ~= nil then
        if not hud.isVisible then return end
    end

    local gid = hud.gameInfoDisplay
    if gid == nil or gid.getPosition == nil then
        return
    end

    if self._appliedIconPreset ~= self.iconPreset then
        self:setIconPreset(self.iconPreset or 1, true, true)
        self._appliedIconPreset = self.iconPreset
    end

    if gid.getVisible ~= nil and not gid:getVisible() then
        return
    end

    local layout = self:_getLayoutMetrics()
    if layout == nil then
        return
    end

    local posX, posY = layout.posX, layout.posY

    local sW = layout.sW
    local sH = layout.sH

    local iconW = sW(self.iconSizePx)
    local iconH = sH(self.iconSizePx)

    local padX = sW(self.textPadPx)
    local gapX = sW(10)
    local gapY = sH(self.gapAboveGameInfoPx)

    local offX = sW(self.offsetXPx)
    local offY = sH(self.offsetYPx)

    local textSize = gid.clockTextSize or gid.moneyTextSize or sH(17)

    local wet, precip, rainScale, snowScale, hailScale = getWetAndRain()
    wet = getAuthoritativeWetnessHudValue(wet)

    local wetText = fmt01(wet, self.roundToPercent)
    local precipText = fmt01(precip, self.roundToPercent)
    local isPrecipNow = precip > 0.05

    if self.showCurrentPrecipRemainingTimer and isPrecipNow then
        local tMs, currentWt = getTimeUntilCurrentPrecipEnds()
        local unitH = getTextSafe(self.l10nUnitHourKey, "h")
        local unitM = getTextSafe(self.l10nUnitMinKey,  "m")

        local tStr = fmtMsToHM(tMs, unitH, unitM)
        if tStr ~= nil then
            local pfx = self.currentPrecipRemainingPrefix or " \226\134\144 "
            local precipTypeText = getPrecipTypeText(currentWt)

            if precipTypeText ~= nil and precipTypeText ~= "" then
                precipText = precipText .. pfx .. tStr .. " (" .. precipTypeText .. ")"
            else
                precipText = precipText .. pfx .. tStr
            end
        end
    end

    if self.showNextPrecipTimer then
        if (not isPrecipNow) or self.showNextPrecipTimerWhenRaining then
            local tMs, nextWt = getTimeUntilNextPrecip()

            local unitH = getTextSafe(self.l10nUnitHourKey, "h")
            local unitM = getTextSafe(self.l10nUnitMinKey,  "m")

            local tStr = fmtMsToHM(tMs, unitH, unitM)
            if tStr ~= nil then
                local pfx = self.nextPrecipTimerPrefix or " \226\134\146 "
                local precipTypeText = getPrecipTypeText(nextWt)

                if precipTypeText ~= nil and precipTypeText ~= "" then
                    precipText = precipText .. pfx .. tStr .. " (" .. precipTypeText .. ")"
                else
                    precipText = precipText .. pfx .. tStr
                end
            end
        end
    end

    setTextAlignment(RenderText.ALIGN_LEFT)
    setTextBold(true)
    setTextColor(1, 1, 1, self.textAlpha)

    local wetW  = iconW + padX + getTextWidth(textSize, wetText)
    local rainW = iconW + padX + getTextWidth(textSize, precipText)

    local blockW = wetW + gapX + rainW
    local blockH = iconH

    local x0 = layout.x0
    local y0 = layout.y0

    -- =====================================================
    -- Background plate
    -- =====================================================
    if self.bgEnable then
        local bgPadX = sW(self.bgPadXPx or 8)
        local bgPadY = sH(self.bgPadYPx or 4)

        local bgX = x0 - bgPadX
        local bgY = y0 - bgPadY
        local bgW = blockW + bgPadX * 2
        local bgH = blockH + bgPadY * 2

        if self.bgLeft ~= nil and self.bgMiddle ~= nil and self.bgRight ~= nil then
            local capW = sW(self.bgCapWidthPx or 10)
            local leftW  = capW
            local rightW = capW
            local midW = math.max(0, bgW - leftW - rightW)

            local a = self.bgAlpha or 0.45
            self.bgLeft:setColor(0, 0, 0, a)
            self.bgMiddle:setColor(0, 0, 0, a)
            self.bgRight:setColor(0, 0, 0, a)

            self.bgLeft:setPosition(bgX, bgY)
            self.bgLeft:setDimension(leftW, bgH)
            self.bgLeft:render()

            self.bgMiddle:setPosition(bgX + leftW, bgY)
            self.bgMiddle:setDimension(midW, bgH)
            self.bgMiddle:render()

            self.bgRight:setPosition(bgX + leftW + midW, bgY)
            self.bgRight:setDimension(rightW, bgH)
            self.bgRight:render()
        elseif self.bgOverlay ~= nil then
            self.bgOverlay:setPosition(bgX, bgY)
            self.bgOverlay:setDimension(bgW, bgH)
            self.bgOverlay:setColor(0, 0, 0, self.bgAlpha or 0.45)
            self.bgOverlay:render()
        end
    end

    -- icons + text
    self.wetIcon:setPosition(x0, y0)
    self.wetIcon:setDimension(iconW, iconH)
    self.wetIcon:setColor(1, 1, 1, self.iconAlpha)
    self.wetIcon:render()

    renderText(x0 + iconW + padX, y0 + sH(2), textSize, wetText)

    local rx = x0 + wetW + gapX

    self.rainIcon:setPosition(rx, y0)
    self.rainIcon:setDimension(iconW, iconH)
    self.rainIcon:setColor(1, 1, 1, self.iconAlpha)
    self.rainIcon:render()

    renderText(rx + iconW + padX, y0 + sH(2), textSize, precipText)

    if self.debug then
        setTextBold(false)
        renderText(
            x0,
            y0 - sH(12),
            sH(12),
            string.format(
                "wet=%.2f precip=%.2f rain=%.2f snow=%.2f hail=%.2f timer=%s",
                wet, precip, rainScale, snowScale, hailScale,
                string.format("next=%s current=%s", tostring(self.showNextPrecipTimer), tostring(self.showCurrentPrecipRemainingTimer))
            )
        )
        setTextBold(true)
    end

    setTextBold(false)
end

local function MudWetnessMiniHUD_onPlayerLoaded(player)
    if player ~= nil and player.isOwner and g_inputBinding ~= nil then
        g_inputBinding:beginActionEventsModification(PlayerInputComponent.INPUT_CONTEXT_NAME)
        MudWetnessMiniHUD:registerActionEvents()
        g_inputBinding:endActionEventsModification()
    end
end
Player.load = Utils.appendedFunction(Player.load, MudWetnessMiniHUD_onPlayerLoaded)
