/*
   Copyright (C) 1997-2001 Id Software, Inc.

   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 2
   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, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 */

#include "../g_local.h"
#include "ai_local.h"

//ACE


//==========================================
// AI_Init
// Inits global parameters
//==========================================
void AI_Init( void )
{
	//Init developer mode
	bot_showpath = trap_Cvar_Get( "bot_showpath", "0", 0 );
	bot_showcombat = trap_Cvar_Get( "bot_showcombat", "0", 0 );
	bot_showsrgoal = trap_Cvar_Get( "bot_showsrgoal", "0", 0 );
	bot_showlrgoal = trap_Cvar_Get( "bot_showlrgoal", "0", 0 );
	bot_debugmonster = trap_Cvar_Get( "bot_debugmonster", "0", 0 );
	bot_dummy = trap_Cvar_Get( "bot_dummy", "0", 0 );

	AIDevel.debugMode = qfalse;
	AIDevel.debugChased = qfalse;
	AIDevel.chaseguy = NULL;
	AIDevel.showPLinks = qfalse;
	AIDevel.plinkguy = NULL;
}

//==========================================
// AI_NewMap
// Inits Map local parameters
//==========================================
void AI_NewMap( void )
{
	edict_t	*ent;

	//Load nodes
	AI_InitNavigationData();
	AI_InitAIWeapons();

	// count bots
	game.numBots = 0;
	for( ent = game.edicts + 1; PLAYERNUM( ent ) < game.maxclients; ent++ )
	{
		if( !ent->r.inuse || !ent->ai.type ) continue;
		if( ent->r.svflags & SVF_FAKECLIENT && ent->ai.type == AI_ISBOT )
			game.numBots++;
	}
}

//==========================================
// G_FreeAI
// removes the AI handle from memory
//==========================================
void G_FreeAI( edict_t *ent )
{
	if( !ent->ai.type ) return;
	if( ent->ai.type == AI_ISBOT )
	{
		game.numBots--;
	}

	ent->ai.type = AI_INACTIVE;
	//memset( &ent->ai, 0, sizeof( ai_handle_t ) );
	//G_Free( ent->ai );
	//ent->ai = NULL;
}

//==========================================
// G_SpawnAI
// allocate ai_handle_t for this entity
//==========================================
void G_SpawnAI( edict_t *ent )
{
	//if( !ent->ai )
	//	ent->ai = G_Malloc(sizeof(ai_handle_t));

	memset( &ent->ai, 0, sizeof( ai_handle_t ) );
	if( ent->r.svflags & SVF_FAKECLIENT )
		ent->ai.type = AI_ISBOT;
	else
		ent->ai.type = AI_ISMONSTER;
}


//==========================================
// AI_SetUpMoveWander
//==========================================
void AI_SetUpMoveWander( edict_t *ent )
{
	ent->ai.state = BOT_STATE_WANDER;
	ent->ai.wander_timeout = level.time + 1000;
	ent->ai.nearest_node_tries = 0;

	ent->ai.next_move_time = level.time;
	ent->ai.blocked_timeout = level.time + 15000;

	ent->ai.goal_node = INVALID;
	ent->ai.current_node = INVALID;
	ent->ai.next_node = INVALID;
	ent->ai.current_link_type = LINK_INVALID;
}


//==========================================
// AI_ResetWeights
// Init bot weights from bot-class weights.
//==========================================
void AI_ResetWeights( edict_t *ent )
{
	//restore defaults from bot persistant
	memset( ent->ai.status.inventoryWeights, 0, sizeof( ent->ai.status.inventoryWeights ) );
	memcpy( ent->ai.status.inventoryWeights, ent->ai.pers.inventoryWeights, sizeof( ent->ai.pers.inventoryWeights ) );
}


