#using scripts\codescripts\struct;
#using scripts\shared\array_shared;
#using scripts\shared\callbacks_shared;
#using scripts\shared\killstreaks_shared;
#using scripts\shared\math_shared;
#using scripts\shared\rank_shared;
#using scripts\shared\system_shared;
#using scripts\shared\util_shared;
#using scripts\shared\weapons_shared;
#using scripts\shared\weapons\_weapons;

#using scripts\shared\bots\_bot;
#using scripts\shared\bots\_bot_combat;
#using scripts\shared\bots\bot_traversals;
#using scripts\shared\bots\bot_buttons;
#using scripts\mp\bots\_bot_ball;
#using scripts\mp\bots\_bot_clean;
#using scripts\mp\bots\_bot_combat;
#using scripts\mp\bots\_bot_conf;
#using scripts\mp\bots\_bot_ctf;
#using scripts\mp\bots\_bot_dem;
#using scripts\mp\bots\_bot_dom;
#using scripts\mp\bots\_bot_escort;
#using scripts\mp\bots\_bot_hq;
#using scripts\mp\bots\_bot_koth;
#using scripts\mp\bots\_bot_loadout;
#using scripts\mp\bots\_bot_sd;
#using scripts\mp\killstreaks\_killstreakrules;
#using scripts\mp\killstreaks\_killstreaks;
#using scripts\mp\killstreaks\_ai_tank;
#using scripts\mp\killstreaks\_airsupport;
#using scripts\mp\killstreaks\_combat_robot;
#using scripts\mp\killstreaks\_counteruav;
#using scripts\mp\killstreaks\_dart;
#using scripts\mp\killstreaks\_dogs;
#using scripts\mp\killstreaks\_drone_strike;
#using scripts\mp\killstreaks\_emp;
#using scripts\mp\killstreaks\_flak_drone;
#using scripts\mp\killstreaks\_helicopter;
#using scripts\mp\killstreaks\_helicopter_gunner;
#using scripts\mp\killstreaks\_killstreak_bundles;
#using scripts\mp\killstreaks\_killstreak_detect;
#using scripts\mp\killstreaks\_killstreak_hacking;
#using scripts\mp\killstreaks\_killstreakrules;
#using scripts\mp\killstreaks\_killstreaks;
#using scripts\mp\killstreaks\_microwave_turret;
#using scripts\mp\killstreaks\_planemortar;
#using scripts\mp\killstreaks\_qrdrone;
#using scripts\mp\killstreaks\_raps;
#using scripts\mp\killstreaks\_rcbomb;
#using scripts\mp\killstreaks\_remote_weapons;
#using scripts\mp\killstreaks\_remotemissile;
#using scripts\mp\killstreaks\_satellite;
#using scripts\mp\killstreaks\_sentinel;
#using scripts\mp\killstreaks\_supplydrop;
#using scripts\mp\killstreaks\_turret;
#using scripts\mp\killstreaks\_uav;

#using scripts\mp\teams\_teams;
#using scripts\mp\_util;

#insert scripts\shared\shared.gsh;
#insert scripts\mp\bots\_bot.gsh;

#define MAX_LOCAL_PLAYERS	10
#define MAX_ONLINE_PLAYERS	18
#define MAX_ONLINE_PLAYERS_PER_TEAM	6

#define RESPAWN_DELAY	0
#define RESPAWN_INTERVAL 0.2
	
#namespace bot;

REGISTER_SYSTEM( "bot_mp", &__init__, undefined )
	
function __init__()
{
	callback::on_start_gametype( &init );
	
	level.getBotSettings = &get_bot_settings;
	
	level.onBotConnect = &on_bot_connect;
	level.onBotSpawned = &on_bot_spawned;
	level.onBotKilled = &on_bot_killed;
	
	level.botIdle = &bot_idle;
	
	level.botThreatLost = &bot_combat::chase_threat;

	level.botPreCombat = &bot_combat::mp_pre_combat;
	level.botCombat = &bot_combat::combat_think;
	level.botPostCombat = &bot_combat::mp_post_combat;
	
	level.botIgnoreThreat = &bot_combat::bot_ignore_threat;
	
	level.enemyEmpActive = &emp::EnemyEmpActive;
}

