/*
	New Away System - plugin for Miranda IM
	Copyright (c) 2005-2007 Chervov Dmitry

    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 "Common.h"
#include "ContactList.h"
#include "Properties.h"

#define INTM_CONTACTDELETED (WM_USER + 1)
#define INTM_ICONCHANGED (WM_USER + 2)
#define INTM_INVALIDATE (WM_USER + 3)

#define HCONTACT_ISGROUP 0x80000000
#define HCONTACT_ISINFO 0xFFFF0000
#define IsHContactInfo(h) (((unsigned)(h) & HCONTACT_ISINFO) == HCONTACT_ISINFO)
#define IsHContactGroup(h) (!IsHContactInfo(h) && ((unsigned)(h) & HCONTACT_ISGROUP))
#define IsHContactContact(h) (((unsigned)(h) & HCONTACT_ISGROUP) == 0)

#define EXTRAICON_XSTEP (GetSystemMetrics(SM_CXSMICON) + 1)

static HANDLE hCLWindowList;


static int CLContactDeleted(WPARAM wParam, LPARAM lParam)
{
	WindowList_Broadcast(hCLWindowList, INTM_CONTACTDELETED, wParam, lParam);
	return 0;
}

static int CLContactIconChanged(WPARAM wParam, LPARAM lParam)
{
	WindowList_Broadcast(hCLWindowList, INTM_ICONCHANGED, wParam, lParam);
	return 0;
}

static int CLIconsChanged(WPARAM wParam, LPARAM lParam)
{
	WindowList_Broadcast(hCLWindowList, INTM_INVALIDATE, 0, 0);
	return 0;
}

void LoadCListModule()
{
	hCLWindowList = (HANDLE)CallService(MS_UTILS_ALLOCWINDOWLIST, 0, 0);
	HookEvent(ME_DB_CONTACT_DELETED, CLContactDeleted);
	HookEvent(ME_CLIST_CONTACTICONCHANGED, CLContactIconChanged);
	HookEvent(ME_SKIN_ICONSCHANGED, CLIconsChanged);
}


static LRESULT CALLBACK ParentSubclassProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
	CCList *dat = CWndUserData(hWnd).GetCList();
	switch (Msg)
	{
		case WM_NOTIFY:
		{
			LPNMHDR pnmh = (LPNMHDR)lParam;
			if (pnmh->hwndFrom == dat->hTreeView)
			{
				switch (pnmh->code)
				{
					case TVN_ITEMEXPANDED: // just set an appropriate group image
					{
						LPNMTREEVIEW pnmtv = (LPNMTREEVIEW)lParam;
						TVITEM tvItem;
						tvItem.hItem = pnmtv->itemNew.hItem;
						tvItem.mask = TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE;
						tvItem.iImage = tvItem.iSelectedImage = (pnmtv->itemNew.state & TVIS_EXPANDED) ? IMAGE_GROUPOPEN : IMAGE_GROUPSHUT;
						TreeView_SetItem(dat->hTreeView, &tvItem);
					} break;
					case TVN_SELCHANGED:
					{
						LPNMTREEVIEW pnmtv = (LPNMTREEVIEW)lParam;
						TREEITEMARRAY OldSelection = dat->SelectedItems;
						int I;
						for (I = 0; I < dat->SelectedItems.GetSize(); I++)
						{
							if (dat->SelectedItems[I] != pnmtv->itemNew.hItem)
							{
								TreeView_SetItemState(dat->hTreeView, dat->SelectedItems[I], 0, TVIS_SELECTED);
							}
						}
						dat->SelectedItems.RemoveAll();
						if (pnmtv->itemNew.hItem)
						{
							dat->SelectedItems.AddElem(pnmtv->itemNew.hItem);
							dat->SelectGroups(pnmtv->itemNew.hItem, true);
						}
						NMCLIST nm;
						nm.hdr.code = MCLN_SELCHANGED;
						nm.hdr.hwndFrom = dat->hTreeView;
						nm.hdr.idFrom = GetDlgCtrlID(dat->hTreeView);
						nm.OldSelection = &OldSelection;
						nm.NewSelection = &dat->SelectedItems;
						SendMessage(hWnd, WM_NOTIFY, 0, (LPARAM)&nm);
					} break;
					case TVN_DELETEITEM:
					{
						if (dat->Items.GetSize()) // if Items size = 0, then this TVN_DELETEITEM came after WM_DESTROY, so there is no need to do anything
						{
							LPNMTREEVIEW pnmtv = (LPNMTREEVIEW)lParam;
							TREEITEMARRAY OldSelection = dat->SelectedItems;
							int Index = dat->SelectedItems.Find(pnmtv->itemOld.hItem);
							if (Index != -1)
							{
								dat->SelectedItems.RemoveElem(Index);
							}
						// find an item to pass to SelectGroups()
							HTREEITEM hItem = TreeView_GetNextSibling(dat->hTreeView, pnmtv->itemOld.hItem);
							if (!hItem)
							{
								hItem = TreeView_GetPrevSibling(dat->hTreeView, pnmtv->itemOld.hItem);
								if (!hItem)
								{
									hItem = TreeView_GetParent(dat->hTreeView, pnmtv->itemOld.hItem);
								}
							}
							if (hItem) // if it wasn't one of the root items
							{
								dat->SelectGroups(hItem, dat->SelectedItems.Find(hItem) != -1);
							}
							NMCLIST nm;
							nm.hdr.code = MCLN_SELCHANGED;
							nm.hdr.hwndFrom = dat->hTreeView;
							nm.hdr.idFrom = GetDlgCtrlID(dat->hTreeView);
							nm.OldSelection = &OldSelection;
							nm.NewSelection = &dat->SelectedItems;
							SendMessage(hWnd, WM_NOTIFY, 0, (LPARAM)&nm);
							dat->Items[pnmtv->itemOld.lParam].hContact = INVALID_HANDLE_VALUE;
						}
					} break;
					case NM_CUSTOMDRAW:
					{
						LPNMTVCUSTOMDRAW lpNMCD = (LPNMTVCUSTOMDRAW)lParam;
						switch (lpNMCD->nmcd.dwDrawStage)
						{
							case CDDS_PREPAINT: // the control is about to start painting
							{
								return CDRF_NOTIFYITEMDRAW; // instruct the control to return information when it draws items
							} break;
							case CDDS_ITEMPREPAINT:
							{
								return CDRF_NOTIFYPOSTPAINT;
							} break;
							case CDDS_ITEMPOSTPAINT:
							{
								RECT rc;
								if (TreeView_GetItemRect(dat->hTreeView, (HTREEITEM)lpNMCD->nmcd.dwItemSpec, &rc, false))
								{
									int I;
									for (I = 0; I < MAXEXTRAICONS; I++)
									{
										BYTE nIndex = dat->Items[lpNMCD->nmcd.lItemlParam].ExtraIcons[I];
										if (nIndex != CLC_EXTRAICON_EMPTY)
										{
											ImageList_DrawEx(dat->ExtraImageList, nIndex, lpNMCD->nmcd.hdc, rc.right - EXTRAICON_XSTEP * (I + 1), rc.top, 0, 0, /*GetSysColor(COLOR_WINDOW)*/CLR_NONE, CLR_NONE, ILD_NORMAL);
										}
									}
								}
							} break;
						}
					} break;
				}
			}
		}
	}
	return CallWindowProc(dat->OrigParentProc, hWnd, Msg, wParam, lParam);
}


