// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "pch.h"
#include "WindowEmperor.h"

#include <CoreWindow.h>
#include <ScopedResourceLoader.h>
#include <WtExeUtils.h>
#include <til/hash.h>
#include <wil/token_helpers.h>
#include <winrt/TerminalApp.h>
#include <sddl.h>

#include "AppHost.h"
#include "resource.h"
#include "VirtualDesktopUtils.h"
#include "../../types/inc/User32Utils.hpp"
#include "../../types/inc/utils.hpp"

using namespace winrt;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Windows::Foundation;
using namespace ::Microsoft::Console;
using namespace std::chrono_literals;
using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;

#ifdef _WIN64
static constexpr ULONG_PTR TERMINAL_HANDOFF_MAGIC = 0x4c414e494d524554; // 'TERMINAL'
#else
static constexpr ULONG_PTR TERMINAL_HANDOFF_MAGIC = 0x4d524554; // 'TERM'
#endif

extern "C" IMAGE_DOS_HEADER __ImageBase;

// A convenience function around CommandLineToArgv.
static std::vector<winrt::hstring> commandlineToArgArray(const wchar_t* commandLine)
{
    int argc = 0;
    const wil::unique_hlocal_ptr<LPWSTR> argv{ CommandLineToArgvW(commandLine, &argc) };
    argc = std::max(argc, 0);

    std::vector<winrt::hstring> args;
    args.reserve(argc);
    for (int i = 0; i < argc; i++)
    {
        args.emplace_back(argv.get()[i]);
    }
    return args;
}

// Returns the length of a double-null encoded string *excluding* the trailing double-null character.
static wil::zwstring_view stringFromDoubleNullTerminated(const wchar_t* beg)
{
    auto end = beg;

    for (; *end; end += wcsnlen(end, SIZE_T_MAX) + 1)
    {
    }

    return { beg, gsl::narrow_cast<size_t>(end - beg) };
}

// Appends an uint32_t to a byte vector.
static void serializeUint32(std::vector<uint8_t>& out, uint32_t value)
{
    out.insert(out.end(), reinterpret_cast<const uint8_t*>(&value), reinterpret_cast<const uint8_t*>(&value) + sizeof(value));
}

// Parses an uint32_t from the input iterator. Performs bounds-checks.
// Returns an iterator that points past it.
static const uint8_t* deserializeUint32(const uint8_t* it, const uint8_t* end, uint32_t& val)
{
    if (static_cast<size_t>(end - it) < sizeof(uint32_t))
    {
        throw std::out_of_range("Not enough data for uint32_t");
    }
    val = *reinterpret_cast<const uint32_t*>(it);
    return it + sizeof(uint32_t);
}

// Writes a null-terminated string to `out`: A uint32_t length prefix,
// *including null byte*, followed by the string data, followed by the null-terminator.
static void serializeString(std::vector<uint8_t>& out, wil::zwstring_view str)
{
    const auto ptr = reinterpret_cast<const uint8_t*>(str.data());
    const auto len = str.size() + 1;
    serializeUint32(out, gsl::narrow<uint32_t>(len));
    out.insert(out.end(), ptr, ptr + len * sizeof(wchar_t));
}

// Counter-part to `serializeString`. Performs bounds-checks.
// Returns an iterator that points past it.
static const uint8_t* deserializeString(const uint8_t* it, const uint8_t* end, wil::zwstring_view& str)
{
    uint32_t len;
    it = deserializeUint32(it, end, len);

    const auto bytes = static_cast<size_t>(len) * sizeof(wchar_t);

    if (bytes == 0 || static_cast<size_t>(end - it) < bytes)
    {
        throw std::out_of_range("Not enough data for string content");
    }

    str = { reinterpret_cast<const wchar_t*>(it), len - 1 };
    return it + bytes;
}

struct Handoff
{
    wil::zwstring_view args;
    wil::zwstring_view env;
    wil::zwstring_view cwd;
    uint32_t show;
};

// Serializes all relevant parameters to a byte blob for a WM_COPYDATA message.
// This allows us to hand off an invocation to an existing instance of the Terminal.
static std::vector<uint8_t> serializeHandoffPayload(int nCmdShow)
{
    const auto args = GetCommandLineW();
    const wil::unique_environstrings_ptr envMem{ GetEnvironmentStringsW() };
    const auto env = stringFromDoubleNullTerminated(envMem.get());
    const auto cwd = wil::GetCurrentDirectoryW<std::wstring>();

    std::vector<uint8_t> out;
    serializeString(out, args);
    serializeString(out, env);
    serializeString(out, cwd);
    serializeUint32(out, static_cast<uint32_t>(nCmdShow));
    return out;
}

// Counterpart to serializeHandoffPayload.
static Handoff deserializeHandoffPayload(const uint8_t* beg, const uint8_t* end)
{
    Handoff result{};
    auto it = beg;
    it = deserializeString(it, end, result.args);
    it = deserializeString(it, end, result.env);
    it = deserializeString(it, end, result.cwd);
    it = deserializeUint32(it, end, result.show);
    return result;
}

// Either acquires unique ownership over the given `className` mutex,
// or attempts to pass the commandline to the existing instance.
static wil::unique_mutex acquireMutexOrAttemptHandoff(const wchar_t* className, int nCmdShow)
{
    // If the process that owns the mutex has not finished creating the window yet,
    // FindWindowW will return nullptr. We'll retry a few times just in case.
    // At the 1.5x growth rate, this will retry up to ~30s in total.
    for (DWORD sleep = 50; sleep < 10000; sleep += sleep / 2)
    {
        wil::unique_mutex mutex{ CreateMutexW(nullptr, TRUE, className) };
        if (GetLastError() == ERROR_ALREADY_EXISTS)
        {
            mutex.reset();
        }
        if (mutex)
        {
            return mutex;
        }

        // I found that FindWindow() with no other filters is substantially faster than
        // using FindWindowEx() and restricting the search to just HWND_MESSAGE windows.
        // In both cases it's quite fast though at ~1us/op vs ~3us/op (good old Win32 <3).
        if (const auto hwnd = FindWindowW(className, nullptr))
        {
            auto payload = serializeHandoffPayload(nCmdShow);
            const COPYDATASTRUCT cds{
                .dwData = TERMINAL_HANDOFF_MAGIC,
                .cbData = gsl::narrow<DWORD>(payload.size()),
                .lpData = payload.data(),
            };

            // Allow the existing instance to gain foreground rights.
            DWORD processId = 0;
            if (GetWindowThreadProcessId(hwnd, &processId) && processId)
            {
                AllowSetForegroundWindow(processId);
            }

            if (SendMessageTimeoutW(hwnd, WM_COPYDATA, 0, reinterpret_cast<LPARAM>(&cds), SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 5000, nullptr))
            {
                return {};
            }
        }

        Sleep(sleep);
    }

    return {};
}

