#include "minesweeper_game_screen.h"

static const Icon* tile_icons[13] = {
    &I_tile_empty_8x8,
    &I_tile_0_8x8,
    &I_tile_1_8x8,
    &I_tile_2_8x8,
    &I_tile_3_8x8,
    &I_tile_4_8x8,
    &I_tile_5_8x8,
    &I_tile_6_8x8,
    &I_tile_7_8x8,
    &I_tile_8_8x8,
    &I_tile_mine_8x8,
    &I_tile_flag_8x8,
    &I_tile_uncleared_8x8,
};

// They way this enum is set up allows us to index the Icon* array above for some mine types
typedef enum {
    MineSweeperGameScreenTileNone = 0,
    MineSweeperGameScreenTileZero,
    MineSweeperGameScreenTileOne,
    MineSweeperGameScreenTileTwo,
    MineSweeperGameScreenTileThree,
    MineSweeperGameScreenTileFour,
    MineSweeperGameScreenTileFive,
    MineSweeperGameScreenTileSix,
    MineSweeperGameScreenTileSeven,
    MineSweeperGameScreenTileEight,
    MineSweeperGameScreenTileMine,
    MineSweeperGameScreenTileTypeCount,
} MineSweeperGameScreenTileType;

typedef enum {
    MineSweeperGameScreenTileStateFlagged,
    MineSweeperGameScreenTileStateUncleared,
    MineSweeperGameScreenTileStateCleared,
} MineSweeperGameScreenTileState;

struct MineSweeperGameScreen {
    View* view;
    void* context;
    GameScreenInputCallback input_callback;
};

typedef struct {
    int16_t x_abs, y_abs;
} CurrentPosition;

typedef struct {
    uint16_t x_abs, y_abs;
    const Icon* icon;
} IconElement;

typedef struct {
    IconElement icon_element;
    MineSweeperGameScreenTileState tile_state;
    MineSweeperGameScreenTileType tile_type;
} MineSweeperTile;

typedef struct {
    MineSweeperTile board[MINESWEEPER_BOARD_MAX_TILES];
    CurrentPosition curr_pos;
    uint8_t right_boundary, bottom_boundary, board_width, board_height, board_difficulty;
    uint16_t mines_left;
    uint16_t flags_left;
    uint16_t tiles_left;
    uint32_t start_tick;
    FuriString* info_str;
    bool ensure_solvable_board;
    bool is_restart_triggered;
    bool is_holding_down_button;
    bool has_lost_game;
} MineSweeperGameScreenModel;

// Multipliers for ratio of mines to tiles
static const float difficulty_multiplier[3] = {
    0.15f,
    0.17f,
    0.19f,
};

// Offsets array used consistently when checking surrounding tiles
static const int8_t offsets[8][2] = {
    {-1, 1},
    {0, 1},
    {1, 1},
    {1, 0},
    {1, -1},
    {0, -1},
    {-1, -1},
    {-1, 0},
};

static MineSweeperTile board_t[MINESWEEPER_BOARD_MAX_TILES];

/****************************************************************
 * Function declarations
 *
 * Non public function declarations
 ***************************************************************/

// Static helper functions

static void setup_board(MineSweeperGameScreen* instance);

static bool check_board_with_verifier(
    MineSweeperTile* board,
    const uint8_t board_width,
    const uint8_t board_height,
    uint16_t total_mines);

static inline void bfs_tile_clear_verifier(
    MineSweeperTile* board,
    const uint8_t board_width,
    const uint8_t board_height,
    const uint16_t x,
    const uint16_t y,
    point_deq_t* edges,
    point_set_t* visited);

static uint16_t bfs_tile_clear(
    MineSweeperTile* board,
    const uint8_t board_width,
    const uint8_t board_height,
    const uint16_t x,
    const uint16_t y);

static void mine_sweeper_game_screen_set_board_information(
    MineSweeperGameScreen* instance,
    const uint8_t width,
    const uint8_t height,
    const uint8_t difficulty,
    bool is_solvable);

static bool try_clear_surrounding_tiles(MineSweeperGameScreenModel* model);

static void
    bfs_to_closest_tile(MineSweeperGameScreen* instance, MineSweeperGameScreenModel* model);

// Currently not using enter/exit callback
static void mine_sweeper_game_screen_view_enter(void* context);
static void mine_sweeper_game_screen_view_exit(void* context);

// Different input/draw callbacks for play/win/lose state
static void mine_sweeper_game_screen_view_end_draw_callback(Canvas* canvas, void* _model);
static void mine_sweeper_game_screen_view_play_draw_callback(Canvas* canvas, void* _model);

// These consolidate the function calls for led/haptic/sound for specific events
static void mine_sweeper_long_ok_effect(void* context);
static void mine_sweeper_short_ok_effect(void* context);
static void mine_sweeper_flag_effect(void* context);
static void mine_sweeper_move_effect(void* context);
static void mine_sweeper_oob_effect(void* context);
static void mine_sweeper_lose_effect(void* context);
static void mine_sweeper_win_effect(void* context);

static inline bool handle_player_move(
    MineSweeperGameScreen* instance,
    MineSweeperGameScreenModel* model,
    InputEvent* event);
static int8_t
    handle_short_ok_input(MineSweeperGameScreen* instance, MineSweeperGameScreenModel* model);
static int8_t
    handle_long_ok_input(MineSweeperGameScreen* instance, MineSweeperGameScreenModel* model);
static bool
    handle_long_back_flag_input(MineSweeperGameScreen* instance, MineSweeperGameScreenModel* model);

static bool mine_sweeper_game_screen_view_end_input_callback(InputEvent* event, void* context);
static bool mine_sweeper_game_screen_view_play_input_callback(InputEvent* event, void* context);

/**************************************************************
 * Function definitions
 *************************************************************/

/**
 * This function is called on alloc, reset, and win/lose condition.
 * It sets up a random board to be checked by the verifier
 */
