-- =============================================================
-- Real Fuel Consumption — RFCCore.lua
-- Core module: settings, save/load, and mission lifecycle.

-- Guard against double-load
if RealFuelConsumption ~= nil then
    return
end

RealFuelConsumption = {}
RealFuelConsumption.MOD_NAME = g_currentModName
RealFuelConsumption.MOD_DIR  = Utils.getFilename("modules/RealFuelConsumption/", g_currentModDirectory)
RealFuelConsumption.VERSION  = "1.0.3.0"

-- =============================================================
-- CONSTANTS
-- =============================================================

-- Base multiplier options
RealFuelConsumption.BASE_MULT_VALUES  = { 1.0, 1.2, 1.5, 1.8, 2.0, 2.5, 3.0 }
RealFuelConsumption.BASE_MULT_DEFAULT = 3  -- index → 1.5x

-- Load sensitivity levels (extra factor per unit of engine load 0..1)
-- load bonus = loadFraction * LOAD_FACTOR[level]
RealFuelConsumption.LOAD_FACTORS = {
    [1] = 0.0,   -- Off
    [2] = 0.20,  -- Low
    [3] = 0.45,  -- Medium
    [4] = 0.75,  -- High
}
RealFuelConsumption.LOAD_DEFAULT = 3  -- Medium

-- Work type bonus (fraction added on top when that work is detected)
-- These are additive to the base+load result.
RealFuelConsumption.WORK_BONUSES = {
    PLOW       = 0.40,  -- plowing / subsoiling
    CULTIVATE  = 0.35,  -- cultivating / harrowing
    HARVEST    = 0.35,  -- combine threshing
    MOW        = 0.20,  -- mowing / tedding / windrowing
    SPRAY      = 0.15,  -- spraying / fertilizing
    SPREAD     = 0.15,  -- spreading (manure, lime)
    SOW        = 0.10,  -- sowing / seeding
    BALE       = 0.25,  -- baling
}

-- RPM sensitivity: fraction of "how close to max RPM" added to multiplier
-- extra = rpmRatio * RPM_FACTOR  (rpmRatio = currentRpm/maxRpm, clamped 0..1)
RealFuelConsumption.RPM_FACTOR = 0.30

-- =============================================================
-- ENGINE MODES (based on real DLG PowerMix data)
--   ECO    → limits RPM, less consumption, less power
--   NORMAL → baseline (100%)
--   POWER  → boost, more consumption, more power
-- Values calibrated from real Tier 4 / Stage V tractor statistics.
-- =============================================================
RealFuelConsumption.MODE_ECO    = 1
RealFuelConsumption.MODE_NORMAL = 2
RealFuelConsumption.MODE_POWER  = 3

RealFuelConsumption.ENGINE_MODES = {
    [1] = {  -- ECO — real-world sourced behaviour (per Negro's research,
             -- 2026-07): smooth/lazy throttle curve to avoid consumption
             -- spikes, and the engine stays near its peak-torque RPM band
             -- rather than climbing to nominal max. We cap maxRpm by
             -- PERCENTAGE (not a fixed RPM number) so this scales correctly
             -- across every vehicle in the fleet — a small tractor at 2500
             -- max RPM and a big one at 1900 max RPM each land in their own
             -- proportionally-correct "torque zone" instead of all being
             -- forced toward one hardcoded number that only suits one engine.
        name         = "eco",
        fuelMult     = 0.80,   -- -20% consumption
        powerScale   = 0.65,   -- -35% peak torque (was -28%, widened per
                                -- tester feedback: differences felt too subtle)
        rpmLimit     = 0.72,   -- caps max RPM to 72% (was 78%; still lands
                                -- near the real peak-torque band)
        -- Load-based speed drop: at 0% load ECO barely limits you; at full
        -- engine load it caps speed hard. This is what "feels heavy" under
        -- a plough without being an arbitrary flat cap.
        loadSpeedFloor = 6.0,  -- km/h ceiling when the engine is maxed out
                                -- (was 8.0 — sags harder under heavy load)
        loadSpeedCeil  = 26.0, -- km/h ceiling when the engine is unloaded
        -- Confirmed against the official GDN LuaDoc source (VehicleMotor.new):
        -- motorRotationAccelerationLimit controls how fast RPM can climb per
        -- second (rad/s²). Lowering it makes the throttle feel sluggish —
        -- a genuinely "heavy" engine response, not a fake wheel brake.
        accelLimitScale = 0.45, -- revs climb 55% slower than stock (was 45%)
        -- setLowBrakeForce is the native low-speed engine-drag brake (the
        -- same field EngineBrakeforceCompensation manipulates). A small
        -- additive bump here gives ECO extra creep resistance at low speed,
        -- reinforcing the "heavy" feel without touching the wheels.
        lowBrakeBoost = 0.35,   -- additive bump to lowBrakeForceScale
        color        = {0.20, 0.75, 0.30},  -- green (now matches icon exactly)
    },
    [2] = {  -- NORMAL — stock behaviour, no interference
        name         = "normal",
        fuelMult     = 1.00,
        powerScale   = 1.00,
        rpmLimit     = 1.00,
        accelLimitScale = 1.00,
        lowBrakeBoost   = 0.0,
        color        = {0.72, 0.74, 0.78},  -- medium gray (was amber — mismatched
                                             -- the gray icon; also darkened from
                                             -- the icon's original pale gray per
                                             -- feedback that it looked washed out)
    },
    [3] = {  -- POWER — real-world sourced behaviour (per Negro's research,
             -- 2026-07): the ECU fights to keep RPM UP under load and
             -- ignores efficiency so the implement never stalls. This is
             -- the OPPOSITE of "lugs down calmly" — a prior version of this
             -- mode lowered minRpm to simulate a relaxed low-rev feel,
             -- which actively contradicted that behaviour. Removed.
        name         = "power",
        fuelMult     = 1.15,   -- +15% consumption
        powerScale   = 1.20,   -- +20% peak torque (was +12%, widened per
                                -- tester feedback: differences felt too subtle)
        rpmLimit     = 1.00,   -- was 1.12: removed. setRpmLimit can only cap
                                -- DOWNWARD, never push an engine past its own
                                -- real maxRpm — and that field is shared with
                                -- the base game's fuel consumption formula, so
                                -- inflating it broke consumption per-vehicle
                                -- (some halved, some far too high) and could
                                -- throw off gear/speed calculations too.
                                -- POWER still differentiates via +20% torque
                                -- and +45% faster throttle response.
        -- minRpmScale intentionally omitted (stays at stock 1.0): an eager,
        -- high-holding engine should NOT have a lowered rev floor.
        accelLimitScale = 1.45, -- aggressive throttle — revs climb 45% faster (was 30%)
        lowBrakeBoost   = 0.0,
        color        = {0.88, 0.15, 0.15},  -- clean red (was orange in the icon —
                                             -- unified per feedback; POWER should
                                             -- read as red, not orange)
    },
}

