/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2014 Franklin Wei
 *
 * Clone of 2048 by Gabriele Cirulli
 *
 * Thanks to [Saint], saratoga, and gevaerts for answering all my n00b
 * questions :)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ***************************************************************************/

/* TODO
 * Sounds!
 * Better animations!
 */

/* includes */

#include <plugin.h>

#include "lib/display_text.h"

#include "lib/helper.h"
#include "lib/highscore.h"
#include "lib/playback_control.h"
#include "lib/pluginlib_actions.h"
#include "lib/pluginlib_exit.h"

#include "pluginbitmaps/_2048_background.h"
#include "pluginbitmaps/_2048_tiles.h"

/* some constants */

static const int ANIM_SLEEPTIME = (HZ/20);
static const int NUM_STARTING_TILES = 2;
static const int VERT_SPACING = 4;
static const int WHAT_FONT = FONT_UI;
static const unsigned int WINNING_TILE = 2048;

/* must use macros for these */
#define GRID_SIZE 4
#define HISCORES_FILE PLUGIN_GAMES_DATA_DIR "/2048.score"
#define MIN_SPACE (BMPHEIGHT__2048_tiles * 0.134)
#define NUM_SCORES 5
#define RESUME_FILE PLUGIN_GAMES_DATA_DIR "/2048.save"
#define SPACES (GRID_SIZE * GRID_SIZE)

/* screen-specific configuration */

#if (LCD_WIDTH < LCD_HEIGHT) /* tall screens */
#  define TITLE_X 0
#  define TITLE_Y 0
#  define BASE_Y (BMPHEIGHT__2048_tiles*1.5)
#  define BASE_X (BMPHEIGHT__2048_tiles*.5-MIN_SPACE)
#  define SCORE_X 0
#  define SCORE_Y (max_numeral_height)
#  define BEST_SCORE_X 0
#  define BEST_SCORE_Y (2*max_numeral_height)
#else /* wide or square screens */
#  define TITLE_X 0
#  define TITLE_Y 0
#  define BASE_X (LCD_WIDTH-(GRID_SIZE*BMPHEIGHT__2048_tiles)-(((GRID_SIZE+1)*MIN_SPACE)))
#  define BASE_Y (BMPHEIGHT__2048_tiles*.5-MIN_SPACE)
#  define SCORE_X 0
#  define SCORE_Y (max_numeral_height)
#  define BEST_SCORE_X 0
#  define BEST_SCORE_Y (2*max_numeral_height)
#endif /* LCD_WIDTH < LCD_HEIGHT */

#if LCD_DEPTH > 1
/* where to draw the background bitmap */
static const int BACKGROUND_X = (BASE_X-MIN_SPACE);
static const int BACKGROUND_Y = (BASE_Y-MIN_SPACE);
#endif

/* key mappings */
#define KEY_UP PLA_UP
#define KEY_DOWN PLA_DOWN
#define KEY_LEFT PLA_LEFT
#define KEY_RIGHT PLA_RIGHT
#define KEY_UNDO PLA_SELECT

#if (CONFIG_KEYPAD == IPOD_1G2G_PAD) \
    || (CONFIG_KEYPAD == IPOD_3G_PAD) \
    || (CONFIG_KEYPAD == IPOD_4G_PAD)
#define KEY_EXIT PLA_SELECT_REPEAT
#else
#define KEY_EXIT PLA_CANCEL
#endif

/* notice how "color" is spelled :P */
#ifdef HAVE_LCD_COLOR

/* colors */

static const unsigned BACKGROUND = LCD_RGBPACK(0xfa, 0xf8, 0xef);
static const unsigned BOARD_BACKGROUND = LCD_RGBPACK(0xbb, 0xad, 0xa0);
static const unsigned TEXT_COLOR = LCD_RGBPACK(0x77, 0x6e, 0x65);

#endif

/* PLA data */
static const struct button_mapping *plugin_contexts[] = { pla_main_ctx };

/*** game data structures ***/

struct game_ctx_t {
    unsigned int grid[GRID_SIZE][GRID_SIZE]; /* 0 = empty */
    unsigned int score;
    unsigned int cksum;                      /* sum of grid, XORed by score */
    bool already_won;                        /* has the player gotten 2048 yet? */
} game_ctx;

