/*
 * Copyright 2025 Rive
 */

#include "testing_window.hpp"

#if !defined(RIVE_WEBGPU) || (RIVE_WEBGPU < 2)

TestingWindow* TestingWindow::MakeWGPU(const BackendParams&) { return nullptr; }

#else

#include "common/offscreen_render_target.hpp"
#include "rive/renderer/rive_renderer.hpp"
#include "rive/renderer/rive_render_image.hpp"
#include "rive/renderer/webgpu/render_context_webgpu_impl.hpp"

#ifdef RIVE_WAGYU
#include <webgpu/webgpu_wagyu.h>
#endif

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

namespace rive::gpu
{
static const char* wgpu_backend_name(wgpu::BackendType backendType)
{
    switch (backendType)
    {
        case wgpu::BackendType::Undefined:
            return "<unknown backend>";
        case wgpu::BackendType::Null:
            return "<null backend>";
        case wgpu::BackendType::WebGPU:
            return "WebGPU";
        case wgpu::BackendType::D3D11:
            return "D3D11";
        case wgpu::BackendType::D3D12:
            return "D3D12";
        case wgpu::BackendType::Metal:
            return "Metal";
        case wgpu::BackendType::Vulkan:
            return "Vulkan";
        case wgpu::BackendType::OpenGL:
            return "OpenGL";
        case wgpu::BackendType::OpenGLES:
            return "OpenGLES";
    }
    RIVE_UNREACHABLE();
}

static const char* pls_impl_name(
    const RenderContextWebGPUImpl::Capabilities& capabilities)
{
#ifdef RIVE_WAGYU
    switch (capabilities.plsType)
    {
        case RenderContextWebGPUImpl::PixelLocalStorageType::
            GL_EXT_shader_pixel_local_storage:
            return "GL_EXT_shader_pixel_local_storage";
        case RenderContextWebGPUImpl::PixelLocalStorageType::
            VK_EXT_rasterization_order_attachment_access:
            return "VK_EXT_rasterization_order_attachment_access";
        case RenderContextWebGPUImpl::PixelLocalStorageType::none:
            break;
    }
#endif
    return "<no pixel local storage>";
}

class TestingWindowWGPU : public TestingWindow
{
public:
    TestingWindowWGPU(const BackendParams& backendParams) :
        m_backendParams(backendParams)
    {
        m_instance = wgpu::CreateInstance(nullptr);
        assert(m_instance);

        m_instance.RequestAdapter(
            {},
            wgpu::CallbackMode::AllowSpontaneous,
            [](wgpu::RequestAdapterStatus status,
               wgpu::Adapter adapter,
               wgpu::StringView message,
               TestingWindowWGPU* this_) {
                assert(status == wgpu::RequestAdapterStatus::Success);
                this_->m_adapter = adapter;
            },
            this);
        while (!m_adapter)
        {
            emscripten_sleep(1);
        }

        m_adapter.RequestDevice(
            {},
            wgpu::CallbackMode::AllowSpontaneous,
            [](wgpu::RequestDeviceStatus status,
               wgpu::Device device,
               wgpu::StringView message,
               TestingWindowWGPU* this_) {
                assert(status == wgpu::RequestDeviceStatus::Success);
                this_->m_device = device;
            },
            this);
        while (!m_device)
        {
            emscripten_sleep(1);
        }

        m_queue = m_device.GetQueue();
        assert(m_queue);

        {
            wgpu::EmscriptenSurfaceSourceCanvasHTMLSelector htmlSelector;
            htmlSelector.selector = "#canvas";

            wgpu::SurfaceDescriptor surfaceDesc = {
                .nextInChain = &htmlSelector,
            };

            m_surface = m_instance.CreateSurface(&surfaceDesc);
            assert(m_surface);

            int w, h;
            emscripten_get_canvas_element_size("#canvas", &w, &h);
            m_width = w;
            m_height = h;
        }

        {
            wgpu::SurfaceCapabilities capabilities;
            m_surface.GetCapabilities(m_adapter, &capabilities);
            assert(capabilities.formatCount > 0);
            m_format = capabilities.formats[0];
            assert(m_format != wgpu::TextureFormat::Undefined);
            if (m_format != wgpu::TextureFormat::RGBA8Unorm &&
                m_format != wgpu::TextureFormat::BGRA8Unorm)
            {
                m_format = wgpu::TextureFormat::RGBA8Unorm;
            }
        }

        {
            wgpu::SurfaceConfiguration conf = {
                .device = m_device,
                .format = m_format,
            };
            m_surface.Configure(&conf);
        }

        RenderContextWebGPUImpl::ContextOptions contextOptions;
        m_renderContext = RenderContextWebGPUImpl::MakeContext(m_adapter,
                                                               m_device,
                                                               m_queue,
                                                               contextOptions);

        wgpu::AdapterInfo adapterInfo;
        m_adapter.GetInfo(&adapterInfo);
        printf("==== WGPU device: %s %s %s (%s, %s) ====\n",
               adapterInfo.vendor.data,
               adapterInfo.device.data,
               adapterInfo.description.data,
               wgpu_backend_name(impl()->capabilities().backendType),
               pls_impl_name(impl()->capabilities()));
    }

