#include "fiddle_context.hpp"

#include "rive/math/simd.hpp"
#include "rive/artboard.hpp"
#include "rive/file.hpp"
#include "rive/refcnt.hpp"
#include "rive/layout.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/static_scene.hpp"
#include "rive/profiler/profiler_macros.h"

#include <filesystem>
#include <fstream>
#include <iterator>
#include <vector>
#include <sstream>

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

#define GLFW_INCLUDE_NONE
#include "GLFW/glfw3.h"

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

using namespace rive;

constexpr static char kMoltenVKICD[] =
    "dependencies/MoltenVK/Package/Release/MoltenVK/dynamic/dylib/macOS/"
    "MoltenVK_icd.json";

constexpr static char kSwiftShaderICD[] = "dependencies/SwiftShader/build/"
#ifdef __APPLE__
                                          "Darwin"
#elif defined(_WIN32)
                                          "Windows"
#else
                                          "Linux"
#endif
                                          "/vk_swiftshader_icd.json";

#ifdef _WIN32
extern "C"
{
    // https://stackoverflow.com/questions/68469954/how-to-choose-specific-gpu-when-create-opengl-context:
    //
    //   OpenGL, or rather the Win32 GDI integration of it, doesn't offer means
    //   to explicitly select the desired device. However the drivers of Nvidia
    //   and AMD offer a workaround to have programs select, that they prefer to
    //   execute on the discrete GPU rather than the CPU integrated one.
    //
    // These also appear to select the discrete "Arc" GPU on an Intel system,
    // and to influence the GPU selection on D3D11.
    __declspec(dllexport) uint32_t NvOptimusEnablement = 1;
    __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
#endif

static FiddleContextOptions options;
static GLFWwindow* window = nullptr;
static uint32_t msaa = 0;
static bool forceAtomicMode = false;
static bool wireframe = false;
static bool disableFill = false;
static bool disableStroke = false;
static bool clockwiseFill = false;
static bool hotloadShaders = false;

static std::unique_ptr<FiddleContext> fiddleContext;

static float2 pts[] = {{260 + 2 * 100, 60 + 2 * 500},
                       {260 + 2 * 257, 60 + 2 * 233},
                       {260 + 2 * -100, 60 + 2 * 300},
                       {260 + 2 * 100, 60 + 2 * 200},
                       {260 + 2 * 250, 60 + 2 * 0},
                       {260 + 2 * 400, 60 + 2 * 200},
                       {260 + 2 * 213, 60 + 2 * 200},
                       {260 + 2 * 213, 60 + 2 * 300},
                       {260 + 2 * 391, 60 + 2 * 480},
                       {1400, 1400}}; // Feather control.
constexpr static int NUM_INTERACTIVE_PTS = sizeof(pts) / sizeof(*pts);

static float strokeWidth = 70;

static float2 translate;
static float scale = 1;

static StrokeJoin join = StrokeJoin::round;
static StrokeCap cap = StrokeCap::round;

static bool doClose = false;
static bool paused = false;
static bool unlockedLogic = true;

static int dragIdx = -1;
static float2 dragLastPos;

static int animation = -1;
static int stateMachine = -1;
static int horzRepeat = 0;
static int upRepeat = 0;
static int downRepeat = 0;

rcp<File> rivFile;
std::vector<std::unique_ptr<Artboard>> artboards;
std::vector<std::unique_ptr<Scene>> scenes;
std::vector<rive::rcp<rive::ViewModelInstance>> viewModelInstances;

static void clear_scenes()
{
    artboards.clear();
    scenes.clear();
    viewModelInstances.clear();
}

static void make_scenes(size_t count)
{
    clear_scenes();
    for (size_t i = 0; i < count; ++i)
    {
        // Tip: you can change the artboard shown here
        // auto artboard = rivFile->artboardAt(2);
        auto artboard = rivFile->artboardDefault();
        std::unique_ptr<Scene> scene;
        if (stateMachine >= 0)
        {
            scene = artboard->stateMachineAt(stateMachine);
        }
        else if (animation >= 0)
        {
            scene = artboard->animationAt(animation);
        }
        else
        {
            scene = artboard->animationAt(0);
        }
        if (scene == nullptr)
        {
            // This is a riv without any animations or state machines. Just draw
            // the artboard.
            scene = std::make_unique<StaticScene>(artboard.get());
        }

        int viewModelId = artboard.get()->viewModelId();
        viewModelInstances.push_back(
            viewModelId == -1
                ? rivFile->createViewModelInstance(artboard.get())
                : rivFile->createViewModelInstance(viewModelId, 0));
        artboard->bindViewModelInstance(viewModelInstances.back());
        if (viewModelInstances.back() != nullptr)
        {
            scene->bindViewModelInstance(viewModelInstances.back());
        }

        scene->advanceAndApply(scene->durationSeconds() * i / count);
        artboards.push_back(std::move(artboard));
        scenes.push_back(std::move(scene));
    }
}

#ifdef __EMSCRIPTEN__
EM_JS(int, window_inner_width, (), { return window["innerWidth"]; });
EM_JS(int, window_inner_height, (), { return window["innerHeight"]; });
EM_JS(char*, get_location_hash_str, (), {
    var jsString = window.location.hash.substring(1);
    var lengthBytes = lengthBytesUTF8(jsString) + 1;
    var stringOnWasmHeap = _malloc(lengthBytes);
    stringToUTF8(jsString, stringOnWasmHeap, lengthBytes);
    return stringOnWasmHeap;
});
#endif

static void mouse_button_callback(GLFWwindow* window,
                                  int button,
                                  int action,
                                  int mods)
{
    double x, y;
    glfwGetCursorPos(window, &x, &y);
    float dpiScale = fiddleContext->dpiScale(window);
    x *= dpiScale;
    y *= dpiScale;
    dragLastPos = float2{(float)x, (float)y};
    if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS)
    {
        dragIdx = -1;
        if (!rivFile)
        {
            for (int i = 0; i < NUM_INTERACTIVE_PTS; ++i)
            {
                if (simd::all(simd::abs(dragLastPos - (pts[i] + translate)) <
                              100))
                {
                    dragIdx = i;
                    break;
                }
            }
        }
    }
}

