/*
 * astyle --force-indent=tab=4 --brackets=linux --indent-switches
 *		  --pad=oper --one-line=keep-blocks  --unpad=paren
 *
 * Miranda IM: the free IM client for Microsoft* Windows*
 *
 * Copyright 2000-2009 Miranda ICQ/IM project,
 * all portions of this codebase are copyrighted to the people
 * listed in contributors.txt.
 *
 * 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, 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
 *
 * $Id: sendlater.cpp 14225 2012-05-08 19:49:18Z george.hazan $
 *
 * the sendlater class implementation
 */

#include "commonheaders.h"
#pragma hdrstop

#define U_PREF_UNICODE PREF_UNICODE
/*
 * implementation of the CSendLaterJob class
 */

CSendLaterJob::CSendLaterJob()
{
	ZeroMemory(this, sizeof(CSendLaterJob));
	fSuccess = false;
}

/**
 * return true if this job is persistent (saved to the database).
 * such a job will survive a restart of Miranda
 */
bool CSendLaterJob::isPersistentJob()
{
	return(szId[0] == 'S' ? true : false);
}

/**
 * check conditions for deletion
 */
bool CSendLaterJob::mustDelete()
{
	if(fSuccess)
		return(true);
	else if(fFailed) {
		if(bCode == JOB_REMOVABLE)
			return(true);
	}
	return(false);
}

/**
 * clean database entries for a persistent job (currently: manual send later jobs)
 */
void CSendLaterJob::cleanDB()
{
	if(isPersistentJob()) {
		char	szKey[100];

		DBDeleteContactSetting(hContact, "SendLater", szId);
		int iCount = M->GetDword(hContact, "SendLater", "count", 0);
		if(iCount)
			iCount--;
		M->WriteDword(hContact, "SendLater", "count", iCount);
		/*
		 * delete flags
		 */
		mir_snprintf(szKey, 100, "$%s", szId);
		DBDeleteContactSetting(hContact, "SendLater", szKey);
	}
}

/**
 * read flags for a persistent jobs from the db
 * flag key name is the job id with a "$" prefix.
 */
void CSendLaterJob::readFlags()
{
	if(isPersistentJob()) {
		char	szKey[100];
		DWORD	localFlags;

		mir_snprintf(szKey, 100, "$%s", szId);
		localFlags = M->GetDword(hContact, "SendLater", szKey, 0);

		if(localFlags & SLF_SUSPEND)
			bCode = JOB_HOLD;
	}
}

/**
 * write flags for a persistent jobs from the db
 * flag key name is the job id with a "$" prefix.
 */
void CSendLaterJob::writeFlags()
{
	if(isPersistentJob()) {
		DWORD localFlags = (bCode == JOB_HOLD ? SLF_SUSPEND : 0);
		char	szKey[100];

		mir_snprintf(szKey, 100, "$%s", szId);
		M->WriteDword(hContact, "SendLater", szKey, localFlags);
	}
}

/**
 * delete a send later job
 */
CSendLaterJob::~CSendLaterJob()
{
	if(fSuccess || fFailed) {
		POPUPDATAT_V2 ppd = {0};

		if((sendLater->haveErrorPopups() && fFailed) || (sendLater->haveSuccessPopups() && fSuccess)) {
			bool fShowPopup = true;

			if(fFailed && bCode == JOB_REMOVABLE)	// no popups for jobs removed on user's request
				fShowPopup = false;
			/*
			 * show a popup notification, unless they are disabled
			 */
			if (PluginConfig.g_PopupAvail && fShowPopup) {
				TCHAR	*tszName = (TCHAR *)CallService(MS_CLIST_GETCONTACTDISPLAYNAME, (WPARAM)hContact, GCDNF_TCHAR);

				ZeroMemory((void *)&ppd, sizeof(ppd));
				ppd.lchContact = hContact;
				ppd.cbSize = sizeof(ppd);
				mir_sntprintf(ppd.lptzContactName, MAX_CONTACTNAME, _T("%s"), tszName ? tszName : CTranslator::get(CTranslator::GEN_UNKNOWN_CONTACT));
				TCHAR *msgPreview = Utils::GetPreviewWithEllipsis(reinterpret_cast<TCHAR *>(&pBuf[lstrlenA((char *)pBuf) + 1]), 100);
				if(fSuccess) {
					mir_sntprintf(ppd.lptzText, MAX_SECONDLINE, CTranslator::get(CTranslator::GEN_SQ_SENDLATER_SUCCESS_POPUP),
								  msgPreview);
					mir_free(msgPreview);
				}
				else if(fFailed) {
					mir_sntprintf(ppd.lptzText, MAX_SECONDLINE, CTranslator::get(CTranslator::GEN_SQ_SENDLATER_FAILED_POPUP),
						msgPreview);
					mir_free(msgPreview);
				}
				/*
				 * use message settings (timeout/colors) for success popups
				 */
				ppd.colorText = fFailed ? RGB(255, 245, 225) : nen_options.colTextMsg;
				ppd.colorBack = fFailed ? RGB(191, 0, 0) : nen_options.colBackMsg;
				ppd.PluginWindowProc = reinterpret_cast<WNDPROC>(Utils::PopupDlgProcError);
				ppd.lchIcon = fFailed ? PluginConfig.g_iconErr : PluginConfig.g_IconMsgEvent;
				ppd.PluginData = (void *)hContact;
				ppd.iSeconds = fFailed ? -1 : nen_options.iDelayMsg;
				CallService(MS_POPUP_ADDPOPUPW, (WPARAM)&ppd, 0);
			}
		}
		if(fFailed && (bCode == JOB_AGE || bCode == JOB_REMOVABLE) && szId[0] == 'S')
			cleanDB();
		mir_free(sendBuffer);
		mir_free(pBuf);
	}
}