    rive::Factory* factory() override { return m_renderContext.get(); }

    rive::gpu::RenderContext* renderContext() const override
    {
        return m_renderContext.get();
    }

    rcp<rive_tests::OffscreenRenderTarget> makeOffscreenRenderTarget(
        uint32_t width,
        uint32_t height,
        bool riveRenderable) const override
    {
        return rive_tests::OffscreenRenderTarget::MakeWebGPU(impl(),
                                                             width,
                                                             height);
    }

    void resize(int width, int height) override
    {
        if (m_width != width || m_height != height)
        {
            m_overflowTexture = {};
            m_overflowTextureView = {};
            m_pixelReadBuff = {};
            TestingWindow::resize(width, height);
        }
    }

    std::unique_ptr<rive::Renderer> beginFrame(
        const FrameOptions& options) override
    {
        assert(m_currentCanvasTexture == nullptr);

        wgpu::SurfaceTexture surfaceTexture;
        m_surface.GetCurrentTexture(&surfaceTexture);

        m_currentCanvasTexture = surfaceTexture.texture;
        uint32_t surfaceWidth = m_currentCanvasTexture.GetWidth();
        uint32_t surfaceHeight = m_currentCanvasTexture.GetHeight();

        wgpu::TextureViewDescriptor textureViewDesc = {
            .format = m_format,
            .dimension = wgpu::TextureViewDimension::e2D,
        };

        m_currentCanvasTextureView =
            m_currentCanvasTexture.CreateView(&textureViewDesc);

        if (surfaceWidth < m_width || surfaceHeight < m_height)
        {
            if (!m_overflowTexture)
            {
                wgpu::TextureDescriptor overflowTextureDesc = {
                    .usage = wgpu::TextureUsage::RenderAttachment |
                             wgpu::TextureUsage::CopySrc,
                    .dimension = wgpu::TextureDimension::e2D,
                    .size = {m_width, m_height},
                    .format = m_format,
                };

                m_overflowTexture =
                    m_device.CreateTexture(&overflowTextureDesc);
                m_overflowTextureView = m_overflowTexture.CreateView();
            }
            assert(m_overflowTexture.GetWidth() == m_width);
            assert(m_overflowTexture.GetHeight() == m_height);

            m_renderTarget =
                m_renderContext->static_impl_cast<RenderContextWebGPUImpl>()
                    ->makeRenderTarget(m_format, m_width, m_height);
            m_renderTarget->setTargetTextureView(m_overflowTextureView,
                                                 m_overflowTexture);
        }
        else
        {
            m_renderTarget =
                m_renderContext->static_impl_cast<RenderContextWebGPUImpl>()
                    ->makeRenderTarget(m_format, surfaceWidth, surfaceHeight);
            m_renderTarget->setTargetTextureView(m_currentCanvasTextureView,
                                                 m_currentCanvasTexture);
        }

        rive::gpu::RenderContext::FrameDescriptor frameDescriptor = {
            .renderTargetWidth = m_renderTarget->width(),
            .renderTargetHeight = m_renderTarget->height(),
            .loadAction = options.doClear
                              ? rive::gpu::LoadAction::clear
                              : rive::gpu::LoadAction::preserveRenderTarget,
            .clearColor = options.clearColor,
            .msaaSampleCount = m_backendParams.msaa ? 4u : 0u,
            .disableRasterOrdering = options.disableRasterOrdering,
            .wireframe = options.wireframe,
            .clockwiseFillOverride =
                m_backendParams.clockwise || options.clockwiseFillOverride,
            .synthesizedFailureType = options.synthesizedFailureType,
        };
        m_renderContext->beginFrame(frameDescriptor);
        return std::make_unique<RiveRenderer>(m_renderContext.get());
    }

