// Server - the network hub for enabling online games

#include <stdexcept>
#include <iostream>
#include <memory>
#include <chrono>
#include <thread>
#include <cstdint>
#include <cstring>
#include <vector>
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <arpa/inet.h>
#include <evhttp.h>

#include <unordered_map>
#include <map>
#include <set>
#include <random>
#include <chrono>
#include <algorithm>

#include "core.h"
#include "sha1.h"

static std::random_device random_dev;
static std::mt19937 mtwister(random_dev());
static std::uniform_int_distribution<uint32_t> dist32(1, 4294967295);
typedef std::chrono::high_resolution_clock HighClock;
typedef std::chrono::high_resolution_clock::time_point TimePoint;

struct ConnState {
	// connection state flags
	uint32_t connf;
	uint32_t cpid; // connection's player id once it becomes available
	struct sockaddr address;
	TimePoint last_active; // this is used to watch for timeouts
};

// a 6x6 box of cells, needed to localize players inside the room
struct Block {
	std::set<uint32_t> bpids; // block's map of pids
};

// room contains information about the maze and players inside it
struct Room {
	uint32_t rid; // room id
	bool is_closing; // is this room closing
	std::vector<uint8_t> rmaze; // the actual maze data: each cell is a byte
	uint32_t rwidth; // how wide is this room in cells?
	std::vector<Block> rblocks; // room is logically split into blocks,
		// the room blocks do not affect anything: they are just there
		// to optimize access to all the players inside it
};

// player's position iside room is counted from the bottom left cell's bottom
// left corner. each cell is 1000 units wide and high
struct Player {
	uint32_t pid; // player id
	uint32_t prid; // player's current room id
	uint16_t pxp; // player's experience points (how many rooms completed)
	bool looped; // whether player has already looped in this room
	struct sockaddr address; // player's address and socket
	std::array<int32_t, 2> pos; // player's pos inside room, 1cell = 1000
	std::array<int32_t, 2> vel; // player's velocity, units per second
	ConnState *connst; // connection associated with this player or nullptr
	TimePoint last_active; // updated only when the player gets disconnected
};

struct ServerState {
	event_base *eb;
	std::map<uint64_t, ConnState> connst; // all active connections
	std::map<uint32_t, Room> rooms; // all active rooms on this server
	std::map<uint32_t, Player> players; // all known players
	std::vector<Room *> campaign; // rooms ordered by size
	uint32_t desired_dispatch_interval_us; // desirable microseconds between
		// dispatch events. when server is overloaded we must skip.
	// time keeping to skip dispatch events to that they don't pile up
	TimePoint prev_dispatch_start, prev_dispatch_end,
			dispatch_start, dispatch_end;
};

