/**
 * Internal dependencies
 */
import { getFileBasename } from './utils';
import { parseHeic } from './heic-parser';

/**
 * Converts an image file to JPEG using the browser's native decoder and canvas.
 *
 * Tries three decoding strategies:
 * 1. createImageBitmap() + OffscreenCanvas (works in Safari, future Chrome).
 * 2. WebCodecs ImageDecoder API (uses platform codecs; may work in future
 *    Chrome if HEIC is added to its image decoder pipeline).
 * 3. HEIC container parsing + WebCodecs VideoDecoder (Chrome 107+ on macOS).
 *    Parses the HEIC/ISOBMFF container to extract the HEVC bitstream, then
 *    decodes it using Chrome's platform HEVC video decoder (hardware-
 *    accelerated via macOS VideoToolbox).
 *
 * This avoids shipping our own HEVC decoder, sidestepping patent/licensing concerns.
 *
 * @param file    Source image file (e.g., HEIC/HEIF).
 * @param quality JPEG quality (0-1). Default 0.82.
 * @return JPEG File object.
 */
export async function canvasConvertToJpeg(
	file: File,
	quality = 0.82
): Promise< File > {
	const baseName = getFileBasename( file.name );

	// Strategy 1: createImageBitmap + OffscreenCanvas.
	try {
		const bitmap = await createImageBitmap( file );
		try {
			const canvas = new OffscreenCanvas( bitmap.width, bitmap.height );
			const ctx = canvas.getContext( '2d' );

			if ( ! ctx ) {
				throw new Error( 'Could not get canvas 2d context' );
			}

			ctx.drawImage( bitmap, 0, 0 );

			const jpegBlob = await canvas.convertToBlob( {
				type: 'image/jpeg',
				quality,
			} );

			return new File( [ jpegBlob ], `${ baseName }.jpg`, {
				type: 'image/jpeg',
			} );
		} finally {
			bitmap.close();
		}
	} catch {
		// createImageBitmap doesn't support HEIC in this browser.
		// Fall through to strategy 2.
	}

	// Strategy 2: WebCodecs ImageDecoder API.
	// Uses platform codecs (e.g., macOS HEIC support) that may not be
	// exposed through createImageBitmap or <img> elements.
	if ( typeof ImageDecoder !== 'undefined' ) {
		const supported = await ImageDecoder.isTypeSupported( file.type );
		if ( supported ) {
			const decoder = new ImageDecoder( {
				type: file.type,
				data: file.stream(),
			} );
			try {
				const { image: videoFrame } = await decoder.decode();
				try {
					const canvas = new OffscreenCanvas(
						videoFrame.displayWidth,
						videoFrame.displayHeight
					);
					const ctx = canvas.getContext( '2d' );

					if ( ! ctx ) {
						throw new Error( 'Could not get canvas 2d context' );
					}

					ctx.drawImage( videoFrame, 0, 0 );

					const jpegBlob = await canvas.convertToBlob( {
						type: 'image/jpeg',
						quality,
					} );

					return new File( [ jpegBlob ], `${ baseName }.jpg`, {
						type: 'image/jpeg',
					} );
				} finally {
					videoFrame.close();
				}
			} finally {
				decoder.close();
			}
		}
	}

	// Strategy 3: HEIC container parsing + WebCodecs VideoDecoder.
	// Chrome 107+ on macOS supports HEVC *video* decoding via platform codecs
	// (macOS VideoToolbox), even though it doesn't support HEIC through image
	// APIs.  A HEIC file is an ISOBMFF container with HEVC-encoded tiles —
	// we parse the container and decode each tile via VideoDecoder.
	if ( typeof VideoDecoder !== 'undefined' ) {
		try {
			const heicData = parseHeic( await file.arrayBuffer() );

			const support = await VideoDecoder.isConfigSupported( {
				codec: heicData.codecString,
			} );

			if ( support.supported ) {
				const canvas = new OffscreenCanvas(
					heicData.outputWidth,
					heicData.outputHeight
				);
				const ctx = canvas.getContext( '2d' );

				if ( ! ctx ) {
					throw new Error( 'Could not get canvas 2d context' );
				}

				// Decode each tile and draw it at its grid position.
				for ( const tile of heicData.tiles ) {
					const frame = await decodeHevcFrame(
						heicData.codecString,
						heicData.description,
						heicData.tileWidth,
						heicData.tileHeight,
						tile.data
					);
					try {
						ctx.drawImage( frame, tile.x, tile.y );
					} finally {
						frame.close();
					}
				}

				// Apply orientation: the native ISOBMFF `irot` transform when
				// present, otherwise the EXIF orientation tag (libheif applies
				// the former but ignores the latter for HEIF-family inputs).
				const outputCanvas =
					heicData.rotation !== 0
						? applyRotation( canvas, heicData.rotation )
						: applyExifOrientation(
								canvas,
								heicData.exifOrientation
						  );

				const jpegBlob = await outputCanvas.convertToBlob( {
					type: 'image/jpeg',
					quality,
				} );

				return new File( [ jpegBlob ], `${ baseName }.jpg`, {
					type: 'image/jpeg',
				} );
			}
		} catch {
			// VideoDecoder HEVC not available or HEIC parsing failed.
			// Fall through to error.
		}
	}

	throw new Error(
		'This browser cannot decode HEIC images. Please use Safari or convert to JPEG before uploading.'
	);
}

