/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2006 Eli Sherer
 *               2007 Antoine Cellerier
 *
 * 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 "helper.h"

PLUGIN_HEADER

#if (CONFIG_KEYPAD == IRIVER_H100_PAD) || (CONFIG_KEYPAD == IRIVER_H300_PAD)

#define QUIT BUTTON_OFF
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define PAUSE BUTTON_MODE
#define UP BUTTON_UP
#define DOWN BUTTON_DOWN

#define RC_QUIT BUTTON_RC_STOP

#elif (CONFIG_KEYPAD == ARCHOS_AV300_PAD)

#define QUIT BUTTON_OFF
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define PAUSE BUTTON_ON
#define UP BUTTON_UP
#define DOWN BUTTON_DOWN

#elif (CONFIG_KEYPAD == IPOD_4G_PAD) || \
      (CONFIG_KEYPAD == IPOD_3G_PAD) || \
      (CONFIG_KEYPAD == IPOD_1G2G_PAD)

#define QUIT (BUTTON_SELECT | BUTTON_MENU)
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define PAUSE BUTTON_SELECT
#define MENU_UP BUTTON_SCROLL_FWD
#define MENU_DOWN BUTTON_SCROLL_BACK
#define UP BUTTON_MENU
#define DOWN BUTTON_PLAY

#elif CONFIG_KEYPAD == IAUDIO_X5M5_PAD

#define QUIT BUTTON_POWER
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define UP BUTTON_UP
#define DOWN BUTTON_DOWN
#define PAUSE BUTTON_PLAY

#elif (CONFIG_KEYPAD == GIGABEAT_PAD)

#define QUIT BUTTON_POWER
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define UP BUTTON_UP
#define DOWN BUTTON_DOWN
#define PAUSE BUTTON_A

#elif (CONFIG_KEYPAD == SANSA_E200_PAD) || \
(CONFIG_KEYPAD == SANSA_C200_PAD)

#define QUIT BUTTON_POWER
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define UP BUTTON_UP
#define DOWN BUTTON_DOWN
#define PAUSE BUTTON_REC


#elif CONFIG_KEYPAD == IRIVER_H10_PAD

#define QUIT BUTTON_POWER
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define UP BUTTON_SCROLL_UP
#define DOWN BUTTON_SCROLL_DOWN
#define PAUSE BUTTON_PLAY

#elif CONFIG_KEYPAD == RECORDER_PAD

#define QUIT BUTTON_OFF
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define DOWN BUTTON_DOWN
#define UP BUTTON_UP
#define PAUSE BUTTON_PLAY

#elif CONFIG_KEYPAD == ONDIO_PAD

#define QUIT BUTTON_OFF
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define DOWN BUTTON_DOWN
#define UP BUTTON_UP
#define PAUSE BUTTON_MENU

#elif (CONFIG_KEYPAD == GIGABEAT_S_PAD)

#define QUIT BUTTON_BACK
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define UP BUTTON_UP
#define DOWN BUTTON_DOWN
#define PAUSE BUTTON_PLAY

#elif (CONFIG_KEYPAD == MROBE100_PAD)

#define QUIT BUTTON_POWER
#define LEFT BUTTON_LEFT
#define RIGHT BUTTON_RIGHT
#define UP BUTTON_UP
#define DOWN BUTTON_DOWN
#define PAUSE BUTTON_DISPLAY

#elif CONFIG_KEYPAD == IAUDIO_M3_PAD

#define QUIT BUTTON_RC_REC
#define LEFT BUTTON_RC_REW
#define RIGHT BUTTON_RC_FF
#define UP BUTTON_RC_VOL_UP
#define DOWN BUTTON_RC_VOL_DOWN
#define PAUSE BUTTON_RC_PLAY

#elif CONFIG_KEYPAD == COWOND2_PAD

#define QUIT BUTTON_POWER

#else
#error No keymap defined!
#endif

