/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2014 Franklin Wei, Benjamin Brown
 *
 * 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: */
/* vertical stride support (as of Dec. 2014, only the M:Robe 500 has a color,
   vertical stride LCD) */

/* monochrome/grayscale support (many grayscale targets have vertical strides,
   so get that working first!) */

#include "plugin.h"

#include "lib/display_text.h"
#include "lib/helper.h"
#include "lib/pluginlib_actions.h"
#include "lib/pluginlib_bmp.h"
#include "lib/pluginlib_exit.h"

#include "sys.h"
#include "parts.h"
#include "engine.h"
#include "keymaps.h"

static struct System* save_sys;

static bool sys_save_settings(struct System* sys)
{
    File f;
    file_create(&f, false);
    if(!file_open(&f, SETTINGS_FILE, sys->e->_saveDir, "wb"))
    {
        return false;
    }
    file_write(&f, &sys->settings, sizeof(sys->settings));
    file_close(&f);
    return true;
}

static void sys_rotate_keymap(struct System* sys)
{
    switch(sys->settings.rotation_option)
    {
    case 0:
        sys->keymap.up = BTN_UP;
        sys->keymap.down = BTN_DOWN;
        sys->keymap.left = BTN_LEFT;
        sys->keymap.right = BTN_RIGHT;
#if (CONFIG_KEYPAD == SANSA_FUZEPLUS_PAD)
        sys->keymap.upleft = BTN_UP_LEFT;
        sys->keymap.upright = BTN_UP_RIGHT;
        sys->keymap.downleft = BTN_DOWN_RIGHT;
        sys->keymap.downright = BTN_DOWN_LEFT;
#endif
        break;
    case 1:
        sys->keymap.up = BTN_RIGHT;
        sys->keymap.down = BTN_LEFT;
        sys->keymap.left = BTN_UP;
        sys->keymap.right = BTN_DOWN;
#if (CONFIG_KEYPAD == SANSA_FUZEPLUS_PAD)
        sys->keymap.upleft = BTN_UP_RIGHT;
        sys->keymap.upright = BTN_DOWN_RIGHT;
        sys->keymap.downleft = BTN_UP_LEFT;
        sys->keymap.downright = BTN_DOWN_LEFT;
#endif
        break;
    case 2:
        sys->keymap.up = BTN_LEFT;
        sys->keymap.down = BTN_RIGHT;
        sys->keymap.left = BTN_DOWN;
        sys->keymap.right = BTN_UP;
#if (CONFIG_KEYPAD == SANSA_FUZEPLUS_PAD)
        sys->keymap.upleft = BTN_DOWN_LEFT;
        sys->keymap.upright = BTN_UP_LEFT;
        sys->keymap.downleft = BTN_DOWN_RIGHT;
        sys->keymap.downright = BTN_DOWN_LEFT;
#endif
        break;
    default:
        error("sys_rotate_keymap: fall-through!");
    }
}

static bool sys_load_settings(struct System* sys)
{
    File f;
    file_create(&f, false);
    if(!file_open(&f, SETTINGS_FILE, sys->e->_saveDir, "rb"))
    {
        return false;
    }
    file_read(&f, &sys->settings, sizeof(sys->settings));
    file_close(&f);
    sys_rotate_keymap(sys);
    return true;
}

void exit_handler(void)
{
    sys_save_settings(save_sys);
    sys_stopAudio(save_sys);
    rb->timer_unregister();
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    rb->cpu_boost(false);
#endif
    backlight_use_settings();
}

static bool sys_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);
    char *help_text[] = {
        "XWorld", "",
        "XWorld", "is", "a", "port", "of", "a", "bytecode", "interpreter", "for", "`Another", "World',", "a", "cinematic", "adventure", "game", "by", "Eric", "Chahi.",
        "",
        "",
        "Level", "Codes:", "",
        "Level", "1:", "LDKD", "",
        "Level", "2:", "HTDC", "",
        "Level", "3:", "CLLD", "",
        "Level", "4:", "LBKG", "",
        "Level", "5:", "XDDJ", "",
        "Level", "6:", "FXLC", "",
        "Level", "7:", "KRFK", "",
        "Level", "8:", "KFLB", "",
        "Level", "9:", "DDRX", "",
        "Level", "10:", "BFLX", "",
        "Level", "11:", "BRTD", "",
        "Level", "12:", "TFBB", "",
        "Level", "13:", "TXHF", "",
        "Level", "14:", "CKJL", "",
        "Level", "15:", "LFCK", "",
    };
    struct style_text style[] = {
        { 0, TEXT_CENTER | TEXT_UNDERLINE },
    };

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

static const struct opt_items scaling_settings[3] = {
    { "Disabled", -1 },
    { "Fast"    , -1 },
#ifdef HAVE_LCD_COLOR
    { "Good"    , -1 }
#endif
};

