/**
 * Helper functions for managing and retrieving information for the Loop Builder in Spectra Pro.
 *
 * This file contains various utility functions that aid in managing post types, taxonomies,
 * and handling related data for the Loop Builder block. Functions include:
 * - Retrieving and filtering post types and taxonomies.
 * - Mapping and structuring entities for efficient lookup and retrieval.
 * - Managing controls for the Loop Builder block and ensuring accessibility.
 * - Providing helper functions for querying and manipulating data paths within objects.
 *
 * Dependencies:
 * - WordPress Data Store (`@wordpress/data`)
 * - WordPress Core Data (`@wordpress/core-data`)
 * - WordPress Blocks Store (`@wordpress/blocks`)
 * - WordPress HTML Entities (`@wordpress/html-entities`)
 * - WordPress I18n (`@wordpress/i18n`)
 *
 * Functions:
 * - `usePostTypes`: Fetches post types and returns their associated taxonomies, format support, and select options.
 * - `useIsPostTypeHierarchical`: Determines if a specific post type is hierarchical.
 * - `useTaxonomies`: Retrieves taxonomies associated with a specific post type.
 * - `useAllowedControls`: Returns allowed controls for the Loop Builder block based on active block variation.
 * - `isControlAllowed`: Checks if a control is allowed based on the list of allowed controls.
 * - `getValueFromObjectPath`: Fetches a value from an object based on a specified path.
 * - `mapToIHasNameAndId`: Maps entities to implement the `IHasNameAndId` interface.
 * - `getEntitiesInfo`: Generates a structured object with entities mapped by ID and name for efficient lookup.
 * - `htmlElementMessages`: Contains predefined messages for various HTML elements to guide proper usage in the Loop Builder block.
 *
 * @since x.x.x
 */

/**
 * External dependencies.
 */
import { useSelect } from '@wordpress/data';
import { useMemo } from '@wordpress/element';
import { store as coreStore } from '@wordpress/core-data';
import { store as blocksStore } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';

/**
 * Defines a basic structure for objects that have both an ID and a name.
 * Useful for handling various entities like posts, taxonomies, categories, etc.
 *
 * @since x.x.x
 *
 * @typedef IHasNameAndId
 * @property {string|number} id   The entity's unique identifier.
 * @property {string}        name The entity's human-readable name.
 */

/**
 * Represents a structured collection of query entities along with helper mappings
 * for easier retrieval and lookup of entities by their ID or name.
 *
 * This is useful for efficiently managing lists of posts, taxonomies, categories, etc.
 *
 *
 * @since x.x.x
 *
 * @typedef {Object} QueryEntitiesInfo
 * @property {IHasNameAndId[]}               entities  The array of entities.
 * @property {Object<string, IHasNameAndId>} mapById   Object mapping entity ID to entity object.
 * @property {Object<string, IHasNameAndId>} mapByName Object mapping entity name to entity object.
 * @property {string[]}                      names     Array containing entity names.
 */

/**
 * Retrieves post types along with associated taxonomies and post format support.
 *
 * @since x.x.x
 *
 * @return {Object} Object containing post type options, taxonomy mappings, and format support.
 */
