/*

Miranda NG: the free IM client for Microsoft* Windows*

Copyright (�) 2012-15 Miranda NG project (http://miranda-ng.org),
Copyright (c) 2000-08 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.
*/

// File contains implementation of animated avatars in contact list

#include "stdafx.h"

#define IMMEDIATE_DRAW (!s_bSeparateWindow)

void GDIPlus_ExtractAnimatedGIF(TCHAR *szName, int width, int height, HBITMAP &pBmp, int* &pframesDelay, int &pframesCount, SIZE &sizeAvatar);
BOOL GDIPlus_IsAnimatedGif(TCHAR *szName);

/* Next is module */
#define ANIAVAWINDOWCLASS _T("MirandaModernAniAvatar")
#define aacheck if (!s_bModuleStarted) return

#define AAO_HAS_BORDER	  0x01
#define AAO_ROUND_CORNERS 0x02
#define AAO_HAS_OVERLAY	  0x04
#define AAO_OPAQUE		  0x08

//messages
enum {
	AAM_FIRST = WM_USER,
	AAM_SETAVATAR,					//sync		WPARAM: TCHAR * filename, LPARAM: SIZE * size, RESULT: actual size
	AAM_SETPOSITION,				//async		LPARAM: pointer to set pos info - the handler will empty it, RESULT: 0
	AAM_REDRAW,						//async
	AAM_STOP,						//async		stops animation, timer, hide window - prepeare for deleting
	AAM_PAUSE,						//sync		keep timer and window, but do not process painting -need before graphics change
	AAM_RESUME,						//async		remobe previous flag. repaints if required
	AAM_REMOVEAVATAR,				//sync		WPARAM: if y more then wParam, LPARAM: shift up to lParam( remove if values is same)
	AAM_SETPARENT,					//async	    WPARAM: handle of new parent window
	AAM_RENDER,						//sync
	AAM_LAST,
};

struct ANIAVA_OBJECT : public MZeroedObject
{
	MCONTACT hContact;
	HWND   hWindow;
	BOOL   bInvalidPos;
	DWORD  dwAvatarUniqId;
	SIZE   ObjectSize;

	~ANIAVA_OBJECT()
	{
		if (hWindow)
			DestroyWindow(hWindow);
	}
};

struct ANIAVA_INFO
{
	DWORD  dwAvatarUniqId;
	TCHAR *tcsFilename;
	int    nRefCount;
	int    nStripTop;
	int    nFrameCount;
	int   *pFrameDelays;
	SIZE   FrameSize;
};

struct ANIAVA_WINDOWINFO
{
	HWND  hWindow;
	RECT  rcPos;
	SIZE  sizeAvatar;
	BOOL  StopTimer;
	int   TimerId;
	int   nFramesCount;
	int  *delaysInterval;
	int   currentFrame;

	POINT ptFromPoint;

	BOOL  bPlaying;
	int   overlayIconIdx;
	BYTE  bAlpha;
	BOOL  bOrderTop;

	BOOL  bPaused;			// was request do not draw
	BOOL  bPended;			// till do not draw - was painting - need to be repaint
};

struct ANIAVA_POSINFO
{
	RECT rcPos;
	int  idxOverlay;
	BYTE bAlpha;
};

struct ANIAVA_SYNCCALLITEM
{
	WPARAM  wParam;
	LPARAM  lParam;
	INT_PTR nResult;
	HANDLE  hDoneEvent;
	PSYNCCALLBACKPROC pfnProc;
};

struct ANIAVATARIMAGEINFO
{
	POINT ptImagePos;
	int	nFramesCount;
	int  *pFrameDelays;
	SIZE	szSize;
};

// protection
static BOOL s_bModuleStarted;
static mir_cs s_CS;

// options
static BYTE s_bFlags;				// 0x1 has border, 0x2 has round corners, 0x4 has overlay, 0x8 background color
static BYTE s_cornerRadius;
static COLORREF s_borderColor;
static COLORREF s_bkgColor;
static HIMAGELIST s_overlayIconImageList;

// animations
static HDC     s_hAniAvaDC;
static HBITMAP s_hAniAvaBitmap;
static HBITMAP s_hAniAvaOldBitmap;
static int     s_width, s_height;
static UINT    s_AnimationThreadID;
static HANDLE  s_AnimationThreadHandle;
static HANDLE  s_hExitEvent;

/////////////////////////////////////////////////////////////////////////////////////////

static int _AniAva_SortAvatarInfo(const ANIAVA_INFO *aai1, const ANIAVA_INFO *aai2)
{
	int res;
	if (aai1 && aai1->tcsFilename && aai2 && aai2->tcsFilename)
		res = mir_tstrcmpi(aai2->tcsFilename, aai1->tcsFilename);
	else {
		int a1 = (aai1 != NULL && aai1->tcsFilename != NULL);
		int a2 = (aai2 != NULL && aai2->tcsFilename != NULL);
		res = a1 - a2;
	}
	if (res)
		return res;

	return !(aai1->FrameSize.cx == aai2->FrameSize.cx && aai1->FrameSize.cy == aai2->FrameSize.cy);
}

static LIST<ANIAVA_INFO> s_AniAvatarList(1, _AniAva_SortAvatarInfo);

// Objects
static OBJLIST<ANIAVA_OBJECT> s_Objects(2, NumericKeySortT);
static BOOL s_bSeparateWindow;

