#include <Windows.h>
#include <iostream>
#include <array>
#include <vector>

std::wstring get_process_path(DWORD pid) noexcept
{
  auto process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, TRUE, pid);
  std::wstring name;
  if (process != INVALID_HANDLE_VALUE)
  {
    name.resize(MAX_PATH);
    DWORD name_length = static_cast<DWORD>(name.length());
    if (QueryFullProcessImageNameW(process, 0, (LPWSTR)name.data(), &name_length) == 0)
    {
      name_length = 0;
    }
    name.resize(name_length);
    CloseHandle(process);
  }
  return name;
}

std::wstring get_process_path(HWND window) noexcept
{
  const static std::wstring app_frame_host = L"ApplicationFrameHost.exe";
  DWORD pid{};
  GetWindowThreadProcessId(window, &pid);
  auto name = get_process_path(pid);
  if (name.length() >= app_frame_host.length() &&
    name.compare(name.length() - app_frame_host.length(), app_frame_host.length(), app_frame_host) == 0)
  {
    // It is a UWP app. We will enumarate the windows and look for one created
    // by something with a different PID
    DWORD new_pid = pid;
    EnumChildWindows(window, [](HWND hwnd, LPARAM param) -> BOOL {
      auto new_pid_ptr = reinterpret_cast<DWORD*>(param);
      DWORD pid;
      GetWindowThreadProcessId(hwnd, &pid);
      if (pid != *new_pid_ptr)
      {
        *new_pid_ptr = pid;
        return FALSE;
      }
      else
      {
        return TRUE;
      }
    }, reinterpret_cast<LPARAM>(&new_pid));
    // If we have a new pid, get the new name.
    if (new_pid != pid)
    {
      return get_process_path(new_pid);
    }
  }
  return name;
}

std::string window_styles(LONG style)
{
  std::string result;
  if (style == 0)
    result = "WS_OVERLAPPED ";
#define TEST_STYLE(x) if ((style & x) == x) result += #x " ";
  TEST_STYLE(WS_POPUP);
  TEST_STYLE(WS_CHILD);
  TEST_STYLE(WS_MINIMIZE);
  TEST_STYLE(WS_VISIBLE);
  TEST_STYLE(WS_DISABLED);
  TEST_STYLE(WS_CLIPSIBLINGS);
  TEST_STYLE(WS_CLIPCHILDREN);
  TEST_STYLE(WS_MAXIMIZE);
  TEST_STYLE(WS_CAPTION);
  TEST_STYLE(WS_BORDER);
  TEST_STYLE(WS_DLGFRAME);
  TEST_STYLE(WS_VSCROLL);
  TEST_STYLE(WS_HSCROLL);
  TEST_STYLE(WS_SYSMENU);
  TEST_STYLE(WS_THICKFRAME);
  TEST_STYLE(WS_GROUP);
  TEST_STYLE(WS_TABSTOP);
  TEST_STYLE(WS_MINIMIZEBOX);
  TEST_STYLE(WS_MAXIMIZEBOX);
  TEST_STYLE(WS_ICONIC);
  TEST_STYLE(WS_SIZEBOX);
  TEST_STYLE(WS_TILEDWINDOW);
  TEST_STYLE(WS_OVERLAPPEDWINDOW);
  TEST_STYLE(WS_POPUPWINDOW);
  TEST_STYLE(WS_CHILDWINDOW);
#undef TEST_STYLE
  if (result.size() > 0)
    result.pop_back();
  return result;
}

std::string window_exstyles(LONG style)
{
  std::string result;
#define TEST_STYLE(x) if ((style & x) == x) result += #x " ";
  TEST_STYLE(WS_EX_DLGMODALFRAME);
  TEST_STYLE(WS_EX_NOPARENTNOTIFY);
  TEST_STYLE(WS_EX_TOPMOST);
  TEST_STYLE(WS_EX_ACCEPTFILES);
  TEST_STYLE(WS_EX_TRANSPARENT);
  TEST_STYLE(WS_EX_MDICHILD);
  TEST_STYLE(WS_EX_TOOLWINDOW);
  TEST_STYLE(WS_EX_WINDOWEDGE);
  TEST_STYLE(WS_EX_CLIENTEDGE);
  TEST_STYLE(WS_EX_CONTEXTHELP);
  TEST_STYLE(WS_EX_RIGHT);
  TEST_STYLE(WS_EX_LEFT);
  TEST_STYLE(WS_EX_RTLREADING);
  TEST_STYLE(WS_EX_LTRREADING);
  TEST_STYLE(WS_EX_LEFTSCROLLBAR);
  TEST_STYLE(WS_EX_RIGHTSCROLLBAR);
  TEST_STYLE(WS_EX_CONTROLPARENT);
  TEST_STYLE(WS_EX_STATICEDGE);
  TEST_STYLE(WS_EX_APPWINDOW);
  TEST_STYLE(WS_EX_OVERLAPPEDWINDOW);
  TEST_STYLE(WS_EX_PALETTEWINDOW);
  TEST_STYLE(WS_EX_LAYERED);
  TEST_STYLE(WS_EX_NOINHERITLAYOUT);
  TEST_STYLE(WS_EX_NOREDIRECTIONBITMAP);
  TEST_STYLE(WS_EX_LAYOUTRTL);
  TEST_STYLE(WS_EX_COMPOSITED);
#undef TEST_STYLE
  if (result.size() > 0)
    result.pop_back();
  return result;
}


