/**
 * WP Post object. Only properties needed by the code are included.
 *
 * @typedef {object} WPPost
 * @property {number}   id             ID of post.
 * @property {number}   author         Post author ID.
 * @property {number[]} categories     IDs of associated categories.
 * @property {number[]} tags           IDs of associated tags.
 * @property {number}   featured_media ID of featured image.
 */

/**
 * Get a collection of REST resources.
 *
 * @param {string}  route   Route to GET.
 * @param {object}  [query] Query parameter map (optional).
 * @param {boolean} [retry] Whether to allow retry on failure (optional).
 * @returns {Promise} Promise to JSON results of query.
 */
const get = async ( route, query = {}, retry = true ) => {
	try {
		const result = await fetch( `/wp-json${ route }?${ ( new URLSearchParams( query ) ).toString() }` );
		return result.json();
	} catch ( e ) {
		if ( retry ) {
			// Retry once.
			return get( route, query, false );
		}

		// Re-throw if failure.
		throw e;
	}
};

/**
 * Register and then fetch multiple API resources.
 */
class Resource {
	/**
	 * Construct the API Resource object.
	 *
	 * @param {string} route Collection endpoint for this API resource.
	 * @param {object} query Query parameters to use when fetching.
	 */
	constructor( route, query = {} ) {
		this.route = route;
		this.query = query;
		// Dictionary of resources to fetch, and later, their values.
		this.resources = {};
	}

	/**
	 * Prepare to fetch one or more resources by ID.
	 *
	 * @param {number|number[]} resourceId One or more resource IDs.
	 */
	include( resourceId ) {
		if ( Array.isArray( resourceId ) ) {
			resourceId.forEach( ( id ) => {
				this.resources[ id ] = true;
			} );
		} else {
			this.resources[ resourceId ] = true;
		}
	}

	/**
	 * Set a resource value by ID.
	 *
	 * @param {number} id       ID of resource.
	 * @param {object} resource Resource object.
	 */
	set( id, resource ) {
		this.resources[ id ] = resource;
	}

	/**
	 * Get one or more resources from the fetched data.
	 *
	 * @param {number|number[]} id ID of resource to return.
	 * @returns {object|number|number[]} Resource object, or unchanged ID if resource not found.
	 */
	get( id ) {
		return this.resources[ id ] || id;
	}

	/**
	 * Get multiple resources from the fetched data.
	 *
	 * @param {number[]} ids IDs of resource to return.
	 * @returns {Array} Array of resources, or their IDs if not found.
	 */
	getMultiple( ids ) {
		return ids.map( ( id ) => this.get( id ) );
	}

	/**
	 * Fetch all registered IDs and store them in the resources dictionary.
	 *
	 * @async
	 * @returns {Promise<Array>} Resolves to array of returned resources.
	 */
	async fetch() {
		const ids = Object.keys( this.resources );
		const resources = await get( this.route, {
			...this.query,
			include: ids.join(),
			per_page: ids.length,
		} );
		resources.forEach( ( resource ) => {
			this.set( resource.id, resource );
		} );
		return resources;
	}
}

/**
 * Get recent posts with minimal unnecessary fetching.
 *
 * @returns {Promise<object[]>} Promise to array of recent posts, including embedded values.
 */
const getRecentPosts = async () => {
	/** @type {WPPost[]} */
	let posts = [];
	// Create instances of our Resource class for each "embedded" resource.
	const authors = new Resource( '/wp/v2/users', {
		_fields: 'id,link,name,avatar_urls',
	} );
	const media = new Resource( '/wp/v2/media', {
		_fields: 'id,media_details',
	} );
	const tags = new Resource( '/wp/v2/tags', {
		_fields: 'id,name,link',
	} );
	const categories = new Resource( '/wp/v2/categories', {
		_fields: 'id,name,link',
	} );

	try {
		// Fetch the posts.
		posts = await get( '/wp/v2/posts', {
			_fields: 'id,author,categories,date_gmt,excerpt,featured_media,link,modified_gmt,tags,title',
		} );

		// Then set up the Resource objects with the IDs of linked resources.
		posts.forEach( ( post ) => {
			authors.include( post.author );
			media.include( post.featured_media );
			tags.include( post.tags );
			categories.include( post.categories );
		} );

		// Get all the "embedded" data in parallel.
		await Promise.all( [
			authors.fetch(),
			tags.fetch(),
			categories.fetch(),
			media.fetch(),
		] );
	} catch ( e ) {
		console.error( e );
	}
	return posts.map( ( post ) => ( {
		...post,
		author: authors.get( post.author ),
		tags: tags.getMultiple( post.tags ),
		categories: categories.getMultiple( post.categories ),
		media: media.get( post.featured_media ),
	} ) );
};