/***************************************************************************
*             __________               __   ___.
*   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
*   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
*   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
*   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
*                     \/            \/     \/    \/            \/
* $Id$
*
* Plugin for video playback
* Reads raw image data + audio data from a file
* !!!!!!!!!! Code Police free zone !!!!!!!!!!
*
* Copyright (C) 2003-2004 Jörg Hohensohn aka [IDC]Dragon
*
* All files in this archive are subject to the GNU General Public License.
* See the file COPYING in the source tree root for full license agreement.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/


/****************** imports ******************/

#include "plugin.h"
#include "sh7034.h"
#include "system.h"
#include "../apps/recorder/widgets.h" /* not in search path, booh */

#ifndef SIMULATOR /* not for simulator by now */
#ifdef HAVE_LCD_BITMAP /* and definitely not for the Player, haha */

PLUGIN_HEADER

/* variable button definitions */
#if CONFIG_KEYPAD == RECORDER_PAD
#define VIDEO_STOP_SEEK BUTTON_PLAY
#define VIDEO_RESUME BUTTON_PLAY
#define VIDEO_DEBUG BUTTON_F1
#define VIDEO_CONTRAST_DOWN BUTTON_F2
#define VIDEO_CONTRAST_UP BUTTON_F3

#elif CONFIG_KEYPAD == ONDIO_PAD
#define VIDEO_STOP_SEEK_PRE BUTTON_MENU
#define VIDEO_STOP_SEEK (BUTTON_MENU | BUTTON_REL)
#define VIDEO_RESUME BUTTON_RIGHT
#define VIDEO_CONTRAST_DOWN (BUTTON_MENU | BUTTON_DOWN)
#define VIDEO_CONTRAST_UP (BUTTON_MENU | BUTTON_UP)

#endif
/****************** constants ******************/

#define SCREENSIZE (LCD_WIDTH*LCD_HEIGHT/8) /* in bytes */
#define FPS 68 /* default fps for headerless (old video-only) file */
#define MAX_ACC 20 /* maximum FF/FR speedup */
#define FF_TICKS 3000; /* experimentally found nice */

/* trigger levels, we need about 80 kB/sec */
#define SPINUP_INIT 5000 /* from what level on to refill, in milliseconds */
#define SPINUP_SAFETY 700 /* how much on top of the measured spinup time */
#define CHUNK (1024*32) /* read size */


/****************** prototypes ******************/
void timer4_isr(void); /* IMIA4 ISR */
int check_button(void); /* determine next relative frame */


/****************** data types ******************/

/* plugins don't introduce headers, so structs are repeated from rvf_format.h */

#define HEADER_MAGIC 0x52564668 /* "RVFh" at file start  */
#define AUDIO_MAGIC  0x41756446 /* "AudF" for each audio block */
#define FILEVERSION  100 /* 1.00 */

/* format type definitions */
#define VIDEOFORMAT_NO_VIDEO       0
#define VIDEOFORMAT_RAW            1
#define AUDIOFORMAT_NO_AUDIO       0
#define AUDIOFORMAT_MP3            1
#define AUDIOFORMAT_MP3_BITSWAPPED 2

/* bit flags */
#define FLAG_LOOP 0x00000001 /* loop the playback, e.g. for stills */

typedef struct /* contains whatever might be useful to the player */
{
    /* general info (16 entries = 64 byte) */
    unsigned long magic; /* HEADER_MAGIC */
    unsigned long version; /* file version */
    unsigned long flags; /* combination of FLAG_xx */
    unsigned long blocksize; /* how many bytes per block (=video frame) */
    unsigned long bps_average; /* bits per second of the whole stream */
    unsigned long bps_peak; /* max. of above (audio may be VBR) */
    unsigned long resume_pos; /* file position to resume to */
    unsigned long reserved[9]; /* reserved, should be zero */

    /* video info (16 entries = 64 byte) */
    unsigned long video_format; /* one of VIDEOFORMAT_xxx */
    unsigned long video_1st_frame; /* byte position of first video frame */
    unsigned long video_duration; /* total length of video part, in ms */
    unsigned long video_payload_size; /* total amount of video data, in bytes */
    unsigned long video_bitrate; /* derived from resolution and frame time, in bps */
    unsigned long video_frametime; /* frame interval in 11.0592 MHz clocks */
    long video_preroll; /* video is how much ahead, in 11.0592 MHz clocks */
    unsigned long video_width; /* in pixels */
    unsigned long video_height; /* in pixels */
    unsigned long video_reserved[7]; /* reserved, should be zero */

    /* audio info (16 entries = 64 byte) */
    unsigned long audio_format; /* one of AUDIOFORMAT_xxx */
    unsigned long audio_1st_frame; /* byte position of first video frame */
    unsigned long audio_duration; /* total length of audio part, in ms */
    unsigned long audio_payload_size; /* total amount of audio data, in bytes */
    unsigned long audio_avg_bitrate; /* average audio bitrate, in bits per second */
    unsigned long audio_peak_bitrate; /* maximum bitrate */
    unsigned long audio_headersize; /* offset to payload in audio frames */
    long audio_min_associated; /* minimum offset to video frame, in bytes */
    long audio_max_associated; /* maximum offset to video frame, in bytes */
    unsigned long audio_reserved[7]; /* reserved, should be zero */

    /* more to come... ? */

    /* Note: padding up to 'blocksize' with zero following this header */
} tFileHeader;

