/*
  NMDCHub
  ŻŻŻŻŻŻŻ
*/

#include "nmdc.h"
#include "../common/mUtil.h"
#include <queue>
#include <map>
using namespace std;

typedef std::map<std::string, NMDCUser> UMAP;

struct NMDCHubData {
	NMDCUser self;
	mSock *sock;
	bool bUsedPassword;
	UMAP userlist;
	std::string protoBuf;
	std::queue<NMDCEvent> events;
	bool bLastOK;
};

/////////////////////////////////////////////////////////
// CONSTRUCTION + DECONSTRUCTION
/////////////////////////////////////////////////////////


NMDCHub::NMDCHub(const char* szAddress, int iPort, const char* szNick, uint64 iShareSize){
	Initialise(szAddress, iPort, szNick, iShareSize);
}

NMDCHub::NMDCHub(const char* szAddress, const char* szNick, uint64 iShareSize) {
	// Check for a port specifier in szAddress
	vector<string> parts;
	string strAddress = szAddress;
	explode(parts, strAddress, ":");

	if (parts.size() >= 2) {
		Initialise(parts[0].c_str(), atoi(parts[1].c_str()), szNick, iShareSize);
	} else {
		Initialise(strAddress.c_str(), 411, szNick, iShareSize);
	}
}

NMDCHub::~NMDCHub() {
	if (_d->sock) delete _d->sock;
	delete _d;
}

void NMDCHub::Initialise(const char* szAddress, int iPort, const char* szNick, uint64 iShareSize) {
	_d = new NMDCHubData;

	_d->bUsedPassword = false;
	_d->self.iShareSize = iShareSize;
	_d->self.szNick = szNick;
	_d->protoBuf.assign("");
	_d->bLastOK = false;

	_d->sock = new mSock(szAddress, iPort);
	_d->sock->doEvents();
	push_event(NMDC_DEBUG_INFO, "CONN", szAddress);
}

void NMDCHub::Disconnect() {
	_d->sock->close();
}


/////////////////////////////////////////////////////////
// PUBLIC METHODS
/////////////////////////////////////////////////////////


void NMDCHub::SayPublic(const char* szMessage) {
	string sanitisedMessage = szMessage;
	chatSanitise(sanitisedMessage);

	string szBuff = format("<%s> %s|", _d->self.szNick.c_str(), sanitisedMessage.c_str() );
	SayExplicit(szBuff.c_str());

}

void NMDCHub::SayPrivate(const char* szUsername, const char* szMessage) {
	string sanitisedMessage = szMessage;
	chatSanitise(sanitisedMessage);

	string szBuff = format("$To: %s From: %s $<%s> %s|",
		szUsername, _d->self.szNick.c_str(), _d->self.szNick.c_str(), sanitisedMessage.c_str()
	);
	SayExplicit(szBuff.c_str());
}

void NMDCHub::Login(const char* szPassword) {
	string szBuff = format("$MyPass %s|", szPassword);
	SayExplicit(szBuff.c_str());
	_d->bUsedPassword = true;
}

void NMDCHub::SayExplicit(const char* szMessage) {
	if (! _d->sock->isValid() ) return;
	push_event(NMDC_DEBUG_INFO, "SEND", szMessage);
	_d->sock->put(szMessage);
	_d->sock->doEvents();	
}

bool NMDCHub::Connected() {
	process_messages();
	return _d->sock->isValid();
}

NMDCUser NMDCHub::GetUserSelf() {
	return _d->self;
}

int NMDCHub::GetUserCount() {
	return (int)_d->userlist.size();
}

void NMDCHub::GetUserNicks(NMDCUser* arrayDest, int maxcount) {
	memset(arrayDest, 0, maxcount * sizeof(NMDCUser) );
	UMAP::iterator ui = _d->userlist.begin();
	int idx = 0;
	for (;;) {
		arrayDest[idx] = ui->second;		
		if (idx++ == maxcount || ++ui == _d->userlist.end()) break;
	}
}

bool NMDCHub::GetUserInfo(const char* szUsername, NMDCUser* out) {
	UMAP::iterator ui;
	std::string ssUser(szUsername);
	ui = _d->userlist.find(ssUser);
	if (ui != _d->userlist.end()) {
		memcpy(out, &(ui->second), sizeof(NMDCUser) );
		return true;
	} else {
		return false;
	}
}


/////////////////////////////////////////////////////////
// EVENT HANDLING
/////////////////////////////////////////////////////////


void NMDCHub::push_event(int iType, const char* data1, const char* data2) {
	NMDCEvent ev;
	ev.type = iType;
	ev.data1.assign(data1);
	ev.data2.assign(data2);
	_d->events.push(ev);
}

NMDCEvent NMDCHub::NextEvent() {
	process_messages();

	NMDCEvent next;
	if (_d->events.empty()) {
		next.type = NMDC_NONE;
	} else {
		next = _d->events.front();
		_d->events.pop();
	}
	return next;
}


/////////////////////////////////////////////////////////
// PROTOCOL HANDLERS
/////////////////////////////////////////////////////////


