#include "DrawUtils.hpp"
#include "Entity.hpp"
#include "Game.hpp"
#include "Levels.hpp"
#include "Projectile.hpp"
#include "Sprites.h"
#include "adc.h"
#include "avr/pgmspace.h"
#include "util/delay.h"

Game::Game(Keypad keypad, Joystick joystick)
    : utils(), enemy_pos(), doors_pos(), keypad(keypad), joystick(joystick) {
    player_posX = 100;
    player_posY = 50;
    player_prevX = 100;
    player_prevY = 50;
    level = 0;
    enemy_count = 0;
    doors_num = 0;
    last_door_type = None;
    player_hp = 4;
    player_attack_power = 1;
    player_attack_power = 1;
    number_of_rooms_entered = 0;
    enemies_killed = 0;
    hitCheck = 5;
    game_over = 0;
    enemy_hp = 2;
    chest_count = 0;
    restart = false;
    chest_balance = 0;
    invert_shoot = false;
    enemy_update_idx = 0;
}

void Game::initGame() {
    utils.initLCD();
    utils.changeBackground(0, 0, 0);
    const char *lvl = loadLevel(level);
    levelLoadRAM(lvl);
    drawLevel();
}

void Game::loop() {
    char key = keypad.GetKey();
    if (player_hp == 0) {
        if (key == '1') {
            resetGame();
            return;
        }
        if (!game_over) {
            game_over = 1;
            utils.changeBackground(0, 0, 0);
        }
        utils.drawTextBig(80, 10, 0, (char *)"GAME OVER", 0xF800);
        char rooms[100];
        char enemies[100];

        sprintf(rooms, "Number of rooms explored %d", number_of_rooms_entered);
        sprintf(enemies, "Number of enemies killed %d", enemies_killed);

        utils.drawTextSmall(50, 80, 0, rooms, 0xA01F);
        utils.drawTextSmall(50, 100, 0, enemies, 0xA01F);

        return;
    }

    updatePlayerPos();

    if (key == '1') {
        shootProjectiles();
    } else if (key == '4') {
        interactChest();
    } else if (key == '3') {
        invert_shoot = !invert_shoot;
    }

    utils.moveSprite(player_prevX, player_prevY, player_posX, player_posY, 32,
                     32, player_sprite, 0x0000);

    player_prevX = player_posX;
    player_prevY = player_posY;

    updateEnemies();
    updateProjectiles();
    checkProjectileHitsEnemy();
    if (checkEnemyHitsPlayer() && !restart) {
        if (!hitCheck) {
            player_hp--;
            utils.clearRect(22, 0, 40, 17);
            char hp[5];
            sprintf(hp, "%d", player_hp);
            utils.drawTextBig(22, 0, 0, hp, 0xFFFF);
            hitCheck = 5;
        }
        hitCheck--;
    }

    if (playerHitsDoor()) {
        number_of_rooms_entered++;
        enemy_update_idx = 0;
        level = pickNextLevel();
        utils.changeBackground(0, 0, 0);
        const char *lvl = loadLevel(level);
        levelLoadRAM(lvl);
        drawLevel();
        reload_adc();
        _delay_ms(300);
    }
}

void Game::resetGame() {
    player_hp = 4;

    level = 0;
    number_of_rooms_entered = 0;
    enemies_killed = 0;

    enemy_count = 0;
    doors_num = 0;

    hitCheck = 0;
    restart = false;
    game_over = 0;

    last_door_type = None;

    for (int i = 0; i < PROJECTILE_SIZE; i++) {
        projectiles[i].visible = false;
    }

    utils.changeBackground(0, 0, 0);

    const char *lvl = loadLevel(level);
    levelLoadRAM(lvl);
    drawLevel();
}

void Game::updatePlayerPos() {
    uint16_t x = joystick.getX();
    uint16_t y = joystick.getY();

    float dx = normalizeX(x);
    float dy = normalizeY(y);

    if (dx > 0 && dy == 0) {
        dir_x = 1;
        dir_y = 0;
    } else if (dx < 0 && dy == 0) {
        dir_x = -1;
        dir_y = 0;
    } else if (dx == 0 && dy > 0) {
        dir_y = 1;
        dir_x = 0;
    } else if (dx == 0 && dy < 0) {
        dir_y = -1;
        dir_x = 0;
    } else if (dx > 0 && dy > 0) {
        dir_x = 1;
        dir_y = 1;
    } else if (dx > 0 && dy < 0) {
        dir_x = 1;
        dir_y = -1;
    } else if (dx < 0 && dy > 0) {
        dir_y = 1;
        dir_x = -1;
    } else if (dx < 0 && dy < 0) {
        dir_y = -1;
        dir_x = -1;
    } else {
        dir_x = 1;
        dir_y = 0;
    }

    if (invert_shoot) {
        dir_y *= -1;
        dir_x *= -1;
    }

    int16_t next_x = player_posX + (int)(dx * 4);
    int16_t next_y = player_posY + (int)(dy * 4);

    checkCollisionPlayer(next_x, next_y);

    if (player_posX < 0) {
        player_posX = 0;
    } else if (player_posX >= SCREEN_WIDTH) {
        player_posX = SCREEN_WIDTH;
    } else if (player_posY < 0) {
        player_posY = 0;
    } else if (player_posY >= SCREEN_HEIGH) {
        player_posY = SCREEN_HEIGH;
    }
}

