/*
 * Copyright 2024 Rive
 */

// Don't compile this file as part of the "tests" project.
#ifndef TESTING

#include <sstream>
#include "common/test_harness.hpp"
#include "common/testing_window.hpp"
#include "rive/artboard.hpp"
#include "rive/refcnt.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/file.hpp"
#include "rive/text/font_hb.hpp"
#include "rive/text/raw_text.hpp"
#include "assets/roboto_flex.ttf.hpp"
#include <stdio.h>
#include <fstream>

#ifdef RIVE_ANDROID
#include "common/rive_android_app.hpp"
#endif

#ifdef __EMSCRIPTEN__
#include "common/rive_wasm_app.hpp"
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#endif

static void update_parameter(int& val, int multiplier, char key, bool seenBang)
{
    if (seenBang)
        val = multiplier;
    else if (key >= 'a')
        val += multiplier;
    else
        val -= multiplier;
}

enum class InputMode : uint8_t
{
    chars,
    consolecommands
};

static int copiesLeft = 0;
static int copiesAbove = 0;
static int copiesRight = 0;
static int copiesBelow = 0;
static int rotations90 = 0;
static int zoomLevel = 0;
static int spacing = 0;
static int monitorIdx = 0;
static bool wireframe = false;
static bool paused = false;
static bool forceFixedDeltaTime = false;
static bool quit = false;
static InputMode inputMode = InputMode::chars;
static bool hotloadShaders = false;
static void key_pressed(char key)
{
    static int multiplier = 0;
    static bool seenDigit = false;
    static bool seenBang = false;
    if (key >= '0' && key <= '9')
    {
        multiplier = multiplier * 10 + (key - '0');
        seenDigit = true;
        return;
    }
    if (key == '!')
    {
        seenBang = true;
        return;
    }
    if (!seenDigit)
    {
        multiplier = seenBang ? 0 : 1;
    }
    switch (key)
    {
        case 'h':
        case 'H':
            update_parameter(copiesLeft, multiplier, key, seenBang);
            break;
        case 'k':
        case 'K':
            update_parameter(copiesAbove, multiplier, key, seenBang);
            break;
        case 'l':
        case 'L':
            update_parameter(copiesRight, multiplier, key, seenBang);
            break;
        case 'j':
        case 'J':
            update_parameter(copiesBelow, multiplier, key, seenBang);
            break;
        case 'x':
        case 'X':
            update_parameter(copiesLeft, multiplier, key, seenBang);
            update_parameter(copiesRight, multiplier, key, seenBang);
            break;
        case 'y':
        case 'Y':
            update_parameter(copiesAbove, multiplier, key, seenBang);
            update_parameter(copiesBelow, multiplier, key, seenBang);
            break;
        case 'r':
        case 'R':
            update_parameter(rotations90, multiplier, key, seenBang);
            break;
        case 'z':
        case 'Z':
            update_parameter(zoomLevel, multiplier, key, seenBang);
            break;
        case 's':
        case 'S':
            update_parameter(spacing, multiplier, key, seenBang);
            break;
        case 'm':
            monitorIdx += multiplier;
            break;
        case 'w':
            wireframe = !wireframe;
            break;
        case 'p':
            paused = !paused;
            break;
        case 'f':
            forceFixedDeltaTime = !forceFixedDeltaTime;
            break;
        case 'q':
        case '\x03': // ^C
            quit = true;
            break;
        case '\x1b': // Esc
            break;
        case '`':
            hotloadShaders = true;
            break;
        case '~':
            if (inputMode == InputMode::chars)
            {
                inputMode = InputMode::consolecommands;
            }
            else
            {
                inputMode = InputMode::chars;
            }
            break;
        default:
            // fprintf(stderr, "invalid option: %c\n", key);
            // abort();
            break;
    }
    multiplier = 0;
    seenDigit = false;
    seenBang = false;
}

