// -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*-
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <config.h>

#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <filesystem>
#include <map>
#include <regex>
#include <string>
#include <string_view>
#include <thread>
#include <vector>

#include <Windows.h>
#include <appmodel.h>
#include <shlobj.h>
#include <shlwapi.h>
#include <shobjidl.h>
#include <shobjidl_core.h>

#include <wincrypt.h>
#include <winhttp.h>

#include <WebView2.h>
#include <WebView2EnvironmentOptions.h>

#include <wrl.h>
#include <wil/com.h>

#include <Poco/MemoryStream.h>

#include "litecask.h"

#include <common/AIHttpTransport.hpp>
#include <common/Clipboard.hpp>
#include <common/JsonUtil.hpp>
#include <common/LangUtil.hpp>
#include <common/Protocol.hpp>
#include <common/Log.hpp>
#include <common/MobileApp.hpp>
#include <common/RecentFiles.hpp>
#include <common/SettingsStorage.hpp>
#include <common/StringVector.hpp>
#include <common/Uri.hpp>
#include <net/FakeSocket.hpp>
#include <wsd/COOLWSD.hpp>
#include <wsd/DocumentBroker.hpp>
#include <wsd/RequestDetails.hpp>

#include "Resource.h"
#include "windows.hpp"

extern std::map<std::string, std::shared_ptr<DocumentBroker>> DocBrokers;
extern std::mutex DocBrokersMutex;

// Note that all pathnames in this code that are plain narrow strings (std::string) are in UTF-8 and
// can thus *not* be used for actual file system operations. They must always be converted to UTF-16
// first with Util::string_to_wide_string(). URIs one the other hand are valid as such as narrow
// strings.

enum class DocumentType
{
    TEXT,
    SPREADSHEET,
    PRESENTATION,
};

enum class DocumentMode { EDIT, NEW, WELCOME, STARTER };

struct FilenameAndUri
{
    // Just the basename (and extension), without folder
    std::string filename;

    // Complete file: URI
    std::string uri;
};

// Various document window specific data
struct WindowData
{
    HWND hWnd;
    HWND hConsoleWnd = 0;
    HWND hParentWnd = 0;
    int numMonitors = 0;
    RECT originalRect;
    LONG originalStyle;
    POINT previousSize; // After a WM_SIZE
    bool isFullScreen = false;
    bool isPresFullScreen = false;
    bool isConsole = false;
    int fakeClientFd;
    int closeNotificationPipeForForwardingThread[2];
    FilenameAndUri filenameAndUri;
    DocumentMode mode;
    int appDocId;
    wil::com_ptr<ICoreWebView2Controller> webViewController;
    wil::com_ptr<ICoreWebView2> webView;
    std::thread app2js;
};

struct PersistedDocumentWindowSize
{
    POINT size;
    WPARAM resizeType;
};

static std::map<HWND, WindowData> windowData;

static bool enableWebDriver = false;

static HINSTANCE appInstance;
static int appShowMode;

static bool weOwnTheClipboard = false;

// The COKit Office object, captured when the clipboard provider is installed, so a clipboard read
// can go straight to the process-shared clipboard without needing a particular document.
static kit::Office* office = nullptr;

const char* user_name = nullptr;
int coolwsd_server_socket_fd = -1;

static std::string app_exe_path;
std::string app_installation_path;

std::string app_installation_uri;

std::string localAppData;

static std::wstring appUserModelId;

static std::string uiLanguage = "en-US";
static std::wstring appName;

static COOLWSD* coolwsd = nullptr;

static std::thread coolwsdThread;

// The main window class name.
static const wchar_t windowClass[] = L"CODA";

// The hidden file open dialog and clipboard owner window class name.
static const wchar_t hiddenOwnerWindowClass[] = L"CODAHiddenOwnerWindow";
// The handle of that dummy window.
static HWND hiddenOwnerWindow;

static const int CODA_WM_EXECUTESCRIPT = WM_APP + 1;
static const int CODA_WM_LOADNEXTDOCUMENT = WM_APP + 2;
static const int CODA_WM_POSTWEBMESSAGE = WM_APP + 3;

static HMONITOR primaryMonitor;

static litecask::Datastore persistentWindowSizeStore;
static bool persistentWindowSizeStoreOK;

static RecentFiles recentFiles;

static FilenameAndUri fileSaveDialog(const std::string& name,
                                     const std::string& folder,
                                     const std::vector<COMDLG_FILTERSPEC>& extensions);

static void openCOOLWindow(const FilenameAndUri& filenameAndUri, DocumentMode mode);

static HANDLE copyEngineClipboardData(UINT format, const std::string& mimeType);

static std::string MIME_type_for_clipboard_format(UINT format);

static std::set<std::string> currentlyOpenDocumens()
{
    std::set<std::string> result;

    for (const auto& i : windowData)
        result.insert(i.second.filenameAndUri.uri);

    return result;
}

// Vector of documents to open passed on the command line, or multiple documents to open selected in
// a file open dialog. We open the next one only as soon as the previous one has finished loading.
static std::deque<FilenameAndUri> filenamesAndUrisToOpen;

void load_next_document()
{
    if (filenamesAndUrisToOpen.size() > 0)
    {
        // Open the next document (from the command line or selected in the file open dialog), if
        // any.
        if (windowData.size() > 0)
        {
            // Post a message to one randomly selected window that can be a starter backstage window
            // or a document window, it doesn't matter, they use the same window procedure.
            PostMessageW(windowData.begin()->second.hWnd, CODA_WM_LOADNEXTDOCUMENT, 0, 0);
        }
        else
        {
            // We have no window open, so we can just call openCOOLWindow() directly.
            auto nextDocument = filenamesAndUrisToOpen.front();
            filenamesAndUrisToOpen.pop_front();
            openCOOLWindow(nextDocument, DocumentMode::EDIT);
        }
    }
}

static void processMessage(WindowData& data, wil::unique_cotaskmem_string& message);

[[noreturn]] static void fatal(const std::string& message)
{
    MessageBoxW(hiddenOwnerWindow, Util::string_to_wide_string(message).c_str(), L"ERROR", MB_OK);
    std::abort();
}

static FilenameAndUri generate_new_copy(const std::wstring& templateSourcePath,
                                        const std::wstring& templateBasename,
                                        const std::wstring& templateExtension)
{
    PWSTR documents;
    SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &documents);

    int counter = 0;
    std::wstring templateCopyPath;

    do
    {
        std::wstring number = L"";
        if (counter > 0)
            number = L" (" + std::to_wstring(counter) + L")";
        templateCopyPath = std::wstring(documents) + L"\\" + templateBasename + number + L"." +
            templateExtension;
        counter++;
    } while (std::filesystem::exists(std::filesystem::path(templateCopyPath)));

    std::error_code ec;
    std::filesystem::copy_file(templateSourcePath, templateCopyPath, ec);

    if (ec)
        return {};

    auto path = Poco::Path(Util::wide_string_to_string(templateCopyPath));

    return { Util::wide_string_to_string(templateCopyPath), Poco::URI(path).toString() };
}

static std::wstring new_document(DocumentType type,
                                 const std::string& templateRelativePath,
                                 std::string basename)
{
    std::wstring templateBasename, templateExtension, templateSourcePath;

    if (templateRelativePath == "")
    {
        // Old-style simple blank documents
        switch (type)
        {
            case DocumentType::TEXT:
                templateBasename = L"TextDocument";
                templateExtension = L"odt";
                break;
            case DocumentType::SPREADSHEET:
                templateBasename = L"Spreadsheet";
                templateExtension = L"ods";
                break;
            case DocumentType::PRESENTATION:
                templateBasename = L"Presentation";
                templateExtension = L"odp";
                break;
            default:
                fatal("Unexpected case in new_document()");
        }
        basename = Util::wide_string_to_string(templateBasename);
        templateSourcePath = Util::string_to_wide_string(app_installation_path) +
            L"..\\templates\\" + templateBasename + L"." + templateExtension;
    }
    else
    {
        // A template chosen from the "Backstage"
        std::string decodedTemplateRelativePath;
        Poco::URI::decode(templateRelativePath, decodedTemplateRelativePath);

        templateSourcePath =
            Util::string_to_wide_string(app_installation_path +
                                        "..\\cool\\" + decodedTemplateRelativePath);
        auto wrelpath = Util::string_to_wide_string(decodedTemplateRelativePath);
        auto const lastDot = wrelpath.find_last_of(L'.');
        if (lastDot == std::wstring::npos)
            return L"";
        templateExtension = wrelpath.substr(lastDot + 1);
    }

    // The basename is URI-encoded because in some localisation it might contain spaces.
    std::string decodedBasename;
    Poco::URI::decode(basename, decodedBasename);
    auto filenameAndUri = generate_new_copy(templateSourcePath,
                                            Util::string_to_wide_string(decodedBasename),
                                            templateExtension);

    // If creating a new copy of the template failed, return an empty string
    if (filenameAndUri.uri == "")
        return L"";

    auto path = Poco::URI(filenameAndUri.uri).getPath();
    if (path.length() > 4 && path[0] == '/' && path[2] == ':' && path[3] == '/')
        path = path.substr(1);
    auto templateCopyPath = Util::string_to_wide_string(Poco::Path(path).toString());

    return templateCopyPath;
}

static int generate_new_app_doc_id()
{
    // Start with a random document id to catch code that might assume it to be some fixed value,
    // like 0 or 1. Also make it obvious that this numeric "app doc id", used by the mobile apps and
    // CODA, is not related to the string document ids (usually with several leading zeroes) used in
    // the C++ bits of normal COOL.
    static int id = 42 + (std::time(nullptr) % 100);

    DocumentData::allocate(id);
    return id++;
}

static void send2JS(const HWND hWnd, const char* buffer, int length)
{
    const bool binaryMessage = COOLProtocol::isBinaryMessage(buffer, static_cast<size_t>(length));
    std::string pretext{ binaryMessage
                             ? "window.TheFakeWebSocket.onmessage({'data': window.atob('"
                             : "window.TheFakeWebSocket.onmessage({'data': window.b64d('" };
    const std::string posttext{ "')});" };

    DWORD base64len = length * 2 + 100;
    std::vector<char> base64(base64len);
    if (!CryptBinaryToStringA((BYTE*)buffer, length, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,
                              base64.data(), &base64len))
    {
        LOG_ERR("CryptBinaryToStringA failed: " << GetLastError());
        return;
    }

    if (binaryMessage)
        LOG_TRC("To execute in JS: " << pretext << "stuff" << posttext);
    else
    {
        auto s = std::string(buffer, length);
        LOG_TRC("To execute in JS: " << pretext << s << posttext);
    }
    Log::flush();

    char* wparam = _strdup((pretext + std::string(base64.data()) + posttext).c_str());

    PostMessageW(hWnd, CODA_WM_EXECUTESCRIPT, (WPARAM)wparam, 0);
}

// Convert a file: URI to a native Windows path. A UNC location arrives as
// "file://host/share/..." with the host in the authority and becomes
// "\\host\share\..."; a local path arrives as "/C:/dir/file" and becomes
// "C:\dir\file". Non-file URIs, and anything that fails to parse, are returned
// unchanged.
static std::string fileUriToWindowsPath(const std::string& uri)
{
    if (!uri.starts_with("file:"))
        return uri;

    try
    {
        const Poco::URI parsedUri(uri);
        const std::string host = parsedUri.getHost();
        std::string path = parsedUri.getPath();
        if (!host.empty())
            path = "\\\\" + host + path;
        else if (path.size() > 3 && path[0] == '/' && path[2] == ':')
            path = path.substr(1);
        for (char& c : path)
        {
            if (c == '/')
                c = '\\';
        }
        return path;
    }
    catch (const std::exception& exc)
    {
        LOG_ERR("Bad file URI [" << uri << "]: " << exc.what());
        return uri;
    }
}

// COKit file save dialog callback.
void output_file_dialog_from_core(const char* suggestedURI, char* result, size_t resultLen)
{
    // Some sanity checks first.
    if (resultLen == 0)
        return;

    // The absolutely shortest file: URI on Windows would be something like "file:///C:/f".
    if (resultLen < 12)
    {
        result[0] = '\0';
        return;
    }

    // fileUriToWindowsPath returns a backslash-separated native path (UNC-aware),
    // so split the folder and filename on backslash.
    const std::string path = fileUriToWindowsPath(suggestedURI);
    const auto lastSlash = path.find_last_of('\\');
    const auto filename = path.substr(lastSlash + 1);
    const auto folder = path.substr(0, lastSlash);
    auto lastPeriod = filename.find_last_of('.');
    auto extension = filename.substr(lastPeriod + 1);
    auto filenameAndUri = fileSaveDialog(filename, folder,
                                         {
                                             {
                                                 Util::string_to_wide_string(extension).c_str(),
                                                 Util::string_to_wide_string("*." + extension).c_str()
                                             }
                                         });

    if (filenameAndUri.uri.size() < resultLen)
        strcpy(result, filenameAndUri.uri.c_str());
    else
        result[0] = '\0';
}

// COKit reveal-in-file-manager callback: open Explorer with the document selected.
void reveal_in_file_manager(const char* uri)
{
    if (uri == nullptr || *uri == '\0')
        return;

    const std::wstring widePath = Util::string_to_wide_string(fileUriToWindowsPath(uri));
    PIDLIST_ABSOLUTE pidl = nullptr;
    if (SUCCEEDED(SHParseDisplayName(widePath.c_str(), nullptr, &pidl, 0, nullptr)) && pidl)
    {
        SHOpenFolderAndSelectItems(pidl, 0, nullptr, 0);
        CoTaskMemFree(pidl);
    }
}

static void stopServer()
{
    SigUtil::requestShutdown();

    // Wait until coolwsdThread is torn down, so that we don't start cleaning up too early.
    coolwsdThread.join();
}

static void createAndStartMessagePumpThread(WindowData& data)
{
    // Create a socket pair to notify the below thread when the document has been closed
    fakeSocketPipe2(data.closeNotificationPipeForForwardingThread);

    // Start another thread to read responses and forward them to the JavaScript
    data.app2js = std::thread(
        [&data]
        {
            ProcUtil::setThreadName("app2js " + std::to_string(data.appDocId));
            while (true)
            {
                struct pollfd pollfd[2];
                pollfd[0].fd = data.fakeClientFd;
                pollfd[0].events = POLLIN;
                pollfd[1].fd = data.closeNotificationPipeForForwardingThread[1];
                pollfd[1].events = POLLIN;
                if (fakeSocketPoll(pollfd, 2, -1) > 0)
                {
                    if (pollfd[1].revents == POLLIN)
                    {
                        // The code below handling the "BYE" fake Websocket message has closed the other
                        // end of the closeNotificationPipeForForwardingThread. Let's close the other
                        // end too just for cleanliness, even if a FakeSocket as such is not a system
                        // resource so nothing is saved by closing it.
                        fakeSocketClose(data.closeNotificationPipeForForwardingThread[1]);

                        // Close our end of the fake socket connection to the ClientSession thread, so
                        // that it terminates.
                        fakeSocketClose(data.fakeClientFd);

                        return;
                    }
                    if (pollfd[0].revents == POLLIN)
                    {
                        int n = fakeSocketAvailableDataLength(data.fakeClientFd);
                        // I don't want to check for n being -1 here, even if that will lead to a crash,
                        // as n being -1 is a sign of something being wrong elsewhere anyway, and I
                        // prefer to fix the root cause. Let's see how well this works out.
                        if (n == 0)
                            return;
                        std::vector<char> buf(n);
                        n = fakeSocketRead(data.fakeClientFd, buf.data(), n);
                        send2JS(data.hWnd, buf.data(), n);
                    }
                }
                else
                {
                    break;
                }
            }
            assert(false);
        });
}

