#include "blind_select.h"

#include "affine_background.h"
#include "audio_utils.h"
#include "background_blind_select_gfx.h"
#include "blind.h"
#include "button.h"
#include "game.h"
#include "game_variables.h"
#include "graphic_utils.h"
#include "layout.h"
#include "soundbank.h"
#include "sprite.h"
#include "state_machine.h"
#include "timer.h"
#include "util.h"

#include <maxmod.h>

static const u32 BLIND_SELECT_BTN_PID = 15;
static const u32 BLIND_SKIP_BTN_PID = 5;
static const u32 BLIND_SKIP_BTN_SELECTED_BORDER_PID = 10;
static const u32 BLIND_SELECT_BTN_SELECTED_BORDER_PID = 18;

static const u32 TM_DISP_BLIND_PANEL_FINISH = 7;
static const u32 TM_DISP_BLIND_PANEL_START = 1;

static int timer;

static void game_blind_select_start_anim_seq(void);
static void game_blind_select_handle_input(void);
static void game_blind_select_selected_anim_seq(void);
static void game_blind_select_display_blind_panel(void);
static void game_blind_select_exit(void);
static Rect game_blind_select_get_req_score_rect(enum BlindTokens blind);
static void game_blind_select_print_blinds_reqs_and_rewards(void);
static enum BlindType get_blind_type_from_token(enum BlindTokens blind);
static void blind_tokens_init(void);

enum BlindSelectState
{
    START_ANIM_SEQ,
    BLIND_SELECT,
    BLIND_SELECTED_ANIM_SEQ,
    DISPLAY_BLIND_PANEL,
    BLIND_SELECT_EXIT,
    BLIND_SELECT_MAX,
};

// TODO: this will be refactored into common state machine
static StateInfo state_info[] = {
    STATE_INFO_UPDATE_FN_ONLY(game_blind_select_start_anim_seq),
    STATE_INFO_UPDATE_FN_ONLY(game_blind_select_handle_input),
    STATE_INFO_UPDATE_FN_ONLY(game_blind_select_selected_anim_seq),
    STATE_INFO_UPDATE_FN_ONLY(game_blind_select_display_blind_panel),
    STATE_INFO_UPDATE_FN_ONLY(game_blind_select_exit),
};

static StateMachine blind_select_sm = STATE_MACHINE_DEFINE(state_info, BLIND_SELECT_MAX);

// clang-format off
// Points                                                x        y
static const BG_POINT TOP_LEFT_PANEL_EMPTY_3W_ROW_POS = {29,      31};
// Rects                                                 left     top     right   bottom
static const Rect BLIND_SKIP_BTN_GRAY_RECT            = {0,       24,     4,      27};
static const Rect BLIND_SKIP_BTN_PREANIM_DEST_RECT    = {9,       29,     19,     31};
static const Rect SINGLE_BLIND_SEL_REQ_SCORE_RECT     = {80,      120,    104,    128};
static const Rect SINGLE_BLIND_SELECT_RECT            = {9,       7,      13,     31};
// clang-format on

static const u32 TM_END_ANIM_SEQ = 12;
static const u32 TM_BLIND_SELECT_START = 1;

static const u32 BLIND_LEFT_X = 80;
static const u32 BLIND_CENTER_X = 120;
static const u32 BLIND_RIGHT_X = 160;

static const u32 BLIND_ROW = 0;
static const u32 SKIP_ROW = 1;

static int selection_x = 0;
static int selection_y = 0;

static Sprite* blind_select_tokens[NUM_BLINDS_PER_ANTE] = {NULL};

static void game_blind_select_start_anim_seq()
{
    // main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP);
    main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP);

    for (int i = 0; i < NUM_BLINDS_PER_ANTE; i++)
    {
        sprite_position(
            blind_select_tokens[i],
            blind_select_tokens[i]->pos.x,
            blind_select_tokens[i]->pos.y - TILE_SIZE
        );
    }

    if (timer == TM_END_ANIM_SEQ)
    {
        game_blind_select_print_blinds_reqs_and_rewards();
        state_machine_change_state(&blind_select_sm, BLIND_SELECT);
        timer = TM_ZERO; // Reset the timer
    }
}