/////////////////////////////////////////////////////////////////////////////////////////
/// IMPLEMENTATION

static void _AniAva_AnimationTreadProc(void*)
{
	Netlib_Logf(NULL, "AnimationTreadProc thread start");

	// wait forever till hExitEvent signalled
	HANDLE hThread = 0;
	s_AnimationThreadID = GetCurrentThreadId();
	DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);
	s_AnimationThreadHandle = hThread;
	SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST);
	for (;;) {
		DWORD rc = MsgWaitForMultipleObjectsEx(1, &s_hExitEvent, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE);
		if (MirandaExiting())
			break;

		ResetEvent(s_hExitEvent);
		if (rc == WAIT_OBJECT_0 + 1) {
			MSG msg;
			while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
				if (msg.hwnd != NULL && IsDialogMessage(msg.hwnd, &msg)) /* Wine fix. */
					continue;
				TranslateMessage(&msg);
				DispatchMessage(&msg);
			}
		}
		else if (rc == WAIT_OBJECT_0)
			break;
	}

	Netlib_Logf(NULL, "AnimationTreadProc thread end");
	CloseHandle(s_AnimationThreadHandle);
	s_AnimationThreadHandle = NULL;

	CloseHandle(s_hExitEvent);
	s_hExitEvent = NULL;
}

/////////////////////////////////////////////////////////////////////////////////////////
// Static procedures

__forceinline ANIAVA_OBJECT* FindAvatarByContact(MCONTACT hContact)
{
	return s_Objects.find((ANIAVA_OBJECT*)&hContact);
}

static void CALLBACK _AniAva_SyncCallerUserAPCProc(DWORD_PTR dwParam)
{
	ANIAVA_SYNCCALLITEM *item = (ANIAVA_SYNCCALLITEM*)dwParam;
	item->nResult = item->pfnProc(item->wParam, item->lParam);
	SetEvent(item->hDoneEvent);
}

static INT_PTR _AniAva_CreateAvatarWindowSync_Worker(WPARAM tszName, LPARAM)
{
	HWND hwnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TRANSPARENT | WS_EX_NOPARENTNOTIFY, ANIAVAWINDOWCLASS,
		(TCHAR*)tszName, WS_POPUP, 0, 0, 1, 1, pcli->hwndContactList, NULL, pcli->hInst, NULL);
	return (INT_PTR)hwnd;
}

static HWND _AniAva_CreateAvatarWindowSync(TCHAR *szFileName)
{
	if (s_AnimationThreadHandle == 0 || s_AnimationThreadID == 0)
		return NULL;

	ANIAVA_SYNCCALLITEM item = { 0 };
	item.wParam = (WPARAM)szFileName;
	item.pfnProc = _AniAva_CreateAvatarWindowSync_Worker;
	item.hDoneEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
	if (GetCurrentThreadId() != s_AnimationThreadID)
		QueueUserAPC(_AniAva_SyncCallerUserAPCProc, s_AnimationThreadHandle, (DWORD_PTR)&item);
	else
		_AniAva_SyncCallerUserAPCProc((DWORD_PTR)&item);
	WaitForSingleObject(item.hDoneEvent, INFINITE);
	CloseHandle(item.hDoneEvent);
	return (HWND)item.nResult;
}

static void _AniAva_PausePainting()
{
	for (int i = 0; i < s_Objects.getCount(); i++)
		SendMessage(s_Objects[i].hWindow, AAM_PAUSE, 0, 0);
}

static void _AniAva_ResumePainting()
{
	for (int i = 0; i < s_Objects.getCount(); i++)
		SendNotifyMessage(s_Objects[i].hWindow, AAM_RESUME, 0, 0);
}

static void _AniAva_ReduceAvatarImages(int startY, int dY, BOOL bDestroyWindow)
{
	for (int i = s_Objects.getCount() - 1; i >= 0; i--) {
		ANIAVA_OBJECT &pai = s_Objects[i];
		int res = SendMessage(pai.hWindow, AAM_REMOVEAVATAR, (WPARAM)startY, (LPARAM)dY);
		if (res == 0xDEAD && bDestroyWindow)
			s_Objects.remove(i);
	}
}

static void _AniAva_RemoveAniAvaDC()
{
	if (s_hAniAvaDC) {
		SelectObject(s_hAniAvaDC, s_hAniAvaOldBitmap);
		DeleteObject(s_hAniAvaBitmap);
		DeleteDC(s_hAniAvaDC);
		s_hAniAvaDC = NULL;
		s_height = 0;
		s_width = 0;
		s_hAniAvaBitmap = NULL;
	}
}