static const struct opt_items rotation_settings[3] = {
    { "Disabled"        , -1 },
    { "Clockwise"       , -1 },
    { "Counterclockwise", -1 }
};

static void do_video_settings(struct System* sys)
{
    MENUITEM_STRINGLIST(menu, "Video Settings", NULL,
                        "Negative",
#ifdef SYS_MOTION_BLUR
                        "Motion Blur",
#endif
                        "Scaling",
                        "Rotation",
                        "Show FPS",
                        "Zoom on Code",
                        "Back");
    int sel = 0;
    while(1)
    {
        switch(rb->do_menu(&menu, &sel, NULL, false))
        {
        case 0:
            rb->set_bool("Negative", &sys->settings.negative_enabled);
            break;
#ifdef SYS_MOTION_BLUR
        case 1:
            rb->set_bool("Motion Blur", &sys->settings.blur);
            break;
#endif
#ifndef SYS_MOTION_BLUR
        case 1:
#else
        case 2:
#endif
            rb->set_option("Scaling", &sys->settings.scaling_quality, INT, scaling_settings,
#ifdef HAVE_LCD_COLOR
                           3
#else
                           2
#endif
                           , NULL);
            if(sys->settings.scaling_quality &&
               sys->settings.zoom)
            {
                rb->splash(HZ*2, "Zoom automatically disabled.");
                sys->settings.zoom = false;
            }
            break;
#ifndef SYS_MOTION_BLUR
        case 2:
#else
        case 3:
#endif
            rb->set_option("Rotation", &sys->settings.rotation_option, INT, rotation_settings, 3, NULL);
            if(sys->settings.rotation_option &&
               sys->settings.zoom)
            {
                rb->splash(HZ*2, "Zoom automatically disabled.");
                sys->settings.zoom = false;
            }
            sys_rotate_keymap(sys);
            break;
#ifndef SYS_MOTION_BLUR
        case 3:
#else
        case 4:
#endif
            rb->set_bool("Show FPS", &sys->settings.showfps);
            break;
#ifndef SYS_MOTION_BLUR
        case 4:
#else
        case 5:
#endif
            rb->set_bool("Zoom on Code", &sys->settings.zoom);
            /* zoom only works with scaling and rotation disabled */
            if(sys->settings.zoom && (sys->settings.scaling_quality || sys->settings.rotation_option))
            {
                rb->splash(HZ*2, "Scaling and rotation automatically disabled.");
                sys->settings.scaling_quality = 0;
                sys->settings.rotation_option = 0;
            }
            break;
        default:
            sys_save_settings(sys);
            return;
        }
    }
}

#define MAX_SOUNDBUF_SIZE 256

static void do_sound_settings(struct System* sys)
{
    MENUITEM_STRINGLIST(menu, "Sound Settings", NULL,
                        "Enabled",
                        "Buffer Level",
                        "Volume",
                        "Back",
                       );
    int sel = 0;
    while(1)
    {
        switch(rb->do_menu(&menu, &sel, NULL, false))
        {
        case 0:
            rb->set_bool("Enabled", &sys->settings.sound_enabled);
            break;
        case 1:
            rb->set_int("Buffer Level", "samples", UNIT_INT, &sys->settings.sound_bufsize, NULL, 16, 16, MAX_SOUNDBUF_SIZE, NULL);
            break;
        case 2:
        {
            const struct settings_list* vol =
                rb->find_setting(&rb->global_settings->volume, NULL);
            rb->option_screen((struct settings_list*)vol, NULL, false, "Volume");
            break;
        }
        case 3:
        default:
            sys_save_settings(sys);
            return;
        }
    }
}

static void sys_reset_settings(struct System* sys)
{
    sys->settings.negative_enabled = false;
    sys->settings.rotation_option = 0;   // off
    sys->settings.scaling_quality = 1;   // fast
    sys->settings.sound_enabled = true;
    sys->settings.sound_bufsize = 32;    /* keep this low */
    sys->settings.showfps = false;
    sys->settings.zoom = false;
    sys->settings.blur = false;
    sys_rotate_keymap(sys);
}

static struct System* mainmenu_sysptr;

static int mainmenu_cb(int action, const struct menu_item_ex *this_item)
{
    int idx = ((intptr_t)this_item);
    if(action == ACTION_REQUEST_MENUITEM && !mainmenu_sysptr->loaded && (idx == 0 || idx == 8 || idx == 10))
        return ACTION_EXIT_MENUITEM;
    return action;
}

static AudioCallback audio_callback = NULL;
static void get_more(const void** start, size_t* size);
static void* audio_param;
static struct System* audio_sys;

/************************************** MAIN MENU ***************************************/
/* called after game init */

