/*
	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 "stdafx.h"
#include "ContactList.h"
#include "Properties.h"

#define EXTRAICON_XSTEP (GetSystemMetrics(SM_CXSMICON) + 1)

static MWindowList 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, LPARAM)
{
	WindowList_Broadcast(hCLWindowList, INTM_INVALIDATE, 0, 0);
	return 0;
}

void LoadCListModule()
{
	hCLWindowList = WindowList_Create();
	HookEvent(ME_DB_CONTACT_DELETED, CLContactDeleted);
	HookEvent(ME_CLIST_CONTACTICONCHANGED, CLContactIconChanged);
	HookEvent(ME_SKIN_ICONSCHANGED, CLIconsChanged);
}

LRESULT CALLBACK CCList::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;
						for (int 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_CONTACT_ID;
					}
					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
					case CDDS_ITEMPREPAINT:
						return CDRF_NOTIFYPOSTPAINT;
					case CDDS_ITEMPOSTPAINT:
						RECT rc;
						if (TreeView_GetItemRect(dat->hTreeView, (HTREEITEM)lpNMCD->nmcd.dwItemSpec, &rc, false)) {
							for (int i = 0; i < MAXEXTRAICONS; i++) {
								uint8_t 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);
}

LRESULT CALLBACK ContactListSubclassProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
	TVITEM tvi;
	CCList *dat = CWndUserData(GetParent(hWnd)).GetCList();

	switch (Msg) {
	case INTM_CONTACTDELETED: // wParam = (HANDLE)hContact
		{
			HTREEITEM hItem = dat->FindContact(wParam);
			if (hItem)
				TreeView_DeleteItem(hWnd, hItem);
		}
		break;

	case INTM_ICONCHANGED: // wParam = (HANDLE)hContact, lParam = IconID
		tvi.hItem = dat->FindContact(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, nullptr, false);
		}
		break;

	case INTM_INVALIDATE:
		InvalidateRect(hWnd, nullptr, 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);

	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;
				}
				// 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
				if (hItem == TreeView_GetSelection(hWnd) && (dat->SelectedItems.GetSize() != 1 || (dat->SelectedItems.GetSize() == 1 && dat->SelectedItems[0] != hItem))) {
					TreeView_SetItemState(hWnd, hItem, TVIS_SELECTED, TVIS_SELECTED);
					NMTREEVIEW nm = {};
					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:
		for (int 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, nullptr, false);
		break;

	case WM_MEASUREITEM:
		if (!wParam) // if the message was sent by a menu
			return Menu_MeasureItem(lParam);
		break;

	case WM_DRAWITEM:
		if (!wParam) // if the message was sent by a menu
			return Menu_DrawItem(lParam);
		break;

	case WM_CONTEXTMENU:
		{
			POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
			HTREEITEM hItem = nullptr;
			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 = nullptr;
			}
			if (hItem) {
				MCONTACT hContact = dat->GetItemData(hItem).hContact;
				if (IsHContactContact(hContact)) {
					HMENU hMenu = Menu_BuildContactMenu(hContact);
					if (hMenu) {
						ClientToScreen(hWnd, &pt);
						Clist_MenuProcessCommand(TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD, pt.x, pt.y, 0, hWnd, nullptr), MPCF_CONTACTMENU, 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(nullptr)
{
	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, Clist_GetImageList(), 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(nullptr);
}

// adds a new contact if it doesn't exist yet; returns its hItem
HTREEITEM CCList::AddContact(MCONTACT hContact)
{
	_ASSERT(IsHContactContact(hContact));
	HTREEITEM hContactItem = FindContact(hContact);
	if (hContactItem)
		return hContactItem;

	TVINSERTSTRUCT tvIns;
	memset(&tvIns, 0, sizeof(tvIns));
	tvIns.hParent = AddGroup(db_get_s(hContact, "CList", "Group", L""));
	tvIns.item.pszText = Clist_GetContactDisplayName(hContact);
	tvIns.hInsertAfter = TVI_ROOT;
	tvIns.item.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM;
	tvIns.item.iImage = tvIns.item.iSelectedImage = Clist_GetContactIcon(hContact);
	tvIns.item.lParam = Items.AddElem(CCLItemData(hContact));
	return TreeView_InsertItem(hTreeView, &tvIns);
}

// adds a new group if it doesn't exist yet; returns its hItem
HTREEITEM CCList::AddGroup(TCString GroupName)
{
	if (GroupName == L"")
		return TVI_ROOT;

	MGROUP hGroupId = Clist_GroupExists(GroupName);
	if (hGroupId == NULL)
		return nullptr;

	MCONTACT hContact = UINT_PTR(hGroupId) - 1 + HCONTACT_ISGROUP;
	HTREEITEM hGroupItem = FindContact(hContact);
	if (hGroupItem)
		return hGroupItem; // exists already, just return its handle

	TVINSERTSTRUCT tvIns = {};
	tvIns.hParent = TVI_ROOT;
	tvIns.item.pszText = wcsrchr(GroupName, '\\');
	if (tvIns.item.pszText) {
		TCString ParentGroupName(L"");
		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(hContact));
	return TreeView_InsertItem(hTreeView, &tvIns);
}

HTREEITEM CCList::AddInfo(TCString Title, HTREEITEM hParent, HTREEITEM hInsertAfter, LPARAM lParam, HICON hIcon)
{
	TVINSERTSTRUCT tvi = {};
	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);
}

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;

	if (IsHContactInfo(dat->Items[lParam2].hContact))
		return 1;

	if (IsHContactGroup(dat->Items[lParam1].hContact)) { // groups precedes contacts
		if (IsHContactGroup(dat->Items[lParam2].hContact))
			return (unsigned)dat->Items[lParam1].hContact - (unsigned)dat->Items[lParam2].hContact;
		return -1;
	}

	if (IsHContactGroup(dat->Items[lParam2].hContact))
		return 1;
	
	return Clist_ContactCompare(dat->Items[lParam1].hContact, dat->Items[lParam2].hContact);
}

void CCList::SortContacts()
{
	TVSORTCB 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, nullptr, false);
}

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

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

HTREEITEM CCList::GetNextItem(uint32_t Flags, HTREEITEM hItem)
{
	switch (Flags & ~(MCLGN_MULTILEVEL | MCLGN_NOTCHILD | MCLGN_ANY)) {
	case MCLGN_ROOT:
		return TreeView_GetRoot(hTreeView);

	case MCLGN_LAST:
		{
			HTREEITEM hNextItem = TVI_ROOT;
			do {
				hItem = hNextItem;
				hNextItem = TreeView_GetLastChild(hTreeView, hNextItem);
			} while (hNextItem);
			return (hItem == TVI_ROOT) ? nullptr : hItem;
		}

	case MCLGN_CHILD:
		return TreeView_GetChild(hTreeView, hItem);

	case MCLGN_LASTCHILD:
		return TreeView_GetLastChild(hTreeView, hItem);

	case MCLGN_PARENT:
		return TreeView_GetParent(hTreeView, hItem);

	case MCLGN_NEXT:
		do {
			if (Flags & MCLGN_MULTILEVEL) {
				HTREEITEM hNextItem = nullptr;
				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;

	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;

	default:
		_ASSERT(0);
	}
	return nullptr;
}

MCONTACT CCList::GethContact(HTREEITEM hItem) // returns hContact, hGroup or hInfo
{
	MCONTACT hContact = GetItemData(hItem).hContact;
	if (IsHContactContact(hContact))
		return hContact;
	if (IsHContactGroup(hContact))
		return hContact & ~HCONTACT_ISGROUP;
	return 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);

	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 hTree, HTREEITEM hItem)
{
	HTREEITEM hPrevItem = TreeView_GetChild(hTree, hItem);
	hItem = hPrevItem;
	while (hItem) { // find last sibling
		hPrevItem = hItem;
		hItem = TreeView_GetNextSibling(hTree, hPrevItem);
	}
	return hPrevItem;
}

HTREEITEM CCList::FindContact(MCONTACT 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 nullptr;
}

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);
	}
}