#ifdef HAVE_TOUCHPAD
#ifndef QUIT
#define QUIT  BUTTON_TOPLEFT
#endif
#ifndef LEFT
#define LEFT  BUTTON_MIDLEFT
#endif
#ifndef RIGHT
#define RIGHT BUTTON_MIDRIGHT
#endif
#ifndef UP
#define UP    BUTTON_TOPMIDDLE
#endif
#ifndef DOWN
#define DOWN  BUTTON_BOTTOMMIDDLE
#endif
#ifndef PAUSE
#define PAUSE BUTTON_CENTER
#endif
#endif

#define MOVE_NO 0               /* player movement */
#define MOVE_UP 1               /*    1    */
#define MOVE_DN 2               /*  3 0 4  */
#define MOVE_LT 3               /*    2    */
#define MOVE_RT 4

/* ball movement (12 ways) */
/*    UUL   UR    */
/*   UL       UR   */
/* ULL    .    URR */
/* DLL         DRR */
/*   DL       DR   */
/*    DDL   DDR    */

#define DIR_UU (1<<7)
#define DIR_U  (1<<6)
#define DIR_RR (1<<5)
#define DIR_R  (1<<4)
#define DIR_DD (1<<3)
#define DIR_D  (1<<2)
#define DIR_LL (1<<1)
#define DIR_L  (1<<0)

#define MOVE_UUR ( DIR_UU | DIR_R  )
#define MOVE_UR  ( DIR_U  | DIR_R  )
#define MOVE_URR ( DIR_U  | DIR_RR )
#define MOVE_DRR ( DIR_D  | DIR_RR )
#define MOVE_DR  ( DIR_D  | DIR_R  )
#define MOVE_DDR ( DIR_DD | DIR_R  )
#define MOVE_DDL ( DIR_DD | DIR_L  )
#define MOVE_DL  ( DIR_D  | DIR_L  )
#define MOVE_DLL ( DIR_D  | DIR_LL )
#define MOVE_ULL ( DIR_U  | DIR_LL )
#define MOVE_UL  ( DIR_U  | DIR_L  )
#define MOVE_UUL ( DIR_UU | DIR_L  )

#if (LCD_WIDTH>112) && (LCD_HEIGHT>64)
#   define CUBE_SIZE 8             /* 8x22=176 */
#   define pos(a) ((a)>>3)
#else
#   define CUBE_SIZE 4
#   define pos(a) ((a)>>2)
#endif

#define STARTING_QIXES 2
#define MAX_LEVEL 10
#define MAX_QIXES MAX_LEVEL+STARTING_QIXES
#define BOARD_W ((int)(LCD_WIDTH/CUBE_SIZE))
#define BOARD_H ((int)(LCD_HEIGHT/CUBE_SIZE))
#define BOARD_X (LCD_WIDTH-BOARD_W*CUBE_SIZE)/2
#define BOARD_Y (LCD_HEIGHT-BOARD_H*CUBE_SIZE)/2

#ifdef HAVE_LCD_COLOR
#define CLR_RED  LCD_RGBPACK(255,0,0)         /* used to imply danger */
#define CLR_LTBLUE LCD_RGBPACK(125, 145, 180) /* used for frame and filling */
#define PLR_COL  LCD_WHITE                    /* color used for the player */
#elif LCD_DEPTH>=2
#define CLR_RED  LCD_DARKGRAY     /* used to imply danger */
#define CLR_LTBLUE LCD_LIGHTGRAY  /* used for frame and filling */
#define PLR_COL  LCD_BLACK        /* color used for the player */
#endif

#if LCD_DEPTH>=2
#define EMPTIED LCD_BLACK       /* empty spot */
#define FILLED  CLR_LTBLUE      /* filled spot */
#define TRAIL   CLR_RED         /* the red trail of the player */
#define QIX     LCD_WHITE
#else
#define EMPTIED 0
#define FILLED 1
#define TRAIL 2
#define QIX 3
#endif
#define UNCHECKED 0
#define CHECKED 1
#define PAINTED -1
#define PIC_QIX 0
#define PIC_PLAYER 1

#define MENU_START 0
#define MENU_QUIT 1

/* The time (in ms) for one iteration through the game loop - decrease this
   to speed up the game - note that current_tick is (currently) only accurate
   to 10ms.
*/
static int speed = 6; /* CYCLETIME = (11-speed)*10 ms */
static int difficulty = 75; /* Percentage of screen that needs to be filled
                             * in order to win the game */

