#include <Item/Item.hpp>
#include <MachineComponent/ChunkMonitorComponent/ChunkMonitorComponent.hpp>
#include <Milestone/DiggingDeeperMilestone/DiggingDeeperMilestone.hpp>
#include <Utilities/Parse.hpp>
#include <Widget/Builder/Builder.hpp>
#include <Widget/Label/Label.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <Utilities/Utilities.hpp>
#include <Log/Log.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <Settings/Settings.hpp>
#include <Machine/Machine.hpp>
#include <Serialize/Serialize.hpp>
#include <Utilities/Async.hpp>
#include <Entity/PhysicsEntity/ItemEntity/ItemEntity.hpp>
#include <Block/IMachineBlock.hpp>
#include <WorldIndex/WorldIndex.hpp>
#include <WorldType/WorldType.hpp>
#include <WorldTypeRegistry/WorldTypeRegistry.hpp>
#include <ChunkWindow/ChunkWindow.hpp>
#include <ChatCommand/TpCommand/TpCommand.hpp>
#include <ChatCommand/TimeCommand/TimeCommand.hpp>
#include <ChatCommand/SetCommand/SetCommand.hpp>
#include <ChatCommand/GiveMeCommand/GiveMeCommand.hpp>
#include <ChatCommand/NoclipCommand/NoclipCommand.hpp>
#include <ChatCommand/CompDumpCommand/CompDumpCommand.hpp>
#include <ChatCommand/HelpCommand/HelpCommand.hpp>
#include <iomanip>

void LocalWorld::saveChunks()
{
    for (const std::shared_ptr<Chunk> &chunk : chunksToSave)
    {
        chunk->save(planner);
    }

    chunksToSave.clear();
}

void LocalWorld::saveMachineTable()
{
    if (!machineTableDirty)
    {
        return;
    }

    machineTableDirty = false;
    machineTable.clear();

    for (const std::shared_ptr<Machine> &machine : machines)
    {
        MachineTableEntry entry;
        entry.pos = machine->pos;
        entry.blockId = machine->blockId;
        machineTable.push_back(entry);
    }

    serializeInto(planner.newFile("machtab.dat"), machineTable);
}

void LocalWorld::saveMachines()
{
    for (const std::shared_ptr<Machine> &machine : machines)
    {
        machine->serializeMachineFile(planner.newFile(machine->stateFilePath));
    }
}

void LocalWorld::updateMachines()
{
    std::vector<std::shared_ptr<Machine>> machinesCopy = machines;

    for (const std::shared_ptr<Machine> &machine : machinesCopy)
    {
        machine->updateAll();
    }
}

void LocalWorld::updateEntities()
{
    // Make a copy of the entity table, and then update entities by iterating the copy.
    // This enables methods which modify the table to be called from entity updates.
    std::unordered_map<uint64_t, std::shared_ptr<Entity>> snapshotEntities = entities;
    for (const auto& [id, entity] : snapshotEntities)
    {
        entity->update();
    }
}

void LocalWorld::dispatchSignals()
{
    auto scheduleIterator = pendingSignals.find(gameTicks);
    if (scheduleIterator != pendingSignals.end())
    {
        for (const auto &ss : scheduleIterator->second)
        {
            ChunkHandle chunk = requestChunk(ss.pos & glm::ivec3(~0xF));
            if (!chunk->isReady())
            {
                pendingSignals[gameTicks+1].push_back(ss);
            }
            else
            {
                int id = std::get<0>(chunk->getBlockAndMeta(ss.pos & glm::ivec3(0xF)));
                Block &block = getBlockById(id);
                block.onSignal(*this, ss.pos, ss.code);
            }
        }

        pendingSignals.erase(scheduleIterator);
    }
}

void LocalWorld::commitPlan()
{
    if (!isManagerBusy.load() && (SDL_GetTicks()-lastWriteTicks) > 2000)
    {
        saveDynamicState();

        isManagerBusy = true;
        uint64_t thisPlanGeneration = planner.getCurrentGeneration();
        std::unordered_map<std::string, std::vector<uint8_t>> plan = planner.unpack();

        dispatchTask([this, plan, thisPlanGeneration]()
        {
            this->dataStore.update(plan, thisPlanGeneration);
            this->lastWriteTicks = SDL_GetTicks();
            this->isManagerBusy = false;
        });
    }
}

