#include "animia/win/win32.h"
#include "animia.h"
#include "animia/util/win32.h"
#include "animia/win.h"

#include <set>
#include <string>

#include <windows.h>

namespace animia::internal::win32 {

static std::wstring GetWindowClassName(HWND hwnd) {
	// The maximum size for lpszClassName, according to the documentation of
	// WNDCLASSEX structure
	constexpr int kMaxSize = 256;

	std::wstring buffer(kMaxSize, L'\0');
	const auto size = ::GetClassNameW(hwnd, &buffer.front(), buffer.length());
	/* for some reason GetClassName returns the actual size of the buffer *with* the
	   terminating NULL byte */
	buffer.resize(size);
	return buffer;
}

static std::wstring GetWindowText(HWND hwnd) {
	const int estimated_size = ::GetWindowTextLengthW(hwnd);

	std::wstring buffer(estimated_size + 1, L'\0');
	const auto size = ::GetWindowTextW(hwnd, &buffer.front(), buffer.length());
	/* GetWindowTextLength docs:
	   "Under certain conditions, the GetWindowTextLength function may return a value
	    that is larger than the actual length of the text." */
	buffer.resize(size);
	return buffer;
}

static DWORD GetWindowProcessId(HWND hwnd) {
	DWORD process_id = 0;
	::GetWindowThreadProcessId(hwnd, &process_id);
	return process_id;
}

static std::wstring GetProcessPath(DWORD process_id) {
	// If we try to open a SYSTEM process, this function fails and the last error
	// code is ERROR_ACCESS_DENIED.
	//
	// Note that if we requested PROCESS_QUERY_INFORMATION access right instead
	// of PROCESS_QUERY_LIMITED_INFORMATION, this function would fail when used
	// to open an elevated process.
	Handle process_handle(::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id));

	if (!process_handle)
		return std::wstring();

	std::wstring buffer(MAX_PATH, L'\0');
	DWORD buf_size = buffer.length();

	// Note that this function requires Windows Vista or above. You may use
	// GetProcessImageFileName or GetModuleFileNameEx on earlier versions.
	if (!::QueryFullProcessImageNameW(process_handle.get(), 0, &buffer.front(), &buf_size))
		return std::wstring();

	buffer.resize(buf_size);
	return buffer;
}

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

static bool VerifyWindowStyle(HWND hwnd) {
	const auto window_style = ::GetWindowLong(hwnd, GWL_STYLE);
	const auto window_ex_style = ::GetWindowLong(hwnd, GWL_EXSTYLE);

	auto has_style = [&window_style](DWORD style) { return (window_style & style) != 0; };
	auto has_ex_style = [&window_ex_style](DWORD ex_style) { return (window_ex_style & ex_style) != 0; };

	// Toolbars, tooltips and similar topmost windows
	if (has_style(WS_POPUP) && has_ex_style(WS_EX_TOOLWINDOW))
		return false;
	if (has_ex_style(WS_EX_TOPMOST) && has_ex_style(WS_EX_TOOLWINDOW))
		return false;

	return true;
}

static bool VerifyClassName(const std::wstring& name) {
	static const std::set<std::wstring> invalid_names = {
	    // System classes
	    L"#32770",        // Dialog box
	    L"CabinetWClass", // Windows Explorer
	    L"ComboLBox",
	    L"DDEMLEvent",
	    L"DDEMLMom",
	    L"DirectUIHWND",
	    L"GDI+ Hook Window Class",
	    L"IME",
	    L"Internet Explorer_Hidden",
	    L"MSCTFIME UI",
	    L"tooltips_class32",
	};

	return !name.empty() && !invalid_names.count(name);
}

static bool VerifyProcessPath(const std::wstring& path) {
	return !path.empty() && !IsSystemDirectory(path);
}

static bool VerifyProcessFileName(const std::wstring& name) {
	static const std::set<std::wstring> invalid_names = {
	    // System files
	    L"explorer",   // Windows Explorer
	    L"taskeng",    // Task Scheduler Engine
	    L"taskhost",   // Host Process for Windows Tasks
	    L"taskhostex", // Host Process for Windows Tasks
	    L"Taskmgr",    // Task Manager
	};

	return !name.empty() && !invalid_names.count(name);
}

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

static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM param) {
	if (!::IsWindowVisible(hwnd))
		return TRUE;

	if (!VerifyWindowStyle(hwnd))
		return TRUE;

	Window window;
	window.id = static_cast<unsigned int>(reinterpret_cast<ULONG_PTR>(hwnd));
	window.text = ToUtf8String(GetWindowText(hwnd));

	{
		std::wstring class_name = GetWindowClassName(hwnd);
		window.class_name = ToUtf8String(class_name);
		if (!VerifyClassName(class_name))
			return TRUE;
	}

	Process process;
	process.pid = GetWindowProcessId(hwnd);

	const auto path = GetProcessPath(process.pid);
	if (!VerifyProcessPath(path))
		return TRUE;

	{
		std::wstring name = GetFileNameWithoutExtension(GetFileNameFromPath(path));
		process.name = ToUtf8String(name);
		if (!VerifyProcessFileName(name))
			return TRUE;
	}

	auto& window_proc = *reinterpret_cast<window_proc_t*>(param);
	if (!window_proc(process, window))
		return FALSE;

	return TRUE;
}

bool Win32WinTools::EnumerateWindows(window_proc_t window_proc) {
	if (!window_proc)
		return false;

	const auto param = reinterpret_cast<LPARAM>(&window_proc);

	// Note that EnumWindows enumerates only top-level windows of desktop apps
	// (as opposed to UWP apps) on Windows 8 and above.
	return ::EnumWindows(EnumWindowsProc, param) != FALSE;
}

} // namespace animia::internal::win32