export const description = `
Interface matching between vertex and fragment shader validation for createRenderPipeline.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { hasFeature, range, unreachable } from '../../../../common/util/util.js';
import { GPUTestSubcaseBatchState } from '../../../gpu_test.js';
import * as vtu from '../validation_test_utils.js';

import { CreateRenderPipelineValidationTest } from './common.js';

function getVarName(i: number) {
  return `v${i}`;
}

function skipIfDisallowedInterpolationParameter(t: GPUTestSubcaseBatchState, ...wgsl: string[]) {
  if (t.isCompatibility) {
    for (const s of wgsl) {
      if (s.includes('linear') || s.includes('sample')) {
        t.skip(`unsupported interpolation parameter in compat: ${wgsl}`);
      }
    }
  }
}

class InterStageMatchingValidationTest extends CreateRenderPipelineValidationTest {
  getVertexStateWithOutputs(outputs: readonly string[]): GPUVertexState {
    return {
      module: this.device.createShaderModule({
        code: `
        struct A {
            ${outputs.map((v, i) => v.replace('__', getVarName(i))).join(',\n')},
            @builtin(position) pos: vec4<f32>,
        }
        @vertex fn main() -> A {
            var vertexOut: A;
            vertexOut.pos = vec4<f32>(0.0, 0.0, 0.0, 1.0);
            return vertexOut;
        }
        `,
      }),
      entryPoint: 'main',
    };
  }

  getFragmentStateWithInputs(
    inputs: readonly string[],
    hasBuiltinPosition: boolean = false
  ): GPUFragmentState {
    return {
      targets: [{ format: 'rgba8unorm' }],
      module: this.device.createShaderModule({
        code: `
        ${hasFeature(this.device.features, 'primitive-index') ? 'enable primitive_index;' : ''}
        ${hasFeature(this.device.features, 'subgroups') ? 'enable subgroups;' : ''}
        struct B {
            ${inputs.map((v, i) => v.replace('__', getVarName(i))).join(',\n')},
            ${hasBuiltinPosition ? '@builtin(position) pos: vec4<f32>' : ''}
        }
        @fragment fn main(fragmentIn: B) -> @location(0) vec4<f32> {
            return vec4<f32>(1.0, 1.0, 1.0, 1.0);
        }
        `,
      }),
      entryPoint: 'main',
    };
  }

  getDescriptorWithStates(
    vertex: GPUVertexState,
    fragment: GPUFragmentState
  ): GPURenderPipelineDescriptor {
    return {
      layout: 'auto',
      vertex,
      fragment,
    };
  }
}

export const g = makeTestGroup(InterStageMatchingValidationTest);

g.test('location,mismatch')
  .desc(`Tests that missing declaration at the same location should fail validation.`)
  .params(u =>
    u.combine('isAsync', [false, true]).combineWithParams([
      { outputs: ['@location(0) __: f32'], inputs: ['@location(0) __: f32'], _success: true },
      { outputs: ['@location(0) __: f32'], inputs: ['@location(1) __: f32'], _success: false },
      { outputs: ['@location(1) __: f32'], inputs: ['@location(0) __: f32'], _success: false },
      {
        outputs: ['@location(0) __: f32', '@location(1) __: f32'],
        inputs: ['@location(1) __: f32', '@location(0) __: f32'],
        _success: true,
      },
      {
        outputs: ['@location(1) __: f32', '@location(0) __: f32'],
        inputs: ['@location(0) __: f32', '@location(1) __: f32'],
        _success: true,
      },
    ])
  )
  .fn(t => {
    const { isAsync, outputs, inputs, _success } = t.params;

    const descriptor = t.getDescriptorWithStates(
      t.getVertexStateWithOutputs(outputs),
      t.getFragmentStateWithInputs(inputs)
    );

    vtu.doCreateRenderPipelineTest(t, isAsync, _success, descriptor);
  });

g.test('location,superset')
  .desc(`Tests that validation should succeed when vertex output is superset of fragment input`)
  .params(u => u.combine('isAsync', [false, true]))
  .fn(t => {
    const { isAsync } = t.params;

    const descriptor = t.getDescriptorWithStates(
      t.getVertexStateWithOutputs(['@location(0) vout0: f32', '@location(1) vout1: f32']),
      t.getFragmentStateWithInputs(['@location(1) fin1: f32'])
    );

    vtu.doCreateRenderPipelineTest(t, isAsync, true, descriptor);
  });

g.test('location,subset')
  .desc(`Tests that validation should fail when vertex output is a subset of fragment input.`)
  .params(u => u.combine('isAsync', [false, true]))
  .fn(t => {
    const { isAsync } = t.params;

    const descriptor = t.getDescriptorWithStates(
      t.getVertexStateWithOutputs(['@location(0) vout0: f32']),
      t.getFragmentStateWithInputs(['@location(0) fin0: f32', '@location(1) fin1: f32'])
    );

    vtu.doCreateRenderPipelineTest(t, isAsync, false, descriptor);
  });

g.test('type')
  .desc(
    `Tests that validation should fail when type of vertex output and fragment input at the same location doesn't match.`
  )
  .params(u =>
    u.combine('isAsync', [false, true]).combineWithParams([
      { output: 'f32', input: 'f32' },
      { output: 'i32', input: 'f32' },
      { output: 'u32', input: 'f32' },
      { output: 'u32', input: 'i32' },
      { output: 'i32', input: 'u32' },
      { output: 'vec2<f32>', input: 'vec2<f32>' },
      { output: 'vec3<f32>', input: 'vec2<f32>' },
      { output: 'vec2<f32>', input: 'vec3<f32>' },
      { output: 'vec2<f32>', input: 'f32' },
      { output: 'f32', input: 'vec2<f32>' },
    ])
  )
  .fn(t => {
    const { isAsync, output, input } = t.params;

    const descriptor = t.getDescriptorWithStates(
      t.getVertexStateWithOutputs([`@location(0) @interpolate(flat, either) vout0: ${output}`]),
      t.getFragmentStateWithInputs([`@location(0) @interpolate(flat, either) fin0: ${input}`])
    );

    vtu.doCreateRenderPipelineTest(t, isAsync, output === input, descriptor);
  });