static void _AniAva_RealRemoveAvatar(DWORD UniqueID)
{
	for (int j = 0; j < s_AniAvatarList.getCount(); j++) {
		ANIAVA_INFO *aai = (ANIAVA_INFO *)s_AniAvatarList[j];
		if (aai->dwAvatarUniqId == UniqueID) {
			aai->nRefCount--;
			if (aai->nRefCount == 0) {
				_AniAva_PausePainting();

				mir_free(aai->tcsFilename);
				free(aai->pFrameDelays);
				_AniAva_ReduceAvatarImages(aai->nStripTop, aai->FrameSize.cx*aai->nFrameCount, FALSE);
				for (int k = 0; k < s_AniAvatarList.getCount(); k++) {
					if (k != j) {
						ANIAVA_INFO *taai = (ANIAVA_INFO *)s_AniAvatarList[k];
						if (taai->nStripTop>aai->nStripTop)
							taai->nStripTop -= aai->FrameSize.cx*aai->nFrameCount;
					}
				}

				if (s_AniAvatarList.getCount() > 0) {
					int newWidth = s_width - aai->FrameSize.cx*aai->nFrameCount;
					int newHeight = 0;
					for (int i = 0; i < s_AniAvatarList.getCount(); i++)
						if (i != j)
							newHeight = max(newHeight, ((ANIAVA_INFO *)s_AniAvatarList[i])->FrameSize.cy);

					HDC hNewDC = CreateCompatibleDC(NULL);
					HBITMAP hNewBmp = ske_CreateDIB32(newWidth, newHeight);
					HBITMAP hNewOldBmp = (HBITMAP)SelectObject(hNewDC, hNewBmp);
					// copy from old and from new strip
					if (aai->nStripTop>0)
						BitBlt(hNewDC, 0, 0, aai->nStripTop, newHeight, s_hAniAvaDC, 0, 0, SRCCOPY);
					if (aai->nStripTop + aai->FrameSize.cx*aai->nFrameCount < s_width)
						BitBlt(hNewDC, aai->nStripTop, 0, s_width - (aai->nStripTop + aai->FrameSize.cx*aai->nFrameCount), newHeight, s_hAniAvaDC, aai->nStripTop + aai->FrameSize.cx*aai->nFrameCount, 0, SRCCOPY);

					_AniAva_RemoveAniAvaDC();
					s_width = newWidth;
					s_height = newHeight;
					s_hAniAvaDC = hNewDC;
					s_hAniAvaBitmap = hNewBmp;
					s_hAniAvaOldBitmap = hNewOldBmp;
				}
				else _AniAva_RemoveAniAvaDC();

				s_AniAvatarList.remove(j);
				mir_free(aai);
				_AniAva_ResumePainting();
				break;
			}
		}
	}
}

static int	_AniAva_LoadAvatarFromImage(TCHAR * szFileName, int width, int height, ANIAVATARIMAGEINFO *pRetAII)
{
	ANIAVA_INFO aai = { 0 };
	aai.tcsFilename = szFileName;
	aai.FrameSize.cx = width;
	aai.FrameSize.cy = height;

	ANIAVA_INFO *paai;
	int idx = s_AniAvatarList.getIndex(&aai);
	if (idx == -1) { //item not present in list
		paai = (ANIAVA_INFO *)mir_calloc(sizeof(ANIAVA_INFO));
		paai->tcsFilename = mir_tstrdup(szFileName);
		paai->dwAvatarUniqId = rand();

		// get image strip
		HBITMAP hBitmap = NULL;
		GDIPlus_ExtractAnimatedGIF(szFileName, width, height, hBitmap, paai->pFrameDelays, paai->nFrameCount, paai->FrameSize);

		// copy image to temp DC
		HDC     hTempDC = CreateCompatibleDC(NULL);
		HBITMAP hOldBitmap = (HBITMAP)SelectObject(hTempDC, hBitmap);

		int     newWidth = s_width + paai->FrameSize.cx*paai->nFrameCount;
		int     newHeight = max(paai->FrameSize.cy, s_height);

		HDC     hNewDC = CreateCompatibleDC(NULL);
		HBITMAP hNewBmp = ske_CreateDIB32(newWidth, newHeight);
		HBITMAP hNewOldBmp = (HBITMAP)SelectObject(hNewDC, hNewBmp);

		_AniAva_PausePainting();
		GdiFlush();
		// copy from old and from new strip
		BitBlt(hNewDC, 0, 0, s_width, s_height, s_hAniAvaDC, 0, 0, SRCCOPY);
		BitBlt(hNewDC, s_width, 0, paai->FrameSize.cx*paai->nFrameCount, paai->FrameSize.cy, hTempDC, 0, 0, SRCCOPY);

		paai->nStripTop = s_width;
		s_AniAvatarList.insert(paai);

		GdiFlush();
		// remove temp DC
		SelectObject(hTempDC, hOldBitmap);
		DeleteObject(hNewBmp);
		DeleteDC(hTempDC);
		DeleteObject(hBitmap);

		// delete old
		_AniAva_RemoveAniAvaDC();

		// setNewDC;
		s_width = newWidth;
		s_height = newHeight;
		s_hAniAvaDC = hNewDC;
		s_hAniAvaBitmap = hNewBmp;
		s_hAniAvaOldBitmap = hNewOldBmp;
		GdiFlush();
		_AniAva_ResumePainting();
	}
	else paai = (ANIAVA_INFO*)s_AniAvatarList[idx];

	if (paai == NULL)
		return 0;

	paai->nRefCount++;
	pRetAII->nFramesCount = paai->nFrameCount;
	pRetAII->pFrameDelays = paai->pFrameDelays;
	pRetAII->ptImagePos.x = paai->nStripTop;
	pRetAII->ptImagePos.y = 0;
	pRetAII->szSize = paai->FrameSize;
	return paai->dwAvatarUniqId;
}