typedef struct /* the little header for all audio blocks */
{
    unsigned long magic; /* AUDIO_MAGIC indicates an audio block */
    unsigned char previous_block; /* previous how many blocks backwards */
    unsigned char next_block; /* next how many blocks forward */
    short associated_video; /* offset to block with corresponding video */
    unsigned short frame_start; /* offset to first frame starting in this block */
    unsigned short frame_end; /* offset to behind last frame ending in this block */
} tAudioFrameHeader;



/****************** globals ******************/

static struct plugin_api* rb; /* here is a global api struct pointer */
static char gPrint[32]; /* a global printf buffer, saves stack */


/* playstate */
static struct 
{
    enum 
    {
        paused,
        playing,
    } state;
    bool bAudioUnderrun;
    bool bVideoUnderrun;
    bool bHasAudio;
    bool bHasVideo;
    int nTimeOSD; /* OSD should stay for this many frames */
    bool bDirtyOSD; /* OSD needs redraw */
    bool bRefilling; /* set if refilling buffer */
    bool bSeeking;
    int nSeekAcc; /* accelleration value for seek */
    int nSeekPos; /* current file position for seek */
    bool bDiskSleep; /* disk is suspended */
#if FREQ == 12000000 /* Ondio speed kludge */
    int nFrameTimeAdjusted;
#endif
} gPlay;

/* buffer information */
static struct
{
    int bufsize;
    int granularity; /* common multiple of block and sector size */
    unsigned char* pBufStart; /* start of ring buffer */
    unsigned char* pBufEnd; /* end of ring buffer */
    unsigned char* pOSD; /* OSD memory (112 bytes for 112*8 pixels) */

    int vidcount; /* how many video blocks are known in a row */
    unsigned char* pBufFill; /* write pointer for disk, owned by main task */
    unsigned char* pReadVideo; /* video readout, maintained by timer ISR */
    unsigned char* pReadAudio; /* audio readout, maintained by demand ISR */
    bool bEOF; /* flag for end of file */
    int low_water; /* reload threshold  */
    int high_water; /* end of reload threshold */
    int spinup_safety; /* safety margin when recalculating low_water */
    int nReadChunk; /* how much data for normal buffer fill */
    int nSeekChunk; /* how much data while seeking */
} gBuf;

/* statistics */
static struct
{
    int minAudioAvail;
    int minVideoAvail;
    int nAudioUnderruns;
    int nVideoUnderruns;
    long minSpinup;
    long maxSpinup;
} gStats;

tFileHeader gFileHdr; /* file header */

/****************** implementation ******************/

/* tool function: return how much playable audio/video is left */
int Available(unsigned char* pSnapshot)
{
    if (pSnapshot <= gBuf.pBufFill)
        return gBuf.pBufFill - pSnapshot;
    else
        return gBuf.bufsize - (pSnapshot - gBuf.pBufFill);
}

/* debug function to draw buffer indicators */
void DrawBuf(void)
{
    int fill, video, audio;

    rb->memset(gBuf.pOSD, 0x10, LCD_WIDTH); /* draw line */
    gBuf.pOSD[0] = gBuf.pOSD[LCD_WIDTH-1] = 0xFE; /* ends */

    /* calculate new tick positions */
    fill = 1 + ((gBuf.pBufFill - gBuf.pBufStart) * (LCD_WIDTH-2)) / gBuf.bufsize;
    video = 1 + ((gBuf.pReadVideo - gBuf.pBufStart) * (LCD_WIDTH-2)) / gBuf.bufsize;
    audio = 1 + ((gBuf.pReadAudio - gBuf.pBufStart) * (LCD_WIDTH-2)) / gBuf.bufsize;

    gBuf.pOSD[fill] |= 0x20; /* below the line, two pixels */
    gBuf.pOSD[video] |= 0x08; /* one above */
    gBuf.pOSD[audio] |= 0x04; /* two above */

    if (gPlay.state == paused) /* we have to draw ourselves */
        rb->lcd_update_rect(0, LCD_HEIGHT-8, LCD_WIDTH, 8);
    else
        gPlay.bDirtyOSD = true; /* redraw it with next timer IRQ */
}


