#include <stdlib.h>
#include "main.h"
#include "game.h"


static int explosion_lvol = 0;
static int explosion_rvol = 0;

static void create_explosion(GAME *game, int x, int y, int phase)
{
	switch (game->level.map[y][x]) {
		case TILE_WALL:
		case TILE_WEAK_WALL:
		case TILE_DEFLECTOR_UP_RIGHT:
		case TILE_DEFLECTOR_DOWN_RIGHT:
		case TILE_MOUND:
			game->under[y][x] = TILE_BLANK;
			break;
		case TILE_EXPLOSION_BASE:
		case TILE_EXPLOSION_BASE + 1:
		case TILE_EXPLOSION_BASE + 2:
		case TILE_EXPLOSION_BASE + 3:
			break;
		case TILE_BOMB:
			game->under[y][x] = TILE_BLANK;
			/* fall through */
		case TILE_EXPLOSION_SOURCE_BASE:
		case TILE_EXPLOSION_SOURCE_BASE + 1:
		case TILE_EXPLOSION_SOURCE_BASE + 2:
		case TILE_EXPLOSION_SOURCE_BASE + 3:
			game->level.map[y][x] = TILE_EXPLOSION_SOURCE_BASE + phase;
			return;
		case TILE_BALL_LEFT:
		case TILE_BALL_RIGHT:
		case TILE_BALL_UP:
		case TILE_BALL_DOWN:
		case TILE_BALL_LEFT_STUCK:
		case TILE_BALL_RIGHT_STUCK:
		case TILE_BALL_UP_STUCK:
		case TILE_BALL_DOWN_STUCK:
			game->balls_left--;
			switch (game->under[y][x]) {
				case TILE_DEFLECTOR_UP_RIGHT:
				case TILE_DEFLECTOR_DOWN_RIGHT:
				case TILE_MOUND:
					game->under[y][x] = TILE_BLANK;
			}
			break;
		default:
			game->under[y][x] = game->level.map[y][x];
	}
	game->level.map[y][x] = TILE_EXPLOSION_BASE + phase;
	if (x == 0 || x == LEVEL_W-1 || y == 0 || y == LEVEL_H-1)
		game->under[y][x] = TILE_WALL;
	explosion_lvol += LEVEL_W*4 - 2 - x*4;
	explosion_rvol += 2 + x*4;
}

static void sound_explosions(void)
{
	if (explosion_lvol | explosion_rvol) {
		int vol = explosion_lvol + explosion_rvol;
		int pan = explosion_rvol * 255 / vol;
		play_sample(dat[SFX_EXPLOSION].dat, MIN(vol, 128), pan, 1000, 0);
		explosion_lvol = explosion_rvol = 0;
	}
}