void Game::checkCollisionPlayerBlock(int16_t x, int16_t y) {

    if (!checkCollisionBlock(
            x + PLAYER_HITBOX_X_OFFSET, player_posY + PLAYER_HITBOX_Y_OFFSET,
            PLAYER_HITBOX_W, PLAYER_HITBOX_H + PLAYER_HITBOX_Y_OFFSET_BOTTOM)) {

        player_posX = x;
    }

    if (!checkCollisionBlock(player_posX + PLAYER_HITBOX_X_OFFSET,
                             y + PLAYER_HITBOX_Y_OFFSET, PLAYER_HITBOX_W,
                             PLAYER_HITBOX_H + PLAYER_HITBOX_Y_OFFSET_BOTTOM)) {

        player_posY = y;
    }
}

void Game::checkCollisionPlayerChest(int16_t x, int16_t y) {
    if (!checkCollisionChest(
            x + PLAYER_HITBOX_X_OFFSET, player_posY + PLAYER_HITBOX_Y_OFFSET,
            PLAYER_HITBOX_W, PLAYER_HITBOX_H + PLAYER_HITBOX_Y_OFFSET_BOTTOM)) {

        player_posX = x;
    }

    if (!checkCollisionChest(player_posX + PLAYER_HITBOX_X_OFFSET,
                             y + PLAYER_HITBOX_Y_OFFSET, PLAYER_HITBOX_W,
                             PLAYER_HITBOX_H + PLAYER_HITBOX_Y_OFFSET_BOTTOM)) {

        player_posY = y;
    }
}

void Game::checkCollisionPlayer(int16_t x, int16_t y) {

    bool blockX = checkCollisionBlock(
        x + PLAYER_HITBOX_X_OFFSET, player_posY + PLAYER_HITBOX_Y_OFFSET,
        PLAYER_HITBOX_W, PLAYER_HITBOX_H + PLAYER_HITBOX_Y_OFFSET_BOTTOM);

    bool chestX = checkCollisionChest(
        x + PLAYER_HITBOX_X_OFFSET, player_posY + PLAYER_HITBOX_Y_OFFSET,
        PLAYER_HITBOX_W, PLAYER_HITBOX_H + PLAYER_HITBOX_Y_OFFSET_BOTTOM);

    if (!blockX && !chestX) {
        player_posX = x;
    }

    bool blockY = checkCollisionBlock(
        player_posX + PLAYER_HITBOX_X_OFFSET, y + PLAYER_HITBOX_Y_OFFSET,
        PLAYER_HITBOX_W, PLAYER_HITBOX_H + PLAYER_HITBOX_Y_OFFSET_BOTTOM);

    bool chestY = checkCollisionChest(
        player_posX + PLAYER_HITBOX_X_OFFSET, y + PLAYER_HITBOX_Y_OFFSET,
        PLAYER_HITBOX_W, PLAYER_HITBOX_H + PLAYER_HITBOX_Y_OFFSET_BOTTOM);

    if (!blockY && !chestY) {
        player_posY = y;
    }
}