static void mousemove_callback(GLFWwindow* window, double x, double y)
{
    float dpiScale = fiddleContext->dpiScale(window);
    x *= dpiScale;
    y *= dpiScale;
    if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS)
    {
        float2 pos = float2{(float)x, (float)y};
        if (dragIdx >= 0)
        {
            pts[dragIdx] += (pos - dragLastPos);
        }
        else
        {
            translate += (pos - dragLastPos);
        }
        dragLastPos = pos;
    }
}

int lastWidth = 0, lastHeight = 0;
double fpsLastTime = 0;
double fpsLastTimeLogic = 0;
int fpsFrames = 0;
static bool needsTitleUpdate = false;

enum class API
{
    gl,
    metal,
    d3d,
    d3d12,
    dawn,
    vulkan,
};

API api =
#if defined(__APPLE__)
    API::metal
#elif defined(_WIN32)
    API::d3d
#else
    API::gl
#endif
    ;

bool angle = false;
bool skia = false;

static void key_callback(GLFWwindow* window,
                         int key,
                         int scancode,
                         int action,
                         int mods)
{
    bool shift = mods & GLFW_MOD_SHIFT;
    if (action == GLFW_PRESS)
    {
        switch (key)
        {
            case GLFW_KEY_ESCAPE:
                glfwSetWindowShouldClose(window, 1);
                break;
            case GLFW_KEY_GRAVE_ACCENT: // ` key, backtick
                hotloadShaders = true;
                break;
            case GLFW_KEY_A:
                forceAtomicMode = !forceAtomicMode;
                fpsLastTime = 0;
                fpsFrames = 0;
                needsTitleUpdate = true;
                break;
            case GLFW_KEY_D:
                printf("static float scale = %f;\n", scale);
                printf("static float2 translate = {%f, %f};\n",
                       translate.x,
                       translate.y);
                printf("static float2 pts[] = {");
                for (int i = 0; i < NUM_INTERACTIVE_PTS; i++)
                {
                    printf("{%g, %g}", pts[i].x, pts[i].y);
                    if (i < NUM_INTERACTIVE_PTS - 1)
                    {
                        printf(", ");
                    }
                    else
                    {
                        printf("};\n");
                    }
                }
                fflush(stdout);
                break;
            case GLFW_KEY_Z:
                fiddleContext->toggleZoomWindow();
                break;
            case GLFW_KEY_MINUS:
                strokeWidth /= 1.5f;
                break;
            case GLFW_KEY_EQUAL:
                strokeWidth *= 1.5f;
                break;
            case GLFW_KEY_W:
                wireframe = !wireframe;
                break;
            case GLFW_KEY_C:
                cap = static_cast<StrokeCap>((static_cast<int>(cap) + 1) % 3);
                break;
            case GLFW_KEY_O:
                doClose = !doClose;
                break;
            case GLFW_KEY_S:
                if (shift)
                {
                    // Toggle Skia.
                    clear_scenes();
                    rivFile = nullptr;
                    skia = !skia;
                    fiddleContext = skia ? FiddleContext::MakeGLSkia()
                                         : FiddleContext::MakeGLPLS();
                    lastWidth = 0;
                    lastHeight = 0;
                    fpsLastTime = 0;
                    fpsFrames = 0;
                    needsTitleUpdate = true;
                }
                else
                {
                    disableStroke = !disableStroke;
                }
                break;
            case GLFW_KEY_F:
                disableFill = !disableFill;
                break;
            case GLFW_KEY_X:
                clockwiseFill = !clockwiseFill;
                break;
            case GLFW_KEY_P:
                paused = !paused;
                break;
            case GLFW_KEY_H:
                if (!shift)
                    ++horzRepeat;
                else if (horzRepeat > 0)
                    --horzRepeat;
                break;
            case GLFW_KEY_K:
                if (!shift)
                    ++upRepeat;
                else if (upRepeat > 0)
                    --upRepeat;
                break;
            case GLFW_KEY_J:
                if (!rivFile)
                    join = static_cast<StrokeJoin>(
                        (static_cast<int>(join) + 1) % 3);
                else if (!shift)
                    ++downRepeat;
                else if (downRepeat > 0)
                    --downRepeat;
                break;
            case GLFW_KEY_UP:
            {
                float oldScale = scale;
                scale *= 1.25;
                double x = 0, y = 0;
                glfwGetCursorPos(window, &x, &y);
                float2 cursorPos = float2{(float)x, (float)y} *
                                   fiddleContext->dpiScale(window);
                translate =
                    cursorPos + (translate - cursorPos) * scale / oldScale;
                break;
            }
            case GLFW_KEY_DOWN:
            {
                float oldScale = scale;
                scale /= 1.25;
                double x = 0, y = 0;
                glfwGetCursorPos(window, &x, &y);
                float2 cursorPos = float2{(float)x, (float)y} *
                                   fiddleContext->dpiScale(window);
                translate =
                    cursorPos + (translate - cursorPos) * scale / oldScale;
                break;
            }
            case GLFW_KEY_U:
                unlockedLogic = !unlockedLogic;
        }
    }
}