void sys_menu(struct System* sys)
{
    sys_stopAudio(sys);

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    /* boost for load */
    rb->cpu_boost(true);
#endif

    rb->splash(0, "Loading...");
    sys->loaded = engine_loadGameState(sys->e, 0);

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    rb->cpu_boost(false);
#endif

    rb->lcd_update();

    mainmenu_sysptr = sys;
    MENUITEM_STRINGLIST(menu, "XWorld Menu", mainmenu_cb,
                        "Resume Game",          /* 0  */
                        "Start New Game",       /* 1  */
                        "Video Settings",       /* 2  */
                        "Sound Settings",       /* 3  */
                        "Fast Mode",            /* 4  */
                        "Help",                 /* 5  */
                        "Reset Settings",       /* 6  */
                        "Load",                 /* 7  */
                        "Save",                 /* 8  */
                        "Quit without Saving",  /* 9  */
                        "Save and Quit");       /* 10 */
    bool quit = false;
    while(!quit)
    {
        switch(rb->do_menu(&menu, NULL, NULL, false))
        {
        case 0:
            quit = true;
            break;
        case 1:
            vm_initForPart(&sys->e->vm, GAME_PART_FIRST); // This game part is the protection screen
            quit = true;
            break;
        case 2:
            do_video_settings(sys);
            break;
        case 3:
            do_sound_settings(sys);
            break;
        case 4:
            rb->set_bool("Fast Mode", &sys->e->vm._fastMode);
            sys_save_settings(sys);
            break;
        case 5:
            sys_do_help();
            break;
        case 6:
            sys_reset_settings(sys);
            sys_save_settings(sys);
            break;
        case 7:
            rb->splash(0, "Loading...");
            sys->loaded = engine_loadGameState(sys->e, 0);
            rb->lcd_update();
            break;
        case 8:
            sys->e->_stateSlot = 0;
            rb->splash(0, "Saving...");
            engine_saveGameState(sys->e, sys->e->_stateSlot, "quicksave");
            rb->lcd_update();
            break;
        case 9:
            engine_deleteGameState(sys->e, 0);
            exit(PLUGIN_OK);
            break;
        case 10:
            /* saves are NOT deleted on loading */
            exit(PLUGIN_OK);
            break;
        default:
            break;
        }
    }

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    /* boost for game */
    rb->cpu_boost(true);
#endif

    sys_startAudio(sys, audio_callback, audio_param);
}

void sys_init(struct System* sys, const char* title)
{
    (void) title;
    backlight_ignore_timeout();
    rb_atexit(exit_handler);
    save_sys = sys;
    rb->memset(&sys->input, 0, sizeof(sys->input));
    sys->mutex_bitfield = ~0;
    if(!sys_load_settings(sys))
    {
        sys_reset_settings(sys);
    }
}

void sys_destroy(struct System* sys)
{
    (void) sys;
    rb->splash(HZ, "Exiting...");
}

void sys_setPalette(struct System* sys, uint8_t start, uint8_t n, const uint8_t *buf)
{
    for(int i = start; i < start + n; ++i)
    {
        uint8_t c[3];
        for (int j = 0; j < 3; j++) {
            uint8_t col = buf[i * 3 + j];
            c[j] = (col << 2) | (col & 3);
        }
#if (LCD_DEPTH > 16) && (LCD_DEPTH <= 24)
        sys->palette[i] = (fb_data) {
            c[2], c[1], c[0]
        };
#elif defined(HAVE_LCD_COLOR)
        sys->palette[i] = FB_RGBPACK(c[0], c[1], c[2]);
#elif LCD_DEPTH > 1
        sys->palette[i] = LCD_BRIGHTNESS((c[0] + c[1] + c[2]) / 3);
#endif
    }
}

/****************************** THE MAIN DRAWING METHOD ********************************/

