/*
 *  acm : an aerial combat simulator for X
 *  Copyright (C) 1991-1998  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.
 */

#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "pm.h"
#include "dis_if.h"
#include "effects.h"
#include "damage.h"
#include "inventory.h"
#include "planes.h"
#include "terrain.h"
#include "../util/error.h"
#include "../util/memory.h"
#include "../util/units.h"

#define missile_IMPORT
#include "missile.h"

/**
 * Estimated closer point to the target.
 */
typedef struct missile_CloserPoint {
	
	/** Target. */
	craft    *c;
	
	/** Time to target (s). */
	double    time;
	
	/** Distance to target (m). */
	double    min;
	
	/** Position of the missile (world coordinates, m). */
	VPoint    Sg;
	
	/** Position of explosion relative in the target frame (m). */
	VPoint    rpos;
	
	/** Velocity of the missile in target frame (m/s). */
	VPoint    rvel;
	
	/** Next estimated entry. */
	struct missile_CloserPoint *next;
	
} missile_CloserPoint;

static int mdebug = 0;
static int isMissileHit(double min, craft * c);
static int  missile_lookForImpacts(craft *m);
static void missile_trackTarget(craft * c);
static void missile_kill(craft * c, char *reason);

/**
 * Tells if the craft is above (0), in (1), below (2) the clouds.
 * Returns 0 when no clouds at all.
 */
static int inCloud(craft * c)
{
	int       state;
	
	if (clouds_top <= clouds_base || c->w.z > clouds_top) {
		state = 0;
	}
	else if (c->w.z > clouds_base) {
		state = 1;
	}
	else {
		state = 2;
	}
	return state;
}


static char *
missile_update(craft * c)
{
	double    q;
	double    FDrag, FWeight;
	double    dNorth, dEast, dmag;
	VPoint    F, Fg;

/*
 *  Check for ground impact.  We do this at the beginning to permit us to
 *  kill ground targets.
 */

	if (c->w.z < terrain_localAltitude(c)) {
		q = -c->prevSg.z / (c->Sg.z - c->prevSg.z);
		c->Sg.x = c->prevSg.x + q * (c->Sg.x - c->prevSg.x);
		c->Sg.y = c->prevSg.y + q * (c->Sg.y - c->prevSg.z);
		c->Sg.z = 0.0;
		return "hit the ground";
	}

	missile_trackTarget(c);

/*
 *  If we haven't armed the missile, yet.  Decrement the delay timer.
 *  If the FL_BALLISTIC flag is set, we have no target; self-destruct
 *  if the timer expires.
 */

	if (c->armTimer != 0.0) {
		if ((c->armTimer -= deltaT) < 0.0) {
			if (c->flags & FL_BALLISTIC) {
				return "no target -- self destruct";
			}
			c->armTimer = 0.0;
		}
	}

/*
 *  Re-orient the body of the missile towards it's intended target.
 */

	c->prevSg = c->Sg;

	air_update(&c->air, units_METERStoFEET(c->w.z));

/*
 *  Compute the resultant force vector on the missile.
 */

	c->VT = VMagnitude(&c->Cg);
	q = c->air.rho * c->cinfo->wingS * c->VT * c->VT * 0.5;
	FDrag = c->cinfo->CDOrigin * q;

	F.x = c->curThrust - FDrag;
	F.y = 0.0;
	F.z = 0.0;

/*
 *  Now calculate changes in position (Sg) and velocity (Cg).
 */

	if ((c->fuel -= planes_fuelUsed(c)) <= 0.0) {
/*
		if (c->curThrust > 0.0)
			if (mdebug)
				printf("Missile burnout; velocity = %g fps (%g kt)\n", c->VT,
					   units_FPStoKT(c->VT));
*/
		c->fuel = 0.0;
		c->curThrust = 0.0;
	}

/*
 *  The missile's trihedral matrix is managed by 
 *  missile_trackTarget().
 */

	VTransform(&F, &c->AXYZtoNED, &Fg);
	FWeight = c->cinfo->emptyWeight + c->fuel + c->payload;
	Fg.z += FWeight;
	pm_calcGForces(c, &Fg, FWeight);

/*
	if (mdebug) {
		printf("v = %g kt, Fg = { %g, %g, %g }\n", units_FPStoKT(c->VT),
			   Fg.x, Fg.y, Fg.z);
		printf("F = { %g, %g, %g }\n", F.x, F.y, F.z);
	}
*/

/*
 *  Update the missile's position and velocity.
 */

	dNorth = units_FEETtoMETERS(c->Cg.x * deltaT + Fg.x / FWeight
						  * units_earth_g * halfDeltaTSquared);
	dEast = units_FEETtoMETERS(c->Cg.y * deltaT + Fg.y / FWeight
						 * units_earth_g * halfDeltaTSquared);
	c->w.z -= units_FEETtoMETERS(c->Cg.z * deltaT + Fg.z / FWeight
						   * units_earth_g * halfDeltaTSquared);

	dmag = sqrt(dNorth * dNorth + dEast * dEast);

	earth_updateLatLon(&c->w, dNorth / dmag, dEast / dmag, dmag);
	earth_LatLonAltToXYZ(&c->w, &c->Sg);
	earth_generateWorldToLocalMatrix(&c->w, &c->XYZtoNED);

	c->Cg.x += Fg.x / FWeight * units_earth_g * deltaT;
	c->Cg.y += Fg.y / FWeight * units_earth_g * deltaT;
	c->Cg.z += Fg.z / FWeight * units_earth_g * deltaT;

/*
	if (mdebug) {
		printf("Altitude = %g ft\n", units_METERStoFEET(c->w.z));
		printf("Euler angles { %g, %g, %g }\n", units_RADtoDEG(c->curRoll),
			   units_RADtoDEG(c->curPitch), units_RADtoDEG(c->curHeading));
		printf("Cg = { %g, %g, %g }\n", c->Cg.x, c->Cg.y, c->Cg.z);
		printf("Sg = { %g, %g, %g }\n", c->Sg.x, c->Sg.y, c->Sg.z);
	}
*/
	
	if( missile_lookForImpacts(c) )
		return "target hit";
	
	dis_if_updateLocal(c);

	return NULL;
}


