#include <furi.h>
#include <gui/gui.h>
#include <input/input.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <furi_hal_resources.h>
#include <furi_hal_gpio.h>
#include <dolphin/dolphin.h>

#define BORDER_OFFSET 1
#define MARGIN_OFFSET 3
#define BLOCK_HEIGHT 6
#define BLOCK_WIDTH 6

#define FIELD_WIDTH 11
#define FIELD_HEIGHT 24

#define MAX_FALL_SPEED 500
#define MIN_FALL_SPEED 100

typedef struct Point {
    // Also used for offset data, which is sometimes negative
    int8_t x, y;
} Point;

// Rotation logic taken from
// https://www.youtube.com/watch?v=yIpk5TJ_uaI
typedef enum { OffsetTypeCommon, OffsetTypeI, OffsetTypeO } OffsetType;

// Since we only support rotating clockwise, these are actual translation values,
// not values to be subtracted to get translation values

static const Point rotOffsetTranslation[3][4][5] = {
    {{{0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2}},
     {{0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2}},
     {{0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2}},
     {{0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2}}},
    {{{1, 0}, {-1, 0}, {2, 0}, {-1, 1}, {2, -2}},
     {{0, 1}, {-1, 1}, {2, 1}, {-1, -1}, {2, 2}},
     {{-1, 0}, {1, 0}, {-2, 0}, {1, -1}, {-2, 2}},
     {{0, -1}, {1, -1}, {-2, -1}, {1, 1}, {-2, -2}}},
    {{{0, -1}, {0, 0}, {0, 0}, {0, 0}, {0, 0}},
     {{1, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}},
     {{0, 1}, {0, 0}, {0, 0}, {0, 0}, {0, 0}},
     {{-1, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}}}};

typedef struct {
    Point p[4];
    uint8_t rotIdx;
    OffsetType offsetType;
} Piece;

// Shapes @ spawn locations, rotation point first
static Piece shapes[] = {
    {.p = {{5, 1}, {4, 0}, {5, 0}, {6, 1}}, .rotIdx = 0, .offsetType = OffsetTypeCommon}, // Z
    {.p = {{5, 1}, {4, 1}, {5, 0}, {6, 0}}, .rotIdx = 0, .offsetType = OffsetTypeCommon}, // S
    {.p = {{5, 1}, {4, 1}, {6, 1}, {6, 0}}, .rotIdx = 0, .offsetType = OffsetTypeCommon}, // L
    {.p = {{5, 1}, {4, 0}, {4, 1}, {6, 1}}, .rotIdx = 0, .offsetType = OffsetTypeCommon}, // J
    {.p = {{5, 1}, {4, 1}, {5, 0}, {6, 1}}, .rotIdx = 0, .offsetType = OffsetTypeCommon}, // T
    {.p = {{5, 1}, {4, 1}, {6, 1}, {7, 1}}, .rotIdx = 0, .offsetType = OffsetTypeI}, // I
    {.p = {{5, 1}, {5, 0}, {6, 0}, {6, 1}}, .rotIdx = 0, .offsetType = OffsetTypeO} // O
};

typedef enum { GameStatePlaying, GameStateGameOver } GameState;

typedef struct {
    bool playField[FIELD_HEIGHT][FIELD_WIDTH];
    Piece currPiece;
    uint16_t numLines;
    uint16_t fallSpeed;
    GameState gameState;
    FuriTimer* timer;
    FuriMutex* mutex;
} TetrisState;

typedef enum {
    EventTypeTick,
    EventTypeKey,
} EventType;

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

static void tetris_game_draw_border(Canvas* const canvas) {
    canvas_draw_line(canvas, 0, 0, 0, 127);
    canvas_draw_line(canvas, 0, 127, 63, 127);
    canvas_draw_line(canvas, 63, 127, 63, 0);

    canvas_draw_line(canvas, 2, 0, 2, 125);
    canvas_draw_line(canvas, 2, 125, 61, 125);
    canvas_draw_line(canvas, 61, 125, 61, 0);
}