void sys_copyRect(struct System* sys, uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *buf, uint32_t pitch)
{
    static int last_timestamp = 1;

    /* get the address of the temporary framebuffer that has been allocated in the audiobuf */
    fb_data* framebuffer = (fb_data*) sys->e->res._memPtrStart + MEM_BLOCK_SIZE + (4 * VID_PAGE_SIZE);
    buf += y * pitch + x;

    /************************ BLIT THE TEMPORARY FRAMEBUFFER ***********************/

    if(sys->settings.rotation_option)
    {
        /* clockwise */
        if(sys->settings.rotation_option == 1)
        {
            while (h--) {
                /* one byte gives two pixels */
                for (int i = 0; i < w / 2; ++i) {
                    uint8_t pix1 = *(buf + i) >> 4;
                    uint8_t pix2 = *(buf + i) & 0xF;
#if defined(LCD_STRIDEFORMAT) && (LCD_STRIDEFORMAT == VERTICAL_STRIDE)
                    framebuffer[( (h * 2)    ) * 320 + i] = sys->palette[pix1];
                    framebuffer[( (h * 2) + 1) * 320 + i] = sys->palette[pix2];
#else
                    framebuffer[( (i * 2)    ) * 200 + h] = sys->palette[pix1];
                    framebuffer[( (i * 2) + 1) * 200 + h] = sys->palette[pix2];
#endif
                }
                buf += pitch;
            }
        }
        /* counterclockwise */
        else
        {
            while (h--) {
                /* one byte gives two pixels */
                for (int i = 0; i < w / 2; ++i) {
                    uint8_t pix1 = *(buf + i) >> 4;
                    uint8_t pix2 = *(buf + i) & 0xF;
#if defined(LCD_STRIDEFORMAT) && (LCD_STRIDEFORMAT == VERTICAL_STRIDE)
                    framebuffer[(200 - h * 2    ) * 320 + ( 320 - i )] = sys->palette[pix1];
                    framebuffer[(200 - h * 2 - 1) * 320 + ( 320 - i )] = sys->palette[pix2];
#else
                    framebuffer[(320 - i * 2    ) * 200 + ( 200 - h )] = sys->palette[pix1];
                    framebuffer[(320 - i * 2 - 1) * 200 + ( 200 - h )] = sys->palette[pix2];
#endif
                }
                buf += pitch;
            }
        }
    }
    /* no rotation */
    else
    {
        int next = 0;
#if defined(LCD_STRIDEFORMAT) && (LCD_STRIDEFORMAT == VERTICAL_STRIDE)
        for(int x = 0; x < w / 2; ++x)
        {
            for(int y = 0; y < h; ++y)
            {
                uint8_t pix1 = buf[ y * w + x ] >>  4;
                uint8_t pix2 = buf[ y * w + x ] & 0xF;
                framebuffer[(x + 0)*h + y] = sys->palette[pix1];
                framebuffer[(x + 1)*h + y] = sys->palette[pix2];
            }
        }
#else
        while (h--) {
            /* one byte gives two pixels */
            for (int i = 0; i < w / 2; ++i) {
                uint8_t pix1 = *(buf + i) >> 4;
                uint8_t pix2 = *(buf + i) & 0xF;
                framebuffer[next] = sys->palette[pix1];
                ++next;
                framebuffer[next] = sys->palette[pix2];
                ++next;
            }
            buf += pitch;
        }
#endif
    }

    /*************************** NOW SCALE IT! ***************************/

    if(sys->settings.scaling_quality)
    {
        struct bitmap in_bmp;
        if(sys->settings.rotation_option)
        {
#if defined(LCD_STRIDEFORMAT) && (LCD_STRIDEFORMAT == VERTICAL_STRIDE)
            in_bmp.width = 320;
            in_bmp.height = 200;
#else
            in_bmp.width = 200;
            in_bmp.height = 320;
#endif
        }
        else
        {
#if defined(LCD_STRIDEFORMAT) && (LCD_STRIDEFORMAT == VERTICAL_STRIDE)
            in_bmp.width = 200;
            in_bmp.height = 320;
#else
            in_bmp.width = 320;
            in_bmp.height = 200;
#endif
        }
        in_bmp.data = (unsigned char*) framebuffer;
        struct bitmap out_bmp;
        out_bmp.width = LCD_WIDTH;
        out_bmp.height = LCD_HEIGHT;
        out_bmp.data = (unsigned char*) rb->lcd_framebuffer;

#ifdef HAVE_LCD_COLOR
        if(sys->settings.scaling_quality == 1)
#endif
            simple_resize_bitmap(&in_bmp, &out_bmp);
#ifdef HAVE_LCD_COLOR
        else
            smooth_resize_bitmap(&in_bmp, &out_bmp);
#endif
    }
    else
    {
#if defined(LCD_STRIDEFORMAT) && (LCD_STRIDEFORMAT == VERTICAL_STRIDE)
        for(int x = 0; x < 320; ++x)
        {
            for(int y = 0; y < 200; ++y)
            {
                rb->lcd_set_foreground(framebuffer[x * 200 + y]);
                rb->lcd_drawpixel(x, y);
            }
        }
#else
        if(sys->settings.zoom)
        {
            rb->lcd_bitmap_part(framebuffer, CODE_X, CODE_Y, STRIDE(SCREEN_MAIN, 320, 200), 0, 0, 320 - CODE_X, 200 - CODE_Y);
        }
        else
        {
            if(sys->settings.rotation_option)
                rb->lcd_bitmap(framebuffer, 0, 0, 200, 320);
            else
                rb->lcd_bitmap(framebuffer, 0, 0, 320, 200);
        }
#endif
    }

    /************************************* APPLY FILTERS ******************************************/

    if(sys->settings.negative_enabled)
    {
        for(int y = 0; y < LCD_HEIGHT; ++y)
        {
            for(int x = 0; x < LCD_WIDTH; ++x)
            {
#ifdef HAVE_LCD_COLOR
                int r, g, b;
                fb_data pix = rb->lcd_framebuffer[y * LCD_WIDTH + x];
#if (LCD_DEPTH == 24)
                r = 0xff - pix.r;
                g = 0xff - pix.g;
                b = 0xff - pix.b;
                rb->lcd_framebuffer[y * LCD_WIDTH + x] = (fb_data) { b, g, r };
#else
                r = RGB_UNPACK_RED  (pix);
                g = RGB_UNPACK_GREEN(pix);
                b = RGB_UNPACK_BLUE (pix);
                rb->lcd_framebuffer[y * LCD_WIDTH + x] = LCD_RGBPACK(0xff - r, 0xff - g, 0xff - b);
#endif
#else
                rb->lcd_framebuffer[y * LCD_WIDTH + x] = LCD_BRIGHTNESS(0xff - rb->lcd_framebuffer[y * LCD_WIDTH + x]);
#endif
            }
        }
    }

#ifdef SYS_MOTION_BLUR
    if(sys->settings.blur)
    {
        static fb_data *prev_frames = NULL;
        static fb_data *orig_fb = NULL;
        static int prev_baseidx = 0; /* circular buffer */
        if(!prev_frames)
        {
            prev_frames = sys_get_buffer(sys, sizeof(fb_data) * LCD_WIDTH * LCD_HEIGHT * BLUR_FRAMES);
            orig_fb = sys_get_buffer(sys, sizeof(fb_data) * LCD_WIDTH * LCD_HEIGHT);

            rb->memset(prev_frames, 0, sizeof(fb_data) * LCD_WIDTH * LCD_HEIGHT * BLUR_FRAMES);
        }
        if(prev_frames && orig_fb)
        {

            rb->memcpy(orig_fb, rb->lcd_framebuffer, sizeof(fb_data) * LCD_WIDTH * LCD_HEIGHT);
            /* fancy useless slow motion blur */
            for(int y = 0; y < LCD_HEIGHT; ++y)
            {
                for(int x = 0; x < LCD_WIDTH; ++x)
                {
                    int r, g, b;
                    fb_data pix = rb->lcd_framebuffer[y * LCD_WIDTH + x];
                    r = RGB_UNPACK_RED  (pix);
                    g = RGB_UNPACK_GREEN(pix);
                    b = RGB_UNPACK_BLUE (pix);
                    r *= BLUR_FRAMES + 1;
                    g *= BLUR_FRAMES + 1;
                    b *= BLUR_FRAMES + 1;
                    for(int i = 0; i < BLUR_FRAMES; ++i)
                    {
                        fb_data prev_pix = prev_frames[ (prev_baseidx + i * (LCD_WIDTH * LCD_HEIGHT)) % (LCD_WIDTH * LCD_HEIGHT * BLUR_FRAMES) + y * LCD_WIDTH + x];
                        r += (BLUR_FRAMES-i) * RGB_UNPACK_RED(prev_pix);
                        g += (BLUR_FRAMES-i) * RGB_UNPACK_GREEN(prev_pix);
                        b += (BLUR_FRAMES-i) * RGB_UNPACK_BLUE(prev_pix);
                    }
                    r /= (BLUR_FRAMES + 1) / 2 * (1 + BLUR_FRAMES + 1);
                    g /= (BLUR_FRAMES + 1) / 2 * (1 + BLUR_FRAMES + 1);
                    b /= (BLUR_FRAMES + 1) / 2 * (1 + BLUR_FRAMES + 1);
                    rb->lcd_framebuffer[y * LCD_WIDTH + x] = LCD_RGBPACK(r, g, b);
                }
            }
            prev_baseidx -= LCD_WIDTH * LCD_HEIGHT;
            if(prev_baseidx < 0)
                prev_baseidx += BLUR_FRAMES * LCD_WIDTH * LCD_HEIGHT;
            rb->memcpy(prev_frames + prev_baseidx, orig_fb, sizeof(fb_data) * LCD_WIDTH * LCD_HEIGHT);
        }
    }
#endif

    /*********************** SHOW FPS *************************/

    int current_time = sys_getTimeStamp(sys);
    if(sys->settings.showfps)
    {
        rb->lcd_set_foreground(LCD_BLACK);
        rb->lcd_set_background(LCD_WHITE);
        /* use 1000 and not HZ here because getTimeStamp is in milliseconds */
        rb->lcd_putsf(0, 0, "FPS: %d", 1000 / ((current_time - last_timestamp == 0) ? 1 : current_time - last_timestamp));
    }
    rb->lcd_update();
    last_timestamp = sys_getTimeStamp(sys);
}

