#include "shell/Shell.h"

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

#ifdef __APPLE__
#include <OpenGL/gl.h>
#include <OpenGL/glu.h>
#else
#include <GL/gl.h>
#include <GL/glu.h>
#endif

#include "chipmunk.h"

#include "constants/AdhesionConstants.h"
#include "game/GameState.h"
#include "game/Level.h"
#include "game/LevelList.h"
#include "graphics/Texture.h"
#include "graphics/TextureManager.h"
#include "shell/Shell.h"
#include "shell/ShellKeyCodes.h"
#include "utilities/FixedIntervalRunLoop.h"
#include "utilities/IOUtilities.h"
#include "utilities/JSONParser.h"


#define DEFAULT_WINDOW_WIDTH 800
#define DEFAULT_WINDOW_HEIGHT 600


static FixedIntervalRunLoop * runLoop;
static int windowWidth, windowHeight;

static GameState * gameState;
static LevelList * levelList;
static int levelIndex;

static bool dragging;
static float dragStartX, dragStartY;
static float dragEndX, dragEndY;
static bool levelCompleted;
static bool died;

static void run(void * context);
static void levelCompletedCallback(void);
static void diedCallback(void);

static const GLubyte backgroundColor[]   = {0x00, 0x00, 0x00};
static const GLubyte stickyWallColor[]   = {0xFF, 0xFF, 0xFF};
static const GLubyte bouncyWallColor[]   = {0x7F, 0x3F, 0xFF};
static const GLubyte deadlyWallColor[]   = {0xFF, 0x00, 0x00};
static const GLubyte exitColor[]         = {0x3F, 0xFF, 0x3F};
static const GLubyte ballFrameColor[]    = {0xCF, 0xCF, 0xFF};
static const GLubyte ballInteriorColor[] = {0x9F, 0x9F, 0xFF};
static const GLubyte dragColor[]         = {0xFF, 0xFF, 0x00};

static void setProjection() {
	float ratio;
	
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	
	ratio = (float) windowWidth / (float) windowHeight;
	glOrtho(-ratio, ratio, -1.0f, 1.0f, -1.0f, 1.0f);
	glMatrixMode(GL_MODELVIEW);
}

static void initGL() {
	glEnableClientState(GL_VERTEX_ARRAY);
	
	glClearColor(backgroundColor[0] / 255.0f, backgroundColor[1] / 255.0f, backgroundColor[1] / 255.0f, 0.0f);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	
	setProjection();
}

static void initGame() {
	JSONNode * node;
	
	TextureManager_loadTextureList(resourcePath("textures.json"), Shell_getResourcePath());
	
	node = jsonFromFile(resourcePath("levels.json"));
	if (node == NULL) {
		fprintf(stderr, "Couldn't load levels.json; bailing");
		exit(EXIT_FAILURE);
	}
	levelList = LevelList_fromJSON(node);
	JSONParser_freeNodeContents(node);
	free(node);
	
	if (levelList->levelCount < 1) {
		fprintf(stderr, "No levels in levels.json; bailing");
		exit(EXIT_FAILURE);
	}
	
	levelIndex = 0;
	gameState = GameState_create(levelCompletedCallback, diedCallback);
	GameState_loadLevel(gameState, levelList->levels[levelIndex]);
	
	dragging = false;
	
	runLoop = FixedIntervalRunLoop_create(ADHESION_UPDATE_INTERVAL, run, NULL);
}

static void run(void * context) {
	levelCompleted = false;
	died = false;
	
	GameState_step(gameState, ADHESION_UPDATE_INTERVAL);
	
	if (levelCompleted) {
		levelIndex++;
		levelIndex %= levelList->levelCount;
		GameState_loadLevel(gameState, levelList->levels[levelIndex]);
	} else if (died) {
		GameState_loadLevel(gameState, levelList->levels[levelIndex]);
	}
}

#define BALL_SUBDIVISIONS 32

