import {
	ColorManagement,
	FloatType,
	HalfFloatType,
	UnsignedByteType,
	RGBAFormat,
	RGFormat,
	RGIntegerFormat,
	RedFormat,
	RedIntegerFormat,
	NoColorSpace,
	LinearSRGBColorSpace,
	SRGBColorSpace,
	SRGBTransfer,
	DataTexture,
	REVISION,
} from 'three';

import {
	createDefaultContainer,
	write,
	KHR_DF_CHANNEL_RGBSDA_ALPHA,
	KHR_DF_CHANNEL_RGBSDA_BLUE,
	KHR_DF_CHANNEL_RGBSDA_GREEN,
	KHR_DF_CHANNEL_RGBSDA_RED,
	KHR_DF_MODEL_RGBSDA,
	KHR_DF_PRIMARIES_BT709,
	KHR_DF_PRIMARIES_UNSPECIFIED,
	KHR_DF_SAMPLE_DATATYPE_FLOAT,
	KHR_DF_SAMPLE_DATATYPE_LINEAR,
	KHR_DF_SAMPLE_DATATYPE_SIGNED,
	KHR_DF_TRANSFER_LINEAR,
	KHR_DF_TRANSFER_SRGB,
	VK_FORMAT_R16_SFLOAT,
	VK_FORMAT_R16G16_SFLOAT,
	VK_FORMAT_R16G16B16A16_SFLOAT,
	VK_FORMAT_R32_SFLOAT,
	VK_FORMAT_R32G32_SFLOAT,
	VK_FORMAT_R32G32B32A32_SFLOAT,
	VK_FORMAT_R8_SRGB,
	VK_FORMAT_R8_UNORM,
	VK_FORMAT_R8G8_SRGB,
	VK_FORMAT_R8G8_UNORM,
	VK_FORMAT_R8G8B8A8_SRGB,
	VK_FORMAT_R8G8B8A8_UNORM,
} from '../libs/ktx-parse.module.js';

/**
 * References:
 * - https://github.khronos.org/KTX-Specification/ktxspec.v2.html
 * - https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html
 * - https://github.com/donmccurdy/KTX-Parse
 */

const VK_FORMAT_MAP = {

	[ RGBAFormat ]: {
		[ FloatType ]: {
			[ NoColorSpace ]: VK_FORMAT_R32G32B32A32_SFLOAT,
			[ LinearSRGBColorSpace ]: VK_FORMAT_R32G32B32A32_SFLOAT,
		},
		[ HalfFloatType ]: {
			[ NoColorSpace ]: VK_FORMAT_R16G16B16A16_SFLOAT,
			[ LinearSRGBColorSpace ]: VK_FORMAT_R16G16B16A16_SFLOAT,
		},
		[ UnsignedByteType ]: {
			[ NoColorSpace ]: VK_FORMAT_R8G8B8A8_UNORM,
			[ LinearSRGBColorSpace ]: VK_FORMAT_R8G8B8A8_UNORM,
			[ SRGBColorSpace ]: VK_FORMAT_R8G8B8A8_SRGB,
		},
	},

	[ RGFormat ]: {
		[ FloatType ]: {
			[ NoColorSpace ]: VK_FORMAT_R32G32_SFLOAT,
			[ LinearSRGBColorSpace ]: VK_FORMAT_R32G32_SFLOAT,
		},
		[ HalfFloatType ]: {
			[ NoColorSpace ]: VK_FORMAT_R16G16_SFLOAT,
			[ LinearSRGBColorSpace ]: VK_FORMAT_R16G16_SFLOAT,
		},
		[ UnsignedByteType ]: {
			[ NoColorSpace ]: VK_FORMAT_R8G8_UNORM,
			[ LinearSRGBColorSpace ]: VK_FORMAT_R8G8_UNORM,
			[ SRGBColorSpace ]: VK_FORMAT_R8G8_SRGB,
		},
	},

	[ RedFormat ]: {
		[ FloatType ]: {
			[ NoColorSpace ]: VK_FORMAT_R32_SFLOAT,
			[ LinearSRGBColorSpace ]: VK_FORMAT_R32_SFLOAT,
		},
		[ HalfFloatType ]: {
			[ NoColorSpace ]: VK_FORMAT_R16_SFLOAT,
			[ LinearSRGBColorSpace ]: VK_FORMAT_R16_SFLOAT,
		},
		[ UnsignedByteType ]: {
			[ NoColorSpace ]: VK_FORMAT_R8_UNORM,
			[ LinearSRGBColorSpace ]: VK_FORMAT_R8_UNORM,
			[ SRGBColorSpace ]: VK_FORMAT_R8_SRGB,
		},
	},

};

