/*
------------------------------------------------------------
	Fixed Rate Pig - a fixed logic frame rate demo
------------------------------------------------------------
 * Copyright (C) 2004 David Olofson <david@olofson.net>
 *
 * This software is released under the terms of the GPL.
 *
 * Contact author for permission if you want to use this
 * software, or work derived from it, under other terms.
 */

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include "engine.h"


/* Graphics defines */
#define	SCREEN_W	800
#define	SCREEN_H	600
#define	TILE_W		32
#define	TILE_H		32
#define	MAP_W		25
#define	MAP_H		17
#define	FONT_SPACING	45
#define	PIG_FRAMES	12

/* World/physics constants */
#define	GRAV_ACC	4
#define	JUMP_SPEED	28

/* Sprite collision groups */
#define	GROUP_ENEMY	0x0001
#define	GROUP_POWERUP	0x0002

typedef enum
{
	POWER_LIFE,
	POWER_BONUS1,
	POWER_BONUS2
} POWERUPS;


typedef struct GAMESTATE
{
	/* I/O */
	PIG_engine	*pe;
	Uint8		*keys;
	int		nice;
	int		refresh_screen;
	int		jump;

	/* Sprites */
	int		lifepig;
	int		scorefont;
	int		glassfont;
	int		icons;
	int		stars;
	int		pigframes;
	int		evil;
	int		slime;

	/* Global game state */
	int		running;
	int		level;
	int		lives;
	float		lives_wobble;
	float		lives_wobble_time;
	int		score;
	float		score_wobble;
	float		score_wobble_time;
	float		dashboard_time;
	int		fun_count;
	int		enemycount;
	int		messages;

	/* Objects */
	PIG_object	*player;

	/* Statistics */
	int		logic_frames;
	int		rendered_frames;
} GAMESTATE;


static void add_life(GAMESTATE *gs);
static void remove_life(GAMESTATE *gs);
static void inc_score(GAMESTATE *gs, int v);
static void inc_score_nobonus(GAMESTATE *gs, int v);
static PIG_object *new_player(GAMESTATE *gs);
static void message(GAMESTATE *gs, const char *s);
static PIG_object *new_powerup(GAMESTATE *gs,
		int x, int y, int speed, POWERUPS type);
static PIG_object *new_star(GAMESTATE *gs, int x, int y, int vx, int vy);
static PIG_object *new_evil(GAMESTATE *gs,
		int x, int y, int speed);
static PIG_object *new_slime(GAMESTATE *gs,
		int x, int y, int speed);


/*----------------------------------------------------------
	Init, load stuff etc
----------------------------------------------------------*/

