// 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 <cmath>
#include <memory>
#include <string>
#include <vector>

#include "GLFW/glfw3.h"
#include "src/dawn/common/Constants.h"
#include "src/dawn/tests/DawnTest.h"
#include "src/dawn/utils/ComboRenderBundleEncoderDescriptor.h"
#include "src/dawn/utils/ComboRenderPipelineDescriptor.h"
#include "src/dawn/utils/WGPUHelpers.h"
#include "src/utils/compiler.h"
#include "webgpu/webgpu_glfw.h"

namespace dawn::utils {
static constexpr std::array<wgpu::CompositeAlphaMode, 5> kAllAlphaModes = {
    wgpu::CompositeAlphaMode::Auto,          wgpu::CompositeAlphaMode::Opaque,
    wgpu::CompositeAlphaMode::Premultiplied, wgpu::CompositeAlphaMode::Unpremultiplied,
    wgpu::CompositeAlphaMode::Inherit,
};
static constexpr std::array<wgpu::PresentMode, 4> kAllPresentModes = {
    wgpu::PresentMode::Fifo,
    wgpu::PresentMode::FifoRelaxed,
    wgpu::PresentMode::Immediate,
    wgpu::PresentMode::Mailbox,
};
}  // namespace dawn::utils

namespace dawn {
namespace {

struct GLFWindowDestroyer {
    void operator()(GLFWwindow* ptr) { glfwDestroyWindow(ptr); }
};

class SurfaceConfigurationValidationTests : public DawnTest {
  public:
    void SetUp() override {
        DawnTest::SetUp();
        DAWN_TEST_UNSUPPORTED_IF(UsesWire());
        DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("skip_validation"));

        glfwSetErrorCallback([](int code, const char* message) {
            ErrorLog() << "GLFW error " << code << " " << message;
        });

        // GLFW can fail to start in headless environments, in which SurfaceTests are
        // inapplicable. Skip this cases without producing a test failure.
        if (glfwInit() == GLFW_FALSE) {
            GTEST_SKIP();
        }

        // Set GLFW_NO_API to avoid GLFW bringing up a GL context that we won't use.
        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
        window.reset(glfwCreateWindow(500, 400, "SurfaceConfigurationValidationTests window",
                                      nullptr, nullptr));

        int width;
        int height;
        glfwGetFramebufferSize(window.get(), &width, &height);

        baseConfig.device = device;
        baseConfig.width = width;
        baseConfig.height = height;
        baseConfig.usage = wgpu::TextureUsage::RenderAttachment;
        baseConfig.viewFormatCount = 0;
        baseConfig.viewFormats = nullptr;
    }

    wgpu::Surface CreateTestSurface() {
        return wgpu::glfw::CreateSurfaceForWindow(GetInstance(), window.get());
    }

    wgpu::SurfaceConfiguration GetPreferredConfiguration(wgpu::Surface surface) {
        wgpu::SurfaceCapabilities capabilities;
        surface.GetCapabilities(adapter, &capabilities);

        wgpu::SurfaceConfiguration config = baseConfig;
        config.format = capabilities.formats[0];
        config.alphaMode = capabilities.alphaModes[0];
        config.presentMode = capabilities.presentModes[0];
        return config;
    }

    bool SupportsFormat(const wgpu::SurfaceCapabilities& capabilities, wgpu::TextureFormat format) {
        for (size_t i = 0; i < capabilities.formatCount; ++i) {
            if (DAWN_UNSAFE_TODO(capabilities.formats[i]) == format) {
                return true;
            }
        }
        return false;
    }

    bool SupportsAlphaMode(const wgpu::SurfaceCapabilities& capabilities,
                           wgpu::CompositeAlphaMode mode) {
        if (mode == wgpu::CompositeAlphaMode::Auto) {
            return true;
        }

        for (size_t i = 0; i < capabilities.alphaModeCount; ++i) {
            if (DAWN_UNSAFE_TODO(capabilities.alphaModes[i]) == mode) {
                return true;
            }
        }
        return false;
    }

    bool SupportsPresentMode(const wgpu::SurfaceCapabilities& capabilities,
                             wgpu::PresentMode mode) {
        for (size_t i = 0; i < capabilities.presentModeCount; ++i) {
            if (DAWN_UNSAFE_TODO(capabilities.presentModes[i]) == mode) {
                return true;
            }
        }
        return false;
    }