void LocalWorld::saveMiscDynamicData()
{
    serializeInto(planner.newFile("dynamic.dat"), gameTicks, timeOfDay, pendingSignals);
}

void LocalWorld::updateClocks()
{
    timeOfDay++;
    timeOfDay = timeOfDay % World::cycleDuration;
    gameTicks++;
}

void LocalWorld::saveDynamicState()
{
    saveMachineTable();
    saveMachines();
    saveChunks();
    saveMiscDynamicData();
    beltManager.save();
}

void LocalWorld::updateNodes()
{
    int i;
    for (i=0; i<8; i++)
    {
        for (const std::shared_ptr<WorldGridNode> &node : nodesToUpdate[i])
        {
            node->updateComponents(*this, planner);
        }

        nodesToUpdate[i].clear();
    }
}

void LocalWorld::registerGenericCommands()
{
    registerChatCommand<TpCommand>("tp");
    registerChatCommand<TimeCommand>("time", *this);
    registerChatCommand<SetCommand>("set");
    registerChatCommand<GiveMeCommand>("giveme");
    registerChatCommand<NoclipCommand>("noclip");
    registerChatCommand<CompDumpCommand>("compdump", *this);
    registerChatCommand<HelpCommand>("help", *this);
}

void LocalWorld::processCommand(Player &player, const std::string &cmdline)
{
    Log::info(formatString("[$+] /$+", player.getName(), cmdline));

    std::vector<std::string> tokens = Parse::tokenize(cmdline);
    if (tokens.empty())
    {
        return;
    }

    player.writeChat(formatString("/$+", cmdline), Color::cmdEcho);
    std::string cmd = tokens[0];

    if (!commandTable.contains(cmd))
    {
        player.writeChat(formatString("Unknown command '$+'", cmd), Color::error);
        Log::error(formatString("[$+] Unknown command `$+'", player.getName(), cmd));

        return;
    }

    commandTable[cmd]->execute(player, tokens);
}

void LocalWorld::onBeltViewUpdated(BeltView &view)
{
    // NOP
}

void LocalWorld::onBeltViewRemoved(uint64_t id)
{
    // NOP
}

void LocalWorld::onChunkLoaded(std::shared_ptr<Chunk> chunk)
{
    // NOP
}

LocalWorld::LocalWorld(std::string path) :
    nextEntityID(1),
    gameTicks(0),
    isManagerBusy(false),
    lastWriteTicks(SDL_GetTicks()),
    beltManager(*this)
{
    registerGenericCommands();

    dataStore.open(path);

    fatalError = dataStore.getError();
    if (fatalError.size() != 0)
    {
        return;
    }

    // Load the world index.
    WorldIndex worldIndex;
    std::vector<uint8_t> data;
    planner.read("windex.dat", dataStore, data);

    std::string indexLoadError;
    if (worldIndex.deserialize(data.data(), data.size(), indexLoadError) == 0)
    {
        fatalError = indexLoadError;
        return;
    }

    areCheatsEnabled = worldIndex.areCheatsEnabled();

    Log::info(formatString(
        "Loading world '$+' from '$+' (cheats $+)",
        worldIndex.getID(),
        path,
        areCheatsEnabled ? "ENABLED" : "DISABLED"
    ));

    // Load the dynamic data.
    planner.read("dynamic.dat", dataStore, data);
    deserialize(data.data(), data.size(), gameTicks, timeOfDay, pendingSignals);

    beltManager.load();

    // Sync the block table.
    worldIndex.syncBlockTable();

    // Try to load the world type settings.
    std::shared_ptr<WorldTypeSettings> worldTypeSettings = worldIndex.loadWorldTypeSettings(fatalError);
    if (worldTypeSettings == nullptr)
    {
        // fatalError already set.
        return;
    }

    // Update windex.dat.
    serializeInto(planner.newFile("windex.dat"), worldIndex);

    // Initialize the world.
    onBlockTableLoaded(worldIndex.getBlockTable());

    // Create the world generator.
    worldGenerator = worldTypeSettings->makeWorldGenerator(*this);

    // Initialize weather.
    weather.init(worldGenerator->getWeatherGenNode());

    // Initialize machines.
    planner.read("machtab.dat", dataStore, data);
    deserialize(data.data(), data.size(), machineTable);

    for (const MachineTableEntry &entry : machineTable)
    {
        std::string machineFile = formatString("wgrid/lvl0/x$+/y$+/z$+/mach_$+_$+_$+.dat",
            entry.pos.x & ~0xF, entry.pos.y & ~0xF, entry.pos.z & ~0xF, entry.pos.x, entry.pos.y, entry.pos.z);
        std::vector<uint8_t> machineBytes;
        planner.read(machineFile, dataStore, machineBytes);

        IMachineBlock *machineBlock = dynamic_cast<IMachineBlock*>(&getBlockById(entry.blockId));
        std::shared_ptr<Machine> machine = machineBlock->initMachine(*this, entry.pos);
        machine->blockId = entry.blockId;
        machine->deserializeMachineFile(machineBytes.data(), machineBytes.size());
        machines.push_back(machine);
    }
}

