export const description = `
Validation tests for constructor built-in functions.
`;

import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
import { keysOf } from '../../../../../../common/util/data_tables.js';
import {
  isAbstractType,
  isConvertible,
  isFloatType,
  MatrixType,
  ScalarType,
  scalarTypeOf,
  Type,
  VectorType,
} from '../../../../../util/conversion.js';
import { ShaderValidationTest } from '../../../shader_validation_test.js';

export const g = makeTestGroup(ShaderValidationTest);

const kScalarTypes = ['bool', 'i32', 'u32', 'f32', 'f16'];

g.test('scalar_zero_value')
  .desc('Tests zero value scalar constructors')
  .params(u => u.combine('type', kScalarTypes))
  .fn(t => {
    const enable = t.params.type === 'f16' ? 'enable f16;' : '';
    const code = `${enable}
    const x : ${t.params.type} = ${t.params.type}();
    const_assert x == ${t.params.type}(0);`;
    t.expectCompileResult(true, code);
  });

g.test('scalar_value')
  .desc('Tests scalar value constructors')
  .params(u =>
    u
      .combine('type', kScalarTypes)
      .combine('value_type', [...kScalarTypes, 'vec2u', 'S', 'array<u32, 2>'])
  )
  .fn(t => {
    const enable = t.params.type === 'f16' || t.params.value_type === 'f16' ? 'enable f16;' : '';
    const code = `${enable}
    const x : ${t.params.type} = ${t.params.type}(${t.params.value_type}());`;
    t.expectCompileResult(kScalarTypes.includes(t.params.value_type), code);
  });

g.test('vector_zero_value')
  .desc('Tests zero value vector constructors')
  .params(u =>
    u
      .combine('type', [...kScalarTypes, 'abstract-int', 'abstract-float'] as const)
      .beginSubcases()
      .combine('size', [2, 3, 4] as const)
  )
  .fn(t => {
    const abstract = t.params.type === 'abstract-int' || t.params.type === 'abstract-float';
    const param = abstract ? '' : `<${t.params.type}>`;
    const decl = `vec${t.params.size}${param}`;
    const enable = t.params.type === 'f16' ? 'enable f16;' : '';
    const comparison = abstract ? '0' : `${t.params.type}(0)`;
    let code = `${enable}
    const x ${abstract ? '' : `: ${decl}`} = ${decl}();\n`;
    for (let i = 0; i < t.params.size; i++) {
      code += `const_assert x[${i}] == ${comparison};\n`;
    }
    t.expectCompileResult(true, code);
  });

g.test('vector_splat')
  .desc('Test vector splat constructors')
  .params(u =>
    u
      .combine('type', [
        'bool',
        'i32',
        'u32',
        'f32',
        'f16',
        'abstract-int',
        'abstract-float',
      ] as const)
      .combine('ele_type', [
        'bool',
        'i32',
        'u32',
        'f32',
        'f16',
        'abstract-int',
        'abstract-float',
        'mat2x2f',
        'mat3x3h',
        'vec2i',
        'vec3f',
      ] as const)
      .beginSubcases()
      .combine('size', [2, 3, 4] as const)
  )
  .fn(t => {
    const eleTy = Type[t.params.ele_type];
    const abstract = t.params.type === 'abstract-int' || t.params.type === 'abstract-float';
    const param = abstract ? '' : `<${t.params.type}>`;
    const decl = `vec${t.params.size}${param}`;
    const enable = t.params.type === 'f16' || t.params.ele_type === 'f16' ? 'enable f16;' : '';
    const eleValue = eleTy.create(1).wgsl();
    const valueCall = decl;
    const code = `${enable}
    const x ${abstract ? '' : `: ${decl}`} = ${valueCall}(${eleValue});`;
    const ty = Type[t.params.type];
    const expect =
      (eleTy instanceof ScalarType && (isConvertible(eleTy, ty) || isAbstractType(ty))) ||
      (eleTy instanceof VectorType && eleTy.width === t.params.size);
    t.expectCompileResult(expect, code);
  });

