export const description = `createView validation tests.`;

import { AllFeaturesMaxLimitsGPUTest, kResourceStates } from '../.././gpu_test.js';
import { kUnitCaseParamsBuilder } from '../../../common/framework/params_builder.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { unreachable } from '../../../common/util/util.js';
import {
  isValidTextureUsageCombination,
  kTextureAspects,
  kTextureDimensions,
  kTextureUsages,
  kTextureViewDimensions,
} from '../../capability_info.js';
import { GPUConst } from '../../constants.js';
import {
  kAllTextureFormats,
  kFeaturesForFormats,
  filterFormatsByFeature,
  textureFormatsAreViewCompatible,
  isDepthTextureFormat,
  isTextureFormatUsableWithStorageAccessMode,
  isTextureFormatColorRenderable,
  isColorTextureFormat,
  isStencilTextureFormat,
  getBlockInfoForTextureFormat,
  isTextureFormatPossiblyUsableAsRenderAttachment,
} from '../../format_info.js';
import {
  getTextureDimensionFromView,
  reifyTextureViewDescriptor,
  viewDimensionsForTextureDimension,
} from '../../util/texture/base.js';
import { reifyExtent3D } from '../../util/unions.js';

import * as vtu from './validation_test_utils.js';

export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);

const kLevels = 6;

g.test('format')
  .desc(
    `Views must have the view format compatible with the base texture, for all {texture format}x{view format}.`
  )
  .params(u =>
    u
      .combine('textureFormatFeature', kFeaturesForFormats)
      .combine('viewFormatFeature', kFeaturesForFormats)
      .beginSubcases()
      .expand('textureFormat', ({ textureFormatFeature }) =>
        filterFormatsByFeature(textureFormatFeature, kAllTextureFormats)
      )
      .expand('viewFormat', ({ viewFormatFeature }) =>
        filterFormatsByFeature(viewFormatFeature, [undefined, ...kAllTextureFormats])
      )
      .combine('useViewFormatList', [false, true])
  )
  .fn(t => {
    const { textureFormat, viewFormat, useViewFormatList } = t.params;
    const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(textureFormat);

    t.skipIfTextureFormatNotSupported(textureFormat, viewFormat);

    const compatible =
      viewFormat === undefined ||
      textureFormatsAreViewCompatible(t.device.features, textureFormat, viewFormat);

    const texture = t.createTextureTracked({
      format: textureFormat,
      size: [blockWidth, blockHeight],
      usage: GPUTextureUsage.TEXTURE_BINDING,

      // This is a test of createView, not createTexture. Don't pass viewFormats here that
      // are not compatible, as that is tested in createTexture.spec.ts.
      viewFormats:
        useViewFormatList && compatible && viewFormat !== undefined ? [viewFormat] : undefined,
    });

    // Successful if there is no view format, no reinterpretation was required, or the formats are compatible
    // and is was specified in the viewFormats list.
    const success =
      viewFormat === undefined || viewFormat === textureFormat || (compatible && useViewFormatList);
    t.expectValidationError(() => {
      texture.createView({ format: viewFormat });
    }, !success);
  });

g.test('dimension')
  .desc(
    `For all {texture dimension}, {view dimension}, test that they must be compatible:
  - 1d -> 1d
  - 2d -> 2d, 2d-array, cube, or cube-array
  - 3d -> 3d`
  )
  .params(u =>
    u
      .combine('textureDimension', kTextureDimensions)
      .combine('viewDimension', [...kTextureViewDimensions, undefined])
  )
  .fn(t => {
    const { textureDimension, viewDimension } = t.params;
    t.skipIfTextureViewDimensionNotSupported(t.params.viewDimension);

    const size = textureDimension === '1d' ? [4] : [4, 4, 6];
    const textureDescriptor = {
      format: 'rgba8unorm' as const,
      dimension: textureDimension,
      size,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };
    const texture = t.createTextureTracked(textureDescriptor);

    const view = { dimension: viewDimension };
    const reified = reifyTextureViewDescriptor(textureDescriptor, view);

    const success = getTextureDimensionFromView(reified.dimension) === textureDimension;
    t.expectValidationError(() => {
      texture.createView(view);
    }, !success);
  });

