/*****************************************************************************
 * window_os2.c: OS/2 non-embedded video window provider
 *****************************************************************************
 * Copyright (C) 2020 VLC authors and VideoLAN
 *
 * Authors: KO Myung-Hun <komh@chollian.net>
 *
 * 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.
 *****************************************************************************/

/*****************************************************************************
 * Preamble
 *****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

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

#include <float.h>
#include <ctype.h>

/*****************************************************************************
 * Module descriptor
 *****************************************************************************/
static int Open( vlc_window_t *wnd );

vlc_module_begin ()
    set_shortname( N_("OS/2 window"))
    set_description( N_("OS/2 non-embeded window video"))
    set_subcategory( SUBCAT_VIDEO_VOUT )
    set_capability ("vout window", 30 )
    set_callback( Open )
vlc_module_end ()

typedef struct vout_window_sys_t
{
    TID tid;
    HEV ack_event;
    int i_result;
    HAB hab;
    HMQ hmq;
    HWND frame;
    HWND client;
    LONG i_screen_width;
    LONG i_screen_height;
    RECTL client_rect;
    unsigned button_pressed;
    bool is_mouse_hidden;
    ULONG cursor_timeout;
} vout_window_sys_t;

/*****************************************************************************
 * Local prototypes
 *****************************************************************************/
static int ConvertKey( USHORT i_pmkey );
static void MousePressed( vlc_window_t *wnd, HWND hwnd, unsigned button );
static void MouseReleased( vlc_window_t *wnd, unsigned button );
static MRESULT EXPENTRY WndProc( HWND hwnd, ULONG msg,
                                 MPARAM mp1, MPARAM mp2 );
static void MorphToPM( void );
static void PMThread( void *arg );

static int Enable( vlc_window_t *wnd, const vlc_window_cfg_t *cfg );
static void Disable( struct vlc_window *wnd );
static void Resize( vlc_window_t *wnd, unsigned width, unsigned height );
static void Close(vlc_window_t *wnd );
static void SetState( vlc_window_t *wnd, unsigned state );
static void UnsetFullscreen( vlc_window_t *wnd );
static void SetFullscreen( vlc_window_t *wnd, const char *id );
static void SetTitle( vlc_window_t *wnd, const char *title );

#define WC_VLC_WINDOW_OS2 "WC_VLC_WINDOW_OS2"

#define WM_VLC_ENABLE       ( WM_USER + 1 )
#define WM_VLC_RESIZE       ( WM_USER + 2 )
#define WM_VLC_SETSTATE     ( WM_USER + 3 )
#define WM_VLC_FULLSCREEN   ( WM_USER + 4 )
#define WM_VLC_SETTITLE     ( WM_USER + 5 )

#define TID_HIDE_MOUSE  0x1010

static const struct
{
    USHORT i_pmkey;
    int    i_vlckey;
} pmkeys_to_vlckeys[] =
{
    { VK_LEFT, KEY_LEFT },
    { VK_RIGHT, KEY_RIGHT },
    { VK_UP, KEY_UP },
    { VK_DOWN, KEY_DOWN },
    { VK_SPACE, ' ' },
    { VK_NEWLINE, KEY_ENTER },
    { VK_ENTER, KEY_ENTER },
    { 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_HOME, KEY_HOME },
    { VK_END, KEY_END },
    { VK_INSERT, KEY_INSERT },
    { VK_DELETE, KEY_DELETE },
/*
    Not supported
    {, KEY_MENU },
*/
    { VK_ESC, KEY_ESC },
    { VK_PAGEUP, KEY_PAGEUP },
    { VK_PAGEDOWN, KEY_PAGEDOWN },
    { VK_TAB, KEY_TAB },
    { VK_BACKSPACE, KEY_BACKSPACE },
/*
    Not supported
    {, KEY_MOUSEWHEELUP },
    {, KEY_MOUSEWHEELDOWN },
    {, KEY_MOUSEWHEELLEFT },
    {, KEY_MOUSEWHEELRIGHT },

    {, KEY_BROWSER_BACK },
    {, KEY_BROWSER_FORWARD },
    {, KEY_BROWSER_REFRESH },
    {, KEY_BROWSER_STOP },
    {, KEY_BROWSER_SEARCH },
    {, KEY_BROWSER_FAVORITES },
    {, KEY_BROWSER_HOME },
    {, KEY_VOLUME_MUTE },
    {, KEY_VOLUME_DOWN },
    {, KEY_VOLUME_UP },
    {, KEY_MEDIA_NEXT_TRACK },
    {, KEY_MEDIA_PREV_TRACK },
    {, KEY_MEDIA_STOP },
    {, KEY_MEDIA_PLAY_PAUSE },
*/

    { 0, 0 }
};

