/* school_alg.c, Copyright (c) 2005-2006 David C. Lambert <dcl@panix.com>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

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

#include "yarandom.h"
#include "glschool_alg.h"

/* for xscreensaver */
#undef drand48
#define drand48()	frand(1.0)

#define RAD2DEG		(180.0 / M_PI)


static inline double
norm(double *dv)
{
	return sqrt(dv[0]*dv[0] + dv[1]*dv[1] + dv[2]*dv[2]);
}


static inline void
addVector(double *v, double *d)
{
	v[0] += d[0];
	v[1] += d[1];
	v[2] += d[2];
}


static inline void
clearVector(double *v)
{
	v[0] = v[1] = v[2] = 0.0;
}


static inline void
scaleVector(double *v, double s)
{
	v[0] *= s;
	v[1] *= s;
	v[2] *= s;
}


static inline void
addScaledVector(double *v, double *d, double s)
{
	v[0] += d[0]*s;
	v[1] += d[1]*s;
	v[2] += d[2]*s;
}


static inline void
getDifferenceVector(double *v0, double *v1, double *diff)
{
	diff[0] = v0[0] - v1[0];
	diff[1] = v0[1] - v1[1];
	diff[2] = v0[2] - v1[2];
}


void
glschool_initFish(Fish *f, double *mins, double *ranges)
{
	int i;

	for(i = 0; i < 3; i++) {
		FISH_IPOS(f, i) = mins[i] + drand48()*ranges[i];
		FISH_IACC(f, i) = 0.0;
		FISH_IVEL(f, i) = drand48();
		FISH_IMAGIC(f, i) = 0.70 + 0.60*drand48();
		FISH_IOLDVEL(f, i) = 0.0;
	}
}


void
glschool_initFishes(School *s)
{
	int		i;
	Fish	*f = (Fish *)0;
	int		nFish = SCHOOL_NFISH(s);
	BBox	*bbox = &SCHOOL_BBOX(s);
	double	*mins = BBOX_MINS(bbox);
	double	*ranges = SCHOOL_BBRANGES(s);
	Fish	*theFishes = SCHOOL_FISHES(s);

	for(i = 0, f = theFishes; i < nFish; i++, f++)
		glschool_initFish(f, mins, ranges);
}


static void
applyFishMovements(Fish *f, BBox *bbox, double minVel, double maxVel, double momentum)
{
	int			i;
	int			oob = 0;
	double		vMag = 0.0;

	for(i = 0; i < 3; i++) {
		double	pos = FISH_IPOS(f, i);
		oob = (pos > BBOX_IMAX(bbox, i) || pos < BBOX_IMIN(bbox, i));
		if (oob == 0) FISH_IVEL(f, i) += FISH_IACC(f, i) * FISH_IMAGIC(f, i);
		vMag += (FISH_IVEL(f, i) * FISH_IVEL(f, i));
	}
	vMag = sqrt(vMag);

	if (vMag > maxVel)
		scaleVector(FISH_VEL(f), maxVel/vMag);
	else if (vMag < minVel)
		scaleVector(FISH_VEL(f), minVel/vMag);

	for(i = 0; i < 3; i++) {
		FISH_IVEL(f, i) = momentum * FISH_IOLDVEL(f, i) + (1.0-momentum) * FISH_IVEL(f, i);
		FISH_IPOS(f, i) += FISH_IVEL(f, i);
		FISH_IOLDVEL(f, i) = FISH_IVEL(f, i);

		if (FISH_IPOS(f, i) < BBOX_IMIN(bbox, i))
			FISH_IPOS(f, i) = BBOX_IMAX(bbox, i);
		else if (FISH_IPOS(f, i) > BBOX_IMAX(bbox, i))
			FISH_IPOS(f, i) = BBOX_IMIN(bbox, i);
	}
}