uint64_t LocalWorld::allocEntityId()
{
    return nextEntityID++;
}

uint64_t LocalWorld::allocBeltId()
{
    return nextBeltID++;
}

void LocalWorld::markNeedsSaving(std::shared_ptr<Chunk> chunk)
{
    if (!chunk->needsSaving)
    {
        chunk->needsSaving = true;
        chunksToSave.push_back(chunk);
    }
}

void LocalWorld::markNodeForUpdate(std::shared_ptr<WorldGridNode> node)
{
    if (!node->needsCompUpdate)
    {
        node->needsCompUpdate = false;
        nodesToUpdate[node->getLevel()].push_back(node);
    }
}

void LocalWorld::onBeltUpdated(const Belt &belt, bool isActive)
{
    std::unique_lock<std::shared_mutex> lock(beltViewMutex);

    auto [iter, inserted] = beltViews.try_emplace(belt.id);

    BeltView &view = iter->second;
    if (inserted)
    {
        assignBeltViewSprite(belt.id);
    }

    if (inserted || view.needsUpdate(belt, isActive))
    {
        view.state = belt;
        view.isActive = isActive;
        onBeltViewUpdated(view);
    }
}

void LocalWorld::onBeltRemoved(uint64_t id)
{
    std::unique_lock<std::shared_mutex> lock(beltViewMutex);

    beltViews.erase(id);
    eraseBeltViewSprite(id);
    onBeltViewRemoved(id);
}

bool LocalWorld::canBreak(glm::ivec3 pos)
{
    std::shared_ptr<Chunk> chunk = getChunk(pos & glm::ivec3(~0xF));
    if (chunk == nullptr || !chunk->isReady())
    {
        return false;
    }

    for (const glm::ivec3 &machinePos : chunk->monitorPositions)
    {
        std::shared_ptr<Machine> machine = getMachine(machinePos);
        if (machine == nullptr)
        {
            crash(
                formatString(
                    "Sanity check failed: a non-existent machine at $+ is monitoring the chunk at $+",
                    machinePos,
                    pos
                )
            );
        }

        if (!machine->isReady())
        {
            return false;
        }

        ChunkMonitorComponent &monitor = machine->getComponent<ChunkMonitorComponent>();
        if (!monitor.canBreak(pos))
        {
            return false;
        }
    }

    return true;
}

void LocalWorld::onChunkRequested(std::shared_ptr<Chunk> chunk, float relativeDeadline)
{
    dispatchTask([this, chunk]()
    {
        if (!shuttingDown)
        {
            // We can't read from the planner here as it is not thread-safe, but it is safe to read directly
            // from the manager. The main thread will not try to write to the same file because if we were called,
            // it means the chunk is not yet loaded. This task will only be started when the main thread does not
            // have this chunk, and thus it is safe.
            // If the chunk HAS been loaded before, it is not unloaded until its last save is committed.
            std::vector<uint8_t> chunkGridData;
            if (!dataStore.read(chunk->getGridFilePath(), chunkGridData) || chunkGridData.empty())
            {
                worldGenerator->generateChunk(*chunk);
            }
            else
            {
                chunk->deserializeGrid(chunkGridData);
            }
        }

        chunk->markLoadingComplete();
        onChunkLoaded(chunk);
    }, relativeDeadline, PriorityClass::WorldGen);
}

