#include <furi.h>
#include <gui/gui.h>
#include <input/input.h>
#include <stdlib.h>
#include <string.h>
#include <lib/toolbox/args.h>
#include <lib/subghz/receiver.h>
#include <lib/subghz/subghz_keystore.h>
#include <lib/subghz/protocols/base.h>
#include <lib/subghz/protocols/raw.h>
#include <lib/subghz/protocols/princeton.h>
#include <lib/subghz/subghz_tx_rx_worker.h>
#include <tanks_icons.h>
#include "helpers/radio_device_loader.h"

#include "constants.h"

typedef struct {
    //    +-----x
    //    |
    //    |
    //    y
    uint8_t x;
    uint8_t y;
} Point;

typedef enum {
    CellEmpty = 1,
    CellWall,
    CellExplosion,
    CellTankUp,
    CellTankRight,
    CellTankDown,
    CellTankLeft,
    CellEnemyUp,
    CellEnemyRight,
    CellEnemyDown,
    CellEnemyLeft,
    CellProjectileUp,
    CellProjectileRight,
    CellProjectileDown,
    CellProjectileLeft,
} GameCellState;

typedef enum {
    MenuStateSingleMode,
    MenuStateCooperativeServerMode,
    MenuStateCooperativeClientMode,
} MenuState;

typedef enum {
    GameStateMenu,
    GameStateSingle,
    GameStateCooperativeServer,
    GameStateCooperativeClient,
    GameStateGameOver,
} GameState;

typedef enum {
    DirectionUp,
    DirectionRight,
    DirectionDown,
    DirectionLeft,
} Direction;

typedef enum {
    ModeSingle,
    ModeCooperative,
} Mode;

typedef struct {
    Point coordinates;
    Direction direction;
    bool explosion;
    bool is_p1;
    bool is_p2;
} ProjectileState;

typedef struct {
    Point coordinates;
    uint16_t score;
    uint8_t lives;
    Direction direction;
    bool moving;
    bool shooting;
    bool live;
    uint8_t cooldown;
    uint8_t respawn_cooldown;
} PlayerState;

typedef struct {
    // char map[FIELD_WIDTH][FIELD_HEIGHT];
    char thisMap[16][11];
    Point team_one_respawn_points[3];
    Point team_two_respawn_points[3];
    Mode mode;
    bool server;
    GameState state;
    MenuState menu_state;
    ProjectileState* projectiles[100];
    PlayerState* bots[6];
    uint8_t enemies_left;
    uint8_t enemies_live;
    uint8_t enemies_respawn_cooldown;
    uint8_t received;
    uint8_t sent;
    PlayerState* p1;
    PlayerState* p2;
    FuriMutex* mutex;
} TanksState;

typedef enum {
    EventTypeTick,
    EventTypeKey,
} EventType;

typedef struct {
    EventType type;
    InputEvent input;
} TanksEvent;

typedef enum {
    GoesUp,
    GoesRight,
    GoesDown,
    GoesLeft,
    Shoots,
} ClientAction;

//char map[FIELD_HEIGHT][FIELD_WIDTH + 1] = {
char thisMap[11][16 + 1] = {
    "*       -  *   -",
    "  -  -  =       ",
    "        -  -   2",
    "1    =     - -- ",
    "--   =     - -- ",
    "a-1  =  -  =   2",
    "--   =     - -- ",
    "1    =     - -- ",
    "        -  -   2",
    "  -  -  =       ",
    "*       -  *   -",
};

static void tanks_game_write_cell(unsigned char* data, int8_t x, int8_t y, GameCellState cell) {
    uint8_t index = y * 16 + x;
    data[index] = cell;
    //    if (x % 2) {
    //        data[index] = (data[index] & 0b00001111) + (cell << 4);
    //    } else {
    //        data[index] = (data[index] & 0b0000) + cell;
    //    }
}

// Enum with < 16 items => 4 bits in cell, 2 cells in byte
unsigned char* tanks_game_serialize(const TanksState* const tanks_state) {
    static unsigned char result[11 * 16 + 1];

    for(int8_t x = 0; x < FIELD_WIDTH; x++) {
        for(int8_t y = 0; y < FIELD_HEIGHT; y++) {
            result[(y * FIELD_WIDTH + x)] = 0;

            GameCellState cell = CellEmpty;

            if(tanks_state->thisMap[x][y] == '-') {
                cell = CellWall;

                tanks_game_write_cell(result, x, y, cell);
            }
        }
    }

    for(uint8_t i = 0; i < 6; i++) {
        if(tanks_state->bots[i] != NULL) {
            GameCellState cell = CellEmpty;

            switch(tanks_state->bots[i]->direction) {
            case DirectionUp:
                cell = CellEnemyUp;
                break;
            case DirectionDown:
                cell = CellEnemyDown;
                break;
            case DirectionRight:
                cell = CellEnemyRight;
                break;
            case DirectionLeft:
                cell = CellEnemyLeft;
                break;
            default:
                break;
            }

            tanks_game_write_cell(
                result,
                tanks_state->bots[i]->coordinates.x,
                tanks_state->bots[i]->coordinates.y,
                cell);
        }
    }

    for(int8_t x = 0; x < 100; x++) {
        if(tanks_state->projectiles[x] != NULL) {
            GameCellState cell = CellEmpty;

            switch(tanks_state->projectiles[x]->direction) {
            case DirectionUp:
                cell = CellProjectileUp;
                break;
            case DirectionDown:
                cell = CellProjectileDown;
                break;
            case DirectionRight:
                cell = CellProjectileRight;
                break;
            case DirectionLeft:
                cell = CellProjectileLeft;
                break;
            default:
                break;
            }

            tanks_game_write_cell(
                result,
                tanks_state->projectiles[x]->coordinates.x,
                tanks_state->projectiles[x]->coordinates.y,
                cell);
        }
    }

    if(tanks_state->p1 != NULL && tanks_state->p1->live) {
        GameCellState cell = CellEmpty;

        switch(tanks_state->p1->direction) {
        case DirectionUp:
            cell = CellTankUp;
            break;
        case DirectionDown:
            cell = CellTankDown;
            break;
        case DirectionRight:
            cell = CellTankRight;
            break;
        case DirectionLeft:
            cell = CellTankLeft;
            break;
        default:
            break;
        }

        tanks_game_write_cell(
            result, tanks_state->p1->coordinates.x, tanks_state->p1->coordinates.y, cell);
    }

    if(tanks_state->p2 != NULL && tanks_state->p2->live) {
        GameCellState cell = CellEmpty;

        switch(tanks_state->p2->direction) {
        case DirectionUp:
            cell = CellTankUp;
            break;
        case DirectionDown:
            cell = CellTankDown;
            break;
        case DirectionRight:
            cell = CellTankRight;
            break;
        case DirectionLeft:
            cell = CellTankLeft;
            break;
        default:
            break;
        }

        tanks_game_write_cell(
            result, tanks_state->p2->coordinates.x, tanks_state->p2->coordinates.y, cell);
    }

    return result;
}

