/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2002 Eric Linenberg
 * February 2003: Robert Hak performs a cleanup/rewrite/feature addition.
 *                Eric smiles.  Bjorn cries.  Linus say 'huh?'.
 * March 2007: Sean Morrisey performs a major rewrite/feature addition.
 *
 * 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 "lib/playback_control.h"

#ifdef HAVE_LCD_BITMAP

PLUGIN_HEADER

#if LCD_DEPTH >= 2 && ((LCD_HEIGHT >= 96 && LCD_WIDTH >= 152) || \
    (LCD_HEIGHT >= 121 && LCD_WIDTH >= 120))
extern const fb_data sokoban_tiles[];
#endif

#define SOKOBAN_TITLE        "Sokoban"

#define SOKOBAN_LEVELS_FILE  PLUGIN_GAMES_DIR "/sokoban.levels"
#define SOKOBAN_SAVE_FILE    PLUGIN_GAMES_DIR "/sokoban.save"
#define SOKOBAN_SAVE_FOLDER  "/games"

/* Magnify is the number of pixels for each block.
 * Set dynamically so all targets can support levels
 * that fill their entire screen, less the stat box.
 * 16 rows & 20 cols minimum */
#if (LCD_HEIGHT >= 224) && (LCD_WIDTH >= 320)
#define MAGNIFY 14
#define ROWS (LCD_HEIGHT/MAGNIFY)
#define COLS ((LCD_WIDTH-40)/MAGNIFY)
#elif (LCD_HEIGHT >= 249) && (LCD_WIDTH >= 280)
#define MAGNIFY 14
#define ROWS ((LCD_HEIGHT-25)/MAGNIFY)
#define COLS (LCD_WIDTH/MAGNIFY)
#elif (LCD_HEIGHT >= 144) && (LCD_WIDTH >= 220)
#define MAGNIFY 9
#define ROWS (LCD_HEIGHT/MAGNIFY)
#define COLS ((LCD_WIDTH-40)/MAGNIFY)
#elif (LCD_HEIGHT >= 169) && (LCD_WIDTH+4 >= 180) /* plus 4 for sansa */
#define MAGNIFY 9
#define ROWS ((LCD_HEIGHT-25)/MAGNIFY)
#define COLS ((LCD_WIDTH+4)/MAGNIFY)
#elif (LCD_HEIGHT >= 96) && (LCD_WIDTH >= 160)
#define MAGNIFY 6
#define ROWS (LCD_HEIGHT/MAGNIFY)
#define COLS ((LCD_WIDTH-40)/MAGNIFY)
#elif (LCD_HEIGHT >= 121) && (LCD_WIDTH >= 120)
#define MAGNIFY 6
#define ROWS ((LCD_HEIGHT-25)/MAGNIFY)
#define COLS (LCD_WIDTH/MAGNIFY)
#else
#define MAGNIFY 4
#define ROWS 16
#define COLS 20
#endif

/* Use either all but 16k of the plugin buffer for level data
 * or 128k, which ever is less */
#if PLUGIN_BUFFER_SIZE - 0x4000 < 0x20000
#define MAX_LEVEL_DATA       (PLUGIN_BUFFER_SIZE - 0x4000)
#else
#define MAX_LEVEL_DATA       0x20000
#endif

/* Number of levels for which to allocate buffer indexes */
#define MAX_LEVELS           MAX_LEVEL_DATA/70

/* Use 4k plus remaining plugin buffer (-12k for prog) for undo, up to 64k */
#if PLUGIN_BUFFER_SIZE - MAX_LEVEL_DATA - 0x3000 > 0x10000
#define MAX_UNDOS            0x10000
#else
#define MAX_UNDOS            (PLUGIN_BUFFER_SIZE - MAX_LEVEL_DATA - 0x3000)
#endif

/* Move/push definitions for undo */
#define SOKOBAN_PUSH_LEFT    'L'
#define SOKOBAN_PUSH_RIGHT   'R'
#define SOKOBAN_PUSH_UP      'U'
#define SOKOBAN_PUSH_DOWN    'D'
#define SOKOBAN_MOVE_LEFT    'l'
#define SOKOBAN_MOVE_RIGHT   'r'
#define SOKOBAN_MOVE_UP      'u'
#define SOKOBAN_MOVE_DOWN    'd'

#define SOKOBAN_MOVE_DIFF    (SOKOBAN_MOVE_LEFT-SOKOBAN_PUSH_LEFT)
#define SOKOBAN_MOVE_MIN     SOKOBAN_MOVE_DOWN

/* variable button definitions */
#if (CONFIG_KEYPAD == RECORDER_PAD) || \
    (CONFIG_KEYPAD == ARCHOS_AV300_PAD)
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_UP
#define SOKOBAN_DOWN BUTTON_DOWN
#define SOKOBAN_MENU BUTTON_OFF
#define SOKOBAN_UNDO BUTTON_ON
#define SOKOBAN_REDO BUTTON_PLAY
#define SOKOBAN_LEVEL_DOWN BUTTON_F1
#define SOKOBAN_LEVEL_REPEAT BUTTON_F2
#define SOKOBAN_LEVEL_UP BUTTON_F3
#define SOKOBAN_PAUSE BUTTON_PLAY
#define BUTTON_SAVE BUTTON_ON
#define BUTTON_SAVE_NAME "ON"

#elif CONFIG_KEYPAD == ONDIO_PAD
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_UP
#define SOKOBAN_DOWN BUTTON_DOWN
#define SOKOBAN_MENU BUTTON_OFF
#define SOKOBAN_UNDO_PRE BUTTON_MENU
#define SOKOBAN_UNDO (BUTTON_MENU | BUTTON_REL)
#define SOKOBAN_REDO (BUTTON_MENU | BUTTON_DOWN)
#define SOKOBAN_LEVEL_DOWN (BUTTON_MENU | BUTTON_LEFT)
#define SOKOBAN_LEVEL_REPEAT (BUTTON_MENU | BUTTON_UP)
#define SOKOBAN_LEVEL_UP (BUTTON_MENU | BUTTON_RIGHT)
#define SOKOBAN_PAUSE BUTTON_MENU
#define BUTTON_SAVE BUTTON_MENU
#define BUTTON_SAVE_NAME "MENU"

#elif (CONFIG_KEYPAD == IRIVER_H100_PAD) || \
      (CONFIG_KEYPAD == IRIVER_H300_PAD)
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_UP
#define SOKOBAN_DOWN BUTTON_DOWN
#define SOKOBAN_MENU BUTTON_OFF
#define SOKOBAN_UNDO BUTTON_REC
#define SOKOBAN_REDO BUTTON_MODE
#define SOKOBAN_LEVEL_DOWN (BUTTON_ON | BUTTON_DOWN)
#define SOKOBAN_LEVEL_REPEAT BUTTON_ON
#define SOKOBAN_LEVEL_UP (BUTTON_ON | BUTTON_UP)
#define SOKOBAN_PAUSE BUTTON_ON
#define BUTTON_SAVE BUTTON_MODE
#define BUTTON_SAVE_NAME "MODE"

#define SOKOBAN_RC_MENU BUTTON_RC_STOP