//==========================================
// AI_ResetNavigation
// Init bot navigation. Called at first spawn & each respawn
//==========================================
void AI_ResetNavigation( edict_t *ent )
{
	int i;

	ent->enemy = NULL;
	ent->movetarget = NULL;
	ent->ai.state_combat_timeout = 0;

	ent->ai.state = BOT_STATE_WANDER;
	ent->ai.wander_timeout = level.time;
	ent->ai.nearest_node_tries = 0;

	ent->ai.next_move_time = level.time;
	ent->ai.blocked_timeout = level.time + 15000;

	ent->ai.goal_node = INVALID;
	ent->ai.current_node = INVALID;
	ent->ai.next_node = INVALID;

	VectorSet( ent->ai.move_vector, 0, 0, 0 );

	//reset bot_roams timeouts
	for( i = 0; i < nav.num_broams; i++ )
		ent->ai.status.broam_timeouts[i] = 0.0;
}


//==========================================
// AI_BotRoamForLRGoal
//
// Try assigning a bot roam node as LR Goal
//==========================================
static qboolean AI_BotRoamForLRGoal( edict_t *self, int current_node )
{
	int i;
	float cost;
	float weight, best_weight = 0.0;
	int goal_node = INVALID;
	int best_broam = INVALID;
	float dist;

	if( !nav.num_broams )
		return qfalse;

	for( i = 0; i < nav.num_broams; i++ )
	{
		if( self->ai.status.broam_timeouts[i] > level.time )
			continue;

		//limit cost finding by distance
		dist = DistanceFast( self->s.origin, nodes[nav.broams[i].node].origin );
		if( dist > 10000 )
			continue;

		//find cost
		cost = AI_FindCost( current_node, nav.broams[i].node, self->ai.pers.moveTypesMask );
		if( cost == INVALID || cost < 3 )  // ignore invalid and very short hops
			continue;

		cost *= random(); // Allow random variations for broams
		weight = nav.broams[i].weight / cost; // Check against cost of getting there

		if( weight > best_weight )
		{
			best_weight = weight;
			goal_node = nav.broams[i].node;
			best_broam = i;
		}
	}

	if( best_weight == 0.0 || goal_node == INVALID )
		return qfalse;

	//set up the goal
	self->ai.state = BOT_STATE_MOVE;
	self->ai.tries = 0; // Reset the count of how many times we tried this goal

	//if(AIDevel.debugChased && bot_showlrgoal->integer)
	//	G_PrintMsg (AIDevel.chaseguy, "%s: selected a bot roam of weight %f at node %d for LR goal.\n",self->ai.pers.netname, nav.broams[best_broam].weight, goal_node);
	AI_SetGoal( self, goal_node );

	return qtrue;
}



qboolean AI_IsTimedItem( edict_t *item )
{

	if( (game.gametype != GAMETYPE_TDM) && game.gametype != GAMETYPE_DUEL )
	    return qfalse;

	if( !item || !item->classname )
		return qfalse;

	return !Q_stricmp( item->classname, "item_armor_ra" ) ||
	       !Q_stricmp( item->classname, "item_health_mega" ) ||
	       !Q_stricmp( item->classname, "item_warshell" ) ||
	       !Q_stricmp( item->classname, "item_quad" );
}

