/***************************************************************************
*             __________               __   ___.
*   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
*   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
*   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
*   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
*                     \/            \/     \/    \/            \/
* $Id$
*
* Copyright (C) 2005 Adam Boot
*
* 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.
*
****************************************************************************/

#include "plugin.h"
#include "button.h"
#include "lcd.h"

#ifdef HAVE_LCD_BITMAP

/* colors */
#ifdef HAVE_LCD_COLOR
#define COLOR_BLACK  LCD_RGBPACK(0,0,0)
#define COLOR_RED    LCD_RGBPACK(255,0,0)
#define COLOR_YELLOW LCD_RGBPACK(128,128,0)
#define COLOR_GREEN  LCD_RGBPACK(0,255,0)
#define COLOR_BLUE   LCD_RGBPACK(0,0,255)
#define COLOR_PINK   LCD_RGBPACK(255,0,255)
#define COLOR_PURPLE LCD_RGBPACK(128,0,128)
#define COLOR_ORANGE LCD_RGBPACK(255,128,64)
static unsigned jewel_color[8]={COLOR_BLACK, COLOR_RED, COLOR_ORANGE,
                                COLOR_PURPLE, COLOR_BLUE, COLOR_YELLOW,
                                COLOR_GREEN, COLOR_PINK};
#endif

/* save files */
#define SCORE_FILE PLUGIN_DIR "/bejeweled.score"
#define SAVE_FILE  PLUGIN_DIR "/bejeweled.save"

/* final game return status */
#define BJ_END  3
#define BJ_USB  2
#define BJ_QUIT 1
#define BJ_LOSE 0

/* button definitions */
#if CONFIG_KEYPAD == RECORDER_PAD
#define BEJEWELED_QUIT   BUTTON_OFF
#define BEJEWELED_START  BUTTON_ON
#define BEJEWELED_SELECT BUTTON_PLAY
#define BEJEWELED_RESUME BUTTON_F1

#elif CONFIG_KEYPAD == ONDIO_PAD
#define BEJEWELED_QUIT       BUTTON_OFF
#define BEJEWELED_START      BUTTON_RIGHT
#define BEJEWELED_SELECT     (BUTTON_MENU|BUTTON_REL)
#define BEJEWELED_SELECT_PRE BUTTON_MENU
#define BEJEWELED_RESUME     (BUTTON_MENU|BUTTON_OFF)

#elif (CONFIG_KEYPAD == IRIVER_H100_PAD) || (CONFIG_KEYPAD == IRIVER_H300_PAD)
#define BEJEWELED_QUIT   BUTTON_OFF
#define BEJEWELED_START  BUTTON_ON
#define BEJEWELED_SELECT BUTTON_SELECT
#define BEJEWELED_RESUME BUTTON_MODE

#else
    #error BEJEWELED: Unsupported keypad
#endif

/* swap directions */
#define SWAP_UP    0
#define SWAP_RIGHT 1
#define SWAP_DOWN  2
#define SWAP_LEFT  3

/* play board dimension */
#define BJ_HEIGHT 9
#define BJ_WIDTH  8

/* next level threshold */
#define LEVEL_PTS 100

/* sleep time for animations (1/x seconds) */
#define FALL_TIMER 50
#define SWAP_TIMER 30

#if (LCD_HEIGHT == 176) && (LCD_WIDTH == 220)
/* use 22x22 tiles */

/* size of a tile */
#define TILE_WIDTH  22
#define TILE_HEIGHT 22

/* number of high scores to save */
#define NUM_SCORES 10

