/*
Facebook plugin for Miranda Instant Messenger
_____________________________________________
Copyright � 2009-11 Michal Zelinka, 2011-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 .
*/
#include "stdafx.h"
/**
 * Helper function for loading name from database (or use default one specified as parameter), used for title of few notifications.
 */
std::string getContactName(FacebookProto *proto, MCONTACT hContact, const char *defaultName)
{
	std::string name = defaultName;
	DBVARIANT dbv;
	if (!proto->getStringUtf(hContact, FACEBOOK_KEY_NICK, &dbv)) {
		name = dbv.pszVal;
		db_free(&dbv);
	}
	return name;
}
void FacebookProto::ProcessFriendList(void*)
{
	if (isOffline())
		return;
	facy.handle_entry("load_friends");
	// Get friends list
	HttpRequest *request = new UserInfoAllRequest(&facy);
	http::response resp = facy.sendRequest(request);
	if (resp.code != HTTP_CODE_OK) {
		facy.handle_error("load_friends");
		return;
	}
	debugLogA("*** Starting processing friend list");
	try {
		std::map friends;
		bool loadAllContacts = getBool(FACEBOOK_KEY_LOAD_ALL_CONTACTS, DEFAULT_LOAD_ALL_CONTACTS);
		bool pagesAlwaysOnline = getBool(FACEBOOK_KEY_PAGES_ALWAYS_ONLINE, DEFAULT_PAGES_ALWAYS_ONLINE);
		facebook_json_parser(this).parse_friends(&resp.data, &friends, loadAllContacts);
		// Check and update old contacts
		for (MCONTACT hContact = db_find_first(m_szModuleName); hContact; hContact = db_find_next(hContact, m_szModuleName)) {
			if (isChatRoom(hContact))
				continue;
			// TODO RM: change name of "Deleted" key to "DeletedTS", remove this code in some next version
			int deletedTS = getDword(hContact, "Deleted", 0);
			if (deletedTS != 0) {
				delSetting(hContact, "Deleted");
				setDword(hContact, FACEBOOK_KEY_DELETED, deletedTS);
			}
			// If this contact is page, set it as invisible (if enabled in options)
			if (pagesAlwaysOnline && getByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE) == CONTACT_PAGE) {
				setWord(hContact, "Status", ID_STATUS_INVISIBLE);
			}
			facebook_user *fbu;
			ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
			if (id != NULL) {
				std::map< std::string, facebook_user* >::iterator iter;
				if ((iter = friends.find(std::string(id))) != friends.end()) {
					// Found contact, update it and remove from map
					fbu = iter->second;
					// TODO RM: remove, because contacts cant change it, so its only for "first run"
					// - but what with contacts, that was added after logon?
					// Update gender
					setByte(hContact, "Gender", (int)fbu->gender);
					// TODO: remove this in some future version?
					// Remove old useless "RealName" field
					delSetting(hContact, "RealName");
					// Update real name and nick
					if (!fbu->real_name.empty()) {
						SaveName(hContact, fbu);
					}
					// Update username
					if (!fbu->username.empty())
						setString(hContact, FACEBOOK_KEY_USERNAME, fbu->username.c_str());
					else
						delSetting(hContact, FACEBOOK_KEY_USERNAME);
					// Update contact type
					setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, fbu->type);
					// TODO: remove that popup and use "Contact added you" event?
					// Wasn't contact removed from "server-list" someday? And is it friend now? (as we can get also non-friends from this request now)?
					if (fbu->type == CONTACT_FRIEND && getDword(hContact, FACEBOOK_KEY_DELETED, 0)) {
						delSetting(hContact, FACEBOOK_KEY_DELETED);
						// Notify it, if user wants to be notified
						if (getByte(FACEBOOK_KEY_EVENT_FRIENDSHIP_ENABLE, DEFAULT_EVENT_FRIENDSHIP_ENABLE)) {
							std::string url = FACEBOOK_URL_PROFILE + fbu->user_id;
							std::string contactname = getContactName(this, hContact, !fbu->real_name.empty() ? fbu->real_name.c_str() : fbu->user_id.c_str());
							ptrW szTitle(mir_utf8decodeW(contactname.c_str()));
							NotifyEvent(szTitle, TranslateT("Contact is back on server-list."), hContact, EVENT_FRIENDSHIP, &url);
						}
					}
					// Check avatar change
					CheckAvatarChange(hContact, fbu->image_url);
					// Mark this contact as deleted ("processed") and delete them later (as there may be some duplicit contacts to use)
					fbu->deleted = true;
				}
				else {
					// Contact is not on "server-list", notify it was removed (if it was real friend before)
					// Was this real friend before?
					if (getByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE) == CONTACT_FRIEND) {
						setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE);
						// Wasn't we already been notified about this contact?
						if (!getDword(hContact, FACEBOOK_KEY_DELETED, 0)) {
							setDword(hContact, FACEBOOK_KEY_DELETED, ::time(NULL));
							// Notify it, if user wants to be notified
							if (getByte(FACEBOOK_KEY_EVENT_FRIENDSHIP_ENABLE, DEFAULT_EVENT_FRIENDSHIP_ENABLE)) {
								std::string url = FACEBOOK_URL_PROFILE + std::string(id);
								std::string contactname = getContactName(this, hContact, id);
								ptrW szTitle(mir_utf8decodeW(contactname.c_str()));
								NotifyEvent(szTitle, TranslateT("Contact is no longer on server-list."), hContact, EVENT_FRIENDSHIP, &url);
							}
						}
					}
				}
			}
		}
		// Check remaining contacts in map and add them to contact list
		for (std::map< std::string, facebook_user* >::iterator it = friends.begin(); it != friends.end();) {
			if (!it->second->deleted)
				AddToContactList(it->second, true); // we know this contact doesn't exists, so we force add it
			delete it->second;
			it = friends.erase(it);
		}
		friends.clear();
		debugLogA("*** Friend list processed");
	}
	catch (const std::exception &e) {
		debugLogA("*** Error processing friend list: %s", e.what());
	}
}
void FacebookProto::ProcessUnreadMessages(void*)
{
	if (isOffline())
		return;
	facy.handle_entry("ProcessUnreadMessages");
	HttpRequest *request = new UnreadThreadsRequest(&facy);
	http::response resp = facy.sendRequest(request);
	if (resp.code != HTTP_CODE_OK) {
		facy.handle_error("ProcessUnreadMessages");
		return;
	}
	try {
		std::vector threads;
		facebook_json_parser(this).parse_unread_threads(&resp.data, &threads);
		ForkThread(&FacebookProto::ProcessUnreadMessage, new std::vector(threads));
		debugLogA("*** Unread threads list processed");
	}
	catch (const std::exception &e) {
		debugLogA("*** Error processing unread threads list: %s", e.what());
	}
	facy.handle_success("ProcessUnreadMessages");
}
void FacebookProto::ProcessUnreadMessage(void *pParam)
{
	if (pParam == NULL)
		return;
	std::vector *threads = (std::vector*)pParam;
	if (isOffline()) {
		delete threads;
		return;
	}
	facy.handle_entry("ProcessUnreadMessage");
	int offset = 0;
	int limit = 21;
	// FIXME: Rework this whole request as offset doesn't work anyway, and allow to load all the unread messages for each thread (IMHO could be done in 2 single requests = 1) get number of messages for all threads 2) load the counts of messages for all threads)
	// TODO: First load info about amount of unread messages, then load exactly this amount for each thread
	while (!threads->empty()) {		
		
		LIST ids(1);
		for (std::vector::size_type i = 0; i < threads->size(); i++) {
			ids.insert(mir_strdup(threads->at(i).c_str()));
		}
		HttpRequest *request = new ThreadInfoRequest(&facy, ids, offset, limit);
		http::response resp = facy.sendRequest(request);
		FreeList(ids);
		ids.destroy();
		if (resp.code == HTTP_CODE_OK) {
			try {
				std::vector messages;
				facebook_json_parser(this).parse_thread_messages(&resp.data, &messages, false);
				ReceiveMessages(messages, true);
				debugLogA("*** Unread messages processed");
			}
			catch (const std::exception &e) {
				debugLogA("*** Error processing unread messages: %s", e.what());
			}
			facy.handle_success("ProcessUnreadMessage");
		}
		else {
			facy.handle_error("ProcessUnreadMessage");
		}
		offset += limit;
		limit = 20; // TODO: use better limits?
		threads->clear(); // TODO: if we have limit messages from one user, there may be more unread messages... continue with it... otherwise remove that threadd from threads list -- or do it in json parser? hm			 = allow more than "limit" unread messages to be parsed
	}
	delete threads;
}
void FacebookProto::LoadLastMessages(void *pParam)
{
	if (pParam == NULL)
		return;
	MCONTACT hContact = *(MCONTACT*)pParam;
	delete (MCONTACT*)pParam;
	if (isOffline())
		return;
	facy.handle_entry("LoadLastMessages");
	if (!isOnline())
		return;
	bool isChat = isChatRoom(hContact);
	if (isChat && (!m_enableChat || IsSpecialChatRoom(hContact))) // disabled chats or special chatroom (e.g. nofitications)
		return;
	ptrA item_id(getStringA(hContact, isChat ? FACEBOOK_KEY_TID : FACEBOOK_KEY_ID));
	if (item_id == NULL) {
		debugLogA("!!! LoadLastMessages(): Contact has no TID/ID");
		return;
	}
	int count = min(FACEBOOK_MESSAGES_ON_OPEN_LIMIT, getByte(FACEBOOK_KEY_MESSAGES_ON_OPEN_COUNT, DEFAULT_MESSAGES_ON_OPEN_COUNT));
	HttpRequest *request = new ThreadInfoRequest(&facy, isChat, (const char*) item_id, count);
	http::response resp = facy.sendRequest(request);
	if (resp.code != HTTP_CODE_OK || resp.data.empty()) {
		facy.handle_error("LoadLastMessages");
		return;
	}
	// Temporarily disable marking messages as read for this contact
	facy.ignore_read.insert(hContact);
	try {
		std::vector messages;
		facebook_json_parser(this).parse_thread_messages(&resp.data, &messages, false);
		ReceiveMessages(messages, true);
		debugLogA("*** Thread messages processed");
	}
	catch (const std::exception &e) {
		debugLogA("*** Error processing thread messages: %s", e.what());
	}
	facy.handle_success("LoadLastMessages");
	// Enable marking messages as read for this contact
	facy.ignore_read.erase(hContact);
	// And force mark read
	OnDbEventRead(hContact, NULL);
}
void FacebookProto::LoadHistory(void *pParam)
{
	if (pParam == NULL)
		return;
	MCONTACT hContact = *(MCONTACT*)pParam;
	delete (MCONTACT*)pParam;
	ScopedLock s(facy.loading_history_lock_);
	// Allow loading history only from one contact at a time
	if (!isOnline() || facy.loading_history)
		return;
	facy.handle_entry("LoadHistory");
	bool isChat = isChatRoom(hContact);
	if (isChat) // TODO: Support chats?
		return;
	/*if (isChat && (!m_enableChat || IsSpecialChatRoom(hContact))) // disabled chats or special chatroom (e.g. nofitications)
		return;*/
	ptrA item_id(getStringA(hContact, isChat ? FACEBOOK_KEY_TID : FACEBOOK_KEY_ID));
	if (item_id == NULL) {
		debugLogA("!!! LoadHistory(): Contact has no TID/ID");
		return;
	}
	// first get info about this thread and how many messages is there
	http::response resp = facy.sendRequest(new ThreadInfoRequest(&facy, isChat, item_id));
	if (resp.code != HTTP_CODE_OK || resp.data.empty()) {
		facy.handle_error("LoadHistory");
		return;
	}
	int messagesCount = -1;
	int unreadCount = -1;
	facebook_json_parser* p = new facebook_json_parser(this);
	if (p->parse_messages_count(&resp.data, &messagesCount, &unreadCount) == EXIT_FAILURE) {
		delete p;
		facy.handle_error("LoadHistory");
		return;
	}
	// Temporarily disable marking messages as read for this contact
	facy.ignore_read.insert(hContact);
	// Mark we're loading history, so we can behave differently (e.g., stickers won't be refreshed as it slows the whole process down drastically)
	facy.loading_history = true;
	POPUPDATAW pd = { sizeof(pd) };
	pd.iSeconds = 5;
	pd.lchContact = hContact;
	pd.lchIcon = IcoLib_GetIconByHandle(GetIconHandle("conversation")); // TODO: Use better icon
	wcsncpy(pd.lptzContactName, m_tszUserName, MAX_CONTACTNAME);
	wcsncpy(pd.lptzText, TranslateT("Loading history started."), MAX_SECONDLINE);
	HWND popupHwnd = NULL;
	if (ServiceExists(MS_POPUP_ADDPOPUPW)) {
		popupHwnd = (HWND)CallService(MS_POPUP_ADDPOPUPW, (WPARAM)&pd, (LPARAM)APF_RETURN_HWND);
	}
	std::vector messages;
	std::string firstTimestamp = "";
	std::string firstMessageId = "";
	std::string lastMessageId = "";
	int loadedMessages = 0;
	int messagesPerBatch = messagesCount > 10000 ? 500 : 100;
	for (int batch = 0; batch < messagesCount; batch += messagesPerBatch) {
		if (!isOnline())
			break;
		// Load batch of messages
		resp = facy.sendRequest(new ThreadInfoRequest(&facy, isChat, item_id, batch, firstTimestamp.c_str(), messagesPerBatch));
		if (resp.code != HTTP_CODE_OK || resp.data.empty()) {
			facy.handle_error("LoadHistory");
			break;
		}
		// Parse the result
		try {
			messages.clear();
			p->parse_history(&resp.data, &messages, &firstTimestamp);
			// Receive messages
			std::string previousFirstMessageId = firstMessageId;
			for (std::vector::size_type i = 0; i < messages.size(); i++) {
				facebook_message &msg = messages[i];
				// First message might overlap (as we are using it's timestamp for the next loading), so we need to check for it
				if (i == 0) {
					firstMessageId = msg.message_id;
				}
				if (previousFirstMessageId == msg.message_id) {
					continue;
				}
				lastMessageId = msg.message_id;
				// We don't use ProtoChainRecvMsg here as this is just loading of old messages, which we just add to log
				DBEVENTINFO dbei = {};
				if (msg.type == MESSAGE)
					dbei.eventType = EVENTTYPE_MESSAGE;
				else if (msg.type == VIDEO_CALL || msg.type == PHONE_CALL)
					dbei.eventType = FACEBOOK_EVENTTYPE_CALL;
				else
					dbei.eventType = EVENTTYPE_URL; // FIXME: Use better and specific type for our other event types.
				dbei.flags = DBEF_UTF;
				if (!msg.isIncoming)
					dbei.flags |= DBEF_SENT;
				if (!msg.isUnread)
					dbei.flags |= DBEF_READ;
				dbei.szModule = m_szModuleName;
				dbei.timestamp = msg.time;
				dbei.cbBlob = (DWORD)msg.message_text.length() + 1;
				dbei.pBlob = (PBYTE)msg.message_text.c_str();
				db_event_add(hContact, &dbei);
				loadedMessages++;
			}
			// Save last message id of first batch which is latest message completely, because we're going backwards
			if (batch == 0 && !lastMessageId.empty()) {
				setString(hContact, FACEBOOK_KEY_MESSAGE_ID, lastMessageId.c_str());
			}
			debugLogA("*** Load history messages processed");
		}
		catch (const std::exception &e) {
			debugLogA("*** Error processing load history messages: %s", e.what());
			break;
		}
		// Update progress popup
		CMStringW text;
		text.AppendFormat(TranslateT("Loading messages: %d/%d"), loadedMessages, messagesCount);
		if (ServiceExists(MS_POPUP_CHANGETEXTW) && popupHwnd) {
			PUChangeTextW(popupHwnd, text);
		}
		else if (ServiceExists(MS_POPUP_ADDPOPUPW)) {
			wcsncpy(pd.lptzText, text, MAX_SECONDLINE);
			pd.iSeconds = 1;
			popupHwnd = (HWND)CallService(MS_POPUP_ADDPOPUPW, (WPARAM)&pd, (LPARAM)0);
		}
		// There is no more messages
		if (messages.empty() || loadedMessages > messagesCount) {
			break;
		}
	}
	delete p;
	facy.handle_success("LoadHistory");
	// Enable marking messages as read for this contact
	facy.ignore_read.erase(hContact);
	// Reset loading history flag
	facy.loading_history = false;
	if (ServiceExists(MS_POPUP_CHANGETEXTW) && popupHwnd) {
		PUChangeTextW(popupHwnd, TranslateT("Loading history completed."));
	} else if (ServiceExists(MS_POPUP_ADDPOPUPW)) {
		pd.iSeconds = 5;
		wcsncpy(pd.lptzText, TranslateT("Loading history completed."), MAX_SECONDLINE);
		popupHwnd = (HWND)CallService(MS_POPUP_ADDPOPUPW, (WPARAM)&pd, (LPARAM)0);
	}
	// PUDeletePopup(popupHwnd);
}
void parseFeeds(const std::string &text, std::vector &news, DWORD &last_post_time, bool filterAds = true) {
	std::string::size_type pos = 0;
	UINT limit = 0;
	DWORD new_time = last_post_time;
	while ((pos = text.find("fbUserPost\"", pos)) != std::string::npos && limit <= 25)
	{
		std::string post = text.substr(pos, text.find("", pos) - pos);
		pos += 5;
		std::string post_header = utils::text::source_get_value(&post, 3, "", "
");
		std::string post_message = utils::text::source_get_value(&post, 3, " userContent\"", ">", "