-- SelfFixAndRepaint.lua
-- Mod principale: rileva se il giocatore tiene in mano il trapano o lo spray
-- Autore: NYKK3

-- ─────────────────────────────────────────────────────────────────────────────
-- Evento di rete: server → client(i) — imposta la soglia
-- ─────────────────────────────────────────────────────────────────────────────
SFARThresholdEvent = {}
local SFARThresholdEvent_mt = Class(SFARThresholdEvent, Event)
InitEventClass(SFARThresholdEvent, "SFARThresholdEvent")

function SFARThresholdEvent.emptyNew()
    return Event.new(SFARThresholdEvent_mt)
end

function SFARThresholdEvent.new(threshold)
    local self = SFARThresholdEvent.emptyNew()
    self.threshold = threshold
    return self
end

function SFARThresholdEvent:writeStream(streamId, connection)
    streamWriteFloat32(streamId, self.threshold)
end

function SFARThresholdEvent:readStream(streamId, connection)
    self.threshold = streamReadFloat32(streamId)
    self:run(connection)
end

function SFARThresholdEvent:run(connection)
    -- Ricevuto da remoto: applica senza salvare il file locale
    SelfFixAndRepaint.applyThreshold(self.threshold, true)
    -- Se il server riceve la modifica da un client, la ridistribuisce a tutti gli altri
    if g_server ~= nil and not connection:getIsServer() then
        g_server:broadcastEvent(SFARThresholdEvent.new(self.threshold), nil, connection)
    end
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Evento di rete: server → client — conferma danno reale dopo riparazione
-- Evita che il client mostri valori stale quando la replicazione FS25 è lenta.
-- ─────────────────────────────────────────────────────────────────────────────
SFARRepairAckEvent = {}
local SFARRepairAckEvent_mt = Class(SFARRepairAckEvent, Event)
InitEventClass(SFARRepairAckEvent, "SFARRepairAckEvent")

function SFARRepairAckEvent.emptyNew()
    return Event.new(SFARRepairAckEvent_mt)
end

function SFARRepairAckEvent.new(vehicle, newDamage)
    local self = SFARRepairAckEvent.emptyNew()
    self.vehicle   = vehicle
    self.newDamage = newDamage
    return self
end

function SFARRepairAckEvent:writeStream(streamId, connection)
    NetworkUtil.writeNodeObjectId(streamId, NetworkUtil.getObjectId(self.vehicle))
    streamWriteFloat32(streamId, self.newDamage)
end

function SFARRepairAckEvent:readStream(streamId, connection)
    self.vehicle   = NetworkUtil.getObject(NetworkUtil.readNodeObjectId(streamId))
    self.newDamage = streamReadFloat32(streamId)
    self:run(connection)
end

function SFARRepairAckEvent:run(connection)
    if self.vehicle == nil then return end
    -- Conserva il valore anche quando è 0: se lo cancelliamo il fallback stale
    -- di getDamageAmount (ancora 0.02) fa ripartire il loop di riparazione.
    SelfFixAndRepaint.serverDamageCache[self.vehicle] = self.newDamage
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Evento di rete: server → client — conferma usura reale dopo verniciatura
-- ─────────────────────────────────────────────────────────────────────────────
SFARRepaintAckEvent = {}
local SFARRepaintAckEvent_mt = Class(SFARRepaintAckEvent, Event)
InitEventClass(SFARRepaintAckEvent, "SFARRepaintAckEvent")

function SFARRepaintAckEvent.emptyNew()
    return Event.new(SFARRepaintAckEvent_mt)
end

function SFARRepaintAckEvent.new(vehicle, newWear)
    local self = SFARRepaintAckEvent.emptyNew()
    self.vehicle = vehicle
    self.newWear  = newWear
    return self
end

function SFARRepaintAckEvent:writeStream(streamId, connection)
    NetworkUtil.writeNodeObjectId(streamId, NetworkUtil.getObjectId(self.vehicle))
    streamWriteFloat32(streamId, self.newWear)
end

function SFARRepaintAckEvent:readStream(streamId, connection)
    self.vehicle = NetworkUtil.getObject(NetworkUtil.readNodeObjectId(streamId))
    self.newWear  = streamReadFloat32(streamId)
    self:run(connection)
end

function SFARRepaintAckEvent:run(connection)
    if self.vehicle == nil then return end
    SelfFixAndRepaint.serverWearCache[self.vehicle] = self.newWear
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Evento di rete: client → server — riparazione veicolo
-- Il client manda il veicolo da riparare, la quantità e il costo;
-- il server applica addDamageAmount e addMoney in modo autoritativo.
-- ─────────────────────────────────────────────────────────────────────────────
SFARRepairEvent = {}
local SFARRepairEvent_mt = Class(SFARRepairEvent, Event)
InitEventClass(SFARRepairEvent, "SFARRepairEvent")

function SFARRepairEvent.emptyNew()
    return Event.new(SFARRepairEvent_mt)
end

function SFARRepairEvent.new(vehicle, repairAmount, frameCost, farmId)
    local self = SFARRepairEvent.emptyNew()
    self.vehicle      = vehicle
    self.repairAmount = repairAmount
    self.frameCost    = frameCost
    self.farmId       = farmId
    return self
end

function SFARRepairEvent:writeStream(streamId, connection)
    NetworkUtil.writeNodeObjectId(streamId, NetworkUtil.getObjectId(self.vehicle))
    streamWriteFloat32(streamId, self.repairAmount)
    streamWriteFloat32(streamId, self.frameCost)
    streamWriteInt32(streamId,   self.farmId)
