#include <utility>
#include <string>
#include <string_view>
#include <windows.h>
#include <wininet.h>

#include "util.h"
#include "win.h"

std::wstring WideFromNarrow(const std::string_view src, const int cp)
{
	int cchNarrow = src.length()+1;
	int cchWide = MultiByteToWideChar(cp, 0, src.data(), cchNarrow, nullptr, 0);
	std::wstring dst(cchWide, 0);
	if (!MultiByteToWideChar(cp, 0, src.data(), cchNarrow, dst.data(), cchWide))
		throw Win32Error();
	return dst;
}

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_local auto procNext = proc;
	static thread_local HHOOK hHook = Require(SetWindowsHookExW(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_local HWND hWndParent;
	static thread_local HWND hWndNext;
	static thread_local HHOOK hHook;

	hWndParent = hWnd;
	hWndNext = nullptr;
	hHook = Require(SetWindowsHookExW(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 = GetWindowLongW(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);
}

void ShowException(const wchar_t* const fmt, const wchar_t* const title, const UINT uType) noexcept
{
	try {
		std::rethrow_exception(std::current_exception());
	} catch (const WideException& e) {
		std::wstring msg(wcslen(fmt)+wcslen(e.What()), 0);
		Swprintf(msg, fmt, e.What());
		EBMessageBox(msg, title, uType);
	} catch (const std::exception& e) {
		std::wstring what = WideFromNarrow(e.what());
		std::wstring msg(wcslen(fmt)+what.length(), 0);
		Swprintf(msg, fmt, what.c_str());
		EBMessageBox(msg, title, uType);
	} catch (...) {
		const wchar_t* what = L"an unknown error occurred";
		std::wstring msg(wcslen(fmt)+wcslen(what), 0);
		Swprintf(msg, fmt, what);
		EBMessageBox(msg, title, 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(const DWORD code, const HMODULE hModule) noexcept
	: code(code), hModule(hModule) {}

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_FROM_HMODULE
		    |FORMAT_MESSAGE_IGNORE_INSERTS,
		    hModule,
		    code,
		    MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
		    (char*)&m_szMsg,
		    0, nullptr);
	return m_szMsg? m_szMsg: "An unknown error occurred";
}

const wchar_t* Win32Error::What() const noexcept
{
	if (!m_wszMsg)
		FormatMessageW(
		    FORMAT_MESSAGE_ALLOCATE_BUFFER
		    |FORMAT_MESSAGE_FROM_SYSTEM
		    |FORMAT_MESSAGE_FROM_HMODULE
		    |FORMAT_MESSAGE_IGNORE_INSERTS,
		    hModule,
		    code,
		    MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
		    (wchar_t*)&m_wszMsg,
		    0, nullptr);
	return m_wszMsg? m_wszMsg: L"An unknown error occurred";
}

InternetError::InternetError(DWORD codeSystem)
{
	if (codeSystem != ERROR_INTERNET_EXTENDED_ERROR)
		throw Win32Error(codeSystem, GetModuleHandle(L"wininet.dll"));

	DWORD len, cch;
	InternetGetLastResponseInfo(&code, nullptr, &len);

	cch = len+1;
	m_szMsg = new char[cch];
	if (!InternetGetLastResponseInfoA(&code, m_szMsg, &cch))
		throw Win32Error();

	cch = len+1;
	m_wszMsg = new wchar_t[cch];
	if (!InternetGetLastResponseInfoW(&code, m_wszMsg, &len))
		throw Win32Error();
}

InternetError::~InternetError()
{
	delete m_szMsg;
	delete m_wszMsg;
}

const char* InternetError::what() const noexcept
{
	return m_szMsg;
}

const wchar_t* InternetError::What() const noexcept
{
	return m_wszMsg;
}

std::optional<Library> Library::Maybe(const wchar_t* const lib) noexcept
{
	HMODULE hModule = LoadLibraryW(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 = LoadLibraryW(lib);
	if (!m_hModule)
		throw Win32Error();
}

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