//==========================================
// AI_PickLongRangeGoal
//
// Evaluate the best long range goal and send the bot on
// its way. This is a good time waster, so use it sparingly.
// Do not call it for every think cycle.
//
// jal: I don't think there is any problem by calling it,
// now that we have stored the costs at the nav.costs table (I don't do it anyway)
//==========================================
void AI_PickLongRangeGoal( edict_t *self )
{
	int i;
	int node;
	float weight, best_weight = 0.0;
	int current_node, goal_node = INVALID;
	edict_t *goal_ent = NULL;
	float cost;
	float dist;

	self->ai.vsay_goalent = NULL;

	// look for a target
	current_node = AI_FindClosestReachableNode( self->s.origin, self, ( ( 1+self->ai.nearest_node_tries )*NODE_DENSITY ), NODE_ALL );
	self->ai.current_node = current_node;

	if( current_node == -1 ) //failed. Go wandering :(
	{
		if( AIDevel.debugChased && bot_showlrgoal->integer )
			G_PrintMsg( AIDevel.chaseguy, "%s: LRGOAL: Closest node not found. Tries:%i\n", self->ai.pers.netname, self->ai.nearest_node_tries );

		if( self->ai.state != BOT_STATE_WANDER )
			AI_SetUpMoveWander( self );

		self->ai.wander_timeout = level.time + 1000;
		self->ai.nearest_node_tries++; //extend search radius with each try
		return;
	}
	self->ai.nearest_node_tries = 0;


	// Items
	for( i = 0; i < nav.num_items; i++ )
	{
		// Ignore items that are not there (solid)
		if( !nav.items[i].ent || nav.items[i].ent->r.solid == SOLID_NOT )
		{

			// nah... timed items...
			if( !AI_IsTimedItem( nav.items[i].ent ) )
				continue;
		}

		//ignore items wich can't be weighted (must have a valid item flag)
		if( !nav.items[i].ent->item || !( nav.items[i].ent->item->type & ( IT_AMMO|IT_HEALTH|IT_ARMOR|IT_WEAPON|IT_POWERUP|IT_FLAG ) ) )
		{
			continue;
		}

		if( !G_Gametype_CanPickUpItem( nav.items[i].ent->item ) )
		{
			continue;
		}

		weight = AI_ItemWeight( self, nav.items[i].ent );
		if( weight == 0.0f ) //ignore zero weighted items
		{
			continue;
		}

		//limit cost finding distance
		dist = DistanceFast( self->s.origin, nav.items[i].ent->s.origin );

		//different distance limits for different types
		if( !AI_IsTimedItem( nav.items[i].ent ) )
		{
			if( dist > 30000 )  // 30000 is insane, only a huge terrain map could have it
				continue;
			else if( nav.items[i].ent->item->type & ( IT_AMMO ) && dist > 2000 )
				continue;
			else if( nav.items[i].ent->item->type & ( IT_HEALTH|IT_ARMOR ) && dist > 5000 )
				continue;
			else if( nav.items[i].ent->item->type & ( IT_POWERUP ) && dist > 10000 )
				continue;
			else if( nav.items[i].ent->item->type & IT_WEAPON && dist > 10000 )
				continue;
			//else if( nav.items[i].ent->item->type & IT_FLAG && dist > 20000 )
			//	continue;
		}

		cost = AI_FindCost( current_node, nav.items[i].node, self->ai.pers.moveTypesMask );
		if( cost == INVALID )  // ignore invalid
			continue;

		//if( (!AI_IsTimedItem( nav.items[i].ent) || !(nav.items[i].ent->item->type & IT_CAPTURE_AREA )) && cost < 3 ) {
		if( ( !AI_IsTimedItem( nav.items[i].ent ) ) && cost < 3 )
		{
			// ignore ery short hops
			// XXX: torbenh... why ?
			continue;
		}

		//weight *= random(); // Allow random variations
		weight /= cost; // Check against cost of getting there

		if( weight > best_weight )
		{
			best_weight = weight;
			goal_node = nav.items[i].node;
			goal_ent = nav.items[i].ent;
		}
	}


	// Players: This should be its own function and is for now just finds a player to set as the goal.
	for( i = 0; i < num_AIEnemies; i++ )
	{
		// ignore self & spectators
		if( AIEnemies[i] == self || AIEnemies[i]->r.solid == SOLID_NOT )
			continue;

		// ignore zero weighted players
		if( self->ai.status.playersWeights[i] == 0.0f )
			continue;

		node = AI_FindClosestReachableNode( AIEnemies[i]->s.origin, AIEnemies[i], NODE_DENSITY, NODE_ALL );
		cost = AI_FindCost( current_node, node, self->ai.pers.moveTypesMask );

		if( cost == INVALID || cost < 1 )  // ignore invalid and very short hops
			continue;

		//precomputed player weights
		weight = self->ai.status.playersWeights[i];

		//weight *= random(); // Allow random variations
		weight /= cost; // Check against cost of getting there

		if( weight > best_weight )
		{
			best_weight = weight;
			goal_node = node;
			goal_ent = AIEnemies[i];
		}
	}

	// If do not find a goal, go wandering....
	if( best_weight == 0.0 || goal_node == INVALID )
	{
		//BOT_ROAMS
		if( !AI_BotRoamForLRGoal( self, current_node ) )
		{
			self->ai.goal_node = INVALID;
			self->ai.state = BOT_STATE_WANDER;
			self->ai.wander_timeout = level.time + 1000;
			if( AIDevel.debugChased && bot_showlrgoal->integer )
				G_PrintMsg( AIDevel.chaseguy, "%s: did not find a LR goal, wandering.\n", self->ai.pers.netname );
		}
		return; // no path?
	}

	// OK, everything valid, let's start moving to our goal.
	self->ai.state = BOT_STATE_MOVE;
	self->ai.tries = 0; // Reset the count of how many times we tried this goal

	if( goal_ent != NULL && AIDevel.debugChased && bot_showlrgoal->integer )
		G_PrintMsg( AIDevel.chaseguy, "%s: selected a %s at node %d for LR goal. (weight %f)\n", self->ai.pers.netname, goal_ent->classname, goal_node, best_weight );

	self->ai.vsay_goalent = goal_ent;

	AI_SetGoal( self, goal_node );
}


