#include "pch.h"
#include "log.h"
#include "shader.h"
#include "shape.h"
#include "texture.h"
#include "image.h"
#include "app.h"
#include "canvas.h"
#include "keymap.h"
#include "hmd.h"
#include "legacy_gl_runtime_dispatch.h"
#include "legacy_preference_storage.h"
#include "renderer_gl/opengl_capabilities.h"
#include "platform_windows/windows_platform_services.h"
#include "../resource.h"

#include <shellscalingapi.h>
#include <WbemCli.h>
#include <deque>
#include <chrono>

#include "wacom.h"
#include "abr.h"

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

#include <iomanip>
#include <ctime>
#include <sstream>

#define WM_USER_CLOSE (WM_USER + 1)
#define WM_USER_WAKEUP (WM_USER + 2)

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);
HINSTANCE hInst;
HWND      hWnd;
HDC       hDC;
HGLRC     hRC;
const wchar_t*  className;
bool      keys[256];
std::mutex gl_mutex;
std::mutex async_mutex;
std::thread::id gl_thread;
std::map<kKey, int> vkey_map;
static wchar_t window_title[512];

std::thread hmd_renderer;
int vr_frames = 0;
int running = -1;
int vr_running = 0;

int gl_count = 0;
std::deque<std::packaged_task<void()>> main_tasklist;
std::mutex main_task_mutex;
float timer_stylus = 0;
float timer_ink_touch = 0;
float timer_ink_pen = 0;
bool sandboxed = false;

#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");
}

BOOL(*GetPointerInfo_fn)(UINT32 pointerId, POINTER_INFO* pointerInfo);
BOOL(*GetPointerType_fn)(UINT32 pointerId, POINTER_INPUT_TYPE* pointerType);
BOOL(*GetPointerTouchInfo_fn)(UINT32 pointerId, POINTER_TOUCH_INFO* touchInfo);
BOOL(*GetPointerPenInfo_fn)(UINT32 pointerId, POINTER_PEN_INFO* penInfo);
void init_ink_API()
{
    HMODULE dll = LoadLibrary(L"User32.dll");
    if (!dll)
    {
        LOG("cannot load User32.dll");
        return;
    }
    LOG("loaded User32.dll");
    GetPointerInfo_fn = (decltype(GetPointerInfo_fn))GetProcAddress(dll, "GetPointerInfo");
    GetPointerType_fn = (decltype(GetPointerType_fn))GetProcAddress(dll, "GetPointerType");
    GetPointerTouchInfo_fn = (decltype(GetPointerTouchInfo_fn))GetProcAddress(dll, "GetPointerTouchInfo");
    GetPointerPenInfo_fn = (decltype(GetPointerPenInfo_fn))GetProcAddress(dll, "GetPointerPenInfo");
}

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

    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);

    //Free the buffer.
    LocalFree(messageBuffer);

    return message;
}

void destroy_window()
{
    std::lock_guard<std::mutex> lock(main_task_mutex);
    main_tasklist.emplace_back([=] {
        PostMessage(hWnd, WM_USER_CLOSE, 0, 0);
    });
}

void async_lock()
{
    //std::lock_guard<std::mutex> _lock(async_mutex);
    if (gl_count == 0 || gl_thread != std::this_thread::get_id())
    {
        gl_mutex.lock();
        bool ret = wglMakeCurrent(hDC, hRC);
        if (ret == false)
            LOG("FAILED wglMakeCurrent: %s", GetLastErrorAsString().c_str());
        gl_thread = std::this_thread::get_id();
        //LOG("lock");
    }
    gl_count++;
    //assert(ret == true);
}

bool async_lock_try()
{
    //std::lock_guard<std::mutex> _lock(async_mutex);
    if (gl_count == 0 || gl_thread != std::this_thread::get_id())
    {
        if (!gl_mutex.try_lock())
            return false;
        bool ret = wglMakeCurrent(hDC, hRC);
        if (ret == false)
            LOG("FAILED wglMakeCurrent: %s", GetLastErrorAsString().c_str());
        gl_thread = std::this_thread::get_id();
        //LOG("lock");
    }
    gl_count++;
    //assert(ret == true);
    return true;
}