static BOOL _AniAva_GetAvatarImageInfo(DWORD dwAvatarUniqId, ANIAVATARIMAGEINFO * avii)
{
	BOOL res = FALSE;
	for (int j = 0; j < s_AniAvatarList.getCount(); j++) {
		ANIAVA_INFO * aai = (ANIAVA_INFO *)s_AniAvatarList[j];
		if (aai->dwAvatarUniqId == dwAvatarUniqId) {
			avii->nFramesCount = aai->nFrameCount;
			avii->pFrameDelays = aai->pFrameDelays;
			avii->ptImagePos.x = aai->nStripTop;
			avii->ptImagePos.y = 0;
			avii->szSize = aai->FrameSize;
			res = TRUE;
			break;
		}
	}
	return res;
}

static void _AniAva_Clear_ANIAVA_WINDOWINFO(ANIAVA_WINDOWINFO *pavwi)
{
	pavwi->delaysInterval = NULL;
	pavwi->nFramesCount = 0;
	KillTimer(pavwi->hWindow, 2);
	pavwi->bPlaying = FALSE;
	pavwi->TimerId = 0;
}

static void _AniAva_LoadOptions()
{
	aacheck;
	mir_cslock lck(s_CS);

	s_bFlags = (db_get_b(NULL, "CList", "AvatarsDrawBorders", SETTINGS_AVATARDRAWBORDER_DEFAULT) ? AAO_HAS_BORDER : 0) |
		(db_get_b(NULL, "CList", "AvatarsRoundCorners", SETTINGS_AVATARROUNDCORNERS_DEFAULT) ? AAO_ROUND_CORNERS : 0) |
		(db_get_b(NULL, "CList", "AvatarsDrawOverlay", SETTINGS_AVATARDRAWOVERLAY_DEFAULT) ? AAO_HAS_OVERLAY : 0) |
		((0) ? AAO_OPAQUE : 0);

	if (s_bFlags & AAO_HAS_BORDER)
		s_borderColor = (COLORREF)db_get_dw(NULL, "CList", "AvatarsBorderColor", SETTINGS_AVATARBORDERCOLOR_DEFAULT);
	if (s_bFlags & AAO_ROUND_CORNERS)
		s_cornerRadius = db_get_b(NULL, "CList", "AvatarsUseCustomCornerSize", SETTINGS_AVATARUSECUTOMCORNERSIZE_DEFAULT) ? db_get_w(NULL, "CList", "AvatarsCustomCornerSize", SETTINGS_AVATARCORNERSIZE_DEFAULT) : 0;
	if (s_bFlags & AAO_HAS_OVERLAY) {
		// check image list
		BYTE type = db_get_b(NULL, "CList", "AvatarsOverlayType", SETTINGS_AVATAROVERLAYTYPE_DEFAULT);
		switch (type) {
		case SETTING_AVATAR_OVERLAY_TYPE_NORMAL:
			s_overlayIconImageList = hAvatarOverlays;
			break;
		case SETTING_AVATAR_OVERLAY_TYPE_PROTOCOL:
		case SETTING_AVATAR_OVERLAY_TYPE_CONTACT:
			s_overlayIconImageList = g_himlCListClc;
			break;
		default:
			s_overlayIconImageList = NULL;
		}
	}
	if (s_bFlags & AAO_OPAQUE)
		s_bkgColor = 0;
	s_bSeparateWindow = db_get_b(NULL, "CList", "AvatarsInSeparateWnd", SETTINGS_AVATARINSEPARATE_DEFAULT);
}

static void _AniAva_InvalidateParent(ANIAVA_WINDOWINFO * dat)
{
	if (!IMMEDIATE_DRAW) return;
	HWND hwndParent = pcli->hwndContactTree;
	RECT rcPos = dat->rcPos;
	pcli->pfnInvalidateRect(hwndParent, &rcPos, FALSE);
}

