import { assert, unreachable } from '../../../common/util/util.js';
import { UncompressedTextureFormat, EncodableTextureFormat } from '../../format_info.js';
import { kValue } from '../constants.js';
import {
  assertInIntegerRange,
  float32ToFloatBits,
  float32ToFloat16Bits,
  floatAsNormalizedInteger,
  gammaCompress,
  gammaDecompress,
  normalizedIntegerAsFloat,
  packRGB9E5UFloat,
  floatBitsToNumber,
  float16BitsToFloat32,
  floatBitsToNormalULPFromZero,
  kFloat32Format,
  kFloat16Format,
  kUFloat9e5Format,
  numberToFloat32Bits,
  float32BitsToNumber,
  numberToFloatBits,
  ufloatM9E5BitsToNumber,
} from '../conversion.js';
import { clamp, signExtend } from '../math.js';

/** A component of a texture format: R, G, B, A, Depth, or Stencil. */
export const enum TexelComponent {
  R = 'R',
  G = 'G',
  B = 'B',
  A = 'A',
  Depth = 'Depth',
  Stencil = 'Stencil',
}

/** Arbitrary data, per component of a texel format. */
export type PerTexelComponent<T> = { [c in TexelComponent]?: T };

/** How a component is encoded in its bit range of a texel format. */
export type ComponentDataType = 'uint' | 'sint' | 'unorm' | 'snorm' | 'float' | 'ufloat' | null;

/**
 * Maps component values to component values
 * @param {PerTexelComponent<number>} components - The input components.
 * @returns {PerTexelComponent<number>} The new output components.
 */
type ComponentMapFn = (components: PerTexelComponent<number>) => PerTexelComponent<number>;

/**
 * Packs component values as an ArrayBuffer
 * @param {PerTexelComponent<number>} components - The input components.
 * @returns {ArrayBuffer} The packed data.
 */
type ComponentPackFn = (components: PerTexelComponent<number>) => ArrayBuffer;

/** Unpacks component values from a Uint8Array */
type ComponentUnpackFn = (data: Uint8Array) => PerTexelComponent<number>;

/**
 * Create a PerTexelComponent object filled with the same value for all components.
 * @param {TexelComponent[]} components - The component names.
 * @param {T} value - The value to assign to each component.
 * @returns {PerTexelComponent<T>}
 */
function makePerTexelComponent<T>(components: TexelComponent[], value: T): PerTexelComponent<T> {
  const values: PerTexelComponent<T> = {};
  for (const c of components) {
    values[c] = value;
  }
  return values;
}

/**
 * Create a function which applies clones a `PerTexelComponent<number>` and then applies the
 * function `fn` to each component of `components`.
 * @param {(value: number) => number} fn - The mapping function to apply to component values.
 * @param {TexelComponent[]} components - The component names.
 * @returns {ComponentMapFn} The map function which clones the input component values, and applies
 *                           `fn` to each of component of `components`.
 */
function applyEach(
  fn: (value: number, component: TexelComponent) => number,
  components: TexelComponent[]
): ComponentMapFn {
  return (values: PerTexelComponent<number>) => {
    values = Object.assign({}, values);
    for (const c of components) {
      assert(values[c] !== undefined);
      values[c] = fn(values[c]!, c);
    }
    return values;
  };
}

/**
 * A `ComponentMapFn` for encoding sRGB.
 * @param {PerTexelComponent<number>} components - The input component values.
 * @returns {TexelComponent<number>} Gamma-compressed copy of `components`.
 */
const encodeSRGB: ComponentMapFn = components => {
  assert(
    components.R !== undefined && components.G !== undefined && components.B !== undefined,
    'sRGB requires all of R, G, and B components'
  );
  return applyEach(gammaCompress, kRGB)(components);
};

/**
 * A `ComponentMapFn` for decoding sRGB.
 * @param {PerTexelComponent<number>} components - The input component values.
 * @returns {TexelComponent<number>} Gamma-decompressed copy of `components`.
 */
const decodeSRGB: ComponentMapFn = components => {
  components = Object.assign({}, components);
  assert(
    components.R !== undefined && components.G !== undefined && components.B !== undefined,
    'sRGB requires all of R, G, and B components'
  );
  return applyEach(gammaDecompress, kRGB)(components);
};

/**
 * Makes a `ComponentMapFn` for clamping values to the specified range.
 */
export function makeClampToRange(format: EncodableTextureFormat): ComponentMapFn {
  const repr = kTexelRepresentationInfo[format];
  assert(repr.numericRange !== null, 'Format has unknown numericRange');
  const perComponentRanges = repr.numericRange as PerComponentNumericRange;
  const range = repr.numericRange as NumericRange;

  return applyEach((x, component) => {
    const perComponentRange = perComponentRanges[component];
    return clamp(x, perComponentRange ? perComponentRange : range);
  }, repr.componentOrder);
}

