-- BunkerSiloExtension
-- Version: 1.1.0.0
-- Author: Tidde

if g_bunkerSiloExtension ~= nil then
    bunkerSiloExtension = g_bunkerSiloExtension
else
    bunkerSiloExtension = {}
    g_bunkerSiloExtension = bunkerSiloExtension
end
if bunkerSiloExtension._initialized then
    return
end
bunkerSiloExtension._initialized = true

bunkerSiloExtension.debug = false

bunkerSiloExtension.switchPolicy = {
    liters = 1000
}

bunkerSiloExtension.luaMappings = {
    { baseFillType = "CHAFF", input = "SILO_GRASS",   output = "GRASS_FERMENTED",   extraFillTypes = { "GRASS_WINDROW", "SILO_GRASS" } },
    { baseFillType = "CHAFF", input = "CHOPPEDMAIZE",   output = "CHOPPEDMAIZE_FERMENTED",   extraFillTypes = { "CHOPPEDMAIZE" } },
    { baseFillType = "CHAFF", input = "SILO_ALFALFA", output = "ALFALFA_FERMENTED", extraFillTypes = { "SILO_ALFALFA", "ALFALFA_WINDROW" } },
    { baseFillType = "CHAFF", input = "SILO_CLOVER",  output = "CLOVER_FERMENTED",  extraFillTypes = { "SILO_CLOVER", "CLOVER_WINDROW" } },
    { baseFillType = "CHAFF", input = "CCMRAW",       output = "CCM",               extraFillTypes = { "CCMRAW" } },
    { baseFillType = "CHAFF", input = "COMPOSTRAW",   output = "COMPOST",           extraFillTypes = { "COMPOSTRAW" } },
}

bunkerSiloExtension.beforeSwitchLookup = {}
bunkerSiloExtension.baseToExtras       = {}

function bunkerSiloExtension:getSwitchThreshold(silo)
    local threshold = self.switchPolicy and self.switchPolicy.liters or 1000
    if threshold == nil or threshold < 1 then
        threshold = 1000
    end
    return threshold
end

local function getFillTypeIndex(name)
    if name == nil or name == "" then
        return nil
    end
    return g_fillTypeManager:getFillTypeIndexByName(string.upper(name))
end

local function acceptFillType(silo, index)
    if index ~= nil then
        silo.acceptedFillTypes[index] = true
    end
end

local function getSiloTotalFillLevel(silo)
    if silo.fillLevel ~= nil then
        return silo.fillLevel
    end

    local area = silo.bunkerSiloArea
    if area == nil then
        return 0
    end

    local sum = 0
    for fillTypeIndex, isAccepted in pairs(silo.acceptedFillTypes or {}) do
        if isAccepted then
            local areaLevel = DensityMapHeightUtil.getFillLevelAtArea(fillTypeIndex, area.sx, area.sz, area.wx, area.wz, area.hx, area.hz) or 0
            sum = sum + areaLevel
        end
    end
    return sum
end

local function resetSiloFillTypesToDefault(silo)
    if silo.bunkerSiloArea ~= nil then
        g_densityMapHeightManager:removeConvertingFillTypeAreas(silo.bunkerSiloArea)
    end

    silo.inputFillType  = silo._bunkerSiloExtension_defaultInput  or silo.inputFillType
    silo.outputFillType = silo._bunkerSiloExtension_defaultOutput or silo.outputFillType

    if silo.bunkerSiloArea ~= nil then
        g_densityMapHeightManager:setConvertingFillTypeAreas(silo.bunkerSiloArea, silo.acceptedFillTypes, silo.inputFillType)
    end

    silo._bunkerSiloExtension_locked = false
end

local function probeVehicleFillType(vehicle)
    local fillUnitSpec = vehicle and vehicle.spec_fillUnit
    if not fillUnitSpec or not fillUnitSpec.fillUnits then
        return nil
    end

    local bestType, bestLevel = nil, 0
    for _, fillUnit in ipairs(fillUnitSpec.fillUnits) do
        local level = fillUnit.fillLevel or 0
        local fillType = fillUnit.fillType
        if level > bestLevel and fillType and fillType ~= FillType.UNKNOWN then
            bestLevel, bestType = level, fillType
        end
    end
    return bestType