static void _AniAva_RenderAvatar(ANIAVA_WINDOWINFO * dat, HDC hdcParent = NULL, RECT *rcInParent = NULL)
{
	if (dat->bPaused > 0) { dat->bPended = TRUE;	return; }
	else dat->bPended = FALSE;

	if (IMMEDIATE_DRAW && hdcParent == NULL) return;
	GdiFlush();

	if (dat->bPlaying && IsWindowVisible(dat->hWindow)) {
		POINT ptWnd = { 0 };
		SIZE szWnd = { dat->rcPos.right - dat->rcPos.left, dat->rcPos.bottom - dat->rcPos.top };
		BLENDFUNCTION bf = { AC_SRC_OVER, 0, BYTE(g_CluiData.bCurrentAlpha * dat->bAlpha / 256), AC_SRC_ALPHA };
		POINT pt_from = { 0, 0 };
		HDC hDC_animation = GetDC(NULL);
		HDC copyFromDC;
		RECT clistRect;
		HDC tempDC = NULL;
		HBITMAP hBmp = NULL;
		HBITMAP hOldBmp = NULL;

		if (s_bFlags == 0) { //simple and fastest method - no borders, round corners and etc. just copy
			pt_from.x = dat->ptFromPoint.x + dat->currentFrame*dat->sizeAvatar.cx;
			pt_from.y = dat->ptFromPoint.y;
			copyFromDC = s_hAniAvaDC;
		}
		else {
			// ... need to create additional hDC_animation
			HRGN hRgn = NULL;
			int cornerRadius = s_cornerRadius;
			tempDC = CreateCompatibleDC(NULL);
			hBmp = ske_CreateDIB32(szWnd.cx, szWnd.cy);
			hOldBmp = (HBITMAP)SelectObject(tempDC, hBmp);
			if (s_bFlags & AAO_ROUND_CORNERS)
				if (!cornerRadius)  //auto radius
					cornerRadius = min(szWnd.cx, szWnd.cy) / 5;

			if (s_bFlags & AAO_HAS_BORDER) {
				// if has borders - create region (round corners) and fill it, remember internal as clipping
				HBRUSH hBrush = CreateSolidBrush(s_borderColor);
				HBRUSH hOldBrush = (HBRUSH)SelectObject(tempDC, hBrush);
				HRGN rgnOutside = CreateRoundRectRgn(0, 0, szWnd.cx + 1, szWnd.cy + 1, cornerRadius << 1, cornerRadius << 1);
				hRgn = CreateRoundRectRgn(1, 1, szWnd.cx, szWnd.cy, cornerRadius << 1, cornerRadius << 1);
				CombineRgn(rgnOutside, rgnOutside, hRgn, RGN_DIFF);
				FillRgn(tempDC, rgnOutside, hBrush);
				ske_SetRgnOpaque(tempDC, rgnOutside, TRUE);
				SelectObject(tempDC, hOldBrush);
				DeleteObject(hBrush);
				DeleteObject(rgnOutside);
			}
			else if (cornerRadius > 0)
				// else create clipping area (round corners)
				hRgn = CreateRoundRectRgn(0, 0, szWnd.cx + 1, szWnd.cy + 1, cornerRadius << 1, cornerRadius << 1);
			else
				hRgn = CreateRectRgn(0, 0, szWnd.cx + 1, szWnd.cy + 1);

			// select clip area
			if (hRgn)
				ExtSelectClipRgn(tempDC, hRgn, RGN_AND);

			if (s_bFlags & AAO_OPAQUE) {
				// if back color - fill clipping area
				HBRUSH hBrush = CreateSolidBrush(s_bkgColor);
				FillRgn(tempDC, hRgn, hBrush);
				ske_SetRgnOpaque(tempDC, hRgn, TRUE);
			}
			// draw avatar
			if (!(s_bFlags & AAO_OPAQUE))
				BitBlt(tempDC, 0, 0, szWnd.cx, szWnd.cy, s_hAniAvaDC, dat->ptFromPoint.x + dat->sizeAvatar.cx*dat->currentFrame, dat->ptFromPoint.y, SRCCOPY);
			else {
				BLENDFUNCTION abf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
				ske_AlphaBlend(tempDC, 0, 0, szWnd.cx, szWnd.cy, s_hAniAvaDC, dat->ptFromPoint.x + dat->sizeAvatar.cx*dat->currentFrame, dat->ptFromPoint.y, szWnd.cx, szWnd.cy, abf);
			}
			// reset clip area
			if (hRgn) {
				DeleteObject(hRgn);
				hRgn = CreateRectRgn(0, 0, szWnd.cx, szWnd.cy);
				SelectClipRgn(tempDC, hRgn);
				DeleteObject(hRgn);
			}

			if ((s_bFlags & AAO_HAS_OVERLAY) && dat->overlayIconIdx != -1 && s_overlayIconImageList) {
				// if overlay - draw overlay icon
				// position - on avatar
				int x = szWnd.cx - ICON_WIDTH;
				int y = szWnd.cy - ICON_HEIGHT;
				ske_ImageList_DrawEx(s_overlayIconImageList, dat->overlayIconIdx & 0xFFFF, tempDC, x, y, ICON_WIDTH, ICON_HEIGHT, CLR_NONE, CLR_NONE, ILD_NORMAL);
			}
			copyFromDC = tempDC;
		}

		// intersect visible area
		// update layered window
		GetWindowRect(pcli->hwndContactTree, &clistRect);
		if (dat->rcPos.top < 0) {
			pt_from.y += -dat->rcPos.top;
			szWnd.cy += dat->rcPos.top;
		}
		if (dat->rcPos.bottom>clistRect.bottom - clistRect.top)
			szWnd.cy -= (dat->rcPos.bottom - (clistRect.bottom - clistRect.top));

		ptWnd.x = dat->rcPos.left + clistRect.left;
		ptWnd.y = (dat->rcPos.top > 0 ? dat->rcPos.top : 0) + clistRect.top;
		if (szWnd.cy > 0) {
			if (hdcParent && rcInParent && IMMEDIATE_DRAW) {
				if (s_bFlags & AAO_OPAQUE)
					BitBlt(hdcParent, rcInParent->left, rcInParent->top, szWnd.cx, szWnd.cy, copyFromDC, pt_from.x, pt_from.y, SRCCOPY);
				else {
					BLENDFUNCTION abf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
					ske_AlphaBlend(hdcParent, rcInParent->left, rcInParent->top, szWnd.cx, szWnd.cy, copyFromDC, pt_from.x, pt_from.y, szWnd.cx, szWnd.cy, abf);
				}
			}
			else if (!UpdateLayeredWindow(dat->hWindow, hDC_animation, &ptWnd, &szWnd, copyFromDC, &pt_from, RGB(0, 0, 0), &bf, ULW_ALPHA)) {
				LONG_PTR exStyle;
				exStyle = GetWindowLongPtr(dat->hWindow, GWL_EXSTYLE);
				exStyle |= WS_EX_LAYERED;
				SetWindowLongPtr(dat->hWindow, GWL_EXSTYLE, exStyle);
				if (!IMMEDIATE_DRAW)
					SetWindowPos(pcli->hwndContactTree, dat->hWindow, 0, 0, 0, 0, SWP_ASYNCWINDOWPOS | SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOSENDCHANGING);
				UpdateLayeredWindow(dat->hWindow, hDC_animation, &ptWnd, &szWnd, copyFromDC, &pt_from, RGB(0, 0, 0), &bf, ULW_ALPHA);
			}

			g_CluiData.fAeroGlass = false;
			CLUI_UpdateAeroGlass();
		}
		else dat->bPlaying = FALSE;

		ReleaseDC(NULL, hDC_animation);
		if (tempDC) {
			SelectObject(tempDC, hOldBmp);
			DeleteObject(hBmp);
			DeleteDC(tempDC);
		}
	}
	if (!dat->bPlaying) {
		ShowWindow(dat->hWindow, SW_HIDE);
		KillTimer(dat->hWindow, 2);  //stop animation till set pos will be called
	}
	GdiFlush();
}