class Player
{
public:
    void init(std::string rivName, std::vector<uint8_t> rivBytes)
    {
        m_rivName = std::move(rivName);
        m_file = rive::File::import(rivBytes, TestingWindow::Get()->factory());
        assert(m_file);
        m_artboard = m_file->artboardDefault();
        assert(m_artboard);
        m_scene = m_artboard->defaultStateMachine();
        if (!m_scene)
        {
            m_scene = m_artboard->animationAt(0);
        }
        assert(m_scene);

        // Setup FPS.
        m_roboto = HBFont::Decode(assets::roboto_flex_ttf());
        m_blackStroke = TestingWindow::Get()->factory()->makeRenderPaint();
        m_blackStroke->color(0xff000000);
        m_blackStroke->style(rive::RenderPaintStyle::stroke);
        m_blackStroke->thickness(4);
        m_whiteFill = TestingWindow::Get()->factory()->makeRenderPaint();
        m_whiteFill->color(0xffffffff);
        m_timeLastFPSUpdate = std::chrono::high_resolution_clock::now();
        m_timestampPrevFrame = std::chrono::high_resolution_clock::now();
    }

    void doFrame()
    {
        if (quit || TestingWindow::Get()->shouldQuit()
#ifdef RIVE_ANDROID
            || !rive_android_app_poll_once()
#endif
        )
        {
            printf("\nShutting down\n");
            TestingWindow::Destroy(); // Exercise our PLS teardown process now
                                      // that we're done.
            TestHarness::Instance().shutdown();
#ifdef __EMSCRIPTEN__
            emscripten_cancel_main_loop();
            EM_ASM(if (window && window.close) window.close(););
#else
            exit(0);
#endif
            return;
        }

#ifdef __EMSCRIPTEN__
        {
            // Fit the canvas to the browser window size.
            int windowWidth = EM_ASM_INT(return window["innerWidth"]);
            int windowHeight = EM_ASM_INT(return window["innerHeight"]);
            double devicePixelRatio = emscripten_get_device_pixel_ratio();
            int canvasExpectedWidth = windowWidth * devicePixelRatio;
            int canvasExpectedHeight = windowHeight * devicePixelRatio;
            if (TestingWindow::Get()->width() != canvasExpectedWidth ||
                TestingWindow::Get()->height() != canvasExpectedHeight)
            {
                printf("Resizing HTML canvas to %i x %i.\n",
                       canvasExpectedWidth,
                       canvasExpectedHeight);
                TestingWindow::Get()->resize(canvasExpectedWidth,
                                             canvasExpectedHeight);
                emscripten_set_element_css_size("#canvas",
                                                windowWidth,
                                                windowHeight);
            }
        }
#endif

        std::chrono::time_point timeNow =
            std::chrono::high_resolution_clock::now();
        const double elapsedS =
            std::chrono::duration_cast<std::chrono::nanoseconds>(
                timeNow - m_timestampPrevFrame)
                .count() /
            1e9; // convert to s
        m_timestampPrevFrame = timeNow;

        float advanceDeltaTime = static_cast<float>(elapsedS);
        if (forceFixedDeltaTime)
        {
            advanceDeltaTime = 1.0f / 120;
        }

        m_scene->advanceAndApply(paused ? 0 : advanceDeltaTime);

        copiesLeft = std::max(copiesLeft, 0);
        copiesAbove = std::max(copiesAbove, 0);
        copiesRight = std::max(copiesRight, 0);
        copiesBelow = std::max(copiesBelow, 0);
        int copyCount =
            (copiesLeft + 1 + copiesRight) * (copiesAbove + 1 + copiesBelow);
        if (copyCount != lastReportedCopyCount ||
            paused != lastReportedPauseState)
        {
            printf("Drawing %i copies of %s%s at %u x %u\n",
                   copyCount,
                   m_rivName.c_str(),
                   paused ? " (paused)" : "",
                   TestingWindow::Get()->width(),
                   TestingWindow::Get()->height());
            lastReportedCopyCount = copyCount;
            lastReportedPauseState = paused;
        }

        auto renderer = TestingWindow::Get()->beginFrame({
            .clearColor = 0xff303030,
            .doClear = true,
            .wireframe = wireframe,
        });

        if (hotloadShaders)
        {
            hotloadShaders = false;
#ifndef RIVE_NO_STD_SYSTEM
            std::system("sh rebuild_shaders.sh /tmp/rive");
            TestingWindow::Get()->hotloadShaders();
#endif
        }

        renderer->save();

        uint32_t width = TestingWindow::Get()->width();
        uint32_t height = TestingWindow::Get()->height();
        for (int i = rotations90; (i & 3) != 0; --i)
        {
            renderer->transform(rive::Mat2D(0, 1, -1, 0, width, 0));
            std::swap(height, width);
        }
        if (zoomLevel != 0)
        {
            float scale = powf(1.25f, zoomLevel);
            renderer->translate(width / 2.f, height / 2.f);
            renderer->scale(scale, scale);
            renderer->translate(width / -2.f, height / -2.f);
        }

        // Draw the .riv.
        renderer->save();
        renderer->align(rive::Fit::contain,
                        rive::Alignment::center,
                        rive::AABB(0, 0, width, height),
                        m_artboard->bounds());
        float spacingPx = spacing * 5 + 150;
        renderer->translate(-spacingPx * copiesLeft, -spacingPx * copiesAbove);
        for (int y = -copiesAbove; y <= copiesBelow; ++y)
        {
            renderer->save();
            for (int x = -copiesLeft; x <= copiesRight; ++x)
            {
                m_artboard->draw(renderer.get());
                renderer->translate(spacingPx, 0);
            }
            renderer->restore();
            renderer->translate(0, spacingPx);
        }
        renderer->restore();

        if (m_fpsText != nullptr)
        {
            // Draw FPS.
            renderer->save();
            renderer->translate(0, 20);
            m_fpsText->render(renderer.get(), m_blackStroke);
            m_fpsText->render(renderer.get(), m_whiteFill);
            renderer->restore();
        }

        renderer->restore();
        TestingWindow::Get()->endFrame();

        // Count FPS.
        ++m_fpsFrames;
        const double elapsedFPSUpdate =
            std::chrono::duration_cast<std::chrono::nanoseconds>(
                timeNow - m_timeLastFPSUpdate)
                .count() /
            1e9; // convert to s
        if (elapsedFPSUpdate >= 2.0)
        {
            double fps = m_fpsFrames / elapsedFPSUpdate;
            if (inputMode == InputMode::chars)
            {
                printf("[%.3f FPS]\n", fps);
            }

            char fpsRawText[32];
            snprintf(fpsRawText, sizeof(fpsRawText), "   %.1f FPS   ", fps);
            m_fpsText = std::make_unique<rive::RawText>(
                TestingWindow::Get()->factory());
            m_fpsText->maxWidth(width);
#ifdef RIVE_ANDROID
            m_fpsText->align(rive::TextAlign::center);
#else
            m_fpsText->align(rive::TextAlign::right);
#endif
            m_fpsText->sizing(rive::TextSizing::fixed);
            m_fpsText->append(fpsRawText, nullptr, m_roboto, 50.f);

            m_fpsFrames = 0;
            m_timeLastFPSUpdate = timeNow;
        }

        const rive::Mat2D alignmentMat =
            computeAlignment(rive::Fit::contain,
                             rive::Alignment::center,
                             rive::AABB(0, 0, width, height),
                             m_artboard->bounds());

        // Consume all input events until none are left in the queue
        TestingWindow::InputEventData inputEventData;
        while (TestingWindow::Get()->consumeInputEvent(inputEventData))
        {
            const rive::Vec2D mousePosAligned =
                alignmentMat.invertOrIdentity() *
                rive::Vec2D(inputEventData.metadata.posX,
                            inputEventData.metadata.posY);

            switch (inputEventData.eventType)
            {
                case TestingWindow::InputEvent::KeyPress:
                    key_pressed(inputEventData.metadata.key);
                    break;

                case TestingWindow::InputEvent::MouseMove:
                    m_scene->pointerMove(mousePosAligned);
                    break;

                case TestingWindow::InputEvent::MouseDown:
                    m_scene->pointerDown(mousePosAligned);
                    break;

                case TestingWindow::InputEvent::MouseUp:
                    m_scene->pointerUp(mousePosAligned);
                    break;
            }
        }

        std::string m_command;
        m_command.clear();
        char key;

        if (inputMode == InputMode::consolecommands)
        {
            while (TestHarness::Instance().peekChar(key))
            {
                m_command += key;
            }

            std::istringstream iss(m_command);
            std::string first, second;
            iss >> first >> second;
            if (first == "~")
            {
                inputMode = InputMode::chars;
            }
            else if (first == "fire")
            {
                if (!second.empty())
                {
                    if (auto* trigger = m_scene->getTrigger(second.c_str()))
                    {
                        trigger->fire();
                    }
                }
            }
        }
        else
        {
            while (TestHarness::Instance().peekChar(key))
            {
                key_pressed(key);
            }
        }
    }

private:
    std::string m_rivName;
    rive::rcp<rive::File> m_file;
    std::unique_ptr<rive::ArtboardInstance> m_artboard;
    std::unique_ptr<rive::Scene> m_scene;

