// TODO Icon.
// TODO Music.
// TODO Draw() with rotation.
// TODO Saving with new file IO.

#include <essence.h>

EsInstance *instance;
uint32_t backgroundColor;
int musicIndex;
bool keysPressedSpace, keysPressedEscape, keysPressedX;
uint32_t previousControllerButtons[ES_GAME_CONTROLLER_MAX_COUNT];
uint32_t *targetBits;
size_t targetWidth, targetHeight, targetStride;
double updateTimeAccumulator;
float gameScale;
uint32_t gameOffsetX, gameOffsetY, gameWidth, gameHeight;

#define GAME_SIZE (380)
#define BRAND "fly"

struct Texture { 
	uint32_t width, height; 
	uint32_t *bits;
};

void Transform(float *destination, float *left, float *right) {
	float d[6];
	d[0] = left[0] * right[0] + left[1] * right[3];
	d[1] = left[0] * right[1] + left[1] * right[4];
	d[2] = left[0] * right[2] + left[1] * right[5] + left[2];
	d[3] = left[3] * right[0] + left[4] * right[3];
	d[4] = left[3] * right[1] + left[4] * right[4];
	d[5] = left[3] * right[2] + left[4] * right[5] + left[5];
	destination[0] = d[0];
	destination[1] = d[1];
	destination[2] = d[2];
	destination[3] = d[3];
	destination[4] = d[4];
	destination[5] = d[5];
}

__attribute__((optimize("-O2"))) 
void Draw(Texture *texture, 
		float x, float y, float w = -1, float h = -1, 
		float sx = 0, float sy = 0, float sw = -1, float sh = -1, 
		float _a = 1, float _r = 1, float _g = 1, float _b = 1,
		float rot = 0) {
	(void) rot;

	if (sw == -1 && sh == -1) sw = texture->width, sh = texture->height;
	if (w == -1 && h == -1) w = sw, h = sh;
	if (_a <= 0) return;
	
	if (x + w < 0 || y + h < 0 || x > GAME_SIZE || y > GAME_SIZE) return;

	x *= gameScale, y *= gameScale, w *= gameScale, h *= gameScale;

	float m[6] = {};
	float m1[6] = { sw / w, 0, 0, 0, sh / h, 0 };
	float m2[6] = { 1, 0, -x * (sw / w), 0, 1, -y * (sh / h) };

	Transform(m, m2, m1);

	intptr_t yStart = y - 1, yEnd = y + h + 1, xStart = x - 1, xEnd = x + w + 1;
	// if (rot) yStart -= h * 0.45f, yEnd += h * 0.45f, xStart -= w * 0.45f, xEnd += w * 0.45f;
	if (yStart < 0) yStart = 0; 
	if (yStart > gameHeight) yStart = gameHeight;
	if (yEnd < 0) yEnd = 0; 
	if (yEnd > gameHeight) yEnd = gameHeight;
	if (xStart < 0) xStart = 0; 
	if (xStart > gameWidth) xStart = gameWidth;
	if (xEnd < 0) xEnd = 0; 
	if (xEnd > gameWidth) xEnd = gameWidth;
	m[2] += m[0] * xStart, m[5] += m[3] * xStart;

	uint32_t *scanlineStart = targetBits + (gameOffsetY + yStart) * targetStride / 4 + (gameOffsetX + xStart);

	uint32_t r = _r * 255, g = _g * 255, b = _b * 255, a = _a * 255;

	for (intptr_t y = yStart; y < yEnd; y++, scanlineStart += targetStride / 4) {
		uint32_t *output = scanlineStart;

		float tx = m[1] * y + m[2];
		float ty = m[4] * y + m[5];

		for (intptr_t x = xStart; x < xEnd; x++, output++, tx += m[0], ty += m[3]) {
			if (tx + sx < 0 || ty + sy < 0) continue;
			uintptr_t txi = tx + sx, tyi = ty + sy;
			if (txi < sx || tyi < sy || txi >= sx + sw || tyi >= sy + sh) continue;
			uint32_t modified = texture->bits[tyi * texture->width + txi];
			if (!(modified & 0xFF000000)) continue;
			uint32_t original = *output;
			uint32_t alpha1 = (((modified & 0xFF000000) >> 24) * a) >> 8;
			uint32_t alpha2 = (255 - alpha1) << 8;
			uint32_t r2 = alpha2 * ((original & 0x00FF0000) >> 16);
			uint32_t g2 = alpha2 * ((original & 0x0000FF00) >> 8);
			uint32_t b2 = alpha2 * ((original & 0x000000FF) >> 0);
			uint32_t r1 = r * alpha1 * ((modified & 0x00FF0000) >> 16);
			uint32_t g1 = g * alpha1 * ((modified & 0x0000FF00) >> 8);
			uint32_t b1 = b * alpha1 * ((modified & 0x000000FF) >> 0);
			*output = 0xFF000000 
				| (0x00FF0000 & ((r1 + r2) << 0)) 
				| (0x0000FF00 & ((g1 + g2) >> 8)) 
				| (0x000000FF & ((b1 + b2) >> 16));
		}
	}
}

