class SpawnManager extends Actor;

var private const int canBuildNodesAtOnce;
var private const int canBuildSpawnersAtOnce;
var public int nodesBuilt;//for building nodes only
var public int spawnersBuilt;//for building nodes only

var private bool    isInitializing;
var private int     initializationStage;
var public array<ZombieVolume>  cachedZombieVolumes;
var public array<KFUseTrigger>  cachedTriggers;
var public array<KFHumanPawn>   cachedPawns;
var public array<Vector>        cachedEyes;
struct Node
{
    var public NavigationPoint      point;
    var public array<int>           adjacentNodesIndicies;
    var public array<float>         adjacentNodesDistances;
    var public array<int>           connectedVolumesIndicies;
    var public array<KFDoorMover>   connectedDoors;
    var public bool                 populated;
};
var public array<Node> allNodes;
var public int depopulationIndex;
var public int populatedSigIndex;

struct Spawner
{
    var public ZombieVolume     volume;
    var public int              connectedNodeIndex;
    var public bool             active;
    var public array<Vector>    checkPositions;
};
var public array<Spawner> allSpawners;
var public int counter;
public function Initialize()
{
    isInitializing      = true;
    initializationStage = 0;
    Log("PHASE #1");
    StopWatch(false);
    CacheNodes();
    CacheZombieVolumes();
    CacheUseTriggers();
    StopWatch(true);
    KFGameType(level.game).bDisableZedSpawning = true;
}

public function /*function*/ ReportConnectedSpawners()
{
    local int i;
    for (i = 0; i < allNodes.length; i += 1)
    {
        Log("Node #" $ i $ " has " $ allNodes[i].connectedVolumesIndicies.length $ " conencted ZVs!");
    }
}

public function Tick(float delta)
{
    local int i;
    if (isInitializing)
    {
        HandleInitialization();
        return;
    }
    CacheKFPawns();
    //StopWatch(false);
    for (i = 0; i < cachedPawns.length; i += 1) {
        DisableVisibleZombieVolumes(cachedPawns[i]);
    }
    CacheEyes();
    DepopulateNodes();
    EnableHiddenZombieVolumes();
    //StopWatch(true);
    /*counter += 1;
    if (counter % 60 == 0) {
        LogActiveZV();
    }*/
}

private function LogActiveZV()
{
    local int i, j;
    local int scount;
    local string s;
    for (i = 0; i < allSpawners.length; i += 1)
    {
        if (allSpawners[i].active) {
            scount += 1;
        }
        else {
            Log("Disabled spawner has populated point tho:" @ allNodes[allSpawners[i].connectedNodeIndex].populated);
            Log("Also" @ allSpawners[i].volume.disabledWaveNums.length);
            Log("But" @ allSpawners[i].volume.bVolumeIsEnabled);
        }
    }
    Log("Active spawners right now:" @ scount);
    scount = 0;
    for (i = 0; i < allNodes.length; i += 1)
    {
        if (allNodes[i].populated)
        {
            scount += 1;
        }
        else
        {
            s = "Node" @ i @ "is not populated, but:";
            for (j = 0; j < allNodes[i].adjacentNodesIndicies.length; j += 1)
            {
                s = s @ allNodes[i].adjacentNodesIndicies[j] $ ", ";
            }
            Log(s);
        }
    }
    Log("Populated nodes right now:" @ scount);
}

private function DepopulateNodes()
{
    local int       i, j;
    local int       nodesPerUpdate;
    local Vector    testPosition;
    nodesPerUpdate = allNodes.length / 30;
    while (i < nodesPerUpdate)
    {
        testPosition = allNodes[depopulationIndex].point.location;
        for (j = 0; j < cachedEyes.length; j += 1)
        {
            if (    allNodes[depopulationIndex].populated
                &&  FastTrace(testPosition, cachedEyes[j]))
            {
                allNodes[depopulationIndex].populated = false;
                break;
            }
        }
        i += 1;
        depopulationIndex += 1;
        if (depopulationIndex >= allNodes.length) {
            depopulationIndex = 0;
        }
    }
}

private function PopulateActiveSpawnerNodes()
{
    local int i;
    for (i = 0; i < allSpawners.length; i += 1)
    {
        if (allSpawners[i].active) {
            allSpawners[i].volume.bVolumeIsEnabled = true;
            allNodes[allSpawners[i].connectedNodeIndex].populated = true;
        }
    }
}