g.test('aspect')
  .desc(
    `For every {format}x{aspect}, test that the view aspect must exist in the format:
  - "all" is allowed for any format
  - "depth-only" is allowed only for depth and depth-stencil formats
  - "stencil-only" is allowed only for stencil and depth-stencil formats`
  )
  .params(u =>
    u //
      .combine('format', kAllTextureFormats)
      .combine('aspect', kTextureAspects)
  )
  .fn(t => {
    const { format, aspect } = t.params;
    const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(format);

    t.skipIfTextureFormatNotSupported(format);

    const texture = t.createTextureTracked({
      format,
      size: [blockWidth, blockHeight, 1],
      usage: GPUTextureUsage.TEXTURE_BINDING,
    });

    const success =
      aspect === 'all' ||
      (aspect === 'depth-only' && isDepthTextureFormat(format)) ||
      (aspect === 'stencil-only' && isStencilTextureFormat(format));
    t.expectValidationError(() => {
      texture.createView({ aspect });
    }, !success);
  });

const kTextureAndViewDimensions = kUnitCaseParamsBuilder
  .combine('textureDimension', kTextureDimensions)
  .expand('viewDimension', p => [
    undefined,
    ...viewDimensionsForTextureDimension(p.textureDimension),
  ]);

function validateCreateViewLayersLevels(tex: GPUTextureDescriptor, view: GPUTextureViewDescriptor) {
  const textureLevels = tex.mipLevelCount ?? 1;
  const textureLayers = tex.dimension === '2d' ? reifyExtent3D(tex.size).depthOrArrayLayers : 1;
  const reified = reifyTextureViewDescriptor(tex, view);

  let success =
    reified.mipLevelCount > 0 &&
    reified.baseMipLevel < textureLevels &&
    reified.baseMipLevel + reified.mipLevelCount <= textureLevels &&
    reified.arrayLayerCount > 0 &&
    reified.baseArrayLayer < textureLayers &&
    reified.baseArrayLayer + reified.arrayLayerCount <= textureLayers;
  if (reified.dimension === '1d' || reified.dimension === '2d' || reified.dimension === '3d') {
    success &&= reified.arrayLayerCount === 1;
  } else if (reified.dimension === 'cube') {
    success &&= reified.arrayLayerCount === 6;
  } else if (reified.dimension === 'cube-array') {
    success &&= reified.arrayLayerCount % 6 === 0;
  }
  return success;
}