/* helper function to draw a position indicator */
void DrawPosition(int pos, int total)
{
    int w,h;
    int sec; /* estimated seconds */


    /* print the estimated position */   
    sec = pos / (gFileHdr.bps_average/8);
    if (sec < 100*60) /* fits into mm:ss format */
        rb->snprintf(gPrint, sizeof(gPrint), "%02d:%02dm", sec/60, sec%60);
    else /* a very long clip, hh:mm format */
        rb->snprintf(gPrint, sizeof(gPrint), "%02d:%02dh", sec/3600, (sec/60)%60);
    rb->lcd_puts(0, 7, gPrint);

    /* draw a slider over the rest of the line */
    rb->lcd_getstringsize(gPrint, &w, &h);
    w++;
    rb->scrollbar(w, LCD_HEIGHT-7, LCD_WIDTH-w, 7, total, 0, pos, HORIZONTAL);

    if (gPlay.state == paused) /* we have to draw ourselves */
        rb->lcd_update_rect(0, LCD_HEIGHT-8, LCD_WIDTH, 8);
    else /* let the display time do it */
    {
        gPlay.nTimeOSD = 70;
        gPlay.bDirtyOSD = true; /* redraw it with next timer IRQ */
    }
}


/* helper function to change the volume by a certain amount, +/- */
void ChangeVolume(int delta)
{
    int minvol = rb->sound_min(SOUND_VOLUME);
    int maxvol = rb->sound_max(SOUND_VOLUME);
    int vol = rb->global_settings->volume + delta;

    if (vol > maxvol) vol = maxvol;
    else if (vol < minvol) vol = minvol;
    if (vol != rb->global_settings->volume)
    {
        rb->sound_set(SOUND_VOLUME, vol);
        rb->global_settings->volume = vol;
        rb->snprintf(gPrint, sizeof(gPrint), "Vol: %d dB", vol);
        rb->lcd_puts(0, 7, gPrint);
        if (gPlay.state == paused) /* we have to draw ourselves */
            rb->lcd_update_rect(0, LCD_HEIGHT-8, LCD_WIDTH, 8);
        else /* let the display time do it */
        {
            gPlay.nTimeOSD = 50; /* display it for 50 frames */
            gPlay.bDirtyOSD = true; /* let the refresh copy it to LCD */
        }
    }
}


/* helper function to change the LCD contrast by a certain amount, +/- */
void ChangeContrast(int delta)
{
    static int mycontrast = -1; /* the "permanent" value while running */
    int contrast; /* updated value */

    if (mycontrast == -1)
        mycontrast = rb->global_settings->contrast;

    contrast = mycontrast + delta;
    if (contrast > 63) contrast = 63;
    else if (contrast < 5) contrast = 5;
    if (contrast != mycontrast)
    {
        rb->lcd_set_contrast(contrast);
        mycontrast = contrast;
        rb->snprintf(gPrint, sizeof(gPrint), "Contrast: %d", contrast);
        rb->lcd_puts(0, 7, gPrint);
        if (gPlay.state == paused) /* we have to draw ourselves */
            rb->lcd_update_rect(0, LCD_HEIGHT-8, LCD_WIDTH, 8);
        else /* let the display time do it */
        {
            gPlay.nTimeOSD = 50; /* display it for 50 frames */
            gPlay.bDirtyOSD = true; /* let the refresh copy it to LCD */
        }
    }
}


/* sync the video to the current audio */
void SyncVideo(void)
{
    tAudioFrameHeader* pAudioBuf;

    pAudioBuf = (tAudioFrameHeader*)(gBuf.pReadAudio);
    if (pAudioBuf->magic == AUDIO_MAGIC)
    {
        gBuf.vidcount = 0; /* nothing known */
        /* sync the video position */
        gBuf.pReadVideo = gBuf.pReadAudio + 
            (long)pAudioBuf->associated_video * (long)gFileHdr.blocksize;
    
        /* handle possible wrap */
        if (gBuf.pReadVideo >= gBuf.pBufEnd)
            gBuf.pReadVideo -= gBuf.bufsize;
        else if (gBuf.pReadVideo < gBuf.pBufStart)
            gBuf.pReadVideo += gBuf.bufsize;
    }
}