void win32_async_swap()
{
    SwapBuffers(hDC);
    //LOG("swap");
}

void async_unlock()
{
    //std::lock_guard<std::mutex> _lock(async_mutex);
    gl_count--;
    if (gl_count == 0)
    {
        //LOG("unlock");
        wglMakeCurrent(0, 0);
        gl_mutex.unlock();
    }
}

void win32_update_stylus(float dt)
{
    timer_stylus += dt;
    timer_ink_touch += dt;
    timer_ink_pen += dt;

    if (timer_stylus > 0.1 && (WacomTablet::I.m_stylus || WacomTablet::I.m_eraser))
    {
        WacomTablet::I.m_stylus = false;
        WacomTablet::I.m_eraser = false;
        App::I->redraw = true;
    }
    if (timer_ink_pen > 0.1 && WacomTablet::I.m_ink_pen)
    {
        WacomTablet::I.m_ink_pen = false;
        App::I->redraw = true;
    }
    if (timer_ink_touch > 0.1 && WacomTablet::I.m_ink_touch)
    {
        WacomTablet::I.m_ink_touch = false;
        App::I->redraw = true;
    }
}

void win32_update_fps(int frames)
{
    static wchar_t title_fps[512];
    if (App::I->vr_active)
        swprintf_s(title_fps, L"%s - %d fps - %d vr fps", window_title, frames, vr_frames);
    else
        swprintf_s(title_fps, L"%s - %d fps", window_title, frames);

    {
        std::lock_guard<std::mutex> lock(main_task_mutex);
        main_tasklist.emplace_back([] {
            SetWindowText(hWnd, title_fps);
        });
    }
    PostMessage(hWnd, WM_USER_WAKEUP, 0, 0);
}

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");
                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;
}

INT_PTR g_iLogHandle = -1;
static void SetupExceptionHandler()
{
    // 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
//     g_iLogHandle = BT_OpenLogFile(NULL, BTLF_TEXT);
//     BT_SetLogSizeInEntries(g_iLogHandle, 100);
//     BT_SetLogFlags(g_iLogHandle, BTLF_SHOWTIMESTAMP);
//     BT_SetLogEchoMode(g_iLogHandle, BTLE_STDERR | BTLE_DBGOUT);
// 
//     PCTSTR pszLogFileName = BT_GetLogFileName(g_iLogHandle);
    TCHAR wpath[MAX_PATH];
    //GetFullPathNameW(L"panopainter-log.txt", 1024, wpath, nullptr);
    auto log_file = App::I->data_path + "/panopainter-log.txt";
    std::mbstowcs(wpath, log_file.c_str(), log_file.size());
    BT_AddLogFile(wpath);

    BT_SetPreErrHandler([](INT_PTR){
        if (Canvas::I && Canvas::I->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::I->data_path + "/" + App::I->doc_name + "-recovery (" + oss.str() + ").ppi";
            Canvas::I->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(hWnd, message, "File Recovery", MB_OK | MB_ICONWARNING);
        }
        LogRemote::I.file_close();
    }, 0);
}

// create a reverse map from kKey to VK_XXX
void init_vk_map()
{
    for (int k = 1; k < 256; k++) // ignore kKey::Unknown = 0
    {
        for (int vk = 0; vk < 256; vk++)
        {
            if (k == (int)convert_key(vk))
            {
                if (vkey_map.find((kKey)k) == vkey_map.end())
                {
                    vkey_map.insert({ (kKey)k, vk });
                }
                else
                {
                    LOG("KEY MAP COLLISION %d and %d maps to %d", 
                        (int)vk, (int)vkey_map[(kKey)k], (int)k);
                }
            }
        }
    }
}

