#include "pch.h"

#include "platform_windows/windows_bootstrap_helpers.h"
#include "platform_windows/windows_async_render_context.h"
#include "platform_windows/windows_main_window_session.h"
#include "platform_windows/windows_runtime_shell.h"
#include "platform_windows/windows_window_shell.h"

#include "app.h"
#include "legacy_gl_runtime_dispatch.h"
#include "legacy_preference_storage.h"
#include "log.h"

#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <iomanip>
#include <WbemCli.h>
#include <sstream>
#include <shellscalingapi.h>
#include <string>

#if __has_include(<renderdoc_app.h>)
#include <renderdoc_app.h>
#define USE_RENDERDOC
#endif

namespace pp::platform::windows {
App* bound_app() noexcept;
}

namespace pp::platform::windows {

#ifdef USE_RENDERDOC
RENDERDOC_API_1_4_0* rdoc_api = NULL;

bool win32_renderdoc_init()
{
    // At init, on windows
    if (HMODULE mod = GetModuleHandleA("renderdoc.dll"))
    {
        pRENDERDOC_GetAPI RENDERDOC_GetAPI =
            (pRENDERDOC_GetAPI)GetProcAddress(mod, "RENDERDOC_GetAPI");
        return RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_1_2, (void**)&rdoc_api);
    }
    return false;
}

void win32_renderdoc_frame_start()
{
    if (rdoc_api)
        rdoc_api->StartFrameCapture(NULL, NULL);
}

void win32_renderdoc_frame_end()
{
    if (rdoc_api)
        rdoc_api->EndFrameCapture(NULL, NULL);
}
#else
void win32_renderdoc_frame_start() { }
void win32_renderdoc_frame_end() { }
#endif

HRESULT(*GetDpiForMonitor_fn)(HMONITOR hmonitor, MONITOR_DPI_TYPE dpiType, UINT* dpiX, UINT* dpiY);
HRESULT(*SetProcessDpiAwareness_fn)(PROCESS_DPI_AWARENESS value);

void init_shcore_API()
{
    HMODULE dll = LoadLibrary(L"Shcore.dll");
    if (!dll)
    {
        LOG("cannot load Shcore.dll");
        return;
    }
    LOG("loaded Shcore.dll");
    GetDpiForMonitor_fn = (decltype(GetDpiForMonitor_fn))GetProcAddress(dll, "GetDpiForMonitor");
    SetProcessDpiAwareness_fn = (decltype(SetProcessDpiAwareness_fn))GetProcAddress(dll, "SetProcessDpiAwareness");
}

// Returns the last Win32 error, in string format. Returns an empty string if there is no error.
std::string GetLastErrorAsString()
{
    DWORD errorMessageID = ::GetLastError();
    if (errorMessageID == 0)
        return std::string();

    LPSTR messageBuffer = nullptr;
    size_t size = FormatMessageA(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);

    std::string message(messageBuffer, size);
    LocalFree(messageBuffer);
    return message;
}

void ensure_runtime_data_directory()
{
    FILE* fp_check = fopen("data\\layout.xml", "rb");
    if (!fp_check)
    {
        LOG("data files not found");
        static char path[MAX_PATH];
        GetModuleFileNameA(NULL, path, MAX_PATH);
        LOG("current dir %s", path);
        PathRemoveFileSpecA(path);
        SetCurrentDirectoryA(path);
        LOG("change dir to %s", path);
        return;
    }

    fclose(fp_check);
    LOG("data files ok");
}

int run_winmain_entry(int (*entry_point)(int, char**))
{
    int argc = 0;

    auto wargs = CommandLineToArgvW(GetCommandLine(), &argc);
    auto argv = new char*[argc + 1];
    for (int i = 0; i < argc; i++)
    {
        auto len = wcslen(wargs[i]) + 1;
        argv[i] = new char[len];
        wcstombs_s(nullptr, argv[i], len, wargs[i], len);
    }

    LocalFree(wargs);

    return entry_point(argc, argv);
}