/* timer interrupt handler to display a frame */
void timer4_isr(void)
{
    int available;
    tAudioFrameHeader* pAudioBuf;
    int height; /* height to display */

    /* reduce height if we have OSD on */
    height = gFileHdr.video_height/8;        
    if (gPlay.nTimeOSD > 0)
    {
        gPlay.nTimeOSD--;
        height = MIN(LCD_HEIGHT/8-1, height); /* reserve bottom line */
        if (gPlay.bDirtyOSD)
        {   /* OSD to bottom line */
            rb->lcd_blit(gBuf.pOSD, 0, LCD_HEIGHT/8-1, 
                LCD_WIDTH, 1, LCD_WIDTH);
            gPlay.bDirtyOSD = false;
        }
    }

    rb->lcd_blit(gBuf.pReadVideo, 0, 0, 
        gFileHdr.video_width, height, gFileHdr.video_width);

    available = Available(gBuf.pReadVideo);

    /* loop to skip audio frame(s) */
    while(1)
    {
        /* just for the statistics */
        if (!gBuf.bEOF && available < gStats.minVideoAvail)
            gStats.minVideoAvail = available;

        if (available <= (int)gFileHdr.blocksize)
        {   /* no data for next frame */

            if (gBuf.bEOF && (gFileHdr.flags & FLAG_LOOP))
            {   /* loop now, assuming the looped clip fits in memory */
                gBuf.pReadVideo = gBuf.pBufStart + gFileHdr.video_1st_frame;
                /* FixMe: pReadVideo is incremented below */
            }
            else
            {
                gPlay.bVideoUnderrun = true;
                rb->timer_unregister(); /* disable ourselves */
                return; /* no data available */
            }
        }
        else /* normal advance for next time */
        {
            gBuf.pReadVideo += gFileHdr.blocksize;
            if (gBuf.pReadVideo >= gBuf.pBufEnd)
                gBuf.pReadVideo -= gBuf.bufsize; /* wraparound */
            available -= gFileHdr.blocksize;
        }

        if (!gPlay.bHasAudio)
            break; /* no need to skip any audio */
        
        if (gBuf.vidcount)
        {   
            /* we know the next is a video frame */
            gBuf.vidcount--;
            break; /* exit the loop */
        }
        
        pAudioBuf = (tAudioFrameHeader*)(gBuf.pReadVideo);
        if (pAudioBuf->magic == AUDIO_MAGIC)
        {   /* we ran into audio, can happen after seek */
            gBuf.vidcount = pAudioBuf->next_block;
            if (gBuf.vidcount)
                gBuf.vidcount--; /* minus the audio block */
        }
    } /* while */
}


/* ISR function to get more mp3 data */
void GetMoreMp3(unsigned char** start, int* size)
{
    int available;
    int advance;

    tAudioFrameHeader* pAudioBuf = (tAudioFrameHeader*)(gBuf.pReadAudio);

    advance = pAudioBuf->next_block * gFileHdr.blocksize;

    available = Available(gBuf.pReadAudio);

    /* just for the statistics */
    if (!gBuf.bEOF && available < gStats.minAudioAvail)
        gStats.minAudioAvail = available;
    
    if (available < advance + (int)gFileHdr.blocksize || advance == 0)
    {
        gPlay.bAudioUnderrun = true;
        return; /* no data available */
    }

    gBuf.pReadAudio += advance;
    if (gBuf.pReadAudio >= gBuf.pBufEnd)
        gBuf.pReadAudio -= gBuf.bufsize; /* wraparound */

    *start = gBuf.pReadAudio + gFileHdr.audio_headersize;
    *size = gFileHdr.blocksize - gFileHdr.audio_headersize;
}


int WaitForButton(void)
{
    int button;
    
    do
    {
        button = rb->button_get(true);
        rb->default_event_handler(button);
    } while ((button & BUTTON_REL) && button != SYS_USB_CONNECTED);
    
    return button;
}


bool WantResume(int fd)
{
    int button;

    rb->lcd_puts(0, 0, "Resume to this");
    rb->lcd_puts(0, 1, "last position?");
    rb->lcd_puts(0, 2, "PLAY = yes");
    rb->lcd_puts(0, 3, "Any Other = no");
    rb->lcd_puts(0, 4, " (plays from start)");
    DrawPosition(gFileHdr.resume_pos, rb->filesize(fd));
    rb->lcd_update();

    button = WaitForButton();
    return (button == VIDEO_RESUME);
}