static int ball_look_ahead(GAME *game, unsigned char *tile, unsigned char *under, int x, int y)
#define SFX_FLAGS_REFLECTED 1
#define SFX_FLAGS_DEFLECTED 2
{
	static const unsigned char ball_reverse[] = {
		TILE_BALL_RIGHT, TILE_BALL_LEFT, TILE_BALL_DOWN, TILE_BALL_UP
	};
	int xs, ys;
	int sfx_flags = 0;
	struct { unsigned char tile, under; } state[16];
	int n_states = 0;
	int i;
	int rv = 0;

	if (!TILE_IS_UNSTUCK_BALL(*tile)) return 0;

	repeat:

	if (n_states >= 16) {
		*tile = STICK_TILE(*tile);
		return 0;
	}

	for (i = 0; i < n_states; i++) {
		if (*tile == state[i].tile) {
			if (!under || *under == state[i].under) {
				*tile = STICK_TILE(*tile);
				return 0;
			}
		}
	}

	state[n_states].tile = *tile;
	if (under) state[n_states].under = *under;
	n_states++;

	if (under) {
		static const unsigned char deflect_up_right[] = {
			TILE_BALL_DOWN, TILE_BALL_UP, TILE_BALL_RIGHT, TILE_BALL_LEFT
		};
		static const unsigned char deflect_down_right[] = {
			TILE_BALL_UP, TILE_BALL_DOWN, TILE_BALL_LEFT, TILE_BALL_RIGHT
		};
		switch (*under) {
			case TILE_DEFLECTOR_UP_RIGHT:
				*under = TILE_DEFLECTOR_DOWN_RIGHT;
				*tile = deflect_up_right[TILE_RAW_DIR(*tile)];
				sfx_flags |= SFX_FLAGS_DEFLECTED;
				break;
			case TILE_DEFLECTOR_DOWN_RIGHT:
				*under = TILE_DEFLECTOR_UP_RIGHT;
				*tile = deflect_down_right[TILE_RAW_DIR(*tile)];
				sfx_flags |= SFX_FLAGS_DEFLECTED;
				break;
			case TILE_PIT:
				game->balls_left--;
				*tile = TILE_PIT;
				// create ball falling animation?
				play_sample(dat[SFX_BALL_IN_HOLE].dat, 128, (x * 256 + 128) / LEVEL_W, 1000, 0);
				return 1;
			case TILE_GOAL:
				game->balls_left--;
				game->goals_left--;
				*tile = TILE_FILLED_GOAL;
				play_sample(dat[SFX_BALL_IN_HOLE].dat, 128, (x * 256 + 128) / LEVEL_W, 1000, 0);
				return 1;
			case TILE_MOUND:
				/* Mounds will be considered later. */
				return 0;
		}
	}

	xs = x;
	ys = y;
	switch (*tile) {
		case TILE_BALL_LEFT:
			xs--;
			if (xs < 0) {
				create_explosion(game, x, y, 0);
				return 1;
			}
			break;
		case TILE_BALL_RIGHT:
			xs++;
			if (xs >= LEVEL_W) {
				create_explosion(game, x, y, 0);
				return 1;
			}
			break;
		case TILE_BALL_UP:
			ys--;
			if (ys < 0) {
				create_explosion(game, x, y, 0);
				return 1;
			}
			break;
		default: /* TILE_BALL_DOWN */
			ys++;
			if (ys >= LEVEL_H) {
				create_explosion(game, x, y, 0);
				return 1;
			}
			break;
	}
	/* xs,ys represents where the ball will move next. */

	switch (game->level.map[ys][xs]) {
		case TILE_WALL:
			*tile = ball_reverse[TILE_RAW_DIR(*tile)];
			sfx_flags |= SFX_FLAGS_REFLECTED;
			goto repeat;
		case TILE_WEAK_WALL:
			create_explosion(game, xs, ys, 0);
			*tile = ball_reverse[TILE_RAW_DIR(*tile)];
			n_states = 0;
			goto repeat;
		case TILE_BOMB:
			*tile = STICK_TILE(*tile);
			create_explosion(game, xs, ys, 0);
			break;
		case TILE_BALL_LEFT:
		case TILE_BALL_RIGHT:
		case TILE_BALL_UP:
		case TILE_BALL_DOWN:
		case TILE_BALL_LEFT_STUCK:
		case TILE_BALL_RIGHT_STUCK:
		case TILE_BALL_UP_STUCK:
		case TILE_BALL_DOWN_STUCK:
			create_explosion(game, xs, ys, 0);
			create_explosion(game, x, y, 0);
			rv = 1;
			break;
		/* default: nothing to do. Ball will roll. */
	}
	if (sfx_flags & SFX_FLAGS_REFLECTED) play_sample(dat[SFX_BALL_HIT_WALL].dat, 64, (x * 256 + 128) / LEVEL_W, 1000, 0);
	if (sfx_flags & SFX_FLAGS_DEFLECTED) play_sample(dat[SFX_DEFLECT_BALL].dat, 64, (x * 256 + 128) / LEVEL_W, 1000, 0);
	return rv;
}
#undef SFX_FLAGS_REFLECTED
#undef SFX_FLAGS_DEFLECTED