void CreateTexture(Texture *texture, const char *cName) {
	size_t dataBytes;
	const void *data = EsEmbeddedFileGet(cName, -1, &dataBytes);
	texture->bits = (uint32_t *) EsImageLoad(data, dataBytes, &texture->width, &texture->height, 4);
	EsAssert(texture->bits);
}

void ExitGame() {
	EsInstanceDestroy(instance);
}

///////////////////////////////////////////////////////////

#define TAG_ALL (-1)
#define TAG_SOLID (-2)
#define TAG_KILL (-3)
#define TAG_WORLD (-4)

#define TILE_SIZE (16)

struct Entity {
	uint8_t tag;
	uint8_t layer;
	bool solid, kill, hide;
	char symbol;
	int8_t frameCount, stepsPerFrame;
	Texture *texture;
	float texOffX, texOffY, w, h;
	int uid;
	
	void (*create)(Entity *);
	void (*destroy)(Entity *);
	void (*stepAlways)(Entity *); // Called even if level editing.
	void (*step)(Entity *);
	void (*draw)(Entity *);
	void (*drawAfter)(Entity *);
	
	float x, y, px, py;
	int stepIndex;
	bool isUsed, isDestroyed;
	
	union {
		struct {
			float cx, cy;
			float th, dth;
			float dths;
			int respawn, respawnGrow, dash;
		} player;
		
		struct {
			int random;
		} block;
		
		struct {
			float vel;
		} moveBlock;
		
		struct {
			float th, cx, cy;
		} spin;
		
		struct {
			float vx, vy, th, dth;
			int life;
			uint32_t col;
		} star;
	};
	
	void Destroy() {
		if (isDestroyed) return;
		isDestroyed = true;
		if (destroy) destroy(this);
	}
};

Texture textureDashMessage, textureKeyMessage, textureKey, textureControlsMessage, textureWhite;

Entity typePlayer, typeBlock, typeCheck, typeMoveH, typeStar, typeMoveV, typePowerup, typeShowMessage, typeKey, typeLock, typeSpin, typeEnd;

// All the entities that can be loaded from the rooms.
Entity *entityTypes[] = { 
	&typeBlock, &typeCheck, &typeMoveH, &typeMoveV, &typePowerup, &typeKey, &typeLock, &typeSpin, &typeEnd,
	nullptr,
};
	
int levelTick;
bool justLoaded = true;

#define MAX_ENTITIES (1000)

struct SaveState {
	float checkX, checkY;
	int roomX, roomY;
	bool hasDash, hasKey;
	int deathCount;
	uint32_t check;
};

struct GameState : SaveState {
	Entity entities[MAX_ENTITIES];
	int world;
	Entity *player;
};

GameState state;

Entity *AddEntity(Entity *templateEntity, float x, float y, int uid = 0) {
	for (int i = 0; i < MAX_ENTITIES; i++) {
		if (!state.entities[i].isUsed) {
			EsMemoryCopy(state.entities + i, templateEntity, sizeof(Entity));
			state.entities[i].isUsed = true;
			if (!state.entities[i].frameCount) state.entities[i].frameCount = 1;
			if (!state.entities[i].stepsPerFrame) state.entities[i].stepsPerFrame = 1;
			state.entities[i].x = state.entities[i].px = x;
			state.entities[i].y = state.entities[i].py = y;
			if (!state.entities[i].w) state.entities[i].w = state.entities[i].texture->width - 1;
			if (!state.entities[i].h) state.entities[i].h = state.entities[i].texture->height / state.entities[i].frameCount - 1;
			state.entities[i].uid = uid;
			if (state.entities[i].create) state.entities[i].create(state.entities + i);
			return state.entities + i;
		}
	}
	
	static Entity fake = {};
	return &fake;
}
	
