/*

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.
*/
#include "..\..\core\commonheaders.h"
#include "clc.h"

int InitGenMenu(void);
int UnitGenMenu(void);

void InitCustomMenus(void);
void UninitCustomMenus(void);

void MTG_OnmodulesLoad(void);

static BOOL bModuleInitialized = FALSE;
static HANDLE hClcWindowList;
static HANDLE hShowInfoTipEvent;
HANDLE hHideInfoTipEvent;
static HANDLE hAckHook;
static HANDLE hClcSettingsChanged;

int g_IconWidth, g_IconHeight;

void FreeDisplayNameCache(void);

void fnClcBroadcast(int msg, WPARAM wParam, LPARAM lParam)
{
	WindowList_Broadcast(hClcWindowList, msg, wParam, lParam);
}

void fnClcOptionsChanged(void)
{
	cli.pfnClcBroadcast(INTM_RELOADOPTIONS, 0, 0);
}

HMENU fnBuildGroupPopupMenu(struct ClcGroup* group)
{
    HMENU hMenu = LoadMenu(cli.hInst, MAKEINTRESOURCE(IDR_CONTEXT));
    HMENU hGroupMenu = GetSubMenu(hMenu, 2);
    RemoveMenu(hMenu, 2, MF_BYPOSITION);
    DestroyMenu(hMenu);
    TranslateMenu(hGroupMenu);

    CheckMenuItem(hGroupMenu, POPUP_GROUPHIDEOFFLINE, group->hideOffline ? MF_CHECKED : MF_UNCHECKED);
	return hGroupMenu;
}

/////////////////////////////////////////////////////////////////////////////////////////
// standard CLC services

static int ClcSettingChanged(WPARAM wParam, LPARAM lParam)
{
	DBCONTACTWRITESETTING *cws = (DBCONTACTWRITESETTING *) lParam;
	if ((HANDLE)wParam != NULL && !strcmp(cws->szModule, "CList")) {
		if ( !strcmp(cws->szSetting, "MyHandle")) {
			cli.pfnInvalidateDisplayNameCacheEntry((HANDLE) wParam);
			cli.pfnClcBroadcast(INTM_NAMECHANGED, wParam, lParam);
		}
		else if ( !strcmp(cws->szSetting, "Group"))
			cli.pfnClcBroadcast(INTM_GROUPCHANGED, wParam, lParam);
		else if ( !strcmp(cws->szSetting, "Hidden"))
			cli.pfnClcBroadcast(INTM_HIDDENCHANGED, wParam, lParam);
		else if ( !strcmp(cws->szSetting, "NotOnList"))
			cli.pfnClcBroadcast(INTM_NOTONLISTCHANGED, wParam, lParam);
		else if ( !strcmp(cws->szSetting, "Status"))
			cli.pfnClcBroadcast(INTM_INVALIDATE, 0, 0);
		else if ( !strcmp(cws->szSetting, "NameOrder"))
			cli.pfnClcBroadcast(INTM_NAMEORDERCHANGED, 0, 0);
	}
	else if ( !strcmp(cws->szModule, "CListGroups")) {
		cli.pfnClcBroadcast(INTM_GROUPSCHANGED, wParam, lParam);
	}
	else {
		char* szProto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, wParam, 0);
		if (szProto != NULL && (HANDLE) wParam != NULL) {
			char *id = NULL;
			if ( !strcmp(cws->szModule, "Protocol") && !strcmp(cws->szSetting, "p"))
				cli.pfnClcBroadcast(INTM_PROTOCHANGED, wParam, lParam);

			// something is being written to a protocol module
			if ( !strcmp(szProto, cws->szModule)) {
				// was a unique setting key written?
				id = (char *) CallProtoServiceInt(NULL,szProto, PS_GETCAPS, PFLAG_UNIQUEIDSETTING, 0);
				if ((INT_PTR) id != CALLSERVICE_NOTFOUND && id != NULL && !strcmp(id, cws->szSetting))
					cli.pfnClcBroadcast(INTM_PROTOCHANGED, wParam, lParam);
			}
		}
		if (szProto == NULL || strcmp(szProto, cws->szModule))
			return 0;
		if ( !strcmp(cws->szSetting, "Nick") || !strcmp(cws->szSetting, "FirstName") || !strcmp(cws->szSetting, "e-mail")
			 ||  !strcmp(cws->szSetting, "LastName") || !strcmp(cws->szSetting, "UIN"))
			cli.pfnClcBroadcast(INTM_NAMECHANGED, wParam, lParam);
		else if ( !strcmp(cws->szSetting, "ApparentMode"))
			cli.pfnClcBroadcast(INTM_APPARENTMODECHANGED, wParam, lParam);
		else if ( !strcmp(cws->szSetting, "IdleTS"))
			cli.pfnClcBroadcast(INTM_IDLECHANGED, wParam, lParam);
	}
	return 0;
}

static int ClcAccountsChanged(WPARAM, LPARAM)
{
	int i, cnt;
	for (i=0, cnt=0; i < accounts.getCount(); ++i)
		if (Proto_IsAccountEnabled(accounts[i])) ++cnt;

	cli.hClcProtoCount = cnt;
	cli.clcProto = (ClcProtoStatus *) mir_realloc(cli.clcProto, sizeof(ClcProtoStatus) * cli.hClcProtoCount);

	for (i=0, cnt=0; i < accounts.getCount(); ++i) {
		if (Proto_IsAccountEnabled(accounts[i])) {
			cli.clcProto[cnt].szProto = accounts[i]->szModuleName;
			cli.clcProto[cnt].dwStatus = CallProtoServiceInt(NULL,accounts[i]->szModuleName, PS_GETSTATUS, 0, 0);
			++cnt;
		}
	}
	return 0;
}

static int ClcModulesLoaded(WPARAM, LPARAM)
{
	ClcAccountsChanged(0, 0);
	MTG_OnmodulesLoad();
	return 0;
}

static int ClcProtoAck(WPARAM, LPARAM lParam)
{
	ACKDATA *ack = (ACKDATA *) lParam;
	int i;

	if (ack->type == ACKTYPE_STATUS) {
		WindowList_BroadcastAsync(hClcWindowList, INTM_INVALIDATE, 0, 0);
		if (ack->result == ACKRESULT_SUCCESS) {
			for (i=0; i < cli.hClcProtoCount; i++) {
				if ( !lstrcmpA(cli.clcProto[i].szProto, ack->szModule)) {
					cli.clcProto[i].dwStatus = (WORD) ack->lParam;
					break;
				}
			}
		}
	}
	return 0;
}

static int ClcContactAdded(WPARAM wParam, LPARAM lParam)
{
	WindowList_BroadcastAsync(hClcWindowList, INTM_CONTACTADDED, wParam, lParam);
	return 0;
}

static int ClcContactDeleted(WPARAM wParam, LPARAM lParam)
{
	WindowList_BroadcastAsync(hClcWindowList, INTM_CONTACTDELETED, wParam, lParam);
	return 0;
}

static int ClcContactIconChanged(WPARAM wParam, LPARAM lParam)
{
	WindowList_BroadcastAsync(hClcWindowList, INTM_ICONCHANGED, wParam, lParam);
	return 0;
}

static int ClcIconsChanged(WPARAM, LPARAM)
{
	WindowList_BroadcastAsync(hClcWindowList, INTM_INVALIDATE, 0, 0);
	return 0;
}

static INT_PTR SetInfoTipHoverTime(WPARAM wParam, LPARAM)
{
	DBWriteContactSettingWord(NULL, "CLC", "InfoTipHoverTime", (WORD) wParam);
	cli.pfnClcBroadcast(INTM_SETINFOTIPHOVERTIME, wParam, 0);
	return 0;
}