static constexpr bool IsInputKey(WORD vkey) noexcept
{
    return vkey != VK_CONTROL &&
           vkey != VK_LCONTROL &&
           vkey != VK_RCONTROL &&
           vkey != VK_MENU &&
           vkey != VK_LMENU &&
           vkey != VK_RMENU &&
           vkey != VK_SHIFT &&
           vkey != VK_LSHIFT &&
           vkey != VK_RSHIFT &&
           vkey != VK_LWIN &&
           vkey != VK_RWIN &&
           vkey != VK_SNAPSHOT;
}

HWND WindowEmperor::GetMainWindow() const noexcept
{
    _assertIsMainThread();
    return _window.get();
}

AppHost* WindowEmperor::GetWindowById(uint64_t id) const noexcept
{
    _assertIsMainThread();

    for (const auto& window : _windows)
    {
        if (window->Logic().WindowProperties().WindowId() == id)
        {
            return window.get();
        }
    }
    return nullptr;
}

AppHost* WindowEmperor::GetWindowByName(std::wstring_view name) const noexcept
{
    _assertIsMainThread();

    for (const auto& window : _windows)
    {
        if (window->Logic().WindowProperties().WindowName() == name)
        {
            return window.get();
        }
    }
    return nullptr;
}

void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args)
{
    _assertIsMainThread();

    uint64_t id = args.Id();
    bool needsNewId = id == 0;
    uint64_t newId = 0;

    for (const auto& host : _windows)
    {
        const auto existingId = host->Logic().WindowProperties().WindowId();
        newId = std::max(newId, existingId);
        needsNewId |= existingId == id;
    }

    if (needsNewId)
    {
        args.Id(newId + 1);
    }

    auto host = std::make_shared<AppHost>(this, _app.Logic(), std::move(args));
    host->Initialize();

    _windowCount += 1;
    _windows.emplace_back(std::move(host));

    if (_windowCount == 1)
    {
        // The first CoreWindow is created implicitly by XAML and parented to the
        // first XAML island. We parent it to our initial window for 2 reasons:
        // * On Windows 10 the CoreWindow will show up as a visible window on the taskbar
        //   due to a WinUI bug, and this will hide it, because our initial window is hidden.
        // * When we DestroyWindow() the island it will destroy the CoreWindow,
        //   and it's not possible to recreate it. That's also a WinUI bug.
        //
        // Note that this must be done after the first window (= first island) is created.
        if (const auto coreWindow = winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread())
        {
            if (const auto interop = coreWindow.try_as<ICoreWindowInterop>())
            {
                HWND coreHandle = nullptr;
                if (SUCCEEDED(interop->get_WindowHandle(&coreHandle)) && coreHandle)
                {
                    SetParent(coreHandle, _window.get());
                }
            }
        }
    }
}

AppHost* WindowEmperor::_mostRecentWindow() const noexcept
{
    int64_t max = INT64_MIN;
    AppHost* mostRecent = nullptr;

    for (const auto& w : _windows)
    {
        const auto lastActivatedTime = w->GetLastActivatedTime();
        if (lastActivatedTime > max)
        {
            max = lastActivatedTime;
            mostRecent = w.get();
        }
    }

    return mostRecent;
}

