/***************************************************************************
 *            world.c
 *
 *  Fri Jan 28 16:43:56 2005
 *  Copyright  2005  Peter Baldwin
 *  pgbaldwi@uwaterloo.ca
 ****************************************************************************/


#include "world.h"
#include "console.h"

#include <stdlib.h>
#include <math.h>

#define MAX_GNIMMELS 256
#define MAX_BLOCKS 256
#define MAX_SOUNDS 256
#define MAX_ENTRANCES 16
#define MAX_EXITS 16
#define MAX_MAGNETS 64
#define MAX_TRIGGERS 32
#define BLOCK_MASS HUGE_VAL
#define MAGNET_MASS HUGE_VAL
const double MIN_GROUND_TIME = 0.05; //gnimmels are allowed to accelerate for at least this amount of time each time they hit the ground
const double MIN_BUMP_TIME = 0.5;  //minimum time that the gnimmel must be collision free in order for it to make another sound
const double GNIMMEL_SIZE = 0.1;
const double GNIMMEL_MASS = 1;
const double GNIMMEL_MASS_FROZEN = 1000;
const double GNIMMEL_UIMPACT = 0.6;
const double BLOCK_UIMPACT = 0;
const vector GRAVITY = {0,0.5}; //down
const double UF = 0.05; //coefficient of friction
const double GNIMMEL_SPEED = 0.2; //gnimmels *prefer* to travel at this speed
const double GNIMMEL_FORCE = 0.5; //gnimmels can exert a force of this magnitude by rolling
const double MAGNET_FORCE = 0.05; //0.025 is optimal
const double MAGNET_SIZE = 0.125;

const wSound SOUND_BUMP  = { "Umpf!", {0,0}, 1 };
const wSound SOUND_DEATH = { "Eep!",  {0,0}, 2 };

bool wToggleRoll = TRUE;
bool wToggleExtension = TRUE;


double wAspectRatio = (double)2;

int num_gnimmels;
int num_blocks;
int num_entrances;
int num_exits;
int num_sounds;
int num_magnets;
int num_triggers;
int goal;
char *title;
wGnimmel gnimmels[MAX_GNIMMELS];
wBlock   blocks  [ MAX_BLOCKS ];
wSound   sounds  [ MAX_SOUNDS ];
wEntrance entrances [MAX_ENTRANCES];
wExit     exits     [  MAX_EXITS  ];
wMagnet   magnets   [ MAX_MAGNETS ];
wTrigger  triggers  [ MAX_TRIGGERS];

int 	wGetGoal() { return goal; }

char   *wGetLevelTitle() { return title; }

bool wOutOfBounds(vector v) {
	//the actual bounds go above the top of the screen 
	//(in case a magnet flings a gnimmel really high)
	return v.x < 0 || v.y < -1 || v.x > wAspectRatio || v.y > 1;
}

bool wIsAirborne(wGnimmel *g) {
	return g->hangtime > MIN_GROUND_TIME;
}

bool wIsActive(wGnimmel *g) {
	return g->alive && !g->exited;
}

void wFreezeGnimmel(wGnimmel *g) {
	//remember direction
	if (g->o.vel.x > 0) {
		g->roll = RIGHT;
	} else if (g->o.vel.x < 0) {
		g->roll = LEFT;
	} //else: same as entrance
	g->o.vel.x = 0; //stop horizontal movement
	g->o.mass = GNIMMEL_MASS_FROZEN;
	
	
	g->frozen = TRUE;
	
}
void wDefrostGnimmel(wGnimmel *g) {
	g->o.vel.x = 0;	//stop any horizontal movement, 
					//so gnimmel uses remembered direction
	g->o.mass = GNIMMEL_MASS;
	g->frozen = FALSE;
}



void wMakeSound(vector pos, wSound sound) {
	int i;
	int index = -1;
	double min_t = HUGE_VAL;
	int min_t_index = 0;
	//attempt to re-use an exisitng sound that has expired
	for (i=0; i<num_sounds; i++) {
		if (sounds[i].t <= 0) {
			index = i;
			break;
		} else if (sounds[i].t < min_t) {
			min_t = min_t;
			min_t_index = i;
		}
	}
	
	if (index!=-1) {
		//do nothing, already have a valid index
	} else if (num_sounds == MAX_SOUNDS) {
		//maximum sounds reached, and all are active:
		//replace the oldest sound
		index = min_t_index;
	} else {
		//make a new sound
		index = num_sounds++;
	}
	
	sounds[index] = sound;
	sounds[index].pos = pos;
}