g.test('vector_copy')
  .desc('Test vector copy constructors')
  .params(u =>
    u
      .combine('decl_type', [
        'bool',
        'i32',
        'u32',
        'f32',
        'f16',
        'abstract-int',
        'abstract-float',
      ] as const)
      .combine('value_type', [
        'bool',
        'i32',
        'u32',
        'f32',
        'f16',
        'abstract-int',
        'abstract-float',
      ] as const)
      .beginSubcases()
      .combine('decl_size', [2, 3, 4] as const)
      .combine('value_size', [2, 3, 4] as const)
  )
  .fn(t => {
    const declTy = Type[t.params.decl_type];
    const valueTy = Type[t.params.value_type];
    const valueVecTy = Type['vec'](t.params.value_size, valueTy);
    const enable = declTy.requiresF16() || valueTy.requiresF16() ? 'enable f16;' : '';
    const decl = `vec${t.params.decl_size}<${t.params.decl_type}>`;
    const ctor = `vec${t.params.decl_size}${
      isAbstractType(declTy) ? '' : `<${t.params.decl_type}>`
    }`;
    const code = `${enable}
    const x ${isAbstractType(declTy) ? '' : `: ${decl}`} = ${ctor}(${valueVecTy
      .create(1)
      .wgsl()});`;

    t.expectCompileResult(t.params.decl_size === t.params.value_size, code);
  });

g.test('vector_elementwise')
  .desc('Test element-wise vector constructors')
  .params(u =>
    u
      .combine('type', [
        'bool',
        'i32',
        'u32',
        'f32',
        'f16',
        'abstract-int',
        'abstract-float',
      ] as const)
      .combine('ele_type', [
        'bool',
        'i32',
        'u32',
        'f32',
        'f16',
        'abstract-int',
        'abstract-float',
        'mat2x2f',
        'mat3x3h',
        'vec2i',
        'vec3f',
      ] as const)
      .beginSubcases()
      .combine('size', [2, 3, 4] as const)
      .combine('num_eles', [2, 3, 4, 5] as const)
      .combine('full_type', [true, false] as const)
  )
  .fn(t => {
    const eleTy = Type[t.params.ele_type];
    const abstract = t.params.type === 'abstract-int' || t.params.type === 'abstract-float';
    const param = abstract ? '' : `<${t.params.type}>`;
    const decl = `vec${t.params.size}${param}`;
    const enable = t.params.type === 'f16' || t.params.ele_type === 'f16' ? 'enable f16;' : '';
    const eleValue = eleTy.create(1).wgsl();
    const valueCall = t.params.full_type ? decl : `vec${t.params.size}`;
    let code = `${enable}
    const x ${abstract ? '' : `: ${decl}`} = ${valueCall}(`;
    for (let i = 0; i < t.params.num_eles; i++) {
      code += `${eleValue},`;
    }
    code += `);`;
    const ty = Type[t.params.type];
    // WGSL requires:
    // * number of elements match
    // * element types match (or auto convert, vector special case)
    //   * abstract decl works because it is untyped and inferred as a different type
    const num_eles =
      eleTy instanceof VectorType ? t.params.num_eles * eleTy.width : t.params.num_eles;
    const expect =
      !(eleTy instanceof MatrixType) &&
      t.params.size === num_eles &&
      (isConvertible(scalarTypeOf(eleTy), ty) ||
        t.params.type === 'abstract-int' ||
        t.params.type === 'abstract-float');
    t.expectCompileResult(expect, code);
  });