static void setup_board(MineSweeperGameScreen* instance) {
    furi_assert(instance);

    uint16_t board_tile_count = 0;
    uint8_t board_width = 0, board_height = 0, board_difficulty = 0;

    with_view_model(
        instance->view,
        MineSweeperGameScreenModel * model,
        {
            board_width = model->board_width;
            board_height = model->board_height;
            board_tile_count = (model->board_width * model->board_height);
            board_difficulty = model->board_difficulty;
        },
        false);

    uint16_t num_mines = board_tile_count * difficulty_multiplier[board_difficulty];

    /** We can use a temporary buffer to set the tile types initially
     * and manipulate then save to actual model
     */
    MineSweeperGameScreenTileType tiles[MINESWEEPER_BOARD_MAX_TILES];
    memset(&tiles, MineSweeperGameScreenTileZero, sizeof(tiles));

    // Randomly place tiles except in the corners to help guarantee solvability
    for(uint16_t i = 0; i < num_mines; i++) {
        uint16_t rand_pos;
        uint16_t x;
        uint16_t y;
        bool is_invalid_position;
        do {
            rand_pos = furi_hal_random_get() % board_tile_count;
            x = rand_pos / board_width;
            y = rand_pos % board_width;

            is_invalid_position =
                ((rand_pos == 0) || (x == 0 && y == 1) || (x == 1 && y == 0) ||
                 rand_pos == board_tile_count - 1 || (x == 0 && y == board_width - 1) ||
                 (x == board_height - 1 && y == 0));

        } while(tiles[rand_pos] == MineSweeperGameScreenTileMine || is_invalid_position);

        tiles[rand_pos] = MineSweeperGameScreenTileMine;
    }

    /** All mines are set so we look at each tile for surrounding mines */
    for(uint16_t i = 0; i < board_tile_count; i++) {
        MineSweeperGameScreenTileType tile_type = tiles[i];

        if(tile_type == MineSweeperGameScreenTileMine) {
            continue;
        }

        uint16_t mine_count = 0;

        uint16_t x = i / board_width;
        uint16_t y = i % board_width;

        for(uint8_t j = 0; j < 8; j++) {
            int16_t dx = x + (int16_t)offsets[j][0];
            int16_t dy = y + (int16_t)offsets[j][1];

            if(dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
                continue;
            }

            uint16_t pos = dx * board_width + dy;
            if(tiles[pos] == MineSweeperGameScreenTileMine) {
                mine_count++;
            }
        }

        tiles[i] = (MineSweeperGameScreenTileType)mine_count + 1;
    }

    // Save tiles to view model
    // Because of way tile enum and tile_icons array is set up we can
    // index tile_icons with the enum type to get the correct Icon*
    with_view_model(
        instance->view,
        MineSweeperGameScreenModel * model,
        {
            for(uint16_t i = 0; i < board_tile_count; i++) {
                model->board[i].tile_type = tiles[i];
                model->board[i].tile_state = MineSweeperGameScreenTileStateUncleared;
                model->board[i].icon_element.icon = tile_icons[tiles[i]];
                model->board[i].icon_element.x_abs = (i / model->board_width);
                model->board[i].icon_element.y_abs = (i % model->board_width);
            }

            model->mines_left = num_mines;
            model->flags_left = num_mines;
            model->tiles_left = (model->board_width * model->board_height) - model->mines_left;
            model->curr_pos.x_abs = 0;
            model->curr_pos.y_abs = 0;
            model->right_boundary = MINESWEEPER_SCREEN_TILE_WIDTH;
            model->bottom_boundary = MINESWEEPER_SCREEN_TILE_HEIGHT;
            model->is_restart_triggered = false;
            model->has_lost_game = false;
        },
        true);
}

/**
 *  This function serves as the verifier for a board to check whether it has to be solved ambiguously or not
 *
 *  Returns true if it is unambiguously solvable.
 */
static bool check_board_with_verifier(
    MineSweeperTile* board,
    const uint8_t board_width,
    const uint8_t board_height,
    uint16_t total_mines) {
    furi_assert(board);

    // Double ended queue used to track edges.
    point_deq_t deq;
    point_set_t visited;

    // Ordered Set for visited points
    point_deq_init(deq);
    point_set_init(visited);

    bool is_solvable = false;

    // Point_t pos will be used to keep track of the current point
    Point_t pos;
    pointobj_init(pos);

    // Starting position is 0,0
    Point start_pos = (Point){.x = 0, .y = 0};
    pointobj_set_point(pos, start_pos);

    // Initially bfs clear from 0,0 as it is safe. We should push all 'edges' found
    // into the deq and this will be where we start off from
    bfs_tile_clear_verifier(board, board_width, board_height, 0, 0, &deq, &visited);

    //While we have valid edges to check and have not solved the board
    while(!is_solvable && point_deq_size(deq) > 0) {
        bool is_stuck =
            true; // This variable will track if any flag was placed for any edge to see if we are stuck

        uint16_t deq_size = point_deq_size(deq);

        // Iterate through all edge tiles and push new ones on
        while(deq_size-- > 0) {
            // Pop point and get 1d position in buffer
            point_deq_pop_front(&pos, deq);
            Point curr_pos = pointobj_get_point(pos);
            uint16_t curr_pos_1d = curr_pos.x * board_width + curr_pos.y;

            // Get tile at 1d position
            MineSweeperTile tile = board[curr_pos_1d];
            uint8_t tile_num = tile.tile_type - 1;

            // Track total surrounding tiles and flagged tiles
            uint8_t num_surrounding_tiles = 0;
            uint8_t num_flagged_tiles = 0;

            for(uint8_t j = 0; j < 8; j++) {
                int16_t dx = curr_pos.x + (int16_t)offsets[j][0];
                int16_t dy = curr_pos.y + (int16_t)offsets[j][1];

                if(dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
                    continue;
                }

                uint16_t pos = dx * board_width + dy;
                if(board[pos].tile_state == MineSweeperGameScreenTileStateUncleared) {
                    num_surrounding_tiles++;
                } else if(board[pos].tile_state == MineSweeperGameScreenTileStateFlagged) {
                    num_surrounding_tiles++;
                    num_flagged_tiles++;
                }
            }

            if(num_flagged_tiles == tile_num) {
                // If the tile has the same number of surrounding flags as its type we bfs clear the uncleared surrounding tiles
                // pushing new unvisited edges on deq

                for(uint8_t j = 0; j < 8; j++) {
                    int16_t dx = curr_pos.x + (int16_t)offsets[j][0];
                    int16_t dy = curr_pos.y + (int16_t)offsets[j][1];

                    if(dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
                        continue;
                    }

                    uint16_t pos = dx * board_width + dy;
                    if(board[pos].tile_state == MineSweeperGameScreenTileStateUncleared) {
                        bfs_tile_clear_verifier(
                            board, board_width, board_height, dx, dy, &deq, &visited);
                    }
                }

                is_stuck = false;

            } else if(num_surrounding_tiles == tile_num) {
                // If the number of surrounding tiles is the tile num it is unambiguous so we place a flag on those tiles,
                // decrement the mine count appropriately and check win condition, and then mark stuck as false

                for(uint8_t j = 0; j < 8; j++) {
                    int16_t dx = curr_pos.x + (int16_t)offsets[j][0];
                    int16_t dy = curr_pos.y + (int16_t)offsets[j][1];

                    if(dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
                        continue;
                    }

                    uint16_t pos = dx * board_width + dy;
                    if(board[pos].tile_state == MineSweeperGameScreenTileStateUncleared) {
                        board[pos].tile_state = MineSweeperGameScreenTileStateFlagged;
                    }
                }

                total_mines -= (num_surrounding_tiles - num_flagged_tiles);

                if(total_mines == 0) is_solvable = true;

                is_stuck = false;

            } else if(num_surrounding_tiles != 0) {
                // If we have tiles around this position but the number of flagged tiles != tile num
                // and the surrounding tiles != tile num this means the tile is ambiguous. We can push
                // it back on the deq to be reprocessed with any other new edges

                point_deq_push_back(deq, pos);
            }
        }

        // If we are stuck we break as it is an ambiguous map generation
        if(is_stuck) {
            break;
        }
    }

    point_set_clear(visited);
    point_deq_clear(deq);

    return is_solvable;
}