static void do_hullo_handling_things(WindowData& data)
{
    // Now we know that the JS has started completely

    // Contact the permanently (during app lifetime) listening COOLWSD server "public" socket
    assert(coolwsd_server_socket_fd != -1);
    int rc = fakeSocketConnect(data.fakeClientFd, coolwsd_server_socket_fd);
    (void)rc;
    assert(rc != -1);

    createAndStartMessagePumpThread(data);

    // First we must send the URL. This corresponds to the GET request with Upgrade to WebSocket.
    // This *must* be the first message written to the "client" thread. We don't need to do this
    // write in a separate thread, and we can't, because if we do that, we will occasionally run into
    // a bug when the "coolclient" message sent by the JS is received and gets forwarded to the
    // "client" thread before we have written the URL to it.

    std::string message(data.filenameAndUri.uri + " " + std::to_string(data.appDocId));
    fakeSocketWriteQueue(data.fakeClientFd, message.c_str(), message.size());
}

static void do_welcome_handling_things(WindowData& data)
{
    const auto welcomeSlideshow = Poco::Path(app_installation_path + "..\\cool\\welcome\\welcome-slideshow.odp");

    if (!Poco::File(welcomeSlideshow).exists())
        return;

    openCOOLWindow({ welcomeSlideshow.getFileName(), Poco::URI(welcomeSlideshow).toString() }, DocumentMode::WELCOME);
}

static void enter_full_screen(WindowData& data, HMONITOR monitor, bool saveRestoreInfo)
{
    if (data.isFullScreen)
        return;

    LONG style = GetWindowLong(data.hWnd, GWL_STYLE);

    if (saveRestoreInfo)
    {
        GetWindowRect(data.hWnd, &data.originalRect);
        data.originalStyle = style;
    }

    // Remove window borders and title bar
    style &= ~(WS_OVERLAPPEDWINDOW);
    SetWindowLong(data.hWnd, GWL_STYLE, style);

    MONITORINFO monitorInfo = { sizeof(monitorInfo) };
    GetMonitorInfo(monitor, &monitorInfo);

    // Resize window to fill the entire monitor
    SetWindowPos(data.hWnd, NULL,
                 monitorInfo.rcMonitor.left, monitorInfo.rcMonitor.top,
                 monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left,
                 monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top,
                 SWP_NOZORDER | SWP_FRAMECHANGED);
    data.isFullScreen = true;
}

static void leave_full_screen(WindowData& data)
{
    if (!data.isFullScreen)
        return;

    SetWindowLong(data.hWnd, GWL_STYLE, data.originalStyle);

    // Restore in two steps, position first, then size.
    // So if we restore from external monitor at a different resolution than
    // the laptop monitor, that a WM_DPICHANGED triggered from changing monitor
    // (which gets processed during SetWindowPos) doesn't mangle the size.
    SetWindowPos(data.hWnd, NULL,
                 data.originalRect.left, data.originalRect.top,
                 data.originalRect.right - data.originalRect.left,
                 data.originalRect.bottom - data.originalRect.top,
                 SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOSIZE);
    SetWindowPos(data.hWnd, NULL,
                 data.originalRect.left, data.originalRect.top,
                 data.originalRect.right - data.originalRect.left,
                 data.originalRect.bottom - data.originalRect.top,
                 SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOMOVE);

    data.isFullScreen = false;
}

static void do_bye_handling_things(const WindowData& data)
{
    LOG_TRC_NOFILE(
        "Document window terminating on JavaScript side. Closing our end of the socket.");

    // Close one end of the socket pair, that will wake up the forwarding thread above
    fakeSocketClose(data.closeNotificationPipeForForwardingThread[0]);

    // For the welcome slideshow we need to close the window ourselves
    if (data.mode == DocumentMode::WELCOME)
        PostMessageW(data.hWnd, WM_CLOSE, 0, 0);
}

static void do_print(int appDocId)
{
    const std::string tempFile = FileUtil::createRandomTmpDir() + "\\p.pdf";
    const std::string tempFileUri = Poco::URI(Poco::Path(tempFile)).toString();

    DocumentData::get(appDocId).loKitDocument->saveAs(tempFileUri.c_str(), "pdf", nullptr);

    STARTUPINFOW startupInfo{ sizeof(STARTUPINFOW) };
    PROCESS_INFORMATION processInformation;

    if (!CreateProcessW(
            Util::string_to_wide_string(app_installation_path + "..\\PrintPDFAndDelete.exe").c_str(),
            Util::string_to_wide_string("PrintPDFAndDelete " + tempFileUri).data(), NULL, NULL,
            TRUE, 0, NULL, NULL, &startupInfo, &processInformation))
        LOG_ERR("CreateProcess failed: " << GetLastError());
}

static void do_other_message_handling_things(const WindowData& data, const char* message)
{
    LOG_TRC_NOFILE("Handling other message:'" << message << "'");

    fakeSocketWriteQueue(data.fakeClientFd, message, strlen(message));
}

namespace
{
// Deliver a reply to a browser-side postMobileCall() as a posted web message,
// i.e. structured data rather than generated JS source. We build the envelope
// {"id":<id>,"reply":<value>} and marshal it to the WebView2 UI thread, where
// PostWebMessageAsJson() hands the parsed value to the page's
// chrome.webview 'message' listener (see global.js). The browser receives
// <reply> as a real JS value (string or object), so Windows paths, file names
// etc. never get interpolated into a JS string literal. This mirrors how the
// macOS (WKScriptMessageHandlerWithReply) and Qt (QWebChannel) apps return
// values; JSON encoding takes care of all escaping.
void postReplyToCall(HWND hWnd, int id, const Poco::Dynamic::Var& reply)
{
    Poco::JSON::Object::Ptr envelope = new Poco::JSON::Object();
    envelope->set("id", id);
    envelope->set("reply", reply);
    std::ostringstream oss;
    envelope->stringify(oss);
    PostMessageW(hWnd, CODA_WM_POSTWEBMESSAGE, (WPARAM)_strdup(oss.str().c_str()), 0);
}
} // namespace

static void do_getrecentdocs(const WindowData& data, int id)
{
    postReplyToCall(data.hWnd, id, recentFiles.serialiseFiltered(currentlyOpenDocumens()));
}

// It happens that some other process opens the clipboard for a short time, and if we happen to try
// during that time it will fail. Workaround for that: a function that tries a couple of times.

static BOOL try_open_clipboard(HWND hWnd)
{
    const int NTRIES = 10;
    for (int i = 0; i < NTRIES; i++)
    {
        if (OpenClipboard(hWnd))
            return TRUE;
        Sleep(5);
    }
    LOG_ERR("OpenClipboard() failed repeatedly, last error: " << GetLastError());
    return FALSE;
}

static std::wstring get_clipboard_format_name(UINT format)
{
    const int NNAME{ 1000 };
    wchar_t name[NNAME];
    int nwc = GetClipboardFormatNameW(format, name, NNAME);
    if (nwc)
        return std::wstring(name, nwc);
    return L"";
}

static std::string get_html_clipboard_fragment(const char* data)
{
    std::string htmlData(data);

    std::regex startRegex("(\r|\n)StartFragment:(\\d+)(\r|\n)");
    std::regex endRegex("(\r|\n)EndFragment:(\\d+)(\r|\n)");

    std::smatch match;
    size_t startPos = std::string::npos;
    size_t endPos = std::string::npos;

    if (std::regex_search(htmlData, match, startRegex))
        startPos = std::stoul(match[2]);

    if (std::regex_search(htmlData, match, endRegex))
        endPos = std::stoul(match[2]);

    if (startPos == std::string::npos || endPos == std::string::npos || startPos >= endPos || endPos > htmlData.size())
        return "";

    return htmlData.substr(startPos, endPos - startPos);
}

// Convert an HTML document into the Windows "HTML Format" clipboard payload.
//
// See
// https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
// for the specification of that format.
//
// This is deliberately a tiny tokenizer, not a full parser. It is not likely that
// engine would actually produce pathological HTML with things like the string "<body"
// inside an attribute, comment, or whatever, but be careful anyway.
//   * The <body> start tag may carry attributes; we find the true end '>'
//     of the start tag while respecting quoted attribute values.
//   * The substring "<body" may occur inside an attribute value in <head>
//     (e.g. <meta content="<body>">). Because we scan tag by tag and skip
//     over quoted attribute regions, such occurrences are not mistaken for
//     the body element.
//   * Comments, <!doctype>/<?...?> declarations, and the raw-text elements
//     <script>/<style>/<textarea>/<title> are skipped wholesale, so "<body"
//     or "</body>" appearing as text/inside them is ignored too.
//
//  Header lines use CRLF, matching what Windows browsers emit.

struct BodySpan
{
    bool found = false;
    bool endFound = false;
    std::size_t startBegin = 0;
    std::size_t endEnd = 0;
};

static bool case_insensitive_match(const std::string& haystack, std::size_t pos, const std::string_view needle)
{
    if (pos + needle.size() > haystack.size())
        return false;
    for (std::size_t k = 0; k < needle.size(); ++k)
    {
        auto a = static_cast<unsigned char>(haystack[pos + k]);
        auto b = static_cast<unsigned char>(needle[k]);
        if (std::tolower(a) != std::tolower(b))
            return false;
    }
    return true;
}

// i points at the '<' of a tag. Return the index just past the tag's
// closing '>', honoring single- and double-quoted attribute values (so a
// '>' inside a quoted value does not end the tag).
static std::size_t skip_tag(const std::string& s, std::size_t i)
{
    const std::size_t n = s.size();
    ++i;
    char quote = 0;
    while (i < n)
    {
        char c = s[i];
        if (quote)
        {
            if (c == quote)
                quote = 0;
        }
        else if (c == '"' || c == '\'')
            quote = c;
        else if (c == '>')
            return i + 1;
        ++i;
    }
    return n; // unterminated tag: treat rest of string as the tag
}

static bool name_equals(const std::string_view name, const std::string_view lit)
{
    if (name.size() != lit.size())
        return false;
    for (std::size_t k = 0; k < lit.size(); ++k)
    {
        if (std::tolower(static_cast<unsigned char>(name[k])) !=
            std::tolower(static_cast<unsigned char>(lit[k])))
            return false;
    }
    return true;
}

static BodySpan find_body(const std::string& s)
{
    BodySpan r;
    const std::size_t n = s.size();
    std::size_t i = 0;

    while (i < n)
    {
        if (s[i] != '<')
        {
            ++i;
            continue;
        }

        // Comment: <!-- ... -->
        if (case_insensitive_match(s, i, "<!--"))
        {
            std::size_t e = s.find("-->", i + 4);
            i = (e == std::string::npos) ? n : e + 3;
            continue;
        }
        // Declaration or processing instruction: <! ... >  /  <? ... >
        if (i + 1 < n && (s[i + 1] == '!' || s[i + 1] == '?'))
        {
            std::size_t e = s.find('>', i + 2);
            i = (e == std::string::npos) ? n : e + 1;
            continue;
        }

        const bool isEnd = (i + 1 < n && s[i + 1] == '/');
        const std::size_t nameStart = i + (isEnd ? 2 : 1);

        // Read an ASCII tag name (letters, digits, '-').
        std::size_t j = nameStart;
        while (j < n)
        {
            unsigned char u = static_cast<unsigned char>(s[j]);
            if (std::isalnum(u) || u == '-')
                ++j;
            else
                break;
        }
        std::string_view name(s.data() + nameStart, j - nameStart);

        if (name.empty()) // a bare '<' that isn't a real tag
        {
            ++i;
            continue;
        }

        if (!isEnd && name_equals(name, "body"))
        {
            r.found = true;
            r.startBegin = i;
            i = skip_tag(s, i); // step past the (possibly attributed) start tag
            continue;
        }
        if (isEnd && name_equals(name, "body"))
        {
            r.endFound = true;
            r.endEnd = skip_tag(s, i);
            break; // done: we have both ends
        }

        // Raw-text elements: skip their entire content to the matching end tag
        // so their text (which may contain '<', '>', quotes, "<body>", etc.)
        // never confuses the scan.
        if (!isEnd && (name_equals(name, "script") || name_equals(name, "style") ||
                       name_equals(name, "textarea") || name_equals(name, "title")))
        {
            std::size_t after = skip_tag(s, i);
            std::string endTag = "</";
            endTag.append(name);
            std::size_t e = after;
            std::size_t close = std::string::npos;
            while (e + endTag.size() <= n)
            {
                if (case_insensitive_match(s, e, endTag))
                {
                    close = e;
                    break;
                }
                ++e;
            }
            i = (close == std::string::npos) ? n : skip_tag(s, close);
            continue;
        }

        // Any other tag: skip it.
        i = skip_tag(s, i);
    }
    return r;
}

static std::string num10(std::size_t v)
{
    char b[24];
    std::snprintf(b, sizeof b, "%010zu", v);
    return std::string(b);
}

static std::string build_header(std::size_t startHtml, std::size_t endHtml,
                                std::size_t startFrag, std::size_t endFrag)
{
    std::string h;
    h += "Version:0.9\r\n";
    h += "StartHTML:"     + num10(startHtml) + "\r\n";
    h += "EndHTML:"       + num10(endHtml)   + "\r\n";
    h += "StartFragment:" + num10(startFrag) + "\r\n";
    h += "EndFragment:"   + num10(endFrag)   + "\r\n";
    return h;
}

std::string generate_html_format(const std::string& html)
{
    static const std::string SF = "<!--StartFragment-->";
    static const std::string EF = "<!--EndFragment-->";

    // Measure the headers once with dummy values.
    const std::size_t headerLen = build_header(0, 0, 0, 0).size();

    // Locate the body. Degrade gracefully if it's missing or unterminated.
    const BodySpan b = find_body(html);
    const std::size_t startBegin = b.found ? b.startBegin : 0;
    const std::size_t endEnd = (b.found && b.endFound) ? b.endEnd : html.size();

    // Assemble the content: prefix + <!--StartFragment--> + <body>..</body>
    //                       + <!--EndFragment--> + suffix.
    std::string content;
    content.reserve(html.size() + SF.size() + EF.size());
    content.append(html, 0, startBegin);
    content.append(SF);
    content.append(html, startBegin, endEnd - startBegin);
    content.append(EF);
    content.append(html, endEnd, std::string::npos);

    // Offsets are byte positions from the very start of the payload.
    const std::size_t startHtml = headerLen;
    const std::size_t endHtml = headerLen + content.size();
    const std::size_t startFrag = headerLen + startBegin + SF.size(); // at '<body'
    const std::size_t endFrag = headerLen + endEnd + SF.size(); // just past '</body>'

    return build_header(startHtml, endHtml, startFrag, endFrag) + content;
}

static void do_open_hyperlink(HWND hWnd, std::wstring url)
{
    // For file: URIs hand ShellExecute a native (UNC-aware) Windows path; other
    // schemes (http:, mailto:, ...) pass through unchanged.
    const std::wstring target =
        Util::string_to_wide_string(fileUriToWindowsPath(Util::wide_string_to_string(url)));
    ShellExecuteW(hWnd, NULL, target.c_str(), NULL, NULL, SW_SHOW);
}

