/*
 *  acm : an aerial combat simulator for X
 *  Copyright (C) 1991-1996  Riley Rainey
 *
 *  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; version 2 dated June, 1991.
 *
 *  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., 675 Mass Ave., Cambridge, MA 02139, USA.
 */

 /*
  *  The original "Smart Drone" code was created by
  *  Jason Nyberg (nyberg@ctron.com).  Enhancements added by Riley Rainey.
  *
  *  2007-07-23 Umberto Salsi <salsi@icosaedro.it>
  *  - made a proper "module"
  *  - drone now has a data structure by its own (see (craft)->drone field)
  *  - the 'aggressiveness' now sets the % of the max vertical load
  */

#include <stdlib.h>
#include <string.h>

#include "../util/error.h"
#include "../util/memory.h"
#include "../util/prng.h"
#include "../util/units.h"
#include "pm.h"
#include "alarm.h"
#include "aps.h"
#include "browse.h"
#include "dis_if.h"
#include "flaps.h"
#include "gear.h"
#include "inventory.h"
#include "players.h"
#include "prompt.h"
#include "radar.h"
#include "weapon.h"

#define drone_IMPORT
#include "drone.h"
#include "aps.h"

static drone_Mode mode = drone_DOG_FIGHT_MODE;
static double aggressiveness = 0.5;

/* Drone status: */
#define DM_ATTACK       0     /* normal drone attack mode */
#define DM_RETURN       1     /* return to drone activation point 
								   (interceptStartPoint) */
#define DM_RETURN_CAPTURED 2  /* in return mode and moving towards return pt */

#define MIN_VEL       300     /* min speed (KIAS) */
#define MED_VEL       350     /* medium speed (KIAS) */
#define MAX_VEL       400     /* max speed (KIAS) */

#define MIN_ALTITUDE 15000    /* min altitude (ft) */

/**
 * Drone state.
 */
typedef struct _drone_data {
	/*
	 * If takes commands over existing player's aircraft, here saves the original
	 * update procedure before replacing with the drone specific algorithm;
	 * restores when user leaves the drone mode and gets back commands.
	 */
	char * (*update_original)(craft *c);
	int       curOpponent;		/* who this drone is trying to kill; -1 = none */
	int       curDroneMode;     /* drone operating mode (DM_* constants ) */
	int       holdCount;		/* non-zero when drones holding fire */
	VPoint    interceptStartPoint; /* geocentric coordinates location
									  where an end-game intercept was
									  initiated */
} drone_Type;


void drone_set_mode(drone_Mode drone_mode)
{
	mode = drone_mode;
}


void drone_set_aggressiveness(double a)
{
	if( a < 0.05 )
		a = 0.05;
	else if( a > 1.0 )
		a = 1.0;
	aggressiveness = a;
}


static char *drone_update(craft *c);
static void  drone_kill(craft *c, char *reason);