static void
    tanks_game_render_cell(GameCellState cell, uint8_t x, uint8_t y, Canvas* const canvas) {
    const Icon* icon;

    if(cell == CellEmpty) {
        return;
    }

    switch(cell) {
    case CellWall:
        icon = &I_tank_wall;
        break;
    case CellExplosion:
        icon = &I_tank_explosion;
        break;
    case CellTankUp:
        icon = &I_tank_up;
        break;
    case CellTankRight:
        icon = &I_tank_right;
        break;
    case CellTankDown:
        icon = &I_tank_down;
        break;
    case CellTankLeft:
        icon = &I_tank_left;
        break;
    case CellEnemyUp:
        icon = &I_enemy_up;
        break;
    case CellEnemyRight:
        icon = &I_enemy_right;
        break;
    case CellEnemyDown:
        icon = &I_enemy_down;
        break;
    case CellEnemyLeft:
        icon = &I_enemy_left;
        break;
    case CellProjectileUp:
        icon = &I_projectile_up;
        break;
    case CellProjectileRight:
        icon = &I_projectile_right;
        break;
    case CellProjectileDown:
        icon = &I_projectile_down;
        break;
    case CellProjectileLeft:
        icon = &I_projectile_left;
        break;
    default:
        return;
        break;
    }

    canvas_draw_icon(canvas, x * CELL_LENGTH_PIXELS, y * CELL_LENGTH_PIXELS - 1, icon);
}

static void tanks_game_render_constant_cells(Canvas* const canvas) {
    for(int8_t x = 0; x < FIELD_WIDTH; x++) {
        for(int8_t y = 0; y < FIELD_HEIGHT; y++) {
            char cell = thisMap[y][x];

            if(cell == '=') {
                canvas_draw_icon(
                    canvas, x * CELL_LENGTH_PIXELS, y * CELL_LENGTH_PIXELS - 1, &I_tank_stone);
                continue;
            }

            if(cell == '*') {
                canvas_draw_icon(
                    canvas, x * CELL_LENGTH_PIXELS, y * CELL_LENGTH_PIXELS - 1, &I_tank_hedgehog);
                continue;
            }

            if(cell == 'a') {
                canvas_draw_icon(
                    canvas, x * CELL_LENGTH_PIXELS, y * CELL_LENGTH_PIXELS - 1, &I_tank_base);
                continue;
            }
        }
    }
}

void tanks_game_deserialize_and_write_to_state(unsigned char* data, TanksState* const tanks_state) {
    for(uint8_t i = 0; i < 11 * 16; i++) {
        uint8_t x = i % 16;
        uint8_t y = i / 16;
        tanks_state->thisMap[x][y] = data[i];
    }
}

void tanks_game_deserialize_and_render(unsigned char* data, Canvas* const canvas) {
    //for (uint8_t i = 0; i < 11 * 16 / 2; i++) {
    for(uint8_t i = 0; i < 11 * 16; i++) {
        char cell = data[i];
        uint8_t x = i % 16; // One line (16 cells) = 8 bytes
        uint8_t y = i / 16;

        //        GameCellState first = cell >> 4;
        //        GameCellState second = cell & 0b00001111;

        tanks_game_render_cell(cell, x, y, canvas);
        //        tanks_game_render_cell(second, x + 1, y, canvas);
    }

    tanks_game_render_constant_cells(canvas);
}