CSendLater::CSendLater()
{
	m_sendLaterContactList.clear();
	m_sendLaterJobList.clear();
	m_fAvail = M->GetByte("sendLaterAvail", 0) ? true : false;
	m_last_sendlater_processed = time(0);
	m_hwndDlg = 0;
	m_fIsInteractive = false;
	m_fErrorPopups = M->GetByte(0, SRMSGMOD_T, "qmgrErrorPopups", 0) ? true : false;
	m_fSuccessPopups = M->GetByte(0, SRMSGMOD_T, "qmgrSuccessPopups", 0) ? true : false;
}

/**
 * clear all open send jobs. Only called on system shutdown to remove
 * the jobs from memory. Must _NOT_ delete any sendlater related stuff from
 * the database (only successful sends may do this).
 */
CSendLater::~CSendLater()
{
	if(m_hwndDlg)
		::DestroyWindow(m_hwndDlg);

	if(m_sendLaterJobList.empty())
		return;

	SendLaterJobIterator it = m_sendLaterJobList.begin();

	while(it != m_sendLaterJobList.end()) {
		mir_free((*it)->sendBuffer);
		mir_free((*it)->pBuf);
		(*it)->fSuccess = false;					// avoid clearing jobs from the database
		delete *it;
		it++;
	}
}

void CSendLater::startJobListProcess()
{ 
	m_jobIterator = m_sendLaterJobList.begin(); 
			
	if (m_hwndDlg)
		Utils::enableDlgControl(m_hwndDlg, IDC_QMGR_LIST, false);
}

/**
 * checks if the current job in the timer-based process queue is subject
 * for deletion (that is, it has failed or succeeded)
 * 
 * if not, it will send the job and increment the list iterator.
 *
 * this method is called once per tick from the timer based scheduler in
 * hotkeyhandler.cpp.
 *
 * returns true if more jobs are awaiting processing, false otherwise.
 */
bool CSendLater::processCurrentJob()
{
	if(m_sendLaterJobList.empty() || m_jobIterator == m_sendLaterJobList.end())
		return(false);

	if((*m_jobIterator)->fSuccess || (*m_jobIterator)->fFailed) {
		if((*m_jobIterator)->mustDelete()) {
			delete *m_jobIterator;
			m_jobIterator = m_sendLaterJobList.erase(m_jobIterator);
		}
		else
			m_jobIterator++;
		return(m_jobIterator == m_sendLaterJobList.end() ? false : true);
	}
	sendIt(*m_jobIterator);
	m_jobIterator++;
	return(m_jobIterator == m_sendLaterJobList.end() ? false : true);
}

/**
 * stub used as enum proc for the database enumeration, collecting
 * all entries in the SendLater module
 * (static function)
 */
int _cdecl CSendLater::addStub(const char *szSetting, LPARAM lParam)
{
	return(sendLater->addJob(szSetting, lParam));
}

/**
 * Process a single contact from the list of contacts with open send later jobs
 * enum the "SendLater" module and add all jobs to the list of open jobs.
 * addJob() will deal with possible duplicates
 * @param hContact HANDLE: contact's handle
 */
void CSendLater::processSingleContact(const HANDLE hContact)
{
	int iCount = M->GetDword(hContact, "SendLater", "count", 0);

	if(iCount) {
		DBCONTACTENUMSETTINGS ces = {0};

		ces.pfnEnumProc = CSendLater::addStub;
		ces.szModule = "SendLater";
		ces.lParam = (LPARAM)hContact;

		CallService(MS_DB_CONTACT_ENUMSETTINGS, (WPARAM)hContact, (LPARAM)&ces);
	}
}

/**
 * called periodically from a timer, check if new contacts were added 
 * and process them
 */
void CSendLater::processContacts()
{
	if(m_fAvail && !m_sendLaterContactList.empty()) {
		std::vector<HANDLE>::iterator it = m_sendLaterContactList.begin();
		while(it != m_sendLaterContactList.end()) {
			processSingleContact(*it);
			it++;
		}
		m_sendLaterContactList.clear();
	}
}

