#include "TaskbarThread.h"
#include <Psapi.h>
#include <iomanip>
#include <sstream>
#include "../Utility.h"
#include <AppModel.h>
#include "App.h"
#include <Dwmapi.h>
#include <Propsys.h>
#include <Propkey.h>

std::string GetWindowClass(HWND hwnd)
{
	char buff[MAX_PATH];
	GetClassName(hwnd, buff, MAX_PATH);
	return buff;
}

std::string GetWindowTitle(HWND hwnd)
{
	DWORD_PTR length = 0;
	if (SendMessageTimeoutW(hwnd, WM_GETTEXTLENGTH, NULL, NULL, SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG, 200, &length) == 0) return std::string();
	if (length == 0) return std::string();

	WCHAR* buffer = new WCHAR[length + 1];

	if(SendMessageTimeoutW(hwnd, WM_GETTEXT, length+1, (LPARAM)buffer, SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG, 200, nullptr) == 0)
	{
		delete[] buffer;
		return std::string();
	}

	std::string title(Utility::WStrToStr(buffer));
	delete[] buffer;
	return title;
	/*

	int length = GetWindowTextLengthW(hwnd);
	if (length == 0) return std::string();

	std::string title;
	title.resize(length + 1);
	GetWindowTextW(hwnd, &title[0], title.size());

	return title;*/
}

LONG GetProcessId(HWND hwnd)
{
	LONG processId;
	GetWindowThreadProcessId(hwnd, (LPDWORD)&processId);
	return processId;
}

HANDLE GetProcessHandle(LONG processId)
{
	return OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
}

std::string GetProcessName(HANDLE processHandle)
{
	char buff[MAX_PATH];
	GetModuleBaseName(processHandle, NULL, buff, MAX_PATH);
	return buff;
}

std::string GetProcessPath(HANDLE processHandle)
{
	char buff[MAX_PATH];
	GetModuleFileNameEx(processHandle, NULL, buff, MAX_PATH);
	return buff;
}

std::string GetExeFileDescription(const std::string& path)
{
	std::string exeDescription;
	DWORD aVersionInfoSize;
	DWORD dwHandle;
	LPCWSTR pathW = Utility::StrToWStr(path).c_str();
	aVersionInfoSize = GetFileVersionInfoSizeW(pathW, &dwHandle);

	if (aVersionInfoSize != 0)
	{
		LPBYTE res = new BYTE[aVersionInfoSize];
		if (GetFileVersionInfoW(pathW, dwHandle, aVersionInfoSize, (void**)res))
		{
			DWORD* buffer;
			UINT length;
			DWORD   m_dwLangCharset;
			if (VerQueryValueW((void**)res, L"\\VarFileInfo\\Translation", (void**)&buffer, &length))
			{
				m_dwLangCharset = MAKELONG(HIWORD(buffer[0]), LOWORD(buffer[0]));
				DWORD* result;
				std::wstringstream stream;
				stream << L"\\StringFileInfo\\" << std::hex << std::setfill(L'0') << std::setw(8) << m_dwLangCharset << L"\\FileDescription";
				if (VerQueryValueW((void**)res, stream.str().c_str(), (void**)&result, &length))
				{
					exeDescription = Utility::WStrToStr((LPCWSTR)result);
				}
			}
		}

		delete[] res;
	}

	return exeDescription;
}

HWND GetLastPopupHwnd(HWND hwnd)
{
	int maxLevel = 50;
	HWND currentHwnd = hwnd;
	while (--maxLevel >= 0)
	{
		HWND last = GetLastActivePopup(currentHwnd);

		if (IsWindowVisible(last)) return last;

		if (last == currentHwnd) return (HWND)NULL;

		currentHwnd = last;
	}
	return (HWND)NULL;
}

BOOL CALLBACK EnumChildren(HWND hwnd, LPARAM param)
{
	std::string wndClass = GetWindowClass(hwnd);

	if (Utility::IsEqual(wndClass.c_str(), "Windows.UI.Core.CoreWindow", false))
	{
		bool* isValid = (bool*)param;
		(*isValid) = true;
	}
	return true;
}

auto shell = GetShellWindow();

bool IsValidWindow(HWND hwnd)
{
	if (hwnd == shell) return false;

	LONG exStyles = GetWindowLong(hwnd, GWL_EXSTYLE);
	if (exStyles & WS_EX_TOOLWINDOW)
	{
		return false;
	}

	LONG styles = GetWindowLong(hwnd, GWL_STYLE);

	if (styles & WS_VISIBLE)
	{
		if (!(styles & WS_CAPTION))
		{
			return false;
		}

		std::string winClass = GetWindowClass(hwnd);
		if (Utility::IsEqual(winClass.c_str(), "ApplicationFrameWindow", false))
		{
			return false;
		}

		return true;
	}

	return false;
}

