/*
 * cenosServer.cpp
 *
 *  Created on: May 11, 2017
 *      Author: vadims
 */
#include <WinSock2.h>
#include <ctime>

#include <fstream>
#include <set>
#include <streambuf>
#include <stdexcept>
#include <boost/filesystem.hpp>

//#include "cenosFns.hpp"
#include "easylogging++/easylogging++.h"
#include "misc/messagedata.hpp"
#include "nlohmann/json.hpp"
#include "cenosServer.hpp"
#include "cenos_exception.h"


// static initialisations
int cenosServer::portNr;
websocketpp::server<websocketpp::config::asio> cenosServer::server;
map<string, websocketpp::connection_hdl> cenosServer::websockets;
boost::mutex cenosServer::wsMutex;
std::vector<std::string> cenosServer::clientNames;

// namespace merging
using websocketpp::connection_hdl;
using json = nlohmann::json;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;



void const cenosServer::readClientNames()
{
	LOG(INFO) << "in cenosServer::getClientNames";

	char const* tmp = getenv("CENOS_CONFIG");
	std::string tmp_s = "";

	if (!tmp) {
		LOG(ERROR) << "variable CENOS_CONFIG not set ";
	}
	else
	{
		tmp_s = std::string(tmp);
	}

	std::string clientConfigJson = tmp_s  + "\\jsons\\ids.json";
	json j;

	if (boost::filesystem::exists(clientConfigJson))
	{
		std::ifstream infile(clientConfigJson, std::ifstream::in);
		if (infile.is_open()) {
			std::stringstream strStream;
			strStream << infile.rdbuf();
			string str = strStream.str();
			std::istringstream iss(str);
			iss >> j;
		}
		else
		{
			LOG(ERROR) << "Execution will be terminated ";
			LOG(FATAL) << "Could not open file " << clientConfigJson;
		}
		infile.close();
	}
	else
	{
		LOG(ERROR) << "Execution will be terminated ";
		LOG(FATAL) << "File " << clientConfigJson << " not found";
	}
	clientNames = j["ids"].get<std::vector<std::string>>();
}


websocketpp::connection_hdl cenosServer::getHdlById(const string& id)
{
	std::map<string, websocketpp::connection_hdl>::iterator it = websockets.find(id);
	if (it == websockets.end())
		throw cenos_exception("No connection handler " + id);
	return websockets[id];
}

cenosServer::cenosServer()
{

}

bool cenosServer::init(int port)
{
	LOG(INFO) << "in cenosServer::init";
	LOG(INFO) << "Setting port number to " << std::to_string(port);

	portNr = port;
	// Initialising WebsocketServer.
	server.init_asio();

	server.set_access_channels(websocketpp::log::alevel::all);
	server.clear_access_channels(websocketpp::log::alevel::frame_payload);

	// Set the message handlers.
	LOG(INFO) << "Setting server handlers";

//	writeToLog("Setting server handlers");
	server.set_validate_handler(bind(&cenosServer::on_validate, ::_1));
	server.set_fail_handler(bind(&cenosServer::on_fail, ::_1));
	server.set_close_handler(bind(&cenosServer::on_close));
	server.set_message_handler(bind(&cenosServer::on_message, ::_1, ::_2));

	// Listen on port.
	try {
		server.listen(portNr);
		LOG(INFO) << "Server listening to port number " << std::to_string(port);
	}
	catch (websocketpp::exception const& e) {
		LOG(ERROR) << "Exception on listen " << e.what();
	}


	// Starting Websocket accept.
	websocketpp::lib::error_code ec;
	LOG(INFO) << "Server starting accepting connections ";

	server.start_accept(ec);
	if (ec) {
		LOG(ERROR) << "Error on start_accept " << ec.message();
		return false;
	}
	return true;
}


void cenosServer::run()
{
	LOG(INFO) << "in cenosServer::run";

	try {
		LOG(INFO) << "Running server";
		server.run();

	}
	catch (websocketpp::exception const& e) {
		LOG(ERROR) << "Server exception " << e.what();
		std::cout << "Server exception " << e.what();
	}
	catch (std::exception e)
	{
		LOG(ERROR) << "Server exception " << e.what();
		std::cout << "Server exception " << e.what();
	}
	catch (std::string e)
	{
		LOG(ERROR) << "Server exception " << e;
		std::cout << "Server exception " << e;
	}
}