void NMDCHub::process_messages() {
	_d->sock->doEvents();
	if (! _d->sock->isValid()) {
		if (_d->bLastOK) {
			_d->bLastOK = false;
			push_event(NMDC_KICKED, "", "");
		}
		return;
	}

	string sBuff = _d->sock->get();
	if (sBuff.length()) {

		push_event(NMDC_DEBUG_INFO, "RECV", sBuff.c_str());
		_d->bLastOK = true;

		vector<string> messages;
		explode(messages, sBuff, "|");
		if (! messages.size()) return;

		// Deal with last remnant of message (protocol buffering)
		if (messages.size() >= 2) {
			messages[0].assign(format("%s%s", _d->protoBuf.c_str(), messages[0].c_str()));
			_d->protoBuf.assign("");
		}
		if (messages[messages.size() - 1].length() > 0) {
			_d->protoBuf.append(messages[messages.size() - 1]);
		}

		// Parse all messages
		for (int i = 0, e = messages.size()-1; i!=e; i++) { 
			string* m = &(messages[i]);
			switch ((*m).c_str()[0]) {
				case '$': {
					handle_command( (*m) );
				} break;

				case '<': {
					size_t fpos =  (*m).find("> ");
					if (fpos == string::npos) {
						push_event(NMDC_DEBUG_INFO, "ERROR", "Invalid public message");
					} else {
						string sUName =  (*m).substr(1, fpos-1);
						string sUMsg  =  (*m).substr(fpos+2);
						chatDesanitise(sUMsg);
						push_event(NMDC_PUBLIC_CHAT, sUName.c_str(), sUMsg.c_str() );
					}
				} break;

				case '*':
				default:
					push_event(NMDC_SYSTEM_MSG, "\0", (*m).c_str());
					break;
			}
		}
	}
}

void NMDCHub::handle_command(string &szCommand) {

	// Tokenise the recieved command
	vector<string> tokens;
	explode(tokens, szCommand, " ");

	// Split (hc) into command and remainder
	string hcCmd = tokens[0];
	string hcNext;
	if (szCommand.length() > hcCmd.length()) {
		hcNext = szCommand.substr(hcCmd.length() + 1);
	}
	
	// Switch over commands
	if (hcCmd == "$Lock") {
		push_event(NMDC_SYSTEM_MSG, "\0", "Found a hub.");
		int iPkPos = hcNext.find(" Pk=");
		string key;
		if (iPkPos == string::npos) {
			key = parse_lock(hcNext.c_str());
		} else {
			key = parse_lock(hcNext.substr(0, iPkPos).c_str());
		}

		SayExplicit(format(
			"$Supports NoGetINFO UserIP2|$Key %s|$ValidateNick %s|",
			key.c_str(), _d->self.szNick.c_str()
		).c_str());

	} else if(hcCmd == "$Hello") {
		if (hcNext == _d->self.szNick) {
			//we just joined
			SayExplicit("$Version 1,0091|");
			SayExplicit("$GetNickList|");
			SayExplicit(format(
				"$MyINFO $ALL %s <mNMDClib V:1,M:A,H:1/0/0,S:5>$ $10  $$%d$|",
				_d->self.szNick.c_str(), _d->self.iShareSize
			).c_str());
			push_event(NMDC_SYSTEM_MSG, "\0", format(
				"Joined the hub as '%s'.", _d->self.szNick.c_str()
			).c_str());
		} else {
			//add + report the user here, and silently update on myinfo
			joinUser(hcNext.c_str());
		}

	} else if(hcCmd == "$HubName") {
		push_event(NMDC_SYSTEM_MSG, "\0", format(
			"Now talking on '%s'.", hcNext.c_str()
		).c_str());

	} else if(hcCmd == "$Supports") { // safely ignorable
		push_event(NMDC_DEBUG_INFO, "\0", format(
			"The server supports extensions: %s", hcNext.c_str()
		).c_str());

	} else if(hcCmd == "$ValidateDenide") {
		if (_d->bUsedPassword) {
			push_event(NMDC_SYSTEM_MSG, "\0", "Invalid password!");
		} else {
			push_event(NMDC_SYSTEM_MSG, "\0", "This nick is already in use!");
		}

	} else if(hcCmd == "$UserIP") { // safely ignorable
		push_event(NMDC_DEBUG_INFO, "\0", format(
			"The server detected our IP as %s.", hcNext.c_str()
		).c_str() );

	} else if(hcCmd == "$GetPass") {
		push_event(NMDC_NEED_PASS, "\0", "\0");

	} else if(hcCmd == "$MyINFO") {
		joinUser(parse_myinfo(hcNext.c_str()) );

	} else if(hcCmd == "$Quit") {
		UMAP::iterator ei = _d->userlist.find(hcNext);
		if (ei != _d->userlist.end()) _d->userlist.erase(ei); 
		push_event(NMDC_USER_PART, hcNext.c_str(), "\0");

	} else if(hcCmd == "$UserCommand") { //safely ignorable
		push_event(NMDC_DEBUG_INFO, "\0", format(
			"A usercommand is avaliable: '%s'.", hcNext.c_str()
		).c_str());

	} else if(hcCmd == "$To:") {
		parse_to(hcNext.c_str());

	} else if(hcCmd == "$UserList") {
		push_event(NMDC_DEBUG_INFO, "\0", "Got a user list.");

	} else if(hcCmd == "$NickList") {
		push_event(NMDC_DEBUG_INFO, "\0", "Got a nicklist.");
		parse_nicklist(hcNext.c_str());

	} else if(hcCmd == "$OpList") {
		push_event(NMDC_DEBUG_INFO, "\0", "Got an operator list.");

	} else {
		push_event(NMDC_SYSTEM_MSG, "\0", format(
			"The server sent an unknown command: '%s'.", szCommand.c_str()
		).c_str());

	}

}


