DiseaseFieldDamage = {}

local function clamp(v, a, b)
    return math.max(a, math.min(b, v))
end

local function getDamageDataForFruit(fruitIndex)
    if fruitIndex == nil or fruitIndex == FruitType.UNKNOWN or g_fruitTypeManager == nil then
        return nil
    end

    local desc = g_fruitTypeManager:getFruitTypeByIndex(fruitIndex)
    if desc == nil or desc.terrainDataPlaneId == nil then
        return nil
    end

    local groundTypeMapId = nil
    if g_currentMission ~= nil and g_currentMission.fieldGroundSystem ~= nil then
        groundTypeMapId = select(1, g_currentMission.fieldGroundSystem:getDensityMapData(FieldDensityMap.GROUND_TYPE))
    end

    return {
        desc = desc,
        modifier = DensityMapModifier.new(desc.terrainDataPlaneId, desc.startStateChannel, desc.numStateChannels, g_terrainNode),
        fruitFilter = DensityMapFilter.new(desc.terrainDataPlaneId, desc.startStateChannel, desc.numStateChannels),
        perlinFilter = groundTypeMapId ~= nil and PerlinNoiseFilter.new(groundTypeMapId, 11, 1, 0.5, math.random(0, 10000)) or nil
    }
end

local function executeDirectDeletePatch(damageData, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, threshold)
    if damageData == nil then
        return 0
    end

    damageData.modifier:setParallelogramWorldCoords(
        startWorldX, startWorldZ,
        widthWorldX, widthWorldZ,
        heightWorldX, heightWorldZ,
        DensityCoordType.POINT_POINT_POINT
    )
    damageData.modifier:setNewTypeIndexMode(DensityIndexCompareMode.ZERO)

    local changed = 0
    if damageData.perlinFilter ~= nil then
        damageData.perlinFilter:setValueCompareParams(DensityValueCompareType.GREATER, threshold)
        _, changed = damageData.modifier:executeSetWithStats(0, damageData.fruitFilter, damageData.perlinFilter)
    else
        _, changed = damageData.modifier:executeSetWithStats(0, damageData.fruitFilter)
    end

    return changed or 0
end

local function executeDirectDeletePatchRaw(damageData, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)
    if damageData == nil then
        return 0
    end

    damageData.modifier:setParallelogramWorldCoords(
        startWorldX, startWorldZ,
        widthWorldX, widthWorldZ,
        heightWorldX, heightWorldZ,
        DensityCoordType.POINT_POINT_POINT
    )
    damageData.modifier:setNewTypeIndexMode(DensityIndexCompareMode.ZERO)

    local changed = 0
    _, changed = damageData.modifier:executeSetWithStats(0, damageData.fruitFilter)
    return changed or 0
end

local function createDamageContext(field, diseaseState, risk, threatCategory, infectedSpots)
    if g_server == nil or field == nil or diseaseState == nil or diseaseState <= 0 then
        return nil
    end

    local fieldState = field:getFieldState()
    local fruitIndex = fieldState and fieldState.fruitTypeIndex or FruitType.UNKNOWN
    local growthState = fieldState and fieldState.growthState or 0
    local damageData = getDamageDataForFruit(fruitIndex)
    if damageData == nil then
        return nil
    end

    if damageData.desc ~= nil then
        local isAliveCrop = false
        if damageData.desc.getIsGrowing ~= nil and damageData.desc:getIsGrowing(growthState) then
            isAliveCrop = true
        elseif growthState >= (damageData.desc.minHarvestingGrowthState or 999)
            and growthState <= (damageData.desc.maxHarvestingGrowthState or -1) then
            isAliveCrop = true
        end
        if not isAliveCrop then
            return nil
        end
    end

    if infectedSpots == nil or #infectedSpots == 0 then
        return nil
    end

    -- A field can contain different live growth states while growth updates are being
    -- processed. Destroy any living/harvestable crop inside a severe disease patch.
    local minDestroyState = 1
    local maxDestroyState = math.max(growthState, damageData.desc.maxHarvestingGrowthState or growthState)
    damageData.fruitFilter:setValueCompareParams(DensityValueCompareType.BETWEEN, minDestroyState, maxDestroyState)
    local defs = _G.DiseaseDefs
    local tuning = defs and defs.TUNING or {}
    local destroyBias = 0
    if threatCategory == "pest" then
        destroyBias = tuning.damageDestroyBasePest or 0.08
    else
        destroyBias = tuning.damageDestroyBaseDisease or 0.03
    end
    destroyBias = destroyBias
        + math.max(0, diseaseState - 1) * (tuning.damageDestroyStateBonus or 0.05)
        + math.max(0, (risk or 0) - 0.55) * (tuning.damageDestroyRiskBonus or 0.12)
    destroyBias = clamp(destroyBias, tuning.damageDestroyBiasMin or 0.02, tuning.damageDestroyBiasMax or 0.28)

    return {
        field = field,
        diseaseState = diseaseState,
        risk = risk,
        threatCategory = threatCategory,
        infectedSpots = infectedSpots,
        damageData = damageData,
        fruitName = damageData.desc.name or "?",
        growthState = growthState,
        minDestroyState = minDestroyState,
        maxDestroyState = maxDestroyState,
        tuning = tuning,
        destroyBias = destroyBias,
        index = 1,
        currentSpotJob = nil,
        totalChanged = 0,
        totalDestroyed = 0
    }
