#include <Authenticator/PasswordAuthenticator/PasswordAuthenticator.hpp>
#include <Display/WidgetDisplay/ConnectingDisplay/ConnectingDisplay.hpp>
#include <Display/WidgetDisplay/MessageDisplay/MessageDisplay.hpp>
#include <VoxelverseSession/VoxelverseClient/VoxelverseClient.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Display/PreloaderDisplay/PreloaderDisplay.hpp>
#include <Utilities/Utilities.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <ItemRegistry/ItemRegistry.hpp>
#include <Server/Server.hpp>
#include <Utilities/Socket.hpp>
#include <EntityRegistry/EntityRegistry.hpp>
#include <ControllerRegistry/ControllerRegistry.hpp>
#include <ItemRecipeRegistry/ItemRecipeRegistry.hpp>
#include <FuelRuleRegistry/FuelRuleRegistry.hpp>
#include <SmeltingRuleRegistry/SmeltingRuleRegistry.hpp>
#include <FluidRegistry/FluidRegistry.hpp>
#include <Settings/Settings.hpp>
#include <Utilities/Async.hpp>
#include <Machine/MChemicalReactor/MChemicalReactor.hpp>
#include <MaceratorRuleRegistry/MaceratorRuleRegistry.hpp>
#include <AudioManager/AudioManager.hpp>
#include <Version/Version.hpp>
#include <Utilities/ServerInit.hpp>
#include <Utilities/ServerSessionTable.hpp>
#include <World/LocalWorld/SinglePlayerWorld/SinglePlayerWorld.hpp>
#include <WorldTypeRegistry/WorldTypeRegistry.hpp>
#include <WorldComponentRegistry/WorldComponentRegistry.hpp>
#include <AverageTimeTracker/AverageTimeTracker.hpp>
#include <ChunkWindow/ChunkWindow.hpp>
#include <chrono>

#ifdef _WIN32
#   include <windows.h>
#   include <shobjidl.h>
#endif

volatile bool running = true;

static AverageTimeTracker fastUpdateTracker;
static AverageTimeTracker updateTracker;
static AverageTimeTracker frameTracker;

void endGame()
{
    running = false;
}

void notifyFrameTime(double ms)
{
    frameTracker.submitSample(ms);
}

double getAverageFrameTime()
{
    return frameTracker.getAverage();
}

double getAverageUpdateTime()
{
    return updateTracker.getAverage();
}

double getAverageFastUpdateTime()
{
    return fastUpdateTracker.getAverage();
}

void VoxelverseClient::onPreloadingFinished()
{
    if (args.size() == 1)
    {
        return;
    }

    const std::string &cmd = args[1];
    if (cmd == "--singleplayer")
    {
        if (args.size() < 3)
        {
            Display::navPush<MessageDisplay>(Material::alert, "Error", "--singleplayer: No world specified.");
            return;
        }

        const std::string &path = args[2];
        std::shared_ptr<SinglePlayerWorld> world = std::make_shared<SinglePlayerWorld>(path);

        std::string fatalError = world->getFatalError();
        if (fatalError.size() != 0)
        {
            Display::navPush<MessageDisplay>(Material::alert, "Error", formatString("Error loading '$+':\n$+", path, fatalError));
            return;
        }

        Display::navPush<WorldDisplay>(world, world->getThePlayer());
    }
    else if (cmd == "--multiplayer")
    {
        if (args.size() != 5)
        {
            Display::navPush<MessageDisplay>(Material::alert, "Error", "USAGE:\n--multiplayer <server> <username> <password>");
            return;
        }

        const std::string &server = args[2];
        const std::string &userName = args[3];
        const std::string &password = args[4];

        std::shared_ptr<PasswordAuthenticator> auth = std::make_shared<PasswordAuthenticator>(userName, password);
        Display::navPush<ConnectingDisplay>(auth, server);
    }
    else
    {
        Display::navPush<MessageDisplay>(Material::alert, "Error", formatString("Unrecognised command: '$+'", cmd));
    }
}

void VoxelverseClient::mainLoop()
{
    GraphicsContext context;
    
    Display::navPush<PreloaderDisplay>();

    Uint32 lastUpdateTicks = 0;
    Uint32 nextUpdateTicks = 0;

    SDL_HideCursor();
    
    while (running)
    {
        AudioManager::update();
    
        SDL_Event event;
        while (SDL_PollEvent(&event))
        {
            std::shared_ptr<Display> display = Display::getCurrent();

            if (!windowMaximized && event.type == SDL_EVENT_WINDOW_SHOWN)
            {
                windowMaximized = true;
                context.maximizeWindow();
            }
            
            if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_F10)
            {
                context.requestScreenshot();
            }
            else
            {
                display->onEvent(event);
            }
        }

        std::chrono::time_point<std::chrono::steady_clock> fastUpdateStartTime = std::chrono::steady_clock::now();
        Display::getCurrent()->fastUpdate();
        std::chrono::time_point<std::chrono::steady_clock> fastUpdateEndTime = std::chrono::steady_clock::now();
        std::chrono::duration<double, std::milli> fastUpdateDuration = fastUpdateEndTime - fastUpdateStartTime;
        fastUpdateTracker.submitSample(fastUpdateDuration.count());

        Uint32 currentUpdateTicks = SDL_GetTicks();
        if (currentUpdateTicks >= nextUpdateTicks)
        {
            int numUpdatesToPerform = (currentUpdateTicks - lastUpdateTicks) / updateDuration;
            if (numUpdatesToPerform > 16)
            {
                Log::warn(formatString("Skipped $+ updates! Only 16 will be performed!", numUpdatesToPerform));
                numUpdatesToPerform = 16;
            }

            for (int i=0; i<numUpdatesToPerform; i++)
            {
                std::shared_ptr<Display> display = Display::getCurrent();
                std::chrono::time_point<std::chrono::steady_clock> updateStartTime = std::chrono::steady_clock::now();

                display->update();

                std::chrono::time_point<std::chrono::steady_clock> updateEndTime = std::chrono::steady_clock::now();
                std::chrono::duration<double, std::milli> updateDuration = updateEndTime - updateStartTime;

                updateTracker.submitSample(updateDuration.count());
            }

            nextUpdateTicks = ((currentUpdateTicks / updateDuration) + 1) * updateDuration;
            lastUpdateTicks = currentUpdateTicks;
        }

        context.drawFrame();
    }
}

int VoxelverseClient::run()
{
#ifdef _WIN32
    SetCurrentProcessExplicitAppUserModelID(L"voxelverse");
#endif

    SDL_SetHint(SDL_HINT_APP_ID, "voxelverse");
    if (!SDL_Init(SDL_INIT_VIDEO))
    {
        return 1;
    }

    AudioManager::init();
    initServerSessionTable();

    mainLoop();

    AudioManager::shutdown();
    SDL_Quit();

    return 0;
}

std::string VoxelverseClient::getSessionTypeName()
{
    return "client";
}