static int load_level(GAMESTATE *gs, int map)
{
	const char *m;
	const char *k;
	if(map > 4)
		map = 1;
	gs->level = map;
	pig_object_close_all(gs->pe);
	gs->enemycount = 0;
	gs->messages = 0;
	switch(map)
	{
	  case 1:
	  case 2:
	  case 4:
	  	k =	"abcd" "efgh" "ijkl"	/* Red, green, yellov */
			"0123456789ABCDEFG"	/* Sky */
			"xyz";			/* Single R, G, Y */
		break;
	  case 0:
	  case 3:
	  	k =	"abcd" "efgh" "ijkl"	/* Red, green, yellov */
			"................."
			"xyz"			/* Single R, G, Y */
			"-+012345..ABCDEF";	/* Night sky */
		break;
	}
	switch(map)
	{
	  case 0: m =	"-------------ad----------"
			"-abcd-x-ad--ad-abcd-acd--"
			"-x----x--abcd--x----x--x-"
			"-abd--x---ad---abd--x--x-"
			"-x----x--abcd--x----x--x-"
			"-x----x-ad--ad-abcd-abd--"
			"----efhad-eh--egh-efh----"
			"----y--y-y--y--y--y------"
			"++++efh++efgh++y++eh+++++"
			"0123y50y2y45y12y45y123450"
			"ABCDyFAyCyEFyBCyEFeghDEFA"
			"----ijkjl-ijkl--ijkjl----"
			"----il--il-il--il--------"
			"----ijkjl--il--il-ikl----"
			"----il-----il--il--il----"
			"----il----ijkl--ijkjl----"
			"-------------------------";
			break;
	  case 1: m =	"0000000000000000000000000"
			"1111111111111111111111111"
			"2222222222222222222222222"
			"3333333333333333333333333"
			"4444444444444444444444444"
			"5555555555555555555555555"
			"6666666666666666666666666"
			"7777777ijkjkjjkjkl7777777"
			"8888888888888888888888888"
			"9999999999999999999999999"
			"abcdAAAAAAAAAAAAAAAAAabcd"
			"BBBBBBBBBBBBBBBBBBBBBBBBB"
			"CCCCCCCCCCCCCCCCCCCCCCCCC"
			"efgfgffgfgfgfgfggffgfgfgh"
			"EEEEEEEEEEEEEEEEEEEEEEEEE"
			"FFFFFFFFFFFFFFFFFFFFFFFFF"
			"GGGGGGGGGGGGGGGGGGGGGGGGG";
			new_evil(gs, 2, 0, 5);
			new_evil(gs, 22, 0, 5);
			new_evil(gs, 5, 0, 7);
			new_evil(gs, 19, 0, 7);
			break;
	  case 2: m =	"0000000000000000000000000"
			"1111111111111111111111111"
			"2222222222222222222222222"
			"3333333333333333333333333"
			"4444444444xxxxx4444444444"
			"5555555555x555x5555555555"
			"6666666666x666x6666666666"
			"7777777xxxx777xxxx7777777"
			"8888888x888888888x8888888"
			"9999999x999999999x9999999"
			"AAAAAAAxxxxAAAxxxxAAAAAAA"
			"BBBBBBBBBBxBBBxBBBBBBBBBB"
			"CCCCCCCCCCxCCCxCCCCCCCCCC"
			"DDDDDDDDDDxxxxxDDDDDDDDDD"
			"EEEEEEEEEEEEEEEEEEEEEEEEE"
			"ijklFFFFFFFFFFFFFFFFFijkl"
			"GGGijlGilGilGilGilGiklGGG";
			new_slime(gs, 2, 0, -5);
			new_slime(gs, 22, 0, 5);
			new_evil(gs, 8, 0, 7);
			new_evil(gs, 16, 0, -7);
			break;
	  case 3: m =	"-------------------------"
			"-------------------------"
			"-------------------------"
			"-------------------------"
			"ijkl----------efgh-------"
			"-------------------------"
			"-------------------------"
			"z----------------abcbcbbd"
			"+++++++++++++++++++++++++"
			"01z3450123450123450123450"
			"ABCDEFABCefgfgfghFABCDEFA"
			"----z--------------------"
			"-------------------------"
			"------z--------------ijkl"
			"-------------------------"
			"-------------------------"
			"abdefghijkl---efghijklabd";
			new_slime(gs, 5, 0, -5);
			new_slime(gs, 20, 15, -5);
			new_evil(gs, 1, 0, 7);
			new_evil(gs, 20, 0, 10);
			new_evil(gs, 15, 0, 7);
			break;
	  case 4: m =	"0000000000000000000000000"
			"1111111111111111111111111"
			"2222222222222222222222222"
			"3333333333333333333333333"
			"4444444444444444444444444"
			"555555555555z555555555555"
			"66666666666ijl66666666666"
			"7777777777ijlil7777777777"
			"888888888ijlikkl888888888"
			"99999999ijkjklikl99999999"
			"AAAAAAAikjlijkjkjlAAAAAAA"
			"BBBBBBiklijkjlijkjlBBBBBB"
			"CCCCCijkjlikkjklikklCCCCC"
			"DDDDijklijjklikjkjkklDDDD"
			"EEEijkkjkjlikjkjlijjklEEE"
			"FFijkjlilijkjklikjlikklFF"
			"efggfggfgfgfggfgfgfgfgfgh";
			new_evil(gs, 11, 0, 5);
			new_evil(gs, 10, 0, 6);
			new_evil(gs, 9, 0, 7);
			new_evil(gs, 8, 0, 8);
			new_evil(gs, 7, 0, 9);
			new_evil(gs, 6, 0, 10);
			new_evil(gs, 5, 0, 11);
			new_evil(gs, 4, 0, 12);
			new_evil(gs, 3, 0, 13);
			new_slime(gs, 1, 0, 16);
			new_slime(gs, 24, 0, -14);
			break;
	  default:
		return -1;
	}
	pig_map_from_string(gs->pe->map, k, m);
	gs->refresh_screen = gs->pe->pages;
	return 0;
}


