#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glut.h>

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

#include "draw.h"
#include "level.h"
#include "objects.h"

// test a particular square to see if we can move there next turn

static int test_square(Level *level, LevelObject *src_obj, int x, int y)
{
	if (x < 0 || y < 0 || x >= level->w || y >= level->h)
		return 0;
	
	if (level->data[y * level->w + x] == SQUARE_WALL)
		return 0;

	return 1;
}

// ghost

struct ghost_data {
	GLfloat r, g, b;
	int flyhome;
	int scaretime;
	int start_x, start_y;
};

#define GHOST_SLOW_SPEED 14
#define GHOST_NORMAL_SPEED 10

#define GHOST_VSECTIONS 10
#define GHOST_HSECTIONS 10

static void ghost_collision(Level *level, LevelObject *player,
			    LevelObject *ghost)
{
	struct ghost_data *data = (struct ghost_data *) ghost->extradata;

	if (data->scaretime > 0) {
		data->flyhome = 1;
		data->scaretime = 0;
		ghost->speed = 30;
		ghost->target_x = data->start_x;
		ghost->target_y = data->start_y;
	} else {
		// die
		level->gameover = 1;
	}
}

static void ghost_draw(Level *level, LevelObject *obj)
{
	struct ghost_data *extradata = (struct ghost_data *) obj->extradata;
	
	glRotatef(90, 1, 0, 0);
	glRotatef(180, 0, 1, 0);

	if (extradata->scaretime > 0)
		glColor3f(0.5, 0.5, 0.5);
	else
		glColor3f(extradata->r, extradata->g, extradata->b);
	glScalef(0.4, 0.9, 0.4);
	glTranslatef(0.0, 0.5, 0.0);
	
	glPushMatrix();

	if (!extradata->flyhome)
	{
		glRotatef(level->time * 6.32, 0.0, 1.0, 0.0);
		glPushMatrix();

		{
			// top dome
		
			glTranslatef(0.0, 0.375, 0.0);
			glScalef(1.0, 0.25, 1.0);
			draw_hemisphere(GHOST_HSECTIONS, GHOST_VSECTIONS);
		}
		
		glReloadMatrix();

		{
			glTranslatef(0.0, 0, 0.0);
			glScalef(1.0, 0.75, 1.0);
	
			draw_ring_frill(GHOST_HSECTIONS);
		}

		glPopMatrix();
	}

	glReloadMatrix();
	
	{
		// eyes
	
		glRotatef(90, 0.0, 0.0, 1.0);
		glTranslatef(0.3, 0.9, 0.0);
		
		glPushMatrix();

		{
			glTranslatef(0.0, 0.0, 0.2);
			glScalef(0.1, 0.2, 0.2);
			glScalef(1.3, 1.3, 1.3);
			glColor3f(1.0, 1.0, 1.0);
			
			draw_hemisphere(20, 6);

			glTranslatef(0.0, 0.8, 0.0);
			glScalef(0.5, 0.5, 0.5);
			glColor3f(0, 0, 0);

			draw_hemisphere(20, 6); 
		}

		glReloadMatrix();

		{
			glTranslatef(0.0, 0.0, -0.2);
			glScalef(0.1, 0.2, 0.2);
			glColor3f(1.0, 1.0, 1.0);
			
			draw_hemisphere(20, 6);

			glTranslatef(0.0, 0.8, 0.0);
			glScalef(0.5, 0.5, 0.5);
			glColor3f(0, 0, 0);

			draw_hemisphere(20, 6); 
		}
		
		glPopMatrix();
	}
	
	glPopMatrix();
}

// convert a movedir into coords

static inline void movedir_to_xy(int src_x, int src_y, int movedir,
				 int *x, int *y)
{
	if (movedir != -1)
		movedir %= 360;
	
	switch (movedir) {
	case 0:
		*x = src_x + 1;
		*y = src_y;
		return;
	case 90:
		*x = src_x;
		*y = src_y + 1;
		return;
	case 180:
		*x = src_x - 1;
		*y = src_y;
		return;
	case 270:
		*x = src_x;
		*y = src_y - 1;
		return;
	default:
	case -1:
		*x = src_x;
		*y = src_y;
		return;
	}		
}

static int ghost_testsquare(Level *level, LevelObject *src_obj,
			    int x, int y)
{
	LevelObject *obj;
	
	if (!test_square(level, src_obj, x, y))
		return 0;
	
	for (obj=level->objects; obj; obj = obj->next) {
		if (obj == src_obj)
			continue;
		if (obj->type != OBJECT_PLAYER) {
			if ((obj->x == x && obj->y == y)
	       	         || (obj->target_x == x && obj->target_y == y))
				return 0;
		}
	}

	return 1;
}

// find a new direction to move

