/*
===========================================================================
Copyright (C) 1997-2001 Id Software, Inc.
Copyright (C) 2000-2002 Mr. Hyde and Mad Dog

This file is part of Lazarus Quake 2 Mod source code.

Lazarus Quake 2 Mod source code 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.

Lazarus Quake 2 Mod source code 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 Lazarus Quake 2 Mod source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

#include "g_local.h"

//====================================================================

/* FUNC_PENDULUM

SF 1 = START_ON
   2 = STOP_AT_TOP
   8 = SLOW  (internal use only, used to slow a pendulum to a stop after
              being blocked)
   16= STOPPING (internal use only, set when a STOP_AT_TOP pendulum is triggered off)

radius       = length of pendulum, used for motion equation (not impact)
distance     = total arc that pendulum moves through, must be < 360
move_origin  = vector from origin to c.g. of pendulum, used for impact
mass         = mass of pendulum, used for knockback of func_pushables (not players/monsters)
*/

#define SF_PENDULUM_STARTON      1
#define SF_PENDULUM_STOP_AT_TOP  2
#define SF_PENDULUM_SLOW         8
#define SF_PENDULUM_STOPPING    16

void box_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point);
void pendulum_blocked (edict_t *self, edict_t *other)
{
	trace_t	trace;
	vec3_t	angles;
	vec3_t	forward, left, up;
	vec3_t	dir;
	vec3_t	f1, l1, u1;
	vec3_t	point;
	vec3_t	origin;
	vec3_t	new_velocity;
	vec3_t	new_origin;
	float	speed;
	int		damage;

	// Since this routine is called in response to being blocked, 
	// the current s.angles is for the LAST frame, not the proposed
	// angles for the current frame. Since we're basically overriding
	// normal physics here, go ahead and move to new location. This
	// means that later on we have to move blocker NOW rather than
	// relying on its velocity to get it out of the way.
	// BUT... trouble is doing this with players ends up giving
	// goofy direction in some cases. For players/monsters, use old
	// angles but STILL move 'em out of the way immediately
	if(other->client || (other->svflags & SVF_MONSTER))
		VectorCopy(self->s.angles,angles);
	else
		VectorMA(self->s.angles,FRAMETIME,self->avelocity,angles);

	AngleVectors(angles,forward,left,up);

	speed = fabs(self->avelocity[ROLL]) * M_PI / 180. * self->radius;
	if( (level.time > self->touch_debounce_time) && (speed > 200) )
	{
		damage = (int)( self->dmg * (speed-200)/100 );
		self->touch_debounce_time = level.time + 0.5;
	}
	else
		damage = 0;

	VectorAdd(other->s.origin,other->origin_offset,origin);
	VectorCopy(left,dir);
	if(self->avelocity[ROLL] > 0)
		VectorNegate(dir,dir);

	VectorScale(forward,self->move_origin[0],f1);
	VectorScale(left,-self->move_origin[1],l1);
	VectorScale(up,self->move_origin[2],u1);
	VectorAdd(self->s.origin,f1,point);
	VectorAdd(point,l1,point);
	VectorAdd(point,u1,point);
	VectorSubtract(origin,point,point);
	VectorNormalize(point);

	if(other->client || (other->svflags & SVF_MONSTER))
	{
		if((point[2] < -0.7) && other->groundentity)
		{
			T_Damage (other, self, self, vec3_origin, other->s.origin, vec3_origin, 100000, 1, DAMAGE_NO_PROTECTION, MOD_CRUSH);
			return;
		}

		dir[2] = max(1.0,fabs(dir[2]));
		VectorNormalize(dir);
		// Normal kickback takes too long to take effect and allows embedment. Move
		// the blocker NOW.
		// Give a minimum speed so we can get the poor fool out of the way
		speed = max(100,speed);
		VectorScale(dir,speed,new_velocity);
		VectorMA(other->s.origin,FRAMETIME,new_velocity,new_origin);
		other->solid = SOLID_NOT;
		gi.linkentity(other);
		trace = gi.trace(other->s.origin,other->mins,other->maxs,new_origin,self,other->clipmask);
		VectorCopy(trace.endpos,other->s.origin);
		VectorCopy(new_velocity,other->velocity);
		other->solid = SOLID_BBOX;
		gi.linkentity(other);
		T_Damage (other, self, self, dir, other->s.origin, vec3_origin, damage, 0, 0, MOD_CRUSH);
	}
	else if(other->solid == SOLID_BSP)
	{
		// Other is most likely a func_pushable, since almost all other bmodels aren't
		// clipped to MOVETYPE_PUSH

		vec3_t		org, mins, maxs;
		vec3_t		vn2;
		qboolean	block;
		float		e=self->attenuation;	// coefficient of restitution
		float		m=(float)(other->mass)/(float)(self->mass);
		float		v11 = speed;
		float		v21;		// Initial speed of other in the impact direction
		float		v12, v22;
		float		new_rspeed;
		float		sgor, time, wave;

		if(v11 >= 100)
			gi.sound (self, 0, self->noise_index, 1, 1, 0);

		// If other is on the ground, push it UP regardless of dir
		if(other->groundentity)
			dir[2] = max(1.0,fabs(dir[2]));
		VectorNormalize(dir);

		// If pendulum hits crate from above and crate is on the ground,
		// destroy the crate. This may not be realistic, but there's really
		// no other way since if we stop the pendulum we'd then have to 
		// continously monitor whether the crate moved away or not.
		if((point[2] < -0.7) && (other->velocity[2] == 0))
		{
			box_die (other, self, self, 100000, point);
			return;
		}
		if(e > 0)
		{
			v21 = VectorLength(other->velocity);
			if(v21 > 0)
			{
				VectorCopy(other->velocity,vn2);
				VectorNormalize(vn2);
				v21 *= DotProduct(dir,vn2);
			}

			v22 = ( e*(v11-v21) + v11 + m*v21 ) / (1.0 + m);
			v12 = v22 + e*(v21-v11);
//			gi.dprintf("v11=%g, v21=%g, v12=%g, v22=%g\n",v11,v21,v12,v22);
//			gi.dprintf("av[ROLL]=%g, roll=%g\n",self->avelocity[ROLL],angles[ROLL]);
		}
		else
		{
			v12 = v11;
			if(other->mass > self->mass)
			{
				block = true;
				VectorClear(self->avelocity);
				gi.linkentity(self);
				goto deadstop;
			}
			else
				v22 = v11 * (float)self->mass/(float)other->mass;
		}
		VectorScale(dir,v22,new_velocity);

		if(v12 < 0)
		{
			// Reverse rotation.
			new_rspeed = fabs(v12) / (M_PI / 180. * self->radius);
			if(self->avelocity[ROLL] > 0)
				self->avelocity[ROLL] = -new_rspeed;
			else
				self->avelocity[ROLL] = new_rspeed;
		}
		else
		{
			// Continuing to move in same direction, though slower.
			new_rspeed = v12 / (M_PI / 180. * self->radius);
			if(self->avelocity[ROLL] > 0)
				self->avelocity[ROLL] = new_rspeed;
			else
				self->avelocity[ROLL] = -new_rspeed;
		}
		sgor = sqrt( (float)sv_gravity->value / self->radius );
		wave = fabs( self->avelocity[ROLL] / (angles[ROLL] * sgor) );
		wave = atan(wave);
		if(self->avelocity[ROLL] >= 0)
		{
			if(angles[ROLL] > 0)
				wave = M_PI - wave;
		}
		else
		{
			if(angles[ROLL] > 0)
				wave = M_PI + wave;
			else
				wave = 2*M_PI - wave;
		}
		time = wave/sgor;
		self->startframe = level.framenum - time*10.;
		self->moveinfo.start_angles[ROLL] = -fabs(angles[ROLL] / cos(wave));

		// Now we know the new pendulum velocity and crate velocity, *assuming*
		// nothing else is in the way. Now check to see if crate hits anything
		// else.

		VectorAdd(other->s.origin,other->origin_offset,org);
		VectorMA(org,FRAMETIME,new_velocity,new_origin);

		// Temporarily make crate nonsolid so we can ignore pendulum in our trace
		// (rather than crate)
		other->solid = SOLID_NOT;
		gi.linkentity(other);
		VectorSubtract(other->mins,other->origin_offset,mins);
		VectorSubtract(other->maxs,other->origin_offset,maxs);
		trace = gi.trace (org, mins, maxs, new_origin, self, other->clipmask);
		// restore solidity of crate
		other->solid = SOLID_BSP;
		if(trace.startsolid)
		{
			// Things are completely fouled up. Nuke other and go away.
			T_Damage (other, self, self, vec3_origin, other->s.origin, vec3_origin, 100000, 1, 0, MOD_CRUSH);
			if (other)
				BecomeExplosion1 (other);
			return;
		}
		else if(trace.fraction < 1.0)
		{
			vec3_t	vec;
			float	dist;
			VectorSubtract(trace.endpos,org,vec);
			dist = VectorLength(vec);

			if( (trace.ent->client) || (trace.ent->flags & SVF_MONSTER))
			{
				float	delta=FRAMETIME*VectorLength(new_velocity);

				// If a player or monster is in the way of the crate, AND
				// the pendulum speed is > 100, throw 'em out of the way.
				// If pendulum tangential speed is < 100, give up.
				if(speed < 100)
					block = true;
				else
				{
					if(dist < delta)
					{
						VectorScale(new_velocity,1.25,trace.ent->velocity);
						VectorMA(trace.ent->s.origin,FRAMETIME,trace.ent->velocity,trace.ent->s.origin);
						gi.linkentity(trace.ent);
					}
					block = false;
				}
			}
			else
			{
				if(dist < speed*FRAMETIME)
				{
					block = true;
					VectorScale(vec,10.,other->velocity);
					VectorMA(other->s.origin,FRAMETIME,other->velocity,other->s.origin);
				}
				else
					block = false;
			}
		}
		else
			block = false;

		if(!block)
		{
			VectorCopy(new_velocity,other->velocity);
			VectorMA(other->s.origin,FRAMETIME,other->velocity,other->s.origin);
		}
		gi.linkentity(other);

		// Final checks:
		// 1) If pendulum after-impact speed is < 100, that's too damn slow.
		//    Lie and say it's blocked
		// 2) If not blocked, in its new position test for intersection
		// of crate and pendulum. If they intersect, then most likely pendulum is
		// moving VERY slowly and we need to reverse direction NOW to prevent
		// embedment.
		if(!block)
		{
			if(fabs(v12) < 100)
				block = true;
			else
			{
				VectorAdd(other->s.origin,other->origin_offset,org);
				trace = gi.trace(org,mins,maxs,org,other,MASK_SOLID);
				if(trace.startsolid)
					block = true;
			}
		}
deadstop:
		T_Damage (other, self, self, dir, other->s.origin, vec3_origin, damage, 0, 0, MOD_CRUSH);
		if( block )
		{
			// Then this sucker will still be in the way. Reverse rotation, slow, or stop
			if(fabs(angles[ROLL]) > 2)
			{
				if(abs(angles[ROLL]) < 10)
					self->spawnflags |= SF_PENDULUM_SLOW;
				self->moveinfo.start_angles[ROLL] = angles[ROLL];
				VectorClear(self->avelocity);
				self->startframe = 0;
			}
			else
			{
				self->spawnflags &= ~SF_PENDULUM_STARTON;
				self->moveinfo.start_angles[ROLL] = 0;
				VectorClear(self->s.angles);
				VectorClear(self->avelocity);
			}
			gi.linkentity(self);
		}
		else if((fabs(self->avelocity[ROLL]) < 10) && (fabs(self->s.angles[ROLL]) < 10))
		{
			self->spawnflags |= SF_PENDULUM_SLOW;
			self->moveinfo.start_angles[ROLL] = angles[ROLL];
			VectorClear(self->avelocity);
			self->startframe = 0;
		}
	}
	else
	{
		T_Damage (other, self, self, vec3_origin, other->s.origin, vec3_origin, 100000, 1, 0, MOD_CRUSH);
		// if it's still there, nuke it
		if (other)
			BecomeExplosion1 (other);
	}
}

