export const description = `
Test related to depth buffer, depth op, compare func, etc.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { TypedArrayBufferView } from '../../../../common/util/util.js';
import { isStencilTextureFormat, kDepthTextureFormats } from '../../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import * as ttu from '../../../texture_test_utils.js';
import { TexelView } from '../../../util/texture/texel_view.js';

const backgroundColor = [0x00, 0x00, 0x00, 0xff];
const triangleColor = [0xff, 0xff, 0xff, 0xff];

const kBaseColor = new Float32Array([1.0, 1.0, 1.0, 1.0]);
const kRedStencilColor = new Float32Array([1.0, 0.0, 0.0, 1.0]);
const kGreenStencilColor = new Float32Array([0.0, 1.0, 0.0, 1.0]);

type TestStates = {
  state: GPUDepthStencilState;
  color: Float32Array;
  depth: number;
};

class DepthTest extends AllFeaturesMaxLimitsGPUTest {
  runDepthStateTest(testStates: TestStates[], expectedColor: Float32Array) {
    const renderTargetFormat = 'rgba8unorm';

    const renderTarget = this.createTextureTracked({
      format: renderTargetFormat,
      size: { width: 1, height: 1, depthOrArrayLayers: 1 },
      usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const depthStencilFormat: GPUTextureFormat = 'depth24plus-stencil8';
    const depthTexture = this.createTextureTracked({
      size: { width: 1, height: 1, depthOrArrayLayers: 1 },
      format: depthStencilFormat,
      sampleCount: 1,
      mipLevelCount: 1,
      usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST,
    });

    const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
      view: depthTexture.createView(),
      depthLoadOp: 'load',
      depthStoreOp: 'store',
      stencilLoadOp: 'load',
      stencilStoreOp: 'store',
    };

    const encoder = this.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: renderTarget.createView(),
          loadOp: 'load',
          storeOp: 'store',
        },
      ],
      depthStencilAttachment,
    });

    // Draw a triangle with the given depth state, color, and depth.
    for (const test of testStates) {
      const testPipeline = this.createRenderPipelineForTest(test.state, test.depth);
      pass.setPipeline(testPipeline);
      pass.setBindGroup(
        0,
        this.createBindGroupForTest(testPipeline.getBindGroupLayout(0), test.color)
      );
      pass.draw(1);
    }

    pass.end();
    this.device.queue.submit([encoder.finish()]);

    const expColor = {
      R: expectedColor[0],
      G: expectedColor[1],
      B: expectedColor[2],
      A: expectedColor[3],
    };
    const expTexelView = TexelView.fromTexelsAsColors(renderTargetFormat, _coords => expColor);

    ttu.expectTexelViewComparisonIsOkInTexture(
      this,
      { texture: renderTarget },
      expTexelView,
      [1, 1]
    );
  }

  createRenderPipelineForTest(
    depthStencil: GPUDepthStencilState,
    depth: number
  ): GPURenderPipeline {
    return this.device.createRenderPipeline({
      layout: 'auto',
      vertex: {
        module: this.device.createShaderModule({
          code: `
            @vertex
            fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
                return vec4<f32>(0.0, 0.0, ${depth}, 1.0);
            }
            `,
        }),
        entryPoint: 'main',
      },
      fragment: {
        targets: [{ format: 'rgba8unorm' }],
        module: this.device.createShaderModule({
          code: `
            struct Params {
              color : vec4<f32>
            }
            @group(0) @binding(0) var<uniform> params : Params;

            @fragment fn main() -> @location(0) vec4<f32> {
                return vec4<f32>(params.color);
            }`,
        }),
        entryPoint: 'main',
      },
      primitive: { topology: 'point-list' },
      depthStencil,
    });
  }

  createBindGroupForTest(layout: GPUBindGroupLayout, data: TypedArrayBufferView): GPUBindGroup {
    return this.device.createBindGroup({
      layout,
      entries: [
        {
          binding: 0,
          resource: {
            buffer: this.makeBufferWithContents(data, GPUBufferUsage.UNIFORM),
          },
        },
      ],
    });
  }
}

export const g = makeTestGroup(DepthTest);

g.test('depth_disabled')
  .desc('Tests render results with depth test disabled.')
  .fn(t => {
    const depthSpencilFormat: GPUTextureFormat = 'depth24plus-stencil8';
    const state = {
      format: depthSpencilFormat,
      depthWriteEnabled: false,
      depthCompare: 'always' as GPUCompareFunction,
    };

    const testStates = [
      { state, color: kBaseColor, depth: 0.0 },
      { state, color: kRedStencilColor, depth: 0.5 },
      { state, color: kGreenStencilColor, depth: 1.0 },
    ];

    // Test that for all combinations and ensure the last triangle drawn is the one visible
    // regardless of depth testing.
    for (let last = 0; last < 3; ++last) {
      const i = (last + 1) % 3;
      const j = (last + 2) % 3;

      t.runDepthStateTest([testStates[i], testStates[j], testStates[last]], testStates[last].color);
      t.runDepthStateTest([testStates[j], testStates[i], testStates[last]], testStates[last].color);
    }
  });

g.test('depth_write_disabled')
  .desc(
    `
  Test that depthWriteEnabled behaves as expected.
  If enabled, a depth value of 0.0 is written.
  If disabled, it's not written, so it keeps the previous value of 1.0.
  Use a depthCompare: 'equal' check at the end to check the value.
  `
  )
  .params(u =>
    u //
      .combineWithParams([
        { depthWriteEnabled: false, lastDepth: 0.0, _expectedColor: kRedStencilColor },
        { depthWriteEnabled: true, lastDepth: 0.0, _expectedColor: kGreenStencilColor },
        { depthWriteEnabled: false, lastDepth: 1.0, _expectedColor: kGreenStencilColor },
        { depthWriteEnabled: true, lastDepth: 1.0, _expectedColor: kRedStencilColor },
      ])
  )
  .fn(t => {
    const { depthWriteEnabled, lastDepth, _expectedColor } = t.params;

    const depthStencilFormat: GPUTextureFormat = 'depth24plus-stencil8';

    const stencilState = {
      compare: 'always',
      failOp: 'keep',
      depthFailOp: 'keep',
      passOp: 'keep',
    } as const;

    const baseState = {
      format: depthStencilFormat,
      depthWriteEnabled: true,
      depthCompare: 'always',
      stencilFront: stencilState,
      stencilBack: stencilState,
      stencilReadMask: 0xff,
      stencilWriteMask: 0xff,
    } as const;

    const depthWriteState = {
      format: depthStencilFormat,
      depthWriteEnabled,
      depthCompare: 'always',
      stencilFront: stencilState,
      stencilBack: stencilState,
      stencilReadMask: 0xff,
      stencilWriteMask: 0xff,
    } as const;

    const checkState = {
      format: depthStencilFormat,
      depthWriteEnabled: false,
      depthCompare: 'equal',
      stencilFront: stencilState,
      stencilBack: stencilState,
      stencilReadMask: 0xff,
      stencilWriteMask: 0xff,
    } as const;

    const testStates = [
      // Draw a base point with depth write enabled.
      { state: baseState, color: kBaseColor, depth: 1.0 },
      // Draw a second point without depth write enabled.
      { state: depthWriteState, color: kRedStencilColor, depth: 0.0 },
      // Draw a third point which should occlude the second even though it is behind it.
      { state: checkState, color: kGreenStencilColor, depth: lastDepth },
    ];

    t.runDepthStateTest(testStates, _expectedColor);
  });

g.test('depth_test_fail')
  .desc(
    `
  Test that render results on depth test failure cases with 'less' depthCompare operation and
  depthWriteEnabled is true.
  `
  )
  .params(u =>
    u //
      .combineWithParams([
        { secondDepth: 1.0, lastDepth: 2.0, _expectedColor: kBaseColor }, // fail -> fail.
        { secondDepth: 0.0, lastDepth: 2.0, _expectedColor: kRedStencilColor }, // pass -> fail.
        { secondDepth: 2.0, lastDepth: 0.9, _expectedColor: kGreenStencilColor }, // fail -> pass.
      ] as const)
  )
  .fn(t => {
    const { secondDepth, lastDepth, _expectedColor } = t.params;

    const depthStencilFormat: GPUTextureFormat = 'depth24plus-stencil8';

    const baseState = {
      format: depthStencilFormat,
      depthWriteEnabled: true,
      depthCompare: 'always',
      stencilReadMask: 0xff,
      stencilWriteMask: 0xff,
    } as const;

    const depthTestState = {
      format: depthStencilFormat,
      depthWriteEnabled: true,
      depthCompare: 'less',
      stencilReadMask: 0xff,
      stencilWriteMask: 0xff,
    } as const;

    const testStates = [
      { state: baseState, color: kBaseColor, depth: 1.0 },
      { state: depthTestState, color: kRedStencilColor, depth: secondDepth },
      { state: depthTestState, color: kGreenStencilColor, depth: lastDepth },
    ];

    t.runDepthStateTest(testStates, _expectedColor);
  });

// Use a depth value of 0.4, which is exactly representable in depth16unorm (26214 / (2^16-1))
// and depth24unorm (6710886 / (2^24-1)), and closely approximated in depth32float
// (0.4000000059604644775390625).
// This can help prevent shaders and depthClearValue get rounded in different way making equal
// comparison result unexpected.
const kMiddleDepthValue = 0.4;

g.test('depth_compare_func')
  .desc(
    `Tests each depth compare function works properly. Clears the depth attachment to various values, and renders a point at depth 0.5 with various depthCompare modes.`
  )
  .params(u =>
    u.combine('format', kDepthTextureFormats).combineWithParams([
      { depthCompare: 'never', depthClearValue: 1.0, _expected: backgroundColor },
      { depthCompare: 'never', depthClearValue: kMiddleDepthValue, _expected: backgroundColor },
      { depthCompare: 'never', depthClearValue: 0.0, _expected: backgroundColor },
      { depthCompare: 'less', depthClearValue: 1.0, _expected: triangleColor },
      { depthCompare: 'less', depthClearValue: kMiddleDepthValue, _expected: backgroundColor },
      { depthCompare: 'less', depthClearValue: 0.0, _expected: backgroundColor },
      { depthCompare: 'less-equal', depthClearValue: 1.0, _expected: triangleColor },
      {
        depthCompare: 'less-equal',
        depthClearValue: kMiddleDepthValue,
        _expected: triangleColor,
      },
      { depthCompare: 'less-equal', depthClearValue: 0.0, _expected: backgroundColor },
      { depthCompare: 'equal', depthClearValue: 1.0, _expected: backgroundColor },
      { depthCompare: 'equal', depthClearValue: kMiddleDepthValue, _expected: triangleColor },
      { depthCompare: 'equal', depthClearValue: 0.0, _expected: backgroundColor },
      { depthCompare: 'not-equal', depthClearValue: 1.0, _expected: triangleColor },
      {
        depthCompare: 'not-equal',
        depthClearValue: kMiddleDepthValue,
        _expected: backgroundColor,
      },
      { depthCompare: 'not-equal', depthClearValue: 0.0, _expected: triangleColor },
      { depthCompare: 'greater-equal', depthClearValue: 1.0, _expected: backgroundColor },
      {
        depthCompare: 'greater-equal',
        depthClearValue: kMiddleDepthValue,
        _expected: triangleColor,
      },
      { depthCompare: 'greater-equal', depthClearValue: 0.0, _expected: triangleColor },
      { depthCompare: 'greater', depthClearValue: 1.0, _expected: backgroundColor },
      { depthCompare: 'greater', depthClearValue: kMiddleDepthValue, _expected: backgroundColor },
      { depthCompare: 'greater', depthClearValue: 0.0, _expected: triangleColor },
      { depthCompare: 'always', depthClearValue: 1.0, _expected: triangleColor },
      { depthCompare: 'always', depthClearValue: kMiddleDepthValue, _expected: triangleColor },
      { depthCompare: 'always', depthClearValue: 0.0, _expected: triangleColor },
    ] as const)
  )
  .fn(t => {
    const { depthCompare, depthClearValue, _expected, format } = t.params;
    t.skipIfTextureFormatNotSupported(format);

    const colorAttachmentFormat = 'rgba8unorm';
    const colorAttachment = t.createTextureTracked({
      format: colorAttachmentFormat,
      size: { width: 1, height: 1, depthOrArrayLayers: 1 },
      usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
    });
    const colorAttachmentView = colorAttachment.createView();

    const depthTexture = t.createTextureTracked({
      size: { width: 1, height: 1 },
      format,
      usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
    });
    const depthTextureView = depthTexture.createView();

    const pipelineDescriptor: GPURenderPipelineDescriptor = {
      layout: 'auto',
      vertex: {
        module: t.device.createShaderModule({
          code: `
            @vertex fn main(
              @builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
              return vec4<f32>(0.5, 0.5, ${kMiddleDepthValue}, 1.0);
            }
            `,
        }),
        entryPoint: 'main',
      },
      fragment: {
        module: t.device.createShaderModule({
          code: `
            @fragment fn main() -> @location(0) vec4<f32> {
              return vec4<f32>(1.0, 1.0, 1.0, 1.0);
            }
            `,
        }),
        entryPoint: 'main',
        targets: [{ format: colorAttachmentFormat }],
      },
      primitive: { topology: 'point-list' },
      depthStencil: {
        depthWriteEnabled: true,
        depthCompare,
        format,
      },
    };
    const pipeline = t.device.createRenderPipeline(pipelineDescriptor);

    const encoder = t.device.createCommandEncoder();
    const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
      view: depthTextureView,
      depthClearValue,
      depthLoadOp: 'clear',
      depthStoreOp: 'store',
    };
    if (isStencilTextureFormat(format)) {
      depthStencilAttachment.stencilClearValue = 0;
      depthStencilAttachment.stencilLoadOp = 'clear';
      depthStencilAttachment.stencilStoreOp = 'store';
    }
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: colorAttachmentView,
          clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
      depthStencilAttachment,
    });
    pass.setPipeline(pipeline);
    pass.draw(1);
    pass.end();
    t.device.queue.submit([encoder.finish()]);

    ttu.expectSinglePixelComparisonsAreOkInTexture(t, { texture: colorAttachment }, [
      {
        coord: { x: 0, y: 0 },
        exp: new Uint8Array(_expected),
      },
    ]);
  });

g.test('reverse_depth')
  .desc(
    `Tests simple rendering with reversed depth buffer, ensures depth test works properly: fragments are in correct order and out of range fragments are clipped.
    Note that in real use case the depth range remapping is done by the modified projection matrix.