struct MonitorInfo
{
    HMONITOR hMonitor;
    DWORD dwFlags;
};

typedef std::vector<MonitorInfo> Monitors;

BOOL monitorEnum(HMONITOR monitor, HDC, LPRECT, LPARAM data)
{
    MONITORINFO monitorInfo = { sizeof(monitorInfo) };
    GetMonitorInfo(monitor, &monitorInfo);

    Monitors& monitors = *reinterpret_cast<Monitors*>(data);
    monitors.push_back(MonitorInfo{monitor, monitorInfo.dwFlags});
    return true;
}

Monitors getMonitors()
{
    Monitors monitors;
    EnumDisplayMonitors(nullptr, nullptr, monitorEnum, reinterpret_cast<LPARAM>(&monitors));
    return monitors;
}

static void exchangeMonitors(WindowData& data)
{
    Monitors monitors(getMonitors());
    if (monitors.size() < 2)
        return;

    HMONITOR hConsoleMonitor = data.hConsoleWnd ? MonitorFromWindow(data.hConsoleWnd, MONITOR_DEFAULTTONEAREST) : 0;
    HMONITOR hPresentationMonitor = MonitorFromWindow(data.hWnd, MONITOR_DEFAULTTONEAREST);

    size_t origConsoleMonitor = 0;
    size_t origPresentationMonitor = 0;
    for (size_t i = 0; i < monitors.size(); ++i)
    {
        if (hConsoleMonitor && monitors[i].hMonitor == hConsoleMonitor)
            origConsoleMonitor = i;
        if (monitors[i].hMonitor == hPresentationMonitor)
            origPresentationMonitor = i;
    }

    leave_full_screen(data);

    size_t newPresentationMonitor = origPresentationMonitor;

    if (data.hConsoleWnd)
    {
        leave_full_screen(windowData[data.hConsoleWnd]);

        size_t newConsoleMonitor = (origConsoleMonitor + 1) % monitors.size();
        if (newConsoleMonitor == newPresentationMonitor)
            newPresentationMonitor = (newPresentationMonitor + 1) % monitors.size();

        enter_full_screen(windowData[data.hConsoleWnd], monitors[newConsoleMonitor].hMonitor, false);
    }
    else
    {
        newPresentationMonitor = (newPresentationMonitor + 1) % monitors.size();
    }

    enter_full_screen(data, monitors[newPresentationMonitor].hMonitor, false);
}

static std::string pathToURI(const Poco::Path& path)
{
    auto uri = Poco::URI(path);

    if (path.getNode() == "")
        return uri.toString();

    uri.setHost(path.getNode());
    return uri.toString();
}

static std::vector<FilenameAndUri> fileOpenDialog()
{
    IFileOpenDialog* dialog;

    if (!SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER,
                                    IID_IFileOpenDialog, reinterpret_cast<void**>(&dialog))))
        fatal("CoCreateInstance(CLSID_FileOpenDialog) failed");

    COMDLG_FILTERSPEC filter[] = {
        { L"",
          L"*.odt;*.docx;*.doc;*.rtf;*.txt;*.md;*.ods;*.xlsx;*.xls;*.odp;*.pptx;*.ppt" },
        { L"", L"*.*" }
    };

    if (!SUCCEEDED(dialog->SetFileTypes(sizeof(filter) / sizeof(filter[0]), &filter[0])))
        fatal("dialog->SetFileTypes() failed");

    FILEOPENDIALOGOPTIONS options;
    if (SUCCEEDED(dialog->GetOptions(&options)))
    {
        options |= FOS_ALLOWMULTISELECT;
        options &= ~FOS_DONTADDTORECENT;
        dialog->SetOptions(options);
    }

    HRESULT dialogResult = dialog->Show(hiddenOwnerWindow);

    if (!SUCCEEDED(dialogResult))
        return {};

    std::vector<FilenameAndUri> result;

    IShellItemArray* items;
    if (!SUCCEEDED(dialog->GetResults(&items)))
        fatal("dialog->GetResults() failed");

    DWORD numItems;
    if (!SUCCEEDED(items->GetCount(&numItems)))
        fatal("items->GetCount() failed");

    for (int i = 0; i < numItems; i++)
    {
        IShellItem* item;
        PWSTR fileSysPath;
        if (SUCCEEDED(items->GetItemAt(i, &item)) &&
            SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &fileSysPath)))
        {
            auto path = Poco::Path(Util::wide_string_to_string(std::wstring(fileSysPath)));
            result.push_back({ path.getFileName(), pathToURI(path) });
            CoTaskMemFree(fileSysPath);
            item->Release();
        }
    }
    items->Release();
    dialog->Release();

    return result;
}

static std::vector<COMDLG_FILTERSPEC>getSaveAsFormats(COKitDocumentType docType)
{
    std::vector<COMDLG_FILTERSPEC> result;

    if (docType == COKitDocumentType::TEXT)
    {
        result.push_back({L"ODT", L"*.odt"});
        result.push_back({L"RTF", L"*.rtf"});
        result.push_back({L"DOCX", L"*.docx"});
        result.push_back({L"DOC", L"*.doc"});
    }
    else if (docType == COKitDocumentType::SPREADSHEET)
    {
        result.push_back({L"ODS", L"*.ods"});
        result.push_back({L"XLSX", L"*.xlsx"});
        result.push_back({L"XLS", L"*.xls"});
    }
    else if (docType == COKitDocumentType::PRESENTATION)
    {
        result.push_back({L"ODP", L"*.odp"});
        result.push_back({L"PPTX", L"*.pptx"});
        result.push_back({L"PPT", L"*.ppt"});
    }
    else if (docType == COKitDocumentType::DRAWING)
    {
        result.push_back({L"ODG", L"*.odg"});
    }

    return result;
}

static FilenameAndUri fileSaveDialog(const std::string& name,
                                     const std::string& folder,
                                     const std::vector<COMDLG_FILTERSPEC>& extensions)
{
    IFileSaveDialog* dialog;

    if (!SUCCEEDED(CoCreateInstance(CLSID_FileSaveDialog, NULL, CLSCTX_INPROC_SERVER,
                                    IID_IFileSaveDialog, reinterpret_cast<void**>(&dialog))))
        fatal("CoCreateInstance(CLSID_FileSaveDialog) failed");

    FILEOPENDIALOGOPTIONS options;

    if (!SUCCEEDED(dialog->GetOptions(&options)))
        fatal("dialog->GetOptions() failed");

    options |= FOS_STRICTFILETYPES;

    if (!SUCCEEDED(dialog->SetOptions(options)))
        fatal("dialog->SetOptions() failed");

    if (extensions.size() > 0)
    {
        if (!SUCCEEDED(dialog->SetDefaultExtension(extensions[0].pszSpec)))
            fatal("dialog->SetDefaultExtension() failed");

        if (!SUCCEEDED(dialog->SetFileTypes(extensions.size(), extensions.data())))
            fatal("dialog->SetFileTypes() failed");
    }

    if (name != "" && !SUCCEEDED(dialog->SetFileName(Util::string_to_wide_string(name).c_str())))
        fatal("dialog->SetFileName() failed");

    if (folder != "")
    {
        std::wstring wfolder = Util::string_to_wide_string(folder);
        std::replace(wfolder.begin(), wfolder.end(), L'/', L'\\');

        IShellItem* psiFolder;
        if (SUCCEEDED(SHCreateItemFromParsingName(wfolder.c_str(), nullptr, IID_PPV_ARGS(&psiFolder))))
        {
            if (!SUCCEEDED(dialog->SetFolder(psiFolder)))
                fatal("dialog->SetFolder() failed");
            psiFolder->Release();
        }
    }

    if (!SUCCEEDED(dialog->Show(hiddenOwnerWindow)))
        return {};

    IShellItem* item;
    if (!SUCCEEDED(dialog->GetResult(&item)))
        fatal("dialog->GetResult() failed");

    PWSTR fileSysPath;
    if (!SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &fileSysPath)))
        fatal("item->GetDisplayName() failed");

    auto path = Poco::Path(Util::wide_string_to_string(std::wstring(fileSysPath)));
    CoTaskMemFree(fileSysPath);
    item->Release();
    dialog->Release();

    return { path.getFileName(), pathToURI(path) };
}

static void arrangePresentationWindows(WindowData& data)
{
    Monitors monitors(getMonitors());
    data.numMonitors = monitors.size();

    HMONITOR laptopMonitor = 0;
    HMONITOR externalMonitor = 0;

    for (const auto& monitor : monitors)
    {
        if (monitor.dwFlags & MONITORINFOF_PRIMARY)
        {
            if (!laptopMonitor)
                laptopMonitor = monitor.hMonitor;
        }
        else
        {
            if (!externalMonitor)
                externalMonitor = monitor.hMonitor;
        }
    }

    if (!laptopMonitor || !externalMonitor)
    {
        laptopMonitor = MonitorFromWindow(data.hWnd, MONITOR_DEFAULTTONEAREST);
        externalMonitor = 0;
        for (const auto& monitor : monitors)
        {
            if (monitor.hMonitor != laptopMonitor)
            {
                externalMonitor = monitor.hMonitor;
                break;
            }
        }
    }

    leave_full_screen(data);
    if (data.hConsoleWnd)
        leave_full_screen(windowData[data.hConsoleWnd]);

    HMONITOR presenterMonitor = externalMonitor ? externalMonitor : laptopMonitor;

    enter_full_screen(data, presenterMonitor, true);

    if (data.hConsoleWnd)
    {
        if (externalMonitor)
            enter_full_screen(windowData[data.hConsoleWnd], laptopMonitor, true);
        else
            BringWindowToTop(data.hConsoleWnd);
    }
}

// Identifier for the Ctrl+Shift+I hotkey that opens the WebView2 developer
// tools. The range 0x0000 to 0xBFFF is reserved for application hotkeys.
static const int HOTKEY_ID_DEVTOOLS = 0x00DA;