end

function SFARRepairEvent:readStream(streamId, connection)
    self.vehicle      = NetworkUtil.getObject(NetworkUtil.readNodeObjectId(streamId))
    self.repairAmount = streamReadFloat32(streamId)
    self.frameCost    = streamReadFloat32(streamId)
    self.farmId       = streamReadInt32(streamId)
    self:run(connection)
end

function SFARRepairEvent:run(connection)
    if g_server == nil then return end
    if self.vehicle == nil then return end
    local farm = g_farmManager:getFarmById(self.farmId)
    if farm == nil or farm.money < self.frameCost then return end
    g_currentMission:addMoney(-self.frameCost, self.farmId, MoneyType.VEHICLE_REPAIR, false, true)
    self.vehicle:addDamageAmount(-self.repairAmount)
    -- Rimanda al client il valore autoritativo: evita che il client rimanga bloccato
    -- su un valore stale se la replicazione FS25 non arriva in tempo.
    if not connection:getIsServer() then
        connection:sendEvent(SFARRepairAckEvent.new(self.vehicle, self.vehicle:getDamageAmount()))
    end
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Evento di rete: client → server — riverniciatura veicolo
-- Il server applica addWearAmount in modo autoritativo.
-- ─────────────────────────────────────────────────────────────────────────────
SFARRepaintEvent = {}
local SFARRepaintEvent_mt = Class(SFARRepaintEvent, Event)
InitEventClass(SFARRepaintEvent, "SFARRepaintEvent")

function SFARRepaintEvent.emptyNew()
    return Event.new(SFARRepaintEvent_mt)
end

function SFARRepaintEvent.new(vehicle, repaintAmount)
    local self = SFARRepaintEvent.emptyNew()
    self.vehicle       = vehicle
    self.repaintAmount = repaintAmount
    return self
end

function SFARRepaintEvent:writeStream(streamId, connection)
    NetworkUtil.writeNodeObjectId(streamId, NetworkUtil.getObjectId(self.vehicle))
    streamWriteFloat32(streamId, self.repaintAmount)
end

function SFARRepaintEvent:readStream(streamId, connection)
    self.vehicle       = NetworkUtil.getObject(NetworkUtil.readNodeObjectId(streamId))
    self.repaintAmount = streamReadFloat32(streamId)
    self:run(connection)
end

function SFARRepaintEvent:run(connection)
    if g_server == nil then return end
    if self.vehicle == nil then return end
    self.vehicle:addWearAmount(-self.repaintAmount)
    if not connection:getIsServer() then
        connection:sendEvent(SFARRepaintAckEvent.new(self.vehicle, self.vehicle:getWearTotalAmount()))
    end
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Classe principale
-- ─────────────────────────────────────────────────────────────────────────────
SelfFixAndRepaint = {}
SelfFixAndRepaint.__index = SelfFixAndRepaint

-- Memorizza subito il percorso della mod (g_currentModDirectory è disponibile solo al caricamento)
SelfFixAndRepaint.modDir = g_currentModDirectory

-- Nomi dei file XML degli strumenti (usati per identificarli tramite configFileName)
SelfFixAndRepaint.HANDDRILL_XML  = "handdrill.xml"
SelfFixAndRepaint.SPRAYCAN_XML   = "spraycan.xml"

-- Riparazione: 1% al secondo = 0.01 / 1000ms
SelfFixAndRepaint.REPAIR_PER_MS    = 0.01 / 1000
-- Distanza massima (metri) dal giocatore per riparare un veicolo
SelfFixAndRepaint.REPAIR_DISTANCE  = 1.5
-- Sconto rispetto al prezzo di riparazione in officina
SelfFixAndRepaint.REPAIR_COST_RATIO = 0.25

-- Riverniciatura dinamica: la percentuale per spray dipende dal prezzo di riverniciatura
-- Formula: amount = REPAINT_BASE_COST / repaintPrice (con clamp tra MIN e MAX)
SelfFixAndRepaint.REPAINT_BASE_COST = 400    -- costo "di riferimento" per la calibrazione
SelfFixAndRepaint.REPAINT_MIN       = 0.01   -- minimo 1% per spray (veicoli costosi)
SelfFixAndRepaint.REPAINT_MAX       = 0.30   -- massimo 30% per spray (veicoli economici)
-- Distanza massima per riverniciare
SelfFixAndRepaint.REPAINT_DISTANCE  = 1.5

-- Soglia massima: il trapano ripara solo se danno < soglia, lo spray solo se usura < soglia.
SelfFixAndRepaint.threshold         = 0.40   -- default 40%
SelfFixAndRepaint.THRESHOLD_STEPS   = {0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90, 1.00}

-- Log di debug: imposta a true per abilitare i messaggi in console
SelfFixAndRepaint.DEBUG = false

local function sfar_log(msg)
    if SelfFixAndRepaint.DEBUG then sfar_log(msg) end
end

-- Stato interno
SelfFixAndRepaint.isDrillEquipped    = false
SelfFixAndRepaint.isRepairPressed    = false
SelfFixAndRepaint.isSprayCanEquipped = false
SelfFixAndRepaint.currentTargetVehicle = nil

-- Stato riverniciatura
SelfFixAndRepaint.wasSprayCanSpraying  = false
SelfFixAndRepaint.currentRepaintTarget = nil
SelfFixAndRepaint.cachedRepaintAmount  = nil

-- Stato audio trapano
SelfFixAndRepaint.drillSoundSample    = nil
SelfFixAndRepaint.isDrillSoundPlaying = false