static void do_pause_menu(struct System* sys)
{
    sys_stopAudio(sys);

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    rb->cpu_boost(false);
#endif

    int sel = 0;
    MENUITEM_STRINGLIST(menu, "XWorld Menu", NULL,
                        "Resume Game",         /* 0 */
                        "Start New Game",      /* 1 */
                        "Video Settings",      /* 2 */
                        "Sound Settings",      /* 3 */
                        "Fast Mode",           /* 4 */
                        "Enter Code",          /* 5 */
                        "Help",                /* 6 */
                        "Reset Settings",      /* 7 */
                        "Load",                /* 8 */
                        "Save",                /* 9 */
                        "Quit");               /* 10 */

    bool quit = false;
    while(!quit)
    {
        switch(rb->do_menu(&menu, &sel, NULL, false))
        {
        case 0:
            quit = true;
            break;
        case 1:
            vm_initForPart(&sys->e->vm, GAME_PART_FIRST);
            quit = true;
            break;
        case 2:
            do_video_settings(sys);
            break;
        case 3:
            do_sound_settings(sys);
            break;
        case 4:
            rb->set_bool("Fast Mode", &sys->e->vm._fastMode);
            sys_save_settings(sys);
            break;
        case 5:
            sys->input.code = true;
            quit = true;
            break;
        case 6:
            sys_do_help();
            break;
        case 7:
            sys_reset_settings(sys);
            sys_save_settings(sys);
            break;
        case 8:
            rb->splash(0, "Loading...");
            sys->loaded = engine_loadGameState(sys->e, 0);
            rb->lcd_update();
            quit = true;
            break;
        case 9:
            sys->e->_stateSlot = 0;
            rb->splash(0, "Saving...");
            engine_saveGameState(sys->e, sys->e->_stateSlot, "quicksave");
            rb->lcd_update();
            break;
        case 10:
            exit(PLUGIN_OK);
            break;
        }
    }

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    rb->cpu_boost(true);
#endif

    sys_startAudio(sys, audio_callback, audio_param);
}

