/*
   Copyright (C) 2009 Red Hat, Inc.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/

#include "common.h"

#include <shlobj.h>
#include <io.h>
#include <conio.h>
#include <fcntl.h>

#include "platform.h"
#include "win_platform.h"
#include "utils.h"
#include "threads.h"
#include "debug.h"
#include "monitor.h"
#include "record.h"
#include "playback.h"
#include "cursor.h"
#include "named_pipe.h"
#include "ximage.h"
#include <spice/vd_agent.h>

int gdi_handlers = 0;
extern HINSTANCE instance;

class DefaultEventListener: public Platform::EventListener {
public:
    virtual void on_app_activated() {}
    virtual void on_app_deactivated() {}
    virtual void on_monitors_change() {}
};

static DefaultEventListener default_event_listener;
static Platform::EventListener* event_listener = &default_event_listener;
static HWND platform_win = NULL;
static ProcessLoop* main_loop = NULL;

class DefaultClipboardListener: public Platform::ClipboardListener {
public:
    virtual void on_clipboard_grab(uint32_t *types, uint32_t type_count) {}
    virtual void on_clipboard_request(uint32_t type) {}
    virtual void on_clipboard_notify(uint32_t type, uint8_t* data, int32_t size) {}
    virtual void on_clipboard_release() {}
};

static DefaultClipboardListener default_clipboard_listener;
static Platform::ClipboardListener* clipboard_listener = &default_clipboard_listener;

// The next window in the clipboard viewer chain, which is refered in all clipboard events.
static HWND next_clipboard_viewer_win = NULL;
static HANDLE clipboard_event = NULL;

static const int CLIPBOARD_TIMEOUT_MS = 10000;

static const int CLIPBOARD_FORMAT_MAX_TYPES = 16;

typedef struct ClipboardFormat {
    uint32_t format;
    uint32_t types[CLIPBOARD_FORMAT_MAX_TYPES];
} ClipboardFormat;

static ClipboardFormat clipboard_formats[] = {
    {CF_UNICODETEXT, {VD_AGENT_CLIPBOARD_UTF8_TEXT, 0}},
    //FIXME: support more image types
    {CF_DIB, {VD_AGENT_CLIPBOARD_IMAGE_PNG, VD_AGENT_CLIPBOARD_IMAGE_BMP, 0}},
};

#define clipboard_formats_count (sizeof(clipboard_formats) / sizeof(clipboard_formats[0]))

typedef struct ImageType {
    uint32_t type;
    DWORD cximage_format;
} ImageType;

static ImageType image_types[] = {
    {VD_AGENT_CLIPBOARD_IMAGE_PNG, CXIMAGE_FORMAT_PNG},
    {VD_AGENT_CLIPBOARD_IMAGE_BMP, CXIMAGE_FORMAT_BMP},
};

static std::set<uint32_t> grab_types;

static const unsigned long MODAL_LOOP_TIMER_ID = 1;
static const int MODAL_LOOP_DEFAULT_TIMEOUT = 100;
static bool modal_loop_active = false;
static bool set_modal_loop_timer();

void Platform::send_quit_request()
{
    ASSERT(main_loop);
    main_loop->quit(0);
}

static uint32_t get_clipboard_type(uint32_t format) {
    uint32_t* types = NULL;

    for (int i = 0; i < clipboard_formats_count && !types; i++) {
        if (clipboard_formats[i].format == format) {
            types = clipboard_formats[i].types;
        }
    }
    if (!types) {
        return VD_AGENT_CLIPBOARD_NONE;
    }
    for (uint32_t* ptype = types; *ptype; ptype++) {
        if (grab_types.find(*ptype) != grab_types.end()) {
            return *ptype;
        }
    }
    return VD_AGENT_CLIPBOARD_NONE;
}

static uint32_t get_clipboard_format(uint32_t type) {
    for (int i = 0; i < clipboard_formats_count; i++) {
        for (uint32_t* ptype = clipboard_formats[i].types; *ptype; ptype++) {
            if (*ptype == type) {
                return clipboard_formats[i].format; 
            }
        }
    }
    return 0;
}

static int get_available_clipboard_types(uint32_t** types)
{
    int count = 0;

    *types = new uint32_t[clipboard_formats_count * CLIPBOARD_FORMAT_MAX_TYPES];
    for (int i = 0; i < clipboard_formats_count; i++) {
        if (IsClipboardFormatAvailable(clipboard_formats[i].format)) {
            for (uint32_t* ptype = clipboard_formats[i].types; *ptype; ptype++) {
                (*types)[count++] = *ptype;
            }
        }
    }
    if (!count) {
        delete[] *types;
        *types = NULL;
    }
    return count;
}

static DWORD get_cximage_format(uint32_t type)
{
    for (int i = 0; i < sizeof(image_types) / sizeof(image_types[0]); i++) {
        if (image_types[i].type == type) {
            return image_types[i].cximage_format;
        }
    }
    return 0;
}

static LRESULT CALLBACK PlatformWinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {
    case WM_TIMER:
        if (modal_loop_active) {
            main_loop->timers_action();
            if (!set_modal_loop_timer()) {
                LOG_WARN("failed to set modal loop timer");
            }
        } else {
            LOG_WARN("received WM_TIMER not inside a modal loop");
        }
        break;
    case WM_ACTIVATEAPP:
        if (wParam) {
            event_listener->on_app_activated();
        } else {
            event_listener->on_app_deactivated();
        }
        break;
    case WM_DISPLAYCHANGE:
        event_listener->on_monitors_change();
        break;
    case WM_CHANGECBCHAIN:
        if (next_clipboard_viewer_win == (HWND)wParam) {
            next_clipboard_viewer_win = (HWND)lParam;
        } else if (next_clipboard_viewer_win) {
            SendMessage(next_clipboard_viewer_win, message, wParam, lParam);
        }
        break;
    case WM_DRAWCLIPBOARD:
        if (platform_win != GetClipboardOwner()) {
            int type_count;
            uint32_t* types;
            Platform::set_clipboard_owner(Platform::owner_none);
            if (type_count = get_available_clipboard_types(&types)) {
                clipboard_listener->on_clipboard_grab(types, type_count);
                delete[] types;
            } else {
                LOG_INFO("Unsupported clipboard format");
            }
        }
        if (next_clipboard_viewer_win) {
            SendMessage(next_clipboard_viewer_win, message, wParam, lParam);
        }
        break;
    case WM_RENDERFORMAT: {
        // In delayed rendering, Windows requires us to SetClipboardData before we return from
        // handling WM_RENDERFORMAT. Therefore, we try our best by sending CLIPBOARD_REQUEST to the
        // agent, while waiting alertably for a while (hoping for good) for receiving CLIPBOARD data
        // or CLIPBOARD_RELEASE from the agent, which both will signal clipboard_event.
        uint32_t type = get_clipboard_type(wParam);       
        if (!type) {
            LOG_INFO("Unsupported clipboard format %u", wParam);
            break;
        }
        clipboard_listener->on_clipboard_request(type);
        DWORD start_tick = GetTickCount();
        while (WaitForSingleObjectEx(clipboard_event, 1000, TRUE) != WAIT_OBJECT_0 &&
               GetTickCount() < start_tick + CLIPBOARD_TIMEOUT_MS);
        break;
    }
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

static void create_message_wind()
{
    WNDCLASSEX wclass;
    ATOM class_atom;

    const LPCWSTR class_name = L"spicec_platform_wclass";

    wclass.cbSize = sizeof(WNDCLASSEX);
    wclass.style = 0;
    wclass.lpfnWndProc = PlatformWinProc;
    wclass.cbClsExtra = 0;
    wclass.cbWndExtra = 0;
    wclass.hInstance = instance;
    wclass.hIcon = NULL;
    wclass.hCursor = NULL;
    wclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    wclass.lpszMenuName = NULL;
    wclass.lpszClassName = class_name;
    wclass.hIconSm = NULL;

    if ((class_atom = RegisterClassEx(&wclass)) == 0) {
        THROW("register class failed");
    }

    if (!(platform_win = CreateWindow(class_name, L"", 0, 0, 0, 0, 0, NULL, NULL, instance, NULL))) {
        THROW("create message window failed");
    }

    if (!(next_clipboard_viewer_win = SetClipboardViewer(platform_win)) && GetLastError()) {
        THROW("set clipboard viewer failed");
    }
    if (!(clipboard_event = CreateEvent(NULL, FALSE, FALSE, NULL))) {
        THROW("create clipboard event failed");
    }
}

NamedPipe::ListenerRef NamedPipe::create(const char *name, ListenerInterface& listener_interface)
{
    ASSERT(main_loop && main_loop->is_same_thread(pthread_self()));
    return (ListenerRef)(new WinListener(name, listener_interface, *main_loop));
}

void NamedPipe::destroy(ListenerRef listener_ref)
{
    ASSERT(main_loop && main_loop->is_same_thread(pthread_self()));
    delete (WinListener *)listener_ref;
}

void NamedPipe::destroy_connection(ConnectionRef conn_ref)
{
    ASSERT(main_loop && main_loop->is_same_thread(pthread_self()));
    delete (WinConnection *)conn_ref;
}

int32_t NamedPipe::read(ConnectionRef conn_ref, uint8_t *buf, int32_t size)
{
    return ((WinConnection *)conn_ref)->read(buf, size);
}

int32_t NamedPipe::write(ConnectionRef conn_ref, const uint8_t *buf, int32_t size)
{
    return ((WinConnection *)conn_ref)->write(buf, size);
}

void Platform::msleep(unsigned int msec)
{
    Sleep(msec);
}

void Platform::yield()
{
    Sleep(0);
}

void Platform::set_thread_priority(void* thread, Platform::ThreadPriority in_priority)
{
    ASSERT(thread == NULL);
    int priority;

    switch (in_priority) {
    case PRIORITY_TIME_CRITICAL:
        priority = THREAD_PRIORITY_TIME_CRITICAL;
        break;
    case PRIORITY_HIGH:
        priority = THREAD_PRIORITY_HIGHEST;
        break;
    case PRIORITY_ABOVE_NORMAL:
        priority = THREAD_PRIORITY_ABOVE_NORMAL;
        break;
    case PRIORITY_NORMAL:
        priority = THREAD_PRIORITY_NORMAL;
        break;
    case PRIORITY_BELOW_NORMAL:
        priority = THREAD_PRIORITY_BELOW_NORMAL;
        break;
    case PRIORITY_LOW:
        priority = THREAD_PRIORITY_LOWEST;
        break;
    case PRIORITY_IDLE:
        priority = THREAD_PRIORITY_IDLE;
        break;
    default:
        THROW("invalid priority %d", in_priority);
    }
    SetThreadPriority(GetCurrentThread(), priority);
}

void Platform::set_event_listener(EventListener* listener)
{
    event_listener = listener ? listener : &default_event_listener;
}

uint64_t Platform::get_monolithic_time()
{
    return uint64_t(GetTickCount()) * 1000 * 1000;
}

void Platform::get_temp_dir(std::string& path)
{
    DWORD len = GetTempPathA(0, NULL);
    if (len <= 0) {
        throw Exception("get temp patch failed");
    }
    char* tmp_path = new char[len + 1];
    GetTempPathA(len, tmp_path);
    path = tmp_path;
    delete[] tmp_path;
}

uint64_t Platform::get_process_id()
{
    static uint64_t pid = GetCurrentProcessId();
    return pid;
}

uint64_t Platform::get_thread_id()
{
    return GetCurrentThreadId();
}

void Platform::error_beep()
{
    MessageBeep(MB_ICONERROR);
}

class WinMonitor: public Monitor {
public:
    WinMonitor(int id, const wchar_t* name, const wchar_t* string);

    virtual int get_depth() { return _depth;}
    virtual SpicePoint get_position();
    virtual SpicePoint get_size() const { SpicePoint size = {_width, _height}; return size;}
    virtual bool is_out_of_sync() { return _out_of_sync;}
    virtual int get_screen_id() { return 0;}

protected:
    virtual ~WinMonitor();
    virtual void do_set_mode(int width, int height);
    virtual void do_restore();

private:
    void update_position();
    bool change_display_settings(int width, int height, int depth);
    bool best_display_setting(uint32_t width, uint32_t height, uint32_t depth);

private:
    std::wstring _dev_name;
    std::wstring _dev_string;
    bool _active;
    SpicePoint _position;
    int _width;
    int _height;
    int _depth;
    bool _out_of_sync;
};

WinMonitor::WinMonitor(int id, const wchar_t* name, const wchar_t* string)
    : Monitor(id)
    , _dev_name (name)
    , _dev_string (string)
    , _active (false)
    , _out_of_sync (false)
{
    update_position();
}

WinMonitor::~WinMonitor()
{
    do_restore();
}

void WinMonitor::update_position()
{
    DEVMODE mode;
    mode.dmSize = sizeof(DEVMODE);
    mode.dmDriverExtra = 0;
    EnumDisplaySettings(_dev_name.c_str(), ENUM_CURRENT_SETTINGS, &mode);
    _position.x = mode.dmPosition.x;
    _position.y = mode.dmPosition.y;
    _width = mode.dmPelsWidth;
    _height = mode.dmPelsHeight;
    _depth = mode.dmBitsPerPel;
}

SpicePoint WinMonitor::get_position()
{
    update_position();
    return _position;
}

bool WinMonitor::change_display_settings(int width, int height, int depth)
{
    DEVMODE mode;
    mode.dmSize = sizeof(DEVMODE);
    mode.dmDriverExtra = 0;
    mode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;
    mode.dmPelsWidth = width;
    mode.dmPelsHeight = height;
    mode.dmBitsPerPel = depth;

    return ChangeDisplaySettingsEx(_dev_name.c_str(), &mode, NULL, CDS_FULLSCREEN, NULL)
                                                                        == DISP_CHANGE_SUCCESSFUL;
}

bool WinMonitor::best_display_setting(uint32_t width, uint32_t height, uint32_t depth)
{
    DEVMODE mode;
    DWORD mode_id = 0;
    uint32_t mod_waste = ~0;
    DWORD mod_width;
    DWORD mod_height;
    DWORD mod_depth;
    DWORD mod_frequency;

    mode.dmSize = sizeof(DEVMODE);
    mode.dmDriverExtra = 0;
    while (EnumDisplaySettings(_dev_name.c_str(), mode_id++, &mode)) {
        // Workaround for
        // Lenovo T61p, Nvidia Quadro FX 570M and
        // Lenovo T61, Nvidia Quadro NVS 140M
        //
        // with dual monitors configuration
        //
        // we get strange values from EnumDisplaySettings 640x480x4 frequency 1
        // and calling ChangeDisplaySettingsEx with that configuration result with
        // machine that is stucked for a long period of time
        if (mode.dmDisplayFrequency == 1) {
            continue;
        }

        if (mode.dmPelsWidth >= width && mode.dmPelsHeight >= height) {
            bool replace = false;
            uint32_t curr_waste = mode.dmPelsWidth * mode.dmPelsHeight - width * height;
            if (curr_waste < mod_waste) {
                replace = true;
            } else if (curr_waste == mod_waste) {
                if (mod_depth == mode.dmBitsPerPel) {
                    replace = mode.dmDisplayFrequency > mod_frequency;
                } else if (mod_depth != depth && mode.dmBitsPerPel > mod_depth) {
                    replace = true;
                }
            }
            if (replace) {
                mod_waste = curr_waste;
                mod_width = mode.dmPelsWidth;
                mod_height = mode.dmPelsHeight;
                mod_depth = mode.dmBitsPerPel;
                mod_frequency = mode.dmDisplayFrequency;
            }
        }
    }
    if (mod_waste == ~0) {
        return false;
    }
    mode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL | DM_DISPLAYFREQUENCY;
    mode.dmPelsWidth = mod_width;
    mode.dmPelsHeight = mod_height;
    mode.dmBitsPerPel = mod_depth;
    mode.dmDisplayFrequency = mod_frequency;

    return ChangeDisplaySettingsEx(_dev_name.c_str(), &mode, NULL, CDS_FULLSCREEN, NULL)
                                                                        == DISP_CHANGE_SUCCESSFUL;
}

void WinMonitor::do_set_mode(int width, int height)
{
    update_position();
    if (width == _width && height == _height) {
        _out_of_sync = false;
        return;
    }
    self_monitors_change++;
    if (!change_display_settings(width, height, 32) && !best_display_setting(width, height, 32)) {
        _out_of_sync = true;
    } else {
        _out_of_sync = false;
    }
    self_monitors_change--;
    _active = true;
    update_position();
}

void WinMonitor::do_restore()
{
    if (_active) {
        _active = false;
        self_monitors_change++;
        ChangeDisplaySettingsEx(_dev_name.c_str(), NULL, NULL, 0, NULL);
        self_monitors_change--;
        update_position();
    }
}

static MonitorsList monitors;
static Monitor* primary_monitor = NULL;

const MonitorsList& Platform::init_monitors()
{
    ASSERT(monitors.empty());

    int id = 0;
    Monitor* mon;
    DISPLAY_DEVICE device_info;
    DWORD device_id = 0;
    device_info.cb = sizeof(device_info);
    while (EnumDisplayDevices(NULL, device_id, &device_info, 0)) {
        if ((device_info.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) &&
                 !(device_info.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER)) {
            mon = new WinMonitor(id++, device_info.DeviceName, device_info.DeviceString);
            monitors.push_back(mon);
            if (device_info.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) {
                primary_monitor = mon;
            }
        }
        device_id++;
    }
    return monitors;
}

void Platform::destroy_monitors()
{
    primary_monitor = NULL;
    while (!monitors.empty()) {
        Monitor* monitor = monitors.front();
        monitors.pop_front();
        delete monitor;
    }
}

bool Platform::is_monitors_pos_valid()
{
    return true;
}

void Platform::get_app_data_dir(std::string& path, const std::string& app_name)
{
    char app_data_path[MAX_PATH];
    HRESULT res = SHGetFolderPathA(NULL, CSIDL_APPDATA,  NULL, 0, app_data_path);
    if (res != S_OK) {
        throw Exception("get user app data dir failed");
    }

    path = app_data_path;
    path_append(path, app_name);

    if (!CreateDirectoryA(path.c_str(), NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
        throw Exception("create user app data dir failed");
    }
}

void Platform::path_append(std::string& path, const std::string& partial_path)
{
    path += "\\";
    path += partial_path;
}

static void cleanup()
{
    ChangeClipboardChain(platform_win, next_clipboard_viewer_win);
    CloseHandle(clipboard_event);
}

void Platform::init()
{
    create_message_wind();
    atexit(cleanup);
}

void Platform::set_process_loop(ProcessLoop& main_process_loop)
{
    main_loop = &main_process_loop;
}

WaveRecordAbstract* Platform::create_recorder(RecordClient& client,
                                              uint32_t sampels_per_sec,
                                              uint32_t bits_per_sample,
                                              uint32_t channels)
{
    return new WaveRecorder(client, sampels_per_sec, bits_per_sample, channels);
}

WavePlaybackAbstract* Platform::create_player(uint32_t sampels_per_sec,
                                              uint32_t bits_per_sample,
                                              uint32_t channels)
{
    return new WavePlayer(sampels_per_sec, bits_per_sample, channels);
}

static void toggle_modifier(int key)
{
    INPUT inputs[2];
    memset(inputs, 0, sizeof(inputs));
    inputs[0].type = inputs[1].type = INPUT_KEYBOARD;
    inputs[0].ki.wVk = inputs[1].ki.wVk = key;
    inputs[1].ki.dwFlags = KEYEVENTF_KEYUP;
    SendInput(2, inputs, sizeof(INPUT));
}

uint32_t Platform::get_keyboard_lock_modifiers()
{
    uint32_t modifiers = 0;
    if ((GetKeyState(VK_SCROLL) & 1)) {
        modifiers |= SCROLL_LOCK_MODIFIER;
    }
    if ((GetKeyState(VK_NUMLOCK) & 1)) {
        modifiers |= NUM_LOCK_MODIFIER;
    }
    if ((GetKeyState(VK_CAPITAL) & 1)) {
        modifiers |= CAPS_LOCK_MODIFIER;
    }
    return modifiers;
}

void Platform::set_keyboard_lock_modifiers(uint32_t modifiers)
{
    if (((modifiers >> SCROLL_LOCK_MODIFIER_SHIFT) & 1) != (GetKeyState(VK_SCROLL) & 1)) {
        toggle_modifier(VK_SCROLL);
    }

    if (((modifiers >> Platform::NUM_LOCK_MODIFIER_SHIFT) & 1) != (GetKeyState(VK_NUMLOCK) & 1)) {
        toggle_modifier(VK_NUMLOCK);
    }

    if (((modifiers >> CAPS_LOCK_MODIFIER_SHIFT) & 1) != (GetKeyState(VK_CAPITAL) & 1)) {
        toggle_modifier(VK_CAPITAL);
    }
}

typedef struct KeyboardModifier {
    int vkey;
    int bit;
} KeyboardModifier;

static const KeyboardModifier KEYBOARD_MODIFIERS[] = {
    {VK_LSHIFT, Platform::L_SHIFT_MODIFIER},
    {VK_RSHIFT, Platform::R_SHIFT_MODIFIER},
    {VK_LCONTROL, Platform::L_CTRL_MODIFIER},
    {VK_RCONTROL, Platform::R_CTRL_MODIFIER},
    {VK_LMENU, Platform::L_ALT_MODIFIER},
    {VK_RMENU, Platform::R_ALT_MODIFIER}};

uint32_t Platform::get_keyboard_modifiers()
{
    uint32_t modifiers_state = 0;
    int num_modifiers = sizeof(KEYBOARD_MODIFIERS)/sizeof(KEYBOARD_MODIFIERS[0]);

    for (int i = 0; i < num_modifiers; i++) {
        short key_state = GetAsyncKeyState(KEYBOARD_MODIFIERS[i].vkey);
        modifiers_state |= (key_state & 0x8000) ? KEYBOARD_MODIFIERS[i].bit : 0;
    }

    return modifiers_state;
}

void Platform::reset_cursor_pos()
{
    if (!primary_monitor) {
        return;
    }
    SpicePoint pos =  primary_monitor->get_position();
    SpicePoint size =  primary_monitor->get_size();
    SetCursorPos(pos.x + size.x / 2, pos.y + size.y / 2);
}

class WinBaseLocalCursor: public LocalCursor {
public:
    WinBaseLocalCursor() : _handle (0) {}
    void set(Window window) { SetCursor(_handle);}

protected:
    HCURSOR _handle;
};

class WinLocalCursor: public WinBaseLocalCursor {
public:
    WinLocalCursor(CursorData* cursor_data);
    ~WinLocalCursor();

private:
    bool _shared;
};

WinLocalCursor::WinLocalCursor(CursorData* cursor_data)
    : _shared (false)
{
    const SpiceCursorHeader& header = cursor_data->header();
    const uint8_t* data = cursor_data->data();
    int cur_size;
    int bits = get_size_bits(header, cur_size);
    if (!bits) {
        THROW("invalid curosr type");
    }
    if (header.type == SPICE_CURSOR_TYPE_MONO) {
        _handle = CreateCursor(NULL, header.hot_spot_x, header.hot_spot_y,
                               header.width, header.height, data, data + cur_size);
        return;
    }
    ICONINFO icon;
    icon.fIcon = FALSE;
    icon.xHotspot = header.hot_spot_x;
    icon.yHotspot = header.hot_spot_y;
    icon.hbmColor = icon.hbmMask = NULL;
    HDC hdc = GetDC(NULL);

    switch (header.type) {
    case SPICE_CURSOR_TYPE_ALPHA:
    case SPICE_CURSOR_TYPE_COLOR32:
    case SPICE_CURSOR_TYPE_COLOR16: {
        BITMAPV5HEADER bmp_hdr;
        ZeroMemory(&bmp_hdr, sizeof(bmp_hdr));
        bmp_hdr.bV5Size = sizeof(bmp_hdr);
        bmp_hdr.bV5Width = header.width;
        bmp_hdr.bV5Height = -header.height;
        bmp_hdr.bV5Planes = 1;
        bmp_hdr.bV5BitCount = bits;
        bmp_hdr.bV5Compression = BI_BITFIELDS;
        if (bits == 32) {
            bmp_hdr.bV5RedMask   = 0x00FF0000;
            bmp_hdr.bV5GreenMask = 0x0000FF00;
            bmp_hdr.bV5BlueMask  = 0x000000FF;
        } else if (bits == 16) {
            bmp_hdr.bV5RedMask   = 0x00007C00;
            bmp_hdr.bV5GreenMask = 0x000003E0;
            bmp_hdr.bV5BlueMask  = 0x0000001F;
        }
        if (header.type == SPICE_CURSOR_TYPE_ALPHA) {
            bmp_hdr.bV5AlphaMask = 0xFF000000;
        }
        void* bmp_pixels = NULL;
        icon.hbmColor = CreateDIBSection(hdc, (BITMAPINFO *)&bmp_hdr, DIB_RGB_COLORS, &bmp_pixels,
                                         NULL, 0);
        memcpy(bmp_pixels, data, cur_size);
        icon.hbmMask = CreateBitmap(header.width, header.height, 1, 1,
                                    (header.type == SPICE_CURSOR_TYPE_ALPHA) ? NULL :
                                                                   (CONST VOID *)(data + cur_size));
        break;
    }
    case SPICE_CURSOR_TYPE_COLOR4: {
        BITMAPINFO* bmp_info;
        bmp_info = (BITMAPINFO *)new uint8_t[sizeof(BITMAPINFO) + (sizeof(RGBQUAD) << bits)];
        ZeroMemory(bmp_info, sizeof(BITMAPINFO));
        bmp_info->bmiHeader.biSize = sizeof(bmp_info->bmiHeader);
        bmp_info->bmiHeader.biWidth = header.width;
        bmp_info->bmiHeader.biHeight = -header.height;
        bmp_info->bmiHeader.biPlanes = 1;
        bmp_info->bmiHeader.biBitCount = bits;
        bmp_info->bmiHeader.biCompression =  BI_RGB;
        memcpy(bmp_info->bmiColors, data + cur_size, sizeof(RGBQUAD) << bits);
        icon.hbmColor = CreateDIBitmap(hdc, &bmp_info->bmiHeader, CBM_INIT, data,
                                       bmp_info, DIB_RGB_COLORS);
        icon.hbmMask = CreateBitmap(header.width, header.height, 1, 1,
                                    (CONST VOID *)(data + cur_size + (sizeof(uint32_t) << bits)));
        delete[] (uint8_t *)bmp_info;
        break;
    }
    case SPICE_CURSOR_TYPE_COLOR24:
    case SPICE_CURSOR_TYPE_COLOR8:
    default:
        LOG_WARN("unsupported cursor type %d", header.type);
        _handle = LoadCursor(NULL, IDC_ARROW);
        _shared = true;
        ReleaseDC(NULL, hdc);
        return;
    }

    ReleaseDC(NULL, hdc);

    if (icon.hbmColor && icon.hbmMask) {
        _handle = CreateIconIndirect(&icon);
    }
    if (icon.hbmMask) {
        DeleteObject(icon.hbmMask);
    }
    if (icon.hbmColor) {
        DeleteObject(icon.hbmColor);
    }
}

WinLocalCursor::~WinLocalCursor()
{
    if (_handle && !_shared) {
        DestroyCursor(_handle);
    }
}

LocalCursor* Platform::create_local_cursor(CursorData* cursor_data)
{
    return new WinLocalCursor(cursor_data);
}

class WinInactiveCursor: public WinBaseLocalCursor {
public:
    WinInactiveCursor() { _handle = LoadCursor(NULL, IDC_NO);}
};

LocalCursor* Platform::create_inactive_cursor()
{
    return new WinInactiveCursor();
}

class WinDefaultCursor: public WinBaseLocalCursor {
public:
    WinDefaultCursor() { _handle = LoadCursor(NULL, IDC_ARROW);}
};

LocalCursor* Platform::create_default_cursor()
{
    return new WinDefaultCursor();
}

void Platform::set_display_mode_listner(DisplayModeListener* listener)
{
}

Icon* Platform::load_icon(int id)
{
    HICON icon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(id));
    if (!icon) {
        return NULL;
    }
    return new WinIcon(icon);
}

void WinPlatform::enter_modal_loop()
{
    if (modal_loop_active) {
        LOG_INFO("modal loop already active");
        return;
    }

    if (set_modal_loop_timer()) {
        modal_loop_active = true;
    } else {
        LOG_WARN("failed to create modal loop timer");
    }
}

static bool set_modal_loop_timer()
{
    int timeout = main_loop->get_soonest_timeout();
    if (timeout == INFINITE) {
        timeout = MODAL_LOOP_DEFAULT_TIMEOUT; /* for cases timeouts are added after
                                                 the enterance to the loop*/
    }

    if (!SetTimer(platform_win, MODAL_LOOP_TIMER_ID, timeout, NULL)) {
        return false;
    }
    return true;
}

