/**
 * External dependencies.
 */
import { v4 as uuidv4 } from 'uuid';

/**
 * WordPress dependencies.
 */
import {
	isEqual,
	zip
} from 'lodash';

import {
	dispatch,
	select
} from '@wordpress/data';

import {
	parse
} from '@wordpress/blocks';

import { useEffect, useMemo, useRef, useState } from '@wordpress/element';

/**
 * Initiate the global id tracker with an empty list if it is the case.
 */
window.CoolPluginsGutenberg ??= {};
window.CoolPluginsGutenberg.blockIDs ??= [];

/**
 * Utiliy function for getting the default value of the attribute.
 *
 * @param {string}   name              The block's name provided by WordPress
 * @param {string}   field             Name of the value to be returned
 * @param {Object}   defaultAttributes The default attributes of the block.
 */
export const getDefaultValue = ( name, field, defaultAttributes ) => {
	const blockDefaults = window.CoolPluginsGutenberg.globalDefaults?.[name];
	const value = blockDefaults?.[field] ? blockDefaults?.[field] : defaultAttributes[field]?.default;

	return value;
};

/**
 * An object that keep tracking of the block instances. Is used for preventing id duplication on action like: create, duplicate, copy on editor page.
 *
 * @type {Object.<string, Set.<string>>}
 */
const localIDs = {};

/**
 * Check if the ID is inside a reusable block or a Query Loop.
 * @param {string} clientId The client id of the block.
 * @returns {boolean}
 */
const isSharedBlock = ( clientId ) => getBlockParents( clientId )?.some( id => {
	const { attributes, name } = getBlock( id ) ?? {};
	return 'core/query' === name || attributes?.ref;
});

/**
 * Generate an Id based on the client id of the block. If the new id is also already used, create a new one using the `uuid`.
 *
 * @param {string}       idPrefix The prefix used for generating the block id
 * @param {string}       clientId The block's client id provided by WordPress
 * @param {Set.<string>} idsList  The ids list for the current type of block
 * @returns An uniq id instance
 */
const generateUniqIdInstance = ( idPrefix, clientId, idsList ) => {

	const instanceId = `${ idPrefix }${ clientId.slice( 0, 8 ) }`;
	if ( idsList.has( instanceId ) ) {
		let newInstanceId = `${ idPrefix }${ uuidv4().slice( 0, 8 ) }`;
		while ( idsList.has( newInstanceId ) ) {
			newInstanceId = `${ idPrefix }${ uuidv4().slice( 0, 8 ) }`;
		}
		return newInstanceId;
	}
	return instanceId;
};

/**
 * Generate the id prefix based on the name of the block
 *
 * @param {string} name Name of the block
 * @returns {string}
 */
const generatePrefix = ( name ) => {
	return `wp-block-${ name.replace( '/', '-' ) }-`;
};

const idGenerationStatus = {};

/**
 * THe args definition for the block id generator
 *
 * @typedef {Object} AddBlockIdProps
 * @property {Object}             attributes        The block's attributes provided by WordPress
 * @property {Function}           setAttributes     The block's attributes update function provided by WordPress
 * @property {string}             name              The block's name provided by WordPress
 * @property {string}             clientId          The block's client id provided by WordPress
 * @property {Object}             defaultAttributes The default attributes of the block.
 * @property {(string|undefined)} idPrefix          (Optional) The prefix used for generating the block id
 */

/**
 * Generate an Id for block so that it will create a conlfict with the others.
 * Prevent the duplicate Id for actions like: duplicate, copy
 *
 * @param {AddBlockIdProps} args Block informatin about clientId, attributes, etc
 * @return {Function} A function that clean up the id from the internal list tracking
 * @external addBlockId
 */
export const addBlockId = ( args ) => {

	const { attributes, setAttributes, clientId, idPrefix, name, defaultAttributes } = args;
	idGenerationStatus[clientId] = 'busy';

	/**
	 * Create an alias for the global id tracker
	 *
	 * @type {Array.<string>}
	 */
	const blockIDs = window.CoolPluginsGutenberg?.blockIDs ?? [];

	if ( attributes === undefined || setAttributes === undefined ) {
		return ( savedId ) => {
			localIDs[name]?.delete( savedId );
		};
	}

	// Initialize with an empty array the id list for the given block
	localIDs[name] ??= new Set();

	// Check if the ID is already used. EXCLUDE the one that come from reusable blocks.
	const idIsAlreadyUsed = Boolean( attributes.id && localIDs[name].has( attributes.id ) );

	if ( attributes.id === undefined || idIsAlreadyUsed ) {

		// Auto-generate idPrefix if not provided
		const prefix = idPrefix || generatePrefix( name );
		const instanceId = generateUniqIdInstance( prefix, clientId, localIDs[name]);

		if ( attributes.id === undefined ) {

			// If the id is undefined, then the block is newly created, and so we need to apply the Global Defaults
			// addGlobalDefaults( attributes, setAttributes, name, defaultAttributes );

			// Save the id in all methods
			localIDs[name].add( instanceId );
			blockIDs.push( instanceId );
			setAttributes({ id: instanceId });

			return ( savedId ) => {
				return ( savedId ) => {};
			};

		} else if ( idIsAlreadyUsed ) {

			// The block must be a copy and its is already used
			// Generate a new one and save it to `localIDs` to keep track of it in local mode.
			localIDs[name].add( instanceId );
			setAttributes({ id: instanceId });

			return ( savedId ) => {};
		}
	} else {

		// No conflicts, save the current id only to keep track of it both in local and global mode.
		localIDs[name].add( attributes.id );
		blockIDs.push( attributes.id );
	}

	return ( savedId ) => {
		idGenerationStatus[clientId] = 'free';
		localIDs[name].delete( savedId || attributes?.id );
	};
};