/**
 * This function adds a new job to the list of messages to send unattended
 * used by the send later feature and multisend
 *
 * @param 	szSetting is either the name of the database key for a send later
 * 		  	job OR the utf-8 encoded message for a multisend job prefixed with
 * 			a 'M+timestamp'. Send later job ids start with "S".
 *
 * @param 	lParam: a contact handle for which the job should be scheduled
 * @return 	0 on failure, 1 otherwise
 */
int CSendLater::addJob(const char *szSetting, LPARAM lParam)
{
	HANDLE 		hContact = (HANDLE)lParam;
	DBVARIANT 	dbv = {0};
	char		*szOrig_Utf = 0;

	if(!m_fAvail || !szSetting || !strcmp(szSetting, "count") || lstrlenA(szSetting) < 8)
		return(0);

	if(szSetting[0] != 'S' && szSetting[0] != 'M')
		return(0);

	SendLaterJobIterator it = m_sendLaterJobList.begin();

	/*
	 * check for possible dupes
	 */
	while(it != m_sendLaterJobList.end()) {
		if((*it)->hContact == hContact && !strcmp((*it)->szId, szSetting)) {
			return(0);
		}
		it++;
	}

	if(szSetting[0] == 'S') {
		if(0 == DBGetContactSettingString(hContact, "SendLater", szSetting, &dbv))
			szOrig_Utf = dbv.pszVal;
		else
			return(0);
	}
	else if(szSetting[0] == 'M') {
		char *szSep = strchr(const_cast<char *>(szSetting), '|');
		if(!szSep)
			return(0);
		*szSep = 0;
		szOrig_Utf = szSep + 1;
	}
	else
		return(0);

	CSendLaterJob *job = new CSendLaterJob;

	strncpy(job->szId, szSetting, 20);
	job->szId[19] = 0;
	job->hContact = hContact;
	job->created = atol(&szSetting[1]);

	char	*szAnsi = 0;
	wchar_t *szWchar = 0;
	UINT	required = 0;

	int iLen = lstrlenA(szOrig_Utf);
	job->sendBuffer = reinterpret_cast<char *>(mir_alloc(iLen + 1));
	strncpy(job->sendBuffer, szOrig_Utf, iLen);
	job->sendBuffer[iLen] = 0;

	/*
	 * construct conventional send buffer
	 */

	szAnsi = M->utf8_decodecp(szOrig_Utf, CP_ACP, &szWchar);
	iLen = lstrlenA(szAnsi);
	if(szWchar)
		required = iLen + 1 + ((lstrlenW(szWchar) + 1) * sizeof(wchar_t));
	else
		required = iLen + 1;

	job->pBuf = (PBYTE)mir_calloc(required);

	strncpy((char *)job->pBuf, szAnsi, iLen);
	job->pBuf[iLen] = 0;
	if(szWchar)
		wcsncpy((wchar_t *)&job->pBuf[iLen + 1], szWchar, lstrlenW(szWchar));

	if(szSetting[0] == 'S')
		DBFreeVariant(&dbv);

	mir_free(szWchar);
	job->readFlags();
	m_sendLaterJobList.push_back(job);
	qMgrUpdate();
	return(1);
}

/**
 * Try to send an open job from the job list
 * this is ONLY called from the WM_TIMER handler and should never be executed
 * directly.
 */