void setup_exception_handler(const App& app)
{
    // Setup exception handler
    BT_SetAppName(_T("PanoPainter"));
    BT_SetAppVersion(g_version_w);
    //BT_SetSupportEMail(_T("your@email.com"));
    BT_SetFlags(BTF_DETAILEDMODE | BTF_ATTACHREPORT | BTF_SCREENCAPTURE);

    // = BugTrapServer ===========================================
    //BT_SetSupportServer(_T("omigamedev.ddns.net"), 8088);
    BT_SetSupportEMail(_T("info@panopainter.com"));
    // - or -
    //BT_SetSupportServer(_T("127.0.0.1"), 9999);

    // = BugTrapWebServer ========================================
    //BT_SetSupportServer(_T("http://localhost/BugTrapWebServer/RequestHandler.aspx"), BUGTRAP_HTTP_PORT);
    //BT_SetSupportServer(_T("http://omigamedev.ddns.net:8088/source/Server/BugTrapWebServer/RequestHandler.aspx"), BUGTRAP_HTTP_PORT);
    BT_SetSupportServer(_T("http://panopainter.com/bug/"), BUGTRAP_HTTP_PORT);

    // required for VS 2005 & 2008
    BT_InstallSehFilter();

    // Add custom log file using default name
    TCHAR wpath[MAX_PATH];
    //GetFullPathNameW(L"panopainter-log.txt", 1024, wpath, nullptr);
    auto log_file = app.data_path + "/panopainter-log.txt";
    std::mbstowcs(wpath, log_file.c_str(), log_file.size());
    BT_AddLogFile(wpath);

    BT_SetPreErrHandler([](INT_PTR nErrHandlerParam){
        const auto* app = reinterpret_cast<const App*>(nErrHandlerParam);
        auto* canvas_document = app && app->canvas ? app->canvas->m_canvas.get() : nullptr;
        if (canvas_document && canvas_document->m_unsaved)
        {
            auto t = std::time(nullptr);
            auto tm = *std::localtime(&t);
            std::ostringstream oss;
            oss << std::put_time(&tm, "%d-%m-%Y %H-%M-%S");

            auto path = app->data_path + "/" + app->doc_name + "-recovery (" + oss.str() + ").ppi";
            canvas_document->project_save_thread(path, false);
            static char abspath[MAX_PATH];
            GetFullPathNameA(path.c_str(), MAX_PATH, abspath, NULL);
            static char message[4096];
            snprintf(message, sizeof(message), "File recovered in: %s", abspath);
            MessageBoxA(retained_main_window_handle_ref(), message, "File Recovery", MB_OK | MB_ICONWARNING);
        }
        LogRemote::I.file_close();
    }, reinterpret_cast<INT_PTR>(&app));

    BT_SetTerminate();
}

