/*****************************************************************************
 * ncurses.c : NCurses plugin for vlc
 *****************************************************************************
 * Copyright (C) 2001, 2002, 2003 VideoLAN
 * $Id: ncurses.c,v 1.4 2003/03/30 18:14:38 gbazin Exp $
 *
 * Authors: Samuel Hocevar <sam@zoy.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
 *****************************************************************************/

/*****************************************************************************
 * Preamble
 *****************************************************************************/
#include <stdlib.h>                                      /* malloc(), free() */
#include <string.h>
#include <errno.h>                                                 /* ENOMEM */
#include <stdio.h>
#include <time.h>

#include <curses.h>

#include <vlc/vlc.h>
#include <vlc/intf.h>
#include <vlc/vout.h>

/*****************************************************************************
 * Local prototypes.
 *****************************************************************************/
static int  Open           ( vlc_object_t * );
static void Close          ( vlc_object_t * );

static void Run            ( intf_thread_t * );
static void FullScreen     ( intf_thread_t * );
static void Play           ( intf_thread_t * );
static void Stop           ( intf_thread_t * );
static void Next           ( intf_thread_t * );
static void Eject          ( intf_thread_t * );
static void Pause          ( intf_thread_t * );
static void PrevTitle      ( intf_thread_t * );
static void NextTitle      ( intf_thread_t * );
static void PrevChapter    ( intf_thread_t * );
static void NextChapter    ( intf_thread_t * );

static int  HandleKey      ( intf_thread_t *, int );
static void Redraw           ( intf_thread_t *, time_t * );
static int  PrintFullLine  ( const char *p_fmt, ... );
static void ManageSlider   ( intf_thread_t * );

/*****************************************************************************
 * Module descriptor
 *****************************************************************************/
vlc_module_begin();
    set_description( _("ncurses interface") );
    set_capability( "interface", 10 );
    set_callbacks( Open, Close );
    add_shortcut( "curses" );
vlc_module_end();

/*****************************************************************************
 * intf_sys_t: description and status of ncurses interface
 *****************************************************************************/
struct intf_sys_t
{
    input_thread_t *    p_input;

    float               f_slider_state;
    float               f_slider_state_old;
};

/*****************************************************************************
 * Open: initialize and create window
 *****************************************************************************/
static int Open( vlc_object_t *p_this )
{
    intf_thread_t *p_intf = (intf_thread_t *)p_this;

    /* Allocate instance and initialize some members */
    p_intf->p_sys = malloc( sizeof( intf_sys_t ) );
    if( p_intf->p_sys == NULL )
    {
        msg_Err( p_intf, "out of memory" );
        return VLC_ENOMEM;
    }

    p_intf->p_sys->p_input = NULL;

    p_intf->pf_run = Run;

    /* Initialize the curses library */
    initscr();
    /* Don't do NL -> CR/NL */
    nonl();
    /* Take input chars one at a time */
    cbreak();
    /* Don't echo */
    noecho();

    curs_set(0);
    timeout(0);

    clear();

    return VLC_SUCCESS;
}

/*****************************************************************************
 * Close: destroy interface window
 *****************************************************************************/
static void Close( vlc_object_t *p_this )
{
    intf_thread_t *p_intf = (intf_thread_t *)p_this;

    if( p_intf->p_sys->p_input )
    {
        vlc_object_release( p_intf->p_sys->p_input );
    }

    /* Close the ncurses interface */
    endwin();

    /* Destroy structure */
    free( p_intf->p_sys );
}

/*****************************************************************************
 * Run: ncurses thread
 *****************************************************************************/
static void Run( intf_thread_t *p_intf )
{
    signed char i_key;
    time_t t_last_refresh;

    /*
     * force drawing the interface for the first time
     */
    t_last_refresh = ( time( 0 ) - 1);

    while( !p_intf->b_die )
    {
        msleep( INTF_IDLE_SLEEP );

        /* Update the input */
        if( p_intf->p_sys->p_input == NULL )
        {
            p_intf->p_sys->p_input = vlc_object_find( p_intf, VLC_OBJECT_INPUT,
                                                              FIND_ANYWHERE );
        }
        else if( p_intf->p_sys->p_input->b_dead )
        {
            vlc_object_release( p_intf->p_sys->p_input );
            p_intf->p_sys->p_input = NULL;
        }

        while( (i_key = getch()) != -1 )
        {
            /*
             * HandleKey returns 1 if the screen needs to be redrawn
             */
            if ( HandleKey( p_intf, i_key ) )
            {
                Redraw( p_intf, &t_last_refresh );
            }
        }

        /*
         * redraw the screen every second
         */
        if ( (time(0) - t_last_refresh) >= 1 )
        {
            ManageSlider ( p_intf );
            Redraw( p_intf, &t_last_refresh );
        }
    }
}