/**
 * This is a bfs_tile clear used by the verifier which performs the normal tile clear
 * but also pushes new edges to the deq passed in. There is a separate function used
 * for the bfs_tile_clear used on the user click
 */
static inline void bfs_tile_clear_verifier(
    MineSweeperTile* board,
    const uint8_t board_width,
    const uint8_t board_height,
    const uint16_t x,
    const uint16_t y,
    point_deq_t* edges,
    point_set_t* visited) {
    furi_assert(board);
    furi_assert(edges);
    furi_assert(visited);

    // Init both the set and dequeue
    point_deq_t deq;
    point_set_t set;

    point_deq_init(deq);
    point_set_init(set);

    // Point_t pos will be used to keep track of the current point
    Point_t pos;
    pointobj_init(pos);

    // Starting position is current pos
    Point start_pos = (Point){.x = x, .y = y};
    pointobj_set_point(pos, start_pos);

    point_deq_push_back(deq, pos);

    while(point_deq_size(deq) > 0) {
        point_deq_pop_front(&pos, deq);
        Point curr_pos = pointobj_get_point(pos);
        uint16_t curr_pos_1d = curr_pos.x * board_width + curr_pos.y;

        // If in visited set
        if(point_set_cget(set, pos) != NULL || point_set_cget(*visited, pos) != NULL) {
        }

        // If it is cleared continue
        if(board[curr_pos_1d].tile_state == MineSweeperGameScreenTileStateCleared) {
            continue;
        }

        // Else set tile to cleared
        board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateCleared;

        // Add point to visited set
        point_set_push(set, pos);

        //When we hit a potential edge
        if(board[curr_pos_1d].tile_type != MineSweeperGameScreenTileZero) {
            // We can push this edge into edges if it is not in visited, as it is a new edge
            // and also add to the visited set
            if(point_set_cget(*visited, pos) == NULL) {
                point_deq_push_back(*edges, pos);
                point_set_push(*visited, pos);
            }

            // Continue processing next point for bfs tile clear
            continue;
        }

        // Process all surrounding neighbors and add valid to dequeue
        for(uint8_t i = 0; i < 8; i++) {
            int16_t dx = curr_pos.x + (int16_t)offsets[i][0];
            int16_t dy = curr_pos.y + (int16_t)offsets[i][1];

            if(dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
                continue;
            }

            Point neighbor = (Point){.x = dx, .y = dy};
            pointobj_set_point(pos, neighbor);

            if(point_set_cget(set, pos) != NULL || point_set_cget(*visited, pos) != NULL) continue;

            point_deq_push_back(deq, pos);
        }
    }

    point_set_clear(set);
    point_deq_clear(deq);
}

/**
 * This is a bfs_tile clear used in the input callbacks to clear the board on user input
 */
static inline uint16_t bfs_tile_clear(
    MineSweeperTile* board,
    const uint8_t board_width,
    const uint8_t board_height,
    const uint16_t x,
    const uint16_t y) {
    furi_assert(board);

    // We will return this number as the number of tiles cleared
    uint16_t ret = 0;

    // Init both the set and dequeue
    point_deq_t deq;
    point_set_t set;

    point_deq_init(deq);
    point_set_init(set);

    // Point_t pos will be used to keep track of the current point
    Point_t pos;
    pointobj_init(pos);

    // Starting position is current pos
    Point start_pos = (Point){.x = x, .y = y};
    pointobj_set_point(pos, start_pos);

    point_deq_push_back(deq, pos);

    while(point_deq_size(deq) > 0) {
        point_deq_pop_front(&pos, deq);
        Point curr_pos = pointobj_get_point(pos);
        uint16_t curr_pos_1d = curr_pos.x * board_width + curr_pos.y;

        // If in visited set
        if(point_set_cget(set, pos) != NULL) {
            continue;
        }

        // If it is not uncleared continue
        if(board[curr_pos_1d].tile_state != MineSweeperGameScreenTileStateUncleared) {
            continue;
        }

        // Else set tile to cleared
        board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateCleared;

        // Increment total number of cleared tiles
        ret++;

        // Add point to visited set
        point_set_push(set, pos);

        // If it is not a zero tile continue
        if(board[curr_pos_1d].tile_type != MineSweeperGameScreenTileZero) {
            continue;
        }

        // Process all surrounding neighbors and add valid to dequeue
        for(uint8_t i = 0; i < 8; i++) {
            int16_t dx = curr_pos.x + (int16_t)offsets[i][0];
            int16_t dy = curr_pos.y + (int16_t)offsets[i][1];

            if(dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
                continue;
            }

            Point neighbor = (Point){.x = dx, .y = dy};
            pointobj_set_point(pos, neighbor);

            if(point_set_cget(set, pos) != NULL) continue;

            point_deq_push_back(deq, pos);
        }
    }

    point_set_clear(set);
    point_deq_clear(deq);

    return ret;
}

static void mine_sweeper_game_screen_set_board_information(
    MineSweeperGameScreen* instance,
    uint8_t width,
    uint8_t height,
    uint8_t difficulty,
    bool is_solvable) {
    furi_assert(instance);

    // These are the min/max values that can actually be set
    if(width > 146) {
        width = 146;
    }
    if(width < 16) {
        width = 16;
    }
    if(height > 64) {
        height = 64;
    }
    if(height < 7) {
        height = 7;
    }
    if(difficulty > 2) {
        difficulty = 2;
    }

    with_view_model(
        instance->view,
        MineSweeperGameScreenModel * model,
        {
            model->board_width = width;
            model->board_height = height;
            model->board_difficulty = difficulty;
            model->ensure_solvable_board = is_solvable;
        },
        true);
}