Entity *FindEntity(float x, float y, float w, float h, int tag, Entity *exclude) {
	for (int i = 0; i < MAX_ENTITIES; i++) {
		if (state.entities[i].isUsed && !state.entities[i].isDestroyed
				&& (state.entities[i].tag == tag 
					|| tag == TAG_ALL 
					|| (tag == TAG_SOLID && state.entities[i].solid)
					|| (tag == TAG_KILL && state.entities[i].kill)
					)
				&& state.entities + i != exclude) {
			if (x <= state.entities[i].x + state.entities[i].w && state.entities[i].x <= x + w 
					&& y <= state.entities[i].y + state.entities[i].h && state.entities[i].y <= y + h) {
				return state.entities + i;
			}
		}
	}
	
	return nullptr;
}

char roomName[16];

void UpdateRoomName() {
	roomName[0] = 'R';
	roomName[1] = (state.roomX / 10) + '0';
	roomName[2] = (state.roomX % 10) + '0';
	roomName[3] = '_';
	roomName[4] = (state.roomY / 10) + '0';
	roomName[5] = (state.roomY % 10) + '0';
	roomName[6] = '.';
	roomName[7] = 'D';
	roomName[8] = 'A';
	roomName[9] = 'T';
	roomName[10] = 0;
}

#define ROOM_ID ((state.roomX - 7) + (state.roomY - 9) * 6)

void LoadRoom() {
	state.world = 0;
	
	UpdateRoomName();
	
	roomName[6] = '_';
	const uint8_t *buffer = (const uint8_t *) EsEmbeddedFileGet(roomName, -1);
	
	for (int i = 0; i < MAX_ENTITIES; i++) {
		if (!state.entities[i].isUsed || state.entities[i].isDestroyed) continue;
		
		if (state.entities[i].tag != typePlayer.tag) {
			state.entities[i].Destroy();
		} else {
			state.entities[i].px = state.entities[i].x;
			state.entities[i].py = state.entities[i].y;
		}
	}
	
	int p = 0;
	int iir = 0;
	
	while (true) {
		uint8_t tag = buffer[p++];
		if (!tag) break;
		float x = *(float *) (buffer + p); p += 4;
		float y = *(float *) (buffer + p); p += 4;
		
		if (tag == (uint8_t) TAG_WORLD) {
			state.world = x;
		}
		
		for (int i = 0; entityTypes[i]; i++) {
			if (entityTypes[i]->tag == tag) {
				AddEntity(entityTypes[i], x, y, (state.roomX << 24) | (state.roomY << 16) | iir);
				iir++;
			}
		}
	}
	
	musicIndex = state.world;
}

void CalculateCheck() {
	state.check = 0;
	
	uint8_t *buffer = (uint8_t *) &state;
	uint32_t check = 0x1D471D47;
	
	for (uintptr_t i = 0; i < sizeof(SaveState); i++) {
		check ^= ((uint32_t) buffer[i] + 10) * (i + 100);
	}
	
	state.check = check;
}

float FadeInOut(float t) {
	if (t < 0.3f) return t / 0.3f;
	else if (t < 0.7f) return 1;
	else return 1 - (t - 0.7f) / 0.3f;
}