int
missile_fire(craft * c, int ind)
{
	char *    missileName;
	craftType * missile;
	register craft *m;
	register int i;
	VPoint    s, s1;
	VPoint    cY, mX, mY, mZ;
	double    v;
	double    disLocation[3];
	VPoint    velocity;
	double    disVelocity[3];
	double    disZeroVec[3];
	double    disOrientation[3];

	for ((i = 0, m = &mtbl[0]); i < manifest_MAXPROJECTILES; (++i, ++m))
		if (m->type == CT_FREE) {
			m->type = CT_MISSILE;
			break;
		}

	if (i == manifest_MAXPROJECTILES)
		return -1;

	missileName = c->cinfo->station[ind].type;
	missile = inventory_craftTypeSearchByZoneAndName(NULL, missileName);
	if( missile == NULL )
		error_internal("firing unknow missile type `%s'", missileName);
	memory_strcpy(m->name, sizeof(m->name), missile->name);
	m->cinfo = missile;
	m->fuel = missile->maxFuel;
	m->payload = 0.0;
	m->curThrust = missile->maxThrust;
	m->owner = c->pIndex;

/*
 *  Build trihedral based on the launching aircraft's current velocity vector
 *  rather than simply it's current direction vector.
 *
 *      (1)  build a unit velocity vector.
 *      (2)  calculate missiles local Z axis from
 *              plane's-y-axis CROSS missile's-unit-velocity-vector
 *      (3)  calculate missile's Y axis.
 */

	if ((v = VMagnitude(&c->Cg)) < 1.0) {
		m->trihedral = c->trihedral;
		m->curRoll = c->curRoll;
		m->curPitch = c->curPitch;
		m->curHeading = c->curHeading;
	}
	else {
		mX = c->Cg;
		mX.x /= v;
		mX.y /= v;
		mX.z /= v;
		cY.x = c->trihedral.m[0][1];
		cY.y = c->trihedral.m[1][1];
		cY.z = c->trihedral.m[2][1];

		VCrossProd(&mX, &cY, &mZ);
		VCrossProd(&mZ, &mX, &mY);

		m->trihedral.m[0][0] = mX.x;
		m->trihedral.m[1][0] = mX.y;
		m->trihedral.m[2][0] = mX.z;
		m->trihedral.m[0][1] = mY.x;
		m->trihedral.m[1][1] = mY.y;
		m->trihedral.m[2][1] = mY.z;
		m->trihedral.m[0][2] = mZ.x;
		m->trihedral.m[1][2] = mZ.y;
		m->trihedral.m[2][2] = mZ.z;

		pm_euler(m);
	}

	m->Cg = c->Cg;
	VTransform(&(c->cinfo->wStation[ind]), &(c->trihedral), &s1);
	VReverseTransform_(&s1, &c->XYZtoNED, &s);
	m->Sg.x = c->prevSg.x + units_FEETtoMETERS(s.x);
	m->Sg.y = c->prevSg.y + units_FEETtoMETERS(s.y);
	m->Sg.z = c->prevSg.z + units_FEETtoMETERS(s.z);
	earth_XYZToLatLonAlt(&m->Sg, &m->w);
	earth_generateWorldToLocalMatrix(&m->w, &m->XYZtoNED);
	m->armTimer = missile->armDelay;
	m->flags = FL_HAS_GYRO;
	m->createTime = curTime;

/*
 * kludge
 */

	m->curRadarTarget = c->curRadarTarget;

/*
 *  ACM missiles are DIS "tracked munitions", so we are
 *  responsible for sending entity state PDU's for them
 */

	{
		VPoint    tmp;

		disLocation[0] = m->Sg.x;
		disLocation[1] = m->Sg.y;
		disLocation[2] = m->Sg.z;
		tmp.x = units_FEETtoMETERS(m->Cg.x);
		tmp.y = units_FEETtoMETERS(m->Cg.y);
		tmp.z = units_FEETtoMETERS(m->Cg.z);
		VReverseTransform_(&tmp, &m->XYZtoNED, &velocity);
		disVelocity[0] = velocity.x;
		disVelocity[1] = velocity.y;
		disVelocity[2] = velocity.z;
		disZeroVec[0] = 0.0;
		disZeroVec[1] = 0.0;
		disZeroVec[2] = 0.0;
		disOrientation[0] = m->curHeading;
		disOrientation[1] = m->curPitch;
		disOrientation[2] = m->curRoll;
		dis_if_entityEnter(c->force, m,
				&missile->entityType,
				&missile->altEntityType,
				disLocation, disVelocity,
				disZeroVec, disOrientation,
				disZeroVec, &m->disId);
	}
	m->update = missile_update;
	m->kill = missile_kill;
	return 0;
}

