/*
	sv_phys.c

	@description@

	Copyright (C) 1996-1997  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:

		Free Software Foundation, Inc.
		59 Temple Place - Suite 330
		Boston, MA  02111-1307, USA

*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "QF/cvar.h"
#include "QF/sys.h"

#include "host.h"
#include "server.h"
#include "sv_progs.h"
#include "world.h"

#define sv_frametime host_frametime

cvar_t     *sv_nostep;

// CLIENT MOVEMENT ============================================================

/*
  SV_CheckStuck

  This is a big hack to try and fix the rare case of getting stuck in the world
  clipping hull.
*/
static void
SV_CheckStuck (edict_t *ent)
{
	int         i, j, z;
	vec3_t      org;

	if (!SV_TestEntityPosition (ent)) {
		VectorCopy (SVvector (ent, origin), SVvector (ent, oldorigin));
		return;
	}

	VectorCopy (SVvector (ent, origin), org);
	VectorCopy (SVvector (ent, oldorigin), SVvector (ent, origin));
	if (!SV_TestEntityPosition (ent)) {
		Sys_MaskPrintf (SYS_DEV, "Unstuck.\n");
		SV_LinkEdict (ent, true);
		return;
	}

	for (z = 0; z < 18; z++)
		for (i = -1; i <= 1; i++)
			for (j = -1; j <= 1; j++) {
				SVvector (ent, origin)[0] = org[0] + i;
				SVvector (ent, origin)[1] = org[1] + j;
				SVvector (ent, origin)[2] = org[2] + z;
				if (!SV_TestEntityPosition (ent)) {
					Sys_MaskPrintf (SYS_DEV, "Unstuck.\n");
					SV_LinkEdict (ent, true);
					return;
				}
			}

	VectorCopy (org, SVvector (ent, origin));
	Sys_MaskPrintf (SYS_DEV, "player is stuck.\n");
}

static qboolean
SV_CheckWater (edict_t *ent)
{
	int         cont;
	vec3_t      point;

	point[0] = SVvector (ent, origin)[0];
	point[1] = SVvector (ent, origin)[1];
	point[2] = SVvector (ent, origin)[2] + SVvector (ent, mins)[2] + 1;

	SVfloat (ent, waterlevel) = 0;
	SVfloat (ent, watertype) = CONTENTS_EMPTY;
	cont = SV_PointContents (point);
	if (cont <= CONTENTS_WATER) {
		SVfloat (ent, watertype) = cont;
		SVfloat (ent, waterlevel) = 1;
		point[2] = SVvector (ent, origin)[2] + (SVvector (ent, mins)[2] +
												SVvector (ent, maxs)[2]) * 0.5;
		cont = SV_PointContents (point);
		if (cont <= CONTENTS_WATER) {
			SVfloat (ent, waterlevel) = 2;
			point[2] = SVvector (ent, origin)[2] + SVvector (ent, view_ofs)[2];
			cont = SV_PointContents (point);
			if (cont <= CONTENTS_WATER)
				SVfloat (ent, waterlevel) = 3;
		}
	}

	return SVfloat (ent, waterlevel) > 1;
}

static void
SV_WallFriction (edict_t *ent, trace_t *trace)
{
	float       d, i;
	vec3_t      forward, right, up, into, side;

	AngleVectors (SVvector (ent, v_angle), forward, right, up);
	d = DotProduct (trace->plane.normal, forward);

	d += 0.5;
	if (d >= 0)
		return;

	// cut the tangential velocity
	i = DotProduct (trace->plane.normal, SVvector (ent, velocity));
	VectorScale (trace->plane.normal, i, into);
	VectorSubtract (SVvector (ent, velocity), into, side);

	SVvector (ent, velocity)[0] = side[0] * (1 + d);
	SVvector (ent, velocity)[1] = side[1] * (1 + d);
}

/*
  SV_TryUnstick

  Player has come to a dead stop, possibly due to the problem with limited
  float precision at some angle joins in the BSP hull.

  Try fixing by pushing one pixel in each direction.

  This is a hack, but in the interest of good gameplay...
*/
static int
SV_TryUnstick (edict_t *ent, vec3_t oldvel)
{
	int         i, clip;
	vec3_t      dir, oldorg;
	trace_t     steptrace;

	VectorCopy (SVvector (ent, origin), oldorg);
	VectorZero (dir);

	for (i = 0; i < 8; i++) {
		// try pushing a little in an axial direction
		switch (i) {
		case 0:
			dir[0] = 2;
			dir[1] = 0;
			break;
		case 1:
			dir[0] = 0;
			dir[1] = 2;
			break;
		case 2:
			dir[0] = -2;
			dir[1] = 0;
			break;
		case 3:
			dir[0] = 0;
			dir[1] = -2;
			break;
		case 4:
			dir[0] = 2;
			dir[1] = 2;
			break;
		case 5:
			dir[0] = -2;
			dir[1] = 2;
			break;
		case 6:
			dir[0] = 2;
			dir[1] = -2;
			break;
		case 7:
			dir[0] = -2;
			dir[1] = -2;
			break;
		}

		SV_PushEntity (ent, dir);

		// retry the original move
		SVvector (ent, velocity)[0] = oldvel[0];
		SVvector (ent, velocity)[1] = oldvel[1];
		SVvector (ent, velocity)[2] = 0;
		clip = SV_FlyMove (ent, 0.1, &steptrace);

		if (fabs (oldorg[1] - SVvector (ent, origin)[1]) > 4
			|| fabs (oldorg[0] - SVvector (ent, origin)[0]) > 4) {
//			Sys_MaskPrintf (SYS_DEV, "unstuck!\n");
			return clip;
		}
		// go back to the original pos and try again
		VectorCopy (oldorg, SVvector (ent, origin));
	}

	VectorZero (SVvector (ent, velocity));
	return 7;							// still not moving
}