void Game::drawLevel() {
    enemy_count = 0;
    doors_num = 0;
    chest_count = 0;
    for (int row = 0; row < LEVEL_H; row++) {
        for (int col = 0; col < LEVEL_W; col++) {
            char tile = curr_level[row * (LEVEL_W + 1) + col];

            int screenX = col * 24;
            int screenY = row * 24;

            if (tile == '#') {
                utils.drawSpriteSimple(screenX, screenY, 24, 24, block_sprite,
                                       1);
            } else if (tile == 'D') {
                DoorType type = None;
                if (row == 0) {
                    type = Up;
                } else if (row == LEVEL_H - 1) {
                    type = Down;
                } else if (col == 0) {
                    type = Left;
                } else if (col == LEVEL_W - 1) {
                    type = Right;
                }
                doors_pos[doors_num] = DoorHitbox(screenX, screenY, 24, type);
                doors_num++;
            } else if (tile == 'P') {
                player_posX = screenX;
                player_posY = screenY;
                player_prevX = screenX;
                player_prevY = screenY;
                utils.drawSpriteSimple(player_posX, player_posY, 32, 32,
                                       player_sprite, 1);
            } else if (tile == 'E') {
                utils.drawSpriteSimple(screenX, screenY, 16, 16, enemy_sprite,
                                       1);
                enemy_pos[enemy_count] = Entity(screenX, screenY, 16, enemy_hp);
                enemy_count++;
            } else if (tile == 'B') {
                utils.drawSpriteSimple(screenX, screenY, 16, 16, boss_sprite,
                                       2);
            } else if (tile == 'C') {
                utils.drawSpriteSimple(screenX, screenY, 24, 24, chest_sprite,
                                       1);
                chest_pos[chest_count] = Chest(screenX, screenY);
                chest_count++;
            }
        }
    }
    utils.clearRect(0, 0, 62, 17);
    utils.drawSpriteSimple(0, 0, 16, 16, heart_sprite, 1);
    utils.drawTextSmall(17, 0, 0, (char *)"x", 0xFFFF);
    char hp[5];
    sprintf(hp, "%d", player_hp);
    utils.drawTextBig(22, 0, 0, hp, 0xFFFF);

    int temp = SCREEN_WIDTH - 62;
    utils.clearRect(temp, 0, 62, 17);
    utils.drawSpriteSimple(temp, 0, 32, 32, sword_srpite, 0.6);
    char attack[5];
    sprintf(attack, "%d", player_attack_power);
    utils.drawTextBig(temp + 15, 0, 0, attack, 0xFFFF);
}

void Game::updateEnemies() {
    if (enemy_count == 0) {
        return;
    }

    enemy_update_idx = (enemy_update_idx + 1) % enemy_count;

    if (!enemy_pos[enemy_update_idx].visible) {
        return;
    }

    int16_t old_x = enemy_pos[enemy_update_idx].x;
    int16_t old_y = enemy_pos[enemy_update_idx].y;
    bool moved = moveEnemy(enemy_pos[enemy_update_idx]);
    if (moved && (old_x != enemy_pos[enemy_update_idx].x ||
                  old_y != enemy_pos[enemy_update_idx].y)) {
        utils.moveSprite(old_x, old_y, enemy_pos[enemy_update_idx].x,
                         enemy_pos[enemy_update_idx].y, 16, 16, enemy_sprite,
                         0x0000);
    }
}

uint8_t Game::playerHitsDoor() {
    for (uint8_t i = 0; i < doors_num; i++) {
        DoorHitbox door = doors_pos[i];

        if (door.doorType == last_door_type) {
            continue;
        }

        uint16_t px = player_posX + PLAYER_HITBOX_X_OFFSET;
        uint16_t py = player_posY + PLAYER_HITBOX_Y_OFFSET;
        if (px + PLAYER_HITBOX_W > door.x && px < door.x + 24 &&
            py + PLAYER_HITBOX_H > door.y && py < door.y + 24) {
            last_door_type = door.doorType;
            return true;
        }
    }
    return false;
}

void Game::levelLoadRAM(const char *lvl) {
    memcpy_P(curr_level, lvl, LEVEL_H * (LEVEL_W + 1) + 1);
}

char Game::getTile(int16_t x, int16_t y) {
    int col = x / 24;
    int row = y / 24;

    if (col < 0 || col >= LEVEL_W || row < 0 || row >= LEVEL_H) {
        return '#';
    }
    return curr_level[row * (LEVEL_W + 1) + col];
}

float Game::normalizeX(uint16_t v) {
    int16_t centered = (int16_t)v - JOY_CENTER_X;

    if (centered > -100 && centered < 100)
        return 0;

    return (float)centered / 512.0f;
}

float Game::normalizeY(uint16_t v) {
    int16_t centered = (int16_t)v - JOY_CENTER_Y;

    if (centered > -100 && centered < 100)
        return 0;

    return (float)centered / 512.0f;
}

bool Game::tryMoveEnemy(Entity &enemy, int16_t next_x, int16_t next_y) {

    if (checkEnemyHitsEnemy(enemy, next_x, next_y)) {
        return false;
    }

    if (checkCollisionBlock(next_x, next_y, 16, 16)) {
        return false;
    }

    if (checkCollisionChest(next_x, next_y, 16, 16)) {
        return false;
    }

    enemy.x = next_x;
    enemy.y = next_y;
    return true;
}