//==========================================
// AI_PickShortRangeGoal
// Pick best goal based on importance and range. This function
// overrides the long range goal selection for items that
// are very close to the bot and are reachable.
//==========================================
static void AI_PickShortRangeGoal( edict_t *self )
{
	edict_t *target;
	float weight, best_weight = 0.0;
	edict_t *best = NULL;

	if( !self->r.client )
		return;

	if( match.state != MATCH_STATE_PLAYTIME && match.state != MATCH_STATE_WARMUP )
	{
		self->movetarget = self->goalentity = NULL;
		return;
	}

	// if the bot is jumping, don't try to catch stuff
	if( ( !self->groundentity && ( self->ai.current_link_type & LINK_JUMP ) )
	   || ( !self->groundentity && ( self->ai.current_link_type & LINK_JUMPPAD ) ) )
	{
		self->movetarget = self->goalentity = NULL;
		return;
	}

	// look for a target (should make more efficent later)
	target = findradius( NULL, self->s.origin, AI_GOAL_SR_RADIUS );

	while( target )
	{
		// Missile detection code
		if( target->s.type == ET_BLASTER || target->s.type == ET_ELECTRO_WEAK
		    || target->s.type == ET_GRENADE || target->s.type == ET_PLASMA
		    || target->s.type == ET_ROCKET || target->s.type == ET_SHOCKWAVE )
		{
			if( target->r.owner && !G_IsTeamDamage( self, target->r.owner ) )
			{
				//if player who shoot is a potential enemy
				if( AIDevel.debugChased && bot_showcombat->integer )
					G_PrintMsg( AIDevel.chaseguy, "%s: ROCKET ALERT!\n", self->ai.pers.netname );
				if( !target->r.owner->ai.notarget && self->ai.status.playersWeights[target->r.owner->s.number] )
					AI_NewEnemyInView( self, target->r.owner );
				// set who fired the rocket as enemy
			}
			if( self->enemy )
				return;
		}

		//if( G_Gametype_CanPickUpItem(target->item) )
		//{
		if( AI_ItemIsReachable( self, target->s.origin ) )
		{
			if( G_InFront( self, target ) )
			{
				int target_node = AI_FindClosestReachableNode( target->s.origin, self, NODE_DENSITY, NODE_ALL );
				int cost1, cost2;
				//int goalent_node = AI_FindClosestReachableNode( nodes[0].origin, self, NODE_DENSITY, NODE_ALL );

				int goalent_node = self->ai.path.nodes[0];
//				    if( target->item )
//					target_node = nav.items[target->item->tag].node;
//				    else
//					target_node = self->ai.current_node;




//int AI_FindCost(int from, int to, int movetypes)

				cost1 = AI_FindCost( self->ai.current_node, goalent_node, self->ai.pers.moveTypesMask );
				cost2 = AI_FindCost( target_node, goalent_node, self->ai.pers.moveTypesMask );

				if( cost1 == -1 )
					cost1 = cost2;

				if( cost2 == -1 )
					cost2 = 1000;

				//printf( "cost: %d cost2: %d %d, %d\n", cost1, cost2, target_node, goalent_node );
				if( ( cost2 - cost1 ) < 5 )
				{

					weight = AI_ItemWeight( self, target );
					if( weight > best_weight )
					{
						best_weight = weight;
						best = target;
					}
				}
			}
		}
		//}

		// next target
		target = findradius( target, self->s.origin, AI_GOAL_SR_RADIUS );
	}

	//jalfixme (what's goalentity doing here?)
	if( best_weight )
	{
		self->movetarget = best;
		//self->goalentity = best;
		if( AIDevel.debugChased && bot_showsrgoal->integer && ( self->goalentity != self->movetarget ) )
			G_PrintMsg( AIDevel.chaseguy, "%s: selected a %s for SR goal.\n", self->ai.pers.netname, self->movetarget->classname );
	}
}


