-- Copyright (C) 2022  veden

-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.

-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.

-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <https://www.gnu.org/licenses/>.


if chunkPropertyUtilsG then
    return chunkPropertyUtilsG
end
local chunkPropertyUtils = {}

local constants = require("Constants")
local mathUtils = require("MathUtils")

-- constants

local COOLDOWN_DRAIN = constants.COOLDOWN_DRAIN

local RAIDING_MINIMUM_BASE_THRESHOLD = constants.RAIDING_MINIMUM_BASE_THRESHOLD

local PLAYER_PHEROMONE = constants.PLAYER_PHEROMONE
local BASE_PHEROMONE = constants.BASE_PHEROMONE

local MOVEMENT_GENERATOR_PERSISTANCE = constants.MOVEMENT_GENERATOR_PERSISTANCE
local CHUNK_ALL_DIRECTIONS = constants.CHUNK_ALL_DIRECTIONS

local MAGIC_MAXIMUM_NUMBER = constants.MAGIC_MAXIMUM_NUMBER

-- imported functions

local manhattenDistancePoints = mathUtils.manhattenDistancePoints

local mMin = math.min

-- module code

function chunkPropertyUtils.getTurretCount(map, chunk)
    return map.chunkToTurrets[chunk.id] or 0
end

function chunkPropertyUtils.getTrapCount(map, chunk)
    return map.chunkToTraps[chunk.id] or 0
end

function chunkPropertyUtils.getUtilityCount(map, chunk)
    return map.chunkToUtilities[chunk.id] or 0
end

function chunkPropertyUtils.getHiveCount(map, chunk)
    return map.chunkToHives[chunk.id] or 0
end

function chunkPropertyUtils.addTurretCount(map, chunk, unitNumber)
    map.activeSurface = true
    if not map.chunkToTurretIds[chunk.id] then
        map.chunkToTurretIds[chunk.id] = {}
    end
    if not map.chunkToTurretIds[chunk.id][unitNumber] then
        map.chunkToTurretIds[chunk.id][unitNumber] = true
        map.chunkToTurrets[chunk.id] = (map.chunkToTurrets[chunk.id] or 0) + 1
        return true
    end
    return false
end

function chunkPropertyUtils.removeTurretCount(map, chunk, unitNumber)
    if map.chunkToTurretIds[chunk.id] and map.chunkToTurretIds[chunk.id][unitNumber] then
        map.chunkToTurretIds[chunk.id][unitNumber] = nil
        map.chunkToTurrets[chunk.id] = map.chunkToTurrets[chunk.id] - 1
        if map.chunkToTurrets[chunk.id] == 0 then
            map.chunkToTurretIds[chunk.id] = nil
            map.chunkToTurrets[chunk.id] = nil
        end
        return true
    end
    return false
end

function chunkPropertyUtils.addTrapCount(map, chunk, unitNumber)
    map.activeSurface = true
    if not map.chunkToTrapIds[chunk.id] then
        map.chunkToTrapIds[chunk.id] = {}
    end
    if not map.chunkToTrapIds[chunk.id][unitNumber] then
        map.chunkToTrapIds[chunk.id][unitNumber] = true
        map.chunkToTraps[chunk.id] = (map.chunkToTraps[chunk.id] or 0) + 1
        return true
    end
    return false
end

function chunkPropertyUtils.removeTrapCount(map, chunk, unitNumber)
    if map.chunkToTrapIds[chunk.id] and map.chunkToTrapIds[chunk.id][unitNumber] then
        map.chunkToTrapIds[chunk.id][unitNumber] = nil
        map.chunkToTraps[chunk.id] = map.chunkToTraps[chunk.id] - 1
        if map.chunkToTraps[chunk.id] == 0 then
            map.chunkToTrapIds[chunk.id] = nil
            map.chunkToTraps[chunk.id] = nil
        end
        return true
    end
    return false
end