void sys_processEvents(struct System* sys)
{
    int btn = rb->button_status();
    rb->button_clear_queue();

    static int oldbuttonstate = 0;

    debug(DBG_SYS, "button is 0x%08x", btn);

    /* exit early if we can */
    if(btn == oldbuttonstate)
    {
        return;
    }

    /* handle special keys first */
    switch(btn)
    {
    case BTN_PAUSE:
        do_pause_menu(sys);
        sys->input.dirMask = 0;
        sys->input.button = false;
        /* exit early to avoid unwanted button presses being detected */
        return;
    default:
        exit_on_usb(btn);
        break;
    }

    /* Ignore some buttons that cause errant input */

    btn &= ~BUTTON_REDRAW;

#if (CONFIG_KEYPAD == IPOD_4G_PAD) || \
    (CONFIG_KEYPAD == IPOD_3G_PAD) || \
    (CONFIG_KEYPAD == IPOD_1G2G_PAD)
    if(btn & 0x80000000)
        return;
#endif

#if (CONFIG_KEYPAD == SANSA_E200_PAD)
    if(btn == (BUTTON_SCROLL_FWD || BUTTON_SCROLL_BACK))
        return;
#endif

#if (CONFIG_KEYPAD == SANSA_FUZEPLUS_PAD)
    if(btn == (BUTTON_SELECT))
        return;
#endif

    /* copied from doom which was copied from rockboy... */
    unsigned released = ~btn & oldbuttonstate;
    unsigned pressed = btn & ~oldbuttonstate;
    oldbuttonstate = btn;

    if(released)
    {
        if(released & BTN_FIRE)
            sys->input.button = false;
        if(released & sys->keymap.up)
            sys->input.dirMask &= ~DIR_UP;
        if(released & sys->keymap.down)
            sys->input.dirMask &= ~DIR_DOWN;
        if(released & sys->keymap.left)
            sys->input.dirMask &= ~DIR_LEFT;
        if(released & sys->keymap.right)
            sys->input.dirMask &= ~DIR_RIGHT;
#ifdef BTN_DOWN_LEFT
        if(released & sys->keymap.downleft)
            sys->input.dirMask &= ~(DIR_DOWN | DIR_LEFT);
#endif
#ifdef BTN_DOWN_RIGHT
        if(released & sys->keymap.downright)
            sys->input.dirMask &= ~(DIR_DOWN | DIR_RIGHT);
#endif
#ifdef BTN_UP_LEFT
        if(released & sys->keymap.upleft)
            sys->input.dirMask &= ~(DIR_UP | DIR_LEFT);
#endif
#ifdef BTN_UP_RIGHT
        if(released & sys->keymap.upright)
            sys->input.dirMask &= ~(DIR_UP | DIR_RIGHT);
#endif
    }

    if(pressed)
    {
        if(pressed & BTN_FIRE)
            sys->input.button = true;
        if(pressed & sys->keymap.up)
            sys->input.dirMask |= DIR_UP;
        if(pressed & sys->keymap.down)
            sys->input.dirMask |= DIR_DOWN;
        if(pressed & sys->keymap.left)
            sys->input.dirMask |= DIR_LEFT;
        if(pressed & sys->keymap.right)
            sys->input.dirMask |= DIR_RIGHT;
#ifdef BTN_DOWN_LEFT
        if(pressed & sys->keymap.downleft)
            sys->input.dirMask |= (DIR_DOWN | DIR_LEFT);
#endif
#ifdef BTN_DOWN_RIGHT
        if(pressed & sys->keymap.downright)
            sys->input.dirMask |= (DIR_DOWN | DIR_RIGHT);
#endif
#ifdef BTN_UP_LEFT
        if(pressed & sys->keymap.upleft)
            sys->input.dirMask |= (DIR_UP | DIR_LEFT);
#endif
#ifdef BTN_UP_RIGHT
        if(pressed & sys->keymap.upright)
            sys->input.dirMask |= (DIR_UP | DIR_RIGHT);
#endif
    }

#if 0
    /* handle releases */
    if(btn & BUTTON_REL)
    {
        debug(DBG_SYS, "button_rel");
        btn &= ~BUTTON_REL;

        if(btn & BTN_FIRE)
            sys->input.button = false;
        if(btn & sys->keymap.up)
            sys->input.dirMask &= ~DIR_UP;
        if(btn & sys->keymap.down)
            sys->input.dirMask &= ~DIR_DOWN;
        if(btn & sys->keymap.left)
            sys->input.dirMask &= ~DIR_LEFT;
        if(btn & sys->keymap.right)
            sys->input.dirMask &= ~DIR_RIGHT;
#ifdef BTN_DOWN_LEFT
        if(btn & sys->keymap.downleft)
            sys->input.dirMask &= ~(DIR_DOWN | DIR_LEFT);
#endif
#ifdef BTN_DOWN_RIGHT
        if(btn & sys->keymap.downright)
            sys->input.dirMask &= ~(DIR_DOWN | DIR_RIGHT);
#endif
#ifdef BTN_UP_LEFT
        if(btn & sys->keymap.upleft)
            sys->input.dirMask &= ~(DIR_UP | DIR_LEFT);
#endif
#ifdef BTN_UP_RIGHT
        if(btn & sys->keymap.upright)
            sys->input.dirMask &= ~(DIR_UP | DIR_RIGHT);
#endif
    }
    /* then handle presses */
    else
    {
        if(btn & BTN_FIRE)
            sys->input.button = true;
        if(btn & sys->keymap.up)
            sys->input.dirMask |= DIR_UP;
        if(btn & sys->keymap.down)
            sys->input.dirMask |= DIR_DOWN;
        if(btn & sys->keymap.left)
            sys->input.dirMask |= DIR_LEFT;
        if(btn & sys->keymap.right)
            sys->input.dirMask |= DIR_RIGHT;
#ifdef BTN_DOWN_LEFT
        if(btn & sys->keymap.downleft)
            sys->input.dirMask |= (DIR_DOWN | DIR_LEFT);
#endif
#ifdef BTN_DOWN_RIGHT
        if(btn & sys->keymap.downright)
            sys->input.dirMask |= (DIR_DOWN | DIR_RIGHT);
#endif
#ifdef BTN_UP_LEFT
        if(btn & sys->keymap.upleft)
            sys->input.dirMask |= (DIR_UP | DIR_LEFT);
#endif
#ifdef BTN_UP_RIGHT
        if(btn & sys->keymap.upright)
            sys->input.dirMask |= (DIR_UP | DIR_RIGHT);
#endif
    }
    debug(DBG_SYS, "dirMask is 0x%02x", sys->input.dirMask);
    debug(DBG_SYS, "button is %s", sys->input.button == true ? "true" : "false");
#endif
}