void wAddTrigger(bool unfreeze, bool freeze, bool dig, double magnet, 
	double x, double y, double w, double h) {
		
	if (num_triggers==MAX_TRIGGERS) {
		cWarning("Maximum Triggers Reached");
		return;
	}
	int index = num_triggers++;	
	
	wTrigger newTrigger = {unfreeze, freeze, dig, magnet, 
		x, y, w, h};
	triggers[index] = newTrigger;
}

void wAddBlock(double x, double y, double w, double h, bool soft) {
	if (num_blocks==MAX_BLOCKS) {
		cWarning("Maximum Blocks Reached");
		return;
	}
	int index = num_blocks++;
	wBlock newBlock = { { {x+w/2,y+h/2}, VZERO, BLOCK_MASS, BLOCK_UIMPACT }, w, h, soft};
	blocks[index] = newBlock;
}

void wAddEntrance(double x, double y, double n, double delay, roll_direction direction) {
	if (num_blocks==MAX_ENTRANCES) {
		cWarning("Maximum Entrances Reached");
		return;
	}
	int index = num_entrances++;
	wEntrance newEntrance = { {x,y}, n, delay, direction, delay/2 };
	entrances[index] = newEntrance;
}

void wAddMagnet(double x, double y, magnet_pole pole) {
	if (num_magnets==MAX_MAGNETS) {
		cWarning("Maximum Magnets Reached");
		return;
	}
	int index = num_magnets++;
	wMagnet newMagnet = { {x,y}, MAGNET_SIZE, pole};
	magnets[index] = newMagnet;
}
	
void wAddExit(double x, double y) {
	if (num_exits==MAX_EXITS) {
		cWarning("Maximum Entrances Reached");
		return;
	}
	int index = num_exits++;
	wExit newExit = { {x,y}, 0 };
	exits[index] = newExit;
	
	//roof
	const double w = 0.15;
	const double h = 0.05;
	wAddBlock(x-w/2,y-0.2,w,h,FALSE);
}

void wReleaseGnimmel(double x, double y, short roll) {
	if (num_gnimmels==MAX_GNIMMELS) {
		cWarning("Maximum Gnimmels Reached");
		return;
	}
	int index = num_gnimmels++;
	
	wGnimmel newGnimmel = { 
		{ {x,y}, VZERO, GNIMMEL_MASS, GNIMMEL_UIMPACT },
		FALSE, //frozen
		TRUE,  //alive
		FALSE, //exited
		MIN_GROUND_TIME+1, //hangtime
		MIN_BUMP_TIME, //collisionfree
		0, //magnet
		GNIMMEL_SIZE, //size
		roll, //default roll direction dictated by entrance
		FALSE //dig
	};
	gnimmels[index] = newGnimmel;
}

