// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

/* eslint-disable indent */
import type {TextureLevel} from '@loaders.gl/schema';
import {loadBasisEncoderModule, loadBasisTranscoderModule} from './basis-module-loader';
import {GL_EXTENSIONS_CONSTANTS} from '../gl-extensions';
import {getSupportedGPUTextureFormats} from '../utils/texture-formats';
import {isKTX} from './parse-ktx';

export type BasisFormat =
  | 'etc1'
  | 'etc2'
  | 'bc1'
  | 'bc3'
  | 'bc4'
  | 'bc5'
  | 'bc7-m6-opaque-only'
  | 'bc7-m5'
  | 'pvrtc1-4-rgb'
  | 'pvrtc1-4-rgba'
  | 'astc-4x4'
  | 'atc-rgb'
  | 'atc-rgba-interpolated-alpha'
  | 'rgba32'
  | 'rgb565'
  | 'bgr565'
  | 'rgba4444';

type BasisOutputOptions = {
  basisFormat: number;
  compressed: boolean;
  format?: number;
};

const OutputFormat: Record<string, BasisOutputOptions> = {
  etc1: {
    basisFormat: 0,
    compressed: true,
    format: GL_EXTENSIONS_CONSTANTS.COMPRESSED_RGB_ETC1_WEBGL
  },
  etc2: {basisFormat: 1, compressed: true},
  bc1: {
    basisFormat: 2,
    compressed: true,
    format: GL_EXTENSIONS_CONSTANTS.COMPRESSED_RGB_S3TC_DXT1_EXT
  },
  bc3: {
    basisFormat: 3,
    compressed: true,
    format: GL_EXTENSIONS_CONSTANTS.COMPRESSED_RGBA_S3TC_DXT5_EXT
  },
  bc4: {basisFormat: 4, compressed: true},
  bc5: {basisFormat: 5, compressed: true},
  'bc7-m6-opaque-only': {basisFormat: 6, compressed: true},
  'bc7-m5': {basisFormat: 7, compressed: true},
  'pvrtc1-4-rgb': {
    basisFormat: 8,
    compressed: true,
    format: GL_EXTENSIONS_CONSTANTS.COMPRESSED_RGB_PVRTC_4BPPV1_IMG
  },
  'pvrtc1-4-rgba': {
    basisFormat: 9,
    compressed: true,
    format: GL_EXTENSIONS_CONSTANTS.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG
  },
  'astc-4x4': {
    basisFormat: 10,
    compressed: true,
    format: GL_EXTENSIONS_CONSTANTS.COMPRESSED_RGBA_ASTC_4X4_KHR
  },
  'atc-rgb': {basisFormat: 11, compressed: true},
  'atc-rgba-interpolated-alpha': {basisFormat: 12, compressed: true},
  rgba32: {basisFormat: 13, compressed: false},
  rgb565: {basisFormat: 14, compressed: false},
  bgr565: {basisFormat: 15, compressed: false},
  rgba4444: {basisFormat: 16, compressed: false}
};

/**
 * parse data with a Binomial Basis_Universal module
 * @param data
 * @param options
 * @returns compressed texture data
 */
export async function parseBasis(data: ArrayBuffer, options): Promise<TextureLevel[][]> {
  if (options.basis.containerFormat === 'auto') {
    if (isKTX(data)) {
      const fileConstructors = await loadBasisEncoderModule(options);
      return parseKTX2File(fileConstructors.KTX2File, data, options);
    }
    const {BasisFile} = await loadBasisTranscoderModule(options);
    return parseBasisFile(BasisFile, data, options);
  }
  switch (options.basis.module) {
    case 'encoder':
      const fileConstructors = await loadBasisEncoderModule(options);
      switch (options.basis.containerFormat) {
        case 'ktx2':
          return parseKTX2File(fileConstructors.KTX2File, data, options);
        case 'basis':
        default:
          return parseBasisFile(fileConstructors.BasisFile, data, options);
      }
    case 'transcoder':
    default:
      const {BasisFile} = await loadBasisTranscoderModule(options);
      return parseBasisFile(BasisFile, data, options);
  }
}

/**
 * Parse *.basis file data
 * @param BasisFile - initialized transcoder module
 * @param data
 * @param options
 * @returns compressed texture data
 */
function parseBasisFile(BasisFile, data, options): TextureLevel[][] {
  const basisFile = new BasisFile(new Uint8Array(data));

  try {
    if (!basisFile.startTranscoding()) {
      throw new Error('Failed to start basis transcoding');
    }

    const imageCount = basisFile.getNumImages();
    const images: TextureLevel[][] = [];

    for (let imageIndex = 0; imageIndex < imageCount; imageIndex++) {
      const levelsCount = basisFile.getNumLevels(imageIndex);
      const levels: TextureLevel[] = [];

      for (let levelIndex = 0; levelIndex < levelsCount; levelIndex++) {
        levels.push(transcodeImage(basisFile, imageIndex, levelIndex, options));
      }

      images.push(levels);
    }

    return images;
  } finally {
    basisFile.close();
    basisFile.delete();
  }
}