static GAMESTATE *init_all(SDL_Surface *screen)
{
	int i;
	PIG_map *pm;
	GAMESTATE *gs = (GAMESTATE *)calloc(1, sizeof(GAMESTATE));
	if(!gs)
		return NULL;

	gs->running = 1;

	gs->pe = pig_open(screen);
	if(!gs->pe)
	{
		fprintf(stderr, "Could not open the Pig Engine!\n");
		free(gs);
		return NULL;
	}
	gs->pe->userdata = gs;

	pig_viewport(gs->pe, 0, 0, SCREEN_W, MAP_H * TILE_H);

	i = gs->lifepig = pig_sprites(gs->pe, "lifepig.png", 0, 0);
	i |= gs->scorefont = pig_sprites(gs->pe, "font.png", 44, 56);
	i |= gs->glassfont = pig_sprites(gs->pe, "glassfont.png", 60, 60);
	i |= gs->icons = pig_sprites(gs->pe, "icons.png", 48, 48);
	i |= gs->stars = pig_sprites(gs->pe, "stars.png", 32, 32);
	i |= gs->pigframes = pig_sprites(gs->pe, "pigframes.png", 64, 48);
	i |= gs->evil = pig_sprites(gs->pe, "evil.png", 48, 48);
	i |= gs->slime = pig_sprites(gs->pe, "slime.png", 48, 48);
	if(i < 0)
	{
		fprintf(stderr, "Could not load graphics!\n");
		pig_close(gs->pe);
		free(gs);
		return NULL;
	}
	for(i = gs->icons; i < gs->icons + 3*8; ++i)
		pig_hotspot(gs->pe, i, PIG_CENTER, 45);
	for(i = gs->pigframes; i < gs->pigframes + 12; ++i)
		pig_hotspot(gs->pe, i, PIG_CENTER, 43);
	for(i = gs->evil; i < gs->evil + 16; ++i)
		pig_hotspot(gs->pe, i, PIG_CENTER, 46);
	for(i = gs->slime; i < gs->slime + 16; ++i)
		pig_hotspot(gs->pe, i, PIG_CENTER, 46);

	pm = pig_map_open(gs->pe, MAP_W, MAP_H);
	if(!pm)
	{
		fprintf(stderr, "Could not create map!\n");
		pig_close(gs->pe);
		free(gs);
		return NULL;
	}
	if(pig_map_tiles(pm, "tiles.png", TILE_W, TILE_H) < 0)
	{
		fprintf(stderr, "Could not load background graphics!\n");
		pig_close(gs->pe);
		free(gs);
		return NULL;
	}

	/* Mark tiles for collision detection */
	pig_map_collisions(pm, 0, 12, PIG_ALL);	/* Red, green, yellov */
	pig_map_collisions(pm, 12, 17, PIG_NONE);/* Sky */
	pig_map_collisions(pm, 29, 3, PIG_ALL);	/* Single R, G, Y */

	load_level(gs, 0);
	return gs;
}


/*----------------------------------------------------------
	Render the dashboard
----------------------------------------------------------*/
static void dashboard(GAMESTATE *gs)
{
	SDL_Rect r;
	int i, v;
	float x;
	float t = SDL_GetTicks() * 0.001;
	r.x = 0;
	r.y = SCREEN_H - 56;
	r.w = SCREEN_W;
	r.h = 56;
	SDL_SetClipRect(gs->pe->surface, &r);

	/* Render "plasma bar" */
	for(i = 0; i < 56; ++i)
	{
		float f1, f2, m;
		SDL_Rect cr;
		cr.x = 0;
		cr.w = SCREEN_W;
		cr.y = SCREEN_H - 56 + i;
		cr.h = 1;
		f1 = .25 + .25 * sin(t * 1.7 + (float)i / SCREEN_H * 42);
		f1 += .25 + .25 * sin(-t * 2.1 + (float)i / SCREEN_H * 66);
		f2 = .25 + .25 * sin(t * 3.31 + (float)i / SCREEN_H * 90);
		f2 += .25 + .25 * sin(-t * 1.1 + (float)i / SCREEN_H * 154);
		m = sin((float)i * M_PI / 56.0);
		m = sin(m * M_PI * 0.5);
		m = sin(m * M_PI * 0.5);
		SDL_FillRect(gs->pe->surface,
				&cr, SDL_MapRGB(gs->pe->surface->format,
				((int)128.0 * f1 + 64) * m,
				((int)64.0 * f1 * f2 + 64) * m,
				((int)128.0 * f2 + 32) * m
				));
	}

	/* Draw pigs... uh, lives! */
	x = -10;
	for(i = 0; i < gs->lives; ++i)
	{
		x += 48 + gs->lives_wobble *
				sin(gs->lives_wobble_time * 12) * .2;
		pig_draw_sprite(gs->pe, gs->lifepig,
				(int)x + gs->lives_wobble *
				sin(gs->lives_wobble_time * 20 + i * 1.7),
				SCREEN_H - 56/2);
	}

	/* Print score */
	x = SCREEN_W + 5;
	v = gs->score;
	for(i = 9; i >= 0; --i)
	{
		int n = v % 10;
		x -= 39 - gs->score_wobble *
				sin(gs->score_wobble_time * 15 + i * .5);
		pig_draw_sprite(gs->pe, gs->scorefont + n, (int)x,
				SCREEN_H - 56/2);
		v /= 10;
		if(!v)
			break;
	}

	pig_dirty(gs->pe, &r);
}


