#include <furi.h>
#include <gui/gui.h>
#include <input/input.h>
#include <stdlib.h>
#include <dolphin/dolphin.h>

//ORIGINAL REPO: https://github.com/Dooskington/flipperzero-zombiez
//AUTHORS: https://github.com/Dooskington | https://github.com/DevMilanIan

#include "zombiez.h"

#define ZOMBIES_MAX 3
#define ZOMBIES_WIDTH 5
#define ZOMBIES_HEIGHT 8
#define PROJECTILES_MAX 10

#define MIN_Y 5
#define MAX_Y 58
#define WALL_X 16
#define PLAYER_START_X 8
#define PLAYER_START_Y (MAX_Y - MIN_Y) / 2

typedef enum {
    EventTypeTick,
    EventTypeKey,
} EventType;

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

typedef enum { GameStatePlaying, GameStateGameOver } GameState;

typedef struct {
    int x;
    int y;
} Point;

typedef struct {
    Point position;
    int hp;
} Player;

typedef struct {
    Point position;
    int hp;
} Zombie;

typedef struct {
    Point position;
} Projectile;

typedef struct {
    FuriMutex* mutex;
    GameState game_state;
    Player player;

    size_t zombies_count;
    Zombie* zombies[ZOMBIES_MAX];

    size_t projectiles_count;
    Projectile* projectiles[PROJECTILES_MAX];

    uint16_t score;
    bool input_shoot;
} PluginState;

static void render_callback(Canvas* const canvas, void* ctx) {
    furi_assert(ctx);
    const PluginState* plugin_state = ctx;
    furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);

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

    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str_aligned(
        canvas,
        plugin_state->player.position.x,
        plugin_state->player.position.y,
        AlignCenter,
        AlignCenter,
        "@");

    canvas_draw_line(canvas, WALL_X, 0, WALL_X, 64);
    canvas_draw_line(canvas, WALL_X + 2, 4, WALL_X + 2, 59);

    for(int i = 0; i < PROJECTILES_MAX; ++i) {
        Projectile* p = plugin_state->projectiles[i];
        if(p != NULL) {
            canvas_draw_disc(canvas, p->position.x, p->position.y, 3);
        }
    }

    for(int i = 0; i < ZOMBIES_MAX; ++i) {
        Zombie* z = plugin_state->zombies[i];
        if(z != NULL) {
            for(int h = 0; h < ZOMBIES_HEIGHT; h++) {
                for(int w = 0; w < ZOMBIES_WIDTH; w++) {
                    // Switch animation
                    int zIdx = 0;
                    if(z->position.x % 2 == 0) {
                        zIdx = 1;
                    }

                    // Draw zombie pixels
                    if(zombie_array[zIdx][h][w] == 1) {
                        int x = z->position.x + w;
                        int y = z->position.y + h;

                        canvas_draw_dot(canvas, x, y);
                    }
                }
            }
        }
    }

    int heart;
    if((plugin_state->player.hp - 10) > 5) { // 16, 17, 18, 19, 20
        heart = 0;
    } else if((plugin_state->player.hp - 5) > 5) { // 11, 12, 13, 14, 15
        heart = 1;
    } else if((plugin_state->player.hp - 3) > 2) { // 6, 7, 8, 9, 10
        heart = 2;
    } else if(plugin_state->player.hp > 0) { // 1, 2, 3, 4, 5
        heart = 3;
    } else { // 0
        heart = 4;
    }
    // visual representation of health
    for(int h = 0; h < 5; h++) {
        for(int w = 0; w < 5; w++) {
            if(heart_array[heart][h][w] == 1) {
                int x = 124 - w;
                int y = 56 + h;

                canvas_draw_dot(canvas, x, y);
            }
        }
    }

    // buffer hp + score
    char hpBuffer[8];
    char scoreBuffer[14];

    if(plugin_state->game_state == GameStatePlaying) {
        // display ammo / reload
        if(plugin_state->projectiles_count >= PROJECTILES_MAX) {
            canvas_draw_str_aligned(canvas, 24, 10, AlignLeft, AlignCenter, "RELOAD");
        } else {
            for(uint8_t i = 0; i < (PROJECTILES_MAX - plugin_state->projectiles_count); i++) {
                canvas_draw_box(canvas, 24 + (4 * i), 6, 2, 4);
            }
        }
        // display hp + score
        snprintf(hpBuffer, sizeof(hpBuffer), "%u", plugin_state->player.hp);
        canvas_draw_str_aligned(canvas, 118, 62, AlignRight, AlignBottom, hpBuffer);

        snprintf(scoreBuffer, sizeof(scoreBuffer), "%u", plugin_state->score);
        canvas_draw_str_aligned(canvas, 126, 10, AlignRight, AlignBottom, scoreBuffer);
    }
    // Game Over banner
    if(plugin_state->game_state == GameStateGameOver) {
        // Screen is 128x64 px
        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);
        canvas_draw_str(canvas, 37, 31, "Game Over");

        canvas_set_font(canvas, FontSecondary);
        snprintf(scoreBuffer, sizeof(scoreBuffer), "Score: %u", plugin_state->score);
        canvas_draw_str_aligned(canvas, 64, 41, AlignCenter, AlignBottom, scoreBuffer);
    }

    //char* info = (char*)malloc(16 * sizeof(char));
    //asprintf(&info, "%d, %d", plugin_state->x, plugin_state->y);
    //canvas_draw_str_aligned(canvas, 32, 16, AlignLeft, AlignBottom, info);
    //free(info);

    furi_mutex_release(plugin_state->mutex);
}