-- Notifica veicolo bloccato (riparazione/verniciatura impossibile per soglia)
SelfFixAndRepaint.lastNotifyBlockedVehicle = nil

-- Notifica "senza soldi" durante la riparazione
SelfFixAndRepaint.lastNotifiedNoMoney = false
SelfFixAndRepaint.lastNotifiedNoMoneyVehicle = nil

-- HUD
SelfFixAndRepaint.hudVehicle = nil

-- Cache dei valori autoritativi ricevuti dal server (danno/usura).
-- Sovrascrivono il valore stale di getDamageAmount/getWearTotalAmount sul client
-- finché la replicazione FS25 non aggiorna il valore locale.
SelfFixAndRepaint.serverDamageCache = {}
SelfFixAndRepaint.serverWearCache   = {}

-- Timer check strumento
SelfFixAndRepaint.CHECK_INTERVAL_MS = 200
SelfFixAndRepaint.lastCheckTime     = 0


-- ─────────────────────────────────────────────────────────────────────────────
-- Impostazioni: file nella cartella personale del mod (indipendente dal savegame)
-- ─────────────────────────────────────────────────────────────────────────────
local SFAR_SETTINGS_DIR  = g_currentModSettingsDirectory
local SFAR_SETTINGS_FILE = SFAR_SETTINGS_DIR .. "settings.xml"

-- Carica subito al momento dell'avvio del gioco (prima ancora di caricare una mappa)
do
    local xmlFile = XMLFile.loadIfExists("SFARSettings", SFAR_SETTINGS_FILE, "SFARSettings")
    if xmlFile ~= nil then
        local v = xmlFile:getFloat("SFARSettings#threshold", 0.40)
        SelfFixAndRepaint.threshold = math.max(0.05, math.min(1.00, v))
        xmlFile:delete()
    end
end


-- ─────────────────────────────────────────────────────────────────────────────
-- UI impostazioni (iniettata nel menu Impostazioni Generali del gioco)
-- ─────────────────────────────────────────────────────────────────────────────
local SFARUI = {}
SFARUI.isInitialized = false
SFARUI.thresholdBox  = nil

-- Testi delle opzioni nel multi-option: "10%", "20%", …, "100%"
local SFAR_THRESHOLD_TEXTS = {}
for _, v in ipairs(SelfFixAndRepaint.THRESHOLD_STEPS) do
    table.insert(SFAR_THRESHOLD_TEXTS, string.format("%.0f%%", v * 100))
end

local function sfar_refreshFocusIds(element)
    if element == nil then return end
    element.focusId = FocusManager:serveAutoFocusId()
    for _, child in pairs(element.elements) do
        sfar_refreshFocusIds(child)
    end
end

local function sfar_findMultiTextTemplate(layouts)
    for _, searchLayout in ipairs(layouts) do
        if searchLayout ~= nil then
            for _, elem in ipairs(searchLayout.elements) do
                if elem.typeName == "Bitmap" and #elem.elements >= 1 then
                    if elem.elements[1].typeName == "MultiTextOption" then
                        return elem
                    end
                end
            end
        end
    end
    return nil
end

--- Aggiorna il controllo UI con il valore corrente della soglia.
function SFARUI.populate()
    if SFARUI.thresholdBox == nil then return end
    local option = SFARUI.thresholdBox.elements[1]
    for i, v in ipairs(SelfFixAndRepaint.THRESHOLD_STEPS) do
        if math.abs(v - SelfFixAndRepaint.threshold) < 0.001 then
            option:setState(i)
            break
        end
    end
end

--- Callback del multi-option: il giocatore ha selezionato un nuovo valore.
function SFARUI.onThresholdChanged(_, state)
    local newThreshold = SelfFixAndRepaint.THRESHOLD_STEPS[state]
    if newThreshold == nil then return end
    SelfFixAndRepaint.applyThreshold(newThreshold)
    if g_server ~= nil then
        g_server:broadcastEvent(SFARThresholdEvent.new(newThreshold))
    elseif g_client ~= nil and g_client:getServerConnection() ~= nil then
        g_client:getServerConnection():sendEvent(SFARThresholdEvent.new(newThreshold))
    end
end