-- Save file name
RealFuelConsumption.SAVE_FILE = "FS25_RealFuelConsumption.xml"

-- =============================================================
-- SETTINGS  (index-based to work with FS25 multi-option rows)
-- =============================================================
local s = {
    baseMultiplierIdx  = RealFuelConsumption.BASE_MULT_DEFAULT,
    loadSensitivityIdx = RealFuelConsumption.LOAD_DEFAULT,
    workBonusEnabled   = true,
    rpmSensitivity     = true,
    engineModesEnabled = true,   -- master toggle for the Eco/Power feature
    notifications      = true,
    showFuelHud        = true,   -- on-screen "Diesel: X.X l/h" readout
    showModeHud        = true,   -- on-screen Eco/Normal/Power mode circle
}
RealFuelConsumption.settings = s

-- Helper: get actual base multiplier value from index
function RealFuelConsumption.getBaseMultiplier()
    local idx = s.baseMultiplierIdx
    return RealFuelConsumption.BASE_MULT_VALUES[idx] or 1.5
end

-- Helper: get load factor from sensitivity index
function RealFuelConsumption.getLoadFactor()
    return RealFuelConsumption.LOAD_FACTORS[s.loadSensitivityIdx] or 0.0
end

-- =============================================================
-- SAVE / LOAD
-- Uses the same hooks as proven mods (DynamicFieldPrices):
--   * FSCareerMissionInfo.saveToXMLFile  → fires on every save
--   * Mission00.loadMission00Finished    → fires when game finishes loading
-- This guarantees settings persist with the savegame.
-- =============================================================
local function getSettingsPath()
    if g_currentMission and g_currentMission.missionInfo then
        local dir = g_currentMission.missionInfo.savegameDirectory
        if dir ~= nil then
            return dir .. "/" .. RealFuelConsumption.SAVE_FILE
        end
    end
    return nil
end

function RealFuelConsumption.saveSettings()
    local path = getSettingsPath()
    if path == nil then return end

    local xml = createXMLFile("rfcSettings", path, "rfc")
    if xml == nil or xml == 0 then return end

    setXMLInt (xml, "rfc#baseMultiplierIdx",  s.baseMultiplierIdx)
    setXMLInt (xml, "rfc#loadSensitivityIdx", s.loadSensitivityIdx)
    setXMLBool(xml, "rfc#workBonusEnabled",   s.workBonusEnabled)
    setXMLBool(xml, "rfc#rpmSensitivity",     s.rpmSensitivity)
    setXMLBool(xml, "rfc#engineModesEnabled", s.engineModesEnabled)
    setXMLBool(xml, "rfc#notifications",      s.notifications)
    setXMLBool(xml, "rfc#showFuelHud",        s.showFuelHud)
    setXMLBool(xml, "rfc#showModeHud",        s.showModeHud)

    saveXMLFile(xml)
    delete(xml)
end