static inline void game_blind_select_erase_blind_reqs_and_rewards()
{
    for (enum BlindTokens curr_blind = SMALL_BLIND; curr_blind < NUM_BLINDS_PER_ANTE; curr_blind++)
    {
        Rect blind_req_and_reward_rect = SINGLE_BLIND_SEL_REQ_SCORE_RECT;

        // To account for both raised blind and reward
        blind_req_and_reward_rect.top -= TILE_SIZE;
        blind_req_and_reward_rect.bottom += TILE_SIZE;

        // To account for overflow
        blind_req_and_reward_rect.right += TILE_SIZE;

        blind_req_and_reward_rect.left +=
            curr_blind * rect_width(&SINGLE_BLIND_SELECT_RECT) * TILE_SIZE;
        blind_req_and_reward_rect.right +=
            curr_blind * rect_width(&SINGLE_BLIND_SELECT_RECT) * TILE_SIZE;

        tte_erase_rect_wrapper(blind_req_and_reward_rect);
    }
}

void increment_blind(enum BlindState increment_reason)
{
    switch (g_game_vars.current_blind)
    {
        // defeated small blind: go to big
        case BLIND_TYPE_SMALL:
            g_game_vars.current_blind = BLIND_TYPE_BIG;
            g_game_vars.blinds_states[SMALL_BLIND] = increment_reason;
            g_game_vars.blinds_states[BIG_BLIND] = BLIND_STATE_CURRENT;
            break;
        // defeated big blind: go to next boss
        case BLIND_TYPE_BIG:
            g_game_vars.current_blind = g_game_vars.next_boss_blind;
            g_game_vars.blinds_states[BIG_BLIND] = increment_reason;
            g_game_vars.blinds_states[BOSS_BLIND] = BLIND_STATE_CURRENT;
            break;
        // defeated a boss: reset everything
        default:
            g_game_vars.current_blind = BLIND_TYPE_SMALL;
            g_game_vars.blinds_states[SMALL_BLIND] = BLIND_STATE_CURRENT;
            g_game_vars.blinds_states[BIG_BLIND] = BLIND_STATE_UPCOMING;
            g_game_vars.blinds_states[BOSS_BLIND] = BLIND_STATE_UPCOMING;
            break;
    }
}

// TODO: convert these to proper buttons.
static inline void highlight_select_button(void)
{
    memset16(&pal_bg_mem[BLIND_SELECT_BTN_SELECTED_BORDER_PID], 0xFFFF, 1);
    memcpy16(&pal_bg_mem[BLIND_SKIP_BTN_SELECTED_BORDER_PID], &pal_bg_mem[BLIND_SKIP_BTN_PID], 1);
}

static inline void highlight_skip_button(void)
{
    memcpy16(
        &pal_bg_mem[BLIND_SELECT_BTN_SELECTED_BORDER_PID],
        &pal_bg_mem[BLIND_SELECT_BTN_PID],
        1
    );
    memset16(&pal_bg_mem[BLIND_SKIP_BTN_SELECTED_BORDER_PID], 0xFFFF, 1);
}

