/***************************************************************************
*             __________               __   ___.
*   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
*   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
*   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
*   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
*                     \/            \/     \/    \/            \/
*
* Copyright (C) 2009 Clément Pit--Claudel
*
* 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.
*
****************************************************************************/

#include "plugin.h"
#include "lib/configfile.h"
#include "lib/playback_control.h"
#include "lib/pluginlib_actions.h"



/* Limits */
#define MAX_PIECES_COUNT 5
#define MAX_COLORS_COUNT 8
#define MAX_GUESSES_COUNT 10

const struct button_mapping *plugin_contexts[] = {
    pla_main_ctx,
#ifdef HAVE_REMOTE_LCD
    pla_remote_ctx,
#endif
};

/*
 * Screen structure:
 * * (guesses_count) lines of guesses,
 * * 1 center line of solution (hidden),
 * * 1 line showing available colors.
 *
 * Status vars:
 * * quit: exit the plugin
 * * leave: restart the plugin (leave the current game)
 * * game_ended: the game has ended
 * * found: the combination has been found
 *
 * Colors used are taken from the Tango project.
 *
 * Due to integer truncations, 2 vars are used for some objects' dimensions
 * (eg. true_guess_w, true_score_w). The actual dimension of these objects is
 * stored in the corresponding var. without the "true" prefix.
 */

struct mm_score {
    int correct;
    int misplaced;
};

struct mm_line {
    struct mm_score score;
    int pieces[MAX_PIECES_COUNT];
};

const int colors[MAX_COLORS_COUNT] = {
    LCD_RGBPACK(252, 233,  79),
    LCD_RGBPACK(206,  92,   0),
    LCD_RGBPACK(143,  89,   2),
    LCD_RGBPACK( 78, 154,   6),
    /* LCD_RGBPACK( 32,  74, 135), */
    LCD_RGBPACK( 52, 101, 164),
    /* LCD_RGBPACK(114, 159, 207), */
    LCD_RGBPACK(117,  80, 123),
    /* LCD_RGBPACK(173, 127, 168), */
    LCD_RGBPACK(164,   0,   0),
    LCD_RGBPACK(238, 238, 236),
};

/* Flags */
static bool quit, leave, usb;
static bool found, game_ended;

/* Settings */
struct settings {
    int pieces;
    int colors;
    int guesses;
    bool labeling;
    bool framing;
};
static struct settings settings = {
    5, 7, 10, false, false,
};
static struct settings old_settings;
static int pieces_count;
static int colors_count;
static int guesses_count;

/* Display */
#define ALUMINIUM   LCD_RGBPACK(136, 138, 133)

#define MARGIN  5
#define X_MARGIN    (LCD_WIDTH / 20)
#define Y_MARGIN    (LCD_HEIGHT / 20)
#define GAME_H      (LCD_HEIGHT - (2 * Y_MARGIN))
#define LINE_W      (LCD_WIDTH  - (2 * X_MARGIN))

#define CONFIG_FILE_NAME "codebuster.cfg"

static struct configdata config[] = {
    {TYPE_INT, 0, MAX_PIECES_COUNT, { .int_p = &settings.pieces }, "pieces", NULL},
    {TYPE_INT, 0, MAX_COLORS_COUNT, { .int_p = &settings.colors }, "colors", NULL},
    {TYPE_INT, 0, MAX_GUESSES_COUNT, { .int_p = &settings.guesses }, "guesses", NULL},
    {TYPE_BOOL, 0, 1, { .bool_p = &settings.labeling }, "labeling", NULL},
    {TYPE_BOOL, 0, 1, { .bool_p = &settings.framing }, "framing", NULL},
};

static int line_h;
static int piece_w, tick_w;
static int true_guess_w, true_score_w, guess_w, score_w;

/* Guesses and solution */
struct mm_line solution, hidden;
struct mm_line guesses[MAX_GUESSES_COUNT];

/* Alias for pluginlib_getaction */
static inline int get_button(void) {
    return pluginlib_getaction(TIMEOUT_BLOCK, plugin_contexts,
                               ARRAYLEN(plugin_contexts));
}

/* Computes the margin to center an element */
static inline int get_margin(int width, int full_w) {
    return ((full_w - width) / 2);
}

