#include "Level.h"

#include <stdbool.h>
#include <math.h>

#include "Game.h"
#include "LevelGen.h"
#include "Player.h"
#include "rovdefs.h"
#include "Save.h"
#include "System.h"
#include "World.h"

struct _LevelData Level;

#define LIGHTMAP_ARRAYLEN LEVEL_LIGHTMAP_SIZE * LEVEL_LIGHTMAP_SIZE * sizeof(luxel_t)

void Lvl_Setup(void)
{
	Level.tilemap = SDL_malloc(LEVEL_SIZE * LEVEL_SIZE);
	Level.vismap = SDL_malloc(LEVEL_SIZE * LEVEL_SIZE);
	Level.lightmap = SDL_malloc(LIGHTMAP_ARRAYLEN);
	Level.lightoverlay = SDL_malloc(LIGHTMAP_ARRAYLEN);
	Lvl_Reset();
}

void Lvl_Reset(void)
{
	SDL_memset(Level.tilemap, '.', LEVEL_SIZE * LEVEL_SIZE);
	SDL_memset(Level.vismap, 0, LEVEL_SIZE * LEVEL_SIZE);
	SDL_memset(Level.lightmap, 0, LIGHTMAP_ARRAYLEN);
	SDL_memset(Level.lightoverlay, 0, LIGHTMAP_ARRAYLEN);
	SDL_memset(Level.bossName, 0, 32);
	SDL_memset(Level.songName, 0, 32);
	SDL_memset(Level.enterName, 0, 64);
	Level.enterX = -1;
	Level.enterY = -1;
	Level.exitX = -1;
	Level.exitY = -1;
	Level.flags = LEVEL_HASLADDERUP | LEVEL_HASLADDERDOWN;
	Level.wallHeight = 1;
	Level.fogColour = 0xff000000;
	Level.tileset = 0;
	Level.boss = NULL;
	Level.bossColour = 0xffff0000;
	Level.skybox = NULL;
	Level.ambientParticleColour = 0;
	Level.ambientParticleSpeed = 0.0f;
}

void Lvl_Cleanup(void)
{
	SDL_free(Level.tilemap);
	SDL_free(Level.vismap);
	SDL_free(Level.lightmap);
	SDL_free(Level.lightoverlay);
}

void Lvl_FindLadders(void)
{
	int x, y;
	for (y = 0; y < LEVEL_SIZE; y++)
	{
		for (x = 0; x < LEVEL_SIZE; x++)
		{
			if (Level.tilemap[x + y * LEVEL_SIZE] == TILE_LADDER_DOWN)
			{
				Level.exitX = x;
				Level.exitY = y;
			}
			if (Level.tilemap[x + y * LEVEL_SIZE] == TILE_LADDER_UP)
			{
				Level.enterX = x;
				Level.enterY = y;
			}
		}
	}
}

void Lvl_LoadOrGenerate(int depth)
{
	SDL_memset(Level.vismap, 0, LEVEL_SIZE * LEVEL_SIZE);
	Level.boss = NULL;
	if (!Save_LoadLevel(depth))
	{
		struct SpawnedEntity* spawnedEntities;
		int numSpawnedEntities;

		numSpawnedEntities = LvlGen_Generate(depth, Level.exitX, Level.exitY, &spawnedEntities);

		struct SpawnedEntity* end = &spawnedEntities[numSpawnedEntities];
		for (struct SpawnedEntity* entity = spawnedEntities; entity != end; entity++) {
			Obj_t* spawned = World_NewObject(entity->type, entity->x, entity->y);
			spawned->z = entity->z;
			if (entity->isBoss) {
				Level.boss = spawned;
				spawned->team = ENTITY_TEAM_BOSS;
			}
		}
	}
	Lvl_ComputeLightmap();
	Audio_SetMusic(Level.songName);
}

struct RayHitResult Lvl_Raycast(float x, float y, float xdir, float ydir, bool revealMap)
{
	struct RayHitResult result;

	int tileX = (int)x;
	int tileY = (int)y;

	float sideDistX, sideDistY;

	float deltaDistX = Math_Abs(1.0f / xdir);
	float deltaDistY = Math_Abs(1.0f / ydir);

	int stepX, stepY;

	bool didHit = false;

