#include "pch.h"
#include "platform_windows/windows_async_render_context.h"
#include "platform_windows/windows_bootstrap_helpers.h"
#include "platform_windows/windows_lifecycle_shell.h"
#include "platform_windows/windows_main_window_session.h"
#include "platform_windows/windows_platform_services.h"
#include "platform_windows/windows_runtime_shell.h"
#include "platform_windows/windows_runtime_state.h"
#include "keymap.h"
#include "platform_windows/windows_stylus_input.h"
#include "platform_windows/windows_window_shell.h"

#include "app_runtime.h"
#include "log.h"
#include "legacy_gl_runtime_dispatch.h"
#include "legacy_ui_gl_dispatch.h"
#include "platform_api/network_tls_policy.h"
#include "platform_api/platform_policy.h"
#include "renderer_gl/opengl_capabilities.h"

#include <deque>
#include <map>

void destroy_window();
void async_lock();
void async_unlock();
void win32_async_swap();
void win32_update_fps(int frames);
void win32_update_stylus(float dt);
void win32_save_window_state();
bool win32_vr_start();
void win32_vr_stop();
HWND pp_windows_main_window_handle();

HWND pp_windows_main_window_handle()
{
    return pp::platform::windows::retained_main_window_handle_ref();
}

void destroy_window()
{
    pp::platform::windows::enqueue_main_thread_task(std::packaged_task<void()>([hWnd = pp::platform::windows::retained_main_window_handle_ref()] {
        pp::platform::windows::request_window_close(hWnd);
    }));
}

void async_lock()
{
    pp::platform::windows::lock_async_render_context();
}

bool async_lock_try()
{
    return pp::platform::windows::try_lock_async_render_context();
}

void win32_async_swap()
{
    pp::platform::windows::swap_async_render_context();
}

void async_unlock()
{
    pp::platform::windows::unlock_async_render_context();
}

void win32_update_stylus(float dt)
{
    pp::platform::windows::update_stylus_frame(dt);
}

void win32_update_fps(int frames)
{
    pp::platform::windows::enqueue_main_thread_task(std::packaged_task<void()>([
        hWnd = pp::platform::windows::retained_main_window_handle_ref(),
        window_title = pp::platform::windows::retained_main_window_title(),
        vr = &pp::platform::windows::platform_vr_state(),
        frames] {
        pp::platform::windows::update_window_fps(hWnd, window_title, *vr, frames);
    }));
}

bool win32_vr_start()
{
    return pp::platform::windows::start_window_vr(
        pp::platform::windows::platform_vr_state(),
        pp::platform::windows::retained_main_window_sandboxed());
}

void win32_vr_stop()
{
    pp::platform::windows::stop_window_vr(pp::platform::windows::platform_vr_state());
}

void win32_save_window_state()
{
    pp::platform::windows::save_window_preferences(pp::platform::windows::retained_main_window_handle_ref());
}

namespace pp::platform::windows {

[[nodiscard]] std::map<kKey, int>& retained_virtual_key_map()
{
    static std::map<kKey, int> vkey_map;
    return vkey_map;
}

void initialize_retained_input_state()
{
    auto& vkey_map = retained_virtual_key_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))
                continue;

            auto key = (kKey)k;
            if (vkey_map.find(key) == vkey_map.end())
            {
                vkey_map.insert({ key, vk });
            }
            else
            {
                LOG("KEY MAP COLLISION %d and %d maps to %d",
                    vk, vkey_map[key], k);
            }
        }
    }
}

VrShellState& platform_vr_state() noexcept
{
    static VrShellState state;
    return state;
}

}

namespace pp::platform::windows {
namespace {
} // namespace

void enqueue_main_thread_task(std::packaged_task<void()> task)
{
    if (auto* runtime = retained_bound_runtime())
    {
        runtime->main_thread_task(std::move(task));
        return;
    }

    if (!task.valid())
        return;

    task();
}

void drain_main_thread_tasks()
{
    if (auto* runtime = retained_bound_runtime())
    {
        runtime->drain_main_thread_tasks();
        return;
    }
}
} // namespace pp::platform::windows

