#include "Render.h"

#include <SDL.h>

#include "ExMath.h"
#include "Game.h"
#include "Inventory.h"
#include "Level.h"
#include "Particle.h"
#include "Log.h"
#include "World.h"

#define GFX_SPRITESIZE 32
#define GFX_WALLTEXSIZE 32
#define GFX_FLOORTEXSIZE 32

static Bmp_t *framebuffer = NULL;
static Bmp_t viewport;

// Since this game should support multiple resolutions, the size of zbuf must be defined at runtime
static float *zbuf = NULL;

static Bmp_t texCompass;
static Bmp_t texTiles;
static Bmp_t texStatModIcons;
static Bmp_t texBarBase;
static Bmp_t texBarBaseInv;
static Bmp_t texBarHealth;
static Bmp_t texBarMana;
static Bmp_t texBossBase;
static Bmp_t texBossBar;

static void Gfx_SetupFire(int width, int height);
static void Gfx_CleanupFire(void);

void Gfx_InitSpriteCache(void);
void Gfx_DestroySpriteCache(void);

extern void Gui_LoadButtonGraphicsRemoveThisFunctionLaterPleaseAndThankYou(const RPKFile *rpk);

void Gfx_Init(const RPKFile *rpak, Bmp_t *fb)
{
	framebuffer = fb;

	viewport = Bmp_Create(fb->w, fb->h);
	zbuf = SDL_malloc(viewport.w * viewport.h * sizeof(float));

	texCompass = Bmp_CreateFromPAKEntry(rpak, "CompassDial");
	texTiles = Bmp_CreateFromPAKEntry(rpak, "Tiles");
	texStatModIcons = Bmp_CreateFromPAKEntry(rpak, "StatModIcons");
	texBarBase = Bmp_CreateFromPAKEntry(rpak, "bar_base");
	texBarBaseInv = Bmp_CreateFromPAKEntry(rpak, "bar_base_inv");
	texBarHealth = Bmp_CreateFromPAKEntry(rpak, "bar_hp");
	texBarMana = Bmp_CreateFromPAKEntry(rpak, "bar_mana");
	texBossBase = Bmp_CreateFromPAKEntry(rpak, "bossbar_base");
	texBossBar = Bmp_CreateFromPAKEntry(rpak, "bossbar");

	Gfx_SetupFire(fb->w, fb->h);

	Gfx_InitSpriteCache();
	Gui_LoadButtonGraphicsRemoveThisFunctionLaterPleaseAndThankYou(rpak);
}

void Gfx_Cleanup(void)
{
	SDL_free(zbuf);

	Bmp_Destroy(&viewport);

	Bmp_Destroy(&texTiles);
	Bmp_Destroy(&texCompass);
	Bmp_Destroy(&texStatModIcons);
	Bmp_Destroy(&texBarBase);
	Bmp_Destroy(&texBarBaseInv);
	Bmp_Destroy(&texBarHealth);
	Bmp_Destroy(&texBarMana);
	Bmp_Destroy(&texBossBase);
	Bmp_Destroy(&texBossBar);

	Gfx_DestroySpriteCache();
	Gfx_CleanupFire();
}

void Gfx_Resize(int width, int height)
{
	SDL_assert(framebuffer->w == width && framebuffer->h == height);

	Bmp_Resize(&viewport, framebuffer->w, framebuffer->h);

	SDL_free(zbuf);
	zbuf = SDL_malloc(viewport.w * viewport.h * sizeof(float));

	Gfx_CleanupFire();
	Gfx_SetupFire(width, height);
}

void Gfx_ClearViewport(void)
{
	int i;
	for (i = 0; i < viewport.w * viewport.h; i++)
		zbuf[i] = 9999.0f;
}

static float xCam, yCam, zCam;
static float rCam, rSin, rCos;
static float planeX, planeY;
static float fov;

void Gfx_SetCameraPos(float x, float y, float z, float r)
{
	fov = (viewport.w / 2.0f) / viewport.h;

	xCam = x;
	yCam = y;
	zCam = z;
	rCam = r;

	rSin = Math_Sin(rCam);
	rCos = Math_Cos(rCam);
	planeX = Math_Sin(rCam - MATH_HALFPI) * fov;
	planeY = Math_Cos(rCam - MATH_HALFPI) * fov;
}