void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
{
    std::wstring windowClassName;
    windowClassName.reserve(64); // "Windows Terminal Preview Admin 0123456789012345 0123456789012345"
#if defined(WT_BRANDING_RELEASE)
    windowClassName.append(L"Windows Terminal");
#elif defined(WT_BRANDING_PREVIEW)
    windowClassName.append(L"Windows Terminal Preview");
#elif defined(WT_BRANDING_CANARY)
    windowClassName.append(L"Windows Terminal Canary");
#else
    windowClassName.append(L"Windows Terminal Dev");
#endif
    if (Utils::IsRunningElevated())
    {
        windowClassName.append(L" Admin");
    }
    if (!IsPackaged())
    {
        const auto path = wil::QueryFullProcessImageNameW<std::wstring>();
        const auto hash = til::hash(path);
#ifdef _WIN64
        fmt::format_to(std::back_inserter(windowClassName), FMT_COMPILE(L" {:016x}"), hash);
#else
        fmt::format_to(std::back_inserter(windowClassName), FMT_COMPILE(L" {:08x}"), hash);
#endif
    }

    {
        wil::unique_handle processToken{ GetCurrentProcessToken() };
        const auto userTokenInfo{ wil::get_token_information<TOKEN_USER>(processToken.get()) };
        const auto sidLength{ GetLengthSid(userTokenInfo->User.Sid) };
        const auto hash{ til::hash(userTokenInfo->User.Sid, sidLength) };
#ifdef _WIN64
        fmt::format_to(std::back_inserter(windowClassName), FMT_COMPILE(L" {:016x}"), hash);
#else
        fmt::format_to(std::back_inserter(windowClassName), FMT_COMPILE(L" {:08x}"), hash);
#endif
    }

    // Windows Terminal is a single-instance application. Either acquire ownership
    // over the mutex, or hand off the command line to the existing instance.
    const auto mutex = acquireMutexOrAttemptHandoff(windowClassName.c_str(), nCmdShow);
    if (!mutex)
    {
        // The command line has been handed off. We can now exit.
        // We do it with TerminateProcess() primarily to avoid WinUI shutdown issues.
        TerminateProcess(GetCurrentProcess(), gsl::narrow_cast<UINT>(0));
        __assume(false);
    }

    _app = winrt::TerminalApp::App{};
    _app.Logic().ReloadSettings();

    _createMessageWindow(windowClassName.c_str());
    _setupGlobalHotkeys();
    _checkWindowsForNotificationIcon();
    _setupSessionPersistence(_app.Logic().Settings().GlobalSettings().ShouldUsePersistedLayout());

    // When the settings change, we'll want to update our global hotkeys
    // and our notification icon based on the new settings.
    _app.Logic().SettingsChanged([this](auto&&, const TerminalApp::SettingsLoadEventArgs& args) {
        if (SUCCEEDED(args.Result()))
        {
            _assertIsMainThread();
            _setupGlobalHotkeys();
            _checkWindowsForNotificationIcon();
            _setupSessionPersistence(args.NewSettings().GlobalSettings().ShouldUsePersistedLayout());
        }
    });

    {
        const wil::unique_environstrings_ptr envMem{ GetEnvironmentStringsW() };
        const auto env = stringFromDoubleNullTerminated(envMem.get());
        const auto cwd = wil::GetCurrentDirectoryW<std::wstring>();
        const auto showCmd = gsl::narrow_cast<uint32_t>(nCmdShow);

        // Restore persisted windows.
        const auto state = ApplicationState::SharedInstance();
        const auto layouts = state.PersistedWindowLayouts();
        if (layouts && layouts.Size() > 0)
        {
            _needsPersistenceCleanup = true;

            uint32_t startIdx = 0;
            for (const auto layout : layouts)
            {
                hstring args[] = { L"wt", L"-w", L"new", L"-s", winrt::to_hstring(startIdx) };
                _dispatchCommandlineCommon(args, cwd, env, showCmd);
                startIdx += 1;
            }
        }

        const auto args = commandlineToArgArray(GetCommandLineW());

        if (args.size() == 2 && args[1] == L"-Embedding")
        {
            // We were launched for ConPTY handoff. We have no windows and also don't want to exit.
            //
            // TODO: Here we could start a timer and exit after, say, 5 seconds
            // if no windows are created. But that's a minor concern.
        }
        else
        {
            // Create another window if needed: There aren't any yet, OR we got an explicit command line.
            if (_windows.empty() || args.size() != 1)
            {
                _dispatchCommandlineCommon(args, cwd, env, showCmd);
            }

            // If we created no windows, e.g. because the args are "/?" we can just exit now.
            _postQuitMessageIfNeeded();
        }
    }

    // ALWAYS change the _real_ CWD of the Terminal to system32,
    // so that we don't lock the directory we were spawned in.
    if (std::wstring system32; SUCCEEDED_LOG(wil::GetSystemDirectoryW(system32)))
    {
        LOG_IF_WIN32_BOOL_FALSE(SetCurrentDirectoryW(system32.c_str()));
    }

    {
        TerminalConnection::ConptyConnection::NewConnection([this](TerminalConnection::ConptyConnection conn) {
            TerminalApp::CommandlineArgs args;
            args.ShowWindowCommand(conn.ShowWindow());
            args.Connection(std::move(conn));
            _dispatchCommandline(std::move(args));
            _summonWindow(SummonWindowSelectionArgs{
                .SummonBehavior = nullptr,
            });
        });
        TerminalConnection::ConptyConnection::StartInboundListener();
    }

    // Main message loop. It pumps all windows.
    bool loggedInteraction = false;
    MSG msg{};
    while (GetMessageW(&msg, nullptr, 0, 0))
    {
        // This is `if (WM_KEYDOWN || WM_KEYUP || WM_SYSKEYDOWN || WM_SYSKEYUP)`.
        // It really doesn't need to be written that obtuse, but it at
        // least nicely mirrors our `keyDown = msg.message & 1` logic.
        // FYI: For the key-down/up messages the lowest bit indicates if it's up.
        if ((msg.message & ~1) == WM_KEYDOWN || (msg.message & ~1) == WM_SYSKEYDOWN)
        {
            if (!loggedInteraction)
            {
#if defined(WT_BRANDING_RELEASE)
                constexpr uint8_t branding = 3;
#elif defined(WT_BRANDING_PREVIEW)
                constexpr uint8_t branding = 2;
#elif defined(WT_BRANDING_CANARY)
                constexpr uint8_t branding = 1;
#else
                constexpr uint8_t branding = 0;
#endif
                const uint8_t distribution = IsPackaged()                             ? 2 :
                                             _app.Logic().Settings().IsPortableMode() ? 1 :
                                                                                        0;
                TraceLoggingWrite(
                    g_hWindowsTerminalProvider,
                    "SessionBecameInteractive",
                    TraceLoggingDescription("Event emitted when the session was interacted with"),
                    TraceLoggingValue(branding, "Branding"),
                    TraceLoggingValue(distribution, "Distribution"),
                    TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
                    TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
                loggedInteraction = true;
            }

            const bool keyDown = (msg.message & 1) == 0;
            if (
                // GH#638: The Xaml input stack doesn't allow an application to suppress the "caret browsing"
                // dialog experience triggered when you press F7. Official recommendation from the Xaml
                // team is to catch F7 before we hand it off.
                (msg.wParam == VK_F7 && keyDown) ||
                // GH#6421: System XAML will never send an Alt KeyUp event. Same thing here.
                (msg.wParam == VK_MENU && !keyDown) ||
                // GH#7125: System XAML will show a system dialog on Alt Space.
                // We want to handle it ourselves and there's no way to suppress it.
                // It almost seems like there's a pattern here...
                (msg.wParam == VK_SPACE && msg.message == WM_SYSKEYDOWN))
            {
                _dispatchSpecialKey(msg);
                continue;
            }

            if (msg.message == WM_KEYDOWN)
            {
                const auto vkey = static_cast<WORD>(msg.wParam);

                // Hide the cursor only when the key pressed is an input key (ignore modifier keys).
                if (IsInputKey(vkey))
                {
                    IslandWindow::HideCursor();
                }
            }
        }

        if (IslandWindow::IsCursorHidden())
        {
            IslandWindow::ShowCursorMaybe(msg.message);
        }

        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    _finalizeSessionPersistence();

    if (_notificationIconShown)
    {
        Shell_NotifyIconW(NIM_DELETE, &_notificationIcon);
    }

    // There's a mysterious crash in XAML on Windows 10 if you just let _app get destroyed (GH#15410).
    // We also need to ensure that all UI threads exit before WindowEmperor leaves the scope on the main thread (MSFT:46744208).
    // Both problems can be solved and the shutdown accelerated by using TerminateProcess.
    // std::exit(), etc., cannot be used here, because those use ExitProcess for unpackaged applications.
    TerminateProcess(GetCurrentProcess(), gsl::narrow_cast<UINT>(0));
    __assume(false);
}

void WindowEmperor::_dispatchSpecialKey(const MSG& msg) const
{
    // Each CoreInput window is a child of our IslandWindow.
    // We can figure out the IslandWindow HWND via GetAncestor().
    const auto hwnd = GetAncestor(msg.hwnd, GA_ROOT);
    AppHost* window = nullptr;

    for (const auto& h : _windows)
    {
        const auto w = h->GetWindow();
        if (w && w->GetHandle() == hwnd)
        {
            window = h.get();
            break;
        }
    }

    // Fallback.
    if (!window)
    {
        window = _mostRecentWindow();
        if (!window)
        {
            return;
        }
    }

    const auto vkey = gsl::narrow_cast<uint32_t>(msg.wParam);
    const auto scanCode = gsl::narrow_cast<uint8_t>(msg.lParam >> 16);
    const bool keyDown = (msg.message & 1) == 0;
    window->OnDirectKeyEvent(vkey, scanCode, keyDown);
}

void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs args)
{
    _assertIsMainThread();

    const auto exitCode = args.ExitCode();

    if (const auto msg = args.ExitMessage(); !msg.empty())
    {
        _showMessageBox(msg, exitCode != 0);
        return;
    }

    if (exitCode != 0)
    {
        return;
    }

    const auto parsedTarget = args.TargetWindow();
    WindowingMode windowingBehavior = WindowingMode::UseNew;
    uint64_t windowId = 0;
    winrt::hstring windowName;

    // Figure out the windowing behavior the caller wants
    // and get the sanitized window ID (if any) and window name (if any).
    if (parsedTarget.empty())
    {
        windowingBehavior = _app.Logic().Settings().GlobalSettings().WindowingBehavior();
    }
    else if (const auto opt = til::parse_signed<int64_t>(parsedTarget, 10))
    {
        // Negative window IDs map to WindowingMode::UseNew.
        if (*opt > 0)
        {
            windowId = gsl::narrow_cast<uint64_t>(*opt);
        }
        else if (*opt == 0)
        {
            windowingBehavior = WindowingMode::UseExisting;
        }
    }
    else if (parsedTarget == L"last")
    {
        windowingBehavior = WindowingMode::UseExisting;
    }
    // A window name of "new" maps to WindowingMode::UseNew.
    else if (parsedTarget != L"new")
    {
        windowName = parsedTarget;
    }

    AppHost* window = nullptr;

    // Map from the windowing behavior, ID, and name to a window.
    if (windowId)
    {
        window = GetWindowById(windowId);
    }
    else if (!windowName.empty())
    {
        window = GetWindowByName(windowName);
    }
    else
    {
        switch (windowingBehavior)
        {
        case WindowingMode::UseAnyExisting:
            window = _mostRecentWindow();
            break;
        case WindowingMode::UseExisting:
            _dispatchCommandlineCurrentDesktop(std::move(args));
            return;
        default:
            break;
        }
    }

    if (window)
    {
        window->DispatchCommandline(std::move(args));
    }
    else
    {
        winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) };
        request.WindowName(std::move(windowName));
        CreateNewWindow(std::move(request));
    }
}

