// Copyright 2026 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 <sstream>
#include <vector>

#include "src/dawn/tests/DawnTest.h"
#include "src/dawn/utils/ComboRenderPipelineDescriptor.h"
#include "src/dawn/utils/WGPUHelpers.h"

namespace dawn {
namespace {

enum class TextureType {
    Texture2D,
    Texture2DArray,
};

std::ostream& operator<<(std::ostream& o, TextureType type) {
    switch (type) {
        case TextureType::Texture2D:
            o << "2D";
            break;
        case TextureType::Texture2DArray:
            o << "2DArray";
            break;
    }
    return o;
}

DAWN_TEST_PARAM_STRUCT(TextureSampleCompare2DTestParams, TextureType);

class TextureSampleCompare2DTest : public DawnTestWithParams<TextureSampleCompare2DTestParams> {
  protected:
    void SetUp() override { DawnTestWithParams<TextureSampleCompare2DTestParams>::SetUp(); }
};

// Test that textureSampleCompare with 2D polyfill returns the same result as the native
// implementation.
TEST_P(TextureSampleCompare2DTest, Compare2D) {
    // Only run on Vulkan for this specific polyfill test.
    DAWN_TEST_UNSUPPORTED_IF(!IsVulkan());

    const bool isArray = GetParam().mTextureType == TextureType::Texture2DArray;

    static constexpr uint32_t kSize = 4;
    wgpu::Extent3D textureSize = {kSize, kSize, isArray ? 2u : 1u};

    // Create a depth texture.
    wgpu::TextureDescriptor depthDesc;
    depthDesc.format = wgpu::TextureFormat::Depth32Float;
    depthDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding;
    depthDesc.size = textureSize;
    wgpu::Texture depthTexture = device.CreateTexture(&depthDesc);

    // Create a pipeline to initialize the depth texture.
    utils::ComboRenderPipelineDescriptor setupPipelineDesc;
    setupPipelineDesc.vertex.module = utils::CreateShaderModule(device, R"(
        @vertex fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4f {
            var pos = array(
                vec2f(-1.0, -1.0),
                vec2f( 3.0, -1.0),
                vec2f(-1.0,  3.0));
            return vec4f(pos[VertexIndex], 0.0, 1.0);
        }
    )");
    setupPipelineDesc.cFragment.module = utils::CreateShaderModule(device, R"(
        @fragment fn main(@builtin(position) pos : vec4f) -> @builtin(frag_depth) f32 {
            // Set only texel (1,1) to 1.0, others to 0.0.
            if (u32(pos.x) == 1u && u32(pos.y) == 1u) {
                return 1.0;
            }
            return 0.0;
        }
    )");
    setupPipelineDesc.EnableDepthStencil(wgpu::TextureFormat::Depth32Float);
    setupPipelineDesc.cDepthStencil.depthWriteEnabled = wgpu::OptionalBool::True;
    setupPipelineDesc.cFragment.targetCount = 0;
    wgpu::RenderPipeline setupPipeline = device.CreateRenderPipeline(&setupPipelineDesc);

    // Initialize the depth texture.
    {
        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
        for (uint32_t i = 0; i < textureSize.depthOrArrayLayers; ++i) {
            wgpu::TextureViewDescriptor viewDesc;
            viewDesc.dimension = wgpu::TextureViewDimension::e2D;
            viewDesc.baseArrayLayer = i;
            viewDesc.arrayLayerCount = 1;

            utils::ComboRenderPassDescriptor renderPass({}, depthTexture.CreateView(&viewDesc));
            renderPass.UnsetDepthStencilLoadStoreOpsForFormat(wgpu::TextureFormat::Depth32Float);
            renderPass.cDepthStencilAttachmentInfo.depthClearValue = 0.0f;
            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
            pass.SetPipeline(setupPipeline);
            pass.Draw(3);
            pass.End();
        }
        wgpu::CommandBuffer commands = encoder.Finish();
        queue.Submit(1, &commands);
    }

    // Create a comparison sampler.
    wgpu::SamplerDescriptor samplerDesc;
    samplerDesc.minFilter = wgpu::FilterMode::Linear;
    samplerDesc.magFilter = wgpu::FilterMode::Linear;
    samplerDesc.mipmapFilter = wgpu::MipmapFilterMode::Linear;
    samplerDesc.compare = wgpu::CompareFunction::Less;
    wgpu::Sampler sampler = device.CreateSampler(&samplerDesc);

    // Create a render pipeline that outputs the result of textureSampleCompare.
    utils::ComboRenderPipelineDescriptor pipelineDesc;
    pipelineDesc.vertex.module = utils::CreateShaderModule(device, R"(
        @vertex fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4f {
            var pos = array(
                vec2f(-1.0, -1.0),
                vec2f( 3.0, -1.0),
                vec2f(-1.0,  3.0));
            return vec4f(pos[VertexIndex], 0.0, 1.0);
        }
    )");
    const char* textureType = isArray ? "texture_depth_2d_array" : "texture_depth_2d";
    const char* compareArgs = isArray ? "0u, 0.5" : "0.5";

    std::ostringstream shader;
    shader << "@group(0) @binding(0) var t : " << textureType << R"(;
@group(0) @binding(1) var s : sampler_comparison;

@fragment fn main() -> @location(0) f32 {
    // We are choosing a specific sample location to get a unique bilinear filtered result.
    // This should match the original and polyfilled for all devices that have correct implementation.
    return textureSampleCompare(t, s, vec2f(0.28, 0.27), )"
           << compareArgs << R"();
})";
    pipelineDesc.cFragment.module = utils::CreateShaderModule(device, shader.str().c_str());
    pipelineDesc.cTargets[0].format = wgpu::TextureFormat::R32Float;
    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc);

    // Create an output texture to read the result.
    wgpu::TextureDescriptor outputDesc;
    outputDesc.format = wgpu::TextureFormat::R32Float;
    outputDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
    outputDesc.size = {1, 1, 1};
    wgpu::Texture outputTexture = device.CreateTexture(&outputDesc);
    wgpu::TextureView outputView = outputTexture.CreateView();

    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
    utils::ComboRenderPassDescriptor renderPass({outputView});
    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
    pass.SetPipeline(pipeline);
    pass.SetBindGroup(0, utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
                                              {{0, depthTexture.CreateView()}, {1, sampler}}));
    pass.Draw(3);
    pass.End();

    wgpu::CommandBuffer commands = encoder.Finish();
    queue.Submit(1, &commands);
    // This test only checks the result implementation against a single 'interesting' point.
    // A more robust test is of course possible but this confirms the bilinear filtering (in x and
    // y).
    constexpr float expected2D = 0.3596;

    EXPECT_PIXEL_FLOAT_TOLERANCE_EQ(expected2D, outputTexture, 0, 0, 0.01f);
}

DAWN_INSTANTIATE_TEST_P(TextureSampleCompare2DTest,
                        {VulkanBackend(), VulkanBackend({"vulkan_sample_compare_2d_workaround"})},
                        {TextureType::Texture2D, TextureType::Texture2DArray});

}  // anonymous namespace
}  // namespace dawn