namespace {

static CONSOLE_SCREEN_BUFFER_INFO render_debug_console_info;

[[nodiscard]] GLenum debug_severity_notification() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::debug_severity_notification());
}

[[nodiscard]] GLenum debug_severity_low() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::debug_severity_low());
}

[[nodiscard]] GLenum debug_severity_medium() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::debug_severity_medium());
}

[[nodiscard]] GLenum debug_severity_high() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::debug_severity_high());
}

void handle_gl_callback(
    GLenum source,
    GLenum type,
    GLuint id,
    GLenum severity,
    GLsizei length,
    const GLchar* message,
    const void* userParam)
{
    (void)source;
    (void)type;
    (void)id;
    (void)userParam;

    static std::map<GLenum, WORD> colors = {
        { debug_severity_notification(), static_cast<WORD>(8) },
        { debug_severity_low(), static_cast<WORD>(8) },
        { debug_severity_medium(), static_cast<WORD>(FOREGROUND_GREEN | FOREGROUND_INTENSITY) },
        { debug_severity_high(), static_cast<WORD>(FOREGROUND_RED | FOREGROUND_INTENSITY) },
    };
    if (severity == debug_severity_high()
        || severity == debug_severity_medium()
        || severity == debug_severity_low())
    {
        SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colors[severity]);
        LOG("OPENGL: %.*s", length, message);
        FlushConsoleInputBuffer(GetStdHandle(STD_OUTPUT_HANDLE));
        SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), render_debug_console_info.wAttributes);
#ifdef _DEBUG
        if (severity == debug_severity_high())
            __debugbreak();
#endif
    }
}

void show_cursor(bool visible)
{
    pp::platform::windows::enqueue_main_thread_task(std::packaged_task<void()>([=] {
        if (visible)
            while (ShowCursor(true) < 0);
        else
            while (ShowCursor(false) >= 0);
    }));
}

std::string clipboard_text()
{
    std::string ret;
    if (OpenClipboard(pp_windows_main_window_handle()))
    {
        if (HANDLE h = GetClipboardData(CF_TEXT))
        {
            if (char* s = static_cast<char*>(GlobalLock(h)))
            {
                ret = s;
                GlobalUnlock(h);
            }
        }
        CloseClipboard();
    }
    return ret;
}

bool set_clipboard_text(const std::string& s)
{
    bool success = false;
    if (OpenClipboard(pp_windows_main_window_handle()))
    {
        // owned by SetClipboardData
        if (HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, s.size() + 1))
        {
            if (char* p = static_cast<char*>(GlobalLock(h)))
            {
                std::copy(s.begin(), s.end(), p);
                p[s.size()] = 0;
                GlobalUnlock(h);
                success = true;
            }
            EmptyClipboard();
            SetClipboardData(CF_TEXT, h);
        }
        CloseClipboard();
    }
    return success;
}

std::string open_file(const char* filter)
{
    OPENFILENAMEA ofn;
    char fileName[MAX_PATH] = "";
    ZeroMemory(&ofn, sizeof(ofn));
    ofn.lStructSize = sizeof(OPENFILENAME);
    ofn.hwndOwner = pp_windows_main_window_handle();
    ofn.lpstrFilter = filter;
    ofn.lpstrFile = fileName;
    ofn.nMaxFile = MAX_PATH;
    ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR;
    ofn.lpstrDefExt = "";
    ofn.lpstrInitialDir = "";
    if (GetOpenFileNameA(&ofn) != NULL)
        return fileName;
    return "";
}

std::string save_file(const char* filter)
{
    OPENFILENAMEA ofn;
    char fileName[MAX_PATH] = "";
    ZeroMemory(&ofn, sizeof(ofn));
    ofn.lStructSize = sizeof(OPENFILENAME);
    ofn.hwndOwner = pp_windows_main_window_handle();
    ofn.lpstrFilter = filter;
    ofn.lpstrFile = fileName;
    ofn.nMaxFile = MAX_PATH;
    ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR | OFN_OVERWRITEPROMPT;
    ofn.lpstrDefExt = "";
    ofn.lpstrInitialDir = "";
    if (GetSaveFileNameA(&ofn) != NULL)
        return fileName;
    return "";
}