static INT_PTR GetInfoTipHoverTime(WPARAM, LPARAM)
{
	return DBGetContactSettingWord(NULL, "CLC", "InfoTipHoverTime", 750);
}

static void SortClcByTimer(HWND hwnd)
{
	KillTimer(hwnd, TIMERID_DELAYEDRESORTCLC);
	SetTimer(hwnd, TIMERID_DELAYEDRESORTCLC, 200, NULL);
}

int LoadCLCModule(void)
{
	bModuleInitialized = TRUE;
	
	g_IconWidth = GetSystemMetrics(SM_CXSMICON);
	g_IconHeight = GetSystemMetrics(SM_CYSMICON);

	hClcWindowList = (HANDLE) CallService(MS_UTILS_ALLOCWINDOWLIST, 0, 0);
	hShowInfoTipEvent = CreateHookableEvent(ME_CLC_SHOWINFOTIP);
	hHideInfoTipEvent = CreateHookableEvent(ME_CLC_HIDEINFOTIP);
	CreateServiceFunction(MS_CLC_SETINFOTIPHOVERTIME, SetInfoTipHoverTime);
	CreateServiceFunction(MS_CLC_GETINFOTIPHOVERTIME, GetInfoTipHoverTime);

	InitFileDropping();

	HookEvent(ME_SYSTEM_MODULESLOADED, ClcModulesLoaded);
	HookEvent(ME_PROTO_ACCLISTCHANGED, ClcAccountsChanged);
	hClcSettingsChanged = HookEvent(ME_DB_CONTACT_SETTINGCHANGED, ClcSettingChanged);
	HookEvent(ME_DB_CONTACT_ADDED, ClcContactAdded);
	HookEvent(ME_DB_CONTACT_DELETED, ClcContactDeleted);
	HookEvent(ME_CLIST_CONTACTICONCHANGED, ClcContactIconChanged);
	HookEvent(ME_SKIN_ICONSCHANGED, ClcIconsChanged);
	hAckHook = (HANDLE) HookEvent(ME_PROTO_ACK, ClcProtoAck);

	InitCustomMenus();
	return 0;
}

void UnloadClcModule()
{
	if ( !bModuleInitialized) return;
	
	UnhookEvent(hAckHook);
	UnhookEvent(hClcSettingsChanged);

	mir_free(cli.clcProto);

	FreeDisplayNameCache();

	UninitCustomMenus();
	UnitGenMenu();	
}

/////////////////////////////////////////////////////////////////////////////////////////
// default contact list control window procedure