function init()
{	
	level endon( "game_ended" );
	
	level.botSoak = is_bot_soak();

	if( level.rankedmatch && level.botsoak || !init_bot_gametype() )
	{
		return;
	}
	
	wait_for_host();
	
	level thread populate_bots();
}

// Init Utils
//========================================

function is_bot_soak()
{
	return IsDedicated() && GetDvarInt( "sv_botsoak", 0 );
}

function wait_for_host()
{
	level endon( "game_ended" );
	
	if ( level.botSoak )
	{
		return;
	}
	
	host = util::getHostPlayerForBots();
		
	while ( !isdefined( host ) )
	{
		wait( 0.25 );
		host = util::getHostPlayerForBots();
	}
}

function get_host_team()
{
	host = util::getHostPlayerForBots();
	
	if ( !isdefined( host ) || host.team == "spectator" )
	{
		return "allies";
	}
	
	return host.team;
}

function is_bot_comp_stomp()
{
	return false;
}


// Bot Events
//========================================

function on_bot_connect()
{
	self endon( "disconnect" );
	level endon( "game_ended" );
	
	if ( IS_TRUE( level.disableClassSelection ) )
	{
		self set_rank();
		
		// Doesn't work if we don't do it in this order
		self bot_loadout::pick_hero_gadget();
		self bot_loadout::pick_killstreaks();
		
		return;
	}
	
	if ( !IS_TRUE( self.pers["bot_loadout"] ) )
	{
		self set_rank();
		
		// Doesn't work if we don't do it in this order
		self bot_loadout::build_classes();
		self bot_loadout::pick_hero_gadget();
		self bot_loadout::pick_killstreaks();
		
		self.pers["bot_loadout"] = true;
	}

	self bot_loadout::pick_classes();
	self choose_class();
}

function on_bot_spawned()
{
	self.bot.goalTag = undefined;
}

function on_bot_killed()
{
	self endon("disconnect");
	level endon( "game_ended" );
	self endon( "spawned" );
	self waittill ( "death_delay_finished" );

	wait RESPAWN_DELAY;
	
	if ( self choose_class() && level.playerForceRespawn )
	{
		return;
	}
	
	self thread respawn();
}

function respawn()
{
	self endon( "spawned" );
	self endon( "disconnect" );
	level endon( "game_ended" );

	while( 1 )
	{
		self bot::tap_use_button();
		
		wait RESPAWN_INTERVAL;
	}
}

function bot_idle()
{	
	if ( self do_supplydrop() )
	{
		return;
	}
    
	// TODO: Look for an enemy radar blip
	// TODO: Get points on navmesh and feed into the spawn system to see if an enemy is likely to spawn there
	self bot::navmesh_wander();
	self bot::sprint_to_goal();
}

// Crate maxs: 23.1482
#define CRATE_GOAL_RADIUS 39
#define CRATE_USE_RADIUS 62	// Wild guess on usable radius

function do_supplydrop( maxRange = 1400 ) // A little under minimap width
{
	crates = GetEntArray( "care_package", "script_noteworthy" );
	
	maxRangeSq = maxRange * maxRange;
	
	useRadiusSq = CRATE_USE_RADIUS * CRATE_USE_RADIUS;
	
	closestCrate = undefined;
	closestCrateDistSq = undefined;

	foreach( crate in crates )
	{
		if ( !crate IsOnGround() )
		{
			continue;
		}
		
		crateDistSq = Distance2DSquared( self.origin, crate.origin );
		
		if ( crateDistSq > maxRangeSq )
		{
			continue;
		}
		
		inUse = isdefined( crate.useEnt ) && IS_TRUE( crate.useEnt.inUse );
		
		if ( crateDistSq <= useRadiusSq )
		{
			if ( inUse && !self useButtonPressed() )
			{
				continue;
			}
			
			self bot::press_use_button();
			return true;
		}

		if ( !self has_minimap() && !self BotSightTracePassed( crate ) )
		{
			continue;
		}

		if ( !isdefined( closestCrate ) || crateDistSq < closestCrateDistSq )
		{
			closestCrate = crate;
			closestCrateDistSq = crateDistSq;
		}
	}
	
	if ( isdefined( closestCrate ) )
	{	
		randomAngle = ( 0, RandomInt( 360 ), 0 );
		randomVec = AnglesToForward( randomAngle );
		
		point = closestCrate.origin + randomVec * CRATE_GOAL_RADIUS;
		
		if ( self BotSetGoal( point ) )
		{
			self thread watch_crate( closestCrate );
			return true;
		}
	}
	
	return false;
}