function chunkPropertyUtils.addUtilitiesCount(map, chunk, unitNumber)
    map.activeSurface = true
    if not map.chunkToUtilityIds[chunk.id] then
        map.chunkToUtilityIds[chunk.id] = {}
    end
    if not map.chunkToUtilityIds[chunk.id][unitNumber] then
        map.chunkToUtilityIds[chunk.id][unitNumber] = true
        map.chunkToUtilities[chunk.id] = (map.chunkToUtilities[chunk.id] or 0) + 1
        return true
    end
    return false
end

function chunkPropertyUtils.removeUtilitiesCount(map, chunk, unitNumber)
    if map.chunkToUtilityIds[chunk.id] and map.chunkToUtilityIds[chunk.id][unitNumber] then
        map.chunkToUtilityIds[chunk.id][unitNumber] = nil
        map.chunkToUtilities[chunk.id] = map.chunkToUtilities[chunk.id] - 1
        if map.chunkToUtilities[chunk.id] == 0 then
            map.chunkToUtilityIds[chunk.id] = nil
            map.chunkToUtilities[chunk.id] = nil
        end
        return true
    end
    return false
end

function chunkPropertyUtils.addHiveCount(map, chunk, unitNumber)
    map.activeSurface = true
    if not map.chunkToHiveIds[chunk.id] then
        map.chunkToHiveIds[chunk.id] = {}
    end
    if not map.chunkToHiveIds[chunk.id][unitNumber] then
        map.chunkToHiveIds[chunk.id][unitNumber] = true
        map.chunkToHives[chunk.id] = (map.chunkToHives[chunk.id] or 0) + 1
        return true
    end
    return false
end

function chunkPropertyUtils.removeHiveCount(map, chunk, unitNumber)
    if map.chunkToHiveIds[chunk.id] and map.chunkToHiveIds[chunk.id][unitNumber] then
        map.chunkToHiveIds[chunk.id][unitNumber] = nil
        map.chunkToHives[chunk.id] = map.chunkToHives[chunk.id] - 1
        if map.chunkToHives[chunk.id] == 0 then
            map.chunkToHiveIds[chunk.id] = nil
            map.chunkToHives[chunk.id] = nil
        end
        return true
    end
    return false
end

function chunkPropertyUtils.addNestCount(map, chunk, unitNumber)
    map.activeSurface = true
    local chunkId = chunk.id
    if not map.chunkToNestIds[chunkId] then
        map.chunkToNestIds[chunkId] = {}
    end
    if not map.chunkToNestIds[chunkId][unitNumber] then
        map.chunkToNestIds[chunkId][unitNumber] = true
        local cToN = map.universe.chunkToNests
        local pack = cToN[chunkId]
        if not pack then
            cToN[chunkId] = {
                map = map,
                v = 0
            }
        end
        cToN[chunkId].v = cToN[chunkId].v + 1
        return true
    end
    return false
end

function chunkPropertyUtils.removeNestCount(map, chunk, unitNumber)
    local chunkId = chunk.id
    if map.chunkToNestIds[chunkId] and map.chunkToNestIds[chunkId][unitNumber] then
        map.chunkToNestIds[chunkId][unitNumber] = nil
        local cToN = map.universe.chunkToNests
        cToN[chunkId].v = cToN[chunkId].v - 1
        if cToN[chunkId].v == 0 then
            map.chunkToNestIds[chunkId] = nil
            cToN[chunkId] = nil
            if (map.universe.processMigrationIterator == chunkId) then
                map.universe.processMigrationIterator = nil
            end
            if (map.universe.processNestIterator == chunkId) then
                map.universe.processNestIterator = nil
            end
        end
        return true
    end
    return false
end

function chunkPropertyUtils.getNestCount(map, chunk)
    local nestPack = map.universe.chunkToNests[chunk.id]
    if not nestPack then
        return 0
    end
    return nestPack.v
end