/*----------------------------------------------------------
	Game logic event handlers
----------------------------------------------------------*/
static void before_objects(PIG_engine *pe)
{
	GAMESTATE *gs = (GAMESTATE *)pe->userdata;
	if(gs->lives_wobble > 0)
	{
		gs->lives_wobble *= 0.95;
		gs->lives_wobble -= 0.3;
		if(gs->lives_wobble < 0)
			gs->lives_wobble = 0;
	}
	if(gs->score_wobble > 0)
	{
		gs->score_wobble *= 0.95;
		gs->score_wobble -= 0.3;
		if(gs->score_wobble < 0)
			gs->score_wobble = 0;
	}
	++gs->logic_frames;

	if(0 == gs->level)
	{
		switch(gs->fun_count % 60)
		{
		  case 17:
			new_powerup(gs, 250, -20, -10, POWER_LIFE);
			break;
		  case 29:
			new_powerup(gs, 550, -20, 10, POWER_LIFE);
			break;
		  case 37:
			new_powerup(gs, 250, -20, 10, POWER_BONUS2);
			break;
		  case 51:
			new_powerup(gs, 550, -20, -10, POWER_BONUS1);
			break;
		}
		if(150 == gs->fun_count % 300)
			message(gs, "Press Space!");
		++gs->fun_count;
	}
}


typedef enum
{
	WAITING,
	WALKING,
	FALLING,
	KNOCKED,
	NEXT_LEVEL,
	DEAD
} OBJECT_states;


static void player_handler(PIG_object *po, const PIG_event *ev)
{
	GAMESTATE *gs = (GAMESTATE *)po->owner->userdata;
	switch(ev->type)
	{
	  case PIG_PREFRAME:
		switch(po->state)
		{
		  case WAITING:
			if(1 == po->age)
				message(gs, "Get ready!");
			else if(po->age > 50)
				po->state = FALLING;
			break;
		  case WALKING:
			if(gs->keys[SDLK_LEFT])
			{
				po->ax = -(20 + po->vx) * .4;
				po->target = 3 + po->age % 4 - 1;
				if(5 == po->target)
					po->target = 3;
			}
			else if(gs->keys[SDLK_RIGHT])
			{
				po->ax = (20 - po->vx) * .4;
				po->target = 9 + po->age % 4 - 1;
				if(11 == po->target)
					po->target = 9;
			}
			else
			{
				po->ax = -po->vx * .8;
				if(po->target >= 6)
					po->target = (po->target + 1) %
							PIG_FRAMES;
				else if(po->target)
					--po->target;
			}
			break;
		  case FALLING:
			if(gs->keys[SDLK_LEFT])
				po->ax = -(20 + po->vx) * .2;
			else if(gs->keys[SDLK_RIGHT])
				po->ax = (20 - po->vx) * .2;
			else
				po->ax = -po->vx * .2;
			po->target = (po->target + 1) % PIG_FRAMES;
			break;
		}
		po->timer[0] = 1;
		break;
	  case PIG_TIMER0:
		if(po->x < 0)
			po->x = 0;
		else if(po->x > po->owner->view.w - 1)
			po->x = po->owner->view.w - 1;
		switch(po->state)
		{
		  case WALKING:
			if(po->power)
				--po->power;
			po->image = po->target % PIG_FRAMES;
			if(!pig_test_map(gs->pe, po->x, po->y + 1))
			{
				po->state = FALLING;
				po->ay = GRAV_ACC;
			}
			if(gs->jump || gs->keys[SDLK_UP])
			{
				po->ay = 0;
				po->vy = -JUMP_SPEED;
				po->state = FALLING;
				gs->jump = 0;
			}
			break;
		  case FALLING:
			if(po->vy > 2)
				po->power = 3;
			po->ay = GRAV_ACC;
			po->image = po->target;
			break;
		  case KNOCKED:
			po->power = 0;
			po->ay = GRAV_ACC;
			po->target = (po->target + 2) % PIG_FRAMES;
			po->image = po->target;
			po->ax = -po->vx * .2;
			break;
		  case NEXT_LEVEL:
			po->vx = (SCREEN_W / 2 - po->x) * .1;
			po->target = (po->target + 1) % PIG_FRAMES;
			po->image = po->target;
			break;
		  case DEAD:
			po->ax = po->ay = 0;
			po->vx = po->vy = 0;
			break;
		}
		if(gs->jump)
			--gs->jump;
		if(NEXT_LEVEL != po->state)
		{
			if(gs->enemycount <= 0)
			{
				message(gs, "Well Done!");
				po->state = NEXT_LEVEL;
				po->vy = 0;
				po->ay = -1;
				po->tilemask = 0;
				po->hitgroup = 0;
				po->timer[2] = 50;
			}
		}
		break;

	  case PIG_TIMER1:
		/* Snap out of KNOCKED mode */
		po->state = FALLING;
		break;

	  case PIG_TIMER2:
		switch(po->state)
		{
		  case NEXT_LEVEL:
			add_life(gs);
			pig_object_close(po);
			load_level(gs, gs->level + 1);
			new_player(gs);
			break;
		  default:
			pig_object_close(po);
			if(!new_player(gs))
				load_level(gs, 0);
			break;
		}
		break;

	  case PIG_HIT_TILE:
		if(KNOCKED == po->state)
			break;

		if(ev->cinfo.sides & PIG_TOP)
		{
			po->y = ev->cinfo.y;
			po->vy = 0;
			po->ay = 0;
		}
		po->state = WALKING;
		break;

	  case PIG_HIT_OBJECT:
		if(KNOCKED == po->state)
			break;

		switch(ev->obj->hitgroup)
		{
		  case GROUP_ENEMY:
			if((po->power && ev->cinfo.sides & PIG_TOP) ||
					(po->vy - ev->obj->vy) >= 15)
			{
				/* Win: Stomp! */
				inc_score(gs, ev->obj->score);
				ev->obj->y = ev->cinfo.y + 10;
				if(po->vy > 0)
					ev->obj->vy = po->vy;
				else
					ev->obj->vy = 10;
				ev->obj->ay = GRAV_ACC;
				ev->obj->tilemask = 0;
				ev->obj->hitgroup = 0;
				if(gs->jump || gs->keys[SDLK_UP])
				{
					/* Mega jump! */
					po->vy = -(JUMP_SPEED + 7);
					gs->jump = 0;
				}
				else
				{
					/* Bounce a little */
					po->vy = -15;
				}
				po->y = ev->cinfo.y;
				po->ay = 0;
				po->state = FALLING;
			}
			else
			{
				/* Lose: Knocked! */
				po->vy = -15;
				po->ay = GRAV_ACC;
				po->state = KNOCKED;
				po->timer[1] = 11;
				new_star(gs, po->x, po->y - 20, -5, 3);
				new_star(gs, po->x, po->y - 20, 2, -6);
				new_star(gs, po->x, po->y - 20, 4, 4);
			}
			break;
		  case GROUP_POWERUP:
			switch(ev->obj->score)
			{
			  case POWER_LIFE:
				add_life(gs);
				message(gs, "Extra Life!");
				break;
			  case POWER_BONUS1:
				/* Double or 100k bonus! */
				if(gs->score < 100000)
				{
					inc_score_nobonus(gs, gs->score);
					message(gs, "Double Score!");
				}
				else
				{
					inc_score_nobonus(gs, 100000);
					message(gs, "100 000!");
				}
				break;
			  case POWER_BONUS2:
				inc_score_nobonus(gs, 1000);
				message(gs, "1000!");
				break;
			}
			ev->obj->state = DEAD;
			ev->obj->tilemask = 0;
			ev->obj->hitgroup = 0;
			ev->obj->vy = -20;
			ev->obj->ay = -2;
			break;
		}
		break;
	  case PIG_OFFSCREEN:
		/*
		 * Dead pigs don't care about being off-screen.
		 * A timer is used to remove them, and to continue
		 * the game with a new life.
		 */
		if(DEAD == po->state)
			break;
		if(po->y < 0)	/* Above the playfield is ok. */
			break;
		if(gs->lives)
			message(gs, "Oiiiiiiink!!!");
		else
			message(gs, "Game Over!");
		po->state = DEAD;
		po->timer[2] = 50;
	  default:
		break;
	}
}


