HDPlaceableUpgrade = {}

HDPlaceableUpgrade.SPEC_NAME = g_currentModName .. ".hdBuildingUpgrade"
HDPlaceableUpgrade.SPEC_TABLE_NAME = "spec_" .. HDPlaceableUpgrade.SPEC_NAME

function HDPlaceableUpgrade.getProductionPoint(placeable, productionPoint)
    if productionPoint ~= nil then
        return productionPoint
    end
    if placeable == nil then
        return nil
    end
    local spec = placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    if spec ~= nil and spec.runtimeProductionPoint ~= nil then
        return spec.runtimeProductionPoint
    end
    if placeable.spec_productionPoint ~= nil then
        return placeable.spec_productionPoint.productionPoint
    end
    if placeable.spec_extendedProductionPoint ~= nil then
        return placeable.spec_extendedProductionPoint.productionPoint
    end
    return nil
end

function HDPlaceableUpgrade.getFillTypes(entries)
    local fillTypes = {}
    for key, entry in pairs(entries or {}) do
        if type(entry) == "table" then
            local fillType = entry.type or entry.fillTypeIndex or entry.fillTypeId
            if fillType ~= nil then
                fillTypes[fillType] = true
            end
        elseif type(entry) == "number" then
            fillTypes[entry] = true
        end
    end
    return fillTypes
end

function HDPlaceableUpgrade.isRelayProductionPoint(placeable, productionPoint)
    productionPoint = HDPlaceableUpgrade.getProductionPoint(placeable, productionPoint)
    if productionPoint == nil then
        return false
    end
    for _, production in pairs(productionPoint.productions or {}) do
        local inputs = HDPlaceableUpgrade.getFillTypes(production.inputs)
        local outputs = HDPlaceableUpgrade.getFillTypes(production.outputs)
        for fillType in pairs(inputs) do
            if outputs[fillType] then
                return true
            end
        end
    end
    return false
end

function HDPlaceableUpgrade.getMaxLevel(placeable)
    return HDUpgrade.DEFAULT_MAX_LEVEL
end

function HDPlaceableUpgrade.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(PlaceableSilo, specializations)
        or SpecializationUtil.hasSpecialization(PlaceableProductionPoint, specializations)
        or SpecializationUtil.hasSpecialization(PlaceableObjectStorage, specializations)
end

function HDPlaceableUpgrade.ensureRuntimeSpec(placeable, productionPoint)
    if placeable == nil then
        return nil
    end

    local spec = placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    if spec == nil then
        spec = {
            level = 1,
            base = nil,
            activatable = nil,
            productivities = {}
        }
        placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME] = spec
    end

    spec.productivities = spec.productivities or {}

    if productionPoint ~= nil then
        spec.runtimeProductionPoint = productionPoint
        if spec.base ~= nil and not HDPlaceableUpgrade.hasUpgradeableBase(spec.base) then
            spec.base = nil
        end
    end

    HDPlaceableUpgrade.captureBaseValues(placeable)
    return spec
end

function HDPlaceableUpgrade.getLevelFromXML(xmlFile, key, defaultLevel)
    local level = nil
    if xmlFile ~= nil and xmlFile.handle ~= nil and getXMLInt ~= nil then
        level = getXMLInt(xmlFile.handle, key)
    end
    if type(level) ~= "number" then
        level = defaultLevel or 1
    end
    return math.clamp(level, 1, HDUpgrade.MAX_LEVEL)
end

function HDPlaceableUpgrade.registerEventListeners(placeableType)
    SpecializationUtil.registerEventListener(placeableType, "onLoad", HDPlaceableUpgrade)
    SpecializationUtil.registerEventListener(placeableType, "onPostLoad", HDPlaceableUpgrade)
    SpecializationUtil.registerEventListener(placeableType, "onFinalizePlacement", HDPlaceableUpgrade)
    SpecializationUtil.registerEventListener(placeableType, "onDelete", HDPlaceableUpgrade)
    SpecializationUtil.registerEventListener(placeableType, "onInfoTriggerEnter", HDPlaceableUpgrade)
    SpecializationUtil.registerEventListener(placeableType, "onInfoTriggerLeave", HDPlaceableUpgrade)
    SpecializationUtil.registerEventListener(placeableType, "loadFromXMLFile", HDPlaceableUpgrade)
    SpecializationUtil.registerEventListener(placeableType, "saveToXMLFile", HDPlaceableUpgrade)