#elif (CONFIG_KEYPAD == IPOD_4G_PAD) || \
      (CONFIG_KEYPAD == IPOD_3G_PAD) || \
      (CONFIG_KEYPAD == IPOD_1G2G_PAD)
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_MENU
#define SOKOBAN_DOWN BUTTON_PLAY
#define SOKOBAN_MENU (BUTTON_SELECT | BUTTON_MENU)
#define SOKOBAN_UNDO_PRE BUTTON_SELECT
#define SOKOBAN_UNDO (BUTTON_SELECT | BUTTON_REL)
#define SOKOBAN_REDO (BUTTON_SELECT | BUTTON_PLAY)
#define SOKOBAN_LEVEL_DOWN (BUTTON_SELECT | BUTTON_LEFT)
#define SOKOBAN_LEVEL_UP (BUTTON_SELECT | BUTTON_RIGHT)
#define SOKOBAN_PAUSE BUTTON_SELECT
#define BUTTON_SAVE BUTTON_SELECT
#define BUTTON_SAVE_NAME "SELECT"

/* FIXME: if/when simultaneous button presses work for X5/M5,
 * add level up/down */
#elif CONFIG_KEYPAD == IAUDIO_X5M5_PAD
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_UP
#define SOKOBAN_DOWN BUTTON_DOWN
#define SOKOBAN_MENU BUTTON_POWER
#define SOKOBAN_UNDO_PRE BUTTON_SELECT
#define SOKOBAN_UNDO (BUTTON_SELECT | BUTTON_REL)
#define SOKOBAN_LEVEL_REPEAT BUTTON_REC
#define SOKOBAN_REDO BUTTON_PLAY
#define SOKOBAN_PAUSE BUTTON_PLAY
#define BUTTON_SAVE BUTTON_SELECT
#define BUTTON_SAVE_NAME "SELECT"

#elif CONFIG_KEYPAD == IRIVER_H10_PAD
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_SCROLL_UP
#define SOKOBAN_DOWN BUTTON_SCROLL_DOWN
#define SOKOBAN_MENU BUTTON_POWER
#define SOKOBAN_UNDO_PRE BUTTON_REW
#define SOKOBAN_UNDO (BUTTON_REW | BUTTON_REL)
#define SOKOBAN_REDO BUTTON_FF
#define SOKOBAN_LEVEL_DOWN (BUTTON_PLAY | BUTTON_SCROLL_DOWN)
#define SOKOBAN_LEVEL_REPEAT (BUTTON_PLAY | BUTTON_RIGHT)
#define SOKOBAN_LEVEL_UP (BUTTON_PLAY | BUTTON_SCROLL_UP)
#define SOKOBAN_PAUSE BUTTON_PLAY
#define BUTTON_SAVE BUTTON_PLAY
#define BUTTON_SAVE_NAME "PLAY"

#elif CONFIG_KEYPAD == GIGABEAT_PAD
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_UP
#define SOKOBAN_DOWN BUTTON_DOWN
#define SOKOBAN_MENU BUTTON_POWER
#define SOKOBAN_UNDO BUTTON_SELECT
#define SOKOBAN_REDO BUTTON_A
#define SOKOBAN_LEVEL_DOWN BUTTON_VOL_DOWN
#define SOKOBAN_LEVEL_REPEAT BUTTON_MENU
#define SOKOBAN_LEVEL_UP BUTTON_VOL_UP
#define SOKOBAN_PAUSE BUTTON_SELECT
#define BUTTON_SAVE BUTTON_SELECT
#define BUTTON_SAVE_NAME "SELECT"

#elif CONFIG_KEYPAD == SANSA_E200_PAD
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_UP
#define SOKOBAN_DOWN BUTTON_DOWN
#define SOKOBAN_MENU BUTTON_POWER
#define SOKOBAN_UNDO_PRE BUTTON_SELECT
#define SOKOBAN_UNDO (BUTTON_SELECT | BUTTON_REL)
#define SOKOBAN_REDO BUTTON_REC
#define SOKOBAN_LEVEL_DOWN (BUTTON_SELECT | BUTTON_DOWN)
#define SOKOBAN_LEVEL_REPEAT (BUTTON_SELECT | BUTTON_RIGHT)
#define SOKOBAN_LEVEL_UP (BUTTON_SELECT | BUTTON_UP)
#define SOKOBAN_PAUSE BUTTON_SELECT
#define BUTTON_SAVE BUTTON_SELECT
#define BUTTON_SAVE_NAME "SELECT"

#elif CONFIG_KEYPAD == GIGABEAT_S_PAD
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_UP
#define SOKOBAN_DOWN BUTTON_DOWN
#define SOKOBAN_MENU BUTTON_MENU
#define SOKOBAN_UNDO BUTTON_VOL_UP
#define SOKOBAN_REDO BUTTON_VOL_DOWN
#define SOKOBAN_LEVEL_DOWN BUTTON_PREV
#define SOKOBAN_LEVEL_REPEAT BUTTON_PLAY
#define SOKOBAN_LEVEL_UP BUTTON_NEXT
#define SOKOBAN_PAUSE BUTTON_SELECT
#define BUTTON_SAVE BUTTON_SELECT
#define BUTTON_SAVE_NAME "SELECT"

#elif CONFIG_KEYPAD == MROBE100_PAD
#define SOKOBAN_LEFT BUTTON_LEFT
#define SOKOBAN_RIGHT BUTTON_RIGHT
#define SOKOBAN_UP BUTTON_UP
#define SOKOBAN_DOWN BUTTON_DOWN
#define SOKOBAN_MENU BUTTON_POWER
#define SOKOBAN_UNDO BUTTON_SELECT
#define SOKOBAN_REDO BUTTON_MENU
#define SOKOBAN_LEVEL_DOWN (BUTTON_DISPLAY | BUTTON_DOWN)
#define SOKOBAN_LEVEL_REPEAT (BUTTON_DISPLAY | BUTTON_RIGHT)
#define SOKOBAN_LEVEL_UP (BUTTON_DISPLAY | BUTTON_UP)
#define SOKOBAN_PAUSE BUTTON_SELECT
#define BUTTON_SAVE BUTTON_SELECT
#define BUTTON_SAVE_NAME "SELECT"

#elif CONFIG_KEYPAD == IAUDIO_M3_PAD
#define SOKOBAN_LEFT BUTTON_RC_REW
#define SOKOBAN_RIGHT BUTTON_RC_FF
#define SOKOBAN_UP BUTTON_RC_VOL_UP
#define SOKOBAN_DOWN BUTTON_RC_VOL_DOWN
#define SOKOBAN_MENU BUTTON_RC_REC
#define SOKOBAN_UNDO BUTTON_RC_MODE
#define SOKOBAN_REDO BUTTON_RC_MENU
#define SOKOBAN_PAUSE BUTTON_RC_PLAY
#define BUTTON_SAVE BUTTON_RC_PLAY
#define BUTTON_SAVE_NAME "PLAY"

#define SOKOBAN_RC_MENU BUTTON_REC

#elif CONFIG_KEYPAD == COWOND2_PAD
#define SOKOBAN_MENU BUTTON_MENU
#define SOKOBAN_MENU_NAME "[MENU]"

#else
#error No keymap defined!
#endif

