// Copyright 2024 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "src/dawn/native/metal/MultiDrawEncoder.h"

#include "src/dawn/common/Strings.h"
#include "src/dawn/native/RenderPipeline.h"
#include "src/dawn/native/ToBackend.h"
#include "src/dawn/native/metal/BufferMTL.h"

const char* kShaderSource = "#include <metal_stdlib>\n" DAWN_MULTILINE(
    using namespace metal;

    struct DrawCmd {
        uint32_t vertexCount;
        uint32_t instanceCount;
        uint32_t firstVertex;
        uint32_t firstInstance;
    };

    struct DrawIndexedCmd {
        uint32_t indexCount;
        uint32_t instanceCount;
        uint32_t firstIndex;
        uint32_t baseVertex;
        uint32_t firstInstance;
    };

    struct Constants {
        uint32_t maxDrawCount;
        uint32_t numIndexBufferElementsAfterOffsetLow;
        uint32_t numIndexBufferElementsAfterOffsetHigh;
        uint8_t flags;
        uint8_t primitiveType;
    };

    // Function constant
    constant bool kValidationEnabled [[function_constant(0)]];

    // Primitive types
    constant uint8_t kPointList = 0;
    constant uint8_t kLineList = 1;
    constant uint8_t kLineStrip = 2;
    constant uint8_t kTriangleList = 3;
    constant uint8_t kTriangleStrip = 4;

    // Flags
    constant uint8_t kDrawCountBuffer = 4u;
    constant uint8_t kIndexedDraw = 2u;
    constant uint8_t kIndexType16 = 1u;  // bit indicating 16bit index type, else 32bit

    struct MultiDrawData {
        command_buffer commandBuffer [[id(0)]];
        constant uint32_t* drawCmd [[id(1)]];  // Either DrawCmd or DrawIndexedCmd depending on flags
        // This can either be a buffer of uint16_t or uint32_t depending on flags
        constant uint32_t* indexBuffer [[id(2)]];
        constant uint* countBuffer [[id(3)]];
    };

    primitive_type getPrimitiveType(uint8_t type) {
        switch (type) {
            case kPointList:
                return primitive_type::point;
            case kLineList:
                return primitive_type::line;
            case kLineStrip:
                return primitive_type::line_strip;
            case kTriangleList:
                return primitive_type::triangle;
            case kTriangleStrip:
                return primitive_type::triangle_strip;
        }
        return primitive_type::triangle;
    }

    bool validateIndexed(constant Constants* constants, thread DrawIndexedCmd* cmd) {
        // Validation copied over from IndirectDrawValidationEncoder.cpp

        if (constants->numIndexBufferElementsAfterOffsetHigh >= 2) {
            return true;
        }
        if (constants->numIndexBufferElementsAfterOffsetHigh == 0 &&
            constants->numIndexBufferElementsAfterOffsetLow < cmd->firstIndex) {
            return false;
        }
        uint32_t maxIndexCount = constants->numIndexBufferElementsAfterOffsetLow - cmd->firstIndex;
        if (cmd->indexCount > maxIndexCount) {
            return false;
        }
        return true;
    }

    kernel void convertMultiDrawIndirect(uint2 gid [[thread_position_in_grid]],
                                        constant MultiDrawData* drawData [[buffer(0)]],
                                        constant Constants* constants [[buffer(1)]]) {
        uint32_t drawCount = 0;

        if (constants->flags & kDrawCountBuffer) {
            drawCount = min(constants->maxDrawCount, *drawData->countBuffer);
        } else {
            drawCount = constants->maxDrawCount;
        }

        // TODO(356461286): May want to measure the performance of 1 thread per draw vs 64 threads
        // that process multiple draws. 64 threads approach:
        // https://dawn-review.googlesource.com/c/dawn/+/195494/7/src/dawn/native/metal/MultiDrawEncoder.mm#103
        const uint cmdIndex = gid.x;

        if (cmdIndex >= drawCount) {
            return;
        }

        if (!(constants->flags & kIndexedDraw)) {  // Not indexed
            constant DrawCmd* commands = (constant DrawCmd*)(drawData->drawCmd);

            DrawCmd cmd = commands[cmdIndex];
            render_command command(drawData->commandBuffer, cmdIndex);
            primitive_type topology = getPrimitiveType(constants->primitiveType);

            command.draw_primitives(topology, cmd.firstVertex, cmd.vertexCount,
                                    cmd.instanceCount, cmd.firstInstance);
        } else {
            constant DrawIndexedCmd* commands = (constant DrawIndexedCmd*)(drawData->drawCmd);

            DrawIndexedCmd cmd = commands[cmdIndex];

            if (kValidationEnabled) {
                if (!validateIndexed(constants, &cmd)) {
                    return;
                }
            }

            render_command command(drawData->commandBuffer, cmdIndex);
            primitive_type topology = getPrimitiveType(constants->primitiveType);

            if (constants->flags & kIndexType16) {
                constant uint16_t* offsetteduint16 =
                    ((constant uint16_t*)drawData->indexBuffer) + cmd.firstIndex;
                command.draw_indexed_primitives(topology, cmd.indexCount,
                                                offsetteduint16, cmd.instanceCount, cmd.baseVertex,
                                                cmd.firstInstance);
            } else {
                constant uint32_t* offsetteduint32 =
                    ((constant uint32_t*)drawData->indexBuffer) + cmd.firstIndex;
                command.draw_indexed_primitives(topology, cmd.indexCount,
                                                offsetteduint32, cmd.instanceCount, cmd.baseVertex,
                                                cmd.firstInstance);
            }
        }
    }
);

