import type {TypedArray, TextureFormat, ExternalImage} from '@luma.gl/core';
import {isExternalImage, getExternalImageSize} from '@luma.gl/core';

export type TextureImageSource = ExternalImage;

/**
 * One mip level
 * Basic data structure is similar to `ImageData`
 * additional optional fields can describe compressed texture data.
 */
export type TextureImageData = {
  /** Preferred WebGPU style format string. */
  textureFormat?: TextureFormat;
  /** WebGPU style format string. Defaults to 'rgba8unorm' */
  format?: TextureFormat;
  /** Typed Array with the bytes of the image. @note beware row byte alignment requirements */
  data: TypedArray;
  /** Width of the image, in pixels, @note beware row byte alignment requirements */
  width: number;
  /** Height of the image, in rows */
  height: number;
};

/**
 * A single mip-level can be initialized by data or an ImageBitmap etc
 * @note in the WebGPU spec a mip-level is called a subresource
 */
export type TextureMipLevelData = TextureImageData | TextureImageSource;

/**
 * Texture data for one image "slice" (which can consist of multiple miplevels)
 * Thus data for one slice be a single mip level or an array of miplevels
 * @note in the WebGPU spec each cross-section image in a 3D texture is called a "slice",
 * in a array texture each image in the array is called an array "layer"
 * luma.gl calls one image in a GPU texture a "slice" regardless of context.
 */
export type TextureSliceData = TextureMipLevelData | TextureMipLevelData[];

/** Names of cube texture faces */
export type TextureCubeFace = '+X' | '-X' | '+Y' | '-Y' | '+Z' | '-Z';

/** Array of cube texture faces. @note: index in array is the face index */
// biome-ignore format: preserve layout
export const TEXTURE_CUBE_FACES = ['+X', '-X', '+Y', '-Y', '+Z', '-Z'] as const satisfies readonly TextureCubeFace[];

/** Map of cube texture face names to face indexes */
// biome-ignore format: preserve layout
export const TEXTURE_CUBE_FACE_MAP = {'+X': 0, '-X': 1, '+Y': 2, '-Y': 3, '+Z': 4, '-Z': 5} as const satisfies Record<TextureCubeFace, number>;

/** @todo - Define what data type is supported for 1D textures. TextureImageData with height = 1 */
export type Texture1DData = TextureSliceData;

/** Texture data can be one or more mip levels */
export type Texture2DData = TextureSliceData;

/** 6 face textures */
export type TextureCubeData = Record<TextureCubeFace, TextureSliceData>;

/** Array of textures */
export type Texture3DData = TextureSliceData[];

/** Array of textures */
export type TextureArrayData = TextureSliceData[];

/** Array of 6 face textures */
export type TextureCubeArrayData = Record<TextureCubeFace, TextureSliceData>[];

type TextureData =
  | Texture1DData
  | Texture3DData
  | TextureArrayData
  | TextureCubeArrayData
  | TextureCubeData;

/** Sync data props */
export type TextureDataProps =
  | {dimension: '1d'; data: Texture1DData | null}
  | {dimension?: '2d'; data: Texture2DData | null}
  | {dimension: '3d'; data: Texture3DData | null}
  | {dimension: '2d-array'; data: TextureArrayData | null}
  | {dimension: 'cube'; data: TextureCubeData | null}
  | {dimension: 'cube-array'; data: TextureCubeArrayData | null};

/** Async data props */
export type TextureDataAsyncProps =
  | {dimension: '1d'; data?: Promise<Texture1DData> | Texture1DData | null}
  | {dimension?: '2d'; data?: Promise<Texture2DData> | Texture2DData | null}
  | {dimension: '3d'; data?: Promise<Texture3DData> | Texture3DData | null}
  | {dimension: '2d-array'; data?: Promise<TextureArrayData> | TextureArrayData | null}
  | {dimension: 'cube'; data?: Promise<TextureCubeData> | TextureCubeData | null}
  | {dimension: 'cube-array'; data?: Promise<TextureCubeArrayData> | TextureCubeArrayData | null};

