/*
	GroupCheckbox.cpp
	Copyright (c) 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 "GroupCheckbox.h"
#include "Themes.h"

#define WM_THEMECHANGED 0x031A

#define UM_INITCHECKBOX (WM_USER + 123)
#define UM_AUTOSIZE (WM_USER + 124)

#define CG_CHECKBOX_VERTINDENT 0
#define CG_CHECKBOX_INDENT 1
#define CG_CHECKBOX_WIDTH 16
#define CG_TEXT_INDENT 2
#define CG_ADDITIONAL_WIDTH 3

// states
#define CGS_UNCHECKED BST_UNCHECKED
#define CGS_CHECKED BST_CHECKED
#define CGS_INDETERMINATE BST_INDETERMINATE
#define CGS_PRESSED BST_PUSHED // values above and including CGS_PRESSED must coincide with BST_ constants for BM_GETSTATE to work properly
#define CGS_HOVERED 8

// state masks
#define CGSM_ISCHECKED 3 // mask for BM_GETCHECK
#define CGSM_GETSTATE 7 // mask to get only valid values for BM_GETSTATE

#ifndef lengthof
#define lengthof(s) (sizeof(s) / sizeof(*s))
#endif

class CCheckboxData
{
public:
	CCheckboxData(): OldWndProc(NULL), Style(0), State(0), hFont(NULL) {};

	WNDPROC OldWndProc;
	int Style; // BS_CHECKBOX, BS_AUTOCHECKBOX, BS_3STATE or BS_AUTO3STATE
	int State;
	HFONT hFont;
};

static int CALLBACK CheckboxWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
	CCheckboxData *dat = (CCheckboxData*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
	if (!dat)
	{
		return 0;
	}
	switch (Msg)
	{
		case UM_INITCHECKBOX:
		{
			LOGFONT lf;
			HFONT hFont = (HFONT)SendMessage(hWnd, WM_GETFONT, 0, 0);
			if (!hFont)
			{
				hFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
			}
			GetObject(hFont, sizeof(lf), &lf);
			lf.lfWeight = FW_BOLD;
			dat->hFont = CreateFontIndirect(&lf);
			SendMessage(hWnd, UM_AUTOSIZE, 0, 0);
			return 0;
		} break;
		case UM_AUTOSIZE:
		{
			HTHEME hTheme = pOpenThemeData ? pOpenThemeData(hWnd, L"BUTTON") : NULL;
			int Len = GetWindowTextLength(hWnd) + 1;
			HDC hdc = GetDC(hWnd);
			HFONT hOldFont = (HFONT)SelectObject(hdc, dat->hFont);
			RECT rcText = {0};
			if (hTheme && pGetThemeTextExtent)
			{
				WCHAR *szText = (WCHAR*)malloc(Len * sizeof(WCHAR));
				GetWindowTextW(hWnd, szText, Len);
				pGetThemeTextExtent(hTheme, hdc, BP_GROUPBOX, IsWindowEnabled(hWnd) ? GBS_NORMAL : GBS_DISABLED, szText, -1, DT_CALCRECT | DT_LEFT | DT_VCENTER | DT_SINGLELINE, 0, &rcText);
				free(szText);
			} else
			{
				SIZE size;
				TCHAR *szText = (TCHAR*)malloc(Len * sizeof(TCHAR));
				GetWindowText(hWnd, szText, Len);
				GetTextExtentPoint32(hdc, szText, lstrlen(szText), &size);
				free(szText);
				rcText.right = size.cx;
				rcText.bottom = size.cy;
			}
			
			SelectObject(hdc, hOldFont);
			ReleaseDC(hWnd, hdc);
			if (hTheme && pCloseThemeData)
			{
				pCloseThemeData(hTheme);
			}
			OffsetRect(&rcText, CG_CHECKBOX_INDENT + CG_CHECKBOX_WIDTH + CG_TEXT_INDENT, 0);
			RECT rc;
			GetClientRect(hWnd, &rc);
			SetWindowPos(hWnd, 0, 0, 0, rcText.right + CG_ADDITIONAL_WIDTH, rc.bottom, SWP_NOMOVE | SWP_NOZORDER);
		} break;
		case BM_CLICK:
		{
			SendMessage(hWnd, WM_LBUTTONDOWN, 0, 0);
			SendMessage(hWnd, WM_LBUTTONUP, 0, 0);
			return 0;
		} break;
		case BM_GETCHECK:
		{
			return dat->State & CGSM_ISCHECKED;
		} break;
		case BM_SETCHECK:
		{
			if ((wParam != BST_UNCHECKED && wParam != BST_CHECKED && wParam != BST_INDETERMINATE) || (wParam == BST_INDETERMINATE && dat->Style != BS_3STATE && dat->Style != BS_AUTO3STATE))
			{ // invalid value
				wParam = BST_CHECKED;
			}
			dat->State &= ~CGSM_ISCHECKED;
			dat->State |= wParam;
			InvalidateRect(hWnd, NULL, false);
			SendMessage(GetParent(hWnd), WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(hWnd), BN_CLICKED), (LPARAM)hWnd);
			return 0;
		} break;
		case BM_SETSTATE:
		{
			if (wParam)
			{
				dat->State |= CGS_PRESSED;
			} else
			{
				dat->State &= ~CGS_PRESSED;
			}
			InvalidateRect(hWnd, NULL, false);
			return 0;
		} break;
		case BM_GETSTATE:
		{
			return (dat->State & CGSM_GETSTATE) | ((GetFocus() == hWnd) ? BST_FOCUS : 0);
		} break;
		case WM_GETDLGCODE:
		{
			return DLGC_BUTTON;
		} break;
		case WM_THEMECHANGED:
		case WM_ENABLE:
		{
			InvalidateRect(hWnd, NULL, false);
			return 0;
		} break;
		case WM_SETTEXT:
		{
			if (CallWindowProc(dat->OldWndProc, hWnd, Msg, wParam, lParam))
			{
				SendMessage(hWnd, UM_AUTOSIZE, 0, 0);
			}
			return 0;
		} break;
		case WM_KEYDOWN:
		{
			if (wParam == VK_SPACE)
			{
				SendMessage(hWnd, BM_SETSTATE, true, 0);
			}
			return 0;
		} break;
		case WM_KEYUP:
		{
			if (wParam == VK_SPACE)
			{
				SendMessage(hWnd, BM_SETCHECK, (SendMessage(hWnd, BM_GETCHECK, 0, 0) + 1) % ((dat->Style == BS_AUTO3STATE) ? 3 : 2), 0);
				SendMessage(hWnd, BM_SETSTATE, false, 0);
			}
			return 0;
		} break;
		case WM_CAPTURECHANGED:
		{
			SendMessage(hWnd, BM_SETSTATE, false, 0);
			return 0;
		} break;
		case WM_ERASEBKGND:
		{
			return true;
		} break;
		case WM_LBUTTONDOWN:
		case WM_LBUTTONDBLCLK:
		{
			SetFocus(hWnd);
			SendMessage(hWnd, BM_SETSTATE, true, 0);
			SetCapture(hWnd);
			return 0;
		} break;
		case WM_LBUTTONUP:
		{
			if (GetCapture() == hWnd)
			{
				ReleaseCapture();
			}
			SendMessage(hWnd, BM_SETSTATE, false, 0);
			if (dat->State & CGS_HOVERED && (dat->Style == BS_AUTOCHECKBOX || dat->Style == BS_AUTO3STATE))
			{
				SendMessage(hWnd, BM_SETCHECK, (SendMessage(hWnd, BM_GETCHECK, 0, 0) + 1) % ((dat->Style == BS_AUTO3STATE) ? 3 : 2), 0);
			}
			return 0;
		} break;
		case WM_MOUSEMOVE:
		{
			TRACKMOUSEEVENT tme;
			tme.cbSize = sizeof(tme);
			tme.dwFlags = TME_LEAVE;
			tme.dwHoverTime = HOVER_DEFAULT;
			tme.hwndTrack = hWnd;
			_TrackMouseEvent(&tme);

			POINT pt;
			GetCursorPos(&pt);
			if ((WindowFromPoint(pt) == hWnd) ^ ((dat->State & CGS_HOVERED) != 0))
			{
				dat->State ^= CGS_HOVERED;
				InvalidateRect(hWnd, NULL, false);
			}
			return 0;
		} break;
		case WM_MOUSELEAVE:
		{
			if (dat->State & CGS_HOVERED)
			{
				dat->State &= ~CGS_HOVERED;
				InvalidateRect(hWnd, NULL, false);
			}
			return 0;
		} break;
		case WM_SETFOCUS:
		case WM_KILLFOCUS:
		case WM_SYSCOLORCHANGE:
		{
			InvalidateRect(hWnd, NULL, false);
			return 0;
		} break;
		case WM_PAINT:
		{
			HDC hdc;
			PAINTSTRUCT ps;
			hdc = BeginPaint(hWnd, &ps);
			RECT rc;
			GetClientRect(hWnd, &rc);
			HDC hdcMem = CreateCompatibleDC(hdc);
			HBITMAP hbmMem = CreateCompatibleBitmap(hdc, rc.right, rc.bottom);
			HBITMAP hbmOld = (HBITMAP)SelectObject(hdcMem, hbmMem);
			HTHEME hTheme = pOpenThemeData ? pOpenThemeData(hWnd, L"BUTTON") : NULL;
			if (hTheme && pDrawThemeParentBackground)
			{
				pDrawThemeParentBackground(hWnd, hdcMem, NULL);
			} else
			{
				FillRect(hdcMem, &rc, GetSysColorBrush(COLOR_3DFACE));
			}
			int StateID = 0;
#define CBSCHECK_UNCHECKED 1
#define CBSCHECK_CHECKED 5
#define CBSCHECK_MIXED 9
#define CBSSTATE_NORMAL 0
#define CBSSTATE_HOT 1
#define CBSSTATE_PRESSED 2
#define CBSSTATE_DISABLED 3
			switch (SendMessage(hWnd, BM_GETCHECK, 0, 0))
			{
				case BST_CHECKED:
				{
					StateID += CBSCHECK_CHECKED;
				} break;
				case BST_UNCHECKED:
				{
					StateID += CBSCHECK_UNCHECKED;
				} break;
				case BST_INDETERMINATE:
				{
					StateID += CBSCHECK_MIXED;
				} break;
			}
			if (!IsWindowEnabled(hWnd))
			{
				StateID += CBSSTATE_DISABLED;
			} else if (dat->State & CGS_PRESSED && (GetCapture() != hWnd || dat->State & CGS_HOVERED))
			{
				StateID += CBSSTATE_PRESSED;
			} else if (dat->State & CGS_PRESSED || dat->State & CGS_HOVERED)
			{
				StateID += CBSSTATE_HOT;
			}
			rc.left += CG_CHECKBOX_INDENT;
			rc.right = rc.left + CG_CHECKBOX_WIDTH; // left-align the image in the client area
			rc.top += CG_CHECKBOX_VERTINDENT;
			rc.bottom = rc.top + CG_CHECKBOX_WIDTH; // exact rc dimensions are necessary for DrawFrameControl to draw correctly
			if (hTheme && pDrawThemeBackground)
			{
				pDrawThemeBackground(hTheme, hdcMem, BP_CHECKBOX, StateID, &rc, &rc);
			} else
			{
				int dfcStates[] =
				 {0, 0, DFCS_PUSHED, DFCS_INACTIVE,
					DFCS_CHECKED, DFCS_CHECKED, DFCS_CHECKED | DFCS_PUSHED, DFCS_CHECKED | DFCS_INACTIVE,
					DFCS_BUTTON3STATE | DFCS_CHECKED, DFCS_BUTTON3STATE | DFCS_CHECKED, DFCS_BUTTON3STATE | DFCS_INACTIVE | DFCS_CHECKED | DFCS_PUSHED, DFCS_BUTTON3STATE | DFCS_INACTIVE | DFCS_CHECKED | DFCS_PUSHED};
				_ASSERT(StateID >= 1 && StateID <= lengthof(dfcStates));
				DrawFrameControl(hdcMem, &rc, DFC_BUTTON, dfcStates[StateID - 1]);
			}

			GetClientRect(hWnd, &rc);
			rc.left += CG_CHECKBOX_INDENT + CG_CHECKBOX_WIDTH + CG_TEXT_INDENT;
			int Len = GetWindowTextLength(hWnd) + 1;
			WCHAR *szTextW = NULL;
			TCHAR *szTextT = NULL;
			if (hTheme && pDrawThemeText && pGetThemeTextExtent)
			{
				szTextW = (WCHAR*)malloc(Len * sizeof(WCHAR));
				GetWindowTextW(hWnd, szTextW, Len);
			} else
			{
				szTextT = (TCHAR*)malloc(Len * sizeof(TCHAR));
				GetWindowText(hWnd, szTextT, Len);
			}
			HFONT hOldFont = (HFONT)SelectObject(hdcMem, dat->hFont);
			SetBkMode(hdcMem, TRANSPARENT);
			if (hTheme && pDrawThemeText && pGetThemeTextExtent)
			{
				pDrawThemeText(hTheme, hdcMem, BP_GROUPBOX, IsWindowEnabled(hWnd) ? GBS_NORMAL : GBS_DISABLED, szTextW, -1, DT_LEFT | DT_VCENTER | DT_SINGLELINE, 0, &rc);
			} else
			{
				DrawText(hdcMem, szTextT, -1, &rc, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
			}
			if (GetFocus() == hWnd)
			{
				RECT rcText = {0};
				if (hTheme && pDrawThemeText && pGetThemeTextExtent)
				{
					pGetThemeTextExtent(hTheme, hdcMem, BP_GROUPBOX, IsWindowEnabled(hWnd) ? GBS_NORMAL : GBS_DISABLED, szTextW, -1, DT_CALCRECT | DT_LEFT | DT_VCENTER | DT_SINGLELINE, 0, &rcText);
				} else
				{
					SIZE size;
					GetTextExtentPoint32(hdcMem, szTextT, lstrlen(szTextT), &size);
					rcText.right = size.cx;
					rcText.bottom = size.cy;
				}
				rcText.bottom = rc.bottom;
				OffsetRect(&rcText, rc.left, 0);
				InflateRect(&rcText, 1, -1);
				DrawFocusRect(hdcMem, &rcText);
			}
			free((hTheme && pDrawThemeText && pGetThemeTextExtent) ? (TCHAR*)szTextW : szTextT);
			SelectObject(hdcMem, hOldFont);
			if (hTheme && pCloseThemeData)
			{
				pCloseThemeData(hTheme);
			}
		  BitBlt(hdc, 0, 0, rc.right, rc.bottom, hdcMem, 0, 0, SRCCOPY);
			SelectObject(hdcMem, hbmOld);
			DeleteObject(hbmMem);
			DeleteDC(hdcMem);
			EndPaint(hWnd, &ps);
			return 0;
		} break;
		case WM_DESTROY:
		{
			if (dat->hFont)
			{
				DeleteObject(dat->hFont);
			}
			SetWindowLongPtr(hWnd, GWLP_USERDATA, NULL);
			CallWindowProc(dat->OldWndProc, hWnd, Msg, wParam, lParam);
			delete dat;
			return 0;
		} break;
	}
	return CallWindowProc(dat->OldWndProc, hWnd, Msg, wParam, lParam);
}

int MakeGroupCheckbox(HWND hWndCheckbox)
{ // workaround to make SetTextColor work in WM_CTLCOLORSTATIC with windows themes enabled
	CCheckboxData *dat = new CCheckboxData();
	dat->OldWndProc = (WNDPROC)GetWindowLongPtr(hWndCheckbox, GWLP_WNDPROC);
	dat->State = SendMessage(hWndCheckbox, BM_GETSTATE, 0, 0);
	long Style = GetWindowLongPtr(hWndCheckbox, GWL_STYLE);
	dat->Style = Style & (BS_CHECKBOX | BS_AUTOCHECKBOX | BS_3STATE | BS_AUTO3STATE);
	_ASSERT(dat->Style == BS_CHECKBOX || dat->Style == BS_AUTOCHECKBOX || dat->Style == BS_3STATE || dat->Style == BS_AUTO3STATE);
  Style &= ~(BS_CHECKBOX | BS_AUTOCHECKBOX | BS_3STATE | BS_AUTO3STATE);
  Style |= BS_OWNERDRAW;
	SetWindowLongPtr(hWndCheckbox, GWL_STYLE, Style);
	SetWindowLongPtr(hWndCheckbox, GWLP_USERDATA, (LONG)dat);
	SetWindowLongPtr(hWndCheckbox, GWLP_WNDPROC, (LONG)CheckboxWndProc);
	SendMessage(hWndCheckbox, UM_INITCHECKBOX, 0, 0);
	return 0;
}