    void flushPLSContext(RenderTarget* offscreenRenderTarget) final
    {
        wgpu::CommandEncoder encoder = m_device.CreateCommandEncoder();
        m_renderContext->flush({
            .renderTarget = m_renderTarget.get(),
            .externalCommandBuffer = encoder.Get(),
        });
        wgpu::CommandBuffer commands = encoder.Finish();
        m_queue.Submit(1, &commands);
    }

    void endFrame(std::vector<uint8_t>* pixelData) override
    {
        flushPLSContext(nullptr);
        assert(m_currentCanvasTexture != nullptr);

        if (m_overflowTexture)
        {
            // Blit the overflow texture back to the canvas.
            wgpu::CommandEncoder encoder = m_device.CreateCommandEncoder();

            wgpu::TexelCopyTextureInfo src = {
                .texture = m_overflowTexture,
                .origin = {0, 0, 0},
            };

            wgpu::TexelCopyTextureInfo dst = {
                .texture = m_currentCanvasTexture,
                .origin = {0, 0, 0},
            };

            wgpu::Extent3D copySize{
                std::min(m_width, m_currentCanvasTexture.GetWidth()),
                std::min(m_height, m_currentCanvasTexture.GetHeight()),
            };

            encoder.CopyTextureToTexture(&src, &dst, &copySize);

            wgpu::CommandBuffer commands = encoder.Finish();
            m_queue.Submit(1, &commands);
        }

        if (pixelData != nullptr)
        {
            assert(m_format == wgpu::TextureFormat::RGBA8Unorm ||
                   m_format == wgpu::TextureFormat::BGRA8Unorm);
            bool invertY = false;
#ifdef RIVE_WAGYU
            invertY =
                impl()->capabilities().backendType ==
                    wgpu::BackendType::OpenGLES &&
                wgpuWagyuTextureIsSwapchain(m_currentCanvasTexture.Get()) &&
                m_overflowTexture == nullptr;
#endif
            const uint32_t rowBytesInReadBuff =
                math::round_up_to_multiple_of<256>(m_width * 4);

            // Create a buffer to receive the pixels.
            if (!m_pixelReadBuff)
            {
                wgpu::BufferDescriptor buffDesc{
                    .usage =
                        wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst,
                    .size = m_height * rowBytesInReadBuff,
                };
                m_pixelReadBuff = m_device.CreateBuffer(&buffDesc);
            }
            assert(m_pixelReadBuff.GetSize() == m_height * rowBytesInReadBuff);

            // Blit the framebuffer into m_pixelReadBuff.
            wgpu::CommandEncoder readEncoder = m_device.CreateCommandEncoder();
            wgpu::TexelCopyTextureInfo srcTexture = {
                .texture = m_overflowTexture != nullptr
                               ? m_overflowTexture
                               : m_currentCanvasTexture,
                .origin = {0,
                           invertY ? m_renderTarget->height() - m_height : 0,
                           0},
            };
            wgpu::TexelCopyBufferInfo dstBuffer = {
                .layout =
                    {
                        .offset = 0,
                        .bytesPerRow = rowBytesInReadBuff,
                    },
                .buffer = m_pixelReadBuff,
            };
            wgpu::Extent3D copySize = {
                .width = m_width,
                .height = m_height,
            };
            readEncoder.CopyTextureToBuffer(&srcTexture, &dstBuffer, &copySize);

            wgpu::CommandBuffer commands = readEncoder.Finish(NULL);
            m_queue.Submit(1, &commands);

            {
                // Map m_pixelReadBuff.
                bool mappingFinished = false;
                m_pixelReadBuff.MapAsync(
                    wgpu::MapMode::Read,
                    0,
                    m_height * rowBytesInReadBuff,
                    wgpu::CallbackMode::AllowSpontaneous,
                    [](wgpu::MapAsyncStatus status,
                       wgpu::StringView message,
                       bool* mappingFinished) {
                        if (status != wgpu::MapAsyncStatus::Success)
                        {
                            fprintf(stderr,
                                    "failed to map m_pixelReadBuff: %s\n",
                                    message.data);
                            abort();
                        }
                        *mappingFinished = true;
                    },
                    &mappingFinished);
                while (!mappingFinished)
                {
                    emscripten_sleep(1);
                }
            }

            // Copy the image data from m_pixelReadBuff to pixelData.
            const size_t rowBytesInDst = m_width * 4;
            pixelData->resize(m_height * rowBytesInDst);
            const uint8_t* pixelReadBuffData = reinterpret_cast<const uint8_t*>(
                m_pixelReadBuff.GetConstMappedRange());
            for (size_t y = 0; y < m_height; ++y)
            {
                const uint8_t* src;
                if (invertY)
                {
                    src = &pixelReadBuffData[y * rowBytesInReadBuff];
                }
                else
                {
                    src = &pixelReadBuffData[(m_height - y - 1) *
                                             rowBytesInReadBuff];
                }
                uint8_t* dst = &(*pixelData)[y * rowBytesInDst];
                if (m_format == wgpu::TextureFormat::RGBA8Unorm)
                {
                    memcpy(dst, src, rowBytesInDst);
                }
                else
                {
                    assert(m_format == wgpu::TextureFormat::BGRA8Unorm);
                    for (size_t x = 0; x < rowBytesInDst; x += 4)
                    {
                        // BGBRA -> RGBA.
                        dst[x + 0] = src[x + 2];
                        dst[x + 1] = src[x + 1];
                        dst[x + 2] = src[x + 0];
                        dst[x + 3] = src[x + 3];
                    }
                }
            }
            m_pixelReadBuff.Unmap();
        }

        m_currentCanvasTextureView = {};
        m_currentCanvasTexture = {};
    }

private:
    RenderContextWebGPUImpl* impl() const
    {
        return m_renderContext->static_impl_cast<RenderContextWebGPUImpl>();
    }

    const BackendParams m_backendParams;

    wgpu::Instance m_instance = nullptr;
    wgpu::Adapter m_adapter;
    wgpu::Device m_device;
    wgpu::Surface m_surface;
    wgpu::TextureFormat m_format = wgpu::TextureFormat::Undefined;
    wgpu::Queue m_queue;
    wgpu::Texture m_currentCanvasTexture;
    wgpu::TextureView m_currentCanvasTextureView;
    wgpu::Texture m_overflowTexture;
    wgpu::TextureView m_overflowTextureView;
    wgpu::Buffer m_pixelReadBuff;

    std::unique_ptr<RenderContext> m_renderContext;
    rcp<RenderTargetWebGPU> m_renderTarget;
};
}; // namespace rive::gpu

TestingWindow* TestingWindow::MakeWGPU(const BackendParams& backendParams)
{
    return new rive::gpu::TestingWindowWGPU(backendParams);
}

#endif