static struct game_ctx_t *ctx = &game_ctx;

/*** temporary data ***/

static bool merged_grid[GRID_SIZE][GRID_SIZE];
static int old_grid[GRID_SIZE][GRID_SIZE];

static int max_numeral_height = -1;

#if LCD_DEPTH <= 1
static int max_numeral_width;
#endif

static bool loaded = false; /* has a save been loaded? */

/* the high score */
static unsigned int best_score;

static bool abnormal_exit = true;
static struct highscore highscores[NUM_SCORES];

/***************************** UTILITY FUNCTIONS *****************************/

static inline int rand_range(int min, int max)
{
    return rb->rand() % (max-min + 1) + min;
}

/* prepares for exit */
static void cleanup(void)
{
    backlight_use_settings();
}

/* returns 2 or 4 */
static inline int rand_2_or_4(void)
{
    /* 1 in 10 chance of a four */
    if(rb->rand() % 10 == 0)
        return 4;
    else
        return 2;
}

/* displays the help text */
static bool do_help(void)
{

#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground(LCD_WHITE);
    rb->lcd_set_background(LCD_BLACK);
#endif

    rb->lcd_setfont(FONT_UI);

    static char* help_text[]= {"2048", "", "Aim",
                               "", "Join", "the", "numbers", "to", "get", "to", "the", "2048", "tile!", "", "",
                               "How", "to", "Play", "",
                               "", "Use", "the", "directional", "keys", "to", "move", "the", "tiles.", "When",
                               "two", "tiles", "with", "the", "same", "number", "touch,", "they", "merge", "into", "one!"};

    struct style_text style[] = {
        {0,  TEXT_CENTER | TEXT_UNDERLINE},
        {2,  C_RED},
        {15, C_RED},
        {16, C_RED},
        {17, C_RED},
        LAST_STYLE_ITEM
    };

    return display_text(ARRAYLEN(help_text), help_text, style, NULL, true);
}

/*** tile movement logic ***/

/* this function performs the tile movement */
static inline void slide_internal(int startx, int starty,
                                  int stopx, int stopy,
                                  int dx, int dy,
                                  int lookx, int looky,
                                  bool update_best)
{
    unsigned int best_score_old = best_score;

    /* loop over the rows or columns, moving the tiles in the specified direction */
    for(int y = starty; y != stopy; y += dy)
    {
        for(int x = startx; x != stopx; x += dx)
        {
            if(ctx->grid[x + lookx][y + looky] == ctx->grid[x][y] &&
               ctx->grid[x][y]                                    &&
               !merged_grid[x + lookx][y + looky]                 &&
               !merged_grid[x][y]) /* merge these two tiles */
            {
                /* Each merged tile cannot be merged again */
                merged_grid[x + lookx][y + looky] = true;
                ctx->grid[x + lookx][y + looky] = 2 * ctx->grid[x][y];
                ctx->score += ctx->grid[x + lookx][y + looky];
                ctx->grid[x][y] = 0;
            }
            else if(ctx->grid[x + lookx][y + looky] == 0) /* Empty! */
            {
                ctx->grid[x + lookx][y + looky] = ctx->grid[x][y];
                ctx->grid[x][y] = 0;
            }
        }
    }
    if(ctx->score > best_score_old && update_best)
        best_score = ctx->score;
}

/* these functions move each tile 1 space in the direction specified via calls to slide_internal */

/* Up
   0
   1 ^ ^ ^ ^
   2 ^ ^ ^ ^
   3 ^ ^ ^ ^
   0 1 2 3
*/
static void up(bool update_best)
{
    slide_internal(0, 1,  /* start values */
                   GRID_SIZE, GRID_SIZE, /* stop values */
                   1, 1, /* delta values */
                   0, -1, /* lookahead values */
                   update_best);
}

/* Down
   0 v v v v
   1 v v v v
   2 v v v v
   3
   0 1 2 3
*/
static void down(bool update_best)
{
    slide_internal(0, GRID_SIZE-2,
                   GRID_SIZE, -1,
                   1, -1,
                   0, 1,
                   update_best);
}

