#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_buttons;
#using scripts\shared\bots\bot_traversals;

#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\_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.1
#define RESPAWN_INTERVAL 0.1

#namespace bot;

#precache("eventstring", "mpl_killstreak_cruisemissile");
#precache("eventstring", "mpl_killstreak_raps");

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;

/*
/#
	level.botDevguiCmd = &bot_devgui_cmd;
	level thread system_devgui_gadget_think();
#/
*/
	setDvar("bot_enableWallrun", 1);
}

function init()
{
	level endon("game_ended");

	level.botSoak = is_bot_soak();
	if (!init_bot_gametype())
	{
		return;
	}

	wait_for_host();

	level thread populate_bots();
}

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

function is_bot_soak()
{
	return getDvarInt("sv_botsoak", 0);
}

function wait_for_host()
{
	level endon("game_ended");

	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;
/*
/#
	weapon = undefined;

	if (getDvarInt("scr_botsHasPlayerWeapon") != 0)
	{
		player = util::getHostPlayer();
		weapon = player getCurrentWeapon();
	}

	if (getDvarString("devgui_bot_weapon", "") != "")
	{
		weapon = getWeapon(getDvarString("devgui_bot_weapon"));
	}

	if (isdefined(weapon) && level.weaponNone != weapon)
	{
		self weapons::detach_all_weapons();
		self takeAllWeapons();
		self giveWeapon(weapon);
		self switchToWeapon(weapon);
		self setSpawnWeapon(weapon);

		self teams::set_player_model(self.team, weapon);
	}
#/
*/
}

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_satellite":
	case "killstreak_helicopter_player_gunner":
	case "killstreak_raps":
	case "killstreak_sentinel":
	{
		self switchToWeapon(useWeapon);
		break;
	}
	case "killstreak_ai_tank_drop":
	{
		self use_supply_drop(weapon);
		break;
	}
	case "killstreak_remote_missile":
	{
		self switchToWeapon(weapon);
		self waittill("weapon_change_complete");
		wait 1.5;
		self bot::press_attack_button();
		return;
	}
	}
}

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 killstreak_location(num, weapon)
{
	enemies = get_enemies();

	if (!enemies.size)
		return;

	if (!self switchToWeapon(weapon))
		return;

	self waittill("weapon_change");

	self util::freeze_player_controls(true);
	wait_time = 1;

	while (!isdefined(self.selectinglocation) || self.selectinglocation == 0)
	{
		wait 0.05;
		wait_time -= 0.05;

		if (wait_time <= 0)
		{
			self util::freeze_player_controls(false);
			self switchToWeapon(self.lastnonkillstreakweapon);
			return;
		}
	}

	wait 2;

	for (i = 0; i < num; i++)
	{
		enemies = get_enemies();

		if (enemies.size)
		{
			enemy = randomInt(enemies);
			self notify("confirm_location", enemy.origin, 0);
		}

		wait 0.25;
	}

	self util::freeze_player_controls(false);
}

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();

/*
/#
	for (i = 0; i < enemies.size; i++)
	{
		if (isplayer(enemies[i]) && enemies[i] isInMoveMode("ufo", "noclip"))
		{
			arrayRemoveIndex(enemies, i);
			i--;
		}
	}
#/
*/

	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;
	case "sas":
		return true;
	case "prop":
		return true;
	case "sniperonly":
		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)
{

}

/*
/#

// Devgui
//========================================

function bot_devgui_cmd(cmd)
{
	cmdTokens = strtok(cmd, " ");

	if (cmdTokens.size == 0)
	{
		return false;
	}

	host = util::getHostPlayerForBots();
	team = get_host_team();

	switch (cmdTokens[0])
	{
	case "spawn_enemy":
		team = util::getotherteam(team);
	case "spawn_friendly":
		count = 1;
		if (cmdTokens.size > 1)
		{
			count = int(cmdTokens[1]);
		}
		for (i = 0; i < count; i++)
		{
			add_bot(team);
		}
		return true;
	case "remove_enemy":
		team = util::getotherteam(team);
	case "remove_friendly":
		remove_bots(undefined, team);
		return true;
	case "fixed_spawn_enemy":
		team = util::getotherteam(team);
	case "fixed_spawn_friendly":
		bot = add_bot_at_eye_trace(team);
		if (isdefined(bot))
		{
			bot thread fixed_spawn_override();
		}
		return true;

	case "player_weapon":
		players = getPlayers();
		foreach(player in players)
		{
			if (!player util::is_bot())
			{
				continue;
			}

			weapon = host getCurrentWeapon();

			player weapons::detach_all_weapons();
			player takeAllWeapons();
			player giveWeapon(weapon);
			player switchToWeapon(weapon);
			player setSpawnWeapon(weapon);

			player teams::set_player_model(player.team, weapon);
		}
		return true;
	}

	return false;
}

function system_devgui_gadget_think()
{
	setDvar("devgui_bot_gadget", "");

	for (;; )
	{
		wait(1);

		gadget = getDvarString("devgui_bot_gadget");

		if (gadget.size == 0)
		{
			bot_turn_on_gadget(getWeapon(gadget));
			setDvar("devgui_bot_gadget", "");
		}
	}
}

function bot_turn_on_gadget(gadget)
{
	players = getPlayers();

	foreach(player in players)
	{
		if (!player util::is_bot())
		{
			continue;
		}

		host = util::getHostPlayer();
		weapon = host getCurrentWeapon();

		if (!isdefined(weapon) || weapon == level.weaponNone || weapon == level.weaponNull)
		{
			weapon = getWeapon("smg_standard");
		}

		player weapons::detach_all_weapons();
		player takeAllWeapons();
		player giveWeapon(weapon);
		player switchToWeapon(weapon);
		player setSpawnWeapon(weapon);

		player teams::set_player_model(player.team, weapon);

		player giveWeapon(gadget);
		slot = player gadgetGetSlot(gadget);
		player gadgetPowerSet(slot, 100.0);
		player botPressButtonForGadget(gadget);
	}
}

function fixed_spawn_override()
{
	self endon("disconnect");

	spawnOrigin = self.origin;
	spawnAngles = self.angles;

	while (1)
	{
		self waittill("spawned_player");

		self setOrigin(spawnOrigin);
		self setPlayerAngles(spawnAngles);
	}
}

#/
*/