// make a room of given size and generate a maze, returns the created room
static Room *mkroom(ServerState &sest, uint32_t width) {
	uint32_t room_id = dist32(mtwister);
	auto res = sest.rooms.emplace(room_id, Room());
	if(!res.second) {
		std::cout << "Error creating room\n";
		return 0;
	}
	Room &room = res.first->second;
	uint32_t ncells = width * width;
	room.rid = room_id;
	room.rwidth = width;
	uint32_t nblocks = ((width / 6) + 1) * ((width / 6) + 1);
	room.rmaze.resize(ncells, 0);
	std::vector<uint8_t> &cell = room.rmaze; // convenience alias

	std::uniform_int_distribution<uint32_t> customdist(0, ncells - 1);
	uint32_t cur = customdist(mtwister);
	std::vector<uint32_t> stack;

	stack.push_back(cur); // add first cell to the stack
	std::vector<uint8_t> visitorder = { 1, 2, 4, 8 };

	while(stack.size()) {
		std::shuffle(visitorder.begin(), visitorder.end(), mtwister);
		bool visited = false; // set to true once visited a cell
		for(uint8_t i = 0; i < 4; ++i) {
			// try to visit selected cell
			switch(visitorder[i]) {
			case 1:
				// try to visit cell above
				if(cur + width >= ncells || cell[cur + width]) {
					// can't visit cell above
					break;
				}
				cell[cur] |= 1; // remove northern wall
				cur += width; // go north
				cell[cur] |= 4; // remove southern wall
				// in reality southern and western walls
				// are redundant, but can simplify some checks.
				// they are not needed for replicating the maze
				stack.push_back(cur);
				visited = true;
				break;
			case 2:
				// going east
				if(cur % width == width - 1 || cell[cur + 1]) {
					break;
				}
				cell[cur] |= 2;
				++cur;
				cell[cur] |= 8;
				stack.push_back(cur);
				visited = true;
				break;
			case 4:
				// going south
				if(cur - width > ncells || cell[cur - width]) {
					break;
				}
				cell[cur] |= 4;
				cur -= width;
				cell[cur] |= 1;
				stack.push_back(cur);
				visited = true;
				break;
			case 8:
				// going west
				if(!(cur % width) || cell[cur - 1]) {
					break;
				}
				cell[cur] |= 8;
				--cur;
				cell[cur] |= 2;
				stack.push_back(cur);
				visited = true;
				break;
			}
			if(visited) {
				break;
			}
		}
		if(!visited) {
			cur = stack.back();
			stack.pop_back();
		}
	}

	cell[cur] |= 16; // add a portal

	for(int i = 0; i < ncells; ++i) {
		std::cout << uint16_t(uint8_t(cell[i])) << " ";
	}
	std::cout << std::endl;


	return &room;
}

// we must always call this function instead of bufferevent_free() directly,
// because we also need to unlink the associated connection state and
// mark corresponding player as disconnected
static void disconnect(bufferevent *bev, ServerState &sest) {
	ConnState &cs = sest.connst[uint64_t(bev)];
	cs.connf = Connf::FAILED;
	if(cs.cpid) {
		// connection lost, but the player can still reconnect
		// might be good idea to serialize player to disk here
		Player &player = sest.players[cs.cpid];
		player.connst = nullptr;
		player.last_active = HighClock::now();
	}
	bufferevent_free(bev);
	// we can now delete connection state associated with this bufferevent
	sest.connst.erase(uint64_t(bev));
}

// check if string starts with given prefix
static bool starts_with(const char *str, const char *pre) {
	return strncmp(pre, str, strlen(pre)) == 0;
}

// send a websocket ping message
static void wsping(bufferevent *bev) {
	unsigned char buf[32], *cur;
	int nbytes;
	cur = buf;
	*(cur++) = 0x89;
	*(cur++) = 0x0;
	nbytes = bufferevent_write(bev, buf, cur-buf);
}

static void wspong(bufferevent *bev) {
	unsigned char buf[32], *cur;
	int nbytes;
	cur = buf;
	*(cur++) = 0x8a;
	*(cur++) = 0x0;
	nbytes = bufferevent_write(bev, buf, cur-buf);
}

static void wstext(bufferevent *bev, std::string &str) {
	int nbytes = str.size();
	std::string output;
	output.push_back(0x81);
	if(nbytes > 125) {
		output.push_back(126);
		output.push_back(nbytes >> 8);
		output.push_back(nbytes & 0xff);
	} else {
		output.push_back(uint8_t(nbytes));
	}
	output += str;
	std::cout << "Nbytes = " << nbytes << " output.size() = " <<
			output.size() << "\n" << output << "\n";
	nbytes = bufferevent_write(bev, output.c_str(), output.size());
}