static void game_blind_select_handle_input()
{
    if (timer == TM_BLIND_SELECT_START && g_game_vars.current_blind == BLIND_TYPE_BOSS)
    {
        selection_y = BLIND_ROW;
    }

    // Blind select input logic
    if (key_hit(KEY_UP))
    {
        selection_y = BLIND_ROW;
        highlight_select_button();
    }
    else if (key_hit(KEY_DOWN) && g_game_vars.current_blind <= BLIND_TYPE_BIG)
    {
        selection_y = SKIP_ROW;
        highlight_skip_button();
    }
    else if (key_hit(SELECT_CARD))
    {
        game_blind_select_erase_blind_reqs_and_rewards();

        switch (selection_y)
        {
            case BLIND_ROW:
                play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
                state_machine_change_state(&blind_select_sm, BLIND_SELECTED_ANIM_SEQ);
                timer = TM_ZERO;
                ++g_game_vars.round;
                display_round();
                break;
            case SKIP_ROW:
                if (g_game_vars.current_blind <= BLIND_TYPE_BIG)
                {
                    play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
                    increment_blind(BLIND_STATE_SKIPPED);

                    selection_y = BLIND_ROW; // Reset selection to first option

                    change_background(BG_BLIND_SELECT, true);

                    // TODO: Create a generic vertical move by any number of tiles to avoid for
                    // loops?
                    for (int i = 0; i < 12; i++)
                    {
                        main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP);
                    }

                    for (int i = 0; i < NUM_BLINDS_PER_ANTE; i++)
                    {
                        sprite_position(
                            blind_select_tokens[i],
                            blind_select_tokens[i]->pos.x,
                            blind_select_tokens[i]->pos.y - (TILE_SIZE * 12)
                        );
                    }

                    game_blind_select_print_blinds_reqs_and_rewards();
                    highlight_select_button();

                    timer = TM_ZERO;
                }
                break;
            default:
                break;
        }
    }
}

static void game_blind_select_selected_anim_seq()
{
    if (timer < 15)
    {
        Rect blinds_rect = POP_MENU_ANIM_RECT;
        blinds_rect.top -= 1; // Because of the raised blind
        main_bg_se_move_rect_1_tile_vert(blinds_rect, SCREEN_DOWN);

        for (int i = 0; i < NUM_BLINDS_PER_ANTE; i++)
        {
            sprite_position(
                blind_select_tokens[i],
                blind_select_tokens[i]->pos.x,
                blind_select_tokens[i]->pos.y + TILE_SIZE
            );
        }
    }
    else if (timer >= MENU_POP_OUT_ANIM_FRAMES)
    {
        for (int i = 0; i < NUM_BLINDS_PER_ANTE; i++)
        {
            obj_hide(blind_select_tokens[i]->obj);
        }

        timer = TM_ZERO;
        state_machine_change_state(&blind_select_sm, DISPLAY_BLIND_PANEL);
    }
}

static void game_blind_select_display_blind_panel()
{
    if (timer >= TM_DISP_BLIND_PANEL_FINISH)
    {
        state_machine_change_state(&blind_select_sm, BLIND_SELECT_EXIT);
        return;
    }

    // Switches to the selecting background and clears the blind panel area
    if (timer == TM_DISP_BLIND_PANEL_START)
    {
        change_background(BG_CARD_SELECTING, false);

        // Need to clear the top left panel as a side effect of change_background()
        main_bg_se_copy_expand_3w_row(TOP_LEFT_PANEL_ANIM_RECT, TOP_LEFT_PANEL_EMPTY_3W_ROW_POS);

        reset_top_left_panel_bottom_row();

        if (g_game_vars.current_blind >= BLIND_TYPE_BOSS)
        {
            affine_background_set_color(
                blind_get_color(g_game_vars.current_blind, BLIND_SHADOW_COLOR_INDEX)
            );
        }
    }

    // Shift the blind panel down onto screen
    for (int y = 0; y < timer; y++)
    {
        int y_from = 26 + y - timer;
        int y_to = 0 + y;

        Rect from = {0, y_from, 8, y_from};
        BG_POINT to = {0, y_to};

        main_bg_se_copy_rect(from, to);
    }
}

static void game_blind_select_exit(void)
{
    reset_background();
    game_change_state(GAME_STATE_PLAYING);
}

static Rect game_blind_select_get_req_score_rect(enum BlindTokens blind)
{
    Rect blind_req_score_rect = SINGLE_BLIND_SEL_REQ_SCORE_RECT;

    blind_req_score_rect.left += blind * rect_width(&SINGLE_BLIND_SELECT_RECT) * TILE_SIZE;
    blind_req_score_rect.right += blind * rect_width(&SINGLE_BLIND_SELECT_RECT) * TILE_SIZE;

    if (g_game_vars.blinds_states[blind] == BLIND_STATE_CURRENT)
    {
        // Current blind is raised
        blind_req_score_rect.top -= TILE_SIZE;
        blind_req_score_rect.bottom -= TILE_SIZE;
    }

    return blind_req_score_rect;
}