#ifdef HAVE_TOUCHPAD
#ifndef SOKOBAN_LEFT
#define SOKOBAN_LEFT          BUTTON_MIDLEFT
#endif
#ifndef SOKOBAN_RIGHT
#define SOKOBAN_RIGHT         BUTTON_MIDRIGHT
#endif
#ifndef SOKOBAN_UP
#define SOKOBAN_UP            BUTTON_TOPMIDDLE
#endif
#ifndef SOKOBAN_DOWN
#define SOKOBAN_DOWN          BUTTON_BOTTOMMIDDLE
#endif
#ifndef SOKOBAN_MENU
#define SOKOBAN_MENU          BUTTON_TOPLEFT
#define SOKOBAN_MENU_NAME     "[TOPLEFT]"
#endif
#ifndef SOKOBAN_UNDO
#define SOKOBAN_UNDO          BUTTON_BOTTOMRIGHT
#define SOKOBAN_UNDO_NAME     "[BOTTOMRIGHT]"
#endif
#ifndef SOKOBAN_REDO
#define SOKOBAN_REDO          BUTTON_BOTTOMLEFT
#define SOKOBAN_REDO_NAME     "[BOTTOMLEFT]"
#endif
#ifndef SOKOBAN_PAUSE
#define SOKOBAN_PAUSE         BUTTON_CENTER
#define SOKOBAN_PAUSE_NAME    "[CENTER]"
#endif
#ifndef SOKOBAN_LEVEL_REPEAT
#define SOKOBAN_LEVEL_REPEAT  BUTTON_TOPRIGHT
#define SOKOBAN_LEVEL_REPEAT_NAME "[TOPRIGHT]"
#endif
#ifndef BUTTON_SAVE
#define BUTTON_SAVE           BUTTON_CENTER
#define BUTTON_SAVE_NAME      "CENTER"
#endif
#endif

#define SOKOBAN_FONT FONT_SYSFIXED


/* The Location, Undo and LevelInfo structs are OO-flavored.
 * (oooh!-flavored as Schnueff puts it.)  It makes more you have to know,
 * but the overall data layout becomes more manageable. */

/* Level data & stats */
struct LevelInfo {
    int index;               /* Level index (level number - 1) */
    int moves;               /* Moves & pushes for the stats */
    int pushes;
    short boxes_to_go;       /* Number of unplaced boxes remaining in level */
    short height;            /* Height & width for centering level display */
    short width;
};

struct Location {
    short row;
    short col;
};

/* Our full undo history */
static struct UndoInfo {
    int count;               /* How many undos have been done */
    int current;             /* Which history is the current undo */
    int max;                 /* Which history is the max redoable */
    char history[MAX_UNDOS];
} undo_info;

/* Our playing board */
static struct BoardInfo {
    char board[ROWS][COLS];  /* The current board data */
    struct LevelInfo level;  /* Level data & stats */
    struct Location player;  /* Where the player is */
    int max_level;           /* The number of levels we have */
} current_info;

static struct BufferedBoards {
    char filename[MAX_PATH];   /* Filename of the levelset we're using */
    char data[MAX_LEVEL_DATA]; /* Buffered level data */
    int index[MAX_LEVELS + 1]; /* Where each buffered board begins & ends */
    int start;                 /* Index of first buffered board */
    int end;                   /* Index of last buffered board */
    short prebuffered_boards;  /* Number of boards before current to store */
} buffered_boards;


static struct plugin_api* rb;
MEM_FUNCTION_WRAPPERS(rb);

static char buf[ROWS*(COLS + 1)]; /* Enough for a whole board or a filename */


static void init_undo(void)
{
    undo_info.count = 0;
    undo_info.current = 0;
    undo_info.max = 0;
}

static void get_delta(char direction, short *d_r, short *d_c)
{
    switch (direction) {
        case SOKOBAN_PUSH_LEFT:
        case SOKOBAN_MOVE_LEFT:
            *d_r = 0;
            *d_c = -1;
            break;
        case SOKOBAN_PUSH_RIGHT:
        case SOKOBAN_MOVE_RIGHT:
            *d_r = 0;
            *d_c = 1;
            break;
        case SOKOBAN_PUSH_UP:
        case SOKOBAN_MOVE_UP:
            *d_r = -1;
            *d_c = 0;
            break;
        case SOKOBAN_PUSH_DOWN:
        case SOKOBAN_MOVE_DOWN:
            *d_r = 1;
            *d_c = 0;
    }
}

static bool undo(void)
{
    char undo;
    short r, c;
    short d_r = 0, d_c = 0; /* delta row & delta col */
    char *space_cur, *space_next, *space_prev;
    bool undo_push = false;

    /* If no more undos or we've wrapped all the way around, quit */
    if (undo_info.count == 0 || undo_info.current - 1 == undo_info.max)
        return false;

    /* Move to previous undo in the list */
    if (undo_info.current == 0 && undo_info.count > 1)
        undo_info.current = MAX_UNDOS - 1;
    else
        undo_info.current--;

    undo_info.count--;

    undo = undo_info.history[undo_info.current];

    if (undo < SOKOBAN_MOVE_MIN)
        undo_push = true;

    get_delta(undo, &d_r, &d_c);

    r = current_info.player.row;
    c = current_info.player.col;

    /* Give the 3 spaces we're going to use better names */
    space_cur = &current_info.board[r][c];
    space_next = &current_info.board[r + d_r][c + d_c];
    space_prev = &current_info.board[r - d_r][c - d_c];

    /* Update board info */
    if (undo_push) {
        /* Moving box from goal to floor */
        if (*space_next == '*' && *space_cur == '@')
            current_info.level.boxes_to_go++;
        /* Moving box from floor to goal */
        else if (*space_next == '$' && *space_cur == '+')
            current_info.level.boxes_to_go--;

        /* Move box off of next space... */
        *space_next = (*space_next == '*' ? '.' : ' ');
        /* ...and on to current space */
        *space_cur = (*space_cur == '+' ? '*' : '$');

        current_info.level.pushes--;
    } else
        /* Just move player off of current space */
        *space_cur = (*space_cur == '+' ? '.' : ' ');
    /* Move player back to previous space */
    *space_prev = (*space_prev == '.' ? '+' : '@');

    /* Update position */
    current_info.player.row -= d_r;
    current_info.player.col -= d_c;

    current_info.level.moves--;

    return true;
}

static void add_undo(char undo)
{
    undo_info.history[undo_info.current] = undo;

    /* Wrap around if MAX_UNDOS exceeded */
    if (undo_info.current < (MAX_UNDOS - 1))
        undo_info.current++;
    else
        undo_info.current = 0;

    if (undo_info.count < MAX_UNDOS)
        undo_info.count++;
}

static bool move(char direction, bool redo)
{
    short r, c;
    short d_r = 0, d_c = 0; /* delta row & delta col */
    char *space_cur, *space_next, *space_beyond;
    bool push = false;

    get_delta(direction, &d_r, &d_c);

    r = current_info.player.row;
    c = current_info.player.col;

    /* Check for out-of-bounds */
    if (r + 2*d_r < 0 || r + 2*d_r >= ROWS ||
        c + 2*d_c < 0 || c + 2*d_c >= COLS)
        return false;

    /* Give the 3 spaces we're going to use better names */
    space_cur = &current_info.board[r][c];
    space_next = &current_info.board[r + d_r][c + d_c];
    space_beyond = &current_info.board[r + 2*d_r][c + 2*d_c];

    if (*space_next == '$' || *space_next == '*') {
        /* Change direction from move to push for undo */
        if (direction >= SOKOBAN_MOVE_MIN)
            direction -= SOKOBAN_MOVE_DIFF;
        push = true;
    }
    else if (direction < SOKOBAN_MOVE_MIN)
        /* Change back to move if redo/solution playback push is invalid */
        direction += SOKOBAN_MOVE_DIFF;

    /* Update board info */
    if (push) {
        /* Moving box from goal to floor */
        if (*space_next == '*' && *space_beyond == ' ')
            current_info.level.boxes_to_go++;
        /* Moving box from floor to goal */
        else if (*space_next == '$' && *space_beyond == '.')
            current_info.level.boxes_to_go--;
        /* Check for invalid move */
        else if (*space_beyond != '.' && *space_beyond != ' ')
            return false;

        /* Move player onto next space */
        *space_next = (*space_next == '*' ? '+' : '@');
        /* Move box onto space beyond next */
        *space_beyond = (*space_beyond == '.' ? '*' : '$');

        current_info.level.pushes++;
    } else {
        /* Check for invalid move */
        if (*space_next != '.' && *space_next != ' ')
            return false;

        /* Move player onto next space */
        *space_next = (*space_next == '.' ? '+' : '@');
    }
    /* Move player off of current space */
    *space_cur = (*space_cur == '+' ? '.' : ' ');

    /* Update position */
    current_info.player.row += d_r;
    current_info.player.col += d_c;

    current_info.level.moves++;

    /* Update undo_info.max to current on every normal move,
     * except if it's the same as a redo. */
        /* normal move and either */
    if (!redo &&
          /* moves have been undone... */
        ((undo_info.max != undo_info.current &&
          /* ...and the current move is NOT the same as the one in history */
          undo_info.history[undo_info.current] != direction) ||
         /* or moves have not been undone */
         undo_info.max == undo_info.current)) {
        add_undo(direction);
        undo_info.max = undo_info.current;
    } else /* redo move or move was same as redo */
        add_undo(direction); /* add_undo to update current */

    return true;
}

