/*
 * Copyright (c) 2015, 2019, 2020, 2021, 2022 Valve Corporation
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 * may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* This is a stub steam.exe implementation for use inside Proton. It provides
 * a small subset of the actual Steam functionality for games that expect
 * Windows version of Steam running. */

#include "ntstatus.h"
#define WIN32_NO_STATUS
#include <windows.h>
#include <winternl.h>
#include <shlobj.h>
#include <string.h>
#include <stdio.h>
#include <limits.h>
#include <dlfcn.h>

#pragma push_macro("_WIN32")
#pragma push_macro("__cdecl")
#undef _WIN32
#undef __cdecl
#include "steam_api.h"
#pragma pop_macro("_WIN32")
#pragma pop_macro("__cdecl")

#include "wine/debug.h"

#include "json/json.h"

#include "wine/heap.h"
#include "wine/vulkan.h"
#include "openvr.h"
#include "../src/ivrclientcore.h"

WINE_DEFAULT_DEBUG_CHANNEL(steam);

#define ARRAY_SIZE(a) (sizeof(a) / sizeof(*a))

static bool env_nonzero(const char *env)
{
    const char *v = getenv(env);
    return v != NULL && *v && v[0] != '0';
}

static void set_active_process_pid(void)
{
    DWORD pid = GetCurrentProcessId();
    RegSetKeyValueA(HKEY_CURRENT_USER, "Software\\Valve\\Steam\\ActiveProcess", "pid", REG_DWORD, &pid, sizeof(pid));
}

static DWORD WINAPI create_steam_window(void *arg)
{
    static WNDCLASSEXW wndclass = { sizeof(WNDCLASSEXW) };
    static const WCHAR class_nameW[] = {'v','g','u','i','P','o','p','u','p','W','i','n','d','o','w',0};
    static const WCHAR steamW[] = {'S','t','e','a','m',0};
    MSG msg;

    wndclass.lpfnWndProc = DefWindowProcW;
    wndclass.lpszClassName = class_nameW;

    RegisterClassExW(&wndclass);
    CreateWindowW(class_nameW, steamW, WS_POPUP, 40, 40,
                  400, 300, NULL, NULL, NULL, NULL);

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

    return 0;
}

/* requires steam API to be initialized */
static void setup_steam_registry(void)
{
    const char *ui_lang;
    uint32 appid;
    char buf[256];
    HKEY key;
    LSTATUS status;

    ui_lang = SteamUtils()->GetSteamUILanguage();
    WINE_TRACE("UI language: %s\n", wine_dbgstr_a(ui_lang));
    RegSetKeyValueA(HKEY_CURRENT_USER, "Software\\Valve\\Steam", "language",
                    REG_SZ, ui_lang, strlen(ui_lang) + 1);

    appid = SteamUtils()->GetAppID();
    WINE_TRACE("appid: %u\n", appid);
    sprintf(buf, "Software\\Valve\\Steam\\Apps\\%u", appid);
    status = RegCreateKeyA(HKEY_CURRENT_USER, buf, &key);
    if (!status)
    {
        DWORD value;
        value = 1;
        RegSetKeyValueA(key, NULL, "Installed", REG_DWORD, &value, sizeof(value));
        RegSetKeyValueA(key, NULL, "Running", REG_DWORD, &value, sizeof(value));
        value = 0;
        RegSetKeyValueA(key, NULL, "Updating", REG_DWORD, &value, sizeof(value));
        RegCloseKey(key);
    }
    else WINE_ERR("Could not create key: %u\n", status);
}

static void copy_to_win(const char *unix_path, const WCHAR *win_path)
{
    WCHAR *src_path = wine_get_dos_file_name(unix_path);
    if (!src_path)
        return;

    CopyFileW(src_path, win_path, FALSE);

    HeapFree(GetProcessHeap(), 0, src_path);
}

/* requires steam API to be initialized */
static void setup_battleye_bridge(void)
{
    const unsigned int be_runtime_appid = 1161040;
    char path[2048];
    char *path_end;

    if (!SteamApps()->BIsAppInstalled(be_runtime_appid))
        return;

    if (!SteamApps()->GetAppInstallDir(be_runtime_appid, path, sizeof(path)))
        return;

    WINE_TRACE("Found battleye runtime at %s\n", path);

    setenv("PROTON_BATTLEYE_RUNTIME", path, 1);
}

static void setup_eac_bridge(void)
{
    const unsigned int eac_runtime_appid = 1826330;
    char path[2048];
    char *path_end;

    if (!SteamApps()->BIsAppInstalled(eac_runtime_appid))
        return;

    if (!SteamApps()->GetAppInstallDir(eac_runtime_appid, path, sizeof(path)))
        return;

    WINE_TRACE("Found easyanticheat runtime at %s\n", path);

    setenv("PROTON_EAC_RUNTIME", path, 1);
}

static std::string get_linux_vr_path(void)
{
    const char *e;

    static const char *openvr_path = "/openvr/openvrpaths.vrpath";

    e = getenv("VR_PATHREG_OVERRIDE");
    if(e && *e)
        return e;

    e = getenv("XDG_CONFIG_HOME");
    if(e && *e)
        return std::string(e) + openvr_path;

    e = getenv("HOME");
    if(e && *e)
        return std::string(e) + "/.config" + openvr_path;

    return "";
}

static bool get_windows_vr_path(WCHAR *out_path, bool create)
{
    if(FAILED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA | CSIDL_FLAG_CREATE,
                    NULL, 0, out_path)))
        return false;

    lstrcatW(out_path, L"\\openvr");

    if(create)
        CreateDirectoryW(out_path, NULL);

    lstrcatW(out_path, L"\\openvrpaths.vrpath");

    return true;
}