bool is_system_window(HWND hwnd, const char* class_name)
{
  static auto system_classes = { "SysListView32", "WorkerW", "Shell_TrayWnd", "Shell_SecondaryTrayWnd", "Progman" };
  static auto system_hwnds = { GetDesktopWindow(), GetShellWindow() };
  for (auto system_hwnd : system_hwnds)
  {
    if (hwnd == system_hwnd)
    {
      return true;
    }
  }
  for (const auto& system_class : system_classes)
  {
    if (strcmp(system_class, class_name) == 0)
    {
      return true;
    }
  }
  return false;
}

static bool no_visible_owner(HWND window) noexcept
{
  auto owner = GetWindow(window, GW_OWNER);
  if (owner == nullptr)
  {
    return true; // There is no owner at all
  }
  if (!IsWindowVisible(owner))
  {
    return true; // Owner is invisible
  }
  RECT rect;
  if (!GetWindowRect(owner, &rect))
  {
    return false; // Could not get the rect, return true (and filter out the window) just in case
  }
  // Return false (and allow the window to be zonable) if the owner window size is zero
  // It is enough that the window is zero-sized in one dimension only.
  return rect.top == rect.bottom || rect.left == rect.right;
}

#define TEST_IF(condition) std::cout<<"\t" << #condition <<": " <<((condition) ? "true, window not zonable" : "false")<<"\n"; if (condition) rv = false;

bool test_window(HWND window)
{
  std::cout << "\n";
  std::cout << "HWND:         0x" << window << "\n";
  DWORD pid;
  GetWindowThreadProcessId(window, &pid);
  std::cout << "PID:          0x" << std::hex << pid << "\n";
  std::cout << "FOREGROUND:   0x" << GetForegroundWindow() << "\n";

  auto style = GetWindowLongPtr(window, GWL_STYLE);
  auto exStyle = GetWindowLongPtr(window, GWL_EXSTYLE);
  std::cout << "style:        0x" << std::hex << style << ": " << window_styles(style) << "\n";
  std::cout << "exStyle:      0x" << std::hex << exStyle << ": " << window_exstyles(exStyle) << " \n";
  std::array<char, 256> class_name;
  GetClassNameA(window, class_name.data(), static_cast<int>(class_name.size()));
  std::cout << "Window class: '" << class_name.data() << "' equals:\n";
  auto process_path = get_process_path(window);
  std::wcout<< L"Process path: " << process_path << L"\n";
  bool rv = true;
  std::cout << "Testing if the window is zonable:\n";
  TEST_IF(GetAncestor(window, GA_ROOT) != window);
  TEST_IF(!IsWindowVisible(window));
  if ((style & WS_POPUP) == WS_POPUP &&
      (style & WS_THICKFRAME) == 0 &&
      (style & WS_MINIMIZEBOX) == 0 &&
      (style & WS_MAXIMIZEBOX) == 0)
  {
    std::cout << "\t(style & WS_POPUP) && no frame nor max/min buttons: true, window not zonable\n";
  }
  else
  {
    std::cout << "\t(style & WS_POPUP) && no frame nor max/min buttons: false\n";
  }
  TEST_IF((style & WS_CHILD) == WS_CHILD);
  TEST_IF((style & WS_DISABLED) == WS_DISABLED);
  TEST_IF((exStyle & WS_EX_TOOLWINDOW) == WS_EX_TOOLWINDOW);
  TEST_IF((exStyle & WS_EX_NOACTIVATE) == WS_EX_NOACTIVATE);
  TEST_IF(is_system_window(window, class_name.data()));
  if (strcmp(class_name.data(), "Windows.UI.Core.CoreWindow") == 0 &&
      process_path.ends_with(L"SearchUI.exe"))
  {
    std::cout << "\tapp is Cortana: true, window not zonable\n";
  }
  else
  {
    std::cout << "\tapp is Cortana: false\n";
  }
  TEST_IF(!no_visible_owner(window));
  return rv;
}

LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
  static HWND hwnd = nullptr;
  if (nCode == HC_ACTION)
  {
    POINT point;
    GetCursorPos(&point);
    auto new_hwnd = WindowFromPoint(point);
    if (hwnd != new_hwnd) {
      hwnd = new_hwnd;
      if (test_window(hwnd))
      {
        std::cout << "Window is zonable\n";
      }
      else
      {
        std::cout << "Window is NOT zonable\n";
      }
    }
  }
  return CallNextHookEx(NULL, nCode, wParam, lParam);
}

int main()
{
  HHOOK hhkLowLevelKybd = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, 0, 0);
  MSG msg;
  while (!GetMessage(&msg, NULL, NULL, NULL))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  UnhookWindowsHookEx(hhkLowLevelKybd);
  return(0);
}