static struct plugin_api *rb;

MEM_FUNCTION_WRAPPERS(rb);

static bool quit = false;

static unsigned int board[BOARD_H][BOARD_W];
static int testboard[BOARD_H][BOARD_W];

#if CUBE_SIZE == 8
/*
   00011000 0x18 - 11100111 0xe7
   00111100 0x3c - 11100111 0xe7
   01111110 0x7e - 11000011 0xc3
   11111111 0xff - 00000000 0x00
   11111111 0xff - 00000000 0x00
   01111110 0x7e - 11000011 0xc3
   00111100 0x3c - 11100111 0xe7
   00011000 0x18 - 11100111 0xe7
 */
const unsigned char pics[2][8] = {
    {0x18, 0x3c, 0x7e, 0xff, 0xff, 0x7e, 0x3c, 0x18},   /* Alien (QIX) */
    {0xe7, 0xe7, 0xc3, 0x00, 0x00, 0xc3, 0xe7, 0xe7}    /* Player (XONIX) */
};
#elif CUBE_SIZE == 4
/*
   0110 0x6 - 1001 0x9
   1111 0xf - 0110 0x6
   1111 0xf - 0110 0x6
   0110 0x6 - 1001 0x9
 */
const unsigned char pics[2][4] = {
    {0x6, 0xf, 0xf, 0x6},   /* Alien (QIX) */
    {0x9, 0x6, 0x6, 0x9}    /* Player (XONIX) */
};
#else
#error Incorrect CUBE_SIZE value.
#endif

static struct qix
{
    int velocity;             /* velocity */
    int x, y;                 /* position on screen */
    int angle;                /* angle */
} qixes[MAX_QIXES];             /* black_qix */

static struct splayer
{
    int i, j;                 /* position on board */
    int move, score, level, lives;
    bool drawing;
    bool gameover;
} player;

static int percentage_cache;

/*************************** STACK STUFF **********************/

/* the stack */
#define STACK_SIZE (2*BOARD_W*BOARD_H)
static struct pos
{
    int x, y;                   /* position on board */
} stack[STACK_SIZE];
static int stackPointer;

static inline bool pop (struct pos *p)
{
    if (stackPointer > 0) {
        p->x = stack[stackPointer].x;
        p->y = stack[stackPointer].y;
        stackPointer--;
        return true;
    } else
        return false;           /* SE */
}

static inline bool push (struct pos *p)
{
    if (stackPointer < STACK_SIZE - 1) {
        stackPointer++;
        stack[stackPointer].x = p->x;
        stack[stackPointer].y = p->y;
        return true;
    } else
        return false;           /* SOF */
}

static inline void emptyStack (void)
{
    stackPointer = 0;
}

/*********************** END OF STACK STUFF *********************/

/* calculate the new x coordinate of the ball according to angle and speed */
static inline int get_newx (int x, int len, int deg)
{
    if (deg & DIR_R)
        return x + len;
    else if (deg & DIR_L)
        return x - len;
    else if (deg & DIR_RR)
        return x + len * 2;
    else /* (def & DIR_LL) */
        return x - len * 2;
}

/* calculate the new y coordinate of the ball according to angle and speed */
static inline int get_newy (int y, int len, int deg)
{
    if (deg & DIR_D)
        return y + len;
    else if (deg & DIR_U)
        return y - len;
    else if (deg & DIR_DD)
        return y + len * 2;
    else /* (deg & DIR_UU) */
        return y - len * 2;
}

/* make random function get it's value from the device ticker */
static inline void randomize (void)
{
    rb->srand (*rb->current_tick);
}

/* get a random number between 0 and range-1 */
static int t_rand (int range)
{
    return rb->rand () % range;
}

/* initializes the test help board */
static void init_testboard (void)
{
    int j;                   /* testboard */
    for (j = 0; j < BOARD_H; j++)
        /* UNCHEKED == (int)0 */
        rb->memset( testboard[j], 0, BOARD_W * sizeof( int ) );
}