end

function bunkerSiloExtension:loadMap()
    self.beforeSwitchLookup = {}
    self.baseToExtras       = {}

    for _, mapping in ipairs(self.luaMappings) do
        local baseIndex   = getFillTypeIndex(mapping.baseFillType)
        local inputIndex  = getFillTypeIndex(mapping.input)
        local outputIndex = getFillTypeIndex(mapping.output)

        if baseIndex and inputIndex and outputIndex then
            local extras = self.baseToExtras[baseIndex]
            if extras == nil then
                extras = {}
                self.baseToExtras[baseIndex] = extras
            end

            for _, extraName in ipairs(mapping.extraFillTypes or {}) do
                local extraIndex = getFillTypeIndex(extraName)
                if extraIndex then
                    self.beforeSwitchLookup[extraIndex] = { inIdx = inputIndex, outIdx = outputIndex }
                    extras[extraIndex] = true
                end
            end
        end
    end
end
addModEventListener(bunkerSiloExtension)

-- ---------------------------------------------------------
-- LOAD
-- ---------------------------------------------------------
local function onBunkerSiloLoad(silo, superFunc, components, xmlFile, key, i3dMappings)
    local success = superFunc(silo, components, xmlFile, key, i3dMappings)

    for baseIndex, extras in pairs(bunkerSiloExtension.baseToExtras) do
        if silo.acceptedFillTypes[baseIndex] then
            for extraIndex, _ in pairs(extras) do
                acceptFillType(silo, extraIndex)
            end
        end
    end

    if silo.bunkerSiloArea ~= nil then
        g_densityMapHeightManager:setConvertingFillTypeAreas(silo.bunkerSiloArea, silo.acceptedFillTypes, silo.inputFillType)
    end

    silo._bunkerSiloExtension_defaultInput  = silo.inputFillType
    silo._bunkerSiloExtension_defaultOutput = silo.outputFillType
    silo._bunkerSiloExtension_locked        = false

    return success
end
BunkerSilo.load = Utils.overwrittenFunction(BunkerSilo.load, onBunkerSiloLoad)

-- ---------------------------------------------------------
-- FILL TYPE SWITCH
-- ---------------------------------------------------------
local function trySwitchSiloFillType(silo, vehicle)
    if silo._bunkerSiloExtension_locked then
        return
    end

    local threshold = bunkerSiloExtension:getSwitchThreshold(silo)
    local currentFillLevel = getSiloTotalFillLevel(silo)
    if currentFillLevel > threshold then
        return
    end

    local triggerFillType = probeVehicleFillType(vehicle)
    if not triggerFillType then
        return
    end

    local mappedIO = bunkerSiloExtension.beforeSwitchLookup[triggerFillType]
    if not mappedIO or not mappedIO.inIdx or not mappedIO.outIdx then
        return
    end

    if mappedIO.inIdx == silo.inputFillType then
        return
    end

    if silo.bunkerSiloArea ~= nil then
        g_densityMapHeightManager:removeConvertingFillTypeAreas(silo.bunkerSiloArea)
        g_densityMapHeightManager:setConvertingFillTypeAreas(silo.bunkerSiloArea, silo.acceptedFillTypes, mappedIO.inIdx)
    end

    silo.inputFillType  = mappedIO.inIdx
    silo.outputFillType = mappedIO.outIdx
end