void WinPlatform::exit_modal_loop()
{
    if (!modal_loop_active) {
        LOG_INFO("not inside the loop");
        return;
    }
    KillTimer(platform_win, MODAL_LOOP_TIMER_ID);
    modal_loop_active = false;
}

int Platform::_clipboard_owner = Platform::owner_none;

void Platform::set_clipboard_owner_unlocked(int new_owner)
{
    set_clipboard_owner(new_owner);
}

void Platform::set_clipboard_owner(int new_owner)
{
    const char * const owner_str[] = { "none", "guest", "client" };

    if (new_owner == owner_none) {
        clipboard_listener->on_clipboard_release();

        /* FIXME clear cached clipboard type info and data */
    }
    _clipboard_owner = new_owner;
    LOG_INFO("new clipboard owner: %s", owner_str[new_owner]);
}

bool Platform::on_clipboard_grab(uint32_t *types, uint32_t type_count)
{
    std::set<uint32_t> grab_formats;

    grab_types.clear();
    for (uint32_t i = 0; i < type_count; i++) {
        uint32_t format = get_clipboard_format(types[i]);
        //On first supported type, open and empty the clipboard
        if (format && grab_formats.empty()) {
            if (!OpenClipboard(platform_win)) {
                return false;
            }
            EmptyClipboard();
        }
        //For all supported type set delayed rendering
        if (format) {
            grab_types.insert(types[i]);
            if (grab_formats.insert(format).second) {
                SetClipboardData(format, NULL);
            }
        }
    }
    if (grab_formats.empty()) {
        LOG_INFO("No supported clipboard types in client grab");
        return false;
    }
    CloseClipboard();

    set_clipboard_owner(owner_guest);
    return true;
}