g.test('vector_mixed')
  .desc('Test vector constructors with mixed elements and vectors')
  .params(u =>
    u
      .combine('type', [
        'bool',
        'i32',
        'u32',
        'f32',
        'f16',
        'abstract-int',
        'abstract-float',
      ] as const)
      .combine('ele_type', [
        'bool',
        'i32',
        'u32',
        'f32',
        'f16',
        'abstract-int',
        'abstract-float',
      ] as const)
      .beginSubcases()
      .combine('size', [3, 4] as const)
      .combine('num_eles', [3, 4, 5] as const)
      .combine('full_type', [true, false] as const)
  )
  .fn(t => {
    const eleTy = Type[t.params.ele_type];
    const abstract = t.params.type === 'abstract-int' || t.params.type === 'abstract-float';
    const param = abstract ? '' : `<${t.params.type}>`;
    const decl = `vec${t.params.size}${param}`;
    const enable = t.params.type === 'f16' || t.params.ele_type === 'f16' ? 'enable f16;' : '';
    const v = eleTy.create(1).wgsl();
    const call = t.params.full_type ? decl : `vec${t.params.size}`;
    let code = `${enable}\n`;

    switch (t.params.num_eles) {
      case 3:
        code += `const x1 ${abstract ? '' : `: ${decl}`} = ${call}(${v}, vec2(${v}, ${v}));\n`;
        code += `const x2 ${abstract ? '' : `: ${decl}`} = ${call}(vec2(${v}, ${v}), ${v});\n`;
        break;
      case 4:
        code += `const x1 ${
          abstract ? '' : `: ${decl}`
        } = ${call}(${v}, vec2(${v}, ${v}), ${v});\n`;
        code += `const x2 ${
          abstract ? '' : `: ${decl}`
        } = ${call}(${v}, ${v}, vec2(${v}, ${v}));\n`;
        code += `const x3 ${
          abstract ? '' : `: ${decl}`
        } = ${call}(vec2(${v}, ${v}), ${v}, ${v});\n`;
        code += `const x4 ${
          abstract ? '' : `: ${decl}`
        } = ${call}(vec3(${v}, ${v}, ${v}), ${v});\n`;
        code += `const x5 ${
          abstract ? '' : `: ${decl}`
        } = ${call}(${v}, vec3(${v}, ${v}, ${v}));\n`;
        break;
      case 5:
        // This case is always invalid so try a few only.
        code += `const x1 ${
          abstract ? '' : `: ${decl}`
        } = ${call}(${v}, vec3(${v}, ${v}), ${v});\n`;
        code += `const x1 ${abstract ? '' : `: ${decl}`} = ${call}(${v}, vec4(${v}}), ${v});\n`;
        break;
    }
    const ty = Type[t.params.type];
    // WGSL requires:
    // * number of elements match (in total, not parameters)
    // * element types match (or auto convert)
    //   * abstract decl works because it is untyped and inferred as a different type
    const expect =
      t.params.size === t.params.num_eles &&
      (isConvertible(eleTy, ty) ||
        t.params.type === 'abstract-int' ||
        t.params.type === 'abstract-float');
    t.expectCompileResult(expect, code);
  });

g.test('matrix_zero_value')
  .desc('Tests zero value matrix constructors')
  .params(u =>
    u
      .combine('type', ['f32', 'f16'] as const)
      .beginSubcases()
      .combine('rows', [2, 3, 4] as const)
      .combine('cols', [2, 3, 4] as const)
  )
  .fn(t => {
    const decl = `mat${t.params.cols}x${t.params.rows}<${t.params.type}>`;
    const enable = t.params.type === 'f16' ? 'enable f16;' : '';
    let code = `${enable}
    const x : ${decl} = ${decl}();\n`;
    for (let c = 0; c < t.params.cols; c++) {
      for (let r = 0; r < t.params.rows; r++) {
        code += `const_assert x[${c}][${r}] == ${t.params.type}(0);\n`;
      }
    }
    t.expectCompileResult(true, code);
  });

g.test('matrix_copy')
  .desc('Test matrix copy constructors')
  .params(u =>
    u
      .combine('type1', ['f16', 'f32', 'abstract-float'] as const)
      .combine('type2', ['f16', 'f32', 'abstract-float'] as const)
      .beginSubcases()
      .combine('c1', [2, 3, 4] as const)
      .combine('r1', [2, 3, 4] as const)
      .combine('c2', [2, 3, 4] as const)
      .combine('r2', [2, 3, 4] as const)
  )
  .fn(t => {
    const t1 = Type[t.params.type1];
    const t2 = Type[t.params.type2];
    const m2 = Type['mat'](t.params.c2, t.params.r2, t2);
    const enable = t1.requiresF16() || t2.requiresF16() ? 'enable f16;' : '';
    const decl = `mat${t.params.c1}x${t.params.r1}<${t.params.type1}>`;
    const call = `mat${t.params.c1}x${t.params.r1}${
      isAbstractType(t1) ? '' : `<${t.params.type1}>`
    }`;
    const code = `${enable}
    const m ${isAbstractType(t1) ? '' : `: ${decl}`} = ${call}(${m2.create(0).wgsl()});`;
    t.expectCompileResult(t.params.c1 === t.params.c2 && t.params.r1 === t.params.r2, code);
  });

