import {
  ValidBindableResource,
  BindableResource,
  kMaxQueryCount,
  ShaderStageKey,
} from '../../capability_info.js';
import { ResourceState, GPUTest } from '../../gpu_test.js';

export function createTextureWithState(
  t: GPUTest,
  state: ResourceState,
  descriptor?: Readonly<GPUTextureDescriptor>
): GPUTexture {
  descriptor = descriptor ?? {
    size: { width: 1, height: 1, depthOrArrayLayers: 1 },
    format: 'rgba8unorm',
    usage:
      GPUTextureUsage.COPY_SRC |
      GPUTextureUsage.COPY_DST |
      GPUTextureUsage.TEXTURE_BINDING |
      GPUTextureUsage.STORAGE_BINDING |
      GPUTextureUsage.RENDER_ATTACHMENT,
  };

  switch (state) {
    case 'valid':
      return t.createTextureTracked(descriptor);
    case 'invalid':
      return getErrorTexture(t);
    case 'destroyed': {
      const texture = t.createTextureTracked(descriptor);
      texture.destroy();
      return texture;
    }
  }
}

/**
 * Create a GPUTexture in the specified state. A `descriptor` may optionally be passed;
 * if `state` is `'invalid'`, it will be modified to add an invalid combination of usages.
 */
export function createBufferWithState(
  t: GPUTest,
  state: ResourceState,
  descriptor?: Readonly<GPUBufferDescriptor>
): GPUBuffer {
  descriptor = descriptor ?? {
    size: 4,
    usage: GPUBufferUsage.VERTEX,
  };

  switch (state) {
    case 'valid':
      return t.createBufferTracked(descriptor);

    case 'invalid': {
      // Make the buffer invalid because of an invalid combination of usages but keep the
      // descriptor passed as much as possible (for mappedAtCreation and friends).
      t.device.pushErrorScope('validation');
      const buffer = t.createBufferTracked({
        ...descriptor,
        usage: descriptor.usage | GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_SRC,
      });
      void t.device.popErrorScope();
      return buffer;
    }
    case 'destroyed': {
      const buffer = t.createBufferTracked(descriptor);
      buffer.destroy();
      return buffer;
    }
  }
}

/**
 * Create a GPUQuerySet in the specified state.
 * A `descriptor` may optionally be passed, which is used when `state` is not `'invalid'`.
 */
export function createQuerySetWithState(
  t: GPUTest,
  state: ResourceState,
  desc?: Readonly<GPUQuerySetDescriptor>
): GPUQuerySet {
  const descriptor = { type: 'occlusion' as const, count: 2, ...desc };

  switch (state) {
    case 'valid':
      return t.createQuerySetTracked(descriptor);
    case 'invalid': {
      // Make the queryset invalid because of the count out of bounds.
      descriptor.count = kMaxQueryCount + 1;
      return t.expectGPUError('validation', () => t.createQuerySetTracked(descriptor));
    }
    case 'destroyed': {
      const queryset = t.createQuerySetTracked(descriptor);
      queryset.destroy();
      return queryset;
    }
  }
}

/** Create an arbitrarily-sized GPUBuffer with the STORAGE usage. */
export function getStorageBuffer(t: GPUTest): GPUBuffer {
  return t.createBufferTracked({ size: 1024, usage: GPUBufferUsage.STORAGE });
}

/** Create an arbitrarily-sized GPUBuffer with the UNIFORM usage. */
export function getUniformBuffer(t: GPUTest): GPUBuffer {
  return t.createBufferTracked({ size: 1024, usage: GPUBufferUsage.UNIFORM });
}

/** Return an invalid GPUBuffer. */
export function getErrorBuffer(t: GPUTest): GPUBuffer {
  return createBufferWithState(t, 'invalid');
}

/** Return an invalid GPUSampler. */
export function getErrorSampler(t: GPUTest): GPUSampler {
  t.device.pushErrorScope('validation');
  const sampler = t.device.createSampler({ lodMinClamp: -1 });
  void t.device.popErrorScope();
  return sampler;
}

