/*

Minecraft Dynmap plugin for Miranda Instant Messenger
_____________________________________________

Copyright © 2015-17 Robert Pösel

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "stdafx.h"

http::response MinecraftDynmapProto::sendRequest(const int request_type, std::string *post_data, std::string *get_data)
{
	http::response resp;

	// Prepare the request
	NETLIBHTTPREQUEST nlhr = { sizeof(NETLIBHTTPREQUEST) };

	// FIXME: get server

	// Set request URL
	std::string url = m_server + chooseAction(request_type, get_data);
	nlhr.szUrl = (char*)url.c_str();

	// Set timeout (bigger for channel request)
	nlhr.timeout = 1000 * ((request_type == MINECRAFTDYNMAP_REQUEST_EVENTS) ? 65 : 20);

	// Set request type (GET/POST) and eventually also POST data
	if (post_data != nullptr) {
		nlhr.requestType = REQUEST_POST;
		nlhr.pData = (char*)(*post_data).c_str();
		nlhr.dataLength = (int)post_data->length();
	}
	else {
		nlhr.requestType = REQUEST_GET;
	}

	// Set headers - it depends on requestType so it must be after setting that
	nlhr.headers = get_request_headers(nlhr.requestType, &nlhr.headersCount);

	// Set flags
	nlhr.flags = NLHRF_HTTP11;

#ifdef _DEBUG 
	nlhr.flags |= NLHRF_DUMPASTEXT;
#else
	nlhr.flags |= NLHRF_NODUMP;
#endif	

	// Set persistent connection (or not)
	switch (request_type)
	{
	case MINECRAFTDYNMAP_REQUEST_HOME:
		nlhr.nlc = nullptr;
		break;

	case MINECRAFTDYNMAP_REQUEST_EVENTS:
		nlhr.nlc = hEventsConnection;
		nlhr.flags |= NLHRF_PERSISTENT;
		break;

	default:
		WaitForSingleObject(connection_lock_, INFINITE);
		nlhr.nlc = hConnection;
		nlhr.flags |= NLHRF_PERSISTENT;
		break;
	}

	debugLogA("@@@@@ Sending request to '%s'", nlhr.szUrl);

	// Send the request	
	NETLIBHTTPREQUEST *pnlhr = Netlib_HttpTransaction(m_hNetlibUser, &nlhr);

	mir_free(nlhr.headers);

	// Remember the persistent connection handle (or not)
	switch (request_type)
	{
	case MINECRAFTDYNMAP_REQUEST_HOME:
		break;

	case MINECRAFTDYNMAP_REQUEST_EVENTS:
		hEventsConnection = pnlhr ? pnlhr->nlc : nullptr;
		break;

	default:
		ReleaseMutex(connection_lock_);
		hConnection = pnlhr ? pnlhr->nlc : nullptr;
		break;
	}

	// Check and copy response data
	if (pnlhr != nullptr)
	{
		debugLogA("@@@@@ Got response with code %d", pnlhr->resultCode);
		store_headers(&resp, pnlhr->headers, pnlhr->headersCount);
		resp.code = pnlhr->resultCode;
		resp.data = pnlhr->pData ? pnlhr->pData : "";

		// debugLogA("&&&&& Got response: %s", resp.data.c_str());

		Netlib_FreeHttpRequest(pnlhr);
	} else {
		debugLogA("!!!!! No response from server (time-out)");
		resp.code = HTTP_CODE_FAKE_DISCONNECTED;
		// Better to have something set explicitely as this value is compaired in all communication requests
	}

	return resp;
}

//////////////////////////////////////////////////////////////////////////////

std::string MinecraftDynmapProto::chooseAction(int request_type, std::string *get_data)
{
	switch (request_type) {
		case MINECRAFTDYNMAP_REQUEST_MESSAGE: {
			return "/up/sendmessage";
		}

		case MINECRAFTDYNMAP_REQUEST_CONFIGURATION: {
			return "/up/configuration";
		}

		case MINECRAFTDYNMAP_REQUEST_EVENTS: {
			std::string request = "/up/world/%s/%s";

			// Set world
			std::string world = "world"; // TODO: configurable world?
			utils::text::replace_first(&request, "%s", world);

			// Set timestamp
			utils::text::replace_first(&request, "%s", !m_timestamp.empty() ? m_timestamp : "0");

			return request;
		}

		//case MINECRAFTDYNMAP_REQUEST_HOME:
		default: {
			return "/" + *get_data;
		}
	}
}


NETLIBHTTPHEADER* MinecraftDynmapProto::get_request_headers(int request_type, int* headers_count)
{
	if (request_type == REQUEST_POST)
		*headers_count = 5;
	else
		*headers_count = 4;

	NETLIBHTTPHEADER *headers = (NETLIBHTTPHEADER*)mir_calloc(sizeof(NETLIBHTTPHEADER)*(*headers_count));

	if (request_type == REQUEST_POST) {
		headers[4].szName = "Content-Type";
		headers[4].szValue = "application/json; charset=utf-8";
	}

	headers[3].szName = "Cookie";
	headers[3].szValue = (char *)m_cookie.c_str();
	headers[2].szName = "User-Agent";
	headers[2].szValue = (char *)g_strUserAgent.c_str();
	headers[1].szName = "Accept";
	headers[1].szValue = "*/*";
	headers[0].szName = "Accept-Language";
	headers[0].szValue = "en,en-US;q=0.9";

	return headers;
}