	if (xdir < 0)
	{
		stepX = -1;
		sideDistX = (x - tileX) * deltaDistX;
	}
	else
	{
		stepX = 1;
		sideDistX = (tileX + 1.0f - x) * deltaDistX;
	}
	if (ydir < 0)
	{
		stepY = -1;
		sideDistY = (y - tileY) * deltaDistY;
	}
	else
	{
		stepY = 1;
		sideDistY = (tileY + 1.0f - y) * deltaDistY;
	}

	while (didHit == false)
	{
		if (sideDistX < sideDistY)
		{
			sideDistX += deltaDistX;
			tileX += stepX;
			result.hitSide = 0;
		}
		else
		{
			sideDistY += deltaDistY;
			tileY += stepY;
			result.hitSide = 1;
		}

		if (Lvl_IsWall(tileX, tileY))
			didHit = true;
		if (revealMap)
			Level.vismap[tileX + tileY * LEVEL_SIZE] = 2;
	}

	if (result.hitSide == 0) result.distance = (sideDistX - deltaDistX);
	else                     result.distance = (sideDistY - deltaDistY);

	result.xt = tileX;
	result.yt = tileY;

	return result;
}

int Lvl_IsWall(int x, int y)
{
	if (OUTOFBOUNDS(x, y))
		return 1;
	char tile = Level.tilemap[x + y * LEVEL_SIZE];
	return tile == TILE_BRICK || tile == TILE_STONE || tile == TILE_OUTOFBOUNDS || tile == TILE_DOOR_CLOSED;
}

int Lvl_IsDoor(int x, int y)
{
	if (OUTOFBOUNDS(x, y))
		return 0;
	return Level.tilemap[x + y * LEVEL_SIZE] == TILE_DOOR_OPEN || Level.tilemap[x + y * LEVEL_SIZE] == TILE_DOOR_CLOSED;
}

luxel_t Lvl_GetLuxel(int x, int y)
{
	if (x < 0 || x >= LEVEL_LIGHTMAP_SIZE || y < 0 || y >= LEVEL_LIGHTMAP_SIZE)
		return 0;
	luxel_t lx0 = Level.lightmap[x + y * LEVEL_LIGHTMAP_SIZE];
	luxel_t lx1 = Level.lightoverlay[x + y * LEVEL_LIGHTMAP_SIZE];
	return MATH_MIN(0xff, lx0 + lx1);
}

int Lvl_GetWallTex(int x, int y)
{
	if (OUTOFBOUNDS(x, y))
		return 0;
	switch (Level.tilemap[x + y * LEVEL_SIZE])
	{
	case TILE_STONE: return 0;
	case TILE_BRICK: return 1;
	case TILE_DOOR_CLOSED: return 2;
	default: return 24;
	}
}

int Lvl_GetFloorTex(int x, int y)
{
	if (OUTOFBOUNDS(x, y))
		return 0;
	switch (Level.tilemap[x + y * LEVEL_SIZE])
	{
	case TILE_GROUND: return 3;
	case TILE_FLOOR: return 4;
	case TILE_WATER: return 6;
	case TILE_DOOR_OPEN: return 3;
	case TILE_LADDER_DOWN: return 5;
	case TILE_LADDER_UP: return 3;
	case TILE_WOODPATH: return 7;
	case TILE_ICE: return 8;
	case TILE_HURT: return 9;
	case TILE_LAVA: return 10;
	default: return 24;
	}
}

int Lvl_GetCeilingTex(int x, int y)
{
	if (OUTOFBOUNDS(x, y))
		return 0;
	switch (Level.tilemap[x + y * LEVEL_SIZE])
	{
	case TILE_GROUND: return 3;
	case TILE_FLOOR: return 4;
	case TILE_WATER: return 3;
	case TILE_DOOR_OPEN: return 3;
	case TILE_LADDER_DOWN: return 3;
	case TILE_LADDER_UP: return 5;
	case TILE_WOODPATH: return 3;
	case TILE_ICE: return 3;
	case TILE_HURT: return 3;
	case TILE_LAVA: return 3;
	default: return 12;
	}
}


int Lvl_HasLineOfSight(float xsrc, float ysrc, float xdst, float ydst)
{
	float xd = xdst - xsrc;
	float yd = ydst - ysrc;

	float distToTarget = Math_Sqrt(xd * xd + yd * yd);
	if (distToTarget == 0.0f)
	{
		return 1;
	}

	struct RayHitResult hitResult = Lvl_Raycast(xsrc, ysrc, xd / distToTarget, yd / distToTarget, false);
	return (hitResult.distance >= Math_Abs(distToTarget));
}