/* initializes the game board on with the player,qix's and black qix */
static void init_board (void)
{
    int i, j;
    for (j = 0; j < BOARD_H; j++)
        for (i = 0; i < BOARD_W; i++) { /* make a nice cyan frame */
            if ((i == 0) || (j <= 1) || (i == BOARD_W - 1)
                || (j >= BOARD_H - 2))
                board[j][i] = FILLED;
            else
                board[j][i] = EMPTIED;
        }

    /* (level+2) is the number of qixes */
    for (j = 0; j < player.level + STARTING_QIXES; j++) {
        qixes[j].velocity = t_rand (2) + 1;     /* 1 or 2 pix-per-sec */

        /* not on frame */
        qixes[j].x = CUBE_SIZE*2 + 2*t_rand (((BOARD_W-4)*CUBE_SIZE)/2);
        qixes[j].y = CUBE_SIZE*2 + 2*t_rand (((BOARD_H-4)*CUBE_SIZE)/2);

        const int angle_table[] = {
            MOVE_UUR, MOVE_UR, MOVE_URR, MOVE_DRR, MOVE_DR, MOVE_DDR,
            MOVE_UUL, MOVE_UL, MOVE_ULL, MOVE_DLL, MOVE_DL, MOVE_DDL };
        qixes[j].angle = angle_table[t_rand (12)];
#if CUBE_SIZE == 4
        /* Work arround a nasty bug. FIXME */
        if( qixes[j].angle & (DIR_LL|DIR_RR|DIR_UU|DIR_DD) )
            qixes[j].velocity = 1;
#endif
    }
    /*black_qix.velocity=1;
       black_qix.x=BOARD_X+(BOARD_W*CUBE_SIZE)/2-CUBE_SIZE/2;
       black_qix.y=BOARD_Y+(BOARD_H*CUBE_SIZE)-CUBE_SIZE-CUBE_SIZE/2;
       black_qix.angle=MOVE_UR; */
    player.move = MOVE_NO;
    player.drawing = false;
    player.i = BOARD_W / 2;
    player.j = 1;

    percentage_cache = 0;
}

/* calculates the percentage of the screen filling */
static int percentage (void)
{
    int i, j, filled = 0;
    for (j = 2; j < BOARD_H - 2; j++)
        for (i = 1; i < BOARD_W - 1; i++)
            if (board[j][i] == FILLED)
                filled++;
    return (filled * 100) / ((BOARD_W - 2) * (BOARD_H - 4));
}