// accepts connection initiated by client, returns true on success
bool wsconnect(bufferevent *bev, const char *inbuf, size_t inbuflen) {
	const char *cur = strstr(inbuf, "Sec-WebSocket-Key");
	if(!cur || (cur - inbuf + 47) > inbuflen) {
		return false;
	}

	char c_key[25]; // key sent by client
	strncpy(c_key, cur + 19, 24);
	c_key[24] = '\0';
	std::cout << "Client key = '" << c_key << "' " << std::endl;

	// generate Sec-WebSocket-Accept key
	std::string source = std::string(c_key) +
			"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
	std::string hash;

	SHA1 checksum;
	checksum.update(source);
	hash = checksum.final_bin();

	std::cout << std::endl;

	std::string hash64 = base64_encode(hash);

	std::string reply = "HTTP/1.1 101 Switching Protocols\r\n"
			"Upgrade: websocket\r\n"
			"Connection: Upgrade\r\n"
			"Sec-WebSocket-Accept: " + hash64 + "\r\n\r\n";
	size_t nbytes = bufferevent_write(bev, reply.c_str(), reply.size());

	wsping(bev);

	return true;
}

// unmask the message in place and returns offset to it or zero if error
static size_t unmask(std::vector<char> &inbytes) {
	size_t offset = 0;
	if(inbytes.size() < 6) {
		return 0;
	}
	bool ismasked = inbytes[1] & 0x80;
	// determine offset
	size_t datalen = uint8_t(inbytes[1]) & 0x7f;
	if(!datalen) {
		std::cout << "  pong received" << std::endl;
		return 0;
	}
	if(datalen == 126) {
		datalen = uint8_t(inbytes[2]) << 8 | uint8_t(inbytes[3]);
		offset = ismasked? 8: 4;
	} else if(datalen == 127) {
		if(inbytes.size() < 10) {
			return 0;
		}
		datalen = uint64_t(uint8_t(inbytes[2])) << 56 |
			uint64_t(uint8_t(inbytes[3])) << 48 |
			uint64_t(uint8_t(inbytes[4])) << 40 |
			uint64_t(uint8_t(inbytes[5])) << 32 |
			uint64_t(uint8_t(inbytes[6])) << 24 |
			uint64_t(uint8_t(inbytes[7])) << 16 |
			uint64_t(uint8_t(inbytes[8])) << 8 |
			uint8_t(inbytes[9]);
		offset = ismasked? 14: 10;
	} else {
		offset = ismasked? 6: 2;
	}

	if(datalen + offset + 1 != inbytes.size()) {
		std::cout << "! weird packet received: " << datalen << " + "
			<< offset << " != " << inbytes.size() << std::endl;
		return 0;
	}

	if(ismasked) {
		// unmask message
		for(int i = 0; i < datalen; ++i) {
			inbytes[i + offset] ^= inbytes[(i % 4) + offset - 4];
		}
	}

	return offset;
}