--- Inietta il controllo nel menu Impostazioni di Gioco.
--- Chiamata da Mission00.loadMission00Finished.
function SFARUI.injectUiSettings()
    if SFARUI.isInitialized then return end

    local settingsPage = g_inGameMenu.pageSettings
    if settingsPage == nil then
        sfar_log("[SelfFixAndRepaint] SFARUI: settings page not found.")
        return
    end

    local layout = settingsPage.gameSettingsLayout

    -- ── Intestazione sezione ──────────────────────────────────────────────────
    for _, elem in ipairs(layout.elements) do
        if elem.name == "sectionHeader" then
            local header = elem:clone(layout)
            header:setText(g_i18n:getText("sfar_setting_section_title"))
            header.focusId = FocusManager:serveAutoFocusId()
            table.insert(settingsPage.controlsList, header)
            break
        end
    end

    -- ── Multi-option: soglia ──────────────────────────────────────────────────
    local multiTemplate = sfar_findMultiTextTemplate({layout})

    if multiTemplate == nil then
        sfar_log("[SelfFixAndRepaint] SFARUI: multi-option template not found, setting unavailable.")
        SFARUI.isInitialized = true
        return
    end

    local thresholdBox = multiTemplate:clone(layout)
    sfar_refreshFocusIds(thresholdBox)
    thresholdBox.id = "sfar_thresholdBox"

    local thresholdOption = thresholdBox.elements[1]
    thresholdOption.id = "sfar_threshold"
    thresholdOption:setDisabled(false)
    thresholdOption:setTexts(SFAR_THRESHOLD_TEXTS)
    thresholdOption.onClickCallback = SFARUI.onThresholdChanged

    -- Il FocusManager usa target.name per isolare i controlli per schermata
    SFARUI.name = settingsPage.name

    thresholdBox.elements[2]:setText(g_i18n:getText("sfar_setting_threshold_short"))
    thresholdOption.elements[1]:setText(g_i18n:getText("sfar_setting_threshold_long"))

    table.insert(settingsPage.controlsList, thresholdBox)
    SFARUI.thresholdBox = thresholdBox

    -- ── Hook updateGameSettings per aggiornare il valore all'apertura del tab ──
    InGameMenuSettingsFrame.updateGameSettings = Utils.appendedFunction(
        InGameMenuSettingsFrame.updateGameSettings,
        SFARUI.populate
    )

    -- ── Ri-registra nel FocusManager ad ogni cambio GUI ───────────────────────
    FocusManager.setGui = Utils.appendedFunction(FocusManager.setGui, function()
        if SFARUI.thresholdBox ~= nil then
            if not SFARUI.thresholdBox.focusId
            or not FocusManager.currentFocusData.idToElementMapping[SFARUI.thresholdBox.focusId] then
                FocusManager:loadElementFromCustomValues(SFARUI.thresholdBox, nil, nil, false, false)
            end
        end
        layout:invalidateLayout()
    end)

    SFARUI.populate()
    layout:invalidateLayout()

    SFARUI.isInitialized = true
    sfar_log("[SelfFixAndRepaint] SFARUI: impostazione soglia iniettata nel menu di gioco.")
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Funzioni di utilità
-- ─────────────────────────────────────────────────────────────────────────────

--- Restituisce true se il configFileName dello strumento contiene il nome cercato.
local function isToolOfType(handTool, xmlFileName)
    if handTool == nil then return false end
    local cfg = handTool.configFileName or ""
    cfg = cfg:gsub("\\", "/"):lower()
    return cfg:find(xmlFileName:lower(), 1, true) ~= nil
end

--- Ritorna il danno del veicolo: usa la cache del server se presente (più fresco del valore replicato).
local function getVehicleDamage(vehicle)
    return SelfFixAndRepaint.serverDamageCache[vehicle] or vehicle:getDamageAmount()
end

--- Ritorna l'usura del veicolo: usa la cache del server se presente.
local function getVehicleWear(vehicle)
    return SelfFixAndRepaint.serverWearCache[vehicle] or vehicle:getWearTotalAmount()
end

--- Calcola la percentuale di vernice da applicare per ogni spray in base al prezzo di riverniciatura.
local function calcRepaintPerClick(vehicle)
    local price = vehicle:getRepaintPrice()
    if price == nil or price <= 0 then return 0.05 end
    local amount = SelfFixAndRepaint.REPAINT_BASE_COST / price
    return math.max(SelfFixAndRepaint.REPAINT_MIN, math.min(SelfFixAndRepaint.REPAINT_MAX, amount))
end

--- Applica la nuova soglia.
--- skipSave=true quando l'aggiornamento arriva da remoto (non salvare il file locale).
function SelfFixAndRepaint.applyThreshold(value, skipSave)
    SelfFixAndRepaint.threshold = value
    -- Aggiorna il controllo UI (se il menu è aperto)
    SFARUI.populate()
    -- Salva nel file impostazioni (solo se è una modifica locale)
    if not skipSave then
        createFolder(SFAR_SETTINGS_DIR)
        local xmlFile = XMLFile.create("SFARSettings", SFAR_SETTINGS_FILE, "SFARSettings")
        if xmlFile ~= nil then
            xmlFile:setFloat("SFARSettings#threshold", value)
            xmlFile:save()
            xmlFile:delete()
        end
    end
end

-- Maschera per il raycast
SelfFixAndRepaint.LOOK_RAYCAST_MASK = CollisionFlag.VEHICLE + CollisionFlag.DYNAMIC_OBJECT + CollisionFlag.STATIC_OBJECT + CollisionFlag.TERRAIN
SelfFixAndRepaint._raycastHitNode = nil

function SelfFixAndRepaint.raycastCallback(_, hitObjectId, _, _, _, _, _, _)
    SelfFixAndRepaint._raycastHitNode = hitObjectId
end

local function getVehicleFromNode(nodeId)
    if nodeId == nil or nodeId == 0 then return nil end
    local nodeToObject = g_currentMission.nodeToObject
    local parentId = nodeId
    while parentId ~= nil and parentId ~= 0 do
        local obj = nodeToObject[parentId]
        if obj ~= nil then
            if obj.spec_wearable ~= nil and obj.getOwnerFarmId ~= nil then
                return obj
            end
            return nil
        end
        parentId = getParent(parentId)
    end
    return nil
end

local function findVehicleByLookRay(player, maxDist)
    local rx, ry, rz, rdx, rdy, rdz = player:getLookRay()
    if rx == nil then return nil end
    SelfFixAndRepaint._raycastHitNode = nil
    raycastClosest(rx, ry, rz, rdx, rdy, rdz, maxDist, "raycastCallback", SelfFixAndRepaint, SelfFixAndRepaint.LOOK_RAYCAST_MASK)
    local hitNode = SelfFixAndRepaint._raycastHitNode
    if hitNode == nil then return nil end
    local vehicle = getVehicleFromNode(hitNode)
    if vehicle == nil then return nil end
    if vehicle:getOwnerFarmId() ~= player.farmId then return nil end
    return vehicle
