/*
 * astyle --force-indent=tab=4 --brackets=linux --indent-switches
 *		  --pad=oper --one-line=keep-blocks  --unpad=paren
 *
 * Miranda NG: the free IM client for Microsoft* Windows*
 *
 * Copyright (c) 2000-09 Miranda ICQ/IM project,
 * all portions of this codebase are copyrighted to the people
 * listed in contributors.txt.
 *
 * This programm 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * part of tabSRMM messaging plugin for Miranda.
 *
 * (C) 2005-2010 by silvercircle _at_ gmail _dot_ com and contributors
 *
 * contact cache implementation
 *
 * the contact cache provides various services to the message window(s)
 * it also abstracts meta contacts.
 *
 */

#include "commonheaders.h"

static OBJLIST<CContactCache> arContacts(50, NumericKeySortT);

CContactCache::CContactCache(const MCONTACT hContact)
{
	m_hContact = hContact;
	m_wOldStatus = m_wStatus = ID_STATUS_OFFLINE;

	if (hContact) {
		m_szProto = ::GetContactProto(m_hContact);
		initPhaseTwo();
	}
	else {
		m_szProto = C_INVALID_PROTO;
		m_szAccount = C_INVALID_ACCOUNT;
		m_isMeta = false;
		m_Valid = false;
	}
}

/**
 * 2nd part of the object initialization that must be callable during the
 * object's lifetime (not only on construction).
 */
void CContactCache::initPhaseTwo()
{
	m_szAccount = 0;
	if (m_szProto) {
		PROTOACCOUNT *acc = reinterpret_cast<PROTOACCOUNT *>(::CallService(MS_PROTO_GETACCOUNT, 0, (LPARAM)m_szProto));
		if (acc && acc->tszAccountName)
			m_szAccount = acc->tszAccountName;
	}

	m_Valid = (m_szProto != 0 && m_szAccount != 0) ? true : false;
	if (m_Valid) {
		m_isMeta = PluginConfig.bMetaEnabled && !strcmp(m_szProto, META_PROTO);
		m_isSubcontact = db_mc_isSub(m_hContact) != 0;
		if (m_isMeta)
			updateMeta(true);
		updateState();
		updateFavorite();
	}
	else {
		m_szProto = C_INVALID_PROTO;
		m_szAccount = C_INVALID_ACCOUNT;
		m_isMeta = false;
	}
}

/**
 * reset meta contact information. Used when meta contacts are disabled
 * on user's request.
 */
void CContactCache::resetMeta()
{
	m_isMeta = false;
	m_szMetaProto = 0;
	m_hSubContact = 0;
	initPhaseTwo();
}

/**
 * if the contact has an open message window, close it.
 * window procedure will use setWindowData() to reset m_hwnd to 0.
 */
void CContactCache::closeWindow()
{
	if (m_hwnd)
		::SendMessage(m_hwnd, WM_CLOSE, 1, 2);
}

void CContactCache::updateState()
{
	updateNick();
	updateStatus();
}

/**
 * update private copy of the nick name. Use contact list name cache
 *
 * @return bool: true if nick has changed.
 */
bool CContactCache::updateNick()
{
	bool	fChanged = false;

	if (m_Valid) {
		TCHAR	*tszNick = pcli->pfnGetContactDisplayName(m_hContact, 0);
		if (tszNick)
			fChanged = (_tcscmp(m_szNick, tszNick) ? true : false);
		mir_sntprintf(m_szNick, 80, _T("%s"), tszNick ? tszNick : _T("<undef>"));
	}
	return fChanged;
}

/**
 * update status mode
 * @return	bool: true if status mode has changed, false if not.
 */
bool CContactCache::updateStatus()
{
	if (!m_Valid)
		return false;

	m_wOldStatus = m_wStatus;
	m_wStatus = (WORD)db_get_w(m_hContact, m_szProto, "Status", ID_STATUS_OFFLINE);
	return m_wOldStatus != m_wStatus;
}

/**
 * update meta (subcontact and -protocol) status. This runs when the
 * MC protocol fires one of its events OR when a relevant database value changes
 * in the master contact.
 */