/**
 * Kill missile|cannon with possible target hit.
 * @param c Missile|cannon.
 * @param target Hit target, or NULL.
 * @param rpos Location of the explosion in the target frame (m).
 */
static void
missile_targetHit(craft * missile, craft * target, VPoint * rpos)
{
	VPoint    worldVel;
	worldVel.x = units_FEETtoMETERS(missile->Cg.x);
	worldVel.y = units_FEETtoMETERS(missile->Cg.y);
	worldVel.z = units_FEETtoMETERS(missile->Cg.z);
	VReverseTransform_(&worldVel, &missile->XYZtoNED, &worldVel);

	dis_if_detonation(&missile->cinfo->entityType,
			ptbl[missile->owner].disId,
			target->disId,
			missile->disId,
			(double *) &missile->Sg,
			(double *) rpos,
			(double *) &worldVel);
}


static void missile_kill(craft * missile, char *reason)
{
	/* Set a generic explosion effect: */
	effects_new_explosion(&missile->Sg, &(VPoint){0.0, 0.0, 0.0}, 50.0, 15.0, 1.0);
	
	dis_if_entityExit(missile->disId);

	missile->type = CT_FREE;
	pm_hud_strings_free(missile);
}


/**
 * Returns true if the missile hit something.
 * @param m
 * @return True if the missile hit something.
 */