static bmcol_t Blend(bmcol_t cola, bmcol_t colb, uint8_t alpha)
{
	uint32_t rb1 = (alpha * (cola & 0xff00ff)) >> 8;
	uint32_t rb2 = ((0xff - alpha) * (colb & 0xff00ff)) >> 8;
	uint32_t g1 = (alpha * (cola & 0x00ff00)) >> 8;
	uint32_t g2 = ((0xff - alpha) * (colb & 0x00ff00)) >> 8;
	return ((rb1 + rb2) & 0xff00ff) + ((g1 + g2) & 0x00ff00) | 0xff000000;
}

static bmcol_t Multiply(bmcol_t col, uint8_t brightness)
{
	uint8_t a = col & 0xff000000;
	uint8_t r = (col >> 16) & 0xff;
	uint8_t g = (col >> 8) & 0xff;
	uint8_t b = (col >> 0) & 0xff;

	r = (r * brightness) / 255;
	g = (g * brightness) / 255;
	b = (b * brightness) / 255;

	return a | (r << 16) | (g << 8) | b;
}

static int ShadePixel(bmcol_t col, float zval, luxel_t luxel, int x, int y)
{
	float fogDepth = (float)(Level.fogDepth) * (36.0f / 255.0f);
	int afog = 255 - (int)(zval * fogDepth);
	int lbr = Level.ambientBrightness;
	if (afog < 0)
		afog = 0;
	luxel = (luxel * (255 - lbr)) / 255 + lbr;
	return Blend(Blend(col, 0xff000000, luxel), Level.fogColour, afog);
}

#define WALLTEXSIZE 32

void Gfx_DrawWalls(void)
{
	int x, y;

	int wh = Level.wallHeight;
	if (wh < 0)
		wh = -wh;

	for (x = 0; x < viewport.w; x++)
	{
		float cameraX = 2.0f * x / (float)viewport.w - 1.0f;

		float rayDirX = rSin + planeX * cameraX;
		float rayDirY = rCos + planeY * cameraX;

		struct RayHitResult raycast = Lvl_Raycast(xCam, yCam, rayDirX, rayDirY, true);

		int lineHeight = (int)(viewport.h / raycast.distance) * wh + 4;
		float heightOffs = ((zCam - 0.5f * (wh - 1)) * viewport.h) / raycast.distance;

		int drawStart = -lineHeight / 2 + viewport.h / 2 + (int)heightOffs;
		if (drawStart < 0)
			drawStart = 0;

		int drawEnd = lineHeight / 2 + viewport.h / 2 + (int)heightOffs;
		if (drawEnd >= viewport.h)
			drawEnd = viewport.h;

		float wallX;
		if (raycast.hitSide == 0) wallX = yCam + raycast.distance * rayDirY;
		else                      wallX = xCam + raycast.distance * rayDirX;

		wallX -= Math_Floor(wallX);

		int texX = (int)((1 - wallX) * (float)(WALLTEXSIZE)) % WALLTEXSIZE;
		if (raycast.hitSide == 0 && rayDirX > 0) texX = WALLTEXSIZE - texX - 1;
		if (raycast.hitSide == 1 && rayDirY < 0) texX = WALLTEXSIZE - texX - 1;

		int luxX = (int)(wallX * (float)(LEVEL_LIGHTMAP_SCALE));

		float step = 1.0f * WALLTEXSIZE / (lineHeight / wh);
		float texPos = (drawStart - heightOffs - viewport.h / 2 + (lineHeight / wh) / 2) * step;

		int lxx, lyy;
		if (raycast.hitSide == 0)
		{
			if (rayDirX > 0)
			{
				lxx = (raycast.xt * LEVEL_LIGHTMAP_SCALE) - 1;
				lyy = (raycast.yt * LEVEL_LIGHTMAP_SCALE) + luxX;
			}
			else
			{
				lxx = ((raycast.xt + 1) * LEVEL_LIGHTMAP_SCALE);
				lyy = (raycast.yt * LEVEL_LIGHTMAP_SCALE) + luxX;
			}
		}
		else
		{
			if (rayDirY < 0)
			{
				lxx = (raycast.xt * LEVEL_LIGHTMAP_SCALE) + luxX;
				lyy = ((raycast.yt + 1) * LEVEL_LIGHTMAP_SCALE);
			}
			else
			{
				lxx = (raycast.xt * LEVEL_LIGHTMAP_SCALE) + luxX;
				lyy = (raycast.yt * LEVEL_LIGHTMAP_SCALE) - 1;
			}
		}
		luxel_t lux = Lvl_GetLuxel(lxx, lyy);
		for (y = drawStart; y < drawEnd; y++)
		{
			int texY = (int)texPos & (WALLTEXSIZE - 1);
			int txx = texX + Lvl_GetWallTex(raycast.xt, raycast.yt) * WALLTEXSIZE;
			int tyy = texY + Level.tileset * WALLTEXSIZE;
			txx %= texTiles.w;
			tyy %= texTiles.h;

			texPos += step;
			bmcol_t col = texTiles.pixels[txx + tyy * texTiles.w];
			if (raycast.hitSide == 1)
				col = (col >> 1) & 0xff7f7f7f;
			viewport.pixels[x + y * viewport.w] = ShadePixel(col, raycast.distance, lux, x, y);
			zbuf[x + y * viewport.w] = raycast.distance;
		}
	}
}