int CSendLater::sendIt(CSendLaterJob *job)
{
	HANDLE 		hContact = job->hContact;
	time_t 		now = time(0);
	DWORD   	dwFlags = 0;
	DBVARIANT 	dbv = {0};
	const char* szProto = 0;


	if(job->bCode == CSendLaterJob::JOB_HOLD || job->bCode == CSendLaterJob::JOB_DEFERRED || job->fSuccess || job->fFailed || job->lastSent > now)
		return(0);											// this one is frozen or done (will be removed soon), don't process it now.

	if(now - job->created > SENDLATER_AGE_THRESHOLD) {		// too old, this will be discarded and user informed by popup
		job->fFailed = true;
		job->bCode = CSendLaterJob::JOB_AGE;
		return(0);
	}

	/*
	 * mark job as deferred (5 unsuccessful sends). Job will not be removed, but 
	 * the user must manually reset it in order to trigger a new send attempt.
	 */
	if(job->iSendCount == 5) {
		job->bCode = CSendLaterJob::JOB_DEFERRED;
		return(0);
	}

	if(job->iSendCount > 0 && (now - job->lastSent < SENDLATER_RESEND_THRESHOLD)) {
		//_DebugTraceA("Send it: message %s for %s RESEND but not old enough", job->szId, (char *)CallService(MS_CLIST_GETCONTACTDISPLAYNAME, (WPARAM)hContact, 0));
		return(0);											// this one was sent, but probably failed. Resend it after a while
	}

	CContactCache *c = CContactCache::getContactCache(hContact);
	if(!c)
		return(0);						// should not happen

	if(!c->isValid()) {
		job->fFailed = true;
		job->bCode = CSendLaterJob::INVALID_CONTACT;
		return(0);						// can happen (contact has been deleted). mark the job as failed
	}

	hContact = c->getActiveContact();
	szProto = c->getActiveProto();

	if(!hContact || szProto == 0)
		return(0);

	WORD wMyStatus = (WORD)CallProtoService(szProto, PS_GETSTATUS, 0, 0);
	WORD wContactStatus = c->getActiveStatus();

	/*
	 * status mode checks
	 */
	if(wMyStatus == ID_STATUS_OFFLINE) {
		job->bCode = CSendLaterJob::JOB_MYSTATUS;
		return(0);
	}
	if(job->szId[0] == 'S') {
		if(!(wMyStatus == ID_STATUS_ONLINE || wMyStatus == ID_STATUS_FREECHAT)) {
			job->bCode = CSendLaterJob::JOB_MYSTATUS;
			return(0);
		}
	}
	if(wContactStatus == ID_STATUS_OFFLINE) {
		job->bCode = CSendLaterJob::JOB_STATUS;
		return(0);
	}

	dwFlags = IsUtfSendAvailable(hContact) ? PREF_UTF : U_PREF_UNICODE;

	char *svcName = SendQueue::MsgServiceName(hContact, 0, dwFlags);

	job->lastSent = now;
	job->iSendCount++;
	job->hTargetContact = hContact;
	job->bCode = CSendLaterJob::JOB_WAITACK;

	if(dwFlags & PREF_UTF)
		job->hProcess = (HANDLE)CallContactService(hContact, svcName, dwFlags, (LPARAM)job->sendBuffer);
	else
		job->hProcess = (HANDLE)CallContactService(hContact, svcName, dwFlags, (LPARAM)job->pBuf);
	return(0);
}

/*
 * add a contact to the list of contacts having open send later jobs.
 * This ist is periodically checked for new additions (processContacts()) 
 * and new jobs are created.
 */

void CSendLater::addContact(const HANDLE hContact)
{
	if(!m_fAvail)
		return;

	std::vector<HANDLE>::iterator it = m_sendLaterContactList.begin();

	if(m_sendLaterContactList.empty()) {
		m_sendLaterContactList.push_back(hContact);
		m_last_sendlater_processed = 0;								// force processing at next tick
		return;
	}

	/*
	 * this list should not have duplicate entries
	 */

	while(it != m_sendLaterContactList.end()) {
		if(*it == hContact)
			return;
		it++;
	}
	m_sendLaterContactList.push_back(hContact);
	m_last_sendlater_processed = 0;								// force processing at next tick
}

/**
 * process ACK messages for the send later job list. Called from the proto ack
 * handler when it does not find a match in the normal send queue
 *
 * Add the message to the database and mark it as successful. The job will be
 * removed later by the job list processing code.
 */
HANDLE CSendLater::processAck(const ACKDATA *ack)
{
	if(m_sendLaterJobList.empty() || !m_fAvail)
		return(0);

	SendLaterJobIterator it = m_sendLaterJobList.begin();

	while(it != m_sendLaterJobList.end()) {
		if((*it)->hProcess == ack->hProcess && (*it)->hTargetContact == ack->hContact && !((*it)->fSuccess || (*it)->fFailed)) {
			DBEVENTINFO dbei = {0};

			if(!(*it)->fSuccess) {
				dbei.cbSize = sizeof(dbei);
				dbei.eventType = EVENTTYPE_MESSAGE;
				dbei.flags = DBEF_SENT;
				dbei.szModule = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, (WPARAM)((*it)->hContact), 0);
				dbei.timestamp = time(NULL);
				dbei.cbBlob = lstrlenA((*it)->sendBuffer) + 1;
				dbei.flags |= DBEF_UTF;
				dbei.pBlob = (PBYTE)((*it)->sendBuffer);
				HANDLE hNewEvent = (HANDLE) CallService(MS_DB_EVENT_ADD, (WPARAM)((*it)->hContact), (LPARAM)&dbei);
				
				(*it)->cleanDB();
			}
			(*it)->fSuccess = true;					// mark as successful, job list processing code will remove it later
			(*it)->hProcess = (HANDLE)-1;
			(*it)->bCode = '-';
			qMgrUpdate();
			return(0);
		}
		it++;
	}
	return(0);
}

/*
 * UI stuff (dialog procedures for the queue manager dialog
 */

void CSendLater::qMgrUpdate(bool fReEnable)
{
	if(m_hwndDlg) {
		if(fReEnable)
			Utils::enableDlgControl(m_hwndDlg, IDC_QMGR_LIST, true);
		::SendMessage(m_hwndDlg, WM_USER + 100, 0, 0);		// if qmgr is open, tell it to update
	}
}