static void onsocketread(struct bufferevent *bev, void *ptr) {
	ServerState &sest = *((ServerState *)ptr);

	sest.connst[(uint64_t)bev].last_active = HighClock::now();

	evbuffer *input = bufferevent_get_input(bev);
	size_t evbuflen = evbuffer_get_length(input);

	std::vector<char> inbytes(evbuflen + 1);
	char *inbuf = inbytes.data(), *cur = inbuf;
	size_t nbytes = evbuffer_copyout(input, inbuf, evbuflen);
	inbytes[evbuflen] = '\0';
	evbuffer_drain(input, nbytes);

	std::cout << "[[[[[ Incoming Request: \n" <<
			inbuf << "\n";


	// fetch the connection state
	ConnState &cs = sest.connst[(uint64_t)bev];

	if(cs.connf == Connf::RESET) {
		// new connection, expecting WebSocket handshake
		if(strstr(inbuf, "Upgrade: websocket") &&
				strstr(inbuf, "Connection: Upgrade")) {

			// check if valid player id is provided
			const char *cur = strstr(inbuf, "pid=");
			if(!cur) {
				std::cout <<
				"Websocket connection request missing pid=\n";
				return;
			}

			uint32_t player_id = atoi(cur + 4);
			// check if player was already added
			auto res = sest.players.find(player_id);
			if(res == sest.players.end()) {
				// player not created - error
				std::cout << "Someone is messing with pid\n";
				disconnect(bev, sest);
				return;
			}

			// check if player with this pid has already connected
			Player &player = res->second;
			if(player.connst) {
				// player with this id is already connected
				std::cout <<
				"Player with this pid already connected\n";
				disconnect(bev, sest);
				return;
			}

			if(wsconnect(bev, inbuf, nbytes)) {
				cs.connf |= Connf::WSREQUESTED;
				cs.connf |= Connf::WSACCEPTED;
				// link the player to this connection
				player.connst = &cs;
				cs.cpid = player_id;
				player.address = cs.address;
				// we need to assign the player to a room
				player.prid = sest.campaign[0]->rid;
				player.looped = false;
				// if connection goes south, we must set
				// player's connst back to nullptr,
				// and make it possible for player to reconnect
			} else {
				disconnect(bev, sest);
			}
		} else {
			// search for magic header
			char *header = strstr(inbuf, "canijoinpls: ");
			uint32_t requested; // requested character colors
				// as 2 bytes of uint16_t: one byte per color
			if(header && (header - inbuf) + 14 < nbytes) {
				requested = atoi(header + 13);
			} else {
				std::cout << "Wrong greeting message\n";
				cs.connf = Connf::FAILED;
				disconnect(bev, sest);
				return;
			}
			// generate a random player id, send it to the user
			// and create a local player
			uint32_t player_id = (dist32(mtwister) << 16) |
							(requested & 0xffff);
			auto res = sest.players.emplace(
						player_id, Player());
			if(!res.second) {
				std::cout << "Error creating player\n";
				disconnect(bev, sest);
				return;
			}
			Player &player = res.first->second;
			// we just inserted new player, fill out the form
			player.pid = player_id;
			player.prid = 0;
			player.pxp = 0;
			player.pos = { 0, 0 };
			player.connst = nullptr;

			std::string msgbody = "Hello, " +
					std::to_string(player_id);
			std::cout << " [Http request] "  << inbuf;
			std::string reply = "HTTP/1.1 200 OK\r\n"
				"Content-Type: text/html\r\n"
				"Content-length: " +
					std::to_string(msgbody.size()) + "\r\n"
				"Connection: Closed\r\n\r\n" + msgbody;
			bufferevent_write(bev, reply.c_str(), reply.size());
			// we use Connf::DISCONNECTING, only when we are sending
			// something to the client and want to close connection
			// not before the message is sent
			cs.connf = Connf::DISCONNECTING;
		}
	} else if(cs.connf & Connf::WSACCEPTED) {
		std::cout << "read " << nbytes << " bytes: " << std::hex;
		for(int i = 0; i < nbytes; ++i) {
			std::cout << uint16_t(uint8_t(inbytes[i])) << " ";
		}

		std::cout << " inbytes[0] = " << uint16_t(uint8_t(inbytes[0]));
		std::cout << std::endl;

		if(uint8_t(inbytes[0]) == 0x89) {
			std::cout << "Ping received. Sending pong.\n";
			wspong(bev);
			return;
		}

		// unmask the message in-place
		size_t offset = unmask(inbytes);
		std::cout << (inbuf + offset) << std::endl;

		// perform the action
		char c = *(inbuf + offset);
		if(cs.cpid) {
			Player &player = sest.players[cs.cpid];
			switch(c) {
			case 'u': player.vel[1] = 300; break;
			case 'l': player.vel[0] = -300; break;
			case 'r': player.vel[0] = 300; break;
			case 'd': player.vel[1] = -300; break;
			case 'U': player.vel[1] = 0; break;
			case 'L': player.vel[0] = 0; break;
			case 'R': player.vel[0] = 0; break;
			case 'D': player.vel[1] = 0; break;
			}
		}
	}

//	std::cout << inbytes.data();
}