/* bitmaps for the jewels */
static unsigned char jewel[8][66] = {
    /* empty */
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    /* square */
    {0x00, 0x00, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc,
     0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0x00, 0x00,
     0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
     0x00, 0x00, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
     0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x00, 0x00},
    /* plus */
    {0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xfe, 0xfe, 0xfe, 0x0e, 0x0e,
     0x0e, 0xfe, 0xfe, 0xfe, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00,
     0x00, 0x7f, 0x7f, 0x7f, 0x71, 0x71, 0xf1, 0xf1, 0xf1, 0x00, 0x00,
     0x00, 0xf1, 0xf1, 0xf1, 0x71, 0x71, 0x7f, 0x7f, 0x7f, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x0f, 0x0e, 0x0e,
     0x0e, 0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    /* triangle */
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf0, 0xfc, 0x7e,
     0xfc, 0xf0, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0xc0, 0xf0, 0xfc, 0x7f, 0x1f, 0x07, 0x01, 0x00,
     0x01, 0x07, 0x1f, 0x7f, 0xfc, 0xf0, 0xc0, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x0c, 0x0f, 0x0f, 0x0f, 0x0f, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
     0x0e, 0x0e, 0x0e, 0x0e, 0x0f, 0x0f, 0x0f, 0x0f, 0x0c, 0x00, 0x00},
    /* diamond */
    {0x00, 0x00, 0x00, 0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe,
     0xfc, 0xf8, 0xf0, 0xe0, 0xc0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x04, 0x0e, 0x1f, 0x3f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0x7f, 0x3f, 0x1f, 0x0e, 0x04, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x07, 0x0f,
     0x07, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    /* star */
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf8, 0xfe,
     0xf8, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x01, 0x03, 0x03, 0x07, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0x8f, 0x07, 0x03, 0x03, 0x01, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x0c, 0x07, 0x07, 0x03, 0x01, 0x00, 0x00,
     0x00, 0x01, 0x03, 0x07, 0x07, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00},
    /* circle */
    {0x00, 0x80, 0xe0, 0xf0, 0xf8, 0xfc, 0xfc, 0x7e, 0x3e, 0x3e, 0x3e,
     0x3e, 0x3e, 0x7e, 0xfc, 0xfc, 0xf8, 0xf0, 0xe0, 0x80, 0x00, 0x00,
     0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xe0, 0xc0, 0x80, 0x80, 0x80,
     0x80, 0x80, 0xc0, 0xe0, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x01, 0x03, 0x07, 0x07, 0x0f, 0x0f, 0x0f, 0x0f,
     0x0f, 0x0f, 0x0f, 0x07, 0x07, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00},
    /* heart */
    {0x00, 0xe0, 0xf8, 0xfc, 0xfe, 0xfe, 0xfe, 0xfc, 0xf8, 0xf0, 0xe0,
     0xf0, 0xf8, 0xfc, 0xfe, 0xfe, 0xfe, 0xfc, 0xf8, 0xe0, 0x00, 0x00,
     0x00, 0x03, 0x0f, 0x1f, 0x3f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0x7f, 0x3f, 0x1f, 0x0f, 0x03, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x07, 0x0f,
     0x07, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
};

#elif (LCD_HEIGHT == 128) && (LCD_WIDTH == 160)
/* use 16x16 tiles */

/* size of a tile */
#define TILE_WIDTH  16
#define TILE_HEIGHT 16

/* number of high scores to save */
#define NUM_SCORES 10

/* bitmaps for the jewels */
static unsigned char jewel[8][32] = {
    /* empty */
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    /* square */
    {0x00, 0x00, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc,
     0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0x00, 0x00,
     0x00, 0x00, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,
     0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x00, 0x00},
    /* plus */
    {0x00, 0xe0, 0xe0, 0x60, 0x60, 0x7e, 0x7e, 0x06,
     0x7e, 0x7e, 0x60, 0x60, 0xe0, 0xe0, 0x00, 0x00,
     0x00, 0x03, 0x03, 0x03, 0x03, 0x3f, 0x3f, 0x30,
     0x3f, 0x3f, 0x03, 0x03, 0x03, 0x03, 0x00, 0x00},
    /* triangle */
    {0x00, 0x00, 0x00, 0x00, 0xc0, 0xf0, 0x7c, 0x1e,
     0x7c, 0xf0, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x30, 0x3c, 0x3f, 0x37, 0x31, 0x30, 0x30,
     0x30, 0x31, 0x37, 0x3f, 0x3c, 0x30, 0x00, 0x00},
    /* diamond */
    {0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe,
     0xfc, 0xf8, 0xf0, 0xe0, 0xc0, 0x80, 0x00, 0x00,
     0x00, 0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f,
     0x1f, 0x0f, 0x07, 0x03, 0x01, 0x00, 0x00, 0x00},
    /* star */
    {0x00, 0x40, 0xc0, 0xc0, 0xc0, 0xc0, 0xf8, 0xfe,
     0xf8, 0xc0, 0xc0, 0xc0, 0xc0, 0x40, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x30, 0x1f, 0x1f, 0x0f, 0x07,
     0x0f, 0x1f, 0x1f, 0x30, 0x00, 0x00, 0x00, 0x00},
    /* circle */
    {0x00, 0xe0, 0xf8, 0xfc, 0x3c, 0x1e, 0x0e, 0x0e,
     0x0e, 0x1e, 0x3c, 0xfc, 0xf8, 0xe0, 0x00, 0x00,
     0x00, 0x03, 0x0f, 0x1f, 0x1e, 0x3c, 0x38, 0x38,
     0x38, 0x3c, 0x1e, 0x1f, 0x0f, 0x03, 0x00, 0x00},
    /* heart */
    {0x00, 0x78, 0xfc, 0xfe, 0xfe, 0xfc, 0xf8, 0xf0,
     0xf8, 0xfc, 0xfe, 0xfe, 0xfc, 0x78, 0x00, 0x00,
     0x00, 0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f,
     0x1f, 0x0f, 0x07, 0x03, 0x01, 0x00, 0x00, 0x00}
};

#elif (LCD_HEIGHT == 64) && (LCD_WIDTH == 112)
/* use 10x8 tiles */

/* size of a tile */
#define TILE_WIDTH 10
#define TILE_HEIGHT 8

/* number of high scores to save */
#define NUM_SCORES 8

/* bitmaps for the jewels */
static unsigned char jewel[8][10] = {
    /* empty */
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    /* square */
    {0x00, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x00, 0x00},
    /* plus */
    {0x00, 0x1c, 0x14, 0x77, 0x41, 0x41, 0x77, 0x14, 0x1c, 0x00},
    /* triangle */
    {0x60, 0x70, 0x5c, 0x46, 0x43, 0x46, 0x5c, 0x70, 0x60, 0x00},
    /* diamond */
    {0x00, 0x08, 0x1c, 0x3e, 0x7f, 0x7f, 0x3e, 0x1c, 0x08, 0x00},
    /* star */
    {0x00, 0x04, 0x6c, 0x3c, 0x1c, 0x1f, 0x3c, 0x6c, 0x04, 0x00},
    /* circle */
    {0x00, 0x1c, 0x3e, 0x63, 0x63, 0x63, 0x63, 0x3e, 0x1c, 0x00},
    /* heart */
    {0x06, 0x0f, 0x1f, 0x3e, 0x7c, 0x3e, 0x1f, 0x0f, 0x06, 0x00}
};

#else
  #error BEJEWELED: Unsupported LCD size
#endif

/* global rockbox api */
static struct plugin_api* rb;

/* the tile struct
 * type is the jewel number 0-7
 * falling if the jewel is falling
 * delete marks the jewel for deletion
 */
struct tile {
    int type;
    bool falling;
    bool delete;
};

/* the game context struct
 * score is the current level score
 * segments is the number of cleared segments in the current run
 * level is the current level
 * highscores is the list of high scores
 * resume denotes whether to resume the currently loaded game
 * dirty denotes whether the high scores are out of sync with the saved file
 * playboard is the game playing board (first row is hidden)
 */
struct game_context {
    unsigned int score;
    unsigned int segments;
    unsigned int level;
    unsigned short highscores[NUM_SCORES];
    bool resume;
    bool dirty;
    struct tile playboard[BJ_WIDTH][BJ_HEIGHT];
};

/*****************************************************************************
* bejeweled_init() initializes bejeweled data structures.
******************************************************************************/
static void bejeweled_init(struct game_context* bj) {
    /* seed the rand generator */
    rb->srand(*rb->current_tick);

    /* check for resumed game */
    if(bj->resume) {
        bj->resume = false;
        return;
    }

    /* reset scoring */
    bj->level = 1;
    bj->score = 0;
    bj->segments = 0;

    /* clear playing board */
    rb->memset(bj->playboard, 0, sizeof(bj->playboard));
}

/*****************************************************************************
* bejeweled_drawboard() redraws the entire game board.
******************************************************************************/
static void bejeweled_drawboard(struct game_context* bj) {
    int i, j;
    int w, h;
    unsigned int tempscore;
    char *title = "Level";
    char str[6];

    tempscore = (bj->score>LEVEL_PTS ? LEVEL_PTS : bj->score);

    /* clear screen */
    rb->lcd_clear_display();

    /* draw separator lines */
#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground(COLOR_BLACK);
#endif
    rb->lcd_vline(BJ_WIDTH*TILE_WIDTH, 0, LCD_HEIGHT);
    rb->lcd_hline(BJ_WIDTH*TILE_WIDTH, LCD_WIDTH, 18);
    rb->lcd_hline(BJ_WIDTH*TILE_WIDTH, LCD_WIDTH, LCD_HEIGHT-10);

    /* draw progress bar */
#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground(COLOR_BLUE);
#endif
    rb->lcd_fillrect(BJ_WIDTH*TILE_WIDTH+(LCD_WIDTH-BJ_WIDTH*TILE_WIDTH)/4,
                     (LCD_HEIGHT-10)-(((LCD_HEIGHT-10)-18)*
                         tempscore/LEVEL_PTS),
                     (LCD_WIDTH-BJ_WIDTH*TILE_WIDTH)/2,
                     ((LCD_HEIGHT-10)-18)*tempscore/LEVEL_PTS);

    /* dispay playing board */
    for(i=0; i<BJ_HEIGHT-1; i++){
        for(j=0; j<BJ_WIDTH; j++){
#ifdef HAVE_LCD_COLOR
            rb->lcd_set_foreground(jewel_color[bj->playboard[j][i+1].type]);
#endif
            rb->lcd_mono_bitmap(jewel[bj->playboard[j][i+1].type],
                                j*TILE_WIDTH, i*TILE_HEIGHT,
                                TILE_WIDTH, TILE_HEIGHT);
        }
    }

    /* print text */
#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground(COLOR_BLACK);
#endif
    rb->lcd_getstringsize(title, &w, &h);
    rb->lcd_putsxy(LCD_WIDTH-(LCD_WIDTH-BJ_WIDTH*TILE_WIDTH)/2-w/2, 1, title);

    rb->snprintf(str, 4, "%d", bj->level);
    rb->lcd_getstringsize(str, &w, &h);
    rb->lcd_putsxy(LCD_WIDTH-(LCD_WIDTH-BJ_WIDTH*TILE_WIDTH)/2-w/2, 10, str);

    rb->snprintf(str, 6, "%d", (bj->level-1)*LEVEL_PTS+bj->score);
    rb->lcd_getstringsize(str, &w, &h);
    rb->lcd_putsxy(LCD_WIDTH-(LCD_WIDTH-BJ_WIDTH*TILE_WIDTH)/2-w/2,
                   LCD_HEIGHT-8,
                   str);

    rb->lcd_update();
}

/*****************************************************************************
* bejeweled_putjewels() makes the jewels fall to fill empty spots and adds
* new random jewels at the empty spots at the top of each row.
******************************************************************************/
static void bejeweled_putjewels(struct game_context* bj){
    int i, j, k;
    bool mark, done;

    /* loop to make all the jewels fall */
    while(true) {
        /* mark falling jewels and add new jewels to hidden top row*/
        mark = false;
        done = true;
        for(j=0; j<BJ_WIDTH; j++) {
            if(bj->playboard[j][1].type == 0) {
                bj->playboard[j][0].type = rb->rand()%7+1;
            }
            for(i=BJ_HEIGHT-2; i>=0; i--) {
                if(!mark && bj->playboard[j][i+1].type == 0) {
                    mark = true;
                    done = false;
                }
                if(mark) bj->playboard[j][i].falling = true;
            }
            /*if(bj->playboard[1][j].falling) {
                bj->playboard[0][j].type = rb->rand()%7+1;
                bj->playboard[0][j].falling = true;
            }*/
            mark = false;
        }

        /* break if there are no falling jewels */
        if(done) break;

        /* animate falling jewels */
        for(k=TILE_HEIGHT/8; k<=TILE_HEIGHT; k+=TILE_HEIGHT/8) {
            rb->sleep(HZ/FALL_TIMER);
            for(i=0; i<BJ_HEIGHT-1; i++) {
                for(j=0; j<BJ_WIDTH; j++) {
                    if(bj->playboard[j][i].falling &&
                       bj->playboard[j][i].type != 0) {
                        /* clear old position */
                        rb->lcd_mono_bitmap(jewel[0],
                                            j*TILE_WIDTH,
                                            (i-1)*TILE_HEIGHT+k-TILE_HEIGHT/8,
                                            TILE_WIDTH, TILE_HEIGHT);
                        /* draw new position */
#ifdef HAVE_LCD_COLOR
                        rb->lcd_set_foreground(jewel_color
                                               [bj->playboard[j][i].type]);
#endif
                        rb->lcd_mono_bitmap(jewel[bj->playboard[j][i].type],
                                            j*TILE_WIDTH,
                                            (i-1)*TILE_HEIGHT+k,
                                            TILE_WIDTH, TILE_HEIGHT);
                    }
                }
            }
            rb->lcd_update();
        }
#ifdef HAVE_LCD_COLOR
        rb->lcd_set_foreground(COLOR_BLACK);
#endif

        /* shift jewels down */
        for(j=0; j<BJ_WIDTH; j++) {
            for(i=BJ_HEIGHT-1; i>=1; i--) {
                if(bj->playboard[j][i-1].falling) {
                    bj->playboard[j][i].type = bj->playboard[j][i-1].type;
                }
            }
        }

        /* clear out top row */
        for(j=0; j<BJ_WIDTH; j++) {
            bj->playboard[j][0].type = 0;
        }

        /* mark everything not falling */
        for(i=0; i<BJ_HEIGHT; i++) {
            for(j=0; j<BJ_WIDTH; j++) {
                bj->playboard[j][i].falling = false;
            }
        }
    }
}

/*****************************************************************************
* bejeweled_clearjewels() finds all the connected rows and columns and
*     calculates and returns the points earned.
******************************************************************************/
static unsigned int bejeweled_clearjewels(struct game_context* bj) {
    int i, j;
    int last, run;
    unsigned int points = 0;

    /* check for connected rows */
    for(i=1; i<BJ_HEIGHT; i++) {
        last = 0;
        run = 1;
        for(j=0; j<BJ_WIDTH; j++) {
            if(bj->playboard[j][i].type == last &&
               bj->playboard[j][i].type != 0) {
                run++;

                if(run == 3) {
                    bj->segments++;
                    points += bj->segments;
                    bj->playboard[j][i].delete = true;
                    bj->playboard[j-1][i].delete = true;
                    bj->playboard[j-2][i].delete = true;
                } else if(run > 3) {
                    points++;
                    bj->playboard[j][i].delete = true;
                }
            } else {
                run = 1;
                last = bj->playboard[j][i].type;
            }
        }
    }

    /* check for connected columns */
    for(j=0; j<BJ_WIDTH; j++) {
        last = 0;
        run = 1;
        for(i=1; i<BJ_HEIGHT; i++) {
            if(bj->playboard[j][i].type != 0 &&
               bj->playboard[j][i].type == last) {
                run++;

                if(run == 3) {
                    bj->segments++;
                    points += bj->segments;
                    bj->playboard[j][i].delete = true;
                    bj->playboard[j][i-1].delete = true;
                    bj->playboard[j][i-2].delete = true;
                } else if(run > 3) {
                    points++;
                    bj->playboard[j][i].delete = true;
                }
            } else {
                run = 1;
                last = bj->playboard[j][i].type;
            }
        }
    }

    /* clear deleted jewels */
    for(i=1; i<BJ_HEIGHT; i++) {
        for(j=0; j<BJ_WIDTH; j++) {
            if(bj->playboard[j][i].delete) {
                bj->playboard[j][i].delete = false;
                bj->playboard[j][i].type = 0;
            }
        }
    }

    return points;
}

/*****************************************************************************
* bejeweled_runboard() runs the board until it settles in a fixed state and
*     returns points earned.
******************************************************************************/
static unsigned int bejeweled_runboard(struct game_context* bj) {
    unsigned int points = 0;
    unsigned int ret;

    bj->segments = 0;

    while((ret = bejeweled_clearjewels(bj)) > 0) {
        points += ret;
        bejeweled_drawboard(bj);
        bejeweled_putjewels(bj);
    }

    return points;
}

/*****************************************************************************
* bejeweled_swapjewels() swaps two jewels as long as it results in points and
*     returns points earned.
******************************************************************************/
static unsigned int bejeweled_swapjewels(struct game_context* bj,
                                         int x, int y, int direc) {
    int k;
    int horzmod, vertmod;
    int movelen = 0;
    bool undo = false;
    unsigned int points = 0;

    /* check for invalid parameters */
    if(x < 0 || x >= BJ_WIDTH || y < 0 || y >= BJ_HEIGHT-1 ||
       direc < SWAP_UP || direc > SWAP_LEFT) return 0;

    /* check for invalid directions */
    if((x == 0 && direc == SWAP_LEFT) ||
       (x == BJ_WIDTH-1 && direc == SWAP_RIGHT) ||
       (y == 0 && direc == SWAP_UP) ||
       (y == BJ_HEIGHT-2 && direc == SWAP_DOWN)) {
        return 0;
    }

    /* set direction variables */
    horzmod = 0;
    vertmod = 0;
    switch(direc) {
        case SWAP_UP:
            vertmod = -1;
            movelen = TILE_HEIGHT;
            break;
        case SWAP_RIGHT:
            horzmod = 1;
            movelen = TILE_WIDTH;
            break;
        case SWAP_DOWN:
            vertmod = 1;
            movelen = TILE_HEIGHT;
            break;
        case SWAP_LEFT:
            horzmod = -1;
            movelen = TILE_WIDTH;
            break;
    }

    while(true) {
        /* animate swapping jewels */
        for(k=TILE_HEIGHT/8; k<=movelen;k+=TILE_HEIGHT/8) {
            rb->sleep(HZ/SWAP_TIMER);
            /* clear old position */
            rb->lcd_mono_bitmap(jewel[0],
                                x*TILE_WIDTH+horzmod*(k-TILE_WIDTH/8),
                                y*TILE_HEIGHT+vertmod*(k-TILE_HEIGHT/8),
                                TILE_WIDTH, TILE_HEIGHT);
            rb->lcd_mono_bitmap(jewel[0],
                                (x+horzmod)*TILE_WIDTH-horzmod*(k-TILE_WIDTH/8),
                                (y+vertmod)*TILE_HEIGHT-vertmod*(k-TILE_HEIGHT/8),
                                TILE_WIDTH, TILE_HEIGHT);
            /* draw new position */
#ifdef HAVE_LCD_COLOR
            rb->lcd_set_foreground(jewel_color[bj->playboard[x][y+1].type]);
#endif
            rb->lcd_mono_bitmap(jewel[bj->playboard[x][y+1].type],
                                x*TILE_WIDTH+horzmod*k,
                                y*TILE_HEIGHT+vertmod*k,
                                TILE_WIDTH, TILE_HEIGHT);
#ifdef HAVE_LCD_COLOR
            rb->lcd_set_foreground(jewel_color[bj->playboard
                                   [x+horzmod][y+1+vertmod].type]);
#endif
            rb->lcd_set_drawmode(DRMODE_FG);
            rb->lcd_mono_bitmap(jewel[bj->playboard
                                [x+horzmod][y+1+vertmod].type],
                                (x+horzmod)*TILE_WIDTH-horzmod*k,
                                (y+vertmod)*TILE_HEIGHT-vertmod*k,
                                TILE_WIDTH, TILE_HEIGHT);
            rb->lcd_set_drawmode(DRMODE_SOLID);

            rb->lcd_update();
        }

        /* swap jewels */
        int temp = bj->playboard[x][y+1].type;
        bj->playboard[x][y+1].type =
            bj->playboard[x+horzmod][y+1+vertmod].type;
        bj->playboard[x+horzmod][y+1+vertmod].type = temp;

        if(undo) break;

        points = bejeweled_runboard(bj);
        if(points == 0) {undo = true;} else {break;}
    }
#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground(COLOR_BLACK);
#endif

    return points;
}

/*****************************************************************************
* bejeweled_movesavail() uses pattern matching to see if there are any
*     available move left.
******************************************************************************/
static bool bejeweled_movesavail(struct game_context* bj) {
    int i, j;
    bool moves = false;
    int mytype;

    for(i=1; i<BJ_HEIGHT; i++) {
        for(j=0; j<BJ_WIDTH; j++) {
            mytype = bj->playboard[j][i].type;

            /* check horizontal patterns */
            if(j <= BJ_WIDTH-3) {
                if(i > 1) {
                    if(bj->playboard[j+1][i-1].type == mytype) {
                        if(bj->playboard[j+2][i-1].type == mytype)
                            {moves = true; break;}
                        if(bj->playboard[j+2][i].type == mytype)
                            {moves = true; break;}
                    }
                    if(bj->playboard[j+1][i].type == mytype) {
                        if(bj->playboard[j+2][i-1].type == mytype)
                            {moves = true; break;}
                    }
                }

                if(j <= BJ_WIDTH-4) {
                    if(bj->playboard[j+3][i].type == mytype) {
                        if(bj->playboard[j+1][i].type == mytype)
                            {moves = true; break;}
                        if(bj->playboard[j+2][i].type == mytype)
                            {moves = true; break;}
                    }
                }

                if(i < BJ_HEIGHT-1) {
                    if(bj->playboard[j+1][i].type == mytype) {
                        if(bj->playboard[j+2][i+1].type == mytype)
                            {moves = true; break;}
                    }
                    if(bj->playboard[j+1][i+1].type == mytype) {
                        if(bj->playboard[j+2][i].type == mytype)
                            {moves = true; break;}
                        if(bj->playboard[j+2][i+1].type == mytype)
                            {moves = true; break;}
                    }
                }
            }

            /* check vertical patterns */
            if(i <= BJ_HEIGHT-3) {
                if(j > 0) {
                    if(bj->playboard[j-1][i+1].type == mytype) {
                        if(bj->playboard[j-1][i+2].type == mytype)
                            {moves = true; break;}
                        if(bj->playboard[j][i+2].type == mytype)
                            {moves = true; break;}
                    }
                    if(bj->playboard[j][i+1].type == mytype) {
                        if(bj->playboard[j-1][i+2].type == mytype)
                            {moves = true; break;}
                    }
                }

                if(i <= BJ_HEIGHT-4) {
                    if(bj->playboard[j][i+3].type == mytype) {
                        if(bj->playboard[j][i+1].type == mytype)
                            {moves = true; break;}
                        if(bj->playboard[j][i+2].type == mytype)
                            {moves = true; break;}
                    }
                }

                if(j < BJ_WIDTH-1) {
                    if(bj->playboard[j][i+1].type == mytype) {
                        if(bj->playboard[j+1][i+2].type == mytype)
                            {moves = true; break;}
                    }
                    if(bj->playboard[j+1][i+1].type == mytype) {
                        if(bj->playboard[j][i+2].type == mytype)
                            {moves = true; break;}
                        if (bj->playboard[j+1][i+2].type == mytype)
                            {moves = true; break;}
                    }
                }
            }
        }

        if(moves) break;
    }

    return moves;
}

/*****************************************************************************
* bejeweled_nextlevel() advances the game to the next level and returns
*     points earned.
******************************************************************************/
static unsigned int bejeweled_nextlevel(struct game_context* bj) {
    int i, x, y;
    unsigned int points = 0;

    /* roll over score, change and display level */
    while(bj->score >= LEVEL_PTS) {
        bj->score -= LEVEL_PTS;
        bj->level++;
        rb->splash(HZ*2, true, "Level %d", bj->level);
        bejeweled_drawboard(bj);
    }

    /* randomly clear some jewels */
    for(i=0; i<16; i++) {
        x = rb->rand()%8;
        y = rb->rand()%8;

        if(bj->playboard[x][y].type != 0) {
            points++;
            bj->playboard[x][y].type = 0;
        }
    }
    bejeweled_drawboard(bj);

    /* run the play board */
    bejeweled_putjewels(bj);
    points += bejeweled_runboard(bj);
    return points;
}

/*****************************************************************************
* bejeweld_recordscore() inserts a high score into the high scores list and
*     returns the high score position.
******************************************************************************/
static int bejeweled_recordscore(struct game_context* bj) {
    int i;
    int position = 0;
    unsigned short current, temp;

    /* calculate total score */
    current = (bj->level-1)*LEVEL_PTS+bj->score;
    if(current <= 0) return 0;

    /* insert the current score into the high scores */
    for(i=0; i<NUM_SCORES; i++) {
        if(current >= bj->highscores[i]) {
            if(!position) {
                position = i+1;
                bj->dirty = true;
            }
            temp = bj->highscores[i];
            bj->highscores[i] = current;
            current = temp;
        }
    }

    return position;
 }

/*****************************************************************************
* bejeweled_loadscores() loads the high scores saved file.
******************************************************************************/
static void bejeweled_loadscores(struct game_context* bj) {
    int fd;

    bj->dirty = false;

    /* clear high scores */
    rb->memset(bj->highscores, 0, sizeof(bj->highscores));

    /* open scores file */
    fd = rb->open(SCORE_FILE, O_RDONLY);
    if(fd < 0) return;

    /* read in high scores */
    if(rb->read(fd, bj->highscores, sizeof(bj->highscores)) <= 0) {
        /* scores are bad, reset */
        rb->memset(bj->highscores, 0, sizeof(bj->highscores));
    }

    rb->close(fd);
}

/*****************************************************************************
* bejeweled_savescores() saves the high scores.
******************************************************************************/
static void bejeweled_savescores(struct game_context* bj) {
    int fd;

    /* write out the high scores to the save file */
    fd = rb->open(SCORE_FILE, O_WRONLY|O_CREAT);
    rb->write(fd, bj->highscores, sizeof(bj->highscores));
    rb->close(fd);
    bj->dirty = false;
}

/*****************************************************************************
* bejeweled_loadgame() loads the saved game and returns load success.
******************************************************************************/
static bool bejeweled_loadgame(struct game_context* bj) {
    int fd;
    bool loaded = false;

    /* open game file */
    fd = rb->open(SAVE_FILE, O_RDONLY);
    if(fd < 0) return loaded;

    /* read in saved game */
    while(true) {
        if(rb->read(fd, &bj->score, sizeof(bj->score)) <= 0) break;
        if(rb->read(fd, &bj->level, sizeof(bj->level)) <= 0) break;
        if(rb->read(fd, bj->playboard, sizeof(bj->playboard)) <= 0) break;
        bj->resume = true;
        loaded = true;
        break;
    }

    rb->close(fd);

    /* delete saved file */
    rb->remove(SAVE_FILE);
    return loaded;
}

/*****************************************************************************
* bejeweled_savegame() saves the current game state.
******************************************************************************/
static void bejeweled_savegame(struct game_context* bj) {
    int fd;

    /* write out the game state to the save file */
    fd = rb->open(SAVE_FILE, O_WRONLY|O_CREAT);
    rb->write(fd, &bj->score, sizeof(bj->score));
    rb->write(fd, &bj->level, sizeof(bj->level));
    rb->write(fd, bj->playboard, sizeof(bj->playboard));
    rb->close(fd);

    bj->resume = true;
}

/*****************************************************************************
* bejeweled_callback() is the default event handler callback which is called
*     on usb connect and shutdown.
******************************************************************************/
static void bejeweled_callback(void* param) {
    struct game_context* bj = (struct game_context*) param;
    if(bj->dirty) {
        rb->splash(HZ, true, "Saving high scores...");
        bejeweled_savescores(bj);
    }
}

/*****************************************************************************
* bejeweled() is the main game subroutine, it returns the final game status.
******************************************************************************/
static int bejeweled(struct game_context* bj) {
    int i, j;
    int w, h;
    int button;
    int lastbutton = BUTTON_NONE;
    char str[18];
    char *title = "Bejeweled";
    bool breakout = false;
    bool showscores = false;
    bool selected = false;

    /* the cursor coordinates */
    int x=0, y=0;

    /* don't resume by default */
    bj->resume = false;

    /********************
    *       menu        *
    ********************/
    while(true){
        rb->lcd_clear_display();

        if(!showscores) {
             /* welcome screen to display key bindings */
            rb->lcd_getstringsize(title, &w, &h);
            rb->lcd_putsxy((LCD_WIDTH-w)/2, 0, title);
#if CONFIG_KEYPAD == RECORDER_PAD
            rb->lcd_puts(0, 1, "ON to start");
            rb->lcd_puts(0, 2, "F1 to save/resume");
            rb->lcd_puts(0, 3, "OFF to exit");
            rb->lcd_puts(0, 4, "PLAY to select");
            rb->lcd_puts(0, 5, "& show high scores");
            rb->lcd_puts(0, 6, "Directions to move");
            rb->snprintf(str, 18, "High Score: %d", bj->highscores[0]);
            rb->lcd_puts(0, 7, str);
#elif CONFIG_KEYPAD == ONDIO_PAD
            rb->lcd_puts(0, 1, "RIGHT to start");
            rb->lcd_puts(0, 2, "MENU+OFF to sv/res");
            rb->lcd_puts(0, 3, "OFF to exit");
            rb->lcd_puts(0, 4, "MENU to select");
            rb->lcd_puts(0, 5, "& show high scores");
            rb->lcd_puts(0, 6, "Directions to move");
            rb->snprintf(str, 18, "High Score: %d", bj->highscores[0]);
            rb->lcd_puts(0, 7, str);
#elif (CONFIG_KEYPAD == IRIVER_H100_PAD) || (CONFIG_KEYPAD == IRIVER_H300_PAD)
            rb->lcd_puts(0, 2, "ON to start");
            rb->lcd_puts(0, 3, "MODE to save/resume");
            rb->lcd_puts(0, 4, "OFF to exit");
            rb->lcd_puts(0, 5, "SELECT to select");
            rb->lcd_puts(0, 6, " and show high scores");
            rb->lcd_puts(0, 7, "Directions to move");
            rb->snprintf(str, 18, "High Score: %d", bj->highscores[0]);
            rb->lcd_puts(0, 9, str);
#endif
        } else {
            /* room for a title? */
            j = 0;
            if(LCD_HEIGHT-NUM_SCORES*8 >= 8) {
                rb->snprintf(str, 12, "%s", "High Scores");
                rb->lcd_getstringsize(str, &w, &h);
                rb->lcd_putsxy((LCD_WIDTH-w)/2, 0, str);
                j = 1;
            }

            /* print high scores */
            for(i=0; i<NUM_SCORES; i++) {
                rb->snprintf(str, 11, "#%02d: %d", i+1, bj->highscores[i]);
                rb->lcd_puts(0, i+j, str);
            }
        }

        rb->lcd_update();

        /* handle menu button presses */
        button = rb->button_get(true);
        switch(button){
            case BEJEWELED_START: /* start playing */
                breakout = true;
                break;

            case BEJEWELED_QUIT:  /* quit program */
                if(showscores) {
                    showscores = 0;
                    break;
                }
                return BJ_QUIT;

            case BEJEWELED_RESUME:/* resume game */
                if(!bejeweled_loadgame(bj)) {
                    rb->splash(HZ*2, true, "Nothing to resume");
                } else {
                    breakout = true;
                }
                break;

            case BEJEWELED_SELECT:/* toggle high scores */
#ifdef BEJEWELED_SELECT_PRE
                if(lastbutton != BEJEWELED_SELECT_PRE) break;
#endif
                showscores ^= 1;
                break;

            default:
                if(rb->default_event_handler_ex(button, bejeweled_callback,
                   (void*) bj) == SYS_USB_CONNECTED)
                    return BJ_USB;
                break;
        }

        if(breakout) break;
        if(button != BUTTON_NONE) lastbutton = button;
    }

    lastbutton = BUTTON_NONE;

    /********************
    *       init        *
    ********************/
    bejeweled_init(bj);

    /********************
    *  setup the board  *
    ********************/
    bejeweled_drawboard(bj);
    bejeweled_putjewels(bj);
    bj->score += bejeweled_runboard(bj);
    if (!bejeweled_movesavail(bj)) return BJ_LOSE;

    /**********************
    *        play         *
    **********************/
    while(true) {
        /* refresh the board */
        bejeweled_drawboard(bj);

        /* display the cursor */
        if(selected) {
            rb->lcd_set_drawmode(DRMODE_COMPLEMENT);
            rb->lcd_fillrect(x*TILE_WIDTH, y*TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT);
            rb->lcd_set_drawmode(DRMODE_SOLID);
        } else {
            rb->lcd_drawrect(x*TILE_WIDTH, y*TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT);
        }
        rb->lcd_update_rect(x*TILE_WIDTH, y*TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT);

        /* handle game button presses */
        button = rb->button_get(true);
        switch(button){
            case BEJEWELED_RESUME:           /* save and end game */
                rb->splash(HZ, true, "Saving game...");
                bejeweled_savegame(bj);
                /* fall through to BEJEWELED_QUIT */

            case BEJEWELED_QUIT:             /* end game */
                return BJ_END;

            case BUTTON_LEFT:                /* move cursor left */
            case (BUTTON_LEFT|BUTTON_REPEAT):
                if(selected) {
                    bj->score += bejeweled_swapjewels(bj, x, y, SWAP_LEFT);
                    selected = false;
                    if (!bejeweled_movesavail(bj)) return BJ_LOSE;
                } else {
                    x = (x+BJ_WIDTH-1)%BJ_WIDTH;
                }
                break;

            case BUTTON_RIGHT:               /* move cursor right */
            case (BUTTON_RIGHT|BUTTON_REPEAT):
                if(selected) {
                    bj->score += bejeweled_swapjewels(bj, x, y, SWAP_RIGHT);
                    selected = false;
                    if (!bejeweled_movesavail(bj)) return BJ_LOSE;
                } else {
                    x = (x+1)%BJ_WIDTH;
                }
                break;

            case BUTTON_DOWN:                /* move cursor down */
            case (BUTTON_DOWN|BUTTON_REPEAT):
                if(selected) {
                    bj->score += bejeweled_swapjewels(bj, x, y, SWAP_DOWN);
                    selected = false;
                    if (!bejeweled_movesavail(bj)) return BJ_LOSE;
                } else {
                    y = (y+1)%(BJ_HEIGHT-1);
                }
                break;

            case BUTTON_UP:                  /* move cursor up */
            case (BUTTON_UP|BUTTON_REPEAT):
                if(selected) {
                    bj->score += bejeweled_swapjewels(bj, x, y, SWAP_UP);
                    selected = false;
                    if (!bejeweled_movesavail(bj)) return BJ_LOSE;
                } else {
                    y = (y+(BJ_HEIGHT-1)-1)%(BJ_HEIGHT-1);
                }
                break;

            case BEJEWELED_SELECT:           /* toggle selected */
#ifdef BEJEWELED_SELECT_PRE
                if(lastbutton != BEJEWELED_SELECT_PRE) break;
#endif
                selected ^= 1;
                break;

            default:
                if(rb->default_event_handler_ex(button, bejeweled_callback,
                   (void*) bj) == SYS_USB_CONNECTED)
                    return BJ_USB;
                break;
        }

        if(button != BUTTON_NONE) lastbutton = button;
        if(bj->score >= LEVEL_PTS) bj->score = bejeweled_nextlevel(bj);
    }
}

/*****************************************************************************
* plugin entry point.
******************************************************************************/
enum plugin_status plugin_start(struct plugin_api* api, void* parameter) {
    struct game_context bj;
    bool exit = false;
    int position;
    char str[19];

    /* plugin init */
    TEST_PLUGIN_API(api);
    (void)parameter;
    rb = api;
    /* end of plugin init */

    /* load high scores */
    bejeweled_loadscores(&bj);

    rb->lcd_setfont(FONT_SYSFIXED);

    while(!exit) {
        switch(bejeweled(&bj)){
            case BJ_LOSE:
                rb->splash(HZ*2, true, "No more moves!");
                /* fall through to BJ_END */

            case BJ_END:
                if(!bj.resume) {
                    if((position = bejeweled_recordscore(&bj))) {
                        rb->snprintf(str, 19, "New high score #%d!", position);
                        rb->splash(HZ*2, true, str);
                    }
                }
                break;

            case BJ_USB:
                rb->lcd_setfont(FONT_UI);
                return PLUGIN_USB_CONNECTED;

            case BJ_QUIT:
                if(bj.dirty) {
                    rb->splash(HZ, true, "Saving high scores...");
                    bejeweled_savescores(&bj);
                }
                exit = true;
                break;

            default:
                break;
        }
    }

    rb->lcd_setfont(FONT_UI);
    return PLUGIN_OK;
}

#endif
