import { __, sprintf } from '@wordpress/i18n';
import { useMemo, useState } from '@wordpress/element';
import {
	Button,
	Notice,
	PanelBody,
	Placeholder,
	SelectControl,
	Spinner,
	ToolbarButton,
	__experimentalToolsPanel as ToolsPanel,
	__experimentalToolsPanelItem as ToolsPanelItem,
	__experimentalConfirmDialog as ConfirmDialog,
} from '@wordpress/components';
import {
	BlockContextProvider,
	BlockControls,
	useBlockEditingMode,
	__experimentalUseBlockPreview as useBlockPreview,
} from '@wordpress/block-editor';
import { sharedIcon } from './shared-icon';
import { isGalleryFlexLayout } from './shared';
import { Caption } from '../utils/caption';
import { DEFAULT_ORDERBY, DEFAULT_ORDER, MAX_IMAGES } from './dynamic-source';

/**
 * Ordering options for a dynamic gallery source. Each value is a composite
 * `"orderby/order"` string mapping to the matching `/wp/v2/media` collection
 * params. `menu_order` is deliberately omitted — it isn't a valid REST `orderby`
 * value, so the editor preview couldn't reproduce it (see `dynamic-source.js`).
 */
const ORDER_OPTIONS = [
	{ label: __( 'Newest to oldest' ), value: 'date/desc' },
	{ label: __( 'Oldest to newest' ), value: 'date/asc' },
	{
		/* translators: Label for ordering images by title in ascending order. */
		label: __( 'A → Z' ),
		value: 'title/asc',
	},
	{
		/* translators: Label for ordering images by title in descending order. */
		label: __( 'Z → A' ),
		value: 'title/desc',
	},
];

/**
 * "Order by" control for a dynamic gallery, mirroring the Query Loop block's
 * `OrderControl`: a single `SelectControl` whose value composites `orderby` and
 * `order`, split apart again on change.
 *
 * @param {Object}   props
 * @param {string}   props.orderby  Current `orderby` value.
 * @param {string}   props.order    Current `order` value (`asc`/`desc`).
 * @param {Function} props.onChange Called with `{ orderby, order }` on change.
 */
function OrderControl( { orderby, order, onChange } ) {
	return (
		<SelectControl
			label={ __( 'Order by' ) }
			value={ `${ orderby }/${ order }` }
			options={ ORDER_OPTIONS }
			onChange={ ( value ) => {
				const [ newOrderby, newOrder ] = value.split( '/' );
				onChange( { orderby: newOrderby, order: newOrder } );
			} }
		/>
	);
}

/**
 * Confirmation for leaving dynamic mode, shown from both the block toolbar and
 * the Source panel so the two entry points explain the change identically.
 *
 * Detaching keeps the images the gallery currently shows but breaks the link to
 * its source, so it's worth confirming — mirroring the dialog `GallerySourcePanel`
 * shows for the opposite direction.
 *
 * @param {Object}   props
 * @param {Function} props.onConfirm Called when the user confirms detaching.
 * @param {Function} props.onCancel  Called when the user dismisses the dialog.
 */
function DetachGalleryDialog( { onConfirm, onCancel } ) {
	return (
		<ConfirmDialog
			isOpen
			title={ __( 'Detach Gallery' ) }
			__experimentalHideHeader={ false }
			confirmButtonText={ __( 'Detach' ) }
			onConfirm={ onConfirm }
			onCancel={ onCancel }
			size="medium"
		>
			{ __(
				'The gallery displays the images attached to the post. Detaching will enable you to add, delete, or reorder images. However, new attachments will no longer be added automatically.'
			) }
		</ConfirmDialog>
	);
}

/**
 * The Gallery block's "Source" inspector panel.
 *
 * In dynamic mode it shows the resolved source, a control to detach the gallery
 * from it, and the source ordering. In static mode it offers the entry point
 * into dynamic mode. Either direction is a one-way change, so both are behind a
 * confirmation dialog this panel owns. Rendered inside the block's
 * `InspectorControls`, alongside the Settings panel.
 *
 * @param {Object}  props
 * @param {Object}  props.dynamic           The `useDynamicGallery` result.
 * @param {Object}  props.dropdownMenuProps Shared ToolsPanel dropdown menu props.
 * @param {boolean} props.hasImages         Whether the gallery has manually-added images.
 */