function RealFuelConsumption.loadSettings()
    local path = getSettingsPath()
    if path == nil or not fileExists(path) then return end

    local xml = loadXMLFile("rfcSettings", path)
    if xml == nil or xml == 0 then return end

    local function getInt(key, default)
        return Utils.getNoNil(getXMLInt(xml, key), default)
    end
    local function getBool(key, default)
        return Utils.getNoNil(getXMLBool(xml, key), default)
    end

    s.baseMultiplierIdx  = getInt ("rfc#baseMultiplierIdx",  s.baseMultiplierIdx)
    s.loadSensitivityIdx = getInt ("rfc#loadSensitivityIdx", s.loadSensitivityIdx)
    s.workBonusEnabled   = getBool("rfc#workBonusEnabled",   s.workBonusEnabled)
    s.rpmSensitivity     = getBool("rfc#rpmSensitivity",     s.rpmSensitivity)
    s.engineModesEnabled = getBool("rfc#engineModesEnabled", s.engineModesEnabled)
    s.notifications      = getBool("rfc#notifications",      s.notifications)
    s.showFuelHud        = getBool("rfc#showFuelHud",        s.showFuelHud)
    s.showModeHud        = getBool("rfc#showModeHud",        s.showModeHud)

    -- Clamp indices to valid range
    local maxIdx = #RealFuelConsumption.BASE_MULT_VALUES
    if s.baseMultiplierIdx < 1 or s.baseMultiplierIdx > maxIdx then
        s.baseMultiplierIdx = RealFuelConsumption.BASE_MULT_DEFAULT
    end
    if s.loadSensitivityIdx < 1 or s.loadSensitivityIdx > 4 then
        s.loadSensitivityIdx = RealFuelConsumption.LOAD_DEFAULT
    end

    delete(xml)
end

-- =============================================================
-- MISSION LIFECYCLE
-- =============================================================
local notificationShown   = false
local notificationTimer   = -1     -- ms remaining before showing; -1 = inactive

local function onLoadFinished(mission, node)
    RealFuelConsumption.loadSettings()

    -- Reset notification state on every map load. These are file-local
    -- and persist while the game process stays open, so re-entering a
    -- savegame without restarting FS25 would otherwise skip the message.
    notificationShown = false
    notificationTimer = -1

    -- Arm a short delay so the notification appears once the HUD is ready
    if s.notifications then
        notificationTimer = 3000   -- show after 3 seconds
    end

    -- Refresh settings UI states with loaded values
    if RealFuelConsumptionSettingsUI ~= nil and RealFuelConsumptionSettingsUI.onSettingsLoaded ~= nil then
        RealFuelConsumptionSettingsUI.onSettingsLoaded()
    end
end

-- Robust notification helper (pattern from MudSystemPhysics)
local function showRFCNotification()
    if g_currentMission == nil then return end
    local mult = string.format("%.1f", RealFuelConsumption.getBaseMultiplier())
    local msg  = string.format(g_i18n:getText("rfc_msg_enabled"), mult)

    if g_currentMission.addIngameNotification ~= nil
       and FSBaseMission ~= nil
       and FSBaseMission.INGAME_NOTIFICATION_OK ~= nil then
        g_currentMission:addIngameNotification(FSBaseMission.INGAME_NOTIFICATION_OK, msg)
    elseif g_currentMission.showBlinkingWarning ~= nil then
        g_currentMission:showBlinkingWarning(msg, 4000)
    end
end

-- Update loop: count down the notification timer and fire when ready.
-- FSBaseMission.update is the reliable per-frame update in FS25.
FSBaseMission.update = Utils.appendedFunction(FSBaseMission.update, function(self, dt)
    -- Skip on a dedicated server: it has no screen to show notifications on
    if g_dedicatedServer ~= nil then return end

    if notificationTimer > 0 then
        notificationTimer = notificationTimer - dt
        if notificationTimer <= 0 then
            notificationTimer = -1
            if not notificationShown then
                notificationShown = true
                showRFCNotification()
            end
        end
    end
end)

-- Load settings when the game finishes loading the savegame
Mission00.loadMission00Finished = Utils.appendedFunction(
    Mission00.loadMission00Finished,
    function(self, node)
        onLoadFinished(self, node)
    end
)

-- Save settings every time the game saves (manual save + autosave)
FSCareerMissionInfo.saveToXMLFile = Utils.appendedFunction(
    FSCareerMissionInfo.saveToXMLFile,
    function(self)
        RealFuelConsumption.saveSettings()
    end
)

-- =============================================================
-- MULTIPLAYER: send current settings to each joining client so
-- their consumption config matches the server from connection.
-- =============================================================
if FSBaseMission.sendInitialClientState ~= nil then
    FSBaseMission.sendInitialClientState = Utils.appendedFunction(
        FSBaseMission.sendInitialClientState,
        function(self, connection, user, farm)
            if g_server ~= nil and RFCSyncEvent ~= nil and RFCSyncEvent.sendToClient ~= nil then
                RFCSyncEvent.sendToClient(connection)
            end
        end
    )
end