static void tanks_game_render_callback(Canvas* const canvas, void* ctx) {
    furi_assert(ctx);
    const TanksState* tanks_state = ctx;
    furi_mutex_acquire(tanks_state->mutex, FuriWaitForever);

    // Before the function is called, the state is set with the canvas_reset(canvas)
    if(tanks_state->state == GameStateMenu) {
        canvas_draw_icon(canvas, 0, 0, &I_TanksSplashScreen_128x64);
        canvas_set_font(canvas, FontPrimary);
        canvas_draw_str_aligned(canvas, 124, 10, AlignRight, AlignBottom, "Single");
        canvas_draw_str_aligned(canvas, 124, 25, AlignRight, AlignBottom, "Co-op S");
        canvas_draw_str_aligned(canvas, 124, 40, AlignRight, AlignBottom, "Co-op C");

        switch(tanks_state->menu_state) {
        case MenuStateSingleMode:
            canvas_draw_icon(canvas, 74, 3, &I_tank_right);
            break;
        case MenuStateCooperativeServerMode:
            canvas_draw_icon(canvas, 74, 18, &I_tank_right);
            break;
        case MenuStateCooperativeClientMode:
            canvas_draw_icon(canvas, 74, 33, &I_tank_right);
            break;
        }

        canvas_draw_frame(canvas, 0, 0, 128, 64);

        furi_mutex_release(tanks_state->mutex);
        return;
    }

    // Field right border
    canvas_draw_box(canvas, FIELD_WIDTH * CELL_LENGTH_PIXELS, 0, 2, SCREEN_HEIGHT_TANKS);

    // Cooperative client
    if(tanks_state->mode == ModeCooperative && !tanks_state->server) {
        for(int8_t x = 0; x < FIELD_WIDTH; x++) {
            for(int8_t y = 0; y < FIELD_HEIGHT; y++) {
                tanks_game_render_cell(tanks_state->thisMap[x][y], x, y, canvas);
            }
        }

        tanks_game_render_constant_cells(canvas);

        furi_mutex_release(tanks_state->mutex);
        return;
    }

    // Player
    //    Point coordinates = tanks_state->p1->coordinates;
    //    const Icon *icon;
    //    switch (tanks_state->p1->direction) {
    //    case DirectionUp:
    //        icon = &I_tank_up;
    //        break;
    //    case DirectionDown:
    //        icon = &I_tank_down;
    //        break;
    //    case DirectionRight:
    //        icon = &I_tank_right;
    //        break;
    //    case DirectionLeft:
    //        icon = &I_tank_left;
    //        break;
    //    default:
    //        icon = &I_tank_explosion;
    //    }

    //    if (tanks_state->p1->live) {
    //        canvas_draw_icon(canvas, coordinates.x * CELL_LENGTH_PIXELS, coordinates.y * CELL_LENGTH_PIXELS - 1, icon);
    //    }
    //
    //    for(int8_t x = 0; x < FIELD_WIDTH; x++) {
    //        for(int8_t y = 0; y < FIELD_HEIGHT; y++) {
    //            switch (tanks_state->thisMap[x][y]) {
    //            case '-':
    //                canvas_draw_icon(canvas, x * CELL_LENGTH_PIXELS, y * CELL_LENGTH_PIXELS - 1, &I_tank_wall);
    //                break;
    //
    //            case '=':
    //                canvas_draw_icon(canvas, x * CELL_LENGTH_PIXELS, y * CELL_LENGTH_PIXELS - 1, &I_tank_stone);
    //                break;
    //
    //            case '*':
    //                canvas_draw_icon(canvas, x * CELL_LENGTH_PIXELS, y * CELL_LENGTH_PIXELS - 1, &I_tank_hedgehog);
    //                break;
    //
    //            case 'a':
    //                canvas_draw_icon(canvas, x * CELL_LENGTH_PIXELS, y * CELL_LENGTH_PIXELS - 1, &I_tank_base);
    //                break;
    //            }
    //        }
    //    }

    //    for (
    //        uint8_t i = 0;
    //        i < 6;
    //        i++
    //    ) {
    //        if (tanks_state->bots[i] != NULL) {
    //            const Icon *icon;
    //
    //            switch(tanks_state->bots[i]->direction) {
    //            case DirectionUp:
    //                icon = &I_enemy_up;
    //                break;
    //            case DirectionDown:
    //                icon = &I_enemy_down;
    //                break;
    //            case DirectionRight:
    //                icon = &I_enemy_right;
    //                break;
    //            case DirectionLeft:
    //                icon = &I_enemy_left;
    //                break;
    //            default:
    //                icon = &I_tank_explosion;
    //            }
    //
    //            canvas_draw_icon(
    //                canvas,
    //                tanks_state->bots[i]->coordinates.x * CELL_LENGTH_PIXELS,
    //                tanks_state->bots[i]->coordinates.y * CELL_LENGTH_PIXELS - 1,
    //                icon);
    //        }
    //    }

    //    for(int8_t x = 0; x < 100; x++) {
    //        if (tanks_state->projectiles[x] != NULL) {
    //            ProjectileState *projectile = tanks_state->projectiles[x];
    //
    //            if (projectile->explosion) {
    //                canvas_draw_icon(
    //                    canvas,
    //                    projectile->coordinates.x * CELL_LENGTH_PIXELS,
    //                    projectile->coordinates.y * CELL_LENGTH_PIXELS - 1,
    //                    &I_tank_explosion);
    //                continue;
    //            }
    //
    //            const Icon *icon;
    //
    //            switch(projectile->direction) {
    //            case DirectionUp:
    //                icon = &I_projectile_up;
    //                break;
    //            case DirectionDown:
    //                icon = &I_projectile_down;
    //                break;
    //            case DirectionRight:
    //                icon = &I_projectile_right;
    //                break;
    //            case DirectionLeft:
    //                icon = &I_projectile_left;
    //                break;
    //            default:
    //                icon = &I_tank_explosion;
    //            }
    //
    //            canvas_draw_icon(
    //                canvas,
    //                projectile->coordinates.x * CELL_LENGTH_PIXELS,
    //                projectile->coordinates.y * CELL_LENGTH_PIXELS - 1,
    //                icon);
    //        }
    //    }

    // Info
    canvas_set_color(canvas, ColorBlack);
    canvas_set_font(canvas, FontSecondary);
    char buffer1[13];
    snprintf(buffer1, sizeof(buffer1), "live: %u", tanks_state->enemies_live);
    canvas_draw_str_aligned(canvas, 127, 8, AlignRight, AlignBottom, buffer1);

    snprintf(buffer1, sizeof(buffer1), "left: %u", tanks_state->enemies_left);
    canvas_draw_str_aligned(canvas, 127, 18, AlignRight, AlignBottom, buffer1);

    snprintf(buffer1, sizeof(buffer1), "p1 l: %u", tanks_state->p1->lives);
    canvas_draw_str_aligned(canvas, 127, 28, AlignRight, AlignBottom, buffer1);

    snprintf(buffer1, sizeof(buffer1), "p1 s: %u", tanks_state->p1->score);
    canvas_draw_str_aligned(canvas, 127, 38, AlignRight, AlignBottom, buffer1);

    if(tanks_state->state == GameStateCooperativeServer && tanks_state->p2) {
        snprintf(buffer1, sizeof(buffer1), "rec: %u", tanks_state->received);
        canvas_draw_str_aligned(canvas, 127, 48, AlignRight, AlignBottom, buffer1);

        snprintf(buffer1, sizeof(buffer1), "snt: %u", tanks_state->sent);
        canvas_draw_str_aligned(canvas, 127, 58, AlignRight, AlignBottom, buffer1);
        //        snprintf(buffer1, sizeof(buffer1), "p2 l: %u", tanks_state->p2->lives);
        //        canvas_draw_str_aligned(canvas, 127, 48, AlignRight, AlignBottom, buffer1);
        //
        //        snprintf(buffer1, sizeof(buffer1), "p2 s: %u", tanks_state->p2->score);
        //        canvas_draw_str_aligned(canvas, 127, 58, AlignRight, AlignBottom, buffer1);
    }

    if(tanks_state->state == GameStateCooperativeClient) {
        snprintf(buffer1, sizeof(buffer1), "rec: %u", tanks_state->received);
        canvas_draw_str_aligned(canvas, 127, 48, AlignRight, AlignBottom, buffer1);
    }

    // Game Over banner
    if(tanks_state->state == GameStateGameOver) {
        canvas_set_color(canvas, ColorWhite);
        canvas_draw_box(canvas, 34, 20, 62, 24);

        canvas_set_color(canvas, ColorBlack);
        canvas_draw_frame(canvas, 34, 20, 62, 24);
        canvas_set_font(canvas, FontPrimary);

        if(tanks_state->enemies_left == 0 && tanks_state->enemies_live == 0) {
            canvas_draw_str(canvas, 37, 31, "You win!");
        } else {
            canvas_draw_str(canvas, 37, 31, "Game Over");
        }

        canvas_set_font(canvas, FontSecondary);
        char buffer[13];
        snprintf(buffer, sizeof(buffer), "Score: %u", tanks_state->p1->score);
        canvas_draw_str_aligned(canvas, 64, 41, AlignCenter, AlignBottom, buffer);
    }

    // TEST start
    unsigned char* data = tanks_game_serialize(tanks_state);
    tanks_game_deserialize_and_render(data, canvas);
    // TEST enf

    furi_mutex_release(tanks_state->mutex);
}