std::mutex hmd_render_mutex;
std::condition_variable hmd_render_cv;
Vive* vive = nullptr;
bool win32_vr_start()
{
    if (sandboxed)
        return false;

    vive = new Vive;
    vive->on_draw = [](const glm::mat4& proj, const glm::mat4& view, const glm::mat4& pose) { App::I->vr_draw(proj, view, pose); };
    if (!vive->Initialize())
    {
        delete vive;
        vive = nullptr;
        LOG("VR: failed to initialize vive");
        return false;
    }

    if (hmd_renderer.joinable())
        hmd_renderer.join();
    hmd_renderer = std::thread([&] {
        if (!vive)
            return;

        BT_SetTerminate();
        LOG("start hmd render thread");
        App::I->has_vr = true;
        vr_running = true;

        vive->on_analog_button = std::bind(&App::vr_analog, App::I, std::placeholders::_1,
            std::placeholders::_2, std::placeholders::_3, std::placeholders::_4);
        vive->on_button = std::bind(&App::vr_digital, App::I, std::placeholders::_1,
            std::placeholders::_2, std::placeholders::_3, std::placeholders::_4);

        App::I->render_task([] {
            App::I->vr_draw_ui();
        });

        const float target_tick_rate = 90;
        auto t0 = GetTickCount64();
        float one_sec_timer = 0;
        int frames = 0;
        while (vr_running && running == 1 && vive->Valid())
        {
            std::unique_lock<std::mutex> lock(hmd_render_mutex);
            auto t1 = GetTickCount64();
            float dt = (float)(t1 - t0) / 1000.0f;

            one_sec_timer += dt;
            if (one_sec_timer >= 1.f)
            {
                one_sec_timer = 0;
                vr_frames = frames;
                frames = 0;
            }
            frames++;

            vive->Update();
            App::I->vr_active = vive->m_active;
            App::I->vr_controllers[0] = vive->m_controllers[0];
            App::I->vr_head = vive->m_pose;
            App::I->vr_update(dt);

            if (vr_running && vive->m_active)
            {
                App::I->render_task([] {
                    vive->Draw();
                });
            }

            const int framerate = (1.f / target_tick_rate) * 1000;
            const int diff = framerate - (t1 - t0);
            //hmd_render_cv.wait_for(lock, std::chrono::milliseconds(diff));
            t0 = t1;
        }
        App::I->vr_active = false;
        App::I->has_vr = false;
        vr_running = false;
        vive->Terminate();
        LOG("hmd renderer terminated");
    });
    return true;
}

void win32_vr_stop()
{
    if (vive)
    {
        vr_running = false;
        if (hmd_renderer.joinable())
            hmd_renderer.join();
        vive->Terminate();
        delete vive;
        vive = nullptr;
    }
}

void win32_save_window_state()
{
    WINDOWPLACEMENT p;
    GetWindowPlacement(hWnd, &p);
    pp::panopainter::set_legacy_window_preferences(p.showCmd, {
        p.rcNormalPosition.left,
        p.rcNormalPosition.top,
        p.rcNormalPosition.right,
        p.rcNormalPosition.bottom });
}

HBITMAP image_to_hbitmap(const Image& img)
{
    BITMAPINFOHEADER bmih;
    memset(&bmih, 0, sizeof(BITMAPINFOHEADER));
    bmih.biWidth = img.width;
    bmih.biHeight = img.height;
    bmih.biBitCount = 32;
    bmih.biCompression = BI_RGB;
    bmih.biSize = sizeof(BITMAPINFOHEADER);
    bmih.biPlanes = 1;
    BITMAPINFO* bmi = (BITMAPINFO*)&bmih;
    return CreateDIBitmap(GetDC(NULL), &bmih, CBM_INIT, img.data(), bmi, DIB_RGB_COLORS);
}