/////////////////////////////////////////////////////////
// PROTOCOL HELPERS
/////////////////////////////////////////////////////////

void NMDCHub::chatSanitise(string& message) {
	replace(message, "&", "&amp;");
	replace(message, "|", "&#124;");
	replace(message, "$", "&#36;");
}

void NMDCHub::chatDesanitise(string& message) {
	replace(message, "&#36;",  "$");
	replace(message, "&#124;", "|");
	replace(message, "&amp;",  "&");
}

void NMDCHub::joinUser(NMDCUser usr) {
	bool bUserIsNew = false;
	std::string ssNick(usr.szNick);
	if (_d->userlist.find(ssNick) == _d->userlist.end()) {
		bUserIsNew = true;
	}
	_d->userlist[ssNick] = usr;
	if (bUserIsNew) {
		push_event(NMDC_USER_JOIN, usr.szNick.c_str(), "\0");
	} else {
		push_event(NMDC_USER_UPDATE, usr.szNick.c_str(), "\0");
	}
}

void NMDCHub::joinUser(const char* szNick) {
	NMDCUser usr;
	usr.szNick = szNick;
	joinUser(usr);
};

void NMDCHub::parse_nicklist(const char* list) {
	// $NickList usera$$userb$$userc$$ (trailing $$).
	vector<string> users;
	string sList = list;
	explode(users, sList, "$$");
	for(int i = 0, e = users.size() - 1; i < e; i++) {
		joinUser(users[i].c_str());
	}
};

NMDCUser NMDCHub::parse_myinfo(const char* myinfo) {
	// $MyINFO $ALL <nick> <description>$ $<connection><flag>$<e-mail>$<sharesize>$|
	NMDCUser user;

	vector<string> tokens;
	string sMyInfo = myinfo;
	explode(tokens, sMyInfo, "$");

	if (tokens.size() != 7) {
		push_event(NMDC_DEBUG_INFO, "ERROR", "Invalid MyInfo");
	} else {
		vector<string> tokens_space;
		explode(tokens_space, tokens[1], " ");
		if (tokens_space.size() < 3) {
			push_event(NMDC_DEBUG_INFO, "ERROR", "Invalid MyInfo");
		} else {
			user.szNick = tokens_space[1];
			if (tokens_space[2].length()) {
				size_t offset = tokens_space[0].length() + tokens_space[1].length();
				user.szDescription = tokens[1].substr(offset + 2);
			}
		}
		user.iShareSize = _atoi64(tokens[6].c_str());
	}

	return user;
}

void NMDCHub::parse_to(const char* to) {
	// recipient From: sender $<sender> message|
	NMDCEvent pm;
	pm.type = NMDC_PRIVATE_MSG;

	vector<string> tokens;
	string sTo = to;
	explode(tokens, sTo, " ");

	if (tokens.size() < 4) {
		push_event(NMDC_DEBUG_INFO, "ERROR", "Invalid private message");
	} else {
		pm.data1 = tokens[2];
		string sMessage = sTo.substr(
			tokens[0].length() +
			tokens[1].length() +
			tokens[2].length() +
			tokens[3].length() +
			4
		);
		chatDesanitise(sMessage);
		push_event(NMDC_PRIVATE_MSG, tokens[2].c_str(), sMessage.c_str());
	}
}

string NMDCHub::parse_lock(const char *lock) {
	int len = strlen(lock), len2 = 1;
	char* key = new char[len+1];
	
	// lock-to-key transform
	for(int i = 1; i < len; i++) key[i] = lock[i] ^ lock[i-1];
	key[0] = lock[0] ^ lock[len-1] ^ lock[len-2] ^ 5;

	for(int i = 0; i < len; i++) {
		key[i] = ((key[i]<<4) & 0xF0) | ((key[i]>>4) & 0x0F);
		switch(key[i]) {
			case 0: case 5: case 36: case 96: case 124: case 126:
			len2 += 10;
		break;
		default:
			len2++;
		}
	}

	// sanitise
	char *key_san = new char[len2], *pKey = key_san;
	for(int i = 0; i < len; i++) {
		switch(key[i]) {
			case 0: case 5: case 36: case 96: case 124: case 126:
			sprintf(pKey, "/%%DCN%03d%%/", key[i]);
			pKey += 10;
		break;
		default:
			*(pKey++) = key[i];
		}
	}
	*pKey = '\0';

	// Tidy up
	string response(key_san);
	delete[] key;     key = NULL;	
	delete[] key_san; key_san = NULL;

	return response;
}