static LRESULT CALLBACK _AniAva_WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	ANIAVA_WINDOWINFO *dat = NULL;
	if (msg == WM_TIMER || msg == WM_DESTROY || (msg > AAM_FIRST && msg < AAM_LAST))
		dat = (ANIAVA_WINDOWINFO *)GetWindowLongPtr(hwnd, GWLP_USERDATA);

	switch (msg) {
	case AAM_REMOVEAVATAR:
		if (dat->ptFromPoint.x == (int)wParam) return 0xDEAD;	 //need to destroy window
		else if (dat->ptFromPoint.x>(int)wParam) dat->ptFromPoint.x -= (int)lParam;
		return 0;

	case AAM_PAUSE:
		dat->bPaused++;
		return 0;

	case AAM_RESUME:
		dat->bPaused--;
		if (dat->bPaused) return 0;
		if (dat->bPended)
			if (!IMMEDIATE_DRAW)
				_AniAva_RenderAvatar(dat);
		dat->bPended = FALSE;
		return 0;

	case AAM_STOP:
		if (dat->bPlaying) {
			dat->bPlaying = FALSE;
			KillTimer(hwnd, 2);
			ShowWindow(hwnd, SW_HIDE);
		}
		return 0;

	case AAM_SETAVATAR:
		_AniAva_Clear_ANIAVA_WINDOWINFO(dat);
		{
			ANIAVATARIMAGEINFO *paaii = (ANIAVATARIMAGEINFO*)wParam;
			dat->nFramesCount = paaii->nFramesCount;
			dat->delaysInterval = paaii->pFrameDelays;
			dat->sizeAvatar = paaii->szSize;
			dat->ptFromPoint = paaii->ptImagePos;
		}
		dat->currentFrame = 0;
		dat->bPlaying = FALSE;
		return MAKELONG(dat->sizeAvatar.cx, dat->sizeAvatar.cy);

	case AAM_SETPOSITION:
		if (dat->delaysInterval) {
			ANIAVA_POSINFO *papi = (ANIAVA_POSINFO*)lParam;
			if (papi != NULL) {
				dat->rcPos = papi->rcPos;
				dat->overlayIconIdx = papi->idxOverlay;
				dat->bAlpha = papi->bAlpha;
				free(papi);
				if (!dat->bPlaying) {
					dat->bPlaying = TRUE;
					ShowWindow(hwnd, SW_SHOWNA);
					dat->currentFrame = 0;
					KillTimer(hwnd, 2);
					SetTimer(hwnd, 2, dat->delaysInterval[0], NULL);
				}
				if (!IMMEDIATE_DRAW)
					_AniAva_RenderAvatar(dat);
			}
		}
		return 0;

	case AAM_SETPARENT:
		if (IMMEDIATE_DRAW)
			return 0;

		dat->bOrderTop = ((HWND)wParam != GetDesktopWindow());
		SetParent(hwnd, (HWND)wParam);
		if (dat->bOrderTop)
			SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
		else {
			LONG exStyle = GetWindowLongPtr(pcli->hwndContactList, GWL_EXSTYLE);
			SetWindowPos(pcli->hwndContactList, hwnd, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);
			if (!(exStyle & WS_EX_TOPMOST))
				SetWindowPos(pcli->hwndContactList, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);
		}
		return 0;

	case AAM_REDRAW:
		if (IMMEDIATE_DRAW)
			return 0;

		if (wParam) {
			if (dat->bOrderTop)
				SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
			else {
				LONG exStyle = GetWindowLongPtr(pcli->hwndContactList, GWL_EXSTYLE);
				SetWindowPos(pcli->hwndContactList, hwnd, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);
				if (!(exStyle&WS_EX_TOPMOST))
					SetWindowPos(pcli->hwndContactList, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);
			}
		}

		_AniAva_RenderAvatar(dat);
		return 0;

	case AAM_RENDER:
		_AniAva_RenderAvatar(dat, (HDC)wParam, (RECT*)lParam);
		return 0;

	case WM_CREATE:
		dat = (ANIAVA_WINDOWINFO *)mir_calloc(sizeof(ANIAVA_WINDOWINFO));
		SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)dat);
		dat->hWindow = hwnd;

		// change layered mode
		{
			LONG_PTR exStyle = GetWindowLongPtr(dat->hWindow, GWL_EXSTYLE);
			exStyle |= WS_EX_LAYERED;
			SetWindowLongPtr(dat->hWindow, GWL_EXSTYLE, exStyle);
			exStyle = GetWindowLongPtr(dat->hWindow, GWL_STYLE);
			exStyle &= ~WS_POPUP;
			exStyle |= WS_CHILD;
			SetWindowLongPtr(dat->hWindow, GWL_STYLE, exStyle);
		}
		break;

	case WM_TIMER:
		if (!IsWindowVisible(hwnd)) {
			DestroyWindow(hwnd);
			return 0;
		}
		dat->currentFrame++;
		if (dat->currentFrame >= dat->nFramesCount)
			dat->currentFrame = 0;

		if (!IMMEDIATE_DRAW)
			_AniAva_RenderAvatar(dat);
		else
			_AniAva_InvalidateParent(dat);

		KillTimer(hwnd, 2);
		SetTimer(hwnd, 2, dat->delaysInterval[dat->currentFrame] + 1, NULL);
		return 0;

	case WM_DESTROY:
		_AniAva_Clear_ANIAVA_WINDOWINFO(dat);
		mir_free(dat);
		SetWindowLongPtr(hwnd, GWLP_USERDATA, 0);
		break;
	}
	return DefWindowProc(hwnd, msg, wParam, lParam);
}