void InitialiseGame() {
	state.roomX = 10;
	state.roomY = 10;
	
	CreateTexture(&textureWhite, "white_png");
	CreateTexture(&textureKey, "key_png");
	CreateTexture(&textureDashMessage, "dash_msg_png");
	CreateTexture(&textureKeyMessage, "key_msg_png");
	CreateTexture(&textureControlsMessage, "controls_png");
	
	int tag = 1;
	
	{
		static Texture texture;
		CreateTexture(&texture, "player_png");
		typePlayer.tag = tag++;
		typePlayer.texture = &texture;
		typePlayer.frameCount = 6;
		typePlayer.stepsPerFrame = 5;
		typePlayer.layer = 1;
		
		typePlayer.step = [] (Entity *entity) {
			if (entity->player.respawn) {
				if (entity->stepIndex > entity->player.respawn) {
					entity->player.respawn = 0;
					entity->hide = false;
					entity->player.cx = state.checkX;
					entity->player.cy = state.checkY;
					entity->player.respawnGrow = entity->stepIndex + 20;
					entity->player.dash = 0;
				} else {
					return;
				}
			}
			
			if (!entity->player.respawnGrow && !entity->player.dash) {
				if (keysPressedSpace) {
					entity->player.cx += (entity->x - entity->player.cx) * 2;
					entity->player.cy += (entity->y - entity->player.cy) * 2;
					entity->player.th += 3.15f;
					entity->player.dth = -entity->player.dth;
					entity->player.dths = 5;
				} else if (keysPressedX && state.hasDash) {
					entity->player.dash = 10;
				}
			}
			
			float rd = 40;
			
			if (entity->player.respawnGrow) {
				if (entity->stepIndex > entity->player.respawnGrow) {
					entity->player.respawnGrow = 0;
				} else {
					rd *= 1 - (entity->player.respawnGrow - entity->stepIndex) / 20.0f;
				}
			}
			
			entity->x = rd * EsCRTcosf(entity->player.th) + entity->player.cx;
			entity->y = rd * EsCRTsinf(entity->player.th) + entity->player.cy + 5 * EsCRTsinf(4.71f + entity->stepIndex * 0.1f);
			
			if (entity->player.dash) {
				float pt = entity->player.th - entity->player.dth;
				float px = rd * EsCRTcosf(pt) + entity->player.cx;
				float py = rd * EsCRTsinf(pt) + entity->player.cy + 5 * EsCRTsinf(4.71f + (entity->stepIndex - 1) * 0.1f);
				float dx = entity->x - px;
				float dy = entity->y - py;
				entity->player.cx += dx * 3.2f;
				entity->player.cy += dy * 3.2f;
				entity->player.dash--;
				AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0xFF;
			} else {
				entity->player.th += entity->player.dth;
			}
			
			if (entity->player.respawnGrow) {
				entity->px = entity->x;
				entity->py = entity->y;
			}
			
			if (entity->player.dths) {
				entity->player.th += entity->player.dth;
				entity->player.dths--;
				entity->stepIndex = 5 * 3;
			}
			
			if (FindEntity(entity->x + 5, entity->y + 5, entity->w - 10, entity->h - 10, TAG_KILL, 0)) {
				for (int i = 0; i < 20; i++) {
					AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0xFF0000;
				}
				
				entity->hide = true;
				entity->player.respawn = entity->stepIndex + 20;
				state.deathCount++;
			}
			
			if (entity->x > GAME_SIZE - 8) {
				entity->x -= GAME_SIZE;
				entity->player.cx -= GAME_SIZE;
				state.checkX -= GAME_SIZE;
				state.roomX++;
				LoadRoom();
			} else if (entity->x < -8) {
				entity->x += GAME_SIZE;
				entity->player.cx += GAME_SIZE;
				state.checkX += GAME_SIZE;
				state.roomX--;
				LoadRoom();
			}
			
			if (entity->y > GAME_SIZE - 8) {
				entity->y -= GAME_SIZE;
				entity->player.cy -= GAME_SIZE;
				state.checkY -= GAME_SIZE;
				state.roomY++;
				LoadRoom();
			} else if (entity->y < -8) {
				entity->y += GAME_SIZE;
				entity->player.cy += GAME_SIZE;
				state.checkY += GAME_SIZE;
				state.roomY--;
				LoadRoom();
			}
		};
		
		typePlayer.create = [] (Entity *entity) { 
			state.player = entity; 
			entity->player.cx = entity->x;
			entity->player.cy = entity->y;
			entity->player.dth = 0.06f;
		};
	}
	
	{
		static Texture texture;
		CreateTexture(&texture, "block_png");
		
		static Texture block2;
		CreateTexture(&block2, "block2_png");
		
		typeBlock.tag = tag++;
		typeBlock.texture = &texture;
		typeBlock.kill = true;
		typeBlock.hide = true;
		typeBlock.layer = 2;
		
		typeBlock.create = [] (Entity *entity) {
			uint8_t r = EsRandomU8();
			
			if (r < 20) {
				entity->block.random = 1;
			} else if (r < 50) {
				entity->block.random = 2;
			} else {
				entity->block.random = 0;
			}
		};
		
		typeBlock.stepAlways = [] (Entity *entity) {
			if (FindEntity(entity->x + 1, entity->y + 1, entity->w - 2, entity->h - 2, entity->tag, entity)) {
				entity->Destroy();
			}
		};
		
		typeBlock.drawAfter = [] (Entity *entity) {
			if (!FindEntity(entity->x - 16, entity->y, 1, 1, entity->tag, entity)) {
				Draw(&block2, entity->x - 16, entity->y, 16, 16, 0, entity->block.random * 16, 16, 16, 1);
			}
			
			if (!FindEntity(entity->x + 16, entity->y, 1, 1, entity->tag, entity)) {
				Draw(&block2, entity->x + 16, entity->y, 16, 16, 32, entity->block.random * 16, 16, 16, 1);
			}
			
			if (!FindEntity(entity->x, entity->y - 16, 1, 1, entity->tag, entity)) {
				Draw(&block2, entity->x, entity->y - 16, 16, 16, 16, entity->block.random * 16, 16, 16, 1);
			}
			
			if (!FindEntity(entity->x, entity->y + 16, 1, 1, entity->tag, entity)) {
				Draw(&block2, entity->x, entity->y + 16, 16, 16, 48, entity->block.random * 16, 16, 16, 1);
			}
		};
	}
	
	{
		static Texture check1, check2;
		CreateTexture(&check1, "check1_png");
		CreateTexture(&check2, "check2_png");
		typeCheck.tag = tag++;
		typeCheck.texture = &check1;
		
		typeCheck.step = [] (Entity *entity) {
			if (state.checkX == entity->x && state.checkY == entity->y) {
				entity->texture = &check2;
			} else {
				entity->texture = &check1;
			}
			
			if (FindEntity(entity->x - 4, entity->y - 4, entity->w + 8, entity->h + 8, typePlayer.tag, 0)) {
				if (state.checkX != entity->x || state.checkY != entity->y) {
					for (int i = 0; i < 10; i++) AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0xFFFFFF;
				}
				
				state.checkX = entity->x;
				state.checkY = entity->y;
				
				CalculateCheck();
				EsFileWriteAll("|Settings:/Save.dat", -1, &state, sizeof(SaveState)); 
			}
		};
	}
	
	{
		static Texture texture;
		CreateTexture(&texture, "moveblock_png");
		typeMoveH.tag = tag++;
		typeMoveH.texture = &texture;
		typeMoveH.kill = true;
		typeMoveH.w = 16;
		typeMoveH.h = 16;
		typeMoveH.texOffX = -4;
		typeMoveH.texOffY = -4;
		
		typeMoveH.create = [] (Entity *entity) {
			entity->moveBlock.vel = -4;
		};
		
		typeMoveH.step = [] (Entity *entity) {
			entity->x += entity->moveBlock.vel;
			
			if (FindEntity(entity->x, entity->y, entity->w, entity->h, typeBlock.tag, 0)) {
				entity->moveBlock.vel = -entity->moveBlock.vel;
			}
		};
	}
	
	{
		// Removed entity.
		tag++;
	}
	
	{
		static Texture texture;
		CreateTexture(&texture, "star_png");
		typeStar.texture = &texture;
		typeStar.tag = tag++;
		typeStar.hide = true; // Draw manually.
		typeStar.layer = 2;
		
		typeStar.create = [] (Entity *entity) {
			float th = EsRandomU8() / 255.0 * 6.24;
			float sp = EsRandomU8() / 255.0 * 0.5 + 0.5;
			entity->star.vx = sp * EsCRTcosf(th);
			entity->star.vy = sp * EsCRTsinf(th);
			entity->star.life = EsRandomU8();
			entity->star.dth = (EsRandomU8() / 255.0f - 0.5f) * 0.2f;
		};
		
		typeStar.step = [] (Entity *entity) {
			entity->x += entity->star.vx;
			entity->y += entity->star.vy;
			entity->star.th += entity->star.dth;
			
			if (entity->star.life < entity->stepIndex) {
				entity->Destroy();
			}
		};
		
		typeStar.drawAfter = [] (Entity *entity) {
			Draw(entity->texture, entity->x - 4, entity->y - 4, -1, -1, 0, 0, -1, -1, 
				1.0 - (float) entity->stepIndex / entity->star.life, 
				((entity->star.col >> 16) & 0xFF) / 255.0f,
				((entity->star.col >> 8) & 0xFF) / 255.0f,
				((entity->star.col >> 0) & 0xFF) / 255.0f,
				entity->star.th);
		};
	}
	
	{
		static Texture texture;
		CreateTexture(&texture, "moveblock_png");
		typeMoveV.tag = tag++;
		typeMoveV.texture = &texture;
		typeMoveV.kill = true;
		typeMoveV.w = 16;
		typeMoveV.h = 16;
		typeMoveV.texOffX = -4;
		typeMoveV.texOffY = -4;
		
		typeMoveV.create = [] (Entity *entity) {
			entity->moveBlock.vel = -4;
		};
		
		typeMoveV.step = [] (Entity *entity) {
			entity->y += entity->moveBlock.vel;
			
			if (FindEntity(entity->x, entity->y, entity->w, entity->h, typeBlock.tag, 0)) {
				entity->moveBlock.vel = -entity->moveBlock.vel;
			}
		};
	}
	
	{
		static Texture texture;
		CreateTexture(&texture, "powerup_png");
		typePowerup.tag = tag++;
		typePowerup.texture = &texture;
		
		typePowerup.step = [] (Entity *entity) {
			if (state.hasDash) {
				entity->Destroy();
				return;
			}
			
			if (FindEntity(entity->x, entity->y, entity->w, entity->h, typePlayer.tag, 0)) {
				state.hasDash = true;
				AddEntity(&typeShowMessage, 0, 0)->texture = &textureDashMessage;
				entity->Destroy();
			}
		};
	}
	
	{
		typeShowMessage.texture = &textureKeyMessage;
		typeShowMessage.tag = tag++;
		typeShowMessage.hide = true;
		
		typeShowMessage.draw = [] (Entity *entity) {
			Draw(entity->texture, entity->x, entity->y, -1, -1, 0, 0, -1, -1, FadeInOut(entity->stepIndex / 180.0));
			if (entity->stepIndex > 180) entity->Destroy();
		};
	}
	
	{
		typeKey.tag = tag++;
		typeKey.texture = &textureKey;
		
		typeKey.step = [] (Entity *entity) {
			if (state.hasKey) {
				entity->Destroy();
			} else if (FindEntity(entity->x, entity->y, entity->w, entity->h, typePlayer.tag, 0)) {
				state.hasKey = true;
				AddEntity(&typeShowMessage, 0, 0)->texture = &textureKeyMessage;
				entity->Destroy();
				for (int i = 0; i < 10; i++) AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0xFFFFFF;
			}
		};
	}
	
	{
		static Texture texture;
		CreateTexture(&texture, "lock_png");
		typeLock.tag = tag++;
		typeLock.texture = &texture;
		typeLock.kill = true;
		
		typeLock.step = [] (Entity *entity) {
			if (state.hasKey) {
				for (int i = 0; i < 1; i++) AddEntity(&typeStar, entity->x + 8, entity->y + 8)->star.col = 0x000000;
				entity->Destroy();
			}
		};
	}
	
	{
		static Texture texture;
		CreateTexture(&texture, "moveblock_png");
		typeSpin.tag = tag++;
		typeSpin.texture = &texture;
		typeSpin.kill = true;
		typeSpin.w = 16;
		typeSpin.h = 16;
		typeSpin.texOffX = -4;
		typeSpin.texOffY = -4;
		
		typeSpin.create = [] (Entity *entity) {
			entity->spin.cx = entity->x;
			entity->spin.cy = entity->y;
		};
		
		typeSpin.step = [] (Entity *entity) {
			entity->x = 60 * EsCRTcosf(entity->spin.th) + entity->spin.cx;
			entity->y = 60 * EsCRTsinf(entity->spin.th) + entity->spin.cy;
			entity->spin.th += 0.04f;
		};
	}
	
	{
		static Texture msg1, msg2, msg3, num;
		CreateTexture(&msg1, "end1_png");
		CreateTexture(&msg2, "end2_png");
		CreateTexture(&msg3, "end3_png");
		CreateTexture(&num, "numbers_png");
		
		typeEnd.tag = tag++;
		typeEnd.texture = &textureKey;
		typeEnd.hide = true;
		
		typeEnd.create = [] (Entity *) {
			state.player->Destroy();
		};
		
		typeEnd.draw = [] (Entity *entity) {
			float t = entity->stepIndex / 180.0f;
			
			if (t < 1) {
				Draw(&msg1, 40, 150, -1, -1, 0, 0, -1, -1, FadeInOut(t));
			} else if (t < 2) {
				Draw(&msg2, 40, 150, -1, -1, 0, 0, -1, -1, FadeInOut(t - 1));
			} else if (t < 3) {
				Draw(&msg3, 40, 150, -1, -1, 0, 0, -1, -1, FadeInOut(t - 2));
				
				int p = state.deathCount;
				char digits[10];
				int dc = 0;
				
				if (p == 0) {
					digits[dc++] = 0;
				} else {
					while (p) {
						digits[dc++] = p % 10;
						p /= 10;
					}
				}
				
				int w = dc * 16;
				
				for (int i = dc - 1; i >= 0; i--) {
					Draw(&num, 40 + 150 - w / 2 + (dc - 1 - i) * 16, 
						150 + 33, 16, 30, 16 * digits[i], 0, 16, 30, FadeInOut(t - 2));
				}
			} else {
				ExitGame();
			}
		};
	}
	
	state.checkX = GAME_SIZE / 2;
	state.checkY = GAME_SIZE / 2 - 20;

	size_t loadedStateBytes;
	SaveState *loadedState = (SaveState *) EsFileReadAll("|Settings:/Save.dat", -1, &loadedStateBytes);
	bool noSave = true;
	
	if (loadedStateBytes == sizeof(SaveState)) {
		EsMemoryCopy(&state, loadedState, loadedStateBytes);

		uint32_t oldCheck = state.check;
		CalculateCheck();
		EsAssert(oldCheck == state.check);
		
		noSave = false;
	}
	
	LoadRoom();
	
	AddEntity(&typePlayer, state.checkX, state.checkY);

	if (noSave) {
		AddEntity(&typeShowMessage, 0, GAME_SIZE - 65)->texture = &textureControlsMessage;
	}
}