/** Describes data for one sub resource (one mip level of one slice (depth or array layer)) */
export type TextureSubresource = {
  /** slice (depth or array layer)) */
  z: number;
  /** mip level (0 - max mip levels) */
  mipLevel: number;
} & (
  | {
      type: 'external-image';
      image: ExternalImage;
      /** @deprecated is this an appropriate place for this flag? */
      flipY?: boolean;
    }
  | {
      type: 'texture-data';
      data: TextureImageData;
      textureFormat?: TextureFormat;
    }
);

/** Check if texture data is a typed array */
export function isTextureSliceData(data: TextureData): data is TextureImageData {
  const typedArray = (data as TextureImageData)?.data;
  return ArrayBuffer.isView(typedArray);
}

export function getFirstMipLevel(layer: TextureSliceData | null): TextureMipLevelData | null {
  if (!layer) return null;
  return Array.isArray(layer) ? (layer[0] ?? null) : layer;
}

export function getTextureSizeFromData(
  props: TextureDataProps
): {width: number; height: number} | null {
  const {dimension, data} = props;
  if (!data) {
    return null;
  }

  switch (dimension) {
    case '1d': {
      const mipLevel = getFirstMipLevel(data);
      if (!mipLevel) return null;
      const {width} = getTextureMipLevelSize(mipLevel);
      return {width, height: 1};
    }
    case '2d': {
      const mipLevel = getFirstMipLevel(data);
      return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
    }
    case '3d':
    case '2d-array': {
      if (!Array.isArray(data) || data.length === 0) return null;
      const mipLevel = getFirstMipLevel(data[0]);
      return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
    }
    case 'cube': {
      const face = (Object.keys(data)[0] as TextureCubeFace) ?? null;
      if (!face) return null;
      const faceData = (data as Record<TextureCubeFace, TextureSliceData>)[face];
      const mipLevel = getFirstMipLevel(faceData);
      return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
    }
    case 'cube-array': {
      if (!Array.isArray(data) || data.length === 0) return null;
      const firstCube = data[0];
      const face = (Object.keys(firstCube)[0] as TextureCubeFace) ?? null;
      if (!face) return null;
      const mipLevel = getFirstMipLevel(firstCube[face]);
      return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
    }
    default:
      return null;
  }
}

function getTextureMipLevelSize(data: TextureMipLevelData): {width: number; height: number} {
  if (isExternalImage(data)) {
    return getExternalImageSize(data);
  }
  if (typeof data === 'object' && 'width' in data && 'height' in data) {
    return {width: data.width, height: data.height};
  }
  throw new Error('Unsupported mip-level data');
}

/** Type guard: is a mip-level `TextureImageData` (vs ExternalImage or bare typed array) */
function isTextureImageData(data: unknown): data is TextureImageData {
  return (
    typeof data === 'object' &&
    data !== null &&
    'data' in data &&
    'width' in data &&
    'height' in data
  );
}

function isTypedArrayMipLevelData(data: unknown): data is TypedArray {
  return ArrayBuffer.isView(data);
}

export function resolveTextureImageFormat(data: TextureImageData): TextureFormat | undefined {
  const {textureFormat, format} = data;
  if (textureFormat && format && textureFormat !== format) {
    throw new Error(
      `Conflicting texture formats "${textureFormat}" and "${format}" provided for the same mip level`
    );
  }
  return textureFormat ?? format;
}

/** Resolve size for a single mip-level datum */
// function getTextureMipLevelSizeFromData(data: TextureMipLevelData): {
//   width: number;
//   height: number;
// } {
//   if (this.device.isExternalImage(data)) {
//     return this.device.getExternalImageSize(data);
//   }
//   if (this.isTextureImageData(data)) {
//     return {width: data.width, height: data.height};
//   }
//   // Fallback (should not happen with current types)
//   throw new Error('Unsupported mip-level data');
// }

/** Convert cube face label to depth index */
export function getCubeFaceIndex(face: TextureCubeFace): number {
  const idx = TEXTURE_CUBE_FACE_MAP[face];
  if (idx === undefined) throw new Error(`Invalid cube face: ${face}`);
  return idx;
}

/** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
export function getCubeArrayFaceIndex(cubeIndex: number, face: TextureCubeFace): number {
  return 6 * cubeIndex + getCubeFaceIndex(face);
}

// ------------------ Upload helpers ------------------

/** Experimental: Set multiple mip levels (1D) */
export function getTexture1DSubresources(data: Texture1DData): TextureSubresource[] {
  // Not supported in WebGL; left explicit
  throw new Error('setTexture1DData not supported in WebGL.');
  // const subresources: TextureSubresource[] = [];
  // return subresources;
}

/** Normalize 2D layer payload into an array of mip-level items */
function _normalizeTexture2DData(
  data: Texture2DData
): (TextureImageData | ExternalImage | TypedArray)[] {
  return Array.isArray(data) ? data : [data];
}

/** Experimental: Set multiple mip levels (2D), optionally at `z` (depth/array index) */
export function getTexture2DSubresources(
  slice: number,
  lodData: Texture2DData,
  baseLevelSize?: {width: number; height: number},
  textureFormat?: TextureFormat
): TextureSubresource[] {
  const lodArray = _normalizeTexture2DData(lodData);
  const z = slice;

  const subresources: TextureSubresource[] = [];

  for (let mipLevel = 0; mipLevel < lodArray.length; mipLevel++) {
    const imageData = lodArray[mipLevel];
    if (isExternalImage(imageData)) {
      subresources.push({
        type: 'external-image',
        image: imageData,
        z,
        mipLevel
      });
    } else if (isTextureImageData(imageData)) {
      subresources.push({
        type: 'texture-data',
        data: imageData,
        textureFormat: resolveTextureImageFormat(imageData),
        z,
        mipLevel
      });
    } else if (isTypedArrayMipLevelData(imageData) && baseLevelSize) {
      subresources.push({
        type: 'texture-data',
        data: {
          data: imageData,
          width: Math.max(1, baseLevelSize.width >> mipLevel),
          height: Math.max(1, baseLevelSize.height >> mipLevel),
          ...(textureFormat ? {format: textureFormat} : {})
        },
        textureFormat,
        z,
        mipLevel
      });
    } else {
      throw new Error('Unsupported 2D mip-level payload');
    }
  }

  return subresources;
}

/** 3D: multiple depth slices, each may carry multiple mip levels */
export function getTexture3DSubresources(data: Texture3DData): TextureSubresource[] {
  const subresources: TextureSubresource[] = [];
  for (let depth = 0; depth < data.length; depth++) {
    subresources.push(...getTexture2DSubresources(depth, data[depth]));
  }
  return subresources;
}

/** 2D array: multiple layers, each may carry multiple mip levels */
export function getTextureArraySubresources(data: TextureArrayData): TextureSubresource[] {
  const subresources: TextureSubresource[] = [];
  for (let layer = 0; layer < data.length; layer++) {
    subresources.push(...getTexture2DSubresources(layer, data[layer]));
  }
  return subresources;
}

/** Cube: 6 faces, each may carry multiple mip levels */
export function getTextureCubeSubresources(data: TextureCubeData): TextureSubresource[] {
  const subresources: TextureSubresource[] = [];
  for (const [face, faceData] of Object.entries(data) as [TextureCubeFace, TextureSliceData][]) {
    const faceDepth = getCubeFaceIndex(face);
    subresources.push(...getTexture2DSubresources(faceDepth, faceData));
  }
  return subresources;
}

/** Cube array: multiple cubes (faces×layers), each face may carry multiple mips */
export function getTextureCubeArraySubresources(data: TextureCubeArrayData): TextureSubresource[] {
  const subresources: TextureSubresource[] = [];
  data.forEach((cubeData, cubeIndex) => {
    for (const [face, faceData] of Object.entries(cubeData)) {
      const faceDepth = getCubeArrayFaceIndex(cubeIndex, face as TextureCubeFace);
      subresources.push(...getTexture2DSubresources(faceDepth, faceData));
    }
  });
  return subresources;
}
