// TaskbarHider.cpp
// Native Win32 GUI app to hide running windows from the taskbar (send them to the background).
// Hidden windows are persisted to a data file so they survive app restarts.
// Compile (MinGW): g++ -mwindows -municode -o TaskbarHider.exe TaskbarHider.cpp -lole32 -luuid
// Compile (MSVC): cl /EHsc /D_WIN32_WINNT=0x0601 TaskbarHider.cpp ole32.lib uuid.lib user32.lib gdi32.lib

#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0601
#endif
#include <windows.h>
#include <windowsx.h>
#include <shobjidl.h>
#include <commctrl.h>
#include <psapi.h>
#include <vector>
#include <string>
#include <algorithm>
#include <cstdio>

#define IDC_REFRESH    100
#define IDC_HIDE       101
#define IDC_RESTORE    102
#define IDC_RESTOREALL 103
#define IDC_SCAN       104
#define IDC_LIST_VIS   200
#define IDC_LIST_HID   201
#define IDC_LABEL_TOP  300
#define IDC_LABEL_HID  301
#define WM_TRAYICON    (WM_APP + 1)
#define ID_TRAY_SHOW   900
#define ID_TRAY_EXIT   901
#define ID_TRAY_RESTORE_BASE 1000   // + index into g_hidden for per-window tray restore

static const wchar_t* APP_TITLE = L"Taskbar Hider";
static HWND g_hWnd = nullptr;
static HWND g_hListVis = nullptr;
static HWND g_hListHid = nullptr;
static HWND g_hStatus = nullptr;
static ITaskbarList* g_pTaskbar = nullptr;
static NOTIFYICONDATAW g_nid = { 0 };

// Persisted info about a hidden (background) window.
struct HiddenEntry { HWND hwnd; DWORD pid; std::wstring title; };
static std::vector<HiddenEntry> g_hidden;
static std::wstring g_storePath;

// ---- Window enumeration ----------------------------------------------------

struct EnumCtx { HWND self; };

static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
    auto* ctx = reinterpret_cast<EnumCtx*>(lParam);
    if (hwnd == ctx->self) return TRUE;
    if (!IsWindowVisible(hwnd)) return TRUE;

    LONG ex = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
    if (ex & WS_EX_TOOLWINDOW) return TRUE;            // already a tool/background window

    int len = GetWindowTextLengthW(hwnd);
    if (len == 0) return TRUE;                         // no title -> not a real app window

    wchar_t buf[512];
    GetWindowTextW(hwnd, buf, (int)_countof(buf));
    std::wstring title = buf;
    if (title == APP_TITLE) return TRUE;               // skip our own window

    DWORD pid = 0;
    GetWindowThreadProcessId(hwnd, &pid);
    wchar_t label[600];
    swprintf_s(label, (unsigned)_countof(label), L"[%04u] %s", pid, buf);

    int idx = (int)ListBox_AddString(g_hListVis, label);
    if (idx >= 0) ListBox_SetItemData(g_hListVis, idx, (LPARAM)hwnd);
    return TRUE;
}

// ---- Core hide / restore logic --------------------------------------------