//===================
//  AI_CategorizePosition
//  Categorize waterlevel and groundentity/stepping
//===================
void AI_CategorizePosition( edict_t *ent )
{
	qboolean stepping = AI_IsStep( ent );

	ent->was_swim = ent->is_swim;
	ent->was_step = ent->is_step;

	ent->is_ladder = AI_IsLadder( ent->s.origin, ent->s.angles, ent->r.mins, ent->r.maxs, ent );

	G_CategorizePosition( ent );
	if( ent->waterlevel > 2 || ( ent->waterlevel && !stepping ) )
	{
		ent->is_swim = qtrue;
		ent->is_step = qfalse;
		return;
	}

	ent->is_swim = qfalse;
	ent->is_step = stepping;
}


//==========================================
// AI_Think
// think funtion for AIs
//==========================================
void AI_Think( edict_t *self )
{
	if( !self->ai.type )
	{
		return;
	}

	AIDebug_SetChased( self ); //jal:debug shit
	AI_CategorizePosition( self );

	//freeze AI when dead
	if( G_IsDead( self ) || match.state >= MATCH_STATE_POSTMATCH )
	{
		self->ai.pers.deadFrame( self );
		return;
	}

	//if completely stuck somewhere

	if( self->ai.blocked_timeout < level.time )
	{
		self->ai.pers.blockedTimeout( self );
		return;
	}

	if( VectorLengthFast( self->velocity ) > 37 )
		self->ai.blocked_timeout = level.time + 10000;

	//update status information to feed up ai
	self->ai.pers.UpdateStatus( self );

	//if it's at spectator team, return
	if( self->s.team == TEAM_SPECTATOR || self->r.solid == SOLID_NOT )
	{
		return;
	}

	if( AI_PlinkExists( self->ai.current_node, self->ai.next_node ) )
	{
		self->ai.current_link_type = AI_PlinkMoveType( self->ai.current_node, self->ai.next_node );
		//Com_Printf("%s\n", AI_LinkString( current_link_type ));
	}
	else
		self->ai.current_link_type = LINK_INVALID;

	//update position in path, set up move vector
	if( self->ai.state == BOT_STATE_MOVE )
	{
		if( !AI_FollowPath( self ) )
		{
			AI_SetUpMoveWander( self );
			self->ai.wander_timeout = level.time + 1; // do it next frame
		}
	}

	//pick a new long range goal
	if( self->ai.state == BOT_STATE_WANDER && self->ai.wander_timeout < level.time )
		AI_PickLongRangeGoal( self );

	//Find any short range goal
	AI_PickShortRangeGoal( self );

	//run class based states machine
	self->ai.pers.RunFrame( self );
}