function chunkPropertyUtils.getChunkBase(map, chunk)
    return map.chunkToBase[chunk.id]
end

function chunkPropertyUtils.removeChunkBase(map, chunk, base)
    if map.chunkToBase[chunk.id] then
        base.chunkCount = base.chunkCount - 1
        map.chunkToBase[chunk.id] = nil
    end
end

function chunkPropertyUtils.setChunkBase(map, chunk, base)
    if not map.chunkToBase[chunk.id] then
        base.chunkCount = base.chunkCount + 1
        map.chunkToBase[chunk.id] = base
    end
end

function chunkPropertyUtils.getEnemyStructureCount(map, chunk)
    local nests = 0
    local nestPack = map.universe.chunkToNests[chunk.id]
    if nestPack then
        nests = nestPack.v
    end
    return nests + (map.chunkToTurrets[chunk.id] or 0) + (map.chunkToTraps[chunk.id] or 0) +
        (map.chunkToUtilities[chunk.id] or 0) + (map.chunkToHives[chunk.id] or 0)
end

function chunkPropertyUtils.setDrainPylons(map, entity1, entity2)
    local pair = {entity1, entity2}
    map.drainPylons[entity1.unit_number] = pair
    map.drainPylons[entity2.unit_number] = pair
end

function chunkPropertyUtils.removeDrainPylons(map, unitNumber)
    local pair = map.drainPylons[unitNumber]
    if pair then
        local target = pair[1]
        local pole = pair[2]
        if target.unit_number == unitNumber then
            map.drainPylons[unitNumber] = nil
            if pole.valid then
                map.drainPylons[pole.unit_number] = nil
                pole.destroy()
            end
        elseif (pole.unit_number == unitNumber) then
            map.drainPylons[unitNumber] = nil
            if target.valid then
                map.drainPylons[target.unit_number] = nil
                target.die()
            end
        end
    end
end

function chunkPropertyUtils.getDrainPylonPair(map, unitNumber)
    return map.drainPylons[unitNumber]
end

function chunkPropertyUtils.isDrained(map, chunk, tick)
    local pack = map.universe.chunkToDrained[chunk.id]
    if not pack then
        return false
    end
    return (tick - pack.tick) < COOLDOWN_DRAIN
end

function chunkPropertyUtils.setDrainedTick(map, chunk, tick)
    local chunkId = chunk.id
    local pack = map.universe.chunkToDrained[chunkId]
    if not pack then
        pack = {
            map = map,
            tick = 0
        }
        map.universe.chunkToDrained[chunkId] = pack
    end
    pack.tick = tick
end

function chunkPropertyUtils.getRetreatTick(map, chunk)
    local pack = map.universe.chunkToRetreats[chunk.id]
    if not pack then
        return 0
    end
    return pack.tick
end

function chunkPropertyUtils.getRallyTick(map, chunk)
    local pack = map.universe.chunkToRallys[chunk.id]
    if not pack then
        return 0
    end
    return pack.tick
end

function chunkPropertyUtils.setRallyTick(map, chunk, tick)
    local chunkId = chunk.id
    local pack = map.universe.chunkToRallys[chunkId]
    if not pack then
        pack = {
            map = map,
            tick = tick
        }
        map.universe.chunkToRallys[chunkId] = pack
    end
    pack.tick = tick
end

function chunkPropertyUtils.setRetreatTick(map, chunk, tick)
    local chunkId = chunk.id
    local pack = map.universe.chunkToRetreats[chunkId]
    if not pack then
        pack = {
            map = map,
            tick = tick
        }
        map.universe.chunkToRetreats[chunkId] = pack
    end
    pack.tick = tick
end

function chunkPropertyUtils.setResourceGenerator(map, chunk, resourceGenerator)
    if (resourceGenerator <= 0) then
        map.chunkToResource[chunk.id] = nil
    else
        map.chunkToResource[chunk.id] = resourceGenerator
    end