void MinecraftDynmapProto::store_headers(http::response* resp, NETLIBHTTPHEADER* headers, int headersCount)
{
	for (size_t i = 0; i < (size_t)headersCount; i++) {
		std::string header_name = headers[i].szName;
		std::string header_value = headers[i].szValue;
		
		resp->headers[header_name] = header_value;
	}
}

//////////////////////////////////////////////////////////////////////////////

bool MinecraftDynmapProto::doSignOn()
{
	handleEntry(__FUNCTION__);

	http::response resp = sendRequest(MINECRAFTDYNMAP_REQUEST_CONFIGURATION);

	if (resp.code != HTTP_CODE_OK) {
		return handleError(__FUNCTION__, "Can't load configuration", true);
	}

	JSONNode root = JSONNode::parse(resp.data.c_str());
	if (!root)
		return false;

	/*
	const JSONNode &allowchat_ = root["allowchat"]; // boolean
	const JSONNode &allowwebchat_ = root["allowwebchat"]; // boolean
	const JSONNode &loggedin_ = root["loggedin"]; // boolean
	const JSONNode &loginEnabled_ = root["login-enabled"]; // boolean
	const JSONNode &loginRequired_ = root["webchat-requires-login"]; // boolean
	*/

	const JSONNode &title_ = root["title"]; // name of server
	const JSONNode &interval_ = root["webchat-interval"]; // limit in seconds for sending messages
	const JSONNode &rate_ = root["updaterate"]; // probably update rate for events request

	if (!title_ || !interval_ || !rate_) {
		return handleError(__FUNCTION__, "No title, interval or rate in configuration", true);
	}

	m_title = title_.as_string();
	m_interval = interval_.as_int();
	m_updateRate = rate_.as_int();
	m_cookie.clear();

	if (resp.headers.find("Set-Cookie") != resp.headers.end()) {
		// Load Session identifier
		std::string cookies = resp.headers["Set-Cookie"];

		const char *findStr = "JSESSIONID=";
		std::string::size_type start = cookies.find(findStr);
		
		if (start != std::string::npos) {
			m_cookie = cookies.substr(start, cookies.find(";") - start);
		}
	}

	if (m_cookie.empty()) {
		return handleError(__FUNCTION__, "Empty session id", true);
	}

	return handleSuccess(__FUNCTION__);
}

bool MinecraftDynmapProto::doEvents()
{
	handleEntry(__FUNCTION__);

	// Get update
	http::response resp = sendRequest(MINECRAFTDYNMAP_REQUEST_EVENTS);

	if (resp.code != HTTP_CODE_OK)
		return handleError(__FUNCTION__, "Response is not code 200");

	JSONNode root = JSONNode::parse(resp.data.c_str());
	if (!root)
		return handleError(__FUNCTION__, "Invalid JSON response");

	const JSONNode &timestamp_ = root["timestamp"];
	if (!timestamp_)
		return handleError(__FUNCTION__, "Received no timestamp node");

	m_timestamp = timestamp_.as_string();

	const JSONNode &updates_ = root["updates"];
	if (!updates_)
		return handleError(__FUNCTION__, "Received no updates node");

	for (auto it = updates_.begin(); it != updates_.end(); ++it) {
		const JSONNode &type_ = (*it)["type"];
		if (type_ && type_.as_string() == "chat") {
			const JSONNode &time_ = (*it)["timestamp"];
			// const JSONNode &source_ = (*it)["source"]; // e.g. "web"
			const JSONNode &playerName_ = (*it)["playerName"];
			const JSONNode &message_ = (*it)["message"];
			// TODO: there are also "channel" and "account" elements

			if (!time_ || !playerName_ || !message_) {
				debugLogW(L"Error: No player name, time or text for message");
				continue;
			}

			time_t timestamp = utils::time::from_string(time_.as_string());
			std::string name = playerName_.as_string();
			std::string message = message_.as_string();

			debugLogW(L"Received message: [%d] %s -> %s", timestamp, name.c_str(), message.c_str());
			UpdateChat(name.c_str(), message.c_str(), timestamp);
		}
	}

	return handleSuccess(__FUNCTION__);
}