static void draw() {
	GLfloat ballCenterVertices[8];
	GLfloat ballOutlineVertices[BALL_SUBDIVISIONS * 2];
	GLfloat dragVertices[4];
	GLfloat anchorVertices[8] = {-0.15f, 0.0f, 0.15f, 0.0f, 0.0f, -0.15f, 0.0f, 0.15f};
	GLfloat anchorToBallVertices[4];
	unsigned int wallIndex;
	Level * level;
	GLenum error;
	float ratio;
	unsigned int vertexIndex;
	unsigned int anchorIndex;
	Texture * instructionsTexture;
	
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	
	level = levelList->levels[levelIndex];
	
	glPushMatrix();
	ratio = (float) windowWidth / (float) windowHeight;
	if (ratio > level->size.x / level->size.y) {
		glScalef(2.0f / level->size.y, 2.0f / level->size.y, 1.0f);
	} else {
		glScalef(2.0f / level->size.x * ratio, 2.0f / level->size.x * ratio, 1.0f);
	}
	glTranslatef(-level->center.x, -level->center.y, 0.0f);
	
	for (wallIndex = 0; wallIndex < level->wallCount; wallIndex++) {
		switch (level->walls[wallIndex].type) {
			case WALL_TYPE_STICKY:
				glColor3ubv(stickyWallColor);
				break;
			case WALL_TYPE_EXIT:
				glColor3ubv(exitColor);
				break;
			case WALL_TYPE_BOUNCY:
				glColor3ubv(bouncyWallColor);
				break;
			case WALL_TYPE_DEADLY:
				glColor3ubv(deadlyWallColor);
				break;
		}
		glVertexPointer(2, GL_FLOAT, 0, level->walls[wallIndex].vertices);
		glDrawArrays(GL_LINE_STRIP, 0, level->walls[wallIndex].vertexCount);
	}
	
	glPushMatrix();
	glTranslatef(gameState->player->p.x, gameState->player->p.y, 0.0f);
	glRotatef(gameState->player->a * 180.0f / M_PI, 0.0f, 0.0f, 1.0f);
	ballCenterVertices[0] = -level->radius;
	ballCenterVertices[1] = 0.0f;
	ballCenterVertices[2] = level->radius;
	ballCenterVertices[3] = 0.0f;
	ballCenterVertices[4] = 0.0f;
	ballCenterVertices[5] = -level->radius;
	ballCenterVertices[6] = 0.0f;
	ballCenterVertices[7] = level->radius;
	glColor3ubv(ballInteriorColor);
	glVertexPointer(2, GL_FLOAT, 0, ballCenterVertices);
	glDrawArrays(GL_LINES, 0, 4);
	
	for (vertexIndex = 0; vertexIndex < BALL_SUBDIVISIONS; vertexIndex++) {
		ballOutlineVertices[vertexIndex * 2 + 0] = cos(M_PI * 2 * vertexIndex / BALL_SUBDIVISIONS) * level->radius;
		ballOutlineVertices[vertexIndex * 2 + 1] = sin(M_PI * 2 * vertexIndex / BALL_SUBDIVISIONS) * level->radius;
	}
	glColor3ubv(ballFrameColor);
	glVertexPointer(2, GL_FLOAT, 0, ballOutlineVertices);
	glDrawArrays(GL_LINE_LOOP, 0, BALL_SUBDIVISIONS);
	glPopMatrix();
	
	glColor3ub(0xFF, 0x00, 0x00);
	glVertexPointer(2, GL_FLOAT, 0, anchorVertices);
	for (anchorIndex = 0; anchorIndex < gameState->anchorCount; anchorIndex++) {
		glPushMatrix();
		glTranslatef(gameState->anchors[anchorIndex].x, gameState->anchors[anchorIndex].y, 0.0f);
		glDrawArrays(GL_LINES, 0, 4);
		glPopMatrix();
	}
	
	if (gameState->stuck) {
		glColor3ub(0x00, 0xFF, 0xFF);
		glPushMatrix();
		glTranslatef(gameState->anchors[gameState->anchorCount - 1].x, gameState->anchors[gameState->anchorCount - 1].y, 0.0f);
		glRotatef(45.0f, 0.0f, 0.0f, 1.0f);
		glDrawArrays(GL_LINES, 0, 4);
		glPopMatrix();
	}
	
	anchorToBallVertices[2] = gameState->player->p.x;
	anchorToBallVertices[3] = gameState->player->p.y;
	glColor3ub(0x5F, 0x5F, 0x5F);
	glVertexPointer(2, GL_FLOAT, 0, anchorToBallVertices);
	for (anchorIndex = 0; anchorIndex < gameState->anchorCount; anchorIndex++) {
		anchorToBallVertices[0] = gameState->anchors[anchorIndex].x;
		anchorToBallVertices[1] = gameState->anchors[anchorIndex].y;
		glDrawArrays(GL_LINES, 0, 2);
	}
	
	glPopMatrix();
	
	if (dragging) {
		dragVertices[0] = dragStartX;
		dragVertices[1] = dragStartY;
		dragVertices[2] = dragEndX;
		dragVertices[3] = dragEndY;
		glColor3ubv(dragColor);
		glVertexPointer(2, GL_FLOAT, 0, dragVertices);
		glDrawArrays(GL_LINES, 0, 2);
	}
	
	instructionsTexture = TextureManager_getTexture("instructions");
	if (instructionsTexture != NULL) {
		GLint instructionsVertices[8] = {4, -36, 516, -36, 516, -4, 4, -4};
		GLint instructionsTexCoords[8] = {0, 0, 1, 0, 1, 1, 0, 1};
		
		instructionsVertices[1] += windowHeight;
		instructionsVertices[3] += windowHeight;
		instructionsVertices[5] += windowHeight;
		instructionsVertices[7] += windowHeight;
		
		glMatrixMode(GL_PROJECTION);
		glPushMatrix();
		glLoadIdentity();
		glOrtho(0.0f, windowWidth, 0.0f, windowHeight, -1.0f, 1.0f);
		Texture_activate(instructionsTexture);
		glEnableClientState(GL_TEXTURE_COORD_ARRAY);
		glVertexPointer(2, GL_INT, 0, instructionsVertices);
		glTexCoordPointer(2, GL_INT, 0, instructionsTexCoords);
		glColor3ub(0xFF, 0xFF, 0xFF);
		glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
		glDisableClientState(GL_TEXTURE_COORD_ARRAY);
		Texture_deactivate(instructionsTexture);
		glPopMatrix();
		glMatrixMode(GL_MODELVIEW);
	}
	
	error = glGetError();
	if (error != GL_NO_ERROR) {
		puts((char *) gluErrorString(error));
	}
}