static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
    furi_assert(event_queue);
    PluginEvent event = {.type = EventTypeKey, .input = *input_event};
    furi_message_queue_put(event_queue, &event, FuriWaitForever);
}

static void tick(PluginState* const plugin_state) {
    if(plugin_state->input_shoot && (plugin_state->projectiles_count < PROJECTILES_MAX)) {
        Projectile* p = (Projectile*)malloc(sizeof(Projectile));
        p->position.x = plugin_state->player.position.x;
        p->position.y = plugin_state->player.position.y;

        size_t idx = plugin_state->projectiles_count;
        plugin_state->projectiles[idx] = p;
        plugin_state->projectiles_count += 1;
    }

    for(int i = 0; i < ZOMBIES_MAX; ++i) {
        if(!plugin_state->zombies[i]) {
            Zombie* z = (Zombie*)malloc(sizeof(Zombie));
            //z->hp = 20;
            z->position.x = 126;
            z->position.y = MIN_Y + (rand() % (MAX_Y - MIN_Y));

            plugin_state->zombies[i] = z;
            plugin_state->zombies_count += 1;
        }
    }

    for(int i = 0; i < PROJECTILES_MAX; ++i) {
        Projectile* p = plugin_state->projectiles[i];
        if(p != NULL) {
            p->position.x += 2;

            for(int i = 0; i < ZOMBIES_MAX; ++i) {
                Zombie* z = plugin_state->zombies[i];
                if(z != NULL) {
                    if( // projectile close enough to zombie
                        (((z->position.x - p->position.x) <= 2) &&
                         ((z->position.y - p->position.y) <= 4)) &&
                        (((p->position.x - z->position.x) <= 2) &&
                         ((p->position.y - z->position.y) <= 6))) {
                        //z->hp -= 5;
                        //if(z->hp <= 0) {
                        plugin_state->zombies_count -= 1;
                        free(z);
                        plugin_state->zombies[i] = NULL;
                        plugin_state->score++;
                        //if(plugin_state->score % 15 == 0) dolphin_deed(getRandomDeed());
                        //}
                    } else if(z->position.x <= WALL_X && z->position.x > 0) { // zombie got to the wall
                        plugin_state->zombies_count -= 1;
                        free(z);
                        plugin_state->zombies[i] = NULL;
                        if(plugin_state->player.hp > 0) {
                            plugin_state->player.hp--;
                        } else {
                            plugin_state->game_state = GameStateGameOver;
                        }
                    } else {
                        if(furi_get_tick() % 2 == 0) z->position.x--;
                    }
                }
            }

            if(p->position.x >= 128) {
                free(p);
                plugin_state->projectiles[i] = NULL;
            }
        }
    }

    plugin_state->input_shoot = false;
}