/**
 * Apply ISOBMFF irot rotation to a canvas.
 *
 * Returns the original canvas if no rotation is needed, or a new
 * OffscreenCanvas with the rotation applied.
 *
 * @param source   Source canvas with the decoded image.
 * @param rotation Rotation angle in degrees counter-clockwise (0, 90, 180, 270).
 * @return Canvas with rotation applied.
 */
function applyRotation(
	source: OffscreenCanvas,
	rotation: number
): OffscreenCanvas {
	if ( rotation === 0 ) {
		return source;
	}

	const swap = rotation === 90 || rotation === 270;
	const w = swap ? source.height : source.width;
	const h = swap ? source.width : source.height;

	const rotated = new OffscreenCanvas( w, h );
	const ctx = rotated.getContext( '2d' );

	if ( ! ctx ) {
		return source;
	}

	ctx.translate( w / 2, h / 2 );
	// irot angle is CCW; canvas rotate() is CW, so negate.
	ctx.rotate( ( -rotation * Math.PI ) / 180 );
	ctx.drawImage( source, -source.width / 2, -source.height / 2 );

	return rotated;
}

/**
 * Apply an EXIF orientation (1-8) to a canvas.
 *
 * Returns the original canvas for orientation 1, or a new OffscreenCanvas with
 * the rotation/flip applied. Orientations 5-8 swap width and height.
 *
 * @param source      Source canvas with the decoded image.
 * @param orientation EXIF orientation value (1-8).
 * @return Canvas with the orientation applied.
 */
function applyExifOrientation(
	source: OffscreenCanvas,
	orientation: number
): OffscreenCanvas {
	if ( orientation <= 1 || orientation > 8 ) {
		return source;
	}

	const { width: sw, height: sh } = source;
	const swap = orientation >= 5;
	const out = new OffscreenCanvas( swap ? sh : sw, swap ? sw : sh );
	const ctx = out.getContext( '2d' );
	if ( ! ctx ) {
		return source;
	}

	// Affine transforms map EXIF orientation to the upright image. The e/f
	// translation terms use the source width/height. See the EXIF spec tag
	// 0x0112 and the standard orientation matrix.
	switch ( orientation ) {
		case 2: // Flip horizontal.
			ctx.transform( -1, 0, 0, 1, sw, 0 );
			break;
		case 3: // Rotate 180°.
			ctx.transform( -1, 0, 0, -1, sw, sh );
			break;
		case 4: // Flip vertical.
			ctx.transform( 1, 0, 0, -1, 0, sh );
			break;
		case 5: // Transpose.
			ctx.transform( 0, 1, 1, 0, 0, 0 );
			break;
		case 6: // Rotate 90° CW.
			ctx.transform( 0, 1, -1, 0, sh, 0 );
			break;
		case 7: // Transverse.
			ctx.transform( 0, -1, -1, 0, sh, sw );
			break;
		case 8: // Rotate 90° CCW.
			ctx.transform( 0, -1, 1, 0, 0, sw );
			break;
	}

	ctx.drawImage( source, 0, 0 );
	return out;
}

/**
 * Decode a single HEVC key frame using the WebCodecs VideoDecoder API.
 *
 * @param codec       HEVC codec string (e.g. 'hvc1.1.6.L93.B0').
 * @param description HEVCDecoderConfigurationRecord bytes.
 * @param width       Coded width of the frame.
 * @param height      Coded height of the frame.
 * @param data        Raw HEVC bitstream (IDR frame).
 * @return Decoded VideoFrame. Caller must call frame.close().
 */
function decodeHevcFrame(
	codec: string,
	description: Uint8Array,
	width: number,
	height: number,
	data: Uint8Array
): Promise< VideoFrame > {
	return new Promise< VideoFrame >( ( resolve, reject ) => {
		const decoder = new VideoDecoder( {
			output: ( frame ) => {
				decoder.close();
				resolve( frame );
			},
			error: ( e ) => {
				if ( decoder.state !== 'closed' ) {
					decoder.close();
				}
				reject( e );
			},
		} );

		decoder.configure( {
			codec,
			codedWidth: width,
			codedHeight: height,
			description,
		} );

		decoder.decode(
			new EncodedVideoChunk( {
				type: 'key',
				timestamp: 0,
				data,
			} )
		);

		decoder.flush().catch( ( e ) => {
			if ( decoder.state !== 'closed' ) {
				decoder.close();
			}
			reject( e );
		} );
	} );
}
