// main.cpp : Defines the entry point for the console application.
//

#include "commandline.h"
#include "msc.h"
#include "graphchart.h"
#include "flowchart.h"
#include "blockchart.h"
#include "xxxchart.h"
#include "msc_designs.c"
#include "gv_designs.c"
#include "block_designs.c"
#include <gvplugin.h>
#include <gvc.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/resource.h> //getrusage
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <glob.h>
#include <iostream>
#include <filesystem>

#ifdef BUILD_GUI
#include <unistd.h>
#include "gui.h"

#include "imgui.h"
#include "backends/imgui_impl_sdl.h"
#include "backends/imgui_impl_opengl3.h"
#include <stdio.h>
#include <SDL.h>
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <SDL_opengles2.h>
#else
#include <SDL_opengl.h>
#endif

#ifdef HAS_FONTCONFIG
#include <fontconfig/fontconfig.h>
std::set<std::string> GetFontNames();
#endif // HAS_FONTCONFIG

#endif // BUILD_GUI



extern gvplugin_library_t gvplugin_canvas_LTX_library; //defined in gvchart.cpp

lt_symlist_t lt_preloaded_symbols[] = {{"gvplugin_canvas_LTX_library", &gvplugin_canvas_LTX_library},
                                       {0,0}};

//GVC context cannot be initialized, freed and re-initialized again, so we do it once as static
GVC_t *Gvc = gvContextPlugins(lt_preloaded_symbols, 1);
//gvFreeContext(Gvc); should be called at some exit functions, but we do not do it.

extern int block_compile_debug, block_csh_debug, flow_compile_debug, flow_csh_debug, graph_compile_debug, graph_csh_debug, msc_compile_debug, msc_csh_debug;

bool progress(const ProgressBase *progress, void *, ProgressBase::EPreferredAbortMethod)
{
    const int percent = progress->GetPercentage();
    if (percent>=0 && percent<=100)
        fprintf(stderr, "%3d%%\r", percent);
    else
        fprintf(stderr, "    \r");
    return true;
}

//Search for a pattern, read the files and put content into ret
void Search_Read(const char *fname, std::vector<std::pair<std::string, std::string>> &ret)
{
    glob_t g;
    int r = glob(fname, GLOB_NOSORT, NULL, &g);
    if (r) return;
    for (unsigned u = 0; u<g.gl_pathc; u++) {
        const std::string name = g.gl_pathv[u];
        //const size_t pos = name.find_last_of('/');
        //ret.emplace_back(g.gl_pathv[u] + pos + 1, buffer);
        ret.emplace_back(g.gl_pathv[u], "");
    }
    globfree(&g);
}


FILE *OpenNamedFile(const char *utf8filename, bool write, bool binary)
{
   return fopen(utf8filename, write ? binary ? "wb" : "wt" : binary ? "rb" : "rt");
}


/** Reads a file that is included in the document.
 * @returns error message ("" if OK), filename actually read and read text*/
std::tuple<std::string, std::string, std::string>
ReadIncludeFile(std::string_view filename, void *, std::string_view included_from)
{
    std::tuple<std::string, std::string, std::string> ret;
    if (filename.empty()) {
        std::get<0>(ret) = "Can not include file of empty name.";
        return ret;
    } else if (filename.front()=='/' || filename.front()=='\\') {
        //full path
        std::get<1>(ret) = filename;
    } else {
        //try merging paths
        std::get<1>(ret) = included_from; //assumed to be in UNIX style
        auto last = std::get<1>(ret).find_last_of('/');
        if (last==std::string::npos)
            std::get<1>(ret) = filename; //included from has no path - use filename (assume current dir)
        else {
            std::get<1>(ret).erase(last+1);
            std::get<1>(ret).append(filename);
        }
    }
    //convert from Windows style to UNIX style
    std::replace(std::get<1>(ret).begin(), std::get<1>(ret).end(), '\\', '/');
    if (auto file = fopen(std::get<1>(ret).c_str(), "r")) {
        std::get<2>(ret) = ReadFile(file);
        fclose(file);
    } else
        std::get<0>(ret) = "Could not open file: "+std::get<1>(ret);
    return ret;
}