static void glfw_error_callback(int code, const char* message)
{
    printf("GLFW error: %i - %s\n", code, message);
}

static void set_environment_variable(const char* name, const char* value)
{
    if (const char* existingValue = getenv(name))
    {
        printf("warning: %s=%s already set. Overriding with %s=%s\n",
               name,
               existingValue,
               name,
               value);
    }
#ifdef _WIN32
    SetEnvironmentVariableA(name, value);
#else
    setenv(name, value, /*overwrite=*/true);
#endif
}

std::unique_ptr<Renderer> renderer;
std::string rivName;

void riveMainLoop();

int main(int argc, const char** argv)
{
    // Cause stdout and stderr to print immediately without buffering.
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

#ifdef DEBUG
    options.enableVulkanCoreValidationLayers = true;
#endif

#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop(riveMainLoop, 0, false);

    // Override argc/argv with the window location hash string.
    char* hash = get_location_hash_str();
    std::stringstream ss(hash);
    std::vector<std::string> hashStrs;
    std::vector<const char*> hashArgs;
    std::string arg;

    hashStrs.push_back("index.html");
    while (std::getline(ss, arg, ':'))
    {
        hashStrs.push_back(std::move(arg));
    }
    for (const std::string& str : hashStrs)
    {
        hashArgs.push_back(str.c_str());
    }

    argc = hashArgs.size();
    argv = hashArgs.data();
    free(hash);
#endif

    for (int i = 1; i < argc; i++)
    {
        if (!strcmp(argv[i], "--gl"))
        {
            api = API::gl;
        }
        else if (!strcmp(argv[i], "--glatomic"))
        {
            api = API::gl;
            forceAtomicMode = true;
        }
        else if (!strcmp(argv[i], "--glcw"))
        {
            api = API::gl;
            clockwiseFill = true;
        }
        else if (!strcmp(argv[i], "--metal"))
        {
            api = API::metal;
        }
        else if (!strcmp(argv[i], "--metalcw"))
        {
            api = API::metal;
            clockwiseFill = true;
        }
        else if (!strcmp(argv[i], "--metalatomic"))
        {
            api = API::metal;
            forceAtomicMode = true;
        }
        else if (!strcmp(argv[i], "--mvk") || !strcmp(argv[i], "--moltenvk"))
        {
            set_environment_variable("VK_ICD_FILENAMES", kMoltenVKICD);
            api = API::vulkan;
        }
        else if (!strcmp(argv[i], "--mvkatomic") ||
                 !strcmp(argv[i], "--moltenvkatomic"))
        {
            set_environment_variable("VK_ICD_FILENAMES", kMoltenVKICD);
            api = API::vulkan;
            forceAtomicMode = true;
        }
        else if (!strcmp(argv[i], "--sw") || !strcmp(argv[i], "--swiftshader"))
        {
            // Use the swiftshader built by
            // packages/runtime/renderer/make_swiftshader.sh
            set_environment_variable("VK_ICD_FILENAMES", kSwiftShaderICD);
            api = API::vulkan;
        }
        else if (!strcmp(argv[i], "--swatomic") ||
                 !strcmp(argv[i], "--swiftshaderatomic"))
        {
            // Use the swiftshader built by
            // packages/runtime/renderer/make_swiftshader.sh
            set_environment_variable("VK_ICD_FILENAMES", kSwiftShaderICD);
            api = API::vulkan;
            forceAtomicMode = true;
        }
        else if (!strcmp(argv[i], "--dawn"))
        {
            api = API::dawn;
        }
        else if (!strcmp(argv[i], "--d3d"))
        {
            api = API::d3d;
        }
        else if (!strcmp(argv[i], "--d3d12"))
        {
            api = API::d3d12;
        }
        else if (!strcmp(argv[i], "--d3datomic"))
        {
            api = API::d3d;
            forceAtomicMode = true;
        }
        else if (!strcmp(argv[i], "--d3d12atomic"))
        {
            api = API::d3d12;
            forceAtomicMode = true;
        }
        else if (!strcmp(argv[i], "--vulkan") || !strcmp(argv[i], "--vk"))
        {
            api = API::vulkan;
        }
        else if (!strcmp(argv[i], "--vkcw"))
        {
            api = API::vulkan;
            clockwiseFill = true;
        }
        else if (!strcmp(argv[i], "--vulkanatomic") ||
                 !strcmp(argv[i], "--vkatomic"))
        {
            api = API::vulkan;
            forceAtomicMode = true;
        }
#ifdef RIVE_DESKTOP_GL
        else if (!strcmp(argv[i], "--angle_gl"))
        {
            api = API::gl;
            glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE,
                         GLFW_ANGLE_PLATFORM_TYPE_OPENGL);
            angle = true;
        }
        else if (!strcmp(argv[i], "--angle_d3d"))
        {
            api = API::gl;
            glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE,
                         GLFW_ANGLE_PLATFORM_TYPE_D3D11);
            angle = true;
        }
        else if (!strcmp(argv[i], "--angle_vk"))
        {
            api = API::gl;
            glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE,
                         GLFW_ANGLE_PLATFORM_TYPE_VULKAN);
            angle = true;
        }
        else if (!strcmp(argv[i], "--angle_mtl"))
        {
            api = API::gl;
            glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE,
                         GLFW_ANGLE_PLATFORM_TYPE_METAL);
            angle = true;
        }