// Window procedure for a hidden window used as clipboard owner. It lives for the whole app run, so
// its delayed-render promise only has to be materialized (WM_RENDERALLFORMATS) once, when the app
// exits, not when an individual document window closes. Also used as parent window for the file
// open and save dialogs.
static LRESULT CALLBACK HiddenOwnerWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        case WM_RENDERFORMAT:
        {
            UINT format = (UINT)wParam;
            std::string mimeType = MIME_type_for_clipboard_format(format);
            if (!mimeType.empty())
            {
                HANDLE hData = copyEngineClipboardData(format, mimeType);
                if (hData)
                    SetClipboardData(format, hData);
            }
            return 0;
        }

        case WM_RENDERALLFORMATS:
            // This window is about to be destroyed (the app is exiting) while it still owns the
            // clipboard with formats it only promised. Render them all now, so the content outlives
            // the app. The engine still holds the one shared clipboard, so the bytes are available.
            materialize_clipboard_formats();
            return 0;

        case WM_DESTROYCLIPBOARD:
            weOwnTheClipboard = false;
            return 0;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        case WM_CREATE:
            {
                // Contrary to documentation, when you use CW_USEDEFAULT for the x and y parameters
                // in the CreateWindowW() call, Windows will occasionally place the window so that
                // it is partially obscured by the taskbar. Workaround for that.

                MONITORINFO monitorInfo;
                monitorInfo.cbSize = sizeof(monitorInfo);
                GetMonitorInfoW(MonitorFromWindow(hWnd, MONITOR_DEFAULTTOPRIMARY), &monitorInfo);

                CREATESTRUCT *cs = (CREATESTRUCT *)lParam;

                int x = cs->x, y = cs->y;

                if (cs->cx < (monitorInfo.rcWork.right - monitorInfo.rcWork.left))
                {
                    if (cs->x < monitorInfo.rcWork.left)
                    {
                        // Left edge obscured by taskbar at the left. Move window right by the width
                        // of the taskbar.
                        x = cs->x + (monitorInfo.rcWork.left - monitorInfo.rcMonitor.left);
                    } else if (cs->x + cs->cx > monitorInfo.rcWork.right)
                    {
                        // Left edge obscured by taskbar at the right. Move window left.
                        x = cs->x - (monitorInfo.rcMonitor.right - monitorInfo.rcWork.right);
                    }
                }
                if (cs->cy < (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top))
                {
                    if (cs->y < monitorInfo.rcWork.top)
                    {
                        // Top edge obscured by taskbar at the top. Move window down by the height
                        // of the taskbar.
                        y = cs->y + (monitorInfo.rcWork.top - monitorInfo.rcMonitor.top);
                    } else if (cs->y + cs->cy > monitorInfo.rcWork.bottom)
                    {
                        // Bottom edge obscured by taskbar at the bottom. Move window up.
                        y = cs->y - (monitorInfo.rcMonitor.bottom - monitorInfo.rcWork.bottom);
                    }
                }

                if (x != cs->x || y != cs->y)
                    SetWindowPos(hWnd, NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
                return 0;
            }

        case WM_SIZING:
            {
                int minimumWidth = 1000, minimumHeight = 800;

                HMONITOR monitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
                MONITORINFO monitorInfo;
                monitorInfo.cbSize = sizeof(monitorInfo);
                if (GetMonitorInfoW(monitor, &monitorInfo))
                {
                    // If the monitor has a "reasonable" aspect ratio (1:1 to 2:1) (taking into
                    // account it might be in landscape or portrait orientation), set minimum width
                    // to a quarter of monitor width and minimum height to a third of monitior
                    // height. (Because COOL requires more essential space in the vertical
                    // direction, I think.)
                    double aspectRatio =
                        (double)(monitorInfo.rcWork.right - monitorInfo.rcWork.left) / (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top);
                    if (aspectRatio >= 0.49 && aspectRatio <= 2.01)
                    {
                        // Reasonable case
                        minimumWidth = (monitorInfo.rcWork.right - monitorInfo.rcWork.left) / 4;
                        minimumHeight = (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top) / 3;
                    }
                    else if (aspectRatio < 0.49)
                    {
                        // Very narrow, set just minimum height
                        minimumHeight = (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top) / 3;
                    }
                    else if (aspectRatio > 2.01)
                    {
                        // Very wide, set just minimum width
                        minimumWidth = (monitorInfo.rcWork.right - monitorInfo.rcWork.left) / 4;
                    }
                }

                RECT* rect = (RECT*)lParam;
                if (rect->right - rect->left < minimumWidth)
                {
                    switch (wParam)
                    {
                        case WMSZ_LEFT:
                        case WMSZ_TOPLEFT:
                        case WMSZ_BOTTOMLEFT:
                            rect->left = rect->right - minimumWidth;
                            break;
                        case WMSZ_RIGHT:
                        case WMSZ_TOPRIGHT:
                        case WMSZ_BOTTOMRIGHT:
                            rect->right = rect->left + minimumWidth;
                            break;
                        case WMSZ_TOP:
                        case WMSZ_BOTTOM:
                            {
                                // Weird case, resizing height but still the width goes below the
                                // minimum? Grow width on both sizes.
                                auto mid = (rect->left + rect->right) / 2;
                                rect->left = mid - minimumWidth/2;
                                rect->right = rect->left + minimumWidth;
                            }
                            break;
                    }
                }
                if (rect->bottom - rect->top < minimumHeight)
                {
                    switch (wParam)
                    {
                        case WMSZ_TOP:
                        case WMSZ_TOPLEFT:
                        case WMSZ_TOPRIGHT:
                            rect->top = rect->bottom - minimumHeight;
                            break;
                        case WMSZ_BOTTOM:
                        case WMSZ_BOTTOMLEFT:
                        case WMSZ_BOTTOMRIGHT:
                            rect->bottom = rect->top + minimumHeight;
                            break;
                        case WMSZ_LEFT:
                        case WMSZ_RIGHT:
                            {
                                // Weird case, resizing width but still the height goes below the
                                // minimum? Grow height on both sizes.
                                auto mid = (rect->top + rect->bottom) / 2;
                                rect->top = mid - minimumHeight/2;
                                rect->bottom = rect->top + minimumHeight;
                            }
                            break;
                    }
                }
            }
            return TRUE;

        case WM_SIZE:
            if (windowData[hWnd].webViewController != nullptr)
            {
                RECT bounds;
                GetClientRect(hWnd, &bounds);
                windowData[hWnd].webViewController->put_Bounds(bounds);

                if (!windowData[hWnd].isFullScreen &&
                    persistentWindowSizeStoreOK &&
                    (wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED))
                {
                    std::vector<uint8_t> value(sizeof(PersistedDocumentWindowSize));
                    PersistedDocumentWindowSize* p = reinterpret_cast<PersistedDocumentWindowSize*>(value.data());
                    if (wParam == SIZE_RESTORED)
                    {
                        p->size.x = LOWORD(lParam);
                        p->size.y = HIWORD(lParam);
                        windowData[hWnd].previousSize = p->size;
                    }
                    else
                    {
                        p->size = windowData[hWnd].previousSize;
                    }
                    p->resizeType = wParam;
                    persistentWindowSizeStore.put(windowData[hWnd].filenameAndUri.uri.c_str(), value);
                }
            };
            break;

        case WM_SETFOCUS:
            if (windowData.count(hWnd) && windowData[hWnd].webViewController)
                windowData[hWnd].webViewController->MoveFocus(
                    COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
            break;

        case WM_DPICHANGED:
        {
            const RECT* newRect = (RECT*)lParam;
            SetWindowPos(hWnd, NULL, newRect->left, newRect->top, newRect->right - newRect->left,
                         newRect->bottom - newRect->top, SWP_NOZORDER | SWP_NOACTIVATE);
        }
        break;

        case WM_DISPLAYCHANGE:
        {
            auto& data = windowData[hWnd];
            if (data.hConsoleWnd || data.isPresFullScreen)
            {
                int numMonitors = getMonitors().size();
                if (data.numMonitors != numMonitors)
                    arrangePresentationWindows(data);
            }
        }
        break;

        case WM_CLOSE:
            if (windowData[hWnd].mode == DocumentMode::STARTER)
                ; // Nothing
            else if (!windowData[hWnd].isConsole)
            {
                do_bye_handling_things(windowData[hWnd]);

                DocumentData::deallocate(windowData[hWnd].appDocId);
            }
            else
            {
                auto& parent = windowData[windowData[hWnd].hParentWnd];
                leave_full_screen(parent);
                parent.hConsoleWnd = 0;
            }
            DestroyWindow(hWnd);
            break;

        case WM_DESTROY:
            if (windowData[hWnd].app2js.joinable())
                windowData[hWnd].app2js.join();
            if (DocumentData::count() == 0)
            {
                if (persistentWindowSizeStoreOK)
                {
                    persistentWindowSizeStoreOK = false;
                    persistentWindowSizeStore.close();
                }
                stopServer();
            }
            break;

        case WM_NCDESTROY:
        {
            auto it = windowData.find(hWnd);
            if (it != windowData.end())
            {
                if (it->second.isConsole)
                {
                    auto& data = it->second;
                    if (data.webViewController)
                    {
                        data.webViewController->Close();
                        data.webViewController = nullptr;
                    }
                    data.webView = nullptr;
                }
                windowData.erase(hWnd);
            }
            break;
        }


        case CODA_WM_EXECUTESCRIPT:
            windowData[hWnd].webView->ExecuteScript(
                Util::string_to_wide_string(std::string((char*)wParam)).c_str(),
                Microsoft::WRL::Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
                    [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT
                    {
                        // LOG_TRC(Util::wide_string_to_string(resultObjectAsJson));
                        return S_OK;
                    })
                    .Get());
            std::free((char*)wParam);
            break;

        case CODA_WM_POSTWEBMESSAGE:
            windowData[hWnd].webView->PostWebMessageAsJson(
                Util::string_to_wide_string(std::string((char*)wParam)).c_str());
            std::free((char*)wParam);
            break;

        case CODA_WM_LOADNEXTDOCUMENT:
            if (filenamesAndUrisToOpen.size() > 0)
            {
                auto nextDocument = filenamesAndUrisToOpen.front();
                filenamesAndUrisToOpen.pop_front();
                openCOOLWindow(nextDocument, DocumentMode::EDIT);
            }
            break;

        case WM_ACTIVATE:
            // Hold the developer-tools hotkey only while this window is the
            // active one, so the key is not taken from other applications and
            // each document window opens its own developer tools.
            if (LOWORD(wParam) == WA_INACTIVE)
                UnregisterHotKey(hWnd, HOTKEY_ID_DEVTOOLS);
            else
                RegisterHotKey(hWnd, HOTKEY_ID_DEVTOOLS,
                               MOD_CONTROL | MOD_SHIFT | MOD_NOREPEAT, 'I');
            break;

        case WM_HOTKEY:
            if (wParam == HOTKEY_ID_DEVTOOLS)
            {
                auto it = windowData.find(hWnd);
                if (it != windowData.end() && it->second.webView)
                    it->second.webView->OpenDevToolsWindow();
            }
            break;

        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
            break;
    }

    return 0;
}

// From https://stackoverflow.com/questions/51334674/how-to-detect-windows-10-light-dark-mode-in-win32-application

static bool isLightTheme()
{
    int value;
    DWORD cbData = 4;
    auto res = RegGetValueW(
        HKEY_CURRENT_USER,
        L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
        L"AppsUseLightTheme",
        RRF_RT_REG_DWORD,
        NULL,
        &value,
        &cbData);

    if (res != ERROR_SUCCESS)
        return true;

    return value == 1;
}

static HRESULT GetStreamForIFStream(std::ifstream& file, IStream** outStream)
{
    std::vector<char> data((std::istreambuf_iterator<char>(file)),
                           std::istreambuf_iterator<char>());

    wil::com_ptr<IStream> stream;
    stream.attach(SHCreateMemStream(
        reinterpret_cast<const BYTE*>(data.data()),
        static_cast<UINT>(data.size())));
    if (!stream)
        return E_OUTOFMEMORY;
    *outStream = stream.detach();
    return S_OK;
}

static HRESULT webResourceRequestedHandler(ICoreWebView2Environment* env,
                                           ICoreWebView2* sender,
                                           ICoreWebView2WebResourceRequestedEventArgs* args)
{
    wil::com_ptr<ICoreWebView2WebResourceRequest> request;
    HRESULT hr;

    hr = args->get_Request(&request);
    if (!SUCCEEDED(hr))
    {
        LOG_ERR_S("get_Request() failed");
        return hr;
    }

    wil::unique_cotaskmem_string uri;
    hr = request->get_Uri(&uri);
    if (!SUCCEEDED(hr))
    {
        LOG_ERR_S("get_Uri() failed");
        return hr;
    }

    std::string uri2 = Uri::decode(Util::wide_string_to_string(uri.get()));
    Poco::URI requestUri(uri2);
    Poco::URI::QueryParameters params = requestUri.getQueryParameters();
    std::string wopiSrc, tag;

    const bool isMedia = requestUri.getPath() == "/media";
    const bool isVtt = requestUri.getPath() == "/mediavtt";

    if (!isMedia && !isVtt)
    {
        LOG_WRN_S("Unhandled path [" << requestUri.getPath() << ']');
        return E_FAIL;
    }

    for (const auto& it : params)
    {
        if (it.first == "WOPISrc")
            wopiSrc = it.second;
        else if (it.first == "Tag")
            tag = it.second;
    }

    if (tag.empty() || wopiSrc.empty())
    {
        LOG_ERR_S("Missing WOPISrc or Tag in ["
                  << uri2 << ']');
        return E_FAIL;
    }

    // For some reason for local documents the WOPISrc comes
    // here with a drive letter in the host position of the URI.
    // I.e. "file://C:/Users/foo/bar.ext" =>
    // "file:///C:/Users/foo/bar.ext"
    if (wopiSrc.size() > 10)
    {
        if (isalpha((unsigned char)wopiSrc[7]) &&
            wopiSrc[8] == ':')
            wopiSrc = "file:///" +
                std::string(1, wopiSrc[7]) +
                ":" +
                wopiSrc.substr(9);
    }

    std::shared_ptr<DocumentBroker> docBroker;
    const std::string docKey = RequestDetails::getDocKey(wopiSrc);
    {
        std::lock_guard<std::mutex> lock(DocBrokersMutex);
        const auto it = DocBrokers.find(docKey);
        if (it != DocBrokers.end())
            docBroker = it->second;
    }
    if (!docBroker)
    {
        LOG_ERR_S("No DocBroker for WOPISrc [" << wopiSrc << ']');
        return E_FAIL;
    }

    std::string mediaPath = docBroker->getEmbeddedMediaPath(tag);
    if (mediaPath.empty())
    {
        LOG_ERR_S("No media path for tag [" << tag << ']');
        return E_FAIL;
    }

    // Yes, the same code snippet once again. FIXME: Should
    // obviously factor this out into a utility function.
    if (mediaPath.length() > 4 && mediaPath[0] == '/' &&
        mediaPath[2] == ':' && mediaPath[3] == '/')
        mediaPath = mediaPath.substr(1);

    std::ifstream file;
    FileUtil::openFileToIFStream(mediaPath, file);
    if (!file.is_open())
    {
        LOG_ERR_S("Cannot open [" << mediaPath << "]");
        return E_FAIL;
    }

    const std::wstring mimeType = (isVtt ? L"text/vtt" : L"application/octet-stream");
    wil::com_ptr<IStream> contentStream;
    hr = GetStreamForIFStream(file, &contentStream);
    if (!SUCCEEDED(hr))
        return hr;

    wil::com_ptr<ICoreWebView2WebResourceResponse> response;
    hr = env->CreateWebResourceResponse(
        contentStream.get(),
        200, L"OK",
        (L"Content-Type: " + mimeType + L"\r\n" +
         L"Access-Control-Allow-Origin: *\r\n").c_str(),
        &response);
    if (!SUCCEEDED(hr))
    {
        LOG_ERR_S("CreateWebResourceResponse() failed");
        return hr;
    }

    hr = args->put_Response(response.get());
    if (!SUCCEEDED(hr))
    {
        LOG_ERR_S("put_Response() failed");
        return hr;
    }
    return S_OK;
}