function watch_crate( crate )
{
	self endon( "death" );
	self endon( "bot_goal_reached" );
	level endon( "game_ended" );
	
	while ( isdefined( crate ) && !self bot_combat::has_threat() )
	{
		wait level.botSettings.thinkInterval;
	}
	
	self BotSetGoal( self.origin );
}

// Bot Team Population
//========================================

function populate_bots()
{
	level endon( "game_ended" );
	
	if ( level.teambased )
	{
		maxAllies = GetDvarInt( "bot_maxAllies", 0 );
		maxAxis = GetDvarInt( "bot_maxAxis", 0 );
		
		level thread monitor_bot_team_population( maxAllies, maxAxis );
	}
	else
	{
		maxFree = GetDvarInt( "bot_maxFree", 0 );
		
		level thread monitor_bot_population( maxFree );
	}
}

function monitor_bot_team_population( maxAllies, maxAxis )
{
	level endon( "game_ended" );
	
	if ( !maxAllies && !maxAxis )
	{
		return;
	}
	
	fill_balanced_teams( maxAllies, maxAxis );
	
	while ( 1 )
	{
		wait 3;
		
		// TODO: Get a player count that includes 'CON_CONNECTING' players
		allies = GetPlayers( "allies" );
		axis = GetPlayers( "axis" );
		
		if ( allies.size > maxAllies &&
		     remove_best_bot( allies ) )
		{
			continue;
		}
		
		if ( axis.size > maxAxis &&
			 remove_best_bot( axis ) )
		{
			continue;
		}
		
		if ( allies.size < maxAllies || axis.size < maxAxis )
		{
			add_balanced_bot( allies, maxAllies, axis, maxAxis );
		}
	}
}

function fill_balanced_teams( maxAllies, maxAxis )
{
	allies = GetPlayers( "allies" );
	axis = GetPlayers( "axis" );
	
	while ( ( allies.size < maxAllies || axis.size < maxAxis ) &&
	        add_balanced_bot( allies, maxAllies, axis, maxAxis ) )
	{		
		WAIT_SERVER_FRAME;
		
		allies = GetPlayers( "allies" );
		axis = GetPlayers( "axis" );
	}
}

function add_balanced_bot( allies, maxAllies, axis, maxAxis )
{	
	bot = undefined;
	
	if ( allies.size < maxAllies &&
	     ( allies.size <= axis.size || axis.size >= maxAxis ) )
    {
		bot = add_bot( "allies" );
    }
	else if ( axis.size < maxAxis )
	{
		bot = add_bot( "axis" );
	}

	return isdefined( bot );
}

function monitor_bot_population( maxFree )
{
	level endon( "game_ended" );
	
	if ( !maxFree )
	{
		return;
	}
	
	// Initial Fill
	players = GetPlayers( );
	while ( players.size < maxFree )
	{
		add_bot();
		WAIT_SERVER_FRAME;
		players = GetPlayers( );
	}
	
	while ( 1 )
	{
		wait 3;
		
		// TODO: Get a player count that includes 'CON_CONNECTING' players
		players = GetPlayers( );
				
		if ( players.size < maxFree )
		{
			add_bot();
		}
		else if ( players.size > maxFree )
		{
			remove_best_bot( players );
		}
	}
}

function remove_best_bot( players )
{
	bots = filter_bots( players );
	
	if ( !bots.size )
	{
		return false;
	}
	
	// Prefer non-combat bots
	bestBots = [];
	
	foreach( bot in bots )
	{
		// Don't kick bots in the process of connecting
		if ( bot.sessionstate == "spectator" )
		{
			continue;
		}
		
		if ( bot.sessionstate == "dead" || !bot bot_combat::has_threat() )
		{
			bestBots[bestBots.size] = bot;
		}
	}
	
	if ( bestBots.size )
	{
		remove_bot( bestBots[RandomInt( bestBots.size )] );
	}
	else
	{
		remove_bot( bots[RandomInt( bots.size )] );
	}
	
	return true;
}

// Bot Loadouts
//========================================