/////////////////////////////////////////////////////////////////////////////////////////
// public functions
//=======================================================================================
// adds avatars to be displayed

int AniAva_AddAvatar(MCONTACT hContact, TCHAR * szFilename, int width, int heigth)
{
	aacheck 0;
	if (!GDIPlus_IsAnimatedGif(szFilename))
		return 0;

	mir_cslock lck(s_CS);
	// first try to find window for contact avatar
	HWND hwnd = NULL;
	ANIAVA_OBJECT *pavi = FindAvatarByContact(hContact);
	if (pavi != NULL) {
		if (pavi->ObjectSize.cx == width && pavi->ObjectSize.cy == heigth)
			hwnd = pavi->hWindow;
		else {
			DestroyWindow(pavi->hWindow);
			pavi->hWindow = NULL;
			_AniAva_RealRemoveAvatar(pavi->dwAvatarUniqId);
			pavi->dwAvatarUniqId = 0;
		}
	}
	else {
		pavi = new ANIAVA_OBJECT();
		pavi->hContact = hContact;
		s_Objects.insert(pavi);
	}

	// change avatar
	pavi->bInvalidPos = 0;

	// now CreateAvatar
	ANIAVATARIMAGEINFO avii = { 0 };
	if (pavi->dwAvatarUniqId)
		_AniAva_GetAvatarImageInfo(pavi->dwAvatarUniqId, &avii);
	else
		pavi->dwAvatarUniqId = _AniAva_LoadAvatarFromImage(szFilename, width, heigth, &avii);
	if (hwnd)
		SendMessage(hwnd, AAM_SETAVATAR, (WPARAM)&avii, (LPARAM)0);
	pavi->ObjectSize = avii.szSize;
	return MAKELONG(avii.szSize.cx, avii.szSize.cy);
}

/////////////////////////////////////////////////////////////////////////////////////////
// reset positions of avatars to be drawn (still be painted at same place)

int AniAva_InvalidateAvatarPositions(MCONTACT hContact)
{
	aacheck 0;
	mir_cslock lck(s_CS);

	if (hContact) {
		ANIAVA_OBJECT *pai = FindAvatarByContact(hContact);
		if (pai)
			pai->bInvalidPos++;
	}
	else
		for (int i = 0; i < s_Objects.getCount(); i++)
			s_Objects[i].bInvalidPos++;

	return 1;
}

/////////////////////////////////////////////////////////////////////////////////////////
// repaint all avatars at positions (eg on main window movement)

int AniAva_RedrawAllAvatars(BOOL updateZOrder)
{
	aacheck 0;
	mir_cslock lck(s_CS);
	updateZOrder = 1;
	for (int i = 0; i < s_Objects.getCount(); i++) {
		ANIAVA_OBJECT &pai = s_Objects[i];
		if (updateZOrder)
			SendMessage(pai.hWindow, AAM_REDRAW, (WPARAM)updateZOrder, 0);
		else
			SendNotifyMessage(pai.hWindow, AAM_REDRAW, (WPARAM)updateZOrder, 0);
	}
	return 1;
}

/////////////////////////////////////////////////////////////////////////////////////////
// remove avatar

int AniAva_RemoveAvatar(MCONTACT hContact)
{
	aacheck 0;
	mir_cslock lck(s_CS);

	ANIAVA_OBJECT *pai = FindAvatarByContact(hContact);
	if (pai)
		s_Objects.remove(pai);

	return 1;
}

/////////////////////////////////////////////////////////////////////////////////////////
// all avatars without validated position will be stop painted and probably removed

int AniAva_RemoveInvalidatedAvatars()
{
	aacheck 0;
	mir_cslock lck(s_CS);

	for (int i = 0; i < s_Objects.getCount(); i++) {
		ANIAVA_OBJECT &pai = s_Objects[i];
		if (pai.hWindow && pai.bInvalidPos) {
			SendMessage(pai.hWindow, AAM_STOP, 0, 0);
			pai.bInvalidPos = 0;
			DestroyWindow(pai.hWindow);
			pai.hWindow = NULL;
		}
	}
	return 1;
}

/////////////////////////////////////////////////////////////////////////////////////////