static void all_balls_look_ahead(GAME *game)
{
	int x, y;

	for (y = 0; y < LEVEL_H; y++)
		for (x = 0; x < LEVEL_W; x++)
			ball_look_ahead(game, &game->level.map[y][x], &game->under[y][x], x, y);

	{
		BALL **ballp = &game->ball;
		while (*ballp) {
			BALL *ball = *ballp;
			if (ball_look_ahead(game, &ball->dir, NULL, ball->x, ball->y)) {
				game->balls_left--;
				*ballp = ball->next;
				free(ball);
			} else
				ballp = &ball->next;
		}
	}

	for (y = 0; y < LEVEL_H; y++) {
		for (x = 0; x < LEVEL_W; x++) {
			int xs = x, ys = y;
			switch (game->level.map[y][x]) {
				case TILE_BALL_LEFT:  xs--; break;
				case TILE_BALL_RIGHT: xs++; break;
				case TILE_BALL_UP:    ys--; break;
				case TILE_BALL_DOWN:  ys++; break;
				default: continue;
			}
			/* At this point we know we're dealing with a ball at x,y. */
			/* xs,ys represents where the ball will move next. */

			if (game->under[y][x] == TILE_MOUND) {
				BALL **ballp = &game->ball;
				int blown_up = 0;
				/* Check there isn't a ball in the way */
				while (*ballp) {
					BALL *ball = *ballp;
					if (ball->x == x && ball->y == y) {
						game->balls_left--;
						if (!blown_up)
							create_explosion(game, x, y, 0);
						*ballp = ball->next;
						free(ball);
						blown_up = 1;
					} else if (ball->x == xs && ball->y == ys) {
						game->balls_left--;
						if (!blown_up)
							create_explosion(game, x, y, 0);
						create_explosion(game, xs, ys, 0);
						*ballp = ball->next;
						free(ball);
						blown_up = 1;
					} else
						ballp = &ball->next;
				}
				if (!blown_up) {
					BALL *ball = malloc(sizeof(*ball));
					if (!ball) break;
					ball->next = game->ball;
					game->ball = ball;
					ball->x = x;
					ball->y = y;
					ball->phase = 1;
					ball->dir = game->level.map[y][x];
					game->level.map[y][x] = game->under[y][x];
				}
				play_sample(dat[SFX_BALL_BOUNCE].dat, 128, (x * 256 + 128) / LEVEL_W, 1000, 0);
			}
		}
	}
}


/* Returns 1 if a landing sound should be made for flying balls. */
static int move_ball_to(GAME *game, unsigned char b, int x, int y)
{
	switch (game->level.map[y][x]) {
		case TILE_BLANK:
		case TILE_FILLED_GOAL:
		case TILE_MOUND:
		case TILE_DEFLECTOR_UP_RIGHT:
		case TILE_DEFLECTOR_DOWN_RIGHT:
			game->under[y][x] = game->level.map[y][x];
			game->level.map[y][x] = STICK_TILE(b);
			return 1;
		case TILE_PIT:
		case TILE_GOAL:
			game->under[y][x] = game->level.map[y][x];
			game->level.map[y][x] = STICK_TILE(b);
			return 0;
		default: /* assume it's another ball or an explosion */
			game->balls_left--;
			create_explosion(game, x, y, 0);
			return 0;
	}
}


