/* global jQuery, elementor, $e, seovaultseoElementor */

/**
 * SEOvault AI — Elementor Atomic Editor (V4) live content walker.
 *
 * @param {Object} $ jQuery instance.
 */
(function ($) {
	'use strict';

	let previewObserver;
	let observedSettingsModel;
	let lastContent = '';
	let lifecycleRegistered = false;
	const config = seovaultseoElementor;

	function toPlainObject(value) {
		if (value && typeof value.toJSON === 'function') {
			return value.toJSON();
		}

		return value;
	}

	function getCurrentDocument() {
		try {
			return elementor.documents.getCurrent();
		} catch (error) {
			return null;
		}
	}

	function getElements(documentModel) {
		if (!documentModel) {
			return [];
		}

		try {
			const elements = documentModel.container.model.get('elements');
			const plain = toPlainObject(elements);
			if (Array.isArray(plain)) {
				return plain;
			}
		} catch (error) {
			// Fall through to the document config used by some editor builds.
		}

		return documentModel.config &&
			Array.isArray(documentModel.config.elements)
			? documentModel.config.elements
			: [];
	}

	function selectorEscape(value) {
		if (window.CSS && typeof window.CSS.escape === 'function') {
			return window.CSS.escape(String(value));
		}

		return String(value).replace(/["\\]/g, '\\$&');
	}

	function getPreviewNode(element, previewRoot) {
		if (!element || !element.id) {
			return null;
		}

		const id = selectorEscape(element.id);
		const selector =
			'[data-id="' + id + '"], [data-interaction-id="' + id + '"]';

		try {
			const rootMatch = previewRoot
				? previewRoot.querySelector(selector)
				: null;
			if (rootMatch) {
				return rootMatch;
			}

			const iframe = document.getElementById('elementor-preview-iframe');
			return iframe && iframe.contentDocument
				? iframe.contentDocument.querySelector(selector)
				: null;
		} catch (error) {
			return null;
		}
	}

	function serializeNode(node) {
		if (!node) {
			return '';
		}

		const clone = node.cloneNode(true);
		clone
			.querySelectorAll(
				'.elementor-element-overlay, .elementor-background-overlay, ' +
					'.ui-resizable-handle, [data-elementor-ui], script, style'
			)
			.forEach(function (chrome) {
				chrome.remove();
			});

		return clone.outerHTML.replace(/[\n\t]+/g, ' ');
	}

	function walkElements(elements, previewRoot) {
		const result = {
			content: '',
			widgets: [],
		};

		if (!Array.isArray(elements)) {
			return result;
		}

		elements.forEach(function (rawElement) {
			const element = toPlainObject(rawElement);
			if (!element || typeof element !== 'object') {
				return;
			}

			let childResult;
			if (element.elType === 'widget' || element.widgetType) {
				const html = serializeNode(
					getPreviewNode(element, previewRoot)
				);
				childResult = {
					content: html,
					widgets: html
						? [
								{
									id: element.id,
									widgetType: element.widgetType || 'atomic',
									start: 0,
									end: html.length,
								},
							]
						: [],
				};
			} else {
				childResult = walkElements(
					toPlainObject(element.elements),
					previewRoot
				);
			}

			const offset = result.content.length;
			childResult.widgets.forEach(function (widget) {
				result.widgets.push({
					id: widget.id,
					widgetType: widget.widgetType,
					start: widget.start + offset,
					end: widget.end + offset,
				});
			});
			result.content += childResult.content;
		});

		return result;
	}

	function getPreviewRoot(documentModel) {
		if (documentModel && documentModel.$element) {
			if (typeof documentModel.$element.get === 'function') {
				const root = documentModel.$element.get(0);
				if (root) {
					return root;
				}
			}

			if (documentModel.$element instanceof window.Element) {
				return documentModel.$element;
			}
		}

		try {
			const iframe = document.getElementById('elementor-preview-iframe');
			return iframe && iframe.contentDocument
				? iframe.contentDocument.body
				: null;
		} catch (error) {
			return null;
		}
	}

	function collect() {
		const documentModel = getCurrentDocument();
		const previewRoot = getPreviewRoot(documentModel);
		return walkElements(getElements(documentModel), previewRoot);
	}

	function notifyIfChanged() {
		const content = collect().content;
		if (content === lastContent) {
			return;
		}

		lastContent = content;
		document.dispatchEvent(
			new CustomEvent('seovault:elementor-content-change', {
				detail: {
					content,
				},
			})
		);
	}

	function debounce(callback, wait) {
		let timeout;
		const debounced = function () {
			window.clearTimeout(timeout);
			timeout = window.setTimeout(callback, wait);
		};
		debounced.cancel = function () {
			window.clearTimeout(timeout);
		};
		return debounced;
	}

	const scheduleUpdate = debounce(notifyIfChanged, 350);

	function observePreview() {
		if (previewObserver) {
			previewObserver.disconnect();
		}

		const root = getPreviewRoot(getCurrentDocument());
		if (root) {
			previewObserver = new MutationObserver(scheduleUpdate);
			previewObserver.observe(root, {
				attributes: true,
				childList: true,
				subtree: true,
				characterData: true,
			});
		}

		try {
			elementor.channels.editor.off('change', scheduleUpdate);
			elementor.channels.editor.on('change', scheduleUpdate);

			if (observedSettingsModel) {
				observedSettingsModel.off('change', scheduleUpdate);
			}
			observedSettingsModel = elementor.settings.page.model;
			observedSettingsModel.on('change', scheduleUpdate);
		} catch (error) {
			// Mutation and $e observers remain active.
		}

		scheduleUpdate();
	}

	function stopObserving() {
		if (previewObserver) {
			previewObserver.disconnect();
			previewObserver = null;
		}

		try {
			elementor.channels.editor.off('change', scheduleUpdate);
			if (observedSettingsModel) {
				observedSettingsModel.off('change', scheduleUpdate);
			}
		} catch (error) {
			// The document is already closing; no further cleanup is needed.
		}

		observedSettingsModel = null;
		lastContent = '';
		scheduleUpdate.cancel();
	}

	function isTargetDocument(context) {
		let documentId =
			context && context.document ? context.document.id : context?.id;

		if (!documentId) {
			const currentDocument = getCurrentDocument();
			documentId = currentDocument ? currentDocument.id : 0;
		}

		return Number.parseInt(documentId, 10) === Number(config.postId);
	}

	function registerHook(command, id, callback) {
		if (
			typeof $e === 'undefined' ||
			!$e.hooks ||
			!$e.modules ||
			!$e.modules.hookUI ||
			typeof $e.hooks.registerUIAfter !== 'function'
		) {
			return;
		}

		class SeoVaultV4Hook extends $e.modules.hookUI.Base {
			getCommand() {
				return command;
			}

			getId() {
				return id;
			}

			getConditions() {
				return isTargetDocument(arguments[0]);
			}

			apply() {
				return callback.apply(null, arguments);
			}
		}

		$e.hooks.registerUIAfter(new SeoVaultV4Hook());
	}

	function registerLifecycle() {
		if (lifecycleRegistered) {
			return;
		}
		lifecycleRegistered = true;

		registerHook(
			'editor/documents/attach-preview',
			'seovaultseo/v4/observe-preview',
			observePreview
		);
		registerHook(
			'panel/editor/open',
			'seovaultseo/v4/panel-open',
			scheduleUpdate
		);
		registerHook(
			'document/save/save',
			'seovaultseo/v4/document-save',
			scheduleUpdate
		);
		registerHook(
			'document/save/set-is-modified',
			'seovaultseo/v4/document-modified',
			scheduleUpdate
		);
		registerHook(
			'editor/documents/close',
			'seovaultseo/v4/stop-observer',
			stopObserving
		);

		observePreview();
	}

	window.seovaultElementorV4 = {
		getContent() {
			return collect().content;
		},
		getWidgetMap() {
			return collect().widgets;
		},
		refresh: scheduleUpdate,
	};

	$(window).on('elementor:init', function () {
		if (elementor && typeof elementor.on === 'function') {
			elementor.on('panel:init', registerLifecycle);
		}
		window.setTimeout(registerLifecycle, 0);
	});

	if (typeof window.elementor !== 'undefined') {
		window.setTimeout(registerLifecycle, 0);
	}
})(jQuery);
