import { range } from '../../../../../common/util/util.js';
import { align } from '../../../../util/math.js';
import { kMaximumLimitBaseParams, makeLimitTestGroup } from '../limits/limit_utils.js';

function getPipelineDescriptorWithClipDistances(
  device: GPUDevice,
  interStageShaderVariables: number,
  pointList: boolean,
  clipDistances: number,
  startLocation: number = 0
): GPURenderPipelineDescriptor {
  const vertexOutputVariables =
    interStageShaderVariables - (pointList ? 1 : 0) - align(clipDistances, 4) / 4;
  const maxVertexOutputVariables =
    device.limits.maxInterStageShaderVariables - (pointList ? 1 : 0) - align(clipDistances, 4) / 4;

  const varyings = `
      ${range(
        vertexOutputVariables,
        i => `@location(${i + startLocation}) v4_${i + startLocation}: vec4f,`
      ).join('\n')}
  `;

  const code = `
    // test value                        : ${interStageShaderVariables}
    // maxInterStageShaderVariables     : ${device.limits.maxInterStageShaderVariables}
    // num variables in vertex shader : ${vertexOutputVariables}${
      pointList ? ' + point-list' : ''
    }${
      clipDistances > 0
        ? ` + ${align(clipDistances, 4) / 4} (clip_distances[${clipDistances}])`
        : ''
    }
    // maxInterStageVariables:           : ${maxVertexOutputVariables}
    // num used inter stage variables    : ${vertexOutputVariables}
    // vertex output start location      : ${startLocation}

    enable clip_distances;

    struct VSOut {
      @builtin(position) p: vec4f,
      ${varyings}
      ${
        clipDistances > 0
          ? `@builtin(clip_distances) clipDistances: array<f32, ${clipDistances}>,`
          : ''
      }
    }
    struct FSIn {
      ${varyings}
    }
    struct FSOut {
      @location(0) color: vec4f,
    }
    @vertex fn vs() -> VSOut {
      var o: VSOut;
      o.p = vec4f(0);
      return o;
    }
    @fragment fn fs(i: FSIn) -> FSOut {
      var o: FSOut;
      o.color = vec4f(0);
      return o;
    }
  `;
  const module = device.createShaderModule({ code });
  const pipelineDescriptor: GPURenderPipelineDescriptor = {
    layout: 'auto',
    primitive: {
      topology: pointList ? 'point-list' : 'triangle-list',
    },
    vertex: {
      module,
    },
    fragment: {
      module,
      targets: [
        {
          format: 'rgba8unorm',
        },
      ],
    },
  };
  return pipelineDescriptor;
}

const limit = 'maxInterStageShaderVariables';
export const { g, description } = makeLimitTestGroup(limit);

g.test('createRenderPipeline,at_over')
  .desc(`Test using at and over ${limit} limit with clip_distances in createRenderPipeline(Async)`)
  .params(
    kMaximumLimitBaseParams
      .combine('async', [false, true])
      .combine('pointList', [false, true])
      .combine('clipDistances', [1, 2, 3, 4, 5, 6, 7, 8])
  )
  .beforeAllSubcases(t => {
    t.selectDeviceOrSkipTestCase('clip-distances');
  })
  .fn(async t => {
    const { limitTest, testValueName, async, pointList, clipDistances } = t.params;
    await t.testDeviceWithRequestedMaximumLimits(
      limitTest,
      testValueName,
      async ({ device, testValue, shouldError }) => {
        const pipelineDescriptor = getPipelineDescriptorWithClipDistances(
          device,
          testValue,
          pointList,
          clipDistances
        );

        await t.testCreateRenderPipeline(pipelineDescriptor, async, shouldError);
      },
      undefined,
      ['clip-distances']
    );
  });

g.test('createRenderPipeline,max_vertex_output_location')
  .desc(`Test using clip_distances will limit the maximum value of vertex output location`)
  .params(u =>
    u
      .combine('pointList', [false, true])
      .combine('clipDistances', [1, 2, 3, 4, 5, 6, 7, 8])
      .combine('startLocation', [0, 1, 2])
  )
  .beforeAllSubcases(t => {
    t.selectDeviceOrSkipTestCase('clip-distances');
  })
  .fn(async t => {
    const { pointList, clipDistances, startLocation } = t.params;

    const maxInterStageShaderVariables = t.adapter.limits.maxInterStageShaderVariables;
    const deviceInTest = await t.requestDeviceTracked(t.adapter, {
      requiredFeatures: ['clip-distances'],
      requiredLimits: {
        maxInterStageShaderVariables: t.adapter.limits.maxInterStageShaderVariables,
      },
    });
    const pipelineDescriptor = getPipelineDescriptorWithClipDistances(
      deviceInTest,
      maxInterStageShaderVariables,
      pointList,
      clipDistances,
      startLocation
    );
    const vertexOutputVariables =
      maxInterStageShaderVariables - (pointList ? 1 : 0) - align(clipDistances, 4) / 4;
    const maxLocationInTest = startLocation + vertexOutputVariables - 1;
    const maxAllowedLocation = maxInterStageShaderVariables - 1 - align(clipDistances, 4) / 4;
    const shouldError = maxLocationInTest > maxAllowedLocation;

    deviceInTest.pushErrorScope('validation');
    deviceInTest.createRenderPipeline(pipelineDescriptor);
    const error = await deviceInTest.popErrorScope();
    t.expect(!!error === shouldError, `${error?.message || 'no error when one was expected'}`);

    deviceInTest.destroy();
  });