end


-- ─────────────────────────────────────────────────────────────────────────────
-- HUD pannello stato veicolo
-- ─────────────────────────────────────────────────────────────────────────────

function SelfFixAndRepaint.drawHud()
    local vehicle = SelfFixAndRepaint.hudVehicle
    if vehicle == nil then return end
    if g_currentMission == nil or not g_currentMission.isMissionStarted then return end
    if g_gui ~= nil and g_gui:getIsGuiVisible() then return end

    local damage = (vehicle.getDamageAmount ~= nil) and (getVehicleDamage(vehicle) * 100) or 0
    local wear   = (vehicle.getWearTotalAmount ~= nil) and (getVehicleWear(vehicle) * 100) or 0
    local name   = vehicle:getFullName() or "?"
    local thr    = SelfFixAndRepaint.threshold * 100

    local damageText    = string.format(g_i18n:getText("sfar_notify_damage"),    damage)
    local wearText      = string.format(g_i18n:getText("sfar_notify_wear"),      wear)
    local thresholdText = string.format(g_i18n:getText("sfar_notify_threshold"), thr)

    local panelW  = 0.22
    local panelX  = 0.5 - panelW / 2
    local panelY  = 0.88
    local lineH   = 0.024
    local padding = 0.008
    local textSize = 0.016
    local panelH  = padding * 2 + lineH * 4

    drawFilledRect(panelX, panelY, panelW, panelH, 0, 0, 0, 0.55)
    drawFilledRect(panelX, panelY + panelH - 0.003, panelW, 0.003, 0.95, 0.55, 0.0, 1.0)

    local textX = panelX + padding
    local baseY = panelY + padding

    local isDrill = SelfFixAndRepaint.isDrillEquipped
    local isSpray = SelfFixAndRepaint.isSprayCanEquipped

    local damageBlocked = isDrill and (damage > thr)
    local wearBlocked   = isSpray and (wear   > thr)

    local dr, dg
    if damage <= 50 then
        dr = damage / 50
        dg = 1.0
    else
        dr = 1.0
        dg = (100 - damage) / 50
    end
    local da = damageBlocked and 0.35 or 1.0

    local wr, wg
    if wear <= 50 then
        wr = wear / 50
        wg = 1.0
    else
        wr = 1.0
        wg = (100 - wear) / 50
    end
    local wa = wearBlocked and 0.35 or 1.0

    setTextAlignment(RenderText.ALIGN_LEFT)
    setTextBold(false)

    -- Riga in basso: soglia (info statica, testo grigio chiaro)
    setTextColor(0.60, 0.60, 0.60, 1.0)
    renderText(textX, baseY, textSize, thresholdText)

    -- Usura vernice
    setTextColor(wr, wg, 0.0, wa)
    renderText(textX, baseY + lineH, textSize, wearText)

    -- Danno struttura
    setTextColor(dr, dg, 0.0, da)
    renderText(textX, baseY + lineH * 2, textSize, damageText)

    -- In alto: nome veicolo in grassetto
    setTextBold(true)
    setTextColor(1.0, 1.0, 1.0, 1.0)
    renderText(textX, baseY + lineH * 3, textSize, name)

    setTextBold(false)
    setTextColor(1.0, 1.0, 1.0, 1.0)
    setTextAlignment(RenderText.ALIGN_LEFT)
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Logica di riparazione
-- ─────────────────────────────────────────────────────────────────────────────

function SelfFixAndRepaint.updateRepair(dt)
    local player = g_localPlayer
    if player == nil then return end

    local vehicle = findVehicleByLookRay(player, SelfFixAndRepaint.REPAIR_DISTANCE)
    if vehicle ~= nil then
        local dmg = getVehicleDamage(vehicle)
        if dmg <= 0 or dmg > SelfFixAndRepaint.threshold then vehicle = nil end
    end

    if vehicle ~= SelfFixAndRepaint.currentTargetVehicle then
        SelfFixAndRepaint.currentTargetVehicle = vehicle
    end
    if vehicle == nil then return end

    local currentDamage = getVehicleDamage(vehicle)
    if currentDamage <= 0 then return end

    local repairAmount = math.min(SelfFixAndRepaint.REPAIR_PER_MS * dt, currentDamage)
    local frameCost = vehicle:getRepairPrice() * (repairAmount / currentDamage) * SelfFixAndRepaint.REPAIR_COST_RATIO

    local farmId = vehicle:getOwnerFarmId()
    local farm   = g_farmManager:getFarmById(farmId)
    if farm == nil then return end
    if farm.money < frameCost then
        -- Notifica "senza soldi" — mostra solo quando si cambia veicolo o alla prima rilevazione
        if vehicle ~= SelfFixAndRepaint.lastNotifiedNoMoneyVehicle then
            SelfFixAndRepaint.lastNotifiedNoMoneyVehicle = vehicle
            if g_currentMission ~= nil and g_currentMission.isMissionStarted then
                g_currentMission:showBlinkingWarning(g_i18n:getText("sfar_notify_no_money"), 3000)
            end
        end
        return
    end
    -- Se il giocatore ha abbastanza soldi, resetta la notifica
    if SelfFixAndRepaint.lastNotifiedNoMoneyVehicle == vehicle then
        SelfFixAndRepaint.lastNotifiedNoMoneyVehicle = nil
    end

    if g_server ~= nil then
        -- Host/server: applica direttamente (autoritativo)
        g_currentMission:addMoney(-frameCost, farmId, MoneyType.VEHICLE_REPAIR, false, true)
        vehicle:addDamageAmount(-repairAmount)
    elseif g_client ~= nil and g_client:getServerConnection() ~= nil then
        -- Client: delega tutto al server tramite evento
        g_client:getServerConnection():sendEvent(SFARRepairEvent.new(vehicle, repairAmount, frameCost, farmId))
    end
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Logica di riverniciatura
-- ─────────────────────────────────────────────────────────────────────────────