// THIS FUNCTION CAN TRIGGER THE LOSE CONDITION
static bool try_clear_surrounding_tiles(MineSweeperGameScreenModel* model) {
    furi_assert(model);

    uint8_t curr_x = model->curr_pos.x_abs;
    uint8_t curr_y = model->curr_pos.y_abs;
    uint8_t board_width = model->board_width;
    uint8_t board_height = model->board_height;
    uint16_t curr_pos_1d = curr_x * board_width + curr_y;

    MineSweeperTile tile = model->board[curr_pos_1d];

    // Return false if tile is zero tile or not cleared
    if(tile.tile_state != MineSweeperGameScreenTileStateCleared ||
       tile.tile_type == MineSweeperGameScreenTileZero) {
        return false;
    }

    uint8_t num_surrounding_flagged = 0;
    bool was_mine_found = false;
    bool is_lose_condition_triggered = false;

    for(uint8_t j = 0; j < 8; j++) {
        int16_t dx = curr_x + (int16_t)offsets[j][0];
        int16_t dy = curr_y + (int16_t)offsets[j][1];

        if(dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
            continue;
        }

        uint16_t pos = dx * board_width + dy;
        if(model->board[pos].tile_state == MineSweeperGameScreenTileStateFlagged) {
            num_surrounding_flagged++;
        } else if(
            !was_mine_found && model->board[pos].tile_type == MineSweeperGameScreenTileMine &&
            model->board[pos].tile_state != MineSweeperGameScreenTileStateFlagged) {
            was_mine_found = true;
        }
    }

    // We clear surrounding tile
    if(num_surrounding_flagged >= tile.tile_type - 1) {
        if(was_mine_found) is_lose_condition_triggered = true;

        for(uint8_t j = 0; j < 8; j++) {
            int16_t dx = curr_x + (int16_t)offsets[j][0];
            int16_t dy = curr_y + (int16_t)offsets[j][1];

            if(dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
                continue;
            }

            uint16_t pos = dx * board_width + dy;
            if(model->board[pos].tile_state == MineSweeperGameScreenTileStateUncleared) {
                // Decrement tiles left by the amount cleared
                uint16_t tiles_cleared =
                    bfs_tile_clear(model->board, model->board_width, model->board_height, dx, dy);
                model->tiles_left -= tiles_cleared;
            }
        }
    }

    return is_lose_condition_triggered;
}

/**
 * Function is used on a long backpress on a cleared tile and returns the position
 * of the first found uncleared tile using a bfs search
 */
static void
    bfs_to_closest_tile(MineSweeperGameScreen* instance, MineSweeperGameScreenModel* model) {
    furi_assert(model);

    // Init both the set and dequeue
    point_deq_t deq;
    point_set_t set;

    point_deq_init(deq);
    point_set_init(set);

    // Return the value in this point
    Point result = (Point){.x = 0, .y = 0};

    // Point_t pos will be used to keep track of the current point
    Point_t pos;
    pointobj_init(pos);

    // Starting position is current pos
    Point start_pos = (Point){.x = model->curr_pos.x_abs, .y = model->curr_pos.y_abs};
    pointobj_set_point(pos, start_pos);

    point_deq_push_back(deq, pos);

    while(point_deq_size(deq) > 0) {
        point_deq_pop_front(&pos, deq);
        Point curr_pos = pointobj_get_point(pos);
        uint16_t curr_pos_1d = curr_pos.x * model->board_width + curr_pos.y;

        // If the current tile is uncleared and not start pos we save result
        // to this position and break
        if(model->board[curr_pos_1d].tile_state == MineSweeperGameScreenTileStateUncleared &&
           !(start_pos.x == curr_pos.x && start_pos.y == curr_pos.y)) {
            result = curr_pos;
            break;
        }

        // If in visited set continue
        if(point_set_cget(set, pos) != NULL) {
            continue;
        }

        // Add point to visited set
        point_set_push(set, pos);

        // Process all surrounding neighbors and add valid to dequeue
        for(uint8_t i = 0; i < 8; i++) {
            int16_t dx = curr_pos.x + (int16_t)offsets[i][0];
            int16_t dy = curr_pos.y + (int16_t)offsets[i][1];

            if(dx < 0 || dy < 0 || dx >= model->board_height || dy >= model->board_width) {
                continue;
            }

            Point neighbor = (Point){.x = dx, .y = dy};
            pointobj_set_point(pos, neighbor);
            point_deq_push_back(deq, pos);
        }
    }

    point_set_clear(set);
    point_deq_clear(deq);

    // Save cursor to new closest tile position
    // If the cursor moves outisde of the model boundaries we need to
    // move the boundary appropriately

    model->curr_pos.x_abs = result.x;
    model->curr_pos.y_abs = result.y;

    bool is_outside_top_boundary = model->curr_pos.x_abs <
                                   (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT);

    bool is_outside_bottom_boundary = model->curr_pos.x_abs >= model->bottom_boundary;

    bool is_outside_left_boundary = model->curr_pos.y_abs <
                                    (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH);

    bool is_outside_right_boundary = model->curr_pos.y_abs >= model->right_boundary;

    if(is_outside_top_boundary) {
        model->bottom_boundary = model->curr_pos.x_abs + MINESWEEPER_SCREEN_TILE_HEIGHT;
    } else if(is_outside_bottom_boundary) {
        model->bottom_boundary = model->curr_pos.x_abs + 1;
    }

    if(is_outside_right_boundary) {
        model->right_boundary = model->curr_pos.y_abs + 1;
    } else if(is_outside_left_boundary) {
        model->right_boundary = model->curr_pos.y_abs + MINESWEEPER_SCREEN_TILE_WIDTH;
    }

    mine_sweeper_play_happy_bump(instance->context);
}

static void mine_sweeper_short_ok_effect(void* context) {
    furi_assert(context);
    MineSweeperGameScreen* instance = context;

    mine_sweeper_led_blink_magenta(instance->context);
    mine_sweeper_play_ok_sound(instance->context);
    mine_sweeper_play_happy_bump(instance->context);
    mine_sweeper_stop_all_sound(instance->context);
}

static void mine_sweeper_long_ok_effect(void* context) {
    furi_assert(context);
    MineSweeperGameScreen* instance = context;

    mine_sweeper_led_blink_magenta(instance->context);
    mine_sweeper_play_ok_sound(instance->context);
    mine_sweeper_play_long_ok_bump(instance->context);
    mine_sweeper_stop_all_sound(instance->context);
}