LRESULT CSendLater::qMgrAddFilter(const HANDLE hContact, const TCHAR* tszNick)
{
	LRESULT lr;

	lr = ::SendMessage(m_hwndFilter, CB_FINDSTRING, 0, reinterpret_cast<LPARAM>(tszNick));
	if(lr == CB_ERR) {
		lr = ::SendMessage(m_hwndFilter, CB_INSERTSTRING, -1, reinterpret_cast<LPARAM>(tszNick));
		::SendMessage(m_hwndFilter, CB_SETITEMDATA, lr, reinterpret_cast<LPARAM>(hContact));
		if(hContact == m_hFilter)
			m_sel = lr;
	}
	return(m_sel);
}

/**
 * fills the list of jobs with current contents of the job queue
 * filters by m_hFilter (contact handle)
 *
 */
void CSendLater::qMgrFillList(bool fClear)
{
	LVITEM			lvItem = {0};
	unsigned		uIndex = 0;
	CContactCache*	c = 0;
	TCHAR*			formatTime = _T("%Y.%m.%d - %H:%M");
	TCHAR			tszTimestamp[30];
	TCHAR			tszStatus[20];
	const TCHAR*	tszStatusText = 0;
	BYTE			bCode = '-';

	if(fClear) {
		::SendMessage(m_hwndList, LVM_DELETEALLITEMS, 0, 0);
		::SendMessage(m_hwndFilter, CB_RESETCONTENT, 0, 0);
	}

	m_sel = 0;
	::SendMessage(m_hwndFilter, CB_INSERTSTRING, -1, 
				  reinterpret_cast<LPARAM>(CTranslator::get(CTranslator::QMGR_FILTER_ALLCONTACTS)));
	::SendMessage(m_hwndFilter, CB_SETITEMDATA, 0, 0);

	lvItem.cchTextMax = 255;

	SendLaterJobIterator it = m_sendLaterJobList.begin();

	while(it != m_sendLaterJobList.end()) {

		c = CContactCache::getContactCache((*it)->hContact);
		if(c) {
			const TCHAR*	tszNick = c->getNick();
			TCHAR			tszBuf[255];

			if(m_hFilter && m_hFilter != (*it)->hContact) {
				qMgrAddFilter(c->getContact(), tszNick);
				it++;
				continue;
			}
			lvItem.mask = LVIF_TEXT|LVIF_PARAM;
			mir_sntprintf(tszBuf, 255, _T("%s [%s]"), tszNick, c->getRealAccount());
			lvItem.pszText = tszBuf;
			lvItem.iItem = uIndex++;
			lvItem.iSubItem = 0;
			lvItem.lParam = reinterpret_cast<LPARAM>(*it);
			::SendMessage(m_hwndList, LVM_INSERTITEM, 0, reinterpret_cast<LPARAM>(&lvItem));
			qMgrAddFilter(c->getContact(), tszNick);

			lvItem.mask = LVIF_TEXT;
			_tcsftime(tszTimestamp, 30, formatTime, _localtime32((__time32_t *)&(*it)->created));
			tszTimestamp[29] = 0;
			lvItem.pszText = tszTimestamp;
			lvItem.iSubItem = 1;
			::SendMessage(m_hwndList, LVM_SETITEM, 0, reinterpret_cast<LPARAM>(&lvItem));

			TCHAR* msg = M->utf8_decodeT((*it)->sendBuffer);
			TCHAR* preview = Utils::GetPreviewWithEllipsis(msg, 255);
			lvItem.pszText = preview;
			lvItem.iSubItem = 2;
			::SendMessage(m_hwndList, LVM_SETITEM, 0, reinterpret_cast<LPARAM>(&lvItem));
			mir_free(preview);
			mir_free(msg);

			if((*it)->fFailed) {
				tszStatusText = (*it)->bCode == CSendLaterJob::JOB_REMOVABLE ? 
					CTranslator::get(CTranslator::QMGR_STATUS_REMOVED) : CTranslator::get(CTranslator::QMGR_STATUS_FAILED);
			}
			else if((*it)->fSuccess)
				tszStatusText = CTranslator::get(CTranslator::QMGR_STATUS_SENTOK);
			else {
				switch((*it)->bCode) {
					case CSendLaterJob::JOB_DEFERRED:
						tszStatusText = CTranslator::get(CTranslator::QMGR_STATUS_DEFERRED);
						break;
					case CSendLaterJob::JOB_AGE:
						tszStatusText = CTranslator::get(CTranslator::QMGR_STATUS_FAILED);
						break;
					case CSendLaterJob::JOB_HOLD:
						tszStatusText = CTranslator::get(CTranslator::QMGR_STATUS_HOLD);
						break;
					default:
						tszStatusText = CTranslator::get(CTranslator::QMGR_STATUS_PENDING);
						break;
				}
			}
			if((*it)->bCode)
				bCode = (*it)->bCode;
			mir_sntprintf(tszStatus, 20, _T("X/%s[%c] (%d)"), tszStatusText, bCode, (*it)->iSendCount);
			tszStatus[0] = static_cast<TCHAR>((*it)->szId[0]);
			lvItem.pszText = tszStatus;
			lvItem.iSubItem = 3;
			::SendMessage(m_hwndList, LVM_SETITEM, 0, reinterpret_cast<LPARAM>(&lvItem));

			if((*it)->lastSent == 0)
				mir_sntprintf(tszTimestamp, 30, _T("%s"), _T("Never"));
			else {
				_tcsftime(tszTimestamp, 30, formatTime, _localtime32((__time32_t *)&(*it)->lastSent));
				tszTimestamp[29] = 0;
			}
			lvItem.pszText = tszTimestamp;
			lvItem.iSubItem = 4;
			::SendMessage(m_hwndList, LVM_SETITEM, 0, reinterpret_cast<LPARAM>(&lvItem));
		}
		it++;
	}
	if(m_hFilter == 0)
		::SendMessage(m_hwndFilter, CB_SETCURSEL, 0, 0);
	else
		::SendMessage(m_hwndFilter, CB_SETCURSEL, m_sel, 0);
}