/* Left
   0   < < <
   1   < < <
   2   < < <
   3   < < <
   0 1 2 3
*/
static void left(bool update_best)
{
    slide_internal(1, 0,
                   GRID_SIZE, GRID_SIZE,
                   1, 1,
                   -1, 0,
                   update_best);
}

/* Right
   0 > > >
   1 > > >
   2 > > >
   3 > > >
   0 1 2 3
*/
static void right(bool update_best)
{
    slide_internal(GRID_SIZE-2, 0, /* start */
                   -1, GRID_SIZE, /* stop */
                   -1, 1, /* delta */
                   1, 0, /* lookahead */
                   update_best);
}

/* copies old_grid to ctx->grid */
static inline void RESTORE_GRID(void)
{
    memcpy(&ctx->grid, &old_grid, sizeof(ctx->grid));
}

/* slightly modified base 2 logarithm, returns 1 when given zero, and log2(n) + 1 for anything else */
static inline int ilog2(int n)
{
    if(n == 0)
        return 1;
    int log = 0;
    while(n > 1)
    {
        n >>= 1;
        ++log;
    }
    return log + 1;
}

/* low-depth displays resort to text drawing, see the #else case below */

#if LCD_DEPTH > 1

/* draws game screen + updates LCD */
static void draw(void)
{
#ifdef HAVE_LCD_COLOR
    rb->lcd_set_background(BACKGROUND);
#endif

    rb->lcd_clear_display();

    /* draw the background */

    rb->lcd_bitmap(_2048_background,
                   BACKGROUND_X, BACKGROUND_Y,
                   BMPWIDTH__2048_background, BMPWIDTH__2048_background);

    /*
      grey_gray_bitmap(_2048_background, BACKGROUND_X, BACKGROUND_Y, BMPWIDTH__2048_background, BMPHEIGHT__2048_background);
    */

    /* draw the grid */

    for(int y = 0; y < GRID_SIZE; ++y)
    {
        for(int x = 0; x < GRID_SIZE; ++x)
        {
            rb->lcd_bitmap_part(_2048_tiles,                                                                                        /* source */
                                BMPWIDTH__2048_tiles - BMPHEIGHT__2048_tiles * ilog2(ctx->grid[x][y]), 0,                           /* source upper left corner */
                                STRIDE(SCREEN_MAIN, BMPWIDTH__2048_tiles, BMPHEIGHT__2048_tiles),                                   /* stride */
                                (BMPHEIGHT__2048_tiles + MIN_SPACE) * x + BASE_X, (BMPHEIGHT__2048_tiles + MIN_SPACE) * y + BASE_Y, /* dest upper-left corner */
                                BMPHEIGHT__2048_tiles, BMPHEIGHT__2048_tiles);                                                      /* size of the cut section */
        }
    }

    /* draw the title */
    char buf[32];

#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground(TEXT_COLOR);
#endif

    rb->snprintf(buf, sizeof(buf), "%d", WINNING_TILE);

    /* check if the title will overlap the grid */
    int w, h;
    rb->lcd_setfont(FONT_UI);
    rb->font_getstringsize(buf, &w, &h, FONT_UI);
    bool draw_title = true;
    if(w + TITLE_X >= BACKGROUND_X && h + TITLE_Y >= BACKGROUND_Y)
    {
        /* if it goes into the grid, use the system font, which should be smaller */
        rb->lcd_setfont(FONT_SYSFIXED);
        rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
        if(w + TITLE_X >= BACKGROUND_X && h + TITLE_Y >= BACKGROUND_Y)
        {
            /* title can't fit, don't draw it */
            draw_title = false;
            h = 0;
        }
    }

    if(draw_title)
        rb->lcd_putsxy(TITLE_X, TITLE_Y, buf);

    int score_y = TITLE_Y + h + VERT_SPACING;

    /* draw the score */
    rb->snprintf(buf, sizeof(buf), "Score: %d", ctx->score);

#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground(LCD_WHITE);
    rb->lcd_set_background(BOARD_BACKGROUND);
#endif

    rb->lcd_setfont(FONT_UI);
    rb->font_getstringsize(buf, &w, &h, FONT_UI);

    /* try making the score fit */
    if(w + SCORE_X >= BACKGROUND_X && h + SCORE_Y >= BACKGROUND_Y)
    {
        /* score overflows */
        /* first see if it fits with Score: and FONT_SYSFIXED */
        rb->lcd_setfont(FONT_SYSFIXED);
        rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
        if(w + SCORE_X < BACKGROUND_X)
            /* it fits, go and draw it */
            goto draw_lbl;

        /* now try with S: and FONT_UI */
        rb->snprintf(buf, sizeof(buf), "S: %d", ctx->score);
        rb->font_getstringsize(buf, &w, &h, FONT_UI);
        rb->lcd_setfont(FONT_UI);
        if(w + SCORE_X < BACKGROUND_X)
            goto draw_lbl;

        /* now try with S: and FONT_SYSFIXED */
        rb->snprintf(buf, sizeof(buf), "S: %d", ctx->score);
        rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
        rb->lcd_setfont(FONT_SYSFIXED);
        if(w + SCORE_X < BACKGROUND_X)
            goto draw_lbl;

        /* then try without Score: and FONT_UI */
        rb->snprintf(buf, sizeof(buf), "%d", ctx->score);
        rb->font_getstringsize(buf, &w, &h, FONT_UI);
        rb->lcd_setfont(FONT_UI);
        if(w + SCORE_X < BACKGROUND_X)
            goto draw_lbl;

        /* as a last resort, don't use Score: and use the system font */
        rb->snprintf(buf, sizeof(buf), "%d", ctx->score);
        rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
        rb->lcd_setfont(FONT_SYSFIXED);
        if(w + SCORE_X < BACKGROUND_X)
            goto draw_lbl;
        else
            goto skip_draw_score;
    }

draw_lbl:
    rb->lcd_putsxy(SCORE_X, score_y, buf);
    score_y += h + VERT_SPACING;

    /* draw the best score */
skip_draw_score:
    rb->snprintf(buf, sizeof(buf), "Best: %d", best_score);

#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground(LCD_WHITE);
    rb->lcd_set_background(BOARD_BACKGROUND);
#endif

    rb->lcd_setfont(FONT_UI);
    rb->font_getstringsize(buf, &w, &h, FONT_UI);
    if(w + BEST_SCORE_X >= BACKGROUND_X && h + BEST_SCORE_Y >= BACKGROUND_Y)
    {
        /* score overflows */
        /* first see if it fits with Score: and FONT_SYSFIXED */
        rb->lcd_setfont(FONT_SYSFIXED);
        rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
        if(w + BEST_SCORE_X < BACKGROUND_X)
            /* it fits, go and draw it */
            goto draw_best;

        /* now try with S: and FONT_UI */
        rb->snprintf(buf, sizeof(buf), "B: %d", best_score);
        rb->font_getstringsize(buf, &w, &h, FONT_UI);
        rb->lcd_setfont(FONT_UI);
        if(w + BEST_SCORE_X < BACKGROUND_X)
            goto draw_best;

        /* now try with S: and FONT_SYSFIXED */
        rb->snprintf(buf, sizeof(buf), "B: %d", best_score);
        rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
        rb->lcd_setfont(FONT_SYSFIXED);
        if(w + BEST_SCORE_X < BACKGROUND_X)
            goto draw_best;

        /* then try without Score: and FONT_UI */
        rb->snprintf(buf, sizeof(buf), "%d", best_score);
        rb->font_getstringsize(buf, &w, &h, FONT_UI);
        rb->lcd_setfont(FONT_UI);
        if(w + BEST_SCORE_X < BACKGROUND_X)
            goto draw_best;

        /* as a last resort, don't use Score: and use the system font */
        rb->snprintf(buf, sizeof(buf), "%d", best_score);
        rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
        rb->lcd_setfont(FONT_SYSFIXED);
        if(w + BEST_SCORE_X < BACKGROUND_X)
            goto draw_best;
        else
            goto skip_draw_best;
    }
draw_best:
    rb->lcd_putsxy(BEST_SCORE_X, score_y, buf);

skip_draw_best:
    rb->lcd_update();

    /* revert the font */
    rb->lcd_setfont(WHAT_FONT);
}