function SelfFixAndRepaint.updateSprayRepaint()
    local player = g_localPlayer
    if player == nil then return end

    local heldTool = player:getHeldHandTool()
    if heldTool == nil then
        SelfFixAndRepaint.wasSprayCanSpraying = false
        return
    end

    local spec = heldTool.spec_sprayCan
    if spec == nil then
        SelfFixAndRepaint.wasSprayCanSpraying = false
        return
    end

    local currentlySpraying = spec.isSpraying

    if currentlySpraying and not SelfFixAndRepaint.wasSprayCanSpraying then
        local vehicle = findVehicleByLookRay(player, SelfFixAndRepaint.REPAINT_DISTANCE)
        if vehicle ~= nil and getVehicleWear(vehicle) <= 0 then vehicle = nil end

        if vehicle ~= SelfFixAndRepaint.currentRepaintTarget then
            SelfFixAndRepaint.currentRepaintTarget = vehicle
            SelfFixAndRepaint.cachedRepaintAmount  = (vehicle ~= nil) and calcRepaintPerClick(vehicle) or nil
        end

        if vehicle ~= nil then
            local currentWear = getVehicleWear(vehicle)
            sfar_log(string.format("SelfFixAndRepaint [SPRAY]: usura vernice = %.4f (%.1f%%)", currentWear, currentWear * 100))

            if currentWear > SelfFixAndRepaint.threshold then
                sfar_log(string.format("SelfFixAndRepaint [SPRAY]: usura troppo elevata (%.1f%% > %.0f%%)", currentWear * 100, SelfFixAndRepaint.threshold * 100))
            elseif currentWear > 0 then
                local repaintAmount = SelfFixAndRepaint.cachedRepaintAmount
                if g_server ~= nil then
                    -- Host/server: applica direttamente (autoritativo)
                    vehicle:addWearAmount(-repaintAmount)
                    local wearAfter = vehicle:getWearTotalAmount()
                    sfar_log(string.format("SelfFixAndRepaint [SPRAY]: spray applicato (%.1f%%) — usura dopo = %.4f (%.1f%%)", repaintAmount * 100, wearAfter, wearAfter * 100))
                    if wearAfter <= 0 then
                        SelfFixAndRepaint.currentRepaintTarget = nil
                        SelfFixAndRepaint.cachedRepaintAmount  = nil
                    end
                elseif g_client ~= nil and g_client:getServerConnection() ~= nil then
                    -- Client: delega al server tramite evento
                    g_client:getServerConnection():sendEvent(SFARRepaintEvent.new(vehicle, repaintAmount))
                    sfar_log(string.format("SelfFixAndRepaint [SPRAY]: richiesta spray inviata al server (%.1f%%)", repaintAmount * 100))
                end
            else
                sfar_log("SelfFixAndRepaint [SPRAY]: veicolo già in perfette condizioni (usura = 0)")
            end
        else
            sfar_log("SelfFixAndRepaint [SPRAY]: nessun veicolo trovato nel raggio")
        end
    end

    SelfFixAndRepaint.wasSprayCanSpraying = currentlySpraying
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Callback action event riparazione
-- ─────────────────────────────────────────────────────────────────────────────

function SelfFixAndRepaint.onRepairActionInput(_, _, inputValue)
    local wasPressed = SelfFixAndRepaint.isRepairPressed
    SelfFixAndRepaint.isRepairPressed = (inputValue ~= 0)

    if SelfFixAndRepaint.isRepairPressed and not wasPressed then
        if SelfFixAndRepaint.drillSoundSample ~= nil and not SelfFixAndRepaint.isDrillSoundPlaying then
            playSample(SelfFixAndRepaint.drillSoundSample, 0, 1.0, 0, 0, 0)
            SelfFixAndRepaint.isDrillSoundPlaying = true
        end
    elseif not SelfFixAndRepaint.isRepairPressed and wasPressed then
        if SelfFixAndRepaint.drillSoundSample ~= nil and SelfFixAndRepaint.isDrillSoundPlaying then
            stopSample(SelfFixAndRepaint.drillSoundSample, 0, 0)
            SelfFixAndRepaint.isDrillSoundPlaying = false
        end
        SelfFixAndRepaint.currentTargetVehicle = nil
    end
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Logica principale (check strumento, ogni 200ms)
-- ─────────────────────────────────────────────────────────────────────────────