#ifdef SOKOBAN_REDO
static bool redo(void)
{
    /* If no moves have been undone, quit */
    if (undo_info.current == undo_info.max)
        return false;

    return move(undo_info.history[(undo_info.current < MAX_UNDOS ?
                                   undo_info.current : 0)], true);
}
#endif

static void init_boards(void)
{
    rb->strncpy(buffered_boards.filename, SOKOBAN_LEVELS_FILE, MAX_PATH);

    current_info.level.index = 0;
    current_info.player.row = 0;
    current_info.player.col = 0;
    current_info.max_level = 0;

    buffered_boards.start = 0;
    buffered_boards.end = 0;
    buffered_boards.prebuffered_boards = 0;

    init_undo();
}

static bool read_levels(bool initialize)
{
    int fd = 0;
    short len;
    short lastlen = 0;
    short row = 0;
    int level_count = 0;

    int i = 0;
    int level_len = 0;
    bool index_set = false;

    /* Get the index of the first level to buffer */
    if (current_info.level.index > buffered_boards.prebuffered_boards &&
        !initialize)
        buffered_boards.start = current_info.level.index -
                                buffered_boards.prebuffered_boards;
    else
        buffered_boards.start = 0;

    if ((fd = rb->open(buffered_boards.filename, O_RDONLY)) < 0) {
        rb->splash(HZ*2, "Unable to open %s", buffered_boards.filename);
        return false;
    }

    do {
        len = rb->read_line(fd, buf, sizeof(buf));

        /* Correct len when trailing \r's or \n's are counted */
        if (len > 2 && buf[len - 2] == '\0')
            len -= 2;
        else if (len > 1 && buf[len - 1] == '\0')
            len--;

        /* Skip short lines & lines with non-level data */
        if (len >= 3 && ((buf[0] >= '1' && buf[0] <= '9') || buf[0] == '#' ||
            buf[0] == ' ' || buf[0] == '-' || buf[0] == '_')) {
            if (level_count >= buffered_boards.start) {
                /* Set the index of this level */
                if (!index_set &&
                    level_count - buffered_boards.start < MAX_LEVELS) {
                    buffered_boards.index[level_count - buffered_boards.start]
                                                                           = i;
                    index_set = true;
                }
                /* Copy buffer to board data */
                if (i + level_len + len < MAX_LEVEL_DATA) {
                    rb->memcpy(&buffered_boards.data[i + level_len], buf, len);
                    buffered_boards.data[i + level_len + len] = '\n';
                }
            }
            level_len += len + 1;
            row++;

        /* If newline & level is tall enough or is RLE */
        } else if (buf[0] == '\0' && (row > 2 || lastlen > 22)) {
            level_count++;
            if (level_count >= buffered_boards.start) {
                i += level_len;
                if (i < MAX_LEVEL_DATA)
                    buffered_boards.end = level_count;
                else if (!initialize)
                    break;
            }
            row = 0;
            level_len = 0;
            index_set = false;

        } else if (len > 22)
            len = 1;

    } while ((lastlen = len));

    /* Set the index of the end of the last level */
    if (level_count - buffered_boards.start < MAX_LEVELS)
        buffered_boards.index[level_count - buffered_boards.start] = i;

    if (initialize) {
        current_info.max_level = level_count;
        buffered_boards.prebuffered_boards = buffered_boards.end/2;
    }

    rb->close(fd);

    return true;
}

static void load_level(void)
{
    int c, r;
    int i, n;
    int level_size;
    int index = current_info.level.index - buffered_boards.start;
    char *level;

    /* Get the buffered board index of the current level */
    if (current_info.level.index < buffered_boards.start ||
        current_info.level.index >= buffered_boards.end) {
        read_levels(false);
        if (current_info.level.index > buffered_boards.prebuffered_boards)
            index = buffered_boards.prebuffered_boards;
        else
            index = current_info.level.index;
    }
    level = &buffered_boards.data[buffered_boards.index[index]];

    /* Reset level info */
    current_info.level.moves = 0;
    current_info.level.pushes = 0;
    current_info.level.boxes_to_go = 0;
    current_info.level.width = 0;

    /* Clear board */
    for (r = 0; r < ROWS; r++)
        for (c = 0; c < COLS; c++)
            current_info.board[r][c] = 'X';

    level_size = buffered_boards.index[index + 1] -
                 buffered_boards.index[index];

    for (r = 0, c = 0, n = 1, i = 0; i < level_size; i++) {
        if (level[i] == '\n' || level[i] == '|') {
            if (c > 3) {
                /* Update max width of level & go to next row */
                if (c > current_info.level.width)
                    current_info.level.width = c;
                c = 0;
                r++;
                if (r >= ROWS)
                    break;
            }
        } else if (c < COLS) {
            /* Read RLE character's length into n */
            if (level[i] >= '0' && level[i] <= '9') {
                n = level[i++] - '0';
                if (level[i] >= '0' && level[i] <= '9')
                    n = n*10 + level[i++] - '0';
            }

            /* Cleanup & replace */
            if (level[i] == '%')
                level[i] = '*';
            else if (level[i] == '-' || level[i] == '_')
                level[i] = ' ';

            if (n > 1) {
                if (c + n >= COLS)
                    n = COLS - c;

                if (level[i] == '.')
                    current_info.level.boxes_to_go += n;

                /* Put RLE character n times */
                while (n--)
                    current_info.board[r][c++] = level[i];
                n = 1;

            } else {
                if (level[i] == '.' || level[i] == '+')
                    current_info.level.boxes_to_go++;

                if (level[i] == '@' ||level[i] == '+') {
                    current_info.player.row = r;
                    current_info.player.col = c;
                }

                current_info.board[r][c++] = level[i];
            }
        }
    }

    current_info.level.height = r;

#if LCD_DEPTH > 2
    /* Fill in blank space outside level on color targets */
    for (r = 0; r < ROWS; r++)
        for (c = 0; current_info.board[r][c] == ' ' && c < COLS; c++)
            current_info.board[r][c] = 'X';

    for (c = 0; c < COLS; c++) {
        for (r = 0; (current_info.board[r][c] == ' ' ||
             current_info.board[r][c] == 'X') && r < ROWS; r++)
            current_info.board[r][c] = 'X';
        for (r = ROWS - 1; (current_info.board[r][c] == ' ' ||
             current_info.board[r][c] == 'X') && r >= 0; r--)
            current_info.board[r][c] = 'X';
    }
#endif
}