#else /* LCD_DEPTH > 1 */

/* 1-bit display :( */
/* bitmaps are unreadable on these screens, so just resort to text-based drawing */
static void draw(void)
{
    rb->lcd_clear_display();

    /* Draw the grid */
    /* find the biggest tile */
    unsigned int biggest_tile = 0;
    for(int x = 0; x < GRID_SIZE; ++x)
    {
        for(int y = 0; y < GRID_SIZE; ++y)
            if(ctx->grid[x][y] > biggest_tile)
                biggest_tile = ctx->grid[x][y];
    }

    char buf[32];

    rb->snprintf(buf, 32, "%d", biggest_tile);

    int biggest_tile_width = rb->strlen(buf) * rb->font_get_width(rb->font_get(WHAT_FONT), '0') + MIN_SPACE;

    for(int y = 0; y < GRID_SIZE; ++y)
    {
        for(int x = 0; x < GRID_SIZE; ++x)
        {
            if(ctx->grid[x][y])
            {
                if(ctx->grid[x][y] > biggest_tile)
                    biggest_tile = ctx->grid[x][y];
                rb->snprintf(buf, 32, "%d", ctx->grid[x][y]);
                rb->lcd_putsxy(biggest_tile_width * x, y * max_numeral_height + max_numeral_height, buf);
            }
        }
    }

    /* Now draw the score, and the game title */
    rb->snprintf(buf, 32, "Score: %d", ctx->score);
    int buf_width, buf_height;
    rb->font_getstringsize(buf, &buf_width, &buf_height, WHAT_FONT);

    int score_leftmost = LCD_WIDTH - buf_width - 1;
    /* Check if there is enough space to display "Score: ", otherwise, only display the score */
    if(score_leftmost >= 0)
        rb->lcd_putsxy(score_leftmost, 0, buf);
    else
        rb->lcd_putsxy(score_leftmost, 0, buf + rb->strlen("Score: "));

    rb->snprintf(buf, 32, "%d", WINNING_TILE);
    rb->font_getstringsize(buf, &buf_width, &buf_height, WHAT_FONT);
    if(buf_width < score_leftmost)
        rb->lcd_putsxy(0, 0, buf);

    rb->lcd_update();
}

