// ------------------------------------------------------------------
// cball.cpp
// ------------------------------------------------------------------
// This class is the ball of the player
// By Kronoman - In loving memory of my father
// Copyright (c) 2003,2004, Kronoman
// ------------------------------------------------------------------

#include "cball.h"
#include "gerror.h"
#include "sphermap.h"
#include "sound.h"

// ------------------------------------------------------------------
// Constructor
// ------------------------------------------------------------------
CBall::CBall()
{
	x = y = z = dx = dy = dz = 0.0;
	spr = spr_shadow = spr_cache = NULL;
	ctmap = NULL;
	particle_manager = NULL;

	lives = BALL_LIVES;
	anglex = angley = 0.0;
	score = 0;

	// DEBUG - we use all available controllers by default
	control.set_use_mouse(true);
	control.set_use_joystick(true);
	control.set_use_keyboard(true);

}

// ------------------------------------------------------------------
// Destructor
// ------------------------------------------------------------------
CBall::~CBall()
{
	// release RAM
	if (spr_cache != NULL)
	{
		destroy_bitmap(spr_cache);
		spr_cache = NULL;
	}
}

// ------------------------------------------------------------------
// Draw the ball, in bmp, displaced by sx,sy pixels
// This uses 3D Allegro polygon routines
// A *scene* must be initialized!!
// ------------------------------------------------------------------
void CBall::draw(BITMAP *bmp, int sx, int sy)
{
	float s = 0; // scale (is added to the size, not multiplied!)
	if (spr == NULL)
		return ; // can't draw, set the sprite first (texture of sphere)
	if (z < BALL_MIN_Z*0.95)
		return ; // we are dead, the ball is gone, don't draw

	// create sprite cache for rendering sphere texture (this is the size of texture, not the radius of the ball)
	if (spr_cache == NULL)
		spr_cache = create_bitmap(spr->w, spr->h);

	if (spr_cache == NULL)
		raise_error("ERROR: CBall:draw() -> can't create sprite texture render cache (spr_cache) (out of memory?)\n");

	// if the ball is jumping or falling, we scale it acording to Z
	if (z < -1.0)
	{
		s = - (z * BALL_RADIUS / BALL_MIN_Z);
	}

	if (z > 1.0)
	{
		s = (z * BALL_RADIUS * 2 / BALL_MAX_Z);
	}


	// render sphere
	MATRIX m;
	clear_to_color(spr_cache, makecol(255, 0, 255)); // magic pink fill

	get_planet_rotation_matrix(&m, ftofix(anglex), ftofix(angley), 0); // rotate texture
	mapped_sphere_ex(spr_cache, spr_cache->w / 2, spr_cache->h / 2, spr_cache->w / 2, spr, &m); // render texture

	// add shadow enviroment map
	set_trans_blender(200, 200, 200, 200); // transparent blender
	draw_trans_sprite(spr_cache, spr_shadow, 0, 0); // blit the shiny shadow


	// do a quad for render of the ball
	V3D_f *v_xyz_p[4]; // this is needed to pass the array pointer to software renderer
	V3D_f pc_v[4]; // the 4 vertex of the quad are precalculated and stored here

	int poltype = POLYTYPE_ATEX_MASK; // type of polygon to render, usually POLYTYPE_ATEX_MASK

	pc_v[0].x = x - sx - s;
	pc_v[0].y = y - sy - s;

	pc_v[1].x = x - sx + BALL_RADIUS * 2 + s;
	pc_v[1].y = y - sy - s;

	pc_v[2].x = x - sx + BALL_RADIUS * 2 + s;
	pc_v[2].y = y - sy + BALL_RADIUS * 2 + s;

	pc_v[3].x = x - sx - s;
	pc_v[3].y = y - sy + BALL_RADIUS * 2 + s;

	pc_v[0].z = pc_v[1].z = pc_v[2].z = pc_v[3].z = (z < 0) ? 2 : 0.25;


	v_xyz_p[0] = &pc_v[0];
	v_xyz_p[1] = &pc_v[1];
	v_xyz_p[2] = &pc_v[2];
	v_xyz_p[3] = &pc_v[3];

	// texture map coordinates
	pc_v[0].u = 0;
	pc_v[0].v = 0;
	pc_v[1].u = spr->w;
	pc_v[1].v = 0;
	pc_v[2].u = spr->w;
	pc_v[2].v = spr->h;
	pc_v[3].u = 0;
	pc_v[3].v = spr->h;

	scene_polygon3d_f(poltype, spr_cache, 4, v_xyz_p); // render polygon to scene

}