private function CacheEyes()
{
    local int i;
    cachedEyes.length = 0;
    for (i = 0; i < cachedPawns.length; i += 1) {
        cachedEyes[i] = cachedPawns[i].location + cachedPawns[i].EyePosition();
    }
}

private function EnableHiddenZombieVolumes()
{
    local int i;
    local int nodesPerUpdate;
    nodesPerUpdate = allNodes.length / 29;
    while (i < nodesPerUpdate)
    {
        PropagateEnableSignal(populatedSigIndex);
        i += 1;
        populatedSigIndex += 1;
        if (populatedSigIndex >= allNodes.length)
        {
            populatedSigIndex = 0;
            break;
        }
    }
    if (populatedSigIndex == 0 ) {
        PopulateActiveSpawnerNodes();
    }
}

private function PropagateEnableSignal(int nodeIndex)
{
    local bool                  canPropogate;
    local bool                  allDoorsLocked;
    local array<KFDoorMover>    doors;
    local int                   i, j, otherIndex, spawnerIndex;
    local Vector                testPosition;
    local array<int>            adjacentIdx, spawnerIdx;
    if (!allNodes[nodeIndex].populated) {
        return;
    }
    doors = allNodes[nodeIndex].connectedDoors;
    if (doors.length > 0) {
        allDoorsLocked = true;
    }
    // TODO: might need to open doors if such spawner is used...
    for (i = 0; i < doors.length; i += 1)
    {
        if (doors[i].bDoorIsDead || !doors[i].bSealed)
        {
            allDoorsLocked = false;
            break;
        }
    }
    if (allDoorsLocked)
    {
        return;
    }
    testPosition = allNodes[nodeIndex].point.location;
    for (j = 0; j < cachedEyes.length; j += 1)
    {
        if (FastTrace(testPosition, cachedEyes[j]))
        {
            allNodes[nodeIndex].populated = false;
            return;
        }
    }
    adjacentIdx = allNodes[nodeIndex].adjacentNodesIndicies;
    for (i = 0; i < adjacentIdx.length; i += 1)
    {
        canPropogate = true;
        otherIndex = adjacentIdx[i];
        testPosition = allNodes[otherIndex].point.location;
        for (j = 0; j < cachedEyes.length; j += 1)
        {
            if (FastTrace(testPosition, cachedEyes[j]))
            {
                canPropogate = false;
                break;
            }
        }
        testPosition =
            (testPosition + allNodes[nodeIndex].point.location) * 0.5;
        for (j = 0; j < cachedEyes.length; j += 1)
        {
            if (FastTrace(testPosition, cachedEyes[j]))
            {
                canPropogate = false;
                break;
            }
        }
        allNodes[otherIndex].populated = true;
        spawnerIdx = allNodes[otherIndex].connectedVolumesIndicies;
        for (j = 0; j < spawnerIdx.length; j += 1)
        {
            spawnerIndex = spawnerIdx[j];
            allSpawners[spawnerIndex].active = true;
            allSpawners[spawnerIndex].volume.bVolumeIsEnabled = true;
        }
    }
}

private function DisableVisibleZombieVolumes(KFHumanPawn pawn)
{
    local int           i, j;
    local bool          doDisable;
    local bool          testFog;
    local float         spawnZedHeight;
    local float         distance, fogDistance;
    local int           spawnersAmount, spawnPointsAmount;
    local Vector        testPosition, pawnEyePosition;
    local array<Vector> spawnPositions;
    if (pawn == none) {
        return;
    }
    spawnZedHeight = class'ZombieFleshPoundBase'.default.collisionRadius * 1.25;
    pawnEyePosition = pawn.location + pawn.EyePosition();
    testFog = pawn.region.zone.bDistanceFog;
    fogDistance = FMax(0.0, pawn.region.zone.distanceFogEnd);
    spawnersAmount = allSpawners.length;
    for (i = 0; i < spawnersAmount; i += 1)
    {
        doDisable = false;
        spawnPositions = allSpawners[i].checkPositions;
        spawnPointsAmount = spawnPositions.length;
        for (j = 0; j < spawnPositions.length; j += 1)
        {
            testPosition = spawnPositions[j];
            if (testFog) {
                distance = VSize(testPosition - pawn.location);
            }
            if (testFog && distance >= fogDistance)         continue;
            if (!FastTrace(testPosition, pawnEyePosition))  continue;
            testPosition.z += spawnZedHeight;
            if (!FastTrace(testPosition, pawnEyePosition))  continue;
            doDisable = true;
            break;
        }
        if (doDisable) {
            allSpawners[i].active = false;
        }
        allSpawners[i].volume.bVolumeIsEnabled = allSpawners[i].active;
    }
}