void WindowEmperor::_dispatchCommandlineCommon(winrt::array_view<const winrt::hstring> args, wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand)
{
    winrt::TerminalApp::CommandlineArgs c;
    c.Commandline(args);
    c.CurrentDirectory(currentDirectory);
    c.CurrentEnvironment(envString);
    c.ShowWindowCommand(showWindowCommand);
    _dispatchCommandline(std::move(c));
}

// This is an implementation-detail of _dispatchCommandline().
safe_void_coroutine WindowEmperor::_dispatchCommandlineCurrentDesktop(winrt::TerminalApp::CommandlineArgs args)
{
    std::shared_ptr<AppHost> mostRecent;
    AppHost* window = nullptr;

    if (winrt::guid currentDesktop; VirtualDesktopUtils::GetCurrentVirtualDesktopId(reinterpret_cast<GUID*>(&currentDesktop)))
    {
        int64_t max = INT64_MIN;
        for (const auto& w : _windows)
        {
            const auto lastActivatedTime = w->GetLastActivatedTime();
            const auto desktopId = co_await w->GetVirtualDesktopId();
            if (desktopId == currentDesktop && lastActivatedTime > max)
            {
                max = lastActivatedTime;
                mostRecent = w;
            }
        }

        // GetVirtualDesktopId(), as current implemented, should always return on the main thread.
        _assertIsMainThread();
        window = mostRecent.get();
    }
    else
    {
        // If virtual desktops have never been used, and in turn Explorer never set them up,
        // GetCurrentVirtualDesktopId will return false. In this case just use the current (only) desktop.
        window = _mostRecentWindow();
    }

    if (window)
    {
        window->DispatchCommandline(std::move(args));
    }
    else
    {
        CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs{ 0, std::move(args) });
    }
}

bool WindowEmperor::_summonWindow(const SummonWindowSelectionArgs& args) const
{
    _assertIsMainThread();

    AppHost* window = nullptr;

    if (args.WindowID)
    {
        for (const auto& w : _windows)
        {
            if (w->Logic().WindowProperties().WindowId() == args.WindowID)
            {
                window = w.get();
                break;
            }
        }
    }
    else if (!args.WindowName.empty())
    {
        for (const auto& w : _windows)
        {
            if (w->Logic().WindowProperties().WindowName() == args.WindowName)
            {
                window = w.get();
                break;
            }
        }
    }
    else
    {
        window = _mostRecentWindow();
    }

    if (!window)
    {
        return false;
    }

    window->HandleSummon(args.SummonBehavior);
    return true;
}

void WindowEmperor::_summonAllWindows() const
{
    _assertIsMainThread();

    TerminalApp::SummonWindowBehavior args;
    args.ToggleVisibility(false);

    for (const auto& window : _windows)
    {
        window->HandleSummon(args);
    }
}

#pragma region WindowProc

static WindowEmperor* GetThisFromHandle(HWND const window) noexcept
{
    const auto data = GetWindowLongPtrW(window, GWLP_USERDATA);
    return reinterpret_cast<WindowEmperor*>(data);
}

[[nodiscard]] LRESULT __stdcall WindowEmperor::_wndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
{
    if (const auto that = GetThisFromHandle(window))
    {
        return that->_messageHandler(window, message, wparam, lparam);
    }

    if (WM_NCCREATE == message)
    {
        const auto cs = reinterpret_cast<CREATESTRUCT*>(lparam);
        const auto that = static_cast<WindowEmperor*>(cs->lpCreateParams);
        that->_window.reset(window);
        SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(that));
    }

    return DefWindowProcW(window, message, wparam, lparam);
}