// ------------------------------------------------------------------
// Update ball logic, returns true if ball is 'dead'
// Also, does sounds, pick up prizes, etc... :)
// ------------------------------------------------------------------

int CBall::update_logic()
{
	int ret ; // this ret is NOT used as return value of 'return' in this routine.
	bool over_exit_floor = false; // we are over floor that is level's exit?

	if (ctmap == NULL)
		return CBALL_IS_FINE; // sorry, I don't know the map where I move

	if (z < BALL_MIN_Z)
		return CBALL_IS_DEAD; // we are dead (Z too low), we lose, we can't handle the ball, etc... :P // DEBUG : sound ?

	// rotate the thing
	if ((int)dx != 0 || (int)dy != 0 || (int)dz != 0)
	{
		// DEBUG - check this, the rotation is not 100% correct in certain cases
		if ((angley >= 0 && angley <= 64) || (angley >= 192))
			anglex -= dx;
		else
			anglex += dx;

		angley -= dy;

		if (anglex < 0)
			anglex = 255;
		if (anglex > 255)
			anglex = 0;
		if (angley < 0)
			angley = 255;
		if (angley > 255)
			angley = 0;
	}


	// control input
	ret = control.do_input_poll();

	if (ret & KC_UP)
		dy -= BALL_SPEED_Y;
	if (ret & KC_DOWN)
		dy += BALL_SPEED_Y;
	if (ret & KC_LEFT)
		dx -= BALL_SPEED_X;
	if (ret & KC_RIGHT)
		dx += BALL_SPEED_X;

	// apply current tile floor transformations (current floor on center of the ball)
	ret = ctmap->get_tile_type(((int)x + (BALL_RADIUS)) / TMAPS_W, ((int)y + (BALL_RADIUS)) / TMAPS_H, 0);
	if (0 < z && z < 1.0) // only if 0 < Z < 1.0 we are in the floor
	{
		if (ret > 0) // is over a valid tile
		{
			
			if (ctmap->tile_set[ret].sound && (!ctmap->tile_set[ret].exit_level || (ctmap->tile_set[ret].exit_level &&  (ctmap->prize_map_indispensable<1)  ) ) )
				soundw.play_sample(ctmap->tile_set[ret].sound, rand() % 50 + 180, 128, 1000 + rand() % 250 - 125, 0); // SOUND of the tile, DEBUG
			
			dx += ctmap->tile_set[ret].adx;
			dy += ctmap->tile_set[ret].ady;
			dz += ctmap->tile_set[ret].adz;

			dx *= ctmap->tile_set[ret].mdx;
			dy *= ctmap->tile_set[ret].mdy;
			dz *= ctmap->tile_set[ret].mdz;

			// if we are over a floor, we are safe
			if (dz < 0)
				dz = 0.0; // we aren't falling anymore =D
			if (z < 1)
				z = 1.0; // we are over floor =D

			// this floor is exit?
			over_exit_floor = ctmap->tile_set[ret].exit_level;
		}
		else
		{
			// if we are outside map or in a empty floor, we must fall to death... :^O
			dz -= BALL_SPEED_Z; // negative dz, means fall!
		}
	}
	else
	{
		z -= N_BALL_SPEED_Z; // apply gravity
	}

	// comprobations of max values

	if (dy < -BALL_MSPEED)
		dy = -BALL_MSPEED;

	if (dy > BALL_MSPEED)
		dy = BALL_MSPEED;

	if (dx < -BALL_MSPEED)
		dx = -BALL_MSPEED;

	if (dx > BALL_MSPEED)
		dx = BALL_MSPEED;

	if (dz < -BALL_MSPEED)
		dz = -BALL_MSPEED;

	if (dz > BALL_MSPEED)
		dz = BALL_MSPEED;

	// friction
	if (dy > 0)
		dy -= N_BALL_SPEED_Y;

	if (dy < 0)
		dy += N_BALL_SPEED_Y;

	if (dx > 0)
		dx -= N_BALL_SPEED_X;

	if (dx < 0)
		dx += N_BALL_SPEED_X;

	// 'dead zone' of the ball
	if (dy < N_BALL_SPEED_Y*1.1 && dy > 0)
		dy = 0.0;

	if (dy > -N_BALL_SPEED_Y*1.1 && dy < 0)
		dy = 0.0;

	if (dx < N_BALL_SPEED_X*1.1 && dx > 0)
		dx = 0.0;

	if (dx > -N_BALL_SPEED_X*1.1 && dx < 0)
		dx = 0.0;

	// gravity (if dz > 1.0, the ball is jumping)
	if (dz > 0)
		dz -= N_BALL_SPEED_Z;

	// Collision with walls -- DEBUG - DEBUG -- THIS NEEDS HEAVY CHECK!!!
	// predict where the ball goes (for check collisions)
	float xp, yp;
	xp = x + dx;
	yp = y + dy;

	/*
	Atento:
	formula para sacar el rebote

	r = u - 2*n*(n dot u)

	donde 
	r = vector que quiero
	u = vector direccion bola
	n = normal de la superficie (pared)

	dot = producto escalar
	*/


	if (z > 0) // only if z > 0 we are over the floor; we only collide with walls if we are over the floor. duh!
	{
		float n_dot_u = 0, yn = 0, xn = 0; // n dot u , n(x,y)
		float part_x = 0, part_y = 0; // particle spark position

		for (int i = 0; i < 8; i ++) // check 8 points of ball's sprite
		{
			// NOTE: the size of the bounding box is smaller than the ball radius

			// BGAP: size to shrink the bounding box
#define BGAP 6
			switch (i)
			{
				case 0:  // up middle
				part_x = xp + BALL_RADIUS;
				part_y = yp + BGAP;

				xn = 0;
				yn = 1;
				break;

				case 1:  // down middle
				part_x = xp + BALL_RADIUS;
				part_y = yp + BALL_RADIUS * 2 - BGAP;
				xn = 0;
				yn = -1;
				break;

				case 2:  // left middle
				part_x = xp + BGAP;
				part_y = yp + BALL_RADIUS;
				xn = 1;
				yn = 0;
				break;

				case 3:  // right middle
				part_x = xp + BALL_RADIUS * 2 - BGAP;
				part_y = yp + BALL_RADIUS;
				xn = -1;
				yn = 0;
				break;

#define normalized_v 0.707106781
				// this are the four cornes
				// the weird numbers on normal vectors are because they are normalized!
				case 4:  // up left
				part_x = xp + BGAP;
				part_y = yp + BGAP;
				xn = normalized_v;
				yn = normalized_v;
				break;

				case 5:  // down left
				part_x = xp + BGAP;
				part_y = yp + BALL_RADIUS * 2 - BGAP;
				xn = normalized_v;
				yn = -normalized_v;
				break;

				case 6:  // up right
				part_x = xp + BALL_RADIUS * 2 - BGAP;
				part_y = yp + BGAP;
				xn = -normalized_v;
				yn = normalized_v;
				break;

				case 7:  // down right
				part_x = xp + BALL_RADIUS * 2 - BGAP;
				part_y = yp + BALL_RADIUS * 2 - BGAP;
				xn = -normalized_v;
				yn = -normalized_v;
				break;
#undef normalized_v

			}
#undef BGAP
			ret = ctmap->get_tile_type((int)part_x / TMAPS_W, (int)part_y / TMAPS_H, 0); // up

			if (ret > 0)
			{
				if (ctmap->tile_set[ret].solid)
				{
					// DEBUG -- hack way to solve bug of corners!!
					if (i < 4) // hack with corners - it WORKS, and that is what counts :P
					{
						// bounce!
						n_dot_u = dx * xn + dy * yn;
						// this is the real bounce, proper done with vectors (although most of the time it don't work :P)
						dx = dx - 2 * xn * n_dot_u;
						dy = dy - 2 * yn * n_dot_u;
					}
					else
					{
						dx *= -1; // hacky bounce for conners... I know, is wrong... I dont care...
						dy *= -1;
					} // end of hack

					// bounce factor of wall
					dx *= ctmap->tile_set[ret].bounce_factor;
					dy *= ctmap->tile_set[ret].bounce_factor;


					// DEBUG: the particle spark NEEDS to be accurate
					for (int i = 0; i < rand() % 10 + 5; i++) // particle spark
						particle_manager->add_particle(new CBaseParticle(part_x + (rand() % 6) - 3, part_y + (rand() % 6) - 3, (float)(rand() % 300) / 100.0*(dx < 0 ? -1 : 1), (float)(rand() % 300) / 100.0*(dy < 0 ? -1 : 1), makecol(255, rand() % 55 + 200, 0), rand() % 15 + 5));

					// bounce sound
					if (ctmap->tile_set[ret].sound)
						soundw.play_sample(ctmap->tile_set[ret].sound, rand() % 56 + 200, 128 + rand() % 64 - 32, 1000 + rand() % 300 - 150, 0);

					break; // end for (if not, the code will fail, will bounce many times, most of the time just rendering void the bounce)
				}
			}
		}
	}

	// move the ball


	x += dx;
	y += dy;
	z += dz;

	if (x < 0)
		x = 0;

	if (y < 0)
		y = 0;

	if (x > TMAP_SIZE * TMAPS_W)
		x = TMAP_SIZE * TMAPS_W;

	if (y > TMAP_SIZE * TMAPS_H)
		y = TMAP_SIZE * TMAPS_H;

	if (z > BALL_MAX_Z) // top jump, hit the 'roof'
	{
		z = BALL_MAX_Z;

		if (dz > 0)
			dz = dz * 0.8; // decrease jump by 20 % also
	}

	// finally, check the 'prize' layer (only if we are over the floor)

	ret = ctmap->get_tile_type(((int)x + (BALL_RADIUS)) / TMAPS_W, ((int)y + (BALL_RADIUS)) / TMAPS_H, 1);

	if ((ctmap->tile_set[ret].indispensable || ctmap->tile_set[ret].is_a_prize) && z >= 0 && ret > 0)
	{

		score += ctmap->tile_set[ret].score;

		if (ctmap->tile_set[ret].indispensable)
			ctmap->prize_map_indispensable--;

		ctmap->set_tile_type(((int)x + (BALL_RADIUS)) / TMAPS_W, ((int)y + (BALL_RADIUS)) / TMAPS_H, 1, 0); // clean the tile

		// SOUND
		if (ctmap->tile_set[ret].sound)
			soundw.play_sample(ctmap->tile_set[ret].sound, rand() % 26 + 230, 128 + rand() % 64 - 32, 1000 + rand() % 100 - 50, 0);

		// add score message
		if (particle_manager != NULL)
		{
			char score_txt[256];
			usprintf(score_txt, "%c%d", (ctmap->tile_set[ret].score > 0 ? '+' : '-'), ctmap->tile_set[ret].score);

			particle_manager->add_particle(new CExplosiveTextParticle(x + BALL_RADIUS, y, 0.0, (float)(rand() % 100 + 25) / -100.0, makecol(255, rand() % 128 + 128, 0), rand() % 30 + 30, string(score_txt) ) );

			for (int i = 0; i < rand() % 30 + 30; i++)
				particle_manager->add_particle(new CSparkParticle(x + BALL_RADIUS, y + BALL_RADIUS, (float)((rand() % 800) - 400) / 100.0, (float)((rand() % 800) - 400) / 100.0, makecol(rand() % 128 + 128, 255, 0), rand() % 15 + 15, 3));
		}
	}


	// if I'm not dead, and I'm over level exit and I'm still, exit level
	if (over_exit_floor)
		return CBALL_EXIT_LEVEL;

	return CBALL_IS_FINE;	// not dead yet ;)
}