static void update_screen(void)
{
    int c, r;
    int rows, cols;

#if LCD_DEPTH < 2 || ((LCD_HEIGHT < 96 || LCD_WIDTH < 152) && \
    (LCD_HEIGHT < 121 || LCD_WIDTH < 120))
    int i, j;
    int max = MAGNIFY - 1;
    int middle = max/2;
    int ldelta = (middle + 1)/2;
#endif

#if LCD_WIDTH - (COLS*MAGNIFY) < 32
#define STAT_HEIGHT 25
#define STAT_X (LCD_WIDTH - 120)/2
#define STAT_Y (LCD_HEIGHT - STAT_HEIGHT)
#define BOARD_WIDTH LCD_WIDTH
#define BOARD_HEIGHT (LCD_HEIGHT - STAT_HEIGHT)
    rb->lcd_putsxy(STAT_X + 4, STAT_Y + 4, "Level");
    rb->snprintf(buf, sizeof(buf), "%d", current_info.level.index + 1);
    rb->lcd_putsxy(STAT_X + 7, STAT_Y + 14, buf);
    rb->lcd_putsxy(STAT_X + 41, STAT_Y + 4, "Moves");
    rb->snprintf(buf, sizeof(buf), "%d", current_info.level.moves);
    rb->lcd_putsxy(STAT_X + 44, STAT_Y + 14, buf);
    rb->lcd_putsxy(STAT_X + 79, STAT_Y + 4, "Pushes");
    rb->snprintf(buf, sizeof(buf), "%d", current_info.level.pushes);
    rb->lcd_putsxy(STAT_X + 82, STAT_Y + 14, buf);

    rb->lcd_drawrect(STAT_X, STAT_Y, 38, STAT_HEIGHT);
    rb->lcd_drawrect(STAT_X + 37, STAT_Y, 39, STAT_HEIGHT);
    rb->lcd_drawrect(STAT_X + 75, STAT_Y, 45, STAT_HEIGHT);
#else
#if LCD_WIDTH - (COLS*MAGNIFY) > 40
#define STAT_X (LCD_WIDTH - 40)
#else
#define STAT_X COLS*MAGNIFY
#endif
#if LCD_HEIGHT >= 70
#define STAT_Y (LCD_HEIGHT - 70)/2
#else
#define STAT_Y (LCD_HEIGHT - 47)/2
#endif
#define STAT_WIDTH (LCD_WIDTH - STAT_X)
#define BOARD_WIDTH (LCD_WIDTH - STAT_WIDTH)
#define BOARD_HEIGHT LCD_HEIGHT
    rb->lcd_putsxy(STAT_X + 1, STAT_Y + 3, "Level");
    rb->snprintf(buf, sizeof(buf), "%d", current_info.level.index + 1);
    rb->lcd_putsxy(STAT_X + 4, STAT_Y + 13, buf);
    rb->lcd_putsxy(STAT_X + 1, STAT_Y + 26, "Moves");
    rb->snprintf(buf, sizeof(buf), "%d", current_info.level.moves);
    rb->lcd_putsxy(STAT_X + 4, STAT_Y + 36, buf);

    rb->lcd_drawrect(STAT_X, STAT_Y + 0, STAT_WIDTH, 24);
    rb->lcd_drawrect(STAT_X, STAT_Y + 23, STAT_WIDTH, 24);

#if LCD_HEIGHT >= 70
    rb->lcd_putsxy(STAT_X + 1, STAT_Y + 49, "Pushes");
    rb->snprintf(buf, sizeof(buf), "%d", current_info.level.pushes);
    rb->lcd_putsxy(STAT_X + 4, STAT_Y + 59, buf);

    rb->lcd_drawrect(STAT_X, STAT_Y + 46, STAT_WIDTH, 24);
#endif

#endif

    /* load the board to the screen */
    for (rows = 0; rows < ROWS; rows++) {
        for (cols = 0; cols < COLS; cols++) {
            c = cols*MAGNIFY +
                (BOARD_WIDTH - current_info.level.width*MAGNIFY)/2;
            r = rows*MAGNIFY +
                (BOARD_HEIGHT - current_info.level.height*MAGNIFY)/2;

            switch(current_info.board[rows][cols]) {
                case 'X': /* blank space outside of level */
                    break;

#if LCD_DEPTH >= 2 && ((LCD_HEIGHT >= 96 && LCD_WIDTH >= 152) || \
    (LCD_HEIGHT >= 121 && LCD_WIDTH >= 120))
                case ' ': /* floor */
                    rb->lcd_bitmap_part(sokoban_tiles, 0, 0*MAGNIFY, MAGNIFY,
                                        c, r, MAGNIFY, MAGNIFY);
                    break;

                case '#': /* wall */
                    rb->lcd_bitmap_part(sokoban_tiles, 0, 1*MAGNIFY, MAGNIFY,
                                        c, r, MAGNIFY, MAGNIFY);
                    break;

                case '$': /* box */
                    rb->lcd_bitmap_part(sokoban_tiles, 0, 2*MAGNIFY, MAGNIFY,
                                        c, r, MAGNIFY, MAGNIFY);
                    break;

                case '*': /* box on goal */
                    rb->lcd_bitmap_part(sokoban_tiles, 0, 3*MAGNIFY, MAGNIFY,
                                        c, r, MAGNIFY, MAGNIFY);
                    break;

                case '.': /* goal */
                    rb->lcd_bitmap_part(sokoban_tiles, 0, 4*MAGNIFY, MAGNIFY,
                                        c, r, MAGNIFY, MAGNIFY);
                    break;

                case '@': /* player */
                    rb->lcd_bitmap_part(sokoban_tiles, 0, 5*MAGNIFY, MAGNIFY,
                                        c, r, MAGNIFY, MAGNIFY);
                    break;

                case '+': /* player on goal */
                    rb->lcd_bitmap_part(sokoban_tiles, 0, 6*MAGNIFY, MAGNIFY,
                                        c, r, MAGNIFY, MAGNIFY);
                    break;
#else
                case '#': /* wall */
                    for (i = c; i < c + MAGNIFY; i++)
                        for (j = r; j < r + MAGNIFY; j++)
                            if ((i ^ j) & 1)
                                rb->lcd_drawpixel(i, j);
                    break;

                case '$': /* box */
                    rb->lcd_drawrect(c, r, MAGNIFY, MAGNIFY);
                    break;

                case '*': /* box on goal */
                    rb->lcd_drawrect(c, r, MAGNIFY, MAGNIFY);
                    rb->lcd_drawrect(c + MAGNIFY/2 - 1, r + MAGNIFY/2 - 1,
                                     MAGNIFY/2, MAGNIFY/2);
                    break;

                case '.': /* goal */
                    rb->lcd_drawrect(c + MAGNIFY/2 - 1, r + MAGNIFY/2 - 1,
                                     MAGNIFY/2, MAGNIFY/2);
                    break;

                case '@': /* player */
                case '+': /* player on goal */
                    rb->lcd_drawline(c, r + middle, c + max, r + middle);
                    rb->lcd_drawline(c + middle, r, c + middle,
                                     r + max - ldelta);
                    rb->lcd_drawline(c + max - middle, r, c + max - middle,
                                     r + max - ldelta);
                    rb->lcd_drawline(c + middle, r + max - ldelta,
                                     c + middle - ldelta, r + max);
                    rb->lcd_drawline(c + max - middle, r + max - ldelta,
                                     c + max - middle + ldelta, r + max);
                    break;
#endif
            }
        }
    }

    /* print out the screen */
    rb->lcd_update();
}