// MAINTENANCE_TODO: Look into exposing this map to the test fixture so that it can be GCed at the
// end of each test group. That would allow for caching of larger buffers (though it's unclear how
// ofter larger buffers are used by packComponents.)
const smallComponentDataViews = new Map();
function getComponentDataView(byteLength: number): DataView {
  if (byteLength > 32) {
    const buffer = new ArrayBuffer(byteLength);
    return new DataView(buffer);
  }
  let dataView = smallComponentDataViews.get(byteLength);
  if (!dataView) {
    const buffer = new ArrayBuffer(byteLength);
    dataView = new DataView(buffer);
    smallComponentDataViews.set(byteLength, dataView);
  }
  return dataView;
}

/**
 * Helper function to pack components as an ArrayBuffer.
 * @param {TexelComponent[]} componentOrder - The order of the component data.
 * @param {PerTexelComponent<number>} components - The input component values.
 * @param {number | PerTexelComponent<number>} bitLengths - The length in bits of each component.
 *   If a single number, all components are the same length, otherwise this is a dictionary of
 *   per-component bit lengths.
 * @param {ComponentDataType | PerTexelComponent<ComponentDataType>} componentDataTypes -
 *   The type of the data in `components`. If a single value, all components have the same value.
 *   Otherwise, this is a dictionary of per-component data types.
 * @returns {ArrayBuffer} The packed component data.
 */
function packComponents(
  componentOrder: TexelComponent[],
  components: PerTexelComponent<number>,
  bitLengths: number | PerTexelComponent<number>,
  componentDataTypes: ComponentDataType | PerTexelComponent<ComponentDataType>
): ArrayBuffer {
  let bitLengthMap;
  let totalBitLength;
  if (typeof bitLengths === 'number') {
    bitLengthMap = makePerTexelComponent(componentOrder, bitLengths);
    totalBitLength = bitLengths * componentOrder.length;
  } else {
    bitLengthMap = bitLengths;
    totalBitLength = Object.entries(bitLengthMap).reduce((acc, [, value]) => {
      assert(value !== undefined);
      return acc + value;
    }, 0);
  }
  assert(totalBitLength % 8 === 0);

  const componentDataTypeMap =
    typeof componentDataTypes === 'string' || componentDataTypes === null
      ? makePerTexelComponent(componentOrder, componentDataTypes)
      : componentDataTypes;

  const dataView = getComponentDataView(totalBitLength / 8);
  let bitOffset = 0;
  for (const c of componentOrder) {
    const value = components[c];
    const type = componentDataTypeMap[c];
    const bitLength = bitLengthMap[c];
    assert(value !== undefined);
    assert(type !== undefined);
    assert(bitLength !== undefined);

    const byteOffset = Math.floor(bitOffset / 8);
    const byteLength = Math.ceil(bitLength / 8);
    switch (type) {
      case 'uint':
      case 'unorm':
        if (byteOffset === bitOffset / 8 && byteLength === bitLength / 8) {
          switch (byteLength) {
            case 1:
              dataView.setUint8(byteOffset, value);
              break;
            case 2:
              dataView.setUint16(byteOffset, value, true);
              break;
            case 4:
              dataView.setUint32(byteOffset, value, true);
              break;
            default:
              unreachable();
          }
        } else {
          // Packed representations are all 32-bit and use Uint as the data type.
          // ex.) rg10b11float, rgb10a2unorm
          switch (dataView.byteLength) {
            case 4: {
              const currentValue = dataView.getUint32(0, true);

              let mask = 0xffffffff;
              const bitsToClearRight = bitOffset;
              const bitsToClearLeft = 32 - (bitLength + bitOffset);

              mask = (mask >>> bitsToClearRight) << bitsToClearRight;
              mask = (mask << bitsToClearLeft) >>> bitsToClearLeft;

              const newValue = (currentValue & ~mask) | (value << bitOffset);

              dataView.setUint32(0, newValue, true);
              break;
            }
            default:
              unreachable();
          }
        }
        break;
      case 'sint':
      case 'snorm':
        assert(byteOffset === bitOffset / 8 && byteLength === bitLength / 8);
        switch (byteLength) {
          case 1:
            dataView.setInt8(byteOffset, value);
            break;
          case 2:
            dataView.setInt16(byteOffset, value, true);
            break;
          case 4:
            dataView.setInt32(byteOffset, value, true);
            break;
          default:
            unreachable();
        }
        break;
      case 'float':
        assert(byteOffset === bitOffset / 8 && byteLength === bitLength / 8);
        switch (byteLength) {
          case 4:
            dataView.setFloat32(byteOffset, value, true);
            break;
          default:
            unreachable();
        }
        break;
      case 'ufloat':
      case null:
        unreachable();
    }

    bitOffset += bitLength;
  }

  return dataView.buffer;
}

/**
 * Unpack substrings of bits from a Uint8Array, e.g. [8,8,8,8] or [9,9,9,5].
 */