#define FLOORTEXS_PER_ROW 11
void Gfx_DrawFloor(void)
{
	int x, y;

	float xCenter = viewport.w / 2.0f;
	float yCenter = viewport.h / 2.0f;

	int wh = Level.wallHeight;
	int hasCeil = wh > 0;
	if (!hasCeil)
		wh = -wh;

	for (y = 0; y < viewport.h; y++)
	{
		float yd = (yCenter - y) / (float)viewport.h;

		float zd;
		if (yd < 0)
			zd = (0.5f + zCam) / -yd; // Floor
		else
		{
			if (!hasCeil)
				continue;
			zd = (0.5f - zCam + (wh - 1)) / yd; // Ceiling
		}

		for (x = 0; x < viewport.w; x++)
		{
			if (zbuf[x + y * viewport.w] < 9999.0f)
				continue;
			
			float xd = (xCenter - x) / viewport.h;
			xd *= zd;

			float xx = xd * rCos + zd * rSin + xCam;
			float yy = zd * rCos - xd * rSin + yCam;

			int xp = (int)(xx);
			int yp = (int)(yy);

			int xt = (xx * GFX_FLOORTEXSIZE);
			int yt = (yy * GFX_FLOORTEXSIZE);

			int lmx = (int)(xx * LEVEL_LIGHTMAP_SCALE);
			int lmy = (int)(yy * LEVEL_LIGHTMAP_SCALE);
			luxel_t lux = Lvl_GetLuxel(lmx, lmy);

			int tex;
			if (yd < 0)
				tex = Lvl_GetFloorTex(xp, yp);
			else
				tex = Lvl_GetCeilingTex(xp, yp);
			int xa = (xt & (GFX_FLOORTEXSIZE - 1)) + (tex % FLOORTEXS_PER_ROW) * GFX_FLOORTEXSIZE;
			int ya = (yt & (GFX_FLOORTEXSIZE - 1)) + (tex / FLOORTEXS_PER_ROW + Level.tileset) * GFX_FLOORTEXSIZE;
			xa %= texTiles.w;
			ya %= texTiles.h;

			viewport.pixels[x + y * viewport.w] = ShadePixel(texTiles.pixels[xa + ya * texTiles.w], zd, lux, x, y);
			zbuf[x + y * viewport.w] = zd;
		}
	}
}

static int DoSpritePixel(bmcol_t col, float zval, luxel_t luxel, int x, int y, enum SpriteFx fx)
{
	if (fx == SPRITEFX_GLOW)
		return col;
	if (fx == SPRITEFX_BLUR)
		return 0;

	if (fx == SPRITEFX_INVERT)
	{
		int r = (col >> 16) & 0xff;
		int g = (col >> 8) & 0xff;
		int b = (col >> 0) & 0xff;

		r = 256 - r;
		g = 256 - g;
		b = 256 - b;

		return (0xff << 24) | (r << 16) | (g << 8) | b;
	}
	else if (fx == SPRITEFX_DMGRED)
	{
		int r = (col >> 16) & 0xff;
		return (0xff << 24) | (r << 16);
	}

	return ShadePixel(col, zval, luxel, x, y);
}