static int
missile_lookForImpacts(craft *m)
{

	craft    *c;
	missile_CloserPoint p[manifest_MAXPLAYERS], *list, *q, *r, *rprev;
	VPoint    mv, v, s0;
	double    t, d, explosion_diameter_meters;
	int       j;

	if (m->type != CT_MISSILE || m->armTimer > 0.0)
		return 0;
	
	// Missile vel. in world coords. (ft/s):
	VReverseTransform_(&m->Cg, &m->XYZtoNED, &mv);

	list = (missile_CloserPoint *) NULL;
	for (c = ptbl, j = 0; j < manifest_MAXPLAYERS; ++j, ++c) {

		if (c->type == CT_FREE)
			continue;

/*
 * Reduce the relative motion of "c" to a the parametric system
 * of equations:
 * 
 *              x(t) = vx * t + s0x
 *              y(t) = vy * t + s0y
 *              z(t) = vz * t + s0z
 * 
 * where x,y,z is the position of "c" relative to "m" in world coord. (m).
 *
 * We can then compute the time of perigee (closest pass) along with
 * the associated minimum distance.
 */
		VPoint cv;
		VReverseTransform_(&c->Cg, &c->XYZtoNED, &cv);
		v.x = units_FEETtoMETERS(cv.x - mv.x);
		v.y = units_FEETtoMETERS(cv.y - mv.y);
		v.z = units_FEETtoMETERS(cv.z - mv.z);
		s0.x = c->Sg.x - m->Sg.x;
		s0.y = c->Sg.y - m->Sg.y;
		s0.z = c->Sg.z - m->Sg.z;

/*
 * Compute time of minimum distance between the two objects.
 */

		t = -(v.x * s0.x + v.y * s0.y + v.z * s0.z) /
			(v.x * v.x + v.y * v.y + v.z * v.z);

		if (mdebug)
			printf("perigee in %g seconds with player %d\n", t, j);

/*
 *  If the closest pass occurs within a given time interval, check for a hit.
 * 
 *  We'll build a linked list of all craft that this projectile may strike
 *  during this period, arranged in ascending order by time of "perigee"
 *  (closest pass).  We'll then test for strikes.  If a projectile misses
 *  the first object, then it may have struck subsequent objects in the
 *  list ...
 */

/*
 *  One special case occurs when a target or missile's turn suddenly
 *  changes the perigee time from positive to negative.  If the missile
 *  is within hitting range at t=0 and the time of perigee is negative,
 *  then zap 'em.
 */
		
		if (t < 0.0) {
			d = sqrt(s0.x * s0.x + s0.y * s0.y +
					 s0.z * s0.z);
			if (isMissileHit(d, c)) {
				t = 0.0;
			}
		}

		if (t >= 0.0 && t <= 0.05) {
			q = &p[j];

			// Position of the explosion (world coords., m):
			q->Sg = m->Sg;
			q->Sg.x += units_FEETtoMETERS(mv.x) * t;
			q->Sg.y += units_FEETtoMETERS(mv.y) * t;
			q->Sg.z += units_FEETtoMETERS(mv.z) * t;
			
			// Position of the explosion (target local frame, m):
			q->rpos.x = -v.x * t - s0.x;
			q->rpos.y = -v.y * t - s0.y;
			q->rpos.z = -v.z * t - s0.z;
			VTransform_(&q->rpos, &c->XYZtoNED, &q->rpos);
			VReverseTransform_(&q->rpos, &c->AXYZtoNED, &q->rpos);

			// Vel. of the missile (target local frame, m/s):
			VSetPoint(&q->rvel, -v.x, -v.y, -v.z);

			if (list == (missile_CloserPoint *) NULL) {
				q->next = list;
				list = q;
			}
			else if (list->time > t) {
				q->next = list;
				list = q;
			}
			else {
				for (rprev = list, r = list->next; r != (missile_CloserPoint *) NULL;) {
					if (r->time > t)
						break;
					rprev = r;
					r = r->next;
				}
				rprev->next = q;
				q->next = r;
			}
			q->time = t;
			q->c = c;
			q->min = VMagnitude(&q->rpos);
		}
	}

/*
 *  Now look for missile hits in the list of perigees.
 */

	for (r = list; r != (missile_CloserPoint *) NULL; r = r->next){
		if (isMissileHit(r->min, r->c)) {
			m->Sg = r->Sg;	/* Set detonation point for missile_kill() */
			missile_targetHit(m, r->c, &r->rpos);
			/* can only damage local player */
			if (r->c->type != CT_DIS_PLANE){
				if (damage_absorbDISDamage(r->c, 
									&m->cinfo->entityType, 0, 0, 
									r->min,
									VMagnitude(&r->rvel),
									&explosion_diameter_meters) == 0) {
					r->c->kill(r->c, "air-to-air missile");
				}
				if( explosion_diameter_meters > 0 )
					/* Something on the target has been damaged (this is NOT
					 * the explosion of the missile): */
					effects_new_explosion(&(r->Sg), &r->rvel,
						explosion_diameter_meters, 10.0, 3.0);
			}
			return 1;
		}
	}
	return 0;
}