std::string open_directory()
{
    BROWSEINFOA bi;
    char Buffer[MAX_PATH];
    ZeroMemory(Buffer, MAX_PATH);
    ZeroMemory(&bi, sizeof(bi));
    bi.hwndOwner = pp_windows_main_window_handle();
    bi.pszDisplayName = Buffer;
    bi.lpszTitle = "Title";
    bi.ulFlags = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNONLYFSDIRS | BIF_SHAREABLE;
    LPCITEMIDLIST pFolder = SHBrowseForFolderA(&bi);
    if (pFolder == NULL)
        return "";
    if (!SHGetPathFromIDListA(pFolder, Buffer))
        return "";
    return Buffer;
}

void invoke_selected_path(
    const std::string& path,
    const pp::platform::PickedPathCallback& callback)
{
    if (!path.empty())
        callback(path);
}

void ensure_directory(const std::string& path)
{
    if (!PathFileExistsA(path.c_str()))
        CreateDirectoryA(path.c_str(), NULL);
}

[[nodiscard]] pp::platform::XrRuntimeSelection select_windows_xr_runtime() noexcept
{
    // DEBT-0061: OpenXR is the target backend; Windows only exposes the retained OpenVR bridge today.
    return pp::platform::select_desktop_xr_runtime(
        false,
        true,
        true);
}

std::string build_supported_files_filter(const std::vector<std::string>& types)
{
    std::string filter = "Supported Files (";
    bool first_type = true;
    for (const auto& t : types)
    {
        filter.append(std::string(first_type ? "" : " ,") + "*." + t);
        first_type = false;
    }
    filter.append(")");
    filter.push_back(0);
    first_type = true;
    for (const auto& t : types)
    {
        filter.append(std::string(first_type ? "" : ";") + "*." + t);
        first_type = false;
    }
    filter.push_back(0);
    return filter;
}