static void powerup_handler(PIG_object *po, const PIG_event *ev)
{
	GAMESTATE *gs = (GAMESTATE *)po->owner->userdata;
	switch(ev->type)
	{
	  case PIG_PREFRAME:
		if(DEAD == po->state)
			break;
		po->ax = (po->target - po->vx) * .3;
		po->ay = GRAV_ACC;
		po->image = po->age % 8;
		++po->power;
		break;
	  case PIG_HIT_TILE:
		if(DEAD == po->state)
			break;
		if(po->power > 2)
			po->target = -po->target;
		po->power = 0;
		po->vy = 0;
		po->ay = 0;
		po->x = ev->cinfo.x + po->vx;
		po->y = ev->cinfo.y;
		break;
	  case PIG_OFFSCREEN:
		if(po->y > SCREEN_H || (po->y < -100))
		{
			pig_object_close(po);
			--gs->enemycount;
		}
	  default:
		break;
	}
}


static void star_handler(PIG_object *po, const PIG_event *ev)
{
	switch(ev->type)
	{
	  case PIG_PREFRAME:
		if(po->age >= 8)
			pig_object_close(po);
		else
			po->image = po->age;
	  default:
		break;
	}
}


static void evil_handler(PIG_object *po, const PIG_event *ev)
{
	GAMESTATE *gs = (GAMESTATE *)po->owner->userdata;
	int look_x;
	switch(ev->type)
	{
	  case PIG_PREFRAME:
		if(DEAD == po->state)
			break;
		po->ax = (po->target - po->vx) * .5;
		po->ay = GRAV_ACC;
		po->image = po->age % 16;
		break;
	  case PIG_HIT_TILE:
		if(DEAD == po->state)
			break;
		po->vy = 0;
		po->ay = 0;
		po->x = ev->cinfo.x + po->vx;
		po->y = ev->cinfo.y;
		break;
	  case PIG_OFFSCREEN:
		if(po->y > SCREEN_H)
		{
			pig_object_close(po);
			--gs->enemycount;
		}
		break;
	  case PIG_POSTFRAME:
		if(DEAD == po->state)
			break;
		look_x = 10 + fabs(po->vx * 2);
		if(po->target < 0)
			look_x = -look_x;
		if(!pig_test_map(po->owner, po->x + look_x, po->y + 1))
			po->target = -po->target;
	  default:
		break;
	}
}