/* draw the board on with all the game figures */
static void refresh_board (void)
{
    int i, j;
    char str[25];

#if LCD_DEPTH>=2
    rb->lcd_set_background (LCD_BLACK);
#else
    rb->lcd_clear_display ();
#endif
    for (j = 0; j < BOARD_H; j++)
    {
        unsigned last_color = board[j][0];
        int last_i = 0;
        for (i = 1; i < BOARD_W; i++) {
            if( last_color != board[j][i] )
            {
#if LCD_DEPTH>=2
                rb->lcd_set_foreground (last_color);
#else
                if (last_color != EMPTIED)
#endif
                rb->lcd_fillrect (BOARD_X + CUBE_SIZE * (last_i),
                                  BOARD_Y + CUBE_SIZE * j,
                                  CUBE_SIZE  * (i - last_i), CUBE_SIZE );
                last_color = board[j][i];
                last_i = i;
            }
        }
#if LCD_DEPTH>=2
        rb->lcd_set_foreground (last_color);
#else
        if (last_color != EMPTIED)
#endif
        rb->lcd_fillrect (BOARD_X + CUBE_SIZE * (last_i),
                          BOARD_Y + CUBE_SIZE * j,
                          CUBE_SIZE * (i - last_i), CUBE_SIZE);
    }

#if LCD_DEPTH>=2
    rb->lcd_set_foreground (LCD_BLACK);
    rb->lcd_set_background (CLR_LTBLUE);
#else
    rb->lcd_set_drawmode (DRMODE_COMPLEMENT);
#endif
    rb->snprintf (str, sizeof (str), "Level %d", player.level + 1);
    rb->lcd_putsxy (BOARD_X, BOARD_Y, str);
    rb->snprintf (str, sizeof (str), "%d%%", percentage_cache);
    rb->lcd_putsxy (BOARD_X + CUBE_SIZE * BOARD_W - 24, BOARD_Y, str);
    rb->snprintf (str, sizeof (str), "Score: %d", player.score);
    rb->lcd_putsxy (BOARD_X, BOARD_Y + CUBE_SIZE * BOARD_H - 8, str);
    rb->snprintf (str, sizeof (str), "%d Lives", player.lives);
#if LCD_DEPTH>=2
    rb->lcd_putsxy (BOARD_X + CUBE_SIZE * BOARD_W - 60,
                    BOARD_Y + CUBE_SIZE * BOARD_H - 8, str);
#else
    rb->lcd_putsxy (BOARD_X + CUBE_SIZE * BOARD_W - 40,
                    BOARD_Y + CUBE_SIZE * BOARD_H - 8, str);
#endif

#if LCD_DEPTH>=2
    rb->lcd_set_foreground (PLR_COL);
    rb->lcd_set_background (board[player.j][player.i]);
#endif
    rb->lcd_mono_bitmap (pics[PIC_PLAYER], player.i * CUBE_SIZE + BOARD_X,
                         player.j * CUBE_SIZE + BOARD_Y, CUBE_SIZE, CUBE_SIZE);

#if LCD_DEPTH>=2
    rb->lcd_set_background (EMPTIED);
    rb->lcd_set_foreground (LCD_WHITE);
    rb->lcd_set_drawmode (DRMODE_FG);
#else
    rb->lcd_set_drawmode (DRMODE_FG);
#endif
    for (j = 0; j < player.level + STARTING_QIXES; j++)
        rb->lcd_mono_bitmap (pics[PIC_QIX], qixes[j].x + BOARD_X,
                             qixes[j].y + BOARD_Y, CUBE_SIZE, CUBE_SIZE);
#if LCD_DEPTH>=2
    rb->lcd_set_foreground (LCD_BLACK);
#endif
    rb->lcd_set_drawmode (DRMODE_SOLID);

    rb->lcd_update ();
}

static inline int infested_area (int i, int j, int v)
{
    struct pos p;
    p.x = i;
    p.y = j;
    emptyStack ();
    if (!push (&p))
        return -1;
    while (pop (&p)) {
        if (testboard[p.y][p.x] == v) continue;
        if (testboard[p.y][p.x] > UNCHECKED)
            return 1; /* This area was previously flagged as infested */
        testboard[p.y][p.x] = v;
        if (board[p.y][p.x] == QIX)
            return 1; /* Infested area */
        {
            struct pos p1 = { p.x+1, p.y };
            if ((p1.x < BOARD_W)
             && (board[p1.y][p1.x] != FILLED)
             && (!push (&p1)))
                return -1;
        }
        {
            struct pos p1 = { p.x-1, p.y };
            if ((p1.x >= 0)
             && (board[p1.y][p1.x] != FILLED)
             && (!push (&p1)))
                return -1;
        }
        {
            struct pos p1 = { p.x, p.y+1 };
            if ((p1.y < BOARD_H)
             && (board[p1.y][p1.x] != FILLED)
             && (!push (&p1)))
                return -1;
        }
        {
            struct pos p1 = { p.x, p.y-1 };
            if ((p1.y >= 0)
             && (board[p1.y][p1.x] != FILLED)
             && (!push (&p1)))
                return -1;
        }
    }
    return 0;
}

static inline int fill_area (int i, int j)
{
    struct pos p;
    p.x = i;
    p.y = j;
    int v = testboard[p.y][p.x];
    emptyStack ();
    if (!push (&p))
        return -1;
    while (pop (&p)) {
        board[p.y][p.x] = FILLED;
        testboard[p.y][p.x] = PAINTED;
        {
            struct pos p1 = { p.x+1, p.y };
            if ((p1.x < BOARD_W)
             && (testboard[p1.y][p1.x] == v)
             && (!push (&p1)))
                return -1;
        }
        {
            struct pos p1 = { p.x-1, p.y };
            if ((p1.x >= 0)
             && (testboard[p1.y][p1.x] == v)
             && (!push (&p1)))
                return -1;
        }
        {
            struct pos p1 = { p.x, p.y+1 };
            if ((p1.y < BOARD_H)
             && (testboard[p1.y][p1.x] == v)
             && (!push (&p1)))
                return -1;
        }
        {
            struct pos p1 = { p.x, p.y-1 };
            if ((p1.y >= 0)
             && (testboard[p1.y][p1.x] == v)
             && (!push (&p1)))
                return -1;
        }
    }
    return 0;
}


