#include <SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif

#include "ButtonGlyphs.h"
#include "CustomLevels.h"
#include "DeferCallbacks.h"
#include "Editor.h"
#include "Enums.h"
#include "Entity.h"
#include "Exit.h"
#include "FileSystemUtils.h"
#include "Font.h"
#include "FontBidi.h"
#include "Game.h"
#include "Graphics.h"
#include "GraphicsUtil.h"
#include "Input.h"
#include "InterimVersion.h"
#include "KeyPoll.h"
#include "LevelDebugger.h"
#include "Localization.h"
#include "LocalizationStorage.h"
#include "Logic.h"
#include "Map.h"
#include "Music.h"
#include "Network.h"
#include "preloader.h"
#include "ReleaseVersion.h"
#include "Render.h"
#include "RenderFixed.h"
#include "Screen.h"
#include "Script.h"
#include "UtilityClass.h"
#include "Vlogging.h"

scriptclass script;

std::vector<CustomEntity> customentities;
customlevelclass cl;
editorclass ed;

UtilityClass help;
Graphics graphics;
musicclass music;
Game game;
KeyPoll key;
mapclass map;
entityclass obj;
Screen gameScreen;

static bool startinplaytest = false;
static bool savefileplaytest = false;
static int savex = 0;
static int savey = 0;
static int saverx = 0;
static int savery = 0;
static int savegc = 0;
static int savemusic = -1;
static std::string playassets;

static std::string playtestname;

static volatile Uint64 time_ = 0;
static volatile Uint64 timePrev = 0;
static volatile Uint32 accumulator = 0;

#ifndef __EMSCRIPTEN__
static volatile Uint64 f_time = 0;
static volatile Uint64 f_timePrev = 0;
#endif

enum FuncType
{
    Func_null,
    Func_fixed,
    Func_input,
    Func_delta
};

struct ImplFunc
{
    enum FuncType type;
    void (*func)(void);
};

static void runscript(void)
{
    script.run();
}

static void teleportermodeinput(void)
{
    if (game.useteleporter)
    {
        teleporterinput();
    }
    else
    {
        script.run();
        gameinput();
    }
}

static void flipmodeoff(void)
{
    graphics.flipmode = false;
}

static void focused_begin(void);
static void focused_end(void);

static const inline struct ImplFunc* get_gamestate_funcs(
    const int gamestate,
    int* num_implfuncs
) {
    switch (gamestate)
    {

#define FUNC_LIST_BEGIN(GAMESTATE) \
    case GAMESTATE: \
    { \
        static const struct ImplFunc implfuncs[] = { \
            {Func_fixed, focused_begin},

#define FUNC_LIST_END \
            {Func_fixed, focused_end} \
        }; \
        *num_implfuncs = SDL_arraysize(implfuncs); \
        return implfuncs; \
    }

    FUNC_LIST_BEGIN(GAMEMODE)
        {Func_fixed, runscript},
        {Func_fixed, gamerenderfixed},
        {Func_delta, gamerender},
        {Func_input, gameinput},
        {Func_fixed, gamelogic},
    FUNC_LIST_END

    FUNC_LIST_BEGIN(TITLEMODE)
        {Func_input, titleinput},
        {Func_fixed, titlerenderfixed},
        {Func_delta, titlerender},
        {Func_fixed, titlelogic},
    FUNC_LIST_END

    FUNC_LIST_BEGIN(MAPMODE)
        {Func_fixed, maprenderfixed},
        {Func_delta, maprender},
        {Func_input, mapinput},
        {Func_fixed, maplogic},
    FUNC_LIST_END

    FUNC_LIST_BEGIN(TELEPORTERMODE)
        {Func_fixed, teleporterrenderfixed},
        {Func_delta, teleporterrender},
        {Func_input, teleportermodeinput},
        {Func_fixed, maplogic},
    FUNC_LIST_END

    FUNC_LIST_BEGIN(GAMECOMPLETE)
        {Func_fixed, gamecompleterenderfixed},
        {Func_delta, gamecompleterender},
        {Func_input, gamecompleteinput},
        {Func_fixed, gamecompletelogic},
    FUNC_LIST_END

    FUNC_LIST_BEGIN(GAMECOMPLETE2)
        {Func_fixed, gamecompleterenderfixed2},
        {Func_delta, gamecompleterender2},
        {Func_input, gamecompleteinput2},
        {Func_fixed, gamecompletelogic2},
    FUNC_LIST_END

    FUNC_LIST_BEGIN(EDITORMODE)
        {Func_fixed, flipmodeoff},
        {Func_input, editorinput},
        {Func_fixed, editorlogic},
        {Func_fixed, editorrenderfixed},
        {Func_delta, editorrender},
    FUNC_LIST_END

    FUNC_LIST_BEGIN(PRELOADER)
        {Func_input, preloaderinput},
        {Func_fixed, preloaderrenderfixed},
        {Func_delta, preloaderrender},
    FUNC_LIST_END

#undef FUNC_LIST_END
#undef FUNC_LIST_BEGIN

    }

    SDL_assert(0 && "Invalid gamestate!");
    return NULL;
}

