import { Y } from '@wordpress/sync';
import {
	asRichTextOffset,
	findBlockByClientIdInDoc,
	getYTextByAttributeKey,
	richTextOffsetToHtmlIndex,
} from './crdt-utils';
import type { WPBlockSelection, WPSelection } from '../types';

// Default size for selection history (not including current selection)
const SELECTION_HISTORY_DEFAULT_SIZE = 5;

export enum YSelectionType {
	RelativeSelection = 'RelativeSelection',
	BlockSelection = 'BlockSelection',
}

export interface YRelativeSelection {
	type: YSelectionType.RelativeSelection;
	attributeKey: string;
	relativePosition: Y.RelativePosition;
	clientId: string;
	offset: number;
}

export interface YBlockSelection {
	type: YSelectionType.BlockSelection;
	clientId: string;
}

export type YSelection = YRelativeSelection | YBlockSelection;

export type YFullSelection = {
	start: YSelection;
	end: YSelection;
};

export interface YSelectionHistory {
	selection: YFullSelection;
	backupSelections?: YFullSelection[];
}

export interface BlockSelectionHistory {
	getSelectionHistory: () => YFullSelection[];
	updateSelection: ( newSelection: WPSelection ) => void;
}

/**
 * This function is used to track recent block selections to help in restoring
 * a user's selection after an undo or redo operation.
 *
 * Maintains a history array for previous selections, which can be used for
 * backup restoration locations.
 * @param ydoc
 * @param historySize
 */
export function createBlockSelectionHistory(
	ydoc: Y.Doc,
	historySize: number = SELECTION_HISTORY_DEFAULT_SIZE
): BlockSelectionHistory {
	let history: YFullSelection[] = [];

	/**
	 * Get the block history including current selection.
	 */
	const getSelectionHistory = (): YFullSelection[] => {
		return history.slice( 0 );
	};

	/**
	 * Update the selection history with a new selection.
	 * @param newSelection
	 */
	const updateSelection = ( newSelection: WPSelection ): void => {
		if (
			! newSelection?.selectionStart?.clientId ||
			! newSelection?.selectionEnd?.clientId
		) {
			return;
		}

		const { selectionStart, selectionEnd } = newSelection;
		const start = convertWPBlockSelectionToSelection(
			selectionStart,
			ydoc
		);
		const end = convertWPBlockSelectionToSelection( selectionEnd, ydoc );

		addToHistory( { start, end } );
	};

	/**
	 * Add a selection to the history, maintaining only the last `historySize` unique selections.
	 * New selections are added to the front.
	 * Removes any existing entries with the same start and end block combination.
	 * @param yFullSelection
	 */
	const addToHistory = ( yFullSelection: YFullSelection ): void => {
		// Remove any existing entries with the same start and end block combination
		const startClientId = yFullSelection.start.clientId;
		const endClientId = yFullSelection.end.clientId;

		history = history.filter( ( entry ) => {
			const isSameBlockCombination =
				entry.start.clientId === startClientId &&
				entry.end.clientId === endClientId;

			return ! isSameBlockCombination;
		} );

		// Add the new selection to the front
		history.unshift( yFullSelection );

		// Trim to max size (remove oldest entries from the back)
		if ( history.length > historySize + 1 ) {
			history = history.slice( 0, historySize + 1 );
		}
	};

	return {
		getSelectionHistory,
		updateSelection,
	};
}

/**
 * Convert a WPBlockSelection to a YSelection.
 * @param selection
 * @param ydoc
 * @return A YSelection object.
 */
function convertWPBlockSelectionToSelection(
	selection: WPBlockSelection,
	ydoc: Y.Doc
): YSelection {
	const clientId = selection.clientId;
	const block = findBlockByClientIdInDoc( clientId, ydoc );
	const attributes = block?.get( 'attributes' );
	const attributeKey = selection.attributeKey;

	let changedYText: Y.Text | null = null;
	if ( attributeKey && attributes ) {
		changedYText = getYTextByAttributeKey( attributes, attributeKey );
	}

	if (
		! ( changedYText instanceof Y.Text ) ||
		! attributeKey ||
		! clientId
	) {
		// We either don't have a valid YText (it's been deleted) or we've
		// been passed a selection that's just a block clientId.
		// Store as BlockSelection.
		return {
			type: YSelectionType.BlockSelection,
			clientId,
		};
	}

	const offset = selection.offset ?? 0;
	const relativePosition = Y.createRelativePositionFromTypeIndex(
		changedYText,
		richTextOffsetToHtmlIndex(
			changedYText.toString(),
			asRichTextOffset( offset )
		)
	);

	return {
		type: YSelectionType.RelativeSelection,
		attributeKey,
		relativePosition,
		clientId,
		offset,
	};
}