void sys_sleep(struct System* sys, uint32_t duration)
{
    (void) sys;
    /* duration is in ms */
    rb->sleep(duration / (1000/HZ));
}

uint32_t sys_getTimeStamp(struct System* sys)
{
    (void) sys;
    return (uint32_t) (*rb->current_tick * (1000/HZ));
}

/* game provides us mono samples, we need stereo */
static int16_t rb_soundbuf[MAX_SOUNDBUF_SIZE * 2];
static int8_t temp_soundbuf[MAX_SOUNDBUF_SIZE];

static void get_more(const void** start, size_t* size)
{
    if(audio_sys->settings.sound_enabled && audio_callback)
    {
        audio_callback(audio_param, temp_soundbuf, audio_sys->settings.sound_bufsize);

        /* convert xworld format (signed 8-bit) to rockbox format (stereo signed 16-bit) */
        for(int i = 0; i < audio_sys->settings.sound_bufsize; ++i)
        {
            rb_soundbuf[2*i] = rb_soundbuf[2*i+1] = temp_soundbuf[i] * 256;
        }
    }
    else
    {
        rb->memset(rb_soundbuf, 0, audio_sys->settings.sound_bufsize * 2 * sizeof(int16_t));
    }
    *start = rb_soundbuf;
    *size = audio_sys->settings.sound_bufsize * 2 * sizeof(int16_t);
}