#define	STEPSIZE	18

/*
  SV_WalkMove

  Used only by players
*/
static void
SV_WalkMove (edict_t *ent)
{
	int         clip, oldonground;
	vec3_t      upmove, downmove, nosteporg, nostepvel, oldorg, oldvel;
	trace_t     steptrace, downtrace;

	// do a regular slide move unless it looks like you ran into a step
	oldonground = (int) SVfloat (ent, flags) & FL_ONGROUND;
	SVfloat (ent, flags) = (int) SVfloat (ent, flags) & ~FL_ONGROUND;

	VectorCopy (SVvector (ent, origin), oldorg);
	VectorCopy (SVvector (ent, velocity), oldvel);

	clip = SV_FlyMove (ent, sv_frametime, &steptrace);

	if (!(clip & 2))
		return;							// move didn't block on a step

	if (!oldonground && SVfloat (ent, waterlevel) == 0)
		return;							// don't stair up while jumping

	if (SVfloat (ent, movetype) != MOVETYPE_WALK)
		return;							// gibbed by a trigger

	if (sv_nostep->int_val)
		return;

	if ((int) SVfloat (sv_player, flags) & FL_WATERJUMP)
		return;

	VectorCopy (SVvector (ent, origin), nosteporg);
	VectorCopy (SVvector (ent, velocity), nostepvel);

	// try moving up and forward to go up a step
	VectorCopy (oldorg, SVvector (ent, origin));	// back to start pos
	VectorZero (upmove);
	VectorZero (downmove);
	upmove[2] = STEPSIZE;
	downmove[2] = -STEPSIZE + oldvel[2] * sv_frametime;

	// move up
	SV_PushEntity (ent, upmove);		// FIXME: don't link?

	// move forward
	SVvector (ent, velocity)[0] = oldvel[0];
	SVvector (ent, velocity)[1] = oldvel[1];
	SVvector (ent, velocity)[2] = 0;
	clip = SV_FlyMove (ent, sv_frametime, &steptrace);

	// check for stuckness, possibly due to the limited precision of floats
	// in the clipping hulls
	if (clip) {
		if (fabs (oldorg[1] - SVvector (ent, origin)[1]) < 0.03125
			&& fabs (oldorg[0] - SVvector (ent, origin)[0]) < 0.03125) {
			// stepping up didn't make any progress
			clip = SV_TryUnstick (ent, oldvel);
		}
	}
	// extra friction based on view angle
	if (clip & 2)
		SV_WallFriction (ent, &steptrace);

	// move down
	downtrace = SV_PushEntity (ent, downmove);	// FIXME: don't link?

	if (downtrace.plane.normal[2] > 0.7) {
		if (SV_EntCanSupportJump (ent)) {
			SVfloat (ent, flags) = (int) SVfloat (ent, flags) | FL_ONGROUND;
			SVfloat (ent, groundentity) = EDICT_TO_PROG (&sv_pr_state,
														 downtrace.ent);
		}
	} else {
		// if the push down didn't end up on good ground, use the move without
		// the step up.  This happens near wall / slope combinations, and can
		// cause the player to hop up higher on a slope too steep to climb
		VectorCopy (nosteporg, SVvector (ent, origin));
		VectorCopy (nostepvel, SVvector (ent, velocity));
	}
}

/*
  SV_Physics_Client

  Player character actions
*/
void
SV_Physics_Client (edict_t *ent, int num)
{
	if (!svs.clients[num - 1].active)
		return;							// unconnected slot

	// call standard client pre-think
	*sv_globals.time = sv.time;
	*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, ent);
	PR_ExecuteProgram (&sv_pr_state, sv_funcs.PlayerPreThink);

	SVdata (ent)->add_grav = false;

	// do a move
	SV_CheckVelocity (ent);

	// decide which move function to call
	switch ((int) SVfloat (ent, movetype)) {
	case MOVETYPE_NONE:
		if (!SV_RunThink (ent))
			return;
		break;

	case MOVETYPE_WALK:
		if (!SV_RunThink (ent))
			return;
		if (!SV_CheckWater (ent)
			&& !((int) SVfloat (ent, flags) & FL_WATERJUMP))
			SV_AddGravity (ent);
		SV_CheckStuck (ent);
		SV_WalkMove (ent);
		break;

	case MOVETYPE_TOSS:
	case MOVETYPE_BOUNCE:
		SV_Physics_Toss (ent);
		break;

	case MOVETYPE_FLY:
		if (!SV_RunThink (ent))
			return;
		SV_FlyMove (ent, sv_frametime, NULL);
		break;

	case MOVETYPE_NOCLIP:
		if (!SV_RunThink (ent))
			return;
		VectorMultAdd (SVvector (ent, origin), sv_frametime,
					   SVvector (ent, velocity), SVvector (ent, origin));
		break;

	default:
		Sys_Error ("SV_Physics_client: bad movetype %i",
				   (int) SVfloat (ent, movetype));
	}

	SV_LinkEdict (ent, SVfloat (ent, movetype) != MOVETYPE_NOCLIP);

	// call standard player post-think
	*sv_globals.time = sv.time;
	*sv_globals.self = EDICT_TO_PROG (&sv_pr_state, ent);
	PR_ExecuteProgram (&sv_pr_state, sv_funcs.PlayerPostThink);
}