static void draw_level(void)
{
    load_level();
    rb->lcd_clear_display();
    update_screen();
}

static bool save(char *filename, bool solution)
{
    int fd;
    char *loc;
    DIR *dir;
    char dirname[MAX_PATH];

    rb->splash(0, "Saving...");

    /* Create dir if it doesn't exist */
    if ((loc = rb->strrchr(filename, '/')) != NULL) {
        rb->strncpy(dirname, filename, loc - filename);
        dirname[loc - filename] = '\0';
        if(!(dir = rb->opendir(dirname)))
            rb->mkdir(dirname);
        else
            rb->closedir(dir);
    }

    if (filename[0] == '\0' ||
        (fd = rb->open(filename, O_WRONLY|O_CREAT|O_TRUNC)) < 0) {
        rb->splash(HZ*2, "Unable to open %s", filename);
        return false;
    }

    /* Sokoban: S/P for solution/progress : level number : current undo */
    rb->snprintf(buf, sizeof(buf), "Sokoban:%c:%d:%d\n", (solution ? 'S' : 'P'),
                 current_info.level.index + 1, undo_info.current);
    rb->write(fd, buf, rb->strlen(buf));

    /* Filename of levelset */
    rb->write(fd, buffered_boards.filename,
              rb->strlen(buffered_boards.filename));
    rb->write(fd, "\n", 1);

    /* Full undo history */
    rb->write(fd, undo_info.history, undo_info.max);

    rb->close(fd);

    return true;
}

static bool load(char *filename, bool silent)
{
    int fd;
    int i, n;
    int len;
    int button;
    bool play_solution;
    bool paused = false;
    unsigned short speed = 2;
    int delay[] = {HZ/2, HZ/3, HZ/4, HZ/6, HZ/8, HZ/12, HZ/16, HZ/25};

    if (filename[0] == '\0' || (fd = rb->open(filename, O_RDONLY)) < 0) {
        if (!silent)
            rb->splash(HZ*2, "Unable to open %s", filename);
        return false;
    }

    /* Read header, level number, & current undo */
    rb->read_line(fd, buf, sizeof(buf));

    /* If we're opening a level file, not a solution/progress file */
    if (rb->strncmp(buf, "Sokoban", 7) != 0) {
        rb->close(fd);

        rb->strncpy(buffered_boards.filename, filename, MAX_PATH);
        if (!read_levels(true))
            return false;

        current_info.level.index = 0;
        load_level();

        /* If there aren't any boxes to go or the player position wasn't set,
         * the file probably wasn't a Sokoban level file */
        if (current_info.level.boxes_to_go == 0 ||
            current_info.player.row == 0 || current_info.player.col == 0) {
            if (!silent)
                rb->splash(HZ*2, "File is not a Sokoban level file");
            return false;
        }

    } else {

        /* Read filename of levelset */
        rb->read_line(fd, buffered_boards.filename,
                      sizeof(buffered_boards.filename));

        /* Read full undo history */
        len = rb->read_line(fd, undo_info.history, MAX_UNDOS);

        /* Correct len when trailing \r's or \n's are counted */
        if (len > 2 && undo_info.history[len - 2] == '\0')
            len -= 2;
        else if (len > 1 && undo_info.history[len - 1] == '\0')
            len--;

        rb->close(fd);

        /* Check to see if we're going to play a solution or resume progress */
        play_solution = (buf[8] == 'S');

        /* Get level number */
        for (n = 0, i = 10; buf[i] >= '0' && buf[i] <= '9' && i < 15; i++)
            n = n*10 + buf[i] - '0';
        current_info.level.index = n - 1;

        /* Get undo index */
        for (n = 0, i++; buf[i] >= '0' && buf[i] <= '9' && i < 21; i++)
            n = n*10 + buf[i] - '0';
        if (n > len)
            n = len;
        undo_info.max = len;

        if (current_info.level.index < 0) {
            if (!silent)
                rb->splash(HZ*2, "Error loading level");
            return false;
        }
        if (!read_levels(true))
            return false;
        if (current_info.level.index >= current_info.max_level) {
            if (!silent)
                rb->splash(HZ*2, "Error loading level");
            return false;
        }

        load_level();

        if (play_solution) {
            rb->lcd_clear_display();
            update_screen();
            rb->sleep(2*delay[speed]);

            /* Replay solution until menu button is pressed */
            i = 0;
            while (true) {
                if (i < len) {
                    if (!move(undo_info.history[i], true)) {
                        n = i;
                        break;
                    }
                    rb->lcd_clear_display();
                    update_screen();
                    i++;
                } else
                    paused = true;

                rb->sleep(delay[speed]);

                while ((button = rb->button_get(false)) || paused) {
                    switch (button) {
                        case SOKOBAN_MENU:
                            /* Pretend the level is complete so we'll quit */
                            current_info.level.boxes_to_go = 0;
                            return true;

                        case SOKOBAN_PAUSE:
                            /* Toggle pause state */
                            paused = !paused;
                            break;

                        case SOKOBAN_LEFT:
                        case SOKOBAN_LEFT | BUTTON_REPEAT:
                            /* Go back one move */
                            if (paused) {
                                if (undo())
                                    i--;
                                rb->lcd_clear_display();
                                update_screen();
                            }
                            break;

                        case SOKOBAN_RIGHT:
                        case SOKOBAN_RIGHT | BUTTON_REPEAT:
                            /* Go forward one move */
                            if (paused) {
                                if (redo())
                                    i++;
                                rb->lcd_clear_display();
                                update_screen();
                            }
                            break;

                        case SOKOBAN_UP:
                        case SOKOBAN_UP | BUTTON_REPEAT:
                            /* Speed up */
                            if (speed < sizeof(delay)/sizeof(int) - 1)
                                speed++;
                            break;

                        case SOKOBAN_DOWN:
                        case SOKOBAN_DOWN | BUTTON_REPEAT:
                            /* Slow down */
                            if (speed > 0)
                                speed--;
                    }

                    if (paused)
                        rb->sleep(HZ/33);
                }
            }

            /* If level is complete, wait for keypress before quitting */
            if (current_info.level.boxes_to_go == 0)
                rb->button_get(true);

        } else {
            /* Advance to current undo */
            for (i = 0; i < n; i++) {
                if (!move(undo_info.history[i], true)) {
                    n = i;
                    break;
                }
            }

            rb->button_clear_queue();
            rb->lcd_clear_display();
        }

        undo_info.current = n;
    }

    return true;
}