void Gfx_DrawSprite(float xpos, float ypos, float zpos, enum SpriteFx fx, Bmp_t *tex)
{
	/* Note: yc & yy refer to depth in this context */
	float sx = (float)tex->w / GFX_SPRITESIZE;
	float sy = (float)tex->h / GFX_SPRITESIZE;

	float xc = xpos - xCam;
	float yc = ypos - yCam;
	float zc = zpos - zCam;

	float xx = (xc * rCos - yc * rSin) / fov;
	float yy = xc * rSin + yc * rCos;
	float zz = zc * 2.0f + (sy - 1);

	if (yy < 0.1f)
		return;

	float xscl = sx;
	float yscl = sy;
	float aspect = ((float)viewport.w / (float)viewport.h);
	float xs0 = (xx + xscl / aspect) / yy;
	float xs1 = (xx - xscl / aspect) / yy;
	float ys0 = (zz + yscl) / yy;
	float ys1 = (zz - yscl) / yy;

	float xpf0 = (1.0f - ((xs0 + 1.0f) / 2)) * viewport.w;
	float xpf1 = (1.0f - ((xs1 + 1.0f) / 2)) * viewport.w;
	float ypf0 = (1.0f - ((ys0 + 1.0f) / 2)) * viewport.h;
	float ypf1 = (1.0f - ((ys1 + 1.0f) / 2)) * viewport.h;

	int xp0 = (int)Math_Ceil(xpf0);
	int xp1 = (int)Math_Ceil(xpf1);
	int yp0 = (int)Math_Ceil(ypf0);
	int yp1 = (int)Math_Ceil(ypf1);

	if (xp0 < 0) xp0 = 0;
	if (xp1 > viewport.w) xp1 = viewport.w;
	if (yp0 < 0) yp0 = 0;
	if (yp1 > viewport.h) yp1 = viewport.h;

	int lx = (int)(xpos * LEVEL_LIGHTMAP_SCALE);
	int ly = (int)(ypos * LEVEL_LIGHTMAP_SCALE);
	luxel_t lux = Lvl_GetLuxel(lx, ly);

	int x, y;

	for (y = yp0; y < yp1; y++)
	{
		float fyt = (y - ypf0) / (ypf1 - ypf0);
		int yt = (int)(fyt * tex->h);
		for (x = xp0; x < xp1; x++)
		{
			float fxt = (x - xpf0) / (xpf1 - xpf0);
			int xt = (int)(fxt * tex->w);

			if (zbuf[x + y * viewport.w] > yy)
			{
				bmcol_t col = tex->pixels[xt + yt * tex->w];
				if (col != 0)
				{
					viewport.pixels[x + y * viewport.w] = DoSpritePixel(col, yy, lux, x, y, fx);
					zbuf[x + y * viewport.w] = yy;
				}
			}
		}
	}
}

void Gfx_DrawParticle(Particle *particle)
{
	// Convert world-space coordinates to camera-space
	float xc = particle->x - xCam;
	float yc = particle->y - yCam;
	float zc = particle->z - zCam - 0.5f;

	float xx = (xc * rCos - yc * rSin) / fov;
	float yy = xc * rSin + yc * rCos;
	float zz = zc * 2.0f;

	// Discard particles that are near or behind the camera
	if (yy < 0.1f)
		return;

	// Convert camera-space to screen-space
	//float xs = xx / yy;
	//float ys = zz / yy;

	const float scale = 0.01f;
	float xs0 = (xx + scale / ((float)viewport.w / (float)viewport.h)) / yy;
	float xs1 = (xx - scale / ((float)viewport.w / (float)viewport.h)) / yy;
	float ys0 = (zz + scale) / yy;
	float ys1 = (zz - scale) / yy;

	int xp0 = (1.0f - ((xs0 + 1.0f) / 2)) * viewport.w;
	int xp1 = (1.0f - ((xs1 + 1.0f) / 2)) * viewport.w;
	int yp0 = (1.0f - ((ys0 + 1.0f) / 2)) * viewport.h;
	int yp1 = (1.0f - ((ys1 + 1.0f) / 2)) * viewport.h;

	int lx = (int)(particle->x * LEVEL_LIGHTMAP_SCALE);
	int ly = (int)(particle->y * LEVEL_LIGHTMAP_SCALE);
	luxel_t lux = Lvl_GetLuxel(lx, ly);

	for (int y = yp0; y < yp1 + 1; y++)
	{
		if (y < 0 || y >= viewport.h)
			continue;
		for (int x = xp0; x < xp1 + 1; x++)
		{
			if (x < 0 || x >= viewport.w)
				continue;
			if (zbuf[x + y * viewport.w] > yy)
			{
				// Render the particle with dynamic alpha
				viewport.pixels[x + y * viewport.w] = ShadePixel(particle->colour, yy, lux, x, y);
				zbuf[x + y * viewport.w] = yy; // Update depth buffer
			}
		}
	}
}

