import { assert, memcpy } from '../../common/util/util.js';
import { RegularTextureFormat } from '../format_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../gpu_test.js';
import * as ttu from '../texture_test_utils.js';
import { reifyExtent3D, reifyOrigin3D } from '../util/unions.js';

import { makeInPlaceColorConversion } from './color_space_conversion.js';
import { TexelView } from './texture/texel_view.js';
import { TexelCompareOptions } from './texture/texture_ok.js';

/**
 * Predefined copy sub rect meta infos.
 */
export const kCopySubrectInfo = [
  {
    srcOrigin: { x: 2, y: 2 },
    dstOrigin: { x: 0, y: 0, z: 0 },
    srcSize: { width: 16, height: 16 },
    dstSize: { width: 4, height: 4 },
    copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
  },
  {
    srcOrigin: { x: 10, y: 2 },
    dstOrigin: { x: 0, y: 0, z: 0 },
    srcSize: { width: 16, height: 16 },
    dstSize: { width: 4, height: 4 },
    copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
  },
  {
    srcOrigin: { x: 2, y: 10 },
    dstOrigin: { x: 0, y: 0, z: 0 },
    srcSize: { width: 16, height: 16 },
    dstSize: { width: 4, height: 4 },
    copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
  },
  {
    srcOrigin: { x: 10, y: 10 },
    dstOrigin: { x: 0, y: 0, z: 0 },
    srcSize: { width: 16, height: 16 },
    dstSize: { width: 4, height: 4 },
    copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
  },
  {
    srcOrigin: { x: 2, y: 2 },
    dstOrigin: { x: 2, y: 2, z: 0 },
    srcSize: { width: 16, height: 16 },
    dstSize: { width: 16, height: 16 },
    copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
  },
  {
    srcOrigin: { x: 10, y: 2 },
    dstOrigin: { x: 2, y: 2, z: 0 },
    srcSize: { width: 16, height: 16 },
    dstSize: { width: 16, height: 16 },
    copyExtent: { width: 4, height: 4, depthOrArrayLayers: 1 },
  },
] as const;

export class TextureUploadingUtils extends AllFeaturesMaxLimitsGPUTest {
  doFlipY(
    sourcePixels: Uint8ClampedArray,
    width: number,
    height: number,
    bytesPerPixel: number
  ): Uint8ClampedArray {
    const dstPixels = new Uint8ClampedArray(width * height * bytesPerPixel);
    for (let i = 0; i < height; ++i) {
      for (let j = 0; j < width; ++j) {
        const srcPixelPos = i * width + j;
        // WebGL readPixel returns pixels from bottom-left origin. Using CopyExternalImageToTexture
        // to copy from WebGL Canvas keeps top-left origin. So the expectation from webgl.readPixel should
        // be flipped.
        const dstPixelPos = (height - i - 1) * width + j;

        memcpy(
          { src: sourcePixels, start: srcPixelPos * bytesPerPixel, length: bytesPerPixel },
          { dst: dstPixels, start: dstPixelPos * bytesPerPixel }
        );
      }
    }

    return dstPixels;
  }

  getExpectedDstPixelsFromSrcPixels({
    srcPixels,
    srcOrigin,
    srcSize,
    dstOrigin,
    dstSize,
    subRectSize,
    format,
    flipSrcBeforeCopy,
    srcDoFlipYDuringCopy,
    conversion,
  }: {
    srcPixels: Uint8ClampedArray;
    srcOrigin: GPUOrigin2D;
    srcSize: GPUExtent3D;
    dstOrigin: GPUOrigin3D;
    dstSize: GPUExtent3D;
    subRectSize: GPUExtent3D;
    format: RegularTextureFormat;
    flipSrcBeforeCopy: boolean;
    srcDoFlipYDuringCopy: boolean;
    conversion: {
      srcPremultiplied: boolean;
      dstPremultiplied: boolean;
      srcColorSpace?: PredefinedColorSpace;
      dstColorSpace?: PredefinedColorSpace;
    };
  }): TexelView {
    const applyConversion = makeInPlaceColorConversion(conversion);

    const reifySrcOrigin = reifyOrigin3D(srcOrigin);
    const reifySrcSize = reifyExtent3D(srcSize);
    const reifyDstOrigin = reifyOrigin3D(dstOrigin);
    const reifyDstSize = reifyExtent3D(dstSize);
    const reifySubRectSize = reifyExtent3D(subRectSize);

    assert(
      reifyDstOrigin.x + reifySubRectSize.width <= reifyDstSize.width &&
        reifyDstOrigin.y + reifySubRectSize.height <= reifyDstSize.height,
      'subrect is out of bounds'
    );

    const divide = 255.0;
    return TexelView.fromTexelsAsColors(
      format,
      coords => {
        assert(
          coords.x >= reifyDstOrigin.x &&
            coords.y >= reifyDstOrigin.y &&
            coords.x < reifyDstOrigin.x + reifySubRectSize.width &&
            coords.y < reifyDstOrigin.y + reifySubRectSize.height &&
            coords.z === 0,
          'out of bounds'
        );
        // Map dst coords to get candidate src pixel position in y.
        let yInSubRect = coords.y - reifyDstOrigin.y;

        // If srcDoFlipYDuringCopy is true, a flipY op has been applied to src during copy.
        // WebGPU spec requires origin option relative to the top-left corner of the source image,
        // increasing downward consistently.
        // https://www.w3.org/TR/webgpu/#dom-gpuimagecopyexternalimage-flipy
        // Flip only happens in copy rect contents and src origin always top-left.
        // Get candidate src pixel position in y by mirroring in copy sub rect.
        if (srcDoFlipYDuringCopy) yInSubRect = reifySubRectSize.height - 1 - yInSubRect;

        let src_y = yInSubRect + reifySrcOrigin.y;

        // Test might generate flipped source based on srcPixels, e.g. Create ImageBitmap based on srcPixels but set orientation to 'flipY'
        // Get candidate src pixel position in y by mirroring in source.
        if (flipSrcBeforeCopy) src_y = reifySrcSize.height - src_y - 1;

        const pixelPos =
          src_y * reifySrcSize.width + (coords.x - reifyDstOrigin.x) + reifySrcOrigin.x;

        const rgba = {
          R: srcPixels[pixelPos * 4] / divide,
          G: srcPixels[pixelPos * 4 + 1] / divide,
          B: srcPixels[pixelPos * 4 + 2] / divide,
          A: srcPixels[pixelPos * 4 + 3] / divide,
        };
        applyConversion(rgba);
        return rgba;
      },
      { clampToFormatRange: true }
    );
  }

  doTestAndCheckResult(
    imageCopyExternalImage: GPUCopyExternalImageSourceInfo,
    dstTextureCopyView: GPUCopyExternalImageDestInfo,
    expTexelView: TexelView,
    copySize: Required<GPUExtent3DDict>,
    texelCompareOptions: TexelCompareOptions
  ): void {
    this.device.queue.copyExternalImageToTexture(
      imageCopyExternalImage,
      dstTextureCopyView,
      copySize
    );

    ttu.expectTexelViewComparisonIsOkInTexture(
      this,
      { texture: dstTextureCopyView.texture, origin: dstTextureCopyView.origin },
      expTexelView,
      copySize,
      texelCompareOptions
    );
  }
}
