/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import {
	__experimentalText as WCText,
	__experimentalVStack as VStack,
	Button,
	DropZone,
	Notice,
	FormFileUpload,
	FlexItem,
	ProgressBar,
} from '@wordpress/components';
import { useContext, useState } from '@wordpress/element';
import type { FontFace } from '@wordpress/core-data';

/**
 * Internal dependencies
 */
import { ALLOWED_FILE_EXTENSIONS } from './utils/constants';
import { FontLibraryContext } from './context';
import { Font } from './lib/lib-font.browser';
import makeFamiliesFromFaces from './utils/make-families-from-faces';
import { loadFontFaceInBrowser } from './utils';

function UploadFonts() {
	const { installFonts } = useContext( FontLibraryContext );
	const [ isUploading, setIsUploading ] = useState( false );
	const [ notice, setNotice ] = useState< {
		type: 'success' | 'error';
		message: string;
		errors?: string[];
	} | null >( null );

	const handleDropZone = ( files: File[] ) => {
		handleFilesUpload( files );
	};
	const onFilesUpload = ( event: React.ChangeEvent< HTMLInputElement > ) => {
		handleFilesUpload( event.target.files );
	};

	/**
	 * Filters the selected files to only allow the ones with the allowed extensions
	 *
	 * @param {Array} files The files to be filtered
	 * @return {void}
	 */
	const handleFilesUpload = async ( files: FileList | File[] | null ) => {
		if ( ! files ) {
			return;
		}
		setNotice( null );
		setIsUploading( true );
		const uniqueFilenames = new Set();
		const selectedFiles = [ ...files ];
		let hasInvalidFiles = false;

		// Use map to create a promise for each file check, then filter with Promise.all.
		const checkFilesPromises = selectedFiles.map( async ( file ) => {
			const isFont = await isFontFile( file );
			if ( ! isFont ) {
				hasInvalidFiles = true;
				return null; // Return null for invalid files.
			}
			// Check for duplicates
			if ( uniqueFilenames.has( file.name ) ) {
				return null; // Return null for duplicates.
			}
			// Check if the file extension is allowed.
			const fileExtension = (
				( ( file.name ?? '' ).split( '.' ) ?? [] ).pop() ?? ''
			).toLowerCase();
			if ( ALLOWED_FILE_EXTENSIONS.includes( fileExtension ) ) {
				uniqueFilenames.add( file.name );
				return file; // Return the file if it passes all checks.
			}
			return null; // Return null for disallowed file extensions.
		} );

		// Filter out the nulls after all promises have resolved.
		const allowedFiles: File[] = (
			await Promise.all( checkFilesPromises )
		).filter( ( file ) => null !== file );

		if ( allowedFiles.length > 0 ) {
			loadFiles( allowedFiles );
		} else {
			const message = hasInvalidFiles
				? __( 'Sorry, you are not allowed to upload this file type.' )
				: __( 'No fonts found to install.' );

			setNotice( {
				type: 'error',
				message,
			} );
			setIsUploading( false );
		}
	};

	/**
	 * Loads the selected files and reads the font metadata
	 *
	 * @param {Array} files The files to be loaded
	 * @return {void}
	 */
	const loadFiles = async ( files: File[] ) => {
		const fontFacesLoaded = await Promise.all(
			files.map( async ( fontFile: File ) => {
				const fontFaceData = await getFontFaceMetadata( fontFile );
				await loadFontFaceInBrowser(
					fontFaceData,
					fontFaceData.file,
					'all'
				);
				return fontFaceData;
			} )
		);
		handleInstall( fontFacesLoaded );
	};

	/**
	 * Checks if a file is a valid Font file.
	 *
	 * @param {File} file The file to be checked.
	 * @return {boolean} Whether the file is a valid font file.
	 */
	async function isFontFile( file: File ) {
		const font = new Font( 'Uploaded Font' );
		try {
			const buffer = await readFileAsArrayBuffer( file );
			await font.fromDataBuffer( buffer, 'font' );
			return true;
		} catch {
			return false;
		}
	}

	// Create a function to read the file as array buffer
	async function readFileAsArrayBuffer( file: File ) {
		return new Promise( ( resolve, reject ) => {
			const reader = new window.FileReader();
			reader.readAsArrayBuffer( file );
			reader.onload = () => resolve( reader.result );
			reader.onerror = reject;
		} );
	}

	const getFontFaceMetadata = async ( fontFile: File ) => {
		const buffer = await readFileAsArrayBuffer( fontFile );
		const fontObj: Font & {
			onload?: ( val: { detail: { font: any } } ) => void;
		} = new Font( 'Uploaded Font' );
		fontObj.fromDataBuffer( buffer, fontFile.name );
		// Assuming that fromDataBuffer triggers onload event and returning a Promise
		const onloadEvent: { detail: { font: any } } = await new Promise(
			( resolve ) => ( fontObj.onload = resolve )
		);
		const font = onloadEvent.detail.font;
		const { name } = font.opentype.tables;
		const fontName = name.get( 16 ) || name.get( 1 );
		const isItalic = name.get( 2 ).toLowerCase().includes( 'italic' );
		const fontWeight =
			font.opentype.tables[ 'OS/2' ].usWeightClass || 'normal';
		const isVariable = !! font.opentype.tables.fvar;
		const weightAxis =
			isVariable &&
			font.opentype.tables.fvar.axes.find(
				( { tag }: { tag: string } ) => tag === 'wght'
			);
		const weightRange = weightAxis
			? `${ weightAxis.minValue } ${ weightAxis.maxValue }`
			: null;
		return {
			file: fontFile,
			fontFamily: fontName,
			fontStyle: isItalic ? 'italic' : 'normal',
			fontWeight: weightRange || fontWeight,
		};
	};

	/**
	 * Creates the font family definition and sends it to the server
	 *
	 * @param {Array} fontFaces The font faces to be installed
	 * @return {void}
	 */
	const handleInstall = async ( fontFaces: FontFace[] ) => {
		const fontFamilies = makeFamiliesFromFaces( fontFaces );

		try {
			await installFonts( fontFamilies );
			setNotice( {
				type: 'success',
				message: __( 'Fonts were installed successfully.' ),
			} );
		} catch ( error ) {
			const typedError = error as Error & {
				installationErrors?: string[];
			};
			setNotice( {
				type: 'error',
				message: typedError.message,
				errors: typedError?.installationErrors,
			} );
		}

		setIsUploading( false );
	};

	return (
		<div className="font-library__tabpanel-layout">
			<DropZone onFilesDrop={ handleDropZone } />
			<VStack className="font-library__local-fonts" justify="start">
				{ notice && (
					<Notice
						status={ notice.type }
						__unstableHTML
						onRemove={ () => setNotice( null ) }
					>
						{ notice.message }
						{ notice.errors && (
							<ul>
								{ notice.errors.map( ( error, index ) => (
									<li key={ index }>{ error }</li>
								) ) }
							</ul>
						) }
					</Notice>
				) }
				{ isUploading && (
					<FlexItem>
						<div className="font-library__upload-area">
							<ProgressBar />
						</div>
					</FlexItem>
				) }
				{ ! isUploading && (
					<FormFileUpload
						accept={ ALLOWED_FILE_EXTENSIONS.map(
							( ext ) => `.${ ext }`
						).join( ',' ) }
						multiple
						onChange={ onFilesUpload }
						render={ ( { openFileDialog } ) => (
							<Button
								__next40pxDefaultSize
								className="font-library__upload-area"
								onClick={ openFileDialog }
							>
								{ __( 'Upload font' ) }
							</Button>
						) }
					/>
				) }
				<WCText className="font-library__upload-area__text">
					{ __(
						'Uploaded fonts appear in your library and can be used in your theme. Supported formats: .ttf, .otf, .woff, and .woff2.'
					) }
				</WCText>
			</VStack>
		</div>
	);
}

export default UploadFonts;