  protected:
    std::unique_ptr<GLFWwindow, GLFWindowDestroyer> window = nullptr;
    wgpu::SurfaceConfiguration baseConfig;
};

// Using undefined format is not valid
TEST_P(SurfaceConfigurationValidationTests, UndefinedFormat) {
    wgpu::SurfaceConfiguration config;
    config.device = device;
    config.format = wgpu::TextureFormat::Undefined;
    ASSERT_DEVICE_ERROR(CreateTestSurface().Configure(&config));
}

// Supports at least one configuration
TEST_P(SurfaceConfigurationValidationTests, AtLeastOneSupportedConfiguration) {
    wgpu::Surface surface = CreateTestSurface();

    wgpu::SurfaceCapabilities capabilities;
    surface.GetCapabilities(adapter, &capabilities);

    ASSERT_GT(capabilities.formatCount, 0u);
    ASSERT_GT(capabilities.alphaModeCount, 0u);
    ASSERT_GT(capabilities.presentModeCount, 0u);
}

// AlphaMode::Auto should never be reported but always supported.
TEST_P(SurfaceConfigurationValidationTests, AlphaModeAuto) {
    wgpu::Surface surface = CreateTestSurface();

    wgpu::SurfaceCapabilities capabilities;
    surface.GetCapabilities(adapter, &capabilities);

    // Auto is never reported.
    for (size_t i = 0; i < capabilities.alphaModeCount; ++i) {
        DAWN_UNSAFE_TODO(ASSERT_NE(capabilities.alphaModes[i], wgpu::CompositeAlphaMode::Auto));
    }

    // But always supported.
    wgpu::SurfaceConfiguration config = GetPreferredConfiguration(surface);
    config.alphaMode = wgpu::CompositeAlphaMode::Auto;
    surface.Configure(&config);
}

// Using any combination of the reported capability is ok for configuring the surface.
TEST_P(SurfaceConfigurationValidationTests, AnyCombinationOfCapabilities) {
    // TODO(dawn:2320): Fails with "internal drawable creation failed" on the Windows NVIDIA CQ
    // builders but not locally. This is a similar limitation to SurfaceTests.SwitchPresentMode.
    DAWN_SUPPRESS_TEST_IF(IsWindows() && IsVulkan() && IsNvidia());

    // TODO(crbug.com/463551855): Flaky on Snapdragon X Elite SoCs.
    DAWN_SUPPRESS_TEST_IF(IsWindows() && IsQualcomm());

    wgpu::Surface surface = CreateTestSurface();

    wgpu::SurfaceConfiguration config = baseConfig;

    wgpu::SurfaceCapabilities capabilities;
    surface.GetCapabilities(adapter, &capabilities);

    for (wgpu::TextureFormat format : dawn::utils::kAllTextureFormats) {
        for (wgpu::CompositeAlphaMode alphaMode : dawn::utils::kAllAlphaModes) {
            for (wgpu::PresentMode presentMode : dawn::utils::kAllPresentModes) {
                config.format = format;
                config.alphaMode = alphaMode;
                config.presentMode = presentMode;

                if (!SupportsFormat(capabilities, config.format) ||
                    !SupportsAlphaMode(capabilities, config.alphaMode) ||
                    !SupportsPresentMode(capabilities, config.presentMode)) {
                    ASSERT_DEVICE_ERROR(surface.Configure(&config));
                } else {
                    surface.Configure(&config);

                    // Check that we can present
                    wgpu::SurfaceTexture surfaceTexture;
                    surface.GetCurrentTexture(&surfaceTexture);
                    ASSERT_EQ(wgpu::Status::Success, surface.Present());
                }
                device.Tick();
            }
        }
    }
}

// Invalid view format fails
TEST_P(SurfaceConfigurationValidationTests, InvalidViewFormat) {
    wgpu::Surface surface = CreateTestSurface();
    auto invalid = wgpu::TextureFormat::R32Uint;

    wgpu::SurfaceConfiguration config = GetPreferredConfiguration(surface);
    config.viewFormatCount = 1;
    config.viewFormats = &invalid;
    ASSERT_DEVICE_ERROR(surface.Configure(&config));
}

// View format is valid when it matches the config format
TEST_P(SurfaceConfigurationValidationTests, ValidViewFormat) {
    wgpu::Surface surface = CreateTestSurface();

    wgpu::SurfaceConfiguration config = GetPreferredConfiguration(surface);
    config.viewFormatCount = 1;
    config.viewFormats = &config.format;
    surface.Configure(&config);

    // TODO(dawn:2320): Also test the equivalent (non-)sRGB view format
}

// A width of 0 fails
TEST_P(SurfaceConfigurationValidationTests, ZeroWidth) {
    wgpu::Surface surface = CreateTestSurface();

    wgpu::SurfaceConfiguration config = GetPreferredConfiguration(surface);
    config.width = 0;
    ASSERT_DEVICE_ERROR(surface.Configure(&config));
}

// A height of 0 fails
TEST_P(SurfaceConfigurationValidationTests, ZeroHeight) {
    wgpu::Surface surface = CreateTestSurface();

    wgpu::SurfaceConfiguration config = GetPreferredConfiguration(surface);
    config.height = 0;
    ASSERT_DEVICE_ERROR(surface.Configure(&config));
}

// A width that exceeds the maximum texture size fails
TEST_P(SurfaceConfigurationValidationTests, ExcessiveWidth) {
    wgpu::Surface surface = CreateTestSurface();
    wgpu::Limits supported;
    device.GetLimits(&supported);

    wgpu::SurfaceConfiguration config = GetPreferredConfiguration(surface);
    config.width = supported.maxTextureDimension1D + 1;
    ASSERT_DEVICE_ERROR(surface.Configure(&config));
}

// A height that exceeds the maximum texture size fails
TEST_P(SurfaceConfigurationValidationTests, ExcessiveHeight) {
    wgpu::Surface surface = CreateTestSurface();
    wgpu::Limits supported;
    device.GetLimits(&supported);

    wgpu::SurfaceConfiguration config = GetPreferredConfiguration(surface);
    config.height = supported.maxTextureDimension2D + 1;
    ASSERT_DEVICE_ERROR(surface.Configure(&config));
}

// It's valid to unconfigure an already-unconfigured surface.
TEST_P(SurfaceConfigurationValidationTests, UnconfigureNonConfiguredSurface) {
    CreateTestSurface().Unconfigure();
}

// Test that including unsupported usage flag will result in error.
TEST_P(SurfaceConfigurationValidationTests, ErrorIncludeUnsupportedUsage) {
    DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("skip_validation"));