LRESULT CALLBACK splash_proc(HWND hWndDlg, UINT Msg, WPARAM wParam, LPARAM lParam)
{
    switch (Msg)
    {
    case WM_INITDIALOG:
    {
        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);
        float z = (float)x / 96.f;

        static char base_path[MAX_PATH];
        GetCurrentDirectoryA(MAX_PATH, base_path);
        std::string path = std::string(base_path) + "\\data\\splash.jpg";
        //auto hbitmap = (HBITMAP)LoadImageA(NULL, path.c_str(), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
        Image img;
        img.load_file(path);
        img.flip();
        auto hbitmap = image_to_hbitmap(img.resize(512 * z, 200 * z));
        SendMessage(GetDlgItem(hWndDlg, IDC_STATIC_IMAGE), STM_SETIMAGE, (WPARAM)IMAGE_BITMAP, (LPARAM)hbitmap);
        SetDlgItemText(hWndDlg, IDC_STATIC_VERSION, g_version_number_w);

        RECT r;
        GetClientRect(hWndDlg, &r);
        MoveWindow(GetDlgItem(hWndDlg, IDC_STATIC_IMAGE), 0, 0, 512 * z, 200 * z, TRUE);
        SetWindowPos(hWndDlg, HWND_TOP, 0, 0, 512 * z, 200 * z, SWP_NOMOVE);

        HWND hWndVersion = GetDlgItem(hWndDlg, IDC_STATIC_VERSION);
        RECT rv;
        GetClientRect(hWndVersion, &rv);
        MoveWindow(hWndVersion, 0, 200 * z - (rv.bottom - rv.top), r.right - r.left, rv.bottom - rv.top, TRUE);

        return TRUE;
    }
    case WM_USER_CLOSE:
        PostQuitMessage(0);
        return TRUE;
    }

    return DefWindowProc(hWndDlg, Msg, wParam, lParam);;
}

HWND splash_dialog;
void splash_thread_loop()
{
    BT_SetTerminate();
    splash_dialog = CreateDialog(hInst, MAKEINTRESOURCE(IDD_SPLASH), NULL, reinterpret_cast<DLGPROC>(splash_proc));

    MSG msg;
    while (GetMessage(&msg, 0, 0, 0) > 0)
    {
        if (IsDialogMessage(splash_dialog, &msg))
        {
            DispatchMessage(&msg);
            TranslateMessage(&msg);
        }
    }

    DestroyWindow(splash_dialog);
}

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(App::I->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);
    }
}

