import { assert, unreachable } from '../../common/util/util.js';
import {
  getBlockInfoForTextureFormat,
  isDepthOrStencilTextureFormat,
  isDepthStencilTextureFormat,
  isDepthTextureFormat,
  isSintOrUintFormat,
  isStencilTextureFormat,
} from '../format_info.js';
import { GPUTestBase } from '../gpu_test.js';

import { getTextureCopyLayout } from './texture/layout.js';
import { TexelView } from './texture/texel_view.js';
import { reifyExtent3D, reifyOrigin3D } from './unions.js';

// Note: For values that are supposedly unused we use 0.123 as a sentinel for
// float formats and 123 for integer formats. For example, rendering to r8unorm
// returns (v, 9.123, 0.123, 0.123). Since only v should be used this shouldn't
// matter but just in case we set it to 123 so it's more likely to cause an
// issue if something is wrong.
const kLoadValueFromStorageInfo: Partial<{
  [k in GPUTextureFormat]: {
    storageType: string;
    texelType: string;
    unpackWGSL: string;
  };
}> = {
  r8snorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
    return vec4f(unpack4x8snorm(getSrc(byteOffset / 4))[byteOffset % 4], 0.123, 0.123, 0.123)
  `,
  },
  r8unorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
    return vec4f(unpack4x8unorm(getSrc(byteOffset / 4))[byteOffset % 4], 0.123, 0.123, 0.123)
  `,
  },
  r8uint: {
    storageType: 'u32',
    texelType: 'vec4u',
    unpackWGSL: `
    return vec4u(unpack4xU8(getSrc(byteOffset / 4))[byteOffset % 4], 123, 123, 123)
  `,
  },
  r8sint: {
    storageType: 'u32',
    texelType: 'vec4i',
    unpackWGSL: `
    return vec4i(unpack4xI8(getSrc(byteOffset / 4))[byteOffset % 4], 123, 123, 123)
  `,
  },
  rg8snorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
    let v = unpack4x8snorm(getSrc(byteOffset / 4));
    return vec4f(select(v.rg, v.ba, byteOffset % 4 >= 2), 0.123, 0.123)
  `,
  },
  rg8unorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
    let v = unpack4x8unorm(getSrc(byteOffset / 4));
    return vec4f(select(v.rg, v.ba, byteOffset % 4 >= 2), 0.123, 0.123)
  `,
  },
  rg8uint: {
    storageType: 'u32',
    texelType: 'vec4u',
    unpackWGSL: `
    let v = unpack4xU8(getSrc(byteOffset / 4));
    return vec4u(select(v.rg, v.ba, byteOffset % 4 >= 2), 123, 123)
  `,
  },
  rg8sint: {
    storageType: 'u32',
    texelType: 'vec4i',
    unpackWGSL: `
    let v = unpack4xI8(getSrc(byteOffset / 4));
    return vec4i(select(v.rg, v.ba, byteOffset % 4 >= 2), 123, 123)
  `,
  },
  rgba8snorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: 'return unpack4x8snorm(getSrc(byteOffset / 4))',
  },
  rgba8unorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: 'return unpack4x8unorm(getSrc(byteOffset / 4))',
  },
  'rgba8unorm-srgb': {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
      let v = unpack4x8unorm(getSrc(byteOffset / 4));
      let srgb = select(
        v / 12.92,
        pow((v + 0.055) / 1.055, vec4f(2.4)),
        v >= vec4f(0.04045)
      );
      return vec4f(srgb.rgb, v.a);
    `,
  },
  bgra8unorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: 'return unpack4x8unorm(getSrc(byteOffset / 4)).bgra',
  },
  'bgra8unorm-srgb': {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
      let v = unpack4x8unorm(getSrc(byteOffset / 4));
      let srgb = select(
        v / 12.92,
        pow((v + 0.055) / 1.055, vec4f(2.4)),
        v >= vec4f(0.04045)
      );
      return vec4f(srgb.bgr, v.a);
    `,
  },
  rgba8uint: {
    storageType: 'u32',
    texelType: 'vec4u',
    unpackWGSL: 'return unpack4xU8(getSrc(byteOffset / 4))',
  },
  rgba8sint: {
    storageType: 'u32',
    texelType: 'vec4i',
    unpackWGSL: 'return unpack4xI8(getSrc(byteOffset / 4))',
  },
  rg11b10ufloat: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `return unpackRG11B10UFloat(getSrc(byteOffset / 4))`,
  },
  r16float: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL:
      'return vec4f(unpack2x16float(getSrc(byteOffset / 4))[byteOffset % 4 / 2], 0.123, 0.123, 0.123)',
  },
  r16uint: {
    storageType: 'u32',
    texelType: 'vec4u',
    unpackWGSL:
      'return vec4u(extractBits(getSrc(byteOffset / 4), (byteOffset % 4 / 2 * 16), 16), 123, 123, 123)',
  },
  r16sint: {
    storageType: 'i32',
    texelType: 'vec4i',
    unpackWGSL:
      'return vec4i(extractBits(getSrc(byteOffset / 4), byteOffset % 4 / 2 * 16, 16), 123, 123, 123)',
  },
  rg16float: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: 'return vec4f(unpack2x16float(getSrc(byteOffset / 4)), 0.123, 0.123)',
  },
  rg16uint: {
    storageType: 'u32',
    texelType: 'vec4u',
    unpackWGSL: `
      let v = getSrc(byteOffset / 4);
      return vec4u(v & 0xFFFF, v >> 16, 123, 123)
    `,
  },
  rg16sint: {
    storageType: 'i32',
    texelType: 'vec4i',
    unpackWGSL: `
      let v = getSrc(byteOffset / 4);
      return vec4i(
        extractBits(v, 0, 16),
        extractBits(v, 16, 16),
        123, 123)
    `,
  },
  rgba16float: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
      return vec4f(
        unpack2x16float(getSrc(byteOffset / 4)),
        unpack2x16float(getSrc(byteOffset / 4 + 1)))
    `,
  },
  rgba16uint: {
    storageType: 'u32',
    texelType: 'vec4u',
    unpackWGSL: `
      let v0 = getSrc(byteOffset / 4);
      let v1 = getSrc(byteOffset / 4 + 1);
      return vec4u(v0 & 0xFFFF, v0 >> 16, v1 & 0xFFFF, v1 >> 16)
    `,
  },
  rgba16sint: {
    storageType: 'i32',
    texelType: 'vec4i',
    unpackWGSL: `
      let v0 = getSrc(byteOffset / 4);
      let v1 = getSrc(byteOffset / 4 + 1);
      return vec4i(
        extractBits(v0, 0, 16),
        extractBits(v0, 16, 16),
        extractBits(v1, 0, 16),
        extractBits(v1, 16, 16),
      )
    `,
  },
  r16unorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
    let raw = extractBits(getSrc(byteOffset / 4), (byteOffset % 4 / 2) * 16, 16);
    return vec4f(f32(raw) / 65535.0, 0.123, 0.123, 0.123);
  `,
  },
  r16snorm: {
    storageType: 'i32',
    texelType: 'vec4f',
    unpackWGSL: `
    let raw = extractBits(getSrc(byteOffset / 4), (byteOffset % 4 / 2) * 16, 16);
    let signedVal = i32(raw);
    return vec4f(f32(signedVal) / 32767.0, 0.123, 0.123, 0.123);
  `,
  },
  rg16unorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
    let v = getSrc(byteOffset / 4);
    let r = extractBits(v, 0, 16);
    let g = extractBits(v, 16, 16);
    return vec4f(f32(r) / 65535.0, f32(g) / 65535.0, 0.123, 0.123);
  `,
  },
  rg16snorm: {
    storageType: 'i32',
    texelType: 'vec4f',
    unpackWGSL: `
    let v = getSrc(byteOffset / 4);
    let r = i32(extractBits(v, 0, 16));
    let g = i32(extractBits(v, 16, 16));
    return vec4f(f32(r) / 32767.0, f32(g) / 32767.0, 0.123, 0.123);
  `,
  },
  rgba16unorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
    let v0 = getSrc(byteOffset / 4);
    let v1 = getSrc(byteOffset / 4 + 1);
    let r = extractBits(v0, 0, 16);
    let g = extractBits(v0, 16, 16);
    let b = extractBits(v1, 0, 16);
    let a = extractBits(v1, 16, 16);
    return vec4f(
      f32(r) / 65535.0,
      f32(g) / 65535.0,
      f32(b) / 65535.0,
      f32(a) / 65535.0
    );
  `,
  },
  rgba16snorm: {
    storageType: 'i32',
    texelType: 'vec4f',
    unpackWGSL: `
    let v0 = getSrc(byteOffset / 4);
    let v1 = getSrc(byteOffset / 4 + 1);
    let r = i32(extractBits(v0, 0, 16));
    let g = i32(extractBits(v0, 16, 16));
    let b = i32(extractBits(v1, 0, 16));
    let a = i32(extractBits(v1, 16, 16));
    return vec4f(
      f32(r) / 32767.0,
      f32(g) / 32767.0,
      f32(b) / 32767.0,
      f32(a) / 32767.0
    );
  `,
  },
  r32float: {
    storageType: 'f32',
    texelType: 'vec4f',
    unpackWGSL: 'return vec4f(getSrc(byteOffset / 4), 0.123, 0.123, 0.123)',
  },
  rgb10a2uint: {
    storageType: 'u32',
    texelType: 'vec4u',
    unpackWGSL: `
      let v = getSrc(byteOffset / 4);
      return vec4u(
        extractBits(v, 0, 10),
        extractBits(v, 10, 10),
        extractBits(v, 20, 10),
        extractBits(v, 30, 2),
      )
    `,
  },
  rgb10a2unorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
      let v = getSrc(byteOffset / 4);
      return vec4f(
        f32(extractBits(v, 0, 10)) / f32(0x3FF),
        f32(extractBits(v, 10, 10)) / f32(0x3FF),
        f32(extractBits(v, 20, 10)) / f32(0x3FF),
        f32(extractBits(v, 30, 2)) / f32(0x3),
      )
    `,
  },
  depth16unorm: {
    storageType: 'u32',
    texelType: 'vec4f',
    unpackWGSL: `
      let v = unpack2x16unorm(getSrc(byteOffset / 4))[byteOffset % 4 / 2];
      return vec4f(v, 0.123, 0.123, 0.123)
    `,
  },
  depth32float: {
    storageType: 'f32',
    texelType: 'vec4f',
    unpackWGSL: `
      let v = getSrc(byteOffset / 4);
      return vec4f(v, 0.123, 0.123, 0.123)
    `,
  },
  stencil8: {
    storageType: 'u32',
    texelType: 'vec4u',
    unpackWGSL: `
      return vec4u(unpack4xU8(getSrc(byteOffset / 4))[byteOffset % 4], 123, 123, 123)
    `,
  },
};

function getDepthStencilOptionsForFormat(
  format: GPUTextureFormat,
  aspect: GPUTextureAspect | undefined
) {
  return {
    useFragDepth:
      isDepthTextureFormat(format) && (!aspect || aspect === 'all' || aspect === 'depth-only'),
    discardWithStencil:
      isStencilTextureFormat(format) && (!aspect || aspect === 'all' || aspect === 'stencil-only'),
  };
}

function getCopyBufferToTextureViaRenderCode(
  srcFormat: GPUTextureFormat,
  dstFormat: GPUTextureFormat,
  dstAspect: GPUTextureAspect | undefined
) {
  const info = kLoadValueFromStorageInfo[srcFormat];
  assert(!!info);
  const { storageType, texelType, unpackWGSL } = info;
  const { useFragDepth, discardWithStencil } = getDepthStencilOptionsForFormat(
    dstFormat,
    dstAspect
  );
  assert(!useFragDepth || !discardWithStencil, 'can not do both aspects at once');

  const [depthDecl, depthCode] = useFragDepth
    ? ['@builtin(frag_depth) d: f32,', 'fs.d = fs.v[0];']
    : ['', ''];

  const stencilCode = discardWithStencil ? 'if ((fs.v.r & vin.stencilMask) == 0) { discard; }' : '';

  const code = `
    struct Uniforms {
      numTexelRows: u32,
      bytesPerRow: u32,
      bytesPerSample: u32,
      sampleCount: u32,
      offset: u32,
    };

    struct VSOutput {
      @builtin(position) pos: vec4f,
      @location(0) @interpolate(flat, either) sampleIndex: u32,
      @location(1) @interpolate(flat, either) stencilMask: u32,
    };

    @vertex fn vs(@builtin(vertex_index) vNdx: u32, @builtin(instance_index) iNdx: u32) -> VSOutput {
      let points = array(
        vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 1),
      );
      let sampleRow = vNdx / 4;
      let numSampleRows = f32(uni.numTexelRows * uni.sampleCount);
      let rowOffset = f32(sampleRow) / numSampleRows;
      let rowMult = 1.0 / numSampleRows;
      let p = (points[vNdx % 4] * vec2f(1, rowMult) + vec2f(0, rowOffset)) * 2.0 - 1.0;
      return VSOutput(
        vec4f(p, 0, 1),
        uni.sampleCount - sampleRow % uni.sampleCount - 1,
        1u << iNdx);
    }

    @group(0) @binding(0) var<uniform> uni: Uniforms;
    @group(0) @binding(1) var src: texture_2d<${storageType}>;

    // get a u32/i32/f32 from a r32uint/r32sint/r32float as though it was 1d array
    fn getSrc(offset: u32) -> ${storageType} {
      let width = textureDimensions(src, 0).x;
      let x = offset % width;
      let y = offset / width;
      return textureLoad(src, vec2u(x, y), 0).r;
    }

    const kFloat32FormatMantissaBits = 23;
    const kFloat32FormatBias = 127;
    fn floatBitsToNumber(
        rawBits: u32,
        bitOffset: u32,
        exponentBits: u32,
        mantissaBits: u32,
        bias: u32,
        signed: bool) -> f32 {
      let nonSignBits = exponentBits + mantissaBits;
      let allBits = nonSignBits + select(0u, 1u, signed);
      let allMask = (1u << allBits) - 1u;
      let bits = (rawBits >> bitOffset) & allMask;
      let nonSignBitsMask = (1u << nonSignBits) - 1u;
      let exponentAndMantissaBits = bits & nonSignBitsMask;
      let exponentMask = ((1u << exponentBits) - 1u) << mantissaBits;
      let infinityOrNaN = (bits & exponentMask) == exponentMask;
      if (infinityOrNaN) {
        let mantissaMask = (1u << mantissaBits) - 1;
        let signBit = 1u << nonSignBits;
        let isNegative = (bits & signBit) != 0;
        if ((bits & mantissaMask) != 0u) {
          return 0.0; // NaN (does not exist in WGSL)
        }
        if (isNegative) {
          return f32(-2e38); // NEGATIVE_INFINITY (does not exist in WGSL)
        } else {
          return f32(2e38); // POSITIVE_INFINITY (does not exist in WGSL)
        }
      }
      var f32BitsWithWrongBias =
        exponentAndMantissaBits << (kFloat32FormatMantissaBits - mantissaBits);
      // add in the sign
      f32BitsWithWrongBias |= (bits << (31u - nonSignBits)) & 0x80000000u;
      let numberWithWrongBias = bitcast<f32>(f32BitsWithWrongBias);
      return numberWithWrongBias * pow(2.0f, f32(kFloat32FormatBias - bias));
    }

    fn unpackRG11B10UFloat(v: u32) -> vec4f {
      return vec4f(
        floatBitsToNumber(v,  0, 5, 6, 15, false),
        floatBitsToNumber(v, 11, 5, 6, 15, false),
        floatBitsToNumber(v, 22, 5, 5, 15, false),
        1
      );
    }

    fn unpack(byteOffset: u32) -> ${texelType} {
      ${unpackWGSL};
    }

    struct FSOutput {
      @location(0) v: ${texelType},
      ${depthDecl}
    };

    @fragment fn fs(vin: VSOutput) -> FSOutput {
      let coord = vec2u(vin.pos.xy);
      let byteOffset =
        uni.offset +
        coord.y * uni.bytesPerRow +
        (coord.x * uni.sampleCount + vin.sampleIndex) * uni.bytesPerSample;
      var fs: FSOutput;
      fs.v = unpack(byteOffset);
      ${depthCode}
      ${stencilCode}
      return fs;
    }
    `;

  let dataFormat: GPUTextureFormat;
  switch (storageType) {
    case 'f32':
      dataFormat = 'r32float';
      break;
    case 'i32':
      dataFormat = 'r32sint';
      break;
    case 'u32':
      dataFormat = 'r32uint';
      break;
    default:
      unreachable();
  }
  return { code, dataFormat };
}

const s_copyBufferToTextureViaRenderPipelines = new WeakMap<
  GPUDevice,
  Map<string, GPURenderPipeline>
>();

// This function emulates copyBufferToTexture by by rendering into the texture.
// This is for formats that can't be copied to directly. depth textures, stencil
// textures, multisampled textures.
//
// For source data it creates an r32uint/r32sint/r32float texture
// and copies the source buffer into it and then reads the texture
// as a 1d array. It does this because compat mode might not have
// storage buffers in fragment shaders.
function copyBufferToTextureViaRender(
  t: GPUTestBase,
  encoder: GPUCommandEncoder,
  source: GPUTexelCopyBufferInfo,
  sourceFormat: GPUTextureFormat,
  dest: GPUTexelCopyTextureInfo,
  size: GPUExtent3D
) {
  const { format: textureFormat, sampleCount } = dest.texture;
  const origin = reifyOrigin3D(dest.origin ?? [0]);
  const copySize = reifyExtent3D(size);
  const { useFragDepth, discardWithStencil } = getDepthStencilOptionsForFormat(
    dest.texture.format,
    dest.aspect
  );
  const resourcesToDestroy: (GPUTexture | GPUBuffer)[] = [];

  const { device } = t;
  const numBlits = discardWithStencil ? 8 : 1;
  for (let blitCount = 0; blitCount < numBlits; ++blitCount) {
    const { code, dataFormat } = getCopyBufferToTextureViaRenderCode(
      sourceFormat,
      dest.texture.format,
      dest.aspect
    );
    const stencilWriteMask = 1 << blitCount;
    const id = JSON.stringify({
      textureFormat,
      sourceFormat,
      useFragDepth,
      stencilWriteMask,
      discardWithStencil,
      sampleCount,
      code,
    });
    const pipelines =
      s_copyBufferToTextureViaRenderPipelines.get(device) ?? new Map<string, GPURenderPipeline>();
    s_copyBufferToTextureViaRenderPipelines.set(device, pipelines);
    let pipeline = pipelines.get(id);
    if (!pipeline) {
      const module = device.createShaderModule({ code });
      pipeline = device.createRenderPipeline({
        label: `blitCopyFor-${textureFormat}`,
        layout: 'auto',
        vertex: { module },
        ...(discardWithStencil
          ? {
              fragment: {
                module,
                targets: [],
              },
              depthStencil: {
                depthWriteEnabled: false,
                depthCompare: 'always',
                format: textureFormat,
                stencilWriteMask,
                stencilFront: {
                  passOp: 'replace',
                },
              },
            }
          : useFragDepth
          ? {
              fragment: {
                module,
                targets: [],
              },
              depthStencil: {
                depthWriteEnabled: true,
                depthCompare: 'always',
                format: textureFormat,
              },
            }
          : {
              fragment: {
                module,
                targets: [{ format: textureFormat }],
              },
            }),
        primitive: {
          topology: 'triangle-strip',
        },
        ...(sampleCount > 1 && { multisample: { count: sampleCount } }),
      });
      pipelines.set(id, pipeline);
    }

    const width = 1024;
    const bytesPerRow = width * 4;
    const fullRows = Math.floor(source.buffer.size / bytesPerRow);
    const rows = Math.ceil(source.buffer.size / bytesPerRow);
    const srcTexture = t.createTextureTracked({
      format: dataFormat,
      size: [width, rows],
      usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
    });
    resourcesToDestroy.push(srcTexture);

    if (fullRows > 0) {
      encoder.copyBufferToTexture({ buffer: source.buffer, bytesPerRow }, { texture: srcTexture }, [
        width,
        fullRows,
      ]);
    }
    if (rows > fullRows) {
      const totalPixels = source.buffer.size / 4;
      const pixelsCopied = fullRows * width;
      const pixelsInLastRow = totalPixels - pixelsCopied;
      encoder.copyBufferToTexture(
        {
          buffer: source.buffer,
          offset: pixelsCopied * 4,
          bytesPerRow,
        },
        {
          texture: srcTexture,
          origin: [0, fullRows],
        },
        [pixelsInLastRow, 1]
      );
    }
    const baseMipLevel = dest.mipLevel;
    for (let l = 0; l < copySize.depthOrArrayLayers; ++l) {
      const baseArrayLayer = origin.z + l;
      const mipLevelCount = 1;
      const arrayLayerCount = 1;
      const pass = encoder.beginRenderPass(
        discardWithStencil
          ? {
              colorAttachments: [],
              depthStencilAttachment: {
                view: dest.texture.createView({
                  baseMipLevel,
                  baseArrayLayer,
                  mipLevelCount,
                  arrayLayerCount,
                }),
                depthReadOnly: true,
                stencilClearValue: 0,
                stencilLoadOp: 'load',
                stencilStoreOp: 'store',
              },
            }
          : useFragDepth
          ? {
              colorAttachments: [],
              depthStencilAttachment: {
                view: dest.texture.createView({
                  baseMipLevel,
                  baseArrayLayer,
                  mipLevelCount,
                  arrayLayerCount,
                }),
                depthClearValue: 0,
                depthLoadOp: 'clear',
                depthStoreOp: 'store',
                stencilReadOnly: true,
              },
            }
          : {
              colorAttachments: [
                {
                  view: dest.texture.createView({
                    baseMipLevel,
                    baseArrayLayer,
                    mipLevelCount,
                    arrayLayerCount,
                  }),
                  loadOp: 'clear',
                  storeOp: 'store',
                },
              ],
            }
      );
      pass.setViewport(origin.x, origin.y, copySize.width, copySize.height, 0, 1);
      pass.setPipeline(pipeline);

      const info = getBlockInfoForTextureFormat(sourceFormat);
      const offset =
        (source.offset ?? 0) + (source.bytesPerRow ?? 0) * (source.rowsPerImage ?? 0) * l;
      const uniforms = new Uint32Array([
        copySize.height, //  numTexelRows: u32,
        source.bytesPerRow!, //  bytesPerRow: u32,
        info.bytesPerBlock!, //  bytesPerSample: u32,
        dest.texture.sampleCount, //  sampleCount: u32,
        offset, //  offset: u32,
      ]);

      const uniformBuffer = t.makeBufferWithContents(
        uniforms,
        GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM
      );
      resourcesToDestroy.push(uniformBuffer);
      const bindGroup = device.createBindGroup({
        layout: pipeline.getBindGroupLayout(0),
        entries: [
          { binding: 0, resource: { buffer: uniformBuffer } },
          { binding: 1, resource: srcTexture.createView() },
        ],
      });

      pass.setBindGroup(0, bindGroup);
      pass.setStencilReference(0xff);
      pass.draw(4 * copySize.height * dest.texture.sampleCount, 1, 0, blitCount);
      pass.end();
    }
  }

  return resourcesToDestroy;
}

/**
 * Creates a mipmapped texture where each mipmap level's (`i`) content is
 * from `texelViews[i]`.
 */
export function createTextureFromTexelViews(
  t: GPUTestBase,
  texelViews: TexelView[],
  desc: Omit<GPUTextureDescriptor, 'format'> & { format?: GPUTextureFormat }
): GPUTexture {
  // All texel views must be the same format for mipmaps.
  assert(texelViews.length > 0 && texelViews.every(e => e.format === texelViews[0].format));
  const viewsFormat = texelViews[0].format;
  const textureFormat = desc.format ?? viewsFormat;

  // Create the texture and then initialize each mipmap level separately.
  const texture = t.createTextureTracked({
    ...desc,
    format: textureFormat,
    usage: desc.usage | GPUTextureUsage.COPY_DST,
    mipLevelCount: texelViews.length,
  });
  // Note: At the time of this writing there is no such thing as a depth-stencil TexelView
  // so we couldn't have passed in data for "all" aspects. This seems like a code smell issue
  // but it's a big change to fix.
  const aspect = isDepthStencilTextureFormat(textureFormat)
    ? isSintOrUintFormat(viewsFormat)
      ? 'stencil-only'
      : 'depth-only'
    : 'all';
  copyTexelViewsToTexture(t, texture, aspect, texelViews);
  return texture;
}

export function copyTexelViewsToTexture(
  t: GPUTestBase,
  texture: GPUTexture,
  aspect: GPUTextureAspect,
  texelViews: TexelView[]
) {
  const viewsFormat = texelViews[0].format;
  const isTextureFormatDifferentThanTexelViewFormat = texture.format !== viewsFormat;
  const { width, height, depthOrArrayLayers } = texture;

  // Copy the texel view into each mip level layer.
  const commandEncoder = t.device.createCommandEncoder({ label: 'copyTexelViewToTexture' });
  const resourcesToDestroy: (GPUTexture | GPUBuffer)[] = [];
  for (let mipLevel = 0; mipLevel < texelViews.length; mipLevel++) {
    const {
      bytesPerRow,
      rowsPerImage,
      mipSize: [mipWidth, mipHeight, mipDepthOrArray],
    } = getTextureCopyLayout(
      viewsFormat,
      texture.dimension ?? '2d',
      [width, height, depthOrArrayLayers],
      {
        mipLevel,
      }
    );

    // Create a staging buffer to upload the texture mip level contents.
    const stagingBuffer = t.createBufferTracked({
      mappedAtCreation: true,
      size: bytesPerRow * mipHeight * mipDepthOrArray,
      usage: GPUBufferUsage.COPY_SRC,
    });
    resourcesToDestroy.push(stagingBuffer);

    // Write the texels into the staging buffer.
    texelViews[mipLevel].writeTextureData(new Uint8Array(stagingBuffer.getMappedRange()), {
      bytesPerRow,
      rowsPerImage: mipHeight,
      subrectOrigin: [0, 0, 0],
      subrectSize: [mipWidth, mipHeight, mipDepthOrArray],
      sampleCount: texture.sampleCount,
    });
    stagingBuffer.unmap();

    const copyB2TOk =
      !isTextureFormatDifferentThanTexelViewFormat &&
      texture.sampleCount === 1 &&
      !isDepthOrStencilTextureFormat(texture.format);

    if (!copyB2TOk) {
      resourcesToDestroy.push(
        ...copyBufferToTextureViaRender(
          t,
          commandEncoder,
          { buffer: stagingBuffer, bytesPerRow, rowsPerImage },
          viewsFormat,
          { texture, mipLevel, aspect },
          [mipWidth, mipHeight, mipDepthOrArray]
        )
      );
    } else {
      // Copy from the staging buffer into the texture.
      commandEncoder.copyBufferToTexture(
        { buffer: stagingBuffer, bytesPerRow, rowsPerImage },
        { texture, mipLevel, aspect: aspect ?? 'all' },
        [mipWidth, mipHeight, mipDepthOrArray]
      );
    }
  }
  t.device.queue.submit([commandEncoder.finish()]);

  // Cleanup temp buffers and textures.
  resourcesToDestroy.forEach(value => value.destroy());
}