end

function chunkPropertyUtils.getResourceGenerator(map, chunk)
    return map.chunkToResource[chunk.id] or 0
end

function chunkPropertyUtils.addResourceGenerator(map, chunk, delta)
    map.chunkToResource[chunk.id] = (map.chunkToResource[chunk.id] or 0) + delta
end

function chunkPropertyUtils.getDeathGeneratorRating(map, chunk)
    return 1 - (map.chunkToDeathGenerator[chunk.id] or 0)
end

function chunkPropertyUtils.getDeathGenerator(map, chunk)
    return map.chunkToDeathGenerator[chunk.id] or 0
end

function chunkPropertyUtils.getPassable(map, chunk)
    return map.chunkToPassable[chunk.id] or CHUNK_ALL_DIRECTIONS
end


function chunkPropertyUtils.getRaidNestActiveness(map, chunk)
    local activeness = map.universe.chunkToActiveRaidNest[chunk.id]
    if not activeness then
        return 0
    end
    return activeness.v or 0
end

function chunkPropertyUtils.setRaidNestActiveness(map, chunk, value, base)
    local universe = map.universe
    if (value <= 0) then
        if universe.chunkToActiveRaidNest[chunk.id] then
            base.activeRaidNests = base.activeRaidNests - 1
        end
        if (universe.processActiveRaidSpawnerIterator == chunk.id) then
            universe.processActiveRaidSpawnerIterator = nil
        end
        universe.chunkToActiveRaidNest[chunk.id] = nil
    else
        if not universe.chunkToActiveRaidNest[chunk.id] then
            base.activeRaidNests = base.activeRaidNests + 1
            universe.chunkToActiveRaidNest[chunk.id] = {
                map = map,
                v = 0
            }
        end
        universe.chunkToActiveRaidNest[chunk.id].v = value
    end
end

function chunkPropertyUtils.getNestActiveness(map, chunk)
    local activeness = map.universe.chunkToActiveNest[chunk.id]
    if not activeness then
        return 0
    end
    return activeness.v or 0
end

function chunkPropertyUtils.setNestActiveness(map, chunk, value, base)
    local universe = map.universe
    if (value <= 0) then
        if universe.chunkToActiveNest[chunk.id] then
            base.activeNests = base.activeNests - 1
        end
        if (universe.processActiveSpawnerIterator == chunk.id) then
            universe.processActiveSpawnerIterator = nil
        end
        universe.chunkToActiveNest[chunk.id] = nil
    else
        if not universe.chunkToActiveNest[chunk.id] then
            base.activeNests = base.activeNests + 1
            universe.chunkToActiveNest[chunk.id] = {
                map = map,
                v = 0
            }
        end
        universe.chunkToActiveNest[chunk.id].v = value
    end
end

function chunkPropertyUtils.setPassable(map, chunk, value)
    if (value == CHUNK_ALL_DIRECTIONS) then
        map.chunkToPassable[chunk.id] = nil
    else
        map.chunkToPassable[chunk.id] = value
    end
end

function chunkPropertyUtils.getPathRating(map, chunk)
    return map.chunkToPathRating[chunk.id] or 1
end

function chunkPropertyUtils.setPathRating(map, chunk, value)
    if (value == 1) then
        map.chunkToPathRating[chunk.id] = nil
    else
        map.chunkToPathRating[chunk.id] = value
    end
end

function chunkPropertyUtils.addDeathGenerator(map, chunk, value)
    local v = (map.chunkToDeathGenerator[chunk.id] or 0) + value
    if (v >= 1) then
        v = 0.9999
    end
    map.chunkToDeathGenerator[chunk.id] = v
end

function chunkPropertyUtils.setDeathGenerator(map, chunk, value)
    if (value <= 0.001) then
        map.chunkToDeathGenerator[chunk.id] = nil
    else
        map.chunkToDeathGenerator[chunk.id] = value
    end
end

