#include <World/World.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <Entity/PhysicsEntity/ItemEntity/ItemEntity.hpp>
#include <Utilities/Utilities.hpp>
#include <Block/IMachineBlock.hpp>
#include <Settings/Settings.hpp>
#include <Utilities/Async.hpp>
#include <Utilities/Perf.hpp>
#include <Log/Log.hpp>
#include <Block/SpriteBlock/ConveyorBelt/ConveyorBelt.hpp>
#include <ChunkWindow/ChunkWindow.hpp>
#include <chrono>
#include <limits>
#include <sstream>
#include <string>

bool World::isChunkReadyToDestroy(Chunk &chunk, int thisSeqNo)
{
    if (chunk.bookKeepSeqNo == thisSeqNo)
    {
        return false;
    }

    if (!chunk.isReady())
    {
        return false;
    }

    if (!canPurgeChunk(chunk))
    {
        return false;
    }

    if (!chunk.purgeVoxels())
    {
        return false;
    }

    return true;
}

void World::onBlockTableLoaded(const std::vector<std::string> &table)
{
    blockTable = table;

    for (std::string name : blockTable)
    {
        blocks.push_back(&blockRegistry.getBlockByName(name));
    }
}

void World::advanceBeltViews()
{
    std::unique_lock<std::shared_mutex> lock(beltViewMutex);

    for (auto &[id, beltView] : beltViews)
    {
        if (beltView.isActive)
        {
            beltView.advance();
        }
    }
}

void World::assignBeltViewSprite(uint64_t id)
{
    if (window == nullptr)
    {
        return;
    }

    if (beltViewSpriteIds.contains(id))
    {
        return;
    }

    beltViewSpriteIds[id] = window->svo.createNode(GLSL_SVO_VOXEL_AIR);
}

void World::eraseBeltViewSprite(uint64_t id)
{
    if (window == nullptr)
    {
        return;
    }

    auto it = beltViewSpriteIds.find(id);
    window->svo.releaseNode(it->second);
    beltViewSpriteIds.erase(it);
}

void World::onBlockGridUpdated(glm::ivec3 pos, int id, uint8_t meta)
{
    // NOP
}

World::World() :
    randomGenerator(std::chrono::system_clock::now().time_since_epoch().count()),
    worldGrid(*this),
    timeOfDay(60 * 60),
    shuttingDown(false),
    weather(*this)
{}

void World::doShutdownSanityChecks()
{
    if (numLoadedChunks != 0)
    {
        crash(
            formatString(
                "Sanity check failed: "
                "The world has been closed and fully purged, and yet $+ chunks are still loaded!",
                numLoadedChunks.load()
            )
        );
    }
}

std::shared_ptr<Chunk> World::getChunk(glm::ivec3 pos, std::shared_ptr<WorldGridNode> hint)
{
    return worldGrid.getChunk(pos, hint);
}

ChunkHandle World::requestChunk(glm::ivec3 base, std::shared_ptr<WorldGridNode> hint, float relativeDeadline)
{
    return worldGrid.requestChunk(base, hint, relativeDeadline);
}

Block& World::getBlockById(int id)
{
    if (id < 0 || id >= (int) blocks.size())
    {
        return blockRegistry.getAirBlock();
    }

    return *blocks[id];
}

Block* World::getBlockAt(glm::ivec3 pos)
{
    glm::ivec3 chunkBase = pos & glm::ivec3(~0xF);
    glm::ivec3 chunkOffset = pos & glm::ivec3(0xF);

    ChunkHandle chunk = requestChunk(chunkBase);
    if (!chunk->isReady())
    {
        return nullptr;
    }

    int id = std::get<0>(chunk->getBlockAndMeta(chunkOffset));
    return &getBlockById(id);
}

std::optional<uint8_t> World::getBlockMeta(glm::ivec3 pos)
{
    glm::ivec3 chunkBase = pos & glm::ivec3(~0xF);
    glm::ivec3 chunkOffset = pos & glm::ivec3(0xF);

    ChunkHandle chunk = requestChunk(chunkBase);
    if (!chunk->isReady())
    {
        return std::nullopt;
    }

    return std::get<1>(chunk->getBlockAndMeta(chunkOffset));
}