#endif /* LCD_DEPTH > 1 */

/* place a 2 or 4 in a random empty space */
static void place_random(void)
{
    int xpos[SPACES], ypos[SPACES];
    int back = 0;
    /* get the indexes of empty spaces */
    for(int y = 0; y < GRID_SIZE; ++y)
        for(int x = 0; x < GRID_SIZE; ++x)
        {
            if(!ctx->grid[x][y])
            {
                xpos[back] = x;
                ypos[back++] = y;
            }
        }

    if(!back)
        /* no empty spaces */
        return;

    int idx = rand_range(0, back - 1);
    ctx->grid[ xpos[idx] ][ ypos[idx] ] = rand_2_or_4();
}

/* checks for a win or loss */
static bool check_gameover(void)
{
    /* first, check for a loss */
    int oldscore = ctx->score;
    bool have_legal_move = false;

    memset(&merged_grid, 0, SPACES * sizeof(bool));
    up(false);
    if(memcmp(&old_grid, &ctx->grid, sizeof(ctx->grid)))
    {
        RESTORE_GRID();
        ctx->score = oldscore;
        have_legal_move = true;
    }
    RESTORE_GRID();

    memset(&merged_grid, 0, SPACES * sizeof(bool));
    down(false);
    if(memcmp(&old_grid, &ctx->grid, sizeof(ctx->grid)))
    {
        RESTORE_GRID();
        ctx->score = oldscore;
        have_legal_move = true;
    }
    RESTORE_GRID();

    memset(&merged_grid, 0, SPACES * sizeof(bool));
    left(false);
    if(memcmp(&old_grid, &ctx->grid, sizeof(ctx->grid)))
    {
        RESTORE_GRID();
        ctx->score = oldscore;
        have_legal_move = true;
    }
    RESTORE_GRID();

    memset(&merged_grid, 0, SPACES * sizeof(bool));
    right(false);
    if(memcmp(&old_grid, &ctx->grid, sizeof(ctx->grid)))
    {
        RESTORE_GRID();
        ctx->score = oldscore;
        have_legal_move = true;
    }
    ctx->score = oldscore;
    if(!have_legal_move)
    {
        /* no more legal moves */
        draw(); /* Shame the player */
        rb->splash(HZ*2, "Game Over!");
        return true;
    }

    for(int y = 0;y < GRID_SIZE; ++y)
    {
        for(int x = 0; x < GRID_SIZE; ++x)
        {
            if(ctx->grid[x][y] == WINNING_TILE && !ctx->already_won)
            {
                /* Let the user see the tile in its full glory... */
                draw();
                ctx->already_won = true;
                rb->splash(HZ*2,"You win!");
            }
        }
    }
    return false;
}