    wgpu::Surface surface = CreateTestSurface();
    wgpu::SurfaceCapabilities caps;
    surface.GetCapabilities(adapter, &caps);

    // Assuming StorageAttachment is not supported.
    DAWN_TEST_UNSUPPORTED_IF(caps.usages & wgpu::TextureUsage::StorageAttachment);

    wgpu::SurfaceConfiguration config = GetPreferredConfiguration(surface);
    config.usage = wgpu::TextureUsage::StorageAttachment;
    ASSERT_DEVICE_ERROR_MSG(surface.Configure(&config), testing::HasSubstr("Usages requested"));
}

// Test that validation of format capabilities still happens.
TEST_P(SurfaceConfigurationValidationTests, StorageRequiresCapableFormat) {
    DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("skip_validation"));

    wgpu::Surface surface = CreateTestSurface();
    wgpu::SurfaceCapabilities caps;
    surface.GetCapabilities(adapter, &caps);

    // Assuming StorageAttachment is not supported.
    DAWN_TEST_UNSUPPORTED_IF(!(caps.usages & wgpu::TextureUsage::StorageBinding));

    for (uint32_t i = 0; i < caps.formatCount; i++) {
        wgpu::SurfaceConfiguration config = GetPreferredConfiguration(surface);
        config.usage = wgpu::TextureUsage::StorageBinding;
        config.format = DAWN_UNSAFE_TODO(caps.formats[i]);

        if (utils::TextureFormatSupportsStorageTexture(device, config.format)) {
            surface.Configure(&config);
        } else {
            ASSERT_DEVICE_ERROR_MSG(surface.Configure(&config),
                                    testing::HasSubstr("TextureUsage::StorageBinding"));
        }
    }
}

// TODO(crbug.com/465183957): Implement swap chain for WebGPUBackend.
DAWN_INSTANTIATE_TEST(SurfaceConfigurationValidationTests,
                      D3D11Backend(),
                      D3D12Backend(),
                      MetalBackend(),
                      NullBackend(),
                      OpenGLBackend(),
                      OpenGLESBackend(),
                      VulkanBackend());

}  // anonymous namespace
}  // namespace dawn
