#include "TileMap.h"
#include <stdexcept>
#include <iostream>

TileMap::TileMap(const std::string& mapName, int wChunks, int hChunks)
                : name(mapName), mapWidthInChunks(wChunks), mapHeightInChunks(hChunks)
{
    if (wChunks <= 0 || hChunks <=0)
    {
        throw std::invalid_argument("TileMap dimensions must be positive");
    }
}

Chunk* TileMap::GetChunk(int worldChunkX, int worldChunkY)
{
    if (worldChunkX < 0 || worldChunkX >= mapWidthInChunks ||
        worldChunkY < 0 || worldChunkY >= mapHeightInChunks)
    {
        return nullptr; // Out of bounds
    }
    
    uint64_t key = MakeKey(worldChunkX, worldChunkY);
    
    auto it = chunks.find(key);
    if (it == chunks.end())
    {
        // Lazy chunk creation -- only allocate when first accessed
        auto [newIt, inserted] = chunks.emplace(key, Chunk(worldChunkX, worldChunkY));
        return &newIt->second;
    }
    
    return &it->second;
}

const Chunk* TileMap::GetChunk(int worldChunkX, int worldChunkY) const
{
    // Const version delegates to non-const version using const_cast
    return const_cast<TileMap*>(this)->GetChunk(worldChunkX, worldChunkY);
}

// Convert world tile coords into chunk+local coords
// Returns pointer to the Tile or nullptr if out of bounds
Tile* TileMap::GetTile(int worldTileX, int worldTileY)
{
    // Convert world tile position to chunk position + local position inside chunk
    int chunkX = worldTileX / CHUNK_SIZE;
    int chunkY = worldTileY / CHUNK_SIZE;
    
    int localX = worldTileX % CHUNK_SIZE;
    int localY = worldTileY % CHUNK_SIZE;
    
    // Handle negative coords
    if (worldTileX < 0)
    {
        chunkX--;
        localX = CHUNK_SIZE + (worldTileX % CHUNK_SIZE);
    }
    if (worldTileY < 0)
    {
        chunkY--;
        localY = CHUNK_SIZE + (worldTileY % CHUNK_SIZE);
    }
    
    Chunk* chunk = GetChunk(chunkX, chunkY);
    if (!chunk)
    {
        return nullptr;
    }
    
    return &chunk->GetTile(localX, localY);
}

const Tile* TileMap::GetTile(int worldTileX, int worldTileY) const
{
    return const_cast<TileMap*>(this)->GetTile(worldTileX, worldTileY);
}

// Set tile at world coordinates
// Create chunk if necessary
void TileMap::SetTile(int worldTileX, int worldTileY, const Tile& tile)
{
    int chunkX = worldTileX / CHUNK_SIZE;
    int chunkY = worldTileY / CHUNK_SIZE;
    
    int localX = worldTileX % CHUNK_SIZE;
    int localY = worldTileY % CHUNK_SIZE;
    
    if (worldTileX < 0)
    {
        chunkX--;
        localX = CHUNK_SIZE + (worldTileX % CHUNK_SIZE);
    }
    if (worldTileY < 0)
    {
        chunkY--;
        localY = CHUNK_SIZE + (worldTileY % CHUNK_SIZE);
    }
    
    Chunk* chunk = GetChunk(chunkX, chunkY);
    if (chunk)
    {
        chunk->SetTile(localX, localY, tile);
    }
}

// Returns list of chunks that intersects with camera rectangle
// i.e. only draw visible chunks
std::vector<Chunk*> TileMap::GetVisibleChunks(int cameraWorldX, int cameraWorldY,
                                              int cameraWidth, int cameraHeight)
{
    std::vector<Chunk*> visible;
    
    // Convert camera coords to chunk coords
    int startChunkX = cameraWorldX / CHUNK_SIZE;
    int startChunkY = cameraWorldY / CHUNK_SIZE;
    
    int endChunkX = (cameraWorldX + cameraWidth + CHUNK_SIZE - 1) / CHUNK_SIZE;
    int endChunkY = (cameraWorldY + cameraHeight + CHUNK_SIZE - 1) / CHUNK_SIZE;
    
    // Small margin to avoid popping at edges
    startChunkX -= 1;
    startChunkY -= 1;
    endChunkX += 1;
    endChunkY += 1;
    
    for (int cy = startChunkY; cy <= endChunkY; ++cy)
    {
        for (int cx = startChunkX; cx <= endChunkX; ++cx)
        {
            Chunk* chunk = GetChunk(cx, cy);
            if (chunk && chunk->IsValid())
            {
                visible.push_back(chunk);
            }
        }
    }
    
    return visible;
}

void TileMap::PrintInfo() const
{
    std::cout << "[TileMap] Map: " << name
              << " | Size: " << mapWidthInChunks << "x" << mapHeightInChunks
              << " chunks | Loaded chunks: " << chunks.size() << "\n";
}