function choose_class()
{
	if ( IS_TRUE( level.disableClassSelection ) )
	{
		return false;
	}
	
	currClass = self bot_loadout::get_current_class();

	if ( !isdefined( currClass ) || RandomInt( 100 ) < VAL( level.botSettings.changeClassWeight, 0 ) )
	{
		classIndex = RandomInt( self.loadoutClasses.size );
		className = self.loadoutClasses[classIndex].name;
	}
	
	if ( !isdefined(className) || className === currClass )
	{
		return false;
	}
	
	self notify( "menuresponse", MENU_CHANGE_CLASS, className );
	
	return true;
}

// Killstreaks
//========================================

function use_killstreak()
{
	if ( !level.loadoutKillstreaksEnabled ||
	    self emp::EnemyEMPActive() )
	{
		return;
	}
	
	weapons = self GetWeaponsList();
	inventoryWeapon = self GetInventoryWeapon();
		
	foreach( weapon in weapons )
	{	
		killstreak = killstreaks::get_killstreak_for_weapon( weapon );
		
		if ( !isdefined( killstreak ) )
		{
			continue;
		}
		
		if ( weapon != inventoryWeapon && !self GetWeaponAmmoClip( weapon )  )
		{
			continue;
		}
		 
		if ( self killstreakrules::isKillstreakAllowed( killstreak, self.team ) )
		{
			useWeapon = weapon;
			break;
		}
	}
	
	if ( !isdefined( useWeapon ) )
	{
		return;
	}
	
	killstreak_ref = killstreaks::get_menu_name( killstreak );
	
	switch( killstreak_ref )
	{
		case "killstreak_uav":
		case "killstreak_counteruav":
		case "killstreak_remote_missile":
		    {
                self switchtoweapon( weapon );
                self waittill( "weapon_change_complete" );
                wait 1.5;
                self bot::press_attack_button();
            }
            return;
		case "killstreak_satellite":
		case "killstreak_helicopter_player_gunner":
		case "killstreak_ai_tank_drop":
            self use_supply_drop( weapon );
            break;  
		case "killstreak_raps":
		case "killstreak_sentinel":
		{
			self switchtoweapon(useweapon);
			break;
		}
	}
}

function get_closest_enemy( origin, on_radar )
{
    enemies = self get_enemies( on_radar );
    enemies = arraysort( enemies, origin );

    if ( enemies.size )
        return enemies[0];

    return undefined;
}

function use_supply_drop( weapon )
{
    if ( weapon == "inventory_supplydrop_mp" || weapon == "supplydrop_mp" )
    {
        if ( gettime() - self.spawntime > 5000 )
            return;
    }

    yaw = ( 0, self.angles[1], 0 );
    dir = anglestoforward( yaw );
    dir = vectornormalize( dir );
    drop_point = self.origin + vectorscale( dir, 384 );
    end = drop_point + vectorscale( ( 0, 0, 1 ), 2048.0 );

    if ( !sighttracepassed( drop_point, end, 0, undefined ) )
        return;

    if ( !sighttracepassed( self.origin, end, 0, undefined ) )
        return;

    end = drop_point - vectorscale( ( 0, 0, 1 ), 32.0 );

    if ( bullettracepassed( drop_point, end, 0, undefined ) )
        return;

    self addgoal( self.origin, 24, 4, "killstreak" );

    if ( weapon == "missile_drone_mp" || weapon == "inventory_missile_drone_mp" )
        self lookat( drop_point + vectorscale( ( 0, 0, 1 ), 384.0 ) );
    else
        self lookat( drop_point );

    wait 0.5;

    if ( self getcurrentweapon() != weapon )
    {
        self thread weapon_switch_failsafe();
        self switchtoweapon( weapon );

        self waittill( "weapon_change_complete" );
    }

    use_item( weapon );
    self switchtoweapon( self.lastnonkillstreakweapon );
    self clearlookat();
    self cancelgoal( "killstreak" );
}

function use_item( weapon )
{
    self bot::press_attack_button();
    wait 0.5;

    for ( i = 0; i < 10; i++ )
    {
        if ( self getcurrentweapon() == weapon || self getcurrentweapon() == "none" )
            self bot::press_attack_button();
        else
            return;

        wait 0.5;
    }
}

function weapon_switch_failsafe()
{
    self endon( "death" );
    self endon( "disconnect" );
    self endon( "weapon_change_complete" );
    wait 10;
    self notify( "weapon_change_complete" );
}