export function GallerySourcePanel( {
	dynamic,
	dropdownMenuProps,
	hasImages,
} ) {
	const {
		dynamicContent,
		canUseDynamicSource,
		sourceDescriptor,
		sourceOrderby,
		sourceOrder,
		setSourceOrder,
		convertToStatic,
		enableDynamicMode,
		resetSource,
		isResolvingDynamic,
		hasMoreImagesThanCap,
		dynamicMediaTotal,
	} = dynamic;
	const isDynamic = !! dynamicContent;

	const [ isConfirming, setIsConfirming ] = useState( false );
	const [ isConfirmingDetach, setIsConfirmingDetach ] = useState( false );

	// Entering dynamic mode discards any hand-added images, so confirm first
	// when there are images to lose; otherwise switch straight away.
	function requestEnableDynamicMode() {
		if ( hasImages ) {
			setIsConfirming( true );
		} else {
			enableDynamicMode();
		}
	}

	if ( isDynamic ) {
		return (
			<>
				<ToolsPanel
					label={ __( 'Source' ) }
					resetAll={ resetSource }
					dropdownMenuProps={ dropdownMenuProps }
				>
					<div className="wp-block-gallery__source-settings">
						<p className="wp-block-gallery__source-description">
							{ sourceDescriptor?.description ??
								__( 'Dynamic images.' ) }
						</p>
						<Button
							__next40pxDefaultSize
							variant="secondary"
							onClick={ () => setIsConfirmingDetach( true ) }
							// Guard the race where the media is still resolving:
							// detaching now would map over an incomplete (or
							// empty) list and produce a gallery missing images.
							disabled={ isResolvingDynamic }
							accessibleWhenDisabled
						>
							{ __( 'Detach Gallery' ) }
						</Button>
					</div>
					{ hasMoreImagesThanCap && (
						<Notice
							className="wp-block-gallery__source-notice"
							status="warning"
							isDismissible={ false }
						>
							{ sprintf(
								/* translators: 1: number of images shown. 2: total number of matching images. */
								__(
									'Only the first %1$d of %2$d images will be displayed.'
								),
								MAX_IMAGES,
								dynamicMediaTotal
							) }
						</Notice>
					) }
					<ToolsPanelItem
						isShownByDefault
						label={ __( 'Order by' ) }
						hasValue={ () =>
							sourceOrderby !== DEFAULT_ORDERBY ||
							sourceOrder !== DEFAULT_ORDER
						}
						onDeselect={ () =>
							setSourceOrder( undefined, undefined )
						}
					>
						<OrderControl
							orderby={ sourceOrderby }
							order={ sourceOrder }
							onChange={ ( { orderby, order } ) =>
								setSourceOrder( orderby, order )
							}
						/>
					</ToolsPanelItem>
				</ToolsPanel>
				{ isConfirmingDetach && (
					<DetachGalleryDialog
						onConfirm={ () => {
							convertToStatic();
							setIsConfirmingDetach( false );
						} }
						onCancel={ () => setIsConfirmingDetach( false ) }
					/>
				) }
			</>
		);
	}

	// In static mode this panel is just an entry into dynamic mode, so hide it
	// when there's no post type to preview against. This is intentionally
	// stricter than the placeholder's entry button (see `edit.jsx`), which stays
	// available anywhere because the source resolves at render time.
	if ( ! canUseDynamicSource ) {
		return null;
	}

	return (
		<>
			<PanelBody title={ __( 'Source' ) }>
				<div className="wp-block-gallery__source-settings">
					{ /*
					 * Hardcoded on purpose: this single-source entry button (and
					 * its confirm dialog below) is temporary. Once more sources
					 * exist it becomes a "Choose source" select whose options read
					 * from each source descriptor's `title`, with help text
					 * carrying the per-source explanation these strings do today.
					 */ }
					<p className="wp-block-gallery__source-description">
						{ __( 'Images added to the gallery.' ) }
					</p>
					<Button
						__next40pxDefaultSize
						variant="secondary"
						onClick={ requestEnableDynamicMode }
					>
						{ __( 'Use attached images' ) }
					</Button>
				</div>
			</PanelBody>
			{ isConfirming && (
				<ConfirmDialog
					isOpen
					title={ __( 'Use images attached to the post?' ) }
					__experimentalHideHeader={ false }
					confirmButtonText={ __( 'Use attached images' ) }
					onConfirm={ () => {
						enableDynamicMode();
						setIsConfirming( false );
					} }
					onCancel={ () => setIsConfirming( false ) }
					size="medium"
				>
					{ __(
						'The images in this gallery will be replaced, but will remain in the media library.'
					) }
				</ConfirmDialog>
			) }
		</>
	);
}

