// Copyright 2023 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 <CoreFoundation/CoreFoundation.h>
#include <CoreVideo/CVPixelBuffer.h>
#include <IOSurface/IOSurface.h>

#import <Metal/Metal.h>

#include "dawn/common/CoreFoundationRef.h"
#include "dawn/common/NSRef.h"
#include "dawn/tests/white_box/SharedTextureMemoryTests.h"
#include "dawn/webgpu_cpp.h"

namespace dawn {
namespace {

void AddIntegerValue(CFMutableDictionaryRef dictionary, const CFStringRef key, int32_t value) {
    auto number = AcquireCFRef(CFNumberCreate(nullptr, kCFNumberSInt32Type, &value));
    CFDictionaryAddValue(dictionary, key, number.Get());
}

class Backend : public SharedTextureMemoryTestBackend {
  public:
    static Backend* GetInstance() {
        static Backend b;
        return &b;
    }

    std::string Name() const override { return "IOSurface"; }

    std::vector<wgpu::FeatureName> RequiredFeatures(const wgpu::Adapter& device) const override {
        std::vector<wgpu::FeatureName> features = {wgpu::FeatureName::SharedTextureMemoryIOSurface,
                                                   wgpu::FeatureName::SharedFenceMTLSharedEvent,
                                                   wgpu::FeatureName::DawnMultiPlanarFormats};

        if (device.HasFeature(wgpu::FeatureName::MultiPlanarFormatNv12a)) {
            features.push_back(wgpu::FeatureName::MultiPlanarFormatNv12a);
        }

        return features;
    }

    // Create one basic shared texture memory. It should support most operations.
    wgpu::SharedTextureMemory CreateSharedTextureMemory(const wgpu::Device& device) override {
        auto dict = AcquireCFRef(CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
                                                           &kCFTypeDictionaryKeyCallBacks,
                                                           &kCFTypeDictionaryValueCallBacks));
        AddIntegerValue(dict.Get(), kIOSurfaceWidth, 16);
        AddIntegerValue(dict.Get(), kIOSurfaceHeight, 16);
        AddIntegerValue(dict.Get(), kIOSurfacePixelFormat, kCVPixelFormatType_32RGBA);
        AddIntegerValue(dict.Get(), kIOSurfaceBytesPerElement, 4);

        wgpu::SharedTextureMemoryIOSurfaceDescriptor ioSurfaceDesc;
        ioSurfaceDesc.ioSurface = IOSurfaceCreate(dict.Get());

        wgpu::SharedTextureMemoryDescriptor desc;
        desc.nextInChain = &ioSurfaceDesc;

        return device.ImportSharedTextureMemory(&desc);
    }

