/*
Facebook plugin for Miranda Instant Messenger
_____________________________________________
Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-19 Miranda NG team
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.
 */
void FacebookProto::ProcessFriendList(void*)
{
	if (isOffline())
		return;
	facy.handle_entry("load_friends");
	// Get friends list
	http::response resp = facy.sendRequest(facy.userInfoAllRequest());
	if (resp.code != HTTP_CODE_OK) {
		facy.handle_error("load_friends");
		return;
	}
	debugLogA("*** Starting processing friend list");
	bool loadAllContacts = getBool(FACEBOOK_KEY_LOAD_ALL_CONTACTS, DEFAULT_LOAD_ALL_CONTACTS);
	bool pagesAlwaysOnline = getBool(FACEBOOK_KEY_PAGES_ALWAYS_ONLINE, DEFAULT_PAGES_ALWAYS_ONLINE);
	std::map friends;
	if (ParseFriends(&resp.data, &friends, loadAllContacts) != EXIT_SUCCESS) {
		debugLogA("*** Error processing friend list");
		return;
	}
	// Check and update old contacts
	for (auto &hContact : AccContacts()) {
		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);
		ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
		if (id != nullptr) {
			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
				facebook_user *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;
						NotifyEvent(Clist_GetContactDisplayName(hContact), 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(0));
						// 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);
							NotifyEvent(Clist_GetContactDisplayName(hContact), TranslateT("Contact is no longer on server-list."), hContact, EVENT_FRIENDSHIP, &url);
						}
					}
				}
			}
		}
	}
	// Check remaining contacts in map and add them to contact list
	for (auto &it : friends) {
		if (!it.second->deleted)
			AddToContactList(it.second, true); // we know this contact doesn't exists, so we force add it
		delete it.second;
	}
	friends.clear();
	debugLogA("*** Friend list processed");
}
void FacebookProto::ProcessUnreadMessages(void*)
{
	if (isOffline())
		return;
	facy.handle_entry("ProcessUnreadMessages");
	http::response resp = facy.sendRequest(facy.unreadThreadsRequest());
	if (resp.code != HTTP_CODE_OK) {
		facy.handle_error("ProcessUnreadMessages");
		return;
	}
	std::vector threads;
	if (ParseUnreadThreads(&resp.data, &threads) == EXIT_SUCCESS) {
		ForkThread(&FacebookProto::ProcessUnreadMessage, new std::vector(threads));
		debugLogA("*** Unread threads list processed");
	}
	else debugLogA("*** Error processing unread threads list");
	facy.handle_success("ProcessUnreadMessages");
}
void FacebookProto::ProcessUnreadMessage(void *pParam)
{
	if (pParam == nullptr)
		return;
	std::vector *threads = (std::vector*)pParam;
	if (isOffline()) {
		delete threads;
		return;
	}
	facy.handle_entry("ProcessUnreadMessage");
	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()));
		http::response resp = facy.sendRequest(facy.threadInfoRequest(ids, limit));
		FreeList(ids);
		ids.destroy();
		if (resp.code == HTTP_CODE_OK) {
			std::vector messages;
			if (ParseThreadMessages(&resp.data, &messages, false) == EXIT_SUCCESS) {
				ReceiveMessages(messages, true);
				debugLogA("*** Unread messages processed");
			}
			else debugLogA("*** Error processing unread messages");
			facy.handle_success("ProcessUnreadMessage");
		}
		else facy.handle_error("ProcessUnreadMessage");
		// 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 == nullptr)
		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 == nullptr) {
		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));
	http::response resp = facy.sendRequest(facy.threadInfoRequest(isChat, (const char*)item_id, nullptr, count, true));
	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);
	std::vector messages;
	if (ParseThreadMessages(&resp.data, &messages, false) == EXIT_SUCCESS) {
		ReceiveMessages(messages, true);
		debugLogA("*** Thread messages processed");
	}
	else debugLogA("*** Error processing thread messages");
	facy.handle_success("LoadLastMessages");
	// Enable marking messages as read for this contact
	facy.ignore_read.erase(hContact);
	// And force mark read
	OnDbEventRead(hContact, 0);
}
void FacebookProto::LoadHistory(void *pParam)
{
	if (pParam == nullptr)
		return;
	MCONTACT hContact = *(MCONTACT*)pParam;
	delete (MCONTACT*)pParam;
	mir_cslock 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)
		return;
	ptrA item_id(getStringA(hContact, isChat ? FACEBOOK_KEY_TID : FACEBOOK_KEY_ID));
	if (item_id == nullptr) {
		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(facy.threadInfoRequest(isChat, (char*)item_id));
	if (resp.code != HTTP_CODE_OK || resp.data.empty()) {
		facy.handle_error("LoadHistory");
		return;
	}
	int messagesCount = -1;
	int unreadCount = -1;
	if (ParseMessagesCount(&resp.data, &messagesCount, &unreadCount) == EXIT_FAILURE) {
		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.lpwzContactName, m_tszUserName, MAX_CONTACTNAME);
	wcsncpy(pd.lpwzText, TranslateT("Loading history started."), MAX_SECONDLINE);
	HWND popupHwnd = (HWND)PUAddPopupW(&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, batchLimit = messagesCount + messagesPerBatch; batch < batchLimit; batch += messagesPerBatch) {
		if (!isOnline())
			break;
		// Load batch of messages
		resp = facy.sendRequest(facy.threadInfoRequest(isChat, item_id, firstTimestamp.c_str(), messagesPerBatch, true));
		if (resp.code != HTTP_CODE_OK || resp.data.empty()) {
			facy.handle_error("LoadHistory");
			break;
		}
		// Parse the result
		messages.clear();
		if (ParseHistory(&resp.data, messages, &firstTimestamp) == EXIT_SUCCESS) {
			// 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;
				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");
		}
		else {
			debugLogA("*** Error processing load history messages");
			break;
		}
		// Update progress popup
		CMStringW text;
		text.AppendFormat(TranslateT("Loading messages: %d/%d"), loadedMessages, messagesCount);
		if (popupHwnd)
			PUChangeTextW(popupHwnd, text);
		else {
			wcsncpy(pd.lpwzText, text, MAX_SECONDLINE);
			pd.iSeconds = 1;
			popupHwnd = (HWND)PUAddPopupW(&pd);
		}
		// There is no more messages
		if (messages.empty() || loadedMessages > messagesCount) {
			break;
		}
	}
	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 (popupHwnd)
		PUChangeTextW(popupHwnd, TranslateT("Loading history completed."));
	else {
		pd.iSeconds = 5;
		wcsncpy(pd.lpwzText, TranslateT("Loading history completed."), MAX_SECONDLINE);
		popupHwnd = (HWND)PUAddPopupW(&pd);
	}
}
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\"", ">", "