/**
 * Renders the resolved image blocks as a read-only preview.
 *
 * `useBlockPreview` returns a `useDisabled` ref that makes its subtree inert, so
 * previewed images (including any links) aren't interactive in the editor. The
 * ref needs a real element, yet the images must stay flex children of the gallery
 * `<figure>` and sit beside an editable caption. `display: contents` resolves
 * this: the wrapper carries the ref but generates no box, so the image figures
 * remain the figure's flex items and only they are disabled — the caption sibling
 * stays editable. This relies on the gallery's image styles using descendant
 * (not direct-child) selectors, which the box-less wrapper leaves intact.
 *
 * The gallery's layout is passed through so the previewed images see the same
 * parent layout that real inner blocks would (`useBlockPreview` provides it to
 * their layout context). Without it they resolve to the default flow layout and
 * behave as if they weren't in a gallery — most visibly, an image inside a
 * cropped gallery would keep its baseline `height: auto` and defeat the
 * gallery's cropping CSS.
 *
 * @param {Object}   props
 * @param {Object[]} props.imageBlocks Non-persisted `core/image` blocks to preview.
 * @param {Object}   props.layout      The gallery's layout, for the preview's layout context.
 */
function GalleryImagesPreview( { imageBlocks, layout } ) {
	const { children, ref, className } = useBlockPreview( {
		blocks: imageBlocks,
		layout,
	} );
	return (
		<div
			ref={ ref }
			className={ className }
			style={ { display: 'contents' } }
		>
			{ children }
		</div>
	);
}

/**
 * Renders a dynamic-mode gallery on the canvas:
 *
 * - a block-toolbar control to detach the gallery from its source, confirmed in
 *   a dialog;
 * - the gallery `<figure>` wrapper holding a non-editable preview of the
 *   resolved media (or a placeholder while resolving / when nothing is found),
 *   with the gallery's provided context so the previewed images inherit
 *   gallery-wide settings;
 * - an editable gallery-level caption, alongside the read-only preview;
 * - the (empty) inner blocks kept mounted so the container's `allowedBlocks: []`
 *   keeps syncing to block list settings (which blocks insertion and hides the
 *   List View).
 *
 * @param {Object}   props
 * @param {Object}   props.dynamic               The `useDynamicGallery` result.
 * @param {Object}   props.blockProps            The gallery's `useBlockProps()` result.
 * @param {Object}   props.innerBlocksProps      The gallery's `useInnerBlocksProps()` result.
 * @param {Object}   props.attributes            The gallery block attributes.
 * @param {Function} props.setAttributes         The block's `setAttributes`.
 * @param {boolean}  props.isSelected            Whether the gallery block is selected.
 * @param {Function} props.insertBlocksAfter     Inserts blocks after the gallery.
 * @param {boolean}  props.isContentLocked       Whether the gallery is content-locked.
 * @param {boolean}  props.multiGallerySelection Whether multiple galleries are selected.
 */