BOOL CALLBACK EnumWindows(HWND hwnd, LPARAM lparam)
{
	if (!IsValidWindow(hwnd)) return true;



	/*
	* Best way i found to distinguish apps from exe's is that the uwp add is only valid if it is an ApplicationFrameWindow with a child window that is Windows.UI.Core.CoreWindow
	* It seems that when windows sends the app to the background that it detaches the Windows.UI.Core.CoreWindow and lets it stay as a separate top level window.
	* Not sure why they would do it this way, but it is how it is
	* NOT: Seems that when an application is minimized that it also is detached for some stupid fucking reason...
	* By removing the code beneath will there sometimes be that uwp applications will show even though they are not opened and have even never been active since boot
	* This is just some stupid problem with the ApplicationFrameWindow...
	*
	* The current solution is:
	* if CoreWindow glued to ApplicationFrameWindow, show in taskbar
	* if ApplicationFrameWindow is minimized, show in taskbar
	* else hide from taskbar
	*
	* This is because it seems that app windows are always displaying, but somehow not rendering when you start the computer, but when activated they start rendering.
	*/

	// LONG styles = GetWindowLong(hwnd, GWL_STYLE);


	/*if (!Utility::IsEqual(windowClass.c_str(), "ApplicationFrameWindow", false) || styles & WS_MINIMIZE)
	{
		// TODO: DO THIS SHIT TREVOR, NOW >:D
		// auto& vd = VirtualDesktopMeasure::virtualDesktop;
		// UINT id;
		// HRESULT hr = vd.getWindowDesktopId(hwnd, &id);
		// if(SUCCEEDED(hr) && id == vd.getCurrentDesktop())
		{
			bool isValidWindow = false;
			EnumChildWindows(hwnd, EnumChildren, (LPARAM)&isValidWindow);
			if (!isValidWindow) return true;
		}
	}*/

	std::vector<HWND>& handles = *(std::vector<HWND>*)lparam;
	handles.push_back(hwnd);
	return true;
}

