#if defined(_WIN32)	           
	#define NOGDI             // All GDI defines and routines
	#define NOUSER            // All USER defines and routines
	#define WIN32_LEAN_AND_MEAN
#include <windows.h> // or any library that uses Windows.h	
#endif



#if defined(_WIN32)           // raylib uses these names as function parameters
	#undef near
	#undef far
#endif


#include <iostream>
#include <cmath>
#include <cstring>
#include <ctime>
#include <sstream>
#include "includes/raylib-cpp/raylib-cpp.hpp"
#include "includes/paddle.hpp"
#include "includes/ball.hpp"
#include "includes/easysock.hpp"
#include "includes/sign.hpp"
#include "includes/connect_code.hpp"
#include "includes/client.hpp"
#include "includes/server.hpp"
#include "includes/exception_consts.hpp"
#include "includes/serialization.h"

/* Global variables used to instantiate structs */
const int WIDTH = 1500;
const int HEIGHT = 600;
const int RECT_H = HEIGHT / 3;
const int RECT_W = 30;
const int PADDLE_SPEED = 8;
const int CIRC_RAD = 10;
const float BASE_BOUNCE_DEG = 45;
const float BASE_BOUNCE_RAD = (BASE_BOUNCE_DEG / 180.0) * M_PI;
const float BASE_SPEED_COMPONENTS = 18;
const float BASE_SPEED = sqrt(powf(BASE_SPEED_COMPONENTS, 2) * 2);

typedef enum {M_SINGLE, M_CLIENT, M_SERVER} Mode;

/* This struct contains a Mode enum, which indicates the type of game we are
   playing (Single player, client mode or server mode). The netsock parameter is
   a 'Sock' object - Client and Server classes inherit from this object, so this
   parameter can be instantiated to either a client or server, depending on the
   game type. */

typedef struct {
	Mode mode;
	Sock* netsock;
} GameType;

raylib::Vector2 changeVelocityAfterCollision(Paddle paddle, Ball ball) {
	float paddle_mid_y = (paddle.getRect().y + paddle.getRect().GetHeight()) / 2.0; /* Middle y value of rectangle */
	float  ball_y = ball.pos.y; /* Y co-ordinate of ball */

	float offset = paddle_mid_y - ball_y; /* Subtracting the ball coordinate will give us a value between -paddle_mid_y (represents bottom of paddle) and +paddle_mid_y (represents top of paddle) */
	offset /= (paddle.getRect().GetHeight()); /* Normalize the value, by dividing it by its maximum magnitude. It is now a value between -1 and 1. */

	offset *= 0.8 + (float)(std::rand()) / (float) (RAND_MAX / ( 1.2 - 0.8)); // Generate a random float from 0.8 to 1.2

	float bounce_angle = offset * BASE_BOUNCE_RAD; /* Calculate the actual bounce angle from the base bounce angle. */

	/* Calculate new velocities as multiples of the original velocity. I use sine and cosine, because when the ball hits the paddle
	perpendicular to it (bounce angle is 0), the y_velocity should be 0 (i.e. It should bounce straight back). The sin function does
	this for us. A similar reasoning was employed for the use of cosine */
	float new_x_vel = abs(BASE_SPEED * cosf(bounce_angle)) * (-1 * signum(ball.vel.x)); /* Reverse the sign of the x-velocity */
	float new_y_vel = abs(BASE_SPEED * sinf(bounce_angle)) * signum(ball.vel.y); /* Keep the sign of the y-velocity */

	return raylib::Vector2(new_x_vel, new_y_vel);
}

/* This function checks the command-line arguments passed to the program.
It then decides whether the game is in Server or Client mode (or neither), and
instantiates the appropriate object. The (uninitialized) objects are passed to the
function as pointers. It returns a GameType struct, that indicates whether the game
is in server, client or single player mode, and contains the appropriate socket object. */