int read_WMI_info()
{
    // see: http://win32easy.blogspot.co.uk/2011/03/wmi-in-c-query-everyting-from-your-os.html

    HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    if (FAILED(hRes))
    {
        LOG("Unable to launch COM: %x", hRes);
        return 1;
    }

    if ((FAILED(hRes = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, 0))))
    {
        LOG("Unable to initialize security: %x", hRes);
        return 1;
    }

    IWbemLocator* pLocator = NULL;
    if (FAILED(hRes = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pLocator))))
    {
        LOG("Unable to create a WbemLocator: %x", hRes);
        return 1;
    }

    IWbemServices* pService = NULL;
    if (FAILED(hRes = pLocator->ConnectServer(BSTR(L"root\\CIMV2"), NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
    {
        pLocator->Release();
        LOG("Unable to connect to \"CIMV2\": %x", hRes);
        return 1;
    }

    auto log_field = [](const wchar_t* section, IWbemClassObject* clsObj, const wchar_t* field) {
        VARIANT vRet;
        CIMTYPE pType;
        VariantInit(&vRet);
        if (SUCCEEDED(clsObj->Get(field, 0, &vRet, &pType, NULL)))
        {
            if (pType == CIM_STRING && pType != CIM_EMPTY && pType != CIM_ILLEGAL)
            {
                LOGW(L"%s %s: %s", section, field, vRet.bstrVal);
            }
            else if (pType == CIM_UINT32 && pType != CIM_EMPTY && pType != CIM_ILLEGAL)
            {
                LOGW(L"%s %s: %d", section, field, vRet.uintVal);
            }

            VariantClear(&vRet);
        }
    };

    auto get_int = [](IWbemClassObject* clsObj, const wchar_t* field) {
        VARIANT vRet;
        CIMTYPE pType;
        VariantInit(&vRet);
        int ret = 0;
        if (SUCCEEDED(clsObj->Get(field, 0, &vRet, &pType, NULL)))
        {
            if (pType == CIM_UINT32 && pType != CIM_EMPTY && pType != CIM_ILLEGAL)
                ret = vRet.uintVal;
            VariantClear(&vRet);
        }
        return ret;
    };


    // GET DEVICE INFO
    {
        IEnumWbemClassObject* pEnumerator = NULL;
        if (FAILED(hRes = pService->ExecQuery(BSTR(L"WQL"), BSTR(L"SELECT * FROM Win32_ComputerSystem"), WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
        {
            pLocator->Release();
            pService->Release();
            LOG("Unable to retrive desktop monitors: %x", hRes);
            return 1;
        }

        IWbemClassObject* clsObj = NULL;
        int numElems;
        while ((hRes = pEnumerator->Next(WBEM_INFINITE, 1, &clsObj, (ULONG*)&numElems)) != WBEM_S_FALSE)
        {
            if (FAILED(hRes))
                break;

            log_field(L"Machine", clsObj, L"Name");
            log_field(L"Machine", clsObj, L"Model");
            log_field(L"Machine", clsObj, L"Manufacturer");

            clsObj->Release();
        }
        pEnumerator->Release();
    }

    // GET OS INFO
    {
        IEnumWbemClassObject* pEnumerator = NULL;
        if (FAILED(hRes = pService->ExecQuery(BSTR(L"WQL"), BSTR(L"SELECT * FROM Win32_OperatingSystem"), WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
        {
            pLocator->Release();
            pService->Release();
            LOG("Unable to retrive desktop monitors: %x", hRes);
            return 1;
        }

        IWbemClassObject* clsObj = NULL;
        int numElems;
        while ((hRes = pEnumerator->Next(WBEM_INFINITE, 1, &clsObj, (ULONG*)&numElems)) != WBEM_S_FALSE)
        {
            if (FAILED(hRes))
                break;

            log_field(L"OS", clsObj, L"Name");
            log_field(L"OS", clsObj, L"Version");
            log_field(L"OS", clsObj, L"Locale");
            log_field(L"OS", clsObj, L"OSProductSuite");
            log_field(L"OS", clsObj, L"Manufacturer");
            log_field(L"OS", clsObj, L"Description");

            clsObj->Release();
        }
        pEnumerator->Release();
    }


    pService->Release();
    pService = NULL;
    if (FAILED(hRes = pLocator->ConnectServer(BSTR(L"root\\Microsoft\\Windows\\DeviceGuard"), NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
    {
        pLocator->Release();
        LOG("Unable to connect to \"DeviceGuard\": %x", hRes);
        return 1;
    }

    // GET DEVICE GUARD
    {
        IEnumWbemClassObject* pEnumerator = NULL;
        if (FAILED(hRes = pService->ExecQuery(BSTR(L"WQL"), BSTR(L"SELECT * FROM Win32_DeviceGuard"), WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
        {
            pLocator->Release();
            pService->Release();
            LOG("Unable to retrive desktop monitors: %x", hRes);
            return 1;
        }

        IWbemClassObject* clsObj = NULL;
        int numElems;
        while ((hRes = pEnumerator->Next(WBEM_INFINITE, 1, &clsObj, (ULONG*)&numElems)) != WBEM_S_FALSE)
        {
            if (FAILED(hRes))
                break;

            if (get_int(clsObj, L"CodeIntegrityPolicyEnforcementStatus") > 0)
            {
                LOG("SANDBOX DETECTED");
                set_retained_main_window_sandboxed(true);
            }

            SAFEARRAY *psaNames = NULL;
            if (SUCCEEDED(clsObj->GetNames(0, WBEM_FLAG_ALWAYS | WBEM_FLAG_NONSYSTEM_ONLY, 0, &psaNames)))
            {
                // Get the number of properties.
                long lLower, lUpper;
                BSTR PropName = NULL;
                SafeArrayGetLBound(psaNames, 1, &lLower);
                SafeArrayGetUBound(psaNames, 1, &lUpper);

                for (long i = lLower; i <= lUpper; i++)
                {
                    // Get this property.
                    SafeArrayGetElement(psaNames, &i, &PropName);

                    LOGW(L"Prop: %s", PropName);
                    log_field(L"DeviceGuard", clsObj, PropName);

                    SysFreeString(PropName);
                }

                SafeArrayDestroy(psaNames);
            }

            clsObj->Release();
        }
        pEnumerator->Release();
    }


    pLocator->Release();
    CoUninitialize();

    return 0;
}

MainWindowStartupState initialize_main_window_startup_state(App& app)
{
    auto startup = MainWindowStartupState {};
    const auto hInst = GetModuleHandle(NULL);
    const auto className = L"EngineMain";

    startup.window_class.hInstance = hInst;
    startup.window_class.lpfnWndProc = (WNDPROC)main_window_proc;
    startup.window_class.lpszClassName = className;
    startup.window_class.hbrBackground = (HBRUSH)COLOR_WINDOW;
    startup.window_class.hCursor = LoadCursor(NULL, IDC_ARROW);

    RegisterClass(&startup.window_class);

    auto monitor = MonitorFromWindow(0, MONITOR_DEFAULTTOPRIMARY);
    auto x = unsigned{ 96 };
    auto y = unsigned{ 96 };
    if (GetDpiForMonitor_fn)
        GetDpiForMonitor_fn(monitor, MDT_EFFECTIVE_DPI, &x, &y);
    app.display_density = (float)x / 96.f;

    const auto window_preferences = pp::panopainter::read_legacy_window_preferences(SW_NORMAL);
    if (window_preferences.has_ui_scale)
        app.zoom = window_preferences.ui_scale;
    else
        app.zoom = (float)x / 96.f;

    startup.show_command = window_preferences.show_command;
    startup.client_rect = {
        0,
        0,
        static_cast<LONG>(app.width * app.zoom),
        static_cast<LONG>(app.height * app.zoom),
    };
    if (window_preferences.has_window_rect)
    {
        auto wnd_rect = window_preferences.window_rect;
        app.width = wnd_rect.z - wnd_rect.x;
        app.height = wnd_rect.w - wnd_rect.y;
        startup.client_rect = { wnd_rect.x, wnd_rect.y, wnd_rect.z, wnd_rect.w };
        startup.client_pos = { wnd_rect.x, wnd_rect.y };
    }
    else
    {
        AdjustWindowRect(&startup.client_rect, startup.window_style, false);
    }

    return startup;
}

void create_main_window(const MainWindowStartupState& startup, HWND& hWnd, HINSTANCE hInst, const wchar_t* window_title)
{
    const int window_width = startup.client_rect.right - startup.client_rect.left;
    const int window_height = startup.client_rect.bottom - startup.client_rect.top;
    hWnd = CreateWindow(
        startup.window_class.lpszClassName,
        window_title,
        startup.window_style,
        startup.client_pos.x,
        startup.client_pos.y,
        window_width,
        window_height,
        0,
        0,
        hInst,
        0);
}

void initialize_pixel_format_descriptor(PIXELFORMATDESCRIPTOR& pixel_format)
{
    pixel_format.nSize = sizeof(pixel_format);
    pixel_format.nVersion = 1;
    pixel_format.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
    pixel_format.iPixelType = PFD_TYPE_RGBA;
    pixel_format.cColorBits = 32;
    pixel_format.cDepthBits = 24;
    pixel_format.iLayerType = PFD_MAIN_PLANE;
}

bool load_glad_entry_points(HDC device_context)
{
    if (!gladLoadGL())
    {
        LOG("gladLoadGL() failed");
        return false;
    }
    if (!gladLoadWGL(device_context))
    {
        LOG("gladLoadWGL() failed");
        return false;
    }

    return true;
}

pp::renderer::gl::OpenGlRuntimeInfo log_runtime_info()
{
    auto runtime_info = pp::renderer::gl::OpenGlRuntimeInfo {};
    const auto runtime_info_result = pp::renderer::gl::query_opengl_runtime_info(
        pp::legacy::gl_runtime::runtime_info_dispatch());
    if (runtime_info_result.ok())
    {
        runtime_info = runtime_info_result.value();
        LOG("GL version: %s", runtime_info.version);
        LOG("GL vendor: %s", runtime_info.vendor);
        LOG("GL renderer: %s", runtime_info.renderer);
    }
    else
    {
        LOG("OpenGL runtime info query failed: %s", runtime_info_result.status().message);
    }

    return runtime_info;
}

bool upgrade_to_core_gl_context(const MainWindowStartupState& startup, HWND& hWnd, HINSTANCE hInst, const wchar_t* window_title, OpenGlWindowContext& context)
{
    if (!GLAD_WGL_ARB_create_context)
    {
        LOG("WGL_ARB_create_context not supported");
        // If not supported, go fuck yourself we are not gonna support your shitty device
        return false;
    }

    const auto wgl_config = pp::renderer::gl::windows_wgl_core_context_3_3_config();
    UINT num_format = 0;

    wglMakeCurrent(NULL, NULL);
    wglDeleteContext(context.render_context);
    DestroyWindow(hWnd);

    create_main_window(startup, hWnd, hInst, window_title);

    context.device_context = GetDC(hWnd);
    int pixel_format = 0;
    wglChoosePixelFormatARB(
        context.device_context,
        wgl_config.pixel_format_attributes.data(),
        nullptr,
        1,
        &pixel_format,
        &num_format);
    SetPixelFormat(context.device_context, pixel_format, &startup.pixel_format);
    context.render_context = wglCreateContextAttribsARB(
        context.device_context,
        NULL,
        wgl_config.context_attributes.data());
    wglMakeCurrent(context.device_context, context.render_context);
    set_async_render_context(context.device_context, context.render_context);
    return true;
}

MainStartupResult initialize_main_window_and_gl(MainWindowStartupState& startup, HWND& hWnd, HINSTANCE hInst, wchar_t* window_title, OpenGlWindowContext& context)
{
    create_main_window(startup, hWnd, hInst, L"PanoPainter");
    initialize_pixel_format_descriptor(startup.pixel_format);

    context.device_context = GetDC(hWnd);
    const int pixel_format = ChoosePixelFormat(context.device_context, &startup.pixel_format);
    SetPixelFormat(context.device_context, pixel_format, &startup.pixel_format);
    context.render_context = wglCreateContext(context.device_context);
    wglMakeCurrent(context.device_context, context.render_context);
    set_async_render_context(context.device_context, context.render_context);

    if (!load_glad_entry_points(context.device_context))
        return MainStartupResult::GladLoadFailure;

    context.runtime_info = log_runtime_info();

#ifdef USE_RENDERDOC
    if (!win32_renderdoc_init())
        LOG("Renderdoc not started");
#endif

    const auto renderer_name = std::string(context.runtime_info.renderer != nullptr ? context.runtime_info.renderer : "");
    swprintf_s(
        window_title,
        512,
        L"PanoPainter %s (%s)",
        g_version_number_w,
        str2wstr(renderer_name).c_str());

    if (!upgrade_to_core_gl_context(startup, hWnd, hInst, window_title, context))
        return MainStartupResult::MissingCoreContextSupport;

    return MainStartupResult::Ok;
}

BOOL UnadjustWindowRectEx(LPRECT prc, DWORD dwStyle, BOOL fMenu, DWORD dwExStyle)
{
    RECT rc;
    SetRectEmpty(&rc);
    BOOL fRc = AdjustWindowRectEx(&rc, dwStyle, fMenu, dwExStyle);
    if (fRc) {
        prc->left -= rc.left;
        prc->top -= rc.top;
        prc->right -= rc.right;
        prc->bottom -= rc.bottom;
    }
    return fRc;
}

void _pre_call_callback(const char* name, void* funcptr, int len_args, ...)
{
    assert(bound_app()->is_render_thread());
}

void _post_call_callback(const char* name, void* funcptr, int len_args, ...)
{
    GLenum error_code;
    error_code = glad_glGetError();

    if (error_code != pp::renderer::gl::no_error_code())
    {
        LOG("ERROR %d in %s\n", error_code, name);
    }
}

}