static craft *
create_new_drone_craft(craft * p, int force, char *model)
{
	int       i;
	craft    *c;
	VPoint    s, tmp;
	double    v;
	double    disLocation[3];
	double    disZeroVec[3];
	double    disOrientation[3];

	/*
		Allocate new craft data structure:
	*/
	for (i = 0; i < manifest_MAXPLAYERS; ++i) {
		if (ptbl[i].type == CT_FREE) {
			break;
		}
	}

	if (i == manifest_MAXPLAYERS)
		return NULL;

	c = &ptbl[i];

	/*
		Set craft state:
	*/
	c->pIndex = i;
	c->type = CT_DRONE;
	c->force = force;
	c->createTime = curTime;
	c->vl = NULL;
	c->trihedral = p->trihedral;
	c->Cg = p->Cg;
	c->VT = p->VT;
	c->Sg = p->Sg;
	c->w = p->w;
	c->terrain_altitude_timeout = curTime - 10.0; /* ensure will be updated */
	/* c->terrain_altitude = UNKNOWN; */
	c->XYZtoNED = p->XYZtoNED;
	c->air = p->air;
	c->mach = p->mach;
	c->G = p->G;
	c->linAcc = p->linAcc;
	c->prevSg = p->prevSg;
	c->p = p->p;
	c->q = p->q;
	c->r = p->r;
	c->pitchComm = 0.0;
	c->rollComm = 0.0;
	c->steerComm = 0.0;
	c->rudderComm = 0.0;
	c->throttleComm = 32768;
	c->throttle = 32768;
	c->Se = p->Se;
	c->Sa = p->Sa;
	c->Sr = p->Sr;
	c->SeTrim = 0.0;
	c->SaTrim = 0.0;
	c->curHeading = p->curHeading;
	c->curPitch = p->curPitch;
	c->curRoll = p->curRoll;
	c->curThrust = 0.0;
	c->curFlap = 0.0;
	c->flapSetting = 0.0;
	c->curSpeedBrake = 0.0;
	c->speedBrakeSetting = 0.0;
	c->throttle = 32768;
	c->rpm = 1.0;
	c->alpha = p->alpha;
	c->beta = p->beta;
	c->fuel = 0.0;
	c->payload = 0.0;
	c->flags = 0;
	c->damageBits = 0;
	c->structurePts = 0;
	c->leakRate = 0.0;
	c->damageCL = 0.0;
	c->damageCM = 0.0;
	c->radarMode = RM_ACM;
	c->nextRadarTime = 0.0;
	c->cinfo = inventory_craftTypeSearchByZoneAndName(NULL, model);
	sprintf(c->name, "Drone%d", i);
	c->curRadarTarget = -1;
	c->targetDistance = 0.0;
	c->targetClosure = 0.0;
	/* skip fields: relPos, rval */
	pm_hud_strings_alloc(c);
	/* skip fields: curWeapon, station */
	/* skip fields: rinfo, rtop */
	c->showMag = FALSE;
	c->aps = NULL;
	c->curThrust = (c->cinfo->thrust) (c);
	gear_allocate(c);
	gear_up(c);

	if( mode == drone_DOG_FIGHT_MODE ){

		/*
			Position the drone about 1500 meters ahead of the player's craft.
		*/

		s.x = 1500.0 + 500.0 * prng_getDouble2();
		s.y = 200.0 * prng_getDouble2();
		s.z = 200.0 * prng_getDouble2();
	
		c->curHeading = pm_normalize_yaw( p->curHeading
			+ units_DEGtoRAD(30.0) * prng_getDouble2() );
		c->curRoll = 0.0;
		c->curPitch = units_DEGtoRAD(2.0);

		v = p->VT + 50.0 * prng_getDouble2();

		VSetPoint(&c->Cg, v*cos(c->curHeading), v*sin(c->curHeading), 0.0);
	
	} else {  /* mode == DRONE_HUNTING_MODE */

		/*
			Position the drone up to 50 NM away, random altitude,
			random direction, random heading.
		*/

		s.x = 50000.0 * prng_getDouble2();
		s.y = 50000.0 * prng_getDouble2();
		s.z = 5000.0 - p->w.z + (11000.0 - 5000.0)/2.0 * prng_getDouble2();

		c->curHeading = pm_normalize_yaw( 2*M_PI * prng_getDouble2() );
		c->curRoll = 0.0;
		c->curPitch = units_DEGtoRAD(2.0);

		v = 350.0 + 50.0 * prng_getDouble2();

		VSetPoint(&c->Cg, v*cos(c->curHeading), v*sin(c->curHeading), 0.0);
	
	}

	c->r = c->q = c->p = 0.0;

	VEulerToMatrix(c->curRoll, c->curPitch, c->curHeading,
		&(c->trihedral));

	VTransform_(&s, &(p->trihedral), &tmp);

	/*  convert NED (meters) to Geocentric (meters)  */

	VReverseTransform_(&tmp, &(c->XYZtoNED), &c->Sg);
	c->Sg.x += p->Sg.x;
	c->Sg.y += p->Sg.y;
	c->Sg.z += p->Sg.z;
	c->prevSg = c->Sg;
	
	c->update = drone_update;
	c->kill   = drone_kill;

	earth_XYZToLatLonAlt(&c->Sg, &c->w);
	earth_generateWorldToLocalMatrix(&c->w, &c->XYZtoNED);

/*
 *  Rearm and fuel the aircraft.
 */

	(*c->cinfo->resupply) (c);

	weapon_selectByName(c, weapon_AIM9M);  /* will set properly next */

	disLocation[0] = c->Sg.x;
	disLocation[1] = c->Sg.y;
	disLocation[2] = c->Sg.z;
	disZeroVec[0] = 0.0;
	disZeroVec[1] = 0.0;
	disZeroVec[2] = 0.0;
	disOrientation[0] = c->curHeading;
	disOrientation[1] = c->curPitch;
	disOrientation[2] = c->curRoll;
	dis_if_entityEnter(force, c,
					&c->cinfo->entityType,
					&c->cinfo->altEntityType,
					disLocation, disZeroVec,
					disZeroVec, disOrientation,
					disZeroVec, &c->disId);
	dis_if_setRadarMode(c, 1, 1);
	return c;
}