void sys_startAudio(struct System* sys, AudioCallback callback, void *param)
{
    (void) sys;
    audio_callback = callback;
    audio_param = param;
    audio_sys = sys;

    rb->pcm_play_data(get_more, NULL, NULL, 0);
}

void sys_stopAudio(struct System* sys)
{
    (void) sys;
    rb->pcm_play_stop();
}

uint32_t sys_getOutputSampleRate(struct System* sys)
{
    (void) sys;
    return rb->mixer_get_frequency();
}

/* pretty non-reentrant here, but who cares? it's not like someone
   would want to run two instances of the game on Rockbox! :D */

static uint32_t cur_delay;
static void* cur_param;
static TimerCallback user_callback;
static void timer_callback(void)
{
    debug(DBG_SYS, "timer callback with delay of %d ms callback 0x%08x param 0x%08x", cur_delay, timer_callback, cur_param);
    uint32_t ret = user_callback(cur_delay, cur_param);
    if(ret != cur_delay)
    {
        cur_delay = ret;
        rb->timer_register(1, NULL, TIMER_FREQ / 1000 * ret, timer_callback IF_COP(, CPU));
    }
}

void *sys_addTimer(struct System* sys, uint32_t delay, TimerCallback callback, void *param)
{
    (void) sys;
    debug(DBG_SYS, "registering timer with delay of %d ms callback 0x%08x param 0x%08x", delay, callback, param);
    user_callback = callback;
    cur_delay = delay;
    cur_param = param;
    rb->timer_register(1, NULL, TIMER_FREQ / 1000 * delay, timer_callback IF_COP(, CPU));
    return NULL;
}

void sys_removeTimer(struct System* sys, void *timerId)
{
    (void) sys;
    (void) timerId;
    /* there's only one timer per game instance, so ignore both parameters */
    rb->timer_unregister();
}

void *sys_createMutex(struct System* sys)
{
    if(!sys)
        error("sys is NULL!");

    debug(DBG_SYS, "allocating mutex");

    /* this bitfield works as follows: bit set = free, unset = in use */
    for(int i = 0; i < MAX_MUTEXES; ++i)
    {
        /* check that the corresponding bit is 1 (free) */
        if(sys->mutex_bitfield & (1 << i))
        {
            rb->mutex_init(sys->mutex_memory + i);
            sys->mutex_bitfield &= ~(1 << i);
            return sys->mutex_memory + i;
        }
    }
    warning("Out of mutexes!");
    return NULL;
}

void sys_destroyMutex(struct System* sys, void *mutex)
{
    int mutex_number = ((char*)mutex - (char*)sys->mutex_memory) / sizeof(struct mutex); /* pointer arithmetic! check for bugs! */
    sys->mutex_bitfield |= 1 << mutex_number;
}

void sys_lockMutex(struct System* sys, void *mutex)
{
    (void) sys;
    rb->mutex_lock((struct mutex*) mutex);
}

void sys_unlockMutex(struct System* sys, void *mutex)
{
    (void) sys;
    rb->mutex_unlock((struct mutex*) mutex);
}

uint8_t* getOffScreenFramebuffer(struct System* sys)
{
    (void) sys;
    return NULL;
}

void *sys_get_buffer(struct System* sys, size_t sz)
{
    if((signed int)sys->bytes_left - (signed int)sz >= 0)
    {
        void* ret = sys->membuf;
        rb->memset(ret, 0, sz);
        sys->membuf = (char*)(sys->membuf) + sz;
        sys->bytes_left -= sz;
        return ret;
    }
    else
    {
        error("sys_get_buffer: out of memory!");
        return NULL;
    }
}

void MutexStack(struct MutexStack_t* s, struct System *stub, void *mutex)
{
    s->sys = stub;
    s->_mutex = mutex;
    /* FW 2017-2-12: disabled; no blocking ops in IRQ context! */
    /*sys_lockMutex(s->sys, s->_mutex);*/
}

void MutexStack_destroy(struct MutexStack_t* s)
{
    (void) s;
    /*sys_unlockMutex(s->sys, s->_mutex);*/
}