static void tanks_game_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
    furi_assert(event_queue);

    TanksEvent event = {.type = EventTypeKey, .input = *input_event};
    furi_message_queue_put(event_queue, &event, FuriWaitForever);
}

static void tanks_game_update_timer_callback(FuriMessageQueue* event_queue) {
    furi_assert(event_queue);

    TanksEvent event = {.type = EventTypeTick};
    furi_message_queue_put(event_queue, &event, 0);
}

static bool tanks_get_cell_is_free(TanksState* const tanks_state, Point point) {
    // Tiles
    if(tanks_state->thisMap[point.x][point.y] != ' ') {
        return false;
    }

    // Projectiles
    for(int8_t x = 0; x < 100; x++) {
        if(tanks_state->projectiles[x] != NULL) {
            if(tanks_state->projectiles[x]->coordinates.x == point.x &&
               tanks_state->projectiles[x]->coordinates.y == point.y) {
                return false;
            }
        }
    }

    // Player 1
    if(tanks_state->p1 != NULL) {
        if(tanks_state->p1->coordinates.x == point.x &&
           tanks_state->p1->coordinates.y == point.y) {
            return false;
        }
    }

    // Player 2
    if(tanks_state->p2 != NULL) {
        if(tanks_state->p2->coordinates.x == point.x &&
           tanks_state->p2->coordinates.y == point.y) {
            return false;
        }
    }

    // Bots
    for(int8_t x = 0; x < 6; x++) {
        if(tanks_state->bots[x] != NULL) {
            if(tanks_state->bots[x]->coordinates.x == point.x &&
               tanks_state->bots[x]->coordinates.y == point.y) {
                return false;
            }
        }
    }

    return true;
}

static uint8_t tanks_get_random_free_respawn_point_index(
    TanksState* const tanks_state,
    Point respawn_points[3]) {
    uint8_t first = rand() % 3;
    int8_t add = rand() % 2 ? +1 : -1;
    int8_t second = first + add;
    uint8_t third;

    if(second == 4) {
        second = 0;
    } else if(second == -1) {
        second = 3;
    }

    for(uint8_t i = 0; i < 3; i++) {
        if(i != first && i != second) {
            third = i;
        }
    }

    if(tanks_get_cell_is_free(tanks_state, respawn_points[first])) {
        return first;
    }

    if(tanks_get_cell_is_free(tanks_state, respawn_points[second])) {
        return second;
    }

    if(tanks_get_cell_is_free(tanks_state, respawn_points[third])) {
        return third;
    }

    return -1;
}

