-- =============================================================
-- Real Fuel Consumption — RFCVehicleSpec.lua
-- Dynamic per-vehicle fuel consumption logic.
-- Hooks into Motorized.updateFuelUsage to apply:
--   1. Base multiplier
--   2. Engine load bonus (getConsumingLoad)
--   3. Work type bonus (plow, harvest, spray, etc.)
--   4. RPM sensitivity bonus

-- =============================================================
-- IMPLEMENT POWER DEMAND (realism upgrade, v1.1.0.8)
-- Reads the actual PTO/work load the attached implements are pulling
-- via PowerConsumer.getConsumingLoad(). Returns 0..1 where 1 means the
-- implement is working at full demand. Lets the work bonus scale with
-- how hard the tool is actually working — a big plough in heavy clay
-- costs more than a small one in light soil, instead of a flat bonus.
-- =============================================================
local function getImplementWorkLoad(vehicle)
    if vehicle.getAttachedImplements == nil then return nil end
    local maxLoad = nil
    for _, impl in pairs(vehicle:getAttachedImplements()) do
        local obj = impl.object
        if obj ~= nil and obj.spec_powerConsumer ~= nil
           and obj.getConsumingLoad ~= nil then
            local load, count = obj:getConsumingLoad()
            if load ~= nil and count ~= nil and count > 0 then
                load = load / count
                load = math.min(math.max(load, 0.0), 1.0)
                if maxLoad == nil or load > maxLoad then
                    maxLoad = load
                end
            end
        end
    end
    return maxLoad  -- nil if no power-consuming implement is attached
end

-- =============================================================
-- WORK TYPE DETECTION HELPERS
-- =============================================================

-- Returns true if the vehicle (or any attached implement) is plowing
local function isPlowing(vehicle)
    if vehicle.spec_plow ~= nil then return true end
    if vehicle.spec_subsoiler ~= nil then return true end
    -- Check implements
    if vehicle.getAttachedImplements ~= nil then
        for _, impl in pairs(vehicle:getAttachedImplements()) do
            if impl.object ~= nil then
                if impl.object.spec_plow ~= nil then return true end
                if impl.object.spec_subsoiler ~= nil then return true end
            end
        end
    end
    return false
end

-- Returns true if cultivating / harrowing
local function isCultivating(vehicle)
    if vehicle.spec_cultivator ~= nil then return true end
    if vehicle.getAttachedImplements ~= nil then
        for _, impl in pairs(vehicle:getAttachedImplements()) do
            if impl.object ~= nil and impl.object.spec_cultivator ~= nil then
                return true
            end
        end
    end
    return false
end

-- Returns true if the combine thresher is actively harvesting
local function isHarvesting(vehicle)
    local cs = vehicle.spec_combine
    if cs ~= nil then
        if (cs.lastValidArea or 0) > 0.0001 then return true end
        if (cs.lastArea or 0) > 0.0001 then return true end
    end
    if vehicle.getIsCutterActive ~= nil and vehicle:getIsCutterActive() then
        return true
    end
    return false
end

-- Returns true if mowing / tedding / windrowing
local function isMowing(vehicle)
    if vehicle.spec_mower ~= nil then
        local ms = vehicle.spec_mower
        if ms.isWorking or (ms.mowingAreas ~= nil) then return true end
    end
    if vehicle.spec_tedder ~= nil then return true end
    if vehicle.spec_windrower ~= nil then return true end
    -- Check implements
    if vehicle.getAttachedImplements ~= nil then
        for _, impl in pairs(vehicle:getAttachedImplements()) do
            if impl.object ~= nil then
                local o = impl.object
                if o.spec_mower ~= nil or o.spec_tedder ~= nil or o.spec_windrower ~= nil then
                    return true
                end
            end
        end
    end
    return false
end

-- Returns true if spraying or fertilizing
local function isSpraying(vehicle)
    if vehicle.spec_sprayer ~= nil then
        local ss = vehicle.spec_sprayer
        if ss.isWorking then return true end
    end
    if vehicle.getAttachedImplements ~= nil then
        for _, impl in pairs(vehicle:getAttachedImplements()) do
            if impl.object ~= nil and impl.object.spec_sprayer ~= nil then
                local ss = impl.object.spec_sprayer
                if ss.isWorking then return true end
            end
        end
    end
    return false
end

-- Returns true if spreading (manure, slurry, lime)
local function isSpreading(vehicle)
    if vehicle.spec_manureSpreader ~= nil then return true end
    if vehicle.spec_slurryTanker ~= nil then return true end
    if vehicle.spec_fertilizingSpreader ~= nil then return true end
    if vehicle.getAttachedImplements ~= nil then
        for _, impl in pairs(vehicle:getAttachedImplements()) do
            if impl.object ~= nil then
                local o = impl.object
                if o.spec_manureSpreader ~= nil or
                   o.spec_slurryTanker ~= nil or
                   o.spec_fertilizingSpreader ~= nil then
                    return true
                end
            end
        end
    end
    return false
