import clsx from 'clsx';
import { isBlobURL } from '@wordpress/blob';
import {
	createInterpolateElement,
	useEffect,
	useRef,
	useState,
} from '@wordpress/element';
import { __, isRTL } from '@wordpress/i18n';
import {
	RangeControl,
	ResizableBox,
	Spinner,
	ToggleControl,
	ToolbarButton,
	Placeholder,
	Button,
	DropZone,
	__experimentalToolsPanel as ToolsPanel,
	__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import {
	BlockControls,
	InspectorControls,
	MediaPlaceholder,
	MediaReplaceFlow,
	useBlockProps,
	store as blockEditorStore,
	useBlockEditingMode,
	privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { crop, upload } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import { MIN_SIZE } from '../image/constants';
import { MediaControl, MediaControlPreview } from '../utils/media-control';
import { unlock } from '../lock-unlock';
import { useToolsPanelDropdownMenuProps } from '../utils/hooks';

const ALLOWED_MEDIA_TYPES = [ 'image' ];
const { mediaEditKey, openMediaEditorModalKey } = unlock(
	blockEditorPrivateApis
);

const SiteLogo = ( {
	alt,
	attributes: { align, width, height, isLink, linkTarget, shouldSyncIcon },
	isSelected,
	setAttributes,
	setLogo,
	logoUrl,
	siteUrl,
	logoId,
	iconId,
	setIcon,
	canUserEdit,
} ) => {
	const isLargeViewport = useViewportMatch( 'medium' );
	const isWideAligned = [ 'wide', 'full' ].includes( align );
	const isResizable = ! isWideAligned && isLargeViewport;
	const [ { naturalWidth, naturalHeight }, setNaturalSize ] = useState( {} );
	const cropButtonRef = useRef();
	const { toggleSelection, __unstableMarkNextChangeAsNotPersistent } =
		useDispatch( blockEditorStore );
	const dropdownMenuProps = useToolsPanelDropdownMenuProps();

	// Check if we're in contentOnly mode
	const blockEditingMode = useBlockEditingMode();
	const isContentOnlyMode = blockEditingMode === 'contentOnly';

	const {
		imageEditing,
		maxWidth,
		title,
		editMediaEntity,
		openMediaEditorModal,
	} = useSelect( ( select ) => {
		const settings = select( blockEditorStore ).getSettings();
		const siteEntities = select( coreStore ).getEntityRecord(
			'root',
			'__unstableBase'
		);
		return {
			title: siteEntities?.name,
			imageEditing: settings.imageEditing,
			maxWidth: settings.maxWidth,
			editMediaEntity: settings?.[ mediaEditKey ],
			openMediaEditorModal: settings?.[ openMediaEditorModalKey ],
		};
	}, [] );

	useEffect( () => {
		// Turn the `Use as site icon` toggle off if it is on but the logo and icon have
		// fallen out of sync. This can happen if the toggle is saved in the `on` position,
		// but changes are later made to the site icon in the Customizer.
		if ( shouldSyncIcon && logoId !== iconId ) {
			__unstableMarkNextChangeAsNotPersistent();
			setAttributes( { shouldSyncIcon: false } );
		}
	}, [] );

	// Always apply modal updates as snackbar Undo may restore the original id.
	const handleMediaUpdate = ( { id: newId } ) => {
		if ( typeof newId === 'number' ) {
			setLogo( newId );
		}
	};

	function onResizeStart() {
		toggleSelection( false );
	}

	function onResizeStop() {
		toggleSelection( true );
	}

	const img = (
		<>
			<img
				className="custom-logo"
				src={ logoUrl }
				alt={ alt }
				onLoad={ ( event ) => {
					setNaturalSize( {
						naturalWidth: event.target.naturalWidth,
						naturalHeight: event.target.naturalHeight,
					} );
				} }
			/>
			{ isBlobURL( logoUrl ) && <Spinner /> }
		</>
	);

	let imgWrapper = img;
	if ( isLink ) {
		imgWrapper = (
			<a
				href={ siteUrl }
				className="custom-logo-link"
				rel="home"
				title={ title }
				onClick={ ( event ) => event.preventDefault() }
			>
				{ img }
			</a>
		);
	}

	if ( ! isResizable || ! naturalWidth || ! naturalHeight ) {
		return <div style={ { width, height } }>{ imgWrapper }</div>;
	}

	// Set the default width to a responsible size.
	// Note that this width is also set in the attached frontend CSS file.
	const defaultWidth = 120;

	const currentWidth = width || defaultWidth;
	const ratio = naturalWidth / naturalHeight;
	const currentHeight = currentWidth / ratio;
	const minWidth =
		naturalWidth < naturalHeight ? MIN_SIZE : Math.ceil( MIN_SIZE * ratio );
	const minHeight =
		naturalHeight < naturalWidth ? MIN_SIZE : Math.ceil( MIN_SIZE / ratio );

	// With the current implementation of ResizableBox, an image needs an
	// explicit pixel value for the max-width. In absence of being able to
	// set the content-width, this max-width is currently dictated by the
	// vanilla editor style. The following variable adds a buffer to this
	// vanilla style, so 3rd party themes have some wiggleroom. This does,
	// in most cases, allow you to scale the image beyond the width of the
	// main column, though not infinitely.
	// @todo It would be good to revisit this once a content-width variable
	// becomes available.
	const maxWidthBuffer = maxWidth * 2.5;

	let showRightHandle = false;
	let showLeftHandle = false;

	/* eslint-disable no-lonely-if */
	// See https://github.com/WordPress/gutenberg/issues/7584.
	if ( align === 'center' ) {
		// When the image is centered, show both handles.
		showRightHandle = true;
		showLeftHandle = true;
	} else if ( isRTL() ) {
		// In RTL mode the image is on the right by default.
		// Show the right handle and hide the left handle only when it is
		// aligned left. Otherwise always show the left handle.
		if ( align === 'left' ) {
			showRightHandle = true;
		} else {
			showLeftHandle = true;
		}
	} else {
		// Show the left handle and hide the right handle only when the
		// image is aligned right. Otherwise always show the right handle.
		if ( align === 'right' ) {
			showLeftHandle = true;
		} else {
			showRightHandle = true;
		}
	}
	/* eslint-enable no-lonely-if */

	const canEditImage =
		logoId &&
		naturalWidth &&
		naturalHeight &&
		imageEditing &&
		!! editMediaEntity;

	// Hide crop and dimensions editing in write mode
	const shouldShowCropAndDimensions = ! isContentOnlyMode;

	// Support the previous location for the Site Icon settings. To be removed
	// when the required WP core version for Gutenberg is >= 6.5.0.
	const shouldUseNewUrl = ! window?.__experimentalUseCustomizerSiteLogoUrl;

	const siteIconSettingsUrl = shouldUseNewUrl
		? siteUrl + '/wp-admin/options-general.php'
		: siteUrl + '/wp-admin/customize.php?autofocus[section]=title_tagline';

	const syncSiteIconHelpText = createInterpolateElement(
		__(
			'Site Icons are what you see in browser tabs, bookmark bars, and within the WordPress mobile apps. To use a custom icon that is different from your site logo, use the <a>Site Icon settings</a>.'
		),
		{
			a: (
				// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-no-target-blank
				<a
					href={ siteIconSettingsUrl }
					target="_blank"
					rel="noopener"
				/>
			),
		}
	);

	return (
		<>
			<InspectorControls>
				<ToolsPanel
					label={ __( 'Settings' ) }
					dropdownMenuProps={ dropdownMenuProps }
				>
					<ToolsPanelItem
						isShownByDefault
						hasValue={ () => !! width }
						label={ __( 'Image width' ) }
						onDeselect={ () =>
							setAttributes( { width: undefined } )
						}
					>
						<RangeControl
							label={ __( 'Image width' ) }
							onChange={ ( newWidth ) =>
								setAttributes( { width: newWidth } )
							}
							min={ minWidth }
							max={ maxWidthBuffer }
							initialPosition={ Math.min(
								defaultWidth,
								maxWidthBuffer
							) }
							value={ width || '' }
							disabled={ ! isResizable }
						/>
					</ToolsPanelItem>

					<ToolsPanelItem
						isShownByDefault
						hasValue={ () => ! isLink }
						label={ __( 'Link image to home' ) }
						onDeselect={ () => setAttributes( { isLink: true } ) }
					>
						<ToggleControl
							label={ __( 'Link image to home' ) }
							onChange={ () =>
								setAttributes( { isLink: ! isLink } )
							}
							checked={ isLink }
						/>
					</ToolsPanelItem>

					{ isLink && (
						<ToolsPanelItem
							isShownByDefault
							hasValue={ () => linkTarget === '_blank' }
							label={ __( 'Open in new tab' ) }
							onDeselect={ () =>
								setAttributes( { linkTarget: '_self' } )
							}
						>
							<ToggleControl
								label={ __( 'Open in new tab' ) }
								onChange={ ( value ) =>
									setAttributes( {
										linkTarget: value ? '_blank' : '_self',
									} )
								}
								checked={ linkTarget === '_blank' }
							/>
						</ToolsPanelItem>
					) }

					{ canUserEdit && (
						<ToolsPanelItem
							isShownByDefault
							hasValue={ () => !! shouldSyncIcon }
							label={ __( 'Use as Site Icon' ) }
							onDeselect={ () => {
								setAttributes( { shouldSyncIcon: false } );
								setIcon( undefined );
							} }
						>
							<ToggleControl
								label={ __( 'Use as Site Icon' ) }
								onChange={ ( value ) => {
									setAttributes( { shouldSyncIcon: value } );
									setIcon( value ? logoId : undefined );
								} }
								checked={ !! shouldSyncIcon }
								help={ syncSiteIconHelpText }
							/>
						</ToolsPanelItem>
					) }
				</ToolsPanel>
			</InspectorControls>
			{ canEditImage &&
				openMediaEditorModal &&
				shouldShowCropAndDimensions && (
					<BlockControls group="block">
						<ToolbarButton
							ref={ cropButtonRef }
							onClick={ () =>
								openMediaEditorModal( {
									id: logoId,
									onUpdate: handleMediaUpdate,
									onClose: () =>
										cropButtonRef.current?.focus(),
								} )
							}
							aria-haspopup="dialog"
							icon={ crop }
							label={ __( 'Edit image' ) }
						/>
					</BlockControls>
				) }
			{ /* Always render ResizableBox but disable resize functionality in contentOnly mode */ }
			<ResizableBox
				size={ {
					width: currentWidth,
					height: currentHeight,
				} }
				showHandle={ isSelected && shouldShowCropAndDimensions }
				minWidth={ minWidth }
				maxWidth={ maxWidthBuffer }
				minHeight={ minHeight }
				maxHeight={ maxWidthBuffer / ratio }
				lockAspectRatio
				enable={ {
					top: false,
					right: showRightHandle,
					bottom: true,
					left: showLeftHandle,
				} }
				onResizeStart={ onResizeStart }
				onResizeStop={ ( event, direction, elt, delta ) => {
					onResizeStop();
					setAttributes( {
						width: parseInt( currentWidth + delta.width, 10 ),
						height: parseInt( currentHeight + delta.height, 10 ),
					} );
				} }
			>
				{ imgWrapper }
			</ResizableBox>
		</>
	);
};

export default function LogoEdit( {
	attributes,
	className,
	setAttributes,
	isSelected,
} ) {
	const { width, shouldSyncIcon } = attributes;
	const {
		siteLogoId,
		canUserEdit,
		url,
		siteIconId,
		mediaItemData,
		isRequestingMediaItem,
	} = useSelect( ( select ) => {
		const { canUser, getEntityRecord, getEditedEntityRecord } =
			select( coreStore );
		const _canUserEdit = canUser( 'update', {
			kind: 'root',
			name: 'site',
		} );
		const siteSettings = _canUserEdit
			? getEditedEntityRecord( 'root', 'site' )
			: undefined;
		const siteData = getEntityRecord( 'root', '__unstableBase' );
		const _siteLogoId = _canUserEdit
			? siteSettings?.site_logo
			: siteData?.site_logo;
		const _siteIconId = siteSettings?.site_icon;
		const mediaItem =
			_siteLogoId &&
			select( coreStore ).getEntityRecord(
				'postType',
				'attachment',
				_siteLogoId,
				{
					context: 'view',
				}
			);
		const _isRequestingMediaItem =
			!! _siteLogoId &&
			! select( coreStore ).hasFinishedResolution( 'getEntityRecord', [
				'postType',
				'attachment',
				_siteLogoId,
				{ context: 'view' },
			] );

		return {
			siteLogoId: _siteLogoId,
			canUserEdit: _canUserEdit,
			url: siteData?.home,
			mediaItemData: mediaItem,
			isRequestingMediaItem: _isRequestingMediaItem,
			siteIconId: _siteIconId,
		};
	}, [] );
	const { getSettings } = useSelect( blockEditorStore );
	const [ temporaryURL, setTemporaryURL ] = useState();
	const dropdownMenuProps = useToolsPanelDropdownMenuProps();

	const { editEntityRecord } = useDispatch( coreStore );

	const setLogo = ( newValue, shouldForceSync = false ) => {
		// `shouldForceSync` is used to force syncing when the attribute
		// may not have updated yet.
		if ( shouldSyncIcon || shouldForceSync ) {
			setIcon( newValue );
		}

		editEntityRecord( 'root', 'site', undefined, {
			site_logo: newValue,
		} );
	};

	const setIcon = ( newValue ) =>
		// The new value needs to be `null` to reset the Site Icon.
		editEntityRecord( 'root', 'site', undefined, {
			site_icon: newValue ?? null,
		} );

	const { alt_text: alt, source_url: logoUrl } = mediaItemData ?? {};

	const onInitialSelectLogo = ( media ) => {
		// Initialize the syncSiteIcon toggle. If we currently have no Site logo and no
		// site icon, automatically sync the logo to the icon.
		if ( shouldSyncIcon === undefined ) {
			const shouldForceSync = ! siteIconId;
			setAttributes( { shouldSyncIcon: shouldForceSync } );

			// Because we cannot rely on the `shouldSyncIcon` attribute to have updated by
			// the time `setLogo` is called, pass an argument to force the syncing.
			onSelectLogo( media, shouldForceSync );
			return;
		}

		onSelectLogo( media );
	};

	const onSelectLogo = ( media, shouldForceSync = false ) => {
		if ( ! media ) {
			return;
		}

		if ( ! media.id && media.url ) {
			// This is a temporary blob image.
			setTemporaryURL( media.url );
			setLogo( undefined );
			return;
		}

		setLogo( media.id, shouldForceSync );
	};

	const onRemoveLogo = () => {
		setLogo( null );
		setAttributes( { width: undefined } );
	};

	const { createErrorNotice } = useDispatch( noticesStore );
	const onUploadError = ( message ) => {
		createErrorNotice( message, { type: 'snackbar' } );
		setTemporaryURL();
	};

	const onFilesDrop = ( filesList ) => {
		getSettings().mediaUpload( {
			allowedTypes: ALLOWED_MEDIA_TYPES,
			filesList,
			onFileChange( [ image ] ) {
				if ( isBlobURL( image?.url ) ) {
					setTemporaryURL( image.url );
					return;
				}
				onInitialSelectLogo( image );
			},
			onError: onUploadError,
			multiple: false,
		} );
	};

	const mediaReplaceFlowProps = {
		mediaURL: logoUrl,
		name: ! logoUrl ? __( 'Choose logo' ) : __( 'Replace' ),
		onSelect: onSelectLogo,
		onError: onUploadError,
		onReset: onRemoveLogo,
	};
	const controls = canUserEdit && (
		<BlockControls group="other">
			<MediaReplaceFlow
				{ ...mediaReplaceFlowProps }
				allowedTypes={ ALLOWED_MEDIA_TYPES }
				variant="toolbar"
			/>
		</BlockControls>
	);

	let logoImage;
	const isLoading = siteLogoId === undefined || isRequestingMediaItem;
	if ( isLoading ) {
		logoImage = <Spinner />;
	}

	// Reset temporary url when logoUrl is available.
	useEffect( () => {
		if ( logoUrl && temporaryURL ) {
			setTemporaryURL();
		}
	}, [ logoUrl, temporaryURL ] );

	if ( !! logoUrl || !! temporaryURL ) {
		logoImage = (
			<>
				<SiteLogo
					alt={ alt }
					attributes={ attributes }
					className={ className }
					isSelected={ isSelected }
					setAttributes={ setAttributes }
					logoUrl={ temporaryURL || logoUrl }
					setLogo={ setLogo }
					logoId={ mediaItemData?.id || siteLogoId }
					siteUrl={ url }
					setIcon={ setIcon }
					iconId={ siteIconId }
					canUserEdit={ canUserEdit }
				/>
				{ canUserEdit && <DropZone onFilesDrop={ onFilesDrop } /> }
			</>
		);
	}
	const placeholder = ( content ) => {
		const placeholderClassName = clsx(
			'block-editor-media-placeholder',
			className
		);

		return (
			<Placeholder
				className={ placeholderClassName }
				preview={ logoImage }
				withIllustration
				style={ {
					width,
				} }
			>
				{ content }
			</Placeholder>
		);
	};

	const classes = clsx( className, {
		'is-default-size': ! width,
		'is-transient': temporaryURL,
	} );

	const blockProps = useBlockProps( { className: classes } );

	const mediaInspectorPanel = ( canUserEdit || logoUrl ) && (
		<InspectorControls>
			<ToolsPanel
				label={ __( 'Media' ) }
				dropdownMenuProps={ dropdownMenuProps }
			>
				{ ! canUserEdit ? (
					<div
						className="block-library-site-logo__inspector-media-replace-container"
						style={ { gridColumn: '1 / -1' } }
					>
						<MediaControlPreview
							url={ mediaItemData?.source_url }
							filename={
								mediaItemData?.media_details?.sizes?.full
									?.file || mediaItemData?.slug
							}
							itemGroupProps={ {
								isBordered: true,
								className:
									'block-library-site-logo__inspector-readonly-logo-preview',
							} }
							className="block-library-site-logo__inspector-media-replace-title"
						/>
					</div>
				) : (
					<ToolsPanelItem
						hasValue={ () => !! logoUrl }
						label={ __( 'Logo' ) }
						isShownByDefault
					>
						<MediaControl
							mediaId={ siteLogoId }
							mediaUrl={ logoUrl }
							filename={
								mediaItemData?.media_details?.sizes?.full
									?.file || mediaItemData?.slug
							}
							allowedTypes={ ALLOWED_MEDIA_TYPES }
							onSelect={ onSelectLogo }
							onError={ onUploadError }
							onReset={ onRemoveLogo }
							isUploading={ !! temporaryURL }
							emptyLabel={ __( 'Logo' ) }
						/>
					</ToolsPanelItem>
				) }
			</ToolsPanel>
		</InspectorControls>
	);

	return (
		<div { ...blockProps }>
			{ controls }
			{ mediaInspectorPanel }
			{ ( !! logoUrl || !! temporaryURL ) && logoImage }
			{ ( isLoading ||
				( ! temporaryURL && ! logoUrl && ! canUserEdit ) ) && (
				<Placeholder className="site-logo_placeholder" withIllustration>
					{ isLoading && (
						<span className="components-placeholder__preview">
							<Spinner />
						</span>
					) }
				</Placeholder>
			) }
			{ ! isLoading && ! temporaryURL && ! logoUrl && canUserEdit && (
				<MediaPlaceholder
					onSelect={ onInitialSelectLogo }
					allowedTypes={ ALLOWED_MEDIA_TYPES }
					onError={ onUploadError }
					placeholder={ placeholder }
					mediaLibraryButton={ ( { open } ) => {
						return (
							<Button
								__next40pxDefaultSize
								icon={ upload }
								variant="primary"
								label={ __( 'Choose logo' ) }
								showTooltip
								tooltipPosition="middle right"
								onClick={ () => {
									open();
								} }
							/>
						);
					} }
				/>
			) }
		</div>
	);
}