void CContactCache::updateMeta(bool fForce)
{
	if (!m_Valid)
		return;

	MCONTACT hSubContact = CallService(MS_MC_GETMOSTONLINECONTACT, (WPARAM)m_hContact, 0);
	if (hSubContact && (hSubContact != m_hSubContact || fForce)) {
		m_hSubContact = hSubContact;
		m_szMetaProto = GetContactProto(m_hSubContact);
	}
}

/**
 * obtain the UIN. This is only maintained for open message windows
 * it also run when the subcontact for a MC changes.
 */
bool CContactCache::updateUIN()
{
	m_szUIN[0] = 0;

	if (m_Valid) {
		CONTACTINFO ci = { sizeof(ci) };
		ci.hContact = getActiveContact();
		ci.szProto = const_cast<char *>(getActiveProto());
		ci.dwFlag = CNF_DISPLAYUID | CNF_TCHAR;
		if (!CallService(MS_CONTACT_GETCONTACTINFO, 0, (LPARAM)&ci)) {
			switch (ci.type) {
			case CNFT_ASCIIZ:
				mir_sntprintf(m_szUIN, SIZEOF(m_szUIN), _T("%s"), reinterpret_cast<TCHAR *>(ci.pszVal));
				mir_free((void*)ci.pszVal);
				break;
			case CNFT_DWORD:
				mir_sntprintf(m_szUIN, SIZEOF(m_szUIN), _T("%u"), ci.dVal);
				break;
			}
		}
	}

	return false;
}

void CContactCache::updateStats(int iType, size_t value)
{
	if (m_stats == 0)
		allocStats();

	switch(iType) {
	case TSessionStats::UPDATE_WITH_LAST_RCV:
		if (!m_stats->lastReceivedChars)
			break;
		m_stats->iReceived++;
		m_stats->messageCount++;
		m_stats->iReceivedBytes += m_stats->lastReceivedChars;
		m_stats->lastReceivedChars = 0;
		break;
	case TSessionStats::INIT_TIMER:
		m_stats->started = time(0);
		break;
	case TSessionStats::SET_LAST_RCV:
		m_stats->lastReceivedChars = (unsigned int)value;
		break;
	case TSessionStats::BYTES_SENT:
		m_stats->iSent++;
		m_stats->messageCount++;
		m_stats->iSentBytes += (unsigned int)value;
		break;
	}
}

void CContactCache::allocStats()
{
	if (m_stats == 0) {
		m_stats = new TSessionStats;
		::ZeroMemory(m_stats, sizeof(TSessionStats));
	}
}

/**
 * set the window data for this contact. The window procedure of the message
 * dialog will use this in WM_INITDIALOG and WM_DESTROY to tell the cache
 * that a message window is open for this contact.
 *
 * @param hwnd:		window handle
 * @param dat:		_MessageWindowData - window data structure
 */
void CContactCache::setWindowData(const HWND hwnd, TWindowData *dat)
{
	m_hwnd = hwnd;
	m_dat = dat;
	if (hwnd && dat && m_history == 0)
		allocHistory();
	if (hwnd)
		updateStatusMsg();
	else {
		/* release memory - not needed when window isn't open */
		if (m_szStatusMsg) {
			mir_free(m_szStatusMsg);
			m_szStatusMsg = 0;
		}
		if (m_ListeningInfo) {
			mir_free(m_ListeningInfo);
			m_ListeningInfo = 0;
		}
		if (m_xStatusMsg) {
			mir_free(m_xStatusMsg);
			m_xStatusMsg = 0;
		}
	}
}

/**
 * saves message to the input history.
 * it's using streamout in UTF8 format - no unicode "issues" and all RTF formatting is saved to the history.
 */