World::BlockHit World::castRay(wfvec3 start, wfvec3 dir, double maxDistance)
{
    BlockHit hit;
    hit.coords = glm::floor(start);
    hit.hasHit = false;

    glm::dvec3 tDelta = glm::dvec3(1.0) / glm::abs(glm::dvec3(dir));
    
    glm::ivec3 step;
    glm::dvec3 tMax;

    for (int i = 0; i < 3; i++)
    {
        step[i] = dir[i] > wfixed(0) ? 1 : -1;

        if (dir[i] != wfixed(0))
        {
            tMax[i] = ((wfixed) (step[i] > 0 ? (hit.coords[i] + 1) : hit.coords[i]) - start[i]) / dir[i];
        }
        else
        {
            tMax[i] = std::numeric_limits<double>::infinity();
        }
    }

    constexpr double epsilon = 1e-5;
    double currentDistance = 0.0;

    while (!hit.hasHit && currentDistance < maxDistance)
    {
        static const glm::ivec3 axisNumbers = glm::ivec3(0, 2, 4);

        glm::ivec3 traversal = glm::ivec3(
            tMax.x < tMax.y + epsilon && tMax.x < tMax.z + epsilon,
            tMax.x >= tMax.y + epsilon && tMax.y < tMax.z + epsilon,
            0
        );

        traversal.z = !traversal.x && !traversal.y;

        glm::ivec3 sideVec = traversal * (axisNumbers | ((step + glm::ivec3(1)) >> 1) ^ glm::ivec3(1));

        hit.coords += glm::uvec3(step * traversal);
        hit.side = Side(sideVec.x + sideVec.y + sideVec.z);
        currentDistance = tMax[hit.side.axis()];
        tMax += tDelta * glm::dvec3(traversal);

        Block *block = getBlockAt(hit.coords);
        if (block == nullptr)
        {
            // Cannot cast a ray through unloaded chunks.
            break;
        }

        hit.collisionPoint = start + dir * wfixed(currentDistance);
        hit.hasHit = block->isIntersectedByRay(*this, hit.coords, hit.collisionPoint, dir, hit.side, hit.collisionKey);
    }

    return hit;
}

bool World::isShutdownReady()
{
    return true;
}

std::vector<std::string>& World::getBlockTable()
{
    return blockTable;
}

GeneratorNode::ID World::generateRandom()
{
    GeneratorNode::ID result = randomGenerator[0];
    randomGenerator = randomGenerator(1);
    return result;
}

bool World::probably(int chance)
{
    return (int)(generateRandom() % 100) < chance;
}

void World::housekeep()
{
    int thisSeqNo = currentBookKeepSeqNo++;

    int maxToPurge = maxChunksPurgedPerUpdate << (purgeQueue.size() / 1000);
    if (purgeQueue.size() > 7000)
    {
        maxToPurge = purgeQueue.size();
    }

    size_t numPutAway = 0;

    int i;
    for (i=0; i<maxToPurge; i++)
    {
        if (purgeQueue.size() == numPutAway)
        {
            return;
        }

        std::shared_ptr<Chunk> &chunk = purgeQueue.front();
        if (chunk->retainCount != 0)
        {
            chunk->isInPurgeQueue = false;
            purgeQueue.pop_front();
            i--;
            continue;
        }

        if (!isChunkReadyToDestroy(*chunk, thisSeqNo))
        {
            purgeQueue.push_back(chunk);
            purgeQueue.pop_front();
            numPutAway++;
            i--;
            continue;
        }

        chunk->isInPurgeQueue = false;
        chunk->purged = true;

        std::shared_ptr<WorldGridNode> node = chunk->getNode();
        node->purge();
        numLoadedChunks--;

        purgeQueue.pop_front();
    }
}

void World::dumpPurgeQueue()
{
    Log::debug(formatString("There are $+ chunks in the purge queue", purgeQueue.size()));
    for (std::shared_ptr<Chunk> &chunk : purgeQueue)
    {
        std::stringstream ss;
        ss << "Chunk based at " << chunk->base << ":" << std::endl;
        ss << "  Retain count: " << chunk->retainCount << std::endl;
        ss << "  Ready? " << chunk->isReady() << std::endl;
        ss << "  Allowed to purge? " << canPurgeChunk(*chunk) << std::endl;
        ss << "  Meshes purged? " << chunk->purgeVoxels() << std::endl;
        ss << std::endl;

        Log::debug(ss.str());
    }
}

