/********************\
|  Copyright 2024,   |
|       Ulysse Cura  |
\********************/

////////////////////////////////////////
//                                    //
//   Definition de la classe TileMap. //
//                                    //
////////////////////////////////////////

#include <iostream>
#include <stdexcept>
#include "Camera.hpp"
#include "Game.hpp"
#include "MapManager.hpp"
#include "externLibs/nlohmann/json.hpp"
#include "TextureManager.hpp"
#include "TileMap.hpp"
#include "Vector2D.hpp"
#include "ECS/Components.hpp"

using std::string, std::exception, std::vector, std::cerr, std::runtime_error;
using json = nlohmann::json;

// Load a map from memory
void TileMap::LoadTileMap(const string &path)
{
    // Load the map data from memory
    json *mapData;
    mapData = Game::mapManager.LoadMap(path);

    // Get tilemap width and height same for world
    tileMapWidth = mapData->at("TileMapWidth").get<int>();
    tileMapHeight = mapData->at("TileMapHeight").get<int>();

    worldWidth = tileMapWidth * TILE_SIZE * TILEMAP_SCALE;
    worldHeight = tileMapHeight * TILE_SIZE * TILEMAP_SCALE;

    // Clear layers and hitboxes and resize them
    m_tilesLayer1.clear();
    m_tilesLayer2.clear();
    m_tilesLayer3.clear();

    hitboxes.clear();

    m_tilesLayer1.resize(tileMapWidth * tileMapHeight);
    m_tilesLayer2.resize(tileMapWidth * tileMapHeight);
    m_tilesLayer3.resize(tileMapWidth * tileMapHeight);

    hitboxes.resize(tileMapWidth * tileMapHeight);

    // Load layers and hitboxes from map data
    string layers[] {"Layer 1","Layer 2","Layer 3", "Hitboxes"};

    for(const string &layer : layers)
    {
        for(int y {0}; y < tileMapHeight; y++)
        {
            for(int x {0}; x < tileMapWidth; x++)
            {
                TileID tileID {mapData->at(layer).at(y).at(x)};

                if(layer == "Layer 1")
                {
                    m_tilesLayer1[x + y * tileMapWidth] = tileID;
                }
                else if(layer == "Layer 2")
                {
                    m_tilesLayer2[x + y * tileMapWidth] = tileID;
                }
                else if(layer == "Layer 3")
                {
                    m_tilesLayer3[x + y * tileMapWidth] = tileID;
                }
                else if(layer == "Hitboxes")
                {
                    hitboxes[x + y * tileMapWidth] = tileID;
                }
            }
        }
    }

    if(mapData->contains("PlayerInitPos"))
    {
        m_playerInitPos.x = mapData->at("PlayerInitPos").at("x").get<float>() * TILE_SIZE * TILEMAP_SCALE;
        m_playerInitPos.y = mapData->at("PlayerInitPos").at("y").get<float>() * TILE_SIZE * TILEMAP_SCALE;
    }

    m_nextMaps.clear();
    for(auto it {mapData->at("NextMaps").begin()}; it < mapData->at("NextMaps").end(); it++)
    {
        NextMap nextMap;
        nextMap.path = it->at("Path").get<string>();
        nextMap.playerInitPos.x = it->at("PlayerInitPos").at("x").get<float>();
        nextMap.playerInitPos.y = it->at("PlayerInitPos").at("y").get<float>();

        m_nextMaps.emplace(it->at("Number").get<int>(), nextMap);
    }

    // Erase lasts entities and load news if there are some
    std::size_t numberOfEntity {Game::entityManager.getNumberOfEntity()};

    for(std::size_t i {1}; i < numberOfEntity; i++) Game::entityManager.erase(i);

    if(mapData->contains("Entities"))
    {
        for(auto it {mapData->at("Entities").begin()}; it < mapData->at("Entities").end(); it++)
        {
            Entity &e(Game::entityManager.addEntity());

            for(auto component {it->begin()}; component < it->end(); component++)
            {
                if(component->at("Type").get<string>() == "TransformComponent")
                {
                    Vector2D<float> pos {Vector2D<float>(component->at("Position").at("x").get<float>(), component->at("Position").at("y").get<float>())};
                    Vector2D<float> dim {Vector2D<float>(component->at("Dimension").at("x").get<float>(), component->at("Dimension").at("y").get<float>())};

                    pos = pos * TILE_SIZE * TILEMAP_SCALE;

                    e.addComponent<TransformComponent>(pos, dim, component->at("Scale").get<int>(), component->at("Speed").get<int>());
                }
                else if(component->at("Type").get<string>() == "SpriteComponent")
                {
                    e.addComponent<SpriteComponent>(component->at("Path").get<string>());
                }
                else if(component->at("Type").get<string>() == "AnimationSystem")
                {
                    int nbFrames {component->at("NbFrames").get<int>()};
                    int currentFrame {component->at("CurrentFrame").get<int>()};
                    int frameDelay {component->at("FrameDelay").get<int>()};
                    bool playAnimation {component->at("PlayAnimation").get<bool>()};

                    e.addComponent<AnimationSystem>(nbFrames, currentFrame, frameDelay, playAnimation);
                }
                else if(component->at("Type").get<string>() == "HitboxComponent")
                {
                    Vector2D<float> sc {Vector2D<float>(component->at("Scale").at("x").get<float>(), component->at("Scale").at("y").get<float>())};
                    Vector2D<float> pos {Vector2D<float>(component->at("Position").at("x").get<float>(), component->at("Position").at("y").get<float>())};

                    e.addComponent<HitboxComponent>(sc, pos);
                }
                else if(component->at("Type").get<string>() == "Lever")
                {
                    e.addComponent<Lever>(component->at("Channel").get<int>());
                }
                else if(component->at("Type").get<string>() == "Lock")
                {
                    e.addComponent<Lock>(component->at("Channel").get<int>());
                }
                else if(component->at("Type").get<string>() == "Door")
                {
                    unordered_map<int, bool> states {};

                    for(auto state {component->at("States").begin()}; state < component->at("States").end(); state++)
                        states[state->at("Channel").get<int>()] = state->at("State").get<bool>();

                    e.addComponent<Door>(states);
                }
            }
        }
    }

    // Texture renderer creating
    Game::textureRenderer = SDL_CreateTexture(Game::renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, worldWidth, worldHeight);
    if(Game::textureRenderer == NULL)
    {
        cerr << "Erreur SDL_CreateTexture : " << SDL_GetError() << '\n';
        throw runtime_error("Impossible de creer la texture de rendu.\n");
    }
}

