export const description = `
Ensure state is set correctly. Tries to stress state caching (setting different states multiple
times in different orders) for setBindGroup and setPipeline.
`;

import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import { GPUConst } from '../../../../constants.js';
import { kProgrammableEncoderTypes } from '../../../../util/command_buffer_maker.js';

import { ProgrammableStateTest } from './programmable_state_test.js';

export const g = makeTestGroup(ProgrammableStateTest);

const kBufferUsage =
  GPUConst.BufferUsage.COPY_SRC |
  GPUConst.BufferUsage.COPY_DST |
  GPUConst.BufferUsage.STORAGE |
  GPUConst.BufferUsage.UNIFORM;

g.test('bind_group_indices')
  .desc(
    `
    Test that bind group indices can be declared in any order, regardless of their order in the shader.
    - Test places the value of buffer a - buffer b into the out buffer, then reads the result.
  `
  )
  .params(u =>
    u //
      .combine('encoderType', kProgrammableEncoderTypes)
      .beginSubcases()
      .combine('type', ['storage', 'uniform'] as const)
      .combine('groupIndices', [
        { a: 0, b: 1, out: 2 },
        { a: 1, b: 2, out: 0 },
        { a: 2, b: 0, out: 1 },
        { a: 0, b: 2, out: 1 },
        { a: 2, b: 1, out: 0 },
        { a: 1, b: 0, out: 2 },
      ])
  )
  .fn(t => {
    const { encoderType, groupIndices, type } = t.params;
    t.skipIfNeedsStorageBuffersInFragmentStageAndHaveNone(type, encoderType);

    const pipeline = t.createBindingStatePipeline(
      encoderType,
      groupIndices,
      type,
      'a.value - b.value'
    );

    const inputType: GPUBufferBindingType = type === 'storage' ? 'read-only-storage' : 'uniform';
    const out = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
    const bindGroups = {
      a: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
        inputType,
        encoderType
      ),
      b: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
        inputType,
        encoderType
      ),
      out:
        encoderType === 'compute pass' || type === 'storage'
          ? t.createBindGroup(out, 'storage', encoderType)
          : null,
    };

    const { encoder, validateFinishAndSubmit } = t.createEncoderForStateTest(
      type,
      out,
      encoderType
    );

    t.setPipeline(encoder, pipeline);
    encoder.setBindGroup(groupIndices.a, bindGroups.a);
    encoder.setBindGroup(groupIndices.b, bindGroups.b);
    encoder.setBindGroup(groupIndices.out, bindGroups.out);
    t.dispatchOrDraw(encoder);
    validateFinishAndSubmit(true, true);

    t.expectGPUBufferValuesEqual(out, new Int32Array([1]));
  });

g.test('bind_group_order')
  .desc(
    `
    Test that the order in which you set the bind groups doesn't matter.
  `
  )
  .params(u =>
    u //
      .combine('encoderType', kProgrammableEncoderTypes)
      .beginSubcases()
      .combine('type', ['storage', 'uniform'] as const)
      .combine('setOrder', [
        ['a', 'b', 'out'],
        ['b', 'out', 'a'],
        ['out', 'a', 'b'],
        ['b', 'a', 'out'],
        ['a', 'out', 'b'],
        ['out', 'b', 'a'],
      ] as const)
  )
  .fn(t => {
    const { encoderType, setOrder, type } = t.params;
    t.skipIfNeedsStorageBuffersInFragmentStageAndHaveNone(type, encoderType);

    const groupIndices = { a: 0, b: 1, out: 2 };
    const pipeline = t.createBindingStatePipeline(
      encoderType,
      groupIndices,
      type,
      'a.value - b.value'
    );

    const out = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
    const inputType: GPUBufferBindingType = type === 'storage' ? 'read-only-storage' : 'uniform';
    const bindGroups = {
      a: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
        inputType,
        encoderType
      ),
      b: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
        inputType,
        encoderType
      ),
      out:
        encoderType === 'compute pass' || type === 'storage'
          ? t.createBindGroup(out, 'storage', encoderType)
          : null,
    };

    const { encoder, validateFinishAndSubmit } = t.createEncoderForStateTest(
      type,
      out,
      encoderType
    );
    t.setPipeline(encoder, pipeline);

    for (const bindingName of setOrder) {
      encoder.setBindGroup(groupIndices[bindingName], bindGroups[bindingName]);
    }

    t.dispatchOrDraw(encoder);
    validateFinishAndSubmit(true, true);

    t.expectGPUBufferValuesEqual(out, new Int32Array([1]));
  });