void Platform::set_clipboard_listener(ClipboardListener* listener)
{
    clipboard_listener = listener ? listener : &default_clipboard_listener;
}

static HGLOBAL utf8_alloc(LPCSTR data, int size)
{
    HGLOBAL handle; 
    LPVOID buf;
    int len;

    // Received utf8 string is not null-terminated   
    if (!(len = MultiByteToWideChar(CP_UTF8, 0, data, size, NULL, 0))) {
        return NULL;
    }
    len++;
    // Allocate and lock clipboard memory
    if (!(handle = GlobalAlloc(GMEM_DDESHARE, len * sizeof(WCHAR)))) {
        return NULL;
    }
    if (!(buf = GlobalLock(handle))) {
        GlobalFree(handle);
        return NULL;
    }  
    // Translate data and set clipboard content
    if (!(MultiByteToWideChar(CP_UTF8, 0, data, size, (LPWSTR)buf, len))) {
        GlobalUnlock(handle);
        GlobalFree(handle);
        return NULL;
    }
    ((LPWSTR)buf)[len - 1] = L'\0';
    GlobalUnlock(handle);
    return handle;
}

bool Platform::on_clipboard_notify(uint32_t type, const uint8_t* data, int32_t size)
{
    HANDLE clip_data;
    UINT format;
    bool ret = false;

    if (type == VD_AGENT_CLIPBOARD_NONE) {
        SetEvent(clipboard_event);
        return true;
    }
    switch (type) {
    case VD_AGENT_CLIPBOARD_UTF8_TEXT:
        clip_data = utf8_alloc((LPCSTR)data, size);
        break;
    case VD_AGENT_CLIPBOARD_IMAGE_PNG:
    case VD_AGENT_CLIPBOARD_IMAGE_BMP: {
        DWORD cximage_format = get_cximage_format(type);
        ASSERT(cximage_format);
        CxImage image((BYTE *)data, size, cximage_format);
        clip_data = image.CopyToHandle();
        break;
    }
    default:
        LOG_INFO("Unsupported clipboard type %u", type);
        return true;
    }

    format = get_clipboard_format(type);
    if (SetClipboardData(format, clip_data)) {
        SetEvent(clipboard_event);
        return true;
    }
    // We retry clipboard open-empty-set-close only when there is a timeout in WM_RENDERFORMAT
    if (!OpenClipboard(platform_win)) {
        return false;
    }
    EmptyClipboard();
    ret = !!SetClipboardData(format, clip_data);
    CloseClipboard();
    return ret;
}