bool Game::checkCollisionChest(int16_t x, int16_t y, int16_t width,
                               int16_t height) {
    int16_t left = x;
    int16_t right = x + width - 1;
    int16_t top = y;
    int16_t bottom = y + height - 1;

    return (getTile(left, top) == 'C' || getTile(right, top) == 'C' ||
            getTile(left, bottom) == 'C' || getTile(right, bottom) == 'C');
}

bool Game::checkCollisionBlock(int16_t x, int16_t y, int16_t width,
                               int16_t height) {
    int16_t left = x;
    int16_t right = x + width - 1;
    int16_t top = y;
    int16_t bottom = y + height - 1;

    return (getTile(left, top) == '#' || getTile(right, top) == '#' ||
            getTile(left, bottom) == '#' || getTile(right, bottom) == '#');
}

bool Game::moveEnemy(Entity &enemy) {
    int16_t dx = player_posX - enemy.x;
    int16_t dy = player_posY - enemy.y;

    int8_t sx = (dx > 0) ? ENEMY_SPEED : -ENEMY_SPEED;
    int8_t sy = (dy > 0) ? ENEMY_SPEED : -ENEMY_SPEED;

    if (abs(dx) > abs(dy)) {
        if (dx != 0 && tryMoveEnemy(enemy, enemy.x + sx, enemy.y)) {
            return true;
        }

        if (dy != 0 && tryMoveEnemy(enemy, enemy.x, enemy.y + sy)) {
            return true;
        }

    } else {
        if (dy != 0 && tryMoveEnemy(enemy, enemy.x, enemy.y + sy)) {
            return true;
        }

        if (dx != 0 && tryMoveEnemy(enemy, enemy.x + sx, enemy.y)) {
            return true;
        }
    }

    return tryRandomEnemyMove(enemy);
}

bool Game::checkCollisionEntities(int16_t x1, int16_t y1, int16_t w1,
                                  int16_t h1, int16_t x2, int16_t y2,
                                  int16_t w2, int16_t h2) {
    return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2;
}

bool Game::checkEnemyHitsPlayer() {
    int x = player_posX + PLAYER_HITBOX_X_OFFSET;
    int y = player_posY + PLAYER_HITBOX_Y_OFFSET;

    for (int i = 0; i < enemy_count; i++) {
        if (!enemy_pos[i].visible) {
            continue;
        }
        if (checkCollisionEntities(x, y, PLAYER_HITBOX_W, PLAYER_HITBOX_W,
                                   enemy_pos[i].x, enemy_pos[i].y, 16, 16)) {
            return true;
        }
    }
    return false;
}

bool Game::checkEnemyHitsEnemy(Entity &enemy, int16_t next_x, int16_t next_y) {
    for (int i = 0; i < enemy_count; i++) {
        if (!enemy_pos[i].visible) {
            continue;
        }
        if (enemy_pos[i].x == enemy.x && enemy_pos[i].y == enemy.y) {
            continue;
        }

        if (checkCollisionEntities(next_x, next_y, 16, 16, enemy_pos[i].x,
                                   enemy_pos[i].y, 16, 16)) {
            return true;
        }
    }
    return false;
}

bool Game::tryRandomEnemyMove(Entity &enemy) {
    int dx[4] = {ENEMY_SPEED, -ENEMY_SPEED, 0, 0};
    int dy[4] = {0, 0, ENEMY_SPEED, -ENEMY_SPEED};

    int seed = rand() % 4;

    for (int i = 0; i < 4; i++) {

        int idx = (seed + i) % 4;

        int16_t new_x = enemy.x + dx[idx];
        int16_t new_y = enemy.y + dy[idx];

        if (checkCollisionBlock(new_x, new_y, 16, 16) ||
            checkEnemyHitsEnemy(enemy, new_x, new_y)) {
            continue;
        }

        enemy.x = new_x;
        enemy.y = new_y;
        return true;
    }

    return false;
}

void Game::shootProjectiles() {
    for (int i = 0; i < PROJECTILE_SIZE; i++) {
        if (projectiles[i].visible) {
            continue;
        }

        Projectile temp(player_posX + PLAYER_HITBOX_W / 2 + 3,
                        player_posY + PLAYER_HITBOX_H / 2, dir_x, dir_y);
        projectiles[i] = temp;

        break;
    }
}

