/**
 * @file window.c
 * @brief Win32 non-embedded video window provider
 */
/*****************************************************************************
 * Copyright © 2007-2010 VLC authors and VideoLAN
 *             2016-2019 VideoLabs, VLC authors and VideoLAN
 *
 * Authors: Filippo Carone <filippo@carone.org>
 *          Pierre d'Herbemont <pdherbemont # videolan.org>
 *
 * This program 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 program 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 program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <stdatomic.h>
#include <stdarg.h>

#include <vlc_common.h>
#include <vlc_threads.h>
#include <vlc_charset.h>
#include <vlc_frame.h>
#include <vlc_plugin.h>
#include <vlc_window.h>
#include <vlc_mouse.h>
#include <vlc_actions.h>
#include <vlc_ancillary.h>

#include <shellapi.h>                                         /* ExtractIcon */
#include "../wasync_resize_compressor.h"

#define RECTWidth(r)   (LONG)((r).right - (r).left)
#define RECTHeight(r)  (LONG)((r).bottom - (r).top)

#define WM_VLC_CHANGE_TEXT   (WM_APP + 1)
#define WM_VLC_SET_TOP_STATE (WM_APP + 2)

#define IDM_TOGGLE_ON_TOP  (WM_USER + 1)

typedef struct vout_window_sys_t
{
    vlc_thread_t thread;
    vlc_sem_t    ready;

    HWND hwnd;

    HMONITOR monitor; /* last monitor associated with window */
    WCHAR class_main[256];
    HICON vlc_icon;

    /* icc profile */
    char *icc_profile;

    /* mouse */
    unsigned button_pressed;

    /* cursor */
    bool       is_cursor_hidden;
    HCURSOR    cursor_arrow;
    HCURSOR    cursor_empty;
    int64_t    hide_timeout; /* mouse hiding timeout in ms */
    vlc_tick_t last_moved;

    /* state before fullscreen */
    WINDOWPLACEMENT window_placement;
    LONG            i_window_style;

    /* Title */
    wchar_t *_Atomic pwz_title;

    vlc_wasync_resize_compressor_t compressor;

} vout_window_sys_t;


static void Resize(vlc_window_t *wnd, unsigned width, unsigned height)
{
    vout_window_sys_t *sys = wnd->sys;

    /* When you create a window you give the dimensions you wish it to
     * have. Unfortunately these dimensions will include the borders and
     * titlebar. We use the following function to find out the size of
     * the window corresponding to the usable surface we want */
    RECT decorated_window = {
        .right = width,
        .bottom = height,
    };
    LONG i_window_style = GetWindowLong(sys->hwnd, GWL_STYLE);
    AdjustWindowRect( &decorated_window, i_window_style, 0 );
    SetWindowPos(sys->hwnd, 0, 0, 0,
                 RECTWidth(decorated_window), RECTHeight(decorated_window),
                 SWP_NOZORDER|SWP_NOMOVE);
}

static int Enable(vlc_window_t *wnd, const vlc_window_cfg_t *cfg)
{
    vout_window_sys_t *sys = wnd->sys;

    LONG i_window_style;
    if (cfg->is_decorated)
        i_window_style = WS_OVERLAPPEDWINDOW | WS_SIZEBOX;
    else
        i_window_style = WS_POPUP;
    i_window_style |= WS_CLIPCHILDREN;

    /* allow user to regain control over input events if requested */
    bool b_mouse_support = var_InheritBool( wnd, "mouse-events" );
    bool b_key_support = var_InheritBool( wnd, "keyboard-events" );
    if( !b_mouse_support && !b_key_support )
        i_window_style |= WS_DISABLED;
    SetWindowLong(sys->hwnd, GWL_STYLE, i_window_style);

    if (cfg->x || cfg->y)
        MoveWindow(sys->hwnd, cfg->x, cfg->y, cfg->width, cfg->height, TRUE);

    Resize(wnd, cfg->width, cfg->height);

    ShowWindow( sys->hwnd, SW_SHOW );
    return VLC_SUCCESS;
}