end

-- Returns true if sowing / seeding
local function isSowing(vehicle)
    if vehicle.spec_sowingMachine ~= nil then return true end
    if vehicle.getAttachedImplements ~= nil then
        for _, impl in pairs(vehicle:getAttachedImplements()) do
            if impl.object ~= nil and impl.object.spec_sowingMachine ~= nil then
                return true
            end
        end
    end
    return false
end

-- Returns true if baling
local function isBaling(vehicle)
    if vehicle.spec_baler ~= nil then
        local bs = vehicle.spec_baler
        if bs.lastValidArea ~= nil and bs.lastValidArea > 0 then return true end
    end
    return false
end

-- =============================================================
-- WORK BONUS CALCULATION
-- Returns the highest applicable work bonus fraction (0.0 .. 0.40)
-- =============================================================
local function getWorkBonus(vehicle)
    local WB = RealFuelConsumption.WORK_BONUSES

    if isPlowing(vehicle)     then return WB.PLOW      end
    if isHarvesting(vehicle)  then return WB.HARVEST    end
    if isCultivating(vehicle) then return WB.CULTIVATE  end
    if isBaling(vehicle)      then return WB.BALE       end
    if isMowing(vehicle)      then return WB.MOW        end
    if isSpreading(vehicle)   then return WB.SPREAD     end
    if isSpraying(vehicle)    then return WB.SPRAY      end
    if isSowing(vehicle)      then return WB.SOW        end

    return 0.0
end

-- =============================================================
-- ENGINE LOAD FRACTION
-- Uses getConsumingLoad() which FS25 exposes on motorized vehicles.
-- Falls back to spec_motorized.motorLoad if available.
-- Returns a value 0.0 .. 1.0
-- =============================================================
local function getEngineLoadFraction(vehicle)
    -- Primary: getMotorLoadPercentage is the official FS25 accessor and
    -- returns spec.smoothedLoadPercentage (0..1), the same value the base
    -- game uses to compute fuel burn. This is the most accurate source.
    if vehicle.getMotorLoadPercentage ~= nil then
        local load = vehicle:getMotorLoadPercentage()
        if load ~= nil then
            return math.min(math.max(load, 0.0), 1.0)
        end
    end
    -- Fallback: read straight off the motor object
    local spec = vehicle.spec_motorized
    if spec ~= nil and spec.smoothedLoadPercentage ~= nil then
        return math.min(math.max(spec.smoothedLoadPercentage, 0.0), 1.0)
    end
    -- Last resort: legacy getConsumingLoad if present
    if vehicle.getConsumingLoad ~= nil then
        local load = vehicle:getConsumingLoad()
        if load ~= nil then
            return math.min(math.max(load, 0.0), 1.0)
        end
    end
    return 0.0
end

-- =============================================================
-- RPM FRACTION
-- Returns how close the engine is to its maximum RPM (0.0 .. 1.0)
-- =============================================================
local function getRpmFraction(vehicle)
    if vehicle.getMotor == nil then return 0.0 end
    local motor = vehicle:getMotor()
    if motor == nil then return 0.0 end

    local maxRpm = motor:getMaxRpm()
    if maxRpm == nil or maxRpm <= 0 then return 0.0 end

    local curRpm = motor:getLastMotorRpm()
    if curRpm == nil then
        curRpm = motor:getEqualizedMotorRpm() or 0
    end

    return math.min(math.max(curRpm / maxRpm, 0.0), 1.0)
end