static LRESULT CALLBACK ContactListSubclassProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
	CCList *dat = CWndUserData(GetParent(hWnd)).GetCList();
	switch (Msg)
	{
		case INTM_CONTACTDELETED: // wParam = (HANDLE)hContact
		{
			HTREEITEM hItem = dat->FindContact((HANDLE)wParam);
			if (hItem)
			{
				TreeView_DeleteItem(hWnd, hItem);
			}
		} break;
		case INTM_ICONCHANGED: // wParam = (HANDLE)hContact, lParam = IconID
		{
			TVITEM tvi;
			tvi.hItem = dat->FindContact((HANDLE)wParam);
			if (tvi.hItem)
			{
				tvi.mask = TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE;
				tvi.iImage = tvi.iSelectedImage = lParam;
				TreeView_SetItem(hWnd, &tvi);
				dat->SortContacts();
				InvalidateRect(hWnd, NULL, false);
			}
		} break;
		case INTM_INVALIDATE:
		{
			InvalidateRect(hWnd, NULL, true);
		} break;
		case WM_RBUTTONDOWN:
		{
			SetFocus(hWnd);
			TVHITTESTINFO hitTest;
			hitTest.pt.x = (short)LOWORD(lParam);
			hitTest.pt.y = (short)HIWORD(lParam);
			TreeView_HitTest(hWnd, &hitTest);
			if (hitTest.hItem && hitTest.flags & TVHT_ONITEM)
			{
				TreeView_SelectItem(hWnd, hitTest.hItem);
			}
			return DefWindowProc(hWnd, Msg, wParam, lParam);
		} break;
		case WM_LBUTTONDOWN:
		{
			POINT pt = {(short)LOWORD(lParam), (short)HIWORD(lParam)};
			DWORD hitFlags;
			HTREEITEM hItem = dat->HitTest(&pt, &hitFlags);
			if (!hItem)
			{
				break;
			}
			if (hitFlags & MCLCHT_ONITEMICON)
			{
				if (TreeView_GetChild(hWnd, hItem)) // if it's a group, then toggle its state
				{
					NMTREEVIEW nmtv;
					nmtv.hdr.hwndFrom = hWnd;
					nmtv.hdr.idFrom = GetDlgCtrlID(hWnd);
					nmtv.hdr.code = TVN_ITEMEXPANDING;
					nmtv.action = TVE_TOGGLE;
					nmtv.itemNew.hItem = hItem;
					nmtv.itemNew.mask = TVIF_HANDLE | TVIF_STATE | TVIF_PARAM;
					TreeView_GetItem(hWnd, &nmtv.itemNew);
					nmtv.ptDrag = pt;
					if (SendMessage(GetParent(hWnd), WM_NOTIFY, 0, (LPARAM)&nmtv))
					{
						return 0;
					}
					HTREEITEM hOldSelItem = TreeView_GetSelection(hWnd);
					TreeView_Expand(hWnd, hItem, TVE_TOGGLE);
					HTREEITEM hNewSelItem = TreeView_GetSelection(hWnd);
					if (hNewSelItem != hOldSelItem)
					{
						TreeView_SetItemState(hWnd, hOldSelItem, (dat->SelectedItems.Find(hOldSelItem) == -1) ? 0 : TVIS_SELECTED, TVIS_SELECTED);
						TreeView_SetItemState(hWnd, hNewSelItem, (dat->SelectedItems.Find(hNewSelItem) == -1) ? 0 : TVIS_SELECTED, TVIS_SELECTED);
					}
					nmtv.hdr.code = TVN_ITEMEXPANDED;
					TreeView_GetItem(hWnd, &nmtv.itemNew);
					SendMessage(GetParent(hWnd), WM_NOTIFY, 0, (LPARAM)&nmtv);
					return 0;
				}
			}
			if (hitFlags & MCLCHT_ONITEM)
			{
				if (wParam & MK_CONTROL)
				{
					SetFocus(hWnd);
					TREEITEMARRAY OldSelection = dat->SelectedItems;
					int nIndex = dat->SelectedItems.Find(hItem);
					if (nIndex == -1)
					{
						TreeView_SetItemState(hWnd, hItem, TVIS_SELECTED, TVIS_SELECTED);
						dat->SelectedItems.AddElem(hItem);
					} else
					{
						TreeView_SetItemState(hWnd, hItem, 0, TVIS_SELECTED);
						dat->SelectedItems.RemoveElem(nIndex);
					}
					dat->SelectGroups(hItem, nIndex == -1);
					NMCLIST nm;
					nm.hdr.code = MCLN_SELCHANGED;
					nm.hdr.hwndFrom = hWnd;
					nm.hdr.idFrom = GetDlgCtrlID(hWnd);
					nm.OldSelection = &OldSelection;
					nm.NewSelection = &dat->SelectedItems;
					SendMessage(GetParent(hWnd), WM_NOTIFY, 0, (LPARAM)&nm);
					return 0;
				} else
				{
					if (hItem == TreeView_GetSelection(hWnd) && (dat->SelectedItems.GetSize() != 1 || (dat->SelectedItems.GetSize() == 1 && dat->SelectedItems[0] != hItem))) // if it was a click on the selected item and there's need to do something in this case, then send SELCHANGED notification by ourselves, as the tree control doesn't do anything
					{
						TreeView_SetItemState(hWnd, hItem, TVIS_SELECTED, TVIS_SELECTED);
						NMTREEVIEW nm = {0};
						nm.hdr.code = TVN_SELCHANGED;
						nm.hdr.hwndFrom = hWnd;
						nm.hdr.idFrom = GetDlgCtrlID(hWnd);
						nm.itemOld.hItem = TreeView_GetSelection(hWnd);
						nm.itemOld.mask = TVIF_HANDLE | TVIF_STATE | TVIF_PARAM;
						TreeView_GetItem(hWnd, &nm.itemOld);
						nm.itemNew = nm.itemOld;
						SendMessage(GetParent(hWnd), WM_NOTIFY, 0, (LPARAM)&nm);
					}
				}
			}
		} break;
		case WM_SETFOCUS:
		case WM_KILLFOCUS:
		{
			int I;
			for (I = 0; I < dat->SelectedItems.GetSize(); I++)
			{
				RECT rc;
				if (TreeView_GetItemRect(hWnd, dat->SelectedItems[I], &rc, false))
				{
					InvalidateRect(hWnd, &rc, false);
				}
			}
		} break;
		case WM_SIZE:
		case WM_HSCROLL:
		{
			InvalidateRect(hWnd, NULL, false);
		} break;
		case WM_MEASUREITEM:
		{
			if (!wParam) // if the message was sent by a menu
			{
				return CallService(MS_CLIST_MENUMEASUREITEM, wParam, lParam);
			}
		} break;
		case WM_DRAWITEM:
		{
			if (!wParam) // if the message was sent by a menu
			{
				return CallService(MS_CLIST_MENUDRAWITEM, wParam, lParam);
			}
		} break;
		case WM_CONTEXTMENU:
		{
			POINT pt;
			pt.x = (short)LOWORD(lParam);
			pt.y = (short)HIWORD(lParam);
			HTREEITEM hItem = NULL;
			if (pt.x == -1 && pt.y == -1)
			{
				if (dat->SelectedItems.GetSize() == 1)
				{
					hItem = dat->SelectedItems[0];
					TreeView_EnsureVisible(hWnd, hItem);
					RECT rc;
					TreeView_GetItemRect(hWnd, hItem, &rc, true);
					pt.x = rc.left;
					pt.y = rc.bottom;
				}
			} else
			{
				DWORD hitFlags;
				ScreenToClient(hWnd, &pt);
				hItem = dat->HitTest(&pt, &hitFlags);
				if (!(hitFlags & MCLCHT_ONITEM))
				{
					hItem = NULL;
				}
			}
			if (hItem)
			{
				HANDLE hContact = dat->GetItemData(hItem).hContact;
				if (IsHContactContact(hContact))
				{
					HMENU hMenu = (HMENU)CallService(MS_CLIST_MENUBUILDCONTACT, (WPARAM)hContact, 0);
					if (hMenu)
					{
						ClientToScreen(hWnd, &pt);
						CallService(MS_CLIST_MENUPROCESSCOMMAND, MAKEWPARAM(TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD, pt.x, pt.y, 0, hWnd, NULL), MPCF_CONTACTMENU), (LPARAM)hContact);
						DestroyMenu(hMenu);
						return 0;
					}
				}
			}
		} break;
		case WM_DESTROY:
		{
			if (dat->ExtraImageList)
			{
				ImageList_Destroy(dat->ExtraImageList);
			}
			dat->SelectedItems.RemoveAll();
			dat->Items.RemoveAll();
		} break;
	}
	return CallWindowProc(dat->OrigTreeViewProc, hWnd, Msg, wParam, lParam);
}


