/*
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 "common.h"
#include "proto.h"
void CALLBACK TwitterProto::APC_callback(ULONG_PTR p)
{
	reinterpret_cast(p)->LOG("***** Executing APC");
}
template
inline static T db_pod_get(HANDLE hContact, const char *module, const char *setting, 
	T errorValue)
{
	DBVARIANT dbv;
	DBCONTACTGETSETTING cgs;
	cgs.szModule  = module;
	cgs.szSetting = setting;
	cgs.pValue    = &dbv;
	if (CallService(MS_DB_CONTACT_GETSETTING, (WPARAM)hContact, (LPARAM)&cgs))
		return errorValue;
	// TODO: remove this,  it's just a temporary workaround
	if (dbv.type == DBVT_DWORD)
		return dbv.dVal;
	if (dbv.cpbVal != sizeof(T))
		return errorValue;
	return *reinterpret_cast(dbv.pbVal);
}
template
inline static INT_PTR db_pod_set(HANDLE hContact, const char *module, const char *setting, 
	T val)
{
	DBCONTACTWRITESETTING cws;
	cws.szModule     = module;
	cws.szSetting    = setting;
	cws.value.type   = DBVT_BLOB;
	cws.value.cpbVal = sizeof(T);
	cws.value.pbVal  = reinterpret_cast(&val);
	return CallService(MS_DB_CONTACT_WRITESETTING, (WPARAM)hContact, (LPARAM)&cws);
}
void TwitterProto::SignOn(void*)
{
	LOG("***** Beginning SignOn process");
	WaitForSingleObject(&signon_lock_, INFINITE);
	// Kill the old thread if it's still around
	if (hMsgLoop_)
	{
		LOG("***** Requesting MessageLoop to exit");
		QueueUserAPC(APC_callback, hMsgLoop_, (ULONG_PTR)this);
		LOG("***** Waiting for old MessageLoop to exit");
		WaitForSingleObject(hMsgLoop_, INFINITE);
		CloseHandle(hMsgLoop_);
	}
	if (NegotiateConnection()) // Could this be? The legendary Go Time??
	{
		if (!in_chat_ && db_byte_get(0, m_szModuleName, TWITTER_KEY_CHATFEED, 0))
			OnJoinChat(0, true);
		
		SetAllContactStatuses(ID_STATUS_ONLINE);
		hMsgLoop_ = ForkThreadEx(&TwitterProto::MessageLoop, this);
	}
	ReleaseMutex(signon_lock_);
	LOG("***** SignOn complete");
}
bool TwitterProto::NegotiateConnection()
{
	LOG("***** Negotiating connection with Twitter");
	int old_status = m_iStatus;
	std::string user, pass;
	DBVARIANT dbv;
	if ( !DBGetContactSettingString(0, m_szModuleName, TWITTER_KEY_UN, &dbv)) {
		user = dbv.pszVal;
		DBFreeVariant(&dbv);
	}
	else {
		ShowPopup(TranslateT("Please enter a username."));
		return false;
	}
	if ( !DBGetContactSettingString(0, m_szModuleName, TWITTER_KEY_PASS, &dbv)) {
		CallService(MS_DB_CRYPT_DECODESTRING, strlen(dbv.pszVal)+1, 
			reinterpret_cast(dbv.pszVal));
		pass = dbv.pszVal;
		DBFreeVariant(&dbv);
	}
	else {
		ShowPopup(TranslateT("Please enter a password."));
		return false;
	}
	if ( !DBGetContactSettingString(0, m_szModuleName, TWITTER_KEY_BASEURL, &dbv)) {
		ScopedLock s(twitter_lock_);
		twit_.set_base_url(dbv.pszVal);
		DBFreeVariant(&dbv);
	}
	bool success;
	{
		ScopedLock s(twitter_lock_);
		success = twit_.set_credentials(user, pass);
	}
	if (!success) {
		ShowPopup(TranslateT("Your username/password combination was incorrect."));
		ProtoBroadcastAck(m_szModuleName, 0, ACKTYPE_STATUS, ACKRESULT_FAILED, 
			(HANDLE)old_status, m_iStatus);
		// Set to offline
		old_status = m_iStatus;
		m_iDesiredStatus = m_iStatus = ID_STATUS_OFFLINE;
		ProtoBroadcastAck(m_szModuleName, 0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, 
			(HANDLE)old_status, m_iStatus);
		return false;
	}
	
	m_iStatus = m_iDesiredStatus;
	ProtoBroadcastAck(m_szModuleName, 0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus);
	return true;
}
void TwitterProto::MessageLoop(void*)
{
	LOG("***** Entering Twitter::MessageLoop");
	since_id_    = db_pod_get(0, m_szModuleName, TWITTER_KEY_SINCEID, 0);
	dm_since_id_ = db_pod_get(0, m_szModuleName, TWITTER_KEY_DMSINCEID, 0);
	bool new_account = db_byte_get(0, m_szModuleName, TWITTER_KEY_NEW, 1) != 0;
	bool popups      = db_byte_get(0, m_szModuleName, TWITTER_KEY_POPUP_SIGNON, 1) != 0;
	int poll_rate = db_dword_get(0, m_szModuleName, TWITTER_KEY_POLLRATE, 80);
	for(unsigned int i=0;;i++)
	{
		if (m_iStatus != ID_STATUS_ONLINE)
			goto exit;
		if (i%4 == 0)
			UpdateFriends();
		if (m_iStatus != ID_STATUS_ONLINE)
			goto exit;
		UpdateStatuses(new_account, popups);
		if (m_iStatus != ID_STATUS_ONLINE)
			goto exit;
		UpdateMessages(new_account);
		if (new_account) // Not anymore!
		{
			new_account = false;
			DBWriteContactSettingByte(0, m_szModuleName, TWITTER_KEY_NEW, 0);
		}
		if (m_iStatus != ID_STATUS_ONLINE)
			goto exit;
		LOG("***** TwitterProto::MessageLoop going to sleep...");
		if (SleepEx(poll_rate*1000, true) == WAIT_IO_COMPLETION)
			goto exit;
		LOG("***** TwitterProto::MessageLoop waking up...");
		popups = true;
	}
exit:
	{
		ScopedLock s(twitter_lock_);
		twit_.set_credentials("", "", false);
	}
	LOG("***** Exiting TwitterProto::MessageLoop");
}
struct update_avatar
{
	update_avatar(HANDLE hContact, const std::string &url) : hContact(hContact), url(url) {}
	HANDLE hContact;
	std::string url;
};
void TwitterProto::UpdateAvatarWorker(void *p)
{
	if (p == 0)
		return;
	std::auto_ptr data( static_cast(p));
	DBVARIANT dbv;
	if (DBGetContactSettingTString(data->hContact, m_szModuleName, TWITTER_KEY_UN, &dbv))
		return;
	std::string ext = data->url.substr(data->url.rfind('.'));
	std::string filename = GetAvatarFolder() + '\\' + dbv.pszVal + ext;
	DBFreeVariant(&dbv);
	PROTO_AVATAR_INFORMATION ai = {sizeof(ai)};
	ai.hContact = data->hContact;
	ai.format = ext_to_format(ext);
	strncpy(ai.filename, filename.c_str(), MAX_PATH);
	LOG("***** Updating avatar: %s", data->url.c_str());
	WaitForSingleObjectEx(avatar_lock_, INFINITE, true);
	if (CallService(MS_SYSTEM_TERMINATED, 0, 0))
	{
		LOG("***** Terminating avatar update early: %s", data->url.c_str());
		return;
	}
	if (save_url(hAvatarNetlib_, data->url, filename))
	{
		DBWriteContactSettingString(data->hContact, m_szModuleName, TWITTER_KEY_AV_URL, 
			data->url.c_str());
		ProtoBroadcastAck(m_szModuleName, data->hContact, ACKTYPE_AVATAR, 
			ACKRESULT_SUCCESS, &ai, 0);
	}
	else
		ProtoBroadcastAck(m_szModuleName, data->hContact, ACKTYPE_AVATAR, 
			ACKRESULT_FAILED,  &ai, 0);
	ReleaseMutex(avatar_lock_);
	LOG("***** Done avatar: %s", data->url.c_str());
}
void TwitterProto::UpdateAvatar(HANDLE hContact, const std::string &url, bool force)
{
	DBVARIANT dbv;
	if ( !force &&
	  ( !DBGetContactSettingString(hContact, m_szModuleName, TWITTER_KEY_AV_URL, &dbv) &&
	    url == dbv.pszVal))
	{
		LOG("***** 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 = {sizeof(ai), hContact};
			
			db_string_set(hContact, m_szModuleName, TWITTER_KEY_AV_URL, url.c_str());
			ProtoBroadcastAck(m_szModuleName, hContact, ACKTYPE_AVATAR, 
				ACKRESULT_SUCCESS, &ai, 0);
		}
		else
		{
			ForkThread(&TwitterProto::UpdateAvatarWorker,  this, 
				new update_avatar(hContact, url));
		}
	}
	DBFreeVariant(&dbv);
}
void TwitterProto::UpdateFriends()
{
	try
	{
		ScopedLock s(twitter_lock_);
		std::vector friends = twit_.get_friends();
		s.Unlock();
		for(std::vector::iterator i=friends.begin(); i!=friends.end(); ++i) {
			if (i->username == twit_.get_username())
				continue;
			HANDLE hContact = AddToClientList( _A2T(i->username.c_str()), i->status.text.c_str());
			UpdateAvatar(hContact, i->profile_image_url);
		}
		LOG("***** Friends list updated");
	}
	catch(const bad_response &)
	{
		LOG("***** Bad response from server,  signing off");
		SetStatus(ID_STATUS_OFFLINE);
	}
	catch(const std::exception &e)
	{
		ShowPopup( (std::string("While updating friends list,  an error occurred: ")
			+e.what()).c_str());
		LOG("***** Error updating friends list: %s", e.what());
	}
}
void TwitterProto::ShowContactPopup(HANDLE hContact, const std::tstring &text)
{
	if (!ServiceExists(MS_POPUP_ADDPOPUPT) || DBGetContactSettingByte(0, m_szModuleName, TWITTER_KEY_POPUP_SHOW, 0) == 0)
		return;
	POPUPDATAT popup = {};
	popup.lchContact = hContact;
	popup.iSeconds = db_dword_get(0, m_szModuleName, TWITTER_KEY_POPUP_TIMEOUT, 0);
	
	popup.colorBack = db_dword_get(0, m_szModuleName, TWITTER_KEY_POPUP_COLBACK, 0);
	if (popup.colorBack == 0xFFFFFFFF)
		popup.colorBack = GetSysColor(COLOR_WINDOW);
	popup.colorText = db_dword_get(0, m_szModuleName, TWITTER_KEY_POPUP_COLTEXT, 0);
	if (popup.colorBack == 0xFFFFFFFF)
		popup.colorBack = GetSysColor(COLOR_WINDOWTEXT);
	DBVARIANT dbv;
	if ( !DBGetContactSettingString(hContact, "CList", "MyHandle", &dbv) ||
		!DBGetContactSettingString(hContact, m_szModuleName, TWITTER_KEY_UN, &dbv))
	{
		mbcs_to_tcs(CP_UTF8, dbv.pszVal, popup.lptzContactName, MAX_CONTACTNAME);
		DBFreeVariant(&dbv);
	}
	CallService(MS_POPUP_ADDPOPUPT, reinterpret_cast(text.c_str()), 0);
}
void TwitterProto::UpdateStatuses(bool pre_read, bool popups)
{
	try
	{
		ScopedLock s(twitter_lock_);
		twitter::status_list updates = twit_.get_statuses(200, since_id_);
		s.Unlock();
		if (!updates.empty())
			since_id_ = std::max(since_id_,  updates[0].status.id);
		for(twitter::status_list::reverse_iterator i=updates.rbegin(); i!=updates.rend(); ++i)
		{
			if (!pre_read && in_chat_)
				UpdateChat(*i);
			if (i->username == twit_.get_username())
				continue;
			HANDLE hContact = AddToClientList( _A2T(i->username.c_str()), _T(""));
			DBEVENTINFO dbei = {sizeof(dbei)};
			
			dbei.pBlob = (BYTE*)(i->status.text.c_str());
			dbei.cbBlob = i->status.text.size()+1;
			dbei.eventType = TWITTER_DB_EVENT_TYPE_TWEET;
			dbei.flags = DBEF_UTF;
			//dbei.flags = DBEF_READ;
			dbei.timestamp = static_cast(i->status.time);
			dbei.szModule = m_szModuleName;
			CallService(MS_DB_EVENT_ADD,  (WPARAM)hContact,  (LPARAM)&dbei);
			DBWriteContactSettingTString(hContact, "CList", "StatusMsg", i->status.text.c_str());
			if ( !pre_read && popups )
				ShowContactPopup(hContact, i->status.text);
		}
		db_pod_set(0, m_szModuleName, TWITTER_KEY_SINCEID, since_id_);
		LOG("***** Status messages updated");
	}
	catch(const bad_response &)
	{
		LOG("***** Bad response from server,  signing off");
		SetStatus(ID_STATUS_OFFLINE);
	}
	catch(const std::exception &e)
	{
		ShowPopup( (std::string("While updating status messages,  an error occurred: ")
			+e.what()).c_str());
		LOG("***** Error updating status messages: %s", e.what());
	}
}
void TwitterProto::UpdateMessages(bool pre_read)
{
	try
	{
		ScopedLock s(twitter_lock_);
		twitter::status_list messages = twit_.get_direct(dm_since_id_);
		s.Unlock();
		if (messages.size())
			dm_since_id_ = std::max(dm_since_id_,  messages[0].status.id);
		for(twitter::status_list::reverse_iterator i=messages.rbegin(); i!=messages.rend(); ++i) {
			HANDLE hContact = AddToClientList( _A2T(i->username.c_str()), _T(""));
			PROTORECVEVENT recv = {};
			CCSDATA ccs = {};
			recv.flags = PREF_TCHAR;
			if (pre_read)
				recv.flags |= PREF_CREATEREAD;
			recv.szMessage = ( char* )i->status.text.c_str();
			recv.timestamp = static_cast(i->status.time);
			ccs.hContact = hContact;
			ccs.szProtoService = PSR_MESSAGE;
			ccs.wParam = ID_STATUS_ONLINE;
			ccs.lParam = reinterpret_cast(&recv);
			CallService(MS_PROTO_CHAINRECV, 0, reinterpret_cast(&ccs));
		}
		db_pod_set(0, m_szModuleName, TWITTER_KEY_DMSINCEID, dm_since_id_);
		LOG("***** Direct messages updated");
	}
	catch(const bad_response &)
	{
		LOG("***** Bad response from server,  signing off");
		SetStatus(ID_STATUS_OFFLINE);
	}
	catch(const std::exception &e)
	{
		ShowPopup( (std::string("While updating direct messages,  an error occurred: ")
			+e.what()).c_str());
		LOG("***** Error updating direct messages: %s", e.what());
	}
}