export const usePostTypes = () => {
	// Fetch available post types using WordPress data store.
	const postTypes = useSelect( ( select ) => {
		const { getPostTypes } = select( coreStore );
		const excludedPostTypes = [ 'attachment' ];

		// Filter out non-viewable post types and excluded types.
		const filteredPostTypes = getPostTypes( { per_page: -1 } )?.filter(
			( { viewable, slug } ) => viewable && ! excludedPostTypes.includes( slug )
		);
		return filteredPostTypes;
	}, [] );

	/**
	 * Creates a memoized mapping of post types to their associated taxonomies.
	 *
	 * @since x.x.x
	 *
	 * @return {Object} Map of post type slugs to their respective taxonomies.
	 */
	const postTypesTaxonomiesMap = useMemo( () => {
		if ( ! postTypes?.length ) {
			return;
		}

		// Build taxonomy map for each post type
		return postTypes.reduce( ( accumulator, type ) => {
			accumulator[ type.slug ] = type.taxonomies;
			return accumulator;
		}, {} );
	}, [ postTypes ] );

	/**
	 * Generates options for use in a `<SelectControl>` dropdown.
	 *
	 * @since x.x.x
	 *
	 * @return {Array} Array of post type label-value pairs.
	 */
	const postTypesSelectOptions = useMemo(
		() =>
			( postTypes || [] ).map( ( { labels, slug } ) => ( {
				label: labels.singular_name,
				value: slug,
			} ) ),
		[ postTypes ]
	);

	/**
	 * Creates a memoized mapping of post types to their post format support.
	 *
	 * @since x.x.x
	 *
	 * @return {Object} Map of post type slugs to boolean values indicating post format support.
	 */
	const postTypeFormatSupportMap = useMemo( () => {
		if ( ! postTypes?.length ) {
			return {};
		}

		// Build support map for post formats.
		return postTypes.reduce( ( accumulator, type ) => {
			accumulator[ type.slug ] = type.supports?.[ 'post-formats' ] || false;

			return accumulator;
		}, {} );
	}, [ postTypes ] );

	return {
		postTypesTaxonomiesMap,
		postTypesSelectOptions,
		postTypeFormatSupportMap,
	};
};

/**
 * Determines whether a specific post type is hierarchical.
 *
 * @since x.x.x
 *
 * @param {string} postType The post type to check.
 * @return {boolean} True if the post type is hierarchical, false otherwise.
 */
export const useIsPostTypeHierarchical = ( postType ) => {
	return useSelect(
		( select ) => {
			const type = select( coreStore ).getPostType( postType );

			// Return false if type doesn't exist or isn't viewable.
			return type?.viewable && type?.hierarchical;
		},
		[ postType ]
	);
};

/**
 * Hook that returns the taxonomies associated with a specific post type.
 *
 * @since x.x.x
 *
 * @param {string} postType The post type from which to retrieve the associated taxonomies.
 * @return {Object[]} An array of the associated taxonomies.
 */
export const useTaxonomies = ( postType ) => {
	const taxonomies = useSelect(
		( select ) => {
			const { getTaxonomies, getPostType } = select( coreStore );

			// Does the post type have taxonomies?.
			if ( getPostType( postType )?.taxonomies?.length > 0 ) {
				return getTaxonomies( {
					type: postType,
					per_page: -1,
				} );
			}

			// Return empty array if no taxonomies exist for post type
			return [];
		},
		[ postType ]
	);

	// Filter taxonomies to only include publicly queryable ones.
	return useMemo( () => {
		return taxonomies?.filter( ( { visibility } ) => !! visibility?.publicly_queryable );
	}, [ taxonomies ] );
};

/**
 * Hook that returns the query properties' names defined by the active
 * block variation, to determine which block's filters to show.
 *
 * @since x.x.x
 *
 * @param {Object} attributes Block attributes.
 * @return {string[]} An array of the query attributes.
 */
export const useAllowedControls = ( attributes ) => {
	return useSelect(
		( select ) =>
			select( blocksStore ).getActiveBlockVariation( 'spectra-pro/loop-builder', attributes )?.allowedControls,

		[ attributes ]
	);
};

/**
 * Checks if a control is allowed based on the allowed controls list.
 *
 * If the list is not defined, every control is allowed.
 *
 * @since x.x.x
 *
 * @param {string[]} allowedControls List of allowed controls.
 * @param {string}   key             Control key to check.
 * @return {boolean} Is the control allowed?
 */
export const isControlAllowed = ( allowedControls, key ) => {
	// Every controls is allowed if the list is not defined.
	if ( ! allowedControls ) {
		return true;
	}

	return allowedControls.includes( key );
};