const { getBlock } = select( 'core/block-editor' );
const { getBlockParents } = select( 'core/block-editor' );
const { updateBlockAttributes } = dispatch( 'core/block-editor' );
const { getSelectedBlockClientId } = select( 'core/block-editor' );

/**
 * Create the function that behaves like `setAttributes` using the client id
 *
 * @param {*} clientId The block's client id provided by WordPress
 * @returns {Function} Function that mimics `setAttributes`
 */
const updateAttrs = ( clientId ) => ( attr ) => {
	updateBlockAttributes( clientId, attr );
};

/**
 * Extract the attributes, setAttributes, and the name of the block using the data api
 *
 * @param {string} clientId The block's client id provided by WordPress
 * @returns {BlockData}
 */
const extractBlockData = ( clientId ) => {
	const block = getBlock( clientId );
	return { attributes: block?.attributes, name: block?.name };
};


/**
 * Generate the id attribute for the given block.
 * This function is a simple wrapper around {@link addBlockId}
 *
 * @param {string} clientId          The block's client id provided by WordPress
 * @param {Object} defaultAttributes The default attributes of the block.
 * @return {Function} A function that clean up the id from the internal list tracking
 * @example
 * import defaultAttributes from './attributes'
 * const Block = ({ clientId }) => {
 * 		useEffect(() => {
 * 			const unsubscribe = blockInit(clientId, defaultAttributes);
 * 			return () => unsubscribe( attributes.id );
 * 		}, [ attributes.id ])
 * }
 */
export const blockInit = ( clientId, defaultAttributes ) => {
	if ( undefined === idGenerationStatus[clientId]) {
		idGenerationStatus[clientId] = 'free';
	}

	return (
		'busy' !== idGenerationStatus[clientId] &&
		( ! isSharedBlock( clientId ) || getSelectedBlockClientId() === clientId )
	) ?
		addBlockId({
			clientId,
			defaultAttributes,
			setAttributes: updateAttrs( clientId ),
			...extractBlockData( clientId )
		}) : () => {};
};


/**
 * Create a Style node for handling `head` Node change when working in a Tablet, Mobile mode or in FSE Editor.
 *
 * @param {import('./blocks.js').cfbNodeCSSOptions } options The options.
 * @returns {import('./blocks.js').cfbNodeCSSReturn} The name of the node and function handler.
 */
export const useCSSNode = ( options = {}) => {
	const [ cssList, setCSSProps ] = useState({
		css: [],
		media: []
	});
	const [ settings, setSettings ] = useState({
		node: null,
		cssNodeName: ''
	});

	/**
	 *	Set CSS of the node.
	 *
	 * The `css` and `media` have a 1-1 relationship.
	 *
	 * @param {string[]} css A list with CSS code.
	 * @param {string[]} media A list CSS media options. One for each CSS item.
	 *
	 * @example Simple usage.
	 *
	 */
	const setNodeCSS = ( css = [], media = []) => {
		setCSSProps({
			css,
			media
		});
	};

	useEffect( () => {

		let anchor;

		// Create the CSS node.
		const n = document.createElement( 'style' );
		n.type = 'text/css';
		n.setAttribute( 'data-generator', 'cfb-blocks' );

		setTimeout( () => {

			// A small delay for the iFrame to properly initialize.
			anchor = parent.document.querySelector( 'iframe[name="editor-canvas"]' )?.contentWindow.document.head || document.head;
			anchor?.appendChild( n );
		}, 500 );

		setSettings({
			node: n,
			cssNodeName: options?.selector ?? `cfb-block-node-${uuidv4()}`
		});

		return () => {
			anchor?.removeChild( n );
		};
	}, [ ]);

	useEffect( () => {
		if ( settings.node && settings.cssNodeName && cssList.media !== undefined ) {

			// Create the CSS text by combining the list of CSS items with their media..
			const text =  zip( cssList.css, cssList.media )
				.map( x => {
					const [ css, media ] = x;
					if ( media ) {
						return `${media} { \n\t .${settings.cssNodeName}${options?.appendToRoot ? '' : ' '}${css} }`;
					}
					return `.${settings.cssNodeName}${options?.appendToRoot ? '' : ' '}${css}`;
				})
				.join( '\n' ) || '';
			settings.node.textContent = text;
		}
	}, [ cssList.css, cssList.media, settings.node, settings.cssNodeName ]);

	return [ settings.cssNodeName, setNodeCSS, setSettings ];
};