g.test('interpolation_type')
  .desc(
    `Tests that validation should fail when interpolation type of vertex output and fragment input at the same location doesn't match.`
  )
  .params(u =>
    u.combine('isAsync', [false, true]).combineWithParams([
      // default is @interpolate(perspective, center)
      { output: '', input: '' },
      { output: '', input: '@interpolate(perspective)', _success: true },
      { output: '', input: '@interpolate(perspective, center)', _success: true },
      { output: '@interpolate(perspective)', input: '', _success: true },
      { output: '', input: '@interpolate(linear)' },
      { output: '@interpolate(perspective)', input: '@interpolate(perspective)' },
      { output: '@interpolate(linear)', input: '@interpolate(perspective)' },
      { output: '@interpolate(flat, either)', input: '@interpolate(perspective)' },
      { output: '@interpolate(linear)', input: '@interpolate(flat, either)' },
      {
        output: '@interpolate(linear, center)',
        input: '@interpolate(linear, center)',
        _compat_success: false,
      },
    ])
  )
  .beforeAllSubcases(t => {
    skipIfDisallowedInterpolationParameter(t, t.params.output, t.params.input);
  })
  .fn(t => {
    const { isAsync, output, input, _success, _compat_success } = t.params;

    const descriptor = t.getDescriptorWithStates(
      t.getVertexStateWithOutputs([`@location(0) ${output} vout0: f32`]),
      t.getFragmentStateWithInputs([`@location(0) ${input} fin0: f32`])
    );

    const shouldSucceed =
      (_success ?? output === input) && (!t.isCompatibility || _compat_success !== false);

    vtu.doCreateRenderPipelineTest(t, isAsync, shouldSucceed, descriptor);
  });
1;
g.test('interpolation_sampling')
  .desc(
    `Tests that validation should fail when interpolation sampling of vertex output and fragment input at the same location doesn't match.`
  )
  .params(u =>
    u.combine('isAsync', [false, true]).combineWithParams([
      // default is @interpolate(perspective, center)
      { output: '@interpolate(perspective)', input: '@interpolate(perspective)' },
      {
        output: '@interpolate(perspective)',
        input: '@interpolate(perspective, center)',
        _success: true,
      },
      {
        output: '@interpolate(linear, center)',
        input: '@interpolate(linear)',
        _success: true,
        _compat_success: false,
      },
      { output: '@interpolate(flat, either)', input: '@interpolate(flat, either)' },
      { output: '@interpolate(perspective)', input: '@interpolate(perspective, sample)' },
      { output: '@interpolate(perspective, center)', input: '@interpolate(perspective, sample)' },
      {
        output: '@interpolate(perspective, center)',
        input: '@interpolate(perspective, centroid)',
      },
      { output: '@interpolate(perspective, centroid)', input: '@interpolate(perspective)' },
    ])
  )
  .beforeAllSubcases(t => {
    skipIfDisallowedInterpolationParameter(t, t.params.output, t.params.input);
  })
  .fn(t => {
    const { isAsync, output, input, _success, _compat_success } = t.params;

    const descriptor = t.getDescriptorWithStates(
      t.getVertexStateWithOutputs([`@location(0) ${output} vout0: f32`]),
      t.getFragmentStateWithInputs([`@location(0) ${input} fin0: f32`])
    );

    const shouldSucceed =
      (_success ?? output === input) && (!t.isCompatibility || _compat_success !== false);

    vtu.doCreateRenderPipelineTest(t, isAsync, shouldSucceed, descriptor);
  });