void Game::updateProjectiles() {
    for (int i = 0; i < PROJECTILE_SIZE; i++) {

        if (!projectiles[i].visible)
            continue;

        int16_t old_x = projectiles[i].x;
        int16_t old_y = projectiles[i].y;

        projectiles[i].x += projectiles[i].dx * 5;
        projectiles[i].y += projectiles[i].dy * 5;

        if (checkCollisionBlock(projectiles[i].x, projectiles[i].y, 4, 4)) {
            utils.eraseEntity(old_x, old_y, 5, 5);
            projectiles[i].visible = false;
            continue;
        }

        if (checkCollisionChest(projectiles[i].x, projectiles[i].y, 4, 4)) {
            utils.eraseEntity(old_x, old_y, 5, 5);
            projectiles[i].visible = false;
            continue;
        }

        utils.eraseEntity(old_x, old_y, 5, 5);
        utils.drawBullet(projectiles[i].x, projectiles[i].y, 0xF800);
    }
}

void Game::checkProjectileHitsEnemy() {
    for (int i = 0; i < enemy_count; i++) {
        if (!enemy_pos[i].visible) {
            continue;
        }
        for (int j = 0; j < PROJECTILE_SIZE; j++) {
            if (!projectiles[j].visible) {
                continue;
            }
            if (checkCollisionEntities(enemy_pos[i].x, enemy_pos[i].y, 16, 16,
                                       projectiles[j].x, projectiles[j].y, 6,
                                       6)) {
                enemy_pos[i].hp -= player_attack_power;
                projectiles[j].visible = false;
                utils.eraseEntity(projectiles[j].x, projectiles[j].y, 5, 5);
                if (enemy_pos[i].hp <= 0) {
                    enemies_killed++;
                    enemy_pos[i].visible = false;
                    utils.eraseEntity(enemy_pos[i].x, enemy_pos[i].y, 16, 16);
                }
            }
        }
    }
}

void Game::interactChest() {
    for (int i = 0; i < chest_count; i++) {

        if (chest_pos[i].opened) {
            continue;
        }

        if (checkCollisionEntities(player_posX + PLAYER_HITBOX_X_OFFSET,
                                   player_posY + PLAYER_HITBOX_Y_OFFSET,
                                   PLAYER_HITBOX_W, PLAYER_HITBOX_H,

                                   chest_pos[i].x - 6, chest_pos[i].y - 6,
                                   24 + 12, 24 + 12)) {

            chest_pos[i].opened = true;

            int prize = rand() % 2;

            if (prize == 1) {
                player_hp++;

                utils.clearRect(22, 0, 40, 17);
                char hp[5];
                sprintf(hp, "%d", player_hp);
                utils.drawTextBig(22, 0, 0, hp, 0xFFFF);
            } else {
                player_attack_power++;

                int temp = SCREEN_WIDTH - 62;
                utils.clearRect(temp, 0, 62, 17);
                utils.drawSpriteSimple(temp, 0, 32, 32, sword_srpite, 0.6);
                char attack[5];
                sprintf(attack, "%d", player_attack_power);
                utils.drawTextBig(temp + 15, 0, 0, attack, 0xFFFF);
            }
        }
    }
}

uint8_t Game::pickNextLevel() {
    uint8_t next_door = -1;
    if (last_door_type == Up) {
        next_door = 0;
    } else if (last_door_type == Down) {
        next_door = 1;
    } else if (last_door_type == Left) {
        next_door = 2;
    } else if (last_door_type == Right) {
        next_door = 3;
    }

    uint8_t levels_to_pick[30];

    uint8_t prefer = EmptyRoom;
    if (chest_balance > 0) {
        prefer = EnemyRoom;
    } else if (chest_balance < 0) {
        prefer = ChestRoom;
    }

    uint8_t idx = 0;

    if (prefer) {
        for (int i = 0; i < 30; i++) {
            if (level_doors[i][next_door] == '1' && prefer == level_type[i]) {
                levels_to_pick[idx] = i;
                idx++;
            }
        }
    }

    if (!idx) {
        for (int i = 0; i < 30; i++) {
            if (level_doors[i][next_door] == '1') {
                levels_to_pick[idx] = i;
                idx++;
            }
        }
    }

    uint8_t pick = levels_to_pick[rand() % idx];

    uint8_t type = level_type[pick];
    if (type == ChestRoom) {
        chest_balance++;
    } else if (type == EnemyRoom) {
        chest_balance--;
    }

    if (chest_balance > 4) {
        chest_balance = 4;
    } else if (chest_balance < 4) {
        chest_balance = -4;
    }

    if ((number_of_rooms_entered % 10 == 0 && number_of_rooms_entered > 0) ||
        (player_attack_power > 3 * enemy_hp)) {
        enemy_hp++;
    }

    return pick;
}