/* loads highscores from disk */
/* creates an empty structure if the file does not exist */
static void load_hs(void)
{
    if(rb->file_exists(HISCORES_FILE))
        highscore_load(HISCORES_FILE, highscores, NUM_SCORES);
    else
        memset(highscores, 0, sizeof(struct highscore) * NUM_SCORES);
}

/* initialize the data structures */
static void init_game(bool newgame)
{
    best_score = highscores[0].score;
    if(loaded && ctx->score > best_score)
        best_score = ctx->score;

    if(newgame)
    {
        /* initialize the game context */
        memset(ctx->grid, 0, sizeof(ctx->grid));
        for(int i = 0; i < NUM_STARTING_TILES; ++i)
        {
            place_random();
        }
        ctx->score = 0;
        ctx->already_won = false;
    }

    /* using the menu resets the font */
    /* set it again here */

    rb->lcd_setfont(WHAT_FONT);

    /* Now calculate font sizes */
    /* Now get the height of the font */
    rb->font_getstringsize("0123456789", NULL, &max_numeral_height, WHAT_FONT);
    max_numeral_height += VERT_SPACING;

#if LCD_DEPTH <= 1
    max_numeral_width = rb->font_get_width(rb->font_get(WHAT_FONT), '0');
#endif

    backlight_ignore_timeout();

    draw();
}

/* save the current game state */
static void save_game(void)
{
    rb->splash(0, "Saving...");
    int fd = rb->open(RESUME_FILE, O_WRONLY|O_CREAT, 0666);
    if(fd < 0)
    {
        return;
    }

    /* calculate checksum */
    ctx->cksum = 0;

    for(int x = 0; x < GRID_SIZE; ++x)
        for(int y = 0; y < GRID_SIZE; ++y)
            ctx->cksum += ctx->grid[x][y];

    ctx->cksum ^= ctx->score;

    rb->write(fd, ctx, sizeof(struct game_ctx_t));
    rb->close(fd);
    rb->lcd_update();
}

/* loads a saved game, returns true on success */
static bool load_game(void)
{
    int success = 0;
    int fd = rb->open(RESUME_FILE, O_RDONLY);
    if(fd < 0)
    {
        rb->remove(RESUME_FILE);
        return false;
    }

    int numread = rb->read(fd, ctx, sizeof(struct game_ctx_t));

    /* verify checksum */
    unsigned int calc = 0;
    for(int x = 0; x < GRID_SIZE; ++x)
        for(int y = 0; y < GRID_SIZE; ++y)
            calc += ctx->grid[x][y];

    calc ^= ctx->score;

    if(numread == sizeof(struct game_ctx_t) && calc == ctx->cksum)
        ++success;

    rb->close(fd);
    rb->remove(RESUME_FILE);

    return (success > 0);
}