static void levelCompletedCallback(void) {
	levelCompleted = true;
}

static void diedCallback(void) {
	died = true;
}

const char * Target_getName() {
	return "Adhesion";
}

void Target_init(int argc, char ** argv) {
	windowWidth = DEFAULT_WINDOW_WIDTH;
	windowHeight = DEFAULT_WINDOW_HEIGHT;
	
	cpInitChipmunk();
	
	initGL();
	initGame();
	
	Shell_mainLoop();
}

void Target_keyDown(int charCode, int keyCode) {
	if (keyCode == KEYBOARD_R) {
		GameState_loadLevel(gameState, levelList->levels[levelIndex]);
	}
}

void Target_keyUp(int charCode, int keyCode) {
}

static float localX(float windowX) {
	float ratio;
	
	ratio = (float) windowWidth / (float) windowHeight;
	return windowX / (windowHeight * 0.5f) - ratio;
}

static float localY(float windowY) {
	return (2.0f - windowY / (windowHeight * 0.5f)) - 1.0f;
}

void Target_mouseDown(int buttonNumber, float x, float y) {
	dragging = true;
	dragStartX = dragEndX = localX(x);
	dragStartY = dragEndY = localY(y);
}

#define FORCE_MULTIPLIER 30

void Target_mouseUp(int buttonNumber, float x, float y) {
	dragging = false;
	GameState_launchPlayer(gameState, (dragStartX - dragEndX) * FORCE_MULTIPLIER, (dragStartY - dragEndY) * FORCE_MULTIPLIER);
}

void Target_mouseMoved(float x, float y) {
}

#define MAX_DISTANCE 1.5

void Target_mouseDragged(int buttonMask, float x, float y) {
	float distance;
	Vector2 dragVector;
	
	dragEndX = localX(x);
	dragEndY = localY(y);
	
	dragVector = Vector2_withValues(dragEndX - dragStartX, dragEndY - dragStartY);
	distance = sqrt(dragVector.x * dragVector.x + dragVector.y * dragVector.y);
	if (distance > MAX_DISTANCE) {
		dragVector.x /= distance;
		dragVector.y /= distance;
		dragVector.x *= MAX_DISTANCE;
		dragVector.y *= MAX_DISTANCE;
		dragEndX = dragStartX + dragVector.x;
		dragEndY = dragStartY + dragVector.y;
	}
}

void Target_resized(int newWidth, int newHeight) {
	windowWidth = newWidth;
	windowHeight = newHeight;
	glViewport(0, 0, newWidth, newHeight);
	setProjection();
}

void Target_draw() {
	runLoop->run(runLoop);
	draw();
	Shell_redisplay();
}