/*
 * set the column headers
 */
#define QMGR_LIST_NRCOLUMNS 5

static char*  szColFormat = "%d;%d;%d;%d;%d";
static char*  szColDefault = "100;120;80;120;120";

void CSendLater::qMgrSetupColumns()
{
	LVCOLUMN	col = {0};
	int			nWidths[QMGR_LIST_NRCOLUMNS];
	DBVARIANT	dbv = {0};
	RECT		rcList;
	LONG		cxList;

	::GetWindowRect(m_hwndList, &rcList);
	cxList = rcList.right - rcList.left;

	if(0 == M->GetString(0, SRMSGMOD_T, "qmgrListColumns", &dbv)) {
		sscanf(dbv.pszVal, szColFormat, &nWidths[0], &nWidths[1], &nWidths[2], &nWidths[3], &nWidths[4]);
		DBFreeVariant(&dbv);
	}
	else
		sscanf(szColDefault, szColFormat, &nWidths[0], &nWidths[1], &nWidths[2], &nWidths[3], &nWidths[4]);

	col.mask = LVCF_TEXT|LVCF_WIDTH|LVCF_SUBITEM;
	col.cx = max(nWidths[0], 10);
	col.pszText = const_cast<TCHAR *>(CTranslator::get(CTranslator::GEN_CONTACT));

	::SendMessage(m_hwndList, LVM_INSERTCOLUMN, 0, reinterpret_cast<LPARAM>(&col));

	col.pszText = const_cast<TCHAR *>(CTranslator::get(CTranslator::QMGR_COL_ODATE));
	col.cx = max(nWidths[1], 10);
	::SendMessage(m_hwndList, LVM_INSERTCOLUMN, 1, reinterpret_cast<LPARAM>(&col));

	col.pszText = const_cast<TCHAR *>(CTranslator::get(CTranslator::QMGR_COL_MESSAGETEXT));
	col.cx = max((cxList - nWidths[0] - nWidths[1] - nWidths[3] - nWidths[4] - 10), 10);
	::SendMessage(m_hwndList, LVM_INSERTCOLUMN, 2, reinterpret_cast<LPARAM>(&col));

	col.pszText = const_cast<TCHAR *>(CTranslator::get(CTranslator::QMGR_COL_STATUS));
	col.cx = max(nWidths[3], 10);
	::SendMessage(m_hwndList, LVM_INSERTCOLUMN, 3, reinterpret_cast<LPARAM>(&col));

	col.pszText = const_cast<TCHAR *>(CTranslator::get(CTranslator::QMGR_COL_LASTSENDINFO));
	col.cx = max(nWidths[4], 10);
	::SendMessage(m_hwndList, LVM_INSERTCOLUMN, 4, reinterpret_cast<LPARAM>(&col));

}

/**
 * save user defined column widths to the database
 */
void CSendLater::qMgrSaveColumns()
{
	char		szColFormatNew[100];
	int			nWidths[QMGR_LIST_NRCOLUMNS], i;
	LVCOLUMN	col = {0};

	col.mask = LVCF_WIDTH;
	for(i = 0; i < QMGR_LIST_NRCOLUMNS; i++) {
		::SendMessage(m_hwndList, LVM_GETCOLUMN, i, reinterpret_cast<LPARAM>(&col));
		nWidths[i] = max(col.cx, 10);
	}
	mir_snprintf(szColFormatNew, 100, "%d;%d;%d;%d;%d", nWidths[0], nWidths[1], nWidths[2], nWidths[3], nWidths[4]);
	::DBWriteContactSettingString(0, SRMSGMOD_T, "qmgrListColumns", szColFormatNew);
}

