/*
 * Copyright 2021 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkColorType.h"
#include "include/core/SkPaint.h"
#include "include/core/SkRect.h"
#include "include/core/SkSurface.h"
#include "include/core/SkTypes.h"
#include "include/effects/SkGradientShader.h"
#include "include/effects/SkRuntimeEffect.h"
#include "include/gpu/ganesh/GrBackendSurface.h"
#include "include/gpu/ganesh/GrDirectContext.h"
#include "include/gpu/ganesh/SkSurfaceGanesh.h"

#include <emscripten/bind.h>
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
// https://github.com/emscripten-core/emscripten/blob/main/system/include/emscripten/html5_webgpu.h
// The import/export functions defined here should allow us to fetch a handle to a given JS
// Texture/Sampler/Device etc if needed.
#include <emscripten/html5_webgpu.h>
// https://github.com/emscripten-core/emscripten/blob/main/system/include/webgpu/webgpu.h
// This defines WebGPU constants and such. It also includes a lot of typedefs that make something
// like WGPUDevice defined as a pointer to something external. These "pointers" are actually just
// a small integer that refers to an array index of JS objects being held by a "manager"
// https://github.com/emscripten-core/emscripten/blob/f47bef371f3464471c6d30b631cffcdd06ced004/src/library_webgpu.js#L192
#include <webgpu/webgpu.h>
// https://github.com/emscripten-core/emscripten/blob/main/system/include/webgpu/webgpu_cpp.h
// This defines the C++ equivalents to the JS WebGPU API.
#include <webgpu/webgpu_cpp.h>

static wgpu::Surface getSurfaceForCanvas(wgpu::Device device,
                                         std::string canvasSelector,
                                         int width,
                                         int height) {
    wgpu::SurfaceDescriptorFromCanvasHTMLSelector surfaceSelector;
    surfaceSelector.selector = canvasSelector.c_str();

    wgpu::SurfaceDescriptor surface_desc;
    surface_desc.nextInChain = &surfaceSelector;
    wgpu::Instance instance;
    wgpu::Surface surface = instance.CreateSurface(&surface_desc);

    wgpu::SurfaceConfiguration config;
    config.device = device;
    config.format = wgpu::TextureFormat::BGRA8Unorm;
    config.usage = wgpu::TextureUsage::RenderAttachment;
    config.presentMode = wgpu::PresentMode::Fifo;
    config.width = width;
    config.height = height;
    surface.Configure(&config);

    return surface;
}

enum class DemoKind {
    SOLID_COLOR,
    GRADIENT,
    RUNTIME_EFFECT,
};

struct DemoUniforms {
    float width;
    float height;
    float time;
};

class Demo final {
public:
    bool init(std::string canvasSelector, int width, int height) {
        GrContextOptions ctxOpts;

        wgpu::Device device = wgpu::Device::Acquire(emscripten_webgpu_get_device());
        sk_sp<GrDirectContext> context = GrDirectContext::MakeDawn(device, ctxOpts);
        if (!context) {
            SkDebugf("Could not create GrDirectContext\n");
            return false;
        }

        const char* sksl =
                "uniform float2 iResolution;"
                "uniform float iTime;"
                "vec2 d;"
                "float b(float a) {"
                "  return step(max(d.x, d.y), a);"
                "}"
                "half4 main(float2 C) {"
                "  vec4 O = vec4(0);"
                "  C.y = iResolution.y - C.y;"
                "  for (float i = 0; i < 3; ++i) {"
                "    vec2 U = C.yx / iResolution.yx;"
                "    U.y -= .5;"
                "    U.x = U.x * .4 + U.y * U.y;"
                "    U.y += U.x * sin(-iTime * 9. + i * 2. + U.x * 25.) * .2;"
                "    U.x -= asin(sin(U.y * 34.))/20.;"
                "    d = abs(U);"
                "    O += .3 * vec4(.8 * b(.3) + b(.2), b(.2), b(.1), -1.);"
                "  }"
                "  return O.xyz1;"
                "}";

        auto [effect, err] = SkRuntimeEffect::MakeForShader(SkString(sksl));
        if (!effect) {
            SkDebugf("Failed to compile SkSL: %s\n", err.c_str());
            return false;
        }

        fWidth = width;
        fHeight = height;
        fCanvasSurface = getSurfaceForCanvas(device, canvasSelector, width, height);
        fContext = context;
        fEffect = effect;

        return true;
    }

    void setKind(DemoKind kind) { fDemoKind = kind; }

    void draw(int timestamp) {
        wgpu::SurfaceTexture surfaceTexture;
        fCanvasSurface.GetCurrentTexture(&surfaceTexture);

        GrDawnRenderTargetInfo rtInfo;
        rtInfo.fTextureView = surfaceTexture.texture.CreateView();
        rtInfo.fFormat = wgpu::TextureFormat::BGRA8Unorm;
        rtInfo.fLevelCount = 1;
        GrBackendRenderTarget backendRenderTarget(fWidth, fHeight, 1, 8, rtInfo);
        SkSurfaceProps surfaceProps(0, kRGB_H_SkPixelGeometry);

        sk_sp<SkSurface> surface = SkSurfaces::WrapBackendRenderTarget(fContext.get(),
                                                                       backendRenderTarget,
                                                                       kTopLeft_GrSurfaceOrigin,
                                                                       kN32_SkColorType,
                                                                       nullptr,
                                                                       &surfaceProps);

        SkPaint paint;
        if (fDemoKind == DemoKind::SOLID_COLOR) {
            drawSolidColor(&paint);
        } else if (fDemoKind == DemoKind::GRADIENT) {
            drawGradient(&paint);
        } else if (fDemoKind == DemoKind::RUNTIME_EFFECT) {
            drawRuntimeEffect(&paint, timestamp);
        }

        // Schedule the recorded commands and wait until the GPU has executed them.
        surface->getCanvas()->drawPaint(paint);
        fContext->flushAndSubmit(surface, true);
        fFrameCount++;
    }

    void drawSolidColor(SkPaint* paint) {
        bool flipColor = fFrameCount % 2 == 0;
        paint->setColor(flipColor ? SK_ColorCYAN : SK_ColorMAGENTA);
    }

    void drawGradient(SkPaint* paint) {
        bool flipColor = fFrameCount % 2 == 0;
        SkColor colors1[2] = {SK_ColorMAGENTA, SK_ColorCYAN};
        SkColor colors2[2] = {SK_ColorCYAN, SK_ColorMAGENTA};

        float x = (float)fWidth / 2.f;
        float y = (float)fHeight / 2.f;
        paint->setShader(SkGradientShader::MakeRadial(SkPoint::Make(x, y),
                                                      std::min(x, y),
                                                      flipColor ? colors1 : colors2,
                                                      nullptr,
                                                      2,
                                                      SkTileMode::kClamp));
    }

    void drawRuntimeEffect(SkPaint* paint, int timestamp) {
        DemoUniforms uniforms;
        uniforms.width = fWidth;
        uniforms.height = fHeight;
        uniforms.time = static_cast<float>(timestamp) / 1000.f;

        sk_sp<SkData> uniformData = SkData::MakeWithCopy(&uniforms, sizeof(uniforms));
        sk_sp<SkShader> shader = fEffect->makeShader(std::move(uniformData), /*children=*/{});
        paint->setShader(shader);
    }

private:
    int fFrameCount = 0;
    int fWidth;
    int fHeight;
    wgpu::Surface fCanvasSurface;
    sk_sp<GrDirectContext> fContext;
    sk_sp<SkRuntimeEffect> fEffect;
    DemoKind fDemoKind = DemoKind::SOLID_COLOR;
};

EMSCRIPTEN_BINDINGS(Skia) {
    emscripten::enum_<DemoKind>("DemoKind")
            .value("SOLID_COLOR", DemoKind::SOLID_COLOR)
            .value("GRADIENT", DemoKind::GRADIENT)
            .value("RUNTIME_EFFECT", DemoKind::RUNTIME_EFFECT);
    emscripten::class_<Demo>("Demo")
            .constructor()
            .function("init", &Demo::init)
            .function("setKind", &Demo::setKind)
            .function("draw", &Demo::draw);
}