int main(int argc, char** argv)
{
    WNDCLASS wc;
    PIXELFORMATDESCRIPTOR pfd;

    App::I = new App();
    App::I->set_platform_services(&pp::platform::windows::platform_services());
    App::I->initLog();
    
    init_shcore_API();
    init_ink_API();

    if(SetProcessDpiAwareness_fn)
        SetProcessDpiAwareness_fn(PROCESS_PER_MONITOR_DPI_AWARE);


    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);
    }
    else
    {
        fclose(fp_check);
        LOG("data files ok");
    }

    std::thread dialog_thread(splash_thread_loop);

    init_vk_map();

    SetupExceptionHandler();
    BT_SetTerminate();

    read_WMI_info();

    App::I->create();

    // Inizialize data structures to zero
    memset(&wc, 0, sizeof(wc));
    memset(&keys, 0, sizeof(keys));
    memset(&pfd, 0, sizeof(pfd));

    // Create the main window

    hInst = GetModuleHandle(NULL);
    className = L"EngineMain";

    wc.hInstance = hInst;
    wc.lpfnWndProc = (WNDPROC)WndProc;
    wc.lpszClassName = className;
    wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);

    RegisterClass(&wc);

    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::I->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::I->zoom = window_preferences.ui_scale;
    else
        App::I->zoom = (float)x / 96.f;

    int show_cmd = window_preferences.show_command;
    DWORD wnd_style = WS_OVERLAPPEDWINDOW;
    //if (show_cmd == SW_MAXIMIZE)
    //    wnd_style != WS_MAXIMIZE;

    RECT clientRect = { 0, 0, (int)App::I->width * App::I->zoom, (int)App::I->height * App::I->zoom };
    POINT clientPos = { CW_USEDEFAULT, CW_USEDEFAULT };
    if (window_preferences.has_window_rect)
    {
        auto wnd_rect = window_preferences.window_rect;
        App::I->width = wnd_rect.z - wnd_rect.x;
        App::I->height = wnd_rect.w - wnd_rect.y;
        clientRect = { wnd_rect.x, wnd_rect.y, wnd_rect.z, wnd_rect.w };
        clientPos = { wnd_rect.x, wnd_rect.y };
    }
    else
    {
        AdjustWindowRect(&clientRect, wnd_style, false);
    }
    hWnd = CreateWindow(wc.lpszClassName, L"PanoPainter", wnd_style, clientPos.x, clientPos.y,
        (float)(clientRect.right - clientRect.left),
        (float)(clientRect.bottom - clientRect.top), 0, 0, hInst, 0);

    // Setup GL Rendering Context
    pfd.nSize = sizeof(pfd);
    pfd.nVersion = 1;
    pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
    pfd.iPixelType = PFD_TYPE_RGBA;
    pfd.cColorBits = 32;
    pfd.cDepthBits = 24;
    pfd.iLayerType = PFD_MAIN_PLANE;

    hDC = GetDC(hWnd);
    int pxfmt = ChoosePixelFormat(hDC, &pfd);
    SetPixelFormat(hDC, pxfmt, &pfd);
    hRC = wglCreateContext(hDC); // Create OpenGL 2.1 or less
    wglMakeCurrent(hDC, hRC);

    // Initialize extensions
    if (!gladLoadGL())
    {
        LOG("gladLoadGL() failed");
        return 0;
    }
    if (!gladLoadWGL(hDC))
    {
        LOG("gladLoadWGL() failed");
        return 0;
    }

    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);
    }

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

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

    // If supported create a 3.3 context
    if (GLAD_WGL_ARB_create_context)
    {
        const auto wgl_config = pp::renderer::gl::windows_wgl_core_context_3_3_config();
        UINT numFormat;

        wglMakeCurrent(NULL, NULL);
        wglDeleteContext(hRC);
        DestroyWindow(hWnd);

        hWnd = CreateWindow(wc.lpszClassName, window_title, wnd_style, clientPos.x, clientPos.y,
            (float)(clientRect.right - clientRect.left),
            (float)(clientRect.bottom - clientRect.top), 0, 0, hInst, 0);

        hDC = GetDC(hWnd);
        wglChoosePixelFormatARB(hDC, wgl_config.pixel_format_attributes.data(), nullptr, 1, &pxfmt, &numFormat);
        SetPixelFormat(hDC, pxfmt, &pfd);
        hRC = wglCreateContextAttribsARB(hDC, NULL, wgl_config.context_attributes.data());
        wglMakeCurrent(hDC, hRC);
    }
    else
    {
        LOG("WGL_ARB_create_context not supported");
        // If not supported, go fuck yourself we are not gonna support your shitty device
        return -1; // A negative number because you are a negative one
    }

    //wglSwapIntervalEXT(1);

    bool start_in_vr = false;
    if (argc > 1)
    {
        switch (const_hash(argv[1]))
        {
        case const_hash("convert"):
            App::I->initShaders();
            App::I->cmd_convert(argv[2], argv[3]);
            return 0;
        case const_hash("-vrmode"):
            start_in_vr = true;
            break;
        default:
            break;
        }
    }

    // link: https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-registertouchwindow
    if (RegisterTouchWindow(hWnd, 0) == 0)
    {
        LOG("RegisterTouchWindow error: %s", GetLastErrorAsString().c_str());
    }

    wglMakeCurrent(NULL, NULL);

    running = 1;
    App::I->render_thread_start();
    App::I->ui_thread_start();

#ifdef _DEBUG
    glad_set_pre_callback(_pre_call_callback);
    glad_set_post_callback(_post_call_callback);