g.test('max_shader_variable_location')
  .desc(
    `Tests that validation should fail when there is location of user-defined output/input variable >= device.limits.maxInterStageShaderVariables`
  )
  .params(u =>
    u
      .combine('isAsync', [false, true])
      // User defined variable location = maxInterStageShaderVariables + locationDelta
      .combine('locationDelta', [0, -1, -2])
  )
  .fn(t => {
    const { isAsync, locationDelta } = t.params;
    const maxInterStageShaderVariables = t.device.limits.maxInterStageShaderVariables;
    const location = maxInterStageShaderVariables + locationDelta;

    const descriptor = t.getDescriptorWithStates(
      t.getVertexStateWithOutputs([`@location(${location}) vout0: f32`]),
      t.getFragmentStateWithInputs([`@location(${location}) fin0: f32`])
    );

    vtu.doCreateRenderPipelineTest(t, isAsync, location < maxInterStageShaderVariables, descriptor);
  });

g.test('max_variables_count,output')
  .desc(
    `Tests that validation should fail when all user-defined outputs > max vertex shader output
    variables.`
  )
  .params(u =>
    u.combine('isAsync', [false, true]).combineWithParams([
      // Number of user-defined output variables in test shader =
      //     device.limits.maxInterStageShaderVariables + numVariablesDelta
      { numVariablesDelta: 0, topology: 'triangle-list', _success: true },
      { numVariablesDelta: 1, topology: 'triangle-list', _success: false },
      { numVariablesDelta: 0, topology: 'point-list', _success: false },
      { numVariablesDelta: -1, topology: 'point-list', _success: true },
    ] as const)
  )
  .fn(t => {
    const { isAsync, numVariablesDelta, topology, _success } = t.params;

    const numVec4 = t.device.limits.maxInterStageShaderVariables + numVariablesDelta;

    const outputs = range(numVec4, i => `@location(${i}) vout${i}: vec4<f32>`);
    const inputs = range(numVec4, i => `@location(${i}) fin${i}: vec4<f32>`);

    const descriptor = t.getDescriptorWithStates(
      t.getVertexStateWithOutputs(outputs),
      t.getFragmentStateWithInputs(inputs)
    );
    descriptor.primitive = { topology };

    vtu.doCreateRenderPipelineTest(t, isAsync, _success, descriptor);
  });

g.test('max_variables_count,input')
  .desc(
    `Tests that validation should fail when all user-defined inputs > max vertex shader output
    variables (with varying proportions of builtins and user-defined variables).`
  )
  .params(u =>
    u
      .combine('isAsync', [false, true])
      .combine('builtins', ['none', 'compat', 'all'])
      .beginSubcases()
      .combine('overLimit', [false, true])
  )
  .fn(t => {
    const { isAsync, builtins, overLimit } = t.params;

    const inputs: string[] = [];
    switch (builtins) {
      case 'all':
        t.skipIf(t.isCompatibility);
        inputs.push(
          '@builtin(sample_mask) sample_mask_in: u32',
          '@builtin(sample_index) sample_index_in: u32'
        );
        if (hasFeature(t.device.features, 'primitive-index')) {
          inputs.push('@builtin(primitive_index) primitive_index_in: u32');
        }
        if (hasFeature(t.device.features, 'subgroups')) {
          inputs.push(
            '@builtin(subgroup_invocation_id) subgroup_invocation_id_in: u32',
            '@builtin(subgroup_size) subgroup_size_in: u32'
          );
        }
      // fallthrough
      case 'compat':
        inputs.push('@builtin(front_facing) front_facing_in: bool');
      // fallthrough
      case 'none':
        break;
      default:
        unreachable();
    }

    const numVec4 =
      t.device.limits.maxInterStageShaderVariables - inputs.length + (overLimit ? 1 : 0);

    const outputs = range(numVec4, i => `@location(${i}) vout${i}: vec4<f32>`);
    inputs.push(...range(numVec4, i => `@location(${i}) fin${i}: vec4<f32>`));

    const descriptor = t.getDescriptorWithStates(
      t.getVertexStateWithOutputs(outputs),
      t.getFragmentStateWithInputs(inputs, true)
    );

    const success = !overLimit;
    vtu.doCreateRenderPipelineTest(t, isAsync, success, descriptor);
  });