-- ---------------------------------------------------------
-- INTERACTION TRIGGER
-- ---------------------------------------------------------
local originalInteractionTriggerCallback = BunkerSilo.interactionTriggerCallback
BunkerSilo.interactionTriggerCallback = function(silo, triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
    local vehicle = g_currentMission.nodeToObject[otherId] or g_currentMission.nodeToObject[otherShapeId]
    if vehicle ~= nil then
        if onEnter or onStay then
            trySwitchSiloFillType(silo, vehicle)
        end
    end

    if originalInteractionTriggerCallback ~= nil then
        return originalInteractionTriggerCallback(silo, triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
    end
end

-- ---------------------------------------------------------
-- UPDATE (Lock-Logik + HUD)
-- ---------------------------------------------------------
local function onBunkerSiloUpdate(silo, superFunc, dt)
    local threshold = bunkerSiloExtension:getSwitchThreshold(silo)

    -- Lock-Reset wenn Silo leer
    if silo._bunkerSiloExtension_locked and getSiloTotalFillLevel(silo) <= 0 then
        resetSiloFillTypesToDefault(silo)
    end

    -- Silo sperren wenn Schwellwert überschritten
    if not silo._bunkerSiloExtension_locked then
        if getSiloTotalFillLevel(silo) > threshold then
            silo._bunkerSiloExtension_locked = true
        end
    end

    -- Eigenes HUD vorbereiten
    local canInteract = false
    local localPlayer = g_localPlayer
    if localPlayer ~= nil then
        if not localPlayer:getIsInVehicle() and silo.playerInRange then
            canInteract = true
        else
            local playerVehicle = localPlayer:getCurrentVehicle()
            if playerVehicle ~= nil and silo.vehiclesInRange ~= nil then
                for vehicle in pairs(silo.vehiclesInRange) do
                    if vehicle:getIsActiveForInput(true) and vehicle == playerVehicle then
                        canInteract = true
                        break
                    end
                end
            end
        end
    end

    local originalAddExtraPrintText = g_currentMission.addExtraPrintText
    g_currentMission.addExtraPrintText = function() end
    local success, result = pcall(superFunc, silo, dt)
    g_currentMission.addExtraPrintText = originalAddExtraPrintText
    if not success then
        error(result)
    end

    if canInteract then
        local fillTypeIndex = silo.inputFillType
        if silo.state == BunkerSilo.STATE_CLOSED
        or silo.state == BunkerSilo.STATE_FERMENTED
        or silo.state == BunkerSilo.STATE_DRAIN then
            fillTypeIndex = silo.outputFillType
        end

        local fillTypeName = ""
        local fillType = g_fillTypeManager:getFillTypeByIndex(fillTypeIndex)
        if fillType ~= nil then
            fillTypeName = fillType.title
        end

        local text = string.format("%s: %d", fillTypeName, math.floor(getSiloTotalFillLevel(silo)))

        if silo.state == BunkerSilo.STATE_FILL then
            text = text .. string.format(" | %s: %d%%",
                g_i18n:getText("info_compacting"), silo.compactedPercent or 0)
        elseif silo.state == BunkerSilo.STATE_CLOSED
            or silo.state == BunkerSilo.STATE_FERMENTED then
            text = text .. string.format(" | %s: %d%%",
                g_i18n:getText("info_fermenting"), math.ceil((silo.fermentingPercent or 0) * 100))
        end

        g_currentMission:addExtraPrintText(text)
    end

    return result
end
BunkerSilo.update = Utils.overwrittenFunction(BunkerSilo.update, onBunkerSiloUpdate)

if BunkerSilo.setIsFermenting ~= nil then
    local originalSetIsFermenting = BunkerSilo.setIsFermenting
    function BunkerSilo:setIsFermenting(isFermenting)
        return originalSetIsFermenting(self, isFermenting)
    end
end

if BunkerSilo.setCovered ~= nil then
    local originalSetCovered = BunkerSilo.setCovered
    function BunkerSilo:setCovered(covered, noEventSend)
        if not covered and self._bunkerSiloExtension_locked and getSiloTotalFillLevel(self) <= 0 then
            resetSiloFillTypesToDefault(self)
        end
        return originalSetCovered(self, covered, noEventSend)
    end
end

function bunkerSiloExtension.registerSavegameXMLPaths(schema, basePath)
    schema:register(XMLValueType.STRING, basePath .. "#bunkerSiloExtensionInputFillType",  "bunkerSiloExtension: current input fillType name")
    schema:register(XMLValueType.STRING, basePath .. "#bunkerSiloExtensionOutputFillType", "bunkerSiloExtension: current output fillType name")
end
BunkerSilo.registerSavegameXMLPaths = Utils.appendedFunction(BunkerSilo.registerSavegameXMLPaths, bunkerSiloExtension.registerSavegameXMLPaths)

function bunkerSiloExtension.saveToXMLFile(silo, xmlFile, key, usedModNames)
    if silo.inputFillType ~= nil and silo.outputFillType ~= nil then
        local inName  = g_fillTypeManager:getFillTypeByIndex(silo.inputFillType).name
        local outName = g_fillTypeManager:getFillTypeByIndex(silo.outputFillType).name
        xmlFile:setValue(key .. "#bunkerSiloExtensionInputFillType",  inName)
        xmlFile:setValue(key .. "#bunkerSiloExtensionOutputFillType", outName)
    end
end
BunkerSilo.saveToXMLFile = Utils.appendedFunction(BunkerSilo.saveToXMLFile, bunkerSiloExtension.saveToXMLFile)

local function onBunkerSiloLoadFromXMLFile(silo, superFunc, xmlFile, key)
    local success = superFunc(silo, xmlFile, key)
    local inName  = xmlFile:getValue(key .. "#bunkerSiloExtensionInputFillType")
    local outName = xmlFile:getValue(key .. "#bunkerSiloExtensionOutputFillType")

    if inName and outName then
        local inputIndex  = getFillTypeIndex(inName)
        local outputIndex = getFillTypeIndex(outName)
        if inputIndex and outputIndex then
            if silo.bunkerSiloArea ~= nil then
                g_densityMapHeightManager:removeConvertingFillTypeAreas(silo.bunkerSiloArea)
            end
            silo.inputFillType  = inputIndex
            silo.outputFillType = outputIndex
            if silo.bunkerSiloArea ~= nil then
                g_densityMapHeightManager:setConvertingFillTypeAreas(silo.bunkerSiloArea, silo.acceptedFillTypes, silo.inputFillType)
            end
        end
    end
    return success
end
BunkerSilo.loadFromXMLFile = Utils.overwrittenFunction(BunkerSilo.loadFromXMLFile, onBunkerSiloLoadFromXMLFile)

function bunkerSiloExtension.writeStream(silo, streamId, connection)
    if not connection:getIsServer() then
        streamWriteUIntN(streamId, silo.inputFillType  or FillType.UNKNOWN, FillTypeManager.SEND_NUM_BITS)
        streamWriteUIntN(streamId, silo.outputFillType or FillType.UNKNOWN, FillTypeManager.SEND_NUM_BITS)
    end
end
BunkerSilo.writeStream = Utils.appendedFunction(BunkerSilo.writeStream, bunkerSiloExtension.writeStream)

function bunkerSiloExtension.readStream(silo, streamId, connection, objectId)
    if connection:getIsServer() then
        local inputIndex  = streamReadUIntN(streamId, FillTypeManager.SEND_NUM_BITS)
        local outputIndex = streamReadUIntN(streamId, FillTypeManager.SEND_NUM_BITS)

        if inputIndex and outputIndex then
            if silo.bunkerSiloArea ~= nil then
                g_densityMapHeightManager:removeConvertingFillTypeAreas(silo.bunkerSiloArea)
            end
            silo.inputFillType  = inputIndex
            silo.outputFillType = outputIndex
            if silo.bunkerSiloArea ~= nil then
                g_densityMapHeightManager:setConvertingFillTypeAreas(silo.bunkerSiloArea, silo.acceptedFillTypes, silo.inputFillType)
            end
        end
    end
end
BunkerSilo.readStream = Utils.appendedFunction(BunkerSilo.readStream, bunkerSiloExtension.readStream)