static void mine_sweeper_flag_effect(void* context) {
    furi_assert(context);
    MineSweeperGameScreen* instance = context;

    mine_sweeper_led_blink_cyan(instance->context);
    mine_sweeper_play_flag_sound(instance->context);
    mine_sweeper_play_happy_bump(instance->context);
    mine_sweeper_stop_all_sound(instance->context);
}

static void mine_sweeper_move_effect(void* context) {
    furi_assert(context);
    MineSweeperGameScreen* instance = context;

    mine_sweeper_play_happy_bump(instance->context);
}

static void mine_sweeper_oob_effect(void* context) {
    furi_assert(context);
    MineSweeperGameScreen* instance = context;

    mine_sweeper_led_blink_red(instance->context);
    mine_sweeper_play_flag_sound(instance->context);
    mine_sweeper_play_oob_bump(instance->context);
    mine_sweeper_stop_all_sound(instance->context);
}

static void mine_sweeper_lose_effect(void* context) {
    furi_assert(context);
    MineSweeperGameScreen* instance = context;

    mine_sweeper_led_set_rgb(instance->context, 255, 0, 000);
    mine_sweeper_play_lose_sound(instance->context);
    mine_sweeper_play_lose_bump(instance->context);
    mine_sweeper_stop_all_sound(instance->context);
}

static void mine_sweeper_win_effect(void* context) {
    furi_assert(context);
    MineSweeperGameScreen* instance = context;

    mine_sweeper_led_set_rgb(instance->context, 0, 0, 255);
    mine_sweeper_play_win_sound(instance->context);
    mine_sweeper_play_win_bump(instance->context);
    mine_sweeper_stop_all_sound(instance->context);
}

static inline bool handle_player_move(
    MineSweeperGameScreen* instance,
    MineSweeperGameScreenModel* model,
    InputEvent* event) {
    bool consumed = false;
    bool is_outside_boundary;

    switch(event->key) {
    case InputKeyUp:
        (model->curr_pos.x_abs - 1 < 0) ? mine_sweeper_oob_effect(instance) :
                                          mine_sweeper_move_effect(instance);

        model->curr_pos.x_abs = (model->curr_pos.x_abs - 1 < 0) ? 0 : model->curr_pos.x_abs - 1;

        is_outside_boundary = model->curr_pos.x_abs <
                              (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT);

        if(is_outside_boundary) {
            model->bottom_boundary--;
        }

        consumed = true;
        break;

    case InputKeyDown:

        (model->curr_pos.x_abs + 1 >= model->board_height) ? mine_sweeper_oob_effect(instance) :
                                                             mine_sweeper_move_effect(instance);

        model->curr_pos.x_abs = (model->curr_pos.x_abs + 1 >= model->board_height) ?
                                    model->board_height - 1 :
                                    model->curr_pos.x_abs + 1;

        is_outside_boundary = model->curr_pos.x_abs >= model->bottom_boundary;

        if(is_outside_boundary) {
            model->bottom_boundary++;
        }

        consumed = true;
        break;

    case InputKeyLeft:
        (model->curr_pos.y_abs - 1 < 0) ? mine_sweeper_oob_effect(instance) :
                                          mine_sweeper_move_effect(instance);

        model->curr_pos.y_abs = (model->curr_pos.y_abs - 1 < 0) ? 0 : model->curr_pos.y_abs - 1;

        is_outside_boundary = model->curr_pos.y_abs <
                              (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH);

        if(is_outside_boundary) {
            model->right_boundary--;
        }

        consumed = true;
        break;

    case InputKeyRight:
        (model->curr_pos.y_abs + 1 >= model->board_width) ? mine_sweeper_oob_effect(instance) :
                                                            mine_sweeper_move_effect(instance);

        model->curr_pos.y_abs = (model->curr_pos.y_abs + 1 >= model->board_width) ?
                                    model->board_width - 1 :
                                    model->curr_pos.y_abs + 1;

        is_outside_boundary = model->curr_pos.y_abs >= model->right_boundary;

        if(is_outside_boundary) {
            model->right_boundary++;
        }

        consumed = true;
        break;

    default:
        consumed = true;
        break;
    }

    return consumed;
}

static int8_t
    handle_short_ok_input(MineSweeperGameScreen* instance, MineSweeperGameScreenModel* model) {
    furi_assert(instance);
    furi_assert(model);

    uint16_t curr_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;
    bool is_win_condition_triggered = false;
    bool is_lose_condition_triggered = false;

    MineSweeperGameScreenTileState state = model->board[curr_pos_1d].tile_state;
    MineSweeperGameScreenTileType type = model->board[curr_pos_1d].tile_type;

    // LOSE/WIN CONDITION OR TILE CLEAR
    if(state == MineSweeperGameScreenTileStateUncleared && type == MineSweeperGameScreenTileMine) {
        is_lose_condition_triggered = true;
        model->board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateCleared;

    } else if(state == MineSweeperGameScreenTileStateUncleared) {
        uint16_t tiles_cleared = bfs_tile_clear(
            model->board,
            model->board_width,
            model->board_height,
            (uint16_t)model->curr_pos.x_abs,
            (uint16_t)model->curr_pos.y_abs);

        model->tiles_left -= tiles_cleared;

        // Check win condition
        if(model->mines_left == 0 && model->flags_left == 0 && model->tiles_left == 0) {
            is_win_condition_triggered = true;
        } else {
            // if not met play ok effect
            mine_sweeper_short_ok_effect(instance);
        }
    }

    if(is_lose_condition_triggered) {
        return -1;
    } else if(is_win_condition_triggered) {
        return 1;
    }

    return 0;
}

static int8_t
    handle_long_ok_input(MineSweeperGameScreen* instance, MineSweeperGameScreenModel* model) {
    furi_assert(instance);
    furi_assert(model);

    uint16_t curr_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;
    bool is_win_condition_triggered = false;
    bool is_lose_condition_triggered = false;

    MineSweeperGameScreenTileType type = model->board[curr_pos_1d].tile_type;

    // Try to clear surrounding tiles if correct number is flagged.
    is_lose_condition_triggered = try_clear_surrounding_tiles(model);
    model->is_holding_down_button = true;

    // Check win condition
    if(model->mines_left == 0 && model->flags_left == 0 && model->tiles_left == 0) {
        is_win_condition_triggered = true;
    }

    // We need to check if it is ok to play this or else we conflict
    // with the lose effect and crash
    if(!is_win_condition_triggered && !is_lose_condition_triggered &&
       type != MineSweeperGameScreenTileZero) {
        mine_sweeper_long_ok_effect(instance);
    }

    if(is_lose_condition_triggered) {
        return -1;
    } else if(is_win_condition_triggered) {
        return 1;
    }

    return 0;
}

