// 9 october 2014
#define UNICODE
#define _UNICODE
#define STRICT
#define STRICT_TYPED_ITEMIDS
// get Windows version right; right now Windows XP
#define WINVER 0x0501
#define _WIN32_WINNT 0x0501
#define _WIN32_WINDOWS 0x0501		/* according to Microsoft's winperf.h */
#define _WIN32_IE 0x0600			/* according to Microsoft's sdkddkver.h */
#define NTDDI_VERSION 0x05010000	/* according to Microsoft's sdkddkver.h */
#include <windows.h>
#include <commctrl.h>
#include <stdint.h>
#include <uxtheme.h>
#include <string.h>
#include <wchar.h>
#include <windowsx.h>
#include <vsstyle.h>
#include <vssym32.h>

// #qo LIBS: user32 kernel32 gdi32

// TODO
// - investigate visual styles
// - make sure redrawing is correct (especially for backgrounds)
// - should the parent window appear deactivated?

HWND popover;

void xpanic(char *msg, DWORD err)
{
	printf("%d | %s\n", err, msg);
	abort();
}

#define ARROWHEIGHT 8
#define ARROWWIDTH 8		/* should be the same for smooth lines */

struct popover {
	void *gopopover;

	// a nice consequence of this design is that it allows four arrowheads to jut out at once; in practice only one will ever be used, but hey — simple implementation!
	LONG arrowLeft;
	LONG arrowRight;
	LONG arrowTop;
	LONG arrowBottom;
};

struct popover _p = { NULL, -1, -1, 20, -1 };
struct popover *p = &_p;

HRGN makePopoverRegion(HDC dc, LONG width, LONG height)
{
	POINT pt[20];
	int n;
	HRGN region;
	LONG xmax, ymax;

	if (BeginPath(dc) == 0)
		xpanic("error beginning path for Popover shape", GetLastError());
	n = 0;

	// figure out the xmax and ymax of the box
	xmax = width;
	if (p->arrowRight >= 0)
		xmax -= ARROWWIDTH;
	ymax = height;
	if (p->arrowBottom >= 0)
		ymax -= ARROWHEIGHT;

	// the first point is either at (0,0), (0,arrowHeight), (arrowWidth,0), or (arrowWidth,arrowHeight)
	pt[n].x = 0;
	if (p->arrowLeft >= 0)
		pt[n].x = ARROWWIDTH;
	pt[n].y = 0;
	if (p->arrowTop >= 0)
		pt[n].y = ARROWHEIGHT;
	n++;

	// the left side
	pt[n].x = pt[n - 1].x;
	if (p->arrowLeft >= 0) {
		pt[n].y = pt[n - 1].y + p->arrowLeft;
		n++;
		pt[n].x = pt[n - 1].x - ARROWWIDTH;
		pt[n].y = pt[n - 1].y + ARROWHEIGHT;
		n++;
		pt[n].x = pt[n - 1].x + ARROWWIDTH;
		pt[n].y = pt[n - 1].y + ARROWHEIGHT;
		n++;
		pt[n].x = pt[n - 1].x;
	}
	pt[n].y = ymax;
	n++;

	// the bottom side
	pt[n].y = pt[n - 1].y;
	if (p->arrowBottom >= 0) {
		pt[n].x = pt[n - 1].x + p->arrowBottom;
		n++;
		pt[n].x = pt[n - 1].x + ARROWWIDTH;
		pt[n].y = pt[n - 1].y + ARROWHEIGHT;
		n++;
		pt[n].x = pt[n - 1].x + ARROWWIDTH;
		pt[n].y = pt[n - 1].y - ARROWHEIGHT;
		n++;
		pt[n].y = pt[n - 1].y;
	}
	pt[n].x = xmax;
	n++;

	// the right side
	pt[n].x = pt[n - 1].x;
	if (p->arrowRight >= 0) {
		pt[n].y = pt[0].y + p->arrowRight + (ARROWHEIGHT * 2);
		n++;
		pt[n].x = pt[n - 1].x + ARROWWIDTH;
		pt[n].y = pt[n - 1].y - ARROWHEIGHT;
		n++;
		pt[n].x = pt[n - 1].x - ARROWWIDTH;
		pt[n].y = pt[n - 1].y - ARROWHEIGHT;
		n++;
		pt[n].x = pt[n - 1].x;
	}
	pt[n].y = pt[0].y;
	n++;

	// the top side
	pt[n].y = pt[n - 1].y;
	if (p->arrowTop >= 0) {
		pt[n].x = pt[0].x + p->arrowTop + (ARROWWIDTH * 2);
		n++;
		pt[n].x = pt[n - 1].x - ARROWWIDTH;
		pt[n].y = pt[n - 1].y - ARROWHEIGHT;
		n++;
		pt[n].x = pt[n - 1].x - ARROWWIDTH;
		pt[n].y = pt[n - 1].y + ARROWHEIGHT;
		n++;
		pt[n].y = pt[n - 1].y;
	}
	pt[n].x = pt[0].x;
	n++;

	if (Polyline(dc, pt, n) == 0)
		xpanic("error drawing lines in Popover shape", GetLastError());
	if (EndPath(dc) == 0)
		xpanic("error ending path for Popover shape", GetLastError());
	region = PathToRegion(dc);
	if (region == NULL)
		xpanic("error converting Popover shape path to region", GetLastError());
	return region;
}