g.test('bind_group_before_pipeline')
  .desc(
    `
    Test that setting bind groups prior to setting the pipeline is still valid.
  `
  )
  .params(u =>
    u //
      .combine('encoderType', kProgrammableEncoderTypes)
      .beginSubcases()
      .combine('type', ['storage', 'uniform'] as const)
      .combineWithParams([
        { setBefore: ['a', 'b'], setAfter: ['out'] },
        { setBefore: ['a'], setAfter: ['b', 'out'] },
        { setBefore: ['out', 'b'], setAfter: ['a'] },
        { setBefore: ['a', 'b', 'out'], setAfter: [] },
      ] as const)
  )
  .fn(t => {
    const { encoderType, type, setBefore, setAfter } = t.params;
    t.skipIfNeedsStorageBuffersInFragmentStageAndHaveNone(type, encoderType);

    const groupIndices = { a: 0, b: 1, out: 2 };
    const pipeline = t.createBindingStatePipeline(
      encoderType,
      groupIndices,
      type,
      'a.value - b.value'
    );

    const out = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
    const inputType: GPUBufferBindingType = type === 'storage' ? 'read-only-storage' : 'uniform';
    const bindGroups = {
      a: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
        inputType,
        encoderType
      ),
      b: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
        inputType,
        encoderType
      ),
      out:
        encoderType === 'compute pass' || type === 'storage'
          ? t.createBindGroup(out, 'storage', encoderType)
          : null,
    };

    const { encoder, validateFinishAndSubmit } = t.createEncoderForStateTest(
      type,
      out,
      encoderType
    );

    for (const bindingName of setBefore) {
      encoder.setBindGroup(groupIndices[bindingName], bindGroups[bindingName]);
    }

    t.setPipeline(encoder, pipeline);

    for (const bindingName of setAfter) {
      encoder.setBindGroup(groupIndices[bindingName], bindGroups[bindingName]);
    }

    t.dispatchOrDraw(encoder);
    validateFinishAndSubmit(true, true);

    t.expectGPUBufferValuesEqual(out, new Int32Array([1]));
  });

g.test('one_bind_group_multiple_slots')
  .desc(
    `
    Test that a single bind group may be bound to more than one slot.
  `
  )
  .params(u =>
    u //
      .combine('encoderType', kProgrammableEncoderTypes)
      .beginSubcases()
      .combine('type', ['storage', 'uniform'] as const)
  )
  .fn(t => {
    const { encoderType, type } = t.params;
    t.skipIfNeedsStorageBuffersInFragmentStageAndHaveNone(type, encoderType);

    const pipeline = t.createBindingStatePipeline(
      encoderType,
      { a: 0, b: 1, out: 2 },
      type,
      'a.value - b.value'
    );

    const out = t.makeBufferWithContents(new Int32Array([1]), kBufferUsage);
    const inputType: GPUBufferBindingType = type === 'storage' ? 'read-only-storage' : 'uniform';
    const bindGroups = {
      ab: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
        inputType,
        encoderType
      ),
      out:
        encoderType === 'compute pass' || type === 'storage'
          ? t.createBindGroup(out, 'storage', encoderType)
          : null,
    };

    const { encoder, validateFinishAndSubmit } = t.createEncoderForStateTest(
      type,
      out,
      encoderType
    );
    t.setPipeline(encoder, pipeline);

    encoder.setBindGroup(0, bindGroups.ab);
    encoder.setBindGroup(1, bindGroups.ab);
    encoder.setBindGroup(2, bindGroups.out);

    t.dispatchOrDraw(encoder);
    validateFinishAndSubmit(true, true);

    t.expectGPUBufferValuesEqual(out, new Int32Array([0]));
  });