/**
 * Return an arbitrarily-configured GPUTexture with the `TEXTURE_BINDING` usage and specified
 * sampleCount. The `RENDER_ATTACHMENT` usage will also be specified if sampleCount > 1 as is
 * required by WebGPU SPEC.
 */
export function getSampledTexture(t: GPUTest, sampleCount: number = 1): GPUTexture {
  const usage =
    sampleCount > 1
      ? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
      : GPUTextureUsage.TEXTURE_BINDING;
  return t.createTextureTracked({
    size: { width: 16, height: 16, depthOrArrayLayers: 1 },
    format: 'rgba8unorm',
    usage,
    sampleCount,
  });
}

/** Return an arbitrarily-configured GPUTexture with the `STORAGE_BINDING` usage. */
function getStorageTexture(t: GPUTest, format: GPUTextureFormat): GPUTexture {
  return t.createTextureTracked({
    size: { width: 16, height: 16, depthOrArrayLayers: 1 },
    format,
    usage: GPUTextureUsage.STORAGE_BINDING,
  });
}

/** Return an arbitrarily-configured GPUTexture with the `RENDER_ATTACHMENT` usage. */
export function getRenderTexture(t: GPUTest, sampleCount: number = 1): GPUTexture {
  return t.createTextureTracked({
    size: { width: 16, height: 16, depthOrArrayLayers: 1 },
    format: 'rgba8unorm',
    usage: GPUTextureUsage.RENDER_ATTACHMENT,
    sampleCount,
  });
}

/** Return an invalid GPUTexture. */
export function getErrorTexture(t: GPUTest): GPUTexture {
  t.device.pushErrorScope('validation');
  const texture = t.createTextureTracked({
    size: { width: 0, height: 0, depthOrArrayLayers: 0 },
    format: 'rgba8unorm',
    usage: GPUTextureUsage.TEXTURE_BINDING,
  });
  void t.device.popErrorScope();
  return texture;
}

/** Return an invalid GPUTextureView (created from an invalid GPUTexture). */
export function getErrorTextureView(t: GPUTest): GPUTextureView {
  t.device.pushErrorScope('validation');
  const view = getErrorTexture(t).createView();
  void t.device.popErrorScope();
  return view;
}

/**
 * Return an arbitrary object of the specified {@link webgpu/capability_info!BindableResource} type
 * (e.g. `'errorBuf'`, `'nonFiltSamp'`, `sampledTexMS`, etc.)
 */
export function getBindingResource(t: GPUTest, bindingType: BindableResource): GPUBindingResource {
  switch (bindingType) {
    case 'errorBuf':
      return { buffer: getErrorBuffer(t) };
    case 'errorSamp':
      return getErrorSampler(t);
    case 'errorTex':
      return getErrorTextureView(t);
    case 'uniformBuf':
      return { buffer: getUniformBuffer(t) };
    case 'storageBuf':
      return { buffer: getStorageBuffer(t) };
    case 'filtSamp':
      return t.device.createSampler({ minFilter: 'linear' });
    case 'nonFiltSamp':
      return t.device.createSampler();
    case 'compareSamp':
      return t.device.createSampler({ compare: 'never' });
    case 'sampledTex':
      return getSampledTexture(t, 1).createView();
    case 'sampledTexMS':
      return getSampledTexture(t, 4).createView();
    case 'readonlyStorageTex':
    case 'writeonlyStorageTex':
    case 'readwriteStorageTex':
      return getStorageTexture(t, 'r32float').createView();
  }
}

/** Create an arbitrarily-sized GPUBuffer with the STORAGE usage from mismatched device. */
export function getDeviceMismatchedStorageBuffer(t: GPUTest): GPUBuffer {
  return t.trackForCleanup(
    t.mismatchedDevice.createBuffer({ size: 4, usage: GPUBufferUsage.STORAGE })
  );
}