std::vector<std::string> FileListProc(std::string_view prefix_, std::string_view included_from_) {
    std::string included_from__(included_from_);
    std::string prefix__(prefix_);
    //convert Windows style to UNIX style
    std::ranges::replace(included_from__, '\\', '/');
    std::ranges::replace(prefix__, '\\', '/');

    namespace fs = std::filesystem;

    fs::path included_from = fs::path(included_from__).parent_path();
    fs::path dir = fs::path(prefix__).parent_path();

    if ((included_from.empty() || included_from.is_relative()) && (dir.empty() || dir.is_relative()))
        return{}; //cannot determine directory
    const bool use_included_from = dir.is_relative();
    if (use_included_from) dir = included_from / dir;
    std::vector<std::string> ret;
    try {
        for (auto& e : fs::directory_iterator(dir))
            if (e.is_regular_file() || e.is_symlink() || e.is_directory()) {
                fs::path p = e.path();
                if (use_included_from) {
                    fs::path p2;
                    for (auto i = std::mismatch(p.begin(), p.end(), included_from.begin(), included_from.end()).first;
                         i!=p.end(); i++)
                        p2 /= *i;
                    ret.push_back(p2.native());
                } else
                    ret.push_back(p.native());
            }
    } catch (const std::filesystem::filesystem_error&) {
    }
    return ret;
}


#define RCDIR_NAME ".msc-genrc"
#define LOAD_FILE_NAME ".msc-generator-load"

#ifdef BUILD_GUI
TextureID TextureFromSurface(cairo_surface_t *rgba_surface);
void DestroyTexture(ImTextureID t);
const char directory_separator = '/';
void GUIBeep() { printf("\a"); }

/** Maps the short name of a documentation file to its full UTF-8 path.*/
std::map<std::string, std::string> Documentation;

std::tuple<int, std::string> POpen(std::string_view cmd) {
    std::unique_ptr<FILE, int(*)(FILE*)> fd{popen(cmd.data(), "r"), pclose};
    using namespace std::string_literals;
    if (!fd)
        return {errno, "popen returned "s + std::strerror(errno)};
    std::string out;
    while (true) {
        char buf[1024];
        if (std::fgets(buf, sizeof buf, fd.get()))
            out += buf;
        auto err = errno;
        if (feof(fd.get()))
            break;
        if (ferror(fd.get()))
            return {err, "problem reading output: "s + std::strerror(err) + " after " + out};
    }
    auto r = pclose(fd.release());
    if (r == -1 && (r = errno))
        return {r, "pclose returned "s + std::strerror(r) + " with process output: " + out};
    if (WIFEXITED(r))
        r = WEXITSTATUS(r);
    else if (WIFSIGNALED(r))
        out = "child process terminated with " + std::to_string(WTERMSIG(r)) + " and output: " + out;
    else
        out = "child process stopped with output: " + out;
    return {r, out};
}

std::optional<std::string> ShowDocumentation(const std::string& doc_name) {
    const auto i = Documentation.find(doc_name);
    if (i == Documentation.end())
        return {};
    if (auto [ret,err] = POpen("exec bash -c '"
                               "for i in xdg-open sensible-browser open; do"
                               " err=`$i $1 2>&1`;"
                               " ret=$?;"
                               " case $ret in"
                               "  126 | 127) ;;"
                               "  *) echo $err; exit $ret;;"
                               " esac;"
                               "done;"
                               "echo No documentation browser found;"
                               "exit 1"
                               "' msc-doc.sh " + i->second);
            ret)
        return {err + " (exit status: " + std::to_string(ret) + ')'};
    return {};
}

std::optional<std::string> ShowLink(std::string_view url) {
    if (auto [ret, err] = POpen("exec bash -c '"
                                "for i in xdg-open sensible-browser open; do"
                                " err=`$i $1 2>&1`;"
                                " ret=$?;"
                                " case $ret in"
                                "  126 | 127) ;;"
                                "  *) echo $err; exit $ret;;"
                                " esac;"
                                "done;"
                                "echo No documentation browser found;"
                                "exit 1"
                                "' msc-doc.sh " + std::string(url));
    ret)
        return {err + " (exit status: " + std::to_string(ret) + ')'};
    return {};
}


