Init()
{
    thread Setup();
}

Setup()
{
    level endon( "game_ended" );
    
    level.commonFunctions.changeTeam                        = "ChangeTeam";
    level.commonFunctions.getTeamCounts                     = "GetTeamCounts";
    level.commonFunctions.getMaxClients                     = "GetMaxClients";
    level.commonFunctions.getTeamBased                      = "GetTeamBased";
    level.commonFunctions.getClientTeam                     = "GetClientTeam";
    level.commonFunctions.getClientKillStreak               = "GetClientKillStreak";
    level.commonFunctions.backupRestoreClientKillStreakData = "BackupRestoreClientKillStreakData";
    level.commonFunctions.waitTillAnyTimeout                = "WaitTillAnyTimeout";
    
    level.overrideMethods[level.commonFunctions.changeTeam]                        = scripts\_integration_base::NotImplementedFunction;
    level.overrideMethods[level.commonFunctions.getTeamCounts]                     = scripts\_integration_base::NotImplementedFunction;
    level.overrideMethods[level.commonFunctions.getTeamBased]                      = scripts\_integration_base::NotImplementedFunction;
    level.overrideMethods[level.commonFunctions.getMaxClients]                     = scripts\_integration_base::NotImplementedFunction;
    level.overrideMethods[level.commonFunctions.getClientTeam]                     = scripts\_integration_base::NotImplementedFunction;
    level.overrideMethods[level.commonFunctions.getClientKillStreak]               = scripts\_integration_base::NotImplementedFunction;
    level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData] = scripts\_integration_base::NotImplementedFunction;
    level.overrideMethods[level.commonFunctions.waitTillAnyTimeout]                = scripts\_integration_base::NotImplementedFunction;
    
    // these can be overridden per game if needed
    level.commonKeys.team1         = "allies"; 
    level.commonKeys.team2         = "axis";
    level.commonKeys.teamSpectator = "spectator";
    
    level.eventTypes.connect     = "connected";
    level.eventTypes.disconnect  = "disconnect";
    level.eventTypes.joinTeam    = "joined_team";
    level.eventTypes.spawned	 = "spawned_player";
    level.eventTypes.gameEnd     = "game_ended";
    
    level.iw4madminIntegrationDefaultPerformance = 200;
    
    if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
    {
        return;
    }

    if ( GetDvarInt( "sv_iw4madmin_autobalance" ) != 1 )
    {
        return;
    }
    
    level thread OnPlayerConnect();
}

OnPlayerConnect()
{
    level endon( level.eventTypes.gameEnd );

    for ( ;; )
    {
        level waittill( level.eventTypes.connect, player );

        if ( ![[level.overrideMethods[level.commonFunctions.getTeamBased]]]() ) 
        {
            continue;
        }
        
        teamToJoin = player GetTeamToJoin();
        player [[level.overrideMethods[level.commonFunctions.changeTeam]]]( teamToJoin );
        
        player thread OnClientFirstSpawn();
        player thread OnClientJoinedTeam();
        player thread OnClientDisconnect();
    }
}

OnClientDisconnect()
{
    level endon( level.eventTypes.gameEnd );
    self endon( "disconnect_logic_end" );

    for ( ;; )
    {
        self waittill( level.eventTypes.disconnect );
        scripts\_integration_base::LogDebug( "client is disconnecting" );

        OnTeamSizeChanged();
        self notify( "disconnect_logic_end" );
    }
}

