#include <utility>
#include <windows.h>

#include "win.h"

void WithNextWindow(void (*proc)(HWND))
{
	/* WithNextWindow uses a CBT hook to call an arbitrary
	 * procedure just before the creation of the next window. The
	 * hook is removed as soon as HCBT_CREATEWND is received.
	 * Thus, proc is guaranteed to be executed only once. Note
	 * that static storage must be used, as SetWindowHookEx cannot
	 * accept a capturing lambda as the hook procedure. */

	static __thread auto procNext = proc;
	static __thread HHOOK hHook = Require(SetWindowsHookEx(WH_CBT, [](const int nCode,
	    const WPARAM wParam, const LPARAM lParam) -> LRESULT CALLBACK
	{
		if (nCode == HCBT_CREATEWND) {
			Require(UnhookWindowsHookEx(hHook));
			procNext(reinterpret_cast<HWND>(wParam));
			return 0;
		} else
			return CallNextHookEx(0, nCode, wParam, lParam);
	}, nullptr, GetCurrentThreadId()));
}

/* Display next created window in the center of given window. */
static void CenterNextWindow(HWND hWnd)
{
	if (!hWnd)
		return;

	/* CenterNextWindow employs a trick similar to that above.
	 * Note, however, that repositioning does not work unless it
	 * is delayed until the new window is activated. This
	 * complicates the code somewhat. */

	static __thread HWND hWndParent;
	static __thread HWND hWndNext;
	static __thread HHOOK hHook;

	hWndParent = hWnd;
	hWndNext = nullptr;
	hHook = Require(SetWindowsHookEx(WH_CBT, [](const int nCode,
	    const WPARAM wParam, const LPARAM lParam) noexcept -> LRESULT CALLBACK
	{
		if (!hWndNext && nCode == HCBT_CREATEWND) {
			hWndNext = reinterpret_cast<HWND>(wParam);
			return 0;
		} else if (nCode == HCBT_ACTIVATE
		    && reinterpret_cast<HWND>(wParam) == hWndNext) {
			Require(UnhookWindowsHookEx(hHook));

			long lStyle = GetWindowLong(hWndNext, GWL_STYLE);
			if (!(lStyle & WS_POPUP)) return 0;

			RECT rcMain, rcMsg;
			GetWindowRect(hWndParent, &rcMain);
			GetWindowRect(hWndNext, &rcMsg);
			SetWindowPos(hWndNext, nullptr,
			    rcMain.left+(rcMain.right-rcMain.left)/2-(rcMsg.right-rcMsg.left)/2,
			    rcMain.top+(rcMain.bottom-rcMain.top)/2-(rcMsg.bottom-rcMsg.top)/2,
			    -1, -1,
			    SWP_NOZORDER|SWP_NOSIZE|SWP_NOACTIVATE);

			return 0;
		} else
			return CallNextHookEx(0, nCode, wParam, lParam);
	}, nullptr, GetCurrentThreadId()));
}

int EBMessageBox(const std::wstring_view text, const std::wstring_view caption, const UINT uType)
{
	extern HWND g_hWnd;
	CenterNextWindow(g_hWnd);
	return MessageBox(g_hWnd, text.data(), caption.data(), uType);
}

int GetRelativeCursorPos(const HWND hWnd, POINT* const pt) noexcept
{
	RECT rc;
	if (!GetClientRect(hWnd, &rc)) return 0;
	SetLastError(ERROR_SUCCESS);
	if (!MapWindowPoints(hWnd, nullptr, (POINT*)&rc, 2)) return 0;

	POINT ptMouse;
	if (!GetCursorPos(&ptMouse)) return 0;
	pt->x = ptMouse.x-rc.left;
	pt->y = ptMouse.y-rc.top;
	return 1;
}

Win32Error::Win32Error() noexcept
	: code(GetLastError()) {}

Win32Error::Win32Error(const DWORD code) noexcept
	: code(code) {}

Win32Error::~Win32Error()
{
	if (m_szMsg)
		HeapFree(GetProcessHeap(), 0, m_szMsg);
	if (m_wszMsg)
		HeapFree(GetProcessHeap(), 0, m_wszMsg);
}

const char* Win32Error::what() const noexcept
{
	if (!m_szMsg)
		FormatMessageA(
		    FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,
		    nullptr,
		    code,
		    MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
		    (char*)&m_szMsg,
		    0, nullptr);
	return m_szMsg;
}

const wchar_t* Win32Error::What() const noexcept
{
	if (!m_wszMsg)
		FormatMessage(
		    FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,
		    nullptr,
		    code,
		    MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
		    (wchar_t*)&m_wszMsg,
		    0, nullptr);
	return m_wszMsg;
}

std::optional<Library> Library::Maybe(const wchar_t* const lib) noexcept
{
	HMODULE hModule = LoadLibrary(lib);
	if (!hModule) return {};
	return Library(hModule);
}

Library::Library(const HMODULE hModule) noexcept
	: m_hModule(hModule) {}

Library::Library(const wchar_t* const lib)
{
	m_hModule = LoadLibrary(lib);
	if (!m_hModule)
		throw Win32Error();
}

Library::~Library()
{
	FreeLibrary(m_hModule);
}