int SeekTo(int fd, int nPos)
{
    int read_now, got_now;

    if (gPlay.bHasAudio)
        rb->mp3_play_stop(); /* stop audio ISR */
    if (gPlay.bHasVideo)
        rb->timer_unregister(); /* stop the timer */

    rb->lseek(fd, nPos, SEEK_SET);

    gBuf.pBufFill = gBuf.pBufStart; /* all empty */
    gBuf.pReadVideo = gBuf.pReadAudio = gBuf.pBufStart;

    read_now = gBuf.low_water - 1; /* less than low water, so loading will continue */
    read_now -= read_now % gBuf.granularity; /* round down to granularity */
    got_now = rb->read(fd, gBuf.pBufFill, read_now);
    gBuf.bEOF = (read_now != got_now);
    gBuf.pBufFill += got_now;

    if (nPos == 0)
    {   /* we seeked to the start */
        if (gPlay.bHasVideo)
            gBuf.pReadVideo += gFileHdr.video_1st_frame;

        if (gPlay.bHasAudio)
            gBuf.pReadAudio += gFileHdr.audio_1st_frame;
    }
    else
    {   /* we have to search for the positions */
        if (gPlay.bHasAudio) /* prepare audio playback, if contained */
        {
            /* search for audio frame */
            while (((tAudioFrameHeader*)(gBuf.pReadAudio))->magic != AUDIO_MAGIC)
                gBuf.pReadAudio += gFileHdr.blocksize;
            
            if (gPlay.bHasVideo)
                SyncVideo(); /* pick the right video for that */
        }
    }

    /* synchronous start */
    gPlay.state = playing;
    if (gPlay.bHasAudio)
    {
        gPlay.bAudioUnderrun = false;
        rb->mp3_play_data(gBuf.pReadAudio + gFileHdr.audio_headersize,
                gFileHdr.blocksize - gFileHdr.audio_headersize, GetMoreMp3);
        rb->mp3_play_pause(true); /* kickoff audio */
    }
    if (gPlay.bHasVideo)
    {
        gPlay.bVideoUnderrun = false;
        /* start display interrupt */
#if FREQ == 12000000 /* Ondio speed kludge */
        rb->timer_register(1, NULL, gPlay.nFrameTimeAdjusted, 1, timer4_isr);
#else
        rb->timer_register(1, NULL, gFileHdr.video_frametime, 1, timer4_isr);
#endif
    }

    return 0;
}

/* called from default_event_handler_ex() or at end of playback */
void Cleanup(void *fd)
{
    rb->close(*(int*)fd); /* close the file */

    if (gPlay.bHasVideo)
        rb->timer_unregister(); /* stop video ISR, now I can use the display again */

    if (gPlay.bHasAudio)
        rb->mp3_play_stop(); /* stop audio ISR */

    /* restore normal backlight setting */
    rb->backlight_set_timeout(rb->global_settings->backlight_timeout);

    /* restore normal contrast */
    rb->lcd_set_contrast(rb->global_settings->contrast);
}