static void tetris_game_draw_playfield(Canvas* const canvas, const TetrisState* tetris_state) {
    // Playfield: 11 x 24

    for(int y = 0; y < FIELD_HEIGHT; y++) {
        for(int x = 0; x < FIELD_WIDTH; x++) {
            if(tetris_state->playField[y][x]) {
                uint16_t xOffset = x * 5;
                uint16_t yOffset = y * 5;

                canvas_draw_rframe(
                    canvas,
                    BORDER_OFFSET + MARGIN_OFFSET + xOffset,
                    BORDER_OFFSET + MARGIN_OFFSET + yOffset - 1,
                    BLOCK_WIDTH,
                    BLOCK_HEIGHT,
                    1);
                canvas_draw_dot(
                    canvas,
                    BORDER_OFFSET + MARGIN_OFFSET + xOffset + 2,
                    BORDER_OFFSET + MARGIN_OFFSET + yOffset + 1);
                canvas_draw_dot(
                    canvas,
                    BORDER_OFFSET + MARGIN_OFFSET + xOffset + 3,
                    BORDER_OFFSET + MARGIN_OFFSET + yOffset + 1);
                canvas_draw_dot(
                    canvas,
                    BORDER_OFFSET + MARGIN_OFFSET + xOffset + 2,
                    BORDER_OFFSET + MARGIN_OFFSET + yOffset + 2);
            }
        }
    }
}

static void tetris_game_render_callback(Canvas* const canvas, void* ctx) {
    furi_assert(ctx);
    const TetrisState* tetris_state = ctx;
    furi_mutex_acquire(tetris_state->mutex, FuriWaitForever);

    tetris_game_draw_border(canvas);
    tetris_game_draw_playfield(canvas, tetris_state);

    // Show score on the game field
    if(tetris_state->gameState == GameStatePlaying) {
        char buffer2[6];
        snprintf(buffer2, sizeof(buffer2), "%u", tetris_state->numLines);
        canvas_draw_str_aligned(canvas, 58, 10, AlignRight, AlignBottom, buffer2);
    }

    if(tetris_state->gameState == GameStateGameOver) {
        // 128 x 64
        canvas_set_color(canvas, ColorWhite);
        canvas_draw_box(canvas, 1, 52, 62, 24);

        canvas_set_color(canvas, ColorBlack);
        canvas_draw_frame(canvas, 1, 52, 62, 24);

        canvas_set_font(canvas, FontPrimary);
        canvas_draw_str(canvas, 4, 63, "Game Over");

        char buffer[13];
        snprintf(buffer, sizeof(buffer), "Lines: %u", tetris_state->numLines);
        canvas_set_font(canvas, FontSecondary);
        canvas_draw_str_aligned(canvas, 32, 73, AlignCenter, AlignBottom, buffer);
    }
    furi_mutex_release(tetris_state->mutex);
}

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

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

static void tetris_game_init_state(TetrisState* tetris_state) {
    tetris_state->gameState = GameStatePlaying;
    tetris_state->numLines = 0;
    tetris_state->fallSpeed = MAX_FALL_SPEED;
    memset(tetris_state->playField, 0, sizeof(tetris_state->playField));

    memcpy(&tetris_state->currPiece, &shapes[rand() % 7], sizeof(tetris_state->currPiece));

    furi_timer_start(tetris_state->timer, tetris_state->fallSpeed);
}

static void tetris_game_remove_curr_piece(TetrisState* tetris_state) {
    for(int i = 0; i < 4; i++) {
        uint8_t x = tetris_state->currPiece.p[i].x;
        uint8_t y = tetris_state->currPiece.p[i].y;

        tetris_state->playField[y][x] = false;
    }
}

static void tetris_game_render_curr_piece(TetrisState* tetris_state) {
    for(int i = 0; i < 4; i++) {
        uint8_t x = tetris_state->currPiece.p[i].x;
        uint8_t y = tetris_state->currPiece.p[i].y;

        tetris_state->playField[y][x] = true;
    }
}

static void tetris_game_rotate_shape(Point currShape[], Point newShape[]) {
    // Copy shape data
    for(int i = 0; i < 4; i++) {
        newShape[i] = currShape[i];
    }

    for(int i = 1; i < 4; i++) {
        int8_t relX = currShape[i].x - currShape[0].x;
        int8_t relY = currShape[i].y - currShape[0].y;

        // Matrix rotation thing
        int8_t newRelX = (relX * 0) + (relY * -1);
        int8_t newRelY = (relX * 1) + (relY * 0);

        newShape[i].x = currShape[0].x + newRelX;
        newShape[i].y = currShape[0].y + newRelY;
    }
}