void CContactCache::saveHistory(WPARAM wParam, LPARAM lParam)
{
	size_t 	iLength = 0, iStreamLength = 0;
	int 	oldTop = 0;
	char*	szFromStream = NULL;

	if (m_hwnd == 0 || m_dat == 0)
		return;

	if (wParam) {
		oldTop = m_iHistoryTop;
		m_iHistoryTop = (int)wParam;
	}

	szFromStream = ::Message_GetFromStream(GetDlgItem(m_hwnd, IDC_MESSAGE), m_dat, (CP_UTF8 << 16) | (SF_RTFNOOBJS | SFF_PLAINRTF | SF_USECODEPAGE));

	iLength = iStreamLength = (strlen(szFromStream) + 1);

	if (iLength > 0 && m_history != NULL) {
		if ((m_iHistoryTop == m_iHistorySize) && oldTop == 0) {         // shift the stack down...
			TInputHistory ihTemp = m_history[0];
			m_iHistoryTop--;
			::MoveMemory((void*)&m_history[0], (void*)&m_history[1], (m_iHistorySize - 1) * sizeof(TInputHistory));
			m_history[m_iHistoryTop] = ihTemp;
		}
		if (iLength > m_history[m_iHistoryTop].lLen) {
			if (m_history[m_iHistoryTop].szText == NULL) {
				if (iLength < HISTORY_INITIAL_ALLOCSIZE)
					iLength = HISTORY_INITIAL_ALLOCSIZE;
				m_history[m_iHistoryTop].szText = (TCHAR*)mir_alloc(iLength);
				m_history[m_iHistoryTop].lLen = iLength;
			} else {
				if (iLength > m_history[m_iHistoryTop].lLen) {
					m_history[m_iHistoryTop].szText = (TCHAR*)mir_realloc(m_history[m_iHistoryTop].szText, iLength);
					m_history[m_iHistoryTop].lLen = iLength;
				}
			}
		}
		::CopyMemory(m_history[m_iHistoryTop].szText, szFromStream, iStreamLength);
		if (!oldTop) {
			if (m_iHistoryTop < m_iHistorySize) {
				m_iHistoryTop++;
				m_iHistoryCurrent = m_iHistoryTop;
			}
		}
	}
	if (szFromStream)
		mir_free(szFromStream);
	if (oldTop)
		m_iHistoryTop = oldTop;
}

/**
 * handle the input history scrolling for the message input area
 * @param wParam: VK_ keyboard code (VK_UP or VK_DOWN)
 */
void CContactCache::inputHistoryEvent(WPARAM wParam)
{
	if (m_hwnd == 0 || m_dat == 0)
		return;

	if (m_history != NULL && m_history[0].szText != NULL) {     // at least one entry needs to be alloced, otherwise we get a nice infinite loop ;)
		HWND		hwndEdit = ::GetDlgItem(m_hwnd, IDC_MESSAGE);
		SETTEXTEX 	stx = {ST_DEFAULT, CP_UTF8};

		if (m_dat->dwFlags & MWF_NEEDHISTORYSAVE) {
			m_iHistoryCurrent = m_iHistoryTop;
			if (::GetWindowTextLengthA(hwndEdit) > 0)
				saveHistory((WPARAM)m_iHistorySize, 0);
			else
				m_history[m_iHistorySize].szText[0] = (TCHAR)'\0';
		}
		if (wParam == VK_UP) {
			if (m_iHistoryCurrent == 0)
				return;
			m_iHistoryCurrent--;
		}
		else {
			m_iHistoryCurrent++;
			if (m_iHistoryCurrent > m_iHistoryTop)
				m_iHistoryCurrent = m_iHistoryTop;
		}
		if (m_iHistoryCurrent == m_iHistoryTop) {
			if (m_history[m_iHistorySize].szText != NULL) {           // replace the temp buffer
				::SetWindowText(hwndEdit, _T(""));
				::SendMessage(hwndEdit, EM_SETTEXTEX, (WPARAM)&stx, (LPARAM)m_history[m_iHistorySize].szText);
				::SendMessage(hwndEdit, EM_SETSEL, (WPARAM)- 1, (LPARAM)- 1);
			}
		}
		else {
			if (m_history[m_iHistoryCurrent].szText != NULL) {
				::SetWindowText(hwndEdit, _T(""));
				::SendMessage(hwndEdit, EM_SETTEXTEX, (WPARAM)&stx, (LPARAM)m_history[m_iHistoryCurrent].szText);
				::SendMessage(hwndEdit, EM_SETSEL, (WPARAM)- 1, (LPARAM)- 1);
			}
			else ::SetWindowText(hwndEdit, _T(""));
		}
		::SendMessage(m_hwnd, WM_COMMAND, MAKEWPARAM(::GetDlgCtrlID(hwndEdit), EN_CHANGE), (LPARAM)hwndEdit);
		m_dat->dwFlags &= ~MWF_NEEDHISTORYSAVE;
	}
}