static int ghost_get_movedir(Level *level, LevelObject *obj)
{
	int movedirs[4];
	int nmovedirs = 0;
	int can_keep_moving = 0, can_go_backwards = 1;
	int i;

	// find all directions we can move

	for (i=0; i<4; ++i) {
		int x, y;

		movedir_to_xy(obj->x, obj->y, i*90, &x, &y);

		if (ghost_testsquare(level, obj, x, y)) {
			// dont go backwards if possible
			// if there is nowhere else to go, save it
			// seperately

			if (i * 90 == (obj->angle + 180) % 360)
				can_go_backwards = 1;
			else
				movedirs[nmovedirs++] = i*90;

			// is this the direction we are moving now?
			
			if (obj->angle == i * 90)
				can_keep_moving = 1;
		}
	}

	if (nmovedirs == 0) {
		// nowhere to go
		// if we are really stuck, try going backwards

		if (can_go_backwards)
			return (obj->angle + 180) % 360;
		else
			return -1;
	}
	
	// if we can keep moving the direction we're going, go that
	// way with a bias

	if (can_keep_moving && rand() % 2 == 0)
		return obj->angle;
	
	// pick a random direction
	
	return movedirs[rand() % nmovedirs];
}

static void ghost_move(Level *level, LevelObject *obj)
{
	struct ghost_data *data = (struct ghost_data *) obj->extradata;

	if (obj->type == OBJECT_GHOST) {
		data->flyhome = 0;

		if (data->scaretime)
			data->scaretime--;
	
		if (data->scaretime)
			obj->speed = GHOST_SLOW_SPEED;
		else
			obj->speed = GHOST_NORMAL_SPEED;
		
		// check for player at same position
		
		if ((obj->x == level->player->x && obj->y == level->player->y)
		 || (obj->x == level->player->target_x && obj->y == level->player->target_y)) {
			ghost_collision(level, level->player, obj);
			return;
		}
	}
	
	obj->angle = ghost_get_movedir(level, obj);
	
	movedir_to_xy(obj->x, obj->y, obj->angle,
		      &obj->target_x, &obj->target_y);
}

static void ghost_destructor(LevelObject *obj)
{
	free(obj->extradata);
}

static int ghost_cycle = 0;

LevelObject *ghost_new(Level *level, int x, int y)
{
	LevelObject *obj = malloc(sizeof(LevelObject));
	struct ghost_data *extradata;
	
	obj->type = OBJECT_GHOST;
	
	obj->x = obj->target_x = x;
	obj->y = obj->target_y = y;
	obj->angle = -1;
	obj->draw = ghost_draw;
	obj->move = ghost_move;
	obj->destructor = ghost_destructor;
	obj->extradata = extradata = malloc(sizeof(struct ghost_data));
	obj->countdown = 0;
	obj->speed = GHOST_NORMAL_SPEED;
	
	extradata->start_x = x;
	extradata->start_y = y;
	extradata->flyhome = 0;
	
	if (ghost_cycle == 0)
		ghost_cycle = 1;

	extradata->r = ghost_cycle & 1 ? 1.0 : 0.0;
	extradata->g = ghost_cycle & 2 ? 1.0 : 0.0;
	extradata->b = ghost_cycle & 4 ? 1.0 : 0.0;
	extradata->scaretime = 0;
	
	ghost_cycle = (ghost_cycle + 1) % 7;
	
	// hook into level
	
	obj->next = level->objects;
	level->objects = obj;

	return obj;
}

// scare all ghosts in a level

static void ghosts_scare(Level *level)
{
	LevelObject *obj;

	for (obj=level->objects; obj; obj = obj->next) {
		if (obj->type == OBJECT_GHOST) {
			struct ghost_data *data
				= (struct ghost_data *) obj->extradata;

			data->scaretime = 30;
		}
	}
}

static inline void drawsphere_point(GLfloat x, GLfloat y, GLfloat z)
{
        glNormal3f(x, y, z);
        glVertex3f(x, y, z);
}

// pacman

struct player_data {
	GLfloat size;
	int movedir;
	int start_x, start_y;
};

#define PLAYER_NORMAL_SPEED 8
#define PLAYER_EATING_SPEED 10

#define PACMAN_VSECTIONS 15
#define PACMAN_HSECTIONS 10

static void player_draw_mouth(int v)
{
	int h;
	GLfloat angle1, cosangle1, sinangle1;

	angle1 = (2 * v * M_PI) / PACMAN_VSECTIONS;
	
	cosangle1 = cos(angle1); sinangle1 = sin(angle1);
	
	// draw the mouth

	glBegin(GL_POLYGON);
	
	for (h=0; h<=PACMAN_HSECTIONS; ++h) {
		GLfloat hangle = (h * M_PI) / PACMAN_HSECTIONS;
		GLfloat coshangle = cos(hangle);
		GLfloat sinhangle = sin(hangle);

		drawsphere_point(coshangle,
			   sinhangle * cosangle1,
			   sinhangle * sinangle1);
	}

	glEnd();
}