end

local function createDamageSpotJob(context, spot)
    local damageData = context.damageData
    local tuning = context.tuning
    local diseaseState = context.diseaseState
    local risk = context.risk
    local destroyBias = context.destroyBias

    local spotSeverity = clamp(tonumber(spot.severity or diseaseState) or diseaseState, 1, 3)
    local isSevere = diseaseState >= 3
    local radiusBase = 1.35 + spotSeverity * 0.8
    local radiusMax = radiusBase + math.max(0.8, (risk or 0) * 2.8)
    local radius = MathUtil.randomFloat(radiusBase, radiusMax)
    local x = spot.x
    local z = spot.z
    if x == nil or z == nil then
        return nil
    end

    local canDestroy = tuning.damageEnableHardDestroy ~= false
        and diseaseState >= (tuning.damageDestroyMinStage or 2)
        and spotSeverity >= diseaseState

    local destroyAllowed = canDestroy
    local destroyRadius = nil
    local destroyThreshold = nil
    local islandCount = 0
    if destroyAllowed then
        local radiusMinFactor = isSevere
            and (tuning.damageDestroyRadiusMinFactor or 0.24)
            or (tuning.damageStage2DestroyRadiusMinFactor or 0.10)
        local radiusMaxFactor = isSevere
            and (tuning.damageDestroyRadiusMaxFactor or 0.45)
            or (tuning.damageStage2DestroyRadiusMaxFactor or 0.18)
        local thresholdMin = isSevere
            and (tuning.damageDestroyThresholdMin or 0.18)
            or (tuning.damageStage2DestroyThresholdMin or 0.28)
        local thresholdMax = isSevere
            and (tuning.damageDestroyThresholdMax or 0.46)
            or (tuning.damageStage2DestroyThresholdMax or 0.52)
        local islandsMin = isSevere
            and (tuning.damageDestroyIslandsMin or 1)
            or (tuning.damageStage2DestroyIslandsMin or 1)
        local islandsMax = isSevere
            and (tuning.damageDestroyIslandsMax or 3)
            or (tuning.damageStage2DestroyIslandsMax or 1)
        destroyRadius = radius * MathUtil.randomFloat(
            radiusMinFactor,
            radiusMaxFactor + destroyBias * 0.10
        )
        destroyThreshold = math.floor(clamp(
            thresholdMin
                + (1 - destroyBias) * (thresholdMax - thresholdMin)
                - spotSeverity * 0.006,
            thresholdMin,
            thresholdMax
        ) * 10000)
        islandCount = math.random(
            islandsMin,
            math.max(islandsMin, islandsMax)
        )
    end

    return {
        x = x,
        z = z,
        radius = radius,
        spotSeverity = spotSeverity,
        destroyAllowed = destroyAllowed,
        destroyRadius = destroyRadius,
        destroyThreshold = destroyThreshold,
        islandCount = islandCount,
        islandIndex = 1,
        fallbackDestroyDone = false,
        totalChanged = 0,
        totalDestroyed = 0
    }
end