g.test('bind_group_multiple_sets')
  .desc(
    `
    Test that the last bind group set to a given slot is used when dispatching.
  `
  )
  .params(u =>
    u //
      .combine('encoderType', kProgrammableEncoderTypes)
      .beginSubcases()
      .combine('type', ['storage', 'uniform'] as const)
  )
  .fn(t => {
    const { encoderType, type } = t.params;
    t.skipIfNeedsStorageBuffersInFragmentStageAndHaveNone(type, encoderType);

    const pipeline = t.createBindingStatePipeline(
      encoderType,
      { a: 0, b: 1, out: 2 },
      type,
      'a.value - b.value'
    );

    const badOut = t.makeBufferWithContents(new Int32Array([-1]), kBufferUsage);
    const out = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
    const inputType: GPUBufferBindingType = type === 'storage' ? 'read-only-storage' : 'uniform';
    const bindGroups = {
      a: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
        inputType,
        encoderType
      ),
      b: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
        inputType,
        encoderType
      ),
      c: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([5]), kBufferUsage),
        inputType,
        encoderType
      ),
      badOut:
        encoderType === 'compute pass' || type === 'storage'
          ? t.createBindGroup(badOut, 'storage', encoderType)
          : null,
      out:
        encoderType === 'compute pass' || type === 'storage'
          ? t.createBindGroup(out, 'storage', encoderType)
          : null,
    };

    const { encoder, validateFinishAndSubmit } = t.createEncoderForStateTest(
      type,
      out,
      encoderType
    );

    encoder.setBindGroup(1, bindGroups.c);

    t.setPipeline(encoder, pipeline);

    encoder.setBindGroup(0, bindGroups.c);
    encoder.setBindGroup(0, bindGroups.a);

    encoder.setBindGroup(2, bindGroups.badOut);

    encoder.setBindGroup(1, bindGroups.b);
    encoder.setBindGroup(2, bindGroups.out);

    t.dispatchOrDraw(encoder);
    validateFinishAndSubmit(true, true);

    t.expectGPUBufferValuesEqual(out, new Int32Array([1]));
    t.expectGPUBufferValuesEqual(badOut, new Int32Array([-1]));
  });

g.test('compatible_pipelines')
  .desc('Test that bind groups can be shared between compatible pipelines.')
  .params(u =>
    u //
      .combine('encoderType', kProgrammableEncoderTypes)
  )
  .fn(t => {
    const { encoderType } = t.params;
    t.skipIfNeedsStorageBuffersInFragmentStageAndHaveNone('storage', encoderType);

    const pipelineA = t.createBindingStatePipeline(
      encoderType,
      { a: 0, b: 1, out: 2 },
      'storage',
      'a.value - b.value'
    );
    const pipelineB = t.createBindingStatePipeline(
      encoderType,
      { a: 0, b: 1, out: 2 },
      'storage',
      'a.value + b.value'
    );

    const outA = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
    const outB = t.makeBufferWithContents(new Int32Array([0]), kBufferUsage);
    const bindGroups = {
      a: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([3]), kBufferUsage),
        'read-only-storage',
        encoderType
      ),
      b: t.createBindGroup(
        t.makeBufferWithContents(new Int32Array([2]), kBufferUsage),
        'read-only-storage',
        encoderType
      ),
      outA: t.createBindGroup(outA, 'storage', encoderType),
      outB: t.createBindGroup(outB, 'storage', encoderType),
    };

    const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType, {
      attachmentInfo: { colorFormats: ['r32sint'] },
    });
    encoder.setBindGroup(0, bindGroups.a);
    encoder.setBindGroup(1, bindGroups.b);

    t.setPipeline(encoder, pipelineA);
    encoder.setBindGroup(2, bindGroups.outA);
    t.dispatchOrDraw(encoder);

    t.setPipeline(encoder, pipelineB);
    encoder.setBindGroup(2, bindGroups.outB);
    t.dispatchOrDraw(encoder);

    validateFinishAndSubmit(true, true);

    t.expectGPUBufferValuesEqual(outA, new Int32Array([1]));
    t.expectGPUBufferValuesEqual(outB, new Int32Array([5]));
  });