function unpackComponentsBits(
  componentOrder: TexelComponent[],
  byteView: Uint8Array,
  bitLengths: number | PerTexelComponent<number>
): PerTexelComponent<number> {
  const components = makePerTexelComponent(componentOrder, 0);

  let bitLengthMap;
  let totalBitLength;
  if (typeof bitLengths === 'number') {
    let index = 0;
    // Optimized cases for when the bit lengths are all a well aligned value.
    switch (bitLengths) {
      case 8:
        for (const c of componentOrder) {
          components[c] = byteView[index++];
        }
        return components;
      case 16: {
        const shortView = new Uint16Array(byteView.buffer, byteView.byteOffset);
        for (const c of componentOrder) {
          components[c] = shortView[index++];
        }
        return components;
      }
      case 32: {
        const longView = new Uint32Array(byteView.buffer, byteView.byteOffset);
        for (const c of componentOrder) {
          components[c] = longView[index++];
        }
        return components;
      }
    }

    bitLengthMap = makePerTexelComponent(componentOrder, bitLengths);
    totalBitLength = bitLengths * componentOrder.length;
  } else {
    bitLengthMap = bitLengths;
    totalBitLength = Object.entries(bitLengthMap).reduce((acc, [, value]) => {
      assert(value !== undefined);
      return acc + value;
    }, 0);
  }

  assert(totalBitLength % 8 === 0);

  const dataView = new DataView(byteView.buffer, byteView.byteOffset, byteView.byteLength);
  let bitOffset = 0;
  for (const c of componentOrder) {
    const bitLength = bitLengthMap[c];
    assert(bitLength !== undefined);

    let value: number;

    const byteOffset = Math.floor(bitOffset / 8);
    const byteLength = Math.ceil(bitLength / 8);
    if (byteOffset === bitOffset / 8 && byteLength === bitLength / 8) {
      switch (byteLength) {
        case 1:
          value = dataView.getUint8(byteOffset);
          break;
        case 2:
          value = dataView.getUint16(byteOffset, true);
          break;
        case 4:
          value = dataView.getUint32(byteOffset, true);
          break;
        default:
          unreachable();
      }
    } else {
      // Packed representations are all 32-bit and use Uint as the data type.
      // ex.) rg10b11float, rgb10a2unorm
      assert(dataView.byteLength === 4);
      const word = dataView.getUint32(0, true);
      value = (word >>> bitOffset) & ((1 << bitLength) - 1);
    }

    bitOffset += bitLength;
    components[c] = value;
  }

  return components;
}

/**
 * Create an entry in `kTexelRepresentationInfo` for normalized integer texel data with constant
 * bitlength.
 * @param {TexelComponent[]} componentOrder - The order of the component data.
 * @param {number} bitLength - The number of bits in each component.
 * @param {{signed: boolean; sRGB: boolean}} opt - Boolean flags for `signed` and `sRGB`.
 */
function makeNormalizedInfo(
  componentOrder: TexelComponent[],
  bitLength: number,
  opt: { signed: boolean; sRGB: boolean }
): TexelRepresentationInfo {
  const encodeNonSRGB = applyEach(
    (n: number) => floatAsNormalizedInteger(n, bitLength, opt.signed),
    componentOrder
  );
  const decodeNonSRGB = applyEach(
    (n: number) => normalizedIntegerAsFloat(n, bitLength, opt.signed),
    componentOrder
  );

  const numberToBitsNonSRGB = applyEach(
    n => floatAsNormalizedInteger(n, bitLength, opt.signed),
    componentOrder
  );
  let bitsToNumberNonSRGB: ComponentMapFn;
  if (opt.signed) {
    bitsToNumberNonSRGB = applyEach(
      n => normalizedIntegerAsFloat(signExtend(n, bitLength), bitLength, opt.signed),
      componentOrder
    );
  } else {
    bitsToNumberNonSRGB = applyEach(
      n => normalizedIntegerAsFloat(n, bitLength, opt.signed),
      componentOrder
    );
  }

  let encode: ComponentMapFn;
  let decode: ComponentMapFn;
  let numberToBits: ComponentMapFn;
  let bitsToNumber: ComponentMapFn;
  if (opt.sRGB) {
    encode = components => encodeNonSRGB(encodeSRGB(components));
    decode = components => decodeSRGB(decodeNonSRGB(components));
    numberToBits = components => numberToBitsNonSRGB(encodeSRGB(components));
    bitsToNumber = components => decodeSRGB(bitsToNumberNonSRGB(components));
  } else {
    encode = encodeNonSRGB;
    decode = decodeNonSRGB;
    numberToBits = numberToBitsNonSRGB;
    bitsToNumber = bitsToNumberNonSRGB;
  }

  let bitsToULPFromZero: ComponentMapFn;
  if (opt.signed) {
    const maxValue = (1 << (bitLength - 1)) - 1; // e.g. 127 for snorm8
    bitsToULPFromZero = applyEach(
      n => Math.max(-maxValue, signExtend(n, bitLength)),
      componentOrder
    );
  } else {
    bitsToULPFromZero = components => components;
  }

  const dataType: ComponentDataType = opt.signed ? 'snorm' : 'unorm';
  const min = opt.signed ? -1 : 0;
  const max = 1;
  return {
    componentOrder,
    componentInfo: makePerTexelComponent(componentOrder, {
      dataType,
      bitLength,
    }),
    encode,
    decode,
    pack: (components: PerTexelComponent<number>) =>
      packComponents(componentOrder, components, bitLength, dataType),
    unpackBits: (data: Uint8Array) => unpackComponentsBits(componentOrder, data, bitLength),
    numberToBits,
    bitsToNumber,
    bitsToULPFromZero,
    numericRange: { min, max, finiteMin: min, finiteMax: max },
  };
}