#endif

int main(int argc, char* argv[])
{
#if YYDEBUG
    block_compile_debug = block_csh_debug = flow_compile_debug = flow_csh_debug = graph_compile_debug = graph_csh_debug = msc_compile_debug = msc_csh_debug = 1;
#endif
    //Get msc-genrc dir
    const char *ch = getenv("MSC_GEN_RC");
    std::string hdir = ch ? ch : "";
    if (hdir.length()==0) {
        ch = getenv("HOME");
        hdir = ch ? ch : "";
        if (hdir.length()==0) {
            struct passwd *pw = getpwuid(getuid());
            hdir = pw->pw_dir;
        }
    }
    hdir.append("/");

    //Generate language packs
    LanguageCollection languages;
    languages.AddLanguage(&MscChart::Factory, FileListProc);
    languages.AddLanguage(&GraphChart::Factory, FileListProc);
    languages.AddLanguage(&BlockChart::Factory, FileListProc);
#ifdef _DEBUG
    languages.AddLanguage(&XxxChart::Factory, FileListProc);
    languages.AddLanguage(&FlowChart::Factory, FileListProc);
#endif

    //Load progress balance data
    std::string load_data_fname = hdir + RCDIR_NAME "/" LOAD_FILE_NAME;
    FILE *fin = fopen(load_data_fname.c_str(), "rb");
    std::string load_data;
    if (fin) {
        struct stat st;
        if (stat(load_data_fname.c_str(), &st) == 0) {
            const off_t len = st.st_size;
            char *buff = (char*)malloc(len+1);
            size_t r = fread(buff, 1, len, fin);
            buff[len] = 0;
            if (r == size_t(len))
                load_data = buff;
            free(buff);
            fclose(fin);
        }
    }

    //Prepare shapes and designs
    std::vector<std::pair<std::string, std::string>> design_files;
    design_files.emplace_back("[designlib].signalling", msc_designs);
    design_files.emplace_back("[designlib].graph", gv_designs);
    design_files.emplace_back("[designlib].block", block_designs);
    for (auto &s : languages.GetExtensions())
        Search_Read(StrCat(hdir, RCDIR_NAME "/*.", s).c_str(), design_files);

    std::vector<std::string> args;
    for (int i = 1; i<argc; i++)
        args.emplace_back(argv[i]);

#ifdef BUILD_GUI
    auto gui_libs = RegisterLibrariesGUI();
    SDL_version v_comp, v_link;
    SDL_VERSION(&v_comp);
    SDL_GetVersion(&v_link);
    char buff[1000];
    if (v_comp.major==v_link.major && v_comp.minor==v_link.minor && v_comp.patch==v_link.patch)
        snprintf(buff, sizeof(buff), "SDL v%d.%d.%d",
                 v_comp.major, v_comp.minor, v_comp.patch);
    else
        snprintf(buff, sizeof(buff), "SDL v%d.%d.%d (compiled for: v%d.%d.%d)",
                 v_link.major, v_link.minor, v_link.patch, v_comp.major, v_comp.minor, v_comp.patch);
    gui_libs[buff];
    gui_libs["OpenGL v3+"];
    languages.AddLibraries(std::move(gui_libs));

    const bool display_only = getenv("DISPLAY") && getenv("DISPLAY")[0] && !isatty(STDIN_FILENO);
#else
    const bool display_only = false;
#endif //BUILD_GUI

    //Do it
    auto ret = do_main(args, languages, std::move(design_files),
                       OpenNamedFile, ReadIncludeFile, nullptr,
                       "\\mn(10)", display_only, progress, NULL, &load_data);
    if (std::holds_alternative<int>(ret)) {
        //We have completed a real command-line operation
        //Fallthrough to end
    } else {
#ifdef BUILD_GUI
        //we need to start the GUI - most of the code below is from the IMGUI example in
        //examples/example_sdl_opengl3/main.cpp
        //One difference is that we have the SDL_GL_SetAttribute() calls before SDL_Init() for Linux (and as default)
        //See https://stackoverflow.com/a/31590995

        // Decide GL+GLSL versions
#if defined(IMGUI_IMPL_OPENGL_ES2)
        // GL ES 2.0 + GLSL 100
        const char *glsl_version = "#version 100";
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#elif defined(__APPLE__)
        // For OS X, initialize before the attributes.
        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0) {
            printf("Error: %s\n", SDL_GetError());
            return EXIT_FAILURE;
        }
        // GL 3.2 Core + GLSL 150
        const char *glsl_version = "#version 150";
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
#else
        // GL 3.0 + GLSL 130
        const char *glsl_version = "#version 130";
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#endif

        // Create window with graphics context
        SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
        SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
        SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);