INT_PTR CALLBACK CSendLater::DlgProcStub(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	CSendLater *s = reinterpret_cast<CSendLater *>(::GetWindowLongPtr(hwnd, GWLP_USERDATA));

	if(s)
		return(s->DlgProc(hwnd, msg, wParam, lParam));

	switch(msg) {
		case WM_INITDIALOG: {
			::SetWindowLongPtr(hwnd, GWLP_USERDATA, lParam);
			s = reinterpret_cast<CSendLater *>(lParam);
			return(s->DlgProc(hwnd, msg, wParam, lParam));
		}
		default:
			break;
	}
	return(FALSE);
}

INT_PTR CALLBACK CSendLater::DlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch(msg) {
		case WM_INITDIALOG:
			m_hwndDlg = hwnd;
			TranslateDialogDefault(hwnd);
			m_hwndList = ::GetDlgItem(m_hwndDlg, IDC_QMGR_LIST);
			m_hwndFilter = ::GetDlgItem(m_hwndDlg, IDC_QMGR_FILTER);
			m_hFilter = reinterpret_cast<HANDLE>(M->GetDword(0, SRMSGMOD_T, "qmgrFilterContact", 0));

			::SetWindowLongPtr(m_hwndList, GWL_STYLE, ::GetWindowLongPtr(m_hwndList, GWL_STYLE) | LVS_SHOWSELALWAYS);
			::SendMessage(m_hwndList, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES|LVS_EX_LABELTIP|LVS_EX_DOUBLEBUFFER);
			qMgrSetupColumns();
			qMgrFillList();
			if(PluginConfig.g_PopupAvail) {
				::CheckDlgButton(m_hwndDlg, IDC_QMGR_SUCCESSPOPUPS, m_fSuccessPopups ? BST_CHECKED : BST_UNCHECKED);
				::CheckDlgButton(m_hwndDlg, IDC_QMGR_ERRORPOPUPS, m_fErrorPopups ? BST_CHECKED : BST_UNCHECKED);
			}
			else {
				Utils::showDlgControl(m_hwndDlg, IDC_QMGR_ERRORPOPUPS, SW_HIDE);
				Utils::showDlgControl(m_hwndDlg, IDC_QMGR_SUCCESSPOPUPS, SW_HIDE);
			}
			::ShowWindow(hwnd, SW_NORMAL);
			return(FALSE);

		case WM_NOTIFY:	{
			NMHDR*	pNMHDR = reinterpret_cast<NMHDR *>(lParam);

			if(pNMHDR->hwndFrom == m_hwndList) {
				switch(pNMHDR->code) {
					case NM_RCLICK: {
						HMENU hMenu = ::LoadMenu(g_hInst, MAKEINTRESOURCE(IDR_TABCONTEXT));
						HMENU hSubMenu = ::GetSubMenu(hMenu, 13);
						::TranslateMenu(hSubMenu);

						POINT pt;
						::GetCursorPos(&pt);
						/*
						 * copy to clipboard only allowed with a single selection
						 */
						if(::SendMessage(m_hwndList, LVM_GETSELECTEDCOUNT, 0, 0) == 1)
							::EnableMenuItem(hSubMenu, ID_QUEUEMANAGER_COPYMESSAGETOCLIPBOARD, MF_ENABLED);

						m_fIsInteractive = true;
						int selection = ::TrackPopupMenu(hSubMenu, TPM_RETURNCMD, pt.x, pt.y, 0, m_hwndDlg, NULL);
						if(selection == ID_QUEUEMANAGER_CANCELALLMULTISENDJOBS) {
							SendLaterJobIterator it = m_sendLaterJobList.begin();
							while(it != m_sendLaterJobList.end()) {
								if((*it)->szId[0] == 'M') {
									(*it)->fFailed = true;
									(*it)->bCode = CSendLaterJob::JOB_REMOVABLE;
								}
								it++;
							}
						}
						else if(selection != 0) {
							::SendMessage(hwnd, WM_COMMAND, MAKEWPARAM(IDC_QMGR_REMOVE, LOWORD(selection)), 0);
							m_last_sendlater_processed = 0;			// force a queue check
						}
						::DestroyMenu(hMenu);
						m_fIsInteractive = false;
						break;
					}
					default:
						break;
				}
			}
			break;
		}

		case WM_COMMAND:
			if(HIWORD(wParam) == CBN_SELCHANGE && reinterpret_cast<HWND>(lParam) == m_hwndFilter) {
				LRESULT lr = ::SendMessage(m_hwndFilter, CB_GETCURSEL, 0, 0);
				if(lr != CB_ERR) {
					m_hFilter = reinterpret_cast<HANDLE>(::SendMessage(m_hwndFilter, CB_GETITEMDATA, lr, 0));
					qMgrFillList();
				}
				break;
			}
			switch(LOWORD(wParam)) {
				case IDOK:
				case IDCANCEL:
					qMgrSaveColumns();
					::DestroyWindow(hwnd);
					break;

				case IDC_QMGR_SUCCESSPOPUPS:
					m_fSuccessPopups = ::IsDlgButtonChecked(m_hwndDlg, IDC_QMGR_SUCCESSPOPUPS) ? true : false;
					M->WriteByte(0, SRMSGMOD_T, "qmgrSuccessPopups", m_fSuccessPopups ? 1 : 0);
					break;

				case IDC_QMGR_ERRORPOPUPS:
					m_fErrorPopups = ::IsDlgButtonChecked(m_hwndDlg, IDC_QMGR_ERRORPOPUPS) ? true : false;
					M->WriteByte(0, SRMSGMOD_T, "qmgrErrorPopups", m_fErrorPopups ? 1 : 0);
					break;

				case IDC_QMGR_HELP:
					CallService(MS_UTILS_OPENURL, 0, reinterpret_cast<LPARAM>("http://wiki.miranda.or.at/TabSRMM/SendLater"));
					break;
				/*
				 * this handles all commands sent by the context menu
				 * mark jobs for removal/reset/hold/unhold
				 * exception: kill all open multisend jobs is directly handled from the context menu
				 */
				case IDC_QMGR_REMOVE: {
					if(::SendMessage(m_hwndList, LVM_GETSELECTEDCOUNT, 0, 0) != 0) {
						LVITEM item = {0};
						LRESULT	items = ::SendMessage(m_hwndList, LVM_GETITEMCOUNT, 0, 0);
						item.mask = LVIF_STATE|LVIF_PARAM;
						item.stateMask = LVIS_SELECTED;

						if(HIWORD(wParam) != ID_QUEUEMANAGER_COPYMESSAGETOCLIPBOARD) {
							if(MessageBox(0, CTranslator::get(CTranslator::QMGR_WARNING_REMOVAL), CTranslator::get(CTranslator::QMGR_TITLE),
										  MB_ICONQUESTION | MB_OKCANCEL) == IDCANCEL)
								break;
						}
						for(LRESULT i = 0; i < items; i++) {
							item.iItem = i;
							::SendMessage(m_hwndList, LVM_GETITEM, 0, reinterpret_cast<LPARAM>(&item));
							if(item.state & LVIS_SELECTED) {
								CSendLaterJob* job = reinterpret_cast<CSendLaterJob *>(item.lParam);
								if(job) {
									switch(HIWORD(wParam)) {
										case ID_QUEUEMANAGER_MARKSELECTEDFORREMOVAL:
											job->bCode = CSendLaterJob::JOB_REMOVABLE;
											job->fFailed = true;
											break;
										case ID_QUEUEMANAGER_HOLDSELECTED:
											job->bCode = CSendLaterJob::JOB_HOLD;
											job->writeFlags();
											break;
										case ID_QUEUEMANAGER_RESUMESELECTED:
											job->bCode = 0;
											job->writeFlags();
											break;
										case ID_QUEUEMANAGER_COPYMESSAGETOCLIPBOARD: {
											TCHAR *msg = M->utf8_decodeT(job->sendBuffer);
											Utils::CopyToClipBoard(msg, m_hwndDlg);
											mir_free(msg);
											break;
										}
										/*
										 * reset deferred and aged jobs
										 * so they can be resent at next processing
										 */
										case ID_QUEUEMANAGER_RESETSELECTED: {
											if(job->bCode == CSendLaterJob::JOB_DEFERRED) {
												job->iSendCount = 0;
												job->bCode = '-';
											}
											else if(job->bCode == CSendLaterJob::JOB_AGE) {
												job->fFailed = false;
												job->bCode = '-';
												job->created = time(0);
											}
											break;
										}
										default:
											break;
									}
								}
							}
						}
						qMgrFillList();
					}
					break;
				 }
			}
			break;

		case WM_USER + 100:
			qMgrFillList();
			break;
		case WM_NCDESTROY: {
			m_hwndDlg = 0;
			M->WriteDword(0, SRMSGMOD_T, "qmgrFilterContact", reinterpret_cast<DWORD>(m_hFilter));
			break;
		}
	}
	return(FALSE);
}

/**
 * invoke queue manager dialog - do nothing if this dialog is already open
 */
void CSendLater::invokeQueueMgrDlg()
{
	if(m_hwndDlg == 0)
		m_hwndDlg = ::CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_SENDLATER_QMGR), 0, CSendLater::DlgProcStub, 
										reinterpret_cast<LPARAM>(this));
}

/*
 * service function to invoke the queue manager
 */

INT_PTR CSendLater::svcQMgr(WPARAM wParam, LPARAM lParam)
{
	sendLater->invokeQueueMgrDlg();
	return(0);
}


CSendLater* sendLater = 0;