end

function HDPlaceableUpgrade:onLoad(savegame)
    local spec = {
        level = 1,
        base = nil,
        activatable = nil,
        productivities = {}
    }
    self[HDPlaceableUpgrade.SPEC_TABLE_NAME] = spec

    HDPlaceableUpgrade.captureBaseValues(self)

end

function HDPlaceableUpgrade:onPostLoad(savegame)
    local spec = self[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    if spec ~= nil and not HDPlaceableUpgrade.hasUpgradeableBase(spec.base) then
        spec.base = nil
    end
    HDPlaceableUpgrade.captureBaseValues(self)
end

function HDPlaceableUpgrade:onFinalizePlacement()
    HDPlaceableUpgrade.captureBaseValues(self)
end

function HDPlaceableUpgrade:onDelete()
    local spec = self[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    if spec ~= nil and spec.activatable ~= nil and g_currentMission ~= nil then
        g_currentMission.activatableObjectsSystem:removeActivatable(spec.activatable)
    end
end

function HDPlaceableUpgrade:loadFromXMLFile(xmlFile, key)
    local spec = self[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    if spec ~= nil and g_currentMission:getIsServer() then
        spec.level = HDPlaceableUpgrade.getLevelFromXML(xmlFile, key .. ".hdBuildingUpgrade#level", spec.level)
        HDPlaceableUpgrade.applyLevel(self, spec.level)
    end
end

function HDPlaceableUpgrade:saveToXMLFile(xmlFile, key, usedModNames)
    local spec = self[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    if spec ~= nil and spec.level > 1 and xmlFile ~= nil and xmlFile.handle ~= nil and setXMLInt ~= nil then
        setXMLInt(xmlFile.handle, key .. ".hdBuildingUpgrade#level", spec.level)
    end
end

function HDPlaceableUpgrade.collectStorages(placeable)
    local storages = {}

    if placeable.spec_silo ~= nil and placeable.spec_silo.storages ~= nil then
        for _, storage in ipairs(placeable.spec_silo.storages) do
            table.insert(storages, storage)
        end
    end

    if placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint ~= nil then
        local storage = placeable.spec_productionPoint.productionPoint.storage
        if storage ~= nil then
            table.insert(storages, storage)
        end
    end

    if placeable.spec_extendedProductionPoint ~= nil
        and placeable.spec_extendedProductionPoint.productionPoint ~= nil then
        local storage = placeable.spec_extendedProductionPoint.productionPoint.storage
        if storage ~= nil then
            table.insert(storages, storage)
        end
    end

    local spec = placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    local runtimeStorage = spec ~= nil
        and spec.runtimeProductionPoint ~= nil
        and spec.runtimeProductionPoint.storage
        or nil
    if runtimeStorage ~= nil then
        local alreadyAdded = false
        for _, storage in ipairs(storages) do
            if storage == runtimeStorage then
                alreadyAdded = true
                break
            end
        end
        if not alreadyAdded then
            table.insert(storages, runtimeStorage)
        end
    end

    return storages
end

function HDPlaceableUpgrade.captureBaseValues(placeable)
    local spec = placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    if spec == nil or spec.base ~= nil then
        return
    end

    local base = {
        storages = {},
        production = nil,
        objectStorageCapacity = nil,
        price = placeable.price or 0
    }
    for _, storage in ipairs(HDPlaceableUpgrade.collectStorages(placeable)) do
        local entry = { storage = storage, capacity = storage.capacity, capacities = {} }
        if storage.capacities ~= nil then
            for fillType, capacity in pairs(storage.capacities) do
                entry.capacities[fillType] = capacity
            end
        end
        table.insert(base.storages, entry)
    end

    if placeable.spec_objectStorage ~= nil then
        base.objectStorageCapacity = placeable.spec_objectStorage.capacity
    end

    local productionPoint = spec.runtimeProductionPoint
    if productionPoint == nil and placeable.spec_productionPoint ~= nil then
        productionPoint = placeable.spec_productionPoint.productionPoint
    elseif productionPoint == nil and placeable.spec_extendedProductionPoint ~= nil then
        productionPoint = placeable.spec_extendedProductionPoint.productionPoint
    end

    if productionPoint ~= nil and productionPoint.productions ~= nil then
        base.production = {}
        for _, production in pairs(productionPoint.productions) do
            table.insert(base.production, {
                production = production,
                id = production.id,
                perMinute = production.cyclesPerMinute,
                perHour = production.cyclesPerHour,
                perMonth = production.cyclesPerMonth,
                costsMinute = production.costsPerActiveMinute,
                costsHour = production.costsPerActiveHour,
                costsMonth = production.costsPerActiveMonth
            })
        end
    end

    spec.base = base
end

function HDPlaceableUpgrade.hasUpgradeableBase(base)
    return base ~= nil and (
        #base.storages > 0
        or base.objectStorageCapacity ~= nil
        or (base.production ~= nil and #base.production > 0)
    )
end

function HDPlaceableUpgrade.getStorageEntries(placeable)
    local spec = HDPlaceableUpgrade.ensureRuntimeSpec(placeable)
    if spec == nil or not HDPlaceableUpgrade.hasUpgradeableBase(spec.base) then
        return nil
    end

    local storages = spec.base.storages
    if storages == nil then
        return nil
    end

    local seen = {}
    local deduped = {}
    for _, entry in ipairs(storages) do
        if entry.storage ~= nil and not seen[entry.storage] then
            seen[entry.storage] = true
            table.insert(deduped, entry)
        end
    end
    return deduped
end

function HDPlaceableUpgrade.isLargeBiogasPlant(placeable)
    local productionPoint = HDPlaceableUpgrade.getProductionPoint(placeable)
    for _, production in pairs(productionPoint ~= nil and productionPoint.productions or {}) do
        if production.id == "biogas" then
            for _, input in pairs(production.inputs or {}) do
                if type(input) == "table" and (input.amount or 0) >= 17000 then
                    return true
                end
            end
        end
    end
    return false
end

function HDPlaceableUpgrade.getMaxProductivity(placeable)
    local spec = placeable ~= nil and placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME] or nil
    local maximum = 100
    for _, productivity in pairs(spec ~= nil and spec.productivities or {}) do
        maximum = math.max(maximum, productivity)
    end
    return maximum
end

function HDPlaceableUpgrade.getBaseAggregateCapacity(placeable)
    local spec = placeable ~= nil and placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME] or nil
    local total = 0
    for _, entry in ipairs(spec ~= nil and spec.base ~= nil and spec.base.storages or {}) do
        if next(entry.capacities or {}) ~= nil then
            for _, capacity in pairs(entry.capacities) do
                total = total + math.max(capacity or 0, 0)
            end
        else
            total = total + math.max(entry.capacity or 0, 0)
        end
    end
    return total
end

function HDPlaceableUpgrade.getCapacityMultiplier(placeable, level)
    local multiplier
    if HDPlaceableUpgrade.isLargeBiogasPlant(placeable) then
        multiplier = 1 + (level - 1) * HDUpgrade.LARGE_BGA_CAPACITY_MULTIPLIER_PER_LEVEL
    else
        multiplier = 1 + (level - 1) * HDUpgrade.CAPACITY_MULTIPLIER_PER_LEVEL
    end

    local spec = placeable ~= nil and placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME] or nil
    if spec ~= nil and spec.base ~= nil and spec.base.production ~= nil then
        multiplier = multiplier * HDPlaceableUpgrade.getMaxProductivity(placeable) / 100
    end

    local baseTotal = HDPlaceableUpgrade.getBaseAggregateCapacity(placeable)
    if baseTotal > 0 then
        multiplier = math.min(multiplier, HDUpgrade.MAX_SAFE_AGGREGATE_CAPACITY / baseTotal)
    end
    return multiplier
end

function HDPlaceableUpgrade.getProductionMultiplier(level)
    return 1 + (level - 1) * HDUpgrade.PRODUCTION_MULTIPLIER_PER_LEVEL
end

function HDPlaceableUpgrade.getProductivity(placeable, productionId)
    local spec = placeable ~= nil and placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME] or nil
    return spec ~= nil and spec.productivities ~= nil and spec.productivities[productionId] or 100
end

function HDPlaceableUpgrade.isValidProductivity(productivity)
    for _, option in ipairs(HDUpgrade.PRODUCTIVITY_OPTIONS) do
        if option == productivity then
            return true
        end
    end
    return false
end

function HDPlaceableUpgrade.setProductivity(placeable, productionId, productivity)
    if not HDPlaceableUpgrade.isValidProductivity(productivity) then
        return false
    end
    local spec = HDPlaceableUpgrade.ensureRuntimeSpec(placeable)
    if spec == nil or spec.base == nil or spec.base.production == nil then
        return false
    end
    local found = false
    for _, entry in ipairs(spec.base.production) do
        if entry.id == productionId then
            found = true
            break
        end
    end
    if not found then
        return false
    end
    spec.productivities[productionId] = productivity
    HDPlaceableUpgrade.applyLevel(placeable, spec.level)
    return true
end

function HDPlaceableUpgrade.applyLevel(placeable, level)
    local spec = placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    if spec == nil then
        return
    end

    HDPlaceableUpgrade.captureBaseValues(placeable)
    if spec.base == nil then
        return
    end

    level = math.clamp(level, 1, HDPlaceableUpgrade.getMaxLevel(placeable))
    local capacityMultiplier = HDPlaceableUpgrade.getCapacityMultiplier(placeable, level)

    for _, entry in ipairs(spec.base.storages) do
        local storage = entry.storage
        if entry.capacity ~= nil then
            storage.capacity = entry.capacity * capacityMultiplier
        end
        if storage.capacities ~= nil then
            for fillType, capacity in pairs(entry.capacities) do
                storage.capacities[fillType] = capacity * capacityMultiplier
            end
        end
    end

    if spec.base.objectStorageCapacity ~= nil and placeable.spec_objectStorage ~= nil then
        placeable.spec_objectStorage.capacity = math.floor(spec.base.objectStorageCapacity * capacityMultiplier)
    end

    if spec.base.production ~= nil then
        local productionMultiplier = HDPlaceableUpgrade.getProductionMultiplier(level)
        for _, entry in ipairs(spec.base.production) do
            local production = entry.production
            local productivityMultiplier = HDPlaceableUpgrade.getProductivity(placeable, entry.id) / 100
            local combinedMultiplier = productionMultiplier * productivityMultiplier
            production.cyclesPerMinute = (entry.perMinute or 0) * combinedMultiplier
            production.cyclesPerHour = (entry.perHour or 0) * combinedMultiplier
            production.cyclesPerMonth = (entry.perMonth or 0) * combinedMultiplier
            production.costsPerActiveMinute = (entry.costsMinute or 0) * combinedMultiplier
            production.costsPerActiveHour = (entry.costsHour or 0) * combinedMultiplier
            production.costsPerActiveMonth = (entry.costsMonth or 0) * combinedMultiplier
        end
    end

    spec.level = level
end

function HDPlaceableUpgrade.getUpgradePrice(placeable)
    local spec = placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    if spec == nil or spec.base == nil then
        return 0
    end
    return math.max(1000, math.floor(spec.base.price * HDUpgrade.UPGRADE_PRICE_FACTOR * spec.level))
end

function HDPlaceableUpgrade.canUpgrade(placeable, farmId)
    local spec = placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    return not HDPlaceableUpgrade.isRelayProductionPoint(placeable)
        and spec ~= nil
        and HDPlaceableUpgrade.hasUpgradeableBase(spec.base)
        and spec.level < HDPlaceableUpgrade.getMaxLevel(placeable)
        and placeable:getOwnerFarmId() == farmId
end

function HDPlaceableUpgrade.upgrade(placeable, farmId)
    if not HDPlaceableUpgrade.canUpgrade(placeable, farmId) then
        return false, "hd_upgrade_max"
    end

    local price = HDPlaceableUpgrade.getUpgradePrice(placeable)
    local farm = g_farmManager:getFarmById(farmId)
    if farm == nil or farm.money < price then
        return false, "hd_upgrade_money"
    end

    g_currentMission:addMoney(-price, farmId, MoneyType.SHOP_PROPERTY_BUY, true, true)
    local spec = placeable[HDPlaceableUpgrade.SPEC_TABLE_NAME]
    HDPlaceableUpgrade.applyLevel(placeable, spec.level + 1)
    return true, nil
end

function HDPlaceableUpgrade:onInfoTriggerEnter(otherId)
    -- Upgrades are handled from the production menu.
end

function HDPlaceableUpgrade:onInfoTriggerLeave(otherId)
    -- Activatable objects remain registered and decide visibility by distance.
end
