/*
Copyright (C) 2013 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"

void CVkProto::ShutdownSession()
{
	if (m_hWorkerThread) {
		m_bTerminated = true;
		SetEvent(m_evRequestsQueue);
	}

	OnLoggedOut();
}

void CVkProto::ConnectionFailed(int iReason)
{
	delSetting("AccessToken");

	ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, iReason);

	ShutdownSession();
}

static VOID CALLBACK TimerProc(HWND, UINT, UINT_PTR pObject, DWORD)
{
	CVkProto *ppro = (CVkProto*)pObject;
	ppro->SetServerStatus(ppro->m_iStatus);
}

void CVkProto::OnLoggedIn()
{
	m_bOnline = true;
	SetServerStatus(m_iDesiredStatus);

	// initialize online timer
	m_timer = SetTimer(NULL, (UINT_PTR)this, 870000, TimerProc);
}

void CVkProto::OnLoggedOut()
{
	m_bOnline = false;

	if (m_pollingConn)
		CallService(MS_NETLIB_SHUTDOWN, (WPARAM)m_pollingConn, 0);

	ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE);
	m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE;

	KillTimer(NULL, m_timer);
	SetAllContactStatuses(ID_STATUS_OFFLINE);
}

void CVkProto::SetServerStatus(int iNewStatus)
{
	if ( !IsOnline() || iNewStatus < ID_STATUS_OFFLINE)
		return;

	int iOldStatus = m_iStatus;

	if (iNewStatus == ID_STATUS_OFFLINE) {
		m_iStatus = ID_STATUS_OFFLINE; 
		HttpParam param = { "access_token", m_szAccessToken };
		PushAsyncHttpRequest(REQUEST_GET, "/method/account.setOffline.json", true, &CVkProto::OnReceiveSmth, 1, &param);
	}
	else if (iNewStatus != ID_STATUS_INVISIBLE) {
		m_iStatus = ID_STATUS_ONLINE; 
		HttpParam param = { "access_token", m_szAccessToken };
		PushAsyncHttpRequest(REQUEST_GET, "/method/account.setOnline.json", true, &CVkProto::OnReceiveSmth, 1, &param);
	}
	else m_iStatus = ID_STATUS_INVISIBLE; 

	ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus);
}

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

static char VK_TOKEN_BEG[] = "access_token=";

void CVkProto::OnOAuthAuthorize(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq)
{
	GrabCookies(reply);

	if (reply->resultCode == 302) { // manual redirect
		LPCSTR pszLocation = findHeader(reply, "Location");
		if (pszLocation) {
			if ( !_strnicmp(pszLocation, VK_REDIRECT_URL, sizeof(VK_REDIRECT_URL)-1)) {
				m_szAccessToken = NULL;
				LPCSTR p = strstr(pszLocation, VK_TOKEN_BEG);
				if (p) {
					p += sizeof(VK_TOKEN_BEG)-1;
					for (LPCSTR q = p+1; *q; q++) {
						if (*q == '&' || *q == '=' || *q == '\"') {
							m_szAccessToken = mir_strndup(p, q-p);
							break;
						}
					}
					if (m_szAccessToken == NULL)
						m_szAccessToken = mir_strdup(p);
					setString("AccessToken", m_szAccessToken);
					RetrieveMyInfo();
				}
				else {
					delSetting("AccessToken");
					ConnectionFailed(LOGINERR_NOSERVER);
				}
			}
			else {
				AsyncHttpRequest *pReq = new AsyncHttpRequest();
				pReq->requestType = REQUEST_GET;
				pReq->flags = NLHRF_DUMPASTEXT | NLHRF_HTTP11;
				pReq->m_pFunc = &CVkProto::OnOAuthAuthorize;
				pReq->AddHeader("Referer", m_prevUrl);
				pReq->Redirect(reply);
				if (pReq->szUrl) {
					ApplyCookies(pReq);
					m_prevUrl = pReq->szUrl;
				}

				PushAsyncHttpRequest(pReq);
			}
		}
		else ConnectionFailed(LOGINERR_NOSERVER);
		return;
	}

	if (reply->resultCode != 200) { // something went wrong
LBL_NoForm:
		ConnectionFailed(LOGINERR_NOSERVER);
		return;
	}

	if ( strstr(reply->pData, "Invalid login or password") ||
		  strstr(reply->pData, ptrA( mir_utf8encodecp("�������� ����� ��� ������", 1251))))
	{
		ConnectionFailed(LOGINERR_WRONGPASSWORD);
		return;
	}

	// Application requests access to user's account
	if ( !strstr(reply->pData, "form method=\"post\""))
		goto LBL_NoForm;

	CMStringA szAction, szBody;
	bool bSuccess = AutoFillForm(reply->pData, szAction, szBody);
	if (!bSuccess || szAction.IsEmpty() || szBody.IsEmpty()) {
		if (m_prevError)
			goto LBL_NoForm;
		m_prevError = true;
	}

	pReq = new AsyncHttpRequest();
	pReq->requestType = REQUEST_POST;
	pReq->flags = NLHRF_DUMPASTEXT | NLHRF_HTTP11;
	pReq->pData = mir_strdup(szBody);
	pReq->dataLength = szBody.GetLength();
	pReq->szUrl = mir_strdup(szAction); m_prevUrl = pReq->szUrl;
	pReq->m_pFunc = &CVkProto::OnOAuthAuthorize;
	pReq->AddHeader("Content-Type", "application/x-www-form-urlencoded");
	pReq->Redirect(reply);
	ApplyCookies(pReq);
	PushAsyncHttpRequest(pReq);
}

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

void CVkProto::RetrieveMyInfo()
{
	HttpParam param = { "access_token", m_szAccessToken };
	PushAsyncHttpRequest(REQUEST_GET, "/method/getUserInfoEx.json", true, &CVkProto::OnReceiveMyInfo, 1, &param);
}

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

	JSONROOT pRoot;
	JSONNODE *pResponse = CheckJsonResponse(pReq, reply, pRoot);
	if (pResponse == NULL)
		return;

	for (size_t i = 0; i < json_size(pResponse); i++) {
		JSONNODE *it = json_at(pResponse, i);
		LPCSTR id = json_name(it);
		if ( !_stricmp(id, "user_id")) {
			m_myUserId = json_as_int(it);
			setDword("ID", m_myUserId);
		}
	}

	OnLoggedIn();
	RetrieveUserInfo(m_myUserId);
	RetrieveFriends();
	RetrieveUnreadMessages();
	RetrievePollingInfo();
}

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

void CVkProto::RetrieveUserInfo(LONG userID)
{
	char szUserId[40];
	_itoa(userID, szUserId, 10);

	HttpParam params[] = {
		{ "fields", "uid,first_name,last_name,photo_medium,sex,bdate,city,relation" },
		{ "uids", szUserId },
		{ "access_token", m_szAccessToken }
	};
	PushAsyncHttpRequest(REQUEST_GET, "/method/getProfiles.json", true, &CVkProto::OnReceiveUserInfo, SIZEOF(params), params);
}

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

	JSONROOT pRoot;
	JSONNODE *pResponse = CheckJsonResponse(pReq, reply, pRoot);
	if (pResponse == NULL)
		return;

	for (size_t i=0; ; i++) {
		JSONNODE *pRecord = json_at(pResponse, i);
		if (pRecord == NULL) break;

		LONG userid = json_as_int( json_get(pRecord, "uid"));
		if (userid == 0)
			return;

		HANDLE hContact;
		if (userid == m_myUserId)
			hContact = NULL;
		else if ((hContact = FindUser(userid, false)) == NULL)
			return;

		CMString tszNick;
		ptrT szValue( json_as_string( json_get(pRecord, "first_name")));
		if (szValue) {
			setTString(hContact, "FirstName", szValue);
			tszNick.Append(szValue);
			tszNick.AppendChar(' ');
		}

		if (szValue = json_as_string( json_get(pRecord, "last_name"))) {
			setTString(hContact, "LastName", szValue);
			tszNick.Append(szValue);
		}

		if ( !tszNick.IsEmpty())
			setTString(hContact, "Nick", tszNick);
	
		setByte(hContact, "Gender", json_as_int( json_get(pRecord, "sex")) == 2 ? 'M' : 'F');
	
		if (szValue = json_as_string( json_get(pRecord, "bdate"))) {
			int d, m, y;
			if ( _stscanf(szValue, _T("%d.%d.%d"), &d, &m, &y) == 3) {
				setByte(hContact, "BirthDay", d);
				setByte(hContact, "BirthMonth", m);
				setWord(hContact, "BirthYear", y);
			}
		}

		szValue = json_as_string( json_get(pRecord, "photo_medium"));
		SetAvatarUrl(hContact, szValue);
	}
}

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

void CVkProto::RetrieveFriends()
{
	debugLogA("CVkProto::RetrieveFriends");

	HttpParam params[] = {
		{ "fields", "uid,first_name,last_name,photo_medium,sex,country,timezone,contacts" },
		{ "count", "1000" },
		{ "access_token", m_szAccessToken }
	};
	PushAsyncHttpRequest(REQUEST_GET, "/method/friends.get.json", true, &CVkProto::OnReceiveFriends, SIZEOF(params), params);
}

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

	JSONROOT pRoot;
	JSONNODE *pResponse = CheckJsonResponse(pReq, reply, pRoot), *pInfo;
	if (pResponse == NULL)
		return;

	for (int i=0; (pInfo = json_at(pResponse, i)) != NULL; i++) {
		ptrT szValue( json_as_string( json_get(pInfo, "uid")));
		if (szValue == NULL)
			continue;

		CMString tszNick;
		HANDLE hContact = FindUser( _ttoi(szValue), true);
		szValue = json_as_string( json_get(pInfo, "first_name"));
		if (szValue) {
			setTString(hContact, "FirstName", szValue);

			tszNick.Append(szValue);
			tszNick.AppendChar(' ');
		}

		if (szValue = json_as_string( json_get(pInfo, "last_name"))) {
			setTString(hContact, "LastName", szValue);
			tszNick.Append(szValue);
		}

		if ( !tszNick.IsEmpty())
			setTString(hContact, "Nick", tszNick);

		szValue = json_as_string( json_get(pInfo, "photo_medium"));
		SetAvatarUrl(hContact, szValue);

		setWord(hContact, "Status", (json_as_int( json_get(pInfo, "online")) == 0) ? ID_STATUS_OFFLINE : ID_STATUS_ONLINE); 

		int iValue = json_as_int( json_get(pInfo, "sex"));
		if (iValue)
			setByte(hContact, "Gender", (iValue == 2) ? 'M' : 'F');

		if ((iValue = json_as_int( json_get(pInfo, "timezone"))) != 0)
			setByte(hContact, "Timezone", iValue * -2);

		szValue = json_as_string( json_get(pInfo, "mobile_phone"));
		if (szValue && *szValue) setTString(hContact, "Cellular", szValue);
		szValue = json_as_string( json_get(pInfo, "home_phone"));
		if (szValue && *szValue) setTString(hContact, "Phone", szValue);
	}
}

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

void CVkProto::RetrieveUnreadMessages()
{
	debugLogA("CVkProto::RetrieveMessages");

	HttpParam params[] = {
		{ "code", "return{\"msgs\":API.messages.get({\"filters\":1})};" },
		{ "access_token", m_szAccessToken }
	};
	PushAsyncHttpRequest(REQUEST_GET, "/method/execute.json", true, &CVkProto::OnReceiveMessages, SIZEOF(params), params);
}

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

	JSONROOT pRoot;
	JSONNODE *pResponse = CheckJsonResponse(pReq, reply, pRoot);
	if (pResponse == NULL)
		return;

	JSONNODE *pMsgs = json_as_array( json_get(pResponse, "msgs"));
	if (pMsgs == NULL)
		pMsgs = pResponse;

	CMStringA mids;

	int numMessages = json_as_int( json_at(pMsgs, 0));
	for (int i=1; i <= numMessages; i++) {
		JSONNODE *pMsg = json_at(pMsgs, i);
		if (pMsg == NULL)
			continue;

		int mid = json_as_int( json_get(pMsg, "mid"));
		int datetime = json_as_int( json_get(pMsg, "date"));
		int isOut = json_as_int( json_get(pMsg, "out"));
		int uid = json_as_int( json_get(pMsg, "uid"));
		int isRead = json_as_int( json_get(pMsg, "read_state"));
		ptrT ptszBody( json_as_string( json_get(pMsg, "body")));

		JSONNODE *pAttachments = json_get(pMsg, "attachments");
		if (pAttachments != NULL) {
			CMString tszBody(ptszBody);
			tszBody.AppendChar('\n');
			tszBody += TranslateT("Attachments:");
			tszBody.AppendChar('\n');
			JSONNODE *pAttach;
			for (int k=0; (pAttach = json_at(pAttachments, k)) != NULL; k++) {
				tszBody.AppendChar('\t');
				ptrT ptszType( json_as_string( json_get(pAttach, "type")));
				if ( !lstrcmp(ptszType, _T("photo"))) {
					JSONNODE *pPhoto = json_get(pAttach, "photo");
					if (pPhoto == NULL) continue;

					ptrT ptszLink( json_as_string( json_get(pPhoto, "src_big")));
					int iWidth = json_as_int( json_get(pPhoto, "width"));
					int iHeight = json_as_int( json_get(pPhoto, "height"));
					tszBody.AppendFormat( _T("%s: %s (%dx%d)"), TranslateT("Photo"), ptszLink, iWidth, iHeight);
				}
				tszBody.AppendChar('\n');
			}
			ptszBody = mir_tstrdup(tszBody);
		}

		char szMid[40];
		_itoa(mid, szMid, 10);
		HANDLE hContact = FindUser(uid, true);

		PROTORECVEVENT recv = { 0 };
		recv.flags = PREF_TCHAR;
		if (isRead)
			recv.flags |= PREF_CREATEREAD;
		if (isOut)
			recv.flags |= PREF_SENT;
		recv.timestamp = datetime;
		recv.tszMessage = ptszBody;
		recv.lParam = isOut;
		recv.pCustomData = szMid;
		recv.cbCustomDataSize = (int)strlen(szMid);
		ProtoChainRecvMsg(hContact, &recv);

		if (!mids.IsEmpty())
			mids.AppendChar(',');
		mids.Append(szMid);
	}

	if (!mids.IsEmpty()) {
		HttpParam params[] = {
			{ "mids", mids },
			{ "access_token", m_szAccessToken }
		};
		PushAsyncHttpRequest(REQUEST_GET, "/method/messages.markAsRead.json", true, &CVkProto::OnReceiveSmth, SIZEOF(params), params);
	}
}

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

void CVkProto::RetrievePollingInfo()
{
	debugLogA("CVkProto::RetrievePollingInfo");

	HttpParam param = { "access_token", m_szAccessToken };
	PushAsyncHttpRequest(REQUEST_GET, "/method/messages.getLongPollServer.json", true, &CVkProto::OnReceivePollingInfo, 1, &param);
}

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

	JSONROOT pRoot;
	JSONNODE *pResponse = CheckJsonResponse(pReq, reply, pRoot);
	if (pResponse == NULL)
		return;

	m_pollingTs = mir_t2a( ptrT( json_as_string( json_get(pResponse, "ts"))));
	m_pollingKey = mir_t2a( ptrT( json_as_string( json_get(pResponse, "key"))));
	m_pollingServer = mir_t2a( ptrT( json_as_string( json_get(pResponse, "server"))));
	if (!m_hPollingThread && m_pollingTs != NULL && m_pollingKey != NULL && m_pollingServer != NULL)
		m_hPollingThread = ForkThreadEx(&CVkProto::PollingThread, NULL, NULL);
}

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

void CVkProto::PollUpdates(JSONNODE *pUpdates)
{
	debugLogA("CVkProto::PollUpdates");

	CMStringA mids;
	int msgid, uid, flags;
	HANDLE hContact;

	JSONNODE *pChild;
	for (int i=0; (pChild = json_at(pUpdates, i)) != NULL; i++) {
		switch (json_as_int( json_at(pChild, 0))) {
		case VKPOLL_MSG_ADDED: // new message
			msgid = json_as_int( json_at(pChild, 1));
			flags = json_as_int( json_at(pChild, 2));

			// skip outgoing messages sent from a client
			if ((flags & VKFLAG_MSGOUTBOX) && !(flags & VKFLAG_MSGCHAT))
				break;

			if ( !CheckMid(msgid)) {
				if ( !mids.IsEmpty())
					mids.AppendChar(',');
				mids.AppendFormat("%d", msgid);
			}
			break;

		case VKPOLL_USR_ONLINE:
			uid = -json_as_int( json_at(pChild, 1));
			if ((hContact = FindUser(uid)) != NULL)
				setWord(hContact, "Status", ID_STATUS_ONLINE);
			break;

		case VKPOLL_USR_OFFLINE:
			uid = -json_as_int( json_at(pChild, 1));
			if ((hContact = FindUser(uid)) != NULL)
				setWord(hContact, "Status", ID_STATUS_OFFLINE);
			break;

		case VKPOLL_USR_UTN:
			uid = json_as_int( json_at(pChild, 1));
			if ((hContact = FindUser(uid)) != NULL)
				CallService(MS_PROTO_CONTACTISTYPING, (WPARAM)hContact, 5);
			break;
		}
	}

	if ( !mids.IsEmpty()) {
		HttpParam params[] = {
			{ "mids", mids },
			{ "access_token", m_szAccessToken }
		};
		PushAsyncHttpRequest(REQUEST_GET, "/method/messages.getById.json", true, &CVkProto::OnReceiveMessages, SIZEOF(params), params);
	}
}

int CVkProto::PollServer()
{
	debugLogA("CVkProto::PollServer");

	NETLIBHTTPREQUEST req = { sizeof(req) };
	req.requestType = REQUEST_GET;
	req.szUrl = NEWSTR_ALLOCA(CMStringA().Format("http://%s?act=a_check&key=%s&ts=%s&wait=25&access_token=%s", m_pollingServer, m_pollingKey, m_pollingTs, m_szAccessToken));
	req.flags = NLHRF_NODUMPHEADERS | NLHRF_PERSISTENT;
	req.timeout = 30000;
	req.nlc = m_pollingConn;

	NETLIBHTTPREQUEST *reply = (NETLIBHTTPREQUEST*)CallService(MS_NETLIB_HTTPTRANSACTION, (WPARAM)m_hNetlibUser, (LPARAM)&req);
	if (reply == NULL) {
		m_pollingConn = NULL;
		return 0;
	}

	int retVal = 0;
	if (reply->resultCode == 200) {
		JSONROOT pRoot(reply->pData);
		JSONNODE *pFailed = json_get(pRoot, "failed");
		if (pFailed != NULL && json_as_int(pFailed) == 2) {
			RetrievePollingInfo();
			retVal = -1;
			debugLogA("Polling key expired, restarting polling thread");
		}
		else if ( CheckJsonResult(NULL, reply, pRoot)) {
			m_pollingTs = mir_t2a( ptrT( json_as_string( json_get(pRoot, "ts"))));
			JSONNODE *pUpdates = json_get(pRoot, "updates");
			if (pUpdates != NULL)
				PollUpdates(pUpdates);
			retVal = 1;
		}
	}

	m_pollingConn = reply->nlc;

	CallService(MS_NETLIB_FREEHTTPREQUESTSTRUCT, 0, (LPARAM)reply);
	return retVal;
}

void CVkProto::PollingThread(void*)
{
	debugLogA("CVkProto::PollingThread: entering");

	while (!m_bTerminated)
		if (PollServer() == -1)
			break;

	m_hPollingThread = NULL;
	m_pollingConn = NULL;
	debugLogA("CVkProto::PollingThread: leaving");
}