bool Platform::on_clipboard_request(uint32_t type)
{
    UINT format = get_clipboard_format(type);
    HANDLE clip_data;
    LPCWSTR clip_buf;
    uint8_t* new_data = NULL;
    long new_size;

    bool ret = false;

    if (!format || !IsClipboardFormatAvailable(format) || !OpenClipboard(platform_win)) {
        return false;
    }
    if (!(clip_data = GetClipboardData(format))) {
        CloseClipboard();
        return false;
    }

    switch (type) {
    case VD_AGENT_CLIPBOARD_UTF8_TEXT: {
        if (!(clip_buf = (LPCWSTR)GlobalLock(clip_data))) {
            break;
        }
        size_t len = wcslen((wchar_t*)clip_buf);
        new_size = WideCharToMultiByte(CP_UTF8, 0, clip_buf, len, NULL, 0, NULL, NULL);
        if (!new_size) {
            GlobalUnlock(clip_data);
            break;
        }
        new_data = new uint8_t[new_size];
        if (WideCharToMultiByte(CP_UTF8, 0, clip_buf, len, (LPSTR)new_data, new_size,
                                NULL, NULL)) {
            clipboard_listener->on_clipboard_notify(type, new_data, new_size);
            ret = true;
        }
        delete[] (uint8_t *)new_data;
        GlobalUnlock(clip_data);
        break;
    }
    case VD_AGENT_CLIPBOARD_IMAGE_PNG:
    case VD_AGENT_CLIPBOARD_IMAGE_BMP: {
        DWORD cximage_format = get_cximage_format(type);
        ASSERT(cximage_format);
        CxImage image;
        if (!image.CreateFromHANDLE(clip_data)) {
            LOG_INFO("Image create from handle failed");
            break;
        }
        if (!image.Encode(new_data, new_size, cximage_format)) {
            LOG_INFO("Image encode to type %u failed", type);
            break;
        }
        LOG_INFO("Image encoded to %u bytes", new_size);
        clipboard_listener->on_clipboard_notify(type, new_data, new_size);
        image.FreeMemory(new_data);
        ret = true;
        break;
    }
    default:
        LOG_INFO("Unsupported clipboard type %u", type);
    }

    CloseClipboard();
    return ret;
}