void WindowEmperor::_createMessageWindow(const wchar_t* className)
{
    const auto instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
    const auto icon = LoadIconW(instance, MAKEINTRESOURCEW(IDI_APPICON));

    const WNDCLASS wc{
        .lpfnWndProc = &_wndProc,
        .hInstance = instance,
        .hIcon = icon,
        .lpszClassName = className,
    };
    RegisterClassW(&wc);

    WM_TASKBARCREATED = RegisterWindowMessageW(L"TaskbarCreated");

    // NOTE: This cannot be a HWND_MESSAGE window as otherwise we don't
    // receive any HWND_BROADCAST messages, like WM_QUERYENDSESSION.
    // NOTE: Before CreateWindowExW() returns it invokes our WM_NCCREATE
    // message handler, which then stores the HWND in this->_window.
    WINRT_VERIFY(CreateWindowExW(
        /* dwExStyle    */ 0,
        /* lpClassName  */ className,
        /* lpWindowName */ L"Windows Terminal",
        /* dwStyle      */ 0,
        /* X            */ 0,
        /* Y            */ 0,
        /* nWidth       */ 0,
        /* nHeight      */ 0,
        /* hWndParent   */ nullptr,
        /* hMenu        */ nullptr,
        /* hInstance    */ instance,
        /* lpParam      */ this));

    _notificationIcon.cbSize = sizeof(NOTIFYICONDATA);
    _notificationIcon.hWnd = _window.get();
    _notificationIcon.uID = 1;
    _notificationIcon.uFlags = NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP | NIF_ICON;
    _notificationIcon.uCallbackMessage = WM_NOTIFY_FROM_NOTIFICATION_AREA;
    _notificationIcon.hIcon = icon;
    _notificationIcon.uVersion = NOTIFYICON_VERSION_4;

    // AppName happens to be in the ContextMenu's Resources, see GH#12264
    const ScopedResourceLoader loader{ L"TerminalApp/ContextMenu" };
    const auto appNameLoc = loader.GetLocalizedString(L"AppName");
    StringCchCopy(_notificationIcon.szTip, ARRAYSIZE(_notificationIcon.szTip), appNameLoc.c_str());
}

// Posts a WM_QUIT as soon as we have no reason to exist anymore.
// That basically means no windows and no message boxes.
void WindowEmperor::_postQuitMessageIfNeeded() const
{
    if (
        _messageBoxCount <= 0 &&
        _windowCount <= 0 &&
        !_app.Logic().Settings().GlobalSettings().AllowHeadless())
    {
        PostQuitMessage(0);
    }
}

safe_void_coroutine WindowEmperor::_showMessageBox(winrt::hstring message, bool error)
{
    // Prevent the main loop from exiting until the message box is closed.
    // Once the loop exits, the app exits, and the message box will be closed.
    _messageBoxCount += 1;
    const auto decrement = wil::scope_exit([hwnd = _window.get()]() noexcept {
        PostMessageW(hwnd, WM_MESSAGE_BOX_CLOSED, 0, 0);
    });

    // We must yield to a background thread, because MessageBoxW() is a blocking call, and we can't
    // block the main thread. That would prevent us from servicing WM_COPYDATA messages and similar.
    //
    // NOTE: All remaining code of this function doesn't touch `this`, so we don't need weak/strong_ref.
    // NOTE NOTE: Don't touch `this` when you make changes here.
    co_await winrt::resume_background();

    const auto messageTitle = error ? IDS_ERROR_DIALOG_TITLE : IDS_HELP_DIALOG_TITLE;
    const auto messageIcon = error ? MB_ICONERROR : MB_ICONWARNING;
    // The dialog cannot have our _window as the parent, because that one is always hidden/invisible.
    // TODO:GH#4134: polish this dialog more, to make the text more like msiexec /?
    MessageBoxW(nullptr, message.c_str(), GetStringResource(messageTitle).c_str(), MB_OK | messageIcon);
}

LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM const wParam, LPARAM const lParam) noexcept
{
    try
    {
        switch (message)
        {
        case WM_CLOSE_TERMINAL_WINDOW:
        {
            const auto globalSettings = _app.Logic().Settings().GlobalSettings();
            // Keep the last window in the array so that we can persist it on exit.
            // We check for AllowHeadless(), as that being true prevents us from ever quitting in the first place.
            // (= If we avoided closing the last window you wouldn't be able to reach a headless state.)
            const auto shouldKeepWindow =
                _windows.size() == 1 &&
                globalSettings.ShouldUsePersistedLayout() &&
                !globalSettings.AllowHeadless();

            if (!shouldKeepWindow)
            {
                // Did the window counter get out of sync? It shouldn't.
                assert(_windowCount == gsl::narrow_cast<int32_t>(_windows.size()));

                // !!! NOTE !!!
                // At least theoretically the lParam pointer may be invalid.
                // We should only access it if we find it in our _windows array.
                const auto host = reinterpret_cast<AppHost*>(lParam);
                auto it = _windows.begin();
                const auto end = _windows.end();

                for (; it != end; ++it)
                {
                    if (host == it->get())
                    {
                        // NOTE: AppHost::Close is highly non-trivial.
                        //
                        // It _may_ call into XAML, which _may_ pump the message loop, which would then recursively
                        // re-enter this function, which _may_ then handle another WM_CLOSE_TERMINAL_WINDOW,
                        // which would change the _windows array, and invalidate our iterator and crash.
                        //
                        // We can prevent this by deferring Close() until after the erase() call.
                        //
                        // Wrapping it in an exception handler will prevent us from losing track of the window count, at
                        // the perceived cost of leaking all the resources. However, the resources were being leaked
                        // anyway (since we threw and exited this message handler) so this at least gives back our
                        // deterministic window count management.
                        const auto strong = *it;
                        _windows.erase(it);
                        try
                        {
                            strong->Close();
                        }
                        CATCH_LOG();
                        break;
                    }
                }
            }

            // Counterpart specific to CreateNewWindow().
            _windowCount -= 1;
            _postQuitMessageIfNeeded();
            return 0;
        }
        case WM_MESSAGE_BOX_CLOSED:
            // Counterpart specific to _showMessageBox().
            _messageBoxCount -= 1;
            _postQuitMessageIfNeeded();
            return 0;
        case WM_IDENTIFY_ALL_WINDOWS:
            for (const auto& host : _windows)
            {
                host->Logic().IdentifyWindow();
            }
            return 0;
        case WM_NOTIFY_FROM_NOTIFICATION_AREA:
            switch (LOWORD(lParam))
            {
            case NIN_SELECT:
            case NIN_KEYSELECT:
            {
                SummonWindowSelectionArgs args;
                args.SummonBehavior.MoveToCurrentDesktop(false);
                args.SummonBehavior.ToMonitor(winrt::TerminalApp::MonitorBehavior::InPlace);
                args.SummonBehavior.ToggleVisibility(false);
                std::ignore = _summonWindow(std::move(args));
                break;
            }
            case WM_CONTEXTMENU:
                _notificationAreaMenuRequested(wParam);
                break;
            default:
                break;
            }
            return 0;
        case WM_WINDOWPOSCHANGING:
        {
            // No, we really don't want this window to be visible. It hides the buggy CoreWindow that XAML creates.
            // We can't make it a HWND_MESSAGE window, because then we don't get WM_QUERYENDSESSION messages.
            const auto wp = reinterpret_cast<WINDOWPOS*>(lParam);
            wp->flags &= ~SWP_SHOWWINDOW;
            return 0;
        }
        case WM_MENUCOMMAND:
            _notificationAreaMenuClicked(wParam, lParam);
            return 0;
        case WM_SETTINGCHANGE:
            // Currently, we only support checking when the OS theme changes. In that case, wParam is 0.
            // GH#15102: Re-evaluate when we decide to reload env vars.
            //
            // ImmersiveColorSet seems to be the notification that the OS theme
            // changed. If that happens, let the app know, so it can hot-reload
            // themes, color schemes that might depend on the OS theme
            if (wParam == 0 && lParam != 0 && wcscmp(reinterpret_cast<const wchar_t*>(lParam), L"ImmersiveColorSet") == 0)
            {
                // GH#15732: Don't update the settings, unless the theme
                // _actually_ changed. ImmersiveColorSet gets sent more often
                // than just on a theme change. It notably gets sent when the PC
                // is locked, or the UAC prompt opens.
                const auto isCurrentlyDark = Theme::IsSystemInDarkTheme();
                if (isCurrentlyDark != _currentSystemThemeIsDark)
                {
                    _currentSystemThemeIsDark = isCurrentlyDark;

                    // GH#19505: WM_SETTINGCHANGE gets sent out with a SendMessage() call, which means
                    // that COM methods marked as [input_sync] cannot be called. Well, our CascadiaSettings
                    // loader does call such methods. This results in RPC_E_CANTCALLOUT_ININPUTSYNCCALL, aka:
                    // "An outgoing call cannot be made since the application is dispatching an input-synchronous call."
                    // The solution is to simply do it in another tick.
                    _app.Logic().ReloadSettingsThrottled();
                }
            }
            return 0;
        case WM_COPYDATA:
            if (const auto cds = reinterpret_cast<COPYDATASTRUCT*>(lParam); cds->dwData == TERMINAL_HANDOFF_MAGIC)
            {
                const auto handoff = deserializeHandoffPayload(static_cast<const uint8_t*>(cds->lpData), static_cast<const uint8_t*>(cds->lpData) + cds->cbData);
                const auto argv = commandlineToArgArray(handoff.args.c_str());
                _dispatchCommandlineCommon(argv, handoff.cwd, handoff.env, handoff.show);
            }
            return 0;
        case WM_HOTKEY:
            _hotkeyPressed(static_cast<long>(wParam));
            return 0;
        case WM_QUERYENDSESSION:
            // For WM_QUERYENDSESSION and WM_ENDSESSION, refer to:
            // https://docs.microsoft.com/en-us/windows/win32/rstmgr/guidelines-for-applications
            // ENDSESSION_CLOSEAPP: The application is using a file that must be replaced,
            // the system is being serviced, or system resources are exhausted.
            RegisterApplicationRestart(nullptr, RESTART_NO_CRASH | RESTART_NO_HANG);
            return TRUE;
        case WM_ENDSESSION:
            _finalizeSessionPersistence();
            _skipPersistence = true;
            PostQuitMessage(0);
            return 0;
        default:
            // We'll want to receive this message when explorer.exe restarts
            // so that we can re-add our icon to the notification area.
            // This unfortunately isn't a switch case because we register the
            // message at runtime.
            if (message == WM_TASKBARCREATED)
            {
                _notificationIconShown = false;
                _checkWindowsForNotificationIcon();
                return 0;
            }
        }
    }
    catch (...)
    {
        LOG_CAUGHT_EXCEPTION();
    }

    return DefWindowProcW(window, message, wParam, lParam);
}

void WindowEmperor::_setupSessionPersistence(bool enabled)
{
    if (!enabled)
    {
        _persistStateTimer.Stop();
        return;
    }
    _persistStateTimer.Interval(std::chrono::minutes(5));
    _persistStateTimer.Tick([&](auto&&, auto&&) {
        _persistState(ApplicationState::SharedInstance());
    });
    _persistStateTimer.Start();
}

void WindowEmperor::_persistState(const ApplicationState& state) const
{
    // Calling an `ApplicationState` setter triggers a write to state.json.
    // With this if condition we avoid an unnecessary write when persistence is disabled.
    if (state.PersistedWindowLayouts())
    {
        state.PersistedWindowLayouts(nullptr);
    }

    if (_app.Logic().Settings().GlobalSettings().FirstWindowPreference() != FirstWindowPreference::DefaultProfile)
    {
        for (const auto& w : _windows)
        {
            w->Logic().PersistState();
        }
    }

    // Ensure to write the state.json
    state.Flush();
}