/**
 * Create an entry in `kTexelRepresentationInfo` for integer texel data with constant bitlength.
 * @param {TexelComponent[]} componentOrder - The order of the component data.
 * @param {number} bitLength - The number of bits in each component.
 * @param {{signed: boolean}} opt - Boolean flag for `signed`.
 */
function makeIntegerInfo(
  componentOrder: TexelComponent[],
  bitLength: number,
  opt: { signed: boolean }
): TexelRepresentationInfo {
  assert(bitLength <= 32);
  const min = opt.signed ? -(2 ** (bitLength - 1)) : 0;
  const max = opt.signed ? 2 ** (bitLength - 1) - 1 : 2 ** bitLength - 1;
  const numericRange = { min, max, finiteMin: min, finiteMax: max };
  const maxUnsignedValue = 2 ** bitLength;
  const encode = applyEach(
    (n: number) => (assertInIntegerRange(n, bitLength, opt.signed), n),
    componentOrder
  );
  const decode = applyEach(
    (n: number) => (assertInIntegerRange(n, bitLength, opt.signed), n),
    componentOrder
  );
  const bitsToNumber = applyEach((n: number) => {
    const decodedN = opt.signed ? (n > numericRange.max ? n - maxUnsignedValue : n) : n;
    assertInIntegerRange(decodedN, bitLength, opt.signed);
    return decodedN;
  }, componentOrder);

  let bitsToULPFromZero: ComponentMapFn;
  if (opt.signed) {
    bitsToULPFromZero = applyEach(n => signExtend(n, bitLength), componentOrder);
  } else {
    bitsToULPFromZero = components => components;
  }

  const dataType: ComponentDataType = opt.signed ? 'sint' : 'uint';
  const bitMask = (1 << bitLength) - 1;
  return {
    componentOrder,
    componentInfo: makePerTexelComponent(componentOrder, {
      dataType,
      bitLength,
    }),
    encode,
    decode,
    pack: (components: PerTexelComponent<number>) =>
      packComponents(componentOrder, components, bitLength, dataType),
    unpackBits: (data: Uint8Array) => unpackComponentsBits(componentOrder, data, bitLength),
    numberToBits: applyEach(v => v & bitMask, componentOrder),
    bitsToNumber,
    bitsToULPFromZero,
    numericRange,
  };
}

/**
 * Create an entry in `kTexelRepresentationInfo` for floating point texel data with constant
 * bitlength.
 * @param {TexelComponent[]} componentOrder - The order of the component data.
 * @param {number} bitLength - The number of bits in each component.
 */
function makeFloatInfo(
  componentOrder: TexelComponent[],
  bitLength: number,
  { restrictedDepth = false }: { restrictedDepth?: boolean } = {}
): TexelRepresentationInfo {
  let encode: ComponentMapFn;
  let numberToBits;
  let bitsToNumber;
  let bitsToULPFromZero;
  switch (bitLength) {
    case 32:
      if (restrictedDepth) {
        encode = applyEach(v => {
          assert(v >= 0.0 && v <= 1.0, 'depth out of range');
          return new Float32Array([v])[0];
        }, componentOrder);
      } else {
        encode = applyEach(v => new Float32Array([v])[0], componentOrder);
      }
      numberToBits = applyEach(numberToFloat32Bits, componentOrder);
      bitsToNumber = applyEach(float32BitsToNumber, componentOrder);
      bitsToULPFromZero = applyEach(
        v => floatBitsToNormalULPFromZero(v, kFloat32Format),
        componentOrder
      );
      break;
    case 16:
      if (restrictedDepth) {
        encode = applyEach(v => {
          assert(v >= 0.0 && v <= 1.0, 'depth out of range');
          return float16BitsToFloat32(float32ToFloat16Bits(v));
        }, componentOrder);
      } else {
        encode = applyEach(v => float16BitsToFloat32(float32ToFloat16Bits(v)), componentOrder);
      }
      numberToBits = applyEach(float32ToFloat16Bits, componentOrder);
      bitsToNumber = applyEach(float16BitsToFloat32, componentOrder);
      bitsToULPFromZero = applyEach(
        v => floatBitsToNormalULPFromZero(v, kFloat16Format),
        componentOrder
      );
      break;
    default:
      unreachable();
  }
  const decode = applyEach(identity, componentOrder);

  return {
    componentOrder,
    componentInfo: makePerTexelComponent(componentOrder, {
      dataType: 'float' as const,
      bitLength,
    }),
    encode,
    decode,
    pack: (components: PerTexelComponent<number>) => {
      switch (bitLength) {
        case 16:
          components = applyEach(float32ToFloat16Bits, componentOrder)(components);
          return packComponents(componentOrder, components, 16, 'uint');
        case 32:
          return packComponents(componentOrder, components, bitLength, 'float');
        default:
          unreachable();
      }
    },
    unpackBits: (data: Uint8Array) => unpackComponentsBits(componentOrder, data, bitLength),
    numberToBits,
    bitsToNumber,
    bitsToULPFromZero,
    numericRange: restrictedDepth
      ? { min: 0, max: 1, finiteMin: 0, finiteMax: 1 }
      : {
          min: Number.NEGATIVE_INFINITY,
          max: Number.POSITIVE_INFINITY,
          finiteMin: bitLength === 32 ? kValue.f32.negative.min : kValue.f16.negative.min,
          finiteMax: bitLength === 32 ? kValue.f32.positive.max : kValue.f16.positive.max,
        },
  };
}

