#include <cmath>
#include <string>
#include <vector>
#include <iostream>

#include"allegro5/allegro5.h"
#include"allegro5/allegro_primitives.h"
#include"allegro5/allegro_image.h"
#include"allegro5/allegro_font.h"
#include"allegro5/allegro_ttf.h"

#include"stdint.h"

#define screenWidth 1280
#define screenHeight 720
#define mapWidth 24
#define mapHeight 24
#define texWidth 64
#define texHeight 64

int worldMap[mapWidth][mapHeight] =
{
	{ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,2,2,2,2,2,0,0,0,0,3,0,3,0,3,0,0,0,1 },
	{ 1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,3,0,0,0,3,0,0,0,1 },
	{ 1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,2,2,0,2,2,0,0,0,0,3,0,3,0,3,0,0,0,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,4,0,4,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,4,0,0,0,0,5,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,4,0,4,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,4,0,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 },
	{ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 }
};

bool keys[] = { false, false, false, false, false, false, false, false };
enum keys { UP, DOWN, LEFT, RIGHT, SPACE, ENTER, MOUSELEFT, MOUSERIGHT };


int main()
{
	bool done = false;
	bool redraw = false;

	int mouseX = 0;
	int mouseY = 0;

	double posX = 22, posY = 12;  //x and y start position
	double dirX = -1, dirY = 0; //initial direction vector
	double planeX = 0, planeY = 0.66; //the 2d raycaster version of camera plane

	double moveSpeed = 0.075; //the constant value is in squares/second
	double rotSpeed = 0.05; //the constant value is in radians/second

	ALLEGRO_DISPLAY *display = NULL;
	ALLEGRO_EVENT_QUEUE *eventQueue = NULL;
	ALLEGRO_TIMER *timer = NULL;

	ALLEGRO_FONT *font = NULL;
	ALLEGRO_BITMAP *titleImage = NULL;

	srand(time(NULL));

	al_init();
	al_init_primitives_addon();
	al_init_image_addon();
	al_init_font_addon();
	al_init_ttf_addon();
	al_install_mouse();
	al_install_keyboard();

	al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP);

	ALLEGRO_BITMAP* texture[6];
	ALLEGRO_LOCKED_REGION* lock[6];
	int w = screenWidth;
	int h = screenHeight;

	display = al_create_display(w, h);

	texture[0] = al_load_bitmap("redbrick.png");
	texture[1] = al_load_bitmap("eagle.png");
	texture[2] = al_load_bitmap("purplestone.png");
	texture[3] = al_load_bitmap("mossy.png");
	texture[4] = al_load_bitmap("bluestone.png");
	texture[5] = al_load_bitmap("colorstone.png");

	for (int i = 0; i < 6; i++)
		lock[i] = al_lock_bitmap(texture[i], al_get_bitmap_format(texture[i]), ALLEGRO_LOCK_READONLY);


	al_set_new_bitmap_flags(ALLEGRO_VIDEO_BITMAP | ALLEGRO_NO_PRESERVE_TEXTURE);

	ALLEGRO_BITMAP* screen = al_create_bitmap(screenWidth, screenHeight);
	ALLEGRO_LOCKED_REGION *screenlock = NULL;
	eventQueue = al_create_event_queue();
	timer = al_create_timer(1.0 / 60);

	al_register_event_source(eventQueue, al_get_timer_event_source(timer));
	al_register_event_source(eventQueue, al_get_display_event_source(display));
	al_register_event_source(eventQueue, al_get_keyboard_event_source());
	al_register_event_source(eventQueue, al_get_mouse_event_source());
	int h128 = (h * 128);
	al_start_timer(timer);
	while (!done)
	{
		ALLEGRO_EVENT ev;
		al_wait_for_event(eventQueue, &ev);

		if (ev.type == ALLEGRO_EVENT_KEY_DOWN)
		{
			switch (ev.keyboard.keycode)
			{
			case ALLEGRO_KEY_UP:
				keys[UP] = true;
				break;
			case ALLEGRO_KEY_DOWN:
				keys[DOWN] = true;
				break;
			case ALLEGRO_KEY_LEFT:
				keys[LEFT] = true;
				break;
			case ALLEGRO_KEY_RIGHT:
				keys[RIGHT] = true;
				break;
			case ALLEGRO_KEY_SPACE:
				keys[SPACE] = true;
				break;
			case ALLEGRO_KEY_ENTER:
				keys[ENTER] = true;
				break;
			}
		}
		else if (ev.type == ALLEGRO_EVENT_KEY_UP)
		{
			switch (ev.keyboard.keycode)
			{
			case ALLEGRO_KEY_UP:
				keys[UP] = false;
				break;
			case ALLEGRO_KEY_DOWN:
				keys[DOWN] = false;
				break;
			case ALLEGRO_KEY_LEFT:
				keys[LEFT] = false;
				break;
			case ALLEGRO_KEY_RIGHT:
				keys[RIGHT] = false;
				break;
			case ALLEGRO_KEY_SPACE:
				keys[SPACE] = false;
				break;
			case ALLEGRO_KEY_ENTER:
				keys[ENTER] = false;
				break;
			}
		}
		else if (ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN)
		{
			if (ev.mouse.button & 1)
			{
				keys[MOUSELEFT] = true;
			}
			else if (ev.mouse.button & 2)
			{
				keys[MOUSERIGHT] = true;
			}
		}
		else if (ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP)
		{
			if (ev.mouse.button & 1)
			{
				keys[MOUSELEFT] = false;
			}
			else if (ev.mouse.button & 2)
			{
				keys[MOUSERIGHT] = false;
			}
		}
		else if (ev.type == ALLEGRO_EVENT_MOUSE_AXES)
		{
			mouseX = ev.mouse.x;
			mouseY = ev.mouse.y;
		}
		else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE)
		{
			done = true;
		}
		else if (ev.type == ALLEGRO_EVENT_TIMER)
		{

			if (keys[RIGHT])
			{
				//both camera direction and camera plane must be rotated
				double oldDirX = dirX;
				dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed);
				dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed);
				double oldPlaneX = planeX;
				planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed);
				planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed);
			}
			else if (keys[LEFT])
			{
				//both camera direction and camera plane must be rotated
				double oldDirX = dirX;
				dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed);
				dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed);
				double oldPlaneX = planeX;
				planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed);
				planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed);
			}
			if (keys[UP])
			{
				if (worldMap[int(posX + dirX * moveSpeed)][int(posY)] == false) posX += dirX * moveSpeed;
				if (worldMap[int(posX)][int(posY + dirY * moveSpeed)] == false) posY += dirY * moveSpeed;
			}
			else if (keys[DOWN])
			{
				if (worldMap[int(posX - dirX * moveSpeed)][int(posY)] == false) posX -= dirX * moveSpeed;
				if (worldMap[int(posX)][int(posY - dirY * moveSpeed)] == false) posY -= dirY * moveSpeed;
			}

			redraw = true;
		}
		if (redraw && al_is_event_queue_empty(eventQueue))
		{
			redraw = false;
			//al_set_target_bitmap(screen);
			al_clear_to_color(al_map_rgb(0,0,0));
			al_draw_filled_rectangle(0, 0, w, h / 2, al_map_rgb(47, 79, 79));
			al_draw_filled_rectangle(0, h / 2, w, h, al_map_rgb(112, 128, 144));
			//al_set_target_bitmap(al_get_backbuffer(display));
			screenlock = al_lock_bitmap(screen, ALLEGRO_PIXEL_FORMAT_ANY, ALLEGRO_LOCK_WRITEONLY);

			for (int x = 0; x < w; ++x)
			{
				//calculate ray position and direction
				double cameraX = 2 * x / double(w) - 1; //x-coordinate in camera space
				double rayPosX = posX;
				double rayPosY = posY;
				double rayDirX = dirX + planeX * cameraX;
				double rayDirY = dirY + planeY * cameraX;
				//which box of the map we're in
				int mapX = int(rayPosX);
				int mapY = int(rayPosY);

				//length of ray from current position to next x or y-side
				double sideDistX;
				double sideDistY;

				//length of ray from one x or y-side to next x or y-side
				double deltaDistX = sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX));
				double deltaDistY = sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY));
				double perpWallDist;

				//what direction to step in x or y-direction (either +1 or -1)
				int stepX;
				int stepY;

				int hit = 0; //was there a wall hit?
				int side; //was a NS or a EW wall hit?
						  //calculate step and initial sideDist
				if (rayDirX < 0)
				{
					stepX = -1;
					sideDistX = (rayPosX - mapX) * deltaDistX;
				}
				else
				{
					stepX = 1;
					sideDistX = (mapX + 1.0 - rayPosX) * deltaDistX;
				}
				if (rayDirY < 0)
				{
					stepY = -1;
					sideDistY = (rayPosY - mapY) * deltaDistY;
				}
				else
				{
					stepY = 1;
					sideDistY = (mapY + 1.0 - rayPosY) * deltaDistY;
				}
				//perform DDA
				while (hit == 0)
				{
					//jump to next map square, OR in x-direction, OR in y-direction
					if (sideDistX < sideDistY)
					{
						sideDistX += deltaDistX;
						mapX += stepX;
						side = 0;
					}
					else
					{
						sideDistY += deltaDistY;
						mapY += stepY;
						side = 1;
					}
					//Check if ray has hit a wall
					if (worldMap[mapX][mapY] > 0) hit = 1;
				}
				//Calculate distance projected on camera direction (oblique distance will give fisheye effect!)
				if (side == 0) perpWallDist = (mapX - rayPosX + (1 - stepX) / 2) / rayDirX;
				else           perpWallDist = (mapY - rayPosY + (1 - stepY) / 2) / rayDirY;

				//Calculate height of line to draw on screen
				int lineHeight = (int)(h / perpWallDist);

				//calculate lowest and highest pixel to fill in current stripe
				int drawStart = -lineHeight / 2 + h / 2;
				if (drawStart < 0)drawStart = 0;
				int drawEnd = lineHeight / 2 + h / 2;
				if (drawEnd >= h)drawEnd = h - 1;

				//texturing calculations
				int texNum = worldMap[mapX][mapY] - 1; //1 subtracted from it so that texture 0 can be used!

													   //calculate value of wallX
				double wallX; //where exactly the wall was hit
				if (side == 0) wallX = rayPosY + perpWallDist * rayDirY;
				else           wallX = rayPosX + perpWallDist * rayDirX;
				wallX -= (int)((wallX));

				//x coordinate on the texture
				int texX = int(wallX * double(texWidth));
				if (side == 0 && rayDirX > 0) texX = texWidth - texX - 1;
				if (side == 1 && rayDirY < 0) texX = texWidth - texX - 1;


				unsigned char r;
				unsigned char g;
				unsigned char b;
				unsigned char a;

				//al_set_target_bitmap(buffer);
				int xPos = 4 * x;
				int yMul = 0;
				int y256 = 0;
				int line128 = lineHeight * 128;
				uint8_t *ptr = (uint8_t *)screenlock->data + xPos;
				for (int y = 0; y < h; ++y)
				{
					
					if (y >= drawStart && y < drawEnd)
					{
						int d = y256 - h128 + line128;  //256 and 128 factors to avoid floats
						int texY = ((d * texHeight) / lineHeight) / 256;

						ALLEGRO_COLOR c = al_get_pixel(texture[texNum], texX, texY);
						al_unmap_rgba(c, &r, &g, &b, &a);

						*((uint32_t *)ptr) = (b | (g << 8) | (r << 16) | (a << 24));
					}
					else
					{
						*((uint32_t *)ptr) = 0;
					}
					ptr += screenlock->pitch;
					y256 += 256;
				}
			}
			al_unlock_bitmap(screen);
			al_draw_bitmap(screen, 0, 0, 0);
			al_flip_display();
		}
	}

	al_destroy_bitmap(texture[0]);
	al_destroy_bitmap(texture[1]);
	al_destroy_bitmap(texture[2]);
	al_destroy_bitmap(texture[3]);
	al_destroy_bitmap(texture[4]);
	al_destroy_bitmap(texture[5]);

	al_destroy_display(display);
	al_destroy_event_queue(eventQueue);
	al_destroy_timer(timer);

	return 0;
}