/** Create an arbitrarily-sized GPUBuffer with the UNIFORM usage from mismatched device. */
export function getDeviceMismatchedUniformBuffer(t: GPUTest): GPUBuffer {
  return t.trackForCleanup(
    t.mismatchedDevice.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM })
  );
}

/** Return a GPUTexture with descriptor from mismatched device. */
export function getDeviceMismatchedTexture(
  t: GPUTest,
  descriptor: GPUTextureDescriptor
): GPUTexture {
  return t.trackForCleanup(t.mismatchedDevice.createTexture(descriptor));
}

/** Return an arbitrarily-configured GPUTexture with the `SAMPLED` usage from mismatched device. */
export function getDeviceMismatchedSampledTexture(t: GPUTest, sampleCount: number = 1): GPUTexture {
  return getDeviceMismatchedTexture(t, {
    size: { width: 4, height: 4, depthOrArrayLayers: 1 },
    format: 'rgba8unorm',
    usage: GPUTextureUsage.TEXTURE_BINDING,
    sampleCount,
  });
}

/** Return an arbitrarily-configured GPUTexture with the `STORAGE` usage from mismatched device. */
export function getDeviceMismatchedStorageTexture(
  t: GPUTest,
  format: GPUTextureFormat
): GPUTexture {
  return getDeviceMismatchedTexture(t, {
    size: { width: 4, height: 4, depthOrArrayLayers: 1 },
    format,
    usage: GPUTextureUsage.STORAGE_BINDING,
  });
}

/** Return an arbitrarily-configured GPUTexture with the `RENDER_ATTACHMENT` usage from mismatched device. */
export function getDeviceMismatchedRenderTexture(t: GPUTest, sampleCount: number = 1): GPUTexture {
  return getDeviceMismatchedTexture(t, {
    size: { width: 4, height: 4, depthOrArrayLayers: 1 },
    format: 'rgba8unorm',
    usage: GPUTextureUsage.RENDER_ATTACHMENT,
    sampleCount,
  });
}

export function getDeviceMismatchedBindingResource(
  t: GPUTest,
  bindingType: ValidBindableResource
): GPUBindingResource {
  switch (bindingType) {
    case 'uniformBuf':
      return { buffer: getDeviceMismatchedUniformBuffer(t) };
    case 'storageBuf':
      return { buffer: getDeviceMismatchedStorageBuffer(t) };
    case 'filtSamp':
      return t.mismatchedDevice.createSampler({ minFilter: 'linear' });
    case 'nonFiltSamp':
      return t.mismatchedDevice.createSampler();
    case 'compareSamp':
      return t.mismatchedDevice.createSampler({ compare: 'never' });
    case 'sampledTex':
      return getDeviceMismatchedSampledTexture(t, 1).createView();
    case 'sampledTexMS':
      return getDeviceMismatchedSampledTexture(t, 4).createView();
    case 'readonlyStorageTex':
    case 'writeonlyStorageTex':
    case 'readwriteStorageTex':
      return getDeviceMismatchedStorageTexture(t, 'r32float').createView();
  }
}

/** Return a no-op shader code snippet for the specified shader stage. */
export function getNoOpShaderCode(stage: ShaderStageKey): string {
  switch (stage) {
    case 'VERTEX':
      return `
        @vertex fn main() -> @builtin(position) vec4<f32> {
          return vec4<f32>();
        }
      `;
    case 'FRAGMENT':
      return `@fragment fn main() {}`;
    case 'COMPUTE':
      return `@compute @workgroup_size(1) fn main() {}`;
  }
}

/** Create a GPURenderPipeline in the specified state. */
export function createRenderPipelineWithState(
  t: GPUTest,
  state: 'valid' | 'invalid'
): GPURenderPipeline {
  return state === 'valid' ? createNoOpRenderPipeline(t) : createErrorRenderPipeline(t);
}