(see https://developer.nvidia.com/content/depth-precision-visualized).`
  )
  .params(u => u.combine('reversed', [false, true]))
  .fn(t => {
    const colorAttachmentFormat = 'rgba8unorm';
    const colorAttachment = t.createTextureTracked({
      format: colorAttachmentFormat,
      size: { width: 1, height: 1, depthOrArrayLayers: 1 },
      usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
    });
    const colorAttachmentView = colorAttachment.createView();

    const depthBufferFormat = 'depth32float';
    const depthTexture = t.createTextureTracked({
      size: { width: 1, height: 1 },
      format: depthBufferFormat,
      usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
    });
    const depthTextureView = depthTexture.createView();

    const pipelineDescriptor: GPURenderPipelineDescriptor = {
      layout: 'auto',
      vertex: {
        module: t.device.createShaderModule({
          code: `
            struct Output {
              @builtin(position) Position : vec4<f32>,
              @location(0) color : vec4<f32>,
            };

            @vertex fn main(
              @builtin(vertex_index) VertexIndex : u32,
              @builtin(instance_index) InstanceIndex : u32) -> Output {
              let zv = array(0.2, 0.3, -0.1, 1.1);
              let z = zv[InstanceIndex];

              var output : Output;
              output.Position = vec4<f32>(0.5, 0.5, z, 1.0);
              var colors : array<vec4<f32>, 4> = array<vec4<f32>, 4>(
                  vec4<f32>(1.0, 0.0, 0.0, 1.0),
                  vec4<f32>(0.0, 1.0, 0.0, 1.0),
                  vec4<f32>(0.0, 0.0, 1.0, 1.0),
                  vec4<f32>(1.0, 1.0, 1.0, 1.0)
              );
              output.color = colors[InstanceIndex];
              return output;
            }
            `,
        }),
        entryPoint: 'main',
      },
      fragment: {
        module: t.device.createShaderModule({
          code: `
            @fragment fn main(
              @location(0) color : vec4<f32>
              ) -> @location(0) vec4<f32> {
              return color;
            }
            `,
        }),
        entryPoint: 'main',
        targets: [{ format: colorAttachmentFormat }],
      },
      primitive: { topology: 'point-list' },
      depthStencil: {
        depthWriteEnabled: true,
        depthCompare: t.params.reversed ? 'greater' : 'less',
        format: depthBufferFormat,
      },
    };
    const pipeline = t.device.createRenderPipeline(pipelineDescriptor);

    const encoder = t.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: colorAttachmentView,
          clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
      depthStencilAttachment: {
        view: depthTextureView,

        depthClearValue: t.params.reversed ? 0.0 : 1.0,
        depthLoadOp: 'clear',
        depthStoreOp: 'store',
      },
    });
    pass.setPipeline(pipeline);
    pass.draw(1, 4);
    pass.end();
    t.device.queue.submit([encoder.finish()]);

    ttu.expectSinglePixelComparisonsAreOkInTexture(t, { texture: colorAttachment }, [
      {
        coord: { x: 0, y: 0 },
        exp: new Uint8Array(
          t.params.reversed ? [0x00, 0xff, 0x00, 0xff] : [0xff, 0x00, 0x00, 0xff]
        ),
      },
    ]);
  });