bool MinecraftDynmapProto::doSendMessage(const std::string &message_text)
{
	handleEntry(__FUNCTION__);

	JSONNode json(JSON_NODE);
	json.push_back(JSONNode("name", m_nick.c_str()));
	json.push_back(JSONNode("message", message_text.c_str()));
	std::string data = json.write();

	http::response resp = sendRequest(MINECRAFTDYNMAP_REQUEST_MESSAGE, &data);

	if (resp.code == HTTP_CODE_OK) {
		JSONNode root = JSONNode::parse(resp.data.c_str());
		if (root) {
			const JSONNode &error_ = root["error"];
			if (error_) {
				std::string error = error_.as_string();
				if (error == "none") {
					return handleSuccess(__FUNCTION__);
				}
				else if (error == "not-allowed") {
					UpdateChat(nullptr, Translate("Message was not sent. Probably you are sending them too fast or chat is disabled completely."));
				}
			}
		}
	}

	return handleError(__FUNCTION__);
}

std::string MinecraftDynmapProto::doGetPage(const int request_type)
{
	handleEntry(__FUNCTION__);

	http::response resp = sendRequest(request_type);

	if (resp.code == HTTP_CODE_OK) {
		handleSuccess(__FUNCTION__);
	} else {
		handleError(__FUNCTION__);
	}

	return resp.data;
}

void MinecraftDynmapProto::SignOnWorker(void*)
{
	SYSTEMTIME t;
	GetLocalTime(&t);
	debugLogA("[%d.%d.%d] Using Omegle Protocol %s", t.wDay, t.wMonth, t.wYear, __VERSION_STRING_DOTS);

	ScopedLock s(signon_lock_);

	int old_status = m_iStatus;

	// Load server from database
	ptrA str(db_get_sa(NULL, m_szModuleName, MINECRAFTDYNMAP_KEY_SERVER));
	if (!str || !str[0]) {
		MessageBox(nullptr, TranslateT("Set server address to connect."), m_tszUserName, MB_OK);
		SetStatus(ID_STATUS_OFFLINE);
		return;
	}
	m_server = str;

	// Fix format of given server
	if (m_server.substr(0, 7) != "http://" && m_server.substr(0, 8) != "https://")
		m_server = "http://" + m_server;
	if (m_server.substr(m_server.length() - 1, 1) == "/")
		m_server = m_server.substr(0, m_server.length() -1);

	if (doSignOn()) {
		// Signed in, switch to online, create chatroom and start events loop
		m_iStatus = m_iDesiredStatus;
		ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus);

		setDword("LogonTS", (DWORD)time(nullptr));
		ClearChat();
		OnJoinChat(0, false);

		ResetEvent(events_loop_event_);

		ForkThread(&MinecraftDynmapProto::EventsLoop, this);
	}
	else {
		// Some error
		ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_FAILED, (HANDLE)old_status, m_iStatus);
	}

}

void MinecraftDynmapProto::SignOffWorker(void*)
{
	ScopedLock s(signon_lock_);

	SetEvent(events_loop_event_);

	m_cookie.clear();
	m_title.clear();
	m_server.clear();
	m_timestamp.clear();

	int old_status = m_iStatus;
	m_iStatus = ID_STATUS_OFFLINE;

	Netlib_Shutdown(hEventsConnection);

	OnLeaveChat(NULL, NULL);

	delSetting("LogonTS");

	ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus);

	//SetAllContactStatuses(ID_STATUS_OFFLINE);
	//ToggleStatusMenuItems(false);

	if (hConnection)
		Netlib_CloseHandle(hConnection);
	hConnection = nullptr;

	if (hEventsConnection)
		Netlib_CloseHandle(hEventsConnection);
	hEventsConnection = nullptr;
}

void MinecraftDynmapProto::EventsLoop(void *)
{
	ScopedLock s(events_loop_lock_);

	time_t tim = ::time(nullptr);
	debugLogA(">>>>> Entering %s[%d]", __FUNCTION__, tim);

	while (doEvents())
	{
		if (!isOnline())
			break;

		if (WaitForSingleObjectEx(events_loop_event_, m_updateRate, true) != WAIT_TIMEOUT) // FIXME: correct timeout
			break;

		debugLogA("***** %s[%d] refreshing...", __FUNCTION__, tim);
	}

	ResetEvent(events_loop_event_);
	ResetEvent(events_loop_lock_);	
	debugLogA("<<<<< Exiting %s[%d]", __FUNCTION__, tim);
}

void MinecraftDynmapProto::SendMsgWorker(void *p)
{
	if (p == nullptr)
		return;

	ScopedLock s(send_message_lock_);

	std::string data = *(std::string*)p;
	delete (std::string*)p;

	data = utils::text::trim(data);

	if (isOnline() && data.length()) {
		doSendMessage(data);
	}
}