/* following functions are local */

static int HandleKey( intf_thread_t *p_intf, int i_key )
{
    switch( i_key )
    {
        case 'q':
        case 'Q':
            p_intf->b_die = 1;
            return 0;

        case 'f':
            FullScreen( p_intf );
            return 1;

        case 'p':
            Play( p_intf );
            return 1;

        case ' ':
            Pause( p_intf );
            return 1;

        case 's':
            Stop( p_intf );
            return 1;

        case 'n':
            Next( p_intf );
            return 1;

        case 'e':
            Eject( p_intf );
            return 1;

        case '[':
            PrevTitle( p_intf );
            break;

        case ']':
            NextTitle( p_intf );
            break;

        case '<':
            PrevChapter( p_intf );
            break;

        case '>':
            NextChapter( p_intf );
            break;

        case KEY_RIGHT:
            p_intf->p_sys->f_slider_state += 100;
            ManageSlider ( p_intf );
            break;

        case KEY_LEFT:
            p_intf->p_sys->f_slider_state--;
            ManageSlider ( p_intf );
            break;

        /*
         * ^l should clear and redraw the screen
         */
        case 0x0c:
            clear();
            return 1;

        default:
            break;
    }

    return 0;
}

static int PrintFullLine ( const char *p_fmt, ... )
{
    va_list  vl_args;
    char *    p_buf        = NULL;
    int       i_len;

    va_start ( vl_args, p_fmt );
    vasprintf ( &p_buf, p_fmt, vl_args );
    va_end ( vl_args );

    if ( p_buf == NULL )
    {
        return -1;
    }

    i_len = strlen( p_buf );

    /*
     * make sure we don't exceed the border on the right side
     */
    if ( i_len > COLS )
    {
        p_buf[COLS] = '\0';
        i_len = COLS;
        printw( "%s", p_buf );
    }
    else
    {
        printw( "%s", p_buf );
        hline( ' ', COLS - i_len );
    }

    free ( p_buf );

    return i_len;
}

static void
Redraw ( intf_thread_t *p_intf, time_t *t_last_refresh )
{
    int row = 0;

    move ( row, 0 );

    attrset ( A_REVERSE );
    PrintFullLine( VOUT_TITLE " (ncurses interface)" );
    attroff ( A_REVERSE );

    row++;

    row++;
    move ( row, 0 );

    if ( p_intf->p_sys->p_input != NULL )
    {
        PrintFullLine ( " DVD Chapter:%3d     DVD Title:%3d",
            p_intf->p_sys->p_input->stream.p_selected_area->i_part,
            p_intf->p_sys->p_input->stream.p_selected_area->i_id );
    }

    row++;
    mvaddch ( row, 0, ACS_ULCORNER );
    mvhline ( row, 1, ACS_HLINE, COLS-2 );
    mvaddch ( row, COLS-1, ACS_URCORNER );

    row++;
    mvaddch ( row, 0, ACS_VLINE );
    attrset ( A_REVERSE );
    mvhline ( row, 1, ' ', ( (int) p_intf->p_sys->f_slider_state % COLS-2) );
    attroff ( A_REVERSE );
    mvaddch ( row, COLS-1, ACS_VLINE );

    row++;
    mvaddch ( row, 0, ACS_LLCORNER );
    mvhline ( row, 1, ACS_HLINE, COLS-2 );
    mvaddch ( row, COLS-1, ACS_LRCORNER );

    refresh();

    *t_last_refresh = time( 0 );
}