enum IndexCode
{
    Index_none,
    Index_end
};

static const struct ImplFunc* gamestate_funcs = NULL;
static int num_gamestate_funcs = 0;
static int gamestate_func_index = -1;

static enum IndexCode increment_gamestate_func_index(void)
{
    gamestate_func_index++;

    if (gamestate_func_index == num_gamestate_funcs)
    {
        /* Reached the end of current gamestate order.
         * Re-fetch for new order if gamestate changed.
         */
        gamestate_funcs = get_gamestate_funcs(
            game.gamestate,
            &num_gamestate_funcs
        );

        /* Also run callbacks that were deferred to end of func sequence. */
        DEFER_execute_callbacks();

        gamestate_func_index = 0;

        return Index_end;
    }

    return Index_none;
}

static void unfocused_run(void);

static const struct ImplFunc unfocused_func_list[] = {
    {
        Func_input, /* we still need polling when unfocused */
        NULL
    },
    {
        Func_delta,
        unfocused_run
    }
};
static const struct ImplFunc* unfocused_funcs = unfocused_func_list;
static int num_unfocused_funcs = SDL_arraysize(unfocused_func_list);
static int unfocused_func_index = 0; // This does not get incremented on start, do NOT use -1!

static enum IndexCode increment_unfocused_func_index(void)
{
    unfocused_func_index++;

    if (unfocused_func_index == num_unfocused_funcs)
    {
        unfocused_func_index = 0;

        return Index_end;
    }

    return Index_none;
}

static const struct ImplFunc** active_funcs = NULL;
static int* num_active_funcs = NULL;
static int* active_func_index = NULL;
static enum IndexCode (*increment_func_index)(void) = NULL;

enum LoopCode
{
    Loop_continue,
    Loop_stop
};

static enum LoopCode loop_assign_active_funcs(void)
{
    if (key.isActive)
    {
        active_funcs = &gamestate_funcs;
        num_active_funcs = &num_gamestate_funcs;
        active_func_index = &gamestate_func_index;
        increment_func_index = &increment_gamestate_func_index;
    }
    else
    {
        active_funcs = &unfocused_funcs;
        num_active_funcs = &num_unfocused_funcs;
        active_func_index = &unfocused_func_index;
        increment_func_index = &increment_unfocused_func_index;
    }
    return Loop_continue;
}

static enum LoopCode loop_run_active_funcs(void)
{
    while ((*active_funcs)[*active_func_index].type != Func_delta)
    {
        const struct ImplFunc* implfunc = &(*active_funcs)[*active_func_index];
        enum IndexCode index_code;

        if (implfunc->type == Func_input && !game.inputdelay)
        {
            key.Poll();
        }

        if (implfunc->type != Func_null && implfunc->func != NULL)
        {
            implfunc->func();
        }

        index_code = increment_func_index();

        if (index_code == Index_end)
        {
            return Loop_continue;
        }
    }

    /* About to switch over to rendering... but call this first. */
    graphics.renderfixedpre();

    return Loop_stop;
}

static enum LoopCode loop_begin(void);
static enum LoopCode loop_end(void);

static enum LoopCode (*const meta_funcs[])(void) = {
    loop_begin,
    loop_assign_active_funcs,
    loop_run_active_funcs,
    loop_end
};
static int meta_func_index = 0;

static void inline fixedloop(void)
{
    while (true)
    {
        enum LoopCode loop_code = meta_funcs[meta_func_index]();

        if (loop_code == Loop_stop)
        {
            break;
        }

        meta_func_index = (meta_func_index + 1) % SDL_arraysize(meta_funcs);
    }
}