static void move_balls(GAME *game)
{
	BALL **ballp;
	int x, y;

	for (y = 0; y < LEVEL_H; y++) {
		for (x = 0; x < LEVEL_W; x++) {
			switch (game->level.map[y][x]) {
				case TILE_BALL_LEFT:
					move_ball_to(game, game->level.map[y][x], x-1, y);
					game->level.map[y][x] = game->under[y][x];
					break;
				case TILE_BALL_RIGHT:
					move_ball_to(game, game->level.map[y][x], x+1, y);
					game->level.map[y][x] = game->under[y][x];
					break;
				case TILE_BALL_UP:
					move_ball_to(game, game->level.map[y][x], x, y-1);
					game->level.map[y][x] = game->under[y][x];
					break;
				case TILE_BALL_DOWN:
					move_ball_to(game, game->level.map[y][x], x, y+1);
					game->level.map[y][x] = game->under[y][x];
					break;
			}
		}
	}

	ballp = &game->ball;
	while (*ballp) {
		BALL *ball = *ballp;
		switch (ball->dir) {
			case TILE_BALL_LEFT:          ball->x--; break;
			case TILE_BALL_RIGHT:         ball->x++; break;
			case TILE_BALL_UP:            ball->y--; break;
			default: /* TILE_BALL_DOWN */ ball->y++; break;
		}
		if (ball->phase == 0) {
			if (move_ball_to(game, ball->dir, ball->x, ball->y))
				play_sample(dat[SFX_BALL_HIT_WALL].dat, 64, (ball->x * 256 + 128) / LEVEL_W, 1000, 0);
			*ballp = ball->next;
			free(ball);
		} else {
			ball->phase--;
			ballp = &ball->next;
		}
	}

	for (y = 0; y < LEVEL_H; y++)
		for (x = 0; x < LEVEL_W; x++)
			if (TILE_IS_STUCK(game->level.map[y][x]))
				game->level.map[y][x] = UNSTICK_TILE(game->level.map[y][x]);

	{
		BALL *ball;
		for (ball = game->ball; ball; ball = ball->next)
			if (TILE_IS_STUCK(ball->dir))
				ball->dir = UNSTICK_TILE(ball->dir);
	}
}


static void propagate_explosions(GAME *game, int phase)
{
	int x, y;

	/* Remove spent explosions. */
	for (y = 0; y < LEVEL_H; y++) {
		for (x = 0; x < LEVEL_W; x++) {
			if (game->level.map[y][x] == TILE_EXPLOSION_BASE + phase ||
			    game->level.map[y][x] == TILE_EXPLOSION_SOURCE_BASE + phase)
			{
				game->level.map[y][x] = game->under[y][x];
			}
		}
	}

	/* Propagate source explosions. */
	for (y = 1; y < LEVEL_H-1; y++) {
		for (x = 1; x < LEVEL_W-1; x++) {
			if (TILE_IS_SOURCE_EXPLOSION(game->level.map[y][x]) &&
				game->level.map[y][x] != TILE_EXPLOSION_SOURCE_BASE + phase)
			{
				game->level.map[y][x] = TILE_UNSOURCE_EXPLOSION(game->level.map[y][x]);
				create_explosion(game, x-1, y-1, phase);
				create_explosion(game, x  , y-1, phase);
				create_explosion(game, x+1, y-1, phase);
				create_explosion(game, x-1, y  , phase);
				create_explosion(game, x+1, y  , phase);
				create_explosion(game, x-1, y+1, phase);
				create_explosion(game, x  , y+1, phase);
				create_explosion(game, x+1, y+1, phase);
				/* Remove any airborne balls that were stuck when they hit a bomb */
				{
					BALL **ballp = &game->ball;
					while (*ballp) {
						BALL *ball = *ballp;
						if (TILE_IS_STUCK(ball->dir)) {
							if (ball->x >= x-1 && ball->x <= x+1 && ball->y >= y-1 && ball->y <= y+1) {
								game->balls_left--;
								*ballp = ball->next;
								free(ball);
								continue;
							}
						}
						ballp = &ball->next;
					}
				}
			}
		}
	}
}


void propagate_game(GAME *game)
{
	if (game->advance == 0)
		all_balls_look_ahead(game);

	game->advance++;
	if (game->advance == ADVANCE_MAX) {
		game->advance = 0;
		propagate_explosions(game, 0);
		move_balls(game);
	} else if ((game->advance & 3) == 0)
		propagate_explosions(game, game->advance >> 2);

	sound_explosions();
}