// Register the ODF IFilter (odffilter.dll) with Windows Search and the
// FullDetails / PreviewDetails property strings Explorer uses, both under
// HKCU\Software\Classes. The two bindings the MSIX manifest cannot
// express declaratively are:
//
//   - the per-extension IFilter chain Windows Search walks:
//       .odt -> PersistentHandler ->
//       PersistentAddinsRegistered\{IID_IFilter} -> IFilter CLSID
//   - the per-extension FullDetails / PreviewDetails strings under
//       SystemFileAssociations\.<ext>, which control the field list shown
//       in Properties->Details and in the preview pane (the property
//       handler itself is wired via desktop2:DesktopPropertyHandler in
//       the manifest, but it only fills the fields Explorer asks for).
//
// HKCU\Software\Classes is silo'd for packaged processes - writes from
// this process would land in a per-package virtual class hive that
// Windows Search and Explorer never read. ProcMon confirmed this even
// with unvirtualizedResources and with a self-respawned breakaway child:
// the silo follows packaged-EXE images regardless. So instead we build a
// .reg file and shell out to System32\reg.exe (unpackaged, escapes the
// silo with the DesktopAppBreakaway attribute). reg.exe writes the
// values to real HKCU where the shell looks for them.
//
// Idempotent on every launch.
//
// Limitation: HKCU writes survive package uninstall. Users who remove
// Collabora Office may continue to see (silent) failed lookups for ODF
// IFilter until the keys are pruned.
static void registerOdfShellExtensions()
{
    // CLSIDs match engine/shell/source/win32/shlxthandler/odffilter/odffilter.hxx
    static constexpr wchar_t kPersistentHandlerClsid[] =
        L"{3EE9BB34-748E-4FBA-B6A5-94C200A11455}";
    static constexpr wchar_t kIFilterClsid[] =
        L"{DEB88601-6245-4803-81A5-13082BB738FF}";
    // Microsoft's well-known IID_IFilter
    static constexpr wchar_t kIidIFilter[] =
        L"{89BCB740-6119-101A-BCB7-00DD010655AF}";

    // Extensions we can actually filter - matches OOFileExtensionTable in
    // engine/shell/source/win32/shlxthandler/util/fileextensions.cxx
    static constexpr const wchar_t* kExtensions[] = {
        L".odt", L".ott", L".odm", L".oth",
        L".ods", L".ots",
        L".odg", L".otg",
        L".odp", L".otp",
        L".odf", L".odb",
        L".sxw", L".stw", L".sxg",
        L".sxc", L".stc",
        L".sxi", L".sti",
        L".sxd", L".std",
        L".sxm",
        // Flat ODF (single XML file, no zip container).
        L".fodt", L".fods", L".fodg", L".fodp",
    };

    static constexpr wchar_t kFullDetails[] =
        L"prop:System.PropGroup.Description;"
        L"System.Title;System.Author;System.Subject;"
        L"System.Keywords;System.Comment;"
        L"System.PropGroup.FileSystem;"
        L"System.ItemNameDisplay;System.ItemTypeText;"
        L"System.ItemFolderPathDisplay;System.Size;"
        L"System.DateCreated;System.DateModified;System.FileAttributes";

    static constexpr wchar_t kPreviewDetails[] =
        L"prop:System.Title;*System.Author;*System.Subject;*System.Comment";

    // Build the .reg file content.
    std::wstring reg = L"Windows Registry Editor Version 5.00\r\n\r\n";
    for (const wchar_t* ext : kExtensions)
    {
        reg += L"[HKEY_CURRENT_USER\\Software\\Classes\\";
        reg += ext;
        reg += L"\\PersistentHandler]\r\n@=\"";
        reg += kPersistentHandlerClsid;
        reg += L"\"\r\n\r\n";
    }
    reg += L"[HKEY_CURRENT_USER\\Software\\Classes\\CLSID\\";
    reg += kPersistentHandlerClsid;
    reg += L"]\r\n@=\"Collabora Office ODF IFilter Persistent Handler\"\r\n\r\n";
    reg += L"[HKEY_CURRENT_USER\\Software\\Classes\\CLSID\\";
    reg += kPersistentHandlerClsid;
    reg += L"\\PersistentAddinsRegistered\\";
    reg += kIidIFilter;
    reg += L"]\r\n@=\"";
    reg += kIFilterClsid;
    reg += L"\"\r\n\r\n";
    for (const wchar_t* ext : kExtensions)
    {
        reg += L"[HKEY_CURRENT_USER\\Software\\Classes\\SystemFileAssociations\\";
        reg += ext;
        reg += L"]\r\n\"FullDetails\"=\"";
        reg += kFullDetails;
        reg += L"\"\r\n\"PreviewDetails\"=\"";
        reg += kPreviewDetails;
        reg += L"\"\r\n\r\n";
    }

    // Write the .reg file under %LocalAppData%\<appName> with the UTF-16 LE
    // BOM that reg.exe import expects.
    PWSTR appDataFolder = nullptr;
    if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &appDataFolder)))
        return;
    std::wstring regFilePath = appDataFolder;
    CoTaskMemFree(appDataFolder);
    regFilePath += L"\\";
    regFilePath += appName;
    CreateDirectoryW(regFilePath.c_str(), nullptr);
    regFilePath += L"\\register-shellext.reg";

    HANDLE hFile = CreateFileW(regFilePath.c_str(), GENERIC_WRITE, 0, nullptr,
                               CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (hFile == INVALID_HANDLE_VALUE)
        return;
    const WORD bom = 0xFEFF;
    DWORD written = 0;
    WriteFile(hFile, &bom, sizeof(bom), &written, nullptr);
    WriteFile(hFile, reg.data(),
              static_cast<DWORD>(reg.size() * sizeof(wchar_t)),
              &written, nullptr);
    CloseHandle(hFile);

    // Spawn System32\reg.exe import outside the silo via the
    // DesktopAppBreakaway process attribute. The breakaway attribute does
    // not let our own (packaged) EXE escape, but reg.exe is unpackaged so
    // it goes to a plain desktop process whose writes hit real HKCU.
    wchar_t systemDir[MAX_PATH];
    if (GetSystemDirectoryW(systemDir, ARRAYSIZE(systemDir)) == 0)
        return;
    std::wstring cmdLine = L"\"";
    cmdLine += systemDir;
    cmdLine += L"\\reg.exe\" import \"";
    cmdLine += regFilePath;
    cmdLine += L"\"";

    SIZE_T attrSize = 0;
    InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
    if (attrSize == 0)
        return;
    std::vector<BYTE> attrBuffer(attrSize);
    LPPROC_THREAD_ATTRIBUTE_LIST attrList =
        reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attrBuffer.data());
    if (!InitializeProcThreadAttributeList(attrList, 1, 0, &attrSize))
        return;
    DWORD policy = PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_ENABLE_PROCESS_TREE;
    if (!UpdateProcThreadAttribute(attrList, 0,
                                   PROC_THREAD_ATTRIBUTE_DESKTOP_APP_POLICY,
                                   &policy, sizeof(policy), NULL, NULL))
    {
        DeleteProcThreadAttributeList(attrList);
        return;
    }

    // Capture reg.exe's stderr/stdout via a pipe so a failure message can
    // be surfaced verbatim (e.g. "ERROR: Access is denied.") instead of
    // just the numeric exit code.
    SECURITY_ATTRIBUTES sa = { sizeof(sa), nullptr, TRUE };
    HANDLE hReadPipe = nullptr;
    HANDLE hWritePipe = nullptr;
    if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 65536))
    {
        DeleteProcThreadAttributeList(attrList);
        return;
    }
    SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0);

    STARTUPINFOEXW si = {};
    si.StartupInfo.cb = sizeof(si);
    si.StartupInfo.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
    si.StartupInfo.wShowWindow = SW_HIDE;
    si.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
    si.StartupInfo.hStdOutput = hWritePipe;
    si.StartupInfo.hStdError = hWritePipe;
    si.lpAttributeList = attrList;

    PROCESS_INFORMATION pi = {};
    BOOL ok = CreateProcessW(NULL, cmdLine.data(), NULL, NULL, TRUE,
                             EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
                             NULL, NULL,
                             reinterpret_cast<LPSTARTUPINFOW>(&si), &pi);
    DeleteProcThreadAttributeList(attrList);
    // Parent must close its copy of the write end so the read end sees EOF
    // when reg.exe exits.
    CloseHandle(hWritePipe);
    if (!ok)
    {
        CloseHandle(hReadPipe);
        return;
    }

    WaitForSingleObject(pi.hProcess, 5000);

    std::string output;
    char buf[1024];
    DWORD nRead = 0;
    while (ReadFile(hReadPipe, buf, sizeof(buf), &nRead, nullptr) && nRead > 0)
        output.append(buf, nRead);
    CloseHandle(hReadPipe);

    DWORD exitCode = STILL_ACTIVE;
    GetExitCodeProcess(pi.hProcess, &exitCode);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    if (exitCode != 0)
    {
        while (!output.empty() && (output.back() == '\r' || output.back() == '\n'
                                   || output.back() == ' ' || output.back() == '\t'))
            output.pop_back();
        // reg.exe writes in the user's OEM (console) codepage; widen for
        // OutputDebugStringW. Log:: is not initialized yet at this point
        // in wWinMain. DebugView (Sysinternals) captures the output.
        int wlen = MultiByteToWideChar(CP_OEMCP, 0,
                                       output.c_str(), static_cast<int>(output.size()),
                                       nullptr, 0);
        std::wstring wide(wlen, L'\0');
        MultiByteToWideChar(CP_OEMCP, 0,
                            output.c_str(), static_cast<int>(output.size()),
                            wide.data(), wlen);
        std::wstring msg = L"registerOdfShellExtensions: reg.exe failed: ";
        msg += wide.empty() ? L"(no diagnostic on stderr)" : wide;
        msg += L"\n";
        OutputDebugStringW(msg.c_str());
    }
    DeleteFileW(regFilePath.c_str());
}

static void openCOOLWindow(const FilenameAndUri& filenameAndUri, DocumentMode mode)
{
    bool havePersistedSize = false;

    int width, height;
    int welcomeX = CW_USEDEFAULT, welcomeY = CW_USEDEFAULT;
    bool maximize = false;

    if (mode != DocumentMode::WELCOME && mode != DocumentMode::STARTER && persistentWindowSizeStoreOK)
    {
        std::vector<uint8_t> value;
        if (persistentWindowSizeStore.get(filenameAndUri.uri.c_str(), value) == litecask::Status::Ok)
        {
            if (value.size() == sizeof(POINT))
            {
                // We used to store just the size
                const POINT* p = reinterpret_cast<POINT*>(value.data());
                width = p->x;
                height = p->y;
                havePersistedSize = true;
            }
            else if (value.size() == sizeof(PersistedDocumentWindowSize))
            {
                // Currently we also store the last wParam in the WM_SIZE message
                const PersistedDocumentWindowSize* p = reinterpret_cast<PersistedDocumentWindowSize*>(value.data());
                width = p->size.x;
                height = p->size.y;
                if (p->resizeType == SIZE_MAXIMIZED)
                    maximize = true;
                havePersistedSize = true;
            }
        }
    }

    if (!havePersistedSize)
    {
        // Set size of document window to be 90% of monitor width and height. For the welcome
        // slideshow always set width:height to 16:9 because we know it is that aspect ratio.

        // The welcome slideshow is displayed without decorations.

        // FIXME: Should we actually, at least for text documents, ideally peek into the document and
        // check what its page size is, and in the common case of a portrait orientation text document,
        // make the document window also (if the monitor is large enough) higher than wider? On small
        // monitors (1280x768 or less?) we should probably default to making the document window
        // full-screen?

        // FIXME: My initial assumption that the COOL window would open up on the monitor where the
        // file section dialog was is incorrect.

        MONITORINFO monitorInfo;

        monitorInfo.cbSize = sizeof(monitorInfo);
        if (GetMonitorInfoW(primaryMonitor, &monitorInfo))
        {
            if (mode == DocumentMode::WELCOME)
            {
                double aspectRatio =
                    (double)(monitorInfo.rcWork.right - monitorInfo.rcWork.left) / (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top);
                if (aspectRatio < 16.0/9.0)
                {
                    width = 0.9 * (monitorInfo.rcWork.right - monitorInfo.rcWork.left);
                    welcomeX = monitorInfo.rcWork.left + 0.05 * (monitorInfo.rcWork.right - monitorInfo.rcWork.left);
                    height = width / (16.0/9.0);
                    welcomeY = monitorInfo.rcWork.top + ((monitorInfo.rcWork.bottom - monitorInfo.rcWork.top) - height) / 2;
                }
                else
                {
                    height = 0.9 * (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top);
                    welcomeY = monitorInfo.rcWork.top + 0.05 * (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top);
                    width = (16.0/9.0) * height;
                    welcomeX = monitorInfo.rcWork.left + ((monitorInfo.rcWork.right - monitorInfo.rcWork.left) - width) / 2;
                }
            }
            else
            {
                width = 0.9 * (monitorInfo.rcWork.right - monitorInfo.rcWork.left);
                height = 0.9 * (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top);
            }
        }
        else
        {
            if (mode == DocumentMode::WELCOME)
            {
                width = 1280;
                height = 720;
            }
            else
            {
                width = 1200;
                height = 900;
            }
        }
    }

    HWND hWnd;
    if (mode == DocumentMode::WELCOME)
        hWnd = CreateWindowW(
            windowClass, Util::string_to_wide_string(APP_NAME).c_str(),
            WS_POPUP, welcomeX, welcomeY, width, height, NULL, NULL, appInstance,
            NULL);
    else if (mode == DocumentMode::STARTER)
        hWnd = CreateWindowW(
            windowClass, Util::string_to_wide_string(APP_NAME).c_str(),
            WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, NULL, NULL, appInstance,
            NULL);
    else
        hWnd = CreateWindowW(
            windowClass, Util::string_to_wide_string(filenameAndUri.filename + " - " APP_NAME).c_str(),
            WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, NULL, NULL, appInstance,
            NULL);

    auto& data = windowData[hWnd];
    data.hWnd = hWnd;
    data.previousSize.x = width;
    data.previousSize.y = height;
    data.isFullScreen = false;
    if (mode == DocumentMode::STARTER)
    {
        data.fakeClientFd = -1;
        data.appDocId = 0;
    }
    else
    {
        data.fakeClientFd = fakeSocketSocket();
        data.appDocId = generate_new_app_doc_id();
    }
    data.filenameAndUri = filenameAndUri;
    data.mode = mode;

    if (maximize)
        ShowWindow(hWnd, SW_MAXIMIZE);
    else
        ShowWindow(hWnd, appShowMode);
    UpdateWindow(hWnd);

    AddClipboardFormatListener(hWnd);

    // Configure the "cool" custom scheme registration
    auto schemeRegistration =
        Microsoft::WRL::Make<CoreWebView2CustomSchemeRegistration>(L"cool");
    if (!SUCCEEDED(schemeRegistration->put_TreatAsSecure(TRUE)))
        fatal("schemeRegistration->put_TreatAsSecure() failed");
    if (!SUCCEEDED(schemeRegistration->put_HasAuthorityComponent(TRUE)))
        fatal("schemeRegistration->put_HasAuthorityComponent() failed");

    // We show a page from a file: URI, so we need to use "*"
    LPCWSTR allowedOrigins[1] = { L"*" };
    if (!SUCCEEDED(schemeRegistration->SetAllowedOrigins(1, allowedOrigins)))
        fatal("schemeRegistration->SetAllowedOrigins() failed");

    // Add the registration to the options (requires ICoreWebView2EnvironmentOptions4)
    ICoreWebView2CustomSchemeRegistration* registrations[1] =
        { schemeRegistration.Get() };

    // Required for instantiating new Web Workers, which otherwise fail with a
    // cross-origin SecurityError because file:// gets origin 'null'.
    std::wstring additionalArgs = L"--allow-file-access-from-files";
    if (enableWebDriver)
        additionalArgs += L" --remote-debugging-port=9222";

    auto options = Microsoft::WRL::Make<CoreWebView2EnvironmentOptions>();
    options->put_AdditionalBrowserArguments(additionalArgs.c_str());

    Microsoft::WRL::ComPtr<ICoreWebView2EnvironmentOptions4> options4;
    if (!SUCCEEDED(options.As(&options4)))
        fatal("options.As() failed");
    if (!SUCCEEDED(options4->SetCustomSchemeRegistrations(1, registrations)))
        fatal("options4->SetCustomSchemeRegistrations() failed");

    CreateCoreWebView2EnvironmentWithOptions(
        nullptr,
        (Util::string_to_wide_string(localAppData) + L"\\UDF").c_str(),
        options.Get(),
        Microsoft::WRL::Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
            [&data](HRESULT result, ICoreWebView2Environment* env) -> HRESULT
            {
                // Create a CoreWebView2Controller and get the associated CoreWebView2 whose parent is the main window hWnd
                env->CreateCoreWebView2Controller(
                    data.hWnd,
                    Microsoft::WRL::Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
                        [&data, env](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT
                        {
                            if (!controller)
                                return E_FAIL;

                            ICoreWebView2* webView;
                            controller->get_CoreWebView2(&webView);
                            data.webView = wil::com_ptr<ICoreWebView2>(webView);
                            data.webViewController = controller;

                            wil::com_ptr<ICoreWebView2_22> webView22 = data.webView.try_query<ICoreWebView2_22>();
                            if (!webView22)
                                fatal("Could not get webView22");

                            // Add a few settings for the webview
                            // The demo step is redundant since the values are the default settings
                            wil::com_ptr<ICoreWebView2Settings> settings;
                            webView->get_Settings(&settings);
                            settings->put_IsScriptEnabled(TRUE);
                            settings->put_AreDefaultScriptDialogsEnabled(TRUE);
                            settings->put_IsWebMessageEnabled(TRUE);
                            // Stop browser shortcut keys (such as F12 for the
                            // developer tools and F5 for reload) from being
                            // handled by the WebView, so the keys reach the
                            // document instead. F12 then toggles the numbered
                            // list rather than opening the developer tools.
                            wil::com_ptr<ICoreWebView2Settings4> settings4
                                = settings.try_query<ICoreWebView2Settings4>();
                            if (settings4)
                                settings4->put_AreBrowserAcceleratorKeysEnabled(FALSE);

                            // Resize WebView to fit the bounds of the parent window
                            RECT bounds;
                            GetClientRect(data.hWnd, &bounds);
                            data.webViewController->put_Bounds(bounds);

                            EventRegistrationToken token;
                            HRESULT hr;

                            hr = (webView22->AddWebResourceRequestedFilterWithRequestSourceKinds(
                                      L"cool://*",
                                      COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL,
                                      COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL));
                            if (!SUCCEEDED(hr))
                            {
                                LOG_ERR_S("AddWebResourceRequestedFilterWithRequestSourceKinds() failed");
                                return hr;
                            }

                            hr = webView->add_WebResourceRequested(
                                Microsoft::WRL::Callback<ICoreWebView2WebResourceRequestedEventHandler>(
                                    [env](ICoreWebView2* sender,
                                          ICoreWebView2WebResourceRequestedEventArgs* args) -> HRESULT
                                    {
                                        return webResourceRequestedHandler(env, sender, args);
                                    }).Get(), &token);

                            if (!SUCCEEDED(hr))
                            {
                                LOG_ERR_S("add_WebResourceRequested() failed");
                                return hr;
                            }

                            // Communication between host and web content
                            // Set an event handler for the host to return received message back to the web content
                            webView->add_WebMessageReceived(
                                Microsoft::WRL::Callback<
                                    ICoreWebView2WebMessageReceivedEventHandler>(
                                    [&data](
                                        ICoreWebView2* webView,
                                        ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT
                                    {
                                        wil::unique_cotaskmem_string message;
                                        args->TryGetWebMessageAsString(&message);
                                        processMessage(data, message);
                                        return S_OK;
                                    })
                                    .Get(),
                                &token);

                            webView->add_ContainsFullScreenElementChanged(
                                Microsoft::WRL::Callback<ICoreWebView2ContainsFullScreenElementChangedEventHandler>(
                                    [&data](ICoreWebView2* sender, IUnknown* args) -> HRESULT
                                    {
                                        BOOL containsFullscreenElement;
                                        sender->get_ContainsFullScreenElement(&containsFullscreenElement);
                                        if (containsFullscreenElement)
                                        {
                                            HMONITOR monitor = MonitorFromWindow(data.hWnd, MONITOR_DEFAULTTONEAREST);
                                            enter_full_screen(data, monitor, true);
                                        }
                                        else
                                            leave_full_screen(data);
                                        return S_OK;
                                    })
                                    .Get(),
                                nullptr);

                            // New windows appear to need to reuse the original env of the parent, a good explanation
                            // of use at: https://github.com/MicrosoftEdge/WebView2Feedback/discussions/4501#discussioncomment-9215801
                            webView->add_NewWindowRequested(
                                Microsoft::WRL::Callback<ICoreWebView2NewWindowRequestedEventHandler>(
                                    [env, &data](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args)
                                    {
                                        wil::com_ptr<ICoreWebView2Deferral> deferral;
                                        args->GetDeferral(&deferral);

                                        HMONITOR hMonitor = MonitorFromWindow(data.hWnd, MONITOR_DEFAULTTONEAREST);
                                        MONITORINFO monitorInfo = { sizeof(monitorInfo) };
                                        GetMonitorInfo(hMonitor, &monitorInfo);
                                        const RECT& area = monitorInfo.rcWork;
                                        int areaWidth = area.right - area.left;
                                        int areaHeight = area.bottom - area.top;
                                        int width = areaWidth * 17 / 20;
                                        int height = areaHeight * 17 / 20;
                                        int x = area.left + (areaWidth - width) / 2;
                                        int y = area.top + (areaHeight - height) / 2;

                                        data.hConsoleWnd = CreateWindowW(windowClass,
                                                Util::string_to_wide_string(APP_NAME).c_str(),
                                                WS_OVERLAPPEDWINDOW,
                                                x, y, width, height,
                                                NULL, NULL, appInstance, NULL);

                                        auto& consoleData = windowData[data.hConsoleWnd];
                                        consoleData.hWnd = data.hConsoleWnd;
                                        consoleData.hParentWnd = data.hWnd;
                                        consoleData.isConsole = true;
                                        consoleData.previousSize.x = width;
                                        consoleData.previousSize.y = height;

                                        ShowWindow(data.hConsoleWnd, appShowMode);

                                        env->CreateCoreWebView2Controller(
                                            data.hConsoleWnd,
                                            Microsoft::WRL::Callback<
                                                ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
                                                [&consoleData, &data, args, deferral](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT
                                                {
                                                    if (!controller)
                                                        return E_FAIL;

                                                    ICoreWebView2* webView;
                                                    controller->get_CoreWebView2(&webView);
                                                    consoleData.webView = wil::com_ptr<ICoreWebView2>(webView);

                                                    webView->add_WindowCloseRequested(
                                                        Microsoft::WRL::Callback<ICoreWebView2WindowCloseRequestedEventHandler>(
                                                            [&consoleData](ICoreWebView2* sender, IUnknown* args)
                                                            {
                                                                PostMessageW(consoleData.hWnd, WM_CLOSE, 0, 0);
                                                                return S_OK;
                                                            })
                                                            .Get(),
                                                        nullptr);

                                                    controller->put_IsVisible(TRUE);

                                                    consoleData.webViewController = controller;

                                                    // Resize WebView to fit the bounds of the parent window
                                                    RECT bounds;
                                                    GetClientRect(consoleData.hWnd, &bounds);
                                                    controller->put_Bounds(bounds);

                                                    args->put_NewWindow(consoleData.webView.get());
                                                    args->put_Handled(TRUE);
                                                    deferral->Complete();

                                                    arrangePresentationWindows(data);

                                                    return S_OK;
                                                })
                                                .Get());
                                            return S_OK;

                                        return S_OK;
                                    })
                                    .Get(),
                                nullptr);

                            std::string coolURL =
                                app_installation_uri + "../cool/cool.html?";
                            if (data.mode == DocumentMode::STARTER)
                                coolURL += "starterMode=true";
                            else
                            {
                                if (data.mode != DocumentMode::WELCOME)
                                    recentFiles.add(data.filenameAndUri.uri);
                                coolURL +=
                                    "file_path=" + data.filenameAndUri.uri +
                                    std::string("&permission=edit") +
                                    std::string("&appdocid=") + std::to_string(data.appDocId) +
                                    std::string("&userinterfacemode=notebookbar");
                            }

                            coolURL += "&lang=" + uiLanguage;
                            coolURL += "&dir=" + std::string(LangUtil::isRtlLanguage(uiLanguage) ? "rtl" : "");

                            // Saved choice wins, otherwise follow the system theme.
                            const bool darkMode = Desktop::getDarkMode().value_or(!isLightTheme());
                            coolURL += darkMode ? "&darkTheme=true" : "&darkTheme=false";

                            if (data.mode != DocumentMode::STARTER)
                                coolURL +=
                                    std::string((data.mode != DocumentMode::NEW ? "&startreadonly=true" : "")) +
                                    std::string((data.mode == DocumentMode::WELCOME ? "&welcome=true" : ""));

                            webView->Navigate(Util::string_to_wide_string(coolURL).c_str());
                            controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);

                            return S_OK;
                        })
                        .Get());
                return S_OK;
            })
            .Get());
}