function SelfFixAndRepaint.checkEquippedTool()
    local player = g_localPlayer
    if player == nil then return end

    local heldTool = player:getHeldHandTool()

    local drillNow = isToolOfType(heldTool, SelfFixAndRepaint.HANDDRILL_XML)
    if drillNow and not SelfFixAndRepaint.isDrillEquipped then
        SelfFixAndRepaint.isDrillEquipped = true
        SelfFixAndRepaint.lastNotifiedVehicle = nil
        SelfFixAndRepaint.onDrillEquipped(heldTool)
    elseif not drillNow and SelfFixAndRepaint.isDrillEquipped then
        SelfFixAndRepaint.isDrillEquipped = false
        SelfFixAndRepaint.isRepairPressed = false
        SelfFixAndRepaint.currentTargetVehicle = nil
        SelfFixAndRepaint.hudVehicle = nil
        SelfFixAndRepaint.onDrillUnequipped()
    end

    if SelfFixAndRepaint.isDrillEquipped then
        SelfFixAndRepaint.hudVehicle = findVehicleByLookRay(player, SelfFixAndRepaint.REPAIR_DISTANCE)
    end

    local sprayNow = isToolOfType(heldTool, SelfFixAndRepaint.SPRAYCAN_XML)
    if sprayNow and not SelfFixAndRepaint.isSprayCanEquipped then
        SelfFixAndRepaint.isSprayCanEquipped = true
        SelfFixAndRepaint.hudVehicle = nil
        SelfFixAndRepaint.onSprayCanEquipped()
    elseif not sprayNow and SelfFixAndRepaint.isSprayCanEquipped then
        SelfFixAndRepaint.isSprayCanEquipped = false
        SelfFixAndRepaint.hudVehicle = nil
        SelfFixAndRepaint.onSprayCanUnequipped()
    end

    if SelfFixAndRepaint.isSprayCanEquipped then
        SelfFixAndRepaint.hudVehicle = findVehicleByLookRay(player, SelfFixAndRepaint.REPAINT_DISTANCE)
    end

    if not SelfFixAndRepaint.isDrillEquipped and not SelfFixAndRepaint.isSprayCanEquipped then
        SelfFixAndRepaint.hudVehicle = nil
    end

    -- Notifica se il veicolo puntato è fuori soglia (troppo danneggiato o usurato)
    local blockedMsg = nil
    local hv = SelfFixAndRepaint.hudVehicle
    if hv ~= nil then
        if SelfFixAndRepaint.isDrillEquipped then
            local dmg = getVehicleDamage(hv)
            if dmg > SelfFixAndRepaint.threshold then
                blockedMsg = g_i18n:getText("sfar_notify_repair_blocked")
            end
        elseif SelfFixAndRepaint.isSprayCanEquipped then
            local wear = getVehicleWear(hv)
            if wear > SelfFixAndRepaint.threshold then
                blockedMsg = g_i18n:getText("sfar_notify_repaint_blocked")
            end
        end
    end
    -- Mostra la notifica solo alla prima volta che si punta un veicolo bloccato
    local curBlockedVehicle = (blockedMsg ~= nil) and hv or nil
    if curBlockedVehicle ~= SelfFixAndRepaint.lastNotifyBlockedVehicle then
        SelfFixAndRepaint.lastNotifyBlockedVehicle = curBlockedVehicle
        if blockedMsg ~= nil and g_currentMission ~= nil and g_currentMission.isMissionStarted then
            g_currentMission:showBlinkingWarning(blockedMsg, 3000)
        end
    end
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Callback strumenti
-- ─────────────────────────────────────────────────────────────────────────────

function SelfFixAndRepaint.onDrillEquipped(tool)
    sfar_log("SelfFixAndRepaint: Trapano equipaggiato — riparazione disponibile")
    local _, actionEventId = tool:addActionEvent(
        InputAction.ACTIVATE_HANDTOOL,
        SelfFixAndRepaint,
        SelfFixAndRepaint.onRepairActionInput,
        true, true, true, true, nil
    )
    g_inputBinding:setActionEventText(actionEventId, g_i18n:getText("sfar_action_repair"))
    g_inputBinding:setActionEventTextPriority(actionEventId, GS_PRIO_VERY_HIGH)

    if g_soundManager ~= nil and SelfFixAndRepaint.drillSoundSample == nil then
        local fileName = Utils.getFilename("data/drill.ogg", SelfFixAndRepaint.modDir)
        SelfFixAndRepaint.drillSoundSample = createSample("SelfFixAndRepaint_Drill")
        loadSample(SelfFixAndRepaint.drillSoundSample, fileName, false)
    end
end

function SelfFixAndRepaint.onDrillUnequipped()
    sfar_log("SelfFixAndRepaint: Trapano rimosso — riparazione non disponibile")
    if SelfFixAndRepaint.drillSoundSample ~= nil and SelfFixAndRepaint.isDrillSoundPlaying then
        stopSample(SelfFixAndRepaint.drillSoundSample, 0, 0)
        SelfFixAndRepaint.isDrillSoundPlaying = false
    end
    SelfFixAndRepaint.drillSoundSample = nil
    SelfFixAndRepaint.lastNotifyBlockedVehicle = nil
    SelfFixAndRepaint.lastNotifiedNoMoneyVehicle = nil
    -- Pulisce le entry della cache per il veicolo corrente (evita spazzatura in memoria)
    if SelfFixAndRepaint.currentTargetVehicle ~= nil then
        SelfFixAndRepaint.serverDamageCache[SelfFixAndRepaint.currentTargetVehicle] = nil
    end
end

function SelfFixAndRepaint.onSprayCanEquipped()
    SelfFixAndRepaint.wasSprayCanSpraying  = false
    SelfFixAndRepaint.currentRepaintTarget = nil
    sfar_log("SelfFixAndRepaint: Spray equipaggiato — riverniciatura disponibile")
end