void drone_take_commands(craft *c)
{
	drone_Type *dd;
	
	gear_up(c);
	flaps_up(c);
	flaps_up(c);
	flaps_up(c);
	flaps_up(c);
	flaps_speed_brakes_retract(c);
	flaps_speed_brakes_retract(c);
	flaps_speed_brakes_retract(c);
	flaps_speed_brakes_retract(c);
	aps_off(c);

	c->type = CT_DRONE;
	/* ignore = */ weapon_selectByName(c, weapon_AIM9M);

	dd = (drone_Type *) c->drone;
	if( dd == NULL )
		dd = memory_allocate(sizeof(drone_Type), NULL);
	
	dd->curOpponent = -1;
	dd->curDroneMode = DM_ATTACK;
	dd->holdCount = 0;
	/* dd->inceptStartPoint = ignored in DM_ATTACK mode; */

	c->drone = dd;
	dd->update_original = c->update;
	c->update = drone_update;
}


void drone_new(craft * p)
{
	int force =  p->force == DISForceOpposing? DISForceFriendly : DISForceOpposing;
	char *model = force == DISForceFriendly? "F-16" : "MiG-29";
	craft *c = create_new_drone_craft(p, force, model);
	if( c == NULL ){
		prompt_craft_print(p, "Memory exhausted -- can't create new drone");
		return;
	}

	drone_take_commands(c);

	char s[100];
	sprintf(s, "%s drone generated for %s practice [%d]", c->name,
		(mode == drone_DOG_FIGHT_MODE)? "dog fight" : "hunting", mode);
	
	prompt_craft_print(p, s);
}


void drone_release_commands(craft *c)
{
	drone_Type *dd = c->drone;
	
	if( dd == NULL )
		return;
	
	c->update = dd->update_original;
	memory_dispose(c->drone);
	c->drone = NULL;
	c->type = CT_PLANE;
}


void drone_reset_opponent(craft *c)
{
	drone_Type *dd;

	if( c->drone == NULL )
		return;
	
	dd = (drone_Type *) c->drone;
	dd->curOpponent = -1;
	dd->holdCount = 0;
}


/*
 *  convert target plane coords from world to drone's
 */

static void
myCoordSys(craft * c, craft * p, VPoint * pos, VPoint * vel)
{
	VPoint    tpos;

	VTransform(&p->prevSg, &c->XYZtoNED, &tpos);
	VReverseTransform_(&tpos, &c->trihedral, pos);
	VTransform_(&p->Cg, &c->XYZtoNED, &tpos);
	VReverseTransform_(&tpos, &c->trihedral, vel);
}

static void
unholdFireAlarm(void *arg1, void *arg2)
{
	craft    *c = (craft *) arg1;
	drone_Type *dd;

	if( c->drone == NULL )
		return;
	
	dd = (drone_Type *) c->drone;

	if (dd->holdCount > 0) {
		dd->holdCount--;
	}
}

/* 
 *  Dumbly choose the closest hostile plane to be target
 */