void WindowEmperor::_finalizeSessionPersistence() const
{
    using namespace std::string_view_literals;

    if (_skipPersistence)
    {
        // We received WM_ENDSESSION and persisted the state.
        // We don't need to persist it again.
        return;
    }

    const auto state = ApplicationState::SharedInstance();

    _persistState(state);

    const auto firstWindowPreference = _app.Logic().Settings().GlobalSettings().FirstWindowPreference();
    const auto wantsPersistence = firstWindowPreference != FirstWindowPreference::DefaultProfile;

    // If we neither need a cleanup nor want to persist, we can exit early.
    if (!_needsPersistenceCleanup && !wantsPersistence)
    {
        return;
    }

    const std::filesystem::path settingsDirectory{ std::wstring_view{ CascadiaSettings::SettingsDirectory() } };
    const auto admin = _app.Logic().IsRunningElevated();
    const auto filenamePrefix = admin ? L"elevated_"sv : L"buffer_"sv;
    const auto persistBuffers = firstWindowPreference == FirstWindowPreference::PersistedLayoutAndContent;
    std::unordered_set<std::wstring, til::transparent_hstring_hash, til::transparent_hstring_equal_to> bufferFilenames;

    if (persistBuffers)
    {
        // If the app is running elevated, we create files with a mandatory
        // integrity label (ML) of "High" (HI) for reading and writing (NR, NW).
        wil::unique_hlocal_security_descriptor sd;
        SECURITY_ATTRIBUTES sa{};
        if (admin)
        {
            unsigned long cb;
            THROW_IF_WIN32_BOOL_FALSE(ConvertStringSecurityDescriptorToSecurityDescriptorW(L"S:(ML;;NRNW;;;HI)", SDDL_REVISION_1, wil::out_param_ptr<PSECURITY_DESCRIPTOR*>(sd), &cb));
            sa.nLength = sizeof(SECURITY_ATTRIBUTES);
            sa.lpSecurityDescriptor = sd.get();
        }

        // Persist all terminal buffers to "buffer_{guid}.txt" files.
        // We remember the filenames so that we can clean up old ones later.
        for (const auto& w : _windows)
        {
            const auto panes = w->Logic().Panes();
            for (const auto pane : panes)
            {
                try
                {
                    const auto term = pane.try_as<winrt::TerminalApp::ITerminalPaneContent>();
                    if (!term)
                    {
                        continue;
                    }
                    const auto control = term.GetTermControl();
                    if (!control)
                    {
                        continue;
                    }
                    const auto connection = control.Connection();
                    if (!connection)
                    {
                        continue;
                    }
                    const auto sessionId = connection.SessionId();
                    if (sessionId == winrt::guid{})
                    {
                        continue;
                    }

                    auto filename = fmt::format(FMT_COMPILE(L"{}{}.txt"), filenamePrefix, sessionId);
                    const auto path = settingsDirectory / filename;

                    if (wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) })
                    {
                        control.PersistTo(reinterpret_cast<int64_t>(file.get()));
                        bufferFilenames.emplace(std::move(filename));
                    }
                }
                CATCH_LOG();
            }
        }
    }

    // Now remove the "buffer_{guid}.txt" files that shouldn't be there.
    if (_needsPersistenceCleanup)
    {
        const auto filter = fmt::format(FMT_COMPILE(L"{}\\{}*"), settingsDirectory.native(), filenamePrefix);

        // This could also use std::filesystem::directory_iterator.
        // I was just slightly bothered by how it doesn't have a O(1) .filename()
        // function, even though the underlying Win32 APIs provide it for free.
        // Both work fine.
        WIN32_FIND_DATAW ffd;
        const wil::unique_hfind handle{ FindFirstFileExW(filter.c_str(), FindExInfoBasic, &ffd, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH) };
        if (!handle)
        {
            return;
        }

        do
        {
            const auto nameLen = wcsnlen_s(&ffd.cFileName[0], ARRAYSIZE(ffd.cFileName));
            const std::wstring_view filename{ &ffd.cFileName[0], nameLen };
            if (!bufferFilenames.contains(filename))
            {
                std::filesystem::remove(settingsDirectory / filename);
            }
        } while (FindNextFileW(handle.get(), &ffd));
    }
}

void WindowEmperor::_notificationAreaMenuRequested(const WPARAM wParam)
{
    const auto menu = CreatePopupMenu();
    if (!menu)
    {
        assert(false);
        return;
    }

    static constexpr MENUINFO mi{
        .cbSize = sizeof(MENUINFO),
        .fMask = MIM_STYLE | MIM_APPLYTOSUBMENUS | MIM_MENUDATA,
        .dwStyle = MNS_NOTIFYBYPOS,
    };
    SetMenuInfo(menu, &mi);

    // The "Focus Terminal" menu item.
    AppendMenuW(menu, MF_STRING, 0, RS_(L"NotificationIconFocusTerminal").c_str());
    AppendMenuW(menu, MF_SEPARATOR, 0, L"");

    // A submenu to focus a specific window. Lists all windows that we manage.
    if (const auto submenu = CreatePopupMenu())
    {
        static constexpr MENUINFO submenuInfo{
            .cbSize = sizeof(MENUINFO),
            .fMask = MIM_MENUDATA,
            .dwStyle = MNS_NOTIFYBYPOS,
        };
        SetMenuInfo(submenu, &submenuInfo);

        std::wstring displayText;
        displayText.reserve(64);

        for (const auto& host : _windows)
        {
            const auto logic = host->Logic();
            const auto props = logic.WindowProperties();
            const auto id = props.WindowId();

            displayText.clear();
            fmt::format_to(std::back_inserter(displayText), L"#{}", id);
            if (const auto title = logic.Title(); !title.empty())
            {
                fmt::format_to(std::back_inserter(displayText), L": {}", title);
            }
            if (const auto name = props.WindowName(); !name.empty())
            {
                fmt::format_to(std::back_inserter(displayText), L" [{}]", name);
            }

            AppendMenuW(submenu, MF_STRING, gsl::narrow_cast<UINT_PTR>(id), displayText.c_str());
        }

        AppendMenuW(menu, MF_POPUP, reinterpret_cast<UINT_PTR>(submenu), RS_(L"NotificationIconWindowSubmenu").c_str());
    }

    // We'll need to set our window to the foreground before calling
    // TrackPopupMenuEx or else the menu won't dismiss when clicking away.
    SetForegroundWindow(_window.get());

    // User can select menu items with the left and right buttons.
    const auto rightAlign = GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0;
    const UINT uFlags = TPM_RIGHTBUTTON | (rightAlign ? TPM_RIGHTALIGN : TPM_LEFTALIGN);
    TrackPopupMenuEx(menu, uFlags, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam), _window.get(), nullptr);

    if (_currentWindowMenu)
    {
        // This will recursively destroy the submenu(s) as well.
        DestroyMenu(_currentWindowMenu);
    }
    _currentWindowMenu = menu;
}