static enum BlindType get_blind_type_from_token(enum BlindTokens blind)
{
    enum BlindType blind_type;
    switch (blind)
    {
        case SMALL_BLIND:
            blind_type = BLIND_TYPE_SMALL;
            break;
        case BIG_BLIND:
            blind_type = BLIND_TYPE_BIG;
            break;
        default:
            blind_type = g_game_vars.next_boss_blind;
            break;
    }
    return blind_type;
}

static inline void game_blind_select_print_blind_req(enum BlindTokens blind)
{
    Rect blind_req_score_rect = game_blind_select_get_req_score_rect(blind);

    u32 blind_req = blind_get_requirement(get_blind_type_from_token(blind), g_game_vars.ante);

    char blind_req_str_buff[UINT_MAX_DIGITS + 1];
    truncate_uint_to_suffixed_str(
        blind_req,
        rect_width(&blind_req_score_rect) / TTE_CHAR_SIZE,
        blind_req_str_buff
    );

    update_text_rect_to_right_align_str(&blind_req_score_rect, blind_req_str_buff, OVERFLOW_RIGHT);

    tte_printf(
        "#{P:%d,%d; cx:0x%X000}%s",
        blind_req_score_rect.left,
        blind_req_score_rect.top,
        TTE_RED_PB,
        blind_req_str_buff
    );
}

static inline void game_blind_select_print_blind_reward(enum BlindTokens blind)
{
    int blind_reward = blind_get_reward(get_blind_type_from_token(blind));
    Rect blind_reward_rect = game_blind_select_get_req_score_rect(blind);

    // The reward is right below the score.
    blind_reward_rect.top += TILE_SIZE;
    blind_reward_rect.bottom += TILE_SIZE;

    char blind_reward_str_buff[UINT_MAX_DIGITS + 2]; // +2 for null terminator and "$"
    snprintf(blind_reward_str_buff, sizeof(blind_reward_str_buff), "$%d", blind_reward);

    update_text_rect_to_right_align_str(&blind_reward_rect, blind_reward_str_buff, OVERFLOW_RIGHT);

    tte_printf(
        "#{P:%d,%d; cx:0x%X000}%s",
        blind_reward_rect.left,
        blind_reward_rect.top,
        TTE_YELLOW_PB,
        blind_reward_str_buff
    );
}

static void game_blind_select_print_blinds_reqs_and_rewards(void)
{
    for (enum BlindTokens curr_blind = 0; curr_blind < NUM_BLINDS_PER_ANTE; curr_blind++)
    {
        game_blind_select_print_blind_req(curr_blind);
        game_blind_select_print_blind_reward(curr_blind);
    }
}

static inline void reroll_boss_blind(bool no_tiles)
{
    // Showdown blinds only show up on ante 8, 16, etc...
    g_game_vars.next_boss_blind =
        roll_blind_type((g_game_vars.ante % 8 == 0) && (g_game_vars.ante > 0));
    if (!no_tiles)
    {
        apply_blind_tiles(g_game_vars.next_boss_blind, BOSS_BLIND_TOKEN_LAYER);
    }
}

static void blind_tokens_init()
{
    if (g_game_vars.current_blind == BLIND_TYPE_SMALL)
        reroll_boss_blind(true);

    sprite_destroy(&blind_select_tokens[SMALL_BLIND]);
    sprite_destroy(&blind_select_tokens[BIG_BLIND]);
    sprite_destroy(&blind_select_tokens[BOSS_BLIND]);

    blind_select_tokens[SMALL_BLIND] = blind_token_new(
        BLIND_TYPE_SMALL,
        CUR_BLIND_TOKEN_POS.x,
        CUR_BLIND_TOKEN_POS.y,
        SMALL_BLIND_TOKEN_LAYER
    );
    blind_select_tokens[BIG_BLIND] = blind_token_new(
        BLIND_TYPE_BIG,
        CUR_BLIND_TOKEN_POS.x,
        CUR_BLIND_TOKEN_POS.y,
        BIG_BLIND_TOKEN_LAYER
    );
    blind_select_tokens[BOSS_BLIND] = blind_token_new(
        g_game_vars.next_boss_blind,
        CUR_BLIND_TOKEN_POS.x,
        CUR_BLIND_TOKEN_POS.y,
        BOSS_BLIND_TOKEN_LAYER
    );

    for (int i = 0; i < NUM_BLINDS_PER_ANTE; i++)
    {
        obj_hide(blind_select_tokens[i]->obj);
    }
}