CCList::CCList(HWND hTreeView): hTreeView(hTreeView), ExtraImageList(NULL)
{
	CWndUserData(GetParent(hTreeView)).SetCList(this);
	OrigTreeViewProc = (WNDPROC)SetWindowLongPtr(hTreeView, GWLP_WNDPROC, (LONG_PTR)ContactListSubclassProc);
	OrigParentProc = (WNDPROC)SetWindowLongPtr(GetParent(hTreeView), GWLP_WNDPROC, (LONG_PTR)ParentSubclassProc);
	TreeView_SetImageList(hTreeView, CallService(MS_CLIST_GETICONSIMAGELIST, 0, 0), TVSIL_NORMAL);
	WindowList_Add(hCLWindowList, hTreeView, NULL);
	TreeView_SetIndent(hTreeView, 5); // doesn't set it less than the initial value on my system, and I guess it's because of icons... but who knows - maybe it will work somewhere
}


CCList::~CCList()
{
	WindowList_Remove(hCLWindowList, hTreeView);
	_ASSERT(GetWindowLongPtr(GetParent(hTreeView), GWLP_WNDPROC) == (LONG_PTR)ParentSubclassProc); // we won't allow anyone to change our WNDPROC. otherwise we're not sure that we're setting the right WNDPROC back
	SetWindowLongPtr(hTreeView, GWLP_WNDPROC, (LONG_PTR)OrigTreeViewProc);
	SetWindowLongPtr(GetParent(hTreeView), GWLP_WNDPROC, (LONG_PTR)OrigParentProc);
	CWndUserData(GetParent(hTreeView)).SetCList(NULL);
}