/* returns >0 if continue, =0 to stop, <0 to abort (USB) */
int PlayTick(int fd)
{
    int button;
    static int lastbutton = 0;
    int avail_audio = -1, avail_video = -1;
    int retval = 1;
    int filepos;

    /* check buffer level */
    
    if (gPlay.bHasAudio)
        avail_audio = Available(gBuf.pReadAudio);
    if (gPlay.bHasVideo)
        avail_video = Available(gBuf.pReadVideo);

    if ((gPlay.bHasAudio && avail_audio < gBuf.low_water)
     || (gPlay.bHasVideo && avail_video < gBuf.low_water))
    {
        gPlay.bRefilling = true; /* go to refill mode */
    }

    if ((!gPlay.bHasAudio || gPlay.bAudioUnderrun)
     && (!gPlay.bHasVideo || gPlay.bVideoUnderrun)
     && gBuf.bEOF)
    {
        if (gFileHdr.resume_pos)
        {   /* we played til the end, clear resume position */
            gFileHdr.resume_pos = 0;
            rb->lseek(fd, 0, SEEK_SET); /* save resume position */
            rb->write(fd, &gFileHdr, sizeof(gFileHdr));
        }
        Cleanup(&fd);
        return 0; /* all expired */
    }

    if (!gPlay.bRefilling || gBuf.bEOF)
    {   /* nothing to do */
        button = rb->button_get_w_tmo(HZ/10);
    }
    else
    {   /* refill buffer */
        int read_now, got_now;
        int buf_free;
        long spinup; /* measure the spinup time */
        
        /* how much can we reload, don't fill completely, would appear empty */
        buf_free = gBuf.bufsize - MAX(avail_audio, avail_video) - gBuf.high_water;
        if (buf_free < 0)
            buf_free = 0; /* just for safety */
        buf_free -= buf_free % gBuf.granularity; /* round down to granularity */

        /* in one piece max. up to buffer end (wrap after that) */
        read_now = MIN(buf_free, gBuf.pBufEnd - gBuf.pBufFill);

        /* load piecewise, to stay responsive */
        read_now = MIN(read_now, gBuf.nReadChunk);

        if (read_now == buf_free)
            gPlay.bRefilling = false; /* last piece requested */

        spinup = *rb->current_tick; /* in case this is interesting below */
        
        got_now = rb->read(fd, gBuf.pBufFill, read_now);
        if (got_now != read_now || read_now == 0)
        {
            gBuf.bEOF = true;
            gPlay.bRefilling = false;
        }

        if (gPlay.bDiskSleep) /* statistics about the spinup time */
        {
            spinup = *rb->current_tick - spinup;
            gPlay.bDiskSleep = false;
            if (spinup > gStats.maxSpinup)
                gStats.maxSpinup = spinup;
            if (spinup < gStats.minSpinup)
                gStats.minSpinup = spinup;

            /* recalculate the low water mark from real measurements */
            gBuf.low_water = (gStats.maxSpinup + gBuf.spinup_safety) 
                             * gFileHdr.bps_peak / 8 / HZ;
        }

        if (!gPlay.bRefilling 
            && rb->global_settings->disk_spindown < 20) /* condition for test only */
        {
            rb->ata_sleep(); /* no point in leaving the disk run til timeout */
            gPlay.bDiskSleep = true;
        }

        gBuf.pBufFill += got_now;
        if (gBuf.pBufFill >= gBuf.pBufEnd)
            gBuf.pBufFill = gBuf.pBufStart; /* wrap */

        rb->yield(); /* have mercy with the other threads */
        button = rb->button_get(false);
    }

    /* check keypresses */

    if (button != BUTTON_NONE)
    {
        filepos = rb->lseek(fd, 0, SEEK_CUR);

        if (gPlay.bHasVideo) /* video position is more accurate */
            filepos -= Available(gBuf.pReadVideo); /* take video position */
        else
            filepos -= Available(gBuf.pReadAudio); /* else audio */
            
        switch (button) {   /* set exit conditions */
        case BUTTON_OFF:
            if (gFileHdr.magic == HEADER_MAGIC /* only if file has header */
                && !(gFileHdr.flags & FLAG_LOOP)) /* not for stills */
            {
                gFileHdr.resume_pos = filepos;
                rb->lseek(fd, 0, SEEK_SET); /* save resume position */
                rb->write(fd, &gFileHdr, sizeof(gFileHdr));
            }
            Cleanup(&fd);
            retval = 0; /* signal "stopped" to caller */
            break;
        case VIDEO_STOP_SEEK:
#ifdef VIDEO_STOP_SEEK_PRE
            if (lastbutton != VIDEO_STOP_SEEK_PRE)
                break;
#endif
            if (gPlay.bSeeking)
            {
                gPlay.bSeeking = false;
                gPlay.state = playing;
                SeekTo(fd, gPlay.nSeekPos);
            }
            else if (gPlay.state == playing)
            {
                gPlay.state = paused;
                if (gPlay.bHasAudio)
                    rb->mp3_play_pause(false); /* pause audio */
                if (gPlay.bHasVideo)
                    rb->timer_unregister(); /* stop the timer */
            }
            else if (gPlay.state == paused)
            {
                gPlay.state = playing;
                if (gPlay.bHasAudio)
                {
                    if (gPlay.bHasVideo)
                        SyncVideo();
                    rb->mp3_play_pause(true); /* play audio */
                }
                if (gPlay.bHasVideo)
                {   /* start the video */
#if FREQ == 12000000 /* Ondio speed kludge */
                    rb->timer_register(1, NULL,
                        gPlay.nFrameTimeAdjusted, 1, timer4_isr);
#else
                    rb->timer_register(1, NULL,
                        gFileHdr.video_frametime, 1, timer4_isr);
#endif
                }
            }
            break;
        case BUTTON_UP:
        case BUTTON_UP | BUTTON_REPEAT:
            if (gPlay.bHasAudio)
                ChangeVolume(1);
            break;
        case BUTTON_DOWN:
        case BUTTON_DOWN | BUTTON_REPEAT:
            if (gPlay.bHasAudio)
                ChangeVolume(-1);
            break;
        case BUTTON_LEFT:
        case BUTTON_LEFT | BUTTON_REPEAT:
            if (!gPlay.bSeeking) /* prepare seek */
            {
                gPlay.nSeekPos = filepos;
                gPlay.bSeeking = true;
                gPlay.nSeekAcc = 0;
            }
            else if (gPlay.nSeekAcc > 0) /* other direction, stop sliding */
                gPlay.nSeekAcc = 0;
            else
                gPlay.nSeekAcc--;
            break;
        case BUTTON_RIGHT:
        case BUTTON_RIGHT | BUTTON_REPEAT:
            if (!gPlay.bSeeking) /* prepare seek */
            {
                gPlay.nSeekPos = filepos;
                gPlay.bSeeking = true;
                gPlay.nSeekAcc = 0;
            }
            else if (gPlay.nSeekAcc < 0) /* other direction, stop sliding */
                gPlay.nSeekAcc = 0;
            else
                gPlay.nSeekAcc++;
            break;
#ifdef VIDEO_DEBUG
        case VIDEO_DEBUG: /* debug key */
        case VIDEO_DEBUG | BUTTON_REPEAT:
            DrawBuf(); /* show buffer status */
            gPlay.nTimeOSD = 30;
            gPlay.bDirtyOSD = true;
            break;
#endif
        case VIDEO_CONTRAST_DOWN: /* contrast down */
        case VIDEO_CONTRAST_DOWN | BUTTON_REPEAT:
            if (gPlay.bHasVideo)
                ChangeContrast(-1);
            break;
        case VIDEO_CONTRAST_UP: /* contrast up */
        case VIDEO_CONTRAST_UP | BUTTON_REPEAT:
            if (gPlay.bHasVideo)
                ChangeContrast(1);
            break;
        default:
            if (rb->default_event_handler_ex(button, Cleanup, &fd)
                == SYS_USB_CONNECTED)
                retval = -1; /* signal "aborted" to caller */
            break;
        }

        lastbutton = button;
    } /*  if (button != BUTTON_NONE) */

    
    /* handle seeking */
    
    if (gPlay.bSeeking) /* seeking? */
    {
        if (gPlay.nSeekAcc < -MAX_ACC)
            gPlay.nSeekAcc = -MAX_ACC;
        else if (gPlay.nSeekAcc > MAX_ACC)
            gPlay.nSeekAcc = MAX_ACC;
        
        gPlay.nSeekPos += gPlay.nSeekAcc * gBuf.nSeekChunk;
        if (gPlay.nSeekPos < 0)
            gPlay.nSeekPos = 0;
        if (gPlay.nSeekPos > rb->filesize(fd) - gBuf.granularity)
        {
            gPlay.nSeekPos = rb->filesize(fd);
            gPlay.nSeekPos -= gPlay.nSeekPos % gBuf.granularity;
        }
        DrawPosition(gPlay.nSeekPos, rb->filesize(fd));
    }


    /* check + recover underruns */
    
    if ((gPlay.bAudioUnderrun || gPlay.bVideoUnderrun) && !gBuf.bEOF)
    {
        gBuf.spinup_safety += HZ/2; /* add extra spinup time for the future */
        filepos = rb->lseek(fd, 0, SEEK_CUR);

        if (gPlay.bHasVideo && gPlay.bVideoUnderrun)
        {
            gStats.nVideoUnderruns++;
            filepos -= Available(gBuf.pReadVideo); /* take video position */
            SeekTo(fd, filepos);
        }
        else if (gPlay.bHasAudio && gPlay.bAudioUnderrun)
        {
            gStats.nAudioUnderruns++;
            filepos -= Available(gBuf.pReadAudio); /* else audio */
            SeekTo(fd, filepos);
        }
    }

    return retval;
}