static void timer_callback(void* ctx) {
    furi_assert(ctx);
    FuriMessageQueue* event_queue = ctx;
    PluginEvent event = {.type = EventTypeTick};
    furi_message_queue_put(event_queue, &event, 0);
}

static void zombiez_state_init(PluginState* const plugin_state) {
    plugin_state->player.position.x = PLAYER_START_X;
    plugin_state->player.position.y = PLAYER_START_Y;
    plugin_state->player.hp = 20;

    plugin_state->projectiles_count = 0;
    plugin_state->zombies_count = 0;
    plugin_state->score = 0;

    for(int i = 0; i < PROJECTILES_MAX; i++) {
        plugin_state->projectiles[i] = NULL;
    }

    for(int i = 0; i < ZOMBIES_MAX; i++) {
        plugin_state->zombies[i] = NULL;
    }

    plugin_state->game_state = GameStatePlaying;
    plugin_state->input_shoot = false;
}

int32_t zombiez_game_app(void* p) {
    UNUSED(p);
    uint32_t return_code = 0;
    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));

    PluginState* plugin_state = malloc(sizeof(PluginState));
    zombiez_state_init(plugin_state);

    plugin_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
    if(!plugin_state->mutex) {
        FURI_LOG_E("Zombiez", "cannot create mutex\r\n");
        return_code = 255;
        goto free_and_exit;
    }

    // Set system callbacks
    ViewPort* view_port = view_port_alloc();
    view_port_draw_callback_set(view_port, render_callback, plugin_state);
    view_port_input_callback_set(view_port, input_callback, event_queue);

    FuriTimer* timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, event_queue);
    furi_timer_start(timer, furi_kernel_get_tick_frequency() / 22);

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

    // Call dolphin deed on game start
    dolphin_deed(DolphinDeedPluginGameStart);

    PluginEvent event;
    bool isRunning = true;
    while(isRunning) {
        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
        furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);
        if(event_status == FuriStatusOk) {
            if(event.type == EventTypeKey) {
                if(event.input.type == InputTypePress) {
                    switch(event.input.key) {
                    case InputKeyUp:
                        if(plugin_state->player.position.y > MIN_Y &&
                           plugin_state->game_state == GameStatePlaying) {
                            plugin_state->player.position.y--;
                        }
                        break;
                    case InputKeyDown:
                        if(plugin_state->player.position.y < MAX_Y &&
                           plugin_state->game_state == GameStatePlaying) {
                            plugin_state->player.position.y++;
                        }
                        break;
                    case InputKeyOk:
                        if(plugin_state->projectiles_count < PROJECTILES_MAX &&
                           plugin_state->game_state == GameStatePlaying) {
                            plugin_state->input_shoot = true;
                        }
                        break;
                    case InputKeyBack:
                        break;
                    default:
                        break;
                    }
                } else if(
                    event.input.type == InputTypeRepeat &&
                    plugin_state->game_state == GameStatePlaying) {
                    switch(event.input.key) {
                    case InputKeyUp:
                        if(plugin_state->player.position.y > (MIN_Y + 1)) {
                            plugin_state->player.position.y -= 4;
                        }
                        break;
                    case InputKeyDown:
                        if(plugin_state->player.position.y < (MAX_Y - 1)) {
                            plugin_state->player.position.y += 4;
                        }
                        break;
                    default:
                        break;
                    }
                } else if(event.input.type == InputTypeLong) {
                    if(event.input.key == InputKeyOk) {
                        if(plugin_state->game_state == GameStateGameOver) {
                            zombiez_state_init(plugin_state);
                        } else if(plugin_state->projectiles_count >= PROJECTILES_MAX) {
                            plugin_state->projectiles_count = 0;
                            plugin_state->player.hp++;
                        }
                    } else if(event.input.key == InputKeyBack) {
                        isRunning = false;
                    }
                }
            } else if(event.type == EventTypeTick) {
                tick(plugin_state);
            }
        }

        view_port_update(view_port);
        furi_mutex_release(plugin_state->mutex);
    }

    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_mutex_free(plugin_state->mutex);

free_and_exit:
    free(plugin_state);
    furi_message_queue_free(event_queue);

    return return_code;
}