#define SKY_WIDTH 1024	/* Width of a full wrap-around sky. Must be a power of two, engine also accepts 512px wide skies */
void Gfx_DrawSky(void)
{
	const float scalePi = MATH_PI * ((float)SKY_WIDTH / ((float)SKY_WIDTH * 2.0f));
	int x, y;

	if (!Level.skybox)
		return;

	const int skyW = Level.skybox->w;
	for (y = 0; y < Level.skybox->h; y++)
	{
		for (x = 0; x < viewport.w; x++)
		{
			int xOffs = (int)(x - rCam * skyW / scalePi) & (skyW - 1);
			viewport.pixels[x + y * viewport.w] = Level.skybox->pixels[xOffs + y * Level.skybox->w];
		}
	}
}

int Gfx_GetSpriteAngle(float rot)
{
	float angle = (rCam + MATH_PI) - rot;
	int spriteAngle = (int)((angle / (2.f * MATH_PI)) * 8.0f + 0.5f);
	spriteAngle = spriteAngle % 8;
	spriteAngle = spriteAngle < 0 ? spriteAngle + 8 : spriteAngle;
	return spriteAngle;
}

// TODO: Would be nice if the RPK said how many sprites there were ahead of time
// I don't want to require changes to the RPK files in v0.6 though.

#define CACHE_INITIAL_CAPACITY 1024
#define MAX_SPRITES 1024

// Each sprite "frame" can have 8 actual sprites for each viewing angle
// The values here are indices into the array of loaded sprites
static int spriteFrames[MAX_SPRITES][8];
static char spriteNames[MAX_SPRITES][RPK_NAME_LENGTH];
static int numLoadedFrames;

static Bmp_t *spriteCache;
static size_t numsprites, numcapacity;

void Gfx_InitSpriteCache(void)
{
	spriteCache = SDL_calloc(CACHE_INITIAL_CAPACITY, sizeof(Bmp_t));
	numsprites = 0;
	numcapacity = CACHE_INITIAL_CAPACITY;
}

void Gfx_DestroySpriteCache(void)
{
	SDL_free(spriteCache);
}

int Gfx_CacheLoadBitmap(const RPKFile *rpk, const char *name)
{
	if (numsprites == numcapacity)
	{
		numcapacity *= 2;
		spriteCache = SDL_realloc(spriteCache, numcapacity);
	}
	spriteCache[numsprites] = Bmp_CreateFromPAKEntry(rpk, name);
	return numsprites++;
}

int Gfx_LoadSprite(const RPKFile *rpk, const char *name)
{
	char entryname[RPK_NAME_LENGTH + 1];

	SDL_assert(SDL_isalnum(name[0]));

	// Check if this sprite has already been loaded
	for (int i = 0; i < numLoadedFrames; i++)
	{
		if (SDL_strncmp(spriteNames[i], name, RPK_NAME_LENGTH) == 0)
			return i;
	}
	// If not, add to the list
	if (numLoadedFrames == MAX_SPRITES)
	{
		Msg_Fatal("Ran out of sprite frames while loading %s!", name);
		return 0;
	}
	SDL_strlcpy(spriteNames[numLoadedFrames], name, RPK_NAME_LENGTH);

	// Check for directional sprite name first
	SDL_snprintf(entryname, RPK_NAME_LENGTH, "%sR%d", name, 0);
	if (RPK_GetIndexForName(rpk, entryname) != -1)
	{
		// Directional sprite name exists, load all frames
		for (int i = 0; i < 8; i++)
		{
			SDL_snprintf(entryname, RPK_NAME_LENGTH, "%sR%d", name, i);
			spriteFrames[numLoadedFrames][i] = Gfx_CacheLoadBitmap(rpk, entryname);
		}
	}
	else
	{
		int cached = Gfx_CacheLoadBitmap(rpk, name);
		for (int i = 0; i < 8; i++)
		{
			spriteFrames[numLoadedFrames][i] = cached;
		}
	}

	return numLoadedFrames++;
}