void pendulum_rotate (edict_t *self)
{
	float	this_angle;
	float	wave;
	float	sgor;

	if(!(self->spawnflags & SF_PENDULUM_STARTON))
		return;

	if(self->spawnflags & SF_PENDULUM_SLOW)
	{
		if(self->startframe == 0)
		{
			// Then we just started moving again after being blocked
			self->avelocity[ROLL] = -self->s.angles[ROLL];
			self->startframe = level.framenum;
		}
		else
		{
			float	next_angle;

			next_angle = self->s.angles[ROLL] + self->avelocity[ROLL]*FRAMETIME;
			if( (next_angle >= 0 && self->s.angles[ROLL] < 0) ||
				(next_angle <= 0 && self->s.angles[ROLL] > 0)   )
			{
				VectorClear(self->s.angles);
				VectorClear(self->avelocity);
				gi.linkentity(self);
				return;
			}
		}
		self->nextthink = level.time + FRAMETIME;
	}
	else
	{
		float	old_velocity = self->avelocity[ROLL];

		if(!self->startframe)
			self->startframe = level.framenum;

		sgor = sqrt( (float)sv_gravity->value / self->radius );
		wave = sgor * (level.framenum - self->startframe) * 0.1;
		this_angle = self->moveinfo.start_angles[ROLL] * cos(wave);
		self->avelocity[ROLL] = -self->moveinfo.start_angles[ROLL] * sgor * sin(wave);
		if( (self->spawnflags & SF_PENDULUM_STOPPING) && (cos(wave) > 0.0))
		{
			if( ((old_velocity > 0) && (self->avelocity[ROLL] <= 0)) ||
				((old_velocity < 0) && (self->avelocity[ROLL] >= 0))    )
			{
				self->spawnflags &= ~SF_PENDULUM_STARTON;
				VectorClear(self->avelocity);
				self->nextthink = 0;
				gi.linkentity(self);
				return;
			}
		}
		self->s.angles[ROLL] = this_angle;
		self->nextthink = level.time + FRAMETIME;
	}
	gi.linkentity(self);
}