#endif
        else if (!strcmp(argv[i], "--skia"))
        {
            skia = true;
        }
        else if (sscanf(argv[i], "-a%i", &animation) == 1)
        {
            // Already updated animation.
        }
        else if (sscanf(argv[i], "-s%i", &stateMachine) == 1)
        {
            // Already updated stateMachine.
        }
        else if (sscanf(argv[i], "-h%i", &horzRepeat) == 1)
        {
            // Already updated horzRepeat.
        }
        else if (sscanf(argv[i], "-j%i", &downRepeat) == 1)
        {
            // Already updated downRepeat.
        }
        else if (sscanf(argv[i], "-k%i", &upRepeat) == 1)
        {
            // Already updated upRepeat.
        }
        else if (!strcmp(argv[i], "-p"))
        {
            paused = true;
        }
        else if (!strcmp(argv[i], "--d3d12Warp"))
        {
            options.d3d12UseWarpDevice = true;
        }
        else if (!strcmp(argv[i], "--atomic"))
        {
            forceAtomicMode = true;
        }
        else if (!strcmp(argv[i], "--cw"))
        {
            clockwiseFill = true;
        }
        else if (sscanf(argv[i], "--msaa%u", &msaa) == 1)
        {
            // Already updated msaa
        }
        else if (!strcmp(argv[i], "--core"))
        {
            options.coreFeaturesOnly = true;
        }
        else if (!strcmp(argv[i], "--validation"))
        {
            options.enableVulkanCoreValidationLayers = true;
        }
        else if (!strcmp(argv[i], "--gpu") || !strcmp(argv[i], "-g"))
        {
            options.gpuNameFilter = argv[++i];
        }
        else if (!strcmp(argv[i], "--integrated") || !strcmp(argv[i], "-i"))
        {
            options.gpuNameFilter = "integrated";
        }
        else if (!strncmp(argv[i], "--", 2))
        {
            fprintf(stderr, "Unknown command-line option %s\n", argv[i]);
            return 1;
        }
        else
        {
            rivName = argv[i];
        }
    }