g.test('matrix_column')
  .desc('Test matrix column constructors')
  .params(u =>
    u
      .combine('type1', ['f16', 'f32', 'abstract-float'] as const)
      .combine('type2', ['f16', 'f32', 'abstract-float', 'i32', 'u32', 'bool'] as const)
      .beginSubcases()
      .combine('c1', [2, 3, 4] as const)
      .combine('r1', [2, 3, 4] as const)
      .combine('c2', [2, 3, 4] as const)
      .combine('r2', [2, 3, 4] as const)
  )
  .fn(t => {
    const t1 = Type[t.params.type1];
    const t2 = Type[t.params.type2];
    const enable = t1.requiresF16() || t2.requiresF16() ? 'enable f16;' : '';
    const vecTy2 = Type['vec'](t.params.r2, t2);
    let values = ``;
    for (let i = 0; i < t.params.c2; i++) {
      values += `${vecTy2.create(1).wgsl()},`;
    }
    const decl = `mat${t.params.c1}x${t.params.r1}<${t.params.type1}>`;
    const call = `mat${t.params.c1}x${t.params.r1}${
      isAbstractType(t1) ? '' : `<${t.params.type1}>`
    }`;
    const code = `${enable}
    const m ${isAbstractType(t1) ? '' : `: ${decl}`} = ${call}(${values});`;
    const expect =
      isFloatType(t2) &&
      t.params.c1 === t.params.c2 &&
      t.params.r1 === t.params.r2 &&
      (t1 === t2 || isAbstractType(t1) || isAbstractType(t2));
    t.expectCompileResult(expect, code);
  });

g.test('matrix_elementwise')
  .desc('Test matrix element-wise constructors')
  .params(u =>
    u
      .combine('type1', ['f16', 'f32', 'abstract-float'] as const)
      .combine('type2', ['f16', 'f32', 'abstract-float', 'i32', 'u32', 'bool'] as const)
      .beginSubcases()
      .combine('c1', [2, 3, 4] as const)
      .combine('r1', [2, 3, 4] as const)
      .combine('c2', [2, 3, 4] as const)
      .combine('r2', [2, 3, 4] as const)
  )
  .fn(t => {
    const t1 = Type[t.params.type1];
    const t2 = Type[t.params.type2];
    const enable = t1.requiresF16() || t2.requiresF16() ? 'enable f16;' : '';
    let values = ``;
    for (let i = 0; i < t.params.c2 * t.params.r2; i++) {
      values += `${t2.create(1).wgsl()},`;
    }
    const decl = `mat${t.params.c1}x${t.params.r1}<${t.params.type1}>`;
    const call = `mat${t.params.c1}x${t.params.r1}${
      isAbstractType(t1) ? '' : `<${t.params.type1}>`
    }`;
    const code = `${enable}
    const m ${isAbstractType(t1) ? '' : `: ${decl}`} = ${call}(${values});`;
    const expect =
      isFloatType(t2) &&
      t.params.c1 * t.params.r1 === t.params.c2 * t.params.r2 &&
      (t1 === t2 || isAbstractType(t1) || isAbstractType(t2));
    t.expectCompileResult(expect, code);
  });

interface ArrayCase {
  element: string;
  size: number | string;
  valid: boolean;
  values: string;
}

const kArrayCases: Record<string, ArrayCase> = {
  i32: {
    element: 'i32',
    size: 4,
    valid: true,
    values: '1,2,3,4',
  },
  f32: {
    element: 'f32',
    size: 1,
    valid: true,
    values: '0',
  },
  u32: {
    element: 'u32',
    size: 2,
    valid: true,
    values: '2,4',
  },
  valid_array: {
    element: 'array<u32, 2>',
    size: 2,
    valid: true,
    values: 'array(0,1), array(2,3)',
  },
  invalid_rta: {
    element: 'u32',
    size: '',
    valid: false,
    values: '0',
  },
  invalid_override_array: {
    element: 'u32',
    size: 'o',
    valid: false,
    values: '1',
  },
  valid_struct: {
    element: 'valid_S',
    size: 1,
    valid: true,
    values: 'valid_S(0)',
  },
  invalid_struct: {
    element: 'invalid_S',
    size: 1,
    valid: false,
    values: 'array(0)',
  },
  invalid_atomic: {
    element: 'atomic<u32>',
    size: 1,
    valid: false,
    values: '0',
  },
};