static void FullScreen( intf_thread_t *p_intf )
{
    vout_thread_t *p_vout;

    p_vout = vlc_object_find( p_intf->p_sys->p_input,
                              VLC_OBJECT_VOUT, FIND_CHILD );
    if( p_vout == NULL )
    {
        return;
    }

    p_vout->i_changes |= VOUT_FULLSCREEN_CHANGE;
    vlc_object_release( p_vout );
}

static void Eject ( intf_thread_t *p_intf )
{
    char *psz_device = NULL, *psz_parser, *psz_name;

    /*
     * Get the active input
     * Determine whether we can eject a media, ie it's a VCD or DVD
     * If it's neither a VCD nor a DVD, then return
     */

    playlist_t * p_playlist = vlc_object_find( p_intf, VLC_OBJECT_PLAYLIST,
                                                       FIND_ANYWHERE );

    if( p_playlist == NULL )
    {
        return;
    }

    vlc_mutex_lock( &p_playlist->object_lock );

    if( p_playlist->i_index < 0 )
    {
        vlc_mutex_unlock( &p_playlist->object_lock );
        vlc_object_release( p_playlist );
        return;
    }

    psz_name = p_playlist->pp_items[ p_playlist->i_index ]->psz_name;

    if( psz_name )
    {
        if( !strncmp(psz_name, "dvd:", 4) )
        {
            switch( psz_name[4] )
            {
            case '\0':
            case '@':
                psz_device = config_GetPsz( p_intf, "dvd_device" );
                break;
            default:
                /* Omit the first 4 characters */
                psz_device = strdup( psz_name + 4 );
                break;
            }
        }
        else if( !strncmp(psz_name, "vcd:", 4) )
        {
            switch( psz_name[4] )
            {
            case '\0':
            case '@':
                psz_device = config_GetPsz( p_intf, "vcd_device" );
                break;
            default:
                /* Omit the first 4 characters */
                psz_device = strdup( psz_name + 4 );
                break;
            }
        }
        else
        {
            psz_device = strdup( psz_name );
        }
    }

    vlc_mutex_unlock( &p_playlist->object_lock );
    vlc_object_release( p_playlist );

    if( psz_device == NULL )
    {
        return;
    }

    /* Remove what we have after @ */
    psz_parser = psz_device;
    for( psz_parser = psz_device ; *psz_parser ; psz_parser++ )
    {
        if( *psz_parser == '@' )
        {
            *psz_parser = '\0';
            break;
        }
    }

    /* If there's a stream playing, we aren't allowed to eject ! */
    if( p_intf->p_sys->p_input == NULL )
    {
        msg_Dbg( p_intf, "ejecting %s", psz_device );

        intf_Eject( p_intf, psz_device );
    }

    free(psz_device);
    return;
}

static void Play ( intf_thread_t *p_intf )
{
    playlist_t * p_playlist = vlc_object_find( p_intf, VLC_OBJECT_PLAYLIST,
                                                       FIND_ANYWHERE );
    if( p_playlist )
    {
        vlc_mutex_lock( &p_playlist->object_lock );

        if( p_playlist->i_size )
        {
            vlc_mutex_unlock( &p_playlist->object_lock );
            playlist_Play( p_playlist );
            vlc_object_release( p_playlist );
        }
        else
        {
            vlc_mutex_unlock( &p_playlist->object_lock );
            vlc_object_release( p_playlist );
        }
    }
}

static void Pause ( intf_thread_t *p_intf )
{
    if( p_intf->p_sys->p_input == NULL )
    {
        return;
    }

    input_SetStatus( p_intf->p_sys->p_input, INPUT_STATUS_PAUSE );

    return;
}

static void Stop ( intf_thread_t *p_intf )
{
    playlist_t * p_playlist = vlc_object_find( p_intf, VLC_OBJECT_PLAYLIST,
                                                       FIND_ANYWHERE );
    if( p_playlist == NULL )
    {
        return;
    }

    playlist_Stop( p_playlist );
    vlc_object_release( p_playlist );

    return;
}

static void Next ( intf_thread_t *p_intf )
{
    input_area_t * p_area;
    int i_id;

    vlc_mutex_lock( &p_intf->p_sys->p_input->stream.stream_lock );
    i_id = p_intf->p_sys->p_input->stream.p_selected_area->i_id + 1;

    if ( i_id < p_intf->p_sys->p_input->stream.i_area_nb )
    {
        p_area = p_intf->p_sys->p_input->stream.pp_areas[i_id];
        vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );

        input_ChangeArea( p_intf->p_sys->p_input, p_area );
        input_SetStatus( p_intf->p_sys->p_input, INPUT_STATUS_PLAY );
    }

    vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );
}