void wKillGnimmel(wGnimmel *victim) {
	victim->alive = FALSE;
	wMakeSound(victim->o.pos, SOUND_DEATH);
}
void wInit(level lvl) {
	
	num_gnimmels = 0;
	num_sounds = 0;
	num_entrances = 0;
	num_exits = 0;
	num_blocks = 0;
	num_magnets = 0;
	num_triggers = 0;
	switch (lvl) {
		case TITLE_LEVEL:
			wAddEntrance(0.1,0.2,MAX_GNIMMELS,2,RIGHT);
			wAddBlock(0,0.2,1.6,0.1,TRUE);
			wAddTrigger(FALSE,FALSE,FALSE,0.5,1.75,0,0.25,1); //magnet
			wAddMagnet(1.9,0.35,NORTH);
			wAddExit(0.1,0.8);
			wAddBlock(0,0.8,1.6,0.1,TRUE);
			goal = MAX_GNIMMELS+1; //can't be reached
			title = "Gnimmel: The Gname";
			break;
		case TUTORIAL1: //dig
			wAddEntrance(1.0,0.2,1,2,RIGHT);
			wAddBlock(0.5,0.2,1,0.1,TRUE);
			wAddBlock(0.5,0.75,1,0.1,FALSE);
			wAddExit(1,0.75);
			wAddBlock(0.4,0.1,0.1,0.75,FALSE);
			wAddBlock(1.5,0.1,0.1,0.75,FALSE);
			goal = 1;
			title = "Right-Click to Dig!";
			break;
		case TUTORIAL2: //block
			wAddEntrance(0.75,0.75,2,2,LEFT);
			wAddBlock(0,0.75,2,0.1,FALSE);
			wAddExit(1.25,0.75);
			goal = 1;
			title = "Middle-Click to Freeze/Unfreeze!";
			break;
		case TUTORIAL3: //magnet
			wAddEntrance(0.5,0.75,1,2,RIGHT);
			wAddBlock(  0,0.15,  2,0.1,FALSE);
			wAddBlock(  0,0.15,0.1,0.6,FALSE);
			wAddBlock(1.9,0.15,0.1,0.6,FALSE);
			wAddBlock(0,0.75,0.9,0.1,FALSE);
			wAddMagnet(1,0.9,NORTH);
			wAddBlock(1.1,0.75,0.9,0.1,FALSE);
			wAddExit(1.5,0.75);
			goal = 1;
			title = "Left-Click to Charge!";
			
			break;
		case LEVEL1:
			wAddEntrance(0.1,0.2,2,2,RIGHT);
			wAddBlock(0,0.2,1,0.1,TRUE);
			wAddBlock(0, 0.8,1.9,0.1,TRUE);
			wAddExit(1.8,0.8);
			goal = 2;
			title = "Level 1: Daniel's Drop";
			break;
		case LEVEL2:
			wAddEntrance(0.5,0.2,5,2,RIGHT);
			wAddBlock(0,0.2,2,0.1,TRUE);
			wAddBlock(1.8,0.1,0.1,0.1,FALSE);
			wAddBlock(0, 0.8,0.65,0.1,TRUE);
			wAddExit(0.55,0.8);
			goal = 4;
			title = "Level 2: The Brink";
			break;
		case LEVEL3:
			wAddEntrance(0.1,0.5,5,4,RIGHT);
			wAddBlock(0,0.5,0.75,0.1,FALSE);
			wAddMagnet(1,0.2,SOUTH);
			wAddMagnet(1,0.825,NORTH);
			wAddBlock(1.25,0.5,0.75,0.1,FALSE);
			wAddExit(1.9,0.5);
			goal = 2;
			title = "Level 3: Particle Accelerator";
			break;
		case LEVEL4:
			wAddEntrance(0.1,0.4,5,3,RIGHT);
			wAddBlock(0,0.4,1,0.1,FALSE);
			wAddBlock(0.25,0.5,0.75,0.1,TRUE);
			wAddBlock(0.50,0.6,0.50,0.1,TRUE);
			wAddBlock(0.75,0.7,0.25,0.1,TRUE);
			wAddMagnet(1.2,0.1, SOUTH);
			wAddBlock(1.5, 0.8,0.4,0.1,TRUE);
			wAddBlock(1.4, 0.6,0.14,0.31,TRUE);
			wAddExit(1.8,0.8);
			goal = 4;
			title = "Level 4: The Gap";
			break;
		case TEST_LEVEL:
		default:
			wAddEntrance(0.5,0.75,50,3,RIGHT);
			wAddEntrance(1.5,0.75,50,3,LEFT);
			wAddBlock(0.25,0.75,0.5,0.1,FALSE);
			wAddMagnet(1,0.25,NORTH);
			wAddExit(1.75,0.25);
			wAddBlock(1.5,0.25,0.5,0.1,TRUE);
			wAddBlock(1.25,0.75,0.5,0.1,TRUE);
			wAddMagnet(0.5,0.25,SOUTH);
			wReleaseGnimmel(0.5,0.1, 1);
			goal = 0;
			title = "Test Level";
			break;	
	}
}
void wTickEntrance(wEntrance *e, double t) {
	if (e->n > 0) {
		//more gnimmels to release
		e->lastrelease+=t; //account for elapsed time
		
		//release any gnimmels which are due to be released
		if (e->lastrelease  > e->delay ) {
			//note: could use while loop, in case an unusually
			//large time period has elapsed
			//but then two gnimmels
			//would be exactly on top of eachother and the
			//the collision would not be well defined
			e->lastrelease-=e->delay;
			wReleaseGnimmel(e->pos.x,e->pos.y-GNIMMEL_SIZE/1.5,e->roll);
			e->n--;
		}
	}
}