#endif

    if (!sandboxed)
    {
        LOG("init WinTab");
        WacomTablet::I.init(hWnd);
    }
    else
    {
        LOG("SKIP init WinTab");
    }

    LOG("change icon");
    SendMessage(hWnd, WM_SETICON, ICON_SMALL,
        (LPARAM)LoadIcon(GetModuleHandle(0), MAKEINTRESOURCE(IDI_ICON1)));

    App::I->ui_sync();

    {
        WINDOWPLACEMENT wp;
        GetWindowPlacement(hWnd, &wp);
        wp.showCmd = show_cmd;
        SetWindowPlacement(hWnd, &wp);
        //GetClientRect(hWnd, &clientRect);
        //App::I->width = clientRect.right - clientRect.left;
        //App::I->height = clientRect.bottom - clientRect.top;
    }

    if (start_in_vr)
        App::I->vr_start();

    LOG("show main window");
    SetForegroundWindow(hWnd);
    //ShowWindow(hWnd, show_cmd);

    SendMessage(splash_dialog, WM_USER_CLOSE, 0, 0);
    if (dialog_thread.joinable())
        dialog_thread.join();

    MSG msg;
    LOG("start main loop");
    while (running == 1)
    {
        // If there any message in the queue process it
        auto present = App::I->animate || App::I->redraw ?
            PeekMessage(&msg, 0, 0, 0, PM_REMOVE) : GetMessage(&msg, 0, 0, 0);
        
        if (msg.message == WM_QUIT)
            running = 0;

        if (present)
        {
            DispatchMessage(&msg);
            TranslateMessage(&msg);
        }

        // list of tasks for the main thread
        {
            std::deque<std::packaged_task<void()>> working_list;
            {
                std::lock_guard<std::mutex> lock(main_task_mutex);
                working_list = std::move(main_tasklist);
            }

            if (!working_list.empty())
            {
                while (!working_list.empty())
                {
                    working_list.front()();
                    working_list.pop_front();
                }
            }
        }
    }
    // Clean up
    WacomTablet::I.terminate();

    UnregisterClass(className, hInst);
    LogRemote::I.stop();
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
    static bool leftDown = false;
    static DWORD lastTime;
    static glm::vec2 lastPoint;

    auto extra = GetMessageExtraInfo();