void
glschool_applyMovements(School *s)
{
	int		i = 0;
	Fish	*f = (Fish *)0;
	int		nFish = SCHOOL_NFISH(s);
	double	minVel = SCHOOL_MINVEL(s);
	double	maxVel = SCHOOL_MAXVEL(s);
	double	momentum = SCHOOL_MOMENTUM(s);
	BBox	*bbox = &SCHOOL_BBOX(s);
	Fish	*theFishes = SCHOOL_FISHES(s);

	for(i = 0, f = theFishes; i < nFish; i++, f++)
		applyFishMovements(f, bbox, minVel, maxVel, momentum);
}


School *
glschool_initSchool(int nFish, double accLimit, double maxV, double minV, double distExp, double momentum,
		   double minRadius, double avoidFact, double matchFact, double centerFact, double targetFact,
		   double distComp)
{
	School	*s = (School *)0;

	if ((s = (School *)malloc(sizeof(School))) == (School *)0) {
		perror("initSchool School allocation failed: ");
		return s;
	}

	if ((SCHOOL_FISHES(s) = (Fish *)malloc(sizeof(Fish)*nFish)) == (Fish *)0) {
		perror("initSchool Fish array allocation failed: ");
		free(s);
		return (School *)0;
	}

	SCHOOL_NFISH(s) = nFish;
	SCHOOL_ACCLIMIT(s) = accLimit;
	SCHOOL_MAXVEL(s) = maxV;
	SCHOOL_MINVEL(s) = minV;
	SCHOOL_DISTEXP(s) = distExp;
	SCHOOL_MOMENTUM(s) = momentum;
	SCHOOL_MINRADIUS(s) = minRadius;
	SCHOOL_MINRADIUSEXP(s) = pow(minRadius, distExp);
	SCHOOL_MATCHFACT(s) = matchFact;
	SCHOOL_AVOIDFACT(s) = avoidFact;
	SCHOOL_CENTERFACT(s) = centerFact;
	SCHOOL_TARGETFACT(s) = targetFact;
	SCHOOL_DISTCOMP(s) = distComp;

	return s;
}

void
glschool_freeSchool(School *s)
{
	free(SCHOOL_FISHES(s));
	free(s);
}

void
glschool_setBBox(School *s, double xMin, double xMax, double yMin, double yMax, double zMin, double zMax)
{
	int		i;
	BBox	*bbox = &SCHOOL_BBOX(s);

	BBOX_XMIN(bbox) = xMin; BBOX_XMAX(bbox) = xMax;
	BBOX_YMIN(bbox) = yMin; BBOX_YMAX(bbox) = yMax;
	BBOX_ZMIN(bbox) = zMin; BBOX_ZMAX(bbox) = zMax;

	for(i = 0; i < 3; i++) {
		SCHOOL_IMID(s, i) = BBOX_IMID(bbox, i);
		SCHOOL_IRANGE(s, i) = BBOX_IRANGE(bbox, i);
	}
}


void
glschool_newGoal(School *s)
{
	SCHOOL_IGOAL(s,0) = 0.85*(drand48()-0.5)*SCHOOL_IRANGE(s,0) + SCHOOL_IMID(s,0);
	SCHOOL_IGOAL(s,1) = 0.40*(drand48()-0.5)*SCHOOL_IRANGE(s,1) + SCHOOL_IMID(s,1);
	SCHOOL_IGOAL(s,2) = 0.85*(drand48()-0.5)*SCHOOL_IRANGE(s,2) + SCHOOL_IMID(s,2);
}


double
glschool_computeNormalAndThetaToPlusZ(double *v, double *xV)
{
	double	x1 = 0.0;
	double	y1 = 0.0;
	double	z1 = 1.0;
	double	x2 = v[0];
	double	y2 = v[1];
	double	z2 = v[2];
	double	theta = 0.0;
	double	xVNorm = 0.0;
	double	sinTheta = 0.0;
	double	v2Norm = norm(v);

	if (v2Norm == 0.0) {
		xV[1] = 1.0;
		xV[0] = xV[2] = 0.0;
		return theta;
	}
	xV[0] = (y1*z2 - z1*y2);
	xV[1] = -(x1*z2 - z1*x2);
	xV[2] = (x1*y2 - y1*x2);
	xVNorm = norm(xV);

	sinTheta = xVNorm/v2Norm;
	return (asin(sinTheta) * RAD2DEG);
}