static void Disable(struct vlc_window *wnd)
{
    vout_window_sys_t *sys = wnd->sys;
    ShowWindow( sys->hwnd, SW_HIDE );
}

static void SetState(vlc_window_t *wnd, unsigned state)
{
    vout_window_sys_t *sys = wnd->sys;
    PostMessage( sys->hwnd, WM_VLC_SET_TOP_STATE, state, 0);
}

static void SetTitle(vlc_window_t *wnd, const char *title)
{
    vout_window_sys_t *sys = wnd->sys;
    char *psz_title = var_InheritString( wnd, "video-title" );
    wchar_t *pwz_title = ToWide(psz_title ? psz_title : title);

    free(psz_title);

    if (unlikely(pwz_title == NULL))
        return;

    free(atomic_exchange(&sys->pwz_title, pwz_title));
    PostMessage( sys->hwnd, WM_VLC_CHANGE_TEXT, 0, 0 );
}

static void SetFullscreen(vlc_window_t *wnd, const char *id)
{
    VLC_UNUSED(id);
    msg_Dbg(wnd, "entering fullscreen mode");
    vout_window_sys_t *sys = wnd->sys;

    sys->window_placement.length = sizeof(WINDOWPLACEMENT);
    GetWindowPlacement(sys->hwnd, &sys->window_placement);

    sys->i_window_style = GetWindowLong(sys->hwnd, GWL_STYLE);

    /* Change window style, no borders and no title bar */
    SetWindowLong(sys->hwnd, GWL_STYLE, WS_CLIPCHILDREN | WS_VISIBLE);

    /* Retrieve current window position so fullscreen will happen
     * on the right screen */
    HMONITOR hmon = MonitorFromWindow(sys->hwnd, MONITOR_DEFAULTTONEAREST);
    MONITORINFO mi;
    mi.cbSize = sizeof(MONITORINFO);
    if (GetMonitorInfo(hmon, &mi))
        SetWindowPos(sys->hwnd, 0,
                     mi.rcMonitor.left,
                     mi.rcMonitor.top,
                     RECTWidth(mi.rcMonitor),
                     RECTHeight(mi.rcMonitor),
                     SWP_NOZORDER|SWP_FRAMECHANGED);
}

static void UnsetFullscreen(vlc_window_t *wnd)
{
    msg_Dbg(wnd, "leaving fullscreen mode");
    vout_window_sys_t *sys = wnd->sys;

    /* return to normal window for non embedded vout */
    if (sys->window_placement.length)
    {
        SetWindowLong(sys->hwnd, GWL_STYLE, sys->i_window_style);
        SetWindowPlacement(sys->hwnd, &sys->window_placement);
        sys->window_placement.length = 0;
    }
    ShowWindow(sys->hwnd, SW_SHOWNORMAL);
}