static void ApplyHideStyle(HWND hwnd) {
    if (!hwnd || !IsWindow(hwnd)) return;
    LONG ex = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
    SetWindowLongPtr(hwnd, GWL_EXSTYLE, (ex | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW);
    if (g_pTaskbar) {
        if (SUCCEEDED(g_pTaskbar->HrInit())) g_pTaskbar->DeleteTab(hwnd);
    }
    ShowWindow(hwnd, SW_HIDE);
}

static void ApplyRestoreStyle(HWND hwnd) {
    if (!hwnd || !IsWindow(hwnd)) return;
    LONG ex = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
    SetWindowLongPtr(hwnd, GWL_EXSTYLE, (ex & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW);
    ShowWindow(hwnd, SW_SHOW);
    if (g_pTaskbar) {
        if (SUCCEEDED(g_pTaskbar->HrInit())) g_pTaskbar->AddTab(hwnd);
    }
    SetForegroundWindow(hwnd);
}

// Re-draw the hidden listbox from the g_hidden vector (single source of truth).
static void RebuildHiddenListBox() {
    ListBox_ResetContent(g_hListHid);
    for (auto& e : g_hidden) {
        wchar_t label[600];
        swprintf_s(label, (unsigned)_countof(label), L"[%04u] %s [hidden]", e.pid, e.title.c_str());
        int idx = (int)ListBox_AddString(g_hListHid, label);
        if (idx >= 0) ListBox_SetItemData(g_hListHid, idx, (LPARAM)e.hwnd);
    }
}

// Persist the hidden list (HWND + owning PID) to disk. The window title is not
// stored (it can contain unicode); it is re-read on load.
static void SaveHidden() {
    FILE* fp = _wfopen(g_storePath.c_str(), L"w");
    if (!fp) return;
    for (auto& e : g_hidden)
        fwprintf(fp, L"%llu %lu\n", (unsigned long long)e.hwnd, (unsigned long)e.pid);
    fclose(fp);
}

// Load previously hidden windows and keep only the ones that are still alive
// and owned by the same process (guards against HWND reuse).
static void LoadHidden() {
    FILE* fp = _wfopen(g_storePath.c_str(), L"r");
    if (!fp) return;
    unsigned long long h; unsigned long pid;
    while (fwscanf(fp, L"%llu %lu", &h, &pid) == 2) {
        HWND hw = (HWND)h;
        if (!IsWindow(hw)) continue;
        DWORD cpid = 0;
        GetWindowThreadProcessId(hw, &cpid);
        if (cpid != pid) continue;                    // HWND reused by another process
        wchar_t buf[512];
        GetWindowTextW(hw, buf, (int)_countof(buf));
        std::wstring title = buf;
        if (title.empty()) title = L"(no title)";
        g_hidden.push_back({ hw, pid, title });
    }
    fclose(fp);
    RebuildHiddenListBox();
    SaveHidden();                                     // rewrite, dropping dead entries
}

static void RefreshList() {
    ListBox_ResetContent(g_hListVis);
    EnumCtx ctx{ g_hWnd };
    EnumWindows(EnumWindowsProc, (LPARAM)&ctx);
}

// Find windows that were hidden by forcing WS_EX_TOOLWINDOW + SW_HIDE, even if
// they are NOT recorded in TaskbarHider_hidden.dat (e.g. hidden by an older
// build, or the data file was lost). These are exactly the windows we can restore.
static BOOL CALLBACK EnumHiddenProc(HWND hwnd, LPARAM) {
    if (hwnd == g_hWnd) return TRUE;
    if (IsWindowVisible(hwnd)) return TRUE;             // only windows that are hidden
    LONG ex = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
    if (!(ex & WS_EX_TOOLWINDOW)) return TRUE;          // only ones flagged as background
    int len = GetWindowTextLengthW(hwnd);
    if (len == 0) return TRUE;                          // skip faceless tool windows
    for (auto& e : g_hidden)                            // skip already-tracked
        if (e.hwnd == hwnd) return TRUE;

    wchar_t buf[512];
    GetWindowTextW(hwnd, buf, (int)_countof(buf));
    DWORD pid = 0;
    GetWindowThreadProcessId(hwnd, &pid);
    std::wstring title = buf;
    if (title.empty()) title = L"(no title)";
    g_hidden.push_back({ hwnd, pid, title });
    return TRUE;
}

static void ScanHidden() {
    EnumWindows(EnumHiddenProc, 0);
    RebuildHiddenListBox();
    SaveHidden();
}

// ---- Details panel removed: PID is shown inline in each list item ------------

// Reposition/resize every control from the current client rectangle so the UI
// can never overflow the window, at any size or DPI. Called on create and resize.
static void Layout(HWND hWnd) {
    RECT rc; GetClientRect(hWnd, &rc);
    int W = rc.right, H = rc.bottom;
    const int m = 12;
    const int btnW = 120;
    const int btnX = W - m - btnW;
    const int listX = m;
    int listW = btnX - m - m;
    if (listW < 140) listW = 140;
    const int btnH = 28, gap = 8, labelH = 18;

    int statusY = H - m - labelH;
    int contentTop = 8 + labelH + 2;                 // top list y
    int contentBottom = statusY - 6;
    int listH = (contentBottom - contentTop - (labelH + 2 + 10)) / 2;
    if (listH < 40) listH = 40;

    SetWindowPos(GetDlgItem(hWnd, IDC_LABEL_TOP), nullptr, listX, 8, listW, labelH, SWP_NOZORDER);
    SetWindowPos(g_hListVis, nullptr, listX, contentTop, listW, listH, SWP_NOZORDER);
    SetWindowPos(GetDlgItem(hWnd, IDC_REFRESH), nullptr, btnX, contentTop, btnW, btnH, SWP_NOZORDER);
    SetWindowPos(GetDlgItem(hWnd, IDC_HIDE),    nullptr, btnX, contentTop + btnH + gap, btnW, btnH, SWP_NOZORDER);

    int hidLabelY = contentTop + listH + 10;
    int hidListY = hidLabelY + labelH + 2;
    SetWindowPos(GetDlgItem(hWnd, IDC_LABEL_HID), nullptr, listX, hidLabelY, listW, labelH, SWP_NOZORDER);
    SetWindowPos(g_hListHid, nullptr, listX, hidListY, listW, listH, SWP_NOZORDER);
    SetWindowPos(GetDlgItem(hWnd, IDC_RESTORE),    nullptr, btnX, hidListY, btnW, btnH, SWP_NOZORDER);
    SetWindowPos(GetDlgItem(hWnd, IDC_RESTOREALL), nullptr, btnX, hidListY + btnH + gap, btnW, btnH, SWP_NOZORDER);
    SetWindowPos(GetDlgItem(hWnd, IDC_SCAN),       nullptr, btnX, hidListY + 2 * (btnH + gap), btnW, btnH, SWP_NOZORDER);

    SetWindowPos(g_hStatus, nullptr, listX, statusY, listW, labelH, SWP_NOZORDER);
}

// ---- Tray icon -------------------------------------------------------------

static void AddTrayIcon() {
    g_nid.cbSize = sizeof(g_nid);
    g_nid.hWnd = g_hWnd;
    g_nid.uID = 1;
    g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
    g_nid.uCallbackMessage = WM_TRAYICON;
    g_nid.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
    wcscpy_s(g_nid.szTip, (unsigned)_countof(g_nid.szTip), APP_TITLE);
    Shell_NotifyIconW(NIM_ADD, &g_nid);
}

static void RemoveTrayIcon() {
    Shell_NotifyIconW(NIM_DELETE, &g_nid);
}

static void TrayMenuHandler(HWND hWnd, WPARAM wParam) {
    UINT id = LOWORD(wParam);
    if (id >= ID_TRAY_RESTORE_BASE) {                  // restore a specific hidden window
        int idx = (int)(id - ID_TRAY_RESTORE_BASE);
        if (idx >= 0 && idx < (int)g_hidden.size()) {
            HWND hw = g_hidden[idx].hwnd;
            if (IsWindow(hw)) ApplyRestoreStyle(hw);
            g_hidden.erase(g_hidden.begin() + idx);
            RebuildHiddenListBox();
            SaveHidden();
        }
        return;
    }
    switch (id) {
    case ID_TRAY_SHOW:
        if (IsWindowVisible(hWnd)) ShowWindow(hWnd, SW_HIDE);
        else { ShowWindow(hWnd, SW_RESTORE); SetForegroundWindow(hWnd); }
        break;
    case ID_TRAY_EXIT:
        DestroyWindow(hWnd);
        break;
    }
}

// ---- Window procedure ------------------------------------------------------

static LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_CREATE: {
        g_hWnd = hWnd;
        HINSTANCE hInst = GetModuleHandle(nullptr);

        // Controls are created with placeholder geometry; Layout() positions them.
        CreateWindowW(L"STATIC", L"Taskbar windows:", WS_CHILD | WS_VISIBLE,
            0, 0, 10, 10, hWnd, (HMENU)IDC_LABEL_TOP, hInst, nullptr);
        g_hListVis = CreateWindowW(L"LISTBOX", nullptr,
            WS_CHILD | WS_VISIBLE | WS_BORDER | LBS_NOTIFY | LBS_HASSTRINGS | LBS_EXTENDEDSEL | WS_VSCROLL,
            0, 0, 10, 10, hWnd, (HMENU)IDC_LIST_VIS, hInst, nullptr);

        CreateWindowW(L"BUTTON", L"Refresh", WS_CHILD | WS_VISIBLE,
            0, 0, 10, 10, hWnd, (HMENU)IDC_REFRESH, hInst, nullptr);
        CreateWindowW(L"BUTTON", L"Hide to tray", WS_CHILD | WS_VISIBLE,
            0, 0, 10, 10, hWnd, (HMENU)IDC_HIDE, hInst, nullptr);

        CreateWindowW(L"STATIC", L"Hidden (background):", WS_CHILD | WS_VISIBLE,
            0, 0, 10, 10, hWnd, (HMENU)IDC_LABEL_HID, hInst, nullptr);
        g_hListHid = CreateWindowW(L"LISTBOX", nullptr,
            WS_CHILD | WS_VISIBLE | WS_BORDER | LBS_NOTIFY | LBS_HASSTRINGS | LBS_EXTENDEDSEL | WS_VSCROLL,
            0, 0, 10, 10, hWnd, (HMENU)IDC_LIST_HID, hInst, nullptr);

        CreateWindowW(L"BUTTON", L"Restore", WS_CHILD | WS_VISIBLE,
            0, 0, 10, 10, hWnd, (HMENU)IDC_RESTORE, hInst, nullptr);
        CreateWindowW(L"BUTTON", L"Restore all", WS_CHILD | WS_VISIBLE,
            0, 0, 10, 10, hWnd, (HMENU)IDC_RESTOREALL, hInst, nullptr);
        CreateWindowW(L"BUTTON", L"Scan hidden", WS_CHILD | WS_VISIBLE,
            0, 0, 10, 10, hWnd, (HMENU)IDC_SCAN, hInst, nullptr);

        g_hStatus = CreateWindowW(L"STATIC", L"Tip: Shift/Ctrl+click to multi-select; Hide to tray / Restore act on all selected.",
            WS_CHILD | WS_VISIBLE, 0, 0, 10, 10, hWnd, nullptr, hInst, nullptr);

        HFONT hFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
        SendMessage(g_hListVis, WM_SETFONT, (WPARAM)hFont, TRUE);
        SendMessage(g_hListHid, WM_SETFONT, (WPARAM)hFont, TRUE);

        Layout(hWnd);                                   // position from real client rect

        RefreshList();
        LoadHidden();                                  // restore persisted hidden windows
        AddTrayIcon();
        return 0;
    }

    case WM_COMMAND: {
        int id = LOWORD(wParam);
        if (id == IDC_REFRESH) {
            RefreshList();
            SetWindowTextW(g_hStatus, L"Refreshed window list.");
        } else if (id == IDC_HIDE) {
            int nSel = ListBox_GetSelCount(g_hListVis);
            if (nSel <= 0) {
                SetWindowTextW(g_hStatus, L"Select one or more windows from the top list first.");
            } else {
                int* items = new int[nSel];
                ListBox_GetSelItems(g_hListVis, nSel, items);
                int done = 0;
                for (int k = 0; k < nSel; ++k) {
                    HWND hw = (HWND)ListBox_GetItemData(g_hListVis, items[k]);
                    if (!hw || !IsWindow(hw)) continue;
                    DWORD pid = 0;
                    GetWindowThreadProcessId(hw, &pid);
                    wchar_t buf[512];
                    GetWindowTextW(hw, buf, (int)_countof(buf));
                    std::wstring title = buf;
                    if (title.empty()) title = L"(no title)";
                    ApplyHideStyle(hw);
                    g_hidden.push_back({ hw, pid, title });
                    ++done;
                }
                delete[] items;
                RebuildHiddenListBox();
                SaveHidden();
                RefreshList();   // hidden windows drop out of the taskbar list
                std::wstring msg = std::to_wstring(done) +
                    (done == 1 ? L" window sent to system tray." : L" windows sent to system tray.");
                SetWindowTextW(g_hStatus, msg.c_str());
            }
        } else if (id == IDC_RESTORE) {
            int nSel = ListBox_GetSelCount(g_hListHid);
            if (nSel <= 0) {
                SetWindowTextW(g_hStatus, L"Select one or more hidden windows first.");
            } else {
                int* items = new int[nSel];
                ListBox_GetSelItems(g_hListHid, nSel, items);
                // Restore in descending index order so erasing doesn't shift remaining ones.
                std::sort(items, items + nSel, std::greater<int>());
                int done = 0;
                for (int k = 0; k < nSel; ++k) {
                    int idx = items[k];
                    if (idx < 0 || idx >= (int)g_hidden.size()) continue;
                    HWND hw = g_hidden[idx].hwnd;
                    if (IsWindow(hw)) { ApplyRestoreStyle(hw); ++done; }
                    g_hidden.erase(g_hidden.begin() + idx);
                }
                delete[] items;
                RebuildHiddenListBox();
                SaveHidden();
                std::wstring msg = std::to_wstring(done) +
                    (done == 1 ? L" window restored to taskbar." : L" windows restored to taskbar.");
                SetWindowTextW(g_hStatus, msg.c_str());
            }
        } else if (id == IDC_RESTOREALL) {
            for (int i = (int)g_hidden.size() - 1; i >= 0; --i) {
                if (IsWindow(g_hidden[i].hwnd)) ApplyRestoreStyle(g_hidden[i].hwnd);
            }
            g_hidden.clear();
            RebuildHiddenListBox();
            SaveHidden();
            SetWindowTextW(g_hStatus, L"All hidden windows restored.");
        } else if (id == IDC_SCAN) {
            int before = (int)g_hidden.size();
            ScanHidden();
            int added = (int)g_hidden.size() - before;
            if (added > 0)
                SetWindowTextW(g_hStatus, L"Found and listed hidden windows not in the data file.");
            else
                SetWindowTextW(g_hStatus, L"No extra hidden windows found.");
        }
        return 0;
    }

    case WM_SIZE:
        if (wParam != SIZE_MINIMIZED) Layout(hWnd);
        return 0;

    case WM_TRAYICON: {
        if (LOWORD(lParam) == WM_RBUTTONUP) {
            POINT pt; GetCursorPos(&pt);
            HMENU hMenu = CreatePopupMenu();
            AppendMenuW(hMenu, MF_STRING, ID_TRAY_SHOW, L"Show / Hide Taskbar Hider");
            if (!g_hidden.empty()) {
                AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr);
                AppendMenuW(hMenu, MF_STRING | MF_DISABLED | MF_GRAYED, 0, L"Hidden to tray:");
                for (size_t i = 0; i < g_hidden.size(); ++i) {
                    std::wstring t = L"  Restore: " + g_hidden[i].title;
                    if (t.size() > 50) { t.resize(47); t += L"..."; }
                    AppendMenuW(hMenu, MF_STRING, ID_TRAY_RESTORE_BASE + (UINT)i, t.c_str());
                }
            }
            AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr);
            AppendMenuW(hMenu, MF_STRING, ID_TRAY_EXIT, L"Exit");
            SetForegroundWindow(hWnd);
            TrackPopupMenu(hMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, hWnd, nullptr);
            DestroyMenu(hMenu);
        } else if (LOWORD(lParam) == WM_LBUTTONDBLCLK) {
            ShowWindow(hWnd, SW_RESTORE);
            SetForegroundWindow(hWnd);
        }
        return 0;
    }

    case WM_SYSCOMMAND: {
        if ((wParam & 0xFFF0) == SC_MINIMIZE) {
            ShowWindow(hWnd, SW_HIDE);   // minimize => go to tray
            return 0;
        }
        break;
    }

    case WM_CLOSE: {
        ShowWindow(hWnd, SW_HIDE);       // closing hides to tray; tray "Exit" quits
        return 0;
    }

    case WM_DESTROY: {
        RemoveTrayIcon();
        if (g_pTaskbar) { g_pTaskbar->Release(); g_pTaskbar = nullptr; }
        PostQuitMessage(0);
        return 0;
    }
    }
    return DefWindowProcW(hWnd, msg, wParam, lParam);
}