#ifndef __APPLE__
        // Setup SDL - Experience shows that for Linux, we need to do this *after* the attribute settings.
        // (Some versions of SDL before <2.0.10 appears to have performance/stalling issues on a minority of Windows systems,
        // depending on whether SDL_INIT_GAMECONTROLLER is enabled or disabled.. updating to latest version of SDL is recommended!)
        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0) {
            printf("Error: %s\n", SDL_GetError());
            return EXIT_FAILURE;
        }
#endif // !__APPLE__


        std::string last_window_title;
        bool we_are_fullscreen = false;
#define WINDOW_TITLE_PREFIX "Msc-generator GUI"
        auto window_flags = SDL_WindowFlags(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
        int width = 1280, height = 720;
        SDL_Rect bounds(0,0,1920,1080);
        if (SDL_GetDisplayUsableBounds(0, &bounds))
            std::cerr << "Could not query display size: " << SDL_GetError() << '\n';
        else
            width = bounds.w / 2, height = bounds.h / 2;
        const auto dpi_mul = [](const SDL_Rect &bounds) { return bounds.h/1080.0f; }; //1080p height screen is the multiplier of 1.
        auto window = SDL_CreateWindow(WINDOW_TITLE_PREFIX, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, window_flags);
        SDL_GLContext gl_context = SDL_GL_CreateContext(window);
        SDL_GL_MakeCurrent(window, gl_context);
        SDL_GL_SetSwapInterval(1); // Enable vsync
#include "icon.h"
        Uint32 const
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
                     rmask = 0xff000000, gmask = 0x00ff0000, bmask = 0x0000ff00, amask = 0x000000ff;
#else
                     rmask = 0x000000ff, gmask = 0x0000ff00, bmask = 0x00ff0000, amask = 0xff000000;
#endif
        auto icon = SDL_CreateRGBSurfaceFrom((void*)msc_icon.pixel_data, msc_icon.width, msc_icon.height, msc_icon.bytes_per_pixel * 8, msc_icon.bytes_per_pixel * msc_icon.width, rmask, gmask, bmask, amask);
        SDL_SetWindowIcon(window, icon);
        SDL_FreeSurface(icon);
        // Setup Dear ImGui context
        IMGUI_CHECKVERSION();
        ImGui::CreateContext();
        auto &io = ImGui::GetIO();
        io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
        io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls - Why not?
        io.IniFilename = nullptr;                                 // Manually save status data

        //Calculate canonical path and name of the file to open
        if (auto &init = std::get<GUIInit>(ret); init.file_to_open_path.size()) {
            std::filesystem::path path = init.file_to_open_path;
            std::error_code ec;
            path = canonical(path, ec);
            if (!ec) {
                init.file_to_open_path = path.native();
                path = path.filename();
                if (!path.empty())
                    init.file_to_open_name = path.native();
            }
        }
        //Ensure 'settings_dir' exists
        std::string settings_dir = hdir+RCDIR_NAME "/";
        std::error_code ec;
        std::filesystem::create_directories(settings_dir, ec);
        if (ec)
            std::cerr<<"Could not create settings directory '"<<settings_dir<<"':"<<ec.message()<<std::endl;

        // Setup Dear ImGui style
        ImGui::StyleColorsLight();

        // Setup Platform/Renderer backends
        ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
        ImGui_ImplOpenGL3_Init(glsl_version);

#ifdef HAS_FONTCONFIG
        std::get<GUIInit>(ret).languages.cshParams->fontnames = GetFontNames();
#endif

#define STRING2(s) #s
#define STRING(s) STRING2(s)

        const std::vector example_dirs = {
            //1. Search for a binary compiled in-place
            //If the executable path is with "XXX/src/unix-msc-gen/msc-gen" then search for "XXX/examples/"
            [argv]()->std::string {
                if (!argv[0] || !argv[0][0]) return {};
                try {
                    std::filesystem::path p(argv[0]);
                    p = canonical(p);
                    if (p.filename()!="msc-gen") return {};
                    p = p.parent_path();
                    if (p.filename()!="unix-msc-gen") return {};
                    p = p.parent_path();
                    if (p.filename()!="src") return {};
                    return (p.parent_path() / "examples").native()+"/";
                } catch (std::filesystem::filesystem_error&) { return {}; }
            }(),
#ifdef MSC_GEN_EXAMPLES_DIR
            //2. automake will compile us with MSC_GEN_EXAMPLES_DIR set to the dir where
            //the examples will be installed when issuing make install
            //We dont check but this shall be an absolute path...
            std::string(STRING(MSC_GEN_EXAMPLES_DIR) "/"),
#else
            //If this is being compiled without automake, emit a warning
            #warning "MSC_GEN_EXAMPLES_DIR is not defined, msc-gen will not find the examples. Do you compile from Makefile.am?"
#endif
        };

        //Fill in what documentation files do we have. Search through some dirs
        std::vector<std::string> doc_dirs;
        if (example_dirs[0].size()) //for in-place compiled stuff we replace "examples" to "doc"
            doc_dirs.push_back((std::filesystem::path(example_dirs[0]).parent_path().parent_path() / "doc").native()+"/");
#ifdef MSC_GEN_DOC_DIR
        //2. automake will compile us with MSC_GEN_DOC_DIR set to the dir where
        //the doc will be installed when issuing make install
        //We dont check but this shall be an absolute path...
        doc_dirs.push_back(STRING(MSC_GEN_DOC_DIR) "/");
#else
        //If this is being compiled without automake, emit a warning
        #warning "MSC_GEN_DOC_DIR is not defined, msc-gen will not find the documentation. Do you compile from Makefile.am?"
#endif
        for (auto [name, file] : std::multimap<const char*, const char*>{{"PDF", "msc-gen.pdf"}, {"HTML", "html/index.html"}, {"HTML", "msc-gen.html/index.html"}})
            for (const std::string& dir : doc_dirs)
                if (std::error_code ec; std::filesystem::exists(dir+file, ec)) {
                    Documentation[name] = dir+file;
                    break;
                }
        std::vector<std::string> doc_names;
        for (const auto& [name, _] : Documentation)
            doc_names.push_back(name);

        GUIWindowGeometry w = InitGUI(std::get<GUIInit>(ret), dpi_mul(bounds),
                                      settings_dir, std::filesystem::current_path().native(),
                                      example_dirs, std::move(doc_names));
        SDL_DisplayMode DM{0,0,0,0,0};
        SDL_GetCurrentDisplayMode(0, &DM);
        if (DM.w>0 && DM.h>0)
            w.ensure_fit_in(DM.w, DM.h);
        if (w.has_size()) SDL_SetWindowSize(window, w.cx, w.cy);
        if (w.has_pos()) SDL_SetWindowPosition(window, w.x, w.y);
        SDL_ShowWindow(window);
        SDL_EventState(SDL_DROPFILE, SDL_ENABLE);

        timespec last_refresh;
        clock_gettime(CLOCK_REALTIME, &last_refresh);
        const long long inter_frame_time_ns = 1e9 / std::get<GUIInit>(ret).max_fps;

        // Main loop
        while (true) {
            //Sleep if needed to limit max FPS
            timespec now;
            clock_gettime(CLOCK_REALTIME, &now);
            const long long diff_ns = (long long)(now.tv_sec - last_refresh.tv_sec) * 1'000'000'000 + (now.tv_nsec - last_refresh.tv_nsec);
            if (diff_ns < inter_frame_time_ns) {
                const timespec sleep_ns{0, std::min(inter_frame_time_ns - diff_ns, 999'999'999LL)};
                nanosleep(&sleep_ns, nullptr);
            }
            last_refresh = now;

            GUIEvents events;
            // Poll and handle events (inputs, window resize, etc.)
            // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
            // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application.
            // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application.
            // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
            SDL_Event event;
            while (SDL_PollEvent(&event)) {
                ImGui_ImplSDL2_ProcessEvent(&event);
                if (event.type == SDL_QUIT)
                    events.request_exit = true;
                if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE && event.window.windowID == SDL_GetWindowID(window))
                    events.request_exit = true;
                if (event.type == SDL_DROPFILE && event.drop.file) {
                    events.dropped.full_names.emplace_back(event.drop.file);
                    SDL_free(event.drop.file);
                }
            }
            if (!SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(window), &bounds))
                if (PreFrameGUI(dpi_mul(bounds))) {
                    ImGui_ImplOpenGL3_DestroyFontsTexture();
                    ImGui_ImplOpenGL3_CreateFontsTexture();
                }

            // Start the Dear ImGui frame
            ImGui_ImplOpenGL3_NewFrame();
            ImGui_ImplSDL2_NewFrame();
            //We need to do this because the above function calls SDL_GetGlobalMouseState() which dont work.
            int x,y;
            SDL_GetMouseState(&x, &y);
            ImGui::GetIO().MousePos = ImVec2(float(x), float(y));

            ImGui::NewFrame();

            if (rusage t; !getrusage(RUSAGE_SELF, &t))
                events.total_cpu = std::chrono::microseconds(t.ru_utime.tv_usec) + std::chrono::seconds(t.ru_utime.tv_sec)
                                 + std::chrono::microseconds(t.ru_stime.tv_usec) + std::chrono::seconds(t.ru_stime.tv_sec);
            else
                events.total_cpu.reset();

            GUIReturn res = DoGUI(events);

            // Rendering
            ImGui::Render();
            ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
            SDL_GL_SwapWindow(window);

            if (res.window_title.size() && res.window_title != last_window_title) {
                last_window_title = res.window_title;
                SDL_SetWindowTitle(window, (WINDOW_TITLE_PREFIX " - " + last_window_title).c_str());
            }
            if (we_are_fullscreen!=(res.action == GUIReturn::FullScreen)) {
                if (SDL_SetWindowFullscreen(window, res.action == GUIReturn::FullScreen ? SDL_WINDOW_FULLSCREEN : 0))
                    std::cerr << "Could not change fullscreen status: " << SDL_GetError() << std::endl;
                else
                    we_are_fullscreen = res.action == GUIReturn::FullScreen;
            }

            if (res.action == GUIReturn::Exit) break;
            if (res.action == GUIReturn::Focus) ::SDL_RaiseWindow(window);
        }

        SDL_GetWindowPosition(window, &w.x, &w.y);
        SDL_GetWindowSize(window, &w.cx, &w.cy);
        ShutdownGUI(w);

        // Cleanup
        ImGui_ImplOpenGL3_Shutdown();
        ImGui_ImplSDL2_Shutdown();
        ImGui::DestroyContext();

        SDL_GL_DeleteContext(gl_context);
        SDL_DestroyWindow(window);
        SDL_Quit();

        ret = EXIT_SUCCESS;