static int sokoban_menu(void)
{
    int button;
    int selection = 0;
    int i;
    bool menu_quit;
    int start_selected = 0;
    int prev_level = current_info.level.index;

    MENUITEM_STRINGLIST(menu, "Sokoban Menu", NULL,
                        "Resume", "Select Level", "Audio Playback", "Keys",
                        "Load Default Level Set", "Quit Without Saving",
                        "Save Progress & Quit");

    do {
        menu_quit = true;
        selection = rb->do_menu(&menu, &start_selected, NULL, false);

        switch (selection) {
            case 0: /* Resume */
                break;

            case 1: /* Select level */
                current_info.level.index++;
                rb->set_int("Select Level", "", UNIT_INT,
                            &current_info.level.index, NULL, 1, 1,
                            current_info.max_level, NULL);
                current_info.level.index--;
                if (prev_level != current_info.level.index) {
                    init_undo();
                    draw_level();
                } else
                    menu_quit = false;
                break;

            case 2: /* Audio playback control */
                playback_control(rb, NULL);
                menu_quit = false;
                break;

            case 3: /* Keys */
                FOR_NB_SCREENS(i)
                    rb->screens[i]->clear_display();
                rb->lcd_setfont(SOKOBAN_FONT);

#if (CONFIG_KEYPAD == RECORDER_PAD) || \
    (CONFIG_KEYPAD == ARCHOS_AV300_PAD)
                rb->lcd_putsxy(3,  6, "[OFF] Menu");
                rb->lcd_putsxy(3, 16, "[ON] Undo");
                rb->lcd_putsxy(3, 26, "[PLAY] Redo");
                rb->lcd_putsxy(3, 36, "[F1] Down a Level");
                rb->lcd_putsxy(3, 46, "[F2] Restart Level");
                rb->lcd_putsxy(3, 56, "[F3] Up a Level");
#elif CONFIG_KEYPAD == ONDIO_PAD
                rb->lcd_putsxy(3,  6, "[OFF] Menu");
                rb->lcd_putsxy(3, 16, "[MODE] Undo");
                rb->lcd_putsxy(3, 26, "[MODE+DOWN] Redo");
                rb->lcd_putsxy(3, 36, "[MODE+LEFT] Previous Level");
                rb->lcd_putsxy(3, 46, "[MODE+UP] Restart Level");
                rb->lcd_putsxy(3, 56, "[MODE+RIGHT] Up Level");
#elif (CONFIG_KEYPAD == IRIVER_H100_PAD) || \
      (CONFIG_KEYPAD == IRIVER_H300_PAD)
                rb->lcd_putsxy(3,  6, "[STOP] Menu");
                rb->lcd_putsxy(3, 16, "[REC] Undo");
                rb->lcd_putsxy(3, 26, "[MODE] Redo");
                rb->lcd_putsxy(3, 36, "[PLAY+DOWN] Previous Level");
                rb->lcd_putsxy(3, 46, "[PLAY] Restart Level");
                rb->lcd_putsxy(3, 56, "[PLAY+UP] Next Level");
#elif (CONFIG_KEYPAD == IPOD_4G_PAD) || \
      (CONFIG_KEYPAD == IPOD_3G_PAD) || \
      (CONFIG_KEYPAD == IPOD_1G2G_PAD)
                rb->lcd_putsxy(3,  6, "[SELECT+MENU] Menu");
                rb->lcd_putsxy(3, 16, "[SELECT] Undo");
                rb->lcd_putsxy(3, 26, "[SELECT+PLAY] Redo");
                rb->lcd_putsxy(3, 36, "[SELECT+LEFT] Previous Level");
                rb->lcd_putsxy(3, 46, "[SELECT+RIGHT] Next Level");
#elif CONFIG_KEYPAD == IAUDIO_X5M5_PAD
                rb->lcd_putsxy(3,  6, "[POWER] Menu");
                rb->lcd_putsxy(3, 16, "[SELECT] Undo");
                rb->lcd_putsxy(3, 26, "[PLAY] Redo");
                rb->lcd_putsxy(3, 36, "[REC] Restart Level");
#elif CONFIG_KEYPAD == IRIVER_H10_PAD
                rb->lcd_putsxy(3,  6, "[POWER] Menu");
                rb->lcd_putsxy(3, 16, "[REW] Undo");
                rb->lcd_putsxy(3, 26, "[FF] Redo");
                rb->lcd_putsxy(3, 36, "[PLAY+DOWN] Previous Level");
                rb->lcd_putsxy(3, 46, "[PLAY+RIGHT] Restart Level");
                rb->lcd_putsxy(3, 56, "[PLAY+UP] Next Level");
#elif CONFIG_KEYPAD == GIGABEAT_PAD
                rb->lcd_putsxy(3,  6, "[POWER] Menu");
                rb->lcd_putsxy(3, 16, "[SELECT] Undo");
                rb->lcd_putsxy(3, 26, "[A] Redo");
                rb->lcd_putsxy(3, 36, "[VOL-] Previous Level");
                rb->lcd_putsxy(3, 46, "[MENU] Restart Level");
                rb->lcd_putsxy(3, 56, "[VOL+] Next Level");
#elif CONFIG_KEYPAD == SANSA_E200_PAD
                rb->lcd_putsxy(3,  6, "[POWER] Menu");
                rb->lcd_putsxy(3, 16, "[SELECT] Undo");
                rb->lcd_putsxy(3, 26, "[REC] Redo");
                rb->lcd_putsxy(3, 36, "[SELECT+DOWN] Previous Level");
                rb->lcd_putsxy(3, 46, "[SELECT+RIGHT] Restart Level");
                rb->lcd_putsxy(3, 56, "[SELECT+UP] Next Level");
#endif

#ifdef HAVE_TOUCHPAD
                rb->lcd_putsxy(3,  6, SOKOBAN_MENU_NAME " Menu");
                rb->lcd_putsxy(3, 16, SOKOBAN_UNDO_NAME " Undo");
                rb->lcd_putsxy(3, 26, SOKOBAN_REDO_NAME " Redo");
                rb->lcd_putsxy(3, 36, SOKOBAN_PAUSE_NAME " Pause");
                rb->lcd_putsxy(3, 46, SOKOBAN_LEVEL_REPEAT_NAME " Restart Level");
#endif

                FOR_NB_SCREENS(i)
                    rb->screens[i]->update();

                /* Display until keypress */
                do {
                    rb->sleep(HZ/20);
                    button = rb->button_get(false);
                } while (!button || button & BUTTON_REL ||
                         button & BUTTON_REPEAT);

                menu_quit = false;
                break;

            case 4: /* Load default levelset */
                init_boards();
                if (!read_levels(true))
                    return 5; /* Quit */
                load_level();
                break;

            case 5: /* Quit */
                break;

            case 6: /* Save & quit */
                save(SOKOBAN_SAVE_FILE, false);
                rb->reload_directory();
        }

    } while (!menu_quit);

    /* Restore font */
    rb->lcd_setfont(SOKOBAN_FONT);

    FOR_NB_SCREENS(i) {
        rb->screens[i]->clear_display();
        rb->screens[i]->update();
    }

    return selection;
}