static bool handle_long_back_flag_input(
    MineSweeperGameScreen* instance,
    MineSweeperGameScreenModel* model) {
    furi_assert(instance);
    furi_assert(model);

    uint16_t curr_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;
    MineSweeperGameScreenTileState state = model->board[curr_pos_1d].tile_state;

    bool is_win_condition_triggered = false;

    if(state == MineSweeperGameScreenTileStateFlagged) {
        if(model->board[curr_pos_1d].tile_type == MineSweeperGameScreenTileMine)
            model->mines_left++;
        model->board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateUncleared;
        model->flags_left++;

    } else if(model->flags_left > 0) {
        if(model->board[curr_pos_1d].tile_type == MineSweeperGameScreenTileMine)
            model->mines_left--;
        model->board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateFlagged;
        model->flags_left--;
    }

    // WIN CONDITION
    // This can be a win condition where the non-mine tiles are cleared and they place the last flag
    if(model->flags_left == 0 && model->mines_left == 0 && model->tiles_left == 0) {
        is_win_condition_triggered = true;
        mine_sweeper_win_effect(instance);
        mine_sweeper_led_set_rgb(instance->context, 0, 0, 255);
    } else {
        mine_sweeper_flag_effect(instance);
    }

    return is_win_condition_triggered;
}

static void mine_sweeper_game_screen_view_enter(void* context) {
    furi_assert(context);
    UNUSED(context);
}

static void mine_sweeper_game_screen_view_exit(void* context) {
    furi_assert(context);
    UNUSED(context);
}

static void mine_sweeper_game_screen_view_end_draw_callback(Canvas* canvas, void* _model) {
    furi_assert(canvas);
    furi_assert(_model);
    MineSweeperGameScreenModel* model = _model;

    canvas_clear(canvas);

    uint16_t cursor_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;

    for(uint8_t x_rel = 0; x_rel < MINESWEEPER_SCREEN_TILE_HEIGHT; x_rel++) {
        uint16_t x_abs = (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) + x_rel;

        for(uint8_t y_rel = 0; y_rel < MINESWEEPER_SCREEN_TILE_WIDTH; y_rel++) {
            uint16_t y_abs = (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) + y_rel;

            uint16_t curr_rendering_tile_pos_1d = x_abs * model->board_width + y_abs;
            MineSweeperTile tile = model->board[curr_rendering_tile_pos_1d];

            if(cursor_pos_1d == curr_rendering_tile_pos_1d) {
                canvas_set_color(canvas, ColorWhite);
            } else {
                canvas_set_color(canvas, ColorBlack);
            }

            canvas_draw_icon(
                canvas,
                y_rel * icon_get_width(tile.icon_element.icon),
                x_rel * icon_get_height(tile.icon_element.icon),
                tile.icon_element.icon);
        }
    }

    canvas_set_color(canvas, ColorBlack);
    // If any borders are at the limits of the game board we draw a border line

    // Right border
    if(model->right_boundary == model->board_width) {
        canvas_draw_line(canvas, 127, 0, 127, 63 - 8);
    }

    // Left border
    if((model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) == 0) {
        canvas_draw_line(canvas, 0, 0, 0, 63 - 8);
    }

    // Bottom border
    if(model->bottom_boundary == model->board_height) {
        canvas_draw_line(canvas, 0, 63 - 8, 127, 63 - 8);
    }

    // Top border
    if((model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) == 0) {
        canvas_draw_line(canvas, 0, 0, 127, 0);
    }

    const char* end_status_str = "";

    if(model->has_lost_game) {
        end_status_str = "YOU LOSE!  PRESS OK.\0";
    } else {
        end_status_str = "YOU WIN!   PRESS OK.\0";
    }

    // Draw win/lose text
    furi_string_printf(model->info_str, "%s", end_status_str);

    canvas_draw_str_aligned(
        canvas, 0, 64 - 7, AlignLeft, AlignTop, furi_string_get_cstr(model->info_str));

    // Draw time text
    uint32_t ticks_elapsed = furi_get_tick() - model->start_tick;
    uint32_t sec = ticks_elapsed / furi_kernel_get_tick_frequency();
    uint32_t minutes = sec / 60;
    sec = sec % 60;

    furi_string_printf(model->info_str, "%02ld:%02ld", minutes, sec);

    canvas_draw_str_aligned(
        canvas,
        126 - canvas_string_width(canvas, furi_string_get_cstr(model->info_str)),
        64 - 7,
        AlignLeft,
        AlignTop,
        furi_string_get_cstr(model->info_str));
}