/**
 * Parse the particular level image of a basis file
 * @param basisFile
 * @param imageIndex
 * @param levelIndex
 * @param options
 * @returns compressed texture data
 */
function transcodeImage(basisFile, imageIndex, levelIndex, options): TextureLevel {
  const width = basisFile.getImageWidth(imageIndex, levelIndex);
  const height = basisFile.getImageHeight(imageIndex, levelIndex);

  // See https://github.com/BinomialLLC/basis_universal/pull/83
  const hasAlpha = basisFile.getHasAlpha(/* imageIndex, levelIndex */);

  // Check options for output format etc
  const {compressed, format, basisFormat} = getBasisOptions(options, hasAlpha);

  const decodedSize = basisFile.getImageTranscodedSizeInBytes(imageIndex, levelIndex, basisFormat);
  const decodedData = new Uint8Array(decodedSize);

  if (!basisFile.transcodeImage(decodedData, imageIndex, levelIndex, basisFormat, 0, 0)) {
    throw new Error('failed to start Basis transcoding');
  }

  return {
    // standard loaders.gl image category payload
    width,
    height,
    data: decodedData,
    compressed,
    format,

    // Additional fields
    // Add levelSize field.
    hasAlpha
  };
}

/**
 * Parse *.ktx2 file data
 * @param KTX2File
 * @param data
 * @param options
 * @returns compressed texture data
 */
function parseKTX2File(KTX2File, data: ArrayBuffer, options): TextureLevel[][] {
  const ktx2File = new KTX2File(new Uint8Array(data));

  try {
    if (!ktx2File.startTranscoding()) {
      throw new Error('failed to start KTX2 transcoding');
    }
    const levelsCount = ktx2File.getLevels();
    const levels: TextureLevel[] = [];

    for (let levelIndex = 0; levelIndex < levelsCount; levelIndex++) {
      levels.push(transcodeKTX2Image(ktx2File, levelIndex, options));
    }

    return [levels];
  } finally {
    ktx2File.close();
    ktx2File.delete();
  }
}

/**
 * Parse the particular level image of a ktx2 file
 * @param ktx2File
 * @param levelIndex
 * @param options
 * @returns
 */
function transcodeKTX2Image(ktx2File, levelIndex: number, options): TextureLevel {
  const {alphaFlag, height, width} = ktx2File.getImageLevelInfo(levelIndex, 0, 0);

  // Check options for output format etc
  const {compressed, format, basisFormat} = getBasisOptions(options, alphaFlag);

  const decodedSize = ktx2File.getImageTranscodedSizeInBytes(
    levelIndex,
    0 /* layerIndex */,
    0 /* faceIndex */,
    basisFormat
  );
  const decodedData = new Uint8Array(decodedSize);

  if (
    !ktx2File.transcodeImage(
      decodedData,
      levelIndex,
      0 /* layerIndex */,
      0 /* faceIndex */,
      basisFormat,
      0,
      -1 /* channel0 */,
      -1 /* channel1 */
    )
  ) {
    throw new Error('Failed to transcode KTX2 image');
  }

  return {
    // standard loaders.gl image category payload
    width,
    height,
    data: decodedData,
    compressed,

    // Additional fields
    levelSize: decodedSize,
    hasAlpha: alphaFlag,
    format
  };
}

/**
 * Get BasisFormat by loader format option
 * @param options
 * @param hasAlpha
 * @returns BasisFormat data
 */
function getBasisOptions(options, hasAlpha: boolean): BasisOutputOptions {
  let format = options && options.basis && options.basis.format;
  if (format === 'auto') {
    format = selectSupportedBasisFormat();
  }
  if (typeof format === 'object') {
    format = hasAlpha ? format.alpha : format.noAlpha;
  }
  format = format.toLowerCase();
  return OutputFormat[format];
}

/**
 * Select transcode format from the list of supported formats
 * @returns key for OutputFormat map
 */
export function selectSupportedBasisFormat():
  | BasisFormat
  | {
      alpha: BasisFormat;
      noAlpha: BasisFormat;
    } {
  const supportedFormats = getSupportedGPUTextureFormats();
  if (supportedFormats.has('astc')) {
    return 'astc-4x4';
  } else if (supportedFormats.has('dxt')) {
    return {
      alpha: 'bc3',
      noAlpha: 'bc1'
    };
  } else if (supportedFormats.has('pvrtc')) {
    return {
      alpha: 'pvrtc1-4-rgba',
      noAlpha: 'pvrtc1-4-rgb'
    };
  } else if (supportedFormats.has('etc1')) {
    return 'etc1';
  } else if (supportedFormats.has('etc2')) {
    return 'etc2';
  }
  return 'rgb565';
}