void UpdateGame() {	
	if (keysPressedEscape) {
		ExitGame();
	}
	
	for (int i = 0; i < MAX_ENTITIES; i++) {
		if (state.entities[i].isUsed) {
			state.entities[i].stepIndex++;
			
			state.entities[i].px += (state.entities[i].x - state.entities[i].px) * 0.5f;
			state.entities[i].py += (state.entities[i].y - state.entities[i].py) * 0.5f;
		}
	}
	
	for (int i = 0; i < MAX_ENTITIES; i++) if (state.entities[i].isUsed && state.entities[i].stepAlways) state.entities[i].stepAlways(state.entities + i);
	
	for (int i = 0; i < MAX_ENTITIES; i++) if (state.entities[i].isUsed && state.entities[i].step) {
		state.entities[i].step(state.entities + i);
	}
	
	for (int i = 0; i < MAX_ENTITIES; i++) {
		if (state.entities[i].isUsed && state.entities[i].isDestroyed) state.entities[i].isUsed = false;
	}
	
	levelTick++;
	
	state.world %= 3;
	
	if (state.world == 0) {
		backgroundColor = 0xbef1b1;
	} else if (state.world == 1) {
		backgroundColor = 0xcee5f1;
	} else if (state.world == 2) {
		backgroundColor = 0xf3bdf6;
	}
}