int main(char* filename)
{
    int file_size;
    int fd; /* file descriptor handle */
    int read_now, got_now;
    int button = 0;
    int retval;

    /* try to open the file */
    fd = rb->open(filename, O_RDWR);
    if (fd < 0)
        return PLUGIN_ERROR;
    file_size =  rb->filesize(fd);
    
    /* reset pitch value to ensure synchronous playback */
    rb->sound_set_pitch(1000);

    /* init statistics */
    rb->memset(&gStats, 0, sizeof(gStats));
    gStats.minAudioAvail = gStats.minVideoAvail = INT_MAX;
    gStats.minSpinup = INT_MAX;

    /* init playback state */
    rb->memset(&gPlay, 0, sizeof(gPlay));

    /* init buffer */
    rb->memset(&gBuf, 0, sizeof(gBuf));
    gBuf.pOSD = rb->lcd_framebuffer + LCD_WIDTH*7; /* last screen line */
    gBuf.pBufStart = rb->plugin_get_audio_buffer(&gBuf.bufsize);
    /*gBuf.bufsize = 1700*1024; // test, like 2MB version!!!! */
    gBuf.pBufFill = gBuf.pBufStart; /* all empty */

    /* load file header */
    read_now = sizeof(gFileHdr);
    got_now = rb->read(fd, &gFileHdr, read_now);
    rb->lseek(fd, 0, SEEK_SET); /* rewind to restart sector-aligned */
    if (got_now != read_now)
    {
        rb->close(fd);
        return (PLUGIN_ERROR);
    }

    /* check header */
    if (gFileHdr.magic != HEADER_MAGIC)
    {   /* old file, use default info */
        rb->memset(&gFileHdr, 0, sizeof(gFileHdr));
        gFileHdr.blocksize = SCREENSIZE;
        if (file_size < SCREENSIZE * FPS) /* less than a second */
            gFileHdr.flags |= FLAG_LOOP;
        gFileHdr.video_format = VIDEOFORMAT_RAW;
        gFileHdr.video_width = LCD_WIDTH;
        gFileHdr.video_height = LCD_HEIGHT;
        gFileHdr.video_frametime = 11059200 / FPS;
        gFileHdr.bps_peak = gFileHdr.bps_average = LCD_WIDTH * LCD_HEIGHT * FPS;
    }

#if FREQ == 12000000 /* Ondio speed kludge, 625 / 576 == 12000000 / 11059200 */
    gPlay.nFrameTimeAdjusted = (gFileHdr.video_frametime * 625) / 576;
#endif

    /* continue buffer init: align the end, calc low water, read sizes */
    gBuf.granularity = gFileHdr.blocksize;
    while (gBuf.granularity % 512) /* common multiple of sector size */
        gBuf.granularity *= 2;
    gBuf.bufsize -= gBuf.bufsize % gBuf.granularity; /* round down */
    gBuf.pBufEnd = gBuf.pBufStart + gBuf.bufsize;
    gBuf.low_water = SPINUP_INIT * gFileHdr.bps_peak / 8000;
    gBuf.spinup_safety = SPINUP_SAFETY * HZ / 1000; /* in time ticks */
    if (gFileHdr.audio_min_associated < 0)
        gBuf.high_water = 0 - gFileHdr.audio_min_associated;
    else
        gBuf.high_water = 1; /* never fill buffer completely, would appear empty */
    gBuf.nReadChunk = (CHUNK + gBuf.granularity - 1); /* round up */
    gBuf.nReadChunk -= gBuf.nReadChunk % gBuf.granularity;/* and align */
    gBuf.nSeekChunk = rb->filesize(fd) / FF_TICKS;
    gBuf.nSeekChunk += gBuf.granularity - 1; /* round up */
    gBuf.nSeekChunk -= gBuf.nSeekChunk % gBuf.granularity; /* and align */

    /* prepare video playback, if contained */
    if (gFileHdr.video_format == VIDEOFORMAT_RAW)
    {
        gPlay.bHasVideo = true;
        if (rb->global_settings->backlight_timeout > 0)
            rb->backlight_set_timeout(1); /* keep the light on */
    }

    /* prepare audio playback, if contained */
    if (gFileHdr.audio_format == AUDIOFORMAT_MP3_BITSWAPPED)
    {
        gPlay.bHasAudio = true;
    }

    /* start playback by seeking to zero or resume position */
    if (gFileHdr.resume_pos && WantResume(fd)) /* ask the user */
        SeekTo(fd, gFileHdr.resume_pos);
    else
        SeekTo(fd, 0);

    /* all that's left to do is keep the buffer full */
    do /* the main loop */
    {
        retval = PlayTick(fd);
    } while (retval > 0);

    if (retval < 0) /* aborted? */
    {
        return PLUGIN_USB_CONNECTED;
    }

#ifndef DEBUG /* for release compilations, only display the stats in case of error */
    if (gStats.nAudioUnderruns || gStats.nVideoUnderruns)
#endif
    {
        /* display statistics */
        rb->lcd_clear_display();
        rb->snprintf(gPrint, sizeof(gPrint), "%d Audio Underruns", gStats.nAudioUnderruns);
        rb->lcd_puts(0, 0, gPrint);
        rb->snprintf(gPrint, sizeof(gPrint), "%d Video Underruns", gStats.nVideoUnderruns);
        rb->lcd_puts(0, 1, gPrint);
        rb->snprintf(gPrint, sizeof(gPrint), "%d MinAudio bytes", gStats.minAudioAvail);
        rb->lcd_puts(0, 2, gPrint);
        rb->snprintf(gPrint, sizeof(gPrint), "%d MinVideo bytes", gStats.minVideoAvail);
        rb->lcd_puts(0, 3, gPrint);
        rb->snprintf(gPrint, sizeof(gPrint), "MinSpinup %d.%02d", gStats.minSpinup/HZ, gStats.minSpinup%HZ);
        rb->lcd_puts(0, 4, gPrint);
        rb->snprintf(gPrint, sizeof(gPrint), "MaxSpinup %d.%02d", gStats.maxSpinup/HZ, gStats.maxSpinup%HZ);
        rb->lcd_puts(0, 5, gPrint);
        rb->snprintf(gPrint, sizeof(gPrint), "LowWater: %d", gBuf.low_water);
        rb->lcd_puts(0, 6, gPrint);
        rb->snprintf(gPrint, sizeof(gPrint), "HighWater: %d", gBuf.high_water);
        rb->lcd_puts(0, 7, gPrint);
 
        rb->lcd_update();
        button = WaitForButton();
    }
    return (button == SYS_USB_CONNECTED) ? PLUGIN_USB_CONNECTED : PLUGIN_OK;
}


/***************** Plugin Entry Point *****************/

enum plugin_status plugin_start(struct plugin_api* api, void* parameter)
{
    rb = api; /* copy to global api pointer */
    
    if (parameter == NULL)
    {
        rb->splash(HZ*2, true, "Play .rvf file!");
        return PLUGIN_ERROR;
    }

    /* now go ahead and have fun! */
    return main((char*) parameter);
}

#endif /* #ifdef HAVE_LCD_BITMAP */
#endif /* #ifndef SIMULATOR */

