//
// Copyright 2024 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// BufferWgpu.cpp:
//    Implements the class methods for BufferWgpu.
//

#include "libANGLE/renderer/wgpu/BufferWgpu.h"

#include "common/debug.h"
#include "common/mathutil.h"
#include "common/utilities.h"
#include "libANGLE/Context.h"
#include "libANGLE/angletypes.h"
#include "libANGLE/renderer/wgpu/ContextWgpu.h"
#include "libANGLE/renderer/wgpu/wgpu_utils.h"

namespace rx
{
namespace
{
// Based on a buffer binding target, compute the default wgpu usage flags. More can be added if the
// buffer is used in new ways.
wgpu::BufferUsage GetDefaultWGPUBufferUsageForBinding(gl::BufferBinding binding)
{
    switch (binding)
    {
        case gl::BufferBinding::Array:
        case gl::BufferBinding::ElementArray:
            return wgpu::BufferUsage::Vertex | wgpu::BufferUsage::Index |
                   wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;

        case gl::BufferBinding::Uniform:
            return wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopySrc |
                   wgpu::BufferUsage::CopyDst;

        case gl::BufferBinding::PixelPack:
            return wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst;

        case gl::BufferBinding::PixelUnpack:
            return wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc;

        case gl::BufferBinding::CopyRead:
        case gl::BufferBinding::CopyWrite:
        case gl::BufferBinding::ShaderStorage:
        case gl::BufferBinding::Texture:
        case gl::BufferBinding::TransformFeedback:
        case gl::BufferBinding::DispatchIndirect:
        case gl::BufferBinding::DrawIndirect:
        case gl::BufferBinding::AtomicCounter:
            UNIMPLEMENTED();
            return wgpu::BufferUsage::None;

        default:
            UNREACHABLE();
            return wgpu::BufferUsage::None;
    }
}

}  // namespace

BufferWgpu::BufferWgpu(const gl::BufferState &state) : BufferImpl(state) {}

BufferWgpu::~BufferWgpu() {}

angle::Result BufferWgpu::setData(const gl::Context *context,
                                  gl::BufferBinding target,
                                  const void *data,
                                  size_t size,
                                  gl::BufferUsage usage)
{
    ContextWgpu *contextWgpu = webgpu::GetImpl(context);
    wgpu::Device device      = webgpu::GetDevice(context);

    bool hasData = data && size > 0;

    // Allocate a new buffer if the current one is invalid, the size is different, or the current
    // buffer cannot be mapped for writing when data needs to be uploaded.
    if (!mBuffer.valid() || mBuffer.requestedSize() != size ||
        (hasData && !mBuffer.canMapForWrite()))
    {
        // Allocate a new buffer
        ANGLE_TRY(mBuffer.initBuffer(device, size, GetDefaultWGPUBufferUsageForBinding(target),
                                     webgpu::MapAtCreation::Yes));
    }

    if (hasData)
    {
        ASSERT(mBuffer.canMapForWrite());

        if (!mBuffer.getMappedState().has_value())
        {
            ANGLE_TRY(mBuffer.mapImmediate(contextWgpu, wgpu::MapMode::Write, 0, size));
        }

        uint8_t *mappedData = mBuffer.getMapWritePointer(0, size);
        memcpy(mappedData, data, size);
    }

    return angle::Result::Continue;
}

angle::Result BufferWgpu::setSubData(const gl::Context *context,
                                     gl::BufferBinding target,
                                     const void *data,
                                     size_t size,
                                     size_t offset)
{
    ContextWgpu *contextWgpu = webgpu::GetImpl(context);
    wgpu::Device device      = webgpu::GetDevice(context);

    ASSERT(mBuffer.valid());
    if (mBuffer.canMapForWrite())
    {
        if (!mBuffer.getMappedState().has_value())
        {
            ANGLE_TRY(mBuffer.mapImmediate(contextWgpu, wgpu::MapMode::Write, offset, size));
        }

        uint8_t *mappedData = mBuffer.getMapWritePointer(offset, size);
        memcpy(mappedData, data, size);
    }
    else
    {
        // TODO: Upload into a staging buffer and copy to the destination buffer so that the copy
        // happens at the right point in time for command buffer recording.
        wgpu::Queue &queue = contextWgpu->getQueue();
        queue.WriteBuffer(mBuffer.getBuffer(), offset, data, size);
    }

    return angle::Result::Continue;
}

angle::Result BufferWgpu::copySubData(const gl::Context *context,
                                      BufferImpl *source,
                                      GLintptr sourceOffset,
                                      GLintptr destOffset,
                                      GLsizeiptr size)
{
    return angle::Result::Continue;
}

angle::Result BufferWgpu::map(const gl::Context *context, GLenum access, void **mapPtr)
{
    return angle::Result::Continue;
}

angle::Result BufferWgpu::mapRange(const gl::Context *context,
                                   size_t offset,
                                   size_t length,
                                   GLbitfield access,
                                   void **mapPtr)
{
    return angle::Result::Continue;
}

angle::Result BufferWgpu::unmap(const gl::Context *context, GLboolean *result)
{
    *result = GL_TRUE;
    return angle::Result::Continue;
}

angle::Result BufferWgpu::getIndexRange(const gl::Context *context,
                                        gl::DrawElementsType type,
                                        size_t offset,
                                        size_t count,
                                        bool primitiveRestartEnabled,
                                        gl::IndexRange *outRange)
{
    ContextWgpu *contextWgpu = webgpu::GetImpl(context);
    wgpu::Device device      = webgpu::GetDevice(context);

    if (mBuffer.getMappedState())
    {
        ANGLE_TRY(mBuffer.unmap());
    }

    // Create a staging buffer just big enough for this index range
    const GLuint typeBytes = gl::GetDrawElementsTypeSize(type);
    const size_t stagingBufferSize =
        roundUpPow2(count * typeBytes, webgpu::kBufferCopyToBufferAlignment);

    webgpu::BufferHelper stagingBuffer;
    ANGLE_TRY(stagingBuffer.initBuffer(device, stagingBufferSize,
                                       wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead,
                                       webgpu::MapAtCreation::No));

    // Copy the source buffer to staging and flush the commands
    contextWgpu->ensureCommandEncoderCreated();
    wgpu::CommandEncoder &commandEncoder = contextWgpu->getCurrentCommandEncoder();
    commandEncoder.CopyBufferToBuffer(mBuffer.getBuffer(), offset, stagingBuffer.getBuffer(), 0,
                                      stagingBufferSize);

    ANGLE_TRY(contextWgpu->flush(webgpu::RenderPassClosureReason::IndexRangeReadback));

    // Read back from the staging buffer and compute the index range
    ANGLE_TRY(stagingBuffer.mapImmediate(contextWgpu, wgpu::MapMode::Read, 0, stagingBufferSize));
    const uint8_t *data = stagingBuffer.getMapReadPointer(0, stagingBufferSize);
    *outRange           = gl::ComputeIndexRange(type, data, count, primitiveRestartEnabled);
    ANGLE_TRY(stagingBuffer.unmap());

    return angle::Result::Continue;
}

}  // namespace rx