g.test('array_layers')
  .desc(
    `For each texture dimension {1d,2d,3d}, for each possible view dimension for that texture
    dimension (or undefined, which defaults to the texture dimension), test validation of layer
    counts:
  - 1d, 2d, and 3d must have exactly 1 layer
  - 2d-array must have 1 or more layers
  - cube must have 6 layers
  - cube-array must have a positive multiple of 6 layers
  - Defaulting of baseArrayLayer and arrayLayerCount
  - baseArrayLayer+arrayLayerCount must be within the texture`
  )
  .params(
    kTextureAndViewDimensions
      .beginSubcases()
      .expand('textureLayers', ({ textureDimension: d }) => (d === '2d' ? [1, 6, 18] : [1]))
      .combine('textureLevels', [1, kLevels])
      .unless(p => p.textureDimension === '1d' && p.textureLevels !== 1)
      .expand(
        'baseArrayLayer',
        ({ textureLayers: l }) => new Set([undefined, 0, 1, 5, 6, 7, l - 1, l, l + 1])
      )
      .expand('arrayLayerCount', function* ({ textureLayers: l, baseArrayLayer = 0 }) {
        yield undefined;
        for (const lastArrayLayer of new Set([0, 1, 5, 6, 7, l - 1, l, l + 1])) {
          if (baseArrayLayer <= lastArrayLayer) yield lastArrayLayer - baseArrayLayer;
        }
      })
  )
  .fn(t => {
    const {
      textureDimension,
      viewDimension,
      textureLayers,
      textureLevels,
      baseArrayLayer,
      arrayLayerCount,
    } = t.params;

    t.skipIfTextureViewDimensionNotSupported(viewDimension);

    const kWidth = 1 << (kLevels - 1); // 32
    const textureDescriptor: GPUTextureDescriptor = {
      format: 'rgba8unorm',
      dimension: textureDimension,
      size:
        textureDimension === '1d'
          ? [kWidth]
          : textureDimension === '2d'
          ? [kWidth, kWidth, textureLayers]
          : textureDimension === '3d'
          ? [kWidth, kWidth, kWidth]
          : unreachable(),
      mipLevelCount: textureLevels,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    const viewDescriptor = { dimension: viewDimension, baseArrayLayer, arrayLayerCount };
    const success = validateCreateViewLayersLevels(textureDescriptor, viewDescriptor);

    const texture = t.createTextureTracked(textureDescriptor);
    t.expectValidationError(() => {
      texture.createView(viewDescriptor);
    }, !success);
  });

g.test('mip_levels')
  .desc(
    `Views must have at least one level, and must be within the level of the base texture.

  - mipLevelCount=0 at various baseMipLevel values
  - Cases where baseMipLevel+mipLevelCount goes past the end of the texture
  - Cases with baseMipLevel or mipLevelCount undefined (compares against reference defaulting impl)
  `
  )
  .params(
    kTextureAndViewDimensions
      .beginSubcases()
      .combine('textureLevels', [1, kLevels - 2, kLevels])
      .unless(p => p.textureDimension === '1d' && p.textureLevels !== 1)
      .expand(
        'baseMipLevel',
        ({ textureLevels: l }) => new Set([undefined, 0, 1, 5, 6, 7, l - 1, l, l + 1])
      )
      .expand('mipLevelCount', function* ({ textureLevels: l, baseMipLevel = 0 }) {
        yield undefined;
        for (const lastMipLevel of new Set([0, 1, 5, 6, 7, l - 1, l, l + 1])) {
          if (baseMipLevel <= lastMipLevel) yield lastMipLevel - baseMipLevel;
        }
      })
  )
  .fn(t => {
    const { textureDimension, viewDimension, textureLevels, baseMipLevel, mipLevelCount } =
      t.params;

    t.skipIfTextureViewDimensionNotSupported(viewDimension);

    const textureDescriptor: GPUTextureDescriptor = {
      format: 'rgba8unorm',
      dimension: textureDimension,
      size:
        textureDimension === '1d' ? [32] : textureDimension === '3d' ? [32, 32, 32] : [32, 32, 18],
      mipLevelCount: textureLevels,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    const viewDescriptor = { dimension: viewDimension, baseMipLevel, mipLevelCount };
    const success = validateCreateViewLayersLevels(textureDescriptor, viewDescriptor);

    const texture = t.createTextureTracked(textureDescriptor);
    t.debug(`${mipLevelCount} ${success}`);
    t.expectValidationError(() => {
      texture.createView(viewDescriptor);
    }, !success);
  });

g.test('cube_faces_square')
  .desc(
    `Test that the X/Y dimensions of cube and cube array textures must be square.
  - {2d (control case), cube, cube-array}`
  )
  .params(u =>
    u //
      .combine('dimension', ['2d', 'cube', 'cube-array'] as const)
      .combine('size', [
        [4, 4, 6],
        [5, 5, 6],
        [4, 5, 6],
        [4, 8, 6],
        [8, 4, 6],
      ])
  )
  .fn(t => {
    const { dimension, size } = t.params;

    t.skipIfTextureViewDimensionNotSupported(dimension);

    const texture = t.createTextureTracked({
      format: 'rgba8unorm',
      size,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    });

    const success = dimension === '2d' || size[0] === size[1];
    t.expectValidationError(() => {
      texture.createView({ dimension });
    }, !success);
  });

g.test('texture_state')
  .desc(`createView should fail if the texture is invalid (but succeed if it is destroyed)`)
  .paramsSubcasesOnly(u => u.combine('state', kResourceStates))
  .fn(t => {
    const { state } = t.params;
    const texture = vtu.createTextureWithState(t, state);

    t.expectValidationError(() => {
      texture.createView();
    }, state === 'invalid');
  });

g.test('texture_view_usage')
  .desc(
    `Test texture view usage (single, combined, inherited) for every texture format and texture usage`
  )
  .params(u =>
    u //
      .combine('format', kAllTextureFormats)
      .combine('textureUsage', kTextureUsages)
      .unless(({ format, textureUsage }) => {
        return (
          (textureUsage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 &&
          !isTextureFormatPossiblyUsableAsRenderAttachment(format)
        );
      })
      .beginSubcases()
      .combine('textureViewUsage', kTextureUsages)
      .unless(({ textureUsage, textureViewUsage }) => {
        // TRANSIENT_ATTACHMENT is only valid when combined with RENDER_ATTACHMENT.
        return (
          textureUsage === GPUConst.TextureUsage.TRANSIENT_ATTACHMENT ||
          textureViewUsage === GPUConst.TextureUsage.TRANSIENT_ATTACHMENT
        );
      })
  )
  .fn(t => {
    const { format, textureUsage, textureViewUsage } = t.params;

    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatDoesNotSupportUsage(textureUsage, format);

    const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(format);

    const texture = t.createTextureTracked({
      size: [blockWidth, blockHeight, 1],
      format,
      usage: textureUsage,
    });

    let success = true;

    // Texture view usage must be a subset of texture usage
    if ((~textureUsage & textureViewUsage) !== 0) success = false;

    t.expectValidationError(() => {
      texture.createView({
        usage: textureViewUsage,
      });
    }, !success);
  });

g.test('texture_view_usage_of_multiple_usages')
  .desc(
    `For a single format (rgba8unorm), check that createView:
    - allows 0 usages
    - disallows subsetting usages of TRANSIENT_ATTACHMENT textures
  `
  )
  .params(u =>
    u
      .combine('usage1', kTextureUsages)
      .combine('usage2', kTextureUsages)
      .filter(p => p.usage1 <= p.usage2)
      .filter(p => isValidTextureUsageCombination(p.usage1 | p.usage2))
      .beginSubcases()
      .expand('viewUsage', p => new Set([0, p.usage1, p.usage2, p.usage1 | p.usage2]))
  )
  .fn(t => {
    const { usage1, usage2, viewUsage } = t.params;
    const usage = usage1 | usage2;

    // MAINTENANCE_TODO(#4509): Remove this after all implementations have TRANSIENT_ATTACHMENT.
    if ((usage & GPUConst.TextureUsage.TRANSIENT_ATTACHMENT) !== 0) {
      t.skipIfTransientAttachmentNotSupported();
    }

    let isValid = true;
    if (usage & GPUTextureUsage.TRANSIENT_ATTACHMENT) {
      isValid &&= viewUsage === 0 || viewUsage === usage;
    }

    const texture = t.createTextureTracked({ format: 'rgba8unorm', size: [1, 1], usage });
    t.expectGPUError(
      'validation',
      () => {
        texture.createView({ usage: viewUsage });
      },
      !isValid
    );
  });

g.test('texture_view_usage_with_view_format')
  .desc(
    `Test that the texture view usage must be supported by the view's format. Checks for every view
    format possible, and every usage supported by the texture's format`
  )
  .params(u =>
    u
      .combine('textureFormat', kAllTextureFormats)
      .combine('usage', kTextureUsages)
      .beginSubcases()
      .combine('viewFormat', kAllTextureFormats)
      .unless(({ usage }) => {
        // TRANSIENT_ATTACHMENT is only valid when combined with RENDER_ATTACHMENT.
        return usage === GPUConst.TextureUsage.TRANSIENT_ATTACHMENT;
      })
  )
  .fn(t => {
    const { textureFormat, viewFormat, usage } = t.params;

    t.skipIfTextureFormatNotSupported(textureFormat, viewFormat);
    t.skipIfTextureFormatDoesNotSupportUsage(usage, textureFormat);

    if (!textureFormatsAreViewCompatible(t.device.features, textureFormat, viewFormat)) {
      t.skip(`"${textureFormat}" and "${viewFormat}" are not view-compatible`);
    }

    const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(textureFormat);
    const texture = t.createTextureTracked({
      size: [blockWidth, blockHeight, 1],
      format: textureFormat,
      usage,
      viewFormats: [viewFormat],
    });

    let success = true;

    // Texture view usage must be a subset of texture usage
    if (usage & GPUTextureUsage.STORAGE_BINDING) {
      if (!isTextureFormatUsableWithStorageAccessMode(t.device.features, viewFormat, 'write-only'))
        success = false;
    }
    if (usage & GPUTextureUsage.RENDER_ATTACHMENT) {
      if (
        isColorTextureFormat(viewFormat) &&
        !isTextureFormatColorRenderable(t.device.features, viewFormat)
      )
        success = false;
    }

    // Test with explicitly setting the view usage.
    t.expectValidationError(() => {
      texture.createView({
        usage,
        format: viewFormat,
      });
    }, !success);

    // Test with inheriting the view usage.
    t.expectValidationError(() => {
      texture.createView({
        format: viewFormat,
      });
    }, !success);
  });