#define msgPopoverPrepareLeftRight (WM_APP+50)
#define msgPopoverPrepareTopBottom (WM_APP+51)

LRESULT CALLBACK popoverproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	PAINTSTRUCT ps;
	HDC dc;
	HRGN region;
	RECT r;
	LONG width;
	LONG height;
	WINDOWPOS *wp;
	HBRUSH brush;

	switch (uMsg) {
	case WM_NCPAINT:
		if (GetWindowRect(hwnd, &r) == 0)
			xpanic("error getting Popover window rect for shape redraw", GetLastError());
		width = r.right - r.left;
		height = r.bottom - r.top;
		dc = GetWindowDC(hwnd);
		if (dc == NULL)
			xpanic("error getting Popover window DC for drawing border", GetLastError());
		region = makePopoverRegion(dc, width, height);
		// don't call FillRgn(); WM_ERASEBKGND seems to do this to the non-client area for us already :S (TODO confirm)
		// TODO arrow is black in wine
		brush = (HBRUSH) GetStockObject(BLACK_BRUSH);
		if (brush == NULL)
			xpanic("error getting Popover border brush", GetLastError());
		if (FrameRgn(dc, region, brush, 1, 1) == 0)
			xpanic("error drawing Popover border", GetLastError());
		if (DeleteObject(region) == 0)
			xpanic("error deleting Popover shape region", GetLastError());
		if (ReleaseDC(hwnd, dc) == 0)
			xpanic("error releasing Popover window DC for shape drawing", GetLastError());
		return 0;
	case WM_WINDOWPOSCHANGED:
		// this must be here; if it's in WM_NCPAINT weird things happen (see http://stackoverflow.com/questions/26288303/why-is-my-client-rectangle-drawing-behaving-bizarrely-pictures-provided-if-i-t)
		wp = (WINDOWPOS *) lParam;
		if ((wp->flags & SWP_NOSIZE) == 0) {
			dc = GetWindowDC(hwnd);
			if (dc == NULL)
				xpanic("error getting Popover window DC for reshaping", GetLastError());
			region = makePopoverRegion(dc, wp->cx, wp->cy);
			if (SetWindowRgn(hwnd, region, TRUE) == 0)
				xpanic("error setting Popover shape", GetLastError());
			// don't delete the region; the window manager owns it now
			if (ReleaseDC(hwnd, dc) == 0)
				xpanic("error releasing Popover window DC for reshaping", GetLastError());
		}
		break;		// defer to DefWindowProc()
	case WM_NCCALCSIZE:
		{
			RECT *r = (RECT *) lParam;
			NCCALCSIZE_PARAMS *np = (NCCALCSIZE_PARAMS *) lParam;

			if (wParam != FALSE)
				r = &np->rgrc[0];
			r->left++;
			r->top++;
			r->right--;
			r->bottom--;
			if (p->arrowLeft >= 0)
				r->left += ARROWWIDTH;
			if (p->arrowRight >= 0)
				r->right -= ARROWWIDTH;
			if (p->arrowTop >= 0)
				r->top += ARROWHEIGHT;
			if (p->arrowBottom >= 0)
				r->bottom -= ARROWHEIGHT;
			return 0;
		}
	case WM_PAINT:
		dc = BeginPaint(hwnd, &ps);
		GetClientRect(hwnd, &r);
		FillRect(dc, &r, GetSysColorBrush(COLOR_ACTIVECAPTION));
		FrameRect(dc, &r, GetStockPen(WHITE_BRUSH));
		EndPaint(hwnd, &ps);
		return 0;
	case msgPopoverPrepareLeftRight:
	case msgPopoverPrepareTopBottom:
		// TODO window edge detection
		{
			RECT r;
			LONG x, y;
			LONG width = 200, height = 200;

			if (GetWindowRect((HWND) wParam, &r) == 0)
				xpanic("error getting window rect of Popover target", GetLastError());
			width += 2;
			height += 2;
			p->arrowLeft = -1;
			p->arrowRight = -1;
			p->arrowTop = -1;
			p->arrowBottom = -1;
			if (uMsg == msgPopoverPrepareLeftRight) {
				width += ARROWWIDTH;
				p->arrowLeft = height / 2 - ARROWHEIGHT;
				x = r.right;
				y = r.top - ((height - (r.bottom - r.top)) / 2);
			} else {
				height += ARROWHEIGHT;
				p->arrowTop = width / 2 - ARROWWIDTH;
				x = r.left - ((width - (r.right - r.left)) / 2);
				y = r.bottom;
			}
			if (MoveWindow(hwnd, x, y, width, height, TRUE) == 0)
				xpanic("error repositioning Popover", GetLastError());
		}
		return 0;
	}
	return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}