const KHR_DF_CHANNEL_MAP = [

	KHR_DF_CHANNEL_RGBSDA_RED,
	KHR_DF_CHANNEL_RGBSDA_GREEN,
	KHR_DF_CHANNEL_RGBSDA_BLUE,
	KHR_DF_CHANNEL_RGBSDA_ALPHA,

];

// TODO: sampleLower and sampleUpper may change based on color space.
const KHR_DF_CHANNEL_SAMPLE_LOWER_UPPER = {

	[ FloatType ]: [ 0xbf800000, 0x3f800000 ],
	[ HalfFloatType ]: [ 0xbf800000, 0x3f800000 ],
	[ UnsignedByteType ]: [ 0, 255 ],

};

const ERROR_INPUT = 'THREE.KTX2Exporter: Supported inputs are DataTexture, Data3DTexture, or WebGLRenderer and WebGLRenderTarget.';
const ERROR_FORMAT = 'THREE.KTX2Exporter: Supported formats are RGBAFormat, RGFormat, or RedFormat.';
const ERROR_TYPE = 'THREE.KTX2Exporter: Supported types are FloatType, HalfFloatType, or UnsignedByteType."';
const ERROR_COLOR_SPACE = 'THREE.KTX2Exporter: Supported color spaces are SRGBColorSpace (UnsignedByteType only), LinearSRGBColorSpace, or NoColorSpace.';

/**
 * An exporter for KTX2.
 *
 * ```js
 * const exporter = new KTX2Exporter();
 * const result = await exporter.parse( dataTexture );
 * ```
 *
 * @three_import import { KTX2Exporter } from 'three/addons/exporters/KTX2Exporter.js';
 */
export class KTX2Exporter {