/* take care of stuff after xonix has landed on a filled spot */
static void complete_trail (int fill)
{
    int i, j, ret;
    for (j = 0; j < BOARD_H; j++) {
        for (i = 0; i < BOARD_W; i++) {
            if (board[j][i] == TRAIL) {
                if (fill)
                    board[j][i] = FILLED;
                else
                    board[j][i] = EMPTIED;
            }
        }
    }

    if (fill) {
        int v = CHECKED;
        for (i = 0; i < player.level + STARTING_QIXES; i++) /* add qixes to board */
            board[pos(qixes[i].y - BOARD_Y)]
                 [pos(qixes[i].x - BOARD_X)] = QIX;

        init_testboard();
        for (j = 1; j < BOARD_H - 1; j++) {
            for (i = 0; i < BOARD_W - 0; i++) {
                if (board[j][i] != FILLED) {
                    ret = infested_area (i, j, v);
                    if (ret < 0 || ( ret == 0 && fill_area (i, j) ) )
                        quit = true;
                    v++;
                }
            }
        }

        for (i = 0; i < player.level + STARTING_QIXES; i++) /* add qixes to board */
            board[pos(qixes[i].y - BOARD_Y)]
                 [pos(qixes[i].x - BOARD_X)] = EMPTIED;
        percentage_cache = percentage();
     }

     rb->button_clear_queue();
}

/* returns the color the real pixel(x,y) on the lcd is pointing at */
static inline unsigned int getpixel (int x, int y)
{
    const int a = pos (x - BOARD_X), b = pos (y - BOARD_Y);
    if ((a > 0) && (a < BOARD_W) && (b > 0) && (b < BOARD_H))   /* if inside board */
        return board[b][a];
    else
        return FILLED;
}

/* returns the color the ball on (newx,newy) is heading at *----*
   checks the four edge points of the square if 1st of all |    |
   are a trail (cause it's a lose life situation) and 2nd  |    |
   if it's filled so it needs to bounce.                   *____*
 */
static inline unsigned int next_hit (int newx, int newy)
{
    if ((getpixel (newx, newy) == TRAIL)
        || (getpixel (newx, newy + CUBE_SIZE - 1) == TRAIL)
        || (getpixel (newx + CUBE_SIZE - 1, newy) == TRAIL)
        || (getpixel (newx + CUBE_SIZE - 1, newy + CUBE_SIZE - 1) == TRAIL))
        return TRAIL;
    else if ((getpixel (newx, newy) == FILLED)
             || (getpixel (newx, newy + CUBE_SIZE - 1) == FILLED)
             || (getpixel (newx + CUBE_SIZE - 1, newy) == FILLED)
             || (getpixel (newx + CUBE_SIZE - 1, newy + CUBE_SIZE - 1) ==
                 FILLED))
        return FILLED;
    else
        return EMPTIED;
}

static void die (void)
{
    player.lives--;
    if (player.lives == 0)
        player.gameover = true;
    else {
        refresh_board ();
        rb->splash (HZ, "Crash!");
        complete_trail (false);
        player.move = MOVE_NO;
        player.drawing = false;
        player.i = BOARD_W / 2;
        player.j = 1;
    }
}

/* returns true if the (side) of the block          -***-
   starting from (newx,newy) has any filled pixels  *   *
                                                    -***-
 */