g.test('array_zero_value')
  .desc('Tests zero value array constructors')
  .params(u => u.combine('case', keysOf(kArrayCases)))
  .fn(t => {
    const testcase = kArrayCases[t.params.case];
    const decl = `array<${testcase.element}, ${testcase.size}>`;
    const code = `override o : i32 = 1;
    struct valid_S {
      x : u32
    }
    struct invalid_S {
      x : array<u32>
    }
    const x : ${decl} = ${decl}();`;
    t.expectCompileResult(testcase.valid, code);
  });

g.test('array_value')
  .desc('Tests array value constructor')
  .params(u => u.combine('case', keysOf(kArrayCases)))
  .fn(t => {
    const testcase = kArrayCases[t.params.case];
    const decl = `array<${testcase.element}, ${testcase.size}>`;
    const code = `override o : i32 = 1;
    struct valid_S {
      x : u32
    }
    struct invalid_S {
      x : array<u32>
    }
    const x : ${decl} = ${decl}(${testcase.values});`;
    t.expectCompileResult(testcase.valid, code);
  });

const kStructCases = {
  i32: {
    name: 'S',
    decls: `struct S { x : u32 }`,
    valid: true,
    values: '0',
  },
  f32x2: {
    name: 'S',
    decls: `struct S { x : f32, y : f32 }`,
    valid: true,
    values: '0,1',
  },
  vec3u: {
    name: 'S',
    decls: `struct S { x : vec3u }`,
    valid: true,
    values: 'vec3()',
  },
  valid_array: {
    name: 'S',
    decls: `struct S { x : array<u32, 2> }`,
    valid: true,
    values: 'array(1,2)',
  },
  runtime_array: {
    name: 'S',
    decls: `struct S { x : array<u32> }`,
    valid: false,
    values: 'array(0)',
  },
  atomic: {
    name: 'S',
    decls: `struct S { x : atomic<u32> }`,
    valid: false,
    values: '0',
  },
  struct: {
    name: 'S',
    decls: `struct S {
      x : T
    }
    struct T {
      x : u32
    }`,
    valid: true,
    values: 'T(0)',
  },
  many_members: {
    name: 'S',
    decls: `struct S {
      a : bool,
      b : u32,
      c : i32,
      d : vec4f,
    }`,
    valid: true,
    values: 'false, 1u, 32i, vec4f(1.0f)',
  },
};

g.test('struct_zero_value')
  .desc('Tests zero value struct constructors')
  .params(u => u.combine('case', keysOf(kStructCases)))
  .fn(t => {
    const testcase = kStructCases[t.params.case];
    const code = `
    ${testcase.decls}
    const x : ${testcase.name} = ${testcase.name}();`;
    t.expectCompileResult(testcase.valid, code);
  });

g.test('struct_value')
  .desc('Tests struct value constructors')
  .params(u => u.combine('case', keysOf(kStructCases)))
  .fn(t => {
    const testcase = kStructCases[t.params.case];
    const code = `
    ${testcase.decls}
    const x : ${testcase.name} = ${testcase.name}(${testcase.values});`;
    t.expectCompileResult(testcase.valid, code);
  });