export function GalleryDynamicView( {
	dynamic,
	blockProps,
	innerBlocksProps,
	attributes,
	setAttributes,
	isSelected,
	insertBlocksAfter,
	isContentLocked,
	multiGallerySelection,
} ) {
	const {
		sourceDescriptor,
		dynamicImageBlocks,
		galleryContext,
		isResolvingDynamic,
		convertToStatic,
	} = dynamic;

	// Detaching the gallery materializes editable inner blocks, which is a
	// structural change. Only offer it when the block is fully editable:
	// under a content lock (e.g. inside a `contentOnly` group) the editing mode
	// is `'contentOnly'`/`'disabled'`, where structural toolbar controls are
	// hidden and the conversion shouldn't be possible.
	const blockEditingMode = useBlockEditingMode();

	const [ isConfirmingDetach, setIsConfirmingDetach ] = useState( false );

	// The layout the previewed images sit in. Normalized the same way the
	// gallery's own classes are (`isGalleryFlexLayout`), so a layout that the
	// wrapper treats as flex — including a missing or typeless one — is reported
	// as flex to the images rather than resolving to the default flow layout.
	// Memoized because it becomes the preview's layout context value.
	const previewLayout = useMemo(
		() =>
			isGalleryFlexLayout( attributes.layout )
				? { ...attributes.layout, type: 'flex' }
				: attributes.layout,
		[ attributes.layout ]
	);

	// Empty-state copy for the preview. Framed as forward-looking ("… will appear
	// here") rather than as an error, since the same empty result covers both a
	// post with no matching images and a template with no post in context yet —
	// in either case the source simply resolves to nothing right now. The per-
	// source wording comes from the source descriptor.
	const emptyInstructions = isResolvingDynamic
		? __( 'Loading images…' )
		: sourceDescriptor?.emptyMessage ??
		  __( 'Dynamic images will appear here.' );

	return (
		<>
			{ blockEditingMode === 'default' && (
				<>
					<BlockControls group="other">
						<ToolbarButton
							onClick={ () => setIsConfirmingDetach( true ) }
							// Same guard as the inspector's "Detach Gallery": both end in
							// `convertToStatic`, which would map over a
							// still-resolving (or empty) media list.
							// (`ToolbarButton` stays focusable when disabled by
							// default.)
							disabled={ isResolvingDynamic }
						>
							{ __( 'Detach' ) }
						</ToolbarButton>
					</BlockControls>
					{ isConfirmingDetach && (
						<DetachGalleryDialog
							onConfirm={ () => {
								convertToStatic();
								setIsConfirmingDetach( false );
							} }
							onCancel={ () => setIsConfirmingDetach( false ) }
						/>
					) }
				</>
			) }
			<figure { ...blockProps }>
				{ dynamicImageBlocks.length ? (
					<BlockContextProvider value={ galleryContext }>
						<GalleryImagesPreview
							imageBlocks={ dynamicImageBlocks }
							layout={ previewLayout }
						/>
					</BlockContextProvider>
				) : (
					<Placeholder
						icon={ sharedIcon }
						label={ __( 'Gallery' ) }
						instructions={ emptyInstructions }
					>
						{ isResolvingDynamic && <Spinner /> }
					</Placeholder>
				) }
				<Caption
					attributes={ attributes }
					setAttributes={ setAttributes }
					isSelected={ isSelected }
					insertBlocksAfter={ insertBlocksAfter }
					showToolbarButton={
						! multiGallerySelection && ! isContentLocked
					}
					className="blocks-gallery-caption"
					label={ __( 'Gallery caption text' ) }
					placeholder={ __( 'Add gallery caption' ) }
				/>
			</figure>
			{ /*
			 * Dynamic mode shows a preview instead of real inner blocks, but the
			 * empty inner blocks are still rendered here for their side effect:
			 * the `allowedBlocks: []` passed to `useInnerBlocksProps` only syncs
			 * to block list settings while the inner blocks are mounted (via
			 * `useNestedSettingsUpdate`). That setting is what blocks insertion
			 * (`canInsertBlockType`) and hides the now-unusable List View
			 * (`shouldRenderBlockListView`). With no inner blocks and no appender,
			 * this renders no output of its own.
			 */ }
			{ innerBlocksProps.children }
		</>
	);
}