#ifdef _WIN32
    // Set our backdoor GPU selection variables in case the API doesn't allow us
    // to select explicitly.
    if (options.gpuNameFilter != nullptr)
    {
        // "i" and "integrated" are special-case gpuNameFilters that mean "use
        // the integrated GPU".
        bool wantIntegratedGPU = static_cast<uint32_t>(
            strcmp(options.gpuNameFilter, "integrated") == 0 ||
            strcmp(options.gpuNameFilter, "i") == 0);
        NvOptimusEnablement = !wantIntegratedGPU;
        AmdPowerXpressRequestHighPerformance = !wantIntegratedGPU;
    }
#endif

    glfwSetErrorCallback(glfw_error_callback);

    if (!glfwInit())
    {
        fprintf(stderr, "Failed to initialize glfw.\n");
        return 1;
    }

    if (msaa > 0)
    {
        if (msaa > 1)
        {
            glfwWindowHint(GLFW_SAMPLES, msaa);
        }
        glfwWindowHint(GLFW_STENCIL_BITS, 8);
        glfwWindowHint(GLFW_DEPTH_BITS, 16);
    }
    glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
    switch (api)
    {
        case API::metal:
        case API::d3d:
        case API::d3d12:
        case API::dawn:
            glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
            glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_TRUE);
            break;
        case API::vulkan:
            glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
            break;
        case API::gl:
            if (angle)
            {
                glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API);
                glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
                glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
                glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
                glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
            }
            else
            {
                glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
                glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
                glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
            }
            break;
    }
    glfwWindowHint(GLFW_FOCUS_ON_SHOW, GLFW_TRUE);
    // glfwWindowHint(GLFW_FLOATING, GLFW_TRUE);
    window = glfwCreateWindow(1600, 1600, "Rive Renderer", nullptr, nullptr);
    if (!window)
    {
        glfwTerminate();
        fprintf(stderr, "Failed to create window.\n");
        return -1;
    }

    glfwSetMouseButtonCallback(window, mouse_button_callback);
    glfwSetCursorPosCallback(window, mousemove_callback);
    glfwSetKeyCallback(window, key_callback);
    if (api == API::gl)
    {
        glfwMakeContextCurrent(window);
    }
    glfwShowWindow(window);

    switch (api)
    {
        case API::metal:
            fiddleContext = FiddleContext::MakeMetalPLS(options);
            break;
        case API::d3d:
            fiddleContext = FiddleContext::MakeD3DPLS(options);
            break;
        case API::d3d12:
            fiddleContext = FiddleContext::MakeD3D12PLS(options);
            break;
        case API::dawn:
            fiddleContext = FiddleContext::MakeDawnPLS(options);
            break;
        case API::vulkan:
            fiddleContext = FiddleContext::MakeVulkanPLS(options);
            break;
        case API::gl:
            fiddleContext =
                skia ? FiddleContext::MakeGLSkia() : FiddleContext::MakeGLPLS();
            break;
    }
    if (!fiddleContext)
    {
        fprintf(stderr, "Failed to create a fiddle context.\n");
        abort();
    }