HTREEITEM CCList::AddContact(HANDLE hContact)
// adds a new contact if it doesn't exist yet; returns its hItem
{
	_ASSERT(IsHContactContact(hContact));
	HTREEITEM hContactItem = FindContact(hContact);
	if (hContactItem)
	{
		return hContactItem;
	}
	TVINSERTSTRUCT tvIns;
	ZeroMemory(&tvIns, sizeof(tvIns));
	tvIns.hParent = AddGroup(DBGetContactSettingString(hContact, "CList", "Group", _T("")));
/*	if (!tvIns.hParent)
	{
		return NULL;
	}*/ // <- place hidden contacts in the root anyway, as otherwise we won't see icq contacts that are hidden beneath metacontacts; TODO: show metacontacts as groups??
	tvIns.item.pszText = (TCHAR*)CallService(MS_CLIST_GETCONTACTDISPLAYNAME, (WPARAM)hContact, GCDNF_TCHAR);
	tvIns.hInsertAfter = TVI_ROOT;
	tvIns.item.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM;
	tvIns.item.iImage = tvIns.item.iSelectedImage = CallService(MS_CLIST_GETCONTACTICON, (WPARAM)hContact, 0);
	tvIns.item.lParam = Items.AddElem(CCLItemData(hContact));
	return TreeView_InsertItem(hTreeView, &tvIns);
}