void game_blind_select_on_init(void)
{
    timer = TM_ZERO;
    state_machine_register(&blind_select_sm);
    state_machine_change_state(&blind_select_sm, START_ANIM_SEQ);

    selection_x = 0;
    selection_y = 0;

    blind_tokens_init();

    // TODO: silly bug rn, the sprite tokens are unhidden on a background change.
    // this probably shouldn't be here. also need to force redraw or the callback
    // doesn't run that moves the tokens. that should probably happen here.
    change_background(BG_BLIND_SELECT, true);

    highlight_select_button();

    play_sfx(SFX_POP, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
}

void game_blind_select_on_update(void)
{
    timer++;
}

void game_blind_select_on_exit(void)
{
    // For some reason that I haven't figured out yet,
    // if I don't destroy the blind tokens they won't
    // show up on the next run.
    sprite_destroy(&blind_select_tokens[SMALL_BLIND]);
    sprite_destroy(&blind_select_tokens[BIG_BLIND]);
    sprite_destroy(&blind_select_tokens[BOSS_BLIND]);

    reset_background();
    selection_y = 0;

    state_machine_remove(&blind_select_sm);
}

void game_blind_select_change_background(void)
{
    for (int i = 0; i < NUM_BLINDS_PER_ANTE; i++)
    {
        obj_unhide(blind_select_tokens[i]->obj, 0);
    }

    // Default y position for the blind select tokens. 12 is the amount of tiles the background
    // is shifted down by
    const int default_y = 89 + (TILE_SIZE * 12);
    // TODO refactor magic numbers '80/120/160' into a map to loop with
    sprite_position(blind_select_tokens[SMALL_BLIND], BLIND_LEFT_X, default_y);
    sprite_position(blind_select_tokens[BIG_BLIND], BLIND_CENTER_X, default_y);
    sprite_position(blind_select_tokens[BOSS_BLIND], BLIND_RIGHT_X, default_y);

    toggle_windows(false, true);

    GRIT_CPY(pal_bg_mem, background_blind_select_gfxPal);
    GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_blind_select_gfxTiles);
    GRIT_CPY(&se_mem[MAIN_BG_SBB], background_blind_select_gfxMap);

    // Copy boss blind colors to blind select palette
    memset16(
        &pal_bg_mem[1],
        blind_get_color(g_game_vars.next_boss_blind, BLIND_BACKGROUND_MAIN_COLOR_INDEX),
        1
    );
    memset16(
        &pal_bg_mem[7],
        blind_get_color(g_game_vars.next_boss_blind, BLIND_BACKGROUND_SHADOW_COLOR_INDEX),
        1
    );

    // Disable the button highlight colors
    // Select button PID is 15 and the outline is 18
    memcpy16(
        &pal_bg_mem[BLIND_SELECT_BTN_SELECTED_BORDER_PID],
        &pal_bg_mem[BLIND_SELECT_BTN_PID],
        1
    );
    // It seems the skip button (and score multiplier and deck) PB idx is
    // actually 5, not 10. 10 is the selected border color
    // Setting this palette value though doesn't seem to have an
    // effect.
    memcpy16(&pal_bg_mem[BLIND_SKIP_BTN_SELECTED_BORDER_PID], &pal_bg_mem[BLIND_SKIP_BTN_PID], 1);

    for (int i = 0; i < NUM_BLINDS_PER_ANTE; i++)
    {
        Rect curr_blind_rect = SINGLE_BLIND_SELECT_RECT;

        // There's no gap between them
        curr_blind_rect.left += i * rect_width(&SINGLE_BLIND_SELECT_RECT);
        curr_blind_rect.right += i * rect_width(&SINGLE_BLIND_SELECT_RECT);

        if (g_game_vars.blinds_states[i] != BLIND_STATE_CURRENT &&
            (i == BLIND_TYPE_SMALL || i == BLIND_TYPE_BIG)) // Make the skip button gray
        {
            BG_POINT skip_blind_btn_pos_dest = {
                BLIND_SKIP_BTN_PREANIM_DEST_RECT.left,
                BLIND_SKIP_BTN_PREANIM_DEST_RECT.top
            };
            skip_blind_btn_pos_dest.x = curr_blind_rect.left;

            Rect skip_blind_btn_rect_src = BLIND_SKIP_BTN_GRAY_RECT;
            skip_blind_btn_rect_src.top += i * rect_height(&BLIND_SKIP_BTN_GRAY_RECT);
            skip_blind_btn_rect_src.bottom += i * rect_height(&BLIND_SKIP_BTN_GRAY_RECT);

            main_bg_se_copy_rect(skip_blind_btn_rect_src, skip_blind_btn_pos_dest);
        }

        switch (g_game_vars.blinds_states[i])
        {
            case BLIND_STATE_CURRENT: // Raise the blind panel up a bit
            {
                // TODO: Replace copies with main_bg_se_copy_rect() of named rects
                int x_from = 0;
                int y_from = 27;

                main_bg_se_copy_rect_1_tile_vert(curr_blind_rect, SCREEN_UP);

                int x_to = curr_blind_rect.left;
                int y_to = 31;

                if (i == BLIND_TYPE_BIG)
                {
                    y_from = 31;
                }
                else if (i > BLIND_TYPE_BIG)
                {
                    x_from = x_to;
                    y_from = 30;
                }

                // Copy plain tiles onto the bottom of the raised blind panel to fill the gap
                // created by the raise
                Rect gap_fill_rect =
                    {x_from, y_from, x_from + rect_width(&SINGLE_BLIND_SELECT_RECT) - 1, y_from};
                BG_POINT gap_fill_point = {x_to, y_to};
                main_bg_se_copy_rect(gap_fill_rect, gap_fill_point);

                // Move token up by a tile
                sprite_position(
                    blind_select_tokens[i],
                    blind_select_tokens[i]->pos.x,
                    blind_select_tokens[i]->pos.y - TILE_SIZE
                );
                break;
            }
            case BLIND_STATE_UPCOMING: // Change the select icon to "NEXT"
            {
                int x_from = 0;
                int y_from = 20;

                int x_to = 10 + (i * rect_width(&SINGLE_BLIND_SELECT_RECT));
                int y_to = 20;

                memcpy16(
                    &se_mem[MAIN_BG_SBB][x_to + 32 * y_to],
                    &se_mem[MAIN_BG_SBB][x_from + 32 * y_from],
                    3
                );
                break;
            }
            case BLIND_STATE_SKIPPED: // Change the select icon to "SKIP"
            {
                int x_from = 3;
                int y_from = 20;

                int x_to = 10 + (i * 5);
                int y_to = 20;

                memcpy16(
                    &se_mem[MAIN_BG_SBB][x_to + 32 * y_to],
                    &se_mem[MAIN_BG_SBB][x_from + 32 * y_from],
                    3
                );
                break;
            }
            case BLIND_STATE_DEFEATED: // Change the select icon to "DEFEATED"
            {
                int x_from = 6;
                int y_from = 20;

                int x_to = 10 + (i * 5);
                int y_to = 20;

                memcpy16(
                    &se_mem[MAIN_BG_SBB][x_to + 32 * y_to],
                    &se_mem[MAIN_BG_SBB][x_from + 32 * y_from],
                    3
                );
                break;
            }
            default:
                break;
        }
    }
}