    int lastReportedCopyCount = 0;
    bool lastReportedPauseState = paused;

    rive::rcp<rive::Font> m_roboto;
    rive::rcp<rive::RenderPaint> m_blackStroke;
    rive::rcp<rive::RenderPaint> m_whiteFill;
    std::unique_ptr<rive::RawText> m_fpsText;
    int m_fpsFrames = 0;
    std::chrono::high_resolution_clock::time_point m_timeLastFPSUpdate;
    std::chrono::high_resolution_clock::time_point m_timestampPrevFrame;
};

static Player player;

#if defined(RIVE_IOS) || defined(RIVE_IOS_SIMULATOR)
int player_ios_main(int argc, const char* argv[])
#elif defined(RIVE_ANDROID)
int rive_android_main(int argc, const char* const* argv)
#elif defined(__EMSCRIPTEN__)
int rive_wasm_main(int argc, const char* const* argv)
#else
int main(int argc, const char* argv[])
#endif
{
#ifdef _WIN32
    // Cause stdout and stderr to print immediately without buffering.
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
#endif

    std::string rivName;
    std::vector<uint8_t> rivBytes;
    auto backend =
#ifdef __APPLE__
        TestingWindow::Backend::metal;
#else
        TestingWindow::Backend::vk;
#endif
    auto visibility = TestingWindow::Visibility::fullscreen;
    TestingWindow::BackendParams backendParams;

    for (int i = 0; i < argc; ++i)
    {
        if (strcmp(argv[i], "--test_harness") == 0)
        {
            TestHarness::Instance().init(TCPClient::Connect(argv[++i]), 0);
            if (!TestHarness::Instance().fetchRivFile(rivName, rivBytes))
            {
                fprintf(stderr, "failed to fetch a riv file.");
                abort();
            }
        }
        else if (strcmp(argv[i], "--backend") == 0 ||
                 strcmp(argv[i], "-b") == 0)
        {
            backend = TestingWindow::ParseBackend(argv[++i], &backendParams);
        }
        else if (argv[i][0] == '-' &&
                 argv[i][1] == 'b') // "-bvk" without a space.
        {
            backend = TestingWindow::ParseBackend(argv[i] + 2, &backendParams);
        }
        else if (strcmp(argv[i], "--options") == 0 ||
                 strcmp(argv[i], "-k") == 0)
        {
            for (const char* k = argv[++i]; *k; ++k)
            {
                key_pressed(*k);
            }
        }
        else if (argv[i][0] == '-' &&
                 argv[i][1] == 'k') // "-k1234asdf" without a space.
        {
            for (const char* k = argv[i] + 2; *k; ++k)
            {
                key_pressed(*k);
            }
        }
        else if (strcmp(argv[i], "--window") == 0 || strcmp(argv[i], "-w") == 0)
        {
            visibility = TestingWindow::Visibility::window;
        }
        else
        {
            // No argument name defaults to the source riv.
            if (strcmp(argv[i], "--src") == 0 || strcmp(argv[i], "-s") == 0)
            {
                ++i;
            }
            rivName = argv[i];
            std::ifstream rivStream(rivName, std::ios::binary);
            rivBytes =
                std::vector<uint8_t>(std::istreambuf_iterator<char>(rivStream),
                                     {});
        }
    }

    TestingWindow::Init(backend,
                        backendParams,
                        visibility,
#ifdef RIVE_ANDROID
                        rive_android_app_wait_for_window()
#else
                        reinterpret_cast<void*>(
                            static_cast<intptr_t>(monitorIdx))
#endif
    );

    if (rivBytes.empty())
    {
        fprintf(stderr, "no .riv file specified");
        abort();
    }

    player.init(std::move(rivName), std::move(rivBytes));

#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop([]() { player.doFrame(); }, 0, true);
#else
    for (;;)
    {
        player.doFrame();
    }
#endif

    return 0;
}

#endif