void cenosServer::stop()
{
//	writeToLog("in cenosServer::stop");

	// Stopping the Websocket listener and closing outstanding connections.
	websocketpp::lib::error_code ec;
	server.stop_listening(ec);
	if (ec) {
		LOG(ERROR) << "Failed to stop listening " << ec.message();
		return;
	}
	LOG(INFO) << "Stopped listening";

	// Close all existing websocket connections.
	string data = "Terminating connection...";
	map<string, connection_hdl>::iterator it;
	for (it = websockets.begin(); it != websockets.end(); ++it) {
		websocketpp::lib::error_code ec;
		//server.send(it->second, "Closing connection.", websocketpp::frame::opcode::text);
		server.close(it->second, websocketpp::close::status::normal, data, ec); // send text message.
		if (ec) { 
			LOG(ERROR) << "Error closing connection " << ec.message();
		}
	}
	LOG(INFO) << "Server stopped";
	server.stop();
}

bool cenosServer::on_validate(connection_hdl hdl)
{
	LOG(INFO) << "in cenosServer::on_validate";
	websocketpp::server<websocketpp::config::asio>::connection_ptr con = server.get_con_from_hdl(hdl);
	websocketpp::uri_ptr uri = con->get_uri();
	string query = uri->get_query(); // returns empty string if no query string set.
	string id;

	if (!query.empty()) {

		size_t pos = query.find("id");      // position of "id" in str
		id = query.substr(pos + 3, min(query.find("&", pos + 3) - 3, string::npos));

		if (std::find(clientNames.begin(), clientNames.end(), id) != clientNames.end())
		{
			LOG(INFO) << "Incoming connection. ID " << id << " is registered. Connection established.";
			std::cout << "Incoming connection. ID " << id << " is registered. Connection established.";
			if (id != "salome_plugin")
				cout << "Incoming connection. ID " << id << " is registered. Connection established." << endl;
		}
		else {
			LOG(ERROR) << "Incoming connection. ID " << id << " is not found. Connection refused.";
			return false;
		}
	}
	else {
		LOG(ERROR) << "Incoming connection. No ID is specified. Connection refused.";
		return false;
	}


	// TODO What if fails to lock
	wsMutex.lock();

	websockets.insert(std::pair<string, connection_hdl>(id, hdl));
	LOG(INFO) << "Inserted connection with ID " << id << " in list of connections.";

	// TODO What if fails to unlock
	wsMutex.unlock();

	return true;
}

void cenosServer::on_fail(connection_hdl hdl)
{
	LOG(INFO) << "in cenosServer::on_fail";
	websocketpp::server<websocketpp::config::asio>::connection_ptr con = server.get_con_from_hdl(hdl);
	websocketpp::lib::error_code ec = con->get_ec();
	LOG(INFO) << "Websocket connection attempt by client failed. " << ec.message();
}

void cenosServer::on_close()
{
	LOG(INFO) << "in cenosServer::on_close";
	LOG(INFO) << "Websocket connection closed.";
}

void cenosServer::on_message(websocketpp::connection_hdl hdl, message_ptr msg)
{
	//LOG(INFO) << "cenosServer::on_message, Received message :\n" + msg->get_payload();

	json input;
	try
	{
		input = json::parse(msg->get_payload());
	}
	catch (json::parse_error & e)
	{
		LOG(ERROR) << "Invalid format of incoming json. " << e.what();
		return;
	}

	if (input.find("sendTo") == input.end())
	{

		std::string  guidString = (input.find("guid") == input.end()) ? "00000000-0000-0000-0000-000000000000" : input["guid"].get<std::string>();

		messagedata noSendToError = messagedata(messagedata::status::error, messagedata::type::response, "Input error. No sendTo specified in command.");
		noSendToError.guid = guidString;
		noSendToError.sendTo = "gui";

		server.send(hdl, noSendToError.outputData(),
			websocketpp::frame::opcode::text);
		return;
	}

	try {
		server.send(getHdlById(input["sendTo"]), msg->get_payload(), websocketpp::frame::opcode::text);
	}
	catch (std::string msg) {
		LOG(ERROR) << msg;
	}

	if (input["command"] == "close") {
		LOG(INFO) << "Stopping server";
		// TODO: stop clients
		stop();
	}
}

void cenosServer::setLoggingFile(std::string filename)
{
	logFile.open(filename);
	server.get_alog().set_ostream(&logFile);
	server.get_elog().set_ostream(&logFile);
}