void pendulum_use (edict_t *self, edict_t *other, edict_t *activator)
{
	if(self->spawnflags & SF_PENDULUM_STARTON)
	{
		if(self->spawnflags & SF_PENDULUM_STOP_AT_TOP)
		{
			self->spawnflags |= SF_PENDULUM_STOPPING;
		}
		else
		{
			VectorClear(self->avelocity);
			self->spawnflags &= ~SF_PENDULUM_STARTON;
			gi.linkentity(self);
		}
	}
	else
	{
		self->spawnflags |= SF_PENDULUM_STARTON;
		self->spawnflags &= ~SF_PENDULUM_STOPPING;
		self->think = pendulum_rotate;

		if(self->delay > 0)
		{
			float	delay;
			delay = self->delay * 2.0 * M_PI * sqrt(self->radius/(float)sv_gravity->value);
			delay = 0.1 * (int)(10*delay);
			self->nextthink  = level.time + delay;
			self->startframe = level.framenum + delay*10;
			if(!(self->spawnflags & SF_PENDULUM_STOP_AT_TOP))
				self->delay = 0;
		}
		else
		{
			if(self->s.angles[ROLL] == self->moveinfo.start_angles[ROLL])
				self->startframe = level.framenum;
			else
			{
				float	t;
				t = acos (self->s.angles[ROLL] / self->moveinfo.start_angles[ROLL]);
				t /= sqrt((float)sv_gravity->value / self->radius);
				self->startframe = level.framenum - t*10;
			}
			self->think(self);
		}
	}
}