/**
 * allocate the input history (on-demand, when it is requested by
 * opening a message window for this contact).
 *
 * note: it allocs historysize + 1 elements, because the + 1 is used
 * for the temporary buffer which saves the current input line when
 * using input history scrolling.
 */
void CContactCache::allocHistory()
{
	m_iHistorySize = M.GetByte("historysize", 15);
	if (m_iHistorySize < 10)
		m_iHistorySize = 10;
	m_history = (TInputHistory *)mir_alloc(sizeof(TInputHistory) * (m_iHistorySize + 1));
	m_iHistoryCurrent = 0;
	m_iHistoryTop = 0;
	if (m_history)
		ZeroMemory(m_history, sizeof(TInputHistory) * m_iHistorySize);
	m_history[m_iHistorySize].szText = (TCHAR*)mir_alloc((HISTORY_INITIAL_ALLOCSIZE + 1) * sizeof(TCHAR));
	m_history[m_iHistorySize].lLen = HISTORY_INITIAL_ALLOCSIZE;
}

/**
 * release additional memory resources
 */
void CContactCache::releaseAlloced()
{
	if (m_stats) {
		delete m_stats;
		m_stats = 0;
	}

	if (m_history) {
		for (int i = 0; i <= m_iHistorySize; i++)
			mir_free(m_history[i].szText);

		mir_free(m_history);
		m_history = 0;
	}

	mir_free(m_szStatusMsg);
	m_szStatusMsg = NULL;
}

/**
 * when a contact is deleted, mark it as invalid in the cache and release
 * all memory it has allocated.
 */
void CContactCache::deletedHandler()
{
	m_Valid = false;
	if (m_hwnd)
		::SendMessage(m_hwnd, WM_CLOSE, 1, 2);

	releaseAlloced();
	m_hContact = (MCONTACT)-1;
}

/**
 * udpate favorite or recent state. runs when user manually adds
 * or removes a user from that list or when database setting is
 * changed from elsewhere
 */
void CContactCache::updateFavorite()
{
	m_isFavorite = db_get_b(m_hContact, SRMSGMOD_T, "isFavorite", 0) != 0;
	m_isRecent = M.GetDword(m_hContact, "isRecent", 0) ? true : false;
}

/**
 * update all or only the given status message information from the database
 *
 * @param szKey: char* database key name or 0 to reload all messages
 */
void CContactCache::updateStatusMsg(const char *szKey)
{
	if (!m_Valid)
		return;

	if (szKey == 0 || (szKey && !strcmp("StatusMsg", szKey))) {
		if (m_szStatusMsg)
			mir_free(m_szStatusMsg);
		m_szStatusMsg = 0;
		ptrT szStatus(db_get_tsa(m_hContact, "CList", "StatusMsg"));
		if (szStatus != 0)
			m_szStatusMsg = (lstrlen(szStatus) > 0 ? getNormalizedStatusMsg(szStatus) : 0);
	}
	if (szKey == 0 || (szKey && !strcmp("ListeningTo", szKey))) {
		if (m_ListeningInfo)
			mir_free(m_ListeningInfo);
		m_ListeningInfo = 0;
		ptrT szListeningTo(db_get_tsa(m_hContact, m_szProto, "ListeningTo"));
		if (szListeningTo != 0 && *szListeningTo)
			m_ListeningInfo = szListeningTo.detouch();
	}
	if (szKey == 0 || (szKey && !strcmp("XStatusMsg", szKey))) {
		if (m_xStatusMsg)
			mir_free(m_xStatusMsg);
		m_xStatusMsg = 0;
		ptrT szXStatusMsg(db_get_tsa(m_hContact, m_szProto, "XStatusMsg"));
		if (szXStatusMsg != 0 && *szXStatusMsg)
			m_xStatusMsg = szXStatusMsg.detouch();
	}
	m_xStatus = db_get_b(m_hContact, m_szProto, "XStatusId", 0);
}

/**
 * retrieve contact cache entry for the given contact. It _never_ returns zero, for a hContact
 * 0, it retrieves a dummy object.
 * Non-existing cache entries are created on demand.
 *
 * @param 	hContact:			contact handle
 * @return	CContactCache*		pointer to the cache entry for this contact
 */