//     if ((extra & 0xFFFFFF00) == 0xFF515700)
//         LOG("source %s", extra & 0x80 ? "touch" : "pen");
    
    switch (msg)
    {
    case WM_USER_CLOSE:
        running = 0;
        if (hmd_renderer.joinable())
            hmd_renderer.join();
        App::I->ui_thread_stop();
        App::I->render_thread_stop();
        App::I->terminate();
        delete App::I;
        PostQuitMessage(0);
        return 0;
    case WM_PAINT:
        App::I->redraw = true;
        break;
    case WM_CREATE:
        BT_SetTerminate();
        break;
    case WM_CLOSE:
    {
        App::I->ui_task_async([] {
            if (App::I->request_close())
            {
                destroy_window();
            }
        });
        return 1;
        break;
    }
    case WM_SIZE:
    {
        auto w = (float)LOWORD(lp);
        auto h = (float)HIWORD(lp);
        if (h != 0 && running == 1)
        {
            App::I->ui_task_async([=]
            {
                App::I->resize(w, h);
                App::I->redraw = true;
            }, true);
        }
        break;
    }
    case WM_ACTIVATE:
    {
        pp::platform::windows::platform_services().set_cursor_visible(true);
        App::I->ui_task_async([=] {
            int active = GET_WM_ACTIVATE_STATE(wp, lp);
            WacomTablet::I.set_focus(active);
            static BYTE keys[256];
            if (GetKeyboardState(keys))
            {
                bool alt = keys[VK_MENU] & 0x80;
                for (auto k : vkey_map)
                {
                    // ignore alt + tab
                    if (alt && k.first == kKey::KeyTab)
                        continue;
                    bool down = keys[k.second] & 0x80;
                    if (App::I->keys[(int)k.first] && !down)
                        App::I->key_up(k.first);
                    else if(!App::I->keys[(int)k.first] && down)
                        App::I->key_down(k.first);
                }
            }
        });
        break;
    }
//     case WM_TOUCH:
//     {
//         App::I->ui_task_async([=] {
//             //LOG("touch");
//         });
//         break;
//     }
    case WT_PACKET:
    {
        App::I->set_stylus();
        timer_stylus = 0;
        App::I->ui_task_async([=] {
            WacomTablet::I.handle_message(hWnd, msg, wp, lp);
        });
        break;
    }
    case WM_SYSKEYDOWN:
    case WM_KEYDOWN:
        if ((lp >> 30 & 1) == 0 &&  // ignore repeated
            !(wp == VK_TAB && App::I->keys[(int)kKey::KeyAlt])) // ignore alt+tab
        {
            App::I->ui_task_async([wp] {
                App::I->key_down(convert_key((int)wp));
            });
        }
        break;
    case WM_SYSKEYUP:
    case WM_KEYUP:
        if (!(wp == VK_TAB && App::I->keys[(int)kKey::KeyAlt])) // ignore alt+tab
        {
            App::I->ui_task_async([wp] {
                App::I->key_up(convert_key((int)wp));
            });
        }
        break;
    case WM_CHAR:
        {
            App::I->ui_task_async([wp]{
                App::I->key_char((int)wp);
            });
        }
        break;
    case WM_MOUSEMOVE:
        {
/*
            RECT r;
            POINT curpos;
            GetWindowRect(hWnd, &r);
            glm::vec2 center = { r.left + (r.right - r.left) / 2, r.top + (r.bottom - r.top) / 2 };
            GetCursorPos(&curpos);
            SetCursorPos(center.x, center.y);

            //glm::vec2 cur = { GET_X_LPARAM(lp), GET_Y_LPARAM(lp) };
            glm::vec2 sz = { App::I->width, App::I->height };
            glm::vec2 diff = { curpos.x - center.x, curpos.y - center.y };
            lastPoint = glm::clamp(lastPoint + diff, { 0, 0 }, sz);

*/
            lastPoint = { GET_X_LPARAM(lp), GET_Y_LPARAM(lp) };

            auto pt = lastPoint;
            App::I->ui_task_async([pt, extra, p = WacomTablet::I.get_pressure()]{
                kEventSource pointer_source;
                if (WacomTablet::I.m_ink_pen || WacomTablet::I.m_ink_touch)
                {
                    pointer_source = WacomTablet::I.m_ink_pen ? kEventSource::Stylus : kEventSource::Touch;
                }
                else
                {
                    pointer_source = WacomTablet::I.m_stylus ? kEventSource::Stylus : kEventSource::Mouse;
                    if ((extra & 0xFFFFFF00) == 0xFF515700)
                        pointer_source = kEventSource::Touch;
                }
                App::I->mouse_move((float)pt.x, (float)pt.y, p, pointer_source, WacomTablet::I.m_eraser);
            });
        }
        break;
    case WM_LBUTTONDOWN:
        {
            SetCapture(hWnd);
            auto pt = lastPoint;
            App::I->ui_task_async([pt, extra, hWnd, p = WacomTablet::I.get_pressure()]{
                kEventSource pointer_source;
                if (WacomTablet::I.m_ink_pen || WacomTablet::I.m_ink_touch)
                {
                    pointer_source = WacomTablet::I.m_ink_pen ? kEventSource::Stylus : kEventSource::Touch;
                }
                else
                {
                    pointer_source = WacomTablet::I.m_stylus ? kEventSource::Stylus : kEventSource::Mouse;
                    if ((extra & 0xFFFFFF00) == 0xFF515700)
                        pointer_source = kEventSource::Touch;
                }
                App::I->mouse_down(0, (float)pt.x, (float)pt.y, p, pointer_source, WacomTablet::I.m_eraser);
            });
        }
        break;
    case WM_LBUTTONUP:
        {
            ReleaseCapture();
            auto pt = lastPoint;
            App::I->ui_task_async([pt, extra] {
                WacomTablet::I.reset_pressure();
                kEventSource pointer_source;
                if (WacomTablet::I.m_ink_pen || WacomTablet::I.m_ink_touch)
                {
                    pointer_source = WacomTablet::I.m_ink_pen ? kEventSource::Stylus : kEventSource::Touch;
                }
                else
                {
                    pointer_source = WacomTablet::I.m_stylus ? kEventSource::Stylus : kEventSource::Mouse;
                    if ((extra & 0xFFFFFF00) == 0xFF515700)
                        pointer_source = kEventSource::Touch;
                }
                App::I->mouse_up(0, (float)pt.x, (float)pt.y, pointer_source, WacomTablet::I.m_eraser);
            });
        }
        break;
    case WM_RBUTTONDOWN:
        {
            SetCapture(hWnd);
            auto pt = lastPoint;
            App::I->ui_task_async([pt, extra, hWnd] {
                kEventSource pointer_source;
                if (WacomTablet::I.m_ink_pen || WacomTablet::I.m_ink_touch)
                {
                    pointer_source = WacomTablet::I.m_ink_pen ? kEventSource::Stylus : kEventSource::Touch;
                }
                else
                {
                    pointer_source = WacomTablet::I.m_stylus ? kEventSource::Stylus : kEventSource::Mouse;
                    if ((extra & 0xFFFFFF00) == 0xFF515700)
                        pointer_source = kEventSource::Touch;
                }
                App::I->mouse_down(1, (float)pt.x, (float)pt.y, 1.f, pointer_source, 0);
            });
        }
        break;
    case WM_RBUTTONUP:
        {
            ReleaseCapture();
            auto pt = lastPoint;
            App::I->ui_task_async([pt, extra] {
                kEventSource pointer_source;
                if (WacomTablet::I.m_ink_pen || WacomTablet::I.m_ink_touch)
                {
                    pointer_source = WacomTablet::I.m_ink_pen ? kEventSource::Stylus : kEventSource::Touch;
                }
                else
                {
                    pointer_source = WacomTablet::I.m_stylus ? kEventSource::Stylus : kEventSource::Mouse;
                    if ((extra & 0xFFFFFF00) == 0xFF515700)
                        pointer_source = kEventSource::Touch;
                }
                App::I->mouse_up(1, (float)pt.x, (float)pt.y, pointer_source, 0);
            });
        }
        break;
    case WM_MOUSEWHEEL:
    {
        POINT pt;
        pt.x = GET_X_LPARAM(lp);
        pt.y = GET_Y_LPARAM(lp);
        ScreenToClient(hWnd, &pt);
        {
            App::I->ui_task_async([pt, wp] {
                App::I->mouse_scroll((float)pt.x, (float)pt.y,
                    (float)GET_WHEEL_DELTA_WPARAM(wp) / (float)WHEEL_DELTA);
            });
        }
        break;
    }
    case WM_POINTERUPDATE:
    {
        POINTER_TOUCH_INFO   touchInfo;
        POINTER_PEN_INFO     penInfo;
        POINTER_INFO pointerInfo;
        UINT32       pointerId = GET_POINTERID_WPARAM(wp);
        POINTER_INPUT_TYPE pointerType = PT_POINTER;

        if(!GetPointerInfo_fn)
            break;

        // Retrieve common pointer information
        if (!GetPointerInfo_fn(pointerId, &pointerInfo))
        {
            // failure, call GetLastError()
        }
        else
        {
            // success, process pointerInfo
            if (!GetPointerType_fn(pointerId, &pointerType))
            {
                // failure, call GetLastError()
                // set PT_POINTER to fall to default case below
                pointerType = PT_POINTER;
            }
            switch (pointerType)
            {
            case PT_TOUCH:
                // Retrieve touch information
                if (!GetPointerTouchInfo_fn(pointerId, &touchInfo))
                {
                    // failure, call GetLastError()
                }
                else
                {
                    // success, process touchInfo
                    // mark as handled to skip call to DefWindowProc
                    //fHandled = TRUE;
                    timer_ink_touch = 0;
                    WacomTablet::I.m_ink_touch = true;
                    WacomTablet::I.m_pen_pres = 1;
                }
                break;
            case PT_PEN:
                // Retrieve pen information
                if (!GetPointerPenInfo_fn(pointerId, &penInfo))
                {
                    // failure, call GetLastError()
                }
                else
                {
                    // success, process penInfo
                    // mark as handled to skip call to DefWindowProc
                    //fHandled = TRUE;
                    //penInfo.pressure
                    timer_ink_pen = 0;
                    WacomTablet::I.m_ink_pen = true;
                    WacomTablet::I.m_pen_pres = (float)penInfo.pressure / 1024.f;
                    App::I->set_stylus();
                }
                break;
            default:
                if (!GetPointerInfo_fn(pointerId, &pointerInfo))
                {
                    // failure.
                }
                else
                {
                    // success, proceed with pointerInfo.
                    //fHandled = HandleGenericPointerInfo(&pointerInfo);
                }
                break;
            }
        }

        break;
    }
    }

    // avoid annoying alt system menu
    if (wp == SC_KEYMENU && (lp >> 16) <= 0) return 0;

    return DefWindowProc(hWnd, msg, wp, lp);
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    int argc = 0;

    // convert args from char to wchar
    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 main(argc, argv);
}