#else //BUILD_GUI

        std::cerr << "This executable has no GUI compiled into it. " << std::endl;
        return EXIT_FAILURE;

#endif // BUILD_GUI
    }

    //Finally: common to cmdline and GUI: Write out progress balance data
    FILE *fout = fopen(load_data_fname.c_str(), "wb");
    if (fout) {
        fwrite(load_data.c_str(), load_data.length(), 1, fout);
        fclose(fout);
    }
    return std::get<int>(ret);
}


#ifdef BUILD_GUI

TextureID TextureFromSurface(cairo_surface_t *rgba_surface) {
    GLenum err;
    while ((err = glGetError()) != GL_NO_ERROR) { _ASSERT(0); } //purge error queue
    // Load from disk into a raw RGBA buffer
    const int image_width = cairo_image_surface_get_width(rgba_surface);
    const int image_height = cairo_image_surface_get_height(rgba_surface);

    // Create a OpenGL texture identifier
    GLuint image_texture;
    glGenTextures(1, &image_texture);
    glBindTexture(GL_TEXTURE_2D, image_texture);

    // Setup filtering parameters for display
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // This is required on WebGL for non power-of-two textures
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Same

    // Upload pixels into texture
#if defined(GL_UNPACK_ROW_LENGTH) && !defined(__EMSCRIPTEN__)
    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
#endif
    const unsigned char *const image_data = cairo_image_surface_get_data(rgba_surface);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height, 0, GL_BGRA, GL_UNSIGNED_BYTE, image_data);

    bool had_error = false;
    while ((err = glGetError()) != GL_NO_ERROR) had_error = true;

    if (!had_error && image_texture)
        return (ImTextureID)(size_t)image_texture;
    return {};
}