void pendulum_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{
	// Mostly copied from func_explosive_explode. We can't use that function because
	// the origin is a bit different.

	vec3_t	origin;
	vec3_t	forward, left, up;
	vec3_t	chunkorigin;
	vec3_t	size;
	int		count;
	int		mass;

	// Particles originate from business end of pendulum
	AngleVectors(self->s.angles,forward,left,up);
	VectorScale(forward,self->move_origin[0],forward);
	VectorScale(left,-self->move_origin[1],left);
	VectorScale(up,self->move_origin[2],up);
	VectorAdd(self->s.origin,forward,origin);
	VectorAdd(origin,left,origin);
	VectorAdd(origin,up,origin);

	self->mass *= 2;
	self->takedamage = DAMAGE_NO;

	VectorSubtract (origin, self->enemy->s.origin, self->velocity);
	VectorNormalize (self->velocity);
	VectorScale (self->velocity, 150, self->velocity);

	// start chunks towards the center
	VectorScale (size, 0.5, size);

	mass = self->mass;
	if (!mass)
		mass = 75;

	// big chunks
	if (mass >= 100)
	{
		count = mass / 100;
		if (count > 8)
			count = 8;
		while(count--)
		{
			chunkorigin[0] = origin[0] + crandom() * size[0];
			chunkorigin[1] = origin[1] + crandom() * size[1];
			chunkorigin[2] = origin[2] + crandom() * size[2];
			ThrowDebris (self, "models/objects/debris1/tris.md2", 1, chunkorigin, 0, 0);
		}
	}

	// small chunks
	count = mass / 25;
	if (count > 16)
		count = 16;
	while(count--)
	{
		chunkorigin[0] = origin[0] + crandom() * size[0];
		chunkorigin[1] = origin[1] + crandom() * size[1];
		chunkorigin[2] = origin[2] + crandom() * size[2];
		ThrowDebris (self, "models/objects/debris2/tris.md2", 2, chunkorigin, 0, 0);
	}
	G_FreeEdict (self);
}