std::shared_ptr<Machine> LocalWorld::getMachine(glm::ivec3 pos)
{
    for (const std::shared_ptr<Machine> &machine : machines)
    {
        if (machine->pos == pos)
        {
            return machine;
        }
    }

    return nullptr;
}

void LocalWorld::sendSignal(glm::ivec3 pos, uint32_t code, int numTicks)
{
    if (numTicks < 1 || numTicks > maxSignalTicks)
    {
        crash(formatString("sendSignal() called with $+ ticks", numTicks));
    }

    std::vector<ScheduledSignal> &signals = pendingSignals[gameTicks + (uint64_t) numTicks];

    for (auto &signal : signals)
    {
        if (signal.pos == pos)
        {
            return;
        }
    }

    ScheduledSignal scheduled{pos, code};
    signals.push_back(scheduled);
}

void LocalWorld::cancelSignals(glm::ivec3 pos)
{
    for (auto& [ time, signalList ] : pendingSignals)
    {
        auto it = signalList.begin();
        while (it != signalList.end())
        {
            if (it->pos == pos)
            {
                it = signalList.erase(it);
            }
            else
            {
                it++;
            }
        }
    }
}

void LocalWorld::deleteMachineState(glm::ivec3 pos)
{
    for (size_t i=0; i<machines.size(); i++)
    {
        if (machines[i]->pos == pos)
        {
            machines[i]->onRemoving();

            std::string machineStateFile = machines[i]->stateFilePath;

            if (i != machines.size() - 1)
            {
                std::swap(machines[i], machines[machines.size()-1]);
            }

            machines.pop_back();
            machineTableDirty = true;

            planner.clear(machineStateFile);

            return;
        }
    }

    crash(formatString("LocalWorld::deleteMachineState() called on non-existent machine $+", pos));
}

void LocalWorld::close()
{
    // Force-commit the current plan.
    saveDynamicState();

    machines.clear();
    entities.clear();

    uint64_t finalGeneration = planner.getCurrentGeneration();
    std::unordered_map<std::string, std::vector<uint8_t>> plan = planner.unpack();
    afterAsyncBarrier(AsyncBarrier::CurrentlyDispatched, [this, plan = std::move(plan), finalGeneration]()
    {
        this->dataStore.update(plan, finalGeneration);
    });
}

void LocalWorld::update()
{
    // Don't do anything if there is a fatal error.
    if (fatalError.size() != 0)
    {
        return;
    }

    // Check machine chunks and ensure machines are loaded.
    if (!allMachinesReady)
    {
        allMachinesReady = true;
        for (const std::shared_ptr<Machine> &machine : machines)
        {        
            if (!machine->isReady())
            {
                allMachinesReady = false;
                break;
            }
        }

        if (!allMachinesReady)
        {
            return;
        }
    }

    dispatchSignals();
    updateEntities();
    updateMachines();
    advanceBeltViews();
    updateNodes();
    housekeep();
    updateClocks();
    commitPlan();
}

bool LocalWorld::isLocal()
{
    return true;
}

void LocalWorld::onPlayerUpdateBegins(Player &player)
{
    World::onPlayerUpdateBegins(player);
    player.updateDiggingState(*this);

    if (player.pos.z < wfixed(-128))
    {
        player.achieve(DiggingDeeperMilestone::ID, DiggingDeeperMilestone::BreakRingwooditeLayer);
    }
}