function SelfFixAndRepaint.onSprayCanUnequipped()
    SelfFixAndRepaint.wasSprayCanSpraying  = false
    if SelfFixAndRepaint.currentRepaintTarget ~= nil then
        SelfFixAndRepaint.serverWearCache[SelfFixAndRepaint.currentRepaintTarget] = nil
    end
    SelfFixAndRepaint.currentRepaintTarget = nil
    SelfFixAndRepaint.cachedRepaintAmount  = nil
    SelfFixAndRepaint.lastNotifyBlockedVehicle = nil
    sfar_log("SelfFixAndRepaint: Spray rimosso — riverniciatura non disponibile")
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Hook update
-- ─────────────────────────────────────────────────────────────────────────────

function SelfFixAndRepaint:update(dt)
    if g_currentMission == nil or not g_currentMission.isMissionStarted then return end

    SelfFixAndRepaint.lastCheckTime = SelfFixAndRepaint.lastCheckTime + dt
    if SelfFixAndRepaint.lastCheckTime >= SelfFixAndRepaint.CHECK_INTERVAL_MS then
        SelfFixAndRepaint.lastCheckTime = 0
        SelfFixAndRepaint.checkEquippedTool()
    end

    if SelfFixAndRepaint.isDrillEquipped and SelfFixAndRepaint.isRepairPressed then
        SelfFixAndRepaint.updateRepair(dt)
    end

    if SelfFixAndRepaint.isSprayCanEquipped then
        SelfFixAndRepaint.updateSprayRepaint()
    end
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Ciclo di vita della mod
-- ─────────────────────────────────────────────────────────────────────────────

function SelfFixAndRepaint:loadMap()
    BaseMission.update = Utils.appendedFunction(BaseMission.update, SelfFixAndRepaint.update)
    BaseMission.draw   = Utils.appendedFunction(BaseMission.draw,   SelfFixAndRepaint.drawHud)

    -- Intercetta HandToolSprayCan.onLoad solo per il nostro spray
    local originalSprayCanOnLoad = HandToolSprayCan.onLoad
    HandToolSprayCan.onLoad = function(tool, xmlFile)
        local cfg = (tool.configFileName or ""):gsub("\\", "/"):lower()
        if cfg:find(SelfFixAndRepaint.SPRAYCAN_XML, 1, true) then
            local origCreate = tool.createCrosshairOverlayFromFile
            tool.createCrosshairOverlayFromFile = function() return nil end
            local result = originalSprayCanOnLoad(tool, xmlFile)
            tool.createCrosshairOverlayFromFile = origCreate
            tool.spec_sprayCan.sprayDuration = 250
            return result
        end
        return originalSprayCanOnLoad(tool, xmlFile)
    end

    -- Personalizza le label dei tasti solo per il nostro spray
    HandToolSprayCan.onRegisterActionEvents = Utils.appendedFunction(
        HandToolSprayCan.onRegisterActionEvents,
        function(tool)
            local cfg = (tool.configFileName or ""):gsub("\\", "/"):lower()
            if not cfg:find(SelfFixAndRepaint.SPRAYCAN_XML, 1, true) then return end

            local events = tool.actionEvents
            if events == nil then return end

            local activateEvent = events[InputAction.ACTIVATE_HANDTOOL]
            if activateEvent ~= nil then
                g_inputBinding:setActionEventText(activateEvent.actionEventId, g_i18n:getText("sfar_action_repaint"))
            end

            local changeEvent = events[InputAction.SPRAYCAN_CHANGE_MARKER]
            if changeEvent ~= nil then
                g_inputBinding:setActionEventActive(changeEvent.actionEventId, false)
            end
        end
    )

    sfar_log("SelfFixAndRepaint: Mod caricata (modDir: " .. SelfFixAndRepaint.modDir .. ")")
end

function SelfFixAndRepaint:deleteMap()
    SelfFixAndRepaint.isDrillEquipped      = false
    SelfFixAndRepaint.isRepairPressed      = false
    SelfFixAndRepaint.isSprayCanEquipped   = false
    SelfFixAndRepaint.currentTargetVehicle = nil
    SelfFixAndRepaint.wasSprayCanSpraying  = false
    SelfFixAndRepaint.currentRepaintTarget = nil
    SelfFixAndRepaint.cachedRepaintAmount  = nil
    SelfFixAndRepaint.hudVehicle                = nil
    SelfFixAndRepaint.lastCheckTime             = 0
    SelfFixAndRepaint.lastNotifyBlockedVehicle  = nil
    SelfFixAndRepaint.serverDamageCache         = {}
    SelfFixAndRepaint.serverWearCache           = {}
end


-- ─────────────────────────────────────────────────────────────────────────────
-- Iniezione UI dopo il caricamento completo della missione
-- ─────────────────────────────────────────────────────────────────────────────
Mission00.loadMission00Finished = Utils.appendedFunction(
    Mission00.loadMission00Finished,
    function()
        SFARUI.injectUiSettings()
    end
)


-- ─────────────────────────────────────────────────────────────────────────────
-- Sync impostazioni: il server invia la soglia ad ogni client che si connette
-- ─────────────────────────────────────────────────────────────────────────────
FSBaseMission.sendInitialClientState = Utils.appendedFunction(
    FSBaseMission.sendInitialClientState,
    function(_, connection, _, _)
        connection:sendEvent(SFARThresholdEvent.new(SelfFixAndRepaint.threshold))
    end
)


-- ─────────────────────────────────────────────────────────────────────────────
-- Registrazione event listener
-- ─────────────────────────────────────────────────────────────────────────────
addModEventListener(SelfFixAndRepaint)