static void onsocketwrite(bufferevent *bev, void *ptr) {
	ServerState &sest = *((ServerState *)ptr);

	ConnState &cs = sest.connst[(uint64_t)bev];
	if(cs.connf & Connf::DISCONNECTING) {
		std::cout << "Disconnecting a socket...\n";
		disconnect(bev, sest);
	}
}

static void onsocketevent(struct bufferevent *bev, short events, void *ptr) {
	ServerState &sest = *((ServerState *)ptr);

	if(events & BEV_EVENT_ERROR) {
		perror("Error from bufferevent");
		disconnect(bev, sest);
	}
	if(events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
		std::cout << "Suddenly disconnected: error 0x" << std::hex
				<< events << std::endl;
		disconnect(bev, sest);
	}
}

static void onconnect(struct evconnlistener *listener, evutil_socket_t fd,
		struct sockaddr *address, int socklen, void *ptr) {
	ServerState &sest = *((ServerState *)ptr);

	// incoming connection: set up a bufferevent for it
	struct event_base *base = evconnlistener_get_base(listener);
	struct bufferevent *bev = bufferevent_socket_new(
			base, fd, BEV_OPT_CLOSE_ON_FREE);

	// add this connection to the connst map
	sest.connst[(uint64_t)bev] = ConnState{ Connf::RESET, 0, *address,
			HighClock::now() };

	sockaddr addr = *address;
	sockaddr_in *sin = (sockaddr_in *)(&addr);
	std::cout << sin->sin_port << " << PORT" << std::endl;

	bufferevent_setcb(bev, onsocketread, onsocketwrite, onsocketevent, ptr);

	bufferevent_enable(bev, EV_READ | EV_WRITE);
}

// this function goes through all connections and sends a message to each client
static void dispatch(evutil_socket_t fd, short what, void *ptr) {
	ServerState &sest = *((ServerState *)ptr);

	sest.prev_dispatch_start = sest.dispatch_start;
	sest.prev_dispatch_end = sest.dispatch_end;

	sest.dispatch_start = HighClock::now();

	double dt = std::chrono::duration_cast<std::chrono::milliseconds>(
		sest.dispatch_start - sest.prev_dispatch_start).count()
		/ 1000.0;

	std::map<uint64_t, ConnState>::iterator it;
	for(it = sest.connst.begin(); it != sest.connst.end(); ++it) {
		ConnState &cs = it->second;
		if(!cs.cpid) {
			// todo: add timeout checks and maybe close connection
			continue;
		}
		struct bufferevent *bev = (struct bufferevent *)(it->first);
		Player &player = sest.players[cs.cpid];
		Room &room = sest.rooms[player.prid];

		if(!player.pos[0] && !player.pos[1]) {
			// player just entered the room, we need to place
			// him/her randomly.
			std::uniform_int_distribution<uint32_t> customdist(
					1, room.rwidth * 1000 - 1);
			player.pos[0] = customdist(mtwister);
			player.pos[1] = customdist(mtwister);
		} else {
			// update player position
			float x = float(player.pos[0]) + float(player.vel[0]) *
				dt;
			float y = float(player.pos[1]) + float(player.vel[1]) *
				dt;
			// do the checks

			std::cout << "Updating position " << dt << " " << y << std::endl;

			player.pos[0] = int32_t(x);
			player.pos[1] = int32_t(y);
		}

		// serialize surrounding cells from bottom left to right, up
		std::string roomdata;
		int px = player.pos[0] / 1000;
		int py = player.pos[1] / 1000;
		for(int j = -3; j < 4; ++j) {
			for(int i = -3; i < 4; ++i) {
				if(px + i < 0 || px + i > room.rwidth - 1) {
					roomdata.push_back(0x20);
					continue;
				}
				if(py + j < 0 || py + j > room.rwidth - 1) {
					roomdata.push_back(0x20);
					continue;
				}
				roomdata.push_back(room.rmaze[
					px + i + ((py + j) * room.rwidth)]
					+ 0x20);
			}
		}

		// serialize player pos, vel, pose, scolor, pcolor, glow
		// pos is relative to the chunk of the map of course,
		// origin = left bottom corner
		roomdata += std::to_string(player.pos[0] % 1000 + 3000) + ',' +
			std::to_string(player.pos[1] % 1000 + 3000) + ',' +
			std::to_string(player.vel[0]) + ',' +
			std::to_string(player.vel[1]) + ',' +
			std::to_string((player.pid >> 8) & 0xff) + ',' +
			std::to_string((player.pid) & 0xff) + ',' +
			std::to_string(int(player.looped)) + ';';

		wstext(bev, roomdata);
	}

	sest.dispatch_end = HighClock::now();

}