#define LIGHT_STATIC_FALLOFF 6
#define LIGHT_DYNAMIC_FALLOFF 8

/*
 * Optimization:
 * This could be stored in a precomputed lookup table
 */
static luxel_t ExpRamp(luxel_t in)
{
	const float curve = 0.05;
	const float x = (float)((255 - in) / 255.0f);
	float fl = Math_Pow(curve, x) - curve * x;
	return (luxel_t)(fl * 255.0f);
}

static void RaycastLight(int xc, int yc, int decay, luxel_t *lightmap)
{
	const int rays = 512;
	int a;

	for (a = 0; a < rays; a++)
	{
		luxel_t val = 0xff;
		float angle = 2.0f * MATH_PI * (a / (float)rays);
		float dx = Math_Sin(angle);
		float dy = Math_Cos(angle);
		float xx = (float)xc;
		float yy = (float)yc;

		int xt, yt;
		int xi, yi;
		do
		{
			xx += dx;
			yy += dy;
			xt = (int)xx / LEVEL_LIGHTMAP_SCALE;
			yt = (int)yy / LEVEL_LIGHTMAP_SCALE;

			if ((int)val - decay < 0)
				break;
			val -= decay;

			xi = (int)xx & (LEVEL_LIGHTMAP_SIZE - 1);
			yi = (int)yy & (LEVEL_LIGHTMAP_SIZE - 1);

			luxel_t last = lightmap[xi + yi * LEVEL_LIGHTMAP_SIZE];
			lightmap[xi + yi * LEVEL_LIGHTMAP_SIZE] = MATH_MAX(val, last);
		} while (!Lvl_IsWall(xt, yt));
	}
}

void Lvl_ComputeLightmap(void)
{
	int tileCenterX, tileCenterY;
	SDL_memset(Level.lightmap, 0, LIGHTMAP_ARRAYLEN);
	
	for (sprite_t *sprite = World_Sprites; sprite != World_Sprites + WORLD_MAX_ENTITIES; sprite++)
	{
		if (sprite->id >= 0 && sprite->light == SPRITE_LIGHT_STATIC)
		{
			RaycastLight(TOFLOAT(sprite->x) * LEVEL_LIGHTMAP_SCALE, TOFLOAT(sprite->y) * LEVEL_LIGHTMAP_SCALE, LIGHT_STATIC_FALLOFF, Level.lightmap);
		}
	}

	if (Game.depth > 1 && Level.flags & LEVEL_HASLADDERUP)
	{
		tileCenterX = Level.enterX * LEVEL_LIGHTMAP_SCALE + (LEVEL_LIGHTMAP_SCALE / 2);
		tileCenterY = Level.enterY * LEVEL_LIGHTMAP_SCALE + (LEVEL_LIGHTMAP_SCALE / 2);
		RaycastLight(tileCenterX, tileCenterY, LIGHT_STATIC_FALLOFF, Level.lightmap);
	}
	if (!Level.boss && Level.flags & LEVEL_HASLADDERDOWN)
	{
		tileCenterX = Level.exitX * LEVEL_LIGHTMAP_SCALE + (LEVEL_LIGHTMAP_SCALE / 2);
		tileCenterY = Level.exitY * LEVEL_LIGHTMAP_SCALE + (LEVEL_LIGHTMAP_SCALE / 2);
		RaycastLight(tileCenterX, tileCenterY, LIGHT_STATIC_FALLOFF, Level.lightmap);
	}
}

void Lvl_ComputeLightOverlay(void)
{
	SDL_memset(Level.lightoverlay, 0, LIGHTMAP_ARRAYLEN);
	for (sprite_t *sprite = World_Sprites; sprite != World_Sprites + WORLD_MAX_ENTITIES; sprite++)
	{
		if (sprite->id >= 0 && sprite->light == SPRITE_LIGHT_DYNAMIC)
		{
			RaycastLight(TOFLOAT(sprite->x) * LEVEL_LIGHTMAP_SCALE, TOFLOAT(sprite->y) * LEVEL_LIGHTMAP_SCALE, LIGHT_DYNAMIC_FALLOFF, Level.lightoverlay);
		}
	}
}