void RenderGame() {	
	for (int layer = -1; layer <= 3; layer++) {
		for (int i = 0; i < MAX_ENTITIES; i++) {
			Entity *entity = state.entities + i;
			if (!entity->isUsed) continue;
			if (entity->layer != layer) continue;
			if (!entity->texture) continue;
			if (entity->hide) continue;
			
			int frame = entity->stepsPerFrame >= 0 ? ((entity->stepIndex / entity->stepsPerFrame) % entity->frameCount) : (-entity->stepsPerFrame - 1);
			Draw(entity->texture, (int) (entity->px + entity->texOffX + 0.5f), 
				(int) (entity->py + entity->texOffY + 0.5f),
				entity->texture->width, entity->texture->height / entity->frameCount,
				0, entity->texture->height / entity->frameCount * frame, 
				entity->texture->width, entity->texture->height / entity->frameCount);
		}
	}
	
	for (int i = 0; i < MAX_ENTITIES; i++) if (state.entities[i].isUsed && state.entities[i].draw      ) state.entities[i].draw      (state.entities + i);
	for (int i = 0; i < MAX_ENTITIES; i++) if (state.entities[i].isUsed && state.entities[i].drawAfter ) state.entities[i].drawAfter (state.entities + i);
	
	if (state.hasKey) {
		Draw(&textureKey, GAME_SIZE - 20, 4);
	}
}