void World::drawBelts(WorldDisplay &worldDisplay, const Frustum &viewingFrustum)
{
    std::shared_lock<std::shared_mutex> lock(beltViewMutex);

    for (auto &[id, beltView] : beltViews)
    {
        beltView.draw(worldDisplay, viewingFrustum);
    }
}

void World::updateBeltAnimations(WorldDisplay &worldDisplay, const Frustum &viewingFrustum)
{
    std::shared_lock<std::shared_mutex> lock(beltViewMutex);

    for (auto &[id, beltView] : beltViews)
    {
        auto it = beltViewSpriteIds.find(id);
        VOXELVERSE_ASSERT(it != beltViewSpriteIds.end());
        beltView.updateAnimation(worldDisplay, viewingFrustum, beltViewSpriteIds[id]);
    }
}

void World::markChunkDirty(glm::ivec3 base)
{
    std::shared_ptr<Chunk> chunk = getChunk(base);
    if (chunk != nullptr)
    {
        chunk->dirty = true;
    }
}

int World::getBlockId(std::string name)
{
    std::vector<std::string>::iterator it = std::find(blockTable.begin(), blockTable.end(), name);

    return (int) (it - blockTable.begin());
}

bool World::isChunkLoaded(glm::ivec3 pos)
{
    std::shared_ptr<Chunk> chunk = getChunk(pos & glm::ivec3(~0xF));
    return chunk != nullptr && chunk->isReady();
}

int World::getNumLoadedChunks()
{
    return numLoadedChunks;
}

int World::getTimeOfDay()
{
    return timeOfDay;
}

void World::setTimeOfDay(int time)
{
    timeOfDay = time;
}

float World::getSolarAngle()
{
    return 2.0f * 3.1415f * (float) getTimeOfDay() / (float) cycleDuration;
}

float World::getSunIntensity()
{
    return std::max(0.0f, std::sin(getSolarAngle()));
}

float World::getPlayerLightIntensity(Player &player)
{
    return 0.009f;
}

glm::vec3 World::getSkyColor()
{
    float intensity = getSunIntensity();
    float orangeFactor = std::abs(std::pow(std::cos(getSolarAngle()), 20.0f));

    return glm::vec3(
        intensity * 0.05f + 0.07f * orangeFactor,
        intensity * 0.4f + 0.01f * orangeFactor,
        intensity * 0.95f
    );
}

glm::vec3 World::getAmbientColor()
{
    float intensity = std::max(0.0f, std::sin(getSolarAngle()));
    float orangeFactor = std::abs(std::pow(std::cos(getSolarAngle()), 20.0f));

    glm::vec3 color = glm::vec3(
        intensity * 0.8f + 0.3f * orangeFactor,
        intensity * 0.8f + 0.02f * orangeFactor,
        intensity * 0.8f
    );

    glm::vec3 grayscale = glm::vec3((color.x + color.y + color.z) / 3.0f);

    float cloudCover = weather.getCloudCover();
    float lightness = glm::mix(1.0f, 0.3f, cloudCover);

    return glm::mix(color, grayscale * lightness, cloudCover);
}

std::string World::getFatalError()
{
    return fatalError;
}

void World::deliverEntityMessage(uint64_t id, const void *msg, size_t size)
{
    if (entities.count(id) == 1)
    {
        std::shared_ptr<Entity> entity = entities[id];
        entity->onMessage(msg, size);
    }
}

void World::onPlayerUpdateBegins(Player &player)
{
    const BoundingBox<cint32> playerBox = player.getViewBox();
    player.view.setRetainBox(playerBox);
}

void World::forEachEntity(std::function<void(std::shared_ptr<Entity>)> callback)
{
    for (const auto& [id, entity] : entities)
    {
        callback(entity);
    }
}

void World::drawPlayerModel(GraphicsContext &context, WorldDisplay &worldDisplay, Player &player)
{
    // NOP
}

void World::drawBlockSelector(GraphicsContext &context, WorldDisplay &worldDisplay, Player &player)
{
    // NOP
}

void World::writeDebugStats(std::ostream &os)
{
    // NOP
}

void World::writeBlockDebugInfo(std::ostream &os, glm::ivec3 pos)
{
    // NOP
}

size_t World::allocView()
{
    size_t i;
    for (i=0; i<viewInfos.size(); i++)
    {
        if (!viewInfos[i].inUse)
        {
            viewInfos[i].inUse = true;
            viewInfos[i].lastKnownBox = std::nullopt;
            return i;
        }
    }

    size_t newIndex = viewInfos.size();
    viewInfos.push_back({true, std::nullopt});
    return newIndex;
}