Bmp_t *Gfx_GetSprite(int id, float r)
{
	SDL_assert(id >= 0 && id < MAX_SPRITES);
	int angle = Gfx_GetSpriteAngle(r);
	return &spriteCache[spriteFrames[id][angle]];
}

void Gfx_DrawHeldItem(const Item_t *item, float bob, int cooldownTime)
{
	const int scale = 3;
	int next = cooldownTime > 0 ? 32 : 0;
	Bmp_t *sprite = next ? &ItemTypes[item->type].hudGfx1 : &ItemTypes[item->type].hudGfx0;

	int xp = viewport.w - sprite->w * scale - 12;
	int yp = viewport.h - sprite->h * scale + (int)(-bob * 64.0f) + 12;
	Bmp_BlitRegionScaled(&viewport, sprite, xp, yp, 0, 0, sprite->w, sprite->h, scale);
}

static void DrawCompass(int xo, int yo)
{
	int x, y;

	float targetX, targetY;
	if (Level.boss)
	{
		targetX = Level.boss->x;
		targetY = Level.boss->y;
	}
	else
	{
		targetX = (float)Level.exitX + 0.5f;
		targetY = (float)Level.exitY + 0.5f;
	}

	float lx = targetX - xCam;
	float ly = targetY - yCam;
	float viewAngle = -rCam; /* dont ask why this is negative */
	float angle = MATH_PI - (viewAngle - MATH_HALFPI - Math_Atan2(ly, lx));

	float rSin = Math_Sin(angle);
	float rCos = Math_Cos(angle);

	for (y = 0; y < 32; y++)
	{
		int yp = y + yo;
		if (yp < 0 || yp >= framebuffer->h) continue;

		for (x = 0; x < 32; x++)
		{
			int xp = x + xo;
			if (xp < 0 || xp >= framebuffer->w) continue;

			float xf = ((float)-x) / 16.0f + 1.0f; /* Uses -x to make sure its not flipped here */
			float yf = ((float)y) / 16.0f - 1.0f;

			float xr = xf * rCos - yf * rSin;
			float yr = xf * rSin + yf * rCos;

			int xx = (int)((xr + 1.0f) * 16.0f);
			int yy = (int)((yr + 1.0f) * 16.0f);

			if (xx < 0 || xx >= 32 || yy < 0 || yy >= 32)
				continue;

			bmcol_t col = texCompass.pixels[xx + yy * texCompass.w];
			if (col > 0)
				framebuffer->pixels[xp + yp * framebuffer->w] = col;
		}
	}
}