/**
 * Helper util to return a value from a certain path of the object.
 * Path is specified as a string of properties, separated by dots,
 * for example: "parent.child".
 *
 * @since x.x.x
 *
 * @param {Object} object Input object.
 * @param {string} path   Path to the object property.
 * @return {*} Value of the object property at the specified path.
 */
export const getValueFromObjectPath = ( object, path ) => {
	const normalizedPath = path.split( '.' );
	let value = object;

	// Normalize path and get value.
	normalizedPath.forEach( ( fieldName ) => {
		value = value?.[ fieldName ];
	} );
	return value;
};

/**
 * Helper util to map records to add a `name` prop from a
 * provided path, in order to handle all entities in the same
 * fashion(implementing`IHasNameAndId` interface).
 *
 * @since x.x.x
 *
 * @param {Object[]} entities The array of entities.
 * @param {string}   path     The path to map a `name` property from the entity.
 * @return {IHasNameAndId[]} An array of entities that now implement the `IHasNameAndId` interface.
 */
export const mapToIHasNameAndId = ( entities, path ) => {
	return ( entities || [] ).map( ( entity ) => ( {
		...entity,
		name: decodeEntities( getValueFromObjectPath( entity, path ) ),
	} ) );
};

/**
 * Returns a helper object with mapping from Objects that implement
 * the `IHasNameAndId` interface. The returned object is used for
 * integration with `FormTokenField` component.
 *
 * @since x.x.x
 *
 * @param {IHasNameAndId[]} entities The entities to extract of helper object.
 * @return {QueryEntitiesInfo} The object with the entities information.
 */
export const getEntitiesInfo = ( entities ) => {
	/**
	 * Creates a mapping object by reducing the entities array into three structures:
	 * mapById, mapByName, and names array. Uses optional chaining to handle undefined entities.
	 */
	const mapping = entities?.reduce(
		// Reducer function that builds the mapping object for each entity.
		( accumulator, entity ) => {
			// Destructure the accumulator to get individual mapping components.
			const { mapById, mapByName, names } = accumulator;

			// Maps the entity to its ID in the mapById object for quick ID-based lookup.
			mapById[ entity.id ] = entity;

			// Maps the entity to its name in the mapByName object for quick name-based lookup.
			mapByName[ entity.name ] = entity;

			// Adds the entity name to the names array for FormTokenField suggestions.
			names.push( entity.name );

			// Returns the updated accumulator for the next iteration.
			return accumulator;
		},
		{ mapById: {}, mapByName: {}, names: [] }
	);

	//Returns the final object combining the original entities with the generated mappings.Spreads the mapping object to include mapById, mapByName, and names properties.
	return {
		entities,
		...mapping,
	};
};

/**
 * An object with messages for different HTML elements that can be used to
 * determine the allowed HTML elements for the `Loop Builder` block.
 *
 * @since x.x.x
 *
 * @type {Object<string, string>}
 */
export const htmlElementMessages = {
	article: __(
		'The <article> element should represent a self-contained, syndicatable portion of the document.',
		'spectra-pro'
	),
	aside: __(
		"The <aside> element should represent a portion of a document whose content is only indirectly related to the document's main content.",
		'spectra-pro'
	),
	div: __(
		'The <div> element should only be used if the block is a design element with no semantic meaning.',
		'spectra-pro'
	),
	footer: __(
		'The <footer> element should represent a footer for its nearest sectioning element (e.g.: <section>, <article>, <main> etc.).',
		'spectra-pro'
	),
	header: __(
		'The <header> element should represent introductory content, typically a group of introductory or navigational aids.',
		'spectra-pro'
	),
	main: __( 'The <main> element should be used for the primary content of your document only.', 'spectra-pro' ),
	nav: __(
		'The <nav> element should be used to identify groups of links that are intended to be used for website or page content navigation.',
		'spectra-pro'
	),
	section: __(
		"The <section> element should represent a standalone portion of the document that can't be better represented by another element.",
		'spectra-pro'
	),
};