typedef struct
{
	HANDLE hGroup;
	TCString GroupName;
} sGroupEnumData;

int GroupEnum(const char *szSetting, LPARAM lParam)
{
	sGroupEnumData *GroupEnumData = (sGroupEnumData*)lParam;
	TCString GroupName = DBGetContactSettingString(NULL, "CListGroups", szSetting, _T(" "));
	if (!lstrcmp(GroupEnumData->GroupName, &GroupName[1]))
	{
		GroupEnumData->hGroup = (HANDLE)(atol(szSetting) | HCONTACT_ISGROUP);
	}
	return 0;
}

HTREEITEM CCList::AddGroup(TCString GroupName)
// adds a new group if it doesn't exist yet; returns its hItem
{
	if (GroupName == _T(""))
	{
		return TVI_ROOT;
	}
	sGroupEnumData GroupEnumData;
	GroupEnumData.GroupName = GroupName;
	GroupEnumData.hGroup = NULL;
	DBCONTACTENUMSETTINGS dbEnum;
	ZeroMemory(&dbEnum, sizeof(dbEnum));
	dbEnum.lParam = (LPARAM)&GroupEnumData;
	dbEnum.pfnEnumProc = GroupEnum;
	dbEnum.szModule = "CListGroups";
	CallService(MS_DB_CONTACT_ENUMSETTINGS, NULL, (LPARAM)&dbEnum);
	if (!GroupEnumData.hGroup) // means there is no such group in the groups list
	{
		return NULL;
	}
	HTREEITEM hGroupItem = FindContact(GroupEnumData.hGroup);
	if (hGroupItem)
	{
		return hGroupItem; // exists already, just return its handle
	}
	TVINSERTSTRUCT tvIns = {0};
	tvIns.hParent = TVI_ROOT;
	tvIns.item.pszText = _tcsrchr(GroupName, '\\');
	if (tvIns.item.pszText)
	{
		TCString ParentGroupName(_T(""));
		tvIns.hParent = AddGroup(ParentGroupName.DiffCat(GroupName, tvIns.item.pszText));
		tvIns.item.pszText++;
	} else
	{
		tvIns.item.pszText = GroupName;
	}
	tvIns.hInsertAfter = TVI_ROOT;
	tvIns.item.mask = TVIF_TEXT | TVIF_STATE | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM;
	tvIns.item.state = tvIns.item.stateMask = TVIS_BOLD | TVIS_EXPANDED;
	tvIns.item.iImage = tvIns.item.iSelectedImage = IMAGE_GROUPOPEN;
	tvIns.item.lParam = Items.AddElem(CCLItemData(GroupEnumData.hGroup));
	return TreeView_InsertItem(hTreeView, &tvIns);
}


HTREEITEM CCList::AddInfo(TCString Title, HTREEITEM hParent, HTREEITEM hInsertAfter, LPARAM lParam, HICON hIcon)
{
	TVINSERTSTRUCT tvi = {0};
	tvi.item.mask = TVIF_TEXT | TVIF_STATE | TVIF_PARAM;
	tvi.item.pszText = Title;
	tvi.hParent = hParent;
	tvi.hInsertAfter = hInsertAfter;
	tvi.item.lParam = Items.AddElem(CCLItemData());
	Items[tvi.item.lParam].lParam = lParam;
	tvi.item.state = tvi.item.stateMask = TVIS_BOLD | TVIS_EXPANDED;
	if (hIcon)
	{
		HIMAGELIST iml = TreeView_GetImageList(hTreeView, TVSIL_NORMAL);
		tvi.item.mask |= TVIF_IMAGE | TVIF_SELECTEDIMAGE;
		tvi.item.iImage = tvi.item.iSelectedImage = ImageList_AddIcon(iml, hIcon); // we don't check for duplicate icons, but I think that's ok, judging that the check will require some pretty significant amount of additional coding
		TreeView_SetImageList(hTreeView, iml, TVSIL_NORMAL);
	}
	return TreeView_InsertItem(hTreeView, &tvi);
}