int
glschool_computeGroupVectors(School *s, Fish *ref, double *avoidance, double *centroid, double *avgVel)
{
	int		i;
	double	dist;
	double	adjDist;
	double	diffVect[3];
	int		neighborCount = 0;
	Fish	*test = (Fish *)0;
	int		nFish = SCHOOL_NFISH(s);
	double	distExp = SCHOOL_DISTEXP(s);
	double	distComp = SCHOOL_DISTCOMP(s);
	double	minRadiusExp = SCHOOL_MINRADIUSEXP(s);
	Fish	*fishes = SCHOOL_FISHES(s);

	for(i = 0, test = fishes; i < nFish; i++, test++) {
		if (test == ref) continue;

		getDifferenceVector(FISH_POS(ref), FISH_POS(test), diffVect);

		dist = norm(diffVect) - distComp;
		if (dist < 0.0) dist = 0.1;

		adjDist = pow(dist, distExp);
		if (adjDist > minRadiusExp) continue;

		neighborCount++;

		addVector(avgVel, FISH_VEL(test));
		addVector(centroid, FISH_POS(test));

		addScaledVector(avoidance, diffVect, 1.0/adjDist);
	}
	if (neighborCount > 0) {
		scaleVector(avgVel, 1.0/neighborCount);
		scaleVector(centroid, 1.0/neighborCount);
	}
	return neighborCount;
}


void
glschool_computeAccelerations(School *s)
{
	int		i;
	int		j;
	int		neighborCount;
	double	dist;
	double	adjDist;
	double	accMag;
	double	avgVel[3];
	double	diffVect[3];
	double	centroid[3];
	double	avoidance[3];
	Fish	*ref = (Fish *)0;
	int		nFish = SCHOOL_NFISH(s);
	double	*goal = SCHOOL_GOAL(s);
	double	distExp = SCHOOL_DISTEXP(s);
	double	distComp = SCHOOL_DISTCOMP(s);
	double	avoidFact = SCHOOL_AVOIDFACT(s);
	double	matchFact = SCHOOL_MATCHFACT(s);
	double	centerFact = SCHOOL_CENTERFACT(s);
	double	targetFact = SCHOOL_TARGETFACT(s);
	double	accLimit = SCHOOL_ACCLIMIT(s);
	double	minRadius = SCHOOL_MINRADIUS(s);
	Fish	*fishes = SCHOOL_FISHES(s);

	for(i = 0, ref = fishes; i < nFish; i++, ref++) {
		clearVector(avgVel);
		clearVector(centroid);
		clearVector(avoidance);
		clearVector(FISH_ACC(ref));
		neighborCount = glschool_computeGroupVectors(s, ref, avoidance, centroid, avgVel);

		/* avoidanceAccel[] = avoidance[] * AvoidFact */
		scaleVector(avoidance, avoidFact);
		addVector(FISH_ACC(ref), avoidance);

		accMag = norm(FISH_ACC(ref));
		if (neighborCount > 0 && accMag < accLimit) {
			for(j = 0; j < 3; j++) {
				FISH_IAVGVEL(ref, j) = avgVel[j];
				FISH_IACC(ref, j) += ((avgVel[j] - FISH_IVEL(ref, j)) * matchFact);
			}

			accMag = norm(FISH_ACC(ref));
			if (accMag < accLimit) {
				for(j = 0; j < 3; j++)
					FISH_IACC(ref, j) += ((centroid[j] - FISH_IPOS(ref, j)) * centerFact);
			}
		}

		accMag = norm(FISH_ACC(ref));
		if (accMag < accLimit) {
			getDifferenceVector(goal, FISH_POS(ref), diffVect);

			dist = norm(diffVect) - distComp;
			if (dist < 0.0) dist = 0.1;

			/*adjDist = pow(dist, distExp);*/
			if (dist > minRadius) {
				adjDist = pow(dist, distExp);
				for(j = 0; j < 3; j++)
					FISH_IACC(ref, j) += (diffVect[j]*targetFact/adjDist);
			}
		}
	}
}