class WindowsPlatformServices final : public pp::platform::PlatformServices {
public:
    [[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths() override
    {
        std::string data_path;
        CHAR my_documents[MAX_PATH];
        HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
        if (SUCCEEDED(result))
        {
            data_path = std::string(my_documents) + "\\PanoPainter";
            ensure_directory(data_path);
        }
        else
        {
            CHAR path[MAX_PATH];
            GetCurrentDirectoryA(sizeof(path), path);
            data_path = path;
        }

        ensure_directory(data_path + "\\frames");
        ensure_directory(data_path + "\\brushes");
        ensure_directory(data_path + "\\brushes\\thumbs");
        ensure_directory(data_path + "\\patterns");
        ensure_directory(data_path + "\\patterns\\thumbs");
        ensure_directory(data_path + "\\settings");

        return {
            data_path,
            data_path,
            data_path + "\\frames",
            {},
        };
    }

    void log_stacktrace() override
    {
    }

    void trigger_crash_test() override
    {
        __debugbreak();
    }

    [[nodiscard]] std::string clipboard_text() override
    {
        return ::clipboard_text();
    }

    [[nodiscard]] bool set_clipboard_text(std::string_view text) override
    {
        return ::set_clipboard_text(std::string(text));
    }

    void set_cursor_visible(bool visible) override
    {
        show_cursor(visible);
    }

    void set_virtual_keyboard_visible(bool visible) override
    {
        (void)visible;
    }

    void attach_ui_thread() override
    {
    }

    void detach_ui_thread() override
    {
    }

    void acquire_render_context() override
    {
        async_lock();
        bind_default_render_target();
    }

    void release_render_context() override
    {
        async_unlock();
    }

    void present_render_context() override
    {
        win32_async_swap();
    }

    void bind_default_render_target() override
    {
        pp::legacy::ui_gl::bind_opengl_framebuffer(
            pp::renderer::gl::framebuffer_target(),
            pp::renderer::gl::default_framebuffer_id());
    }

    void bind_main_render_target() override
    {
        bind_default_render_target();
    }

    void apply_render_platform_hints() override
    {
        const auto status = pp::renderer::gl::apply_opengl_render_platform_hints(
            pp::renderer::gl::OpenGlRenderPlatformHintDispatch {
                .enable = pp::legacy::ui_gl::enable_opengl_state,
            });
        if (!status.ok())
            LOG("OpenGL render platform hints failed: %s", status.message);
    }

    void install_render_debug_callback() override
    {
        if (!pp::legacy::gl_runtime::has_opengl_debug_message_callback())
            return;

        // colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c
        GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &render_debug_console_info);
        pp::legacy::gl_runtime::install_opengl_debug_message_callback(handle_gl_callback, nullptr);
        const auto status = pp::renderer::gl::apply_opengl_debug_output_states(
            pp::renderer::gl::OpenGlDebugOutputStateDispatch {
                .enable = pp::legacy::ui_gl::enable_opengl_state,
            });
        if (!status.ok())
            LOG("OpenGL debug output states failed: %s", status.message);
    }

    void begin_render_capture_frame() override
    {
        pp::platform::windows::win32_renderdoc_frame_start();
    }

    void end_render_capture_frame() override
    {
        pp::platform::windows::win32_renderdoc_frame_end();
    }

    [[nodiscard]] bool deletes_recorded_files_on_clear() override
    {
        return pp::platform::platform_deletes_recorded_files_on_clear(pp::platform::PlatformFamily::windows);
    }

    void clear_recorded_files(std::string_view recording_path) override
    {
        (void)recording_path;
    }

    void publish_exported_image(std::string_view path) override
    {
        if (!pp::platform::platform_publishes_exported_images(pp::platform::PlatformFamily::windows))
        {
            (void)path;
            return;
        }
        (void)path;
    }

    void flush_persistent_storage() override
    {
        if (!pp::platform::platform_flushes_persistent_storage(pp::platform::PlatformFamily::windows))
            return;
    }

    [[nodiscard]] std::vector<std::string> document_browse_roots(
        std::string_view work_path,
        std::string_view data_path) override
    {
        return pp::platform::platform_document_browse_roots(
            pp::platform::PlatformFamily::windows,
            work_path,
            data_path);
    }

    void save_ui_state() override
    {
        if (!pp::platform::platform_saves_native_ui_state(pp::platform::PlatformFamily::windows))
            return;
        win32_save_window_state();
    }

    [[nodiscard]] bool enables_live_asset_reloading() override
    {
        return pp::platform::platform_enables_live_asset_reloading(pp::platform::PlatformFamily::windows);
    }

    void update_platform_frame(float delta_time_seconds) override
    {
        win32_update_stylus(delta_time_seconds);
    }

    void report_rendered_frames(int frames) override
    {
        win32_update_fps(frames);
    }

    void display_file(std::string_view path) override
    {
        (void)path;
    }

    void share_file(std::string_view path) override
    {
        (void)path;
    }

    void request_app_close() override
    {
        destroy_window();
    }

    [[nodiscard]] bool start_vr_mode() override
    {
        const auto runtime = select_windows_xr_runtime();
        if (runtime.backend == pp::platform::XrRuntimeBackend::openvr)
        {
            active_xr_runtime_backend_ = pp::platform::XrRuntimeBackend::none;
            if (win32_vr_start())
                active_xr_runtime_backend_ = runtime.backend;
            return active_xr_runtime_backend_ == runtime.backend;
        }

        if (runtime.backend == pp::platform::XrRuntimeBackend::openxr)
            LOG("OpenXR runtime selected but the Windows OpenXR backend is not wired yet");

        return false;
    }

    void stop_vr_mode() override
    {
        auto runtime = active_xr_runtime_backend_;
        if (runtime == pp::platform::XrRuntimeBackend::none)
            runtime = select_windows_xr_runtime().backend;

        if (runtime == pp::platform::XrRuntimeBackend::openvr)
            win32_vr_stop();
        else if (runtime == pp::platform::XrRuntimeBackend::openxr)
            LOG("OpenXR runtime selected but the Windows OpenXR stop path is not wired yet");

        active_xr_runtime_backend_ = pp::platform::XrRuntimeBackend::none;
    }

    void pick_image(pp::platform::PickedPathCallback callback) override
    {
        const std::string path = open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png");
        invoke_selected_path(path, callback);
    }

    void pick_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
    {
        const std::string filter = build_supported_files_filter(file_types);
        const std::string path = open_file(filter.c_str());
        invoke_selected_path(path, callback);
    }

    void pick_save_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
    {
        const std::string filter = build_supported_files_filter(file_types);
        const std::string path = save_file(filter.c_str());
        invoke_selected_path(path, callback);
    }

    void pick_directory(pp::platform::PickedPathCallback callback) override
    {
        const std::string path = open_directory();
        invoke_selected_path(path, callback);
    }

    [[nodiscard]] bool supports_working_directory_picker() override
    {
        return pp::platform::platform_supports_working_directory_picker(pp::platform::PlatformFamily::windows);
    }

    [[nodiscard]] std::string format_working_directory_path(std::string_view path) override
    {
        char path_buffer[MAX_PATH] = {};
        const auto length = GetFullPathNameA(
            std::string(path).c_str(),
            static_cast<DWORD>(sizeof(path_buffer)),
            path_buffer,
            nullptr);
        if (length > 0 && length < sizeof(path_buffer))
            return path_buffer;
        return std::string(path);
    }

    [[nodiscard]] bool uses_prepared_file_writes() override
    {
        return pp::platform::platform_uses_prepared_file_writes(pp::platform::PlatformFamily::windows);
    }

    [[nodiscard]] bool uses_work_directory_document_export_collections() override
    {
        return pp::platform::platform_uses_work_directory_document_export_collections(
            pp::platform::PlatformFamily::windows);
    }

    [[nodiscard]] bool disables_network_tls_verification() override
    {
        return pp::platform::default_disables_network_tls_verification();
    }

    [[nodiscard]] bool uses_ppbr_export_data_directory_override() override
    {
        return pp::platform::platform_uses_ppbr_export_data_directory_override(
            pp::platform::PlatformFamily::windows);
    }

    [[nodiscard]] bool supports_sonarpen() override
    {
        return pp::platform::platform_supports_sonarpen(pp::platform::PlatformFamily::windows);
    }

    void start_sonarpen() override
    {
    }

    [[nodiscard]] int default_canvas_resolution() override
    {
        return pp::platform::platform_default_canvas_resolution(pp::platform::PlatformFamily::windows);
    }

    [[nodiscard]] bool draws_canvas_tip_for_pointer(
        bool is_mouse,
        bool is_stylus,
        bool is_left_button_release) override
    {
        return pp::platform::platform_draws_canvas_tip_for_pointer(
            pp::platform::PlatformFamily::windows,
            is_mouse,
            is_stylus,
            is_left_button_release);
    }

    [[nodiscard]] float adjust_canvas_input_pressure(float pressure) override
    {
        const auto curve = [](float x, float max) {
            return x > max ? 1.f : glm::pow(1.f - glm::pow(x / max - 1.f, 2.f), 2.f);
        };
        return curve(pressure, 0.95f);
    }

    [[nodiscard]] pp::platform::PreparedFileTarget prepare_writable_file(
        std::string_view type,
        std::string_view default_name,
        std::string_view data_path,
        std::string_view temporary_path) override
    {
        return pp::platform::plan_platform_writable_file(
            pp::platform::PlatformFamily::windows,
            type,
            default_name,
            data_path,
            temporary_path);
    }

    void save_prepared_file(
        std::string_view path,
        std::string_view suggested_name,
        pp::platform::PreparedFileCallback callback) override
    {
        (void)suggested_name;
        callback(std::string(path), false);
    }

private:
    pp::platform::XrRuntimeBackend active_xr_runtime_backend_ = pp::platform::XrRuntimeBackend::none;
};

}

namespace pp::platform::windows {

PlatformServices& platform_services()
{
    static WindowsPlatformServices services;
    return services;
}

VrSessionSnapshot read_platform_vr_session_snapshot() noexcept
{
    return read_vr_session_snapshot(platform_vr_state());
}

}