void Gfx_DrawHUDPanel(int health, int maxHealth, int invulTime, int lvlDepth)
{
	char healthmsg[32];

	Bmp_Blit(framebuffer, &viewport, 0, 0);

	if (Game.state == GAME_STATE_DEAD)
		return;

	DrawCompass((framebuffer->w - 32) - 4, 4);

	const int itemStartX = (framebuffer->w - 114) / 2;

	int xstart = 12;
	int ystart = framebuffer->h - 40;
	int xend = (int)(texBarHealth.w * ((float)health / (float)maxHealth));

	int i;

	// Health bar
	Bmp_Blit(framebuffer, invulTime > 0 ? &texBarBaseInv : &texBarBase, xstart - 1, ystart - 1);
	Bmp_BlitRegion(framebuffer, &texBarHealth, xstart + 3, ystart, 0, 0, xend, texBarHealth.h);

	Bmp_SetActiveFont(BMPFONT_TINY);

	int healthPercent = (int)((float)health / (float)maxHealth * 100.0f);
	SDL_snprintf(healthmsg, 32, "%d%% %d/%d", healthPercent, health, maxHealth);
	if (World_Players[0].maxMana > 0)
		Bmp_DrawText(framebuffer, healthmsg, 0xff7fff7f, xstart + texBarBase.w + 2, ystart, framebuffer->w - xstart);
	else
		Bmp_DrawText(framebuffer, healthmsg, 0xff7fff7f, xstart, ystart + texBarBase.h + 2, framebuffer->w - xstart);

	// Mana bar
	if (World_Players[0].maxMana > 0)
	{
		Bmp_Blit(framebuffer, &texBarBase, xstart - 1, ystart - 1 + 8);
		xend = (int)(texBarMana.w * ((float)World_Players[0].mana / (float)World_Players[0].maxMana));
		Bmp_BlitRegion(framebuffer, &texBarMana, xstart + 3, ystart + 8, 0, 0, xend, texBarMana.h);

		int manaPercent = (int)((float)World_Players[0].mana / (float)World_Players[0].maxMana * 100.0f);
		SDL_snprintf(healthmsg, 32, "%d%% %d/%d", manaPercent, World_Players[0].mana, World_Players[0].maxMana);
		Bmp_DrawText(framebuffer, healthmsg, 0xff7fafff, xstart + texBarBase.w + 2, ystart + 8, framebuffer->w - xstart);
	}

	// Current floor and XP
	SDL_snprintf(healthmsg, 32, "FLOOR %02d\n%05d XP", lvlDepth, World_Players[0].xp);
	Bmp_DrawText(framebuffer, healthmsg, 0xffaaaaaa, xstart, ystart + texBarBase.h + 12, framebuffer->w - xstart);

	ystart = 10;
	for (i = 0; i < MAX_STATMODS; i++)
	{
		struct StatModifier *modifier = &World_Players[0].entity->modifiers[i];
		if (modifier->duration > 0)
		{
			Bmp_BlitRegion(framebuffer, &texStatModIcons, 10, ystart, modifier->target * 16, modifier->modifier < 0 ? 16 : 0, 16, 16);
			SDL_snprintf(healthmsg, 32, "%d", modifier->duration / GAME_TICKS_PER_SECOND + 1);
			Bmp_DrawText(framebuffer, healthmsg, 0xffffffff, 28, ystart, framebuffer->w);
			ystart += 20;
		}
	}

	for (i = 0; i < 5; i++)
	{
		int slot = INV_HAND_SLOT + i;
		const Item_t *item = Inv_GetItem(&World_Players[0].inventory, slot, NULL);
		int bgCol = 0x501a1a1a;
		if (World_Players[0].handSlot == slot)
			bgCol = 0x7f303030;
		int w, h;
		int xx, yy;
		if (i < 2)
		{
			w = h = 32;
			xx = itemStartX + i * w;
		}
		else
		{
			w = h = 16;
			xx = itemStartX + 65 + (i - 2) * w;
		}
		yy = framebuffer->h - 4 - w;
		//Bmp_FillRegion(framebuffer, 0xff000000, xx - 1, yy - 1, w + 2, h + 2);
		Bmp_FillRegion(framebuffer, bgCol, xx, yy, w, h);
		if (item != NULL)
		{
			int itemx = xx + (w - (ItemTypes[item->type].w * 16)) / 2;
			int itemy = yy + (h - (ItemTypes[item->type].h * 16)) / 2;
			Inv_DrawItem(framebuffer, item, itemx, itemy);
		}
	}
}