void TaskbarThread::Update(TaskbarThread* taskbar, bool runOnce)
{
	if(!runOnce)
	{
		taskbar->m_IsRunning = true;
	}

	while (true)
	{
		std::map<HWND, App>* programMap;

		{
			std::lock_guard<std::mutex> guard(taskbar->m_Mutex);

			programMap = &taskbar->m_ProgramMap;
		} // destroy lock

		std::vector<HWND> handles;
		handles.reserve(programMap->size());
		shell = GetShellWindow();
		EnumWindows(EnumWindows, (LPARAM)&handles);
		for (auto it = handles.begin(); it != handles.end(); ++it)
		{
			auto hwnd = *it;
			std::string windowTitle = GetWindowTitle(hwnd);
			bool isVisible = IsWindowVisible(hwnd);
			if (programMap->find(hwnd) == programMap->end())
			{

				std::string windowClass = GetWindowClass(hwnd);
				auto processId = GetProcessId(hwnd);
				HANDLE processHandle = GetProcessHandle(processId);
				HANDLE testHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);

				std::string processPath;
				std::string processName;
				std::string exeDescription;

				std::string uwpLocation;
				bool isUwp = false;

				IPropertyStore *pps;
				HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_PPV_ARGS(&pps));
				if(SUCCEEDED(hr))
				{
					PROPVARIANT var;

					hr = pps->GetValue(PKEY_AppUserModel_ID, &var);
					pps->Release();
				}

				if (!Utility::IsEqual(windowClass.c_str(), "ApplicationFrameWindow", false))
				{
					processPath = GetProcessPath(processHandle);
					processName = GetProcessName(processHandle);
					exeDescription = GetExeFileDescription(processPath);
					if (Utility::IsEqual(exeDescription.c_str(), "", false) || Utility::IsEqual(exeDescription.c_str(), "COM Surrogate", false))
					{
						auto it = processName.find(L'.');
						if (it == std::string::npos)
						{
							exeDescription = processName;
						}
						else
						{
							exeDescription = processName.substr(0, it);
						}

						if (Utility::IsEqual(exeDescription.c_str(), "DllHost", false))
						{
							exeDescription = "Properties";
						}
					}

					std::string test;
					test.resize(200);
					UINT32 l =0;
					LONG result = GetApplicationUserModelId(testHandle, &l, NULL);

					if(result != ERROR_INSUFFICIENT_BUFFER)
					{
						if(result == APPMODEL_ERROR_NO_APPLICATION)
						{
							int i = 123;
						}
						else
						{
							int i = 123;

						}
					}

				}
				else // Is UWP app
				{
					isUwp = true;
					HWND child = FindWindowEx(hwnd, NULL, "Windows.UI.Core.CoreWindow", NULL);
					if (child)
					{
						auto cprocId = GetProcessId(child);
						HANDLE cprocHandle = GetProcessHandle(cprocId);
						uwpLocation = GetProcessPath(cprocHandle);
						UINT32 length = 0;
						LONG rc = GetPackageFamilyName(cprocHandle, &length, nullptr);
						if (rc != ERROR_INSUFFICIENT_BUFFER)
						{
							if (rc == APPMODEL_ERROR_NO_PACKAGE)
							{
								printf("Could not fetch package name, invalid package");
								printf("DEBUG: %s", std::to_string(rc).c_str());
								printf("DEBUG: %s", std::to_string(length).c_str());
								printf("DEBUG: %s", windowTitle.c_str());
							}
							else
							{
								printf("Could not fetch package name, unknown");
								printf("DEBUG: %s", std::to_string(rc).c_str());
								printf("DEBUG: %s", std::to_string(length).c_str());
								printf("DEBUG: %s", windowTitle.c_str());
							}
						}

						std::wstring name;
						name.resize(length);
						rc = GetPackageFamilyName(cprocHandle, &length, &name[0]);
						if (rc == ERROR_SUCCESS)
						{
							processName = Utility::WStrToStr(name);
							std::stringstream stream;
							stream << "Shell:AppsFolder\\" << processName.c_str() << "!App";
							processPath = stream.str();
						}
					}
				}

				CloseHandle(processHandle);

				if (processName.empty()) processName = windowTitle;
				if (exeDescription.empty()) exeDescription = windowTitle;

				App item;
				item.m_ProcessName = processName;
				item.m_WindowTitle = windowTitle;
				item.m_WindowClass = windowClass;
				item.m_ProgramPath = processPath;
				item.m_ProgramDescription = exeDescription;
				item.m_Hwnd = hwnd;
				item.m_VirtualDesktopId = 0;
				item.m_IsPinned = true; // TODO: Probably has to be set elsewhere or with some pinned table...
				item.m_ProcessId = processId;
				item.m_IsUWP = isUwp;
				item.m_RealPath = uwpLocation;
				item.m_IsVisible = isVisible;

				programMap->insert({ hwnd, item });
			}
			else
			{
				auto& item = programMap->at(hwnd);
				item.m_WindowTitle = windowTitle;
				item.m_IsVisible = isVisible;
			}
		}

		for (auto it2 = programMap->begin(); it2 != programMap->end(); )
		{
			bool found = false;
			for (auto it = handles.begin(); it != handles.end(); ++it)
			{
				if (*it == it2->first)
				{
					found = true;
				}
			}

			if (!found)
			{
				it2 = programMap->erase(it2);
			}
			else
			{
				++it2;
			}
		}

		std::vector<App> programs;

		for (auto it = programMap->begin(); it != programMap->end(); ++it)
		{
			programs.push_back(it->second);
		}

		{
			std::lock_guard<std::mutex> guard(taskbar->m_Mutex);
			taskbar->m_Programs = programs;
			taskbar->m_HasChanged = true;
		} // destroy lock

		if (runOnce)
		{
			return;
		}
		{
			std::unique_lock<std::mutex> guard(taskbar->m_Mutex);
			if (taskbar->m_StopThread.wait_for(guard, std::chrono::milliseconds(100), [&]() {return taskbar->m_Interrupted == true; }))
			{
				taskbar->m_IsRunning = false;
				guard.unlock();
				taskbar->m_ThreadStopped.notify_all();
				return;
			}
		}
	}
}

TaskbarThread::TaskbarThread() : m_Mutex()
{
	Update(this, true);
	m_Thread = std::thread(Update, this, false);
	m_Thread.detach();
}


TaskbarThread::~TaskbarThread()
{
	m_Interrupted = true;
	m_StopThread.notify_all();
	{
		std::unique_lock<std::mutex> guard(m_Mutex);
		m_ThreadStopped.wait(guard, [&]() {return m_IsRunning == false; });

	} // destroy lock

}

std::vector<App>& TaskbarThread::GetPrograms(bool grouped)
{
	if(m_HasChanged)
	{
		{
			std::lock_guard<std::mutex> guard(m_Mutex);
			m_ProgramsCache = m_Programs; // I hope the guard is destroyed after the copy, not sure tho TODO: Check this
			m_HasChanged = false;
		} // force guard release

		std::map<std::string, App> group;

		for(auto& item : m_ProgramsCache)
		{
			item.m_GroupCount = 1;
			if(group.find(item.m_ProgramPath) == group.end())
			{
				group[item.m_ProgramPath] = item;
			}
			else
			{
				group[item.m_ProgramPath].m_GroupCount++;
			}
			group[item.m_ProgramPath].m_Items.push_back(item);
		}

		m_ProgramsGroupedCache.clear();
		for (auto& it : group) m_ProgramsGroupedCache.push_back(it.second);
	}
	return grouped ? m_ProgramsGroupedCache : m_ProgramsCache;
}