static void slime_handler(PIG_object *po, const PIG_event *ev)
{
	GAMESTATE *gs = (GAMESTATE *)po->owner->userdata;
	int look_x;
	switch(ev->type)
	{
	  case PIG_PREFRAME:
		if(DEAD == po->state)
			break;
		po->ax = (po->target - po->vx) * .2;
		po->ay = GRAV_ACC;
		po->image = po->age % 16;
		break;
	  case PIG_HIT_TILE:
		po->vy = -(JUMP_SPEED + GRAV_ACC);
		po->ay = 0;
		po->y = ev->cinfo.y;
		break;
	  case PIG_OFFSCREEN:
		if(po->y > SCREEN_H)
		{
			pig_object_close(po);
			--gs->enemycount;
		}
		break;
	  case PIG_POSTFRAME:
		if(DEAD == po->state)
			break;
		/* Don't bother looking if we're close to a floor. */
		if(pig_test_map_vector(po->owner,
				po->x, po->y,
				po->x, po->y + 48,
				PIG_TOP, NULL))
			break;
		/* Turn around if there's no floor! */
		look_x = 10 + fabs(po->vx * 4);
		if(po->target < 0)
			look_x = -look_x;
		if(!pig_test_map_vector(po->owner,
				po->x + look_x, po->y,
				po->x + look_x, SCREEN_H,
				PIG_TOP, NULL))
			po->target = -po->target;
	  default:
		break;
	}
}


static void chain_head_handler(PIG_object *po, const PIG_event *ev)
{
	GAMESTATE *gs = (GAMESTATE *)po->owner->userdata;
	switch(ev->type)
	{
	  case PIG_PREFRAME:
		po->vx = (po->target - po->x) * .3;
		po->vy = 15 * cos(po->age * .3) - 9;
		if((gs->messages > 1) && (1 == po->state))
			po->timer[1] = 0;
		if(po->timer[1])
			break;
	  case PIG_TIMER1:
		switch(po->state)
		{
		  case 0:
			po->timer[1] = 35;
			++po->state;
			break;
		  case 1:
			po->target = -SCREEN_W;
			po->timer[1] = 50;
			++po->state;
			if(gs->messages > 0)
				--gs->messages;
			break;
		  case 2:
			pig_object_close(po);
			break;
		}
	  default:
		break;
	}
}

static void chain_link_handler(PIG_object *po, const PIG_event *ev)
{
	PIG_object *target = pig_object_find(po, po->target);
	switch(ev->type)
	{
	  case PIG_PREFRAME:
		if(target)
		{
			po->vx = ((target->x + FONT_SPACING) - po->x) * .6;
			po->vy = (target->y - po->y) * .6 - 9;
		}
		else
			pig_object_close(po);
	  default:
		break;
	}
}


/*----------------------------------------------------------
	Accounting (score, lives etc)
----------------------------------------------------------*/

static void add_life(GAMESTATE *gs)
{
	++gs->lives;
	gs->lives_wobble += 10;
	if(gs->lives_wobble > 15)
		gs->lives_wobble = 15;
	gs->lives_wobble_time = 0;
}


static void remove_life(GAMESTATE *gs)
{
	--gs->lives;
	gs->lives_wobble += 10;
	if(gs->lives_wobble > 15)
		gs->lives_wobble = 15;
	gs->lives_wobble_time = 0;
}


static void inc_score_nobonus(GAMESTATE *gs, int v)
{
	int os = gs->score;
	gs->score += v;
	while(v)
	{
		gs->score_wobble += 1;
		v /= 10;
	}
	if(gs->score_wobble > 15)
		gs->score_wobble = 15;
	gs->score_wobble_time = 0;
	if(os / 10000 != gs->score / 10000)
		new_powerup(gs, SCREEN_W / 2, -20, -4, POWER_LIFE);
}

static void inc_score(GAMESTATE *gs, int v)
{
	int os = gs->score;
	inc_score_nobonus(gs, v);
	if(os / 5000 != gs->score / 5000)
		new_powerup(gs, SCREEN_W / 2, -20, 8, POWER_BONUS1);
	else if(os / 1000 != gs->score / 1000)
		new_powerup(gs, SCREEN_W / 2, -20, -6, POWER_BONUS2);
}