void LocalWorld::breakBlock(glm::ivec3 pos, bool notify)
{
    Block *block = getBlockAt(pos);
    if (block == nullptr)
    {
        return;
    }

    std::vector<ItemStack> stacks;
    block->getHarvestedStacks(*this, pos, stacks);

    setBlockAt(pos, 0, 0, notify);

    for (const auto &stack : stacks)
    {
        spawn<ItemEntity>(
            glm::dvec3(pos) + glm::dvec3(0.5),
            glm::vec3(0.0f),
            stack
        );
    }
}

void LocalWorld::dropItem(Player &player)
{
    if (!player.holdingStack().isEmpty())
    {
        glm::dvec3 playerForwardVector = player.getForwardVector();
        ItemStack tStack = player.holdingStack().extractOne();

        spawn<ItemEntity>(
            player.getEyePos() + wfvec3(1.2 * playerForwardVector),
            player.vel + wfvec3(0.4 * playerForwardVector),
            tStack
        );
    }
}

void LocalWorld::despawnEntity(uint64_t id)
{
    std::unordered_map<uint64_t, std::shared_ptr<Entity>>::iterator eit = entities.find(id);
    if (eit != entities.end())
    {
        entities.erase(eit);
    }
}

bool LocalWorld::setBlockAt(glm::ivec3 pos, int id, uint8_t meta, bool notify)
{
    glm::ivec3 chunkBase = pos & glm::ivec3(~0xF);
    glm::ivec3 chunkOffset = pos & glm::ivec3(0xF);

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

    bool hadEffectsBefore = chunk->hasBlockEffects();

    const auto [ prevBlockId, prevMeta ] = chunk->getBlockAndMeta(chunkOffset);
    Block &prevBlock = getBlockById(prevBlockId);

    chunk->setBlockAndMeta(chunkOffset, id, meta);

    prevBlock.onRemoved(*this, *chunk, pos, prevMeta);

    if (window != nullptr && chunk->hasBlockEffects() && !hadEffectsBefore)
    {
        window->addChunkWithEffects(chunk);
    }

    if (dynamic_cast<IMachineBlock*>(&prevBlock) != nullptr)
    {
        deleteMachineState(pos);
    }

    Block &newBlock = getBlockById(id);
    IMachineBlock *machineBlock = dynamic_cast<IMachineBlock*>(&newBlock);
    if (machineBlock != nullptr)
    {
        std::shared_ptr<Machine> machine = machineBlock->initMachine(*this, pos);
        machine->blockId = id;
        machines.push_back(machine);

        machineTableDirty = true;
    }

    onBlockGridUpdated(pos, id, meta);
    cancelSignals(pos);

    for (const glm::ivec3 &monitorPos : chunk->monitorPositions)
    {
        auto monitorMachine = getMachine(monitorPos);
        if (monitorMachine == nullptr)
        {
            crash(formatString("Chunk $+ is monitored by non-existent machine $+", chunk->base, monitorPos));
        }

        ChunkMonitorComponent &monitor = monitorMachine->getComponent<ChunkMonitorComponent>();
        monitor.onBlockChanged(pos, prevBlock, prevMeta, newBlock, meta);
    }

    if (notify)
    {
        Block &block = newBlock;
        block.onPlaced(*this, *chunk, pos);
        notifyNeighborsOf(pos);
    }

    chunk->onBlockUpdated(chunkOffset);
    markNeedsSaving(chunk);

    return true;
}

bool LocalWorld::setBlockAt(glm::ivec3 pos, std::string name, uint8_t meta, bool notify)
{
    std::vector<std::string>::iterator it = std::find(blockTable.begin(), blockTable.end(), name);
    if (it == blockTable.end())
    {
        return false;
    }

    int id = (int) (it - blockTable.begin());
    return setBlockAt(pos, id, meta, notify);
}

bool LocalWorld::setBlockMeta(glm::ivec3 pos, uint8_t meta, bool shouldNotifyNeighbors)
{
    glm::ivec3 chunkBase = pos & glm::ivec3(~0xF);
    glm::ivec3 chunkOffset = pos & glm::ivec3(0xF);

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

    bool hadEffectsBefore = chunk->hasBlockEffects();
    
    const auto [ blockId, currentMeta ] = chunk->getBlockAndMeta(chunkOffset);
    if (currentMeta == meta)
    {
        return true;
    }
    
    chunk->setBlockAndMeta(chunkOffset, blockId, meta);
    chunk->onBlockUpdated(chunkOffset);
    markNeedsSaving(chunk);

    if (window != nullptr && chunk->hasBlockEffects() && !hadEffectsBefore)
    {
        window->addChunkWithEffects(chunk);
    }

    onBlockGridUpdated(pos, blockId, meta);

    if (shouldNotifyNeighbors)
    {
        notifyNeighborsOf(pos);
    }

    return true;
}