static inline bool line_check_lt (int newx, int newy)
{
    return    getpixel (newx, newy + CUBE_SIZE/2-1) == FILLED
           && getpixel (newx, newy + CUBE_SIZE/2  ) == FILLED;
}
static inline bool line_check_rt (int newx, int newy)
{
    return    getpixel (newx + CUBE_SIZE-1, newy + CUBE_SIZE/2-1) == FILLED
           && getpixel (newx + CUBE_SIZE-1, newy + CUBE_SIZE/2  ) == FILLED;
}
static inline bool line_check_up (int newx, int newy)
{
    return    getpixel (newx + CUBE_SIZE/2-1, newy) == FILLED
           && getpixel (newx + CUBE_SIZE/2  , newy) == FILLED;
}
static inline bool line_check_dn (int newx, int newy)
{
    return    getpixel (newx + CUBE_SIZE/2-1, newy + CUBE_SIZE-1) == FILLED
           && getpixel (newx + CUBE_SIZE/2  , newy + CUBE_SIZE-1) == FILLED;
}

static inline void move_qix (struct qix *q)
{
    int newx, newy;
    newx = get_newx (q->x, q->velocity, q->angle);
    newy = get_newy (q->y, q->velocity, q->angle);
    switch (next_hit (newx, newy))
    {
        case EMPTIED:
            q->x = newx;
            q->y = newy;
            break;
        case FILLED:
        {
            const int a = q->angle;
            q->angle =
                ((a&(DIR_UU|DIR_U))
                    ? (line_check_up (newx, newy) ? ((a&(DIR_UU|DIR_U))>>4)
                                                  : (a&(DIR_UU|DIR_U)))
                    : 0)
                |
                ((a&(DIR_RR|DIR_R))
                    ? (line_check_rt (newx, newy) ? ((a&(DIR_RR|DIR_R))>>4)
                                                  : (a&(DIR_RR|DIR_R)))
                    : 0)
                |
                ((a&(DIR_DD|DIR_D))
                    ? (line_check_dn (newx, newy) ? ((a&(DIR_DD|DIR_D))<<4)
                                                  : (a&(DIR_DD|DIR_D)))
                    : 0)
                |
                ((a&(DIR_LL|DIR_L))
                    ? (line_check_lt (newx, newy) ? ((a&(DIR_LL|DIR_L))<<4)
                                                  : (a&(DIR_LL|DIR_L)))
                    : 0);
                q->x = get_newx (q->x, q->velocity, q->angle);
                q->y = get_newy (q->y, q->velocity, q->angle);
            break;
        }
        case TRAIL:
            die();
            break;
    }
}

/* move the board forward timewise */
static inline void move_board (void)
{
    int j, newi, newj;

    for (j = 0; j < player.level + STARTING_QIXES; j++)
        move_qix (&qixes[j]);
    /* move_qix(&black_qix,true); */
    if (player.move) {
        newi = player.i;
        newj = player.j;
        switch (player.move) {
            case MOVE_UP:
                if (player.j > 1)
                    newj--;
                break;
            case MOVE_DN:
                if (player.j < BOARD_H - 2)
                    newj++;
                break;
            case MOVE_LT:
                if (player.i > 0)
                    newi--;
                break;
            case MOVE_RT:
                if (player.i < BOARD_W - 1)
                    newi++;
                break;
            default:
                break;
        }

        if ((player.drawing) && (board[newj][newi] == EMPTIED)) /* continue drawing */
            board[newj][newi] = TRAIL;
        else if ((player.drawing) && (board[newj][newi] == FILLED)) {   /* finish drawing */
            player.move = MOVE_NO;      /* stop moving */
            player.drawing = false;
            complete_trail (true);
        } else if ((board[player.j][player.i] == FILLED)
                   && (board[newj][newi] == EMPTIED)) {
            /* start drawing */
            player.drawing = true;
            board[newj][newi] = TRAIL;
        /* if the block after next is empty and we're moving onto filled, stop */
        } else if ((board[newj][newi] == FILLED)
                   && (board[newj + newj-player.j][newi + newi-player.i] == EMPTIED)) {
            player.move = MOVE_NO;
        }
        player.i = newi;
        player.j = newj;
    }
    if (percentage_cache >= difficulty) {               /* finished level */
        rb->splash (HZ * 2, "Level %d finished", player.level+1);
        player.score += percentage_cache;
        if (player.level < MAX_LEVEL)
            player.level++;
        init_board ();
        refresh_board ();
        rb->splash (HZ * 2, "Ready?");
    }
}