const kR = [TexelComponent.R];
const kRG = [TexelComponent.R, TexelComponent.G];
const kRGB = [TexelComponent.R, TexelComponent.G, TexelComponent.B];
const kRGBA = [TexelComponent.R, TexelComponent.G, TexelComponent.B, TexelComponent.A];
const kBGRA = [TexelComponent.B, TexelComponent.G, TexelComponent.R, TexelComponent.A];

const identity = (n: number) => n;

const kFloat11Format = { signed: 0, exponentBits: 5, mantissaBits: 6, bias: 15 } as const;
const kFloat10Format = { signed: 0, exponentBits: 5, mantissaBits: 5, bias: 15 } as const;

export type PerComponentFiniteMax = Record<TexelComponent, number>;
export type NumericRange = {
  min: number;
  max: number;
  finiteMin: number;
  finiteMax: number | PerComponentFiniteMax;
};
export type PerComponentNumericRange = Partial<
  Record<
    TexelComponent,
    {
      min: number;
      max: number;
      finiteMin: number;
      finiteMax: number;
    }
  >
>;
export type TexelRepresentationInfo = {
  /** Order of components in the packed representation. */
  readonly componentOrder: TexelComponent[];
  /** Data type and bit length of each component in the format. */
  readonly componentInfo: PerTexelComponent<{
    dataType: ComponentDataType;
    bitLength: number;
  }>;
  /** Encode shader values into their data representation. ex.) float 1.0 -> unorm8 255 */
  // MAINTENANCE_TODO: Replace with numberToBits?
  readonly encode: ComponentMapFn;
  /** Decode the data representation into the shader values. ex.) unorm8 255 -> float 1.0 */
  // MAINTENANCE_TODO: Replace with bitsToNumber?
  readonly decode: ComponentMapFn;
  /** Pack texel component values into an ArrayBuffer. ex.) rg8unorm `{r: 0, g: 255}` -> 0xFF00 */
  // MAINTENANCE_TODO: Replace with packBits?
  readonly pack: ComponentPackFn;

  /** Convert integer bit representations into numeric values, e.g. unorm8 255 -> numeric 1.0 */
  readonly bitsToNumber: ComponentMapFn;
  /** Convert numeric values into integer bit representations, e.g. numeric 1.0 -> unorm8 255 */
  readonly numberToBits: ComponentMapFn;
  /** Unpack integer bit representations from an ArrayBuffer, e.g. 0xFF00 -> rg8unorm [0,255] */
  readonly unpackBits: ComponentUnpackFn;
  /** Convert integer bit representations into ULPs-from-zero, e.g. unorm8 255 -> 255 ULPs */
  readonly bitsToULPFromZero: ComponentMapFn;
  /** The valid range of numeric "color" values, e.g. [0, Infinity] for ufloat. */
  readonly numericRange: null | NumericRange | PerComponentNumericRange;

  // Add fields as needed
};

