/**
 * React helper around createAttributeHistory for Morph document state.
 */

import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import {
	cloneState,
	createAttributeHistory,
	isMorphContentEditTarget,
	isNativeTextInput,
	isRedoKeyEvent,
	isUndoKeyEvent,
} from './attributeHistory';

/**
 * @param {object} options
 * @param {string} [options.storageKey]
 * @param {() => object} options.getState
 * @param {(next: object) => void} options.applyState
 * @param {boolean} [options.enabled]
 * @param {boolean} [options.bindKeys]
 * @param {boolean} [options.persist]
 * @return {{
 *   history: ReturnType<typeof createAttributeHistory>,
 *   meta: { canUndo: boolean, canRedo: boolean, pastSize: number, futureSize: number },
 *   record: (before: object, after: object) => boolean,
 *   beginBatch: (base?: object) => void,
 *   endBatch: (after?: object) => boolean,
 *   undo: () => boolean,
 *   redo: () => boolean,
 *   clear: () => void,
 *   trackSetter: (key: string, setState: Function) => Function,
 * }}
 */
export function useDocumentHistory( {
	storageKey,
	getState,
	applyState,
	enabled = true,
	bindKeys = true,
	persist = true,
} ) {
	const getStateRef = useRef( getState );
	const applyStateRef = useRef( applyState );
	getStateRef.current = getState;
	applyStateRef.current = applyState;

	const historyRef = useRef( null );
	if ( ! historyRef.current ) {
		historyRef.current = createAttributeHistory( {
			storageKey,
			persist: Boolean( persist && storageKey ),
			getState: () => getStateRef.current?.() || {},
			applyState: ( next ) => {
				applyStateRef.current?.( next );
			},
		} );
	}

	const [ meta, setMeta ] = useState( () =>
		historyRef.current.getMeta()
	);

	useEffect( () => {
		const history = historyRef.current;
		history.setAccessors( {
			getState: () => getStateRef.current?.() || {},
			applyState: ( next ) => {
				applyStateRef.current?.( next );
			},
		} );
		const unsubscribe = history.subscribe( setMeta );
		return () => {
			unsubscribe();
		};
	}, [] );

	useEffect( () => {
		return () => {
			historyRef.current?.dispose();
			historyRef.current = null;
		};
	}, [] );

	// Re-key storage when the editor document identity changes.
	const storageKeyRef = useRef( storageKey );
	useEffect( () => {
		if ( storageKeyRef.current === storageKey ) {
			return;
		}
		storageKeyRef.current = storageKey;
		const prev = historyRef.current;
		prev?.dispose();
		historyRef.current = createAttributeHistory( {
			storageKey,
			persist: Boolean( persist && storageKey ),
			getState: () => getStateRef.current?.() || {},
			applyState: ( next ) => {
				applyStateRef.current?.( next );
			},
		} );
		setMeta( historyRef.current.getMeta() );
		const unsubscribe = historyRef.current.subscribe( setMeta );
		return () => {
			unsubscribe();
		};
	}, [ storageKey, persist ] );

	const record = useCallback( ( before, after ) => {
		if ( ! enabled ) {
			return false;
		}
		return historyRef.current?.record( before, after ) || false;
	}, [ enabled ] );

	const beginBatch = useCallback( ( base ) => {
		historyRef.current?.beginBatch( base );
	}, [] );

	const endBatch = useCallback( ( after ) => {
		if ( ! enabled ) {
			historyRef.current?.cancelBatch();
			return false;
		}
		return historyRef.current?.endBatch( after ) || false;
	}, [ enabled ] );

	const undo = useCallback( () => {
		if ( ! enabled ) {
			return false;
		}
		return historyRef.current?.undo() || false;
	}, [ enabled ] );

	const redo = useCallback( () => {
		if ( ! enabled ) {
			return false;
		}
		return historyRef.current?.redo() || false;
	}, [ enabled ] );

	const clear = useCallback( () => {
		historyRef.current?.clear();
	}, [] );

	const cancelBatch = useCallback( () => {
		historyRef.current?.cancelBatch();
	}, [] );

	/**
	 * Wrap a React setState so each change records document history for `key`.
	 * Coalesces updates within the same animation frame into one undo step.
	 *
	 * @param {string}   key
	 * @param {Function} setState
	 * @return {Function}
	 */
	const trackSetter = useCallback(
		( key, setState ) => {
			let frame = 0;
			let frameBefore = null;

			return ( updater ) => {
				const history = historyRef.current;
				if ( ! enabled || ! history || history.isApplying() ) {
					setState( updater );
					return;
				}

				const docBefore = cloneState( getStateRef.current?.() || {} );
				if ( frameBefore == null ) {
					frameBefore = docBefore;
				}

				setState( ( prev ) => {
					const next =
						typeof updater === 'function'
							? updater( prev )
							: updater;
					return next;
				} );

				if ( frame ) {
					return;
				}
				frame = window.requestAnimationFrame( () => {
					frame = 0;
					const before = frameBefore;
					frameBefore = null;
					const after = cloneState( getStateRef.current?.() || {} );
					if ( before ) {
						historyRef.current?.record( before, after );
					}
				} );
			};
		},
		[ enabled ]
	);

	useEffect( () => {
		if ( ! enabled || ! bindKeys ) {
			return undefined;
		}

		const onKeyDown = ( event ) => {
			const history = historyRef.current;
			if ( ! history ) {
				return;
			}
			const undoHit = isUndoKeyEvent( event );
			const redoHit = isRedoKeyEvent( event );
			if ( ! undoHit && ! redoHit ) {
				return;
			}

			// Let Morph CE / wysiwyg session handlers own the shortcut.
			if ( isMorphContentEditTarget( event.target ) ) {
				return;
			}
			// Leave native inputs to the browser.
			if ( isNativeTextInput( event.target ) ) {
				return;
			}

			if ( undoHit && history.canUndo() ) {
				event.preventDefault();
				event.stopPropagation();
				history.undo();
				return;
			}
			if ( redoHit && history.canRedo() ) {
				event.preventDefault();
				event.stopPropagation();
				history.redo();
			}
		};

		window.addEventListener( 'keydown', onKeyDown, true );
		return () => {
			window.removeEventListener( 'keydown', onKeyDown, true );
		};
	}, [ enabled, bindKeys ] );

	return {
		history: historyRef.current,
		meta,
		record,
		beginBatch,
		endBatch,
		cancelBatch,
		undo,
		redo,
		clear,
		trackSetter,
	};
}