static inline bool stop_game(void) {
    return (quit || leave || found);
}

static void fill_color_rect(int x, int y, int w, int h, int color) {
    rb->lcd_set_foreground(color);
    rb->lcd_fillrect(x, y, w, h);
    rb->lcd_set_foreground(LCD_WHITE);
}

static void overfill_rect(int x, int y, int w, int h) {
    rb->lcd_fillrect(x - 2, y - 2, w + 4, h + 4);
}

static void draw_piece(int x, int y, int w, int h, int color_id, bool emph) {
    int color = LCD_BLACK;

    if (color_id >= 0)
        color = colors[color_id];
    else if (color_id == -2) /* Hidden piece */
        color = ALUMINIUM;

    if (emph)
        overfill_rect(x, y, w, h);

    if (color_id == -1) /* Uninitialised color */
        rb->lcd_drawrect(x, y, w, h);
    else
        fill_color_rect(x, y, w, h, color);

    if (!emph && settings.framing)
        rb->lcd_drawrect(x, y, w, h);

    if (settings.labeling && color_id >= 0) {
        char text[2];
        rb->snprintf(text, sizeof(text), "%d", color_id);

        int fw, fh; rb->font_getstringsize(text, &fw, &fh, FONT_SYSFIXED);
        rb->lcd_putsxy(x + get_margin(fw, w), y + get_margin(fh, h), text);
    }
}

/* Compute the score for a given guess (expressed in ticks) */
static void validate_guess(struct mm_line* guess) {
    bool solution_match[MAX_PIECES_COUNT];
    bool guess_match[MAX_PIECES_COUNT];

    guess->score.misplaced = 0;
    guess->score.correct = 0;

    int guess_pos;

    /* Initialisation with 0s */
    for (guess_pos = 0; guess_pos < pieces_count; guess_pos++)
        solution_match[guess_pos] = guess_match[guess_pos] = false;

    /* 1st step : detect correctly positioned pieces */
    for (guess_pos = 0; guess_pos < pieces_count; guess_pos++) {
        if (solution.pieces[guess_pos] == guess->pieces[guess_pos]) {
            guess->score.correct += 1;

            guess_match[guess_pos] = solution_match[guess_pos]
                                   = true;
        }
    }

    /* Second step : detect mispositioned pieces */
    for (guess_pos = 0; guess_pos < pieces_count; guess_pos++) {
        if (guess_match[guess_pos]) continue;

        int sol_pos;
        for (sol_pos = 0; sol_pos < pieces_count; sol_pos++) {
            if (guess_match[guess_pos]) break;
            if (solution_match[sol_pos]) continue;

            if (guess->pieces[guess_pos] == solution.pieces[sol_pos]) {
                guess->score.misplaced += 1;

                solution_match[sol_pos] = true;
                break;
            }
        }
    }
}

static void draw_guess(int line, struct mm_line* guess, int cur_guess,
                        int cur_piece, bool show_score) {
    int cur_y = (Y_MARGIN + MARGIN) + 2 * line_h * line;
    int l_margin = X_MARGIN + (show_score ? 0 : get_margin(guess_w, LINE_W));

    int piece;
    for (piece = 0; piece < pieces_count; piece++) {
        int cur_x = l_margin + 2 * piece_w * piece;
        draw_piece(cur_x, cur_y, piece_w, line_h, guess->pieces[piece],
                   line == cur_guess && piece == cur_piece);
    }
}

static void draw_score(int line, struct mm_line* guess) {
    int cur_y = (Y_MARGIN + MARGIN) + 2 * line_h * line;
    int l_margin = X_MARGIN + true_guess_w + MARGIN;

    int tick = 0;
    for (; tick < guess->score.correct; tick++) {
        int cur_x = l_margin + 2 * tick_w * tick;

        fill_color_rect(cur_x, cur_y, tick_w, line_h, LCD_RGBPACK(239, 41, 41));
    }

    for (; tick < guess->score.correct + guess->score.misplaced; tick++) {
        int cur_x = l_margin + 2 * tick_w * tick;

        fill_color_rect(cur_x, cur_y, tick_w, line_h, LCD_RGBPACK(211, 215, 207));
    }
}