static void tanks_game_init_game(TanksState* const tanks_state, GameState type) {
    srand(DWT->CYCCNT);

    tanks_state->state = type;

    for(int8_t x = 0; x < 100; x++) {
        if(tanks_state->projectiles[x] != NULL) {
            free(tanks_state->projectiles[x]);
            tanks_state->projectiles[x] = NULL;
        }
    }

    int8_t team_one_respawn_points_counter = 0;
    int8_t team_two_respawn_points_counter = 0;

    for(int8_t x = 0; x < FIELD_WIDTH; x++) {
        for(int8_t y = 0; y < FIELD_HEIGHT; y++) {
            tanks_state->thisMap[x][y] = ' ';

            if(thisMap[y][x] == '1') {
                Point respawn = {x, y};
                tanks_state->team_one_respawn_points[team_one_respawn_points_counter++] = respawn;
            }

            if(thisMap[y][x] == '2') {
                Point respawn = {x, y};
                tanks_state->team_two_respawn_points[team_two_respawn_points_counter++] = respawn;
            }

            if(thisMap[y][x] == '-') {
                tanks_state->thisMap[x][y] = '-';
            }

            if(thisMap[y][x] == '=') {
                tanks_state->thisMap[x][y] = '=';
            }

            if(thisMap[y][x] == '*') {
                tanks_state->thisMap[x][y] = '*';
            }

            if(thisMap[y][x] == 'a') {
                tanks_state->thisMap[x][y] = 'a';
            }
        }
    }

    uint8_t index1 = tanks_get_random_free_respawn_point_index(
        tanks_state, tanks_state->team_one_respawn_points);
    Point c = {
        tanks_state->team_one_respawn_points[index1].x,
        tanks_state->team_one_respawn_points[index1].y};

    PlayerState p1 = {
        c,
        0,
        4,
        DirectionRight,
        0,
        0,
        1,
        SHOT_COOLDOWN,
        PLAYER_RESPAWN_COOLDOWN,
    };

    PlayerState* p1_state = malloc(sizeof(PlayerState));
    *p1_state = p1;

    tanks_state->p1 = p1_state;

    if(type == GameStateCooperativeServer) {
        int8_t index2 = tanks_get_random_free_respawn_point_index(
            tanks_state, tanks_state->team_one_respawn_points);
        Point c = {
            tanks_state->team_one_respawn_points[index2].x,
            tanks_state->team_one_respawn_points[index2].y};

        PlayerState p2 = {
            c,
            0,
            4,
            DirectionRight,
            0,
            0,
            1,
            SHOT_COOLDOWN,
            PLAYER_RESPAWN_COOLDOWN,
        };

        PlayerState* p2_state = malloc(sizeof(PlayerState));
        *p2_state = p2;

        tanks_state->p2 = p2_state;
    }

    tanks_state->enemies_left = 5;
    tanks_state->enemies_live = 0;
    tanks_state->enemies_respawn_cooldown = RESPAWN_COOLDOWN;
    tanks_state->received = 0;
    tanks_state->sent = 0;

    if(type == GameStateCooperativeClient) {
        for(int8_t x = 0; x < FIELD_WIDTH; x++) {
            for(int8_t y = 0; y < FIELD_HEIGHT; y++) {
                tanks_state->thisMap[x][y] = CellEmpty;
            }
        }
    }
}

static bool
    tanks_game_collision(Point const next_step, bool shoot, TanksState const* const tanks_state) {
    if((int8_t)next_step.x < 0 || (int8_t)next_step.y < 0) {
        return true;
    }

    if(next_step.x >= FIELD_WIDTH || next_step.y >= FIELD_HEIGHT) {
        return true;
    }

    char tile = tanks_state->thisMap[next_step.x][next_step.y];

    if(tile == '*' && !shoot) {
        return true;
    }

    if(tile == '-' || tile == '=' || tile == 'a') {
        return true;
    }

    for(uint8_t i = 0; i < 6; i++) {
        if(tanks_state->bots[i] != NULL) {
            if(tanks_state->bots[i]->coordinates.x == next_step.x &&
               tanks_state->bots[i]->coordinates.y == next_step.y) {
                return true;
            }
        }
    }

    if(tanks_state->p1 != NULL && tanks_state->p1->live &&
       tanks_state->p1->coordinates.x == next_step.x &&
       tanks_state->p1->coordinates.y == next_step.y) {
        return true;
    }

    if(tanks_state->p2 != NULL && tanks_state->p2->live &&
       tanks_state->p2->coordinates.x == next_step.x &&
       tanks_state->p2->coordinates.y == next_step.y) {
        return true;
    }

    return false;
}

static Point tanks_game_get_next_step(Point coordinates, Direction direction) {
    Point next_step = {coordinates.x, coordinates.y};

    switch(direction) {
    // +-----x
    // |
    // |
    // y
    case DirectionUp:
        next_step.y--;
        break;
    case DirectionRight:
        next_step.x++;
        break;
    case DirectionDown:
        next_step.y++;
        break;
    case DirectionLeft:
        next_step.x--;
        break;
    default:
        break;
    }
    return next_step;
}

static uint8_t tanks_game_get_free_projectile_index(TanksState* const tanks_state) {
    uint8_t freeProjectileIndex;
    for(freeProjectileIndex = 0; freeProjectileIndex < 100; freeProjectileIndex++) {
        if(tanks_state->projectiles[freeProjectileIndex] == NULL) {
            return freeProjectileIndex;
        }
    }

    return 0;
}

static void tanks_game_shoot(
    TanksState* const tanks_state,
    PlayerState* tank_state,
    bool is_p1,
    bool is_p2) {
    tank_state->cooldown = SHOT_COOLDOWN;

    uint8_t freeProjectileIndex = tanks_game_get_free_projectile_index(tanks_state);

    ProjectileState* projectile_state = malloc(sizeof(ProjectileState));
    Point next_step = tanks_game_get_next_step(tank_state->coordinates, tank_state->direction);

    projectile_state->direction = tank_state->direction;
    projectile_state->coordinates = next_step;
    projectile_state->is_p1 = is_p1;
    projectile_state->is_p2 = is_p2;

    bool crush = tanks_game_collision(projectile_state->coordinates, true, tanks_state);
    projectile_state->explosion = crush;

    tanks_state->projectiles[freeProjectileIndex] = projectile_state;
}