const kConstructors = {
  u32_0: 'u32()',
  i32_0: 'i32()',
  bool_0: 'bool()',
  f32_0: 'f32()',
  f16_0: 'f16()',
  vec2_0: 'vec2()',
  vec3_0: 'vec3()',
  vec4_0: 'vec4()',
  mat2x2_0: 'mat2x2f()',
  mat2x3_0: 'mat2x3f()',
  mat2x4_0: 'mat2x4f()',
  mat3x2_0: 'mat3x2f()',
  mat3x3_0: 'mat3x3f()',
  mat3x4_0: 'mat3x4f()',
  mat4x2_0_f16: 'mat4x2h()',
  mat4x3_0_f16: 'mat4x3h()',
  mat4x4_0_f16: 'mat4x4h()',
  S_0: 'S()',
  array_0: 'array<u32, 4>()',
  u32: 'u32(1)',
  i32: 'i32(1)',
  bool: 'bool(true)',
  f32: 'f32(1)',
  f16: 'f16(1)',
  vec2f: 'vec2<f32>(1)',
  vec3_f16: 'vec3<f16>(1)',
  vec4: 'vec4(1)',
  mat2x2: 'mat2x2f(1,1,1,1)',
  mat2x3: 'mat2x3f(1,1,1,1,1,1)',
  mat2x4: 'mat2x4f(1,1,1,1,1,1,1,1)',
  mat3x2_f16: 'mat3x2<f16>(vec2h(),vec2h(),vec2h())',
  mat3x3_f16: 'mat3x3<f16>(vec3h(),vec3h(),vec3h())',
  mat3x4_f16: 'mat3x4<f16>(vec4h(),vec4h(),vec4h())',
  mat4x2: 'mat4x2(vec2(),vec2(),vec2(),vec2())',
  mat4x3: 'mat4x3(vec3(),vec3(),vec3(),vec3())',
  mat4x4: 'mat4x4(vec4(),vec4(),vec4(),vec4())',
  S: 'S(1,1)',
  array_abs: 'array(1,2,3)',
  array: 'array<u32, 4>(1,2,3,4)',
};

g.test('must_use')
  .desc('Tests that value constructors must be used')
  .params(u => u.combine('ctor', keysOf(kConstructors)).combine('use', [true, false] as const))
  .fn(t => {
    const code = `
    ${t.params.ctor.includes('f16') ? 'enable f16;' : ''}
    struct S {
      x : u32,
      y : f32,
    }
    fn foo() {
      ${t.params.use ? '_ =' : ''} ${kConstructors[t.params.ctor]};
    }`;
    t.expectCompileResult(t.params.use, code);
  });

g.test('partial_eval')
  .desc('Tests that mixed runtime and early eval expressions catch errors')
  .params(u =>
    u
      .combine('eleTy', ['i32', 'u32'] as const)
      .combine('compTy', ['array', 'vec2', 'vec3', 'vec4', 'S'] as const)
      .combine('stage', ['constant', 'runtime'] as const)
      .beginSubcases()
      .expandWithParams(t => {
        const cases = [];
        switch (t.compTy) {
          case 'array':
            cases.push({ numEles: 2, index: 0 });
            cases.push({ numEles: 2, index: 1 });
            cases.push({ numEles: 3, index: 0 });
            cases.push({ numEles: 3, index: 1 });
            cases.push({ numEles: 3, index: 2 });
            break;
          case 'vec2':
            cases.push({ numEles: 2, index: 0 });
            cases.push({ numEles: 2, index: 1 });
            break;
          case 'vec3':
            cases.push({ numEles: 3, index: 0 });
            cases.push({ numEles: 3, index: 1 });
            cases.push({ numEles: 3, index: 2 });
            break;
          case 'vec4':
            cases.push({ numEles: 4, index: 0 });
            cases.push({ numEles: 4, index: 1 });
            cases.push({ numEles: 4, index: 2 });
            cases.push({ numEles: 4, index: 3 });
            break;
          case 'S':
            cases.push({ numEles: 2, index: 0 });
            cases.push({ numEles: 2, index: 1 });
            break;
        }
        return cases;
      })
  )
  .fn(t => {
    const eleTy = Type['abstract-int'];
    const value = t.params.eleTy === 'i32' ? 0xfffffffff : -1;
    let compParams = '';
    for (let i = 0; i < t.params.numEles; i++) {
      if (t.params.index === i) {
        switch (t.params.stage) {
          case 'constant':
            compParams += `${eleTy.create(value).wgsl()}, `;
            break;
          case 'runtime':
            compParams += `v, `;
            break;
        }
      } else {
        compParams += `v, `;
      }
    }
    const wgsl = `
struct S {
  x : ${t.params.eleTy},
  y : ${t.params.eleTy},
}

fn foo() {
  var v : ${t.params.eleTy};
  let tmp = ${t.params.compTy}(${compParams});
}`;

    const shader_error = t.params.stage === 'constant';
    t.expectCompileResult(!shader_error, wgsl);
  });