static void draw_board(int cur_guess, int cur_piece) {
    rb->lcd_clear_display();

    int line = 0;
    for (; line < guesses_count; line++) {
        draw_guess(line, &guesses[line], cur_guess, cur_piece, true);
        if (line < cur_guess) draw_score(line, &guesses[line]);
    }

    int color;
    int colors_margin = 2;
    int cur_y = (Y_MARGIN + MARGIN) + 2 * line_h * line;
    int color_w = (LINE_W - colors_margin * (colors_count - 1)) / colors_count;

    for (color = 0; color < colors_count; color++) {
        int cur_x = X_MARGIN + color * (color_w + colors_margin);
        draw_piece(cur_x, cur_y, color_w, line_h, color,
                   color == guesses[cur_guess].pieces[cur_piece]);
    }

    line++;

    if(game_ended)
        draw_guess(line, &solution, cur_guess, cur_piece, false);
    else
        draw_guess(line, &hidden, cur_guess, cur_piece, false);

    rb->lcd_update();
}

static void init_vars(void) {
    quit = leave = usb = found = game_ended = false;

    int guess, piece;
    for (guess = 0; guess < guesses_count; guess++) {
        for (piece = 0; piece < pieces_count; piece++)
            guesses[guess].pieces[piece] = -1;
    }
    for (piece = 0; piece < pieces_count; piece++) {
        guesses[0].pieces[piece] = 0;
        hidden.pieces[piece] = -2;
    }
}

static void init_board(void) {

    pieces_count = settings.pieces;
    colors_count = settings.colors;
    guesses_count = settings.guesses;

    line_h = GAME_H / (2 * (guesses_count + 2) - 1);

    true_score_w = LINE_W * 0.25;
    true_guess_w = LINE_W - (true_score_w + MARGIN);

    tick_w  = true_score_w / (2 * pieces_count - 1);
    piece_w = true_guess_w / (2 * pieces_count - 1);

    /* Readjust (due to integer divisions) */
    score_w = tick_w  * (2 * pieces_count - 1);
    guess_w = piece_w * (2 * pieces_count - 1);
}

static void randomize_solution(void) {
    int piece_id;
    for (piece_id = 0; piece_id < pieces_count; piece_id++)
        solution.pieces[piece_id] = rb->rand() % colors_count;
}

static void settings_menu(void) {
    MENUITEM_STRINGLIST(settings_menu, "Settings", NULL,
                        "Number of colours", "Number of pegs",
                        "Number of guesses",
                        "Display labels", "Display frames");
    int cur_item = 0;
    bool menu_quit = false;

    while(!menu_quit) {

        switch(rb->do_menu(&settings_menu, &cur_item, NULL, false)) {
            case 0:
                rb->set_int("Number of colours", "", UNIT_INT, &settings.colors,
                            NULL, -1, MAX_COLORS_COUNT, 1, NULL);
                break;
            case 1:
                rb->set_int("Number of pegs", "", UNIT_INT, &settings.pieces,
                            NULL, -1, MAX_PIECES_COUNT, 1, NULL);
                break;
            case 2:
                rb->set_int("Number of guesses", "", UNIT_INT, &settings.guesses,
                            NULL, -1, MAX_GUESSES_COUNT, 1, NULL);
                break;
            case 3:
                rb->set_bool("Display labels", &settings.labeling);
                break;
            case 4:
                rb->set_bool("Display frames", &settings.framing);
                break;
            case GO_TO_PREVIOUS:
                menu_quit = true;
                break;
            default:
                break;
        }
    }
}

static bool resume;
static int menu_cb(int action,
                   const struct menu_item_ex *this_item,
                   struct gui_synclist *this_list)
{
    (void)this_list;
    int i = ((intptr_t)this_item);
    if ((action == ACTION_REQUEST_MENUITEM) && (!resume && (i==0)))
        return ACTION_EXIT_MENUITEM;
    return action;
}

static void main_menu(void) {
    MENUITEM_STRINGLIST(main_menu, "Codebuster", menu_cb,
                        "Resume Game", "Start New Game", "Settings",
                        "Playback Control", "Quit");
    int cur_item = 0;
    bool menu_quit = false;

    while(!menu_quit) {

        switch(rb->do_menu(&main_menu, &cur_item, NULL, false)) {
            case 0:
                resume = true;
                menu_quit = true;
                break;
            case 1:
                leave = true;
                menu_quit = true;
                break;
            case 2:
                settings_menu();
                break;
            case 3:
                playback_control(NULL);
                break;
            case 4:
                quit = menu_quit = true;
                break;
            case MENU_ATTACHED_USB:
                usb = menu_quit = true;
                break;
            default:
                break;
        }
    }
}