void wSplitBlock(wBlock *b, double x) {
	
	/* 
	     right1   left2
	left1     | x |        right2
	________________________
	|           |          |
	                           */
	const double left1 = b->o.pos.x-b->w/2;
	const double right1 = x-GNIMMEL_SIZE/2;
	const double top1 = b->o.pos.y-b->h/2;
	const double w1 = right1-left1;
	const double h1 = b->h;
	const double x1 = left1+w1/2;
	//const double y1 = b->o.pos.y;
	const double left2 = x+GNIMMEL_SIZE/2;
	const double right2 = b->o.pos.x+b->w/2;
	const double w2 = right2-left2;
	//const double x2 = left2+w2/2;
	
	if (x > left1+GNIMMEL_SIZE/2 && x < right2-GNIMMEL_SIZE/2) {
		b->o.pos.x = x1;
		b->w = w1;
		wAddBlock(left2,top1,w2,h1,b->soft);
	}
}

void wTick(double t) { 
	const double MIN_INTERVAL = 0.01;
	
	if (t > MIN_INTERVAL) {
		//recusively call with smaller intervals
		wTick(MIN_INTERVAL);
		wTick(t-MIN_INTERVAL);
	} else {
		//proceed normally
		int i, j;
		
		//release gnimmels
		for (i=0; i<num_entrances; i++) {
			wTickEntrance(&entrances[i],t);
		}
		//move gnimmels
		for (i=0; i<num_gnimmels; i++) {
			wGnimmel *g = &gnimmels[i];
			if (wIsActive(&gnimmels[i])) {
				vector fgnimmel = VZERO;
				//horizontal velocity
				const double hv = gnimmels[i].o.vel.x;
				const double ahv = fabs(hv);
				if (wToggleRoll &&  !gnimmels[i].frozen && 
						!wIsAirborne(&gnimmels[i]) && 
							ahv < GNIMMEL_SPEED ) {
					//on ground and travelling slower than desired
					if (hv>0) {
						fgnimmel = vNew(GNIMMEL_FORCE, 0);
					} else if (hv < 0) {
						fgnimmel = vNew(-GNIMMEL_FORCE, 0);
					} else {
						fgnimmel = vNew(gnimmels[i].roll * GNIMMEL_FORCE, 0);
					}	
				}
				
				const vector fg = vScale(GRAVITY, gnimmels[i].o.mass);
				const vector ff = wIsAirborne(&gnimmels[i]) ? VZERO : vScale(vUnit(gnimmels[i].o.vel),-1 * UF * vMagnitude(fg));
				
				//magnetic force
				vector fm = VZERO;
				if (wToggleExtension) {
					for (j=0; j<num_magnets; j++) {
						wMagnet *m = &magnets[j];
						if (gnimmels[i].magnet) { //only magnetic gnimmels are attracted
							vector d = vSubtract(m->pos,g->o.pos);
							double dist = vMagnitude(d);
							vector dir = vUnit(d);
							fm = vAdd(fm,vScale(dir,m->pole * MAGNET_FORCE/pow(dist,2)));
						}
						//all gnimmels collide with magnets
						pCircle cg = { &g->o, g->size/2 };
						pObject mo = {m->pos,VZERO,MAGNET_MASS,0};
						pCircle cm = { &mo, m->size/2 };
						pCollisionCircCirc(&cg,&cm);
					}
					
					if (gnimmels[i].magnet) {
						//subtract from magnetic effect time
						gnimmels[i].magnet-=t;
						if (gnimmels[i].magnet<0) gnimmels[i].magnet = 0; //don't allow negative values
					}
				}
				const vector fnet = vAdd(vAdd(vAdd(fgnimmel,fg),ff),fm);
				pMove(&gnimmels[i].o,fnet,t);
				if (wOutOfBounds(gnimmels[i].o.pos)) {
					wKillGnimmel(&gnimmels[i]);
				}
				
				//check triggers
				for (j=0; j<num_triggers; j++) {
					wTrigger *t = &triggers[j];
					if (g->o.pos.x > t->x &&
						g->o.pos.x < t->x + t->w &&
						g->o.pos.y > t->y &&
						g->o.pos.y < t->y + t->h) {
						if (t->magnet) {
							g->magnet=t->magnet;
						}
						if (t->freeze) {
							wFreezeGnimmel(g);
						}
						if (t->unfreeze) {
							wDefrostGnimmel(g);
						}
						if (t->dig) {
							g->dig = TRUE;
						}
					}
				}
				
				//check if exited
				for (j=0; j<num_exits; j++) {
					//don't let frozen gnimmels exit
					
					if (!gnimmels[i].frozen) {
						vector d = vSubtract(gnimmels[i].o.pos,exits[j].pos);
						if (fabs(d.x) < GNIMMEL_SIZE * 0.75 && d.y > -GNIMMEL_SIZE && d.y < 0) {
							gnimmels[i].exited = TRUE;
							exits[j].n++;
						}
					}
					break;
				}
			}
		}
		//inter-gnimmel collisions
	
		for (i=0; i<num_gnimmels; i++) {
			wGnimmel *a = &gnimmels[i];
			if (wIsActive(a)) {
				for (j=i+1; j<num_gnimmels; j++) {
					wGnimmel *b = &gnimmels[j];
					if (wIsActive(b)) {
						bool collision = FALSE;
						if (!a->frozen && !b->frozen) {
							if (wToggleExtension) {
								//both circular
								pCircle ca = { &a->o, a->size/2 };
								pCircle cb = { &b->o, b->size/2 };
								collision = pCollisionCircCirc(&ca,&cb);
							} else {
								collision = FALSE;
							}
						} else if (a->frozen && b->frozen) {
							pRectangle ra = { &a->o, a->size, a->size };
							pRectangle rb = { &b->o, b->size, b->size };
							collision = pCollisionRectRect(&ra,&rb);
							//TODO frozen gnimmels and combos
						} else if ( a->frozen ) {
							pRectangle ra = { &a->o, a->size, a->size };
							pCircle cb = { &b->o, b->size/2 };
							collision = pCollisionCircRect(&cb,&ra);
						} else { //b->frozen
							pCircle ca = { &a->o, a->size/2 };
							pRectangle rb = { &b->o, b->size, b->size };
							collision = pCollisionCircRect(&ca,&rb);
						}
						if (collision) {
							if (a->collisionfree > MIN_BUMP_TIME && b->collisionfree > MIN_BUMP_TIME) {
								//make a sound, if there hasn't been a collision recently
								wMakeSound(a->o.pos,SOUND_BUMP);
							}
							a->collisionfree = 0;
							b->collisionfree = 0;
							a->hangtime = 0;
							b->hangtime = 0;
						} else {
							if (a->collisionfree <= MIN_BUMP_TIME) a->collisionfree += t;
							if (b->collisionfree <= MIN_BUMP_TIME) b->collisionfree += t;
							if (a->hangtime <= MIN_GROUND_TIME) a->hangtime += t;
							if (b->hangtime <= MIN_GROUND_TIME) b->hangtime += t;
						}
					}
				}
			}
		}
		
		//gnimmel-level collisions
		for (i=0; i<num_gnimmels; i++) {
			for (j=0; j<num_blocks; j++) {
				wGnimmel *a = &gnimmels[i];
				wBlock   *b = &blocks  [j];
				if (wIsActive(a)) {
					bool collision;
					if (a->frozen) {
						pRectangle ra = { &a->o, a->size, a->size };
						pRectangle rb = { &b->o, b->w, b->h };
						collision = pCollisionRectRect(&ra, &rb);
					} else {
						pCircle ca = { &a->o, a->size/2 };
						pRectangle rb = { &b->o, b->w, b->h };
						collision = pCollisionCircRect(&ca, &rb);
							
					}
					if (collision) {
						a->hangtime = 0;
						if (a->dig && b->soft) {
							a->dig = FALSE; //only dig once
							a->o.vel = VZERO; //stop moving
							wSplitBlock(b, a->o.pos.x);
						}
					}else {
						if (a->hangtime <= MIN_GROUND_TIME) {
							a->hangtime += t;
						}
					}
				}
			}
		}
		//decrease time on sounds
		for (i=0; i<num_sounds; i++) {
			if (sounds[i].t>0) {
				sounds[i].t-=t;
			}
		}
	}
}

wGnimmel *wGetGnimmels(int *count) {
	*count = num_gnimmels;
	return gnimmels;
}

wBlock   *wGetBlocks(int *count) {
	*count = num_blocks;
	return blocks;
}

wSound   *wGetSounds(int *count) {
	*count = num_sounds;
	return sounds;
}

wEntrance *wGetEntrances(int *count) {
	*count = num_entrances;
	return entrances;
}
wExit     *wGetExits(int *count) {
	*count = num_exits;
	return exits;
}

wMagnet     *wGetMagnets(int *count) {
	*count = num_magnets;
	return magnets;
}