static void ManageSlider ( intf_thread_t *p_intf )
{
    if( p_intf->p_sys->p_input != NULL )
    {
        vlc_mutex_lock( &p_intf->p_sys->p_input->stream.stream_lock );

        if( p_intf->p_sys->p_input->stream.b_seekable &&
            p_intf->p_sys->p_input->stream.control.i_status == PLAYING_S )
        {
            float newvalue = p_intf->p_sys->f_slider_state;

#define p_area p_intf->p_sys->p_input->stream.p_selected_area

            /* If the user hasn't touched the slider since the last time,
             * then the input can safely change it */
            if( newvalue == p_intf->p_sys->f_slider_state_old )
            {
                /* Update the value */
                p_intf->p_sys->f_slider_state =
                    p_intf->p_sys->f_slider_state_old =
                    ( 100 * p_area->i_tell ) / p_area->i_size;
            }
            /* Otherwise, send message to the input if the user has
             * finished dragging the slider */
            else
            {
                off_t i_seek = ( newvalue * p_area->i_size ) / 100;

                /* release the lock to be able to seek */
                vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );
                input_Seek( p_intf, i_seek, INPUT_SEEK_SET );
                vlc_mutex_lock( &p_intf->p_sys->p_input->stream.stream_lock );

                /* Update the old value */
                p_intf->p_sys->f_slider_state_old = newvalue;
            }
#    undef p_area
        }

        vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );
    }
}

static void PrevTitle ( intf_thread_t *p_intf )
{
    input_area_t *  p_area;
    int             i_id;

    vlc_mutex_lock( &p_intf->p_sys->p_input->stream.stream_lock );
    i_id = p_intf->p_sys->p_input->stream.p_selected_area->i_id - 1;

    if ( i_id > 0 )
    {
        p_area = p_intf->p_sys->p_input->stream.pp_areas[i_id];
        vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );

        input_ChangeArea( p_intf->p_sys->p_input, p_area );
        input_SetStatus( p_intf->p_sys->p_input, INPUT_STATUS_PLAY );
    }

    vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );
}

static void NextTitle ( intf_thread_t *p_intf )
{
    input_area_t *  p_area;
    int             i_id;

    vlc_mutex_lock( &p_intf->p_sys->p_input->stream.stream_lock );
    i_id = p_intf->p_sys->p_input->stream.p_selected_area->i_id + 1;

    if ( i_id < p_intf->p_sys->p_input->stream.i_area_nb )
    {
        p_area = p_intf->p_sys->p_input->stream.pp_areas[i_id];
        vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );

        input_ChangeArea( p_intf->p_sys->p_input, p_area );
        input_SetStatus( p_intf->p_sys->p_input, INPUT_STATUS_PLAY );
    }

    vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );
}

static void PrevChapter ( intf_thread_t *p_intf )
{
    input_area_t *  p_area;

    vlc_mutex_lock( &p_intf->p_sys->p_input->stream.stream_lock );
    p_area = p_intf->p_sys->p_input->stream.p_selected_area;

    if ( p_area->i_part > 0 )
    {
        p_area->i_part--;
        vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );

        input_ChangeArea( p_intf->p_sys->p_input, p_area );
        input_SetStatus( p_intf->p_sys->p_input, INPUT_STATUS_PLAY );
    }

    vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );
}

static void NextChapter( intf_thread_t *p_intf )
{
    input_area_t *  p_area;

    vlc_mutex_lock( &p_intf->p_sys->p_input->stream.stream_lock );
    p_area = p_intf->p_sys->p_input->stream.p_selected_area;

    if ( p_area->i_part < p_area->i_part_nb )
    {
        p_area->i_part++;
        vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );

        input_ChangeArea( p_intf->p_sys->p_input, p_area );
        input_SetStatus( p_intf->p_sys->p_input, INPUT_STATUS_PLAY );
    }

    vlc_mutex_unlock( &p_intf->p_sys->p_input->stream.stream_lock );
}