void World::deallocView(size_t index)
{
    ViewInfo &info = viewInfos[index];

    if (info.lastKnownBox.has_value())
    {
        worldGrid.updateChunkCountsWithinBox(*this, *info.lastKnownBox, -1, [](Chunk&){});
    }

    info.inUse = false;
}

void World::setRetainBox(
    size_t viewIndex,
    const BoundingBox<cint32> &box,
    const std::function<void(Chunk&)> &onNewlyVisible
)
{
    ViewInfo &info = viewInfos[viewIndex];
    if (!info.lastKnownBox.has_value())
    {
        worldGrid.updateChunkCountsWithinBox(*this, box, 1, onNewlyVisible);
        info.lastKnownBox = box;
        return;
    }

    if (*info.lastKnownBox != box)
    {
        worldGrid.updateChunkCountsWithinBox(*this, box, 1, [&info, &onNewlyVisible, &box](Chunk &chunk)
        {
            BoundingBox<cint32> chunkBox{
                chunk.base.x, chunk.base.y, chunk.base.z,
                chunk.base.x + 16, chunk.base.y + 16, chunk.base.z + 16,
            };

            if (!info.lastKnownBox->overlaps(chunkBox) && box.overlaps(chunkBox))
            {
                onNewlyVisible(chunk);
            }
        });

        worldGrid.updateChunkCountsWithinBox(*this, *info.lastKnownBox, -1, [](Chunk&){});
        info.lastKnownBox = box;
        return;
    }
}

void World::enqueueForPurge(std::shared_ptr<Chunk> chunk)
{
    chunk->bookKeepSeqNo = currentBookKeepSeqNo;

    if (chunk->isInPurgeQueue)
    {
        return;
    }
    
    chunk->isInPurgeQueue = true;
    purgeQueue.push_back(chunk);
}

size_t World::getPurgeQueueSize()
{
    return purgeQueue.size();
}

void World::beginChunkLoad(std::shared_ptr<Chunk> chunk, float relativeDeadline)
{
    numLoadedChunks++;
    onChunkRequested(chunk, relativeDeadline);
}

std::shared_ptr<WorldGridNode> World::getRootNode()
{
    return worldGrid.getRootNode();
}

void World::onWalkingOverBelt(glm::ivec3 pos, PhysicsEntity &entity)
{
    std::shared_lock<std::shared_mutex> lock(beltViewMutex);

    for (auto &[id, beltView] : beltViews)
    {
        if (beltView.isActive)
        {
            if (beltView.state.getBlockOffset(pos).has_value())
            {
                entity.restVel = glm::dvec3(beltView.state.dir.out()) * ConveyorBelt::entityPushSpeed;
            }
        }
    }
}

uint64_t World::getBeltSprite(glm::ivec3 pos)
{
    std::shared_lock<std::shared_mutex> lock(beltViewMutex);

    for (auto &[id, beltView] : beltViews)
    {
        if (beltView.state.getBlockOffset(pos).has_value())
        {
            auto it = beltViewSpriteIds.find(id);
            if (it == beltViewSpriteIds.end())
            {
                crash("World::getBeltSprite: A sprite was not allocated to an existing belt.");
            }

            return it->second;
        }
    }

    return 0;
}

void World::onWindowInitDone()
{
    for (const auto &[id, _] : beltViews)
    {
        assignBeltViewSprite(id);
    }
}

void World::getWorldStats(StatList &statList)
{
    // NOP
}

std::string World::getTypingIndicatorString()
{
    if (typingPlayerNames.size() == 0)
    {
        return "";
    }

    std::vector typingPlayerNames(this->typingPlayerNames.begin(), this->typingPlayerNames.end());

    std::stringstream ss;
    if (typingPlayerNames.size() == 1)
    {
        ss << typingPlayerNames[0] << " is typing...";
    }
    else if (typingPlayerNames.size() == 2)
    {
        ss << typingPlayerNames[0] << " and " << typingPlayerNames[1] << " are typing...";
    }
    else
    {
        ss << typingPlayerNames[0] << ", " << typingPlayerNames[1] << ", and " << (typingPlayerNames.size() - 2) << " others are typing...";
    }

    return ss.str();
}