private function HandleInitialization()
{
    if (initializationStage == 0)
    {
        Log("PHASE #2");
        StopWatch(false);
        if (BuildNodes()) {
            initializationStage += 1;
        }
        StopWatch(true);
    }
    else if (initializationStage == 1)
    {
        Log("PHASE #3");
        StopWatch(false);
        if (BuildSpawners()) {
            initializationStage += 1;
        }
        StopWatch(true);
    }
    else if (initializationStage == 2)
    {
        Log("PHASE #4");
        StopWatch(false);
        ConnectDoors();
        StopWatch(true);
        initializationStage += 1;
    }
    else if (initializationStage == 3)
    {
        isInitializing = false;
        Log("RESULT");
        ReportConnectedSpawners();
    }
}

private function CacheNodes()
{
    local Node              nextNode;
    local NavigationPoint   nextPoint;
    foreach AllActors(class'NavigationPoint', nextPoint)
    {
        nextNode.point = nextPoint;
        nextNode.populated = true;
        allNodes[allNodes.length] = nextNode;
    }
}

private function CacheZombieVolumes()
{
    local ZombieVolume nextVolume;
    foreach AllActors(class'ZombieVolume', nextVolume)
    {
        if (!nextVolume.bVolumeIsEnabled) continue;
        cachedZombieVolumes[cachedZombieVolumes.length] = nextVolume;
    }
}

private function CacheUseTriggers()
{
    local KFUseTrigger nextTrigger;
    foreach AllActors(class'KFUseTrigger', nextTrigger) {
        cachedTriggers[cachedTriggers.length] = nextTrigger;
    }
}

private function CacheKFPawns()
{
    local KFHumanPawn   nextPawn;
    local Controller    nextController;
    nextController = level.controllerList;
    cachedPawns.length = 0;
    while (nextController != none)
    {
        nextPawn = KFHumanPawn(nextController.pawn);
        if (    nextPawn != none && nextPawn.health > 0
            &&  nextController.bIsPlayer)
        {
            cachedPawns[cachedPawns.length] = nextPawn;
        }
        nextController = nextController.nextController;
    }
}




private function ConnectDoors()
{
    local int                   i, j, k;
    local KFUseTrigger          nextTrigger;
    local int                   nodesAmount, triggersAmount;
    local array<KFDoorMover>    nextDoors;
    nodesAmount = allNodes.length;
    nextDoors[0] = none;
    Log("CHECKING DOORS:" @ cachedTriggers.length);
    for (i = 0; i < nodesAmount; i += 1)
    {
        nextDoors.length = 0;
        triggersAmount = cachedTriggers.length;
        for (j = 0; j < triggersAmount; j += 1)
        {
            nextTrigger = cachedTriggers[j];
            for (k = 0; k < nextTrigger.doorOwners.length; k += 1)
            {
                if (allNodes[i].point == nextTrigger.doorOwners[k].doorPathNode)
                {
                    nextDoors[nextDoors.length] = nextTrigger.doorOwners[k];
                    Log("!FOUND AND ADDED!");
                }
            }
        }
        allNodes[i].connectedDoors = nextDoors;
        Log("Node" @ i @ "has" @ allNodes[i].connectedDoors.length @ "doors!");
    }
}

private function bool BuildNodes()
{
    local int               nodesDoneThisCall;
    local int               i, j;
    local int               nodesAmount, specsAmount;
    local NavigationPoint   nextPoint;
    local array<ReachSpec>  nextSpecs;
    local array<int>        nextIndicies;
    local array<float>      nextDistances;
    nodesAmount = allNodes.length;
    //  To avoid possible crash on `.length = 0` in the loop
    nextIndicies[0] = -1;
    nextDistances[0] = 0;
    for (i = nodesBuilt; i < nodesAmount; i += 1)
    {
        nextIndicies.length = 0;
        nextDistances.length = 0;
        nextPoint = allNodes[i].point;
        nextSpecs = nextPoint.pathList;
        specsAmount = nextSpecs.length;
        for (j = 0; j < specsAmount; j += 1)
        {
            if (nextPoint == nextSpecs[j].start){
                nextIndicies[j] = FindNodeIndex(nextSpecs[j].end);    
            }
            else {
                nextIndicies[j] = FindNodeIndex(nextSpecs[j].start);
            }
            nextDistances[j] =
                VSize(nextSpecs[j].start.location - nextSpecs[j].end.location);
        }
        allNodes[i].adjacentNodesIndicies = nextIndicies;
        allNodes[i].adjacentNodesDistances = nextDistances;
        //  Done too much, bail
        nodesDoneThisCall += 1;
        if (nodesDoneThisCall >= canBuildNodesAtOnce)
        {
            nodesBuilt = i + 1;
            return false;
        }
    }
    return true;
}