/*ARGSUSED */
static int
isMissileHit(double min, craft * c)
{

	return (min < 15.0) ? 1 : 0;
}

#define IRMaxRange	units_FEETtoMETERS(15.0 * units_NmToFeetFactor)

static int
isIRVisible(craft * m, craft * c, VPoint * t, double IRScanSlope)
{

	VPoint    relPos, tmp;
	int       cstate, mstate;

	if (c->type == CT_FREE)
		return 0;

/*
 *  If the seeker is in clouds, or the target is not at the same level
 *  (e.g seeker is above clouds, but target is below), then the target is
 *  not IR visible.
 */

	if ((mstate = inCloud(m)) == 1) {
		return 0;
	}

	if ((cstate = inCloud(c)) != mstate) {
		return 0;
	}

	VTransform(&c->prevSg, &m->XYZtoNED, &tmp);
	VReverseTransform_(&tmp, &m->trihedral, t);

	if (sqrt(t->x * t->x + t->y * t->y + t->z * t->z) > IRMaxRange)
		return 0;

	if (t->x <= 0.0)
		return 0;

	relPos.z = t->z / (t->x * IRScanSlope);
	relPos.y = t->y / (t->x * IRScanSlope);

	return (sqrt(relPos.z * relPos.z + relPos.y * relPos.y) > 1.0) ? 0 : 1;
}

int
missile_getIRTarget(craft * c, VPoint * t, double scanSlope)
{

	int       i, n;
	craft    *p;
	VPoint    tNew, tMin;
	double    m1, min;

	if (c->curRadarTarget != -1
	&& c->type != CT_FREE
	&& isIRVisible(c, &ptbl[c->curRadarTarget], t, scanSlope))
		return c->curRadarTarget;

/*
 *  Look for a target.  Designate the closest one as a new target.
 */

	min = 1000000.0;
	n = -1;
	for (i = 0, p = ptbl; i < manifest_MAXPLAYERS; ++i, ++p) {
		if (p == c)
			continue;
		if (p->type != CT_FREE)
			if (isIRVisible(c, p, &tNew, scanSlope)) {
				m1 = VMagnitude(&tNew);
				if (m1 < min) {
					n = i;
					min = m1;
					tMin = tNew;
				}
			}
	}

	*t = tMin;
	return n;
}

/*
 *  Track target using proportional navigation guidance (N = 4).
 */

#define AIM9SLOPE	0.57735

