#pragma once

#include <memory>
#include <vector>
#include <map>
#include <unordered_map>
#include <glm/glm.hpp>
#include <array>

#include "Mobs.h"
#include "Team.h"

#include "misc/ObjectNotifier.h"

namespace td {
namespace game {
typedef std::pair<std::int16_t, std::int16_t> ChunkCoord;
}
}
namespace std {
template <>
struct hash<td::game::ChunkCoord> {
    std::size_t operator()(const td::game::ChunkCoord& key) const {
        // Compute individual hash values for first,
        // second and third and combine them using XOR
        // and bit shifting:
        return ((std::hash<std::int16_t>()(key.first) ^ (std::hash<std::int16_t>()(key.second) << 1)) >> 1);
    }
};
}

namespace td {

namespace protocol {

class WorldBeginDataPacket;
class WorldDataPacket;

}

namespace game {

class Game;

enum class TileType : std::uint8_t {
    None = 0,
    Tower,
    Walk,
    Decoration,
    /*Heal,
    Lava,
    Bedrock,
    Freeze,
    Ice,*/
};

struct Color {
    std::uint8_t r, g, b;
};

struct Tile {
    virtual TileType getType() const = 0;
};

struct TowerTile : Tile {
    std::uint8_t color_palette_ref;
    TeamColor team_owner;

    virtual TileType getType() const { return TileType::Tower; }
};

struct WalkableTile : Tile {
    Direction direction;

    virtual TileType getType() const { return TileType::Walk; }
};

struct DecorationTile : Tile {
    std::uint16_t color_palette_ref;

    virtual TileType getType() const { return TileType::Decoration; }
};

typedef std::shared_ptr<Tile> TilePtr;
typedef std::vector<std::uint16_t> ChunkPalette;

typedef std::shared_ptr<WalkableTile> WalkableTilePtr;

typedef std::array<std::uint16_t, 32 * 32> ChunkData;
typedef std::uint32_t TileIndex;

//32 x 32 area
struct Chunk {
    enum { ChunkWidth = 32, ChunkHeight = 32, ChunkSize = ChunkWidth * ChunkHeight };
    // stores index of tile palette
    ChunkData tiles{ 0 };
    ChunkPalette palette;

    TileIndex getTileIndex(std::uint16_t tileNumber) const {
        TileIndex chunkPaletteIndex = tiles.at(tileNumber);
        if (chunkPaletteIndex == 0)  // index 0 means empty tile     index 1 = first tile
            return 0;
        return palette.at(chunkPaletteIndex);
    }
};

typedef std::shared_ptr<Chunk> ChunkPtr;

typedef std::array<Color, 2> TowerTileColorPalette;

typedef std::vector<TilePtr> TilePalette;

typedef std::vector<MobPtr> MobList;

typedef std::array<Color, 2> SpawnColorPalette;

typedef std::vector<TowerPtr> TowerList;

class WorldListener {
public:
    WorldListener() {}

    virtual void OnTowerAdd(TowerPtr tower) {}
    virtual void OnTowerRemove(TowerPtr tower) {}

    virtual void OnArcherTowerShot(MobPtr target, ArcherTower* shooter) {}

    virtual void OnArrowShot(MobPtr target, bool fire, Tower* shooter) {}
    virtual void OnExplosion(utils::shape::Circle explosion, float centerDamage, Tower* shooter) {}

    virtual void OnMobDamage(MobPtr target, float damage, Tower* damager) {}

    virtual void OnMobDead(MobPtr mob) {}
};

typedef utils::ObjectNotifier<WorldListener> WorldNotifier;

class World : public WorldNotifier, public WorldListener {
protected:
    TowerTileColorPalette m_TowerPlacePalette;
    Color m_WalkablePalette;
    std::vector<Color> m_DecorationPalette;

    std::unordered_map<ChunkCoord, ChunkPtr> m_Chunks;

    SpawnColorPalette m_SpawnColorPalette;

    TilePalette m_TilePalette;

    MobList m_Mobs;

    TowerList m_Towers;

    Game* m_Game;
public:
    World(Game* game);

    static constexpr std::uint8_t CastleWidth = 5, CastleHeight = 5;

    bool loadMap(const protocol::WorldBeginDataPacket* worldHeader);
    bool loadMap(const protocol::WorldDataPacket* worldData);

    bool loadMapFromFile(const std::string& fileName);
    bool saveMap(const std::string& fileName) const;

    void tick(std::uint64_t delta);

    void spawnMobAt(MobID id, MobType type, std::uint8_t level, PlayerID sender, float x, float y, Direction dir);

    TowerPtr placeTowerAt(TowerID id, TowerType type, std::int32_t x, std::int32_t y, PlayerID builder);

    TilePtr getTile(std::int32_t x, std::int32_t y) const;

    const TowerTileColorPalette& getTowerTileColorPalette() const { return m_TowerPlacePalette; }
    const Color& getWalkableTileColor() const { return m_WalkablePalette; }
    const std::vector<Color>& getDecorationPalette() const { return m_DecorationPalette; }

    const TilePalette& getTilePalette() const { return m_TilePalette; }

    TilePtr getTilePtr(TileIndex index) const {
        if (index == 0)
            return nullptr;
        return m_TilePalette.at(index - 1);
    }

    bool CanPlaceLittleTower(const glm::vec2& worldPos, PlayerID player) const;
    bool CanPlaceBigTower(const glm::vec2& worldPos, PlayerID player) const;

    TowerPtr getTower(const glm::vec2& position) const; // returns null if no tower is here

    const std::unordered_map<ChunkCoord, ChunkPtr>& getChunks() const { return m_Chunks; }

    const Color& getSpawnColor(TeamColor color) const { return m_SpawnColorPalette[(std::size_t)color]; }
    const SpawnColorPalette& getSpawnColors() const { return m_SpawnColorPalette; }

    const MobList& getMobList() const { return m_Mobs; }
    MobList& getMobList() { return m_Mobs; }

    const Color* getTileColor(TilePtr tile) const;

    Team& getRedTeam();
    const Team& getRedTeam() const;

    Team& getBlueTeam();
    const Team& getBlueTeam() const;

    Team& getTeam(TeamColor team);
    const Team& getTeam(TeamColor team) const;

    const TowerList& getTowers() const { return m_Towers; };
    TowerPtr getTowerById(TowerID tower);

    // WorldListener

    virtual void OnArcherTowerShot(MobPtr target, ArcherTower* shooter);

    virtual void OnArrowShot(MobPtr target, bool fire, Tower* shooter);
    virtual void OnExplosion(utils::shape::Circle explosion, float centerDamage, Tower* shooter);

    virtual void OnMobDamage(MobPtr target, float damage, Tower* source);
private:
    void moveMobs(std::uint64_t delta);
    void moveMob(MobPtr mob, std::uint64_t delta);
    void moveBackMob(MobPtr mob, const TeamCastle& castle);
    void changeMobDirection(MobPtr mob, WalkableTilePtr tile);
    bool isMobTouchingCastle(MobPtr mob, const TeamCastle& castle) const;
    void tickMobs(std::uint64_t delta);
    void cleanDeadMobs();
};

} // namespace game
} // namespace td
