/**
 * This file holds The SEO Framework plugin's JS code for forwards compatibility with Gutenberg.
 *
 * This is an intermediate step where I hijack old code to support the new WordPress editor.
 * The current code will be rewritten later. Do not rely on this file. However, the JQ triggers
 * will sustain support until further notice.
 *
 * @author Sybre Waaijer <https://cyberwire.nl/>
 * @link <https://wordpress.org/plugins/autodescription/>
 */

/**
 * The SEO Framework plugin
 * Copyright (C) 2019 - 2024 Sybre Waaijer, CyberWire B.V. (https://cyberwire.nl/)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

'use strict';

/**
 * Holds tsfGBC (tsf Gutenberg Compat) values in an object to avoid polluting global namespace.
 *
 * This is a self-constructed function assigned as an object.
 *
 * @since 3.2.0
 *
 * @constructor
 * @param {!jQuery} $ jQuery object.
 */
window.tsfGBC = function( $ ) {

	const editor   = wp.data.select( 'core/editor' );
	const editPost = wp.data.select( 'core/edit-post' );

	/**
	 * Post data holder.
	 *
	 * @since 3.2.0
	 * @access private
	 * @type {Map<title:string,link:string,content:string;excerpt:string;visibility:string>} postData
	 */
	const postData = new Map();

	/**
	 * Debounces a simple function without arguments.
	 *
	 * @since 5.0.3
	 * @access private
	 *
	 * @function
	 * @param {CallableFunction} func
	 * @param {Int} timeout
	 * @return {Function}
	 */
	function debounce( func, timeout = 0 ) {
		let timeoutId = 0;
		return () => {
			clearTimeout( timeoutId );
			return {
				cb: setTimeout( func, timeout ),
				cancel: () => clearTimeout( timeoutId ),
			};
		};
	}

	/**
	 * Retrieves post attribute.
	 *
	 * @since 3.2.0
	 * @access private
	 *
	 * @function
	 * @param {String} attribute
	 * @return {mixed|null}
	 */
	function getPostAttribute( attribute ) {
		return editor.getEditedPostAttribute( attribute );
	}

	/**
	 * Sets editor data.
	 *
	 * @since 3.2.0
	 * @since 3.2.2 Now sets post visibility.
	 * @since 5.0.3 Renamed from `setData()`.
	 * @access private
	 *
	 * @function
	 */
	function updateData() {
		postData.set( 'title', getPostAttribute( 'title' ) )
				.set( 'link', editor.getPermalink() )
				.set( 'content', getPostAttribute( 'content' ) )
				.set( 'excerpt', getPostAttribute( 'excerpt' ) )
				.set( 'visibility', editor.getEditedPostVisibility() );
	}

	/**
	 * Assesses the editor data, and dispatches the data when changed.
	 *
	 * @since 3.2.0
	 * @since 3.2.2 Now dispatches visibility changes.
	 * @access private
	 *
	 * @function
	 */
	function assessData() {

		const oldData = new Map( postData );

		updateData();

		postData.forEach( ( val, key ) => {
			if ( val !== oldData.get( key ) )
				triggerUpdate( key );
		} );
	}

	/**
	 * Dispatches an event of a data type, also sends data of set type.
	 *
	 * @since 3.2.0
	 * @access public
	 *
	 * @function
	 * @param {String} type
	 */
	const triggerUpdate = type => {
		// Unfortunately, we rely on jQuery here. We can't move away from this, since the data sent is definitely used by other plugins.
		// TODO send deprecation notice ($._data( document, 'events' ), should we?), and implement alternative via event.detail.
		$( document ).trigger(
			`tsf-updated-gutenberg-${type}`,
			[ postData.get( type ) ?? null ]
		);
	}

	/**
	 * Maintains asynchronous save states.
	 * @since 3.2.0
	 * @access private
	 * @type {boolean} saved
	 */
	let saved = false;
	/**
	 * Maintains asynchronous save type.
	 * @since 4.0.0
	 * @access private
	 * @type {String} The saved type. Either 'save', 'autosave', or 'preview'.
	 */
	let savedType = '';
	/**
	 * Checks if the document is saved successfully, and then dispatches an event if so.
	 *
	 * @since 3.2.0
	 * @since 4.0.0 1. Now waits for 7 seconds for the saveDispatcher to resolve before canceling the process.
	 *              2. Added `saveType` checking, to discern events with stale dirty content.
	 * @access private
	 *
	 * @function
	 */
	function saveDispatcher() {
		if ( ! saved ) {
			if ( editor.isSavingPost() ) {
				if ( editor.isPreviewingPost() ) {
					saved     = true;
					savedType = 'preview';
				} else if ( editor.isAutosavingPost() ) {
					saved     = true;
					savedType = 'autosave';
				} else {
					saved     = true;
					savedType = 'save';
				}
			}
		} else {
			if ( editor.didPostSaveRequestSucceed() ) {
				dispatchSaveEventDebouncer();
				revertSaveStateDebouncer().cancel();
				revertSaveState();
			} else {
				revertSaveStateDebouncer();
			}
		}
	}

	const revertSaveStateDebouncer = debounce( revertSaveState, 7000 );
	/**
	 * Reverts save state.
	 *
	 * @since 4.0.0
	 * @access private
	 *
	 * @function
	 */
	function revertSaveState() {
		saved = false;
	}

	const dispatchSaveEventDebouncer = debounce( dispatchSavedEvent, 500 );
	/**
	 * Maintains retry states.
	 * @since 4.0.0
	 * @access private
	 * @type {number} retryDispatch
	 */
	let retryDispatch = 0;
	/**
	 * Dispatches save event.
	 *
	 * @since 3.2.0
	 * @since 4.0.0 1. Added `saveType` checking.
	 *              2. Now forwards the `saveType` parameter in `tsf-gutenberg-saved-document`.
	 * @access private
	 *
	 * @function
	 */
	function dispatchSavedEvent() {
		if ( editor.isPostSavingLocked() ) {
			// Retry again
			if ( ++retryDispatch < 3 ) {
				dispatchSaveEventDebouncer();
			} else {
				dispatchSaveEventDebouncer() && dispatchSaveEventDebouncer().cancel();
				retryDispatch = 0;
			}
		} else {
			retryDispatch = 0;

			// When a new post is being created, autosave and preview are synonymous to saving.
			// With that, hasChangedContent() will be set to "false".
			let triggerOnSaveEvent = ! editor.hasChangedContent();

			switch ( savedType ) {
				case 'preview':
					document.dispatchEvent( new CustomEvent( 'tsf-gutenberg-onpreview' ) );
					break;
				case 'autosave':
					document.dispatchEvent( new CustomEvent( 'tsf-gutenberg-onautosave' ) );
					break;
				case 'save':
					triggerOnSaveEvent = true;
					break;
			}

			if ( triggerOnSaveEvent ) {
				document.dispatchEvent( new CustomEvent( 'tsf-gutenberg-onsave' ) )
					&& document.dispatchEvent( new CustomEvent( 'tsf-gutenberg-onsave-completed' ) );
			}

			document.dispatchEvent(
				new CustomEvent(
					'tsf-gutenberg-saved-document',
					{
						detail: { savedType },
					}
				)
			);
			savedType = '';
		}
	}

	/**
	 * Maintains the sidebar opening/closing states.
	 *
	 * @since 4.0.0
	 * @access private
	 * @type {Object<string, *>}
	 */
	let lastSidebarState = {
		opened: false,
	};
	/**
	 * Checks if user changed the sidebar layout.
	 *
	 * @since 3.2.0
	 * @access private
	 *
	 * @function
	 */
	function sidebarDispatcher() {
		if ( editPost.isEditorSidebarOpened() ) {
			if ( ! lastSidebarState.opened ) {
				lastSidebarState.opened = true;
				document.dispatchEvent( new CustomEvent( 'tsf-gutenberg-sidebar-opened' ) );
			}
		} else {
			if ( lastSidebarState.opened ) {
				lastSidebarState.opened = false;
				document.dispatchEvent( new CustomEvent( 'tsf-gutenberg-sidebar-closed' ) );
			}
		}
	}

	/**
	 * Initializes Gutenberg's compatibility and dispatches event hooks.
	 *
	 * @since 3.2.0
	 * @since 4.0.0 Now adds tooltip boundaries (moved from tt.js)
	 * @access private
	 *
	 * @function
	 */
	const _initCompat = () => {

		const { subscribe } = wp.data;

		subscribe( debounce( sidebarDispatcher, 500 ) );
		subscribe( debounce( assessData, 300 ) );
		subscribe( saveDispatcher );

		document.dispatchEvent( new CustomEvent( 'tsf-subscribed-to-gutenberg' ) );
	}

	return Object.assign( {
		/**
		 * Initialises all aspects of the scripts.
		 * You shouldn't call this.
		 *
		 * @since 4.0.0
		 * @access protected
		 *
		 * @function
		 */
		load: () => {
			document.body.addEventListener( 'tsf-onload', _initCompat );
		},
	}, {
		triggerUpdate,
	} );
}( jQuery );
window.tsfGBC.load();