///////////////////////////////////////////////////////////

int ProcessCanvasMessage(EsElement *element, EsMessage *message) {
	if (message->type == ES_MSG_ANIMATE) {
		message->animate.complete = false;
		updateTimeAccumulator += message->animate.deltaMs / 1000.0;

		while (updateTimeAccumulator > 1 / 60.0) {
			{
				EsGameControllerState state[ES_GAME_CONTROLLER_MAX_COUNT];
				size_t count = EsGameControllerStatePoll(state);

				for (uintptr_t i = 0; i < count; i++) {
					if (state[i].buttons & (1 << 0) && (~previousControllerButtons[i] & (1 << 0))) {
						keysPressedSpace = true;
					} else if (state[i].buttons & (1 << 1) && (~previousControllerButtons[i] & (1 << 1))) {
						keysPressedX = true;
					}

					previousControllerButtons[i] = state[i].buttons;
				}
			}

			UpdateGame();
			updateTimeAccumulator -= 1 / 60.0;
			keysPressedSpace = keysPressedEscape = keysPressedX = false;
		}

		EsElementRepaint(element);
	} else if (message->type == ES_MSG_KEY_DOWN && !message->keyboard.repeat) {
		if (message->keyboard.scancode == ES_SCANCODE_SPACE || message->keyboard.scancode == ES_SCANCODE_Z) {
			keysPressedSpace = true;
		} else if (message->keyboard.scancode == ES_SCANCODE_ESCAPE) {
			keysPressedEscape = true;
		} else if (message->keyboard.scancode == ES_SCANCODE_X) {
			keysPressedX = true;
		}
	} else if (message->type == ES_MSG_PAINT) {
		EsPainter *painter = message->painter;
		EsPaintTargetStartDirectAccess(painter->target, &targetBits, nullptr, nullptr, &targetStride);
		targetBits = (uint32_t *) ((uint8_t *) targetBits + targetStride * painter->offsetY + 4 * painter->offsetX);
		targetWidth = painter->width, targetHeight = painter->height;

		gameScale = (float) painter->width / GAME_SIZE;
		if (gameScale * GAME_SIZE > painter->height) gameScale = (float) painter->height / GAME_SIZE;
		if (gameScale > 1) gameScale = EsCRTfloorf(gameScale);
		gameWidth = GAME_SIZE * gameScale, gameHeight = GAME_SIZE * gameScale;
		gameOffsetX = painter->width / 2 - gameWidth / 2;
		gameOffsetY = painter->height / 2 - gameHeight / 2;

		// TODO Clear margins.

		Draw(&textureWhite, 0, 0, GAME_SIZE, GAME_SIZE, 0, 0, 1, 1, 1, 
				((backgroundColor >> 16) & 0xFF) / 255.0f, 
				((backgroundColor >> 8) & 0xFF) / 255.0f, 
				((backgroundColor >> 0) & 0xFF) / 255.0f);

		RenderGame();

		EsPaintTargetEndDirectAccess(painter->target);
	}

	return 0;
}

void _start() {
	_init();

	while (true) {
		EsMessage *message = EsMessageReceive();

		if (message->type == ES_MSG_INSTANCE_CREATE) {
			instance = EsInstanceCreate(message, BRAND);
			EsWindow *window = instance->window;
			EsPanel *container = EsPanelCreate(window, ES_CELL_FILL, ES_STYLE_PANEL_WINDOW_DIVIDER);
			EsElement *canvas = EsCustomElementCreate(container, ES_CELL_FILL | ES_ELEMENT_FOCUSABLE, {});
			canvas->messageUser = ProcessCanvasMessage;
			EsElementStartAnimating(canvas);
			EsElementFocus(canvas);
			InitialiseGame();
		}
	}
}