-- =============================================================
-- TOTAL MULTIPLIER FOR A VEHICLE
-- =============================================================
local function computeTotalMultiplier(vehicle)
    local RFC = RealFuelConsumption
    local cfg = RFC.settings

    -- 1. Base multiplier
    local mult = RFC.getBaseMultiplier()

    -- 2. Load bonus
    local loadFactor = RFC.getLoadFactor()
    if loadFactor > 0.0 then
        local loadFrac = getEngineLoadFraction(vehicle)
        mult = mult + (loadFrac * loadFactor)
    end

    -- 3. Work type bonus — now scaled by the implement's real work load
    --    when a PowerConsumer implement is attached. A tool sitting idle
    --    (raised, not working) contributes little; a tool pulling at full
    --    demand contributes the full bonus.
    if cfg.workBonusEnabled then
        local workBonus = getWorkBonus(vehicle)
        if workBonus > 0.0 then
            local implLoad = getImplementWorkLoad(vehicle)
            if implLoad ~= nil then
                -- Scale between a 40% floor and full bonus by real demand,
                -- so light work still costs something but heavy work costs
                -- the most. Floor keeps detected work always meaningful.
                local scaled = workBonus * (0.4 + 0.6 * implLoad)
                mult = mult + scaled
            else
                -- No PowerConsumer data (e.g. combine header) → flat bonus
                mult = mult + workBonus
            end
        end
    end

    -- 4. RPM sensitivity
    if cfg.rpmSensitivity then
        local rpmFrac    = getRpmFraction(vehicle)
        local rpmBonus   = rpmFrac * RFC.RPM_FACTOR
        mult = mult + rpmBonus
    end

    -- 5. Engine mode (Eco/Normal/Power) — multiplies the whole result.
    --    ECO gets a "guaranteed saving" treatment: FS25 inflates the base
    --    consumption when we lower ECO's rev ceiling (the engine sits
    --    proportionally higher on its reduced range and the game bills it
    --    as heavy load). That is a base-game artifact, not real physics —
    --    a real ECO map runs the engine in its efficient zone and always
    --    saves. So for ECO we compute a target multiplier that stays below
    --    NORMAL, but with a smaller saving the harder the engine works
    --    (under heavy load even a real ECO engine must give nearly all).
    if cfg.engineModesEnabled and RFCEngineModes ~= nil and RFCEngineModes.getModeFuelMult ~= nil then
        local modeMult = RFCEngineModes.getModeFuelMult(vehicle)

        if RFCEngineModes.isEcoMode ~= nil and RFCEngineModes.isEcoMode(vehicle) then
            -- Load-scaled saving: 20% saving at no load → 6% saving at full
            -- load. Guarantees ECO always ends up cheaper than NORMAL.
            local load = getEngineLoadFraction(vehicle)
            local saving = 0.20 - (0.14 * load)   -- 0.20 .. 0.06
            -- Apply the saving to the WHOLE computed multiplier so the final
            -- litres are always below the NORMAL equivalent, cancelling the
            -- base-game inflation.
            mult = mult * (1.0 - saving)
        else
            mult = mult * modeMult
        end
    end

    return mult
end

-- =============================================================
-- HOOK INTO Motorized.updateFuelUsage
-- Strategy: run the original function first (it subtracts fuel
-- based on vanilla consumption), then subtract the *extra* amount
-- corresponding to (multiplier - 1.0) times what was consumed.
-- This is safer than scaling 'dt' because it doesn't interfere
-- with FS25's own engine load / RPM calculations.
-- =============================================================
-- =============================================================
-- CORE HOOK — Motorized.updateConsumers
--
-- METHOD (fixed in v1.1.0.8):
-- IMPORTANT: FS25 has NO function called `updateFuelUsage`. Fuel is
-- burned inside `Motorized:updateConsumers(dt, accInput)`, which loops
-- over `spec.consumers` (NOT `consumersByFillType`) and consumes
-- `consumer.usage` for every entry with permanentConsumption.
--
-- We therefore wrap updateConsumers: scale each consumer's `usage`
-- up/down by the dynamic multiplier, let vanilla burn fuel, then
-- restore the originals so the scaling never compounds. This makes
-- the tank drain at the real rate AND makes the game's own
-- spec.lastFuelUsage (the L/h value every HUD reads) reflect it.
-- =============================================================
local originalUpdateConsumers = Motorized.updateConsumers

Motorized.updateConsumers = function(self, dt, accInput)
    local spec = self.spec_motorized

    -- No spec or no consumers → just run vanilla
    if spec == nil or spec.consumers == nil then
        return originalUpdateConsumers(self, dt, accInput)
    end

    -- Motor off → nothing to scale
    if self.getIsMotorStarted ~= nil and not self:getIsMotorStarted() then
        return originalUpdateConsumers(self, dt, accInput)
    end

    -- Compute this frame's dynamic multiplier
    local totalMult = computeTotalMultiplier(self)
    spec.rfcLastMult = totalMult  -- stash for the debug HUD readout

    -- Skip only when effectively 1.0 (no change either way). ECO can push
    -- totalMult below 1.0, so we must NOT bail on "<= 1.0".
    if totalMult > 0.999 and totalMult < 1.001 then
        return originalUpdateConsumers(self, dt, accInput)
    end

    -- 1) Scale every consumer's usage (diesel, DEF/AdBlue, methane, electric).
    --    We scale `usage` because that is exactly what updateConsumers
    --    multiplies by dt to compute fuel burned (Motorized.lua ~line 2119).
    local saved = {}
    for i, consumer in pairs(spec.consumers) do
        if consumer ~= nil and type(consumer.usage) == "number" then
            saved[i] = consumer.usage
            consumer.usage = consumer.usage * totalMult
        end
    end

    -- 2) Let vanilla consume using the scaled usage values. It also sets
    --    spec.lastFuelUsage / lastDefUsage from the scaled amount, so the
    --    HUD L/h automatically reflects the real (scaled) consumption.
    originalUpdateConsumers(self, dt, accInput)

    -- 3) Restore original usage so the scaling doesn't compound next frame.
    for i, usage in pairs(saved) do
        local consumer = spec.consumers[i]
        if consumer ~= nil then
            consumer.usage = usage
        end
    end
end
