/*
Copyright (c) 2013-16 Miranda NG project (http://miranda-ng.org)

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 version 2
of the License.

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"

enum
{
	IDM_NONE,
	IDM_TOPIC, IDM_INVITE, IDM_DESTROY,
	IDM_KICK, IDM_INFO, IDM_CHANGENICK, IDM_VISIT_PROFILE
};

static LPCWSTR sttStatuses[] = { LPGENW("Participants"), LPGENW("Owners") };

extern JSONNode nullNode;

CVkChatInfo* CVkProto::AppendChat(int id, const JSONNode &jnDlg)
{
	debugLogW(L"CVkProto::AppendChat");
	if (id == 0)
		return NULL;

	MCONTACT chatContact = FindChat(id);
	if (chatContact && getBool(chatContact, "kicked"))
		return NULL;

	CVkChatInfo *c = m_chats.find((CVkChatInfo*)&id);
	if (c != NULL)
		return c;

	CMStringW wszTitle;
	c = new CVkChatInfo(id);
	if (jnDlg) {
		wszTitle = jnDlg["title"].as_mstring();
		c->m_wszTopic = mir_wstrdup(!wszTitle.IsEmpty() ? wszTitle : L"");
	}

	CMStringW sid; 
	sid.Format(L"%S_%d", m_szModuleName, id);
	c->m_wszId = mir_wstrdup(sid);

	GCSESSION gcw = { sizeof(gcw) };
	gcw.iType = GCW_CHATROOM;
	gcw.pszModule = m_szModuleName;
	gcw.ptszName = wszTitle;
	gcw.ptszID = sid;
	CallServiceSync(MS_GC_NEWSESSION, NULL, (LPARAM)&gcw);

	GC_INFO gci = { 0 };
	gci.pszModule = m_szModuleName;
	gci.pszID = sid;
	gci.Flags = GCF_BYID | GCF_HCONTACT;
	CallServiceSync(MS_GC_GETINFO, 0, (LPARAM)&gci);
	c->m_hContact = gci.hContact;

	setWString(gci.hContact, "Nick", wszTitle);
	m_chats.insert(c);

	GCDEST gcd = { m_szModuleName, sid, GC_EVENT_ADDGROUP };
	GCEVENT gce = { sizeof(gce), &gcd };
	for (int i = _countof(sttStatuses)-1; i >= 0; i--) {
		gce.ptszStatus = TranslateW(sttStatuses[i]);
		CallServiceSync(MS_GC_EVENT, NULL, (LPARAM)&gce);
	}

	setDword(gci.hContact, "vk_chat_id", id);

	CMStringW wszHomepage(FORMAT, L"https://vk.com/im?sel=c%d", id);
	setWString(gci.hContact, "Homepage", wszHomepage);
	
	db_unset(gci.hContact, m_szModuleName, "off");

	if (jnDlg["left"].as_bool())  {
		setByte(gci.hContact, "off", 1);
		m_chats.remove(c);
		return NULL;
	}
	gcd.iType = GC_EVENT_CONTROL;
	gce.ptszStatus = 0;
	CallServiceSync(MS_GC_EVENT, (m_vkOptions.bHideChats) ? WINDOW_HIDDEN : SESSION_INITDONE, (LPARAM)&gce);
	CallServiceSync(MS_GC_EVENT, SESSION_ONLINE, (LPARAM)&gce);

	RetrieveChatInfo(c);
	return c;
}

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

void CVkProto::RetrieveChatInfo(CVkChatInfo *cc)
{

	CMStringA wszQuery(FORMAT, "var ChatId=%d;", cc->m_chatid);
	wszQuery += "var Info=API.messages.getChat({\"chat_id\":ChatId});"
		"var ChatUsers=API.messages.getChatUsers({\"chat_id\":ChatId,\"fields\":\"id,first_name,last_name\"});";

	if (!cc->m_bHistoryRead) {
		wszQuery += "var ChatMsg=API.messages.getHistory({\"chat_id\":ChatId,\"count\":20,\"rev\":0});var UR=parseInt(ChatMsg.unread);"
			"if(UR>20){if(UR>200)UR=200;ChatMsg=API.messages.getHistory({\"chat_id\":ChatId,\"count\":UR,\"rev\":0});};"
			"var FMsgs = ChatMsg.items@.fwd_messages;var Idx = 0;var Uids =[];while (Idx < FMsgs.length){"
			"var Jdx = 0;var CFMsgs = parseInt(FMsgs[Idx].length);while (Jdx < CFMsgs){"
			"Uids.unshift(FMsgs[Idx][Jdx].user_id);Jdx = Jdx + 1;};Idx = Idx + 1;};"
			"var FUsers = API.users.get({\"user_ids\": Uids, \"name_case\":\"gen\"});"
			"var MsgUsers=API.users.get({\"user_ids\":ChatMsg.items@.user_id,\"fields\":\"id,first_name,last_name\"});";
	}

	wszQuery += "return {\"info\":Info,\"users\":ChatUsers";

	if (!cc->m_bHistoryRead)
		wszQuery += ",\"msgs\":ChatMsg,\"fwd_users\":FUsers,\"msgs_users\":MsgUsers";

	wszQuery +="};";

	debugLogA("CVkProto::RetrieveChantInfo(%d)", cc->m_chatid);
	if (!IsOnline())
		return;
	Push(new AsyncHttpRequest(this, REQUEST_GET, "/method/execute.json", true, &CVkProto::OnReceiveChatInfo)
		<< CHAR_PARAM("code", wszQuery))->pUserInfo = cc;
}

void CVkProto::OnReceiveChatInfo(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq)
{
	debugLogA("CVkProto::OnReceiveChatInfo %d", reply->resultCode);
	if (reply->resultCode != 200)
		return;

	JSONNode jnRoot;
	const JSONNode &jnResponse = CheckJsonResponse(pReq, reply, jnRoot);
	if (!jnResponse)
		return;

	CVkChatInfo *cc = (CVkChatInfo*)pReq->pUserInfo;
	if (m_chats.indexOf(cc) == -1)
		return;

	const JSONNode &jnInfo = jnResponse["info"];
	if (jnInfo) {
		if (jnInfo["title"])
			SetChatTitle(cc, jnInfo["title"].as_mstring());

		if (jnInfo["left"].as_bool() || jnInfo["kicked"].as_bool()) {
			setByte(cc->m_hContact, "kicked", jnInfo["kicked"].as_bool());
			LeaveChat(cc->m_chatid);
			return;
		}
		cc->m_admin_id = jnInfo["admin_id"].as_int();
	}

	const JSONNode &jnUsers = jnResponse["users"];
	if (jnUsers) {
		for (int i = 0; i < cc->m_users.getCount(); i++)
			cc->m_users[i].m_bDel = true;

		for (auto it = jnUsers.begin(); it != jnUsers.end(); ++it) {
			const JSONNode &jnUser = (*it);
			if (!jnUser)
				break;

			int uid = jnUser["id"].as_int();
			wchar_t wszId[20];
			_itow(uid, wszId, 10);

			bool bNew;
			CVkChatUser *cu = cc->m_users.find((CVkChatUser*)&uid);
			if (cu == NULL) {
				cc->m_users.insert(cu = new CVkChatUser(uid));
				bNew = true;
			}
			else 
				bNew = cu->m_bUnknown;
			cu->m_bDel = false;

			CMStringW wszNick(ptrW(db_get_wsa(cc->m_hContact, m_szModuleName, CMStringA(FORMAT, "nick%d", cu->m_uid))));
			if (wszNick.IsEmpty()) {
				CMStringW fName(jnUser["first_name"].as_mstring());
				CMStringW lName(jnUser["last_name"].as_mstring());
				wszNick = fName.Trim() + L" " + lName.Trim();
			}
			cu->m_wszNick = mir_wstrdup(wszNick);
			cu->m_bUnknown = false;
			
			if (bNew) {
				GCDEST gcd = { m_szModuleName, cc->m_wszId, GC_EVENT_JOIN };
				GCEVENT gce = { sizeof(GCEVENT), &gcd };
				gce.bIsMe = uid == m_myUserId;
				gce.ptszUID = wszId;
				gce.ptszNick = wszNick;
				gce.ptszStatus = TranslateW(sttStatuses[uid == cc->m_admin_id]);
				gce.dwItemData = (INT_PTR)cu;
				CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gce);
			}
		}

		for (int i = cc->m_users.getCount() - 1; i >= 0; i--) {
			CVkChatUser &cu = cc->m_users[i];
			if (!cu.m_bDel)
				continue;

			wchar_t wszId[20];
			_itow(cu.m_uid, wszId, 10);

			GCDEST gcd = { m_szModuleName, cc->m_wszId, GC_EVENT_PART };
			GCEVENT gce = { sizeof(GCEVENT), &gcd };
			gce.ptszUID = wszId;
			gce.dwFlags = GCEF_REMOVECONTACT | GCEF_NOTNOTIFY;
			gce.time = time(NULL);
			gce.ptszNick = mir_wstrdup(CMStringW(FORMAT, L"%s (https://vk.com/id%s)", cu.m_wszNick, wszId));
			CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gce);

			cc->m_users.remove(i);
		}
	}

	const JSONNode &jnMsgsUsers = jnResponse["msgs_users"];
	for (auto it = jnMsgsUsers.begin(); it != jnMsgsUsers.end(); ++it) {
		const JSONNode &jnUser = (*it);
		LONG uid = jnUser["id"].as_int();
		CVkChatUser *cu = cc->m_users.find((CVkChatUser*)&uid);
		if (cu)
			continue;
		
		MCONTACT hContact = FindUser(uid);
		if (hContact)
			continue;

		hContact = SetContactInfo(jnUser, true);
		db_set_b(hContact, "CList", "Hidden", 1);
		db_set_b(hContact, "CList", "NotOnList", 1);
		db_set_dw(hContact, "Ignore", "Mask1", 0);
	}

	const JSONNode &jnMsgs = jnResponse["msgs"];
	const JSONNode &jnFUsers = jnResponse["fwd_users"];
	if (jnMsgs) {
		
		const JSONNode &jnItems = jnMsgs["items"];
		if (jnItems) {
			for (auto it = jnItems.begin(); it != jnItems.end(); ++it) {
				const JSONNode &jnMsg = (*it);
				if (!jnMsg)
					break;

				AppendChatMessage(cc->m_chatid, jnMsg, jnFUsers, true);
			}
			cc->m_bHistoryRead = true;
		}
	}

	for (int j = 0; j < cc->m_msgs.getCount(); j++) {
		CVkChatMessage &p = cc->m_msgs[j];
		AppendChatMessage(cc, p.m_uid, p.m_date, p.m_wszBody, p.m_bHistory, p.m_bIsAction);
	}
	cc->m_msgs.destroy();
}

void CVkProto::SetChatTitle(CVkChatInfo *cc, LPCWSTR wszTopic)
{
	debugLogW(L"CVkProto::SetChatTitle");
	if (!cc)
		return;

	if (mir_wstrcmp(cc->m_wszTopic, wszTopic) == 0)
		return;

	cc->m_wszTopic = mir_wstrdup(wszTopic);
	setWString(cc->m_hContact, "Nick", wszTopic);

	GCDEST gcd = { m_szModuleName, cc->m_wszId, GC_EVENT_CHANGESESSIONAME };
	GCEVENT gce = { sizeof(GCEVENT), &gcd };
	gce.ptszText = wszTopic;
	CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gce);
}

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

void CVkProto::AppendChatMessage(int id, const JSONNode &jnMsg, const JSONNode &jnFUsers, bool bIsHistory)
{
	debugLogA("CVkProto::AppendChatMessage");
	CVkChatInfo *cc = AppendChat(id, nullNode);
	if (cc == NULL)
		return;

	int mid = jnMsg["id"].as_int();
	int uid = jnMsg["user_id"].as_int();
	bool bIsAction = false;

	int msgTime = jnMsg["date"].as_int();
	time_t now = time(NULL);
	if (!msgTime || msgTime > now)
		msgTime = now;

	CMStringW wszBody(jnMsg["body"].as_mstring());

	const JSONNode &jnFwdMessages = jnMsg["fwd_messages"];
	if (jnFwdMessages) {
		CMStringW wszFwdMessages = GetFwdMessages(jnFwdMessages, jnFUsers, bbcNo);
		if (!wszBody.IsEmpty())
			wszFwdMessages = L"\n" + wszFwdMessages;
		wszBody += wszFwdMessages;
	}

	const JSONNode &jnAttachments = jnMsg["attachments"];
	if (jnAttachments) {
		CMStringW wszAttachmentDescr = GetAttachmentDescr(jnAttachments, bbcNo);
		if (!wszBody.IsEmpty())
			wszAttachmentDescr = L"\n" + wszAttachmentDescr;
		wszBody += wszAttachmentDescr;
	}

	if (jnMsg["action"]) {
		bIsAction = true;
		CMStringW wszAction = jnMsg["action"].as_mstring();
		
		if (wszAction == L"chat_create") {
			CMStringW wszActionText = jnMsg["action_text"].as_mstring();
			wszBody.AppendFormat(L"%s \"%s\"", TranslateT("create chat"), wszActionText.IsEmpty() ? L" " : wszActionText);
		}
		else if (wszAction == L"chat_kick_user") {
			CMStringW wszActionMid = jnMsg["action_mid"].as_mstring();
			if (wszActionMid.IsEmpty())
				wszBody = TranslateT("kick user");
			else {
				CMStringW wszUid(FORMAT, L"%d", uid);
				if (wszUid == wszActionMid) {
					if (cc->m_bHistoryRead)
						return;
					wszBody.AppendFormat(L" (https://vk.com/id%s) %s", wszUid, TranslateT("left chat"));
				}
				else {
					int a_uid = 0;
					int iReadCount = swscanf(wszActionMid, L"%d", &a_uid);
					if (iReadCount == 1) {
						CVkChatUser *cu = cc->m_users.find((CVkChatUser*)&a_uid);
						if (cu == NULL)
							wszBody.AppendFormat(L"%s (https://vk.com/id%d)", TranslateT("kick user"), a_uid);
						else
							wszBody.AppendFormat(L"%s %s (https://vk.com/id%d)", TranslateT("kick user"), cu->m_wszNick, a_uid);
					}
					else 
						wszBody = TranslateT("kick user");
				}
			}
		}
		else if (wszAction == L"chat_invite_user") {
			CMStringW wszActionMid = jnMsg["action_mid"].as_mstring();
			if (wszActionMid.IsEmpty())
				wszBody = TranslateT("invite user");
			else {
				CMStringW wszUid(FORMAT, L"%d", uid);
				if (wszUid == wszActionMid)
					wszBody.AppendFormat(L" (https://vk.com/id%s) %s", wszUid, TranslateT("returned to chat"));
				else {
					int a_uid = 0;
					int iReadCount = swscanf(wszActionMid, L"%d", &a_uid);
					if (iReadCount == 1) {
						CVkChatUser *cu = cc->m_users.find((CVkChatUser*)&a_uid);
						if (cu == NULL)
							wszBody.AppendFormat(L"%s (https://vk.com/id%d)", TranslateT("invite user"), a_uid);
						else
							wszBody.AppendFormat(L"%s %s (https://vk.com/id%d)", TranslateT("invite user"), cu->m_wszNick, a_uid);
					}
					else
						wszBody = TranslateT("invite user");
				}
			}
		}
		else if (wszAction == L"chat_title_update") {
			CMStringW wszTitle = jnMsg["action_text"].as_mstring();
			wszBody.AppendFormat(L"%s \"%s\"", TranslateT("change chat title to"), wszTitle.IsEmpty() ? L" " : wszTitle);

			if (!bIsHistory)
				SetChatTitle(cc, wszTitle);
		}
		else if (wszAction == L"chat_photo_update")
			wszBody.Replace(TranslateT("Attachments:"), TranslateT("changed chat cover:"));
		else if (wszAction == L"chat_photo_remove")
			wszBody = TranslateT("deleted chat cover");
		else
			wszBody.AppendFormat(L": %s (%s)", TranslateT("chat action not supported"), wszAction);
	}

	wszBody.Replace(L"%", L"%%");

	if (cc->m_bHistoryRead) {
		if (jnMsg["title"])
			SetChatTitle(cc, jnMsg["title"].as_mstring());
		AppendChatMessage(cc, uid, msgTime, wszBody, bIsHistory, bIsAction);
	}
	else {
		CVkChatMessage *cm = cc->m_msgs.find((CVkChatMessage *)&mid);
		if (cm == NULL)
			cc->m_msgs.insert(cm = new CVkChatMessage(mid));

		cm->m_uid = uid;
		cm->m_date = msgTime;
		cm->m_wszBody = mir_wstrdup(wszBody);
		cm->m_bHistory = bIsHistory;
		cm->m_bIsAction = bIsAction;
	}
}

void CVkProto::AppendChatMessage(CVkChatInfo *cc, int uid, int msgTime, LPCWSTR pwszBody, bool bIsHistory, bool bIsAction)
{
	debugLogA("CVkProto::AppendChatMessage2");
	MCONTACT hContact = FindUser(uid);
	CVkChatUser *cu = cc->m_users.find((CVkChatUser*)&uid);
	if (cu == NULL) {
		cc->m_users.insert(cu = new CVkChatUser(uid));
		CMStringW wszNick(ptrW(db_get_wsa(cc->m_hContact, m_szModuleName, CMStringA(FORMAT, "nick%d", cu->m_uid))));
		cu->m_wszNick = mir_wstrdup(wszNick.IsEmpty() ? (hContact ? ptrW(db_get_wsa(hContact, m_szModuleName, "Nick")) : TranslateT("Unknown")) : wszNick);
		cu->m_bUnknown = true;
	}

	wchar_t wszId[20];
	_itow(uid, wszId, 10);

	GCDEST gcd = { m_szModuleName, cc->m_wszId, bIsAction ? GC_EVENT_ACTION : GC_EVENT_MESSAGE };
	GCEVENT gce = { sizeof(GCEVENT), &gcd };
	gce.bIsMe = (uid == m_myUserId);
	gce.ptszUID = wszId;
	gce.time = msgTime;
	gce.dwFlags = (bIsHistory) ? GCEF_NOTNOTIFY : GCEF_ADDTOLOG;
	gce.ptszNick = cu->m_wszNick ? mir_wstrdup(cu->m_wszNick) : mir_wstrdup(hContact ? ptrW(db_get_wsa(hContact, m_szModuleName, "Nick")) : TranslateT("Unknown"));
	gce.ptszText = IsEmpty((wchar_t *)pwszBody) ? mir_wstrdup(L"...") : mir_wstrdup(pwszBody);
	CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gce);
	StopChatContactTyping(cc->m_chatid, uid);
}

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

CVkChatInfo* CVkProto::GetChatById(LPCWSTR pwszId)
{
	for (int i = 0; i < m_chats.getCount(); i++)
		if (!mir_wstrcmp(m_chats[i].m_wszId, pwszId))
			return &m_chats[i];

	return NULL;
}

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

void CVkProto::SetChatStatus(MCONTACT hContact, int iStatus)
{
	ptrW wszChatID(getWStringA(hContact, "ChatRoomID"));
	if (wszChatID == NULL)
		return;

	CVkChatInfo *cc = GetChatById(wszChatID);
	if (cc == NULL)
		return;

	GCDEST gcd = { m_szModuleName, wszChatID, GC_EVENT_CONTROL };
	GCEVENT gce = { sizeof(gce), &gcd };
	CallServiceSync(MS_GC_EVENT, (iStatus == ID_STATUS_OFFLINE) ? SESSION_OFFLINE : SESSION_ONLINE, (LPARAM)&gce);
}

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

wchar_t* UnEscapeChatTags(wchar_t *str_in)
{
	wchar_t *s = str_in, *d = str_in;
	while (*s) {
		if (*s == '%' && s[1] == '%')
			s++;
		*d++ = *s++;
	}
	*d = 0;
	return str_in;
}

int CVkProto::OnChatEvent(WPARAM, LPARAM lParam)
{
	GCHOOK *gch = (GCHOOK*)lParam;
	if (gch == NULL)
		return 0;

	if (mir_strcmpi(gch->pDest->pszModule, m_szModuleName))
		return 0;

	CVkChatInfo *cc = GetChatById(gch->pDest->ptszID);
	if (cc == NULL)
		return 0;

	switch (gch->pDest->iType) {
	case GC_USER_MESSAGE:
		if (IsOnline() && mir_wstrlen(gch->ptszText) > 0) {
			ptrW pwszBuf(mir_wstrdup(gch->ptszText));
			rtrimw(pwszBuf);
			UnEscapeChatTags(pwszBuf);
			SendMsg(cc->m_hContact, 0, T2Utf(pwszBuf));
		}
		break;

	case GC_USER_PRIVMESS:
		{
			MCONTACT hContact = FindUser(_wtoi(gch->ptszUID));
			if (hContact == NULL) {
				hContact = FindUser(_wtoi(gch->ptszUID), true);
				db_set_b(hContact, "CList", "Hidden", 1);
				db_set_b(hContact, "CList", "NotOnList", 1);
				db_set_dw(hContact, "Ignore", "Mask1", 0);
				RetrieveUserInfo(_wtoi(gch->ptszUID));
			}
			CallService(MS_MSG_SENDMESSAGEW, hContact);
		}
		break;

	case GC_USER_LOGMENU:
		LogMenuHook(cc, gch);
		break;

	case GC_USER_NICKLISTMENU:
		NickMenuHook(cc, gch);
		break;
	}
	return 0;
}

void CVkProto::OnSendChatMsg(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq)
{
	debugLogA("CVkProto::OnSendChatMsg %d", reply->resultCode);
	int iResult = ACKRESULT_FAILED;
	if (reply->resultCode == 200) {
		JSONNode jnRoot;
		CheckJsonResponse(pReq, reply, jnRoot);
		iResult = ACKRESULT_SUCCESS;
	}
	if (!pReq->pUserInfo)
		return;
	
	CVkFileUploadParam *fup = (CVkFileUploadParam *)pReq->pUserInfo;
	ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, iResult, (HANDLE)(fup));
	if (!pReq->bNeedsRestart || m_bTerminated) {
		delete fup;
		pReq->pUserInfo = NULL;
	}
}

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

LPTSTR CVkProto::ChangeChatTopic(CVkChatInfo *cc)
{
	ENTER_STRING pForm = { sizeof(pForm) };
	pForm.type = ESF_MULTILINE;
	pForm.caption = TranslateT("Enter new chat title");
	pForm.ptszInitVal = cc->m_wszTopic;
	pForm.szModuleName = m_szModuleName;
	pForm.szDataPrefix = "gctopic_";
	return (!EnterString(&pForm)) ? NULL : pForm.ptszResult;
}

void CVkProto::LogMenuHook(CVkChatInfo *cc, GCHOOK *gch)
{
	if (!IsOnline())
		return;

	switch (gch->dwData) {
	case IDM_TOPIC:
		if (LPTSTR pwszNew = ChangeChatTopic(cc)) {
			Push(new AsyncHttpRequest(this, REQUEST_GET, "/method/messages.editChat.json", true, &CVkProto::OnReceiveSmth)
				<< WCHAR_PARAM("title", pwszNew) 
				<< INT_PARAM("chat_id", cc->m_chatid));
			mir_free(pwszNew);
		}
		break;

	case IDM_INVITE:
		{
			CVkInviteChatForm dlg(this);
			if (dlg.DoModal() && dlg.m_hContact != NULL) {
				int uid = getDword(dlg.m_hContact, "ID", VK_INVALID_USER);
				if (uid != VK_INVALID_USER)
					Push(new AsyncHttpRequest(this, REQUEST_GET, "/method/messages.addChatUser.json", true, &CVkProto::OnReceiveSmth)
						<< INT_PARAM("user_id", uid)
						<< INT_PARAM("chat_id", cc->m_chatid));
			}
		}
		break;

	case IDM_DESTROY:
		if (IDYES == MessageBoxW(NULL,
			TranslateT("This chat is going to be destroyed forever with all its contents. This action cannot be undone. Are you sure?"),
			TranslateT("Warning"), MB_YESNO | MB_ICONQUESTION))
		{
			CMStringA code;
			code.Format("API.messages.removeChatUser({\"chat_id\":%d, \"user_id\":%d});"
				"var Hist = API.messages.getHistory({\"chat_id\":%d, \"count\":200});"
				"var countMsg = Hist.count;var itemsMsg = Hist.items@.id; "
				"while (countMsg > 0) { API.messages.delete({\"message_ids\":itemsMsg});"
				"Hist=API.messages.getHistory({\"chat_id\":%d, \"count\":200});"
				"countMsg = Hist.count;itemsMsg = Hist.items@.id;}; return 1;", cc->m_chatid, m_myUserId, cc->m_chatid, cc->m_chatid);
			Push(new AsyncHttpRequest(this, REQUEST_GET, "/method/execute.json", true, &CVkProto::OnChatDestroy)
				<< CHAR_PARAM("code", code))->pUserInfo = cc;
		}
		break;
	}
}

INT_PTR __cdecl CVkProto::OnJoinChat(WPARAM hContact, LPARAM)
{
	debugLogA("CVkProto::OnJoinChat");
	if (!IsOnline())
		return 1;

	if (getBool(hContact, "kicked"))
		return 1;
	
	if (!getBool(hContact, "off"))
		return 1;

	int chat_id = getDword(hContact, "vk_chat_id", VK_INVALID_USER);
	
	if (chat_id == VK_INVALID_USER)
		return 1;

	AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_POST, "/method/messages.send.json", true, &CVkProto::OnSendChatMsg, AsyncHttpRequest::rpHigh)
		<< INT_PARAM("chat_id", chat_id)
		<< WCHAR_PARAM("message", m_vkOptions.pwszReturnChatMessage);
	pReq->AddHeader("Content-Type", "application/x-www-form-urlencoded");
	Push(pReq);
	db_unset(hContact, m_szModuleName, "off");
	return 0;
}

INT_PTR __cdecl CVkProto::OnLeaveChat(WPARAM hContact, LPARAM)
{
	debugLogA("CVkProto::OnLeaveChat");
	if (!IsOnline())
		return 1;

	ptrW wszChatID(getWStringA(hContact, "ChatRoomID"));
	if (wszChatID == NULL)
		return 1;

	CVkChatInfo *cc = GetChatById(wszChatID);
	if (cc == NULL)
		return 1;
	
	Push(new AsyncHttpRequest(this, REQUEST_GET, "/method/messages.removeChatUser.json", true, &CVkProto::OnChatLeave)
		<< INT_PARAM("chat_id", cc->m_chatid)
		<< INT_PARAM("user_id", m_myUserId))->pUserInfo = cc;

	return 0;
}

void CVkProto::LeaveChat(int chat_id, bool close_window, bool delete_chat)
{
	debugLogA("CVkProto::LeaveChat");
	CVkChatInfo *cc = (CVkChatInfo*)m_chats.find((CVkChatInfo*)&chat_id);
	if (cc == NULL)
		return;

	GCDEST gcd = { m_szModuleName, cc->m_wszId, GC_EVENT_QUIT };
	GCEVENT gce = { sizeof(GCEVENT), &gcd };
	CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gce);
	gcd.iType = GC_EVENT_CONTROL;
	CallServiceSync(MS_GC_EVENT, close_window? SESSION_TERMINATE:SESSION_OFFLINE, (LPARAM)&gce);
	if (delete_chat)
		CallService(MS_DB_CONTACT_DELETE, (WPARAM)cc->m_hContact);
	else
		setByte(cc->m_hContact, "off", (int)true);
	m_chats.remove(cc);
}

void CVkProto::KickFromChat(int chat_id, int user_id, const JSONNode &jnMsg, const JSONNode &jnFUsers)
{
	debugLogA("CVkProto::KickFromChat (%d)", user_id);

	MCONTACT chatContact = FindChat(chat_id);
	if (chatContact && getBool(chatContact, "off"))
		return;

	if (user_id == m_myUserId)
		LeaveChat(chat_id);

	CVkChatInfo *cc = (CVkChatInfo*)m_chats.find((CVkChatInfo*)&chat_id);
	if (cc == NULL)
		return;

	MCONTACT hContact = FindUser(user_id, false);
	CMStringW msg(jnMsg["body"].as_mstring());
	if (msg.IsEmpty()) {
		msg = TranslateT("You've been kicked by ");
		if (hContact != NULL)
			msg += ptrW(db_get_wsa(hContact, m_szModuleName, "Nick"));
		else
			msg += TranslateT("(Unknown contact)");
	}
	else 
		AppendChatMessage(chat_id, jnMsg, jnFUsers, false);

	MsgPopup(hContact, msg, TranslateT("Chat"));
	setByte(cc->m_hContact, "kicked", 1);
	LeaveChat(chat_id);
}

void CVkProto::OnChatLeave(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq)
{
	debugLogA("CVkProto::OnChatLeave %d", reply->resultCode);
	if (reply->resultCode != 200)
		return;

	CVkChatInfo *cc = (CVkChatInfo*)pReq->pUserInfo;
	LeaveChat(cc->m_chatid);
}

INT_PTR __cdecl CVkProto::SvcDestroyKickChat(WPARAM hContact, LPARAM)
{
	debugLogA("CVkProto::SvcDestroyKickChat");
	if (!IsOnline())
		return 1;

	if (!getBool(hContact, "off"))
		return 1;
		
	int chat_id = getDword(hContact, "vk_chat_id", VK_INVALID_USER);
	if (chat_id == VK_INVALID_USER)
		return 1;

	CMStringA code;
	code.Format("var Hist = API.messages.getHistory({\"chat_id\":%d, \"count\":200});"
		"var countMsg = Hist.count;var itemsMsg = Hist.items@.id; "
		"while (countMsg > 0) { API.messages.delete({\"message_ids\":itemsMsg});"
		"Hist=API.messages.getHistory({\"chat_id\":%d, \"count\":200});"
		"countMsg = Hist.count;itemsMsg = Hist.items@.id;}; return 1;", chat_id, chat_id);
	Push(new AsyncHttpRequest(this, REQUEST_GET, "/method/execute.json", true, &CVkProto::OnReceiveSmth)
		<< CHAR_PARAM("code", code));

	CallService(MS_DB_CONTACT_DELETE, (WPARAM)hContact);

	return 0;
}

void CVkProto::OnChatDestroy(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq)
{
	debugLogA("CVkProto::OnChatDestroy %d", reply->resultCode);
	if (reply->resultCode != 200)
		return;

	CVkChatInfo *cc = (CVkChatInfo*)pReq->pUserInfo;
	LeaveChat(cc->m_chatid, true, true);
}

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

void CVkProto::NickMenuHook(CVkChatInfo *cc, GCHOOK *gch)
{
	CVkChatUser *cu = cc->GetUserById(gch->ptszUID);
	MCONTACT hContact;
	if (cu == NULL)
		return;

	char szUid[20], szChatId[20];
	_itoa(cu->m_uid, szUid, 10);
	_itoa(cc->m_chatid, szChatId, 10);

	switch (gch->dwData) {
	case IDM_INFO:
		hContact = FindUser(cu->m_uid);
		if (hContact == NULL) {
			hContact = FindUser(cu->m_uid, true);
			db_set_b(hContact, "CList", "Hidden", 1);
			db_set_b(hContact, "CList", "NotOnList", 1);
			db_set_dw(hContact, "Ignore", "Mask1", 0);
		}
		CallService(MS_USERINFO_SHOWDIALOG, hContact);
		break;

	case IDM_VISIT_PROFILE:
		hContact = FindUser(cu->m_uid);
		if (hContact == NULL)
			Utils_OpenUrlW(CMStringW(FORMAT, L"https://vk.com/id%d", cu->m_uid));
		else 
			SvcVisitProfile(hContact, 0);
		break;

	case IDM_CHANGENICK:
	{
		CMStringW wszNewNick = RunRenameNick(cu->m_wszNick);
		if (wszNewNick.IsEmpty() || wszNewNick == cu->m_wszNick)
			break;

		GCDEST gcd = { m_szModuleName, cc->m_wszId, GC_EVENT_NICK };
		GCEVENT gce = { sizeof(GCEVENT), &gcd };

		wchar_t wszId[20];
		_itow(cu->m_uid, wszId, 10);

		gce.ptszNick = mir_wstrdup(cu->m_wszNick);
		gce.bIsMe = (cu->m_uid == m_myUserId);
		gce.ptszUID = wszId;
		gce.ptszText = mir_wstrdup(wszNewNick);
		gce.dwFlags = GCEF_ADDTOLOG;
		gce.time = time(NULL);
		CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gce);

		cu->m_wszNick = mir_wstrdup(wszNewNick);		
		setWString(cc->m_hContact, CMStringA(FORMAT, "nick%d", cu->m_uid), wszNewNick);
	}
		break;

	case IDM_KICK:
		if (!IsOnline())
			return;

		Push(new AsyncHttpRequest(this, REQUEST_GET, "/method/messages.removeChatUser.json", true, &CVkProto::OnReceiveSmth)
			<< INT_PARAM("chat_id", cc->m_chatid) 
			<< INT_PARAM("user_id", cu->m_uid));
		cu->m_bUnknown = true;

		break;
	}
}

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

static gc_item sttLogListItems[] =
{
	{ LPGENW("&Invite a user"), IDM_INVITE, MENU_ITEM },
	{ LPGENW("View/change &title"), IDM_TOPIC, MENU_ITEM },
	{ NULL, 0, MENU_SEPARATOR },
	{ LPGENW("&Destroy room"), IDM_DESTROY, MENU_ITEM }
};

static gc_item sttListItems[] =
{
	{ LPGENW("&User details"), IDM_INFO, MENU_ITEM },
	{ LPGENW("Visit profile"), IDM_VISIT_PROFILE, MENU_ITEM },
	{ LPGENW("Change nick"), IDM_CHANGENICK, MENU_ITEM },
	{ LPGENW("&Kick"), IDM_KICK, MENU_ITEM }
};

int CVkProto::OnGcMenuHook(WPARAM, LPARAM lParam)
{
	GCMENUITEMS *gcmi = (GCMENUITEMS*)lParam;
	if (gcmi == NULL)
		return 0;

	if (mir_strcmpi(gcmi->pszModule, m_szModuleName))
		return 0;

	if (gcmi->Type == MENU_ON_LOG) {
		gcmi->nItems = _countof(sttLogListItems);
		gcmi->Item = sttLogListItems;
	}
	else if (gcmi->Type == MENU_ON_NICKLIST) {
		gcmi->nItems = _countof(sttListItems);
		gcmi->Item = sttListItems;
	}
	return 0;
}

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

void CVkProto::ChatContactTypingThread(void *p)
{
	CVKChatContactTypingParam *param = (CVKChatContactTypingParam *)p;
	if (!p)
		return;

	int iChatId = param->m_ChatId;
	int iUserId = param->m_UserId;

	debugLogA("CVkProto::ChatContactTypingThread %d %d", iChatId, iUserId);

	MCONTACT hChatContact = FindChat(iChatId);
	if (hChatContact && getBool(hChatContact, "off")) {
		delete param;
		return;
	}
	CVkChatInfo *cc = (CVkChatInfo*)m_chats.find((CVkChatInfo*)&iChatId);
	if (cc == NULL) {
		delete param;
		return;
	}
	CVkChatUser *cu = cc->GetUserById(iUserId);
	if (cu == NULL) {
		delete param;
		return;
	}
	
	{
		mir_cslock lck(m_csChatTyping);
		CVKChatContactTypingParam *cp = (CVKChatContactTypingParam *)m_ChatsTyping.find((CVKChatContactTypingParam *)&iChatId);
		if (cp != NULL)
			m_ChatsTyping.remove(cp);	
		m_ChatsTyping.insert(param);
		
		StatusTextData st = { 0 };
		st.cbSize = sizeof(st);
		mir_snwprintf(st.tszText, TranslateT("%s is typing a message..."), cu->m_wszNick);

		CallService(MS_MSG_SETSTATUSTEXT, (WPARAM)hChatContact, (LPARAM)&st);
	}

	Sleep(9500);
	StopChatContactTyping(iChatId, iUserId);
}

void CVkProto::StopChatContactTyping(int iChatId, int iUserId)
{
	debugLogA("CVkProto::StopChatContactTyping %d %d", iChatId, iUserId);
	MCONTACT hChatContact = FindChat(iChatId);
	if (hChatContact && getBool(hChatContact, "off")) 
		return;
	
	CVkChatInfo *cc = (CVkChatInfo*)m_chats.find((CVkChatInfo*)&iChatId);
	if (cc == NULL)
		return;
	
	CVkChatUser *cu = cc->GetUserById(iUserId);
	if (cu == NULL)
		return;
	
	mir_cslock lck(m_csChatTyping);
	CVKChatContactTypingParam *cp = (CVKChatContactTypingParam *)m_ChatsTyping.find((CVKChatContactTypingParam *)&iChatId);

	if (cp != NULL && cp->m_UserId == iUserId) {
		m_ChatsTyping.remove(cp);

		StatusTextData st = { 0 };
		st.cbSize = sizeof(st);
		mir_snwprintf(st.tszText, L" ");
		
		// CallService(MS_MSG_SETSTATUSTEXT, (WPARAM)hChatContact, NULL) clears statusbar very slowly. 
		// (1-10 sec(!!!) for me on tabSRMM O_o)
		// So I call MS_MSG_SETSTATUSTEXT with st.wszText = " " for cleaning of "... is typing a message..." string.
		// It works instantly!
		
		CallService(MS_MSG_SETSTATUSTEXT, (WPARAM)hChatContact, (LPARAM)&st);
				
		// After that I call standard cleaning procedure:

		CallService(MS_MSG_SETSTATUSTEXT, (WPARAM)hChatContact);
	}
}

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

INT_PTR CVkProto::SvcCreateChat(WPARAM, LPARAM)
{
	if (!IsOnline())
		return (INT_PTR)1;
	CVkGCCreateForm dlg(this);
	return (INT_PTR)!dlg.DoModal();
}

void CVkProto::CreateNewChat(LPCSTR uids, LPCWSTR pwszTitle)
{
	if (!IsOnline())
		return;
	Push(new AsyncHttpRequest(this, REQUEST_GET, "/method/messages.createChat.json", true, &CVkProto::OnCreateNewChat)
		<< WCHAR_PARAM("title", pwszTitle ? pwszTitle : L"")
		<< CHAR_PARAM("user_ids", uids));
}

void CVkProto::OnCreateNewChat(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq)
{
	debugLogA("CVkProto::OnCreateNewChat %d", reply->resultCode);
	if (reply->resultCode != 200)
		return;

	JSONNode jnRoot;
	const JSONNode &jnResponse = CheckJsonResponse(pReq, reply, jnRoot);
	if (!jnResponse)
		return;

	int chat_id = jnResponse.as_int();
	if (chat_id != 0)
		AppendChat(chat_id, nullNode);
}