static void mine_sweeper_game_screen_view_play_draw_callback(Canvas* canvas, void* _model) {
    furi_assert(canvas);
    furi_assert(_model);

    MineSweeperGameScreenModel* model = _model;

    canvas_clear(canvas);

    uint16_t cursor_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;

    for(uint8_t x_rel = 0; x_rel < MINESWEEPER_SCREEN_TILE_HEIGHT; x_rel++) {
        uint16_t x_abs = (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) + x_rel;

        for(uint8_t y_rel = 0; y_rel < MINESWEEPER_SCREEN_TILE_WIDTH; y_rel++) {
            uint16_t y_abs = (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) + y_rel;

            uint16_t curr_rendering_tile_pos_1d = x_abs * model->board_width + y_abs;
            MineSweeperTile tile = model->board[curr_rendering_tile_pos_1d];

            if(cursor_pos_1d == curr_rendering_tile_pos_1d) {
                canvas_set_color(canvas, ColorWhite);
            } else {
                canvas_set_color(canvas, ColorBlack);
            }

            switch(tile.tile_state) {
            case MineSweeperGameScreenTileStateFlagged:
                canvas_draw_icon(
                    canvas,
                    y_rel * icon_get_width(tile.icon_element.icon),
                    x_rel * icon_get_height(tile.icon_element.icon),
                    tile_icons[11]);

                break;
            case MineSweeperGameScreenTileStateUncleared:
                canvas_draw_icon(
                    canvas,
                    y_rel * icon_get_width(tile.icon_element.icon),
                    x_rel * icon_get_height(tile.icon_element.icon),
                    tile_icons[12]);

                break;
            case MineSweeperGameScreenTileStateCleared:
                canvas_draw_icon(
                    canvas,
                    y_rel * icon_get_width(tile.icon_element.icon),
                    x_rel * icon_get_height(tile.icon_element.icon),
                    tile.icon_element.icon);
                break;
            default:
                break;
            }
        }
    }

    canvas_set_color(canvas, ColorBlack);
    // If any borders are at the limits of the game board we draw a border line

    // Right border
    if(model->right_boundary == model->board_width) {
        canvas_draw_line(canvas, 127, 0, 127, 63 - 8);
    }

    // Left border
    if((model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) == 0) {
        canvas_draw_line(canvas, 0, 0, 0, 63 - 8);
    }

    // Bottom border
    if(model->bottom_boundary == model->board_height) {
        canvas_draw_line(canvas, 0, 63 - 8, 127, 63 - 8);
    }

    // Top border
    if((model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) == 0) {
        canvas_draw_line(canvas, 0, 0, 127, 0);
    }

    // Draw X Position Text
    furi_string_printf(model->info_str, "X:%03hhd", model->curr_pos.x_abs);

    canvas_draw_str_aligned(
        canvas, 0, 64 - 7, AlignLeft, AlignTop, furi_string_get_cstr(model->info_str));

    // Draw Y Position Text
    furi_string_printf(model->info_str, "Y:%03hhd", model->curr_pos.y_abs);

    canvas_draw_str_aligned(
        canvas, 33, 64 - 7, AlignLeft, AlignTop, furi_string_get_cstr(model->info_str));

    // Draw flag text
    furi_string_printf(model->info_str, "F:%03hd", model->flags_left);

    canvas_draw_str_aligned(
        canvas, 66, 64 - 7, AlignLeft, AlignTop, furi_string_get_cstr(model->info_str));

    // Draw time text
    uint32_t ticks_elapsed = furi_get_tick() - model->start_tick;
    uint32_t sec = ticks_elapsed / furi_kernel_get_tick_frequency();
    uint32_t minutes = sec / 60;
    sec = sec % 60;

    furi_string_printf(model->info_str, "%02ld:%02ld", minutes, sec);

    canvas_draw_str_aligned(
        canvas,
        126 - canvas_string_width(canvas, furi_string_get_cstr(model->info_str)),
        64 - 7,
        AlignLeft,
        AlignTop,
        furi_string_get_cstr(model->info_str));
}

static bool mine_sweeper_game_screen_view_end_input_callback(InputEvent* event, void* context) {
    furi_assert(context);
    furi_assert(event);

    MineSweeperGameScreen* instance = context;
    bool consumed = false;

    with_view_model(
        instance->view,
        MineSweeperGameScreenModel * model,
        {
            if(model->is_holding_down_button && event->type == InputTypeRelease) {
                //When we lose we are holding the button down, record this release

                model->is_holding_down_button = false;
                consumed = true;
            }

            if(!model->is_holding_down_button && event->key == InputKeyOk &&
               event->type == InputTypeRelease) {
                // After release when user presses and releases ok we want to restart the next time this function is pressed

                model->is_restart_triggered = true;
                consumed = true;

            } else if(
                !model->is_holding_down_button && model->is_restart_triggered &&
                event->key == InputKeyOk) {
                // After restart flagged is triggered this should also trigger and restart the game

                mine_sweeper_led_reset(instance->context);

                mine_sweeper_game_screen_reset_clock(instance);
                view_set_draw_callback(
                    instance->view, mine_sweeper_game_screen_view_play_draw_callback);
                view_set_input_callback(
                    instance->view, mine_sweeper_game_screen_view_play_input_callback);

                // Here we are going to generate a valid map for the player
                bool is_valid_board = false;

                size_t memsz = sizeof(MineSweeperTile) * MINESWEEPER_BOARD_MAX_TILES;

                do {
                    setup_board(instance);

                    memset(board_t, 0, memsz);
                    memcpy(
                        board_t,
                        model->board,
                        sizeof(MineSweeperTile) * (model->board_width * model->board_height));

                    is_valid_board = check_board_with_verifier(
                        board_t, model->board_width, model->board_height, model->mines_left);

                } while(model->ensure_solvable_board && !is_valid_board);

                consumed = true;

            } else if((event->type == InputTypePress || event->type == InputTypeRepeat)) {
                // Any other input we consider generic player movement

                consumed = handle_player_move(instance, model, event);
            }
        },
        false);

    return consumed;
}

static bool mine_sweeper_game_screen_view_play_input_callback(InputEvent* event, void* context) {
    furi_assert(context);
    furi_assert(event);

    MineSweeperGameScreen* instance = context;
    bool consumed = false;

    with_view_model(
        instance->view,
        MineSweeperGameScreenModel * model,
        {
            // Checking button types

            if(event->type == InputTypeRelease) {
                model->is_holding_down_button = false;
                consumed = true;

            } else if(event->key == InputKeyOk) { // Attempt to Clear Space !! THIS CAN BE A LOSE CONDITION

                bool is_lose_condition_triggered = false;
                bool is_win_condition_triggered = false;

                if(!model->is_holding_down_button && event->type == InputTypePress) {
                    //ret : -1 lose condition, 1 win condition, 0 neutral
                    int8_t ret = handle_short_ok_input(instance, model);

                    if(ret != 0) {
                        (ret == -1) ? (is_lose_condition_triggered = true) :
                                      (is_win_condition_triggered = true);
                    }

                    // LOSE/WIN CONDITION OR CLEAR SURROUNDING
                } else if(!model->is_holding_down_button && event->type == InputTypeLong) {
                    //ret : -1 lose condition, 1 win condition, 0 neutral
                    int8_t ret = handle_long_ok_input(instance, model);

                    if(ret != 0) {
                        (ret == -1) ? (is_lose_condition_triggered = true) :
                                      (is_win_condition_triggered = true);
                    }
                }

                // Check  if win or lose condition was triggered on OK press
                if(is_lose_condition_triggered) {
                    model->has_lost_game = true;
                    mine_sweeper_lose_effect(instance);

                    view_set_draw_callback(
                        instance->view, mine_sweeper_game_screen_view_end_draw_callback);
                    view_set_input_callback(
                        instance->view, mine_sweeper_game_screen_view_end_input_callback);

                } else if(is_win_condition_triggered) {
                    mine_sweeper_win_effect(instance);

                    view_set_draw_callback(
                        instance->view, mine_sweeper_game_screen_view_end_draw_callback);
                    view_set_input_callback(
                        instance->view, mine_sweeper_game_screen_view_end_input_callback);
                }

                consumed = true;

            } else if(event->key == InputKeyBack) { // We can use holding the back button for either
                // Setting a flag on a covered tile, or moving to
                // the next closest covered tile on when on a uncovered
                // tile

                if(event->type == InputTypeLong ||
                   event->type == InputTypeRepeat) { // Only process longer back keys;
                    // short presses should take
                    // us to the menu

                    bool is_win_condition_triggered = false;

                    uint16_t curr_pos_1d =
                        model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;
                    MineSweeperGameScreenTileState state = model->board[curr_pos_1d].tile_state;

                    if(state == MineSweeperGameScreenTileStateCleared) {
                        // BFS to closest uncovered position
                        bfs_to_closest_tile(instance, model);

                        model->is_holding_down_button = true;

                        // Flag or Unflag tile and check win condition
                    } else if(
                        !model->is_holding_down_button &&
                        state != MineSweeperGameScreenTileStateCleared) {
                        is_win_condition_triggered = handle_long_back_flag_input(instance, model);

                        model->is_holding_down_button = true;

                        if(is_win_condition_triggered) {
                            view_set_draw_callback(
                                instance->view, mine_sweeper_game_screen_view_end_draw_callback);
                            view_set_input_callback(
                                instance->view, mine_sweeper_game_screen_view_end_input_callback);
                        }
                    }

                    consumed = true;
                }
            } else if(
                event->type == InputTypePress ||
                event->type == InputTypeRepeat) { // Finally handle move

                consumed = handle_player_move(instance, model, event);
            }
        },
        true);

    if(!consumed && instance->input_callback != NULL) {
        consumed = instance->input_callback(event, instance->context);
    }

    return consumed;
}