static PIG_object *new_player(GAMESTATE *gs)
{
	PIG_object *po;
	if(!gs->lives)
		return NULL;

	po = pig_object_open(gs->pe, SCREEN_W / 2, -50, 1);
	if(!po)
		return NULL;

	remove_life(gs);
	po->ibase = gs->pigframes;
	po->handler = player_handler;
	po->hitmask = GROUP_POWERUP | GROUP_ENEMY;
	return po;
}


static PIG_object *new_powerup(GAMESTATE *gs,
		int x, int y, int speed, POWERUPS type)
{
	PIG_object *po = pig_object_open(gs->pe, x, y, 1);
	if(!po)
		return NULL;

	++gs->enemycount;
	po->score = type;
	po->ibase = gs->icons + 8 * po->score;
	po->target = speed;
	po->handler = powerup_handler;
	po->tilemask = PIG_TOP;
	po->hitgroup = GROUP_POWERUP;
	return po;
}


static PIG_object *new_star(GAMESTATE *gs, int x, int y, int vx, int vy)
{
	PIG_object *po = pig_object_open(gs->pe, x + vx, y + vy, 1);
	if(!po)
		return NULL;

	po->ibase = gs->stars;
	po->ax = -vx * 0.3;
	po->vx = vx * 3;
	po->ay = -vy * 0.3;
	po->vy = vy * 3;
	po->handler = star_handler;
	return po;
}


static PIG_object *new_evil(GAMESTATE *gs,
		int x, int y, int speed)
{
	PIG_object *po = pig_object_open(gs->pe,
			x * TILE_W, y * TILE_H, 1);
	if(!po)
		return NULL;

	++gs->enemycount;
	po->ibase = gs->evil;
	po->target = speed;
	po->handler = evil_handler;
	po->score = 200;
	po->tilemask = PIG_TOP;
	po->hitgroup = GROUP_ENEMY;
	return po;
}


static PIG_object *new_slime(GAMESTATE *gs,
		int x, int y, int speed)
{
	PIG_object *po = pig_object_open(gs->pe,
			x * TILE_W, y * TILE_H, 1);
	if(!po)
		return NULL;

	++gs->enemycount;
	po->ibase = gs->slime;
	po->target = speed;
	po->handler = slime_handler;
	po->score = 300;
	po->tilemask = PIG_TOP;
	po->hitgroup = GROUP_ENEMY;
	return po;
}


static PIG_object *new_chain_head(GAMESTATE *gs,
		int x, int y, int image, int target_x)
{
	PIG_object *po = pig_object_open(gs->pe, x, y, 1);
	if(!po)
		return NULL;

	po->ibase = image;
	po->handler = chain_head_handler;
	po->target = target_x;
	return po;
}


static PIG_object *new_chain_link(GAMESTATE *gs,
		int x, int y, int image, int target)
{
	PIG_object *po = pig_object_open(gs->pe, x, y, 1);
	if(!po)
		return NULL;

	po->ibase = image;
	po->handler = chain_link_handler;
	po->target = target;
	return po;
}


static void message(GAMESTATE *gs, const char *s)
{
	int i = 0;
	const int x = SCREEN_W + FONT_SPACING;
	const int y = MAP_H * TILE_H - 30;
	int tx = (SCREEN_W - ((signed)strlen(s) - 1) * FONT_SPACING) / 2;
	PIG_object *po = NULL;
	while(s[i])
	{
		int c = toupper(s[i]) - 32 + gs->glassfont;
		if(0 == i)
			po = new_chain_head(gs, x, y, c, tx);
		else
			po = new_chain_link(gs, x, y, c, po->id);
		if(!po)
			return;
		++i;
	}
	++gs->messages;
}


static int start_game(GAMESTATE *gs)
{
	if(0 != gs->level)
		return 0;	/* Already playing! --> */

	gs->score = 0;
	gs->lives = 5;

	if(load_level(gs, 1) < 0)
		return -1;

	gs->player = new_player(gs);
	if(!gs->player)
		return -1;

	return 0;
}


/*----------------------------------------------------------
	Input; events and game control keys
----------------------------------------------------------*/
static void handle_input(GAMESTATE *gs, SDL_Event *ev)
{
	switch(ev->type)
	{
	  case SDL_MOUSEBUTTONUP:
		break;
	  case SDL_KEYDOWN:
		switch(ev->key.keysym.sym)
		{
		  case SDLK_UP:
			gs->jump = 3;
			break;
		  case SDLK_F1:
			gs->pe->interpolation = !gs->pe->interpolation;
			if(gs->pe->interpolation)
				message(gs, "Interpolation: ON");
			else
				message(gs, "Interpolation: OFF");
			break;
		  case SDLK_F2:
			gs->pe->direct = !gs->pe->direct;
			if(gs->pe->direct)
				message(gs, "Rendering: Direct");
			else
				message(gs, "Rendering: Buffered");
			break;
		  case SDLK_F3:
			gs->pe->show_dirtyrects = !gs->pe->show_dirtyrects;
			if(gs->pe->show_dirtyrects)
				message(gs, "Dirtyrects: ON");
			else
				message(gs, "Dirtyrects: OFF");
			break;
		  case SDLK_F4:
			gs->nice = !gs->nice;
			if(gs->nice)
				message(gs, "Be Nice: ON");
			else
				message(gs, "Be Nice: OFF");
			break;
		  case SDLK_SPACE:
			start_game(gs);
		  default:
			break;
		}
		break;
	  case SDL_KEYUP:
		switch(ev->key.keysym.sym)
		{
		  case SDLK_ESCAPE:
			gs->running = 0;
		  default:
			break;
		}
		break;
	  case SDL_QUIT:
		gs->running = 0;
		break;
	}
}