int AniAva_RenderAvatar(MCONTACT hContact, HDC hdcMem, RECT *rc)
{
	aacheck 0;
	mir_cslock lck(s_CS);
	ANIAVA_OBJECT *pai = FindAvatarByContact(hContact);
	if (pai)
		SendMessage(pai->hWindow, AAM_RENDER, (WPARAM)hdcMem, (LPARAM)rc);
	return 0;
}

/////////////////////////////////////////////////////////////////////////////////////////
// update avatars pos

int AniAva_SetAvatarPos(MCONTACT hContact, RECT *rc, int overlayIdx, BYTE bAlpha)
{
	aacheck 0;
	mir_cslock lck(s_CS);

	ANIAVA_OBJECT *pai = FindAvatarByContact(hContact);
	if (pai == NULL)
		return 1;

	ANIAVA_POSINFO *api = (ANIAVA_POSINFO *)malloc(sizeof(ANIAVA_POSINFO));
	if (!pai->hWindow) {
		// not found -> create window
		char szName[150] = "AniAvaWnd_";
		_itoa((int)hContact, szName + 10, 16);

		HWND hwnd = _AniAva_CreateAvatarWindowSync(_A2T(szName));
		HWND parent = GetAncestor(pcli->hwndContactList, GA_PARENT);
		pai->hWindow = hwnd;
		SendMessage(hwnd, AAM_SETPARENT, (WPARAM)parent, 0);

		ANIAVATARIMAGEINFO avii = { 0 };
		if (_AniAva_GetAvatarImageInfo(pai->dwAvatarUniqId, &avii))
			SendMessage(pai->hWindow, AAM_SETAVATAR, (WPARAM)&avii, (LPARAM)0);
	}
	api->bAlpha = bAlpha;
	api->idxOverlay = overlayIdx;
	api->rcPos = *rc;
	SendNotifyMessage(pai->hWindow, AAM_SETPOSITION, 0, (LPARAM)api);

	// the AAM_SETPOSITION is responsible to destroy memory under api
	pai->bInvalidPos = FALSE;
	return 1;
}

/////////////////////////////////////////////////////////////////////////////////////////
// Update options

int AniAva_UpdateOptions()
{
	BOOL bReloadAvatars = FALSE;
	BOOL bBeEnabled = db_get_b(NULL, "CList", "AvatarsAnimated", db_get_b(NULL, "CList", "AvatarsShow", SETTINGS_SHOWAVATARS_DEFAULT));
	if (bBeEnabled && !s_bModuleStarted) {
		AniAva_InitModule();
		bReloadAvatars = TRUE;
	}
	else if (!bBeEnabled && s_bModuleStarted) {
		AniAva_UnloadModule();
		bReloadAvatars = TRUE;
	}

	BOOL oldSeparate = s_bSeparateWindow;
	_AniAva_LoadOptions();
	if (oldSeparate != s_bSeparateWindow) {
		AniAva_InvalidateAvatarPositions(NULL);
		AniAva_RemoveInvalidatedAvatars();
	}

	if (bReloadAvatars)
		PostMessage(pcli->hwndContactTree, INTM_AVATARCHANGED, 0, 0);
	else
		AniAva_RedrawAllAvatars(TRUE);
	return 0;
}

/////////////////////////////////////////////////////////////////////////////////////////
// call windows to set they parent in order to ensure valid zorder

void AniAva_UpdateParent()
{
	aacheck;
	mir_cslock lck(s_CS);
	HWND parent = GetAncestor(pcli->hwndContactList, GA_PARENT);
	for (int i = 0; i < s_Objects.getCount(); i++)
		SendMessage(s_Objects[i].hWindow, AAM_SETPARENT, (WPARAM)parent, 0);
}

/////////////////////////////////////////////////////////////////////////////////////////
// Init AniAva module

int AniAva_InitModule()
{
	if (!db_get_b(NULL, "CList", "AvatarsAnimated", db_get_b(NULL, "CList", "AvatarsShow", SETTINGS_SHOWAVATARS_DEFAULT)))
		return 0;

	WNDCLASSEX wc = { sizeof(wc) };
	wc.lpszClassName = ANIAVAWINDOWCLASS;
	wc.lpfnWndProc = _AniAva_WndProc;
	wc.hCursor = LoadCursor(NULL, IDC_ARROW);
	wc.cbWndExtra = sizeof(ANIAVA_WINDOWINFO*);
	wc.style = CS_GLOBALCLASS;
	RegisterClassEx(&wc);

	s_bModuleStarted = TRUE;

	_AniAva_LoadOptions();

	s_hExitEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
	mir_forkthread(_AniAva_AnimationTreadProc, NULL);
	return 1;
}

/////////////////////////////////////////////////////////////////////////////////////////
// Unload AniAva module

void _AniAva_OnModulesUnload()
{
	if (s_hExitEvent)
		SetEvent(s_hExitEvent);
}

int AniAva_UnloadModule()
{
	aacheck 0;

	mir_cslock lck(s_CS);
	s_bModuleStarted = FALSE;
	s_Objects.destroy();

	for (int i = 0; i < s_AniAvatarList.getCount(); i++) {
		ANIAVA_INFO *aai = s_AniAvatarList[i];
		mir_free(aai->tcsFilename);
		free(aai->pFrameDelays);
		mir_free(aai);
	}
	s_AniAvatarList.destroy();

	_AniAva_RemoveAniAvaDC();
	return 1;
}