LRESULT CALLBACK fnContactListControlWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	struct ClcData *dat;

	dat = (struct ClcData *) GetWindowLongPtr(hwnd, 0);
	if (msg >= CLM_FIRST && msg < CLM_LAST)
		return cli.pfnProcessExternalMessages(hwnd, dat, msg, wParam, lParam);

	switch (msg) {
	case WM_CREATE:
		WindowList_Add(hClcWindowList, hwnd, NULL);
		cli.pfnRegisterFileDropping(hwnd);
		if (dat == NULL) {
			dat = (struct ClcData *) mir_calloc(sizeof(struct ClcData));
			SetWindowLongPtr(hwnd, 0, (LONG_PTR) dat);
		}
		{
			int i;
			for (i=0; i <= FONTID_MAX; i++)
				dat->fontInfo[i].changed = 1;
		}
		dat->selection = -1;
		dat->iconXSpace = 20;
		dat->checkboxSize = 13;
		dat->dragAutoScrollHeight = 30;
		dat->iDragItem = -1;
		dat->iInsertionMark = -1;
		dat->insertionMarkHitHeight = 5;
		dat->iHotTrack = -1;
		dat->infoTipTimeout = DBGetContactSettingWord(NULL, "CLC", "InfoTipHoverTime", 750);
		dat->extraColumnSpacing = 20;
		dat->list.cl.increment = 30;
		dat->needsResort = 1;
		cli.pfnLoadClcOptions(hwnd, dat);
		if ( !IsWindowVisible(hwnd))
			SetTimer(hwnd, TIMERID_REBUILDAFTER, 10, NULL);
		else
		{
			cli.pfnRebuildEntireList(hwnd, dat);
			NMCLISTCONTROL nm;
			nm.hdr.code = CLN_LISTREBUILT;
			nm.hdr.hwndFrom = hwnd;
			nm.hdr.idFrom = GetDlgCtrlID(hwnd);
			SendMessage(GetParent(hwnd), WM_NOTIFY, 0, (LPARAM) & nm);
		}
		break;
	case INTM_SCROLLBARCHANGED:
		if (GetWindowLongPtr(hwnd, GWL_STYLE) & CLS_CONTACTLIST) {
			if (dat->noVScrollbar)
				ShowScrollBar(hwnd, SB_VERT, FALSE);
			else
				cli.pfnRecalcScrollBar(hwnd, dat);
		}
		break;

	case INTM_RELOADOPTIONS:
		cli.pfnLoadClcOptions(hwnd, dat);
		cli.pfnSaveStateAndRebuildList(hwnd, dat);
		break;
	case WM_THEMECHANGED:
		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		break;
	case WM_SIZE:
		cli.pfnEndRename(hwnd, dat, 1);
		KillTimer(hwnd, TIMERID_INFOTIP);
		KillTimer(hwnd, TIMERID_RENAME);
		cli.pfnRecalcScrollBar(hwnd, dat);
		{ // creating imagelist containing blue line for highlight
			HBITMAP hBmp, hBmpMask, hoBmp, hoMaskBmp;
			HDC hdc, hdcMem;
			RECT rc;
			int depth;
			HBRUSH hBrush;

			GetClientRect(hwnd, &rc);
			if (rc.right == 0)
				break;
			rc.bottom = dat->rowHeight;
			hdc = GetDC(hwnd);
			depth = GetDeviceCaps(hdc, BITSPIXEL);
			if (depth < 16)
				depth = 16;
			hBmp = CreateBitmap(rc.right, rc.bottom, 1, depth, NULL);
			hBmpMask = CreateBitmap(rc.right, rc.bottom, 1, 1, NULL);
			hdcMem = CreateCompatibleDC(hdc);
			hoBmp = (HBITMAP) SelectObject(hdcMem, hBmp);
			hBrush = CreateSolidBrush(dat->useWindowsColours ? GetSysColor(COLOR_HIGHLIGHT) : dat->selBkColour);
			FillRect(hdcMem, &rc, hBrush);
			DeleteObject(hBrush);

			hoMaskBmp = (HBITMAP)SelectObject(hdcMem, hBmpMask);
			FillRect(hdcMem, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH));
				SelectObject(hdcMem, hoMaskBmp);
			SelectObject(hdcMem, hoBmp);
			DeleteDC(hdcMem);
			ReleaseDC(hwnd, hdc);
			if (dat->himlHighlight)
				ImageList_Destroy(dat->himlHighlight);
			dat->himlHighlight = ImageList_Create(rc.right, rc.bottom, (IsWinVerXPPlus()? ILC_COLOR32 : ILC_COLOR16) | ILC_MASK, 1, 1);
			ImageList_Add(dat->himlHighlight, hBmp, hBmpMask);
			DeleteObject(hBmpMask);
			DeleteObject(hBmp);
		}
		break;

	case WM_SYSCOLORCHANGE:
		SendMessage(hwnd, WM_SIZE, 0, 0);
		break;

	case WM_GETDLGCODE:
		if (lParam) {
			MSG *msg = (MSG *) lParam;
			if (msg->message == WM_KEYDOWN) {
				if (msg->wParam == VK_TAB)
					return 0;
				if (msg->wParam == VK_ESCAPE && dat->hwndRenameEdit == NULL && dat->szQuickSearch[0] == 0)
					return 0;
			}
			if (msg->message == WM_CHAR) {
				if (msg->wParam == '\t')
					return 0;
				if (msg->wParam == 27 && dat->hwndRenameEdit == NULL && dat->szQuickSearch[0] == 0)
					return 0;
			}
		}
		return DLGC_WANTMESSAGE;

	case WM_KILLFOCUS:
		KillTimer(hwnd, TIMERID_INFOTIP);
		KillTimer(hwnd, TIMERID_RENAME);
	case WM_SETFOCUS:
	case WM_ENABLE:
		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		break;

	case WM_GETFONT:
		return (LRESULT) dat->fontInfo[FONTID_CONTACTS].hFont;

	case INTM_GROUPSCHANGED:
	{
		DBCONTACTWRITESETTING *dbcws = (DBCONTACTWRITESETTING *) lParam;
		if (dbcws->value.type == DBVT_ASCIIZ || dbcws->value.type == DBVT_UTF8) {
			int groupId = atoi(dbcws->szSetting) + 1;
			ClcContact *contact;
			ClcGroup *group;
			TCHAR szFullName[512];
			int i, nameLen, eq;
			//check name of group and ignore message if just being expanded/collapsed
			if (cli.pfnFindItem(hwnd, dat, (HANDLE) (groupId | HCONTACT_ISGROUP), &contact, &group, NULL)) {
				lstrcpy(szFullName, contact->szText);
				while (group->parent) {
					for (i=0; i < group->parent->cl.count; i++)
						if (group->parent->cl.items[i]->group == group)
							break;
					if (i == group->parent->cl.count) {
						szFullName[0] = '\0';
						break;
					}
					group = group->parent;
					nameLen = lstrlen(group->cl.items[i]->szText);
					if (lstrlen(szFullName) + 1 + nameLen > SIZEOF(szFullName)) {
						szFullName[0] = '\0';
						break;
					}
					memmove(szFullName + 1 + nameLen, szFullName, sizeof(TCHAR)*(lstrlen(szFullName) + 1));
					memcpy(szFullName, group->cl.items[i]->szText, sizeof(TCHAR)*nameLen);
					szFullName[nameLen] = '\\';
				}

				if (dbcws->value.type == DBVT_ASCIIZ) {
					WCHAR* wszGrpName = mir_a2u(dbcws->value.pszVal+1);
					eq = !lstrcmp(szFullName, wszGrpName);
					mir_free(wszGrpName);
				}
				else {
					char* szGrpName = NEWSTR_ALLOCA(dbcws->value.pszVal+1);
					WCHAR* wszGrpName;
					Utf8Decode(szGrpName, &wszGrpName);
					eq = !lstrcmp(szFullName, wszGrpName);
					mir_free(wszGrpName);
				}
				if (eq && (contact->group->hideOffline != 0) == ((dbcws->value.pszVal[0] & GROUPF_HIDEOFFLINE) != 0))
					break;  //only expanded has changed: no action reqd
			}
		}
		cli.pfnSaveStateAndRebuildList(hwnd, dat);
		break;
	}
	case INTM_NAMEORDERCHANGED:
		PostMessage(hwnd, CLM_AUTOREBUILD, 0, 0);
		break;

	case INTM_CONTACTADDED:
		cli.pfnAddContactToTree(hwnd, dat, (HANDLE) wParam, 1, 1);
		cli.pfnNotifyNewContact(hwnd, (HANDLE) wParam);
		SortClcByTimer(hwnd);
		break;

	case INTM_CONTACTDELETED:
		cli.pfnDeleteItemFromTree(hwnd, (HANDLE) wParam);
		SortClcByTimer(hwnd);
		break;

	case INTM_HIDDENCHANGED:
	{
		DBCONTACTWRITESETTING *dbcws = (DBCONTACTWRITESETTING *) lParam;
		if (GetWindowLongPtr(hwnd, GWL_STYLE) & CLS_SHOWHIDDEN)
			break;
		if (dbcws->value.type == DBVT_DELETED || dbcws->value.bVal == 0) {
			if (cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, NULL, NULL, NULL))
				break;
			cli.pfnAddContactToTree(hwnd, dat, (HANDLE) wParam, 1, 1);
			cli.pfnNotifyNewContact(hwnd, (HANDLE) wParam);
		}
		else cli.pfnDeleteItemFromTree(hwnd, (HANDLE) wParam);

		dat->needsResort = 1;
		SortClcByTimer(hwnd);
		break;
	}
	case INTM_GROUPCHANGED:
	{
		ClcContact *contact;
		BYTE iExtraImage[MAXEXTRACOLUMNS];
		BYTE flags = 0;
		if ( !cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, &contact, NULL, NULL))
			memset(iExtraImage, 0xFF, SIZEOF(iExtraImage));
		else {
			CopyMemory(iExtraImage, contact->iExtraImage, SIZEOF(iExtraImage));
			flags = contact->flags;
		}
		cli.pfnDeleteItemFromTree(hwnd, (HANDLE) wParam);
		if (GetWindowLongPtr(hwnd, GWL_STYLE) & CLS_SHOWHIDDEN || !DBGetContactSettingByte((HANDLE) wParam, "CList", "Hidden", 0)) {
			NMCLISTCONTROL nm;
			cli.pfnAddContactToTree(hwnd, dat, (HANDLE) wParam, 1, 1);
			if (cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, &contact, NULL, NULL)) {
				CopyMemory(contact->iExtraImage, iExtraImage, SIZEOF(iExtraImage));
				if (flags & CONTACTF_CHECKED)
					contact->flags |= CONTACTF_CHECKED;
			}
			nm.hdr.code = CLN_CONTACTMOVED;
			nm.hdr.hwndFrom = hwnd;
			nm.hdr.idFrom = GetDlgCtrlID(hwnd);
			nm.flags = 0;
			nm.hItem = (HANDLE) wParam;
			SendMessage(GetParent(hwnd), WM_NOTIFY, 0, (LPARAM) & nm);
			dat->needsResort = 1;
		}
		SetTimer(hwnd, TIMERID_REBUILDAFTER, 1, NULL);
		break;
	}
	case INTM_ICONCHANGED:
	{
		ClcContact *contact = NULL;
		ClcGroup *group = NULL;
		int recalcScrollBar = 0, shouldShow;
		WORD status;
		char *szProto;
		HANDLE hSelItem = NULL;
		ClcContact *selcontact = NULL;

		szProto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, wParam, 0);
		if (szProto == NULL)
			status = ID_STATUS_OFFLINE;
		else
			status = DBGetContactSettingWord((HANDLE) wParam, szProto, "Status", ID_STATUS_OFFLINE);

		// this means an offline msg is flashing, so the contact should be shown
		DWORD style = GetWindowLongPtr(hwnd, GWL_STYLE);
		shouldShow = (style & CLS_SHOWHIDDEN || !DBGetContactSettingByte((HANDLE) wParam, "CList", "Hidden", 0))
			&& ( !cli.pfnIsHiddenMode(dat, status) || CallService(MS_CLIST_GETCONTACTICON, wParam, 0) != lParam); 
		if ( !cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, &contact, &group, NULL)) {
			if (shouldShow && CallService(MS_DB_CONTACT_IS, wParam, 0)) {
				if (dat->selection >= 0 && cli.pfnGetRowByIndex(dat, dat->selection, &selcontact, NULL) != -1)
					hSelItem = cli.pfnContactToHItem(selcontact);
				cli.pfnAddContactToTree(hwnd, dat, (HANDLE) wParam, (style & CLS_CONTACTLIST) == 0, 0);
				recalcScrollBar = 1;
				cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, &contact, NULL, NULL);
				if (contact) {
					contact->iImage = (WORD) lParam;
					cli.pfnNotifyNewContact(hwnd, (HANDLE) wParam);
					dat->needsResort = 1;
			}	}
		}
		else { // item in list already
			if (contact->iImage == (WORD) lParam)
				break;
			if ( !shouldShow && !(style & CLS_NOHIDEOFFLINE) && (style & CLS_HIDEOFFLINE || group->hideOffline)) {
				if (dat->selection >= 0 && cli.pfnGetRowByIndex(dat, dat->selection, &selcontact, NULL) != -1)
					hSelItem = cli.pfnContactToHItem(selcontact);
				cli.pfnRemoveItemFromGroup(hwnd, group, contact, (style & CLS_CONTACTLIST) == 0);
				recalcScrollBar = 1;
			}
			else {
				contact->iImage = (WORD) lParam;
				if ( !cli.pfnIsHiddenMode(dat, status))
					contact->flags |= CONTACTF_ONLINE;
				else
					contact->flags &= ~CONTACTF_ONLINE;
			}
			dat->needsResort = 1;
		}
		if (hSelItem) {
			ClcGroup *selgroup;
			if (cli.pfnFindItem(hwnd, dat, hSelItem, &selcontact, &selgroup, NULL))
				dat->selection = cli.pfnGetRowsPriorTo(&dat->list, selgroup, List_IndexOf((SortedList*)&selgroup->cl, selcontact));
			else
				dat->selection = -1;
		}
		SortClcByTimer(hwnd);
		break;
	}
	case INTM_NAMECHANGED:
	{
		ClcContact *contact;
		if ( !cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, &contact, NULL, NULL))
			break;

		lstrcpyn(contact->szText, cli.pfnGetContactDisplayName((HANDLE)wParam, 0), SIZEOF(contact->szText));
		dat->needsResort = 1;
		SortClcByTimer(hwnd);
		break;
	}
	case INTM_PROTOCHANGED:
	{
		ClcContact *contact = NULL;
		if ( !cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, &contact, NULL, NULL))
			break;
		contact->proto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, wParam, 0);
		cli.pfnInvalidateDisplayNameCacheEntry((HANDLE)wParam);
		lstrcpyn(contact->szText, cli.pfnGetContactDisplayName((HANDLE)wParam, 0), SIZEOF(contact->szText));
		SortClcByTimer(hwnd);
		break;
	}
	case INTM_NOTONLISTCHANGED:
	{
		DBCONTACTWRITESETTING *dbcws = (DBCONTACTWRITESETTING *) lParam;
		ClcContact *contact;
		if ( !cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, &contact, NULL, NULL))
			break;
		if (contact->type != CLCIT_CONTACT)
			break;
		if (dbcws->value.type == DBVT_DELETED || dbcws->value.bVal == 0)
			contact->flags &= ~CONTACTF_NOTONLIST;
		else
			contact->flags |= CONTACTF_NOTONLIST;
		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		break;
	}
	case INTM_INVALIDATE:
		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		break;

	case INTM_APPARENTMODECHANGED:
	{
		WORD apparentMode;
		char *szProto;
		ClcContact *contact;
		if ( !cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, &contact, NULL, NULL))
			break;
		szProto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, wParam, 0);
		if (szProto == NULL)
			break;
		apparentMode = DBGetContactSettingWord((HANDLE) wParam, szProto, "ApparentMode", 0);
		contact->flags &= ~(CONTACTF_INVISTO | CONTACTF_VISTO);
		if (apparentMode == ID_STATUS_OFFLINE)
			contact->flags |= CONTACTF_INVISTO;
		else if (apparentMode == ID_STATUS_ONLINE)
			contact->flags |= CONTACTF_VISTO;
		else if (apparentMode)
			contact->flags |= CONTACTF_VISTO | CONTACTF_INVISTO;
		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		break;
	}
	case INTM_SETINFOTIPHOVERTIME:
		dat->infoTipTimeout = wParam;
		break;

	case INTM_IDLECHANGED:
	{
		char *szProto;
		ClcContact *contact;
		if ( !cli.pfnFindItem(hwnd, dat, (HANDLE) wParam, &contact, NULL, NULL))
			break;
		szProto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, wParam, 0);
		if (szProto == NULL)
			break;
		contact->flags &= ~CONTACTF_IDLE;
		if (DBGetContactSettingDword((HANDLE) wParam, szProto, "IdleTS", 0)) {
			contact->flags |= CONTACTF_IDLE;
		}
		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		break;
	}
	case WM_PRINTCLIENT:
		cli.pfnPaintClc(hwnd, dat, (HDC) wParam, NULL);
		break;

	case WM_NCPAINT:
		if (wParam == 1)
			break;
		{
			POINT ptTopLeft = { 0, 0 };
			HRGN hClientRgn;
			ClientToScreen(hwnd, &ptTopLeft);
			hClientRgn = CreateRectRgn(0, 0, 1, 1);
			CombineRgn(hClientRgn, (HRGN) wParam, NULL, RGN_COPY);
			OffsetRgn(hClientRgn, -ptTopLeft.x, -ptTopLeft.y);
			InvalidateRgn(hwnd, hClientRgn, FALSE);
			DeleteObject(hClientRgn);
			UpdateWindow(hwnd);
		}
		break;

	case WM_PAINT:
	{
		HDC hdc;
		PAINTSTRUCT ps;
		hdc = BeginPaint(hwnd, &ps);
		/* we get so many cli.pfnInvalidateRect()'s that there is no point painting, 
		Windows in theory shouldn't queue up WM_PAINTs in this case but it does so
		we'll just ignore them */
		if (IsWindowVisible(hwnd))
			cli.pfnPaintClc(hwnd, dat, hdc, &ps.rcPaint);
		EndPaint(hwnd, &ps);
		break;
	}
	case WM_VSCROLL:
	{
		int desty;
		RECT clRect;
		int noSmooth = 0;

		cli.pfnEndRename(hwnd, dat, 1);
		cli.pfnHideInfoTip(hwnd, dat);
		KillTimer(hwnd, TIMERID_INFOTIP);
		KillTimer(hwnd, TIMERID_RENAME);
		desty = dat->yScroll;
		GetClientRect(hwnd, &clRect);
		switch (LOWORD(wParam)) {
			case SB_LINEUP:     desty -= dat->rowHeight;   break;
			case SB_LINEDOWN:   desty += dat->rowHeight;   break;
			case SB_PAGEUP:     desty -= clRect.bottom - dat->rowHeight; break;
			case SB_PAGEDOWN:   desty += clRect.bottom - dat->rowHeight; break;
			case SB_BOTTOM:     desty = 0x7FFFFFFF;        break;
			case SB_TOP:        desty = 0;                 break;
			case SB_THUMBTRACK: desty = HIWORD(wParam); noSmooth = 1; break;      //noone has more than 4000 contacts, right?
			default:            return 0;
		}
		cli.pfnScrollTo(hwnd, dat, desty, noSmooth);
		break;
	}
	case WM_MOUSEWHEEL:
	{
		UINT scrollLines;
		cli.pfnEndRename(hwnd, dat, 1);
		cli.pfnHideInfoTip(hwnd, dat);
		KillTimer(hwnd, TIMERID_INFOTIP);
		KillTimer(hwnd, TIMERID_RENAME);
		if ( !SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &scrollLines, FALSE))
			scrollLines = 3;
		cli.pfnScrollTo(hwnd, dat, dat->yScroll - (short) HIWORD(wParam) * dat->rowHeight * (signed) scrollLines / WHEEL_DELTA, 0);
		return 0;
	}
	case WM_KEYDOWN:
	{
		int selMoved = 0;
		int changeGroupExpand = 0;
		int pageSize;
		cli.pfnHideInfoTip(hwnd, dat);
		KillTimer(hwnd, TIMERID_INFOTIP);
		KillTimer(hwnd, TIMERID_RENAME);
		if (CallService(MS_CLIST_MENUPROCESSHOTKEY, wParam, MPCF_CONTACTMENU))
			break;
		{
			RECT clRect;
			GetClientRect(hwnd, &clRect);
			pageSize = clRect.bottom / dat->rowHeight;
		}
		switch (wParam) {
		case VK_DOWN:   dat->selection++; selMoved = 1; break;
		case VK_UP:     dat->selection--; selMoved = 1; break;
		case VK_PRIOR:  dat->selection -= pageSize; selMoved = 1; break;
		case VK_NEXT:   dat->selection += pageSize; selMoved = 1; break;
		case VK_HOME:   dat->selection = 0; selMoved = 1; break;
		case VK_END:    dat->selection = cli.pfnGetGroupContentsCount(&dat->list, 1) - 1; selMoved = 1; break;
		case VK_LEFT:   changeGroupExpand = 1; break;
		case VK_RIGHT:  changeGroupExpand = 2; break;
		case VK_RETURN:
			cli.pfnDoSelectionDefaultAction(hwnd, dat);
			dat->szQuickSearch[0] = 0;
			if (dat->filterSearch)
				cli.pfnSaveStateAndRebuildList(hwnd, dat);
			return 0;
		case VK_F2:     cli.pfnBeginRenameSelection(hwnd, dat); return 0;
		case VK_DELETE: cli.pfnDeleteFromContactList(hwnd, dat); return 0;
		default:
			{
				NMKEY nmkey;
				nmkey.hdr.hwndFrom = hwnd;
				nmkey.hdr.idFrom = GetDlgCtrlID(hwnd);
				nmkey.hdr.code = NM_KEYDOWN;
				nmkey.nVKey = wParam;
				nmkey.uFlags = HIWORD(lParam);
				if (SendMessage(GetParent(hwnd), WM_NOTIFY, 0, (LPARAM) & nmkey))
					return 0;
			}
		}
		if (changeGroupExpand) {
			int hit;
			ClcContact *contact;
			ClcGroup *group;
			if (!dat->filterSearch)
				dat->szQuickSearch[0] = 0;
			hit = cli.pfnGetRowByIndex(dat, dat->selection, &contact, &group);
			if (hit != -1) {
				if (changeGroupExpand == 1 && contact->type == CLCIT_CONTACT) {
					if (group == &dat->list)
						return 0;
					dat->selection = cli.pfnGetRowsPriorTo(&dat->list, group, -1);
					selMoved = 1;
				}
				else {
					if (contact->type == CLCIT_GROUP)
						cli.pfnSetGroupExpand(hwnd, dat, contact->group, changeGroupExpand == 2);
					return 0;
				}
			}
			else
				return 0;
		}
		if (selMoved) {
			if ( !dat->filterSearch)
				dat->szQuickSearch[0] = 0;
			if (dat->selection >= cli.pfnGetGroupContentsCount(&dat->list, 1))
				dat->selection = cli.pfnGetGroupContentsCount(&dat->list, 1) - 1;
			if (dat->selection < 0)
				dat->selection = 0;
			cli.pfnInvalidateRect(hwnd, NULL, FALSE);
			cli.pfnEnsureVisible(hwnd, dat, dat->selection, 0);
			UpdateWindow(hwnd);
			return 0;
		}
		break;
	}
	case WM_CHAR:
		cli.pfnHideInfoTip(hwnd, dat);
		KillTimer(hwnd, TIMERID_INFOTIP);
		KillTimer(hwnd, TIMERID_RENAME);
		if (wParam == 27)   //escape
			dat->szQuickSearch[0] = 0;
		else if (wParam == '\b' && dat->szQuickSearch[0])
			dat->szQuickSearch[lstrlen(dat->szQuickSearch) - 1] = '\0';
		else if (wParam < ' ')
			break;
		else if (wParam == ' ' && dat->szQuickSearch[0] == '\0' && GetWindowLongPtr(hwnd, GWL_STYLE) & CLS_CHECKBOXES) {
			ClcContact *contact;
			NMCLISTCONTROL nm;
			if (cli.pfnGetRowByIndex(dat, dat->selection, &contact, NULL) == -1)
				break;
			if (contact->type != CLCIT_CONTACT)
				break;
			contact->flags ^= CONTACTF_CHECKED;
			if (contact->type == CLCIT_GROUP)
				cli.pfnSetGroupChildCheckboxes(contact->group, contact->flags & CONTACTF_CHECKED);
			cli.pfnRecalculateGroupCheckboxes(hwnd, dat);
			cli.pfnInvalidateRect(hwnd, NULL, FALSE);
			nm.hdr.code = CLN_CHECKCHANGED;
			nm.hdr.hwndFrom = hwnd;
			nm.hdr.idFrom = GetDlgCtrlID(hwnd);
			nm.flags = 0;
			nm.hItem = cli.pfnContactToItemHandle(contact, &nm.flags);
			SendMessage(GetParent(hwnd), WM_NOTIFY, 0, (LPARAM) & nm);
		}
		else {
			TCHAR szNew[2];
			szNew[0] = (TCHAR) wParam;
			szNew[1] = '\0';
			if (lstrlen(dat->szQuickSearch) >= SIZEOF(dat->szQuickSearch) - 1) {
				MessageBeep(MB_OK);
				break;
			}
			_tcscat(dat->szQuickSearch, szNew);
		}

		if (dat->filterSearch)
			cli.pfnSaveStateAndRebuildList(hwnd, dat);

		if (dat->szQuickSearch[0]) {
			int index;
			index = cli.pfnFindRowByText(hwnd, dat, dat->szQuickSearch, 1);
			if (index != -1)
				dat->selection = index;
			else {
				MessageBeep(MB_OK);
				dat->szQuickSearch[ lstrlen(dat->szQuickSearch) - 1] = '\0';
				cli.pfnSaveStateAndRebuildList(hwnd, dat);
			}
			cli.pfnInvalidateRect(hwnd, NULL, FALSE);
			cli.pfnEnsureVisible(hwnd, dat, dat->selection, 0);
		}
		else
			cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		break;

	case WM_SYSKEYDOWN:
		cli.pfnEndRename(hwnd, dat, 1);
		cli.pfnHideInfoTip(hwnd, dat);
		KillTimer(hwnd, TIMERID_INFOTIP);
		KillTimer(hwnd, TIMERID_RENAME);
		dat->iHotTrack = -1;
		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		ReleaseCapture();
		if (wParam == VK_F10 && GetKeyState(VK_SHIFT) & 0x8000)
			break;
		SendMessage(GetParent(hwnd), msg, wParam, lParam);
		return 0;

	case WM_TIMER:
		switch(wParam) {
		case TIMERID_RENAME:
			cli.pfnBeginRenameSelection(hwnd, dat);
			break;
		case TIMERID_DRAGAUTOSCROLL:
			cli.pfnScrollTo(hwnd, dat, dat->yScroll + dat->dragAutoScrolling * dat->rowHeight * 2, 0);
			break;
		case TIMERID_INFOTIP:
		{	CLCINFOTIP it;
			ClcContact *contact;
			int hit;
			RECT clRect;
			POINT ptClientOffset = { 0 };

			KillTimer(hwnd, wParam);
			GetCursorPos(&it.ptCursor);
			ScreenToClient(hwnd, &it.ptCursor);
			if (it.ptCursor.x != dat->ptInfoTip.x || it.ptCursor.y != dat->ptInfoTip.y)
				break;
			GetClientRect(hwnd, &clRect);
			it.rcItem.left = 0;
			it.rcItem.right = clRect.right;
			hit = cli.pfnHitTest(hwnd, dat, it.ptCursor.x, it.ptCursor.y, &contact, NULL, NULL);
			if (hit == -1)
				break;
			if (contact->type != CLCIT_GROUP && contact->type != CLCIT_CONTACT)
				break;
			ClientToScreen(hwnd, &it.ptCursor);
			ClientToScreen(hwnd, &ptClientOffset);
			it.isTreeFocused = GetFocus() == hwnd;
			it.rcItem.top = cli.pfnGetRowTopY(dat, hit) - dat->yScroll;
			it.rcItem.bottom = it.rcItem.top + cli.pfnGetRowHeight(dat, hit);
			OffsetRect(&it.rcItem, ptClientOffset.x, ptClientOffset.y);
			it.isGroup = contact->type == CLCIT_GROUP;
			it.hItem = contact->type == CLCIT_GROUP ? (HANDLE) contact->groupId : contact->hContact;
			it.cbSize = sizeof(it);
			dat->hInfoTipItem = cli.pfnContactToHItem(contact);
			NotifyEventHooks(hShowInfoTipEvent, 0, (LPARAM) & it);
			break;
		}
		case TIMERID_REBUILDAFTER:
			KillTimer(hwnd, TIMERID_REBUILDAFTER);
			cli.pfnInvalidateRect(hwnd, NULL, FALSE);
			cli.pfnSaveStateAndRebuildList(hwnd, dat);
			break;

		case TIMERID_DELAYEDRESORTCLC:
			KillTimer(hwnd, TIMERID_DELAYEDRESORTCLC);
			cli.pfnInvalidateRect(hwnd, NULL, FALSE);
			cli.pfnSortCLC(hwnd, dat, 1);
			cli.pfnRecalcScrollBar(hwnd, dat);
			break;
		}
		break;

	case WM_MBUTTONDOWN:
	case WM_LBUTTONDOWN:
	{
		ClcContact *contact;
		ClcGroup *group;
		int hit;
		DWORD hitFlags;

		if (GetFocus() != hwnd)
			SetFocus(hwnd);
		cli.pfnHideInfoTip(hwnd, dat);
		KillTimer(hwnd, TIMERID_INFOTIP);
		KillTimer(hwnd, TIMERID_RENAME);
		cli.pfnEndRename(hwnd, dat, 1);
		dat->ptDragStart.x = (short) LOWORD(lParam);
		dat->ptDragStart.y = (short) HIWORD(lParam);
		if ( !dat->filterSearch)
			dat->szQuickSearch[0] = 0;
		hit = cli.pfnHitTest(hwnd, dat, (short) LOWORD(lParam), (short) HIWORD(lParam), &contact, &group, &hitFlags);
		if (hit != -1) {
			if (hit == dat->selection && hitFlags & CLCHT_ONITEMLABEL && dat->exStyle & CLS_EX_EDITLABELS) {
				SetCapture(hwnd);
				dat->iDragItem = dat->selection;
				dat->dragStage = DRAGSTAGE_NOTMOVED | DRAGSTAGEF_MAYBERENAME;
				dat->dragAutoScrolling = 0;
				break;
		}	}

		if (hit != -1 && contact->type == CLCIT_GROUP)
			if (hitFlags & CLCHT_ONITEMICON) {
				ClcGroup *selgroup;
				ClcContact *selcontact;
				dat->selection = cli.pfnGetRowByIndex(dat, dat->selection, &selcontact, &selgroup);
				cli.pfnSetGroupExpand(hwnd, dat, contact->group, -1);
				if (dat->selection != -1) {
					dat->selection =
						cli.pfnGetRowsPriorTo(&dat->list, selgroup, List_IndexOf((SortedList*)&selgroup->cl, selcontact));
					if (dat->selection == -1)
						dat->selection = cli.pfnGetRowsPriorTo(&dat->list, contact->group, -1);
				}
				cli.pfnInvalidateRect(hwnd, NULL, FALSE);
				UpdateWindow(hwnd);
				break;
			}
			if (hit != -1 && hitFlags & CLCHT_ONITEMCHECK) {
				NMCLISTCONTROL nm;
				contact->flags ^= CONTACTF_CHECKED;
				if (contact->type == CLCIT_GROUP)
					cli.pfnSetGroupChildCheckboxes(contact->group, contact->flags & CONTACTF_CHECKED);
				cli.pfnRecalculateGroupCheckboxes(hwnd, dat);
				cli.pfnInvalidateRect(hwnd, NULL, FALSE);
				nm.hdr.code = CLN_CHECKCHANGED;
				nm.hdr.hwndFrom = hwnd;
				nm.hdr.idFrom = GetDlgCtrlID(hwnd);
				nm.flags = 0;
				nm.hItem = cli.pfnContactToItemHandle(contact, &nm.flags);
				SendMessage(GetParent(hwnd), WM_NOTIFY, 0, (LPARAM) & nm);
			}
			if ( !(hitFlags & (CLCHT_ONITEMICON | CLCHT_ONITEMLABEL | CLCHT_ONITEMCHECK))) {
				NMCLISTCONTROL nm;
				nm.hdr.code = NM_CLICK;
				nm.hdr.hwndFrom = hwnd;
				nm.hdr.idFrom = GetDlgCtrlID(hwnd);
				nm.flags = 0;
				if (hit == -1)
					nm.hItem = NULL;
				else
					nm.hItem = cli.pfnContactToItemHandle(contact, &nm.flags);
				nm.iColumn = hitFlags & CLCHT_ONITEMEXTRA ? HIBYTE(HIWORD(hitFlags)) : -1;
				nm.pt = dat->ptDragStart;
				SendMessage(GetParent(hwnd), WM_NOTIFY, 0, (LPARAM) & nm);
			}
			if (hitFlags & (CLCHT_ONITEMCHECK | CLCHT_ONITEMEXTRA))
				break;
			dat->selection = hit;
			cli.pfnInvalidateRect(hwnd, NULL, FALSE);
			if (dat->selection != -1)
				cli.pfnEnsureVisible(hwnd, dat, hit, 0);
			UpdateWindow(hwnd);
			if (dat->selection != -1 && (contact->type == CLCIT_CONTACT || contact->type == CLCIT_GROUP)
				&& !(hitFlags & (CLCHT_ONITEMEXTRA | CLCHT_ONITEMCHECK))) {
					SetCapture(hwnd);
					dat->iDragItem = dat->selection;
					dat->dragStage = DRAGSTAGE_NOTMOVED;
					dat->dragAutoScrolling = 0;
				}
				break;
	}
	case WM_MOUSEMOVE:
		if (dat->iDragItem == -1) {
			int iOldHotTrack = dat->iHotTrack;
			if (dat->hwndRenameEdit != NULL)
				break;
			if (GetKeyState(VK_MENU) & 0x8000 || GetKeyState(VK_F10) & 0x8000)
				break;
			dat->iHotTrack = cli.pfnHitTest(hwnd, dat, (short) LOWORD(lParam), (short) HIWORD(lParam), NULL, NULL, NULL);
			if (iOldHotTrack != dat->iHotTrack) {
				if (iOldHotTrack == -1)
					SetCapture(hwnd);
				else if (dat->iHotTrack == -1)
					ReleaseCapture();
				if (dat->exStyle & CLS_EX_TRACKSELECT) {
					cli.pfnInvalidateItem(hwnd, dat, iOldHotTrack);
					cli.pfnInvalidateItem(hwnd, dat, dat->iHotTrack);
				}
				cli.pfnHideInfoTip(hwnd, dat);
			}
			KillTimer(hwnd, TIMERID_INFOTIP);
			if (wParam == 0 && dat->hInfoTipItem == NULL) {
				dat->ptInfoTip.x = (short) LOWORD(lParam);
				dat->ptInfoTip.y = (short) HIWORD(lParam);
				SetTimer(hwnd, TIMERID_INFOTIP, dat->infoTipTimeout, NULL);
			}
			break;
		}
		if ((dat->dragStage & DRAGSTAGEM_STAGE) == DRAGSTAGE_NOTMOVED && !(dat->exStyle & CLS_EX_DISABLEDRAGDROP)) {
			if (abs((short) LOWORD(lParam) - dat->ptDragStart.x) >= GetSystemMetrics(SM_CXDRAG)
				 ||  abs((short) HIWORD(lParam) - dat->ptDragStart.y) >= GetSystemMetrics(SM_CYDRAG))
				dat->dragStage = (dat->dragStage & ~DRAGSTAGEM_STAGE) | DRAGSTAGE_ACTIVE;
		}
		if ((dat->dragStage & DRAGSTAGEM_STAGE) == DRAGSTAGE_ACTIVE) {
			HCURSOR hNewCursor;
			RECT clRect;
			POINT pt;
			int target;

			GetClientRect(hwnd, &clRect);
			pt.x = (short) LOWORD(lParam);
			pt.y = (short) HIWORD(lParam);
			hNewCursor = LoadCursor(NULL, IDC_NO);
			cli.pfnInvalidateRect(hwnd, NULL, FALSE);
			if (dat->dragAutoScrolling) {
				KillTimer(hwnd, TIMERID_DRAGAUTOSCROLL);
				dat->dragAutoScrolling = 0;
			}
			target = cli.pfnGetDropTargetInformation(hwnd, dat, pt);
			if (dat->dragStage & DRAGSTAGEF_OUTSIDE && target != DROPTARGET_OUTSIDE) {
				NMCLISTCONTROL nm;
				ClcContact *contact;
				cli.pfnGetRowByIndex(dat, dat->iDragItem, &contact, NULL);
				nm.hdr.code = CLN_DRAGSTOP;
				nm.hdr.hwndFrom = hwnd;
				nm.hdr.idFrom = GetDlgCtrlID(hwnd);
				nm.flags = 0;
				nm.hItem = cli.pfnContactToItemHandle(contact, &nm.flags);
				SendMessage(GetParent(hwnd), WM_NOTIFY, 0, (LPARAM) & nm);
				dat->dragStage &= ~DRAGSTAGEF_OUTSIDE;
			}
			switch (target) {
			case DROPTARGET_ONSELF:
			case DROPTARGET_ONCONTACT:
				break;
			case DROPTARGET_ONGROUP:
				hNewCursor = LoadCursor(cli.hInst, MAKEINTRESOURCE(IDC_DROPUSER));
				break;
			case DROPTARGET_INSERTION:
				hNewCursor = LoadCursor(cli.hInst, MAKEINTRESOURCE(IDC_DROPUSER));
				break;
			case DROPTARGET_OUTSIDE:
				{
					NMCLISTCONTROL nm;
					ClcContact *contact;

					if (pt.x >= 0 && pt.x < clRect.right
						&& ((pt.y < 0 && pt.y > -dat->dragAutoScrollHeight)
						 ||  (pt.y >= clRect.bottom && pt.y < clRect.bottom + dat->dragAutoScrollHeight))) {
							if ( !dat->dragAutoScrolling) {
								if (pt.y < 0)
									dat->dragAutoScrolling = -1;
								else
									dat->dragAutoScrolling = 1;
								SetTimer(hwnd, TIMERID_DRAGAUTOSCROLL, dat->scrollTime, NULL);
							}
							SendMessage(hwnd, WM_TIMER, TIMERID_DRAGAUTOSCROLL, 0);
						}

						dat->dragStage |= DRAGSTAGEF_OUTSIDE;
						cli.pfnGetRowByIndex(dat, dat->iDragItem, &contact, NULL);
						nm.hdr.code = CLN_DRAGGING;
						nm.hdr.hwndFrom = hwnd;
						nm.hdr.idFrom = GetDlgCtrlID(hwnd);
						nm.flags = 0;
						nm.hItem = cli.pfnContactToItemHandle(contact, &nm.flags);
						nm.pt = pt;
						if (SendMessage(GetParent(hwnd), WM_NOTIFY, 0, (LPARAM) & nm))
							return 0;
						break;
				}
			default:
				{
					ClcGroup *group;
					cli.pfnGetRowByIndex(dat, dat->iDragItem, NULL, &group);
					if (group->parent)
						hNewCursor = LoadCursor(cli.hInst, MAKEINTRESOURCE(IDC_DROPUSER));
					break;
				}
			}
			SetCursor(hNewCursor);
		}
		break;

	case WM_LBUTTONUP:
		if (dat->iDragItem == -1)
			break;
		SetCursor((HCURSOR) GetClassLongPtr(hwnd, GCLP_HCURSOR));
		if (dat->exStyle & CLS_EX_TRACKSELECT) {
			dat->iHotTrack = cli.pfnHitTest(hwnd, dat, (short) LOWORD(lParam), (short) HIWORD(lParam), NULL, NULL, NULL);
			if (dat->iHotTrack == -1)
				ReleaseCapture();
		}
		else ReleaseCapture();
		KillTimer(hwnd, TIMERID_DRAGAUTOSCROLL);
		if (dat->dragStage == (DRAGSTAGE_NOTMOVED | DRAGSTAGEF_MAYBERENAME))
			SetTimer(hwnd, TIMERID_RENAME, GetDoubleClickTime(), NULL);
		else if ((dat->dragStage & DRAGSTAGEM_STAGE) == DRAGSTAGE_ACTIVE) {
			POINT pt;
			int target;

			pt.x = (short) LOWORD(lParam);
			pt.y = (short) HIWORD(lParam);
			target = cli.pfnGetDropTargetInformation(hwnd, dat, pt);
			switch (target) {
			case DROPTARGET_ONSELF:
				break;
			case DROPTARGET_ONCONTACT:
				break;
			case DROPTARGET_ONGROUP:
			{
				ClcContact *contactn, *contacto;
				cli.pfnGetRowByIndex(dat, dat->selection, &contactn, NULL);
				cli.pfnGetRowByIndex(dat, dat->iDragItem, &contacto, NULL);
				if (contacto->type == CLCIT_CONTACT) //dropee is a contact
					CallService(MS_CLIST_CONTACTCHANGEGROUP, (WPARAM)contacto->hContact, contactn->groupId);
				else if (contacto->type == CLCIT_GROUP) { //dropee is a group
					TCHAR szNewName[120];
					TCHAR* szGroup = cli.pfnGetGroupName(contactn->groupId, NULL);
					mir_sntprintf(szNewName, SIZEOF(szNewName), _T("%s\\%s"), szGroup, contacto->szText);
					cli.pfnRenameGroup(contacto->groupId, szNewName);
				}
				break;
			}
			case DROPTARGET_INSERTION:
			{
				ClcContact *contact, *destcontact;
				ClcGroup *destgroup;
				cli.pfnGetRowByIndex(dat, dat->iDragItem, &contact, NULL);
				if (cli.pfnGetRowByIndex(dat, dat->iInsertionMark, &destcontact, &destgroup) == -1 || destgroup != contact->group->parent)
					CallService(MS_CLIST_GROUPMOVEBEFORE, contact->groupId, 0);
				else {
					if (destcontact->type == CLCIT_GROUP)
						destgroup = destcontact->group;
					else
						destgroup = destgroup;
					CallService(MS_CLIST_GROUPMOVEBEFORE, contact->groupId, destgroup->groupId);
				}
				break;
			}
			case DROPTARGET_OUTSIDE:
			{
				NMCLISTCONTROL nm;
				ClcContact *contact;
				cli.pfnGetRowByIndex(dat, dat->iDragItem, &contact, NULL);
				nm.hdr.code = CLN_DROPPED;
				nm.hdr.hwndFrom = hwnd;
				nm.hdr.idFrom = GetDlgCtrlID(hwnd);
				nm.flags = 0;
				nm.hItem = cli.pfnContactToItemHandle(contact, &nm.flags);
				nm.pt = pt;
				SendMessage(GetParent(hwnd), WM_NOTIFY, 0, (LPARAM) & nm);
				break;
			}
			default:
				{
					ClcGroup *group;
					ClcContact *contact;
					cli.pfnGetRowByIndex(dat, dat->iDragItem, &contact, &group);
					if (group->parent) {    //move to root
						if (contact->type == CLCIT_CONTACT) //dropee is a contact
							CallService(MS_CLIST_CONTACTCHANGEGROUP, (WPARAM)contact->hContact, 0);
						else if (contact->type == CLCIT_GROUP) {    //dropee is a group
							TCHAR szNewName[120];
							lstrcpyn(szNewName, contact->szText, SIZEOF(szNewName));
							cli.pfnRenameGroup(contact->groupId, szNewName);
		}	}	}	}	}

		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		dat->iDragItem = -1;
		dat->iInsertionMark = -1;
		break;

	case WM_LBUTTONDBLCLK:
	{
		ClcContact *contact;
		DWORD hitFlags;
		ReleaseCapture();
		dat->iHotTrack = -1;
		cli.pfnHideInfoTip(hwnd, dat);
		KillTimer(hwnd, TIMERID_RENAME);
		KillTimer(hwnd, TIMERID_INFOTIP);
		dat->selection = cli.pfnHitTest(hwnd, dat, (short) LOWORD(lParam), (short) HIWORD(lParam), &contact, NULL, &hitFlags);
		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		if (dat->selection != -1)
			cli.pfnEnsureVisible(hwnd, dat, dat->selection, 0);
		if ( !(hitFlags & (CLCHT_ONITEMICON | CLCHT_ONITEMLABEL)))
			break;
		UpdateWindow(hwnd);
		cli.pfnDoSelectionDefaultAction(hwnd, dat);
		dat->szQuickSearch[0] = 0;
		if (dat->filterSearch)
			cli.pfnSaveStateAndRebuildList(hwnd, dat);
		break;
	}
	case WM_CONTEXTMENU:
	{
		ClcContact *contact;
		HMENU hMenu = NULL;
		POINT pt;
		DWORD hitFlags;

		cli.pfnEndRename(hwnd, dat, 1);
		cli.pfnHideInfoTip(hwnd, dat);
		KillTimer(hwnd, TIMERID_RENAME);
		KillTimer(hwnd, TIMERID_INFOTIP);
		if (GetFocus() != hwnd)
			SetFocus(hwnd);
		dat->iHotTrack = -1;
		if (!dat->filterSearch)
			dat->szQuickSearch[0] = 0;
		pt.x = (short) LOWORD(lParam);
		pt.y = (short) HIWORD(lParam);
		if (pt.x == -1 && pt.y == -1) {
			dat->selection = cli.pfnGetRowByIndex(dat, dat->selection, &contact, NULL);
			if (dat->selection != -1)
				cli.pfnEnsureVisible(hwnd, dat, dat->selection, 0);
			pt.x = dat->iconXSpace + 15;
			pt.y = cli.pfnGetRowTopY(dat, dat->selection) - dat->yScroll + (int)(cli.pfnGetRowHeight(dat, dat->selection) * .7);
			hitFlags = dat->selection == -1 ? CLCHT_NOWHERE : CLCHT_ONITEMLABEL;
		}
		else {
			ScreenToClient(hwnd, &pt);
			dat->selection = cli.pfnHitTest(hwnd, dat, pt.x, pt.y, &contact, NULL, &hitFlags);
		}
		cli.pfnInvalidateRect(hwnd, NULL, FALSE);
		if (dat->selection != -1)
			cli.pfnEnsureVisible(hwnd, dat, dat->selection, 0);
		UpdateWindow(hwnd);

		if (dat->selection != -1 && hitFlags & (CLCHT_ONITEMICON | CLCHT_ONITEMCHECK | CLCHT_ONITEMLABEL)) {
			if (contact->type == CLCIT_GROUP) {
				hMenu = cli.pfnBuildGroupPopupMenu(contact->group);
				ClientToScreen(hwnd, &pt);
				TrackPopupMenu(hMenu, TPM_TOPALIGN | TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, NULL);
                DestroyMenu(hMenu);
				return 0;
			}
			else if (contact->type == CLCIT_CONTACT)
				hMenu = (HMENU) CallService(MS_CLIST_MENUBUILDCONTACT, (WPARAM) contact->hContact, 0);
		}
		else {
			//call parent for new group/hide offline menu
			SendMessage(GetParent(hwnd), WM_CONTEXTMENU, wParam, lParam);
		}
		if (hMenu != NULL) {
			ClientToScreen(hwnd, &pt);
			TrackPopupMenu(hMenu, TPM_TOPALIGN | TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, NULL);
			DestroyMenu(hMenu);
		}
		return 0;
	}
	case WM_MEASUREITEM:
		return CallService(MS_CLIST_MENUMEASUREITEM, wParam, lParam);

	case WM_DRAWITEM:
		return CallService(MS_CLIST_MENUDRAWITEM, wParam, lParam);

	case WM_COMMAND:
		{
		ClcContact *contact;
		int hit = cli.pfnGetRowByIndex(dat, dat->selection, &contact, NULL);
		if (hit == -1)
			break;
		if (contact->type == CLCIT_CONTACT)
			if (CallService(MS_CLIST_MENUPROCESSCOMMAND, MAKEWPARAM(LOWORD(wParam), MPCF_CONTACTMENU), (LPARAM) contact->hContact))
				break;
		switch (LOWORD(wParam)) {
		case POPUP_NEWSUBGROUP:
			if (contact->type != CLCIT_GROUP)
				break;
			SetWindowLongPtr(hwnd, GWL_STYLE, GetWindowLongPtr(hwnd, GWL_STYLE) & ~CLS_HIDEEMPTYGROUPS);
			CallService(MS_CLIST_GROUPCREATE, contact->groupId, 0);
			break;
		case POPUP_RENAMEGROUP:
			cli.pfnBeginRenameSelection(hwnd, dat);
			break;
		case POPUP_DELETEGROUP:
			if (contact->type != CLCIT_GROUP)
				break;
			CallService(MS_CLIST_GROUPDELETE, contact->groupId, 0);
			break;
		case POPUP_GROUPHIDEOFFLINE:
			if (contact->type != CLCIT_GROUP)
				break;
			CallService(MS_CLIST_GROUPSETFLAGS, contact->groupId, 
				MAKELPARAM(contact->group->hideOffline ? 0 : GROUPF_HIDEOFFLINE, GROUPF_HIDEOFFLINE));
			break;
		}
		break;
	}
	case WM_DESTROY:
		cli.pfnHideInfoTip(hwnd, dat);
		{
			int i;
			for (i=0; i <= FONTID_MAX; i++)
				if ( !dat->fontInfo[i].changed)
					DeleteObject(dat->fontInfo[i].hFont);
		}
		if (dat->himlHighlight)
			ImageList_Destroy(dat->himlHighlight);
		if (dat->hwndRenameEdit)
			DestroyWindow(dat->hwndRenameEdit);
		if ( !dat->bkChanged && dat->hBmpBackground)
			DeleteObject(dat->hBmpBackground);
		cli.pfnFreeGroup(&dat->list);
		mir_free(dat);
		cli.pfnUnregisterFileDropping(hwnd);
		WindowList_Remove(hClcWindowList, hwnd);
	}
	return DefWindowProc(hwnd, msg, wParam, lParam);
}