    std::vector<std::vector<wgpu::SharedTextureMemory>> CreatePerDeviceSharedTextureMemories(
        const std::vector<wgpu::Device>& devices) override {
        std::vector<std::vector<wgpu::SharedTextureMemory>> memories;

        struct IOSurfaceFormat {
            uint32_t format;
            uint32_t bytesPerElement;
            wgpu::FeatureName requiredFeature = wgpu::FeatureName::Undefined;
        };
        const std::array<IOSurfaceFormat, 12> kFormats{{
            {kCVPixelFormatType_64RGBAHalf, 8},
            {kCVPixelFormatType_TwoComponent16Half, 4},
            {kCVPixelFormatType_OneComponent16Half, 2},
            {kCVPixelFormatType_TwoComponent16, 4, wgpu::FeatureName::Norm16TextureFormats},
            {kCVPixelFormatType_OneComponent16, 2, wgpu::FeatureName::Norm16TextureFormats},
            {kCVPixelFormatType_ARGB2101010LEPacked, 4},
            {kCVPixelFormatType_32RGBA, 4},
            {kCVPixelFormatType_32BGRA, 4},
            {kCVPixelFormatType_TwoComponent8, 2},
            {kCVPixelFormatType_OneComponent8, 1},
            // Below bytes per element isn't correct.
            {kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, 4},
            {kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar, 4,
             wgpu::FeatureName::MultiPlanarFormatNv12a},
            // TODO(dawn:551): Add R10X6BG10X6Biplanar420Unorm support.
        }};

        for (auto f : kFormats) {
            for (uint32_t size : {4, 64}) {
                auto dict = AcquireCFRef(CFDictionaryCreateMutable(
                    kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
                    &kCFTypeDictionaryValueCallBacks));
                AddIntegerValue(dict.Get(), kIOSurfaceWidth, size);
                AddIntegerValue(dict.Get(), kIOSurfaceHeight, size);
                AddIntegerValue(dict.Get(), kIOSurfacePixelFormat, f.format);
                AddIntegerValue(dict.Get(), kIOSurfaceBytesPerElement, f.bytesPerElement);

                wgpu::SharedTextureMemoryIOSurfaceDescriptor ioSurfaceDesc;
                ioSurfaceDesc.ioSurface = IOSurfaceCreate(dict.Get());

                // Internally, the CV enums are defined as their fourcc values. Cast to that and use
                // it as the label. The fourcc value is a four-character name that can be
                // interpreted as a 32-bit integer enum ('ABGR', 'r011', etc.)
                std::string label = std::string(reinterpret_cast<char*>(&f.format), 4) + " " +
                                    std::to_string(size) + "x" + std::to_string(size);
                wgpu::SharedTextureMemoryDescriptor desc;
                desc.label = label.c_str();
                desc.nextInChain = &ioSurfaceDesc;

                std::vector<wgpu::SharedTextureMemory> perDeviceMemories;
                for (auto& device : devices) {
                    if (f.requiredFeature != wgpu::FeatureName::Undefined &&
                        !device.HasFeature(f.requiredFeature)) {
                        continue;
                    }

                    perDeviceMemories.push_back(device.ImportSharedTextureMemory(&desc));
                }

                if (!perDeviceMemories.empty()) {
                    memories.push_back(std::move(perDeviceMemories));
                }
            }
        }

        return memories;
    }
};

// Test that a shared event can be imported, and then exported.
TEST_P(SharedTextureMemoryTests, SharedFenceSuccessfulImportExport) {
    if (@available(macOS 10.14, iOS 12.0, *)) {
        auto mtlDevice = AcquireNSPRef(MTLCreateSystemDefaultDevice());
        auto sharedEvent = AcquireNSPRef([*mtlDevice newSharedEvent]);

        wgpu::SharedFenceMTLSharedEventDescriptor sharedEventDesc;
        sharedEventDesc.sharedEvent = static_cast<void*>(*sharedEvent);

        wgpu::SharedFenceDescriptor fenceDesc;
        fenceDesc.nextInChain = &sharedEventDesc;

        wgpu::SharedFence fence = device.ImportSharedFence(&fenceDesc);

        // Release the Metal objects. They should be retained by the implementation.
        mtlDevice = nil;
        sharedEvent = nil;

        wgpu::SharedFenceMTLSharedEventExportInfo sharedEventInfo;
        wgpu::SharedFenceExportInfo exportInfo;
        exportInfo.nextInChain = &sharedEventInfo;
        fence.ExportInfo(&exportInfo);

        // The exported event should be the same as the imported one.
        EXPECT_EQ(sharedEventInfo.sharedEvent, sharedEventDesc.sharedEvent);
        EXPECT_EQ(exportInfo.type, wgpu::SharedFenceType::MTLSharedEvent);
    }
}

// Test that it is an error to import a shared fence without enabling the feature.
TEST_P(SharedTextureMemoryNoFeatureTests, SharedFenceImportWithoutFeature) {
    if (@available(macOS 10.14, iOS 12.0, *)) {
        auto mtlDevice = AcquireNSPRef(MTLCreateSystemDefaultDevice());
        auto sharedEvent = AcquireNSPRef([*mtlDevice newSharedEvent]);

        wgpu::SharedFenceMTLSharedEventDescriptor sharedEventDesc;
        sharedEventDesc.sharedEvent = static_cast<void*>(*sharedEvent);

        wgpu::SharedFenceDescriptor fenceDesc;
        fenceDesc.nextInChain = &sharedEventDesc;

        ASSERT_DEVICE_ERROR_MSG(wgpu::SharedFence fence = device.ImportSharedFence(&fenceDesc),
                                testing::HasSubstr("MTLSharedEvent is not enabled"));
    }
}

// Test that it is an error to import a shared fence with a null MTLSharedEvent
TEST_P(SharedTextureMemoryTests, SharedFenceImportMTLSharedEventMissing) {
    if (@available(macOS 10.14, iOS 12.0, *)) {
        wgpu::SharedFenceMTLSharedEventDescriptor sharedEventDesc;
        sharedEventDesc.sharedEvent = nullptr;

        wgpu::SharedFenceDescriptor fenceDesc;
        fenceDesc.nextInChain = &sharedEventDesc;

        ASSERT_DEVICE_ERROR_MSG(wgpu::SharedFence fence = device.ImportSharedFence(&fenceDesc),
                                testing::HasSubstr("missing"));
    }
}

// Test exporting info from a shared fence with no chained struct.
// It should be valid and the fence type is exported.
TEST_P(SharedTextureMemoryTests, SharedFenceExportInfoNoChainedStruct) {
    if (@available(macOS 10.14, iOS 12.0, *)) {
        auto mtlDevice = AcquireNSPRef(MTLCreateSystemDefaultDevice());
        auto sharedEvent = AcquireNSPRef([*mtlDevice newSharedEvent]);

        wgpu::SharedFenceMTLSharedEventDescriptor sharedEventDesc;
        sharedEventDesc.sharedEvent = static_cast<void*>(*sharedEvent);

        wgpu::SharedFenceDescriptor fenceDesc;
        fenceDesc.nextInChain = &sharedEventDesc;

        wgpu::SharedFence fence = device.ImportSharedFence(&fenceDesc);

        // Test no chained struct.
        wgpu::SharedFenceExportInfo exportInfo;
        exportInfo.nextInChain = nullptr;

        fence.ExportInfo(&exportInfo);
        EXPECT_EQ(exportInfo.type, wgpu::SharedFenceType::MTLSharedEvent);
    }
}

// Test exporting info from a shared fence with an invalid chained struct.
// It should not be valid, but the fence type should still be exported.
TEST_P(SharedTextureMemoryTests, SharedFenceExportInfoInvalidChainedStruct) {
    if (@available(macOS 10.14, iOS 12.0, *)) {
        auto mtlDevice = AcquireNSPRef(MTLCreateSystemDefaultDevice());
        auto sharedEvent = AcquireNSPRef([*mtlDevice newSharedEvent]);

        wgpu::SharedFenceMTLSharedEventDescriptor sharedEventDesc;
        sharedEventDesc.sharedEvent = static_cast<void*>(*sharedEvent);

        wgpu::SharedFenceDescriptor fenceDesc;
        fenceDesc.nextInChain = &sharedEventDesc;

        wgpu::SharedFence fence = device.ImportSharedFence(&fenceDesc);

        wgpu::ChainedStructOut otherStruct;
        wgpu::SharedFenceExportInfo exportInfo;
        exportInfo.nextInChain = &otherStruct;

        ASSERT_DEVICE_ERROR(fence.ExportInfo(&exportInfo));
    }
}

DAWN_INSTANTIATE_PREFIXED_TEST_P(Metal,
                                 SharedTextureMemoryNoFeatureTests,
                                 {MetalBackend()},
                                 {Backend::GetInstance()});

DAWN_INSTANTIATE_PREFIXED_TEST_P(Metal,
                                 SharedTextureMemoryTests,
                                 {MetalBackend()},
                                 {Backend::GetInstance()});

}  // anonymous namespace
}  // namespace dawn