static int
pickTarget(craft * c)
{
	int       i, target = -1;
	craft    *p;
	double    d, min;
	VPoint    pos, vel;

	min = 100000000.0;
	for (i = 0, p = ptbl; i < manifest_MAXPLAYERS; ++i, ++p) {

		if ( p != c && p->force != c->force
		&& (p->type == CT_PLANE || 
			p->type == CT_DIS_PLANE ||
			p->type == CT_DRONE)
		) {

			myCoordSys(c, p, &pos, &vel);

			d = VMagnitude( &pos );

			if (d < min) {
				min = d;
				target = p->pIndex;
			}
		}
	}

	return target;
}

/*
 *  droneFlyTo
 *
 *  Generate sitck/rudder controls to move the plane to the specified
 *  geocentric point.
 */

static void
droneFlyTo ( craft *c, VPoint *pos )
{
	double phi, rho, w, f, pitch_rate, roll_rate;
	craftType *p;
	VPoint q;

	/*****  Example: climb toward north
	VPoint q;
	q.x = 100.0;
	q.y = 0.0;
	q.z = -20.0;

	VReverseTransform(&q, &c->trihedral, pos);
	*****/

	/*
		Avoid too low altitudes
	*/

	if( c->w.z < units_FEETtoMETERS(MIN_ALTITUDE) ){
		VTransform(pos, &c->trihedral, &q);
		if( q.z >= 0.0 ){
			q.z = -0.5*sqrt(q.x*q.x + q.y*q.y);
		}
		VReverseTransform(&q, &c->trihedral, pos);
	}

	/* d = sqrt(pos->x * pos->x + pos->y * pos->y + pos->z * pos->z); */

	/*
	 * Compute total lift force 'f':
	 */

	p = c->cinfo;
	w = p->emptyWeight + c->fuel + c->payload;
	f = c->G.z * w; /* current vertical load (pounds) */
	phi = atan2 ( pos->y, -pos->z );

	/*
	 *  If the target is behind our 3/9-line, we are defensive.  Pull maximum
	 *  G's into the target (after rolling into him).
	 *
	 *  If we are behind of the target, perform pure pursuit (until the code 
	 *  gets a bit smarter).
	 */

	if (f < -aggressiveness * p->maxLoadZPositive) {
		/*
		 * Close to the positive structural load limit.
		 */
		if ( c->q > 0.0 )
			pitch_rate = 0.95 * c->q;
		else
			pitch_rate = 0.0;
		
	} else if (f > aggressiveness * p->maxLoadZNegative) {
		/*
		 * Close to the negative structural load limit.
		 */
		if ( c->q < 0.0 )
			pitch_rate = 0.95 * c->q;
		else
			pitch_rate = 0.0;

	} else if (pos->x < 0.0) {
		/*
		 *  Target behind me.
		 *  Wait for lift vector to be close to where we want it before
		 *  pulling G's.
		 */
		if ((fabs(phi) > units_DEGtoRAD(130.0))
		|| fabs(phi) < units_DEGtoRAD(50.0)) {
			pitch_rate = units_DEGtoRAD(20.0);
		} else {
			pitch_rate = 0.0;
		}

	} else {
		rho = atan2(-pos->z, pos->x) - units_DEGtoRAD(2.0);
		if( rho > 0.0 ){
			pitch_rate = fmin(rho, units_DEGtoRAD(20.0));
		} else {
			pitch_rate = 0.0;
		}
	}

	c->pitchComm -= deltaT * (pitch_rate - c->q);
	if (c->pitchComm > 1.0)
		c->pitchComm = 1.0;
	else if (c->pitchComm < -1.0)
		c->pitchComm = -1.0;

	/*
	 *  Put the lift vector on the target.
	 *  A lot of conventional 1V1 air combat involves keeping your lift
	 *  vector on the target aircraft.  Phi is the computed angle between our
	 *  target and the lift vector (simplified to be just the negative Z-axis).
	 */

	roll_rate = 0.5 * phi;
	if ( roll_rate > units_DEGtoRAD(30.0) )
		roll_rate = units_DEGtoRAD(30.0);
	else if ( roll_rate < -units_DEGtoRAD(30.0) )
		roll_rate = -units_DEGtoRAD(30.0);
	
	c->rollComm -= deltaT * (roll_rate - c->p);
	if (c->rollComm > 1.0)
		c->rollComm = 1.0;
	else if (c->rollComm < -1.0)
		c->rollComm = -1.0;

	/*
	 *  Don't use the rudder, for now.
	 */

	c->rudderComm = 0.0;

	/*
	 * Adjust engine power
	 */

	if( c->IAS < units_KTtoFPS(MIN_VEL) ){
		c->throttleComm = 32768;  /* max power + AB */
		pm_after_burner_on(c);
	} else if( c->IAS < units_KTtoFPS(MED_VEL) ){
		c->throttleComm = 32768;  /* max power */
		pm_after_burner_off(c);
	} else if( c->IAS < units_KTtoFPS(MAX_VEL) ){
		c->throttleComm = 32768/2;  /* medium power */
		pm_after_burner_off(c);
	} else {
		c->throttleComm = 32768/4;  /* idle */
		pm_after_burner_off(c);
	}

}