void LocalWorld::onPlayerMessage(Player &player, const void *msg, size_t size)
{
    const Player::MessageHeader *header = reinterpret_cast<const Player::MessageHeader*>(msg);
    if (header->type == Player::MessageType::RightClick && size == sizeof(Player::RightClickMessage))
    {
        const Player::RightClickMessage *clickMessage = reinterpret_cast<const Player::RightClickMessage*>(msg);
        World::BlockHit hit = castRay(clickMessage->pos, clickMessage->dir, Player::handRange);
        if (hit.hasHit)
        {
            Block *block = getBlockAt(hit.coords);
            if (player.controlFlags.contains(Player::ControlFlag::Crouch))
            {
                ItemStack &stack = player.holdingStack();
                if (!stack.isEmpty())
                {
                    stack.getItem()->onUse(*this, player, stack, hit.coords, hit.side);
                }
            }
            else
            {
                block->onInteract(*this, player, hit.coords, hit.side, hit.collisionKey);
            }
        }
    }
    else if (header->type == Player::MessageType::Drop)
    {
        dropItem(player);
    }
}

void LocalWorld::notifyNeighborsOf(glm::ivec3 pos)
{
    for (const Side &side : Side::all)
    {
        notifyAboutNeighbor(pos + side.in(), side);
    }
}

void LocalWorld::notifyAboutNeighbor(glm::ivec3 pos, Side side)
{
    Block *block = getBlockAt(pos);
    if (block == nullptr)
    {
        // The block isn't loaded yet, so we must schedule a signal.
        sendSignal(pos, Block::createNeighborUpdatedSignalCode(side), 1);
    }
    else
    {
        block->onNeighborUpdated(*this, pos, side);
    }
}

void LocalWorld::writeDebugStats(std::ostream &os)
{
    os << "Completed world saves: " << dataStore.getLastFinishedGeneration() << std::endl;
    os << "Last world write time: " << dataStore.getLastWriteTimeUS() << "us" << std::endl;
}

void LocalWorld::writeBlockDebugInfo(std::ostream &os, glm::ivec3 pos)
{
    os << "Pending signals:" << std::endl;

    for (const auto &[time, signals] : pendingSignals)
    {
        for (const auto &signal : signals)
        {
            if (signal.pos == pos)
            {
                uint64_t ticksLeft = time - gameTicks;
                os << "0x" << std::hex << std::setw(8) << signal.code << "   " << std::dec << ticksLeft << std::endl;
            }
        }
    }
}

bool LocalWorld::canPurgeChunk(Chunk &chunk)
{
    if (chunk.needsSaving)
    {
        return false;
    }

    if (!chunk.lastFlushGeneration.has_value())
    {
        return true;
    }

    return dataStore.hasGenerationPassed(*chunk.lastFlushGeneration);
}

void LocalWorld::onWorldGridNodeCreated(std::shared_ptr<WorldGridNode> node)
{
    dispatchTask([this, node]()
    {
        if (!shuttingDown)
        {
            node->loadComponents(dataStore);
        }

        node->markLoaded();
    }, 0.0f, PriorityClass::WorldGen);
}

void LocalWorld::getWorldStats(StatList &statList)
{
    statList.push_back({
        "Total world time:",
        new Builder<uint64_t>(
            gameTicks,
            [](uint64_t ticks) -> Widget*
            {
                return new Label(formatTicks(ticks));
            }
        ),
    });
}

uint64_t LocalWorld::getWorldTime()
{
    return gameTicks;
}

std::shared_ptr<Messenger> LocalWorld::getPeerMessenger(Player &player)
{
    return player.client;
}