static void tetris_game_apply_kick(Point points[], Point kick) {
    for(int i = 0; i < 4; i++) {
        points[i].x += kick.x;
        points[i].y += kick.y;
    }
}

static bool tetris_game_is_valid_pos(TetrisState* tetris_state, Point* shape) {
    for(int i = 0; i < 4; i++) {
        if(shape[i].x < 0 || shape[i].x > (FIELD_WIDTH - 1) ||
           tetris_state->playField[shape[i].y][shape[i].x] == true) {
            return false;
        }
    }
    return true;
}

static void tetris_game_try_rotation(TetrisState* tetris_state, Piece* newPiece) {
    uint8_t currRotIdx = tetris_state->currPiece.rotIdx;

    Point* rotatedShape = malloc(sizeof(Point) * 4);
    Point* kickedShape = malloc(sizeof(Point) * 4);

    memcpy(rotatedShape, &tetris_state->currPiece.p, sizeof(tetris_state->currPiece.p));

    tetris_game_rotate_shape(tetris_state->currPiece.p, rotatedShape);

    for(int i = 0; i < 5; i++) {
        memcpy(kickedShape, rotatedShape, (sizeof(Point) * 4));
        tetris_game_apply_kick(
            kickedShape, rotOffsetTranslation[newPiece->offsetType][currRotIdx][i]);

        if(tetris_game_is_valid_pos(tetris_state, kickedShape)) {
            memcpy(&newPiece->p, kickedShape, sizeof(newPiece->p));
            newPiece->rotIdx = (newPiece->rotIdx + 1) % 4;
            break;
        }
    }
    free(rotatedShape);
    free(kickedShape);
}

static bool tetris_game_row_is_line(bool row[]) {
    for(int i = 0; i < FIELD_WIDTH; i++) {
        if(row[i] == false) return false;
    }
    return true;
}

static void
    tetris_game_check_for_lines(TetrisState* tetris_state, uint8_t* lines, uint8_t* numLines) {
    for(int i = 0; i < FIELD_HEIGHT; i++) {
        if(tetris_game_row_is_line(tetris_state->playField[i])) {
            *(lines++) = i;
            *numLines += 1;
        }
    }
}

static bool tetris_game_piece_at_bottom(TetrisState* tetris_state, Piece* newPiece) {
    for(int i = 0; i < 4; i++) {
        Point* pos = (Point*)&newPiece->p;
        if(pos[i].y >= FIELD_HEIGHT || tetris_state->playField[pos[i].y][pos[i].x] == true) {
            return true;
        }
    }
    return false;
}

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

    TetrisEvent event = {.type = EventTypeTick};
    furi_message_queue_put(event_queue, &event, FuriWaitForever);
}

static void
    tetris_game_process_step(TetrisState* tetris_state, Piece* newPiece, bool wasDownMove) {
    if(tetris_state->gameState == GameStateGameOver) return;

    tetris_game_remove_curr_piece(tetris_state);

    if(wasDownMove) {
        if(tetris_game_piece_at_bottom(tetris_state, newPiece)) {
            furi_timer_stop(tetris_state->timer);

            tetris_game_render_curr_piece(tetris_state);
            uint8_t numLines = 0;
            uint8_t lines[] = {0, 0, 0, 0};
            uint16_t nextFallSpeed;

            tetris_game_check_for_lines(tetris_state, lines, &numLines);
            if(numLines > 0) {
                for(int i = 0; i < numLines; i++) {
                    // zero out row
                    for(int j = 0; j < FIELD_WIDTH; j++) {
                        tetris_state->playField[lines[i]][j] = false;
                    }
                    // move all above rows down
                    for(int k = lines[i]; k >= 0; k--) {
                        for(int m = 0; m < FIELD_WIDTH; m++) {
                            tetris_state->playField[k][m] =
                                (k == 0) ? false : tetris_state->playField[k - 1][m];
                        }
                    }
                }

                uint16_t oldNumLines = tetris_state->numLines;
                tetris_state->numLines += numLines;
                if((oldNumLines / 10) % 10 != (tetris_state->numLines / 10) % 10) {
                    nextFallSpeed =
                        tetris_state->fallSpeed - (100 / (tetris_state->numLines / 10));
                    if(nextFallSpeed >= MIN_FALL_SPEED) {
                        tetris_state->fallSpeed = nextFallSpeed;
                    }
                }
            }

            // Check for game over
            Piece* spawnedPiece = &shapes[rand() % 7];
            if(!tetris_game_is_valid_pos(tetris_state, spawnedPiece->p)) {
                tetris_state->gameState = GameStateGameOver;
            } else {
                memcpy(&tetris_state->currPiece, spawnedPiece, sizeof(tetris_state->currPiece));
                furi_timer_start(tetris_state->timer, tetris_state->fallSpeed);
            }
        }
    }

    if(tetris_game_is_valid_pos(tetris_state, newPiece->p)) {
        memcpy(&tetris_state->currPiece, newPiece, sizeof(tetris_state->currPiece));
    }

    tetris_game_render_curr_piece(tetris_state);
}