static void handle_keys(GAMESTATE *gs)
{
}


static int break_received = 0;

#ifndef	RETSIGTYPE
#define	RETSIGTYPE void
#endif
static RETSIGTYPE breakhandler(int sig)
{
	/* For platforms that drop the handlers on the first signal... */
	signal(SIGTERM, breakhandler);
	signal(SIGINT, breakhandler);
	break_received = 1;
#if (RETSIGTYPE != void)
	return 0;
#endif
}


/*----------------------------------------------------------
	main()
----------------------------------------------------------*/
int main(int argc, char* argv[])
{
	SDL_Surface *screen;
	GAMESTATE *gs;
	int i;
	int bpp = 0;
	int last_tick, start_time, end_time;
	int dashframe;
	float logic_fps = 20.0;
	int flags = SDL_DOUBLEBUF | SDL_HWSURFACE;

	SDL_Init(SDL_INIT_VIDEO);
	atexit(SDL_Quit);
	signal(SIGTERM, breakhandler);
	signal(SIGINT, breakhandler);

	for(i = 1; i < argc; ++i)
	{
		if(strncmp(argv[i], "-s", 2) == 0)
			flags &= ~SDL_DOUBLEBUF;
		else if(strncmp(argv[i], "-f", 2) == 0)
			flags |= SDL_FULLSCREEN;
		else
			bpp = atoi(&argv[i][1]);
	}

	screen = SDL_SetVideoMode(SCREEN_W, SCREEN_H, bpp, flags);
	if(!screen)
	{
		fprintf(stderr, "Failed to open screen!\n");
		return 1;
	}

	SDL_WM_SetCaption("Fixed Rate Pig", "Pig");
	SDL_ShowCursor(0);

	gs = init_all(screen);
	if(!gs)
		return 1;

	gs->keys = SDL_GetKeyState(&i);

	gs->logic_frames = 0;
	gs->rendered_frames = 0;
	gs->pe->before_objects = before_objects;

	pig_start(gs->pe, 0);
	gs->refresh_screen = gs->pe->pages;
	start_time = last_tick = SDL_GetTicks();
	while(gs->running)
	{
		int tick;
		float frames, dt;
		SDL_Event ev;

		/* Handle input */
		while(SDL_PollEvent(&ev) > 0)
			handle_input(gs, &ev);
		handle_keys(gs);
		if(break_received)
			gs->running = 0;

		/* Calculate time since last update */
		tick = SDL_GetTicks();
		dt = (tick - last_tick) * 0.001;
		frames = dt * logic_fps;

		/* Run the game logic */
		pig_animate(gs->pe, frames);

		/*
		 * Limit the dashboard frame rate to 15 fps
		 * when there's no wobbling going on.
		 *
		 * The 'dashframe' deal is about keeping the
		 * pages in sync on a double buffered display.
		 */
		if(gs->lives_wobble || gs->score_wobble ||
				(gs->dashboard_time > 1.0/15.0))
		{
			dashframe = gs->pe->pages;
			gs->dashboard_time = 0;
		}
		if(dashframe)
		{
			--dashframe;
			dashboard(gs);
		}

		/* Update sprites */
		if(gs->refresh_screen)
		{
			--gs->refresh_screen;
			pig_refresh_all(gs->pe);
		}
		else
			pig_refresh(gs->pe);

		/* Make the new frame visible */
		pig_flip(gs->pe);

		/* Update statistics, timers and stuff */
		++gs->rendered_frames;
		gs->lives_wobble_time += dt;
		gs->score_wobble_time += dt;
		gs->dashboard_time += dt;

		last_tick = tick;
		if(gs->nice)
			SDL_Delay(10);
	}

	/* Print some statistics */
	end_time = SDL_GetTicks();
	i = end_time - start_time;
	printf("          Total time running: %d ms\n", i);
	if(!i)
		i = 1;
	printf("Average rendering frame rate: %.2f fps\n",
			gs->rendered_frames * 1000.0 / i);
	printf("    Average logic frame rate: %.2f fps\n",
			gs->logic_frames * 1000.0 / i);

	pig_close(gs->pe);
	return 0;
}