function chunkPropertyUtils.addVictoryGenerator(map, chunk, value)
    local cToV = map.universe.chunkToVictory
    local chunkId = chunk.id
    if not cToV[chunkId] then
        cToV[chunkId] = {
            map = map,
            v = 0
        }
    end
    cToV[chunkId].v = cToV[chunkId].v + value
end

function chunkPropertyUtils.decayDeathGenerator(map, chunk)
    local gen = map.chunkToDeathGenerator[chunk.id]
    if gen then
        local v = gen * MOVEMENT_GENERATOR_PERSISTANCE
        if v <= 0.001 then
            map.chunkToDeathGenerator[chunk.id] = nil
        else
            map.chunkToDeathGenerator[chunk.id] = v
        end
    end
end

function chunkPropertyUtils.addPlayerToChunk(map, chunk, name)
    local playerCountChunks = map.chunkToPlayerCount
    local playerChunk = map.playerToChunk[name]
    if not playerChunk then
        map.playerToChunk[name] = chunk
        local playerCount = playerCountChunks[chunk.id]
        if not playerCount then
            playerCountChunks[chunk.id] = 1
        else
            playerCountChunks[chunk.id] = playerCount + 1
        end
    elseif (playerChunk.id ~= chunk.id) then
        map.playerToChunk[name] = chunk
        local playerCount = playerCountChunks[playerChunk.id]
        if playerCount then
            chunkPropertyUtils.setPlayersOnChunk(map, playerChunk, playerCount - 1)
        end
        playerCount = playerCountChunks[chunk.id]
        if not playerCount then
            playerCountChunks[chunk.id] = 1
        else
            playerCountChunks[chunk.id] = playerCount + 1
        end
    end
end

function chunkPropertyUtils.setPlayersOnChunk(map, chunk, value)
    if (value <= 0) then
        map.chunkToPlayerCount[chunk.id] = nil
    else
        map.chunkToPlayerCount[chunk.id] = value
    end
end

function chunkPropertyUtils.getPlayersOnChunk(map, chunk)
    return map.chunkToPlayerCount[chunk.id] or 0
end

function chunkPropertyUtils.getPlayerBaseGenerator(map, chunk)
    return map.chunkToPlayerBase[chunk.id] or 0
end

function chunkPropertyUtils.addSquadToChunk(map, chunk, squad)
    local chunkToSquad = map.chunkToSquad

    if (chunk ~= -1) and ((squad.chunk == -1) or (squad.chunk.id ~= chunk.id)) then
        chunkPropertyUtils.removeSquadFromChunk(map, squad)
        local squads = chunkToSquad[chunk.id]
        if not squads then
            squads = {}
            chunkToSquad[chunk.id] = squads
        end
        squads[squad.groupNumber] = squad
        squad.chunk = chunk
    end
end

function chunkPropertyUtils.removeSquadFromChunk(map, squad)
    local chunkToSquad = map.chunkToSquad
    local chunk = squad.chunk
    if (chunk ~= -1) then
        local squads = chunkToSquad[chunk.id]
        if squads then
            squads[squad.groupNumber] = nil
            if (table_size(squads) == 0) then
                chunkToSquad[chunk.id] = nil
            end
        end
    end
end

function chunkPropertyUtils.getSquadsOnChunk(map, chunk)
    return map.chunkToSquad[chunk.id] or map.emptySquadsOnChunk
end

function chunkPropertyUtils.setPlayerBaseGenerator(map, chunk, playerGenerator)
    if (playerGenerator <= 0) then
        map.chunkToPlayerBase[chunk.id] = nil
    else
        map.chunkToPlayerBase[chunk.id] = playerGenerator
    end
end

function chunkPropertyUtils.addPlayerBaseGenerator(map, chunk, playerGenerator)
    map.chunkToPlayerBase[chunk.id] = (map.chunkToPlayerBase[chunk.id] or 0) + playerGenerator
