/**
 * External dependencies
 */
import { isEqual } from 'lodash'

/**
 * WordPress dependencies
 */
import { useState, useEffect, useCallback } from '@wordpress/element'
import isShallowEqual from '@wordpress/is-shallow-equal'
import { getWidgetIdFromBlock, addWidgetIdToBlock } from '@wordpress/widgets'

/**
 * Internal dependencies
 */
import { blockToWidget, widgetToBlock } from '../../utils'

function widgetsToBlocks(widgets) {
	return widgets.map((widget) => widgetToBlock(widget))
}

export default function useSidebarBlockEditor(sidebar) {
	const [blocks, setBlocks] = useState(() =>
		widgetsToBlocks(sidebar.getWidgets())
	)

	useEffect(() => {
		return sidebar.subscribe((prevWidgets, nextWidgets) => {
			setBlocks((prevBlocks) => {
				const prevWidgetsMap = new Map(
					prevWidgets.map((widget) => [widget.id, widget])
				)

				const prevBlocksMap = new Map(
					prevBlocks.map((block) => [
						getWidgetIdFromBlock(block),
						block,
					])
				)

				const nextBlocks = nextWidgets.map((nextWidget) => {
					const prevWidget = prevWidgetsMap.get(nextWidget.id)

					// Bail out updates.
					if (prevWidget && prevWidget === nextWidget) {
						return prevBlocksMap.get(nextWidget.id)
					}

					return widgetToBlock(nextWidget)
				})

				// Bail out updates.
				if (isShallowEqual(prevBlocks, nextBlocks)) {
					return prevBlocks
				}

				return nextBlocks
			})
		})
	}, [sidebar])

	const onChangeBlocks = useCallback(
		(nextBlocks) => {
			setBlocks((prevBlocks) => {
				if (isShallowEqual(prevBlocks, nextBlocks)) {
					return prevBlocks
				}

				const prevBlocksMap = new Map(
					prevBlocks.map((block) => [
						getWidgetIdFromBlock(block),
						block,
					])
				)

				const nextWidgets = nextBlocks.map((nextBlock) => {
					const widgetId = getWidgetIdFromBlock(nextBlock)

					// Update existing widgets.
					if (widgetId && prevBlocksMap.has(widgetId)) {
						const prevBlock = prevBlocksMap.get(widgetId)
						const prevWidget = sidebar.getWidget(widgetId)

						// Bail out updates by returning the previous widgets.
						// Deep equality is necessary until the block editor's internals changes.
						if (isEqual(nextBlock, prevBlock) && prevWidget) {
							return prevWidget
						}

						return blockToWidget(nextBlock, prevWidget)
					}

					// Add a new widget.
					return blockToWidget(nextBlock)
				})

				// Bail out updates if the updated widgets are the same.
				if (isShallowEqual(sidebar.getWidgets(), nextWidgets)) {
					return prevBlocks
				}

				const addedWidgetIds = sidebar.setWidgets(nextWidgets)

				return nextBlocks.reduce(
					(updatedNextBlocks, nextBlock, index) => {
						const addedWidgetId = addedWidgetIds[index]

						if (addedWidgetId !== null) {
							// Only create a new instance if necessary to prevent
							// the whole editor from re-rendering on every edit.
							if (updatedNextBlocks === nextBlocks) {
								updatedNextBlocks = nextBlocks.slice()
							}

							updatedNextBlocks[index] = addWidgetIdToBlock(
								nextBlock,
								addedWidgetId
							)
						}

						return updatedNextBlocks
					},
					nextBlocks
				)
			})
		},
		[sidebar]
	)

	return [blocks, onChangeBlocks, onChangeBlocks]
}