/* update the highscores with ctx->score */
static void hs_check_update(bool noshow)
{
    /* first, find the biggest tile to show as the level */
    unsigned int biggest = 0;
    for(int x = 0; x < GRID_SIZE; ++x)
    {
        for(int y = 0; y < GRID_SIZE; ++y)
        {
            if(ctx->grid[x][y] > biggest)
                biggest = ctx->grid[x][y];
        }
    }

    int hs_idx = highscore_update(ctx->score,biggest, "", highscores,NUM_SCORES);
    if(!noshow)
    {
        /* show the scores if there is a new high score */
        if(hs_idx >= 0)
        {
            rb->splashf(HZ*2, "New High Score: %d", ctx->score);
            rb->lcd_clear_display();
            highscore_show(hs_idx, highscores, NUM_SCORES, true);
        }
    }
    highscore_save(HISCORES_FILE, highscores, NUM_SCORES);
}

/* asks the user if they wish to quit */
static bool confirm_quit(void)
{
    const struct text_message prompt = { (const char*[]) {"Are you sure?", "This will clear your current game."}, 2};
    enum yesno_res response = rb->gui_syncyesno_run(&prompt, NULL, NULL);
    if(response == YESNO_NO)
        return false;
    else
        return true;
}

/* show the pause menu */
static int do_2048_pause_menu(void)
{
    int sel = 0;
    MENUITEM_STRINGLIST(menu,"2048", NULL,
                        "Resume Game",
                        "Start New Game",
                        "High Scores",
                        "Playback Control",
                        "Help",
                        "Quit without Saving",
                        "Quit");
    while(1)
    {
        switch(rb->do_menu(&menu, &sel, NULL, false))
        {
        case 0:
            draw();
            return 0;
        case 1:
        {
            if(!confirm_quit())
                break;
            else
            {
                hs_check_update(false);
                return 1;
            }
        }
        case 2:
            highscore_show(-1, highscores, NUM_SCORES, true);
            break;
        case 3:
            playback_control(NULL);
            break;
        case 4:
            do_help();
            break;
        case 5: /* quit w/o saving */
        {
            if(!confirm_quit())
                break;
            else
            {
                return 2;
            }
        }
        case 6:
            return 3;
        default:
            break;
        }
    }
}

static void exit_handler(void)
{
    cleanup();
    if(abnormal_exit)
        save_game();
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    rb->cpu_boost(false); /* back to idle */
#endif
    return;
}

static bool check_hs;

/* main game loop */
static enum plugin_status do_game(bool newgame)
{
    init_game(newgame);
    rb_atexit(exit_handler);
    int made_move = 0;
    while(1)
    {
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
        rb->cpu_boost(false); /* Save battery when idling */
#endif
        /* Wait for a button press */
        int button = pluginlib_getaction(-1, plugin_contexts, ARRAYLEN(plugin_contexts));
        made_move = 0;

        memset(&merged_grid, 0, SPACES*sizeof(bool));
        memcpy(&old_grid, &ctx->grid, sizeof(int)*SPACES);

        unsigned int grid_before_anim_step[GRID_SIZE][GRID_SIZE];

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
        rb->cpu_boost(true); /* doing work now... */
#endif
        switch(button)
        {
        case KEY_UP:
            for(int i = 0; i < GRID_SIZE - 1; ++i)
            {
                memcpy(grid_before_anim_step, ctx->grid, sizeof(ctx->grid));
                up(true);
                if(memcmp(grid_before_anim_step, ctx->grid, sizeof(ctx->grid)))
                {
                    rb->sleep(ANIM_SLEEPTIME);
                    draw();
                }
            }
            made_move = 1;
            break;
        case KEY_DOWN:
            for(int i = 0; i < GRID_SIZE - 1; ++i)
            {
                memcpy(grid_before_anim_step, ctx->grid, sizeof(ctx->grid));
                down(true);
                if(memcmp(grid_before_anim_step, ctx->grid, sizeof(ctx->grid)))
                {
                    rb->sleep(ANIM_SLEEPTIME);
                    draw();
                }
            }
            made_move = 1;
            break;
        case KEY_LEFT:
            for(int i = 0; i < GRID_SIZE - 1; ++i)
            {
                memcpy(grid_before_anim_step, ctx->grid, sizeof(ctx->grid));
                left(true);
                if(memcmp(grid_before_anim_step, ctx->grid, sizeof(ctx->grid)))
                {
                    rb->sleep(ANIM_SLEEPTIME);
                    draw();
                }
            }
            made_move = 1;
            break;
        case KEY_RIGHT:
            for(int i = 0; i < GRID_SIZE - 1; ++i)
            {
                memcpy(grid_before_anim_step, ctx->grid, sizeof(ctx->grid));
                right(true);
                if(memcmp(grid_before_anim_step, ctx->grid, sizeof(ctx->grid)))
                {
                    rb->sleep(ANIM_SLEEPTIME);
                    draw();
                }
            }
            made_move = 1;
            break;
        case KEY_EXIT:
            switch(do_2048_pause_menu())
            {
            case 0: /* resume */
                break;
            case 1: /* new game */
                init_game(true);
                made_move = 1;
                continue;
            case 2: /* quit without saving */
                check_hs = true;
                rb->remove(RESUME_FILE);
                return PLUGIN_ERROR;
            case 3: /* save and quit */
                check_hs = false;
                save_game();
                return PLUGIN_ERROR;
            }
            break;
        default:
        {
            exit_on_usb(button); /* handle poweroff and USB events */
            break;
        }
        }

        if(made_move)
        {
            /* Check if any tiles moved, then add random */
            if(memcmp(&old_grid, ctx->grid, sizeof(ctx->grid)))
            {
                place_random();
            }
            memcpy(&old_grid, ctx->grid, sizeof(ctx->grid));
            if(check_gameover())
                return PLUGIN_OK;
            draw();
        }
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
        rb->cpu_boost(false); /* back to idle */
#endif
        rb->yield();
    }
}