void Gfx_DrawBossHealthBar(Bmp_t *screen, int health, int maxHealth)
{
	const int barWidth = (int)(screen->w / 1.25f);
	const int barHeight = texBossBar.h;

	int tw, th;
	int i, j;

	int col = Level.bossColour;

	Bmp_SetActiveFont(BMPFONT_MINISERIF);
	Bmp_TextSize(Level.bossName, &tw, &th, screen->w);
	Bmp_DrawText(screen, Level.bossName, col, (screen->w - tw) / 2, 8, screen->w);

	int xstart = (screen->w - barWidth) / 2;
	int ystart = 8 + th + 4;
	int xend = (int)(barWidth * ((float)health / (float)maxHealth));

	// The base bar textures needs to be stretched out, since the required
	// width is usually longer than the size of the graphic in the files.
	// This is done in three parts:

	// 1. Draw the starting end of the bar.
	Bmp_BlitRegion(framebuffer, &texBossBase, xstart - 9, ystart - 2, 0, 0, 9, texBossBase.h);
	// 2. Repeatedly draw the center section in 182-pixel-wide pieces until the barWidth is filled up.
	int remainingWidth = barWidth;
	while (remainingWidth > 0)
	{
		int pieceWidth = 182;
		if (pieceWidth > remainingWidth)
			pieceWidth = remainingWidth;
		Bmp_BlitRegion(framebuffer, &texBossBase, xstart + barWidth - remainingWidth, ystart - 2, 9, 0, pieceWidth, texBossBase.h);
		remainingWidth -= pieceWidth;
	}
	// 3. Draw the end piece of the bar
	Bmp_BlitRegion(framebuffer, &texBossBase, xstart + barWidth, ystart - 2, 191, 0, 9, texBossBase.h);

	for (i = 0; i < barWidth; i++)
	{
		int xx;
		if (i > xend)
			break;
		if (i <= 5)
		{
			xx = i;
		}
		else if (i > (barWidth - 5))
		{
			xx = (barWidth - i - 1);
		}
		else
		{
			xx = 6;
		}
		for (j = 0; j < barHeight; j++)
		{
			if (((texBossBar.pixels[xx + j * texBossBar.w] >> 24) & 0xff) < 0xff)
				continue;
			bmcol_t c = texBossBar.pixels[xx + j * texBossBar.w] & 0xff;
			//if (i > xend)
			//	c &= 0x7f;
			framebuffer->pixels[(xstart + i) + (ystart + j) * framebuffer->w] = Multiply(Level.bossColour, c);
		}
	}
}

static float *firebuf0;
static float *firebuf1;
static float *fireturbulence;
static int FIREW = 0, FIREH = 0, FIRESIZE;
static int firetime = 0;

static void Gfx_SetupFire(int width, int height)
{
	size_t buflen;
	size_t i;

	FIREW = width;
	FIREH = height;
	FIRESIZE = FIREW * FIREH;

	buflen = FIRESIZE + FIREW;
	firebuf0 = SDL_malloc(buflen * sizeof(float));
	firebuf1 = SDL_malloc(buflen * sizeof(float));
	fireturbulence = SDL_malloc(buflen * sizeof(float));

	for (i = 0; i < buflen; i++)
	{
		firebuf0[i] = 0.0f;
		firebuf1[i] = 0.0f;
		fireturbulence[i] = Math_RandomFloat();
	}
}

static void Gfx_CleanupFire(void)
{
	SDL_free(firebuf0);
	SDL_free(firebuf1);
	SDL_free(fireturbulence);
}

void Gfx_DrawFireBackground(Bmp_t *screen)
{
	const int iMax = FIREW / 4;

	int i;

	float pixel;
	float p0, p1;
	unsigned char r, g, b;

	for (i = 0; i < iMax; i++)
	{
		firebuf0[RNG_RANGE(0, FIREW) + FIREH * FIREW] = 1000.0f;
	}

	/* Set the first row of pixels to black, since we won't be drawing over it */
	SDL_memset(screen->pixels, 0xff000000, FIREW * 4);

	for (i = FIREW; i < FIRESIZE; i++)
	{
		p0 = (firebuf0[i - 1] + firebuf0[i + 1]) * 0.242f;
		p1 = firebuf0[i + FIREW] * fireturbulence[(i + firetime) % FIRESIZE];

		pixel = firebuf1[i] = p0 + p1;

		r = MATH_MIN(255, (int)(pixel) << 2);
		g = MATH_MIN(255, (int)(pixel));
		b = MATH_MIN(255, (int)(pixel) >> 2);
		screen->pixels[i] = (0xff << 24) | (r << 16) | (g << 8) | b;
	}

	SDL_memcpy(firebuf0, firebuf1, (FIRESIZE + FIREW) * sizeof(float));
	firetime += RNG_RANGE(0, FIRESIZE);
}
