/**
 * External dependencies
 */
import clsx from 'clsx';

/**
 * WordPress dependencies
 */
import { memo, useCallback, RawHTML, useContext, useMemo } from '@wordpress/element';
import { getBlockType, getSaveContent, isUnmodifiedDefaultBlock, serializeRawBlock, switchToBlockType, getDefaultBlockName, isUnmodifiedBlock, isReusableBlock, getBlockDefaultClassName, hasBlockSupport, store as blocksStore, privateApis as blocksPrivateApis } from '@wordpress/blocks';
import { withFilters } from '@wordpress/components';
import { withDispatch, useDispatch, useSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { safeHTML } from '@wordpress/dom';

/**
 * Internal dependencies
 */
import BlockEdit from '../block-edit';
import BlockInvalidWarning from './block-invalid-warning';
import BlockCrashWarning from './block-crash-warning';
import BlockCrashBoundary from './block-crash-boundary';
import BlockHtml from './block-html';
import { useBlockProps } from './use-block-props';
import { store as blockEditorStore } from '../../store';
import { useLayout } from './layout';
import { PrivateBlockContext } from './private-block-context';
import { unlock } from '../../lock-unlock';
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
import { Fragment as _Fragment } from "react/jsx-runtime";
const {
  isUnmodifiedBlockContent
} = unlock(blocksPrivateApis);

/**
 * Merges wrapper props with special handling for classNames and styles.
 *
 * @param {Object} propsA
 * @param {Object} propsB
 *
 * @return {Object} Merged props.
 */
function mergeWrapperProps(propsA, propsB) {
  const newProps = {
    ...propsA,
    ...propsB
  };

  // May be set to undefined, so check if the property is set!
  if (propsA?.hasOwnProperty('className') && propsB?.hasOwnProperty('className')) {
    newProps.className = clsx(propsA.className, propsB.className);
  }
  if (propsA?.hasOwnProperty('style') && propsB?.hasOwnProperty('style')) {
    newProps.style = {
      ...propsA.style,
      ...propsB.style
    };
  }
  return newProps;
}
function Block({
  children,
  isHtml,
  ...props
}) {
  return /*#__PURE__*/_jsx("div", {
    ...useBlockProps(props, {
      __unstableIsHtml: isHtml
    }),
    children: children
  });
}
function BlockListBlock({
  block: {
    __unstableBlockSource
  },
  mode,
  isLocked,
  canRemove,
  clientId,
  isSelected,
  isSelectionEnabled,
  className,
  __unstableLayoutClassNames: layoutClassNames,
  name,
  isValid,
  attributes,
  wrapperProps,
  setAttributes,
  onReplace,
  onInsertBlocksAfter,
  onMerge,
  toggleSelection
}) {
  var _wrapperProps;
  const {
    mayDisplayControls,
    mayDisplayParentControls,
    themeSupportsLayout,
    ...context
  } = useContext(PrivateBlockContext);
  const {
    removeBlock
  } = useDispatch(blockEditorStore);
  const onRemove = useCallback(() => removeBlock(clientId), [clientId, removeBlock]);
  const parentLayout = useLayout() || {};

  // We wrap the BlockEdit component in a div that hides it when editing in
  // HTML mode. This allows us to render all of the ancillary pieces
  // (InspectorControls, etc.) which are inside `BlockEdit` but not
  // `BlockHTML`, even in HTML mode.
  let blockEdit = /*#__PURE__*/_jsx(BlockEdit, {
    name: name,
    isSelected: isSelected,
    attributes: attributes,
    setAttributes: setAttributes,
    insertBlocksAfter: isLocked ? undefined : onInsertBlocksAfter,
    onReplace: canRemove ? onReplace : undefined,
    onRemove: canRemove ? onRemove : undefined,
    mergeBlocks: canRemove ? onMerge : undefined,
    clientId: clientId,
    isSelectionEnabled: isSelectionEnabled,
    toggleSelection: toggleSelection,
    __unstableLayoutClassNames: layoutClassNames,
    __unstableParentLayout: Object.keys(parentLayout).length ? parentLayout : undefined,
    mayDisplayControls: mayDisplayControls,
    mayDisplayParentControls: mayDisplayParentControls,
    blockEditingMode: context.blockEditingMode,
    isPreviewMode: context.isPreviewMode
  });
  const blockType = getBlockType(name);

  // Determine whether the block has props to apply to the wrapper.
  if (blockType?.getEditWrapperProps) {
    wrapperProps = mergeWrapperProps(wrapperProps, blockType.getEditWrapperProps(attributes));
  }
  const isAligned = wrapperProps && !!wrapperProps['data-align'] && !themeSupportsLayout;

  // Support for sticky position in classic themes with alignment wrappers.

  const isSticky = className?.includes('is-position-sticky');

  // For aligned blocks, provide a wrapper element so the block can be
  // positioned relative to the block column.
  // This is only kept for classic themes that don't support layout
  // Historically we used to rely on extra divs and data-align to
  // provide the alignments styles in the editor.
  // Due to the differences between frontend and backend, we migrated
  // to the layout feature, and we're now aligning the markup of frontend
  // and backend.
  if (isAligned) {
    blockEdit = /*#__PURE__*/_jsx("div", {
      className: clsx('wp-block', isSticky && className),
      "data-align": wrapperProps['data-align'],
      children: blockEdit
    });
  }
  let block;
  if (!isValid) {
    const saveContent = __unstableBlockSource ? serializeRawBlock(__unstableBlockSource) : getSaveContent(blockType, attributes);
    block = /*#__PURE__*/_jsxs(Block, {
      className: "has-warning",
      children: [/*#__PURE__*/_jsx(BlockInvalidWarning, {
        clientId: clientId
      }), /*#__PURE__*/_jsx(RawHTML, {
        children: safeHTML(saveContent)
      })]
    });
  } else if (mode === 'html') {
    // Render blockEdit so the inspector controls don't disappear.
    // See #8969.
    block = /*#__PURE__*/_jsxs(_Fragment, {
      children: [/*#__PURE__*/_jsx("div", {
        style: {
          display: 'none'
        },
        children: blockEdit
      }), /*#__PURE__*/_jsx(Block, {
        isHtml: true,
        children: /*#__PURE__*/_jsx(BlockHtml, {
          clientId: clientId
        })
      })]
    });
  } else if (blockType?.apiVersion > 1) {
    block = blockEdit;
  } else {
    block = /*#__PURE__*/_jsx(Block, {
      children: blockEdit
    });
  }
  const {
    'data-align': dataAlign,
    ...restWrapperProps
  } = (_wrapperProps = wrapperProps) !== null && _wrapperProps !== void 0 ? _wrapperProps : {};
  const updatedWrapperProps = {
    ...restWrapperProps,
    className: clsx(restWrapperProps.className, dataAlign && themeSupportsLayout && `align${dataAlign}`, !(dataAlign && isSticky) && className)
  };

  // We set a new context with the adjusted and filtered wrapperProps (through
  // `editor.BlockListBlock`), which the `BlockListBlockProvider` did not have
  // access to.
  // Note that the context value doesn't have to be memoized in this case
  // because when it changes, this component will be re-rendered anyway, and
  // none of the consumers (BlockListBlock and useBlockProps) are memoized or
  // "pure". This is different from the public BlockEditContext, where
  // consumers might be memoized or "pure".
  return /*#__PURE__*/_jsx(PrivateBlockContext.Provider, {
    value: {
      wrapperProps: updatedWrapperProps,
      isAligned,
      ...context
    },
    children: /*#__PURE__*/_jsx(BlockCrashBoundary, {
      fallback: /*#__PURE__*/_jsx(Block, {
        className: "has-warning",
        children: /*#__PURE__*/_jsx(BlockCrashWarning, {})
      }),
      children: block
    })
  });
}
const applyWithDispatch = withDispatch((dispatch, ownProps, registry) => {
  const {
    updateBlockAttributes,
    insertBlocks,
    mergeBlocks,
    replaceBlocks,
    toggleSelection,
    __unstableMarkLastChangeAsPersistent,
    moveBlocksToPosition,
    removeBlock,
    selectBlock
  } = dispatch(blockEditorStore);

  // Do not add new properties here, use `useDispatch` instead to avoid
  // leaking new props to the public API (editor.BlockListBlock filter).
  return {
    setAttributes(newAttributes) {
      const {
        getMultiSelectedBlockClientIds
      } = registry.select(blockEditorStore);
      const multiSelectedBlockClientIds = getMultiSelectedBlockClientIds();
      const {
        clientId
      } = ownProps;
      const clientIds = multiSelectedBlockClientIds.length ? multiSelectedBlockClientIds : [clientId];
      updateBlockAttributes(clientIds, newAttributes);
    },
    onInsertBlocks(blocks, index) {
      const {
        rootClientId
      } = ownProps;
      insertBlocks(blocks, index, rootClientId);
    },
    onInsertBlocksAfter(blocks) {
      const {
        clientId,
        rootClientId
      } = ownProps;
      const {
        getBlockIndex
      } = registry.select(blockEditorStore);
      const index = getBlockIndex(clientId);
      insertBlocks(blocks, index + 1, rootClientId);
    },
    onMerge(forward) {
      const {
        clientId,
        rootClientId
      } = ownProps;
      const {
        getPreviousBlockClientId,
        getNextBlockClientId,
        getBlock,
        getBlockAttributes,
        getBlockName,
        getBlockOrder,
        getBlockIndex,
        getBlockRootClientId,
        canInsertBlockType
      } = registry.select(blockEditorStore);
      function switchToDefaultOrRemove() {
        const block = getBlock(clientId);
        const defaultBlockName = getDefaultBlockName();
        if (getBlockName(clientId) !== defaultBlockName) {
          const replacement = switchToBlockType(block, defaultBlockName);
          if (replacement && replacement.length) {
            replaceBlocks(clientId, replacement);
          }
        } else if (isUnmodifiedDefaultBlock(block)) {
          const nextBlockClientId = getNextBlockClientId(clientId);
          if (nextBlockClientId) {
            registry.batch(() => {
              removeBlock(clientId);
              selectBlock(nextBlockClientId);
            });
          }
        }
      }

      /**
       * Moves the block with clientId up one level. If the block type
       * cannot be inserted at the new location, it will be attempted to
       * convert to the default block type.
       *
       * @param {string}  _clientId       The block to move.
       * @param {boolean} changeSelection Whether to change the selection
       *                                  to the moved block.
       */
      function moveFirstItemUp(_clientId, changeSelection = true) {
        const targetRootClientId = getBlockRootClientId(_clientId);
        const blockOrder = getBlockOrder(_clientId);
        const [firstClientId] = blockOrder;
        if (blockOrder.length === 1 && isUnmodifiedBlock(getBlock(firstClientId))) {
          removeBlock(_clientId);
        } else {
          registry.batch(() => {
            const firstBlock = getBlock(firstClientId);
            const isFirstBlockContentUnmodified = isUnmodifiedBlockContent(firstBlock);
            const defaultBlockName = getDefaultBlockName();
            const replacement = switchToBlockType(firstBlock, defaultBlockName);
            const canTransformToDefaultBlock = !!replacement?.length && replacement.every(block => canInsertBlockType(block.name, _clientId));
            if (isFirstBlockContentUnmodified && canTransformToDefaultBlock) {
              // Step 1: If the block is empty and can be transformed to the default block type.
              replaceBlocks(firstClientId, replacement, changeSelection);
            } else if (isFirstBlockContentUnmodified && firstBlock.name === defaultBlockName) {
              // Step 2: If the block is empty and is already the default block type.
              removeBlock(firstClientId);
              const nextBlockClientId = getNextBlockClientId(clientId);
              if (nextBlockClientId) {
                selectBlock(nextBlockClientId);
              }
            } else if (canInsertBlockType(firstBlock.name, targetRootClientId)) {
              // Step 3: If the block can be moved up.
              moveBlocksToPosition([firstClientId], _clientId, targetRootClientId, getBlockIndex(_clientId));
            } else {
              const canLiftAndTransformToDefaultBlock = !!replacement?.length && replacement.every(block => canInsertBlockType(block.name, targetRootClientId));
              if (canLiftAndTransformToDefaultBlock) {
                // Step 4: If the block can be transformed to the default block type and moved up.
                insertBlocks(replacement, getBlockIndex(_clientId), targetRootClientId, changeSelection);
                removeBlock(firstClientId, false);
              } else {
                // Step 5: Continue the default behavior.
                switchToDefaultOrRemove();
              }
            }
            if (!getBlockOrder(_clientId).length && isUnmodifiedBlock(getBlock(_clientId))) {
              removeBlock(_clientId, false);
            }
          });
        }
      }

      // For `Delete` or forward merge, we should do the exact same thing
      // as `Backspace`, but from the other block.
      if (forward) {
        if (rootClientId) {
          const nextRootClientId = getNextBlockClientId(rootClientId);
          if (nextRootClientId) {
            // If there is a block that follows with the same parent
            // block name and the same attributes, merge the inner
            // blocks.
            if (getBlockName(rootClientId) === getBlockName(nextRootClientId)) {
              const rootAttributes = getBlockAttributes(rootClientId);
              const previousRootAttributes = getBlockAttributes(nextRootClientId);
              if (Object.keys(rootAttributes).every(key => rootAttributes[key] === previousRootAttributes[key])) {
                registry.batch(() => {
                  moveBlocksToPosition(getBlockOrder(nextRootClientId), nextRootClientId, rootClientId);
                  removeBlock(nextRootClientId, false);
                });
                return;
              }
            } else {
              mergeBlocks(rootClientId, nextRootClientId);
              return;
            }
          }
        }
        const nextBlockClientId = getNextBlockClientId(clientId);
        if (!nextBlockClientId) {
          return;
        }
        if (getBlockOrder(nextBlockClientId).length) {
          moveFirstItemUp(nextBlockClientId, false);
        } else {
          mergeBlocks(clientId, nextBlockClientId);
        }
      } else {
        const previousBlockClientId = getPreviousBlockClientId(clientId);
        if (previousBlockClientId) {
          mergeBlocks(previousBlockClientId, clientId);
        } else if (rootClientId) {
          const previousRootClientId = getPreviousBlockClientId(rootClientId);

          // If there is a preceding block with the same parent block
          // name and the same attributes, merge the inner blocks.
          if (previousRootClientId && getBlockName(rootClientId) === getBlockName(previousRootClientId)) {
            const rootAttributes = getBlockAttributes(rootClientId);
            const previousRootAttributes = getBlockAttributes(previousRootClientId);
            if (Object.keys(rootAttributes).every(key => rootAttributes[key] === previousRootAttributes[key])) {
              registry.batch(() => {
                moveBlocksToPosition(getBlockOrder(rootClientId), rootClientId, previousRootClientId);
                removeBlock(rootClientId, false);
              });
              return;
            }
          }
          moveFirstItemUp(rootClientId);
        } else {
          switchToDefaultOrRemove();
        }
      }
    },
    onReplace(blocks, indexToSelect, initialPosition) {
      if (blocks.length && !isUnmodifiedDefaultBlock(blocks[blocks.length - 1])) {
        __unstableMarkLastChangeAsPersistent();
      }
      //Unsynced patterns are nested in an array so we need to flatten them.
      const replacementBlocks = blocks?.length === 1 && Array.isArray(blocks[0]) ? blocks[0] : blocks;
      replaceBlocks([ownProps.clientId], replacementBlocks, indexToSelect, initialPosition);
    },
    toggleSelection(selectionEnabled) {
      toggleSelection(selectionEnabled);
    }
  };
});

// This component is used by the BlockListBlockProvider component below. It will
// add the props necessary for the `editor.BlockListBlock` filters.
BlockListBlock = compose(applyWithDispatch, withFilters('editor.BlockListBlock'))(BlockListBlock);

// This component provides all the information we need through a single store
// subscription (useSelect mapping). Only the necessary props are passed down
// to the BlockListBlock component, which is a filtered component, so these
// props are public API. To avoid adding to the public API, we use a private
// context to pass the rest of the information to the filtered BlockListBlock
// component, and useBlockProps.
function BlockListBlockProvider(props) {
  const {
    clientId,
    rootClientId
  } = props;
  const selectedProps = useSelect(select => {
    const {
      isBlockSelected,
      getBlockMode,
      isSelectionEnabled,
      getTemplateLock,
      getBlockWithoutAttributes,
      getBlockAttributes,
      canRemoveBlock,
      canMoveBlock,
      getSettings,
      getTemporarilyEditingAsBlocks,
      getBlockEditingMode,
      getBlockName,
      isFirstMultiSelectedBlock,
      getMultiSelectedBlockClientIds,
      hasSelectedInnerBlock,
      getBlocksByName,
      getBlockIndex,
      isBlockMultiSelected,
      isBlockSubtreeDisabled,
      isBlockHighlighted,
      __unstableIsFullySelected,
      __unstableSelectionHasUnmergeableBlock,
      isBlockBeingDragged,
      isDragging,
      hasBlockMovingClientId,
      canInsertBlockType,
      __unstableHasActiveBlockOverlayActive,
      __unstableGetEditorMode,
      getSelectedBlocksInitialCaretPosition
    } = unlock(select(blockEditorStore));
    const blockWithoutAttributes = getBlockWithoutAttributes(clientId);

    // This is a temporary fix.
    // This function should never be called when a block is not
    // present in the state. It happens now because the order in
    // withSelect rendering is not correct.
    if (!blockWithoutAttributes) {
      return;
    }
    const {
      hasBlockSupport: _hasBlockSupport,
      getActiveBlockVariation
    } = select(blocksStore);
    const attributes = getBlockAttributes(clientId);
    const {
      name: blockName,
      isValid
    } = blockWithoutAttributes;
    const blockType = getBlockType(blockName);
    const {
      supportsLayout,
      __unstableIsPreviewMode: isPreviewMode
    } = getSettings();
    const hasLightBlockWrapper = blockType?.apiVersion > 1;
    const previewContext = {
      isPreviewMode,
      blockWithoutAttributes,
      name: blockName,
      attributes,
      isValid,
      themeSupportsLayout: supportsLayout,
      index: getBlockIndex(clientId),
      isReusable: isReusableBlock(blockType),
      className: hasLightBlockWrapper ? attributes.className : undefined,
      defaultClassName: hasLightBlockWrapper ? getBlockDefaultClassName(blockName) : undefined,
      blockTitle: blockType?.title
    };

    // When in preview mode, we can avoid a lot of selection and
    // editing related selectors.
    if (isPreviewMode) {
      return previewContext;
    }
    const _isSelected = isBlockSelected(clientId);
    const canRemove = canRemoveBlock(clientId);
    const canMove = canMoveBlock(clientId);
    const match = getActiveBlockVariation(blockName, attributes);
    const isMultiSelected = isBlockMultiSelected(clientId);
    const checkDeep = true;
    const isAncestorOfSelectedBlock = hasSelectedInnerBlock(clientId, checkDeep);
    const movingClientId = hasBlockMovingClientId();
    const blockEditingMode = getBlockEditingMode(clientId);
    const multiple = hasBlockSupport(blockName, 'multiple', true);

    // For block types with `multiple` support, there is no "original
    // block" to be found in the content, as the block itself is valid.
    const blocksWithSameName = multiple ? [] : getBlocksByName(blockName);
    const isInvalid = blocksWithSameName.length && blocksWithSameName[0] !== clientId;
    const editorMode = __unstableGetEditorMode();
    return {
      ...previewContext,
      mode: getBlockMode(clientId),
      isSelectionEnabled: isSelectionEnabled(),
      isLocked: !!getTemplateLock(rootClientId),
      templateLock: getTemplateLock(clientId),
      canRemove,
      canMove,
      isSelected: _isSelected,
      isTemporarilyEditingAsBlocks: getTemporarilyEditingAsBlocks() === clientId,
      blockEditingMode,
      mayDisplayControls: _isSelected || isFirstMultiSelectedBlock(clientId) && getMultiSelectedBlockClientIds().every(id => getBlockName(id) === blockName),
      mayDisplayParentControls: _hasBlockSupport(getBlockName(clientId), '__experimentalExposeControlsToChildren', false) && hasSelectedInnerBlock(clientId),
      blockApiVersion: blockType?.apiVersion || 1,
      blockTitle: match?.title || blockType?.title,
      isSubtreeDisabled: blockEditingMode === 'disabled' && isBlockSubtreeDisabled(clientId),
      hasOverlay: __unstableHasActiveBlockOverlayActive(clientId) && !isDragging(),
      initialPosition: _isSelected && (editorMode === 'edit' || editorMode === 'zoom-out') // Don't recalculate the initialPosition when toggling in/out of zoom-out mode
      ? getSelectedBlocksInitialCaretPosition() : undefined,
      isHighlighted: isBlockHighlighted(clientId),
      isMultiSelected,
      isPartiallySelected: isMultiSelected && !__unstableIsFullySelected() && !__unstableSelectionHasUnmergeableBlock(),
      isDragging: isBlockBeingDragged(clientId),
      hasChildSelected: isAncestorOfSelectedBlock,
      isBlockMovingMode: !!movingClientId,
      canInsertMovingBlock: movingClientId && canInsertBlockType(getBlockName(movingClientId), rootClientId),
      isEditingDisabled: blockEditingMode === 'disabled',
      hasEditableOutline: blockEditingMode !== 'disabled' && getBlockEditingMode(rootClientId) === 'disabled',
      originalBlockClientId: isInvalid ? blocksWithSameName[0] : false
    };
  }, [clientId, rootClientId]);
  const {
    isPreviewMode,
    // Fill values that end up as a public API and may not be defined in
    // preview mode.
    mode = 'visual',
    isSelectionEnabled = false,
    isLocked = false,
    canRemove = false,
    canMove = false,
    blockWithoutAttributes,
    name,
    attributes,
    isValid,
    isSelected = false,
    themeSupportsLayout,
    isTemporarilyEditingAsBlocks,
    blockEditingMode,
    mayDisplayControls,
    mayDisplayParentControls,
    index,
    blockApiVersion,
    blockTitle,
    isSubtreeDisabled,
    hasOverlay,
    initialPosition,
    isHighlighted,
    isMultiSelected,
    isPartiallySelected,
    isReusable,
    isDragging,
    hasChildSelected,
    isBlockMovingMode,
    canInsertMovingBlock,
    templateLock,
    isEditingDisabled,
    hasEditableOutline,
    className,
    defaultClassName,
    originalBlockClientId
  } = selectedProps;

  // Users of the editor.BlockListBlock filter used to be able to
  // access the block prop.
  // Ideally these blocks would rely on the clientId prop only.
  // This is kept for backward compatibility reasons.
  const block = useMemo(() => ({
    ...blockWithoutAttributes,
    attributes
  }), [blockWithoutAttributes, attributes]);

  // Block is sometimes not mounted at the right time, causing it be
  // undefined see issue for more info
  // https://github.com/WordPress/gutenberg/issues/17013
  if (!selectedProps) {
    return null;
  }
  const privateContext = {
    isPreviewMode,
    clientId,
    className,
    index,
    mode,
    name,
    blockApiVersion,
    blockTitle,
    isSelected,
    isSubtreeDisabled,
    hasOverlay,
    initialPosition,
    blockEditingMode,
    isHighlighted,
    isMultiSelected,
    isPartiallySelected,
    isReusable,
    isDragging,
    hasChildSelected,
    isBlockMovingMode,
    canInsertMovingBlock,
    templateLock,
    isEditingDisabled,
    hasEditableOutline,
    isTemporarilyEditingAsBlocks,
    defaultClassName,
    mayDisplayControls,
    mayDisplayParentControls,
    originalBlockClientId,
    themeSupportsLayout
  };

  // Here we separate between the props passed to BlockListBlock and any other
  // information we selected for internal use. BlockListBlock is a filtered
  // component and thus ALL the props are PUBLIC API.

  // Note that the context value doesn't have to be memoized in this case
  // because when it changes, this component will be re-rendered anyway, and
  // none of the consumers (BlockListBlock and useBlockProps) are memoized or
  // "pure". This is different from the public BlockEditContext, where
  // consumers might be memoized or "pure".
  return /*#__PURE__*/_jsx(PrivateBlockContext.Provider, {
    value: privateContext,
    children: /*#__PURE__*/_jsx(BlockListBlock, {
      ...props,
      mode,
      isSelectionEnabled,
      isLocked,
      canRemove,
      canMove,
      // Users of the editor.BlockListBlock filter used to be able
      // to access the block prop. Ideally these blocks would rely
      // on the clientId prop only. This is kept for backward
      // compatibility reasons.
      block,
      name,
      attributes,
      isValid,
      isSelected
    })
  });
}
export default memo(BlockListBlockProvider);
//# sourceMappingURL=block.js.map