private function int FindNodeIndex(NavigationPoint point)
{
    local int i;
    local int nodesAmount;
    nodesAmount = allNodes.length;
    for (i = 0; i < nodesAmount; i += 1)
    {
        if (allNodes[i].point == point) {
            return i;
        }
    }
    return -1;
}

private function bool BuildSpawners()
{
    local int               i, j;
    local int               spawnersDoneThisCall;
    local int               volumesAmount, pointsAmount;
    local int               selectedNodeIndex;
    local float             boxMinX, boxMaxX, boxMinY, boxMaxY, boxZ;
    local float             nextDistance, minDistance;
    local Spawner           nextSpawner;
    local ZombieVolume      nextVolume;
    local NavigationPoint   nextPoint;
    local array<Vector>     spawnPositions, checkPositions;
    local array<int>        volumesCopy;
    volumesAmount   = cachedZombieVolumes.length;
    pointsAmount    = allNodes.length;
    for (i = 0; spawnersBuilt < volumesAmount; spawnersBuilt += 1)
    {
        nextVolume = cachedZombieVolumes[spawnersBuilt];
        selectedNodeIndex = -1;
        for (j = 0; j < pointsAmount; j += 1)
        {
            nextPoint = allNodes[j].point;
            if (!FastTrace(nextPoint.location, nextVolume.location)) {
                continue;
            }
            nextDistance = VSize(nextVolume.location - nextPoint.location);
            if (selectedNodeIndex < 0 || nextDistance < minDistance)
            {
                minDistance         = nextDistance;
                selectedNodeIndex   = j;
            }
        }
        if (selectedNodeIndex >= 0)
        {
            nextSpawner.volume              = nextVolume;
            nextSpawner.connectedNodeIndex  = selectedNodeIndex;
            nextSpawner.active              = true;
            volumesCopy = allNodes[selectedNodeIndex].connectedVolumesIndicies;
            volumesCopy[volumesCopy.length] = allSpawners.length; 
            allNodes[selectedNodeIndex].connectedVolumesIndicies = volumesCopy;
            spawnPositions = nextVolume.spawnPos;
            boxMinX = spawnPositions[0].x;
            boxMaxX = spawnPositions[0].x;
            boxMinY = spawnPositions[0].y;
            boxMaxY = spawnPositions[0].y;
            boxZ    = spawnPositions[0].z;
            for (i = 1; i < spawnPositions.length; i += 1)
            {
                boxMinX = FMin(boxMinX, spawnPositions[i].x);
                boxMaxX = FMax(boxMaxX, spawnPositions[i].x);
                boxMinY = FMin(boxMinY, spawnPositions[i].y);
                boxMaxY = FMax(boxMaxY, spawnPositions[i].y);
            }
            checkPositions[0] = GetVect(boxMinX, boxMinY, boxZ);
            checkPositions[1] = GetVect(boxMinX, boxMaxY, boxZ);
            checkPositions[2] = GetVect(boxMaxX, boxMinY, boxZ);
            checkPositions[3] = GetVect(boxMaxX, boxMaxY, boxZ);
            checkPositions[4] = nextVolume.location;
            nextSpawner.checkPositions = checkPositions;
            allSpawners[allSpawners.length] = nextSpawner;
        }
        spawnersDoneThisCall += 1;
        if (spawnersDoneThisCall >= canBuildSpawnersAtOnce) {
            return false;
        }
    }
    return true;
}

public function Vector GetVect(float x, float y, float z)
{
    local Vector result;
    result.x = x;
    result.y = y;
    result.z = z;
    return result;
}

// TODO: what is ZVs are so huge 5 points is not enough?
defaultproperties
{
    canBuildNodesAtOnce = 100
    canBuildSpawnersAtOnce = 100
}