static void accept_error_cb(struct evconnlistener *listener, void *ptr) {
        struct event_base *base = evconnlistener_get_base(listener);
        int err = EVUTIL_SOCKET_ERROR();
        printf("Got an error %d (%s) on the listener. "
                "Shutting down.\n", err, evutil_socket_error_to_string(err));

        event_base_loopexit(base, NULL);
}

int main() {
	ServerState sest;
	sest.prev_dispatch_start = sest.prev_dispatch_end
		= sest.dispatch_start = sest.dispatch_end = HighClock::now();

	sest.campaign.push_back(mkroom(sest, 10));

	// server related configuration
	std::uint16_t const SrvPort = 50200;

#ifdef _WIN32
	WSADATA wsa_data;
	WSAStartup(0x0201, &wsa_data);
#endif

	evconnlistener *listener, *listener_v6;
        struct sockaddr_in sin;

	sest.eb = event_base_new();
	if(!sest.eb) {
		std::cout << "Could not create event_base. Exiting."
				<< std::endl;
		exit(-1);
	}

	// create pure timer event:
	event *timerev = event_new(sest.eb, -1, EV_TIMEOUT | EV_PERSIST,
			dispatch, &sest);
	// ideally, we want this many microseconds between ticks
	sest.desired_dispatch_interval_us = 200000;
	timeval dispatch_interval = { 0, sest.desired_dispatch_interval_us };
	evtimer_add(timerev, &dispatch_interval);

	// Clear the sockaddr before using it, in case there are extra
        // platform-specific fields that can mess us up.
        memset(&sin, 0, sizeof(sin));
        // This is an INET address
        sin.sin_family = AF_INET;
        // Listen on 0.0.0.0
        sin.sin_addr.s_addr = htonl(0);
        // Listen on the given port.
        sin.sin_port = htons(SrvPort);

        listener = evconnlistener_new_bind(sest.eb, onconnect, &sest,
		LEV_OPT_CLOSE_ON_FREE|LEV_OPT_REUSEABLE, -1,
		(struct sockaddr *)&sin, sizeof(sin));
	if(!listener) {
		std::cout << "Couldn't create listener. Exiting." << std::endl;
		return 1;
	}
	evconnlistener_set_error_cb(listener, accept_error_cb);

	struct sockaddr_in6 sin6;
	memset(&sin6, 0, sizeof(sin6));
	sin6.sin6_family = AF_INET6;
	sin6.sin6_port = htons(SrvPort);
	inet_pton(AF_INET6, "::", &sin6.sin6_addr);
	listener_v6 = evconnlistener_new_bind(sest.eb, onconnect, &sest,
		LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE |
		LEV_OPT_BIND_IPV6ONLY,
		-1, (struct sockaddr*) &sin6, sizeof(sin6));

	if(listener_v6 == NULL) {
		std::cout << "Couldn't listen ipv6. Moving on..." << std::endl;
		return -1;
	}

	event_base_dispatch(sest.eb);

	// wrap up and exit
	event_base_free(sest.eb);

	return 0;
}