GameType check_server_client(int argc, char** argv) {
	std::string connect_code;
	std::vector<std::string> addr_port; /* Vector to store (IPv4) address and port */
	GameType type;

	if (argc < 2) { /* Game was not started in client or server mode */
		type.mode = M_SINGLE;
		type.netsock = nullptr;
		return type;
	}

	/* GAME STARTED IN CLIENT MODE */
	if (strcmp(argv[1],"-C") == 0) {
		if (argc < 3) { /* No address was provided */
			throw EXCEPT_TOOFEWARGS;
		}
		connect_code = std::string(argv[2]); /* The connect code is a special string, that contains the server address and port. It is given by the server. */
		try {
			addr_port = connect_code::decode(connect_code);
			Client* client = new Client(4, ES_UDP, addr_port[0].data(), std::stoi(addr_port[1]));
			client->create_socket();
			/* Send a specific message to the server, and wait for the appropriate response, to know that the server is ready */
			client->sendAll("GG");
			std::string msg_from_server = client->recvAll();
			if (msg_from_server == "U2") {
				std::cout << "Connection made. Waiting for server to begin game..." << std::endl;
			} else {
				throw EXCEPT_WRONGRESPONSE;
			}
			type.mode = M_CLIENT;
			type.netsock = client;
			return type;
		} catch (int e) {
			throw;
		} catch (std::exception& e) {
			throw;
		}
	}

	/* GAME STARTED IN SERVER MODE */
	else if (strcmp(argv[1],"-S") == 0) {
		std::string addr;
		uint16_t port;

		/* No IP address or port specified */
		if (argc < 3) {
			throw EXCEPT_TOOFEWARGS;
		}

		/* IP address but no port */
		else if (argc < 4) {
			std::cout << "No port specified, using 6500..." << std::endl;
			addr = std::string(argv[2]);
			port = 6500;
		} else {
			addr = std::string(argv[2]);
			port = std::stoi(std::string(argv[3]));
		}

		/* Check if IP is valid */
		if (check_ip_ver(addr.data()) < 0) {
			throw EXCEPT_INVALIDIP;
		}

		std::string code = connect_code::encode(addr, std::to_string(port));
		std::cout << "Your code is " << code << std::endl;

		/* Create server socket and wait for client to connect */
		Server* server = new Server(4, ES_UDP, addr.data(), port);
		server->create_socket();
		std::cout << "Waiting for connection..." << std::endl;
		std::string response = "";
		char* temp_response = NULL;
		/* Wait for the right client to connect. Since recvAll returns a char*, we need to create a temporary variable to check for NULL. */
		do {
			temp_response = server->recvAll();
		} while (temp_response == NULL);
		response = std::string(temp_response);

		std::cout << "Connection received from " << server->get_peer_addr() << std::endl;
		server->sendAll("U2");
		type.mode = M_SERVER;
		type.netsock = server;
		return type;
	}

	else {
		throw EXCEPT_INVALIDARGS;
	}

}