export const kTexelRepresentationInfo: {
  readonly [k in UncompressedTextureFormat]: TexelRepresentationInfo;
} = {
  .../* prettier-ignore */ {
    'r8unorm':               makeNormalizedInfo(   kR,  8, { signed: false, sRGB: false }),
    'r8snorm':               makeNormalizedInfo(   kR,  8, { signed:  true, sRGB: false }),
    'r8uint':                makeIntegerInfo(      kR,  8, { signed: false }),
    'r8sint':                makeIntegerInfo(      kR,  8, { signed:  true }),
    'r16unorm':              makeNormalizedInfo(   kR, 16, { signed: false, sRGB: false }),
    'r16snorm':              makeNormalizedInfo(   kR, 16, { signed:  true, sRGB: false }),
    'r16uint':               makeIntegerInfo(      kR, 16, { signed: false }),
    'r16sint':               makeIntegerInfo(      kR, 16, { signed:  true }),
    'r16float':              makeFloatInfo(        kR, 16),
    'rg8unorm':              makeNormalizedInfo(  kRG,  8, { signed: false, sRGB: false }),
    'rg8snorm':              makeNormalizedInfo(  kRG,  8, { signed:  true, sRGB: false }),
    'rg8uint':               makeIntegerInfo(     kRG,  8, { signed: false }),
    'rg8sint':               makeIntegerInfo(     kRG,  8, { signed:  true }),
    'r32uint':               makeIntegerInfo(      kR, 32, { signed: false }),
    'r32sint':               makeIntegerInfo(      kR, 32, { signed:  true }),
    'r32float':              makeFloatInfo(        kR, 32),
    'rg16unorm':             makeNormalizedInfo(  kRG, 16, { signed: false, sRGB: false }),
    'rg16snorm':             makeNormalizedInfo(  kRG, 16, { signed:  true, sRGB: false }),
    'rg16uint':              makeIntegerInfo(     kRG, 16, { signed: false }),
    'rg16sint':              makeIntegerInfo(     kRG, 16, { signed:  true }),
    'rg16float':             makeFloatInfo(       kRG, 16),
    'rgba8unorm':            makeNormalizedInfo(kRGBA,  8, { signed: false, sRGB: false }),
    'rgba8unorm-srgb':       makeNormalizedInfo(kRGBA,  8, { signed: false, sRGB:  true }),
    'rgba8snorm':            makeNormalizedInfo(kRGBA,  8, { signed:  true, sRGB: false }),
    'rgba8uint':             makeIntegerInfo(   kRGBA,  8, { signed: false }),
    'rgba8sint':             makeIntegerInfo(   kRGBA,  8, { signed:  true }),
    'bgra8unorm':            makeNormalizedInfo(kBGRA,  8, { signed: false, sRGB: false }),
    'bgra8unorm-srgb':       makeNormalizedInfo(kBGRA,  8, { signed: false, sRGB:  true }),
    'rg32uint':              makeIntegerInfo(     kRG, 32, { signed: false }),
    'rg32sint':              makeIntegerInfo(     kRG, 32, { signed:  true }),
    'rg32float':             makeFloatInfo(       kRG, 32),
    'rgba16unorm':           makeNormalizedInfo(kRGBA, 16, { signed: false, sRGB: false }),
    'rgba16snorm':           makeNormalizedInfo(kRGBA, 16, { signed:  true, sRGB: false }),
    'rgba16uint':            makeIntegerInfo(   kRGBA, 16, { signed: false }),
    'rgba16sint':            makeIntegerInfo(   kRGBA, 16, { signed:  true }),
    'rgba16float':           makeFloatInfo(     kRGBA, 16),
    'rgba32uint':            makeIntegerInfo(   kRGBA, 32, { signed: false }),
    'rgba32sint':            makeIntegerInfo(   kRGBA, 32, { signed:  true }),
    'rgba32float':           makeFloatInfo(     kRGBA, 32),
  },
  ...{
    rgb10a2uint: {
      componentOrder: kRGBA,
      componentInfo: {
        R: { dataType: 'uint', bitLength: 10 },
        G: { dataType: 'uint', bitLength: 10 },
        B: { dataType: 'uint', bitLength: 10 },
        A: { dataType: 'uint', bitLength: 2 },
      },
      encode: components => {
        assertInIntegerRange(components.R!, 10, false);
        assertInIntegerRange(components.G!, 10, false);
        assertInIntegerRange(components.B!, 10, false);
        assertInIntegerRange(components.A!, 2, false);
        return components;
      },
      decode: components => {
        assertInIntegerRange(components.R!, 10, false);
        assertInIntegerRange(components.G!, 10, false);
        assertInIntegerRange(components.B!, 10, false);
        assertInIntegerRange(components.A!, 2, false);
        return components;
      },
      pack: components =>
        packComponents(
          kRGBA,
          components,
          {
            R: 10,
            G: 10,
            B: 10,
            A: 2,
          },
          'uint'
        ),
      unpackBits: (data: Uint8Array) =>
        unpackComponentsBits(kRGBA, data, { R: 10, G: 10, B: 10, A: 2 }),
      numberToBits: components => ({
        R: components.R! & 0x3ff,
        G: components.G! & 0x3ff,
        B: components.B! & 0x3ff,
        A: components.A! & 0x3,
      }),
      bitsToNumber: components => {
        assertInIntegerRange(components.R!, 10, false);
        assertInIntegerRange(components.G!, 10, false);
        assertInIntegerRange(components.B!, 10, false);
        assertInIntegerRange(components.A!, 2, false);
        return components;
      },
      bitsToULPFromZero: components => components,
      numericRange: {
        R: { min: 0, max: 0x3ff, finiteMin: 0, finiteMax: 0x3ff },
        G: { min: 0, max: 0x3ff, finiteMin: 0, finiteMax: 0x3ff },
        B: { min: 0, max: 0x3ff, finiteMin: 0, finiteMax: 0x3ff },
        A: { min: 0, max: 0x3, finiteMin: 0, finiteMax: 0x3 },
      },
    },
    rgb10a2unorm: {
      componentOrder: kRGBA,
      componentInfo: {
        R: { dataType: 'unorm', bitLength: 10 },
        G: { dataType: 'unorm', bitLength: 10 },
        B: { dataType: 'unorm', bitLength: 10 },
        A: { dataType: 'unorm', bitLength: 2 },
      },
      encode: components => {
        return {
          R: floatAsNormalizedInteger(components.R ?? unreachable(), 10, false),
          G: floatAsNormalizedInteger(components.G ?? unreachable(), 10, false),
          B: floatAsNormalizedInteger(components.B ?? unreachable(), 10, false),
          A: floatAsNormalizedInteger(components.A ?? unreachable(), 2, false),
        };
      },
      decode: components => {
        return {
          R: normalizedIntegerAsFloat(components.R ?? unreachable(), 10, false),
          G: normalizedIntegerAsFloat(components.G ?? unreachable(), 10, false),
          B: normalizedIntegerAsFloat(components.B ?? unreachable(), 10, false),
          A: normalizedIntegerAsFloat(components.A ?? unreachable(), 2, false),
        };
      },
      pack: components =>
        packComponents(
          kRGBA,
          components,
          {
            R: 10,
            G: 10,
            B: 10,
            A: 2,
          },
          'uint'
        ),
      unpackBits: (data: Uint8Array) =>
        unpackComponentsBits(kRGBA, data, { R: 10, G: 10, B: 10, A: 2 }),
      numberToBits: components => ({
        R: floatAsNormalizedInteger(components.R ?? unreachable(), 10, false),
        G: floatAsNormalizedInteger(components.G ?? unreachable(), 10, false),
        B: floatAsNormalizedInteger(components.B ?? unreachable(), 10, false),
        A: floatAsNormalizedInteger(components.A ?? unreachable(), 2, false),
      }),
      bitsToNumber: components => ({
        R: normalizedIntegerAsFloat(components.R!, 10, false),
        G: normalizedIntegerAsFloat(components.G!, 10, false),
        B: normalizedIntegerAsFloat(components.B!, 10, false),
        A: normalizedIntegerAsFloat(components.A!, 2, false),
      }),
      bitsToULPFromZero: components => components,
      numericRange: { min: 0, max: 1, finiteMin: 0, finiteMax: 1 },
    },
    rg11b10ufloat: {
      componentOrder: kRGB,
      encode: applyEach(identity, kRGB),
      decode: applyEach(identity, kRGB),
      componentInfo: {
        R: { dataType: 'ufloat', bitLength: 11 },
        G: { dataType: 'ufloat', bitLength: 11 },
        B: { dataType: 'ufloat', bitLength: 10 },
      },
      pack: components => {
        const componentsBits = {
          R: float32ToFloatBits(components.R ?? unreachable(), 0, 5, 6, 15),
          G: float32ToFloatBits(components.G ?? unreachable(), 0, 5, 6, 15),
          B: float32ToFloatBits(components.B ?? unreachable(), 0, 5, 5, 15),
        };
        return packComponents(
          kRGB,
          componentsBits,
          {
            R: 11,
            G: 11,
            B: 10,
          },
          'uint'
        );
      },
      unpackBits: (data: Uint8Array) => unpackComponentsBits(kRGB, data, { R: 11, G: 11, B: 10 }),
      numberToBits: components => ({
        R: numberToFloatBits(components.R ?? unreachable(), kFloat11Format),
        G: numberToFloatBits(components.G ?? unreachable(), kFloat11Format),
        B: numberToFloatBits(components.B ?? unreachable(), kFloat10Format),
      }),
      bitsToNumber: components => ({
        R: floatBitsToNumber(components.R!, kFloat11Format),
        G: floatBitsToNumber(components.G!, kFloat11Format),
        B: floatBitsToNumber(components.B!, kFloat10Format),
      }),
      bitsToULPFromZero: components => ({
        R: floatBitsToNormalULPFromZero(components.R!, kFloat11Format),
        G: floatBitsToNormalULPFromZero(components.G!, kFloat11Format),
        B: floatBitsToNormalULPFromZero(components.B!, kFloat10Format),
      }),
      numericRange: {
        min: 0,
        max: Number.POSITIVE_INFINITY,
        finiteMin: 0,
        finiteMax: {
          R: floatBitsToNumber(0b111_1011_1111, kFloat11Format),
          G: floatBitsToNumber(0b111_1011_1111, kFloat11Format),
          B: floatBitsToNumber(0b11_1101_1111, kFloat10Format),
        } as PerComponentFiniteMax,
      },
    },
    rgb9e5ufloat: {
      componentOrder: kRGB,
      componentInfo: makePerTexelComponent(kRGB, {
        dataType: 'ufloat',
        bitLength: -1, // Components don't really have a bitLength since the format is packed.
      }),
      encode: applyEach(identity, kRGB),
      decode: applyEach(identity, kRGB),
      pack: components =>
        new Uint32Array([
          packRGB9E5UFloat(
            components.R ?? unreachable(),
            components.G ?? unreachable(),
            components.B ?? unreachable()
          ),
        ]).buffer,
      unpackBits: (data: Uint8Array) => {
        const encoded = (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0];
        const redMantissa = (encoded >>> 0) & 0b111111111;
        const greenMantissa = (encoded >>> 9) & 0b111111111;
        const blueMantissa = (encoded >>> 18) & 0b111111111;
        const exponentSharedBits = ((encoded >>> 27) & 0b11111) << 9;
        return {
          R: exponentSharedBits | redMantissa,
          G: exponentSharedBits | greenMantissa,
          B: exponentSharedBits | blueMantissa,
        };
      },
      numberToBits: components => ({
        R: float32ToFloatBits(components.R ?? unreachable(), 0, 5, 9, 15),
        G: float32ToFloatBits(components.G ?? unreachable(), 0, 5, 9, 15),
        B: float32ToFloatBits(components.B ?? unreachable(), 0, 5, 9, 15),
      }),
      bitsToNumber: components => ({
        R: ufloatM9E5BitsToNumber(components.R!, kUFloat9e5Format),
        G: ufloatM9E5BitsToNumber(components.G!, kUFloat9e5Format),
        B: ufloatM9E5BitsToNumber(components.B!, kUFloat9e5Format),
      }),
      bitsToULPFromZero: components => ({
        R: floatBitsToNormalULPFromZero(components.R!, kUFloat9e5Format),
        G: floatBitsToNormalULPFromZero(components.G!, kUFloat9e5Format),
        B: floatBitsToNormalULPFromZero(components.B!, kUFloat9e5Format),
      }),
      numericRange: {
        min: 0,
        max: Number.POSITIVE_INFINITY,
        finiteMin: 0,
        finiteMax: ufloatM9E5BitsToNumber(0b11_1111_1111_1111, kUFloat9e5Format),
      },
    },
    depth32float: makeFloatInfo([TexelComponent.Depth], 32, { restrictedDepth: true }),
    depth16unorm: makeNormalizedInfo([TexelComponent.Depth], 16, { signed: false, sRGB: false }),
    depth24plus: {
      componentOrder: [TexelComponent.Depth],
      componentInfo: { Depth: { dataType: null, bitLength: 24 } },
      encode: applyEach(() => unreachable('depth24plus cannot be encoded'), [TexelComponent.Depth]),
      decode: applyEach(() => unreachable('depth24plus cannot be decoded'), [TexelComponent.Depth]),
      pack: () => unreachable('depth24plus data cannot be packed'),
      unpackBits: () => unreachable('depth24plus data cannot be unpacked'),
      numberToBits: () => unreachable('depth24plus has no representation'),
      bitsToNumber: () => unreachable('depth24plus has no representation'),
      bitsToULPFromZero: () => unreachable('depth24plus has no representation'),
      numericRange: { min: 0, max: 1, finiteMin: 0, finiteMax: 1 },
    },
    stencil8: makeIntegerInfo([TexelComponent.Stencil], 8, { signed: false }),
    'depth32float-stencil8': {
      componentOrder: [TexelComponent.Depth, TexelComponent.Stencil],
      componentInfo: {
        Depth: {
          dataType: 'float',
          bitLength: 32,
        },
        Stencil: {
          dataType: 'uint',
          bitLength: 8,
        },
      },
      encode: components => {
        assert(components.Stencil !== undefined);
        assertInIntegerRange(components.Stencil, 8, false);
        return components;
      },
      decode: components => {
        assert(components.Stencil !== undefined);
        assertInIntegerRange(components.Stencil, 8, false);
        return components;
      },
      pack: () => unreachable('depth32float-stencil8 data cannot be packed'),
      unpackBits: () => unreachable('depth32float-stencil8 data cannot be unpacked'),
      numberToBits: () => unreachable('not implemented'),
      bitsToNumber: () => unreachable('not implemented'),
      bitsToULPFromZero: () => unreachable('not implemented'),
      numericRange: null,
    },
    'depth24plus-stencil8': {
      componentOrder: [TexelComponent.Depth, TexelComponent.Stencil],
      componentInfo: {
        Depth: {
          dataType: null,
          bitLength: 24,
        },
        Stencil: {
          dataType: 'uint',
          bitLength: 8,
        },
      },
      encode: components => {
        assert(components.Depth === undefined, 'depth24plus cannot be encoded');
        assert(components.Stencil !== undefined);
        assertInIntegerRange(components.Stencil, 8, false);
        return components;
      },
      decode: components => {
        assert(components.Depth === undefined, 'depth24plus cannot be decoded');
        assert(components.Stencil !== undefined);
        assertInIntegerRange(components.Stencil, 8, false);
        return components;
      },
      pack: () => unreachable('depth24plus-stencil8 data cannot be packed'),
      unpackBits: () => unreachable('depth24plus-stencil8 data cannot be unpacked'),
      numberToBits: () => unreachable('depth24plus-stencil8 has no representation'),
      bitsToNumber: () => unreachable('depth24plus-stencil8 has no representation'),
      bitsToULPFromZero: () => unreachable('depth24plus-stencil8 has no representation'),
      numericRange: null,
    },
  },
};

