#include "game/World.h"
#include "protocol/PacketDispatcher.h"
#include "protocol/Protocol.h"
#include "game/BaseGame.h"
#include "misc/Random.h"

#include <cmath>
#include "misc/Compression.h"

#include <iostream>

namespace td {
namespace game {

World::World(Game* game) : m_Game(game) {
    bindListener(this);
}

TilePtr World::getTile(std::int32_t x, std::int32_t y) const {
    std::int16_t chunkX = x / Chunk::ChunkWidth;
    std::int16_t chunkY = y / Chunk::ChunkHeight;

    std::uint16_t subChunkX = std::abs(x % Chunk::ChunkWidth);
    std::uint16_t subChunkY = std::abs(y % Chunk::ChunkHeight);

    auto chunkIt = m_Chunks.find({ chunkX, chunkY });
    if (chunkIt == m_Chunks.end())
        return nullptr;

    ChunkPtr chunk = chunkIt->second;

    return getTilePtr(chunk->getTileIndex(subChunkY * Chunk::ChunkWidth + subChunkX));
}

bool World::loadMap(const protocol::WorldBeginDataPacket* worldHeader) {
    m_TowerPlacePalette = worldHeader->getTowerTilePalette();
    m_WalkablePalette = worldHeader->getWalkableTileColor();
    m_DecorationPalette = worldHeader->getDecorationPalette();

    getRedTeam().getSpawn() = worldHeader->getRedSpawn();
    getBlueTeam().getSpawn() = worldHeader->getBlueSpawn();

    m_SpawnColorPalette = worldHeader->getSpawnPalette();

    getRedTeam().getCastle() = worldHeader->getRedCastle();
    getBlueTeam().getCastle() = worldHeader->getBlueCastle();

    m_TilePalette = worldHeader->getTilePalette();

    return true;
}

bool World::loadMap(const protocol::WorldDataPacket* worldData) {
    m_Chunks = worldData->getChunks();
    return true;
}

bool World::loadMapFromFile(const std::string& fileName) {
    DataBuffer buffer;
    if (!buffer.ReadFile(fileName)) {
        std::cerr << "Failed to load map from file " << fileName << " !\n";
        return false;
    }

    std::cout << "Read file : " << fileName << " (File size : " << buffer.GetSize() << ")" << std::endl;

    DataBuffer mapHeaderPacketBuffer = utils::Decompress(buffer);
    DataBuffer mapDataPacketBuffer = utils::Decompress(buffer);

    protocol::PacketType packetType;

    mapHeaderPacketBuffer >> packetType;

    protocol::WorldBeginDataPacket headerPacket;
    headerPacket.Deserialize(mapHeaderPacketBuffer);

    mapDataPacketBuffer >> packetType;

    protocol::WorldDataPacket dataPacket;
    dataPacket.Deserialize(mapDataPacketBuffer);

    loadMap(&headerPacket);
    loadMap(&dataPacket);

    return true;
}

bool World::saveMap(const std::string& fileName) const {
    protocol::WorldBeginDataPacket headerPacket(this);
    protocol::WorldDataPacket dataPacket(this);

    DataBuffer mapHeaderCompressed = utils::Compress(headerPacket.Serialize());
    DataBuffer mapDataCompressed = utils::Compress(dataPacket.Serialize());

    std::cout << "Header Packet Size : " << mapHeaderCompressed.GetSize() << std::endl;
    std::cout << "World Data Packet Size : " << mapDataCompressed.GetSize() << std::endl;

    DataBuffer buffer = mapHeaderCompressed << mapDataCompressed;
    std::cout << "Total Size : " << buffer.GetSize() << std::endl;
    return buffer.WriteFile(fileName);
}

void World::tick(std::uint64_t delta) {
    moveMobs(delta);
    tickMobs(delta);
    for (TowerPtr tower : m_Towers) {
        tower->tick(delta, this);
    }
    cleanDeadMobs();
}

void World::spawnMobAt(MobID id, MobType type, std::uint8_t level, PlayerID sender, float x, float y, Direction dir) {
    MobPtr mob = MobFactory::createMob(id, type, level, sender);
    mob->setCenter({ x, y });
    mob->setDirection(dir);
    m_Mobs.push_back(mob);
}

TowerPtr World::placeTowerAt(TowerID id, TowerType type, std::int32_t x, std::int32_t y, PlayerID builder) {
    TowerPtr tower = TowerFactory::createTower(type, id, x, y, builder);
    m_Towers.push_back(tower);
    return tower;
}

void World::tickMobs(std::uint64_t delta) {
    for (MobPtr mob : m_Mobs) {
        mob->tick(delta);
    }
}

void World::changeMobDirection(MobPtr mob, WalkableTilePtr tile) {
    if (mob->getDirection() == tile->direction) return;

    float tileX = static_cast<float>(static_cast<std::int32_t>(mob->getCenterX()));
    float tileY = static_cast<float>(static_cast<std::int32_t>(mob->getCenterY()));

    switch (mob->getDirection()) {

    case Direction::PositiveY: {
        if (tile->direction == Direction::NegativeX) {
            if (mob->getTileY() > mob->getTileX()) {
                mob->setCenterY(tileY + mob->getTileX());
                mob->setDirection(tile->direction);
            }
        } else { // tile->direction = Direction::PositiveX
            if (mob->getTileY() > 1 - mob->getTileX()) {
                mob->setCenterY(tileY + (1 - mob->getTileX()));
                mob->setDirection(tile->direction);
            }
        }
        return;
    }

    case Direction::NegativeY: {
        if (tile->direction == Direction::PositiveX) {
            if (mob->getTileY() < mob->getTileX()) {
                mob->setCenterY(tileY + mob->getTileX());
                mob->setDirection(tile->direction);
            }
        } else { // tile->direction = Direction::NegativeX
            if (mob->getTileY() < 1 - mob->getTileX()) {
                mob->setCenterY(tileY + (1 - mob->getTileX()));
                mob->setDirection(tile->direction);
            }
        }
        return;
    }

    case Direction::PositiveX: {
        if (tile->direction == Direction::NegativeY) {
            if (mob->getTileX() > mob->getTileY()) {
                mob->setCenterX(tileX + mob->getTileY());
                mob->setDirection(tile->direction);
            }
        } else { // tile->direction = Direction::PositiveY
            if (mob->getTileX() > 1 - mob->getTileY()) {
                mob->setCenterX(tileX + (1 - mob->getTileY()));
                mob->setDirection(tile->direction);
            }
        }
        return;
    }

    case Direction::NegativeX: {
        if (tile->direction == Direction::PositiveY) {
            if (mob->getTileX() < mob->getTileY()) {
                mob->setCenterX(tileX + mob->getTileY());
                mob->setDirection(tile->direction);
            }
        } else { // tile->direction = Direction::NegativeY
            if (mob->getTileX() < 1 - mob->getTileY()) {
                mob->setCenterX(tileX + (1 - mob->getTileY()));
                mob->setDirection(tile->direction);
            }
        }
        return;
    }

    }
}

void World::moveMobs(std::uint64_t delta) {
    for (MobPtr mob : m_Mobs) {
        TilePtr tile = getTile(mob->getCenter().getX(), mob->getCenter().getY());

        if (tile != nullptr && tile->getType() == TileType::Walk) {
            WalkableTilePtr walkTile = std::static_pointer_cast<WalkableTile>(tile);
            changeMobDirection(mob, walkTile);
        }

        if (mob->hasReachedEnemyCastle()) continue;

        moveMob(mob, delta);

        TeamColor mobTeam = m_Game->getPlayerById(mob->getSender()).getTeamColor();

        const TeamCastle* enemyCastle = nullptr;

        if (mobTeam == TeamColor::Red) {
            enemyCastle = &getBlueTeam().getCastle();
        } else if (mobTeam == TeamColor::Blue) {
            enemyCastle = &getRedTeam().getCastle();
        }

        if (isMobTouchingCastle(mob, *enemyCastle)) {
            moveBackMob(mob, *enemyCastle);
            mob->setMobReachedCastle();
        }
    }
}

void World::moveMob(MobPtr mob, std::uint64_t delta) {
    float mobWalkSpeed = mob->getStats()->getMovementSpeed();

    float walkAmount = mobWalkSpeed * ((float)delta / 1000.0f);

    if (mob->hasEffect(EffectType::Slowness))
        walkAmount *= 0.70; // walk 30% slower

    switch (mob->getDirection()) {
    case Direction::NegativeX: {
        mob->setCenterX(mob->getCenterX() - walkAmount);
        break;
    }
    case Direction::PositiveX: {
        mob->setCenterX(mob->getCenterX() + walkAmount);
        break;
    }
    case Direction::NegativeY: {
        mob->setCenterY(mob->getCenterY() - walkAmount);
        break;
    }
    case Direction::PositiveY: {
        mob->setCenterY(mob->getCenterY() + walkAmount);
        break;
    }
    }
}

bool World::isMobTouchingCastle(MobPtr mob, const TeamCastle& enemyCastle) const {
    return enemyCastle.collidesWith(*mob);
}

void World::moveBackMob(MobPtr mob, const TeamCastle& enemyCastle) {
    switch (mob->getDirection()) {
    case Direction::NegativeX: {
        mob->setCenterX(enemyCastle.getBottomRight().getX() + mob->getWidth() / 2.0f);
        break;
    }
    case Direction::PositiveX: {
        mob->setCenterX(enemyCastle.getTopLeft().getX() - mob->getWidth() / 2.0f);
        break;
    }
    case Direction::NegativeY: {
        mob->setCenterY(enemyCastle.getBottomRight().getY() + mob->getHeight() / 2.0f);
        break;
    }
    case Direction::PositiveY: {
        mob->setCenterY(enemyCastle.getTopLeft().getY() - mob->getHeight() / 2.0f);
        break;
    }
    }
}

const Color* World::getTileColor(TilePtr tile) const {
    switch (tile->getType()) {
    case TileType::Tower: {
        TowerTile* towerTile = (TowerTile*)tile.get();
        return &m_TowerPlacePalette[towerTile->color_palette_ref];
    }
    case TileType::Walk: {
        return &m_WalkablePalette;
    }
    case TileType::Decoration: {
        DecorationTile* towerTile = (DecorationTile*)tile.get();
        return &m_DecorationPalette[towerTile->color_palette_ref];
        break;
    }
    case TileType::None: {
        return nullptr;
    }
    }
    return nullptr;
}

bool World::CanPlaceLittleTower(const glm::vec2& worldPos, PlayerID playerID) const {
    TilePtr tile = getTile(worldPos.x, worldPos.y);
    const Player& player = m_Game->getPlayers()[playerID];

    if (tile == nullptr) {
        return false;
    }

    if (tile->getType() == game::TileType::Tower) {
        const TowerTile* towerTile = (const TowerTile*)tile.get();
        if (towerTile->team_owner != player.getTeamColor())
            return false;
        for (int x = -1; x < 2; x++) {
            for (int y = -1; y < 2; y++) {
                game::TilePtr adjacentTile = getTile(worldPos.x + x, worldPos.y + y);
                if (adjacentTile == nullptr || adjacentTile->getType() != game::TileType::Tower || getTower({ worldPos.x + x, worldPos.y + y }) != nullptr) {
                    return false;
                }
            }
        }
        return true;
    }

    return false;
}

bool World::CanPlaceBigTower(const glm::vec2& worldPos, PlayerID playerID) const {
    if (!CanPlaceLittleTower(worldPos, playerID)) return false;

    TilePtr tile = getTile(worldPos.x, worldPos.y);
    const Player& player = m_Game->getPlayers()[playerID];

    if (tile == nullptr) {
        return false;
    }

    if (tile->getType() == game::TileType::Tower) {
        const TowerTile* towerTile = (const TowerTile*)tile.get();
        if (towerTile->team_owner != player.getTeamColor())
            return false;
        for (int x = -2; x < 3; x++) {
            for (int y = -2; y < 3; y++) {
                game::TilePtr adjacentTile = getTile(worldPos.x + x, worldPos.y + y);
                if (adjacentTile == nullptr || adjacentTile->getType() != game::TileType::Tower || getTower({ worldPos.x + x, worldPos.y + y }) != nullptr) {
                    return false;
                }
            }
        }
        return true;
    }

    return false;
}

void World::cleanDeadMobs() {
    for (std::size_t i = 0; i < m_Mobs.size(); i++) {
        MobPtr mob = m_Mobs[i];
        if (mob->isDead()) {
            if (mob->OnDeath(this)) {
                //reward players
                Player& sender = m_Game->getPlayerById(mob->getSender());
                sender.addExp(mob->getStats()->getExpReward());

                Player& killer = m_Game->getPlayerById(mob->getLastDamageTower()->getBuilder());
                killer.addGold(mob->getStats()->getMoneyCost());

                m_Mobs.erase(m_Mobs.begin() + i);
            }
        }
    }
}

TowerPtr World::getTower(const glm::vec2& position) const {
    for (TowerPtr tower : m_Towers) {
        if (tower->getSize() == TowerSize::Big) {
            if (tower->getCenterX() - 2.5f < position.x && tower->getCenterX() + 2.5f > position.x &&
                tower->getCenterY() - 2.5f < position.y && tower->getCenterY() + 2.5f > position.y) {
                return tower;
            }
        } else { // little tower
            if (tower->getCenterX() - 1.5f < position.x && tower->getCenterX() + 1.5f > position.x &&
                tower->getCenterY() - 1.5f < position.y && tower->getCenterY() + 1.5f > position.y) {
                return tower;
            }
        }
    }
    return nullptr;
}

TowerPtr World::getTowerById(TowerID towerID) {
    auto it = std::find_if(m_Towers.begin(), m_Towers.end(), [towerID](TowerPtr tower) { return tower->getID() == towerID;});
    if (it == m_Towers.end()) return nullptr;
    return *it;
}

void World::OnArcherTowerShot(MobPtr target, ArcherTower* shooter) {
    bool fireArrows = shooter->getLevel().getPath() == TowerPath::Bottom;
    bool explosiveArrows = shooter->getLevel().getLevel() == 4 && fireArrows;

    notifyListeners(&WorldListener::OnArrowShot, target, fireArrows, shooter);
    if (explosiveArrows) {
        notifyListeners(&WorldListener::OnExplosion, utils::shape::Circle{ target->getCenterX(), target->getCenterY(), ArcherTower::ExplosionRadius }, shooter->getStats()->getDamage(), shooter);
    }
}

void World::OnArrowShot(MobPtr target, bool fireArrow, Tower* shooter) {
    notifyListeners(&WorldListener::OnMobDamage, target, shooter->getStats()->getDamage(), shooter);
    if (fireArrow) {
        target->addEffect(EffectType::Fire, ArcherTower::FireDurationSec, shooter);
    }
}

void World::OnExplosion(utils::shape::Circle explosion, float centerDamage, Tower* shooter) {
    for (MobPtr mob : m_Mobs) {
        if (mob->isAlive() && mob->collidesWith(explosion)) {
            // linear distance damage reduction
            float explosionDamage = mob->distance(explosion) / explosion.getRadius() * centerDamage;
            notifyListeners(&WorldListener::OnMobDamage, mob, explosionDamage, shooter);
        }
    }
}

void World::OnMobDamage(MobPtr target, float damage, Tower* source) {
    target->damage(damage, source);
    if (target->isDead()) {
        notifyListeners(&WorldListener::OnMobDead, target);
    }
}

Team& World::getRedTeam() {
    return m_Game->getRedTeam();
}

const Team& World::getRedTeam() const {
    return m_Game->getRedTeam();
}

Team& World::getBlueTeam() {
    return m_Game->getBlueTeam();
}

const Team& World::getBlueTeam() const {
    return m_Game->getBlueTeam();
}

Team& World::getTeam(TeamColor team) {
    return m_Game->getTeam(team);
}

const Team& World::getTeam(TeamColor team) const {
    return m_Game->getTeam(team);
}

} // namespace game
} // namespace td