static void tanks_game_process_game_step(TanksState* const tanks_state) {
    if(tanks_state->state == GameStateMenu) {
        return;
    }

    if(tanks_state->enemies_left == 0 && tanks_state->enemies_live == 0) {
        tanks_state->state = GameStateGameOver;
    }

    if(!tanks_state->p1->live && tanks_state->p1->lives == 0) {
        tanks_state->state = GameStateGameOver;
    }

    if(tanks_state->state == GameStateGameOver) {
        return;
    }

    if(tanks_state->p1 != NULL) {
        if(!tanks_state->p1->live && tanks_state->p1->respawn_cooldown > 0) {
            tanks_state->p1->respawn_cooldown--;
        }
    }

    // Player 1 spawn
    if(tanks_state->p1 && !tanks_state->p1->live && tanks_state->p1->lives > 0) {
        int8_t index = tanks_get_random_free_respawn_point_index(
            tanks_state, tanks_state->team_one_respawn_points);

        if(index != -1) {
            Point point = tanks_state->team_one_respawn_points[index];
            Point c = {point.x, point.y};
            tanks_state->p1->coordinates = c;
            tanks_state->p1->live = true;
            tanks_state->p1->direction = DirectionRight;
            tanks_state->p1->cooldown = SHOT_COOLDOWN;
            tanks_state->p1->respawn_cooldown = SHOT_COOLDOWN;
        }
    }

    // Player 2 spawn
    if(tanks_state->state == GameStateCooperativeServer && tanks_state->p2 &&
       !tanks_state->p2->live && tanks_state->p2->lives > 0) {
        int8_t index = tanks_get_random_free_respawn_point_index(
            tanks_state, tanks_state->team_one_respawn_points);

        if(index != -1) {
            Point point = tanks_state->team_one_respawn_points[index];
            Point c = {point.x, point.y};
            tanks_state->p2->coordinates = c;
            tanks_state->p2->live = true;
            tanks_state->p2->direction = DirectionRight;
            tanks_state->p2->cooldown = SHOT_COOLDOWN;
            tanks_state->p2->respawn_cooldown = SHOT_COOLDOWN;
        }
    }

    // Bot turn
    for(uint8_t i = 0; i < 6; i++) {
        if(tanks_state->bots[i] != NULL) {
            PlayerState* bot = tanks_state->bots[i];
            if(bot->cooldown) {
                bot->cooldown--;
            }

            // Rotate
            if(rand() % 3 == 0) {
                bot->direction = (rand() % 4);
            }

            // Move
            if(rand() % 2 == 0) {
                Point next_step = tanks_game_get_next_step(bot->coordinates, bot->direction);
                bool crush = tanks_game_collision(next_step, false, tanks_state);

                if(!crush) {
                    bot->coordinates = next_step;
                }
            }

            // Shoot
            if(bot->cooldown == 0 && rand() % 3 != 0) {
                tanks_game_shoot(tanks_state, bot, false, false);
            }
        }
    }

    // Bot spawn
    if(tanks_state->enemies_respawn_cooldown) {
        tanks_state->enemies_respawn_cooldown--;
    }

    if(tanks_state->enemies_left > 0 && tanks_state->enemies_live <= 4 &&
       tanks_state->enemies_respawn_cooldown == 0) {
        int8_t index = tanks_get_random_free_respawn_point_index(
            tanks_state, tanks_state->team_two_respawn_points);

        if(index != -1) {
            tanks_state->enemies_left--;
            tanks_state->enemies_live++;
            tanks_state->enemies_respawn_cooldown = RESPAWN_COOLDOWN;
            Point point = tanks_state->team_two_respawn_points[index];

            Point c = {point.x, point.y};

            PlayerState bot = {
                c,
                0,
                0,
                DirectionLeft,
                0,
                0,
                1,
                SHOT_COOLDOWN,
                PLAYER_RESPAWN_COOLDOWN,
            };

            uint8_t freeEnemyIndex;
            for(freeEnemyIndex = 0; freeEnemyIndex < 6; freeEnemyIndex++) {
                if(tanks_state->bots[freeEnemyIndex] == NULL) {
                    break;
                }
            }

            PlayerState* bot_state = malloc(sizeof(PlayerState));
            *bot_state = bot;

            tanks_state->bots[freeEnemyIndex] = bot_state;
        }
    }

    if(tanks_state->p1 != NULL && tanks_state->p1->live && tanks_state->p1->moving) {
        Point next_step =
            tanks_game_get_next_step(tanks_state->p1->coordinates, tanks_state->p1->direction);
        bool crush = tanks_game_collision(next_step, false, tanks_state);

        if(!crush) {
            tanks_state->p1->coordinates = next_step;
        }
    }

    // Player 2 spawn
    if(tanks_state->state == GameStateCooperativeServer && tanks_state->p2 &&
       tanks_state->p2->live && tanks_state->p2->moving) {
        Point next_step =
            tanks_game_get_next_step(tanks_state->p2->coordinates, tanks_state->p2->direction);
        bool crush = tanks_game_collision(next_step, false, tanks_state);

        if(!crush) {
            tanks_state->p2->coordinates = next_step;
        }
    }

    for(int8_t x = 0; x < 100; x++) {
        if(tanks_state->projectiles[x] != NULL) {
            ProjectileState* projectile = tanks_state->projectiles[x];
            Point c = projectile->coordinates;

            if(projectile->explosion) {
                // Break a wall
                if(tanks_state->thisMap[c.x][c.y] == '-') {
                    tanks_state->thisMap[c.x][c.y] = ' ';
                }

                // Kill a bot
                for(uint8_t i = 0; i < 6; i++) {
                    if(tanks_state->bots[i] != NULL) {
                        if(tanks_state->bots[i]->coordinates.x == c.x &&
                           tanks_state->bots[i]->coordinates.y == c.y) {
                            if(projectile->is_p1) {
                                tanks_state->p1->score++;
                            }

                            if(projectile->is_p2) {
                                tanks_state->p2->score++;
                            }

                            // No friendly fire
                            if(projectile->is_p1 || projectile->is_p2) {
                                tanks_state->enemies_live--;
                                free(tanks_state->bots[i]);
                                tanks_state->bots[i] = NULL;
                            }
                        }
                    }
                }

                // Destroy the flag
                if(tanks_state->thisMap[c.x][c.y] == 'a') {
                    tanks_state->state = GameStateGameOver;
                    return;
                }

                // Kill a player 1
                if(tanks_state->p1 != NULL) {
                    if(tanks_state->p1->live && tanks_state->p1->coordinates.x == c.x &&
                       tanks_state->p1->coordinates.y == c.y) {
                        tanks_state->p1->live = false;
                        tanks_state->p1->lives--;
                        tanks_state->p1->respawn_cooldown = PLAYER_RESPAWN_COOLDOWN;
                    }
                }

                // Kill a player 2
                if(tanks_state->p2 != NULL) {
                    if(tanks_state->p2->live && tanks_state->p2->coordinates.x == c.x &&
                       tanks_state->p2->coordinates.y == c.y) {
                        tanks_state->p2->live = false;
                        tanks_state->p2->lives--;
                        tanks_state->p2->respawn_cooldown = PLAYER_RESPAWN_COOLDOWN;
                    }
                }

                // Delete projectile
                free(tanks_state->projectiles[x]);
                tanks_state->projectiles[x] = NULL;
                continue;
            }

            Point next_step =
                tanks_game_get_next_step(projectile->coordinates, projectile->direction);
            bool crush = tanks_game_collision(next_step, true, tanks_state);
            projectile->coordinates = next_step;

            if(crush) {
                projectile->explosion = true;
            }
        }
    }

    if(tanks_state->p1->cooldown > 0) {
        tanks_state->p1->cooldown--;
    }

    if(tanks_state->p2 != NULL && tanks_state->p2->cooldown > 0) {
        tanks_state->p2->cooldown--;
    }

    if(tanks_state->p1 != NULL && tanks_state->p1->live && tanks_state->p1->shooting &&
       tanks_state->p1->cooldown == 0) {
        tanks_game_shoot(tanks_state, tanks_state->p1, true, false);
    }

    tanks_state->p1->moving = false;
    tanks_state->p1->shooting = false;

    if(tanks_state->p2 != NULL) {
        tanks_state->p2->moving = false;
        tanks_state->p2->shooting = false;
    }
}