namespace
{
// Result of a WinHTTP request: an HTTP status code (>= 100), or one of the
// ai:: sentinels (HttpNoResponse / HttpConnectFailed) when no response arrived.
struct HttpResult
{
    int statusCode;
    std::string body;
};

// Perform a blocking HTTP request with WinHTTP. The desktop app has no COOL net
// stack for outbound requests, so the AI proxy and the Options dialog's model
// listing reach providers through this. \c authHeader, when non-empty, becomes
// the Authorization header value (e.g. "Bearer <key>").
HttpResult winHttpRequest(const wchar_t* verb, const std::string& url,
                          const std::string& authHeader, const std::string& body,
                          int timeoutSeconds)
{
    HttpResult result{ ai::HttpConnectFailed, std::string() };

    const std::wstring wideUrl = Util::string_to_wide_string(url);
    URL_COMPONENTS components;
    ZeroMemory(&components, sizeof(components));
    components.dwStructSize = sizeof(components);
    wchar_t hostName[256] = {};
    wchar_t urlPath[8192] = {};
    components.lpszHostName = hostName;
    components.dwHostNameLength = ARRAYSIZE(hostName);
    components.lpszUrlPath = urlPath;
    components.dwUrlPathLength = ARRAYSIZE(urlPath);
    if (!WinHttpCrackUrl(wideUrl.c_str(), 0, 0, &components))
        return result;

    HINTERNET session = WinHttpOpen(L"CODA", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
                                    WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
    if (session == nullptr)
        return result;

    if (timeoutSeconds > 0)
    {
        const int ms = timeoutSeconds * 1000;
        WinHttpSetTimeouts(session, ms, ms, ms, ms);
    }

    HINTERNET connection = WinHttpConnect(session, hostName, components.nPort, 0);
    if (connection == nullptr)
    {
        WinHttpCloseHandle(session);
        return result;
    }

    const DWORD flags = (components.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0;
    HINTERNET request = WinHttpOpenRequest(connection, verb, urlPath, nullptr, WINHTTP_NO_REFERER,
                                           WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
    if (request == nullptr)
    {
        WinHttpCloseHandle(connection);
        WinHttpCloseHandle(session);
        return result;
    }

    std::wstring headers = L"Content-Type: application/json\r\n";
    if (!authHeader.empty())
        headers += L"Authorization: " + Util::string_to_wide_string(authHeader) + L"\r\n";
    WinHttpAddRequestHeaders(request, headers.c_str(), static_cast<DWORD>(-1),
                             WINHTTP_ADDREQ_FLAG_ADD);

    BOOL ok = WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
                                 body.empty() ? WINHTTP_NO_REQUEST_DATA
                                              : const_cast<char*>(body.data()),
                                 static_cast<DWORD>(body.size()),
                                 static_cast<DWORD>(body.size()), 0);
    if (ok)
        ok = WinHttpReceiveResponse(request, nullptr);

    if (!ok)
    {
        result.statusCode =
            (GetLastError() == ERROR_WINHTTP_TIMEOUT) ? ai::HttpNoResponse : ai::HttpConnectFailed;
        WinHttpCloseHandle(request);
        WinHttpCloseHandle(connection);
        WinHttpCloseHandle(session);
        return result;
    }

    DWORD statusCode = 0;
    DWORD statusLen = sizeof(statusCode);
    WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
                        WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusLen,
                        WINHTTP_NO_HEADER_INDEX);
    result.statusCode = static_cast<int>(statusCode);

    DWORD available = 0;
    do
    {
        available = 0;
        if (!WinHttpQueryDataAvailable(request, &available) || available == 0)
            break;
        std::string chunk(available, '\0');
        DWORD read = 0;
        if (!WinHttpReadData(request, &chunk[0], available, &read))
            break;
        chunk.resize(read);
        result.body += chunk;
    } while (available > 0);

    WinHttpCloseHandle(request);
    WinHttpCloseHandle(connection);
    WinHttpCloseHandle(session);
    return result;
}

// Register the AI chat HTTP transport. AIChatSession reaches the provider
// through this platform hook (the Windows counterpart of Qt's
// registerAIHttpTransport()). onDone may run on any thread; AIChatSession hops
// back onto its polling thread itself.
void registerAIHttpTransport()
{
    ai::setHttpPostFn(
        [](const std::string& url, const std::string& authHeader, std::string body,
           int timeoutSeconds, ai::HttpDoneCallback onDone)
        {
            // Called on the document's polling thread; run the blocking WinHTTP
            // request on its own thread so we don't stall that thread.
            std::thread(
                [url, authHeader, body = std::move(body), timeoutSeconds,
                 onDone = std::move(onDone)]() mutable
                {
                    ProcUtil::setThreadName("aihttp");
                    const HttpResult result =
                        winHttpRequest(L"POST", url, authHeader, body, timeoutSeconds);
                    onDone(result.statusCode, std::move(result.body));
                })
                .detach();
        });
}

// Fetch an AI provider's model list for the Options dialog. The desktop apps
// have no server-side proxy, so the app issues the request itself, mirroring
// Qt's Desktop::fetchAIModels(). The payload is {"provider","apiKey","baseUrl"};
// returns the provider's JSON body verbatim ({"data":[...]} or its own error
// JSON), or an {"error":...} JSON of our own.
std::string fetchAIModels(const std::string& payload)
{
    Poco::JSON::Object::Ptr obj;
    if (!JsonUtil::parseJSON(payload, obj))
        return R"({"error":"Invalid payload"})";

    std::string provider, apiKey, baseUrl;
    JsonUtil::findJSONValue(obj, "provider", provider);
    JsonUtil::findJSONValue(obj, "apiKey", apiKey);
    JsonUtil::findJSONValue(obj, "baseUrl", baseUrl);

    if (provider.empty() || apiKey.empty())
        return R"({"error":"Missing provider or apiKey"})";

    if (provider != "custom")
    {
        // Keep in sync with preCannedAIProviderBaseUrl() in wsd/FileServer.cpp
        // and the same map in qt/Application.cpp / macos COWrapper.mm.
        static const std::map<std::string, std::string> preCanned = {
            { "openai", "https://api.openai.com" },
            { "groq", "https://api.groq.com/openai" },
            { "together", "https://api.together.xyz" },
            { "mistral", "https://api.mistral.ai" },
        };
        const auto it = preCanned.find(provider);
        if (it == preCanned.end())
            return R"({"error":"Unknown provider"})";
        baseUrl = it->second;
    }
    else if (baseUrl.empty())
    {
        return R"({"error":"Missing baseUrl for custom provider"})";
    }

    if (!baseUrl.empty() && baseUrl.back() == '/')
        baseUrl.pop_back();
    baseUrl += "/v1/models";

    const HttpResult result = winHttpRequest(L"GET", baseUrl, "Bearer " + apiKey, std::string(), 30);
    if (result.statusCode < 100 && result.body.empty())
        return R"({"error":"Failed to reach the AI provider"})";
    return result.body;
}
} // namespace

static void free_getClipboard_results(size_t count, char** mimeTypes, size_t* sizes, char** streams)
{
    for (size_t i = 0; i < count; i++)
    {
        std::free(mimeTypes[i]);
        std::free(streams[i]);
    }
    std::free(mimeTypes);
    std::free(sizes);
    std::free(streams);
}

static HANDLE copyEngineClipboardData(UINT format, const std::string& mimeType)
{
    // The clipboard is process-global (one shared clipboard for the desktop app), so read it
    // straight from the engine; no document is involved.
    if (!office)
        return 0;

    const char *filter[] = { mimeType.c_str(), nullptr };
    size_t outCount = 0;
    char **outMimeTypes = nullptr;
    size_t *outSizes = nullptr;
    char **outStreams = nullptr;
    if (!office->getGlobalClipboard(filter, &outCount, &outMimeTypes, &outSizes, &outStreams) ||
        outCount == 0)
        return 0;

    if (outStreams[0] == nullptr || outSizes[0] == 0)
    {
        free_getClipboard_results(outCount, outMimeTypes, outSizes, outStreams);
        return 0;
    }

    HGLOBAL hMem;
    const void *src;
    size_t size;
    std::wstring wtemp;
    std::string temp;
    if (format == CF_UNICODETEXT && mimeType == "text/plain;charset=utf-8")
    {
        wtemp = Util::string_to_wide_string(std::string_view(outStreams[0], outSizes[0]));
        src = wtemp.c_str();
        size = (wtemp.length() + 1) * 2;
    }
    else if (format == RegisterClipboardFormatW(L"HTML Format") && mimeType == "text/html")
    {
        temp = generate_html_format(std::string(outStreams[0], outSizes[0]));
        src = temp.c_str();
        size = temp.length();
    }
    else
    {
        src = outStreams[0];
        size = outSizes[0];
    }
    hMem = GlobalAlloc(GMEM_MOVEABLE, size);
    if (hMem == NULL)
    {
        free_getClipboard_results(outCount, outMimeTypes, outSizes, outStreams);
        return 0;
    }
    void* dest = GlobalLock(hMem);
    if (dest == nullptr)
    {
        GlobalFree(hMem);
        free_getClipboard_results(outCount, outMimeTypes, outSizes, outStreams);
        return 0;
    }
    std::memcpy(dest, src, size);
    GlobalUnlock(hMem);

    free_getClipboard_results(outCount, outMimeTypes, outSizes, outStreams);

    return hMem;
}

static std::string MIME_type_for_clipboard_format(UINT format)
{
    if (format == CF_UNICODETEXT)
        return "text/plain;charset=utf-8";

    auto name = get_clipboard_format_name(format);

    if (name == L"text/markdown" ||
        name == L"text/rtf" ||
        name == L"image/png" ||
        name == L"image/svg+xml")
        // Clipboard format names that are directly MIME types
        return Util::wide_string_to_string(name);
    else if (name == L"Markdown")
        return "text/markdown";
    else if (name == L"Star Embed Source (XML)")
        return "application/x-openoffice-embed-source-xml;windows_formatname=\"Star Embed Source (XML)\"";
    else if (name == L"Star Object Descriptor (XML)")
        return "application/x-openoffice-objectdescriptor-xml;windows_formatname=\"Star Object Descriptor (XML)\"";
    else if (name == L"PNG")
        return "image/png";
    else if (name == L"Rich Text Format")
        return "text/rtf";
    else if (name == L"HTML Format")
        return "text/html";

    return "";
}

static std::vector<int> clipboard_formats_for_MIME_type(const char* mimeType)
{
    std::vector<int> result;

    if (std::strcmp(mimeType, "text/plain;charset=utf-8") == 0)
        result.push_back(CF_UNICODETEXT);
    else if (std::strcmp(mimeType, "text/rtf") == 0)
        result.push_back(RegisterClipboardFormatW(L"Rich Text Format"));
    else if (std::strcmp(mimeType, "text/html") == 0)
        result.push_back(RegisterClipboardFormatW(L"HTML Format"));
    else if (std::strcmp(mimeType, "text/markdown") == 0)
    {
        result.push_back(RegisterClipboardFormatW(L"text/markdown"));
        result.push_back(RegisterClipboardFormatW(L"Markdown"));
    }
    else if (std::strcmp(mimeType, "image/png") == 0)
    {
        result.push_back(RegisterClipboardFormatW(L"image/png"));
        result.push_back(RegisterClipboardFormatW(L"PNG"));
    }
    else if (std::strcmp(mimeType, "application/x-openoffice-embed-source-xml;windows_formatname=\"Star Embed Source (XML)\"") == 0)
        result.push_back(RegisterClipboardFormatW(L"Star Embed Source (XML)"));
    else if (std::string(mimeType).starts_with("application/x-openoffice-objectdescriptor-xml;"))
        result.push_back(RegisterClipboardFormatW(L"Star Object Descriptor (XML)"));
    else if (std::strcmp(mimeType, "image/svg+xml;windows_formatname=\"image/svg+xml\"") == 0)
        result.push_back(RegisterClipboardFormatW(L"image/svg+xml"));

    return result;
}

/**
 * The clipboard provider the engine drives. On copy the engine advertises its formats through
 * advertise; on an external paste it reads the clipboard one format at a time. The callbacks
 * act on the process, not one window, so the one shared clipboard is reached from whichever
 * document is current.
 */

static void clipboardProviderAdvertise(const char** pMimeTypes)
{
    // Delayed rendering needs a live window to own the clipboard and receive WM_RENDERFORMAT. Use
    // the hidden owner window, which lives for the whole app run. The clipboard is only rendered in
    // full (WM_RENDERALLFORMATS) when the app exits, not when an individual document window closes.
    if (!hiddenOwnerWindow)
        return;

    if (!try_open_clipboard(hiddenOwnerWindow))
        return;

    if (!EmptyClipboard())
    {
        CloseClipboard();
        return;
    }

    bool didSetData = false;

    for (size_t i = 0; pMimeTypes && pMimeTypes[i]; ++i)
    {
        auto formats = clipboard_formats_for_MIME_type(pMimeTypes[i]);
        if (formats.size() != 0)
        {
            for (auto const &format : formats)
                SetClipboardData(format, NULL);
            didSetData = true;
        }
    }

    if (didSetData)
        weOwnTheClipboard = true;

    CloseClipboard();
}

static int clipboardProviderOwns()
{
    return weOwnTheClipboard;
}

static char** clipboardProviderGetMimeTypes()
{
    // Reading needs no owner window, so open the clipboard for the current "task".
    //
    // (Task is an 16-bit Windows term still used in documentation for the clipboard API that is
    // basically unchanged since then. In current Windows, it means thread, more or less.)
    if (!try_open_clipboard(NULL))
        return NULL;

    UINT format = 0;

    std::vector<char*> mimeTypes;
    std::set<std::string> doneMimeTypes;

    while ((format = EnumClipboardFormats(format)) != 0)
    {
        if (format == CF_UNICODETEXT)
        {
            mimeTypes.push_back(_strdup("text/plain;charset=utf-8"));
            doneMimeTypes.insert("text/plain;charset=utf-8");
        }
        else
        {
            auto mimeType = MIME_type_for_clipboard_format(format);

            if (mimeType != "" && doneMimeTypes.count(mimeType) == 0)
            {
                doneMimeTypes.insert(mimeType);
                mimeTypes.push_back(_strdup(mimeType.c_str()));
            }
        }
    }

    CloseClipboard();

    char** result = (char**)std::malloc(sizeof(char*) * (mimeTypes.size() + 1));

    for (int i = 0; i < mimeTypes.size(); i++)
    {
        result[i] = mimeTypes[i];
        mimeTypes[i] = nullptr;
    }
    result[mimeTypes.size()] = nullptr;

    return result;
}

static int clipboardProviderGetData(const char* pMimeType, char** pOutData, size_t* pOutSize)
{
    auto formats = clipboard_formats_for_MIME_type(pMimeType);

    if (formats.size() == 0)
        return 0;

    if (!try_open_clipboard(NULL))
        return 0;

    for (const auto& format : formats)
    {
        HANDLE handle;

        if (format == CF_UNICODETEXT)
        {
            handle = GetClipboardData(format);
            if (!handle)
                continue;
            wchar_t* wtext = (wchar_t*)GlobalLock(handle);
            if (!wtext)
                continue;

            std::string text = Util::wide_string_to_string(std::wstring(wtext));
            GlobalUnlock(handle);
            *pOutData = (char*)std::malloc(text.length());
            *pOutSize = text.length();
            std::memcpy(*pOutData, text.c_str(), text.length());

            CloseClipboard();

            return 1;
        }

        handle = GetClipboardData(format);
        if (!handle)
            continue;
        size_t size = GlobalSize(handle);
        const char* source = (const char*)GlobalLock(handle);
        if (!source)
            continue;
        std::string fragment;
        if (format == RegisterClipboardFormatW(L"HTML Format"))
        {
            fragment = get_html_clipboard_fragment(source).c_str();
            source = fragment.c_str();
            size = std::strlen(source);
        }
        *pOutData = (char*)std::malloc(size);
        *pOutSize = size;
        std::memcpy(*pOutData, source, size);
        GlobalUnlock(handle);
        CloseClipboard();

        return 1;
    }

    CloseClipboard();
    return 0;
}


// Install the process-global clipboard provider (declared in windows.hpp). After this the engine
// advertises formats on copy and reads the clipboard on paste through the callbacks above, using
// one shared clipboard for every document.
void install_clipboard_provider(kit::Office& kitOffice)
{
    office = &kitOffice;

    static COKitClipboardProvider provider{};
    provider.advertiseToPlatform = clipboardProviderAdvertise;
    provider.ownsClipboard = clipboardProviderOwns;
    provider.getMimeTypes = clipboardProviderGetMimeTypes;
    provider.getDataForMimeType = clipboardProviderGetData;
    kitOffice.installClipboardProvider(&provider);
}

void materialize_clipboard_formats()
{
    static bool beenHere = false;

    if (beenHere)
        return;

    beenHere = true;

    if (GetClipboardOwner() == hiddenOwnerWindow)
    {
        if (try_open_clipboard(hiddenOwnerWindow))
        {
            // Collect the promised formats first, then render each. Do not probe with
            // GetClipboardData here(): on a still-promised format it would send
            // WM_RENDERFORMAT again. Re-setting an already-rendered format is harmless.
            std::vector<UINT> formats;
            UINT format = 0;
            while ((format = EnumClipboardFormats(format)) != 0)
                formats.push_back(format);
            for (UINT f : formats)
            {
                std::string mimeType = MIME_type_for_clipboard_format(f);
                if (mimeType.empty())
                    continue;
                HANDLE hData = copyEngineClipboardData(f, mimeType);
                if (hData)
                    SetClipboardData(f, hData);
            }
            CloseClipboard();
        }
    }
}

static void processMessage(WindowData& data, wil::unique_cotaskmem_string& message)
{
    std::wstring s(message.get());
    LOG_TRC(Util::wide_string_to_string(s));
    if (s.starts_with(L"MSG "))
    {
        s = s.substr(4);
        if (s == L"HULLO")
        {
            // If displaying the starter screen, do nothing
            if (data.mode == DocumentMode::STARTER)
                return;

            do_hullo_handling_things(data);
        }
        else if (s == L"WELCOME")
        {
            do_welcome_handling_things(data);
        }
        else if (s == L"BYE")
        {
            do_bye_handling_things(data);
        }
        else if (s == L"PRINT")
        {
            do_print(data.appDocId);
        }
        else if (s.starts_with(L"TEXTCLIPBOARD "))
        {
            // A plain-text copy from the web UI (for example the About dialog), not document
            // content, so it does not go through the engine's clipboard. Own it with the same
            // hidden window the provider uses, so no document window ever owns the clipboard.
            std::wstring text = s.substr(14);
            if (try_open_clipboard(hiddenOwnerWindow))
            {
                EmptyClipboard();
                HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, (text.size() + 1) * sizeof(wchar_t));
                if (hMem)
                {
                    memcpy(GlobalLock(hMem), text.c_str(), (text.size() + 1) * sizeof(wchar_t));
                    GlobalUnlock(hMem);
                    SetClipboardData(CF_UNICODETEXT, hMem);
                }
                CloseClipboard();
            }
        }
        else if (s.starts_with(L"HYPERLINK "))
        {
            do_open_hyperlink(data.hWnd, s.substr(10));
        }
        else if (s == L"LICENSE")
        {
            std::wstring licensePath = Util::string_to_wide_string(app_installation_path + "..\\LICENSE.html");
            ShellExecuteW(nullptr, L"open", licensePath.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
        }
        else if (s == L"EXCHANGEMONITORS")
        {
            exchangeMonitors(data);
        }
        else if (s.starts_with(L"FULLSCREENPRESENTATION "))
        {
            data.isPresFullScreen = s.substr(23) == L"true";
            if (data.isPresFullScreen)
                arrangePresentationWindows(data);
            else
                leave_full_screen(data);
        }
        else if (s == L"SYNCSETTINGS")
        {
            Desktop::syncSettings([&data](const std::vector<char>& buf) {
                send2JS(data.hWnd, buf.data(), buf.size());
            });
        }
        else if (s.starts_with(L"UPLOADSETTINGS "))
        {
            Desktop::uploadSettings(Util::wide_string_to_string(s.substr(strlen("UPLOADSETTINGS "))));
        }
        else if (s.starts_with(L"SETDARKMODE "))
        {
            Desktop::setDarkMode(s.substr(strlen("SETDARKMODE ")) == L"true");
        }
        else if (s.starts_with(L"downloadas "))
        {
            // "downloadas name=document.rtf id=export format=rtf options="
            auto const ns = Util::wide_string_to_string(s);
            auto const tokens = StringVector::tokenize(ns);
            std::string name;
            if (!COOLProtocol::getTokenString(tokens, "name", name))
            {
                LOG_ERR("No name parameter in message '" << ns << "'");
                return;
            }
            auto dot = name.find_last_of('.');
            if (dot == std::string::npos || dot == name.length() - 1)
            {
                LOG_ERR("No file name extension in '" << ns << "'");
                return;
            }
            auto const extension = name.substr(dot + 1);
            auto const basename = data.filenameAndUri.filename.substr(
                0, data.filenameAndUri.filename.find_last_of('.'));
            auto filenameAndUri = fileSaveDialog(basename + "." + extension,
                                                 "",
                                                 {
                                                     {
                                                         Util::string_to_wide_string(extension).c_str(),
                                                         Util::string_to_wide_string("*." + extension).c_str()
                                                     }
                                                 });

            if (filenameAndUri.filename != "")
                DocumentData::get(data.appDocId).loKitDocument->saveAs(filenameAndUri.uri.c_str(), extension.c_str(), nullptr);
        }
        else if (s.starts_with(L"exportfile "))
        {
            // "exportfile url=file:///C:/Users/.../tmp/image.png"
            auto const ns = Util::wide_string_to_string(s);
            auto const tokens = StringVector::tokenize(ns);
            std::string fileUrl;
            if (!COOLProtocol::getTokenString(tokens, "url", fileUrl))
            {
                LOG_ERR("No url parameter in message '" << ns << "'");
                return;
            }

            auto srcPath = Poco::URI(fileUrl).getPath();
            // The usual hack to get rid of the leading slash in what Poco::URI::getPath() returns,
            // like "/C:/Users/bob/AppData/Local/Temp/image.jpg".
            if (srcPath.length() > 4 && srcPath[0] == '/' && srcPath[2] == ':' && srcPath[3] == '/')
                srcPath = srcPath.substr(1);

            if (!std::filesystem::exists(srcPath))
            {
                LOG_ERR("exportfile: source file not found: " << srcPath);
                return;
            }

            auto const extension = Poco::Path(srcPath).getExtension();
            auto filenameAndUri = fileSaveDialog("image." + extension,
                                                 "",
                                                 {
                                                     {
                                                         Util::string_to_wide_string(extension).c_str(),
                                                         Util::string_to_wide_string("*." + extension).c_str()
                                                     }
                                                 });

            if (filenameAndUri.filename != "")
            {
                auto destPath = Poco::URI(filenameAndUri.uri).getPath();
                // As above
                if (destPath.length() > 4 && destPath[0] == '/' && destPath[2] == ':' && destPath[3] == '/')
                    destPath = destPath.substr(1);
                std::error_code ec;
                std::filesystem::copy_file(srcPath, destPath,
                                           std::filesystem::copy_options::overwrite_existing, ec);
                if (ec)
                    LOG_ERR("exportfile: failed to copy to '" << destPath << "': " << ec.message());
                else
                    LOG_INF("exportfile: saved image to " << destPath);
            }
        }
        else if (s.starts_with(L"loaddocument "))
        {
            // "loaddocument url=file:///path/to/file.ext"
            auto const ns = Util::wide_string_to_string(s);
            auto const tokens = StringVector::tokenize(ns);
            std::string url;
            if (!COOLProtocol::getTokenString(tokens, "url", url))
            {
                LOG_ERR("No url parameter in message '" << ns << "'");
                return;
            }
            // Close the existing fakesocket
            if (data.fakeClientFd != -1)
            {
                fakeSocketClose(data.fakeClientFd);
                data.fakeClientFd = -1;
            }

            // Close the existing forwarding thread
            if (data.app2js.joinable()) {
                fakeSocketClose(data.closeNotificationPipeForForwardingThread[0]);
                data.app2js.join();
            }

            data.fakeClientFd = fakeSocketSocket();
            DocumentData::deallocate(data.appDocId);
            data.appDocId = generate_new_app_doc_id();
            auto path = Poco::URI(url).getPath();
            auto lastSlash = path.find_last_of('/');
            auto filename = path.substr(lastSlash + 1);
            data.filenameAndUri = { filename, Poco::URI(url).toString() } ;

            // Connect to COOLWSD
            int rc = fakeSocketConnect(data.fakeClientFd, coolwsd_server_socket_fd);
            if (rc == -1)
            {
                LOG_ERR("loaddocument: failed to connect fakesocket");
                return;
            }

            createAndStartMessagePumpThread(data);

            // Send the initial message with the new file URL and appDocId
            std::string message(data.filenameAndUri.uri + " " + std::to_string(data.appDocId));
            fakeSocketWriteQueue(data.fakeClientFd, message.c_str(), message.size());

            // Update window title with new filename
            SetWindowTextW(data.hWnd, Util::string_to_wide_string(data.filenameAndUri.filename + " - " APP_NAME).c_str());
        }
        else if (s == L"uno .uno:Open")
        {
            auto openResult = fileOpenDialog();
            if (openResult.size() > 0)
            {
                for (const auto& i: openResult)
                    filenamesAndUrisToOpen.push_back(i);

                load_next_document();

                // The picked document opens in its own window; return the
                // originating window to its document view.
                if (data.mode != DocumentMode::STARTER)
                    PostMessageW(data.hWnd, CODA_WM_EXECUTESCRIPT,
                                 (WPARAM)_strdup("window.app?.map?.backstageView?.returnToDocumentView()"),
                                 0);
            }
            // Close the starter window
            if (data.mode == DocumentMode::STARTER)
                PostMessage(data.hWnd, WM_CLOSE, 0, 0);
        }
        else if (s == L"uno .uno:SaveAs")
        {
            auto loKitDoc = DocumentData::get(data.appDocId).loKitDocument;
            const COKitDocumentType docType = loKitDoc->getDocumentType();
            const auto formats = getSaveAsFormats(docType);
            auto filenameAndUri = fileSaveDialog("", "", formats);

            if (filenameAndUri.filename != "")
            {
                auto dot = filenameAndUri.filename.find_last_of('.');
                if (dot == std::string::npos || dot == filenameAndUri.filename.length() - 1)
                {
                    LOG_ERR("No file name extension in '" << filenameAndUri.filename << "'");
                    return;
                }
                // Send saveas command to COOLWSD with the selected format
                std::string saveasCmd = "saveas url=" + filenameAndUri.uri +
                    " format=" + filenameAndUri.filename.substr(dot + 1) +
                    " options=";
                fakeSocketWriteQueue(data.fakeClientFd, saveasCmd.c_str(), saveasCmd.size());
            }
        }
        else if (s == L"uno .uno:CloseWin")
        {
            PostMessageW(data.hWnd, WM_CLOSE, 0, 0);
        }
        else if (s.starts_with(L"newdoc "))
        {
            auto const ns = Util::wide_string_to_string(s);
            auto const tokens = StringVector::tokenize(ns);
            std::string typeToken, templateToken, basenameToken;
            if (!COOLProtocol::getTokenString(tokens, "type", typeToken))
            {
                LOG_ERR("No type parameter in message '" << ns << "'");
                return;
            }
            if (!COOLProtocol::getTokenString(tokens, "basename", basenameToken))
            {
                LOG_ERR("No basename parameter in message '" << ns << "'");
                return;
            }
            DocumentType type;
            if (typeToken == "writer")
                type = DocumentType::TEXT;
            else if (typeToken == "calc")
                type = DocumentType::SPREADSHEET;
            else if (typeToken == "impress")
                type = DocumentType::PRESENTATION;
            else
                fatal("Unexpected type in newdoc message");

            // This might leave templateToken empty if it is an old-style newdoc message with just
            // the type parameter.
            COOLProtocol::getTokenString(tokens, "template", templateToken);
            auto newDocument = new_document(type, templateToken, basenameToken);
            if (newDocument != L"")
            {
                Poco::Path path = Poco::Path(Util::wide_string_to_string(newDocument));
                openCOOLWindow({ path.getFileName(), Poco::URI(path).toString() }, DocumentMode::NEW);
            }
            if (data.mode == DocumentMode::STARTER)
                PostMessage(data.hWnd, WM_CLOSE, 0, 0);
        }
        else if (s.starts_with(L"opendoc "))
        {
            auto const ns = Util::wide_string_to_string(s);
            auto const tokens = StringVector::tokenize(ns);

            // Despite the name, the "file" parameter in the opendoc message is a file: URI, not a
            // pathname. Which is good as it reduces the character set confusion possibilities.
            std::string fileToken;
            if (!COOLProtocol::getTokenString(tokens, "file", fileToken))
            {
                LOG_ERR("No file parameter in message '" << ns << "'");
                return;
            }

            // For some reason, the URI has been pointlessly percent-re-encoded.
            fileToken = Uri::decode(fileToken);

            std::vector<std::string> segments;
            Poco::URI(fileToken).getPathSegments(segments);

            if (segments.empty())
            {
                LOG_ERR("Weird file parameter in message '" << ns << "'");
                return;
            }

            filenamesAndUrisToOpen.push_back({ segments.back(), fileToken });
            load_next_document();
            // Close the starter window
            if (data.mode == DocumentMode::STARTER)
                PostMessage(data.hWnd, WM_CLOSE, 0, 0);
        }
        else
        {
            do_other_message_handling_things(data, Util::wide_string_to_string(s).c_str());
        }
    }
    else if (s.starts_with(L"CALL "))
    {
        s = s.substr(5);
        std::wstringstream ss(s);
        int id;
        ss >> id;
        s = s.substr(s.find_first_of(L' ') + 1);

        if (s == L"GETRECENTDOCS")
        {
            do_getrecentdocs(data, id);
        }
        else if (s.starts_with(L"FETCHSETTINGSFILE "))
        {
            auto result = Desktop::fetchSettingsFile(Util::wide_string_to_string(s.substr(strlen("FETCHSETTINGSFILE "))));
            if (!result.content.empty())
            {
                // The JS caller reads result.content, so the reply is an object.
                Poco::JSON::Object::Ptr reply = new Poco::JSON::Object();
                reply->set("fileName", result.fileName);
                reply->set("mimeType", result.mimeType);
                reply->set("content", result.content);
                postReplyToCall(data.hWnd, id, reply);
            }
        }
        else if (s == L"FETCHSETTINGSCONFIG")
        {
            postReplyToCall(data.hWnd, id, Desktop::fetchSettingsConfig());
        }
        else if (s.starts_with(L"FETCHAIMODELS "))
        {
            // The Options dialog asks for an AI provider's model list. This
            // performs a network request, so run it off the message thread and
            // reply asynchronously, mirroring the Qt and macOS apps.
            const std::string payload =
                Util::wide_string_to_string(s.substr(strlen("FETCHAIMODELS ")));
            const HWND hWnd = data.hWnd;
            std::thread(
                [payload, hWnd, id]()
                {
                    ProcUtil::setThreadName("aimodels");
                    const std::string result = fetchAIModels(payload);
                    postReplyToCall(hWnd, id, result);
                })
                .detach();
        }
        else
            LOG_ERR("Unhandled CALL message: " + Util::wide_string_to_string(s));
    }
    else if (s.starts_with(L"ERR "))
    {
        LOG_ERR("From JS: " + Util::wide_string_to_string(s));
    }
    else if (s.starts_with(L"DBG "))
    {
        LOG_DBG("From JS: " + Util::wide_string_to_string(s));
    }
}

extern "C" BOOLEAN WINAPI GetUserNameExW(
    ULONG NameFormat,   // EXTENDED_NAME_FORMAT underlying type
    LPWSTR lpNameBuffer,
    PULONG nSize
);

static const char* getUserName()
{
    static wchar_t buffer[256];
    static std::string userNameStorage;
    DWORD size = sizeof(buffer) / sizeof(wchar_t);

    // Try full display name
    if (GetUserNameExW(3 /* NameDisplay */, buffer, &size)) {
        if (buffer[0] != L'\0') {
            userNameStorage = Util::wide_string_to_string(std::wstring(buffer));
            return userNameStorage.c_str();
        }
    }

    // Reset size before next call
    size = sizeof(buffer) / sizeof(wchar_t);

    // Fallback: login name
    if (GetUserNameW(buffer, &size)) {
        if (buffer[0] != L'\0') {
            userNameStorage = Util::wide_string_to_string(std::wstring(buffer));
            return userNameStorage.c_str();
        }
    }

    return nullptr;
}

// These functions in the Desktop namespace are called from SettingsStorage.cpp. Unclear whether
// will be needed in the end. The name comes from CODA-Q. Should ideally be changed to FileUtil,
// perhaps.

// Expected to return the folder where installed data for the app is stored. Should not end with a
// slash (or backslash). Note the impedance mismatch with our app_installation_path.
std::string Desktop::getDataDir()
{
    std::string result = app_installation_path;
    if (!(result.ends_with("/") || result.ends_with("\\")))
        result += "/";
    result += "..";
    return result;
}

Poco::Path Desktop::getConfigPath()
{
    return Poco::Path(localAppData);
}

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int showWindowMode)
{
    appInstance = hInstance;
    appShowMode = showWindowMode;

    user_name = getUserName();

    wchar_t fileName[1000];
    GetModuleFileNameW(NULL, fileName, sizeof(fileName) / sizeof(fileName[0]));
    app_installation_path = app_exe_path = Util::wide_string_to_string(std::wstring(fileName));
    app_installation_path.resize(app_installation_path.find_last_of(L'\\') + 1);
    app_installation_uri = Poco::URI(Poco::Path(app_installation_path)).toString();

    SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

    appName = Util::string_to_wide_string(APP_NAME);

    if (!SUCCEEDED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
        fatal("CoInitializeEx() failed");

    primaryMonitor = MonitorFromPoint({ 0, 0 }, MONITOR_DEFAULTTOPRIMARY);

    UINT32 length = 0;
    LONG rc = GetCurrentApplicationUserModelId(&length, NULL);
    if (rc == ERROR_INSUFFICIENT_BUFFER)
    {
        appUserModelId.resize(length);
        GetCurrentApplicationUserModelId(&length, appUserModelId.data());
    }
    else
    {
        appUserModelId = Util::string_to_wide_string(APP_VENDOR) + L"." + appName;
        rc = SetCurrentProcessExplicitAppUserModelID(appUserModelId.c_str());
    }

    PWSTR appDataFolder;
    SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &appDataFolder);
    localAppData = Util::wide_string_to_string(std::wstring(appDataFolder) + L"\\" + appName);
    CoTaskMemFree(appDataFolder);

    // Wire up the ODF IFilter into Windows Search and the FullDetails /
    // PreviewDetails strings the Properties dialog needs, both under
    // HKCU\Software\Classes. See registerOdfShellExtensions() for the
    // silo-escape mechanism. Idempotent on every launch.
    registerOdfShellExtensions();

    // A "LANG" environment variable is not a thing on Windows, but check
    // for a such anyway, for easier testing.
    auto langEnv = std::getenv("LANG");

    wchar_t bcp47[LOCALE_NAME_MAX_LENGTH];

    if (langEnv)
        uiLanguage = langEnv;
    else if (LCIDToLocaleName(MAKELCID(GetUserDefaultUILanguage(), SORT_DEFAULT),
                              bcp47, LOCALE_NAME_MAX_LENGTH, 0))
        uiLanguage = Util::wide_string_to_string(bcp47);

    // Allow overriding log level in the debugger. Note that logging *always* goes just to
    // OutputDebugString() if running under a debugger. Never to stderr or stdout.
    const char* loglevel = std::getenv("CODA_LOGLEVEL");
    // COOLWSD_LOGLEVEL comes from the project file and differs for Debug and Release builds.
    if (!loglevel)
        loglevel = COOLWSD_LOGLEVEL;
    Log::initialize("CODA", loglevel);
    ProcUtil::setThreadName("main");

    persistentWindowSizeStoreOK =
        (persistentWindowSizeStore.open
         (Util::string_to_wide_string(localAppData +
                                      "\\persistentWindowSizes")) == litecask::Status::Ok);

    recentFiles.load(localAppData + "\\recentFiles.txt", 10);

    // Create a dummy hidden owner window so that the file open dialog can inherit its icon for the
    // task switcher (Alt-Tab) from it.

    {
        WNDCLASSEXW wcex;

        wcex.cbSize = sizeof(WNDCLASSEXW);
        wcex.style = 0;
        wcex.lpfnWndProc = HiddenOwnerWndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = 0;
        wcex.hInstance = hInstance;
        wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_CODA));
        wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszMenuName = NULL;
        wcex.lpszClassName = hiddenOwnerWindowClass;
        wcex.hIconSm = NULL;

        if (!RegisterClassExW(&wcex))
        {
            MessageBoxW(NULL, L"Call to RegisterClassExW failed", Util::string_to_wide_string(APP_NAME).c_str(), NULL);
            return 1;
        }

        hiddenOwnerWindow = CreateWindowW(hiddenOwnerWindowClass, L"CODAHiddenOwnerWindow",
                                          WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
                                          100, 100, NULL, NULL,
                                          hInstance, NULL);
        ShowWindow(hiddenOwnerWindow, SW_HIDE);
    }

    if (std::getenv("CODA_ENABLE_WEBDRIVER"))
        enableWebDriver = true;

    DocumentMode mode = DocumentMode::EDIT;
    if (__argc == 1 || wcscmp(__wargv[1], L"--disable-background-networking") == 0)
    {
        // No documents given on the command line, show the "Starter" "Backstage" dialog
        mode = DocumentMode::STARTER;
    }
    else
    {
        for (int i = 1; i < __argc; i++)
        {
            auto path = Poco::Path(Util::wide_string_to_string(__wargv[i]));
            filenamesAndUrisToOpen.push_back({ path.getFileName(), pathToURI(path) });
        }
    }

    fakeSocketSetLoggingCallback([](const std::string& line) { LOG_TRC_NOFILE(line); });

    // Give AIChatSession a native HTTP transport (no server-side AI proxy here).
    registerAIHttpTransport();

    coolwsdThread = std::thread(
        []
        {
            assert(coolwsd == nullptr);
            char* argv[2];
            // Yes, strdup() is apparently not standard, so MS wants you to call it as
            // _strdup(), and warns if you call strdup(). Sure, we could just silence such
            // warnings, but let's try to do as they want.
            argv[0] = _strdup("mobile");
            argv[1] = nullptr;
            ProcUtil::setThreadName("app");
            coolwsd = new COOLWSD();
            coolwsd->run(1, argv);
            // We will actually not get here, Util::forcedExit() will be called
            delete coolwsd;
            coolwsd = nullptr;
            LOG_TRC("COOLWSD completed");
        });

    {
        WNDCLASSEXW wcex;

        wcex.cbSize = sizeof(WNDCLASSEXW);
        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = WndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = 0;
        wcex.hInstance = hInstance;
        wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_CODA));
        wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
        if (isLightTheme())
            wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        else
            wcex.hbrBackground = CreateSolidBrush(RGB(0x12, 0x12, 0x12));
        wcex.lpszMenuName = NULL;
        wcex.lpszClassName = windowClass;
        wcex.hIconSm = NULL;

        if (!RegisterClassExW(&wcex))
        {
            MessageBoxW(NULL, L"Call to RegisterClassExW failed", Util::string_to_wide_string(APP_NAME).c_str(), NULL);
            return 1;
        }
   }

    // Open the first document here, then open the rest one by one once the previous has loaded.
    if (mode == DocumentMode::STARTER)
        openCOOLWindow({ }, mode);
    else
        load_next_document();

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

// vim:set shiftwidth=4 softtabstop=4 expandtab:
