/*
 * Copyright (c) 2020 - 2025 the ThorVG project. All rights reserved.

 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <thorvg.h>
#include <emscripten.h>
#include <emscripten/bind.h>
#include "tvgPicture.h"
#include "tvgWasmDefaultFont.h"

using namespace emscripten;
using namespace std;
using namespace tvg;

static const char* NoError = "None";

struct TvgEngineMethod
{
    virtual ~TvgEngineMethod() {}
    virtual Canvas* init(string&) = 0;
    virtual void resize(Canvas* canvas, int w, int h) = 0;
    virtual val output(int w, int h)
    {
        return val(typed_memory_view<uint8_t>(0, nullptr));
    }

    void loadFont() {
        Text::load("default", requestFont(), DEFAULT_FONT_SIZE, "ttf", false);
    }
};

#ifdef THORVG_SW_RASTER_SUPPORT

struct TvgSwEngine : TvgEngineMethod
{
    uint8_t* buffer = nullptr;

    ~TvgSwEngine()
    {
        std::free(buffer);
        Initializer::term();
        retrieveFont();
    }

    Canvas* init(string&) override
    {
        Initializer::init(0);
        loadFont();
        return SwCanvas::gen();
    }

    void resize(Canvas* canvas, int w, int h) override
    {
        std::free(buffer);
        buffer = (uint8_t*)std::malloc(w * h * sizeof(uint32_t));
        static_cast<SwCanvas*>(canvas)->target((uint32_t *)buffer, w, w, h, ColorSpace::ABGR8888S);
    }

    val output(int w, int h) override
    {
        return val(typed_memory_view(w * h * 4, buffer));
    }
};

#endif


#ifdef THORVG_WG_RASTER_SUPPORT

#include <emscripten/html5_webgpu.h>

static WGPUInstance instance{};
static WGPUAdapter adapter{};
static WGPUDevice device{};
static bool adapterRequested = false;
static bool deviceRequested = false;
static bool initializationFailed = false;

struct TvgWgEngine : TvgEngineMethod
{
    WGPUSurface surface{};

    ~TvgWgEngine()
    {
        wgpuSurfaceRelease(surface);
        Initializer::term();
        retrieveFont();
    }

    Canvas* init(string& selector) override
    {
        WGPUSurfaceDescriptorFromCanvasHTMLSelector canvasDesc{};
        canvasDesc.chain.next = nullptr;
        canvasDesc.chain.sType = WGPUSType_SurfaceDescriptorFromCanvasHTMLSelector;
        canvasDesc.selector = selector.c_str();

        WGPUSurfaceDescriptor surfaceDesc{};
        surfaceDesc.nextInChain = &canvasDesc.chain;
        surface = wgpuInstanceCreateSurface(instance, &surfaceDesc);

        Initializer::init(0);
        loadFont();
        return WgCanvas::gen();
    }

    void resize(Canvas* canvas, int w, int h) override
    {
        if (canvas) static_cast<WgCanvas*>(canvas)->target(device, instance, surface, w, h, ColorSpace::ABGR8888S);
    }

    static int init()
    {
        if (initializationFailed) return 1;

        //Init WebGPU
        if (!instance) instance = wgpuCreateInstance(nullptr);

        // request adapter
        if (!adapter) {
            if (adapterRequested) return 2;

            const WGPURequestAdapterOptions requestAdapterOptions { .nextInChain = nullptr, .powerPreference = WGPUPowerPreference_HighPerformance, .forceFallbackAdapter = false };
            auto onAdapterRequestEnded = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, char const* message, void* pUserData) {
                if (status != WGPURequestAdapterStatus_Success) {
                    initializationFailed = true;
                    return;
                }
                *((WGPUAdapter*)pUserData) = adapter;
            };
            wgpuInstanceRequestAdapter(instance, &requestAdapterOptions, onAdapterRequestEnded, &adapter);

            adapterRequested = true;
            return 2;
        }

        // request device
        if (deviceRequested) return device == nullptr ? 2 : 0;

        WGPUFeatureName featureNames[32]{};
        size_t featuresCount = wgpuAdapterEnumerateFeatures(adapter, featureNames);
        if (!device) {
            const WGPUDeviceDescriptor deviceDesc { .nextInChain = nullptr, .label = "The device", .requiredFeatureCount = featuresCount, .requiredFeatures = featureNames };
            auto onDeviceRequestEnded = [](WGPURequestDeviceStatus status, WGPUDevice device, char const* message, void* pUserData) {
                if (status != WGPURequestDeviceStatus_Success) {
                    initializationFailed = true;
                    return;
                }
                *((WGPUDevice*)pUserData) = device;
            };
            wgpuAdapterRequestDevice(adapter, &deviceDesc, onDeviceRequestEnded, &device);

            deviceRequested = true;
            return 2;
        }
        return 0;
    }

    static void term()
    {
        wgpuDeviceRelease(device);
        wgpuAdapterRelease(adapter);
        wgpuInstanceRelease(instance);
    }

};
#endif


#ifdef THORVG_GL_RASTER_SUPPORT

#include <emscripten/html5_webgl.h>

struct TvgGLEngine : TvgEngineMethod
{
    intptr_t context = 0;

    ~TvgGLEngine()
    {
        if (context) {
            Initializer::term();
            emscripten_webgl_destroy_context(context);
            context = 0;
        }
        retrieveFont();
    }

    Canvas* init(string& selector) override
    {
        EmscriptenWebGLContextAttributes attrs{};
        attrs.alpha = true;
        attrs.depth = false;
        attrs.stencil = false;
        attrs.premultipliedAlpha = true;
        attrs.failIfMajorPerformanceCaveat = false;
        attrs.majorVersion = 2;
        attrs.minorVersion = 0;
        attrs.enableExtensionsByDefault = true;

        context = emscripten_webgl_create_context(selector.c_str(), &attrs);
        if (context == 0) return nullptr;

        emscripten_webgl_make_context_current(context);

        if (Initializer::init(0) != Result::Success) return nullptr;
        loadFont();

        return GlCanvas::gen();
    }

    void resize(Canvas* canvas, int w, int h) override
    {
        if (canvas) static_cast<GlCanvas*>(canvas)->target((void*)context, 0, w, h, ColorSpace::ABGR8888S);
    }
};
#endif


class __attribute__((visibility("default"))) TvgLottieAnimation
{
public:
    ~TvgLottieAnimation()
    {
        delete(animation);
        delete(canvas);
        delete(engine);
    }

    explicit TvgLottieAnimation(string engine = "sw", string selector = "")
    {
        errorMsg = NoError;

#ifdef THORVG_SW_RASTER_SUPPORT
        if (engine == "sw") this->engine = new TvgSwEngine;
#endif
#ifdef THORVG_GL_RASTER_SUPPORT
        if (engine == "gl") this->engine = new TvgGLEngine;
#endif
#ifdef THORVG_WG_RASTER_SUPPORT
        if (engine == "wg") this->engine = new TvgWgEngine;
#endif

        if (!this->engine) {
            errorMsg = "Invalid engine";
            return;
        }

        canvas = this->engine->init(selector);

        if (!canvas) {
            errorMsg = "Unsupported!";
            return;
        }

        animation = Animation::gen();
        if (!animation) errorMsg = "Invalid animation";
    }

    string error()
    {
        return errorMsg;
    }

    val size()
    {
        return val(typed_memory_view(2, psize));
    }

    float duration()
    {
        if (!canvas || !animation) return 0;
        return animation->duration();
    }

    float totalFrame()
    {
        if (!canvas || !animation) return 0;
        return animation->totalFrame();
    }

    float curFrame()
    {
        if (!canvas || !animation) return 0;
        return animation->curFrame();
    }

    bool load(string data, string mimetype, int width, int height, string rpath = "")
    {
        errorMsg = NoError;

        if (!canvas) return false;

        if (data.empty()) {
            errorMsg = "Invalid data";
            return false;
        }

        canvas->remove();

        delete(animation);
        animation = Animation::gen();

        string filetype = mimetype;
        if (filetype == "json") {
            filetype = "lottie+json";
        }

        if (animation->picture()->load(data.c_str(), data.size(), filetype.c_str(), rpath.c_str(), false) != Result::Success) {
            errorMsg = "load() fail";
            return false;
        }

        animation->picture()->size(&psize[0], &psize[1]);

        /* need to reset size to calculate scale in Picture.size internally before calling resize() */
        this->width = 0;
        this->height = 0;

        resize(width, height);

        if (canvas->push(animation->picture()) != Result::Success) {
            errorMsg = "push() fail";
            return false;
        }

        updated = true;

        return true;
    }

    val render()
    {
        errorMsg = NoError;

        if (!canvas || !animation) return val(typed_memory_view<uint8_t>(0, nullptr));

        if (!updated) return engine->output(width, height);

        if (canvas->draw(true) != Result::Success) {
            errorMsg = "draw() fail";
            return val(typed_memory_view<uint8_t>(0, nullptr));
        }

        canvas->sync();

        updated = false;

        return engine->output(width, height);
    }

    bool update()
    {
        if (!updated) return true;

        errorMsg = NoError;

        if (canvas->update() != Result::Success) {
            errorMsg = "update() fail";
            return false;
        }

        return true;
    }

    bool frame(float no)
    {
        if (!canvas || !animation) return false;
        if (animation->frame(no) == Result::Success) {
            updated = true;
        }
        return true;
    }

    bool viewport(float x, float y, float width, float height)
    {
        if (!canvas || !animation) return false;
        if (canvas->viewport(x, y, width, height) != Result::Success) {
            errorMsg = "viewport() fail";
            return false;
        }

        return true;
    }

    void resize(int width, int height)
    {
        if (!canvas || !animation) return;
        if (this->width == width && this->height == height) return;

        canvas->sync();

        this->width = width;
        this->height = height;

        engine->resize(canvas, width, height);

        float scale;
        float shiftX = 0.0f, shiftY = 0.0f;
        if (psize[0] > psize[1]) {
            scale = width / psize[0];
            shiftY = (height - psize[1] * scale) * 0.5f;
        } else {
            scale = height / psize[1];
            shiftX = (width - psize[0] * scale) * 0.5f;
        }
        animation->picture()->scale(scale);
        animation->picture()->translate(shiftX, shiftY);

        updated = true;
    }

    bool save(string data, string mimetype)
    {
        if (mimetype == "gif") return save2Gif(data);
        errorMsg = "Invalid mimetype";
        return false;
    }

    bool save2Gif(string data)
    {
        errorMsg = NoError;

        if (data.empty()) {
            errorMsg = "Invalid data";
            return false;
        }

        auto saver = unique_ptr<Saver>(Saver::gen());
        if (!saver) {
            errorMsg = "Invalid saver";
            return false;
        }

        //animation to save
        auto animation = unique_ptr<Animation>(Animation::gen());
        if (!animation) {
            errorMsg = "Invalid animation";
            return false;
        }

        if (animation->picture()->load(data.c_str(), data.size(), "lot", nullptr, false) != Result::Success) {
            errorMsg = "load() fail";
            return false;
        }

        //gif resolution (600x600)
        constexpr float GIF_SIZE = 600;

        //transform
        float width, height;
        animation->picture()->size(&width, &height);
        float scale;
        if (width > height) scale = GIF_SIZE / width;
        else scale = GIF_SIZE / height;
        animation->picture()->size(width * scale, height * scale);

        //set a white opaque background
        auto bg = tvg::Shape::gen();
        if (!bg) {
            errorMsg = "Invalid bg";
            return false;
        }
        bg->fill(255, 255, 255, 255);
        bg->appendRect(0, 0, GIF_SIZE, GIF_SIZE);

        if (saver->background(bg) != Result::Success) {
            errorMsg = "background() fail";
            return false;
        }

        if (saver->save(animation.release(), "output.gif", 100, 30) != tvg::Result::Success) {
            errorMsg = "save(), fail";
            return false;
        }

        saver->sync();

        return true;
    }

    // TODO: Advanced APIs wrt Interactivity & theme methods...

private:
    string                 errorMsg;
    Canvas*                canvas = nullptr;
    Animation*             animation = nullptr;
    TvgEngineMethod*       engine = nullptr;
    uint32_t               width = 0;
    uint32_t               height = 0;
    float                  psize[2];         //picture size
    bool                   updated = false;
};


// 0: success, 1: fail, 2: wait for async request
int init()
{
#ifdef THORVG_WG_RASTER_SUPPORT
    return TvgWgEngine::init();
#else
    return 0;
#endif
}


void term()
{
#ifdef THORVG_WG_RASTER_SUPPORT
    TvgWgEngine::term();
#endif
}


EMSCRIPTEN_BINDINGS(thorvg_bindings)
{
    emscripten::function("init", &init);
    emscripten::function("term", &term);

    class_<TvgLottieAnimation>("TvgLottieAnimation")
        .constructor<string, string>()
        .function("error", &TvgLottieAnimation ::error, allow_raw_pointers())
        .function("size", &TvgLottieAnimation ::size)
        .function("duration", &TvgLottieAnimation ::duration)
        .function("totalFrame", &TvgLottieAnimation ::totalFrame)
        .function("curFrame", &TvgLottieAnimation ::curFrame)
        .function("render", &TvgLottieAnimation::render)
        .function("load", &TvgLottieAnimation ::load)
        .function("update", &TvgLottieAnimation ::update)
        .function("frame", &TvgLottieAnimation ::frame)
        .function("viewport", &TvgLottieAnimation ::viewport)
        .function("resize", &TvgLottieAnimation ::resize)
        .function("save", &TvgLottieAnimation ::save);
}
