/*
Copyright © 2012-23 Miranda NG team
Copyright © 2009 Jim Porter
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"
void CTwitterProto::OnLoggedIn()
{
	ptrW wszGroup(getWStringA(TWITTER_KEY_GROUP));
	if (wszGroup)
		Clist_GroupCreate(0, wszGroup);
	setAllContactStatuses(ID_STATUS_ONLINE);
	SetChatStatus(ID_STATUS_ONLINE);
	int old_status = m_iStatus;
	// m_szMyId = root["id_str"].as_mstring(); !!!!!!!!!!!!!!
	m_iStatus = m_iDesiredStatus;
	ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus);
}
void CTwitterProto::OnLoggedFail()
{
	ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE);
	m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE;
	setAllContactStatuses(ID_STATUS_OFFLINE, false);
}
void CTwitterProto::BeginConnection()
{
	if (!m_si && getByte(TWITTER_KEY_CHATFEED))
		OnJoinChat(0, true);
	if (!m_hWorkerThreadId)
		ForkThread(&CTwitterProto::ServerThread);
	debugLogA("***** Negotiating connection with Twitter");
	// saving the current status to a temp var
	CMStringA szOauthToken = getMStringA(TWITTER_KEY_OAUTH_TOK);
	CMStringA szOauthTokenSecret = getMStringA(TWITTER_KEY_OAUTH_TOK_SEC);
	m_szUserName = getMStringA(TWITTER_KEY_NICK);
	if (m_szUserName.IsEmpty())
		m_szUserName = getMStringA(TWITTER_KEY_UN);
	if (szOauthToken.IsEmpty() || szOauthTokenSecret.IsEmpty()) {
		// first, reset all the keys so we can start fresh
		ResetOauthKeys();
		m_szUserName.Empty();
		debugLogA("**NegotiateConnection - Requesting oauthTokens");
		RequestOauthAuth();
		return;
	}
	szOauthToken = getMStringA(TWITTER_KEY_OAUTH_ACCESS_TOK);
	szOauthTokenSecret = getMStringA(TWITTER_KEY_OAUTH_ACCESS_SEC);
	debugLogA("**NegotiateConnection - Successfully retrieved Access Tokens");
	StringPairs accessTokenParameters = ParseQueryString("");
	m_szAccessToken = accessTokenParameters[L"oauth_token"];
	m_szAccessTokenSecret = accessTokenParameters[L"oauth_token_secret"];
	m_szUserName = accessTokenParameters[L"screen_name"];
	debugLogA("**NegotiateConnection - screen name is %s", m_szUserName.c_str());
	// save em
	setUString(TWITTER_KEY_OAUTH_ACCESS_TOK, m_szAccessToken);
	setUString(TWITTER_KEY_OAUTH_ACCESS_SEC, m_szAccessTokenSecret);
	setUString(TWITTER_KEY_NICK, m_szUserName);
	setUString(TWITTER_KEY_UN, m_szUserName);
	m_szAccessToken = szOauthToken;
	m_szAccessTokenSecret = szOauthTokenSecret;
}
void CTwitterProto::MessageLoop(void*)
{
	debugLogA("***** Entering Twitter::MessageLoop");
	since_id_ = getId(TWITTER_KEY_SINCEID);
	dm_since_id_ = getId(TWITTER_KEY_DMSINCEID);
	bool new_account = getByte(TWITTER_KEY_NEW, 1) != 0;
	bool popups = getByte(TWITTER_KEY_POPUP_SIGNON, 1) != 0;
	// if this isn't set, it will automatically not turn a tweet into a msg. probably should make the default that it does turn a tweet into a message
	bool tweetToMsg = getByte(TWITTER_KEY_TWEET_TO_MSG) != 0;
	int poll_rate = getDword(TWITTER_KEY_POLLRATE, 80);
	for (unsigned int i = 0;; i++) {
		if (m_iStatus != ID_STATUS_ONLINE)
			break;
		if (i % 10 == 0)
			UpdateFriends();
		if (m_iStatus != ID_STATUS_ONLINE)
			break;
		UpdateStatuses(new_account, popups, tweetToMsg);
		if (m_iStatus != ID_STATUS_ONLINE)
			break;
		UpdateMessages(new_account);
		if (new_account) { // Not anymore!
			new_account = false;
			setByte(TWITTER_KEY_NEW, 0);
		}
		if (m_iStatus != ID_STATUS_ONLINE)
			break;
		debugLogA("***** CTwitterProto::MessageLoop going to sleep...");
		if (SleepEx(poll_rate * 1000, true) == WAIT_IO_COMPLETION)
			break;
		debugLogA("***** CTwitterProto::MessageLoop waking up...");
		popups = true;
	}
	debugLogA("***** Exiting CTwitterProto::MessageLoop");
}
struct update_avatar
{
	update_avatar(MCONTACT hContact, const CMStringA &url) : hContact(hContact), url(url) {}
	MCONTACT hContact;
	CMStringA url;
};
/* void *p should always be a struct of type update_avatar */
void CTwitterProto::UpdateAvatarWorker(void *p)
{
	if (p == nullptr)
		return;
	
	std::unique_ptr data((update_avatar*)p);
	// db_get_s returns 0 when it suceeds, so if this suceeds it will return 0, or false.
	// therefore if it returns 1, or true, we want to return as there is no such user.
	// as a side effect, dbv now has the username in it i think
	CMStringA username(getMStringA(data->hContact, TWITTER_KEY_UN));
	if (username.IsEmpty())
		return;
	CMStringA ext = data->url.Mid(data->url.ReverseFind('.')); // finds the filetype of the avatar
	CMStringW filename(FORMAT, L"%s\\%S%S", GetAvatarFolder().c_str(), username.c_str(), ext.c_str()); // local filename and path
	PROTO_AVATAR_INFORMATION ai = { 0 };
	ai.hContact = data->hContact;
	ai.format = ProtoGetAvatarFormat(filename.c_str());
	if (ai.format == PA_FORMAT_UNKNOWN) {
		debugLogA("***** Update avatar: Terminated for this contact, extension format unknown for %s", data->url.c_str());
		return; // lets just ignore unknown formats... if not it crashes miranda. should probably speak to borkra about this.
	}
	wcsncpy(ai.filename, filename.c_str(), MAX_PATH); // puts the local file name in the avatar struct, to a max of 260 chars (as of now)
	debugLogA("***** Updating avatar: %s", data->url.c_str());
	mir_cslock lck(avatar_lock_);
	if (Miranda_IsTerminated()) { // if miranda is shutting down...
		debugLogA("***** Terminating avatar update early: %s", data->url.c_str());
		return;
	}
	if (save_url(hAvatarNetlib_, data->url, filename)) {
		setString(data->hContact, TWITTER_KEY_AV_URL, data->url.c_str());
		ProtoBroadcastAck(data->hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai);
	}
	else ProtoBroadcastAck(data->hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, &ai);
	debugLogA("***** Done avatar: %s", data->url.c_str());
}
void CTwitterProto::UpdateAvatar(MCONTACT hContact, const CMStringA &url, bool force)
{
	DBVARIANT dbv = { 0 };
	if (!force && (!getString(hContact, TWITTER_KEY_AV_URL, &dbv) && url == dbv.pszVal)) {
		debugLogA("***** Avatar already up-to-date: %s", url.c_str());
	}
	else {
		// TODO: more defaults (configurable?)
		if (url == "http://static.twitter.com/images/default_profile_normal.png") {
			PROTO_AVATAR_INFORMATION ai = { 0 };
			ai.hContact = hContact;
			setString(hContact, TWITTER_KEY_AV_URL, url.c_str());
			ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai);
		}
		else {
			ForkThread(&CTwitterProto::UpdateAvatarWorker, new update_avatar(hContact, url));
		}
	}
	db_free(&dbv);
}
void CTwitterProto::UpdateFriends()
{
	/*
	auto *req = new AsyncHttpRequest(REQUEST_GET, CMStringA(FORMAT, "/2/users/%s/followers", m_szMyId.c_str()));
	http::response resp = Execute(req);
	if (resp.code != 200) {
		debugLogA("Friend list reading failed");
		return;
	}
	JSONNode root = JSONNode::parse(resp.data.c_str());
	if (!root) {
		debugLogA("unable to parse response");
		return;
	}
	for (auto &one : root["users"]) {
		std::string username = one["screen_name"].as_string();
		if (m_szUserName == username.c_str())
			continue;
		std::string id = one["id_str"].as_string();
		std::string real_name = one["name"].as_string();
		std::string profile_image_url = one["profile_image_url"].as_string();
		std::string status_text = one["status"]["text"].as_string();
		MCONTACT hContact = AddToClientList(username.c_str(), status_text.c_str());
		setString(hContact, TWITTER_KEY_ID, id.c_str());
		setUString(hContact, "Nick", real_name.c_str());
		UpdateAvatar(hContact, profile_image_url.c_str());
	}
	*/
	debugLogA("***** Friends list updated");
}
LRESULT CALLBACK PopupWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	CMStringA *url;
	switch (message) {
	case WM_COMMAND:
		// Get the plugin data (we need the Popup service to do it)
		url = (CMStringA *)PUGetPluginData(hwnd);
		if (url != nullptr)
			Utils_OpenUrl(url->c_str());
		// Intentionally no break here
	case WM_CONTEXTMENU:
		// After a click, destroy popup
		PUDeletePopup(hwnd);
		return TRUE;
	case UM_FREEPLUGINDATA:
		// After close, free
		url = (CMStringA *)PUGetPluginData(hwnd);
		delete url;
		return FALSE;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
};
void CTwitterProto::ShowContactPopup(MCONTACT hContact, const CMStringA &text, const CMStringA *url)
{
	if (!getByte(TWITTER_KEY_POPUP_SHOW))
		return;
	POPUPDATAW popup = {};
	popup.lchContact = hContact;
	popup.iSeconds = getDword(TWITTER_KEY_POPUP_TIMEOUT);
	popup.colorBack = getDword(TWITTER_KEY_POPUP_COLBACK);
	if (popup.colorBack == 0xFFFFFFFF)
		popup.colorBack = GetSysColor(COLOR_WINDOW);
	popup.colorText = getDword(TWITTER_KEY_POPUP_COLTEXT);
	if (popup.colorBack == 0xFFFFFFFF)
		popup.colorBack = GetSysColor(COLOR_WINDOWTEXT);
	DBVARIANT dbv;
	if (!db_get_ws(hContact, "CList", "MyHandle", &dbv) || !getWString(hContact, TWITTER_KEY_UN, &dbv)) {
		wcsncpy(popup.lpwzContactName, dbv.pwszVal, MAX_CONTACTNAME);
		db_free(&dbv);
	}
	if (url != nullptr) {
		popup.PluginWindowProc = PopupWindowProc;
		popup.PluginData = (void *)url;
	}
	wcsncpy_s(popup.lpwzText, Utf2T(text), MAX_SECONDLINE);
	PUAddPopupW(&popup);
}
void CTwitterProto::UpdateStatuses(bool pre_read, bool popups, bool tweetToMsg)
{
	/*
	auto *req = new AsyncHttpRequest(REQUEST_GET, "/statuses/home_timeline.json");
	req << INT_PARAM("count", 200);
	if (since_id_ != 0)
		req << INT64_PARAM("since_id", since_id_);
	http::response resp = Execute(req);
	if (resp.code != 200) {
		debugLogA("Status update failed with error %d", resp.code);
		return;
	}
	JSONNode root = JSONNode::parse(resp.data.c_str());
	if (!root) {
		debugLogA("Status update failed: unable to parse response");
		return;
	}
	OBJLIST messages(10);
	for (auto &one : root) {
		const JSONNode &pUser = one["user"];
		auto *u = new twitter_user();
		u->username = pUser["screen_name"].as_string();
		u->real_name = pUser["name"].as_string();
		u->profile_image_url = pUser["profile_image_url"].as_string();
		CMStringA retweeteesName, retweetText;
		// the tweet will be truncated unless we take action.  i hate you CTwitterProto API
		const JSONNode &pStatus = one["retweeted_status"];
		if (pStatus) {
			// here we grab the "retweeted_status" um.. section?  it's in here that all the info we need is
			// at this point the user will get no tweets and an error popup if the tweet happens to be exactly 140 chars, start with
			// "RT @", end in " ...", and notactually be a real retweet.  it's possible but unlikely, wish i knew how to get
			// the retweet_count variable to work :(
			const JSONNode &pRetweet = one["retweeted_status"],
				&pUser2 = pRetweet["user"];
			retweeteesName = pUser2["screen_name"].as_string().c_str(); // the user that is being retweeted
			retweetText = pRetweet["text"].as_string().c_str(); // their tweet in all it's untruncated glory
			// fix html entities in the text
			htmlEntitiesDecode(retweetText);
			u->status.text = "RT @" + retweeteesName + " " + retweetText; // mash it together in some format people will understand
		}
		else {
			// if it's not truncated, then the CTwitterProto API returns the native RT correctly anyway,
			CMStringA rawText = one["text"].as_string().c_str();
			// fix html entities in the text
			htmlEntitiesDecode(rawText);
			u->status.text = rawText;
		}
		u->status.id = _atoi64(one["id"].as_string().c_str());
		if (u->status.id > since_id_)
			since_id_ = u->status.id;
		std::string timestr = one["created_at"].as_string();
		u->status.time = parse_time(timestr.c_str());
		messages.insert(u);
	}
	for (auto &u : messages.rev_iter()) {
		if (!pre_read && m_si)
			UpdateChat(*u);
		if (u->username == m_szUserName.c_str())
			continue;
		MCONTACT hContact = AddToClientList(u->username.c_str(), "");
		UpdateAvatar(hContact, u->profile_image_url.c_str());
		// if we send twits as messages, add an unread event
		if (tweetToMsg) {
			DBEVENTINFO dbei = {};
			dbei.pBlob = (uint8_t*)(u->status.text.c_str());
			dbei.cbBlob = (int)u->status.text.length() + 1;
			dbei.eventType = TWITTER_DB_EVENT_TYPE_TWEET;
			dbei.flags = DBEF_UTF;
			dbei.timestamp = static_cast(u->status.time);
			dbei.szModule = m_szModuleName;
			db_event_add(hContact, &dbei);
		}
		else db_set_utf(hContact, "CList", "StatusMsg", u->status.text.c_str());
		if (!pre_read && popups) {
			Skin_PlaySound("TwitterNew");
			ShowContactPopup(hContact, u->status.text.c_str(), new CMStringA(FORMAT, "https://twitter.com/%s/status/%lld", u->username.c_str(), u->status.id));
		}
	}
	*/
	setId(TWITTER_KEY_SINCEID, since_id_);
	debugLogA("***** Status messages updated");
}
void CTwitterProto::UpdateMessages(bool pre_read)
{
	/*
	auto *req = new AsyncHttpRequest(REQUEST_GET, "/direct_messages/events/list.json");
	req << INT_PARAM("count", 50);
	if (dm_since_id_ != 0)
		req << INT64_PARAM("since_id", dm_since_id_);
	http::response resp = Execute(req);
	if (resp.code != 200) {
		debugLogA("Message request failed, error %d", resp.code);
		return;
	}
	JSONNode root = JSONNode::parse(resp.data.c_str());
	if (!root) {
		debugLogA("unable to parse response");
		return;
	}
	for (auto &one : root["events"]) {
		std::string type = one["type"].as_string();
		if (type != "message_create")
			continue;
		bool bIsMe = false;
		auto &msgCreate = one["message_create"];
		std::string sender = msgCreate["sender_id"].as_string();
		if (m_szMyId == sender.c_str()) {
			bIsMe = true;
			sender = msgCreate["target"]["recipient_id"].as_string();
		}
		MCONTACT hContact = FindContactById(sender.c_str());
		if (hContact == INVALID_CONTACT_ID) {
			hContact = AddToClientList(sender.c_str(), "");
			Contact::RemoveFromList(hContact);
		}
		std::string text = msgCreate["message_data"]["text"].as_string();
		__time64_t time = _atoi64(one["created_timestamp"].as_string().c_str()) / 1000;
		std::string msgid = one["id"].as_string();
		if (db_event_getById(m_szModuleName, msgid.c_str()))
			continue;
		twitter_id id = _atoi64(msgid.c_str());
		if (id > dm_since_id_)
			dm_since_id_ = id;
		PROTORECVEVENT recv = { 0 };
		if (pre_read)
			recv.flags |= PREF_CREATEREAD;
		if (bIsMe)
			recv.flags |= PREF_SENT;
		recv.szMessage = const_cast(text.c_str());
		recv.timestamp = static_cast(time);
		recv.szMsgId = msgid.c_str();
		
		MEVENT hDbEVent = (MEVENT)ProtoChainRecvMsg(hContact, &recv);
		if (!msgid.empty())
			m_arChatMarks.insert(new CChatMark(hDbEVent, msgid.c_str()));
	}
	*/
	setId(TWITTER_KEY_DMSINCEID, dm_since_id_);
	debugLogA("***** Direct messages updated");
}
void CTwitterProto::ResetOauthKeys()
{
	delSetting(TWITTER_KEY_OAUTH_ACCESS_TOK);
	delSetting(TWITTER_KEY_OAUTH_ACCESS_SEC);
	delSetting(TWITTER_KEY_OAUTH_TOK);
	delSetting(TWITTER_KEY_OAUTH_TOK_SEC);
	delSetting(TWITTER_KEY_OAUTH_PIN);
}