static int ConvertKey( USHORT i_pmkey )
{
    int i;
    for( i = 0; pmkeys_to_vlckeys[ i ].i_pmkey != 0; i++ )
    {
        if( pmkeys_to_vlckeys[ i ].i_pmkey == i_pmkey )
            return pmkeys_to_vlckeys[ i ].i_vlckey;
    }
    return 0;
}

static void MousePressed( vlc_window_t *wnd, HWND hwnd, unsigned button )
{
    vout_window_sys_t *sys = wnd->sys;

    if( WinQueryFocus( HWND_DESKTOP ) != hwnd )
        WinSetFocus( HWND_DESKTOP, hwnd );

    if( !sys->button_pressed )
        WinSetCapture( HWND_DESKTOP, 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 )
        WinSetCapture( HWND_DESKTOP, NULLHANDLE );

    vlc_window_ReportMouseReleased(wnd, button);
}

#define WM_MOUSELEAVE   0x41F

static MRESULT EXPENTRY WndProc( HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2 )
{
    vlc_window_t *wnd = WinQueryWindowPtr( hwnd, 0 );
    MRESULT result = ( MRESULT )TRUE;

    if ( !wnd )
        return WinDefWindowProc( hwnd, msg, mp1, mp2 );

    vout_window_sys_t *sys = wnd->sys;
    RECTL rcl;
    SWP   swp;

    if ( sys->is_mouse_hidden &&
         (( msg >= WM_MOUSEFIRST    && msg <= WM_MOUSELAST ) ||
          ( msg >= WM_EXTMOUSEFIRST && msg <= WM_EXTMOUSELAST ) ||
            msg == WM_MOUSELEAVE ))
    {
        WinShowPointer( HWND_DESKTOP, TRUE );
        sys->is_mouse_hidden = false;

        WinStartTimer( sys->hab, sys->client, TID_HIDE_MOUSE,
                       sys->cursor_timeout );
    }

    switch( msg )
    {
        /* the user wants to close the window */
        case WM_CLOSE:
            vlc_window_ReportClose( wnd );
            WinPostMsg( hwnd, WM_QUIT, 0, 0 );
            result = 0;
            break;

        case WM_SIZE:
            vlc_window_ReportSize( wnd,
                                    SHORT1FROMMP( mp2 ), SHORT2FROMMP( mp2 ));
            result = 0;
            break;

        case WM_MOUSEMOVE:
        {
            SHORT i_mouse_x = SHORT1FROMMP( mp1 );
            SHORT i_mouse_y = SHORT2FROMMP( mp1 );

            WinQueryWindowRect( hwnd, &rcl );

            // Invert Y
            int h = rcl.yTop - rcl.yBottom;
            i_mouse_y = ( h - i_mouse_y ) - 1;

            vlc_window_ReportMouseMoved( wnd, i_mouse_x, i_mouse_y );

            result = WinDefWindowProc( hwnd, msg, mp1, mp2 );
            break;
        }

        case WM_BUTTON1DOWN :
            MousePressed( wnd, hwnd, MOUSE_BUTTON_LEFT );
            break;

        case WM_BUTTON2DOWN :
            MousePressed( wnd, hwnd, MOUSE_BUTTON_RIGHT );
            break;

        case WM_BUTTON3DOWN :
            MousePressed( wnd, hwnd, MOUSE_BUTTON_CENTER );
            break;

        case WM_BUTTON1UP :
            MouseReleased( wnd, MOUSE_BUTTON_LEFT );
            break;

        case WM_BUTTON2UP :
            MouseReleased( wnd, MOUSE_BUTTON_RIGHT );
            break;

        case WM_BUTTON3UP :
            MouseReleased( wnd, MOUSE_BUTTON_CENTER );
            break;

        case WM_BUTTON1DBLCLK :
            vlc_window_ReportMouseDoubleClick( wnd, MOUSE_BUTTON_LEFT );
            break;

        case WM_BUTTON2DBLCLK :
            vlc_window_ReportMouseDoubleClick( wnd, MOUSE_BUTTON_RIGHT );
            break;

        case WM_BUTTON3DBLCLK :
            vlc_window_ReportMouseDoubleClick( wnd, MOUSE_BUTTON_CENTER );
            break;

        case WM_TRANSLATEACCEL :
            /* We have no accelerator table at all */
            result = ( MRESULT )FALSE;
            break;

        case WM_CHAR :
        {
            USHORT i_flags = SHORT1FROMMP( mp1 );
            USHORT i_ch    = SHORT1FROMMP( mp2 );
            USHORT i_vk    = SHORT2FROMMP( mp2 );
            int    i_key   = 0;

            if( !( i_flags & KC_KEYUP ))
            {
                if( i_flags & KC_VIRTUALKEY )
                    /* convert the key if possible */
                    i_key = ConvertKey( i_vk );
                else if(( i_flags & KC_CHAR ) && !HIBYTE( i_ch ))
                    i_key = tolower( i_ch );

                if( i_key )
                {
                    if( i_flags & KC_SHIFT )
                       i_key |= KEY_MODIFIER_SHIFT;

                    if( i_flags & KC_CTRL )
                        i_key |= KEY_MODIFIER_CTRL;

                    if( i_flags & KC_ALT )
                        i_key |= KEY_MODIFIER_ALT;

                    vlc_window_ReportKeyPress( wnd, i_key );
                }
            }
            break;
        }

        case WM_TIMER :
            if( !sys->is_mouse_hidden &&
                SHORT1FROMMP( mp1 ) == TID_HIDE_MOUSE )
            {
                POINTL ptl;

                WinQueryPointerPos( HWND_DESKTOP, &ptl );
                if( WinWindowFromPoint( HWND_DESKTOP, &ptl, TRUE )
                        == sys->client )
                {
                    WinShowPointer( HWND_DESKTOP, FALSE );
                    sys->is_mouse_hidden = true;

                    WinStopTimer( sys->hab, sys->client, TID_HIDE_MOUSE );
                }
            }
            break;

        case WM_VLC_ENABLE :
        {
            if( !LONGFROMMP( mp1 ))
            {
                /* Disable */
                WinShowWindow( sys->frame, FALSE );
                break;
            }

            /* Enable */
            vlc_window_cfg_t *cfg = PVOIDFROMMP( mp2 );

            if( cfg->is_decorated )
            {
                WinSetParent( WinWindowFromID( sys->frame, FID_SYSMENU ),
                              sys->frame, FALSE );
                WinSetParent( WinWindowFromID( sys->frame, FID_TITLEBAR ),
                              sys->frame, FALSE );
                WinSetParent( WinWindowFromID( sys->frame, FID_MINMAX ),
                              sys->frame, FALSE );

                WinSetWindowBits( sys->frame, QWL_STYLE,
                                  FS_SIZEBORDER, FS_SIZEBORDER );
            }
            else
            {
                WinSetParent( WinWindowFromID( sys->frame, FID_SYSMENU ),
                              HWND_OBJECT, FALSE );
                WinSetParent( WinWindowFromID( sys->frame, FID_TITLEBAR ),
                              HWND_OBJECT, FALSE );
                WinSetParent( WinWindowFromID( sys->frame, FID_MINMAX ),
                              HWND_OBJECT, FALSE );

                WinSetWindowBits( sys->frame, QWL_STYLE, 0, FS_SIZEBORDER );
            }

            /* 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");
            WinEnableWindow( hwnd, b_mouse_support || b_key_support );

            sys->client_rect.xLeft   = ( sys->i_screen_width - cfg->width )
                                       / 2;
            sys->client_rect.yBottom = ( sys->i_screen_height - cfg->height )
                                       / 2;
            sys->client_rect.xRight  = sys->client_rect.xLeft + cfg->width;
            sys->client_rect.yTop    = sys->client_rect.yBottom + cfg->height;

            if( cfg->is_fullscreen )
                WinQueryWindowRect( HWND_DESKTOP, &rcl );
            else
                rcl = sys->client_rect;

            WinCalcFrameRect( sys->frame, &rcl, FALSE );

            WinSetWindowPos( sys->frame, HWND_TOP,
                             rcl.xLeft, rcl.yBottom,
                             rcl.xRight - rcl.xLeft, rcl.yTop - rcl.yBottom,
                             SWP_ZORDER | SWP_MOVE | SWP_SIZE |
                             SWP_SHOW | SWP_ACTIVATE );

            free( cfg );
            break;
        }

        case WM_VLC_RESIZE :
            rcl.xLeft   = 0;
            rcl.yBottom = 0;
            rcl.xRight  = LONGFROMMP( mp1 );
            rcl.yTop    = LONGFROMMP( mp2 );
            WinCalcFrameRect( sys->frame, &rcl, FALSE );

            WinSetWindowPos( sys->frame, NULLHANDLE,
                             0, 0,
                             rcl.xRight - rcl.xLeft, rcl.yTop - rcl.yBottom,
                             SWP_SIZE );

            WinQueryWindowPos( sys->frame, &swp );
            sys->client_rect.xLeft   = swp.x;
            sys->client_rect.yBottom = swp.y;
            sys->client_rect.xRight  = sys->client_rect.xLeft   + swp.cx;
            sys->client_rect.yTop    = sys->client_rect.yBottom + swp.cy;
            WinCalcFrameRect( sys->frame, &sys->client_rect, TRUE );
            break;

        case WM_VLC_SETSTATE:
            /* TODO */
            break;

        case WM_VLC_FULLSCREEN:
            if( LONGFROMMP( mp1 ))
            {
                /* Fullscreen */
                WinQueryWindowPos( sys->frame, &swp );
                sys->client_rect.xLeft   = swp.x;
                sys->client_rect.yBottom = swp.y;
                sys->client_rect.xRight  = sys->client_rect.xLeft   + swp.cx;
                sys->client_rect.yTop    = sys->client_rect.yBottom + swp.cy;
                WinCalcFrameRect( sys->frame, &sys->client_rect, TRUE );

                WinQueryWindowRect( HWND_DESKTOP, &rcl );
            }
            else
            {
                /* Windowed */
                rcl = sys->client_rect;
            }

            WinCalcFrameRect( sys->frame, &rcl, FALSE );

            WinSetWindowPos( sys->frame, HWND_TOP,
                             rcl.xLeft, rcl.yBottom,
                             rcl.xRight - rcl.xLeft, rcl.yTop - rcl.yBottom,
                             SWP_MOVE | SWP_SIZE | SWP_ZORDER | SWP_SHOW |
                             SWP_ACTIVATE );
            break;

        case WM_VLC_SETTITLE :
        {
            char *title = PVOIDFROMMP( mp1 );

            WinSetWindowText( hwnd, title );

            free( title );
            break;
        }

        default :
            return WinDefWindowProc( hwnd, msg, mp1, mp2 );
    }

    return result;
}

static void MorphToPM( void )
{
    PPIB pib;

    DosGetInfoBlocks(NULL, &pib);

    /* Change flag from VIO to PM */
    if (pib->pib_ultype == 2)
        pib->pib_ultype = 3;
}

static void PMThread( void *arg )
{
    vlc_window_t *wnd = arg;
    vout_window_sys_t *sys = wnd->sys;
    ULONG i_frame_flags;
    QMSG qm;

    /* */
    MorphToPM();

    sys->hab = WinInitialize( 0 );
    sys->hmq = WinCreateMsgQueue( sys->hab, 0);

    WinRegisterClass( sys->hab,
                      WC_VLC_WINDOW_OS2,
                      WndProc,
                      CS_SIZEREDRAW | CS_MOVENOTIFY,
                      sizeof( PVOID ));

    i_frame_flags = FCF_SYSMENU | FCF_TITLEBAR | FCF_MINMAX |
                    FCF_SIZEBORDER | FCF_TASKLIST;

    sys->frame =
        WinCreateStdWindow( HWND_DESKTOP,       /* parent window handle */
                            WS_VISIBLE,         /* frame window style */
                            &i_frame_flags,     /* window style */
                            WC_VLC_WINDOW_OS2,  /* class name */
                            "VLC Video Window", /* window title */
                            0L,                 /* default client style */
                            NULLHANDLE,         /* resource in exe file */
                            1,                  /* frame window id */
                            &sys->client );     /* client window handle */

    if( sys->frame == NULLHANDLE )
    {
        msg_Err( wnd, "cannot create a frame window");

        goto exit_error;
    }

    WinSetWindowPtr( sys->client, 0, wnd );

    sys->cursor_timeout =
        ( ULONG )var_InheritInteger( wnd, "mouse-hide-timeout" );
    WinStartTimer( sys->hab, sys->client, TID_HIDE_MOUSE,
                   sys->cursor_timeout );

    sys->i_screen_width  = WinQuerySysValue( HWND_DESKTOP, SV_CXSCREEN );
    sys->i_screen_height = WinQuerySysValue( HWND_DESKTOP, SV_CYSCREEN );

    /* Prevent SIG_FPE */
    _control87( MCW_EM, MCW_EM );

    sys->i_result = VLC_SUCCESS;
    DosPostEventSem( sys->ack_event );

    while( WinGetMsg( sys->hab, &qm, NULLHANDLE, 0, 0 ))
        WinDispatchMsg( sys->hab, &qm );

    WinDestroyWindow( sys->frame );

    if( sys->is_mouse_hidden )
        WinShowPointer( HWND_DESKTOP, TRUE );

exit_error:
    WinDestroyMsgQueue( sys->hmq );
    WinTerminate( sys->hab );

    sys->i_result = VLC_EGENERIC;
    DosPostEventSem( sys->ack_event );
}

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

static int Open( vlc_window_t *wnd )
{
    vout_window_sys_t *sys;

    wnd->sys = sys = calloc( 1, sizeof( *sys ));
    if( !sys )
        return VLC_ENOMEM;

    DosCreateEventSem( NULL, &sys->ack_event, 0, FALSE );

    sys->tid = _beginthread( PMThread, NULL, 1024 * 1024, wnd );
    DosWaitEventSem( sys->ack_event, SEM_INDEFINITE_WAIT );

    if( sys->i_result != VLC_SUCCESS )
    {
        DosCloseEventSem( sys->ack_event );

        free( sys );

        return VLC_EGENERIC;
    }

    wnd->type = VLC_WINDOW_TYPE_HWND;
    wnd->handle.hwnd = ( void * )sys->client;
    wnd->ops = &ops;
    wnd->info.has_double_click = true;

    return VLC_SUCCESS;
}

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

    vlc_window_cfg_t *config = malloc( sizeof( *config ));
    if( !config )
        return VLC_ENOMEM;

    memcpy( config, cfg, sizeof( *config ));
    WinPostMsg( sys->client, WM_VLC_ENABLE,
                MPFROMLONG( TRUE ), MPFROMP( config ) );

    return VLC_SUCCESS;
}