MineSweeperGameScreen* mine_sweeper_game_screen_alloc(
    uint8_t width,
    uint8_t height,
    uint8_t difficulty,
    bool ensure_solvable) {
    MineSweeperGameScreen* mine_sweeper_game_screen =
        (MineSweeperGameScreen*)malloc(sizeof(MineSweeperGameScreen));

    mine_sweeper_game_screen->view = view_alloc();

    view_set_context(mine_sweeper_game_screen->view, mine_sweeper_game_screen);
    view_allocate_model(
        mine_sweeper_game_screen->view, ViewModelTypeLocking, sizeof(MineSweeperGameScreenModel));

    view_set_draw_callback(
        mine_sweeper_game_screen->view, mine_sweeper_game_screen_view_play_draw_callback);
    view_set_input_callback(
        mine_sweeper_game_screen->view, mine_sweeper_game_screen_view_play_input_callback);

    // This are currently unused
    view_set_enter_callback(mine_sweeper_game_screen->view, mine_sweeper_game_screen_view_enter);
    view_set_exit_callback(mine_sweeper_game_screen->view, mine_sweeper_game_screen_view_exit);

    // Not being used
    mine_sweeper_game_screen->input_callback = NULL;

    // Allocate strings in model
    with_view_model(
        mine_sweeper_game_screen->view,
        MineSweeperGameScreenModel * model,
        {
            model->info_str = furi_string_alloc();
            model->is_holding_down_button = false;
        },
        true);

    // Reset the clock - This will set the start time at the allocation of the game screen
    // but this is a public api as well and can be called in a scene for more accurate start times
    mine_sweeper_game_screen_reset_clock(mine_sweeper_game_screen);

    // We need to initize board width and height before setup
    mine_sweeper_game_screen_set_board_information(
        mine_sweeper_game_screen, width, height, difficulty, ensure_solvable);

    // Here we are going to generate a valid map for the player
    bool is_valid_board = false;
    size_t memsz = sizeof(MineSweeperTile) * MINESWEEPER_BOARD_MAX_TILES;

    do {
        setup_board(mine_sweeper_game_screen);

        uint16_t num_mines = 1;

        uint16_t board_width = 16; //default values
        uint16_t board_height = 7; //default values

        with_view_model(
            mine_sweeper_game_screen->view,
            MineSweeperGameScreenModel * model,
            {
                num_mines = model->mines_left;
                board_width = model->board_width;
                board_height = model->board_height;

                memset(board_t, 0, memsz);
                memcpy(
                    board_t, model->board, sizeof(MineSweeperTile) * (board_width * board_height));
            },
            true);

        is_valid_board = check_board_with_verifier(board_t, board_width, board_height, num_mines);

    } while(ensure_solvable && !is_valid_board);

    return mine_sweeper_game_screen;
}

void mine_sweeper_game_screen_free(MineSweeperGameScreen* instance) {
    furi_assert(instance);

    // Dealloc strings in model
    with_view_model(
        instance->view,
        MineSweeperGameScreenModel * model,
        { furi_string_free(model->info_str); },
        false);

    // Free view and any dynamically allocated members in main struct
    view_free(instance->view);
    free(instance);
}

// This function should be called whenever you want to reset the game state
// This should NOT be called in the on_exit in the game scene
void mine_sweeper_game_screen_reset(
    MineSweeperGameScreen* instance,
    uint8_t width,
    uint8_t height,
    uint8_t difficulty,
    bool ensure_solvable) {
    furi_assert(instance);

    instance->input_callback = NULL;

    // Reset led
    mine_sweeper_led_reset(instance->context);

    // We need to initize board width and height before setup
    mine_sweeper_game_screen_set_board_information(
        instance, width, height, difficulty, ensure_solvable);

    mine_sweeper_game_screen_reset_clock(instance);

    // Here we are going to generate a valid map for the player
    bool is_valid_board = false;
    size_t memsz = sizeof(MineSweeperTile) * MINESWEEPER_BOARD_MAX_TILES;

    do {
        setup_board(instance);

        uint16_t num_mines = 1;

        uint16_t board_width = 16; //default values
        uint16_t board_height = 7; //default values

        with_view_model(
            instance->view,
            MineSweeperGameScreenModel * model,
            {
                num_mines = model->mines_left;
                board_width = model->board_width;
                board_height = model->board_height;

                memset(board_t, 0, memsz);
                memcpy(
                    board_t, model->board, sizeof(MineSweeperTile) * (board_width * board_height));
            },
            true);

        is_valid_board = check_board_with_verifier(board_t, board_width, board_height, num_mines);

    } while(ensure_solvable && !is_valid_board);
}

// This function should be called when you want to reset the game clock
// Already called in reset and alloc function for game, but can be called from
// other scenes that need it like a start scene that plays after alloc
void mine_sweeper_game_screen_reset_clock(MineSweeperGameScreen* instance) {
    furi_assert(instance);

    with_view_model(
        instance->view,
        MineSweeperGameScreenModel * model,
        { model->start_tick = furi_get_tick(); },
        true);
}

View* mine_sweeper_game_screen_get_view(MineSweeperGameScreen* instance) {
    furi_assert(instance);
    return instance->view;
}

void mine_sweeper_game_screen_set_context(MineSweeperGameScreen* instance, void* context) {
    furi_assert(instance);
    instance->context = context;
}