/* the main menu */
static int game_menu (void)
{
    MENUITEM_STRINGLIST(menu, "XOBOX Menu", NULL, "Start New Game",
                        "Speed","Difficulty","Quit");
    int selection = 0;
#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground (rb->global_settings->fg_color);
    rb->lcd_set_background (rb->global_settings->bg_color);
#elif LCD_DEPTH>=2
    rb->lcd_set_foreground(LCD_BLACK);
    rb->lcd_set_background(LCD_WHITE);
#endif
    for (;;) {
        rb->do_menu(&menu,&selection, NULL, false);
        if (selection==1)
            rb->set_int ("Speed", "", UNIT_INT, &speed, NULL, 1, 1, 10, NULL);
        else if (selection==2)
            rb->set_int ("Difficulty", "", UNIT_INT, &difficulty, NULL,
                         5, 50, 95, NULL);
        else
            break;
    }
    if (selection != MENU_START) {
        selection = MENU_QUIT;
    }
    return selection;
}

/* init game's variables */
static void init_game (void)
{
    player.level = 0;
    player.score = 0;
    player.lives = 3;
    player.gameover = false;
    player.drawing = false;
    rb->lcd_setfont(FONT_SYSFIXED);
    init_board ();
    refresh_board ();
    rb->splash (HZ * 2, "Ready?");
}

/* general keypad handler loop */
static int xobox_loop (void)
{
    int button = 0, ret;
    bool pause = false;
    int end;

    while (!quit) {
        end = *rb->current_tick + ((11-speed)*HZ)/100;

#ifdef HAS_BUTTON_HOLD
        if (rb->button_hold()) {
        pause = true;
        rb->splash (HZ, "PAUSED");
        }
#endif

        button = rb->button_get_w_tmo (true);
        switch (button) {
            case UP:
            case UP|BUTTON_REPEAT:
                player.move = MOVE_UP;
                break;
            case DOWN:
            case DOWN|BUTTON_REPEAT:
                player.move = MOVE_DN;
                break;
            case LEFT:
            case LEFT|BUTTON_REPEAT:
                player.move = MOVE_LT;
                break;
            case RIGHT:
            case RIGHT|BUTTON_REPEAT:
                player.move = MOVE_RT;
                break;
            case PAUSE:
                pause = !pause;
                if (pause)
                    rb->splash (HZ, "Paused");
                break;
            case QUIT:
                ret = game_menu ();
                if (ret == MENU_START)
                    init_game ();
                else
                {
                    quit = true;
                    continue;
                }
                break;
            default:
                if (rb->default_event_handler (button) == SYS_USB_CONNECTED)
                    return PLUGIN_USB_CONNECTED;
                break;
        }
        if (!pause) {
            move_board ();
            refresh_board ();
        }
        if (player.gameover) {
            rb->splash (HZ, "Game Over!");
            ret = game_menu ();
            if (ret == MENU_START)
                init_game ();
            else
                quit = true;
        }

        if (end > *rb->current_tick)
            rb->sleep (end - *rb->current_tick);
        else
            rb->yield ();

    }                           /* end while */
    return PLUGIN_OK;           /* for no warnings on compiling */
}

/* plugin main procedure */
enum plugin_status plugin_start (struct plugin_api *api, void *parameter)
{
    int ret = PLUGIN_OK;

    (void) parameter;
    rb = api;

    rb->lcd_setfont (FONT_SYSFIXED);
#if LCD_DEPTH>=2
    rb->lcd_set_backdrop(NULL);
#endif

    /* Turn off backlight timeout */
    backlight_force_on(rb); /* backlight control in lib/helper.c */

    quit = false;

    randomize ();
    if (game_menu () == MENU_START) {
        init_game ();
        ret = xobox_loop ();
    }

    /* Turn on backlight timeout (revert to settings) */
    backlight_use_settings(rb); /* backlight control in lib/helper.c */
    rb->lcd_setfont (FONT_UI);

    return ret;
}