static void SetAbove( vlc_window_t *wnd, enum vlc_window_state state )
{
    vout_window_sys_t *sys = wnd->sys;
    switch (state) {
    case VLC_WINDOW_STATE_NORMAL:
        if ((GetWindowLong(sys->hwnd, GWL_EXSTYLE) & WS_EX_TOPMOST))
        {
            HMENU hMenu = GetSystemMenu(sys->hwnd, FALSE);
            CheckMenuItem(hMenu, IDM_TOGGLE_ON_TOP, MF_BYCOMMAND | MFS_UNCHECKED);
            SetWindowPos(sys->hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE);
        }
        break;
    case VLC_WINDOW_STATE_ABOVE:
        if (!(GetWindowLong(sys->hwnd, GWL_EXSTYLE) & WS_EX_TOPMOST))
        {
            HMENU hMenu = GetSystemMenu(sys->hwnd, FALSE);
            CheckMenuItem(hMenu, IDM_TOGGLE_ON_TOP, MF_BYCOMMAND | MFS_CHECKED);
            SetWindowPos(sys->hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
        }
        break;
    case VLC_WINDOW_STATE_BELOW:
        SetWindowPos(sys->hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
        break;

    }
}

static void MousePressed( vlc_window_t *wnd, HWND hwnd, unsigned button )
{
    vout_window_sys_t *sys = wnd->sys;
    if( !sys->button_pressed )
        SetCapture( hwnd );
    sys->button_pressed |= 1 << button;
    vlc_window_ReportMousePressed(wnd, button);
}

static void MouseReleased( vlc_window_t *wnd, unsigned button )
{
    vout_window_sys_t *sys = wnd->sys;
    sys->button_pressed &= ~(1 << button);
    if( !sys->button_pressed )
        ReleaseCapture();
    vlc_window_ReportMouseReleased(wnd, button);
}

#define MAX_ICC_FILE_SIZE  (100*1024*1024)

static void UpdateICCProfile(vlc_window_t *wnd, const wchar_t *pathw)
{
    vout_window_sys_t *sys = wnd->sys;
    vlc_icc_profile_t *icc = NULL;

    if (!pathw) {
        /* Profile cleared */
        if (sys->icc_profile) {
            free(sys->icc_profile);
            sys->icc_profile = NULL;
            vlc_window_ReportICCProfile(wnd, NULL);
        }
        return;
    }

    char *path = FromWide(pathw);
    if (!path)
        goto error;

    if (sys->icc_profile && strcmp(path, sys->icc_profile) == 0) {
        /* No change to profile */
        free(path);
        return;
    }

    free(sys->icc_profile);
    sys->icc_profile = path;

    /* Open ICC profile */
    vlc_frame_t *frame = vlc_frame_FilePath(path, false);
    if (!frame)
        goto error;
    vlc_frame_cleanup_push(frame);

    icc = malloc(sizeof(*icc) + frame->i_buffer);
    if (!icc)
        goto error;

    icc->size = frame->i_buffer;
    memcpy(icc->data, frame->p_buffer, icc->size);
    /* fall through */

error:
    if (!icc)
        msg_Err(wnd, "Failed to open ICC profile: %s", path ? path : "(unknown)");

    vlc_cleanup_pop();
    vlc_window_ReportICCProfile(wnd, icc);
}

static void MonitorChanged(vlc_window_t *wnd, HMONITOR monitor)
{
    MONITORINFOEXW mi = { .cbSize = sizeof(mi) };
    GetMonitorInfoW(monitor, (LPMONITORINFO) &mi);

    /* Try updating ICC profile */
    HDC ic = CreateICW(mi.szDevice, NULL, NULL, NULL);
    if (!ic)
        return;

    wchar_t iccw[MAX_PATH];
    DWORD iccw_len = ARRAY_SIZE(iccw);
    if (GetICMProfileW(ic, &iccw_len, iccw)) {
        UpdateICCProfile(wnd, iccw);
    } else if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
        wchar_t *piccw = malloc( iccw_len * sizeof(*piccw) );
        if (piccw != NULL && GetICMProfileW(ic, &iccw_len, piccw))
            UpdateICCProfile(wnd, piccw);
        else
            UpdateICCProfile(wnd, NULL);
        free(piccw);
    } else {
        UpdateICCProfile(wnd, NULL);
    }

    DeleteDC(ic);
}


static struct
{
    int i_dxkey;
    int i_vlckey;

} dxkeys_to_vlckeys[] =
{
    { VK_F1, KEY_F1 }, { VK_F2, KEY_F2 }, { VK_F3, KEY_F3 }, { VK_F4, KEY_F4 },
    { VK_F5, KEY_F5 }, { VK_F6, KEY_F6 }, { VK_F7, KEY_F7 }, { VK_F8, KEY_F8 },
    { VK_F9, KEY_F9 }, { VK_F10, KEY_F10 }, { VK_F11, KEY_F11 },
    { VK_F12, KEY_F12 }, { VK_F13, KEY_F(13) }, { VK_F14, KEY_F(14) },
    { VK_F15, KEY_F(15) }, { VK_F16, KEY_F(16) }, { VK_F17, KEY_F(17) },
    { VK_F18, KEY_F(18) }, { VK_F19, KEY_F(19) }, { VK_F20, KEY_F(20) },
    { VK_F21, KEY_F(21) }, { VK_F22, KEY_F(22) }, { VK_F23, KEY_F(23) },
    { VK_F24, KEY_F(24) },

    { VK_RETURN, KEY_ENTER },
    { VK_SPACE, ' ' },
    { VK_ESCAPE, KEY_ESC },

    { VK_LEFT, KEY_LEFT },
    { VK_RIGHT, KEY_RIGHT },
    { VK_UP, KEY_UP },
    { VK_DOWN, KEY_DOWN },

    { VK_HOME, KEY_HOME },
    { VK_END, KEY_END },
    { VK_PRIOR, KEY_PAGEUP },
    { VK_NEXT, KEY_PAGEDOWN },

    { VK_INSERT, KEY_INSERT },
    { VK_DELETE, KEY_DELETE },

    { VK_CONTROL, 0 },
    { VK_SHIFT, 0 },
    { VK_MENU, 0 },

    { VK_BROWSER_BACK, KEY_BROWSER_BACK },
    { VK_BROWSER_FORWARD, KEY_BROWSER_FORWARD },
    { VK_BROWSER_REFRESH, KEY_BROWSER_REFRESH },
    { VK_BROWSER_STOP, KEY_BROWSER_STOP },
    { VK_BROWSER_SEARCH, KEY_BROWSER_SEARCH },
    { VK_BROWSER_FAVORITES, KEY_BROWSER_FAVORITES },
    { VK_BROWSER_HOME, KEY_BROWSER_HOME },
    { VK_VOLUME_MUTE, KEY_VOLUME_MUTE },
    { VK_VOLUME_DOWN, KEY_VOLUME_DOWN },
    { VK_VOLUME_UP, KEY_VOLUME_UP },
    { VK_MEDIA_NEXT_TRACK, KEY_MEDIA_NEXT_TRACK },
    { VK_MEDIA_PREV_TRACK, KEY_MEDIA_PREV_TRACK },
    { VK_MEDIA_STOP, KEY_MEDIA_STOP },
    { VK_MEDIA_PLAY_PAUSE, KEY_MEDIA_PLAY_PAUSE },

    { 0, 0 }
};

static int Win32VoutConvertKey( int i_key )
{
    for( int i = 0; dxkeys_to_vlckeys[i].i_dxkey != 0; i++ )
    {
        if( dxkeys_to_vlckeys[i].i_dxkey == i_key )
        {
            return dxkeys_to_vlckeys[i].i_vlckey;
        }
    }

    return 0;
}

/*****************************************************************************
 * WinVoutEventProc: This is the window event processing function.
 *****************************************************************************
 * On Windows, when you create a window you have to attach an event processing
 * function to it. The aim of this function is to manage "Queued Messages" and
 * "Nonqueued Messages".
 * Queued Messages are those picked up and retransmitted by vout_Manage
 * (using the GetMessage and DispatchMessage functions).
 * Nonqueued Messages are those that Windows will send directly to this
 * procedure (like WM_DESTROY, WM_WINDOWPOSCHANGED...)
 *****************************************************************************/
static LRESULT CALLBACK WinVoutEventProc( HWND hwnd, UINT message,
                                          WPARAM wParam, LPARAM lParam )
{
    if( message == WM_CREATE )
    {
        /* Store wnd for future use */
        CREATESTRUCT *c = (CREATESTRUCT *)lParam;
        SetWindowLongPtr( hwnd, GWLP_USERDATA, (LONG_PTR)c->lpCreateParams );
        return 0;
    }

    LONG_PTR p_user_data = GetWindowLongPtr( hwnd, GWLP_USERDATA );
    if( p_user_data == 0 )
        return DefWindowProc(hwnd, message, wParam, lParam);
    vlc_window_t *wnd = (vlc_window_t *)p_user_data;

    switch( message )
    {
    case WM_CLOSE:
    {
        vout_window_sys_t *sys = wnd->sys;
        /* wait for all pending timers */
        vlc_wasync_resize_compressor_dropOrWait(&sys->compressor);
        vlc_window_ReportClose(wnd);
        break;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    case WM_SIZE:
    {
        vout_window_sys_t *sys = wnd->sys;
        vlc_wasync_resize_compressor_reportSize(&sys->compressor, LOWORD(lParam), HIWORD(lParam));
        return 0;
    }
    case WM_MOUSEMOVE:
        vlc_window_ReportMouseMoved(wnd, LOWORD(lParam), HIWORD(lParam));
        break;
    case WM_NCMOUSEMOVE:
        break;

    case WM_DISPLAYCHANGE:
    {
        vout_window_sys_t *sys = wnd->sys;
        sys->monitor = NULL;
    }
    /* fall through */
    case WM_MOVE:
    {
        vout_window_sys_t *sys = wnd->sys;
        HMONITOR hmon = MonitorFromWindow(sys->hwnd, MONITOR_DEFAULTTONEAREST);
        if (hmon != sys->monitor) {
            sys->monitor = hmon;
            MonitorChanged(wnd, hmon);
        }
        break;
    }

    case WM_CAPTURECHANGED:
    {
        vout_window_sys_t *sys = wnd->sys;
        for( int button = 0; sys->button_pressed; button++ )
        {
            unsigned m = 1 << button;
            if( sys->button_pressed & m )
                vlc_window_ReportMouseReleased(wnd, button);
            sys->button_pressed &= ~m;
        }
        sys->button_pressed = 0;
        break;
    }

    case WM_LBUTTONDOWN:
        MousePressed( wnd, hwnd, MOUSE_BUTTON_LEFT );
        return 0;
    case WM_LBUTTONUP:
        MouseReleased( wnd, MOUSE_BUTTON_LEFT );
        return 0;
    case WM_LBUTTONDBLCLK:
        vlc_window_ReportMouseDoubleClick(wnd, MOUSE_BUTTON_LEFT);
        return 0;

    case WM_MBUTTONDOWN:
        MousePressed( wnd, hwnd, MOUSE_BUTTON_CENTER );
        return 0;
    case WM_MBUTTONUP:
        MouseReleased( wnd, MOUSE_BUTTON_CENTER );
        return 0;
    case WM_MBUTTONDBLCLK:
        vlc_window_ReportMouseDoubleClick(wnd, MOUSE_BUTTON_CENTER);
        return 0;

    case WM_RBUTTONDOWN:
        MousePressed( wnd, hwnd, MOUSE_BUTTON_RIGHT );
        return 0;
    case WM_RBUTTONUP:
        MouseReleased( wnd, MOUSE_BUTTON_RIGHT );
        return 0;
    case WM_RBUTTONDBLCLK:
        vlc_window_ReportMouseDoubleClick(wnd, MOUSE_BUTTON_RIGHT);
        return 0;

    case WM_KEYDOWN:
    case WM_SYSKEYDOWN:
    {
        /* The key events are first processed here and not translated
         * into WM_CHAR events because we need to know the status of the
         * modifier keys. */
        int i_key = Win32VoutConvertKey( wParam );
        if( !i_key )
        {
            /* This appears to be a "normal" (ascii) key */
            i_key = tolower( (unsigned char)MapVirtualKey( wParam, 2 ) );
        }

        if( i_key )
        {
            if( GetKeyState(VK_CONTROL) & KF_UP )
            {
                i_key |= KEY_MODIFIER_CTRL;
            }
            if( GetKeyState(VK_SHIFT) & KF_UP )
            {
                i_key |= KEY_MODIFIER_SHIFT;
            }
            if( GetKeyState(VK_MENU) & KF_UP )
            {
                i_key |= KEY_MODIFIER_ALT;
            }

            vlc_window_ReportKeyPress(wnd, i_key);
        }
        break;
    }

    case WM_SYSCOMMAND:
        switch (wParam)
        {
        case IDM_TOGGLE_ON_TOP:            /* toggle the "on top" status */
        {
            HMENU hMenu = GetSystemMenu(hwnd, FALSE);
            const bool is_on_top = (GetMenuState(hMenu, IDM_TOGGLE_ON_TOP, MF_BYCOMMAND) & MF_CHECKED) == 0;
            SetAbove( wnd, is_on_top ? VLC_WINDOW_STATE_ABOVE : VLC_WINDOW_STATE_NORMAL );
            return 0;
        }
        default:
            break;
        }
        break;

    case WM_VLC_SET_TOP_STATE:
        SetAbove( wnd, (enum vlc_window_state) wParam);
        return 0;

    case WM_VLC_CHANGE_TEXT:
        {
            vout_window_sys_t *sys = wnd->sys;
            wchar_t *pwz_title = atomic_exchange(&sys->pwz_title, NULL);

            if( pwz_title )
            {
                SetWindowTextW( hwnd, pwz_title );
                free( pwz_title );
            }
            break;
        }
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}

static void Close(vlc_window_t *wnd)
{
    vout_window_sys_t *sys = wnd->sys;

    if (sys->hwnd)
        PostMessage( sys->hwnd, WM_CLOSE, 0, 0 );
    vlc_join(sys->thread, NULL);
    free(atomic_load(&sys->pwz_title));

    vlc_wasync_resize_compressor_destroy(&sys->compressor);

    HINSTANCE hInstance = GetModuleHandle(NULL);
    UnregisterClass( sys->class_main, hInstance );
    DestroyCursor( sys->cursor_empty );
    if( sys->vlc_icon )
        DestroyIcon( sys->vlc_icon );
    wnd->sys = NULL;
}

static int CALLBACK enumWindowsProc(HWND hwnd, LPARAM lParam)
{
    HWND *wnd = (HWND *)lParam;

    char name[128];
    name[0] = '\0';
    GetClassNameA( hwnd, name, 128 );

    if( !strcasecmp( name, "WorkerW" ) )
    {
        hwnd = FindWindowEx( hwnd, NULL, TEXT("SHELLDLL_DefView"), NULL );
        if( hwnd ) hwnd = FindWindowEx( hwnd, NULL, TEXT("SysListView32"), NULL );
        if( hwnd )
        {
            *wnd = hwnd;
            return false;
        }
    }
    return true;
}

static HWND GetDesktopHandle(vlc_object_t *obj)
{
    /* Find Program Manager */
    HWND hwnd = FindWindow( TEXT("Progman"), NULL );
    if( hwnd ) hwnd = FindWindowEx( hwnd, NULL, TEXT("SHELLDLL_DefView"), NULL );
    if( hwnd ) hwnd = FindWindowEx( hwnd, NULL, TEXT("SysListView32"), NULL );
    if( hwnd )
        return hwnd;

    msg_Dbg( obj, "Couldn't find desktop icon window,. Trying the hard way." );

    EnumWindows( enumWindowsProc, (LPARAM)&hwnd );
    return hwnd;
}

static void UpdateCursor( vlc_window_t *wnd, bool b_show )
{
    vout_window_sys_t *sys = wnd->sys;
    if( sys->is_cursor_hidden == !b_show )
        return;
    sys->is_cursor_hidden = !b_show;

    HCURSOR cursor = b_show ? sys->cursor_arrow : sys->cursor_empty;
    SetClassLongPtr( sys->hwnd, GCLP_HCURSOR, (LONG_PTR)cursor );

    /* FIXME I failed to find a cleaner way to force a redraw of the cursor */
    SetCursor( cursor );
}

static void CALLBACK HideMouse(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
    VLC_UNUSED(uMsg); VLC_UNUSED(dwTime);
    if (hwnd)
    {
        vlc_window_t *wnd = (vlc_window_t *)idEvent;
        UpdateCursor( wnd, false );
    }
}

static void UpdateCursorMoved( vlc_window_t *wnd )
{
    vout_window_sys_t *sys = wnd->sys;
    UpdateCursor( wnd, true );
    sys->last_moved = vlc_tick_now();
    SetTimer( sys->hwnd, (UINT_PTR)wnd, sys->hide_timeout, HideMouse );
}

static HCURSOR EmptyCursor( HINSTANCE instance )
{
    const int cw = GetSystemMetrics(SM_CXCURSOR);
    const int ch = GetSystemMetrics(SM_CYCURSOR);

    HCURSOR cursor = NULL;
    uint8_t *and = malloc(cw * ch);
    uint8_t *xor = malloc(cw * ch);
    if( and && xor )
    {
        memset(and, 0xff, cw * ch );
        memset(xor, 0x00, cw * ch );
        cursor = CreateCursor( instance, 0, 0, cw, ch, and, xor);
    }
    free( and );
    free( xor );

    return cursor;
}

static inline bool isMouseEvent( WPARAM type )
{
    return type >= WM_MOUSEFIRST &&
           type <= WM_MOUSELAST;
}

static void *EventThread( void *p_this )
{
    vlc_thread_set_name("vlc-window-win");

    vlc_window_t *wnd = (vlc_window_t *)p_this;
    vout_window_sys_t *sys = wnd->sys;

    int canc = vlc_savecancel ();

    HINSTANCE hInstance = GetModuleHandle(NULL);

    LONG i_window_style;
    HWND hwParent;
    if (var_InheritBool( wnd, "video-wallpaper" ))
    {
        hwParent = GetDesktopHandle(p_this);
        i_window_style = WS_CLIPCHILDREN|WS_CHILD;
    }
    else
    {
        hwParent = 0;
        i_window_style = WS_OVERLAPPEDWINDOW | WS_SIZEBOX | WS_CLIPCHILDREN;
    }

    /* allow user to regain control over input events if requested */
    bool b_mouse_support = var_InheritBool( wnd, "mouse-events" );
    bool b_key_support = var_InheritBool( wnd, "keyboard-events" );
    if( !b_mouse_support && !b_key_support )
        i_window_style |= WS_DISABLED;

    /* Create the window */
    sys->hwnd =
        CreateWindowEx( WS_EX_NOPARENTNOTIFY,
                    sys->class_main,                 /* name of window class */
                    TEXT(VOUT_TITLE) TEXT(" (VLC Video Output)"),/* window title */
                    i_window_style,                          /* window style */
                    CW_USEDEFAULT,                   /* default X coordinate */
                    CW_USEDEFAULT,                   /* default Y coordinate */
                    CW_USEDEFAULT,                           /* window width */
                    CW_USEDEFAULT,                          /* window height */
                    hwParent,                               /* parent window */
                    NULL,                          /* no menu in this window */
                    hInstance,            /* handle of this program instance */
                    wnd );                           /* send vd to WM_CREATE */

    vlc_sem_post( &sys->ready );

    if (sys->hwnd == NULL)
    {
        vlc_restorecancel( canc );
        return NULL;
    }

    /* Detect starting monitor */
    sys->monitor = MonitorFromWindow(sys->hwnd, MONITOR_DEFAULTTOPRIMARY);
    MonitorChanged(wnd, sys->monitor);

    /* Append a "Always On Top" entry in the system menu */
    HMENU hMenu = GetSystemMenu( sys->hwnd, FALSE );
    AppendMenu( hMenu, MF_SEPARATOR, 0, TEXT("") );
    AppendMenu( hMenu, MF_STRING | MF_UNCHECKED,
                       IDM_TOGGLE_ON_TOP, TEXT("Always on &Top") );

    POINT mouse_pos = {0}, old_mouse_pos = {0};
    for( ;; )
    {
        MSG msg;
        if( !GetMessage( &msg, 0, 0, 0 ) )
        {
            break;
        }

        /* cursor handling */
        if( msg.message == WM_MOUSEMOVE || msg.message == WM_NCMOUSEMOVE )
        {
            GetCursorPos( &mouse_pos );
            /* FIXME, why this >2 limits ? */
            if( (abs(mouse_pos.x - old_mouse_pos.x) > 2 ||
                (abs(mouse_pos.y - old_mouse_pos.y)) > 2 ) )
            {
                old_mouse_pos = mouse_pos;
                UpdateCursorMoved( wnd );
            }
        }
        else if( isMouseEvent( msg.message ) )
        {
            UpdateCursorMoved( wnd );
        }

        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    vlc_restorecancel(canc);
    return NULL;
}

static const struct vlc_window_operations ops = {
    .enable  = Enable,
    .disable = Disable,
    .resize = Resize,
    .set_title = SetTitle,
    .set_state = SetState,
    .set_fullscreen = SetFullscreen,
    .unset_fullscreen = UnsetFullscreen,
    .destroy = Close,
};

static int Open(vlc_window_t *wnd)
{
    vout_window_sys_t *sys = vlc_obj_calloc(VLC_OBJECT(wnd), 1, sizeof(vout_window_sys_t));
    if (unlikely(sys == NULL))
        return VLC_ENOMEM;

    _snwprintf( sys->class_main, ARRAY_SIZE(sys->class_main),
               TEXT("VLC standalone window %p"), (void *)sys );

    HINSTANCE hInstance = GetModuleHandle(NULL);

    WCHAR app_path[MAX_PATH];
    DWORD read = GetModuleFileNameW( NULL, app_path, ARRAY_SIZE(app_path) );
    if (read != 0 && read != ARRAY_SIZE(app_path))
        sys->vlc_icon = ExtractIconW( hInstance, app_path, 0 );
    else
        sys->vlc_icon = NULL;

    sys->button_pressed = 0;
    sys->is_cursor_hidden = false;
    sys->hide_timeout = var_InheritInteger( wnd, "mouse-hide-timeout" );
    sys->cursor_arrow = LoadCursor(NULL, IDC_ARROW);
    sys->cursor_empty = EmptyCursor(hInstance);

    WNDCLASS wc = { 0 };
    /* Fill in the window class structure */
    wc.style         = CS_OWNDC|CS_DBLCLKS;           /* style: dbl click */
    wc.lpfnWndProc   = WinVoutEventProc;                 /* event handler */
    wc.hInstance     = hInstance;                             /* instance */
    wc.hIcon         = sys->vlc_icon;            /* load the vlc big icon */
    wc.lpszClassName = sys->class_main;            /* use a special class */
    wc.hCursor       = sys->cursor_arrow;

    /* Register the window class */
    if( !RegisterClass(&wc) )
    {
        if( sys->vlc_icon )
            DestroyIcon( sys->vlc_icon );

        msg_Err( wnd, "RegisterClass FAILED (err=%lu)", GetLastError() );
        return VLC_EGENERIC;
    }
    vlc_sem_init( &sys->ready, 0 );
    wnd->sys = sys;

    if( vlc_wasync_resize_compressor_init( &sys->compressor, wnd ) )
    {
        msg_Err( wnd, "Failed to init compressor" );
        Close(wnd);
        return VLC_EGENERIC;
    }

    if( vlc_clone( &sys->thread, EventThread, wnd ) )
    {
        Close(wnd);
        return VLC_EGENERIC;
    }

    vlc_sem_wait( &sys->ready );

    if (sys->hwnd == NULL)
    {
        Close(wnd);
        return VLC_EGENERIC;
    }

    wnd->type = VLC_WINDOW_TYPE_HWND;
    wnd->handle.hwnd = sys->hwnd;
    wnd->ops = &ops;
    wnd->info.has_double_click = true;
    return VLC_SUCCESS;
}

vlc_module_begin()
    set_shortname(N_("Win32 window"))
    set_description(N_("Win32 window"))
    set_subcategory(SUBCAT_VIDEO_VOUT)
    set_capability("vout window", 10)
    set_callback(Open)
vlc_module_end()