int main(int argc, char** argv) {
	/* Check if game was started in server or client mode, and set appropriate variables */

	/* GameType struct, to define whether the game is in single or muilti-player mode, and
	to hold the appropriate socket */
	GameType type;

	try {
		type = check_server_client(argc, argv);
	} catch(int e) {
		if (e == EXCEPT_TOOFEWARGS) {
			std::cout << "Started in client mode, but no address was specified." << std::endl;
			return -1;
		}
		if (e == EXCEPT_INVALIDARGS) {
			std::cout << "Invalid argument." << std::endl;
			return -2;
		}
		if (e == EXCEPT_INVALIDIP) {
			std::cout << "Invalid IP address provided." << std::endl;
			return -5;
		}
		if (e == EXCEPT_WRONGRESPONSE) {
			std::cout << "The server didn't respond with the correct message. Are you sure you have used the right server?" << std::endl;
			return -6;
		}
		else {
			std::cout << strerror(e) << std::endl;
			return -7;
		}
	} catch(std::invalid_argument& inv) {
			std::cout << inv.what() << std::endl;
			return -8;
	}

	/* Initialize window and other variables */
	SetTraceLogLevel(LOG_NONE);
	raylib::Window window = raylib::Window(WIDTH, HEIGHT, "Pong");
	window.ClearBackground(BLACK);
	SetTargetFPS(10);
	SetExitKey(KEY_Q);
	std::string points_str = std::string("0\t\t0");
	bool game_started = false;
	srand(std::time(NULL));

	/* Variable to store the response given by the other player */
	std::string response;
	Serial_Data response_data;
	/* Vector to store peer paddle position */
	raylib::Vector2 peer_pos;
	
	/* Byte array to hold the result of serializing a struct (in order to send it through a socket) */
	Serial_Data to_send_data;
	std::string to_send_string;
	
	/* Instantiate Paddle and Ball objects */
	Paddle pad1 = Paddle(10, (HEIGHT / 2) - (RECT_H / 2), RECT_W, RECT_H);
	Paddle pad2 = Paddle(window.GetWidth() - RECT_W - 10, (HEIGHT / 2) - (RECT_H / 2), RECT_W, RECT_H);
	Ball ball = Ball(window.GetWidth()/2, window.GetHeight()/2, CIRC_RAD, BASE_SPEED, 0);

	window.BeginDrawing();
		pad1.draw();
		pad2.draw();
		ball.draw();
	window.EndDrawing();

	/* Main loop */
	while (!window.ShouldClose()) {

		if (!game_started) {
			/* For the server, or if game is being played in single-player mode */
			if ((type.mode == M_SERVER || type.mode == M_SINGLE) && IsKeyDown(KEY_SPACE)) {
				game_started = true;
				/* Send a start message to the client */
				if (type.mode == M_SERVER) {
					type.netsock->sendAll("S");
				}
			}

			/* For client (wait for start message from server) */
			if (type.mode == M_CLIENT) {
				do {
					response = type.netsock->recvAll();
				} while (response[0] != 'S');
				game_started = true;
				std::cout << "Game has been started by server." << std::endl;
			}
		}

		if (game_started) {
			/* Serialize the data that we need to send, and then send it to the peer paddle */
			if (type.mode == M_SERVER) {
				/* Serial_create_data creates a Serial_Data struct from our values, and Serial_serialize serializes it Paddle 2 is controled by the server, Paddle 1, by the client.*/
				to_send_data = Serial_create_data(pad2.getRect().x, pad2.getRect().y, ball.pos.x, ball.pos.y);
			}
			else if (type.mode == M_CLIENT) {
				/* The _server_ is the authoritative peer for the ball position, so the client sends (0, 0) as the ball position instead of actually sending a position */
				to_send_data = Serial_create_data(pad1.getRect().x, pad1.getRect().y, 0, 0);
			}
			/* Only send and receive data if the game is not in single player mode */
			if (type.mode != M_SINGLE) {
				
				type.netsock->sendAll((char *)Serial_serialize(to_send_data), 9);	
				/* Create Serial_data struct from the response of the server. Since recvAll returns a char*, we need to convert it to a byte array */
				uint8_t* response_array = (uint8_t *)(type.netsock->recvAll());
				if (response_array != NULL) {
				/* If the response is NULL, that means it timed-out. In this case, there's no value to print */
					response_data = Serial_deserialize(response_array);
					std::cout << response_data.pad_x << "\t" << response_data.pad_y << "\t" << response_data.ball_x << "\t" << response_data.ball_y << std::endl;
				} else {
					std::cout << "NOTHING RECEIVED" << std::endl;
				}

			}
			
			/* When updating the paddle positions, update the peer paddle's positions based on the vector set earlier */

			std::string to_send = "";
			/* Update paddle velocity */

			/* Left paddle (controlled by client) - I use type.mode != M_SERVER, because I also want the single player 
			mode to be able to control the paddle. Therefore, the only mode that _can't_ control the paddle is the server
			mode. */
			/* Down motion */
			if (IsKeyPressed(KEY_S) && type.mode != M_SERVER) {
				pad1.velocity.y = PADDLE_SPEED; /* Set positive (downward) velocity, since (0,0) is top-left */
			}

			/* Up motion */
			if (IsKeyPressed(KEY_W) && type.mode != M_SERVER) {
				pad1.velocity.y = (-1) * PADDLE_SPEED; /* Set negative (upward) velocity */
			}

			/* Stop */
			if (((IsKeyReleased(KEY_S) || IsKeyReleased(KEY_W))) && (type.mode != M_SERVER)) {
					pad1.velocity.y = 0;
			}


			/* Right paddle - controlled by server - See above for why I used '!= M_CLIENT' instead of '== M_SERVER' */
			/* Down */
			if (IsKeyPressed(KEY_DOWN) && type.mode != M_CLIENT) {
				pad2.velocity.y = PADDLE_SPEED;
			}

			/* Up */
			if (IsKeyPressed(KEY_UP) && type.mode != M_CLIENT) {
				pad2.velocity.y = (-1) * PADDLE_SPEED;
			}

			/* Stop */
			if ((IsKeyReleased(KEY_UP) || IsKeyReleased(KEY_DOWN)) && type.mode != M_CLIENT) {
				pad2.velocity.y = 0;
			}

			/* The client should set the ball position based on the data sent by the server. It doesn't have to do any calculations of its own. */
			if (type.mode != M_CLIENT) {
				/* Update ball velocity based on collision detection */
				if (pad1.getRect().CheckCollision(ball.pos, ball.radius)) { /* Collision with paddle 1 */
					ball.pos.x = pad1.getRect().x + pad1.getRect().GetWidth() + ball.radius + 1; /* Ensuring that the ball doesn't get stuck inside the paddle */
					ball.vel = changeVelocityAfterCollision(pad1, ball);
				}
				if (pad2.getRect().CheckCollision(ball.pos, ball.radius)) { /* Collision with paddle 2 */
					ball.pos.x = pad2.getRect().x - ball.radius - 1;
					ball.vel = changeVelocityAfterCollision(pad2, ball);
				}
			} else {
				ball.setPosition(raylib::Vector2(response_data.ball_x, response_data.ball_y));
			}
			
			if (ball.pos.x + ball.radius >= window.GetWidth()) { /* Collision with right wall */
				pad1.incrementPoints();
				game_started = false;
				ball.reset();
				pad1.reset();
				pad2.reset();
			}
			if (ball.pos.x - ball.radius <= 0) { /* Collision with left wall */
				pad2.incrementPoints();
				game_started = false;
				ball.reset();
				pad1.reset();
				pad2.reset();
			}
			if (ball.pos.y - ball.radius <= 0) { /* Collision with top wall */
				ball.pos.y = ball.radius + 1;
				ball.vel.y = ball.vel.y * -1;
			}

			if (ball.pos.y + ball.radius >= window.GetHeight()) { /* Collision with bottom wall */
				ball.pos.y = HEIGHT - ball.radius - 1;
				ball.vel.y = ball.vel.y * -1;
			}

			/* Update positions based on velocities - Client only updates pad1, server updates pad2 and ball */
			if (type.mode != M_CLIENT) {
				ball.updatePosition();
				pad2.updatePosition();
			} else {
				pad2.setPosition(response_data.pad_x, response_data.pad_y);
			}
			if (type.mode != M_SERVER) {
				pad1.updatePosition();
			} else {
				pad1.setPosition(response_data.pad_x, response_data.pad_y);
			}
		}

		/* Draw objects */
		window.BeginDrawing();
			window.ClearBackground(BLACK);
			points_str = std::to_string(pad1.getPoints()) + "\t\t" + std::to_string(pad2.getPoints());
			raylib::Text::Draw( points_str, (WIDTH / 2) - 30, HEIGHT / 10, 30, raylib::Color::White() );
			pad1.draw();
			pad2.draw();
			ball.draw();
		window.EndDrawing();
	}

	window.Close();
	sock_quit();

	return 0;
}