HWND button;

LRESULT CALLBACK wndproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {
	case WM_COMMAND:
		if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == 100) {
			SendMessageW(popover, msgPopoverPrepareLeftRight, (WPARAM) button, 0);
			ShowWindow(popover, SW_SHOW);
			UpdateWindow(popover);
			return 0;
		}
		break;
	case WM_CLOSE:
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}

int main(int argc, char *argv[])
{
	WNDCLASSW wc;
	HWND mainwin;
	MSG msg;

	ZeroMemory(&wc, sizeof (WNDCLASSW));
	wc.lpszClassName = L"popover";
	wc.lpfnWndProc = popoverproc;
	wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
	wc.style = CS_DROPSHADOW | CS_NOCLOSE;
	if (RegisterClassW(&wc) == 0)
		abort();
	popover = CreateWindowExW(WS_EX_TOPMOST,
		L"popover", L"",
		WS_POPUP,
		0, 0, 150, 100,
		NULL, NULL, NULL, NULL);
	if (popover == NULL)
		abort();

	ZeroMemory(&wc, sizeof (WNDCLASSW));
	wc.lpszClassName = L"mainwin";
	wc.lpfnWndProc = wndproc;
	wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
	if (RegisterClassW(&wc) == 0)
		abort();
	mainwin = CreateWindowExW(0,
		L"mainwin", L"Main Window",
		WS_OVERLAPPEDWINDOW,
		0, 0, 150, 100,
		NULL, NULL, NULL, NULL);
	if (mainwin == NULL)
		abort();
	button = CreateWindowExW(0,
		L"button", L"Click Me",
		BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE,
		20, 20, 100, 40,
		mainwin, (HMENU) 100, NULL, NULL);
	if (button == NULL)
		abort();
	ShowWindow(mainwin, SW_SHOWDEFAULT);
	if (UpdateWindow(mainwin) == 0)
		abort();
	while (GetMessageW(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessageW(&msg);
	}
	return 0;
}