	/**
	 * This method has two variants.
	 *
	 * - When exporting a data texture, it receives one parameter. The data or 3D data texture.
	 * - When exporting a render target (e.g. a PMREM), it receives two parameters. The renderer and the
	 * render target.
	 *
	 * @async
	 * @param {(DataTexture|Data3DTexture|WebGPURenderer|WebGLRenderer)} arg1 - The data texture to export or a renderer.
	 * @param {RenderTarget} [arg2] - The render target that should be exported
	 * @return {Promise<Uint8Array>} A Promise that resolves with the exported KTX2.
	 */
	async parse( arg1, arg2 ) {

		let texture;

		if ( arg1.isDataTexture || arg1.isData3DTexture ) {

			texture = arg1;

		} else if ( ( arg1.isWebGLRenderer || arg1.isWebGPURenderer ) && arg2.isRenderTarget ) {

			texture = await toDataTexture( arg1, arg2 );

		} else {

			throw new Error( ERROR_INPUT );

		}

		if ( VK_FORMAT_MAP[ texture.format ] === undefined ) {

			throw new Error( ERROR_FORMAT );

		}

		if ( VK_FORMAT_MAP[ texture.format ][ texture.type ] === undefined ) {

			throw new Error( ERROR_TYPE );

		}

		if ( VK_FORMAT_MAP[ texture.format ][ texture.type ][ texture.colorSpace ] === undefined ) {

			throw new Error( ERROR_COLOR_SPACE );

		}

		//

		const array = texture.image.data;
		const channelCount = getChannelCount( texture );
		const container = createDefaultContainer();

		container.vkFormat = VK_FORMAT_MAP[ texture.format ][ texture.type ][ texture.colorSpace ];
		container.typeSize = array.BYTES_PER_ELEMENT;
		container.pixelWidth = texture.image.width;
		container.pixelHeight = texture.image.height;

		if ( texture.isData3DTexture ) {

			container.pixelDepth = texture.image.depth;

		}

		//

		const basicDesc = container.dataFormatDescriptor[ 0 ];

		basicDesc.colorModel = KHR_DF_MODEL_RGBSDA;
		basicDesc.colorPrimaries = texture.colorSpace === NoColorSpace
			? KHR_DF_PRIMARIES_UNSPECIFIED
			: KHR_DF_PRIMARIES_BT709;
		basicDesc.transferFunction = ColorManagement.getTransfer( texture.colorSpace ) === SRGBTransfer
			? KHR_DF_TRANSFER_SRGB
			: KHR_DF_TRANSFER_LINEAR;

		basicDesc.texelBlockDimension = [ 0, 0, 0, 0 ];

		basicDesc.bytesPlane = [

			container.typeSize * channelCount, 0, 0, 0, 0, 0, 0, 0,

		];

		for ( let i = 0; i < channelCount; ++ i ) {

			let channelType = KHR_DF_CHANNEL_MAP[ i ];

			// Assign KHR_DF_SAMPLE_DATATYPE_LINEAR if the channel is linear _and_ differs from the transfer function.
			if ( channelType === KHR_DF_CHANNEL_RGBSDA_ALPHA && basicDesc.transferFunction !== KHR_DF_TRANSFER_LINEAR ) {

				channelType |= KHR_DF_SAMPLE_DATATYPE_LINEAR;

			}

			if ( texture.type === FloatType || texture.type === HalfFloatType ) {

				channelType |= KHR_DF_SAMPLE_DATATYPE_FLOAT;
				channelType |= KHR_DF_SAMPLE_DATATYPE_SIGNED;

			}

			basicDesc.samples.push( {

				channelType: channelType,
				bitOffset: i * array.BYTES_PER_ELEMENT * 8,
				bitLength: array.BYTES_PER_ELEMENT * 8 - 1,
				samplePosition: [ 0, 0, 0, 0 ],
				sampleLower: KHR_DF_CHANNEL_SAMPLE_LOWER_UPPER[ texture.type ][ 0 ],
				sampleUpper: KHR_DF_CHANNEL_SAMPLE_LOWER_UPPER[ texture.type ][ 1 ],

			} );

		}

		//

		container.levelCount = 1;

		container.levels = [ {

			levelData: new Uint8Array( array.buffer, array.byteOffset, array.byteLength ),
			uncompressedByteLength: array.byteLength,

		} ];

		//

		container.keyValue[ 'KTXwriter' ] = `three.js ${ REVISION }`;

		//

		return write( container, { keepWriter: true } );

	}

}

async function toDataTexture( renderer, rtt ) {

	const channelCount = getChannelCount( rtt.texture );

	let view;

	if ( renderer.isWebGLRenderer ) {

		if ( rtt.texture.type === FloatType ) {

			view = new Float32Array( rtt.width * rtt.height * channelCount );

		} else if ( rtt.texture.type === HalfFloatType ) {

			view = new Uint16Array( rtt.width * rtt.height * channelCount );

		} else if ( rtt.texture.type === UnsignedByteType ) {

			view = new Uint8Array( rtt.width * rtt.height * channelCount );

		} else {

			throw new Error( ERROR_TYPE );

		}

		await renderer.readRenderTargetPixelsAsync( rtt, 0, 0, rtt.width, rtt.height, view );

	} else {

		view = await renderer.readRenderTargetPixelsAsync( rtt, 0, 0, rtt.width, rtt.height );

	}

	const texture = new DataTexture( view, rtt.width, rtt.height, rtt.texture.format, rtt.texture.type );

	texture.colorSpace = rtt.texture.colorSpace;

	return texture;

}

function getChannelCount( texture ) {

	switch ( texture.format ) {

		case RGBAFormat:

			return 4;

		case RGFormat:
		case RGIntegerFormat:

			return 2;

		case RedFormat:
		case RedIntegerFormat:

			return 1;

		default:

			throw new Error( ERROR_FORMAT );

	}

}