static void
missile_trackTarget(craft * c)
{

	VMatrix   mtx, mtx1;
	VPoint    t, t1, v, vrel, zeroVec = {0,0,0};
	double    h, maxTurn, omegap, omegay;
	double    hs;
	double    deltaRoll, deltaPitch, deltaYaw;
	craft    *target;

/*
 *  Now let's get to target tracking;  the missile won't start tracking until
 *  0.60 seconds has elapsed.  Then, if we don't already have a target
 *  designated, get one.
 */

	if (curTime - c->createTime < 0.60) {
		deltaPitch = 0.0;
		deltaYaw = 0.0;
		goto change;
	}
	else if ((c->curRadarTarget = missile_getIRTarget(c, &t, AIM9SLOPE)) == -1) {

/*
 *  Not target; missile goes ballistic
 */

		deltaPitch = 0.0;
		deltaYaw = 0.0;
		goto change;

#ifdef manifest_FLIGHTDEBUG
		if (mdebug)
			printf("Missile elects to self-destruct\n");
#endif
		effects_new_explosion(&(c->Sg), &zeroVec, 5.0, 10.0, 3.0);
		missile_kill(c, "lost IR target");
		return;
	}

/*
 *  We'll steer towards the target at a rate proportional to the
 *  rate of change of the target's position in the missile's XZ (pitch)
 *  and XY (yaw) planes.
 */

	target = &ptbl[c->curRadarTarget];

	v.x = target->Cg.x - c->Cg.x;
	v.y = target->Cg.y - c->Cg.y;
	v.z = target->Cg.z - c->Cg.z;

	t.x = units_METERStoFEET(t.x);
	t.y = units_METERStoFEET(t.y);
	t.z = units_METERStoFEET(t.z);

	VReverseTransform_( &v, &c->trihedral, &vrel );

	hs = t.x * t.x + t.y * t.y;

	omegay = (vrel.y * t.x - vrel.x * t.y) / hs;

	omegap = (vrel.z * hs - t.z * (vrel.x * t.x + vrel.y * t.y)) /
		(sqrt(hs) * (hs + t.z * t.z));

	deltaPitch = omegap * 4.0 * deltaT;
	deltaYaw   = omegay * 4.0 * deltaT;

	h = sqrt( deltaPitch * deltaPitch + deltaYaw * deltaYaw );

/*
 *  We'll constrain missile turns to about 20 degree/second unless it's velocity
 *  would make that greater than a 25g load factor.
 */

	if ( c->VT > 0.0 ) {
		maxTurn = (units_earth_g / c->VT) * sqrt( 25.0 * 25.0 - 1.0 );
	}
	else {
		maxTurn = 0.0;
	}

	if (maxTurn > units_DEGtoRAD(20.0)) {
		maxTurn = units_DEGtoRAD(20.0);
	}
	maxTurn *= deltaT;

#ifdef manifest_FLIGHTDEBUG
	if (mdebug)
		printf("\nturn rate = %g; maxTurn = %g\n", h, maxTurn);
#endif

	if (h > maxTurn) {
		deltaPitch *= maxTurn / h;
		deltaYaw *= maxTurn / h;
	}

/*
 *  Re-orient the missile and velocity vector.
 */

  change:

	deltaRoll = 0.0;

#ifdef manifest_FLIGHTDEBUG
	if (mdebug) {
		printf("Missile changes: pitch/yaw: %g %g (deg).\n",
			   units_RADtoDEG(deltaPitch), units_RADtoDEG(deltaYaw));
		printf("position [%g, %g, %g]\n", t.x, t.y, t.z);
		printf("target pitch/yaw rates: %g, %g (deg/sec)\n",
			   units_RADtoDEG(omegap), units_RADtoDEG(omegay));
	}
#endif

	VEulerToMatrix(deltaRoll, -deltaPitch, deltaYaw, &mtx);

	VReverseTransform_(&c->Cg, &c->trihedral, &t);
	VTransform(&t, &mtx, &t1);
	VTransform(&t1, &c->trihedral, &c->Cg);

	VMatrixMultByRank(&mtx, &c->trihedral, &mtx1, 3);
	c->trihedral = mtx1;
	pm_euler(c);

}