// Direct copy of the Metal shader struct
struct Constants {
    uint32_t maxDrawCount;
    uint32_t numIndexBufferElementsAfterOffsetLow;
    uint32_t numIndexBufferElementsAfterOffsetHigh;
    uint8_t flags;
    uint8_t primitiveType;
};

static constexpr uint8_t kDrawCountBuffer = 4u;
static constexpr uint8_t kIndexedDraw = 2u;
static constexpr uint8_t kIndexType16 = 1u;  // bit indicating 16bit index type, else 32bit

// Primitive types
static constexpr uint8_t kPointList = 0;
static constexpr uint8_t kLineList = 1;
static constexpr uint8_t kLineStrip = 2;
static constexpr uint8_t kTriangleList = 3;
static constexpr uint8_t kTriangleStrip = 4;

namespace dawn::native::metal {

static uint8_t GetPrimitiveType(wgpu::PrimitiveTopology primitiveTopology) {
    switch (primitiveTopology) {
        case wgpu::PrimitiveTopology::PointList:
            return kPointList;
        case wgpu::PrimitiveTopology::LineList:
            return kLineList;
        case wgpu::PrimitiveTopology::LineStrip:
            return kLineStrip;
        case wgpu::PrimitiveTopology::TriangleList:
            return kTriangleList;
        case wgpu::PrimitiveTopology::TriangleStrip:
            return kTriangleStrip;
        case wgpu::PrimitiveTopology::Undefined:
            break;
    }
    DAWN_UNREACHABLE();
}

ResultOrError<Ref<MultiDrawConverterPipeline>> MultiDrawConverterPipeline::Create(
    DeviceBase* device) {
    Ref<MultiDrawConverterPipeline> pipeline = AcquireRef(new MultiDrawConverterPipeline());

    DAWN_TRY(pipeline->Initialize(device));

    return pipeline;
}

MaybeError MultiDrawConverterPipeline::Initialize(DeviceBase* device) {
    id<MTLDevice> mtlDevice = ToBackend(device)->GetMTLDevice();
    NSError* error = nil;
    NSString* source = [NSString stringWithUTF8String:kShaderSource];
    id<MTLLibrary> lib = [mtlDevice newLibraryWithSource:source options:nullptr error:&error];
    if (error != nullptr) {
        return DAWN_INTERNAL_ERROR("Error creating multi draw MTLLibrary:" +
                                   std::string([error.localizedDescription UTF8String]));
    }
    DAWN_ASSERT(lib != nil);

    // Function constant for validation
    MTLFunctionConstantValues* funcConstants = [MTLFunctionConstantValues new];
    bool isValidationEnabled = device->IsValidationEnabled();
    [funcConstants setConstantValue:&isValidationEnabled type:MTLDataTypeBool atIndex:0];

    id<MTLFunction> convertFunc = [lib newFunctionWithName:@"convertMultiDrawIndirect"
                                            constantValues:funcConstants
                                                     error:&error];
    if (error != nullptr) {
        return DAWN_INTERNAL_ERROR("Error creating multi draw converter compute function:" +
                                   std::string([error.localizedDescription UTF8String]));
    }
    DAWN_ASSERT(convertFunc != nil);

    mArgumentEncoder = [convertFunc newArgumentEncoderWithBufferIndex:0];
    mPipeline = [mtlDevice newComputePipelineStateWithFunction:convertFunc error:&error];

    if (error != nullptr) {
        return DAWN_INTERNAL_ERROR("Error creating multi draw converter compute pipeline:" +
                                   std::string([error.localizedDescription UTF8String]));
    }
    DAWN_ASSERT(mPipeline != nil);

    return {};
}

id<MTLArgumentEncoder> MultiDrawConverterPipeline::GetMTLArgumentEncoder() const {
    DAWN_ASSERT(mArgumentEncoder != nullptr);
    return mArgumentEncoder.Get();
}

id<MTLComputePipelineState> MultiDrawConverterPipeline::GetMTLPipeline() const {
    DAWN_ASSERT(mPipeline != nullptr);
    return mPipeline.Get();
}

ResultOrError<std::vector<MultiDrawExecutionData>> PrepareMultiDraws(
    DeviceBase* device,
    id<MTLComputeCommandEncoder> encoder,
    const std::vector<IndirectDrawMetadata::IndirectMultiDraw>& multiDraws) {
    // Create the class that stores the pipeline and argument encoder if it doesn't exist
    InternalPipelineStore* store = device->GetInternalPipelineStore();

    if (store->multidrawICBConverterPipeline == nullptr) {
        DAWN_TRY_ASSIGN(store->multidrawICBConverterPipeline,
                        MultiDrawConverterPipeline::Create(device));
    }

    MultiDrawConverterPipeline* pipelineStore =
        reinterpret_cast<MultiDrawConverterPipeline*>(store->multidrawICBConverterPipeline.Get());

    id<MTLComputePipelineState> pipeline = pipelineStore->GetMTLPipeline();

    std::vector<MultiDrawExecutionData> outExecutionData;
    outExecutionData.reserve(multiDraws.size());

    id<MTLDevice> mtlDevice = ToBackend(device)->GetMTLDevice();
    for (auto& draw : multiDraws) {
        MultiDrawExecutionData& drawData = outExecutionData.emplace_back();
        drawData.mMaxDrawCount = draw.cmd->maxDrawCount;

        id<MTLBuffer> indirectBuffer = ToBackend(draw.cmd->indirectBuffer.Get())->GetMTLBuffer();
        id<MTLBuffer> drawCountBuffer = nullptr;
        id<MTLBuffer> indexBuffer = nullptr;

        if (draw.cmd->drawCountBuffer != nullptr) {
            drawCountBuffer = ToBackend(draw.cmd->drawCountBuffer.Get())->GetMTLBuffer();
        }
        if (draw.indexBuffer != nullptr) {
            indexBuffer = ToBackend(draw.indexBuffer)->GetMTLBuffer();
        }

        // Create indirect command buffer
        MTLIndirectCommandBufferDescriptor* descriptor = [MTLIndirectCommandBufferDescriptor new];
        descriptor.commandTypes = draw.type == IndirectDrawMetadata::DrawType::Indexed
                                      ? MTLIndirectCommandTypeDrawIndexed
                                      : MTLIndirectCommandTypeDraw;
        descriptor.inheritBuffers = true;
        descriptor.inheritPipelineState = true;

        drawData.mIndirectCommandBuffer =
            [mtlDevice newIndirectCommandBufferWithDescriptor:descriptor
                                              maxCommandCount:draw.cmd->maxDrawCount
                                                      options:MTLResourceStorageModePrivate];
        if (drawData.mIndirectCommandBuffer == nil) {
            return DAWN_INTERNAL_ERROR("Error creating an indirect command buffer for multi draw");
        }

        id<MTLArgumentEncoder> argEnc = pipelineStore->GetMTLArgumentEncoder();
        id<MTLBuffer> argBuffer = [mtlDevice newBufferWithLength:argEnc.encodedLength
                                                         options:MTLStorageModeShared];
        DAWN_ASSERT(argBuffer != nil);

        [argEnc setArgumentBuffer:argBuffer offset:0];
        [argEnc setIndirectCommandBuffer:drawData.mIndirectCommandBuffer.Get() atIndex:0];
        [argEnc setBuffer:indirectBuffer offset:draw.cmd->indirectOffset atIndex:1];
        [argEnc setBuffer:indexBuffer offset:draw.indexBufferOffsetInBytes atIndex:2];
        if (draw.cmd->drawCountBuffer != nullptr) {
            [argEnc setBuffer:drawCountBuffer offset:draw.cmd->drawCountOffset atIndex:3];
        }

        [encoder setComputePipelineState:pipeline];
        [encoder setBuffer:argBuffer offset:0 atIndex:0];
        {
            Constants constants;
            constants.maxDrawCount = draw.cmd->maxDrawCount;
            constants.flags = 0;
            constants.primitiveType = GetPrimitiveType(draw.topology);
            constants.numIndexBufferElementsAfterOffsetLow = 0;
            constants.numIndexBufferElementsAfterOffsetHigh = 0;

            if (draw.cmd->drawCountBuffer != nullptr) {
                constants.flags |= kDrawCountBuffer;
            }

            if (draw.type == IndirectDrawMetadata::DrawType::Indexed) {
                constants.flags |= kIndexedDraw;

                const size_t formatSize = IndexFormatSize(draw.indexFormat);
                uint64_t numIndexElements = draw.indexBufferSize / formatSize;

                if (draw.indexFormat == wgpu::IndexFormat::Uint16) {
                    constants.flags |= kIndexType16;
                }

                constants.numIndexBufferElementsAfterOffsetLow =
                    static_cast<uint32_t>(numIndexElements & 0xFFFFFFFF);
                constants.numIndexBufferElementsAfterOffsetHigh =
                    static_cast<uint32_t>((numIndexElements >> 32) & 0xFFFFFFFF);
            }
            [encoder setBytes:&constants length:sizeof(Constants) atIndex:1];
        }

        // Metal requires that we call useResource makes the resource resident for the pass.
        [encoder useResource:drawData.mIndirectCommandBuffer.Get() usage:MTLResourceUsageRead];
        [encoder useResource:indirectBuffer usage:MTLResourceUsageRead];
        if (indexBuffer != nullptr) {
            [encoder useResource:indexBuffer usage:MTLResourceUsageRead];
        }
        if (draw.cmd->drawCountBuffer != nullptr) {
            [encoder useResource:drawCountBuffer usage:MTLResourceUsageRead];
        }

        id<MTLComputePipelineState> pipelineState = pipeline;
        uint32_t threadgroupCount = draw.cmd->maxDrawCount / pipelineState.threadExecutionWidth;
        threadgroupCount +=
            (draw.cmd->maxDrawCount % pipelineState.threadExecutionWidth) == 0 ? 0 : 1;

        [encoder dispatchThreadgroups:MTLSizeMake(threadgroupCount, 1, 1)
                threadsPerThreadgroup:MTLSizeMake(pipelineState.threadExecutionWidth, 1, 1)];
    }

    return std::move(outExecutionData);
}

void ExecuteMultiDraw(const MultiDrawExecutionData& data, id<MTLRenderCommandEncoder> encoder) {
    [encoder executeCommandsInBuffer:data.mIndirectCommandBuffer.Get()
                           withRange:NSMakeRange(0, data.mMaxDrawCount)];
}
}  // namespace dawn::native::metal