static bool sokoban_loop(void)
{
    bool moved;
    int i = 0, button = 0, lastbutton = 0;
    short r = 0, c = 0;
    int w, h;
    char *loc;

    while (true) {
        moved = false;

        r = current_info.player.row;
        c = current_info.player.col;

        button = rb->button_get(true);

        switch(button)
        {
#ifdef SOKOBAN_RC_MENU
            case SOKOBAN_RC_MENU:
#endif
            case SOKOBAN_MENU:
                switch (sokoban_menu()) {
                    case 5: /* Quit */
                    case 6: /* Save & quit */
                        return PLUGIN_OK;
                }
                update_screen();
                break;

            case SOKOBAN_UNDO:
#ifdef SOKOBAN_UNDO_PRE
                if (lastbutton != SOKOBAN_UNDO_PRE)
                    break;
#else       /* repeat can't work here for Ondio, iPod, et al */
            case SOKOBAN_UNDO | BUTTON_REPEAT:
#endif
                undo();
                rb->lcd_clear_display();
                update_screen();
                break;

#ifdef SOKOBAN_REDO
            case SOKOBAN_REDO:
            case SOKOBAN_REDO | BUTTON_REPEAT:
                moved = redo();
                rb->lcd_clear_display();
                update_screen();
                break;
#endif

#ifdef SOKOBAN_LEVEL_UP
            case SOKOBAN_LEVEL_UP:
            case SOKOBAN_LEVEL_UP | BUTTON_REPEAT:
                /* next level */
                init_undo();
                if (current_info.level.index + 1 < current_info.max_level)
                    current_info.level.index++;

                draw_level();
                break;
#endif

#ifdef SOKOBAN_LEVEL_DOWN
            case SOKOBAN_LEVEL_DOWN:
            case SOKOBAN_LEVEL_DOWN | BUTTON_REPEAT:
                /* previous level */
                init_undo();
                if (current_info.level.index > 0)
                    current_info.level.index--;

                draw_level();
                break;
#endif

#ifdef SOKOBAN_LEVEL_REPEAT
            case SOKOBAN_LEVEL_REPEAT:
            case SOKOBAN_LEVEL_REPEAT | BUTTON_REPEAT:
                /* same level */
                init_undo();
                draw_level();
                break;
#endif

            case SOKOBAN_LEFT:
            case SOKOBAN_LEFT | BUTTON_REPEAT:
                moved = move(SOKOBAN_MOVE_LEFT, false);
                break;

            case SOKOBAN_RIGHT:
            case SOKOBAN_RIGHT | BUTTON_REPEAT:
                moved = move(SOKOBAN_MOVE_RIGHT, false);
                break;

            case SOKOBAN_UP:
            case SOKOBAN_UP | BUTTON_REPEAT:
                moved = move(SOKOBAN_MOVE_UP, false);
                break;

            case SOKOBAN_DOWN:
            case SOKOBAN_DOWN | BUTTON_REPEAT:
                moved = move(SOKOBAN_MOVE_DOWN, false);
                break;

            default:
                if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
                    return PLUGIN_USB_CONNECTED;
                break;
        }

        lastbutton = button;

        if (moved) {
            rb->lcd_clear_display();
            update_screen();
        }

        /* We have completed this level */
        if (current_info.level.boxes_to_go == 0) {

            if (moved) {
                rb->lcd_clear_display();

                /* Show level complete message & stats */
                rb->snprintf(buf, sizeof(buf), "Level %d Complete!",
                             current_info.level.index + 1);
                rb->lcd_getstringsize(buf, &w, &h);
                rb->lcd_putsxy(LCD_WIDTH/2 - w/2, LCD_HEIGHT/2 - h*3, buf);

                rb->snprintf(buf, sizeof(buf), "%4d Moves ",
                             current_info.level.moves);
                rb->lcd_getstringsize(buf, &w, &h);
                rb->lcd_putsxy(LCD_WIDTH/2 - w/2, LCD_HEIGHT/2 - h, buf);

                rb->snprintf(buf, sizeof(buf), "%4d Pushes",
                             current_info.level.pushes);
                rb->lcd_getstringsize(buf, &w, &h);
                rb->lcd_putsxy(LCD_WIDTH/2 - w/2, LCD_HEIGHT/2, buf);

                if (undo_info.count < MAX_UNDOS) {
                    rb->snprintf(buf, sizeof(buf), "%s: Save solution",
                                 BUTTON_SAVE_NAME);
                    rb->lcd_getstringsize(buf, &w, &h);
                    rb->lcd_putsxy(LCD_WIDTH/2 - w/2, LCD_HEIGHT/2 + h*2, buf);
                }

                rb->lcd_update();
                rb->sleep(HZ/4);
                rb->button_clear_queue();

                /* Display for 4 seconds or until new keypress */
                for (i = 0; i < 80; i++) {
                    rb->sleep(HZ/20);
                    button = rb->button_get(false);
                    if (button && !(button & BUTTON_REL) &&
                        !(button & BUTTON_REPEAT))
                        break;
                }

                if (button == BUTTON_SAVE) {
                    if (undo_info.count < MAX_UNDOS) {
                        /* Set filename to current levelset plus level number
                         * and .sok extension. Use SAVE_FOLDER if using the
                         * default levelset, since it's in a hidden folder. */
                        if (rb->strcmp(buffered_boards.filename,
                                       SOKOBAN_LEVELS_FILE) == 0) {
                            rb->snprintf(buf, sizeof(buf),
                                         "%s/sokoban.%d.sok",
                                         SOKOBAN_SAVE_FOLDER,
                                         current_info.level.index + 1);
                        } else {
                            if ((loc = rb->strrchr(buffered_boards.filename,
                                                   '.')) != NULL)
                                *loc = '\0';
                            rb->snprintf(buf, sizeof(buf), "%s.%d.sok",
                                         buffered_boards.filename,
                                         current_info.level.index + 1);
                            if (loc != NULL)
                                *loc = '.';
                        }

                        if (!rb->kbd_input(buf, MAX_PATH))
                            save(buf, true);
                    } else
                        rb->splash(HZ*2, "Solution too long to save");

                    rb->lcd_setfont(SOKOBAN_FONT); /* Restore font */
                }
            }

            FOR_NB_SCREENS(i) {
                rb->screens[i]->clear_display();
                rb->screens[i]->update();
            }

            current_info.level.index++;

            /* clear undo stats */
            init_undo();

            if (current_info.level.index >= current_info.max_level) {
                /* Show levelset complete message */
                rb->snprintf(buf, sizeof(buf), "You WIN!!");
                rb->lcd_getstringsize(buf, &w, &h);
                rb->lcd_putsxy(LCD_WIDTH/2 - w/2, LCD_HEIGHT/2 - h/2, buf);

                rb->lcd_set_drawmode(DRMODE_COMPLEMENT);
                /* Display for 4 seconds or until keypress */
                for (i = 0; i < 80; i++) {
                    rb->lcd_fillrect(0, 0, LCD_WIDTH, LCD_HEIGHT);
                    rb->lcd_update();
                    rb->sleep(HZ/10);

                    button = rb->button_get(false);
                    if (button && !(button & BUTTON_REL))
                        break;
                }
                rb->lcd_set_drawmode(DRMODE_SOLID);

                /* Reset to first level & show quit menu */
                current_info.level.index = 0;

                switch (sokoban_menu()) {
                    case 5: /* Quit */
                    case 6: /* Save & quit */
                        return PLUGIN_OK;
                }
            }

            load_level();
            update_screen();
        }

    } /* end while */

    return PLUGIN_OK;
}


enum plugin_status plugin_start(struct plugin_api* api, void* parameter)
{
    int w, h;

    (void)(parameter);
    rb = api;

    rb->lcd_setfont(SOKOBAN_FONT);

    rb->lcd_clear_display();
    rb->lcd_getstringsize(SOKOBAN_TITLE, &w, &h);
    rb->lcd_putsxy(LCD_WIDTH/2 - w/2, LCD_HEIGHT/2 - h/2, SOKOBAN_TITLE);
    rb->lcd_update();
    rb->sleep(HZ); /* Show title for 1 second */

    init_boards();

    if (parameter == NULL) {
        /* Attempt to resume saved progress, otherwise start at beginning */
        if (!load(SOKOBAN_SAVE_FILE, true)) {
            init_boards();
            if (!read_levels(true))
                return PLUGIN_OK;
            load_level();
        }

    } else {
        /* The plugin is being used to open a file */
        if (load((char*) parameter, false)) {
            /* If we loaded & played a solution, quit */
            if (current_info.level.boxes_to_go == 0)
                return PLUGIN_OK;
        } else
            return PLUGIN_OK;
    }

    rb->lcd_clear_display();
    update_screen();

    return sokoban_loop();
}

#endif