int32_t tetris_game_app() {
    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(TetrisEvent));

    TetrisState* tetris_state = malloc(sizeof(TetrisState));

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

    // Not doing this eventually causes issues with TimerSvc due to not sleeping/yielding enough in this task
    TaskHandle_t timer_task = xTaskGetHandle(configTIMER_SERVICE_TASK_NAME);

    vTaskPrioritySet(timer_task, configMAX_PRIORITIES - 1);

    ViewPort* view_port = view_port_alloc();
    view_port_set_orientation(view_port, ViewPortOrientationVertical);
    view_port_draw_callback_set(view_port, tetris_game_render_callback, tetris_state);
    view_port_input_callback_set(view_port, tetris_game_input_callback, event_queue);

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

    tetris_state->timer =
        furi_timer_alloc(tetris_game_update_timer_callback, FuriTimerTypePeriodic, event_queue);
    tetris_game_init_state(tetris_state);

    TetrisEvent event;

    Piece* newPiece = malloc(sizeof(Piece));
    uint8_t downRepeatCounter = 0;

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

    for(bool processing = true; processing;) {
        // This 10U implicitly sets the game loop speed. downRepeatCounter relies on this value
        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 10U);

        furi_mutex_acquire(tetris_state->mutex, FuriWaitForever);

        memcpy(newPiece, &tetris_state->currPiece, sizeof(tetris_state->currPiece));
        bool wasDownMove = false;

        if(!furi_hal_gpio_read(&gpio_button_right)) {
            if(downRepeatCounter > 3) {
                for(int i = 0; i < 4; i++) {
                    newPiece->p[i].y += 1;
                }
                downRepeatCounter = 0;
                wasDownMove = true;
            } else {
                downRepeatCounter++;
            }
        }

        if(event_status == FuriStatusOk) {
            if(event.type == EventTypeKey) {
                if(event.input.type == InputTypePress || event.input.type == InputTypeLong ||
                   event.input.type == InputTypeRepeat) {
                    switch(event.input.key) {
                    case InputKeyUp:
                        break;
                    case InputKeyDown:
                        break;
                    case InputKeyRight:
                        for(int i = 0; i < 4; i++) {
                            newPiece->p[i].x += 1;
                        }
                        break;
                    case InputKeyLeft:
                        for(int i = 0; i < 4; i++) {
                            newPiece->p[i].x -= 1;
                        }
                        break;
                    case InputKeyOk:
                        if(tetris_state->gameState == GameStatePlaying) {
                            tetris_game_remove_curr_piece(tetris_state);
                            tetris_game_try_rotation(tetris_state, newPiece);
                            tetris_game_render_curr_piece(tetris_state);
                        } else {
                            tetris_game_init_state(tetris_state);
                        }
                        break;
                    case InputKeyBack:
                        processing = false;
                        break;
                    default:
                        break;
                    }
                }
            } else if(event.type == EventTypeTick) {
                // TODO: This is inverted.  it returns true when the button is not pressed.
                // see macro in input.c and do that
                if(furi_hal_gpio_read(&gpio_button_right)) {
                    for(int i = 0; i < 4; i++) {
                        newPiece->p[i].y += 1;
                    }
                    wasDownMove = true;
                }
            }
        }

        tetris_game_process_step(tetris_state, newPiece, wasDownMove);

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

    furi_timer_free(tetris_state->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(tetris_state->mutex);
    vTaskPrioritySet(timer_task, configTIMER_TASK_PRIORITY);
    free(newPiece);
    free(tetris_state);

    return 0;
}