/** Return a GPURenderPipeline with default options and no-op vertex and fragment shaders. */
export function createNoOpRenderPipeline(
  t: GPUTest,
  layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto',
  colorFormat: GPUTextureFormat = 'rgba8unorm'
): GPURenderPipeline {
  return t.device.createRenderPipeline({
    layout,
    vertex: {
      module: t.device.createShaderModule({
        code: getNoOpShaderCode('VERTEX'),
      }),
      entryPoint: 'main',
    },
    fragment: {
      module: t.device.createShaderModule({
        code: getNoOpShaderCode('FRAGMENT'),
      }),
      entryPoint: 'main',
      targets: [{ format: colorFormat, writeMask: 0 }],
    },
    primitive: { topology: 'triangle-list' },
  });
}

/** Return an invalid GPURenderPipeline. */
export function createErrorRenderPipeline(t: GPUTest): GPURenderPipeline {
  t.device.pushErrorScope('validation');
  const pipeline = t.device.createRenderPipeline({
    layout: 'auto',
    vertex: {
      module: t.device.createShaderModule({
        code: '',
      }),
      entryPoint: '',
    },
  });
  void t.device.popErrorScope();
  return pipeline;
}

/** Return a GPUComputePipeline with a no-op shader. */
export function createNoOpComputePipeline(
  t: GPUTest,
  layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto'
): GPUComputePipeline {
  return t.device.createComputePipeline({
    layout,
    compute: {
      module: t.device.createShaderModule({
        code: getNoOpShaderCode('COMPUTE'),
      }),
      entryPoint: 'main',
    },
  });
}

/** Return an invalid GPUComputePipeline. */
export function createErrorComputePipeline(t: GPUTest): GPUComputePipeline {
  t.device.pushErrorScope('validation');
  const pipeline = t.device.createComputePipeline({
    layout: 'auto',
    compute: {
      module: t.device.createShaderModule({
        code: '',
      }),
      entryPoint: '',
    },
  });
  void t.device.popErrorScope();
  return pipeline;
}

/** Return an invalid GPUShaderModule. */
export function createInvalidShaderModule(t: GPUTest): GPUShaderModule {
  t.device.pushErrorScope('validation');
  const code = 'deadbeaf'; // Something make no sense
  const shaderModule = t.device.createShaderModule({ code });
  void t.device.popErrorScope();
  return shaderModule;
}

/** Helper for testing createRenderPipeline(Async) validation */
export function doCreateRenderPipelineTest(
  t: GPUTest,
  isAsync: boolean,
  _success: boolean,
  descriptor: GPURenderPipelineDescriptor,
  errorTypeName: 'GPUPipelineError' | 'TypeError' = 'GPUPipelineError'
) {
  if (isAsync) {
    if (_success) {
      t.shouldResolve(t.device.createRenderPipelineAsync(descriptor));
    } else {
      t.shouldReject(errorTypeName, t.device.createRenderPipelineAsync(descriptor));
    }
  } else {
    if (errorTypeName === 'GPUPipelineError') {
      t.expectValidationError(() => {
        t.device.createRenderPipeline(descriptor);
      }, !_success);
    } else {
      t.shouldThrow(_success ? false : errorTypeName, () => {
        t.device.createRenderPipeline(descriptor);
      });
    }
  }
}

/** Helper for testing createComputePipeline(Async) validation */
export function doCreateComputePipelineTest(
  t: GPUTest,
  isAsync: boolean,
  _success: boolean,
  descriptor: GPUComputePipelineDescriptor,
  errorTypeName: 'GPUPipelineError' | 'TypeError' = 'GPUPipelineError'
) {
  if (isAsync) {
    if (_success) {
      t.shouldResolve(t.device.createComputePipelineAsync(descriptor));
    } else {
      t.shouldReject(errorTypeName, t.device.createComputePipelineAsync(descriptor));
    }
  } else {
    if (errorTypeName === 'GPUPipelineError') {
      t.expectValidationError(() => {
        t.device.createComputePipeline(descriptor);
      }, !_success);
    } else {
      t.shouldThrow(_success ? false : errorTypeName, () => {
        t.device.createComputePipeline(descriptor);
      });
    }
  }
}