#ifndef __EMSCRIPTEN__
    if (api == API::gl)
    {
        glfwSwapInterval(0);
    }
    while (!glfwWindowShouldClose(window))
    {
        riveMainLoop();
        fiddleContext->tick();
        if (api == API::gl)
        {
            glfwSwapBuffers(window);
        }
        if (!rivName.empty())
        {
            glfwPollEvents();
        }
        else
        {
            glfwWaitEvents();
        }
    }

    // We need to clear the riv scene (which can be holding on to render
    // resources) before releasing the fiddle context
    clear_scenes();
    rivFile = nullptr;

    fiddleContext = nullptr;
    glfwTerminate();
#endif

    return 0;
}

static void update_window_title(double fps,
                                int instances,
                                int width,
                                int height)
{
    std::ostringstream title;
    if (fps != 0)
    {
        title << '[' << fps << " FPS]";
    }
    if (instances > 1)
    {
        title << " (x" << instances << " instances)";
    }
    if (skia)
    {
        title << " | SKIA Renderer";
    }
    else
    {
        title << " | RIVE Renderer";
    }
    if (msaa)
    {
        title << " (msaa" << msaa << ')';
    }
    else if (forceAtomicMode)
    {
        title << " (atomic)";
    }
    title << " | " << width << " x " << height;
    glfwSetWindowTitle(window, title.str().c_str());
}