void WindowEmperor::_notificationAreaMenuClicked(const WPARAM wParam, const LPARAM lParam) const
{
    const auto menu = reinterpret_cast<HMENU>(lParam);
    const auto menuItemIndex = LOWORD(wParam);
    const auto windowId = GetMenuItemID(menu, menuItemIndex);

    // _notificationAreaMenuRequested constructs each menu item with an ID
    // that is either 0 for "Focus Terminal" or >0 for a specific window ID.
    // This works well for us because valid window IDs are always >0.
    SummonWindowSelectionArgs args;
    args.WindowID = windowId;
    args.SummonBehavior.ToggleVisibility(false);
    args.SummonBehavior.MoveToCurrentDesktop(false);
    args.SummonBehavior.ToMonitor(winrt::TerminalApp::MonitorBehavior::InPlace);
    std::ignore = _summonWindow(std::move(args));
}

#pragma endregion
#pragma region GlobalHotkeys

void WindowEmperor::_hotkeyPressed(const long hotkeyIndex)
{
    if (hotkeyIndex < 0 || static_cast<size_t>(hotkeyIndex) > _hotkeys.size())
    {
        return;
    }

    const auto& summonArgs = til::at(_hotkeys, hotkeyIndex);

    // desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false
    // desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false
    // desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true
    SummonWindowSelectionArgs args;
    args.WindowName = summonArgs.Name();
    args.OnCurrentDesktop = summonArgs.Desktop() == DesktopBehavior::OnCurrent;
    args.SummonBehavior.MoveToCurrentDesktop(summonArgs.Desktop() == DesktopBehavior::ToCurrent);
    args.SummonBehavior.ToggleVisibility(summonArgs.ToggleVisibility());
    args.SummonBehavior.DropdownDuration(summonArgs.DropdownDuration());

    switch (summonArgs.Monitor())
    {
    case MonitorBehavior::Any:
        args.SummonBehavior.ToMonitor(TerminalApp::MonitorBehavior::InPlace);
        break;
    case MonitorBehavior::ToCurrent:
        args.SummonBehavior.ToMonitor(TerminalApp::MonitorBehavior::ToCurrent);
        break;
    case MonitorBehavior::ToMouse:
        args.SummonBehavior.ToMonitor(TerminalApp::MonitorBehavior::ToMouse);
        break;
    }

    if (_summonWindow(args))
    {
        return;
    }

    hstring argv[] = { L"wt", L"-w", summonArgs.Name() };
    if (argv[2].empty())
    {
        argv[2] = L"new";
    }

    const wil::unique_environstrings_ptr envMem{ GetEnvironmentStringsW() };
    const auto env = stringFromDoubleNullTerminated(envMem.get());
    const auto cwd = wil::GetCurrentDirectoryW<std::wstring>();
    _dispatchCommandlineCommon(argv, cwd, env, SW_SHOWDEFAULT);
}

void WindowEmperor::_registerHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept
{
    const auto vkey = hotkey.Vkey();
    auto hotkeyFlags = MOD_NOREPEAT;
    {
        const auto modifiers = hotkey.Modifiers();
        WI_SetFlagIf(hotkeyFlags, MOD_WIN, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows));
        WI_SetFlagIf(hotkeyFlags, MOD_ALT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu));
        WI_SetFlagIf(hotkeyFlags, MOD_CONTROL, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control));
        WI_SetFlagIf(hotkeyFlags, MOD_SHIFT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift));
    }

    // TODO GH#8888: We should display a warning of some kind if this fails.
    // This can fail if something else already bound this hotkey.
    LOG_IF_WIN32_BOOL_FALSE(::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey));
}

// Method Description:
// - Call UnregisterHotKey once for each previously registered hotkey.
// Return Value:
// - <none>
void WindowEmperor::_unregisterHotKey(const int index) noexcept
{
    LOG_IF_WIN32_BOOL_FALSE(::UnregisterHotKey(_window.get(), index));
}

void WindowEmperor::_setupGlobalHotkeys()
{
    // Unregister all previously registered hotkeys.
    //
    // RegisterHotKey(), will not unregister hotkeys automatically.
    // If a hotkey with a given HWND and ID combination already exists
    // then a duplicate one will be added, which we don't want.
    // (Additionally we want to remove hotkeys that were removed from the settings.)
    for (auto i = 0, count = gsl::narrow_cast<int>(_hotkeys.size()); i < count; ++i)
    {
        _unregisterHotKey(i);
    }

    _hotkeys.clear();

    // Re-register all current hotkeys.
    for (const auto& [keyChord, cmd] : _app.Logic().GlobalHotkeys())
    {
        if (auto summonArgs = cmd.ActionAndArgs().Args().try_as<Settings::Model::GlobalSummonArgs>())
        {
            const auto index = gsl::narrow_cast<int>(_hotkeys.size());
            _registerHotKey(index, keyChord);
            _hotkeys.emplace_back(summonArgs);
        }
    }
}

#pragma endregion

#pragma region NotificationIcon

void WindowEmperor::_checkWindowsForNotificationIcon()
{
    // We need to check some conditions to show the notification icon.
    //
    // * If there's a Quake window somewhere, we'll want to keep the
    //   notification icon.
    // * There's two settings - MinimizeToNotificationArea and
    //   AlwaysShowNotificationIcon. If either one of them are true, we want to
    //   make sure there's a notification icon.
    //
    // If both are false, we want to remove our icon from the notification area.
    // When we remove our icon from the notification area, we'll also want to
    // re-summon any hidden windows, but right now we're not keeping track of
    // who's hidden, so just summon them all. Tracking the work to do a "summon
    // all minimized" in GH#10448
    //
    // To avoid races between us thinking the settings updated, and the windows
    // themselves getting the new settings, only ask the app logic for the
    // RequestsTrayIcon setting value, and combine that with the result of each
    // window (which won't change during a settings reload).
    const auto globals = _app.Logic().Settings().GlobalSettings();
    auto needsIcon = globals.AlwaysShowNotificationIcon() || globals.MinimizeToNotificationArea();
    if (!needsIcon)
    {
        for (const auto& host : _windows)
        {
            needsIcon |= host->Logic().IsQuakeWindow();
        }
    }

    if (_notificationIconShown == needsIcon)
    {
        return;
    }

    if (needsIcon)
    {
        Shell_NotifyIconW(NIM_ADD, &_notificationIcon);
        Shell_NotifyIconW(NIM_SETVERSION, &_notificationIcon);
    }
    else
    {
        Shell_NotifyIconW(NIM_DELETE, &_notificationIcon);
        // If we no longer want the tray icon, but we did have one, then quick
        // re-summon all our windows, so they don't get lost when the icon
        // disappears forever.
        _summonAllWindows();
    }

    _notificationIconShown = needsIcon;
}

#pragma endregion