static void inline deltaloop(void);

static void cleanup(void);

#ifdef __EMSCRIPTEN__
static void emscriptenloop(void)
{
    timePrev = time_;
    time_ = SDL_GetTicks64();
    deltaloop();
}
#endif

static void keep_console_open(const bool open_console)
{
    if (!open_console)
    {
        return;
    }

    printf("Press ENTER to quit.");

    int c;
    do
    {
        c = getchar();
    }
    while (c != '\n' && c != EOF);
}

int main(int argc, char *argv[])
{
    char* baseDir = NULL;
    char* assetsPath = NULL;
    char* langDir = NULL;
    char* fontsDir = NULL;
    bool seed_use_sdl_getticks = false;
    bool open_console = false;
    bool print_version = false;
    bool print_addresses = false;
    int invalid_arg = 0;
    int invalid_partial_arg = 0;

    vlog_init();

    for (int i = 1; i < argc; ++i)
    {
#define ARG(name) (SDL_strcmp(argv[i], name) == 0)
#define ARG_INNER(code) \
    if (i + 1 < argc) \
    { \
        code \
    } \
    else \
    { \
        invalid_partial_arg = i; \
    }

        if (ARG("-version"))
        {
            print_version = true;
        }
        else if (ARG("-addresses"))
        {
            print_addresses = true;
        }
        else if (ARG("-renderer"))
        {
            ARG_INNER({
                i++;
                SDL_SetHintWithPriority(SDL_HINT_RENDER_DRIVER, argv[i], SDL_HINT_OVERRIDE);
            })
        }
        else if (ARG("-basedir"))
        {
            ARG_INNER({
                i++;
                baseDir = argv[i];
            })
        }
        else if (ARG("-assets"))
        {
            ARG_INNER({
                i++;
                assetsPath = argv[i];
            })
        }
        else if (ARG("-langdir"))
        {
            ARG_INNER({
                i++;
                langDir = argv[i];
            })
        }
        else if (ARG("-fontsdir"))
        {
            ARG_INNER({
                i++;
                fontsDir = argv[i];
            })
        }
        else if (ARG("-playing") || ARG("-p"))
        {
            ARG_INNER({
                i++;
                startinplaytest = true;
                playtestname = std::string("levels/");
                playtestname.append(argv[i]);
                playtestname.append(std::string(".vvvvvv"));
            })
        }
        else if (ARG("-playx") || ARG("-playy") ||
        ARG("-playrx") || ARG("-playry") ||
        ARG("-playgc") || ARG("-playmusic"))
        {
            ARG_INNER({
                savefileplaytest = true;
                int v = help.Int(argv[i+1]);
                if (ARG("-playx")) savex = v;
                else if (ARG("-playy")) savey = v;
                else if (ARG("-playrx")) saverx = v;
                else if (ARG("-playry")) savery = v;
                else if (ARG("-playgc")) savegc = v;
                else if (ARG("-playmusic")) savemusic = v;
                i++;
            })
        }
        else if (ARG("-playassets"))
        {
            ARG_INNER({
                i++;
                // Even if this is a directory, FILESYSTEM_mountAssets() expects '.vvvvvv' on the end
                playassets = "levels/" + std::string(argv[i]) + ".vvvvvv";
            })
        }
        else if (ARG("-leveldebugger"))
        {
            level_debugger::set_forced();
        }
        else if (ARG("-nooutput"))
        {
            vlog_toggle_output(0);
        }
        else if (ARG("-forcecolor") || ARG("-forcecolour"))
        {
            vlog_toggle_color(1);
        }
        else if (ARG("-nocolor") || ARG("-nocolour"))
        {
            vlog_toggle_color(0);
        }
        else if (ARG("-debug"))
        {
            vlog_toggle_debug(1);
        }
        else if (ARG("-noinfo"))
        {
            vlog_toggle_info(0);
        }
        else if (ARG("-nowarn"))
        {
            vlog_toggle_warn(0);
        }
        else if (ARG("-noerror"))
        {
            vlog_toggle_error(0);
        }
        else if (ARG("-translator"))
        {
            loc::show_translator_menu = true;
        }
#ifdef _WIN32
        else if (ARG("-console"))
        {
            open_console = true;
        }
#endif
        else if (ARG("-seed-use-sdl-getticks"))
        {
            seed_use_sdl_getticks = true;
        }
#undef ARG_INNER
#undef ARG
        else
        {
            invalid_arg = i;
        }
    }

#if defined(ALWAYS_SHOW_TRANSLATOR_MENU)
    loc::show_translator_menu = true;
#endif

#ifdef _WIN32
    if (open_console)
    {
        vlog_open_console();
    }
#endif

    if (invalid_arg > 0)
    {
        vlog_error("Error: invalid option: %s", argv[invalid_arg]);
        keep_console_open(open_console);
        VVV_exit(1);
    }
    else if (invalid_partial_arg > 0)
    {
        vlog_error("%s option requires one argument.", argv[invalid_partial_arg]);
        keep_console_open(open_console);
        VVV_exit(1);
    }

    if (print_version)
    {
        /* Just print the version and exit. No vlogging. */
        puts(
            "VVVVVV " RELEASE_VERSION
#ifdef MAKEANDPLAY
            " [M&P]"
#endif
        );
#ifdef INTERIM_VERSION_EXISTS
        puts(COMMIT_DATE);
        puts(INTERIM_COMMIT);
        puts(BRANCH_NAME);
#endif
        keep_console_open(open_console);
        VVV_exit(0);
    }
    else if (print_addresses)
    {
        printf("cl         : %p\n", (void*) &cl);
        printf("ed         : %p\n", (void*) &ed);
        printf("game       : %p\n", (void*) &game);
        printf("gameScreen : %p\n", (void*) &gameScreen);
        printf("graphics   : %p\n", (void*) &graphics);
        printf("help       : %p\n", (void*) &help);
        printf("key        : %p\n", (void*) &key);
        printf("map        : %p\n", (void*) &map);
        printf("music      : %p\n", (void*) &music);
        printf("obj        : %p\n", (void*) &obj);
        printf("script     : %p\n", (void*) &script);

        keep_console_open(open_console);
        VVV_exit(0);
    }

    SDL_SetHintWithPriority(SDL_HINT_IME_SHOW_UI, "1", SDL_HINT_OVERRIDE);
    SDL_SetHintWithPriority(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1", SDL_HINT_OVERRIDE);

    /* We already do the button swapping in ButtonGlyphs, disable SDL's swapping */
    SDL_SetHintWithPriority(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0", SDL_HINT_OVERRIDE);

    if(!FILESYSTEM_init(argv[0], baseDir, assetsPath, langDir, fontsDir))
    {
        vlog_error("Unable to initialize filesystem!");
        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", "Unable to initialize filesystem!", NULL);
        VVV_exit(1);
    }

    SDL_Init(
        SDL_INIT_VIDEO |
        SDL_INIT_AUDIO |
        SDL_INIT_JOYSTICK |
        SDL_INIT_GAMECONTROLLER
    );
    if (SDL_IsTextInputActive() == SDL_TRUE)
    {
        SDL_StopTextInput();
    }

    NETWORK_init();

    vlog_info("\t\t");
    vlog_info("\t\t");
    vlog_info("\t\t       VVVVVV");
    vlog_info("\t\t");
    vlog_info("\t\t");
    vlog_info("\t\t  8888888888888888  ");
    vlog_info("\t\t88888888888888888888");
    vlog_info("\t\t888888    8888    88");
    vlog_info("\t\t888888    8888    88");
    vlog_info("\t\t88888888888888888888");
    vlog_info("\t\t88888888888888888888");
    vlog_info("\t\t888888            88");
    vlog_info("\t\t88888888        8888");
    vlog_info("\t\t  8888888888888888  ");
    vlog_info("\t\t      88888888      ");
    vlog_info("\t\t  8888888888888888  ");
    vlog_info("\t\t88888888888888888888");
    vlog_info("\t\t88888888888888888888");
    vlog_info("\t\t88888888888888888888");
    vlog_info("\t\t8888  88888888  8888");
    vlog_info("\t\t8888  88888888  8888");
    vlog_info("\t\t    888888888888    ");
    vlog_info("\t\t    8888    8888    ");
    vlog_info("\t\t  888888    888888  ");
    vlog_info("\t\t  888888    888888  ");
    vlog_info("\t\t  888888    888888  ");
    vlog_info("\t\t");
    vlog_info("\t\t");

    // Set up screen
    graphics.init();

    game.init();
    game.seed_use_sdl_getticks = seed_use_sdl_getticks;

    game.gamestate = PRELOADER;

    game.menustart = false;

    // Initialize title screen to cyan
    graphics.titlebg.colstate = 10;
    map.nexttowercolour();

    map.ypos = (700-29) * 8;
    map.oldypos = map.ypos;
    map.setbgobjlerp(graphics.towerbg);
    map.setbgobjlerp(graphics.titlebg);

    {
        // Prioritize unlock.vvv first (2.2 and below),
        // but settings have been migrated to settings.vvv (2.3 and up)
        struct ScreenSettings screen_settings;
        SDL_zero(screen_settings);
        ScreenSettings_default(&screen_settings);
        game.loadstats(&screen_settings);
        game.loadsettings(&screen_settings);
        gameScreen.init(&screen_settings);
    }

    BUTTONGLYPHS_init();
    font::load_main();
    font::bidi_init();

    // This loads music too...
    if (!graphics.reloadresources())
    {
        /* Something wrong with the default assets? We can't use them to
         * display the error message, and we have to bail. */
        const char* message;
        if (FILESYSTEM_levelDirHasError())
        {
            message = FILESYSTEM_getLevelDirError();
        }
        else
        {
            message = loc::gettext("Something went wrong, but we forgot the error message.");
        }
        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, NULL);

        VVV_exit(1);
    }

    loc::loadtext(false);
    loc::loadlanguagelist();
    game.createmenu(Menu::mainmenu);

    graphics.create_buffers();

    if (game.skipfakeload)
        game.gamestate = TITLEMODE;
    if (game.slowdown == 0) game.slowdown = 30;

    int min_lang_set = loc::lang_set_current;
    if (baseDir != NULL)
    {
        /* For people who manage tons of basedirs, never re-ask
         * the language, except in the default basedir. */
        min_lang_set = 1;
    }

    if (loc::lang_set < min_lang_set && !loc::languagelist.empty())
    {
        loc::pre_title_lang_menu = true;
        game.gamestate = TITLEMODE;
        game.menustart = true;
        game.createmenu(Menu::language);
        game.currentmenuoption = loc::languagelist_curlang;
    }

    //Check to see if you've already unlocked some achievements here from before the update
    if (game.swnbestrank > 0){
        if(game.swnbestrank >= 1) game.unlockAchievement("vvvvvvsupgrav5");
        if(game.swnbestrank >= 2) game.unlockAchievement("vvvvvvsupgrav10");
        if(game.swnbestrank >= 3) game.unlockAchievement("vvvvvvsupgrav15");
        if(game.swnbestrank >= 4) game.unlockAchievement("vvvvvvsupgrav20");
        if(game.swnbestrank >= 5) game.unlockAchievement("vvvvvvsupgrav30");
        if(game.swnbestrank >= 6) game.unlockAchievement("vvvvvvsupgrav60");
    }

    if (game.unlock[UnlockTrophy_GAME_COMPLETE])
    {
        game.unlockAchievement("vvvvvvgamecomplete");
    }
    if (game.unlock[UnlockTrophy_FLIPMODE_COMPLETE])
    {
        game.unlockAchievement("vvvvvvgamecompleteflip");
    }
    if (game.unlock[UnlockTrophy_NODEATHMODE_COMPLETE])
    {
        game.unlockAchievement("vvvvvvmaster");
    }

    if (game.bestgamedeaths > -1) {
        if (game.bestgamedeaths <= 500) {
            game.unlockAchievement("vvvvvvcomplete500");
        }
        if (game.bestgamedeaths <= 250) {
            game.unlockAchievement("vvvvvvcomplete250");
        }
        if (game.bestgamedeaths <= 100) {
            game.unlockAchievement("vvvvvvcomplete100");
        }
        if (game.bestgamedeaths <= 50) {
            game.unlockAchievement("vvvvvvcomplete50");
        }
    }

    if (game.bestrank[TimeTrial_SPACESTATION1] >= 3)
    {
        game.unlockAchievement("vvvvvvtimetrial_station1_fixed");
    }
    if (game.bestrank[TimeTrial_LABORATORY] >= 3)
    {
        game.unlockAchievement("vvvvvvtimetrial_lab_fixed");
    }
    if (game.bestrank[TimeTrial_TOWER] >= 3)
    {
        game.unlockAchievement("vvvvvvtimetrial_tower_fixed");
    }
    if (game.bestrank[TimeTrial_SPACESTATION2] >= 3)
    {
        game.unlockAchievement("vvvvvvtimetrial_station2_fixed");
    }
    if (game.bestrank[TimeTrial_WARPZONE] >= 3)
    {
        game.unlockAchievement("vvvvvvtimetrial_warp_fixed");
    }
    if (game.bestrank[TimeTrial_FINALLEVEL] >= 3)
    {
        game.unlockAchievement("vvvvvvtimetrial_final_fixed");
    }

    obj.init();

    if (startinplaytest) {
        game.levelpage = 0;
        game.playcustomlevel = 0;
        game.playassets = playassets;
        game.menustart = true;

        LevelMetaData meta;
        CliPlaytestArgs pt_args;
        if (cl.getLevelMetaDataAndPlaytestArgs(playtestname, meta, &pt_args)) {
            cl.ListOfMetaData.clear();
            cl.ListOfMetaData.push_back(meta);
        } else {
            cl.loadZips();
            if (cl.getLevelMetaDataAndPlaytestArgs(playtestname, meta, &pt_args)) {
                cl.ListOfMetaData.clear();
                cl.ListOfMetaData.push_back(meta);
            } else {
                vlog_error("Level not found");
                keep_console_open(open_console);
                VVV_exit(1);
            }
        }

        if (pt_args.valid)
        {
            savefileplaytest = true;
            savex = pt_args.x;
            savey = pt_args.y;
            saverx = pt_args.rx;
            savery = pt_args.ry;
            savegc = pt_args.gc;
            savemusic = pt_args.music;
        }

        game.loadcustomlevelstats();
        game.customleveltitle=cl.ListOfMetaData[game.playcustomlevel].title;
        game.customlevelfilename=cl.ListOfMetaData[game.playcustomlevel].filename;
        if (savefileplaytest) {
            game.playx = savex;
            game.playy = savey;
            game.playrx = saverx;
            game.playry = savery;
            game.playgc = savegc;
            game.playmusic = savemusic;
            game.cliplaytest = true;
            script.startgamemode(Start_CUSTOM_QUICKSAVE);
        } else {
            script.startgamemode(Start_CUSTOM);
        }

        graphics.fademode = FADE_NONE;
    }

    /* Only create the window after we have loaded all the assets. */
    SDL_ShowWindow(gameScreen.m_window);

    key.isActive = true;

    gamestate_funcs = get_gamestate_funcs(game.gamestate, &num_gamestate_funcs);
    loop_assign_active_funcs();

#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop(emscriptenloop, 0, 0);
#else
    while (true)
    {
        f_time = SDL_GetTicks64();

        const Uint64 f_timetaken = f_time - f_timePrev;
        const int timestep = game.get_timestep();
        if (!game.over30mode && f_timetaken < (Uint64) timestep)
        {
            const volatile Uint64 f_delay = timestep - f_timetaken;
            SDL_Delay((Uint32) f_delay);
            f_time = SDL_GetTicks64();
        }

        f_timePrev = f_time;

        timePrev = time_;
        time_ = SDL_GetTicks64();

        deltaloop();
    }

    cleanup();
#endif

    return 0;
}

static void cleanup(void)
{
    /* Order matters! */
    if (FILESYSTEM_isInit()) /* not necessary but silences logs */
    {
        game.savestatsandsettings();
    }

    graphics.grphx.destroy();
    graphics.destroy_buffers();
    graphics.destroy();
    font::bidi_destroy();
    font::destroy();
    gameScreen.destroy();
    music.destroy();
    map.destroy();
    NETWORK_shutdown();
    loc::resettext(true);
    SDL_Quit();
    FILESYSTEM_deinit();
}

SDL_NORETURN void VVV_exit(const int exit_code)
{
    cleanup();
    exit(exit_code);
}

static void inline deltaloop(void)
{
    //timestep limit to 30
    const float rawdeltatime = static_cast<float>(time_ - timePrev);
    accumulator += rawdeltatime;

    Uint32 timesteplimit = game.get_timestep();

    while (accumulator >= timesteplimit)
    {
        enum IndexCode index_code = increment_func_index();

        if (index_code == Index_end)
        {
            loop_assign_active_funcs();
        }

        accumulator = SDL_fmodf(accumulator, timesteplimit);

        /* We are done rendering. */
        graphics.renderfixedpost();

        fixedloop();
    }
    const float alpha = game.over30mode ? static_cast<float>(accumulator) / timesteplimit : 1.0f;
    graphics.alpha = alpha;

    if (active_func_index == NULL
    || *active_func_index == -1
    || active_funcs == NULL)
    {
        /* Somehow the first deltatime has been too small and things haven't
         * initialized. We'll just no-op for now.
         */
    }
    else
    {
        const struct ImplFunc* implfunc = &(*active_funcs)[*active_func_index];

        if (implfunc->type == Func_delta && implfunc->func != NULL)
        {
            graphics.clear();

            graphics.set_render_target(graphics.gameTexture);

            implfunc->func();

            gameScreen.RenderPresent();
        }
    }
}

static enum LoopCode loop_begin(void)
{
    if (game.inputdelay)
    {
        key.Poll();
    }

    // Update network per frame.
    NETWORK_update();

    return Loop_continue;
}

static void unfocused_run(void)
{
    if (!game.blackout)
    {
        graphics.fill_rect(0, 0, 0);
#define FLIP(YPOS) graphics.flipmode ? 232 - YPOS : YPOS
#define FLIP_PR_CJK_LOW (graphics.flipmode ? PR_CJK_HIGH : PR_CJK_LOW)
#define FLIP_PR_CJK_HIGH (graphics.flipmode ? PR_CJK_LOW : PR_CJK_HIGH)
        /* The pause screen can also appear on the language screen, where highlighting
         * a language changes the used language metadata but not the loaded strings... */
        uint32_t flags = PR_CEN | PR_BOR | PR_FONT_IDX(loc::langmeta.font_idx, loc::langmeta.rtl);
        font::print(flags | FLIP_PR_CJK_HIGH, -1, FLIP(110), loc::gettext("Game paused"), 196 - help.glow, 255 - help.glow, 196 - help.glow);

        if (BUTTONGLYPHS_keyboard_is_available())
        {
            font::print(flags | FLIP_PR_CJK_LOW, -1, FLIP(120), loc::gettext("[click to resume]"), 196 - help.glow, 255 - help.glow, 196 - help.glow);

            font::print(flags | FLIP_PR_CJK_HIGH, -1, FLIP(220), loc::gettext("Press M to mute in game"), 164 - help.glow, 196 - help.glow, 164 - help.glow);
            font::print(flags, -1, FLIP(230), loc::gettext("Press N to mute music only"), 164 - help.glow, 196 - help.glow, 164 - help.glow);
        }
#undef FLIP_PR_CJK_HIGH
#undef FLIP_PR_CJK_LOW
#undef FLIP
    }
    graphics.render();
    //We are minimised, so lets put a bit of a delay to save CPU
#ifndef __EMSCRIPTEN__
    SDL_Delay(100);
#endif
}

static void focused_begin(void)
{
    map.nexttowercolour_set = false;
}

static void focused_end(void)
{
    game.gameclock();
    music.processmusic();
    graphics.processfade();
}

static enum LoopCode loop_end(void)
{
    ++game.framecounter;

    //We did editorinput, now it's safe to turn this off
    key.linealreadyemptykludge = false;

    //Mute button
    if (key.isDown(KEYBOARD_m) && game.mutebutton<=0 && !key.textentry())
    {
        game.mutebutton = 8;
        if (game.muted)
        {
            game.muted = false;
        }
        else
        {
            game.muted = true;
        }
    }
    if(game.mutebutton>0)
    {
        game.mutebutton--;
    }

    if (key.isDown(KEYBOARD_n) && game.musicmutebutton <= 0 && !key.textentry())
    {
        game.musicmutebutton = 8;
        game.musicmuted = !game.musicmuted;
    }
    if (game.musicmutebutton > 0)
    {
        game.musicmutebutton--;
    }

    music.updatemutestate();

    if (key.resetWindow)
    {
        key.resetWindow = false;
        gameScreen.ResizeScreen(-1, -1);
    }

    return Loop_continue;
}