local function stepDamageSpotJob(context, spotJob)
    local damageData = context.damageData
    local tuning = context.tuning
    local diseaseState = context.diseaseState
    local isSevere = diseaseState >= 3
    local changed = 0
    local destroyed = 0

    if spotJob.destroyAllowed and spotJob.islandIndex <= spotJob.islandCount then
        local localRadius = spotJob.destroyRadius * MathUtil.randomFloat(
            isSevere and (tuning.damageDestroyIslandSizeMinFactor or 0.70)
                or (tuning.damageStage2DestroyIslandSizeMinFactor or 0.38),
            isSevere and (tuning.damageDestroyIslandSizeMaxFactor or 1.10)
                or (tuning.damageStage2DestroyIslandSizeMaxFactor or 0.62)
        )
        local offsetFactor = tuning.damageDestroyIslandOffsetFactor or 0.45
        local maxOffset = math.max(0, spotJob.radius - localRadius) * offsetFactor
        local angle = MathUtil.randomFloat(0, math.pi * 2)
        local distance = MathUtil.randomFloat(0, maxOffset)
        local cx = spotJob.x + math.cos(angle) * distance
        local cz = spotJob.z + math.sin(angle) * distance
        destroyed = executeDirectDeletePatch(
            damageData,
            cx - localRadius, cz - localRadius,
            cx + localRadius, cz - localRadius,
            cx - localRadius, cz + localRadius,
            spotJob.destroyThreshold
        )
        changed = changed + destroyed
        spotJob.islandIndex = spotJob.islandIndex + 1
    elseif diseaseState >= 2 and spotJob.totalDestroyed <= 0 and spotJob.destroyAllowed
        and not spotJob.fallbackDestroyDone then
        local fallbackRadius = isSevere
            and math.max(0.55, spotJob.radius * 0.20)
            or math.max(0.28, spotJob.radius * 0.10)
        destroyed = executeDirectDeletePatchRaw(
            damageData,
            spotJob.x - fallbackRadius, spotJob.z - fallbackRadius,
            spotJob.x + fallbackRadius, spotJob.z - fallbackRadius,
            spotJob.x - fallbackRadius, spotJob.z + fallbackRadius
        )
        changed = changed + destroyed
        spotJob.fallbackDestroyDone = true
    else
        return true, 0, 0, 0
    end

    spotJob.totalChanged = (spotJob.totalChanged or 0) + (changed or 0)
    spotJob.totalDestroyed = (spotJob.totalDestroyed or 0) + (destroyed or 0)

    local finished = false
    if spotJob.destroyAllowed and spotJob.islandIndex <= spotJob.islandCount then
        finished = false
    elseif diseaseState >= 2 and spotJob.totalDestroyed <= 0 and spotJob.destroyAllowed and not spotJob.fallbackDestroyDone then
        finished = false
    else
        finished = true
    end

    return finished, changed, 0, destroyed
end

function DiseaseFieldDamage:createDamageJob(field, diseaseState, risk, threatCategory, infectedSpots)
    return createDamageContext(field, diseaseState, risk, threatCategory, infectedSpots)
end

function DiseaseFieldDamage:stepDamageJob(job, maxOpsPerStep)
    if job == nil then
        return true, 0, 0, 0
    end

    local opsBudget = math.max(1, tonumber(maxOpsPerStep or 1) or 1)
    local processed = 0
    local changedThisStep = 0
    local destroyedThisStep = 0

    while job.index <= #job.infectedSpots and processed < opsBudget do
        if job.currentSpotJob == nil then
            job.currentSpotJob = createDamageSpotJob(job, job.infectedSpots[job.index])
            if job.currentSpotJob == nil then
                job.index = job.index + 1
            end
        end

        if job.currentSpotJob ~= nil then
            local spotFinished, changed, _, destroyed = stepDamageSpotJob(job, job.currentSpotJob)
            changedThisStep = changedThisStep + (changed or 0)
            destroyedThisStep = destroyedThisStep + (destroyed or 0)
            job.totalChanged = (job.totalChanged or 0) + (changed or 0)
            job.totalDestroyed = (job.totalDestroyed or 0) + (destroyed or 0)
            processed = processed + 1
            if spotFinished then
                job.currentSpotJob = nil
                job.index = job.index + 1
            end
        end
    end

    return job.index > #job.infectedSpots and job.currentSpotJob == nil, changedThisStep, 0, destroyedThisStep
end

function DiseaseFieldDamage:applyDamagePatches(field, diseaseState, risk, threatCategory, infectedSpots)
    local job = createDamageContext(field, diseaseState, risk, threatCategory, infectedSpots)
    if job == nil then
        return 0, 0, 0
    end

    while job.index <= #job.infectedSpots do
        self:stepDamageJob(job, 1)
    end

    return job.totalChanged or 0, 0, job.totalDestroyed or 0
end