void Platform::on_clipboard_release()
{
    SetEvent(clipboard_event);
    set_clipboard_owner(owner_none);
}

static bool has_console = false;
static BOOL parent_console;

static void create_console()
{
    static Mutex console_mutex;

    Lock lock(console_mutex);

    if (has_console) {
        return;
    }

    parent_console = AttachConsole(-1 /* parent */);

    if (!parent_console) {
        AllocConsole();
    }

    HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
    int hConHandle = _open_osfhandle((intptr_t)h, _O_TEXT);
    FILE * fp;
    /* _open_osfhandle can fail, for instance this will fail:
        start /wait spicec.exe --help | more
       however this actually works:
        cmd /c spicec --help | more
       */
    if (hConHandle != -1) {
        fp = _fdopen(hConHandle, "w");
        *stdout = *fp;
    }

    h = GetStdHandle(STD_INPUT_HANDLE);
    hConHandle = _open_osfhandle((intptr_t)h, _O_TEXT);
    if (hConHandle != -1) {
        fp = _fdopen(hConHandle, "r");
        *stdin = *fp;
    }

    h = GetStdHandle(STD_ERROR_HANDLE);
    hConHandle = _open_osfhandle((intptr_t)h, _O_TEXT);
    if (hConHandle != -1) {
        fp = _fdopen(hConHandle, "w");
        *stderr = *fp;
    }

    has_console = true;

    HWND consol_window = GetConsoleWindow();

    if (consol_window && !parent_console) {
        SetForegroundWindow(consol_window);
    }
}

class ConsoleWait {
public:
    ~ConsoleWait()
    {
        if (has_console && !parent_console) {
            Platform::term_printf("\n\nPress any key to exit...");
            _getch();
        }
    }

} console_wait;


void Platform::term_printf(const char* format, ...)
{
    if (!has_console) {
        create_console();
    }

    va_list ap;
    va_start(ap, format);
    vprintf(format, ap);
    va_end(ap);
}