function has_radar()
{
	if ( level.teambased )
	{
		return ( uav::HasUAV( self.team ) || satellite::HasSatellite( self.team ) );
	}
	
	return ( uav::HasUAV( self.entnum ) || satellite::HasSatellite( self.entnum ) );
}

function has_minimap()
{
	if ( self IsEmpJammed() )
	{
		return false;
	}
	
	if ( IS_TRUE( level.hardcoreMode ) )
	{
		return self has_radar();
	}
	
	return true;
}

function get_enemies( on_radar )
{
	if ( !isdefined( on_radar ) )
	{
		on_radar = false;
	}

	enemies = self GetEnemies();

	if ( on_radar && !self has_radar() )
	{
		for ( i = 0; i < enemies.size; i++ )
		{
			if ( !isdefined( enemies[i].lastFireTime ) )
			{
				ArrayRemoveIndex( enemies, i );
				i--;
			}
			else if ( GetTime() - enemies[i].lastFireTime > 2000 ) 
			{
				ArrayRemoveIndex( enemies, i );
				i--;
			}
		}
	}

	return enemies;
}

function set_rank()
{
	players = GetPlayers();

	ranks = [];
	bot_ranks = [];
	human_ranks = [];

	for ( i = 0; i < players.size; i++ )
	{
		if ( players[i] == self )
			continue;

		if ( isdefined( players[i].pers[ "rank" ] ) )
		{
			if ( players[i] util::is_bot() )
			{
				bot_ranks[ bot_ranks.size ] = players[i].pers[ "rank" ];
			}
			else
			{
				human_ranks[ human_ranks.size ] = players[i].pers[ "rank" ];
			}
		}
	}

	if( !human_ranks.size )
		human_ranks[ human_ranks.size ] = 10;

	human_avg = math::array_average( human_ranks );

	while ( bot_ranks.size + human_ranks.size < 5 )
	{
		// add some random ranks for better random number distribution
		r = human_avg + RandomIntRange( -5, 5 );
		rank = math::clamp( r, 0, level.maxRank );
		human_ranks[ human_ranks.size ] = rank;
	}

	ranks = ArrayCombine( human_ranks, bot_ranks, true, false );

	avg = math::array_average( ranks );
	s = math::array_std_deviation( ranks, avg );
	
	rank = Int( math::random_normal_distribution( avg, s, 0, level.maxRank ) );
	
	while ( !isdefined( self.pers["codpoints"] ) )
	{
		wait 0.1;
	}
	
	self.pers[ "rank" ] = rank;
	self.pers[ "rankxp" ] = rank::getRankInfoMinXP( rank );

	self setRank( rank );
	self rank::syncXPStat();
}

function init_bot_gametype()
{
	switch(level.gametype)
	{
		case "ball":
		{
			bot_ball::init();
			return true;
		}
		case "conf":
		{
			bot_conf::init();
			return true;
		}
		case "ctf":
		{
			bot_ctf::init();
			return true;
		}
		case "dem":
		{
			bot_dem::init();
			return true;
		}
		case "dm":
		{
			return true;
		}
		case "dom":
		{
			bot_dom::init();
			return true;
		}
		case "escort":
		{
			bot_escort::init();
			return true;
		}
/*		case "infect":
		{
			return true;
		}
*/		case "gun":
		{
			return true;
		}
		case "koth":
		{
			bot_koth::init();
			return true;
		}
		case "sd":
		{
			bot_sd::init();
			return true;
		}
		case "clean":
		{
			bot_clean::init();
			return true;
		}
		case "tdm":
		{
			return true;
		}
	}

	return false;
}

function get_bot_settings()
{
	switch ( GetDvarInt( "bot_difficulty", 1 ) )
	{
		case 0:
			bundleName = "bot_mp_easy";
			break;
			
		case 1:
			bundleName = "bot_mp_normal";
			break;
		case 2:
			bundleName = "bot_mp_hard";
			break;
		case 3:
		default:
			bundleName = "bot_mp_veteran";
			break;
	}
	
	return struct::get_script_bundle( "botsettings", bundleName );
}

function friend_goal_in_radius( goal_name, origin, radius )
{
	return 0;
}

function friend_in_radius( goal_name, origin, radius )
{
	return false;
}

function get_friends()
{
	return [];
}

function bot_vehicle_weapon_ammo( weaponName )
{
	return false;
}

function navmesh_points_visible( origin, point )
{
	return false;
}

function dive_to_prone( exit_stance )
{
	
}