CContactCache* CContactCache::getContactCache(MCONTACT hContact)
{
	CContactCache *cc = arContacts.find((CContactCache*)&hContact);
	if (cc == NULL) {
		cc = new CContactCache(hContact);
		arContacts.insert(cc);
	}
	return cc;
}

/**
 * when the state of the meta contacts protocol changes from enabled to disabled
 * (or vice versa), this updates the contact cache
 *
 * it is ONLY called from the DBSettingChanged() event handler when the relevant
 * database value is touched.
 */
void CContactCache::cacheUpdateMetaChanged()
{
	bool fMetaActive = (PluginConfig.bMetaEnabled) ? true : false;

	for (int i=0; i < arContacts.getCount(); i++) {
		CContactCache &c = arContacts[i];
		if (c.isMeta() && PluginConfig.bMetaEnabled == false) {
			c.closeWindow();
			c.resetMeta();
		}

		// meta contacts are enabled, but current contact is a subcontact - > close window

		if (fMetaActive && c.isSubContact())
			c.closeWindow();

		// reset meta contact information, if metacontacts protocol became avail

		if (fMetaActive && !strcmp(c.getProto(), META_PROTO))
			c.resetMeta();
	}
}

/**
 * normalize the status message with proper cr/lf sequences.
 * @param src TCHAR*:		original status message
 * @param fStripAll bool:	strip all cr/lf sequences and replace them with spaces (use for title bar)
 * @return TCHAR*:			converted status message. CALLER is responsible to mir_free it, MUST use mir_free()
 */
TCHAR* CContactCache::getNormalizedStatusMsg(const TCHAR *src, bool fStripAll)
{
	size_t	k = 0, i = 0;
	TCHAR*  tszResult = 0;

	if (src == 0 || lstrlen(src) < 2)
		return 0;

	tstring dest;

	for (i=0; i < _tcslen(src); i++) {
		if (src[i] == 0x0d || src[i] == '\t')
			continue;
		if (i && src[i] == (TCHAR)0x0a) {
			if (fStripAll) {
				dest.append(_T(" "));
				continue;
			}
			dest.append(_T("\n"));
			continue;
		}
		dest += src[i];
	}

	if (i) {
		tszResult = (TCHAR*)mir_alloc((dest.length() + 1) * sizeof(TCHAR));
		_tcscpy(tszResult, dest.c_str());
		tszResult[dest.length()] = 0;
	}
	return tszResult;
}

/**
 * retrieve the tab/title icon for the corresponding session.
 */
HICON CContactCache::getIcon(int& iSize) const
{
	if (!m_dat || !m_hwnd)
		return LoadSkinnedProtoIcon(m_szProto, m_wStatus);

	if (m_dat->dwFlags & MWF_ERRORSTATE)
		return PluginConfig.g_iconErr;
	if (m_dat->mayFlashTab)
		return m_dat->iFlashIcon;

	if (m_dat->si && m_dat->iFlashIcon) {
		int sizeX, sizeY;
		Utils::getIconSize(m_dat->iFlashIcon, sizeX, sizeY);
		iSize = sizeX;
		return m_dat->iFlashIcon;
	}
	if (m_dat->hTabIcon == m_dat->hTabStatusIcon && m_dat->hXStatusIcon)
		return m_dat->hXStatusIcon;
	return m_dat->hTabIcon;
}

int CContactCache::getMaxMessageLength()
{
	MCONTACT hContact = getActiveContact();
	LPCSTR szProto = getActiveProto();
	if (szProto) {
		m_nMax = CallProtoService(szProto, PS_GETCAPS, PFLAG_MAXLENOFMESSAGE, hContact);
		if (m_nMax) {
			if (M.GetByte("autosplit", 0)) {
				if (m_hwnd)
					::SendDlgItemMessage(m_hwnd, IDC_MESSAGE, EM_EXLIMITTEXT, 0, 20000);
			}
			else {
				if (m_hwnd)
					::SendDlgItemMessage(m_hwnd, IDC_MESSAGE, EM_EXLIMITTEXT, 0, (LPARAM)m_nMax);
			}
		}
		else {
			if (m_hwnd)
				::SendDlgItemMessage(m_hwnd, IDC_MESSAGE, EM_EXLIMITTEXT, 0, 20000);
			m_nMax = 20000;
		}
	}
	return m_nMax;
}