void riveMainLoop()
{
    RIVE_PROF_FRAME()
    RIVE_PROF_SCOPE()

#ifdef __EMSCRIPTEN__
    {
        // Fit the canvas to the browser window size.
        int windowWidth = window_inner_width();
        int windowHeight = window_inner_height();
        double devicePixelRatio = emscripten_get_device_pixel_ratio();
        int canvasExpectedWidth = windowWidth * devicePixelRatio;
        int canvasExpectedHeight = windowHeight * devicePixelRatio;
        int canvasWidth, canvasHeight;
        glfwGetFramebufferSize(window, &canvasWidth, &canvasHeight);
        if (canvasWidth != canvasExpectedWidth ||
            canvasHeight != canvasExpectedHeight)
        {
            glfwSetWindowSize(window,
                              canvasExpectedWidth,
                              canvasExpectedHeight);
            emscripten_set_element_css_size("#canvas",
                                            windowWidth,
                                            windowHeight);
        }
    }
#endif

    int width = 0, height = 0;
    glfwGetFramebufferSize(window, &width, &height);
    if (lastWidth != width || lastHeight != height)
    {
        printf("size changed to %ix%i\n", width, height);
        lastWidth = width;
        lastHeight = height;
        fiddleContext->onSizeChanged(window, width, height, msaa);
        renderer = fiddleContext->makeRenderer(width, height);
        needsTitleUpdate = true;
    }
    if (needsTitleUpdate)
    {
        update_window_title(0, 1, width, height);
        needsTitleUpdate = false;
    }

    if (!rivName.empty() && !rivFile)
    {
        std::ifstream rivStream(rivName, std::ios::binary);
        std::vector<uint8_t> rivBytes(std::istreambuf_iterator<char>(rivStream),
                                      {});
        rivFile = File::import(rivBytes, fiddleContext->factory());
    }

    // Call right before begin()
    if (hotloadShaders)
    {
        hotloadShaders = false;

#ifndef RIVE_BUILD_FOR_IOS
        // We want to build to /tmp/rive (or the correct equivalent)
        std::filesystem::path tempRiveDir =
            std::filesystem::temp_directory_path() / "rive";

        // Get the u8 version of the path (this is especially necessary on
        // windows where the native path character type is wchar_t, then
        // reinterpret_cast the char8_t pointer to char so we can append it to
        // our string.
        // Store the u8string result to extend its lifetime (need to
        // reinterpret_cast through a const char * pointer because u8string
        // returns a std::u8string in C++20 and newer, but we need it as a
        // "char" string)
        std::string tempRiveDirStr =
            reinterpret_cast<const char*>(tempRiveDir.u8string().c_str());

        std::string rebuildCommand = "sh rebuild_shaders.sh " + tempRiveDirStr;
        std::system(rebuildCommand.c_str());
#endif
        fiddleContext->hotloadShaders();
    }
    fiddleContext->begin({
        .renderTargetWidth = static_cast<uint32_t>(width),
        .renderTargetHeight = static_cast<uint32_t>(height),
        .clearColor = 0xff303030,
        .msaaSampleCount = msaa,
        .disableRasterOrdering = forceAtomicMode,
        .wireframe = wireframe,
        .fillsDisabled = disableFill,
        .strokesDisabled = disableStroke,
        .clockwiseFillOverride = clockwiseFill,
    });

    int instances = 1;
    if (rivFile)
    {
        instances = (1 + horzRepeat * 2) * (1 + upRepeat + downRepeat);
        if (artboards.size() != instances || scenes.size() != instances)
        {
            make_scenes(instances);
        }
        else if (!paused)
        {
            float dT = 1 / 120.f;

            if (!unlockedLogic)
            {
                double time = glfwGetTime();
                dT = time - fpsLastTimeLogic;
                fpsLastTimeLogic = time;
            }

            for (const auto& scene : scenes)
            {
                scene->advanceAndApply(dT);
            }
        }
        Mat2D m = computeAlignment(rive::Fit::contain,
                                   rive::Alignment::center,
                                   rive::AABB(0, 0, width, height),
                                   artboards.front()->bounds());
        renderer->save();
        m = Mat2D(scale, 0, 0, scale, translate.x, translate.y) * m;
        renderer->transform(m);
        float spacing = 200 / m.findMaxScale();
        auto scene = scenes.begin();
        for (int j = 0; j < upRepeat + 1 + downRepeat; ++j)
        {
            renderer->save();
            renderer->transform(Mat2D::fromTranslate(-spacing * horzRepeat,
                                                     (j - upRepeat) * spacing));
            for (int i = 0; i < horzRepeat * 2 + 1; ++i)
            {

                (*scene++)->draw(renderer.get());
                renderer->transform(Mat2D::fromTranslate(spacing, 0));
            }
            renderer->restore();
        }
        renderer->restore();
    }
    else
    {
        float2 p[9];
        for (int i = 0; i < 9; ++i)
        {
            p[i] = pts[i] + translate;
        }
        RawPath rawPath;
        rawPath.moveTo(p[0].x, p[0].y);
        rawPath.cubicTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y);
        float2 c0 = simd::mix(p[3], p[4], float2(2 / 3.f));
        float2 c1 = simd::mix(p[5], p[4], float2(2 / 3.f));
        rawPath.cubicTo(c0.x, c0.y, c1.x, c1.y, p[5].x, p[5].y);
        rawPath.cubicTo(p[6].x, p[6].y, p[7].x, p[7].y, p[8].x, p[8].y);
        if (doClose)
        {
            rawPath.close();
        }

        Factory* factory = fiddleContext->factory();
        auto path = factory->makeRenderPath(rawPath, FillRule::clockwise);

        auto fillPaint = factory->makeRenderPaint();
        float feather = powf(1.5f, (1400 - pts[std::size(pts) - 1].y) / 75);
        if (feather > 1)
        {
            fillPaint->feather(feather);
        }
        fillPaint->color(0xd0ffffff);

        renderer->drawPath(path.get(), fillPaint.get());

        if (!disableStroke)
        {
            auto strokePaint = factory->makeRenderPaint();
            strokePaint->style(RenderPaintStyle::stroke);
            strokePaint->color(0x8000ffff);
            strokePaint->thickness(strokeWidth);
            if (feather > 1)
            {
                strokePaint->feather(feather);
            }

            strokePaint->join(join);
            strokePaint->cap(cap);
            renderer->drawPath(path.get(), strokePaint.get());

            // Draw the interactive points.
            auto pointPaint = factory->makeRenderPaint();
            pointPaint->style(RenderPaintStyle::stroke);
            pointPaint->color(0xff0000ff);
            pointPaint->thickness(14);
            pointPaint->cap(StrokeCap::round);
            pointPaint->join(StrokeJoin::round);

            auto pointPath = factory->makeEmptyRenderPath();
            for (int i : {1, 2, 4, 6, 7})
            {
                float2 pt = pts[i] + translate;
                pointPath->moveTo(pt.x, pt.y);
                pointPath->close();
            }
            renderer->drawPath(pointPath.get(), pointPaint.get());

            // Draw the feather control point.
            pointPaint->color(0xffff0000);
            pointPath = factory->makeEmptyRenderPath();
            float2 pt = pts[std::size(pts) - 1] + translate;
            pointPath->moveTo(pt.x, pt.y);
            renderer->drawPath(pointPath.get(), pointPaint.get());
        }
    }

    fiddleContext->end(window);

    if (rivFile)
    {
        // Count FPS.
        ++fpsFrames;
        double time = glfwGetTime();
        double fpsElapsed = time - fpsLastTime;
        if (fpsElapsed > 2)
        {
            int instances = (1 + horzRepeat * 2) * (1 + upRepeat + downRepeat);
            double fps = fpsLastTime == 0 ? 0 : fpsFrames / fpsElapsed;
            update_window_title(fps, instances, width, height);
            fpsFrames = 0;
            fpsLastTime = time;
        }
    }
}