/*
 *  Drone flight management in Attack Mode (this is most common)
 */

static int
droneCalculationsAttackMode ( craft * c )
{
	drone_Type *dd;
	double    htime;
	VPoint    pos, vel;
	_BOOL     doFire;

	dd = (drone_Type *) c->drone;

	/*
	 * Our opponent has exited?  Return to engagement initiation point.
	 */

	if ( (c->flags & FL_END_GAME_DRONE ) ) {
		 if ( dd->curOpponent != -1 &&
			 ptbl[dd->curOpponent].type == CT_FREE ) {
			 dd->curDroneMode = DM_RETURN;
		 }
	}

	/*
	 *  No opponent, or opponent isn't there anymore
	 */

	else if (dd->curOpponent == -1 ||
		ptbl[dd->curOpponent].type == CT_FREE) {
		dd->curOpponent = pickTarget(c);
		dd->holdCount = 0;
	}

	if (dd->curOpponent != -1) {

		myCoordSys(c, &(ptbl[dd->curOpponent]), &pos, &vel);

		droneFlyTo ( c, &pos );

		/*
		 *  Fire at the target, if appropriate.
		 *  Tryes first with AIM-120, then AIM-9M.
		 *
		 *  We'll have to figure out a way to do lead pursuit in order to 
		 *  fire the cannon; we do pure pursuit now, which is the 
		 *  (somewhat) right thing to fire a missile.
		 */

		if (dd->holdCount == 0 ) {

			/*
				Select weapon. Try AIM-120 first, then AIM-9M.
			*/

			if( weapon_getReadyStation(c, weapon_AIM120) >= 0
			&& weapon_selectByName(c, weapon_AIM120) )
				doFire = TRUE; /* Uses AIM-120 */

			else if( weapon_getReadyStation(c, weapon_AIM9M) >= 0
			&& weapon_selectByName(c, weapon_AIM9M) )
				doFire = TRUE; /* Uses AIM-9M */

			else
				doFire = FALSE; /* No suitable weapon available. */

			if( doFire && weapon_displaySelected(c, (viewer *) NULL, 0, 0) == 1) {
				weapon_fire(c);
				htime = 10.0 + 10.0 * prng_getDouble2();
				alarm_add(htime, unholdFireAlarm, c, NULL);
				dd->holdCount++;
			}
		}


	}

	return 0;
}

/*
 *  Drone flight management in Return Modes
 */

static int
droneCalculationsReturnMode ( craft * c )
{
	drone_Type *dd;
	VPoint tpos, pos, vel;
	double dist_meters, closure_meters_per_sec;
	int result = 0;

	dd = (drone_Type *) c->drone;

	/*
	 *  Generate body relative position of return point
	 */

	VTransform(&dd->interceptStartPoint, &c->XYZtoNED, &tpos);
	VReverseTransform_(&tpos, &c->trihedral, &pos);

	/*
	 *  Convert NED velocity to body-relative velocity
	 */

	VReverseTransform_(&c->Cg, &c->trihedral, &vel);

	dist_meters = VMagnitude( &pos );

	closure_meters_per_sec = 
		( vel.x * pos.x + vel.y * pos.y + vel.z * pos.z ) / dist_meters;

	droneFlyTo ( c, &pos );

	/*
	 * If we are in return mode and turned within 30 degrees towards 
	 * the return point, enter the "return-captured" mode.
	 */

	if ( dd->curDroneMode == DM_RETURN ) {

		if ( closure_meters_per_sec > 0.866 * units_FEETtoMETERS(c->VT) ) {
			dd->curDroneMode = DM_RETURN_CAPTURED;
		}
	}

	/*
	 * If we are in return-capture mode and start to move away from the
	 * intercept point, then we're done; destroy the aircraft.
	 */

	else {
		if ( closure_meters_per_sec < 0.0 ) {
			result = 1;
		}
	}

	return result;
}