static void Disable( struct vlc_window *wnd )
{
    vout_window_sys_t *sys = wnd->sys;

    WinPostMsg( sys->client, WM_VLC_ENABLE, MPFROMLONG( FALSE ), 0 );
}

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

    WinPostMsg( sys->client, WM_VLC_RESIZE,
                MPFROMLONG( width ), MPFROMLONG( height ));
}

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

    WinPostQueueMsg( sys->hmq, WM_QUIT, 0, 0 );

    DosWaitThread( &sys->tid, DCWW_WAIT );

    DosCloseEventSem( sys->ack_event );

    free( sys );
}

static void SetState( vlc_window_t *wnd, unsigned state )
{
    vout_window_sys_t *sys = wnd->sys;

    WinPostMsg( sys->client, WM_VLC_SETSTATE, MPFROMLONG( state ), 0 );
}

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

    WinPostMsg( sys->client, WM_VLC_FULLSCREEN, MPFROMLONG( FALSE ), 0 );
}

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

    WinPostMsg( sys->client, WM_VLC_FULLSCREEN, MPFROMLONG( TRUE ), 0 );
}

static void SetTitle(vlc_window_t *wnd, const char *title)
{
    vout_window_sys_t *sys = wnd->sys;

    WinPostMsg( sys->client, WM_VLC_SETTITLE, MPFROMP( strdup( title )), 0 );
}