int32_t tanks_game_app(void* p) {
    UNUSED(p);
    srand(DWT->CYCCNT);

    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(TanksEvent));

    TanksState* tanks_state = malloc(sizeof(TanksState));

    tanks_state->state = GameStateMenu;
    tanks_state->menu_state = MenuStateSingleMode;

    tanks_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
    if(!tanks_state->mutex) {
        FURI_LOG_E("Tanks", "cannot create mutex\r\n");
        furi_message_queue_free(event_queue);
        free(tanks_state);
        return 255;
    }

    ViewPort* view_port = view_port_alloc();
    view_port_draw_callback_set(view_port, tanks_game_render_callback, tanks_state);
    view_port_input_callback_set(view_port, tanks_game_input_callback, event_queue);

    FuriTimer* timer =
        furi_timer_alloc(tanks_game_update_timer_callback, FuriTimerTypePeriodic, event_queue);
    furi_timer_start(timer, furi_kernel_get_tick_frequency() / 4);

    // Open GUI and register view_port
    Gui* gui = furi_record_open(RECORD_GUI);
    gui_add_view_port(gui, view_port, GuiLayerFullscreen);

    TanksEvent event;

    // Initialize network thing.
    uint32_t frequency = 433920000;
    size_t message_max_len = 180;
    uint8_t incomingMessage[180] = {0};
    SubGhzTxRxWorker* subghz_txrx = subghz_tx_rx_worker_alloc();

    subghz_devices_init();
    const SubGhzDevice* subghz_device =
        radio_device_loader_set(NULL, SubGhzRadioDeviceTypeExternalCC1101);

    subghz_devices_reset(subghz_device);
    subghz_devices_load_preset(subghz_device, FuriHalSubGhzPresetOok650Async, NULL);

    subghz_tx_rx_worker_start(subghz_txrx, subghz_device, frequency);
    furi_hal_power_suppress_charge_enter();

    for(bool processing = true; processing;) {
        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);

        furi_mutex_acquire(tanks_state->mutex, FuriWaitForever);

        if(event_status == FuriStatusOk) {
            // press events
            if(event.type == EventTypeKey) {
                if(event.input.type == InputTypePress) {
                    switch(event.input.key) {
                    case InputKeyUp:
                        if(tanks_state->state == GameStateMenu) {
                            if(tanks_state->menu_state == MenuStateCooperativeServerMode) {
                                tanks_state->menu_state = MenuStateSingleMode;
                            } else if(tanks_state->menu_state == MenuStateCooperativeClientMode) {
                                tanks_state->menu_state = MenuStateCooperativeServerMode;
                            }
                        } else if(tanks_state->state == GameStateCooperativeClient) {
                            FuriString* goesUp = furi_string_alloc();
                            char arr[2];
                            arr[0] = GoesUp;
                            arr[1] = 0;
                            furi_string_set(goesUp, (char*)&arr);

                            subghz_tx_rx_worker_write(
                                subghz_txrx,
                                (uint8_t*)furi_string_get_cstr(goesUp),
                                strlen(furi_string_get_cstr(goesUp)));
                            furi_string_free(goesUp);
                        } else {
                            tanks_state->p1->moving = true;
                            tanks_state->p1->direction = DirectionUp;
                        }
                        break;
                    case InputKeyDown:
                        if(tanks_state->state == GameStateMenu) {
                            if(tanks_state->menu_state == MenuStateSingleMode) {
                                tanks_state->menu_state = MenuStateCooperativeServerMode;
                            } else if(tanks_state->menu_state == MenuStateCooperativeServerMode) {
                                tanks_state->menu_state = MenuStateCooperativeClientMode;
                            }
                        } else if(tanks_state->state == GameStateCooperativeClient) {
                            FuriString* goesDown = furi_string_alloc();
                            char arr[2];
                            arr[0] = GoesDown;
                            arr[1] = 0;
                            furi_string_set(goesDown, (char*)&arr);

                            subghz_tx_rx_worker_write(
                                subghz_txrx,
                                (uint8_t*)furi_string_get_cstr(goesDown),
                                strlen(furi_string_get_cstr(goesDown)));
                            furi_string_free(goesDown);
                        } else {
                            tanks_state->p1->moving = true;
                            tanks_state->p1->direction = DirectionDown;
                        }
                        break;
                    case InputKeyRight:
                        if(!(tanks_state->state == GameStateMenu)) {
                            if(tanks_state->state == GameStateCooperativeClient) {
                                FuriString* goesRight = furi_string_alloc();
                                char arr[2];
                                arr[0] = GoesRight;
                                arr[1] = 0;
                                furi_string_set(goesRight, (char*)&arr);

                                subghz_tx_rx_worker_write(
                                    subghz_txrx,
                                    (uint8_t*)furi_string_get_cstr(goesRight),
                                    strlen(furi_string_get_cstr(goesRight)));
                                furi_string_free(goesRight);
                            } else {
                                tanks_state->p1->moving = true;
                                tanks_state->p1->direction = DirectionRight;
                            }
                        }
                        break;
                    case InputKeyLeft:
                        if(!(tanks_state->state == GameStateMenu)) {
                            if(tanks_state->state == GameStateCooperativeClient) {
                                FuriString* goesLeft = furi_string_alloc();
                                char arr[2];
                                arr[0] = GoesLeft;
                                arr[1] = 0;
                                furi_string_set(goesLeft, (char*)&arr);

                                subghz_tx_rx_worker_write(
                                    subghz_txrx,
                                    (uint8_t*)furi_string_get_cstr(goesLeft),
                                    strlen(furi_string_get_cstr(goesLeft)));
                                furi_string_free(goesLeft);
                            } else {
                                tanks_state->p1->moving = true;
                                tanks_state->p1->direction = DirectionLeft;
                            }
                        }
                        break;
                    case InputKeyOk:
                        if(tanks_state->state == GameStateMenu) {
                            if(tanks_state->menu_state == MenuStateSingleMode) {
                                tanks_state->server = true;
                                tanks_game_init_game(tanks_state, GameStateSingle);
                                break;
                            } else if(tanks_state->menu_state == MenuStateCooperativeServerMode) {
                                tanks_state->server = true;
                                tanks_game_init_game(tanks_state, GameStateCooperativeServer);
                                break;
                            } else if(tanks_state->menu_state == MenuStateCooperativeClientMode) {
                                tanks_state->server = false;
                                tanks_game_init_game(tanks_state, GameStateCooperativeClient);
                                break;
                            }
                        } else if(tanks_state->state == GameStateGameOver) {
                            tanks_game_init_game(tanks_state, tanks_state->state);
                        } else if(tanks_state->state == GameStateCooperativeClient) {
                            FuriString* shoots = furi_string_alloc();
                            char arr[2];
                            arr[0] = Shoots;
                            arr[1] = 0;
                            furi_string_set(shoots, (char*)&arr);

                            subghz_tx_rx_worker_write(
                                subghz_txrx,
                                (uint8_t*)furi_string_get_cstr(shoots),
                                strlen(furi_string_get_cstr(shoots)));
                            furi_string_free(shoots);
                        } else {
                            tanks_state->p1->shooting = true;
                        }
                        break;
                    case InputKeyBack:
                        processing = false;
                        break;
                    default:
                        break;
                    }
                }
            } else if(event.type == EventTypeTick) {
                if(tanks_state->state == GameStateCooperativeServer) {
                    if(subghz_tx_rx_worker_available(subghz_txrx)) {
                        memset(incomingMessage, 0x00, message_max_len);
                        subghz_tx_rx_worker_read(subghz_txrx, incomingMessage, message_max_len);

                        if(incomingMessage != NULL) {
                            tanks_state->received++;

                            switch(incomingMessage[0]) {
                            case GoesUp:
                                tanks_state->p2->moving = true;
                                tanks_state->p2->direction = DirectionUp;
                                break;
                            case GoesRight:
                                tanks_state->p2->moving = true;
                                tanks_state->p2->direction = DirectionRight;
                                break;
                            case GoesDown:
                                tanks_state->p2->moving = true;
                                tanks_state->p2->direction = DirectionDown;
                                break;
                            case GoesLeft:
                                tanks_state->p2->moving = true;
                                tanks_state->p2->direction = DirectionLeft;
                                break;
                            case Shoots:
                                tanks_state->p2->shooting = true;
                                break;
                            default:
                                break;
                            }
                        }
                    }

                    tanks_game_process_game_step(tanks_state);

                    FuriString* serializedData = furi_string_alloc();
                    unsigned char* data = tanks_game_serialize(tanks_state);
                    char arr[11 * 16 + 1];

                    for(uint8_t i = 0; i < 11 * 16; i++) {
                        arr[i] = data[i];
                    }

                    arr[11 * 16] = 0;

                    furi_string_set(serializedData, (char*)&arr);

                    subghz_tx_rx_worker_write(
                        subghz_txrx,
                        (uint8_t*)furi_string_get_cstr(serializedData),
                        strlen(furi_string_get_cstr(serializedData)));
                    furi_string_free(serializedData);
                    tanks_state->sent++;
                } else if(tanks_state->state == GameStateSingle) {
                    tanks_game_process_game_step(tanks_state);
                } else if(tanks_state->state == GameStateCooperativeClient) {
                    if(subghz_tx_rx_worker_available(subghz_txrx)) {
                        memset(incomingMessage, 0x00, message_max_len);
                        subghz_tx_rx_worker_read(subghz_txrx, incomingMessage, message_max_len);

                        tanks_state->received++;

                        tanks_game_deserialize_and_write_to_state(
                            (unsigned char*)incomingMessage, tanks_state);
                    }
                }
            }
        } else {
            // event timeout
        }

        view_port_update(view_port);
        furi_mutex_release(tanks_state->mutex);
        furi_delay_ms(1);
    }

    furi_delay_ms(10);
    furi_hal_power_suppress_charge_exit();

    if(subghz_tx_rx_worker_is_running(subghz_txrx)) {
        subghz_tx_rx_worker_stop(subghz_txrx);
        subghz_tx_rx_worker_free(subghz_txrx);
    }

    subghz_devices_deinit();

    furi_timer_free(timer);
    view_port_enabled_set(view_port, false);
    gui_remove_view_port(gui, view_port);
    furi_record_close(RECORD_GUI);
    view_port_free(view_port);
    furi_message_queue_free(event_queue);
    furi_mutex_free(tanks_state->mutex);

    if(tanks_state->p1 != NULL) {
        free(tanks_state->p1);
    }

    if(tanks_state->p2 != NULL) {
        free(tanks_state->p2);
    }

    free(tanks_state);

    return 0;
}