end

function chunkPropertyUtils.findNearbyBase(map, chunk)
    local x = chunk.x
    local y = chunk.y

    local foundBase = chunkPropertyUtils.getChunkBase(map, chunk)
    if foundBase then
        return foundBase
    end

    local closest = MAGIC_MAXIMUM_NUMBER
    for _, base in pairs(map.bases) do
        local distance = manhattenDistancePoints(base.x, base.y, x, y)
        if (distance <= base.distanceThreshold) and (distance < closest) then
            closest = distance
            foundBase = base
        end
    end

    return foundBase
end

function chunkPropertyUtils.findNearbyBaseByPosition(map, x, y)
    local foundBase

    local closest = MAGIC_MAXIMUM_NUMBER
    for _, base in pairs(map.bases) do
        local distance = manhattenDistancePoints(base.x, base.y, x, y)
        if (distance <= base.distanceThreshold) and (distance < closest) then
            closest = distance
            foundBase = base
        end
    end

    return foundBase
end

function chunkPropertyUtils.processNestActiveness(map, chunk)
    local nests = chunkPropertyUtils.getNestCount(map, chunk)
    local base = chunkPropertyUtils.findNearbyBase(map, chunk)
    if (nests > 0) then
        local surface = map.surface
        local activeness = chunkPropertyUtils.getNestActiveness(map, chunk)
        local universe = map.universe
        local raidActiveness = chunkPropertyUtils.getRaidNestActiveness(map, chunk)
        if universe.attackUsePlayer and (chunk[PLAYER_PHEROMONE] > universe.attackPlayerThreshold) then
            chunkPropertyUtils.setNestActiveness(map, chunk, mMin(activeness + 5, 20), base)
        elseif (chunk[BASE_PHEROMONE] > 0) then
            if (surface.get_pollution(chunk) > 0) then
                chunkPropertyUtils.setNestActiveness(map, chunk, mMin(activeness + 5, 20), base)
            else
                local x = chunk.x
                local y = chunk.y
                local position = {x=0,y=0}
                position.x = x + 32
                position.y = y
                if (surface.get_pollution(position) > 0) then
                    chunkPropertyUtils.setNestActiveness(map, chunk, mMin(activeness + 5, 20), base)
                else
                    position.x = x - 32
                    if (surface.get_pollution(position) > 0) then
                        chunkPropertyUtils.setNestActiveness(map, chunk, mMin(activeness + 5, 20), base)
                    else
                        position.x = x
                        position.y = y - 32
                        if (surface.get_pollution(position) > 0) then
                            chunkPropertyUtils.setNestActiveness(map, chunk, mMin(activeness + 5, 20), base)
                        else
                            position.y = y + 32
                            if (surface.get_pollution(position) > 0) then
                                chunkPropertyUtils.setNestActiveness(map, chunk, mMin(activeness + 5, 20), base)
                            else
                                chunkPropertyUtils.setNestActiveness(map, chunk, activeness - 2, base)
                                if (chunk[BASE_PHEROMONE] > RAIDING_MINIMUM_BASE_THRESHOLD) then
                                    chunkPropertyUtils.setRaidNestActiveness(map, chunk, mMin(raidActiveness + 3, 20), base)
                                else
                                    chunkPropertyUtils.setRaidNestActiveness(map, chunk, raidActiveness - 1, base)
                                end
                            end
                        end
                    end
                end
            end
        else
            chunkPropertyUtils.setNestActiveness(map, chunk, activeness - 5, base)
            chunkPropertyUtils.setRaidNestActiveness(map, chunk, raidActiveness - 5, base)
        end
    elseif base then
        chunkPropertyUtils.setNestActiveness(map, chunk, 0, base)
        chunkPropertyUtils.setRaidNestActiveness(map, chunk, 0, base)
    end
end


chunkPropertyUtilsG = chunkPropertyUtils
return chunkPropertyUtils