static WCHAR *str_to_wchar(const std::string &str)
{
    DWORD sz = MultiByteToWideChar(CP_UNIXCP, 0, str.c_str(), -1, NULL, 0);
    if(!sz)
        return NULL;

    WCHAR *ret = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, sizeof(WCHAR) * sz);
    if(!ret)
        return NULL;

    sz = MultiByteToWideChar(CP_UNIXCP, 0, str.c_str(), -1, ret, sz);
    if(!sz)
    {
        HeapFree(GetProcessHeap(), 0, ret);
        return NULL;
    }

    return ret;
}

static std::string read_text_file(const WCHAR *filename)
{
    HANDLE ifile = CreateFileW(filename, GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
            OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if(ifile == INVALID_HANDLE_VALUE)
        return "";

    LARGE_INTEGER size;

    if(!GetFileSizeEx(ifile, &size))
    {
        CloseHandle(ifile);
        return "";
    }

    char *buf = (char *)HeapAlloc(GetProcessHeap(), 0, size.u.LowPart);
    if(!buf)
    {
        CloseHandle(ifile);
        return "";
    }

    DWORD readed;

    if(!ReadFile(ifile, buf, size.u.LowPart, &readed, NULL))
    {
        HeapFree(GetProcessHeap(), 0, buf);
        CloseHandle(ifile);
        return "";
    }

    CloseHandle(ifile);

    DWORD outsize = 1;
    for(DWORD i = 1; i < readed; ++i)
    {
        if(buf[i] == '\n' && buf[i - 1] == '\r') // CRLF
            buf[outsize - 1] = '\n';
        else
            buf[outsize++] = buf[i];
    }

    std::string ret(buf, outsize);

    HeapFree(GetProcessHeap(), 0, buf);

    return ret;
}

static bool write_string_to_file(const WCHAR *filename, const std::string &contents)
{
    HANDLE ofile = CreateFileW(filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
            FILE_ATTRIBUTE_NORMAL, NULL);
    if(ofile == INVALID_HANDLE_VALUE)
        return false;

    DWORD written;

    if(!WriteFile(ofile, contents.data(), (DWORD)contents.length(), &written, NULL))
    {
        CloseHandle(ofile);
        return false;
    }

    CloseHandle(ofile);

    return true;
}

static bool convert_path_to_win(std::string &s)
{
    WCHAR *path = wine_get_dos_file_name(s.c_str());
    if(!path)
        return false;

    DWORD sz = WideCharToMultiByte(CP_UTF8, 0, path, -1, NULL, 0, NULL, NULL);
    if(!sz)
    {
        HeapFree(GetProcessHeap(), 0, path);
        return false;
    }

    char *pathUTF8 = (char *)HeapAlloc(GetProcessHeap(), 0, sz);
    if(!pathUTF8)
    {
        HeapFree(GetProcessHeap(), 0, path);
        return false;
    }

    sz = WideCharToMultiByte(CP_UTF8, 0, path, -1, pathUTF8, sz, NULL, NULL);
    if(!sz)
    {
        HeapFree(GetProcessHeap(), 0, pathUTF8);
        HeapFree(GetProcessHeap(), 0, path);
        return false;
    }

    s = pathUTF8;

    HeapFree(GetProcessHeap(), 0, pathUTF8);
    HeapFree(GetProcessHeap(), 0, path);

    return true;
}

static void convert_json_array_paths(Json::Value &arr)
{
    for(uint32_t i = 0; i < arr.size(); ++i)
    {
        std::string path(arr[i].asString());
        if(convert_path_to_win(path))
            arr[i] = path;
    }
}

static void convert_environment_path(const char *nameA, const WCHAR *nameW)
{
    /* get linux-side variable */
    const char *e = getenv(nameA);
    if(!e || !*e)
        return;

    /* convert to win and set */
    WCHAR *path = wine_get_dos_file_name(e);
    if(!path)
        return;

    SetEnvironmentVariableW(nameW, path);

    HeapFree(GetProcessHeap(), 0, path);
}

static void set_env_from_unix(const WCHAR *name, const std::string &val)
{
    WCHAR valW[MAX_PATH];
    DWORD sz;

    sz = MultiByteToWideChar(CP_UTF8, 0, val.c_str(), -1, valW, MAX_PATH);
    if(!sz)
    {
        WINE_WARN("Invalid utf8 seq in vr runtime key\n");
        return;
    }

    SetEnvironmentVariableW(name, valW);
}

static bool convert_linux_vrpaths(void)
{
    /* read in linux vrpaths */
    std::string linux_vrpaths = get_linux_vr_path();
    if(linux_vrpaths.empty())
    {
        WINE_TRACE("Couldn't get openvr vrpaths path\n");
        return false;
    }

    WCHAR *linux_vrpathsW = str_to_wchar(linux_vrpaths);
    if(!linux_vrpathsW)
        return false;

    std::string contents = read_text_file(linux_vrpathsW);
    HeapFree(GetProcessHeap(), 0, linux_vrpathsW);
    if(contents.empty())
    {
        WINE_TRACE("openvr vrpaths is empty\n");
        return false;
    }

    Json::Value root;
    Json::Reader reader;

    if(!reader.parse(contents, root))
    {
        WINE_WARN("Invalid openvr vrpaths JSON\n");
        return false;
    }

    /* pass original runtime path into Wine */
    if(root.isMember("runtime") && root["runtime"].isArray() && root["runtime"].size() > 0)
    {
        set_env_from_unix(L"PROTON_VR_RUNTIME", root["runtime"][0].asString());
    }

    /* set hard-coded paths */
    root["runtime"] = Json::Value(Json::ValueType::arrayValue);
    root["runtime"][0] = "C:\\vrclient\\";
    root["runtime"][1] = "C:\\vrclient";

    /* map linux paths into windows filesystem */
    if(root.isMember("config") && root["config"].isArray())
        convert_json_array_paths(root["config"]);

    if(root.isMember("log") && root["log"].isArray())
        convert_json_array_paths(root["log"]);

    /* external_drivers is currently unsupported in Proton */
    root["external_drivers"] = Json::Value(Json::ValueType::nullValue);

    /* write out windows vrpaths */
    SetEnvironmentVariableW(L"VR_PATHREG_OVERRIDE", NULL);
    SetEnvironmentVariableW(L"VR_OVERRIDE", NULL);
    convert_environment_path("VR_CONFIG_PATH", L"VR_CONFIG_PATH");
    convert_environment_path("VR_LOG_PATH", L"VR_LOG_PATH");
    Json::StyledWriter writer;

    WCHAR windows_vrpaths[MAX_PATH];
    if(!get_windows_vr_path(windows_vrpaths, true))
        return false;

    contents = writer.write(root);

    write_string_to_file(windows_vrpaths, contents);

    return true;
}

static void setup_vrpaths(void)
{
    bool success = false;

    try{
        success = convert_linux_vrpaths();
    }catch(std::exception e){
        WINE_ERR("got error parsing vrpaths file\n");
        success = false;
    }

    if(!success)
    {
        /* delete the windows file only if the linux conversion fails */
        WCHAR windows_vrpaths[MAX_PATH];
        if(get_windows_vr_path(windows_vrpaths, false))
        {
            DeleteFileW(windows_vrpaths);
        }
    }
}

static BOOL set_vr_status(HKEY key, DWORD value)
{
    LSTATUS status;

    if ((status = RegSetValueExA(key, "state", 0, REG_DWORD, (BYTE *)&value, sizeof(value))))
    {
        WINE_ERR("Could not set state value, status %#x.\n", status);
        return FALSE;
    }
    return TRUE;
}

void* load_vrclient(void)
{
    WCHAR pathW[PATH_MAX];
    char *pathU;
    DWORD sz;

#ifdef _WIN64
    static const char append_path[] = "/bin/linux64/vrclient.so";
#else
    static const char append_path[] = "/bin/vrclient.so";
#endif

    /* PROTON_VR_RUNTIME is provided by the proton setup script */
    if(!GetEnvironmentVariableW(L"PROTON_VR_RUNTIME", pathW, ARRAY_SIZE(pathW)))
    {
        WINE_TRACE("Linux OpenVR runtime is not available\n");
        return 0;
    }

    sz = WideCharToMultiByte(CP_UNIXCP, 0, pathW, -1, NULL, 0, NULL, NULL);
    if(!sz)
    {
        WINE_ERR("Can't convert path to unixcp! %s\n", wine_dbgstr_w(pathW));
        return NULL;
    }

    pathU = (char *)HeapAlloc(GetProcessHeap(), 0, sz + sizeof(append_path));

    sz = WideCharToMultiByte(CP_UNIXCP, 0, pathW, -1, pathU, sz, NULL, NULL);
    if(!sz)
    {
        WINE_ERR("Can't convert path to unixcp! %s\n", wine_dbgstr_w(pathW));
        return NULL;
    }

    strcat(pathU, append_path);

    WINE_TRACE("got openvr runtime path: %s\n", pathU);

    return dlopen(pathU, RTLD_NOW);
}

static char *strdupA(const char *s)
{
    size_t l = strlen(s) + 1;
    char *r = (char *)heap_alloc(l);
    memcpy(r, s, l);
    return r;
}

static void parse_extensions(const char *in, uint32_t *out_count,
        char ***out_strs)
{
    char *iter, *start;
    char **list, *str = strdupA(in);
    uint32_t extension_count = 0, o = 0;

    iter = str;
    while(*iter){
        if(*iter++ == ' ')
            extension_count++;
    }
    /* count the one ending in NUL */
    if(iter != str)
        extension_count++;
    if(!extension_count){
        *out_count = 0;
        *out_strs = NULL;
        return;
    }

    list = (char **)heap_alloc(extension_count * sizeof(*list));

    start = iter = str;
    do{
        if(*iter == ' '){
            *iter = 0;
            list[o++] = start;
            WINE_TRACE("added %s to list\n", list[o-1]);
            iter++;
            start = iter;
        }else if(*iter == 0){
            list[o++] = start;
            WINE_TRACE("added %s to list\n", list[o-1]);
            break;
        }else{
            iter++;
        }
    }while(1);

    *out_count = extension_count;
    *out_strs = list;
}

extern "C"
{
    VkPhysicalDevice WINAPI __wine_get_native_VkPhysicalDevice(VkPhysicalDevice phys_dev);
};

static DWORD WINAPI initialize_vr_data(void *arg)
{
    int (WINAPI *p__wineopenxr_get_extensions_internal)(char **instance_extensions, char **device_extensions);
    vr::IVRClientCore* (*vrclient_VRClientCoreFactory)(const char *name, int *return_code);
    uint32_t instance_extensions_count, device_count;
    VkPhysicalDevice *phys_devices = NULL;
    VkPhysicalDeviceProperties prop = {};
    VkInstanceCreateInfo inst_info = {};
    char **instance_extensions = NULL;
    VkApplicationInfo app_info = {};
    char *buffer = NULL, *new_buffer;
    vr::IVRClientCore* client_core;
    char *xr_inst_ext, *xr_dev_ext;
    vr::IVRCompositor* compositor;
    VkInstance vk_instance = NULL;
    BOOL vr_initialized = FALSE;
    HKEY vr_key = (HKEY)arg;
    vr::EVRInitError error;
    HMODULE hvulkan = NULL;
    DWORD vr_status = ~0u;
    const char *env_str;
    unsigned int app_id;
    unsigned int length;
    HMODULE hwineopenxr;
    void *lib_vrclient;
    DWORD hmd_present;
    int return_code;
    LSTATUS status;
    unsigned int i;
    VkResult res;

    WINE_TRACE("Starting VR info initialization.\n");

    if (!(lib_vrclient = load_vrclient()))
    {
        WINE_ERR("Could not load libopenvr_api.so.\n");
        set_vr_status(vr_key, ~0u);
        RegCloseKey(vr_key);
        return 0;
    }

    if (!(vrclient_VRClientCoreFactory = reinterpret_cast<decltype(vrclient_VRClientCoreFactory)>
            (dlsym(lib_vrclient, "VRClientCoreFactory"))))
    {
        WINE_ERR("Could not find function %s.\n", vrclient_VRClientCoreFactory);
        goto done;
    }
    if (!(client_core = vrclient_VRClientCoreFactory(vr::IVRClientCore_Version, &return_code)))
    {
        WINE_ERR("Could not get IVRClientCore, error %d.\n", return_code);
    }

    /* Without overriding the app_key vrclient waits 2 seconds for a valid appkey before returning. */
    error = client_core->Init(vr::VRApplication_Background, NULL);
    if (error != vr::VRInitError_None)
    {
        if (error == vr::VRInitError_Init_NoServerForBackgroundApp)
            WINE_TRACE("VR server is not available.\n");
        else
            WINE_ERR("VR init failed, error %u.\n", error);
        goto done;
    }
    vr_initialized = TRUE;

    hmd_present = !!client_core->BIsHmdPresent();
    WINE_TRACE("hmd_present %#x.\n", hmd_present);
    if ((status = RegSetValueExA(vr_key, "is_hmd_present", 0, REG_DWORD, (BYTE *)&hmd_present, sizeof(hmd_present))))
        WINE_ERR("Could not set is_hmd_present value, status %#x.\n", status);

    compositor = reinterpret_cast<vr::IVRCompositor*>(client_core->GetGenericInterface(vr::IVRCompositor_Version, &error));
    if (!compositor)
    {
        WINE_ERR("Could not get compositor, error %u.\n", error);
        goto done;
    }

    length = compositor->GetVulkanInstanceExtensionsRequired(nullptr, 0);
    if (!(buffer = (char *)heap_alloc(length)))
    {
        WINE_ERR("No memory.\n");
        goto done;
    }
    *buffer = 0;
    compositor->GetVulkanInstanceExtensionsRequired(buffer, length);
    WINE_TRACE("Instance extensions %s.\n", buffer);

    if ((status = RegSetValueExA(vr_key, "openvr_vulkan_instance_extensions", 0, REG_SZ, (BYTE *)buffer, length)))
    {
        WINE_ERR("Could not set openvr_vulkan_instance_extensions value, status %#x.\n", status);
        return FALSE;
    }

    if (!(hvulkan = LoadLibraryA("winevulkan.dll")))
    {
        WINE_ERR("Could not load winevulkan.\n");
        goto done;
    }

#define USE_VULKAN_PROC(name) decltype(name) *p##name;\
    if (!(p##name = reinterpret_cast<decltype(name) *>(GetProcAddress(hvulkan, "wine_"#name)))\
            && !(p##name = reinterpret_cast<decltype(name) *>(GetProcAddress(hvulkan, #name))))\
    {\
        WINE_ERR("Could not find function %s.\n", #name);\
        goto done;\
    }
    USE_VULKAN_PROC(vkCreateInstance)
    USE_VULKAN_PROC(vkDestroyInstance)
    USE_VULKAN_PROC(vkEnumeratePhysicalDevices)
    USE_VULKAN_PROC(vkGetPhysicalDeviceProperties)
    USE_VULKAN_PROC(__wine_get_native_VkPhysicalDevice)
#undef USE_VULKAN_PROC

    parse_extensions(buffer, &instance_extensions_count, &instance_extensions);

    app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    app_info.pApplicationName = "proton_vrhelper";
    app_info.applicationVersion = 1;
    app_info.pEngineName = "proton_vrhelper";
    app_info.engineVersion = 1;
    app_info.apiVersion = VK_MAKE_VERSION(1, 1, 0);

    inst_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    inst_info.pApplicationInfo = &app_info;
    inst_info.enabledExtensionCount = instance_extensions_count;
    inst_info.ppEnabledExtensionNames = instance_extensions;

    if ((res = pvkCreateInstance(&inst_info, NULL, &vk_instance)) != VK_SUCCESS)
    {
        WINE_ERR("Could not create VK instance, res %d.\n", res);
        goto done;
    }

    if ((res = pvkEnumeratePhysicalDevices(vk_instance, &device_count, NULL)) != VK_SUCCESS)
    {
        WINE_ERR("Could not enumerate physical devices, res %d.\n", res);
        goto done;
    }
    if (!(phys_devices = (VkPhysicalDevice *)heap_alloc(device_count * sizeof(*phys_devices))))
    {
        WINE_ERR("No memory.\n");
        goto done;
    }
    if ((res = pvkEnumeratePhysicalDevices(vk_instance, &device_count, phys_devices)) != VK_SUCCESS)
    {
        WINE_ERR("Could not enumerate physical devices, res %d.\n", res);
        goto done;
    }

    for (i = 0; i < device_count; ++i)
    {
        char name[256];
        LUID luid;

        pvkGetPhysicalDeviceProperties(phys_devices[i], &prop);
        if (prop.apiVersion < VK_MAKE_VERSION(1, 1, 0))
        {
            WINE_TRACE("Skipping Vulkan 1.0 adapter %s.\n", prop.deviceName);
            continue;
        }

        length = compositor->GetVulkanDeviceExtensionsRequired(p__wine_get_native_VkPhysicalDevice(phys_devices[i]), nullptr, 0);
        if (!(new_buffer = (char *)heap_realloc(buffer, length)))
        {
            WINE_ERR("No memory.\n");
            goto done;
        }
        buffer = new_buffer;
        compositor->GetVulkanDeviceExtensionsRequired(p__wine_get_native_VkPhysicalDevice(phys_devices[i]), buffer, length);
        sprintf(name, "PCIID:%04x:%04x", prop.vendorID, prop.deviceID);
        WINE_TRACE("%s: %s.\n", name, buffer);

        if ((status = RegSetValueExA(vr_key, name, 0, REG_SZ, (BYTE *)buffer, length)))
        {
            WINE_ERR("Could not set %s value, status %#x.\n", name, status);
            return FALSE;
        }
    }

    if ((hwineopenxr = LoadLibraryA("wineopenxr.dll")))
    {
        p__wineopenxr_get_extensions_internal = reinterpret_cast<decltype(p__wineopenxr_get_extensions_internal)>
                (GetProcAddress(hwineopenxr, "__wineopenxr_get_extensions_internal"));
        if (p__wineopenxr_get_extensions_internal)
        {
            if (!p__wineopenxr_get_extensions_internal(&xr_inst_ext, &xr_dev_ext))
            {
                WINE_TRACE("Got XR extensions.\n");
                if ((status = RegSetValueExA(vr_key, "openxr_vulkan_instance_extensions", 0, REG_SZ,
                        (BYTE *)xr_inst_ext, strlen(xr_inst_ext) + 1)))
                {
                    WINE_ERR("Could not set openxr_vulkan_instance_extensions value, status %#x.\n", status);
                    return FALSE;
                }
                if ((status = RegSetValueExA(vr_key, "openxr_vulkan_device_extensions", 0, REG_SZ,
                        (BYTE *)xr_dev_ext, strlen(xr_dev_ext) + 1)))
                {
                    WINE_ERR("Could not set openxr_vulkan_device_extensions value, status %#x.\n", status);
                    return FALSE;
                }
            }
        }
        else
        {
            WINE_ERR("__wineopenxr_get_extensions_internal not found in wineopenxr.dll.\n");
        }
        FreeLibrary(hwineopenxr);
    }
    else
    {
        WINE_WARN("Could not load wineopenxr.dll, err %u.\n", GetLastError());
    }

    vr_status = 1;

done:
    set_vr_status(vr_key, vr_status);

    heap_free(phys_devices);

    if (vk_instance)
        pvkDestroyInstance(vk_instance, NULL);

    if (instance_extensions)
    {
        heap_free(instance_extensions[0]);
        heap_free(instance_extensions);
    }
    if (hvulkan)
        FreeLibrary(hvulkan);
    heap_free(buffer);
    if (vr_initialized)
        client_core->Cleanup();
    WINE_TRACE("Completed VR info initialization.\n");
    dlclose(lib_vrclient);
    RegCloseKey(vr_key);
    return 0;
}

static void setup_vr_registry(void)
{
    LSTATUS status;
    HANDLE thread;
    HKEY vr_key;
    DWORD disp;

    if ((status = RegCreateKeyExA(HKEY_CURRENT_USER, "Software\\Wine\\VR", 0, NULL, REG_OPTION_VOLATILE,
            KEY_ALL_ACCESS, NULL, &vr_key, &disp)))
    {
        WINE_ERR("Could not create key, status %#x.\n", status);
        return;
    }
    if (disp != REG_CREATED_NEW_KEY)
    {
        WINE_ERR("VR key already exists, disp %#x.\n", disp);
        RegCloseKey(vr_key);
        return;
    }

    if (!set_vr_status(vr_key, 0))
    {
        RegCloseKey(vr_key);
        return;
    }

    if (!(thread = CreateThread(NULL, 0, initialize_vr_data, (void *)vr_key, 0, NULL)))
    {
        WINE_ERR("Could not create thread, error %u.\n", GetLastError());
        RegCloseKey(vr_key);
        return;
    }
    CloseHandle(thread);

    WINE_TRACE("Queued VR info initialization.\n");
}

static WCHAR *strchrW(WCHAR *h, WCHAR n)
{
    do
    {
        if(*h == n)
            return h;
    } while (*h++);

    return NULL;
}

int strncmpW(const WCHAR *l, const WCHAR *r, int n)
{
    if(n <= 0)
        return 0;

    while(--n > 0 && *l && *l == *r){
        l++;
        r++;
    }

    return *l - *r;
}

static WCHAR *find_quote(WCHAR *str)
{
    WCHAR *end = strchrW(str, '"'), *ch;
    int odd;
    while (end)
    {
        odd = 0;
        ch = end - 1;
        while (ch >= str && *ch == '\\')
        {
            odd = !odd;
            --ch;
        }
        if (!odd)
            return end;
        end = strchrW(end + 1, '"');
    }
    return NULL;
}

static BOOL WINAPI console_ctrl_handler(DWORD dwCtrlType)
{
    return TRUE;
}

static BOOL streq_niw(const WCHAR *l, const WCHAR *r, size_t len)
{
    while(len > 0){
        if(towlower(*l) != towlower(*r))
            return FALSE;
        ++l; ++r; --len;
    }
    return TRUE;
}

static BOOL should_use_shell_execute(const WCHAR *cmdline)
{
    BOOL use_shell_execute = TRUE;
    BOOL quoted = FALSE;
    const WCHAR *executable_name_end = cmdline;

    /* find the end of the first arg...*/
    while (*executable_name_end != '\0' &&
           (*executable_name_end != ' ' || quoted) &&
           (*executable_name_end != '"' || !quoted))
    {
        quoted ^= *executable_name_end == '"';

        executable_name_end++;
    }

    /* backtrack to before the end of the arg
     * and check if we end in .exe or not
     * and determine whether to use ShellExecute
     * based on that */
    executable_name_end -= strlen(".exe");

    if (executable_name_end >= cmdline)
    {
        static const WCHAR exeW[] = {'.','e','x','e',0};

        if (streq_niw(executable_name_end, exeW, sizeof(exeW) / sizeof(*exeW) - 1))
            use_shell_execute = FALSE;
    }

    return use_shell_execute;
}

static HANDLE run_process(BOOL *should_await)
{
    WCHAR *cmdline = GetCommandLineW();
    STARTUPINFOW si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    DWORD flags = CREATE_UNICODE_ENVIRONMENT;
    BOOL use_shell_execute = TRUE;
    BOOL hide_window;

    /* skip argv[0] */
    if (*cmdline == '"')
    {
        cmdline = find_quote(cmdline + 1);
        if (cmdline) cmdline++;
    }
    else
    {
        cmdline = strchrW(cmdline, ' ');
    }
    if (!cmdline)
    {
        WINE_ERR("Invalid command\n");
        return INVALID_HANDLE_VALUE;
    }
    while (*cmdline == ' ') cmdline++;

    /* convert absolute unix path to dos */
    if (cmdline[0] == '/' ||
            (cmdline[0] == '"' && cmdline[1] == '/'))
    {
        WCHAR *scratchW;
        char *scratchA;
        WCHAR *start, *end, *dos, *remainder, *new_cmdline;
        size_t argv0_len;
        int r;
        DWORD_PTR console;
        SHFILEINFOW sfi;

        static const WCHAR dquoteW[] = {'"',0};

        WINE_TRACE("Converting unix command: %s\n", wine_dbgstr_w(cmdline));

        if (cmdline[0] == '"')
        {
            start = cmdline + 1;
            end = find_quote(start);
            if (!end)
            {
                WINE_ERR("Unmatched quote? %s\n", wine_dbgstr_w(cmdline));
                goto run;
            }
            remainder = end + 1;
        }
        else
        {
            start = cmdline;
            end = strchrW(start, ' ');
            if (!end)
                end = strchrW(start, '\0');
            remainder = end;
        }

        argv0_len = end - start;

        scratchW = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (argv0_len + 1) * sizeof(WCHAR));
        memcpy(scratchW, start, argv0_len * sizeof(WCHAR));
        scratchW[argv0_len] = '\0';

        r = WideCharToMultiByte(CP_UNIXCP, 0, scratchW, -1,
                NULL, 0, NULL, NULL);
        if (!r)
        {
            WINE_ERR("Char conversion size failed?\n");
            goto run;
        }

        scratchA = (char *)HeapAlloc(GetProcessHeap(), 0, r);

        r = WideCharToMultiByte(CP_UNIXCP, 0, scratchW, -1,
                scratchA, r, NULL, NULL);
        if (!r)
        {
            WINE_ERR("Char conversion failed?\n");
            goto run;
        }

        dos = wine_get_dos_file_name(scratchA);

        CoInitialize(NULL);

        console = SHGetFileInfoW(dos, 0, &sfi, sizeof(sfi), SHGFI_EXETYPE);
        if (console)
        {
            if (!HIWORD(console))
                flags |= CREATE_NEW_CONSOLE;
        }

        new_cmdline = (WCHAR *)HeapAlloc(GetProcessHeap(), 0,
                (lstrlenW(dos) + 3 + lstrlenW(remainder) + 1) * sizeof(WCHAR));
        lstrcpyW(new_cmdline, dquoteW);
        lstrcatW(new_cmdline, dos);
        lstrcatW(new_cmdline, dquoteW);
        lstrcatW(new_cmdline, remainder);

        cmdline = new_cmdline;
    }

run:
    WINE_TRACE("Running command %s\n", wine_dbgstr_w(cmdline));

    SetConsoleCtrlHandler( console_ctrl_handler, TRUE );

    use_shell_execute = should_use_shell_execute(cmdline);
    hide_window = env_nonzero("PROTON_HIDE_PROCESS_WINDOW");

    /* only await the process finishing if we launch a process directly...
     * Steam simply calls ShellExecuteA with the same parameters.
     * this avoids the edge case where we could ShellExecute and
     * then that process ends up ShellExecuting something as a throw away */
    *should_await = !use_shell_execute;

    WINE_TRACE("Executing via %s\n",
        wine_dbgstr_a(use_shell_execute ? "ShellExecuteW" : "CreateProcessW"));

    if (use_shell_execute)
    {
        static const WCHAR verb[] = { 'o', 'p', 'e', 'n', 0 };
        ShellExecuteW(NULL, verb, cmdline, NULL, NULL, hide_window ? SW_HIDE : SW_SHOWNORMAL);

        return INVALID_HANDLE_VALUE;
    }
    else
    {
        if (hide_window)
        {
            si.dwFlags |= STARTF_USESHOWWINDOW;
            si.wShowWindow = SW_HIDE;
        }

        if (!CreateProcessW(NULL, cmdline, NULL, NULL, FALSE, flags, NULL, NULL, &si, &pi))
        {
            WINE_ERR("Failed to create process %s: %u\n", wine_dbgstr_w(cmdline), GetLastError());
            return INVALID_HANDLE_VALUE;
        }

        CloseHandle(pi.hThread);

        return pi.hProcess;
    }
}

static BOOL steam_protocol_handler(int argc, char *argv[])
{
    const char *steam_prefix = "steam://";
    STARTUPINFOA si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    char cmd[1024];
    int size;
    int i;

    for (i = 1; i < argc; ++i)
        if (!strcmp(argv[i], "--"))
            break;

    if (i >= argc - 1)
        return FALSE;
    ++i;

    if (strlen(argv[i]) < ARRAY_SIZE(steam_prefix))
        return FALSE;

    if (strncasecmp(argv[i], steam_prefix, ARRAY_SIZE(steam_prefix) - 1))
        return FALSE;

    size = snprintf(cmd, sizeof(cmd), "winebrowser \"%s\"", argv[i]);
    if (size >= sizeof(cmd))
    {
        WINE_ERR("Argument is too large, argv[%d] %s.\n", i, wine_dbgstr_a(argv[i]));
        return TRUE;
    }

    WINE_TRACE("Executing %s.\n", wine_dbgstr_a(cmd));
    if (!CreateProcessA(NULL, cmd, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT, NULL, NULL, &si, &pi))
    {
        WINE_ERR("Failed to create process %s, error %u.\n", wine_dbgstr_a(cmd), GetLastError());
        return TRUE;
    }
    FreeConsole();
    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return TRUE;
}

static void setup_steam_files(void)
{
    static const WCHAR config_pathW[] = L"C:\\Program Files (x86)\\Steam\\config";
    static const WCHAR steamapps_pathW[] = L"C:\\Program Files (x86)\\Steam\\steamapps";
    static const WCHAR libraryfolders_nameW[] = L"C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf";
    const char *steam_install_path = getenv("STEAM_COMPAT_CLIENT_INSTALL_PATH");
    const char *steam_library_paths = getenv("STEAM_COMPAT_LIBRARY_PATHS");
    const char *start, *end, *next;
    unsigned int i, index = 1;
    std::string contents;
    char idx_str[10];

    if (!CreateDirectoryW(config_pathW, NULL) && GetLastError() != ERROR_ALREADY_EXISTS)
    {
        WINE_ERR("Failed to create config directory, GetLastError() %u.\n", GetLastError());
        return;
    }

    if (!CreateDirectoryW(steamapps_pathW, NULL) && GetLastError() != ERROR_ALREADY_EXISTS)
    {
        WINE_ERR("Failed to create steamapps directory, GetLastError() %u.\n", GetLastError());
        return;
    }

    contents += "\"LibraryFolders\"\n{\n";

    WINE_TRACE("steam_install_path %s.\n", wine_dbgstr_a(steam_install_path));

    if (steam_install_path)
    {
        std::string s = steam_install_path;

        if (convert_path_to_win(s))
        {
            sprintf(idx_str, "%u", index);
            ++index;

            for (i = 0; i < s.length(); ++i)
            {
                if (s[i] == '\\')
                {
                    s.insert(i, 1, '\\');
                    ++i;
                }
            }

            contents += std::string("\t\"") + idx_str + "\" \t\"" + s + "\"\n";
        }
        else
        {
            WINE_ERR("Could not convert %s to win path.\n", wine_dbgstr_a(s.c_str()));
        }
    }

    WINE_TRACE("steam_library_paths %s.\n", wine_dbgstr_a(steam_library_paths));

    start = steam_library_paths;
    while (start && *start)
    {
        std::string s;

        if (!(next = strchr(start, ':')))
            next = start + strlen(start);
        end = next;

        if (end != start && end[-1] == '/')
            --end;

        while (end != start && end[-1] != '/' )
            --end;
        if (end != start)
            --end;

        s.append(start, end - start);
        if (convert_path_to_win(s))
        {
            sprintf(idx_str, "%u", index);
            ++index;

            for (i = 0; i < s.length(); ++i)
            {
                if (s[i] == '\\')
                {
                    s.insert(i, 1, '\\');
                    ++i;
                }
            }

            contents += std::string("\t\"") + idx_str + "\" \t\"" + s + "\"\n";
        }
        else
        {
            WINE_ERR("Could not convert %s to win path.\n", wine_dbgstr_a(s.c_str()));
        }
        if (*next == ':')
            ++next;
        start = next;
    }

    contents += "}\n";

    if (!write_string_to_file(libraryfolders_nameW, contents))
        WINE_ERR("Could not write %s.\n", wine_dbgstr_w(libraryfolders_nameW));
}

#ifndef DIRECTORY_QUERY
#define DIRECTORY_QUERY 0x0001
#endif

static HANDLE find_ack_event(void)
{
    static const WCHAR steam_ack_event[] = L"STEAM_START_ACK_EVENT";
    static const WCHAR name[] = L"\\BaseNamedObjects\\Session\\1";
    DIRECTORY_BASIC_INFORMATION *di;
    OBJECT_ATTRIBUTES attr;
    HANDLE dir, ret = NULL;
    ULONG context, size;
    UNICODE_STRING str;
    char buffer[1024];
    NTSTATUS status;

    di = (DIRECTORY_BASIC_INFORMATION *)buffer;

    RtlInitUnicodeString(&str, name);
    InitializeObjectAttributes(&attr, &str, 0, 0, NULL);
    status = NtOpenDirectoryObject( &dir, DIRECTORY_QUERY, &attr );
    if (status)
    {
        WINE_WARN("Failed to open directory %s, status %#x.\n", wine_dbgstr_w(name), status);
        return NULL;
    }

    status = NtQueryDirectoryObject(dir, di, sizeof(buffer), TRUE, TRUE, &context, &size);
    while (!status)
    {
        if (!strncmpW(di->ObjectName.Buffer, steam_ack_event, ARRAY_SIZE(steam_ack_event) - 1))
        {
            WINE_TRACE("Found event %s.\n", wine_dbgstr_w(di->ObjectName.Buffer));
            ret = OpenEventW(SYNCHRONIZE, FALSE, di->ObjectName.Buffer);
            if (!ret)
                WINE_WARN("Failed to create event, err %u.\n", GetLastError());
            break;
        }
        status = NtQueryDirectoryObject(dir, di, sizeof(buffer), TRUE, FALSE, &context, &size);
    }
    if (status && status != STATUS_NO_MORE_ENTRIES)
        WINE_WARN("NtQueryDirectoryObject failed, status %#x.\n", status);
    WINE_TRACE("ret %p.\n", ret);

    CloseHandle(dir);
    return ret;
}

static DWORD WINAPI steam_drm_thread(void *arg)
{
    HANDLE consume, produce;
    HANDLE start_ack = NULL;
    HANDLE child = arg;
    DWORD pid;
    LONG prev;

    consume = CreateSemaphoreA(NULL, 0, 512, "STEAM_DIPC_CONSUME");
    if (!consume)
    {
        WINE_ERR("Failed to create consume semaphore, err %u.\n", GetLastError());
        return -1;
    }
    produce = CreateSemaphoreA(NULL, 1, 512, "SREAM_DIPC_PRODUCE");
    if (!produce)
    {
        CloseHandle(consume);
        WINE_ERR("Failed to create produce semaphore, err %u.\n", GetLastError());
        return -1;
    }

    pid = GetProcessId(child);

    WINE_TRACE("Child pid %04x.\n", pid);

    while (WaitForSingleObject(consume, INFINITE) == WAIT_OBJECT_0)
    {
        WINE_TRACE("Got event.\n");

        if (!start_ack)
            start_ack = find_ack_event();
        if (start_ack)
            SetEvent(start_ack);
        ReleaseSemaphore(produce, 1, &prev);
        WINE_TRACE("prev %d.\n", prev);
    }

    return 0;
}
int main(int argc, char *argv[])
{
    HANDLE wait_handle = INVALID_HANDLE_VALUE;
    HANDLE event2 = INVALID_HANDLE_VALUE;
    HANDLE event = INVALID_HANDLE_VALUE;
    HANDLE child = INVALID_HANDLE_VALUE;
    BOOL game_process = FALSE;
    DWORD rc = 0;

    WINE_TRACE("\n");

    if (steam_protocol_handler(argc, argv))
        return 0;

    if (getenv("SteamGameId"))
    {
        /* do setup only for game process */
        event = CreateEventA(NULL, FALSE, FALSE, "Steam3Master_SharedMemLock");

        /* For 2K Launcher. */
        event2 = CreateEventA(NULL, FALSE, FALSE, "Global\\Valve_SteamIPC_Class");

        CreateThread(NULL, 0, create_steam_window, NULL, 0, NULL);

        set_active_process_pid();

        if (SteamAPI_Init())
        {
            setup_steam_registry();
            setup_battleye_bridge();
            setup_eac_bridge();
        }
        else
        {
            WINE_ERR("SteamAPI_Init failed\n");
        }

        setup_steam_files();

        if (env_nonzero("PROTON_WAIT_ATTACH"))
        {
            unsigned int sleep_count = 0;
            WINE_TRACE("PROTON_WAIT_ATTACH is set, waiting for debugger...\n");
            while (!IsDebuggerPresent())
            {
                Sleep(100);
                ++sleep_count;
                if (sleep_count >= 10)
                {
                    WINE_TRACE("still waiting for debugger...\n");
                    sleep_count = 0;
                }
            }
        }

        SteamAPI_Shutdown();

        game_process = TRUE;
    }

    if (argc > 1)
    {
        BOOL should_await;

        setup_vrpaths();

        if (game_process)
            setup_vr_registry();

        child = run_process(&should_await);

        if (should_await)
        {
            if (child == INVALID_HANDLE_VALUE)
                return 1;

            wait_handle = child;
        }

        if (game_process)
            CreateThread(NULL, 0, steam_drm_thread, child, 0, NULL);
    }

    if (game_process)
        NtSetInformationProcess( GetCurrentProcess(), (PROCESS_INFORMATION_CLASS)1000 /* ProcessWineMakeProcessSystem */,
                                 &wait_handle, sizeof(HANDLE *) );

    if(wait_handle != INVALID_HANDLE_VALUE)
    {
        FreeConsole();
        WaitForSingleObject(wait_handle, INFINITE);
    }

    if (event != INVALID_HANDLE_VALUE)
        CloseHandle(event);
    if (event2 != INVALID_HANDLE_VALUE)
        CloseHandle(event2);

    if (child != INVALID_HANDLE_VALUE)
    {
        GetExitCodeProcess(child, &rc);
        CloseHandle(child);
    }

    return rc;
}