void CCList::SetInfoIcon(HTREEITEM hItem, HICON hIcon)
{
	_ASSERT(hItem && hIcon && GetItemType(hItem) == MCLCIT_INFO);
	TVITEM tvi = {0};
	tvi.mask = TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE;
	tvi.hItem = hItem;
	HIMAGELIST iml = TreeView_GetImageList(hTreeView, TVSIL_NORMAL);
	tvi.iImage = tvi.iSelectedImage = ImageList_AddIcon(iml, hIcon); // again, we don't check for duplicate icons
	TreeView_SetImageList(hTreeView, iml, TVSIL_NORMAL);
	TreeView_SetItem(hTreeView, &tvi);
}


static int CALLBACK CompareItemsCallback(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
	CCList *dat = (CCList*)lParamSort;
	if (IsHContactInfo(dat->Items[lParam1].hContact)) // Info items precede all other items
	{
		return (IsHContactInfo(dat->Items[lParam2].hContact)) ? 0 : -1;
	} else if (IsHContactInfo(dat->Items[lParam2].hContact))
	{
		return 1;
	}
	if (IsHContactGroup(dat->Items[lParam1].hContact)) // groups precede contacts
	{
		if (IsHContactGroup(dat->Items[lParam2].hContact))
		{
			return (unsigned)dat->Items[lParam1].hContact - (unsigned)dat->Items[lParam2].hContact;
		} else
		{
			return -1;
		}
	} else if (IsHContactGroup(dat->Items[lParam2].hContact))
	{
		return 1;
	}
	return CallService(MS_CLIST_CONTACTSCOMPARE, (WPARAM)dat->Items[lParam1].hContact, (LPARAM)dat->Items[lParam2].hContact);
}

void CCList::SortContacts()
{
	TVSORTCB tvSort;
	ZeroMemory(&tvSort, sizeof(tvSort));
	tvSort.lpfnCompare = CompareItemsCallback;
	tvSort.hParent = TVI_ROOT;
	tvSort.lParam = (LPARAM)this;
	while (tvSort.hParent)
	{
		TreeView_SortChildrenCB(hTreeView, &tvSort, 0);
		tvSort.hParent = GetNextItem(MCLGN_NEXT | MCLGN_GROUP | MCLGN_MULTILEVEL, tvSort.hParent);
	}
}


int CCList::GetExtraImage(HTREEITEM hItem, int iColumn) // returns iImage, or CLC_EXTRAICON_EMPTY
{
	_ASSERT(iColumn < MAXEXTRAICONS);
	return GetItemData(hItem).ExtraIcons[iColumn];
}


void CCList::SetExtraImage(HTREEITEM hItem, int iColumn, int iImage) // set iImage to CLC_EXTRAICON_EMPTY to reset image
{
	_ASSERT(iColumn < MAXEXTRAICONS);
	GetItemData(hItem).ExtraIcons[iColumn] = iImage;
	RECT rc;
	if (TreeView_GetItemRect(hTreeView, hItem, &rc, false))
	{
		InvalidateRect(hTreeView, &rc, true);
	}
}


void CCList::SetExtraImageList(HIMAGELIST hImgList)
{
	ExtraImageList = hImgList;
	InvalidateRect(hTreeView, NULL, false);
}


int CCList::GetItemType(HTREEITEM hItem) // returns a MCLCIT_ (see below)
{
	HANDLE hContact = GetItemData(hItem).hContact;
	return (IsHContactInfo(hContact)) ? MCLCIT_INFO : ((IsHContactGroup(hContact)) ? MCLCIT_GROUP : MCLCIT_CONTACT);
}


DWORD CCList::GetItemTypeAsCLGNFlag(HTREEITEM hItem)
{
	HANDLE hContact = GetItemData(hItem).hContact;
	return (IsHContactInfo(hContact)) ? MCLGN_INFO : ((IsHContactGroup(hContact)) ? MCLGN_GROUP : MCLGN_CONTACT);
}


HTREEITEM CCList::GetNextItem(DWORD Flags, HTREEITEM hItem)
{
	switch (Flags & ~(MCLGN_MULTILEVEL | MCLGN_NOTCHILD | MCLGN_ANY))
	{
		case MCLGN_ROOT:
		{
			return TreeView_GetRoot(hTreeView);
		} break;
		case MCLGN_LAST:
		{
			HTREEITEM hNextItem = TVI_ROOT;
			do
			{
				hItem = hNextItem;
				hNextItem = TreeView_GetLastChild(hTreeView, hNextItem);
			} while (hNextItem);
			return (hItem == TVI_ROOT) ? NULL : hItem;
		} break;
		case MCLGN_CHILD:
		{
			return TreeView_GetChild(hTreeView, hItem);
		} break;
		case MCLGN_LASTCHILD:
		{
			return TreeView_GetLastChild(hTreeView, hItem);
		} break;
		case MCLGN_PARENT:
		{
			return TreeView_GetParent(hTreeView, hItem);
		} break;
		case MCLGN_NEXT:
		{
			do
			{
				if (Flags & MCLGN_MULTILEVEL)
				{
					HTREEITEM hNextItem = NULL;
					if ((Flags & MCLGN_NOTCHILD) != MCLGN_NOTCHILD)
					{
						hNextItem = TreeView_GetChild(hTreeView, hItem);
					}
					if (!hNextItem)
					{
						hNextItem = TreeView_GetNextSibling(hTreeView, hItem);
						while (!hNextItem) // move back until we find next sibling of the item or one of its parents
						{
							hItem = TreeView_GetParent(hTreeView, hItem);
							if (!hItem) // means it was the root, there are no items left.
							{
								break; // returns NULL as the next item
							}
							hNextItem = TreeView_GetNextSibling(hTreeView, hItem);
						}
					}
					hItem = hNextItem;
				} else
				{
					hItem = TreeView_GetNextSibling(hTreeView, hItem);
				}
				Flags &= ~(MCLGN_NOTCHILD & ~MCLGN_MULTILEVEL); // clear MCLGN_NOTCHILD flag
			} while (hItem && !(Flags & GetItemTypeAsCLGNFlag(hItem)));
			return hItem;
		} break;
		case MCLGN_PREV:
		{
			do
			{
				if (Flags & MCLGN_MULTILEVEL)
				{
					HTREEITEM hNextItem = TreeView_GetPrevSibling(hTreeView, hItem);
					if (hNextItem)
					{
						if ((Flags & MCLGN_NOTCHILD) != MCLGN_NOTCHILD)
						{
							while (hNextItem)
							{
								hItem = hNextItem;
								hNextItem = TreeView_GetLastChild(hTreeView, hItem);
							}
						} else
						{
							hItem = hNextItem;
						}
					} else
					{
						hItem = TreeView_GetParent(hTreeView, hItem);
					}
				} else
				{
					hItem = TreeView_GetPrevSibling(hTreeView, hItem);
				}
				Flags &= ~(MCLGN_NOTCHILD & ~MCLGN_MULTILEVEL); // clear MCLGN_NOTCHILD flag
			} while (hItem && !(Flags & GetItemTypeAsCLGNFlag(hItem)));
			return hItem;
		} break;
		default:
		{
			_ASSERT(0);
		} break;
	}
	return NULL;
}


HANDLE CCList::GethContact(HTREEITEM hItem) // returns hContact, hGroup or hInfo
{
	HANDLE hContact = GetItemData(hItem).hContact;
	if (IsHContactContact(hContact))
	{
		return hContact;
	} else if (IsHContactGroup(hContact))
	{
		return (HANDLE)((int)hContact & ~HCONTACT_ISGROUP);
	} else
	{
		return (HANDLE)((int)hContact & ~HCONTACT_ISINFO);
	}
}


HTREEITEM CCList::HitTest(LPPOINT pt, PDWORD hitFlags) // pt is relative to control; returns hItem or NULL
{
	TVHITTESTINFO hti;
	hti.pt = *pt;
	TreeView_HitTest(hTreeView, &hti);
	*hitFlags = 0;
	if (hti.flags & TVHT_ABOVE)
	{
		*hitFlags |= MCLCHT_ABOVE;
	}
	if (hti.flags & TVHT_BELOW)
	{
		*hitFlags |= MCLCHT_BELOW;
	}
	if (hti.flags & TVHT_TOLEFT)
	{
		*hitFlags |= MCLCHT_TOLEFT;
	}
	if (hti.flags & TVHT_TORIGHT)
	{
		*hitFlags |= MCLCHT_TORIGHT;
	}
	if (hti.flags & TVHT_NOWHERE)
	{
		*hitFlags |= MCLCHT_NOWHERE;
	}
	if (hti.flags & TVHT_ONITEMINDENT)
	{
		*hitFlags |= MCLCHT_ONITEMINDENT;
	}
	if (hti.flags & (TVHT_ONITEMICON | TVHT_ONITEMSTATEICON))
	{
		*hitFlags |= MCLCHT_ONITEMICON;
	}
	if (hti.flags & TVHT_ONITEMLABEL)
	{
		*hitFlags |= MCLCHT_ONITEMLABEL;
	}
	if (hti.flags & TVHT_ONITEMRIGHT)
	{
		*hitFlags |= MCLCHT_ONITEMRIGHT;
	}
	if (hti.flags & (TVHT_ONITEMINDENT | TVHT_ONITEM | TVHT_ONITEMRIGHT))
	{
	// extraicon tests
		RECT rc;
		if (TreeView_GetItemRect(hTreeView, hti.hItem, &rc, false))
		{
			int nIndex = (rc.right - pt->x - 1) / EXTRAICON_XSTEP;
			if (nIndex >= 0 && nIndex < MAXEXTRAICONS && GetItemData(hti.hItem).ExtraIcons[nIndex] != CLC_EXTRAICON_EMPTY)
			{
				*hitFlags |= MCLCHT_ONITEMEXTRA | (nIndex << 24);
			}
		}
	}
	return hti.hItem;
}