void DestroyTexture(ImTextureID t) {
    const GLuint i = (GLuint)(size_t)t;
    glDeleteTextures(1, &i);
}

#ifdef HAS_FONTCONFIG
std::set<std::string> GetFontNames() {
    std::set<std::string> ret;
    if (!FcInit()) return ret;
    FcPattern * const pat = FcPatternCreate();
    FcObjectSet *const os = FcObjectSetBuild(FC_FAMILY, FC_STYLE, FC_FILE, nullptr);
    const FcChar8 *const format = (const FcChar8 *)"%{family[0]}"; //TODO: remove [0] and parse comma-separated list of family names below
    FcFontSet *fs = FcFontList(0, pat, os);
    if (os)
        FcObjectSetDestroy(os);
    if (pat)
        FcPatternDestroy(pat);
    if (fs) {
        for (int j = 0; j < fs->nfont; j++) {
            FcBool outline = false;
            FcPatternGetBool(fs->fonts[j], "outline", 0, &outline);
            if (!outline) continue;
            if (FcChar8 *s = FcPatternFormat(fs->fonts[j], format)) {
                ret.insert((char*)s);
                FcStrFree(s);
            }
        }
        FcFontSetDestroy(fs);
    }
    FcFini();
    return ret;
}
#endif //HAS_FONTCONFIG