/**
 * Update flight and combat management of the drone.
 * @return FALSE stands for mission in progress, TRUE for drone self-destroy
 * request on mission concluded.
 */
static _BOOL
drone_calculations( craft * c )
{
	drone_Type *dd;

	dd = (drone_Type *) c->drone;

	switch (dd->curDroneMode) {

	case DM_ATTACK:
		return droneCalculationsAttackMode ( c );

	case DM_RETURN:
		return droneCalculationsReturnMode ( c );

	case DM_RETURN_CAPTURED:
		return droneCalculationsReturnMode ( c );

	default:
		error_internal("dd->curDroneMode=%d", dd->curDroneMode);
	}
}


static char *drone_update(craft *c)
{
	if( drone_calculations(c) )
		return "mission completed, self destroy";

	char *killReason = pm_flightCalculations(c);
	if (killReason != NULL)
		return killReason;

	weapon_update(c);
	flaps_update(c);
	radar_droneUpdate(c);
	dis_if_updateLocal(c);
	return NULL;
}


static void drone_kill(craft *c, char *reason)
{
	players_kill(c, reason);
}


void
drone_endGameDistanceCheck(void * p1, void *p2)
{
	double range_meters;
	VPoint del;
	craft *p;
	craft *c =  (craft * ) p1;
	viewer *u = (viewer *) p2;
	int i;
	int done = 0;
	double threshold_meters;

	if ( c->type == CT_DIS_PLANE && (c->flags & FL_END_GAME_DRONE) ) {

		/*
		 * Determine the appropriate range threshold; if one was specified
		 * on the command line, use that. Otherwise use the lock range from
		 * the aircraft definition.
		 */

		if ( end_game_threshold_meters <= 0.0 ) {
			threshold_meters = units_FEETtoMETERS( c->cinfo->radarTRange * units_NmToFeetFactor );
		}
		else {
			threshold_meters = end_game_threshold_meters;
		}

		for ((i = 0, p = ptbl); (i < manifest_MAXPLAYERS) && ( ! done ); (++i, ++p)) {

			/*
			 * Skip this entry if:
			 *
			 *   1) It's the entry for our own aircraft.
			 *   2) The craft isn't a hostile.
			 */

			if (p->pIndex == c->pIndex || c->force == p->force ) {
				continue;
			}

			if ( p->type == CT_PLANE || 
				 p->type == CT_DIS_PLANE ||
				 p->type == CT_DRONE) {

				VSub(&p->Sg, &c->Sg, &del);

				range_meters = VMagnitude( &del );

				/*
				 * If the distance is within our threshold, then
				 * initiate a control request.
				 */

				if ( range_meters <= threshold_meters ) {

					/*
					 *  Record start point of engagement; we will return
					 *  to this point after a kill.
					 */
					if( c->drone != NULL )
						((drone_Type *)(c->drone))->interceptStartPoint = c->Sg;

					dis_if_requestControl ( dis_if_findEntityByID(c->disId),
						browse_controlRequestCallback, u );

					done = 1;
				}

			}
		}
	}

	/*
	 *  This craft is no longer a DIS plane.  No need to continue proximity
	 *  testing.
	 */

	else {
		done = 1;
	}

	/*
	 *  If nothing was within our threshold range, look again after one
	 *  second has elapsed.
	 */

	if ( ! done ) {
		alarm_add(1.0, drone_endGameDistanceCheck, p1, p2);
	}
}