/**
 * Get the `ComponentDataType` for a format. All components must have the same type.
 * @param {UncompressedTextureFormat} format - The input format.
 * @returns {ComponentDataType} The data of the components.
 */
export function getSingleDataType(format: UncompressedTextureFormat): ComponentDataType {
  const infos = Object.values(kTexelRepresentationInfo[format].componentInfo);
  assert(infos.length > 0);
  return infos.reduce((acc, cur) => {
    assert(cur !== undefined);
    assert(acc === undefined || acc === cur.dataType);
    return cur.dataType;
  }, infos[0].dataType);
}

/**
 * Get traits for generating code to readback data from a component.
 * @param {ComponentDataType} dataType - The input component data type.
 * @returns A dictionary containing the respective `ReadbackTypedArray` and `shaderType`.
 */
export function getComponentReadbackTraits(dataType: ComponentDataType) {
  switch (dataType) {
    case 'ufloat':
    case 'float':
    case 'unorm':
    case 'snorm':
      return {
        ReadbackTypedArray: Float32Array,
        shaderType: 'f32' as const,
      };
    case 'uint':
      return {
        ReadbackTypedArray: Uint32Array,
        shaderType: 'u32' as const,
      };
    case 'sint':
      return {
        ReadbackTypedArray: Int32Array,
        shaderType: 'i32' as const,
      };
    default:
      unreachable();
  }
}