int CCList::Array_SetItemState(HTREEITEM hItem, bool bSelected)
{
	_ASSERT(hItem);
	int nIndex = SelectedItems.Find(hItem);
	if (nIndex == -1 && bSelected)
	{
		return SelectedItems.AddElem(hItem);
	} else if (nIndex != -1 && !bSelected)
	{
		SelectedItems.RemoveElem(nIndex);
		return -1;
	}
	return nIndex;
}


CCLItemData& CCList::GetItemData(HTREEITEM hItem)
{
	_ASSERT(hItem && hItem != INVALID_HANDLE_VALUE);
	TVITEM tvi;
	tvi.mask = TVIF_HANDLE | TVIF_PARAM;
	tvi.hItem = hItem;
	int Res = TreeView_GetItem(hTreeView, &tvi);
	_ASSERT(Res);
	return Items[tvi.lParam];
}


HTREEITEM CCList::TreeView_GetLastChild(HWND hTreeView, HTREEITEM hItem)
{
	HTREEITEM hPrevItem = TreeView_GetChild(hTreeView, hItem);
	hItem = hPrevItem;
	while (hItem) // find last sibling
	{
		hPrevItem = hItem;
		hItem = TreeView_GetNextSibling(hTreeView, hPrevItem);
	}
	return hPrevItem;
}


HTREEITEM CCList::FindContact(HANDLE hContact)
{
	TVITEM tvi;
	tvi.mask = TVIF_HANDLE | TVIF_PARAM;
	tvi.hItem = TreeView_GetRoot(hTreeView);
	while (tvi.hItem)
	{
		TreeView_GetItem(hTreeView, &tvi);
		if (Items[tvi.lParam].hContact == hContact)
		{
			return tvi.hItem;
		}
		tvi.hItem = GetNextItem(MCLGN_NEXT | MCLGN_ANY | MCLGN_MULTILEVEL, tvi.hItem);
	}
	return NULL;
}


void CCList::SelectGroups(HTREEITEM hCurItem, bool bSelected)
{
// select/deselect all child items
	HTREEITEM hItem = TreeView_GetChild(hTreeView, hCurItem);
	HTREEITEM hLimitItem = GetNextItem(MCLGN_NEXT | MCLGN_ANY | MCLGN_NOTCHILD, hCurItem);
	while (hItem && hItem != hLimitItem)
	{
		TreeView_SetItemState(hTreeView, hItem, bSelected ? TVIS_SELECTED : 0, TVIS_SELECTED);
		Array_SetItemState(hItem, bSelected);
		hItem = GetNextItem(MCLGN_NEXT | MCLGN_ANY | MCLGN_MULTILEVEL, hItem);
	}
// select/deselect all parent groups
	hCurItem = TreeView_GetParent(hTreeView, hCurItem);
	if (bSelected)
	{
		while (hCurItem) // select until we'll find an unselected item or until we'll reach the root
		{
			hItem = TreeView_GetChild(hTreeView, hCurItem);
			while (hItem) // walk through all siblings
			{
				if (!(TreeView_GetItemState(hTreeView, hItem, TVIS_SELECTED) & TVIS_SELECTED))
				{
					break;
				}
				hItem = TreeView_GetNextSibling(hTreeView, hItem);
			}
			if (hItem) // means there was at least one unselected item
			{
				break;
			}
			TreeView_SetItemState(hTreeView, hCurItem, TVIS_SELECTED, TVIS_SELECTED);
			Array_SetItemState(hCurItem, true);
			hCurItem = TreeView_GetParent(hTreeView, hCurItem);
		}
	}
	while (hCurItem) // and deselect all remaining parent groups
	{
		TreeView_SetItemState(hTreeView, hCurItem, 0, TVIS_SELECTED);
		Array_SetItemState(hCurItem, false);
		hCurItem = TreeView_GetParent(hTreeView, hCurItem);
	}
}