/* decide if this_item should be shown in the main menu */
/* used to hide resume option when there is no save */
static int mainmenu_cb(int action,
                       const struct menu_item_ex *this_item,
                       struct gui_synclist *this_list)
{
    (void)this_list;
    int idx = ((intptr_t)this_item);
    if(action == ACTION_REQUEST_MENUITEM && !loaded && (idx == 0 || idx == 5))
        return ACTION_EXIT_MENUITEM;
    return action;
}

/* show the main menu */
static enum plugin_status do_2048_menu(void)
{
    int sel = 0;
    loaded = load_game();
    MENUITEM_STRINGLIST(menu,
                        "2048",
                        mainmenu_cb,
                        "Resume Game",
                        "Start New Game",
                        "High Scores",
                        "Playback Control",
                        "Help",
                        "Quit without Saving",
                        "Quit");
    while(true)
    {
        switch(rb->do_menu(&menu, &sel, NULL, false))
        {
        case 0: /* Start new game or resume a game */
        case 1:
        {
            if(sel == 1 && loaded)
            {
                if(!confirm_quit())
                    break;
            }
            enum plugin_status ret = do_game(sel == 1);
            switch(ret)
            {
            case PLUGIN_OK:
            {
                loaded = false;
                rb->remove(RESUME_FILE);
                hs_check_update(false);
                break;
            }
            case PLUGIN_USB_CONNECTED:
                save_game();
                /* Don't bother showing the high scores... */
                return ret;
            case PLUGIN_ERROR: /* exit without menu */
                if(check_hs)
                    hs_check_update(false);
                return PLUGIN_OK;
            default:
                break;
            }
            break;
        }
        case 2:
            highscore_show(-1, highscores, NUM_SCORES, true);
            break;
        case 3:
            playback_control(NULL);
            break;
        case 4:
            do_help();
            break;
        case 5:
            if(confirm_quit())
                return PLUGIN_OK;
            break;
        case 6:
            if(loaded)
                save_game();
            return PLUGIN_OK;
        default:
            break;
        }
    }
}

/* plugin entry point */
enum plugin_status plugin_start(const void* param)
{
    (void)param;
    rb->srand(*rb->current_tick);
    load_hs();
    rb->lcd_setfont(WHAT_FONT);

    /* now start the game menu */
    enum plugin_status ret = do_2048_menu();

    highscore_save(HISCORES_FILE, highscores, NUM_SCORES);
    cleanup();

    abnormal_exit = false;

    return ret;
}