OnClientJoinedTeam()
{
    self endon( level.eventTypes.disconnect );

    for( ;; )
    {
        self waittill( level.eventTypes.joinTeam );

        if ( IsDefined( self.wasAutoBalanced ) && self.wasAutoBalanced )
        {
            self.wasAutoBalanced = false;
            continue;
        }
        
        newTeam = self [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
        scripts\_integration_base::LogDebug( self.name + " switched to " + newTeam );
        
        if ( newTeam != level.commonKeys.team1 && newTeam != level.commonKeys.team2 )
        {
            OnTeamSizeChanged();
            scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec"  );
            continue;
        }
        
        properTeam = self GetTeamToJoin();
        if ( newTeam != properTeam )
        {
            self [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( false );
            self [[level.overrideMethods[level.commonFunctions.changeTeam]]]( properTeam );
            wait ( 0.1 );
            self [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( true );
        } 		
    }
}

OnClientFirstSpawn()
{
    self endon( level.eventTypes.disconnect );
    timeoutResult = self [[level.overrideMethods[level.commonFunctions.waitTillAnyTimeout]]]( 30, level.eventTypes.spawned );

    if ( timeoutResult != "timeout" )
    {
       return;
    }

    scripts\_integration_base::LogDebug( "moving " + self.name + " to spectator because they did not spawn within expected duration" );
    self [[level.overrideMethods[level.commonFunctions.changeTeam]]]( level.commonKeys.teamSpectator );
}

OnTeamSizeChanged()
{
    if ( level.players.size < 3 )
    {
        scripts\_integration_base::LogDebug( "not enough clients to autobalance" );
        return;
    }

    if ( !IsDefined( GetSmallerTeam( 1 ) ) )
    {
        scripts\_integration_base::LogDebug( "teams are not unbalanced enough to auto balance" );
        return;
    }
    
    toSwap = FindClientToSwap();
    curentTeam = toSwap [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
    otherTeam = level.commonKeys.team1;

    if ( curentTeam == otherTeam ) 
    {
        otherTeam = level.commonKeys.team2;
    }

    toSwap.wasAutoBalanced = true;
    
    if ( !IsDefined( toSwap.autoBalanceCount ) )
    {
        toSwap.autoBalanceCount = 1;
    }
    else
    {
        toSwap.autoBalanceCount++;
    }

    toSwap [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( false );
    scripts\_integration_base::LogDebug( "swapping " + toSwap.name + " from " + curentTeam + " to " + otherTeam );
    toSwap [[level.overrideMethods[level.commonFunctions.changeTeam]]]( otherTeam );
    wait ( 0.1 ); // give the killstreak on team switch clear event time to execute
    toSwap [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( true );
}

FindClientToSwap()
{
    smallerTeam = GetSmallerTeam( 1 );
    teamPool = level.commonKeys.team1;

    if ( IsDefined( smallerTeam ) )
    {
        if ( smallerTeam == teamPool )
        {
            teamPool = level.commonKeys.team2;
        }
    }
    else
    {
        teamPerformances = GetTeamPerformances();
        team1Perf = teamPerformances[level.commonKeys.team1];
        team2Perf = teamPerformances[level.commonKeys.team2];
        teamPool = level.commonKeys.team1;

        if ( team2Perf > team1Perf )
        {
            teamPool = level.commonKeys.team2;
        }
    }

    client = GetBestSwapCandidate( teamPool );

    if ( !IsDefined( client ) )
    {
        scripts\_integration_base::LogDebug( "could not find candidate to swap teams" );
    }
    else
    {
        scripts\_integration_base::LogDebug( "best candidate to swap teams is " + client.name );
    }

    return client;
}

GetBestSwapCandidate( team )
{
    candidates = [];
    maxClients = [[level.overrideMethods[level.commonFunctions.getMaxClients]]]();

    for ( i = 0; i < maxClients; i++ )
    {
        candidates[i] = GetClosestPerformanceClientForTeam( team, candidates );
    }

    candidate = undefined;
    
    foundCandidate = false;
    for ( i = 0; i < maxClients; i++ )
    {
        if ( !IsDefined( candidates[i] ) )
        {
            continue;
        }

        candidate = candidates[i];
        candidateKillStreak = candidate [[level.overrideMethods[level.commonFunctions.getClientKillStreak]]]();

        scripts\_integration_base::LogDebug( "candidate killstreak is " + candidateKillStreak );
        
        if ( candidateKillStreak > 3 )
        {
            scripts\_integration_base::LogDebug( "skipping candidate selection for " + candidate.name + " because their kill streak is too high" );
            continue;
        }

        if ( IsDefined( candidate.autoBalanceCount ) && candidate.autoBalanceCount > 2 )
        {
            scripts\_integration_base::LogDebug( "skipping candidate selection for " + candidate.name + " they have been swapped too many times" );
            continue;
        }

        foundCandidate = true;
        break;
    }

    if ( foundCandidate )
    {
        return candidate;
    }

    return undefined;
}

GetClosestPerformanceClientForTeam( sourceTeam, excluded )
{
    if ( !IsDefined( excluded ) )
    {
        excluded = [];
    }

    otherTeam = level.commonKeys.team1;

    if ( sourceTeam == otherTeam ) 
    {
        otherTeam = level.commonKeys.team2;
    }

    teamPerformances = GetTeamPerformances();
    players = level.players;
    choice = undefined;
    closest = 9999999;

    for ( i = 0; i < players.size; i++ )
    {
        isExcluded = false;
        
        for ( j = 0; j < excluded.size; j++ )
        {
            if ( excluded[j] == players[i] )
            {
                isExcluded = true;
                break;
            }
        }

        if ( isExcluded )
        {
            continue;
        }

        if ( players[i] [[level.overrideMethods[level.commonFunctions.getClientTeam]]]() != sourceTeam )
        {
            continue;
        }

        clientPerformance = players[i] GetClientPerformanceOrDefault();
        sourceTeamNewPerformance = teamPerformances[sourceTeam] - clientPerformance;
        otherTeamNewPerformance = teamPerformances[otherTeam] + clientPerformance;
        candidateValue = Abs( sourceTeamNewPerformance - otherTeamNewPerformance );

        scripts\_integration_base::LogDebug( "perf=" + clientPerformance + " candidateValue=" + candidateValue + " src=" + sourceTeamNewPerformance + " dst=" + otherTeamNewPerformance );

        if ( !IsDefined( choice ) )
        {
            choice = players[i];
            closest = candidateValue;
        } 

        else if ( candidateValue < closest )
        {
            scripts\_integration_base::LogDebug( candidateValue + " is the new best value ");
            choice = players[i];
            closest = candidateValue;
        }
    }

    scripts\_integration_base::LogDebug( choice.name + " is the best candidate to swap" + " with closest=" + closest );
    return choice;
}

GetTeamToJoin()
{
    smallerTeam = GetSmallerTeam( 1 );

    if ( IsDefined( smallerTeam ) )
    {
        return smallerTeam;
    }
    
    teamPerformances = GetTeamPerformances( self );
    
    if ( teamPerformances[level.commonKeys.team1] < teamPerformances[level.commonKeys.team2] )
    {
        scripts\_integration_base::LogDebug( "Team1 performance is lower, so selecting Team1" );
        return level.commonKeys.team1;
    }
    
    else
    {
        scripts\_integration_base::LogDebug( "Team2 performance is lower, so selecting Team2" );
        return level.commonKeys.team2;
    }
}

GetSmallerTeam( minDiff )
{
    teamCounts = [[level.overrideMethods[level.commonFunctions.getTeamCounts]]]();
    team1Count = teamCounts[level.commonKeys.team1];
    team2Count = teamCounts[level.commonKeys.team2];
    maxClients = [[level.overrideMethods[level.commonFunctions.getMaxClients]]]();

    if ( team1Count == team2Count )
    {
        return undefined;
    }
    
    if ( team2Count == maxClients / 2 )
    {
        scripts\_integration_base::LogDebug( "Team2 is full, so selecting Team1" );
        return level.commonKeys.team1;
    }
    
    if ( team1Count == maxClients / 2 )
    {
        scripts\_integration_base::LogDebug( "Team1 is full, so selecting Team2" );
        return level.commonKeys.team2;
    }
    
    sizeDiscrepancy = Abs( team1Count - team2Count );
    
    if ( sizeDiscrepancy > minDiff ) 
    {
        scripts\_integration_base::LogDebug( "Team size differs by more than 1" );
        
        if ( team1Count < team2Count )
        {
            scripts\_integration_base::LogDebug( "Team1 is smaller, so selecting Team1" );
            return level.commonKeys.team1;
        }
        
        else
        {
            scripts\_integration_base::LogDebug( "Team2 is smaller, so selecting Team2" );
            return level.commonKeys.team2;
        }
    }

    return undefined;
}

GetTeamPerformances( ignoredClient )
{
    players = level.players;
    
    team1 = 0;
    team2 = 0;
    
    for ( i = 0; i < players.size; i++ )
    {
        if ( IsDefined( ignoredClient ) && players[i] == ignoredClient )
        {
            continue;
        }
        
        performance = players[i] GetClientPerformanceOrDefault();
        clientTeam = players[i] [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
        
        if ( clientTeam == level.commonKeys.team1 )
        {
            team1 = team1 + performance;
        }
        else
        {
            team2 = team2 + performance;
        }
    }
    
    result = [];
    result[level.commonKeys.team1] = team1;
    result[level.commonKeys.team2] = team2;
    return result;
}

GetClientPerformanceOrDefault()
{
    clientData = self.pers[level.clientDataKey];
    performance = level.iw4madminIntegrationDefaultPerformance;
        
    if ( IsDefined( clientData ) && IsDefined( clientData.performance ) ) 
    {
        performance = int( clientData.performance );
    }

    return performance;
}