// Load the corresponding next map from memory
void TileMap::LoadNextMap(int nextMapNumber)
{
    NextMap nextMap = m_nextMaps.at(nextMapNumber);

    LoadTileMap(nextMap.path);

    m_playerInitPos = nextMap.playerInitPos * TILE_SIZE * TILEMAP_SCALE;
}

// Load a tileset
void TileMap::LoadTileset(const string &path)
{
    m_tileset = Game::textureManager.LoadTexture(path);

    SDL_QueryTexture(m_tileset, nullptr, nullptr, &m_tilesetWidth, &m_tilesetHeight);

    m_tilesetWidth  /= TILE_SIZE;
    m_tilesetHeight /= TILE_SIZE;
}

// Draw corresponding layer
void TileMap::draw(int layer)
{
    if(layer == 1)
    {
        m_draw(m_tilesLayer1);
    }
    else if(layer == 2)
    {
        m_draw(m_tilesLayer2);
    }
    else if(layer == 3)
    {
        m_draw(m_tilesLayer3);
    }
    else
    {
        cerr << "Erreur : La tileMap " << layer << " n'est pas prise en charge.\n";
        throw runtime_error("Impossible de dessiner la tileMap.\n");
    }
}

// Draw the layer given
void TileMap::m_draw(const vector<TileID> &tiles)
{
    SDL_Rect visibleTilesR;
    visibleTilesR.x = Game::camera.camR.x / (TILE_SIZE * TILEMAP_SCALE);
    visibleTilesR.y = Game::camera.camR.y / (TILE_SIZE * TILEMAP_SCALE);
    visibleTilesR.w = (Game::camera.camR.w + Game::camera.camR.x) / (TILE_SIZE * TILEMAP_SCALE);
    visibleTilesR.h = (Game::camera.camR.h + Game::camera.camR.y) / (TILE_SIZE * TILEMAP_SCALE);

    for(int tileY {visibleTilesR.y}; tileY <= visibleTilesR.h; tileY++)
    {
        for(int tileX {visibleTilesR.x}; tileX <= visibleTilesR.w; tileX++)
        {
            const TileID tile = tiles[tileX + tileY * tileMapWidth] - 1;

            if(tile)
            {
                SDL_Rect srcR {(tile % m_tilesetWidth) * TILE_SIZE, (tile / m_tilesetWidth) * TILE_SIZE, TILE_SIZE, TILE_SIZE};
                SDL_Rect dstR {tileX * TILE_SIZE * TILEMAP_SCALE, tileY * TILE_SIZE * TILEMAP_SCALE, TILE_SIZE * TILEMAP_SCALE, TILE_SIZE * TILEMAP_SCALE};

                Game::textureManager.Draw(m_tileset, srcR, dstR, false);
            }
        }
    }
}

// Get loaded player inital position
Vector2D<float> TileMap::getPlayerInitPos()
{
    return m_playerInitPos;
}

// Destroy texture renderer
TileMap::~TileMap()
{
    if(Game::textureRenderer != NULL) SDL_DestroyTexture(Game::textureRenderer);
}