void SP_func_pendulum (edict_t *ent)
{
	float	max_speed;

	ent->class_id = ENTITY_FUNC_PENDULUM;

	ent->solid = SOLID_BSP;
	ent->movetype = MOVETYPE_PENDULUM;
	if(!st.distance)
		ent->moveinfo.distance = 90;
	else
		ent->moveinfo.distance = st.distance;

	if(st.noise)
		ent->noise_index = gi.soundindex(st.noise);
	else
		ent->noise_index = gi.soundindex("world/land.wav");

	if(ent->moveinfo.distance >= 360)
	{
		gi.dprintf("func_pendulum distance must be < 360\n");
		ent->moveinfo.distance = 359.;
	}

	if (!ent->speed)
		ent->speed = 100;
	if (!ent->radius)
		ent->radius = 100;
	if (!ent->mass)
		ent->mass = 200;
	if (st.phase > 0)
		ent->delay = st.phase;
	else
		ent->delay = 0;
	if(ent->delay > 1.0)
		ent->delay -= (int)(ent->delay);

	// Coefficient of restitution - default = 0.5, must be <= 1.0
	if(ent->attenuation == 0.0)
		ent->attenuation = 0.5;
	else if(ent->attenuation > 1.0)
		ent->attenuation = 1.0;

	if (!ent->dmg)
		ent->dmg = 5;
	// This is the damage delivered by the pendulum at max speed. Convert to
	// a damage scale used in our damage equation.
	max_speed = ent->moveinfo.distance/2 * M_PI / 180. * sqrt((float)sv_gravity->value * ent->radius );
	if(max_speed <= 200.)
		ent->dmg = 0;
	else
	{
		float	dmg;
		dmg = (float)(ent->dmg) * 100. / (max_speed - 200.);
		ent->dmg = (int)(dmg - 0.5) + 1;
	}
	if(ent->health > 0)
	{
		ent->die = pendulum_die;
		ent->takedamage = DAMAGE_YES;
	}

	ent->blocked = pendulum_blocked;
//	ent->touch = pendulum_touch;

	if(!ent->accel)
		ent->accel = 1;
	else if (ent->accel > ent->speed)
		ent->accel = ent->speed;

	if(!ent->decel)
		ent->decel = 1;
	else if (ent->decel > ent->speed)
		ent->decel = ent->speed;

	gi.setmodel (ent, ent->model);

	ent->s.angles[ROLL] = ent->moveinfo.distance/2;
	ent->moveinfo.start_angles[ROLL] = ent->s.angles[ROLL];
	if(ent->spawnflags & SF_PENDULUM_STARTON)
	{
		ent->think = pendulum_rotate;
		ent->nextthink = level.time + FRAMETIME;
	}
	else
	{
		ent->use = pendulum_use;
	}
	gi.linkentity (ent);

}