#ifdef __APPLE__

#include "clipboard.h"
//On the Mac, it is in clipboard.mm
std::optional<std::pair<ClipboardFormat, std::string>> GetClipboard() {
    MacClipboardFormat format;
    unsigned len = 0;
    char* content = MacGetClipboard(&format, &len);
    std::optional<std::pair<ClipboardFormat, std::string>> ret;
    ClipboardFormat fmt;
    switch (format) {
    case MacClipboardFormat::Art_GVML: fmt = ClipboardFormat::Art_GVML; break;
    case MacClipboardFormat::Escaped_Text: fmt = ClipboardFormat::Escaped_Text; break;
    default:
    case MacClipboardFormat::Else: fmt = ClipboardFormat::Else; break;
    case MacClipboardFormat::No_Change: return ret;
    }
    ret.emplace(fmt, "");
    if (content && len) {
        ret->second.assign(content, len);
        free(content);
    }
    return ret;
}

bool CopyToClipboard(ClipboardFormat format, const std::string& content, std::string_view type) {
    std::string mac_type(type);
    const MacClipboardFormat mac_format = [format]() {
        switch (format) {
        case ClipboardFormat::Art_GVML: return MacClipboardFormat::Art_GVML;
        case ClipboardFormat::Escaped_Text: return MacClipboardFormat::Escaped_Text;
        default: _ASSERT(0); return (MacClipboardFormat)format;
        }
    }();
    return 0!=MacCopyToClipboard(mac_format, content.data(), content.length(), mac_type.c_str());
}

#else
//On Linux we have no clipboard support for now
std::optional<std::pair<ClipboardFormat, std::string>> GetClipboard() {
    return {};
}

bool CopyToClipboard(ClipboardFormat /*format*/, const std::string& /*content*/, std::string_view /*type*/) {
    return true; //dont try again
}

#endif //ifndef(__APPLE__)

#endif // BUILD_GUI