int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, LPWSTR, int) {
    // Resolve the data file path: <exe dir>\TaskbarHider_hidden.dat
    wchar_t exePath[MAX_PATH];
    GetModuleFileNameW(nullptr, exePath, MAX_PATH);
    std::wstring path(exePath);
    size_t pos = path.find_last_of(L"\\/");
    path = (pos == std::wstring::npos) ? L"" : path.substr(0, pos + 1);
    g_storePath = path + L"TaskbarHider_hidden.dat";

    CoInitialize(nullptr);
    if (FAILED(CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_ALL,
                               IID_ITaskbarList, (void**)&g_pTaskbar))) {
        g_pTaskbar = nullptr;
    }

    WNDCLASSW wc = { 0 };
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInst;
    wc.lpszClassName = L"TaskbarHiderClass";
    wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    RegisterClassW(&wc);

    HWND hWnd = CreateWindowW(wc.lpszClassName, APP_TITLE,
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 560, 560,
        nullptr, nullptr, hInst, nullptr);
    g_hWnd = hWnd;
    ShowWindow(hWnd, SW_SHOW);
    UpdateWindow(hWnd);

    MSG msg;
    while (GetMessageW(&msg, nullptr, 0, 0)) {
        // Route tray popup-menu commands (WM_COMMAND with our IDs) to the handler.
        if (msg.message == WM_COMMAND && msg.hwnd == hWnd) {
            UINT id = LOWORD(msg.wParam);
            if (id == ID_TRAY_SHOW || id == ID_TRAY_EXIT || id >= ID_TRAY_RESTORE_BASE) {
                TrayMenuHandler(hWnd, msg.wParam);
                continue;
            }
        }
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    CoUninitialize();
    return 0;
}