enum plugin_status plugin_start(const void* parameter) {
    (void)parameter;

    rb->srand(*rb->current_tick);
    rb->lcd_setfont(FONT_SYSFIXED);
    rb->lcd_set_backdrop(NULL);
    rb->lcd_set_foreground(LCD_WHITE);
    rb->lcd_set_background(LCD_BLACK);

    configfile_load(CONFIG_FILE_NAME, config, ARRAYLEN(config), 0);
    rb->memcpy(&old_settings, &settings, sizeof(settings));

    main_menu();
    while (!quit) {
        init_board();
        randomize_solution();
        init_vars();

        draw_board(0, 0);
        int button = 0, guess = 0, piece = 0;
        for (guess = 0; guess < guesses_count && !stop_game(); guess++) {
            while(!stop_game()) {
                draw_board(guess, piece);

                button = get_button();
                if (button == PLA_SELECT)
                    break;

#if (CONFIG_KEYPAD == IPOD_1G2G_PAD) \
    || (CONFIG_KEYPAD == IPOD_3G_PAD) \
    || (CONFIG_KEYPAD == IPOD_4G_PAD)
                if (button == PLA_UP) /* Menu button */
                    button = PLA_CANCEL;
#endif

                switch (button) {

                    /* Exit */
                    case PLA_EXIT:
                    case PLA_CANCEL:
                        resume = true;
                        main_menu();
                        break;

                    /* Next piece */
                    case PLA_RIGHT:
                    case PLA_RIGHT_REPEAT:
                        piece = (piece + 1) % pieces_count;
                        break;

                    /* Previous piece */
                    case PLA_LEFT:
                    case PLA_LEFT_REPEAT:
                        piece = (piece + pieces_count - 1) % pieces_count;
                        break;

                    /* Next color */
#ifdef HAVE_SCROLLWHEEL
                    case PLA_SCROLL_FWD:
                    case PLA_SCROLL_FWD_REPEAT:
#endif
                    case PLA_DOWN:
                    case PLA_DOWN_REPEAT:
                         guesses[guess].pieces[piece] =
                            (guesses[guess].pieces[piece] + 1)
                            % colors_count;
                        break;

                    /* Previous color */
#ifdef HAVE_SCROLLWHEEL
                    case PLA_SCROLL_BACK:
                    case PLA_SCROLL_BACK_REPEAT:
#endif
                    case PLA_UP:
                    case PLA_UP_REPEAT:
                        guesses[guess].pieces[piece] =
                            (guesses[guess].pieces[piece] + colors_count - 1)
                            % colors_count;
                        break;

                    default:
                        if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
                            quit = usb = true;
                }

                if (guesses[guess].pieces[piece] == -1)
                    guesses[guess].pieces[piece] = 0;
            }

            if (!quit) {
                validate_guess(&guesses[guess]);

                if (guesses[guess].score.correct == pieces_count)
                    found = true;

                if (guess + 1 < guesses_count && !found)
                    guesses[guess + 1] = guesses[guess];
            }
        }

        game_ended = true;
        resume = false;
        if (!quit && !leave) {
            draw_board(guess, piece);

            if (found)
                rb->splash(HZ, "Well done :)");
            else
                rb->splash(HZ, "Wooops :(");
            do {
                button = rb->button_get(true);
                if (rb->default_event_handler(button) == SYS_USB_CONNECTED) {
                    quit = usb = true;
                }
            } while( ( button == BUTTON_NONE )
                  || ( button & (BUTTON_REL|BUTTON_REPEAT) ) );
            main_menu();
        }
    }
    if (rb->memcmp(&old_settings, &settings, sizeof(settings)))
        configfile_save(CONFIG_FILE_NAME, config, ARRAYLEN(config), 0);

    rb->lcd_setfont(FONT_UI);
    return (usb) ? PLUGIN_USB_CONNECTED : PLUGIN_OK;
}