static void player_draw_body(int mouthsize)
{
	int v, h;

	for (v=0; v<PACMAN_VSECTIONS; ++v) {
		float angle1, angle2;
		float sinangle1, cosangle1, sinangle2, cosangle2;
		
		if (v >= (PACMAN_VSECTIONS - mouthsize) / 2
		 && v < (PACMAN_VSECTIONS + mouthsize) / 2)
			continue;
		
		angle1 = (2 * M_PI * v) / PACMAN_VSECTIONS;
		angle2 = (2 * M_PI * (v+1)) / PACMAN_VSECTIONS;
		
		sinangle1 = sin(angle1); cosangle1 = cos(angle1);
		sinangle2 = sin(angle2); cosangle2 = cos(angle2);
		
		glBegin(GL_QUAD_STRIP);
		
		for (h=0; h<=PACMAN_HSECTIONS; ++h) {
			GLfloat hangle = (h * M_PI) / PACMAN_HSECTIONS;
			GLfloat coshangle = cos(hangle);
			GLfloat sinhangle = sin(hangle);
			
			drawsphere_point(coshangle,
					 sinhangle * cosangle1,
					 sinhangle * sinangle1);
			drawsphere_point(coshangle,
					 sinhangle * cosangle2,
					 sinhangle * sinangle2);
		}
		
		glEnd();
	}

	glColor3f(1, 0, 0);

	player_draw_mouth((PACMAN_VSECTIONS + mouthsize) / 2);
	player_draw_mouth((PACMAN_VSECTIONS - mouthsize) / 2);
}

void player_draw(Level *level, LevelObject *obj)
{
	float time_frac = 1 - ((float) obj->countdown) / obj->speed;
	struct player_data *extradata = (struct player_data *) obj->extradata;
	int mouthsize;
	
//	glScalef(0.4, 0.4, 0.4);
	glScalef(extradata->size, extradata->size, extradata->size);
	glRotatef(90, 0, 0, 1);
//	glRotatef(45, -1, 0, 0);
	glColor3f(1, 1, 0);
	glTranslatef(0, 0, 1.0);

	if (time_frac < 0.2)
		mouthsize = 0;
	else if (time_frac < 0.5)
		mouthsize = 1;
	else if (time_frac < 0.7)
		mouthsize = 2;
	else
		mouthsize = 1;
	
	player_draw_body(mouthsize);
}

static void player_move(Level *level, LevelObject *obj)
{
	LevelSquare *sq = &level->data[obj->y * level->w + obj->x];
	struct player_data *extradata = (struct player_data *) obj->extradata;
	
	switch (*sq) {
	case SQUARE_DOT:
		// score++;
		--level->dots_remaining;
		if (level->dots_remaining <= 0) {
			level->gameover = 1;
			level->draw_fireworks = 1;
		}
		*sq = SQUARE_EMPTY;
		obj->speed = PLAYER_EATING_SPEED;
		break;
	case SQUARE_PILL:
		ghosts_scare(level);
		*sq = SQUARE_EMPTY;
		obj->speed = PLAYER_EATING_SPEED;
		break;
	default:
		obj->speed = PLAYER_NORMAL_SPEED;
	}

	obj->angle = (obj->angle + extradata->movedir) % 360;

	while (obj->angle < 0)
		obj->angle += 360;

	// in tour mode, use ghost ai to move around
	
	if (level->tourmode) {
		ghost_move(level, obj);
		return;
	}		
	
	extradata->movedir = 0;

	{
		int x, y;

		movedir_to_xy(obj->x, obj->y, obj->angle, &x, &y);

		if (test_square(level, obj, x, y)) {
			obj->target_x = x;
			obj->target_y = y;
		}
	}
}

static void player_destructor(LevelObject *obj)
{
	free(obj->extradata);
}

LevelObject *player_new(Level *level, int x, int y)
{
	LevelObject *obj = malloc(sizeof(LevelObject));
	struct player_data *extradata;
	
	if (level->player) {
		fprintf(stderr, "player_new: more than one player!\n");
		exit(-1);
	}

	level->player = obj;

	obj->type = OBJECT_PLAYER;
	obj->x = obj->target_x = x;
	obj->y = obj->target_y = y;
	obj->angle = 0;
	obj->draw = player_draw;
	obj->move = player_move;
	obj->destructor = player_destructor;
	obj->extradata = extradata = malloc(sizeof(struct player_data));
	obj->countdown = 0;
	obj->speed = PLAYER_NORMAL_SPEED;
	
	extradata->size = 0.4;
	extradata->movedir = 0;
	
	// hook in
	
	obj->next = level->objects;
	level->objects = obj;

	return obj;
}

void player_set_movedir(LevelObject *obj, int angle)
{
	struct player_data *data = (struct player_data *) obj->extradata;

	data->movedir = angle;
}
