/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n'
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs, safeDecodeURI, filterURLForDisplay } from '@wordpress/url';
import { decodeEntities } from '@wordpress/html-entities';

import { emit } from '../../../utilities/emit.js';
import { ready } from '../../../utilities/ready.js';

customElements.define( 'auto-complete', class extends HTMLElement {

	// Define the attributes to observe, which auto calls attributeChangedCallback.
	static observedAttributes = ['mode', 'content-types', 'taxonomy', 'disabled', 'placeholder'];

	/**
	 * Runs only once the complete element has been parsed and added to the DOM.
	 */
	connectedCallback() {
		ready(this);
	}

	// Initialize the component
	readyInit () {

		// Don't run if already initialized
		if (this.hasAttribute('is-ready')) return;

		// Get the required template for working. Used to format searched items.
		this.itemTemplate = this.querySelector('template')?.content;
		if ( ! this.itemTemplate ) return;

		// Internal variables, not configured via attributes
		this.currentSearch = '';
		this.currentPage = 1;
		this.totalPages = 1;
		this.perPage = 10;
		this.minChars = 2;
		this.initialiseMode();

		// DOM references
		this.searchInput = null;
		this.clearSearch = null;
		this.searchResults = null;
		this.displaySelected = null;
		// Setup default UI
		this.setupDOM();

		// Populate initial list based on already set items
		this.renderSelection();

		// Listen for events
		this.searchInput.addEventListener('keyup', this); // On the search input
		this.clearSearch.addEventListener('click', this); // On the search
		this.searchResults.addEventListener('click', this); // On the search results
		this.displaySelected.addEventListener('click', this); // On the buttons to remove items

		// Check for immediate disablement.
		if ( 0 !== this.selectedItems.length && this.maxItems && this.maxItems <= this.selectedItems.length) {
			this.setAttribute('disabled', '');
			// Directly trigger handling as technically not 'ready' yet.
			this.handleChangedisabled(null,'');
		}

		// Emit ready event.
		emit(this, 'auto-complete', 'ready', {
			items: this.selectedItems
		});
		this.setAttribute('is-ready', '');
	}

	/**
	 * Add the additional HTML for search input, display of items etc
	 */
	setupDOM() {

		if ( ! this.searchInput && ! this.clearSearch ) {
			let search = document.createElement('div');
			search.classList.add('search');

			// Search Input
			this.searchInput = document.createElement('input');
			this.searchInput.setAttribute('type', 'text');
			this.searchInput.setAttribute('autocomplete', 'off');
			if ( this.disabled ) {
				this.searchInput.setAttribute('disabled', '');
			}
			this.searchInput.classList.add('auto-complete','search-icon');
			this.searchInput.id = this.getAttribute('label-id') || 'auto-complete-' + Math.floor(Math.random() * 100);
			this.searchInput.setAttribute('placeholder', this.getAttribute('placeholder') || `${ __('Search', 'personalizewp') }`);
			search.append(this.searchInput);

			// Reset search
			this.clearSearch = document.createElement('button');
			this.clearSearch.setAttribute('type', 'button');
			this.clearSearch.setAttribute('data-type', 'reset');
			this.clearSearch.setAttribute('aria-label', `${ __('Reset search', 'personalizewp') }`);
			this.clearSearch.innerHTML = '<svg width="24" height="24"><use xlink:href="#pwp-icon-cross"></use></svg>'; // SVG will exist as part of autocomplete template
			this.hideClearSearch();
			search.append(this.clearSearch);
			this.append(search);
		}

		// Container for search results
		if ( ! this.searchResults ) {
			this.searchResults = document.createElement('ul');
			this.searchResults.classList.add('search-results');
			this.append(this.searchResults);
		}
		// Container for selected items
		if ( ! this.displaySelected ) {
			this.displaySelected = document.createElement('ul');
			this.displaySelected.classList.add('tag-list','cluster');
			this.append(this.displaySelected);
		}
	}

	/**
	 * Initialise/update mode, and base required properties via attributes.
	 * Some modes have additional setup, such as terms to confirm valid taxonomy.
	 */
	initialiseMode() {

		// Define base dynamic properties
		this.mode = this.getAttribute('mode') || 'post';
		this.disabled = this.getAttribute('disabled') || false; // Field can start disabled
		this.taxonomy = this.getAttribute('taxonomy') || null;
		this.contentTypes = this.getAttribute('content-types') || [];
		if ( this.hasAttribute('max-items') ) {
			this.maxItems = parseInt( this.getAttribute('max-items'), 10 ) || false;
		}
		if ( 0 !== this.contentTypes.length ) {
			// Convert string to sanitised array
			this.contentTypes = this.contentTypes.split(',').map( (v) => v.replace( /[^a-z0-9_\-]/, '' ) );
		}
		// Keep a list of internal chosen IDs/titles
		this.selectedItems = this.getAttribute('content').trim() || [];
		if ( 0 !== this.selectedItems.length ) {
			this.selectedItems = JSON.parse(this.selectedItems);
		}

		this.restBase = null;
		// Additional defaults depend on specific mode
		switch( this.mode ) {
			case 'term':
				if ( window.PWPTaxonomyRestBases ) {
					this.restBase = window.PWPTaxonomyRestBases?.[`${this.taxonomy}`] || null;
				}
				if ( ! this.restBase ) {
					// Maybe new taxonomy, maybe items not cached. Find restBase from API.
					apiFetch({
						path: 'wp/v2/taxonomies',
					})
						.then((results) => {
							// Cache results for future calls and switches in taxonomy.
							const restBases = {};
							for (const tax in results ) {
								restBases[`${tax}`] = results[`${tax}`].rest_base;
							}
							window.PWPTaxonomyRestBases = restBases;
							this.restBase = window.PWPTaxonomyRestBases?.[`${this.taxonomy}`] || null;
							if ( ! this.restBase ) {
								// Just stop here as unknown taxonomy
								this.setAttribute('disabled', '');
							}
						})
						.catch((error) => {
							console.log( 'catching error...', error );
							// Just stop here
							this.setAttribute('disabled', '');
							return;
						});
				}
				break;

			case 'form':
			case 'product':
			case 'user':
			case 'post':
				break;
		}

	}

	/**
	 * Handle events wrapper
	 * @param {Event} event
	 */
	handleEvent(event) {
		this[`handle${event.type}`](event);
	}

	/**
	 * Search input keyup
	 * @param {Event} event
	 */
	handlekeyup(event) {
		// Clear existing search
		clearTimeout(this.debouce);
		if ( '' === this.searchInput.value ) {
			this.hideClearSearch();
		} else {
			this.showClearSearch();
		}
		// Wait a number of milliseconds before searching in case still typing
		this.debouce = setTimeout( () => {
			this.handleSearchStringChange(this.searchInput.value, this.currentPage);
		}, 250 );
	}

	/**
	 * Adding/Removing items
	 * @param {Event} event
	 */
	handleclick(event) {
		event.preventDefault();
		let button = event.target.closest('button');
		if ( ! button ) return;
		let type = button.getAttribute('data-type');
		if ( ! type ) return;

		switch( type ) {
			case 'reset':
				this.currentPage = 1;
				this.currentSearch = '';
				this.searchInput.value = '';
				this.resetSearchResults();
				this.searchInput.focus();
				this.hideClearSearch();
				break;

			case 'load-more':
				this.currentPage++;
				this.handleSearchStringChange(this.searchInput.value, this.currentPage);
				// Remove load more for now.
				button.closest('li').remove();
				break;

			case 'selected':
				// Remove choice.
				button.closest('li').remove();
				// Update selectedItems
				this.removeSelectedItem(button.value);
			break;

			case 'search':
				// Update selectedItems
				this.addSelectedItem(button.value, button.querySelector('.header').textContent);
			break;
		}
	}

	/**
	 * Runs when the value of an attribute is changed on the component.
	 * Also runs when a components' attribute is first seen, before connectedCallback() is run.
	 * @param  {String} name     The attribute name
	 * @param  {String} oldValue The old attribute value
	 * @param  {String} newValue The new attribute value
	 */
	attributeChangedCallback(name, oldValue, newValue) {
		// Don't run if not initialized, ensures data integrity.
		if (! this.hasAttribute('is-ready')) return;

		// Rationalise the attribute name for function name support
		name = name.replace( /[^a-zA-Z0-9]/i, '' );
		this[`handleChange${name}`](oldValue, newValue);
	}

	/**
	 * Updating placeholder on search input
	 * @param {Event} event
	 */
	handleChangeplaceholder(event) {
		if ( this.searchInput ) {
			this.searchInput.setAttribute('placeholder', this.getAttribute('placeholder') || `${ __('Search', 'personalizewp') }`);
		}
	}

	/**
	 * Handle [mode] attribute changes
	 * @param  {String} oldValue The old attribute value
	 * @param  {String} newValue The new attribute value
	 */
	handleChangemode(oldValue, newValue) {
		this.initialiseMode();
	}

	/**
	 * Handle [content-types] attribute changes
	 * @param  {String} oldValue The old attribute value
	 * @param  {String} newValue The new attribute value
	 */
	handleChangecontenttypes(oldValue, newValue) {
		if ( newValue && 0 !== newValue.length ) {
			this.contentTypes = newValue.split(',').map( (v) => v.replace( /[^a-z0-9_\-]/, '' ) );
		} else {
			this.contentTypes = [];
		}
	}

	/**
	 * Handle [mode] attribute changes
	 * @param  {String} oldValue The old attribute value
	 * @param  {String} newValue The new attribute value
	 */
	handleChangetaxonomy(oldValue, newValue) {
		if ( null === newValue || '' === newValue ) {
			this.taxonomy = null;
			// Clearly can't do anything.
			this.setAttribute('disabled', '');
			return;
		}
		this.taxonomy = newValue;
		// Remove any searching.
		this.resetSearchResults();
		// Has a category, so (re)initialise.
		this.initialiseMode();

		// Use placeholder value for initial search/replace.
		if ( null === oldValue || '' === oldValue ) {
			oldValue = '%TAXONOMY%';
		}
		// Trigger change of name of template input, updating existing inputs
		const input = this.itemTemplate.querySelector('input[type="hidden"][name^="pwp_form"]');
		if ( input ) {
			input.setAttribute( 'name', input.getAttribute('name').replace( `[${oldValue}]`, `[${this.taxonomy}]` ) );
		}
		const items = this.displaySelected.querySelectorAll('input[type="hidden"][name^="pwp_form"]');
		items.forEach( ( item ) => {
			item.setAttribute( 'name', item.getAttribute('name').replace( `[${oldValue}]`, `[${this.taxonomy}]` ) );
		});
		this.removeAttribute('disabled');
	}

	/**
	 * Handle [disabled] attribute changes
	 * @param  {String} oldValue The old attribute value
	 * @param  {String} newValue The new attribute value
	 */
	handleChangedisabled(oldValue, newValue) {
		// Null when removed, something (generally empty) when added
		if (null !== newValue) {
			this.disabled = true;
			if ( this.searchInput ) {
				this.searchInput.value = '';
				this.searchInput.setAttribute('disabled', '');
			}
			// Ensure no results shown.
			this.resetSearchResults();
			this.hideClearSearch();
		} else {
			this.disabled = false;
			if ( this.searchInput ) {
				this.searchInput.removeAttribute('disabled');
			}
		}
	}

	/**
	 * Removes an item from selection
	 * @param {string} value
	 */
	removeSelectedItem(value) {
		let index = this.selectedItems.findIndex((item) => {
			return item.value === value;
		});
		if ( -1 === index ) return; // Can't find
		this.selectedItems.splice(index, 1);
		this.renderSelection();
		if ( this.maxItems && this.maxItems > this.selectedItems.length ) {
			this.removeAttribute('disabled');
		}
		// Fire event to hook into.
		emit( this, 'auto-complete', 'remove-item', {
			item: value,
			items: this.selectedItems
		} );
	}

	/**
	 * Adds an item to the selection
	 * @param {string} value
	 * @param {string} text
	 */
	addSelectedItem(value, text) {
		// Check doesn't already exist
		let index = this.selectedItems.findIndex((item) => {
			return item.value === value;
		});
		if ( -1 !== index ) return; // Already there

		this.selectedItems.unshift({value, text})
		this.renderSelection();
		if ( this.maxItems && this.maxItems <= this.selectedItems.length ) {
			this.setAttribute('disabled', '');
		}
		// Fire event to hook into.
		emit( this, 'auto-complete', 'add-item', {
			newItem: value,
			items: this.selectedItems
		} );
	}

	/**
	 * Inject the selected Items into the list container
	 */
	renderSelection() {
		if ( ! this.displaySelected ) return;
		this.displaySelected.replaceChildren(
			...Array.from(this.selectedItems).map((item) => {
				let html = document.createElement('li');
				html.append(this.itemTemplate.cloneNode(true));
				html.querySelector('span').textContent = item.text;
				html.querySelector('input').value = item.value;
				html.querySelector('button').value = item.value;
				html.querySelector('button').setAttribute('data-type', 'selected');
				return html;
			})
		);
	}

	/**
	 * Mark being busy, https://wordpress.github.io/gutenberg/?path=/docs/components-spinner--docs
	 */
	showLoading() {
		if(this.searchResults.querySelector('.loading')) return;
		let li = document.createElement('li');
		li.classList.add('loading');
		li.innerHTML = '<svg class="components-spinner" viewBox="0 0 100 100" width="20" height="20" xmlns="http://www.w3.org/2000/svg" role="presentation" focusable="false"><circle cx="50" cy="50" r="50" vector-effect="non-scaling-stroke" fill="transparent" stroke="#ddd" stroke-width="2"></circle><path d="m 50 0 a 50 50 0 0 1 50 50" vector-effect="non-scaling-stroke" fill="transparent" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>';
		this.searchResults.append(li);
	}

	/**
	 * Remove the loading indicator.
	 */
	hideLoading() {
		const loading = this.searchResults.querySelector('.loading');
		if(loading) loading.remove();
	}

	/**
	 * Nothing found.
	 */
	showNothingFound() {
		if(this.searchResults.querySelector('.nothing')) return;
		let li = document.createElement('li');
		li.classList.add('nothing');
		li.textContent = `${ __('Nothing found.', 'personalizewp') }`;
		this.searchResults.replaceChildren(li);
	}

	/**
	 * Hides the clear search button via inline css.
	 */
	hideClearSearch() {
		this.clearSearch.style.display = 'none';
	}

	/**
	 * Restores the default css for clear search button.
	 */
	showClearSearch() {
		this.clearSearch.style.display = '';
	}

	/**
	 * Load more.
	 */
	showLoadMore() {
		if(this.searchResults.querySelector('.load-more')) return;
		let li = document.createElement('li');
		li.classList.add('load-more');
		li.innerHTML = `<button type="button" data-type="load-more">${ __('Load more', 'personalizewp') }</button>`;
		this.searchResults.append(li);
	}

	/**
	 * Clears the search results of any content, inc loading etc.
	 */
	resetSearchResults() {
		if ( this.searchResults ) {
			this.searchResults.replaceChildren();
		}
	}

	/**
	 * Resets the component to its initial state
	 * Clears both search results and selected items
	 */
	resetAll() {
		// Clear selected items
		this.selectedItems = [];
		this.renderSelection();

		// Clear search results and reset search state
		this.currentSearch = '';
		this.currentPage = 1;
		this.resetSearchResults();

		// Undisable
		this.removeAttribute('disabled');

		// Clear search input
		if ( this.searchInput ) {
			this.searchInput.value = '';
		}
		// Fire event to hook into.
		emit( this, 'auto-complete', 'reset-all' );
	}

	/**
	 * Inject the search results into the list container, taking into account additions of no results, loading, load more
	 * @param {array} results
	 */
	renderResults(results) {
		this.hideLoading();

		if (0 === results.length) {
			this.showNothingFound();
			return;
		}

		// Remove all existing results when the current page is at the start
		// Allows 'additional' pages of results to be added to what is there
		if ( 1 === this.currentPage ) {
			this.resetSearchResults();
		}

		this.searchResults.append(
			...Array.from(results).map((item) => {
				let text = document.createElement('span');
				text.classList.add('header');
				text.textContent = item.text;
				let button = document.createElement('button');
				button.value = item.value;
				button.setAttribute('type', 'button');
				button.setAttribute('data-type', 'search');
				button.append(text);
				// Optional extra info
				if (item.info) {
					let info = document.createElement('span');
					info.classList.add('info');
					info.setAttribute('aria-hidden', 'true');
					info.textContent = item.info;
					button.append(info);
				}
				let li = document.createElement('li');
				li.append(button);
				return li;
			})
		);

		// If there are more to show add a load more
		if (this.totalPages > this.currentPage) {
			this.showLoadMore();
		}
	}

	/**
	 * Normalise the results to use the same array structure
	 * @param {array} results
	 */
	normalizeResults(results) {
		let normalizedResults = [];
		switch(this.mode) {
			case 'form' :
			case 'product' :
				results.forEach((item) => {
					let result = {
						value: item.value,
						text: decodeEntities(item.title)
					}
					if ( item.url ) {
						result.info = filterURLForDisplay(safeDecodeURI(item.url), 30);
					}
					normalizedResults.push(result);
				});
				break;

			case 'term':
				results.forEach((item) => {
					normalizedResults.push({
						value: item.id,
						text: decodeEntities(item.name),
						info: filterURLForDisplay(safeDecodeURI(item.link), 30)
					});
				});
				break;

			case 'user':
				results.forEach((item) => {
					normalizedResults.push({
						value: item.id,
						text: decodeEntities(item.name),
						info: item.slug ? `@${item.slug}` : ''
					});
				});
				break;

			case 'post':
			// default:
				results.forEach((item) => {
					normalizedResults.push({
						value: item.id,
						text: decodeEntities(item.title),
						info: filterURLForDisplay(safeDecodeURI(item.url), 30)
					});
				});
				break;
		}
		return normalizedResults;
	}

	/**
	 * handleSearchStringChange
	 *
	 * @param {string} keyword search query string
	 * @param {string} page page query string
	 */
	handleSearchStringChange(keyword, page) {
		keyword = keyword.trim();
		if ('' === keyword) {
			// Reset the results
			this.currentSearch = '';
			this.currentPage = 1;
			this.resetSearchResults();
			return;
		}
		// Track changes to reset vars
		if (keyword !== this.currentSearch) {
			this.currentPage = 1;
			this.currentSearch = keyword;
		}
		// minimum number of characters before we start to generate suggestions
		if (keyword.length < this.minChars) {
			return;
		}
		this.showLoading();

		let preparedQuery = this.prepareSearchQuery(keyword, page);
		this.contentSearch(preparedQuery);
	}

	/**
	 * Converts the keyword into the various REST Api calls
	 *
	 * @param {string} keyword
	 * @param {string} page
	 * @returns
	 */
	prepareSearchQuery(keyword, page) {
		let searchQuery, _searchArgs;

		switch (this.mode) {
			case 'product':
				_searchArgs = {
					search: keyword,
					_embed: true,
					per_page: this.perPage,
					page,
				};
				// Use content-types attribute and pass as subtype (consistent with post case)
				if ( this.contentTypes && 0 !== this.contentTypes.length ) {
					_searchArgs.subtype = this.contentTypes.join(',');
				}
				searchQuery = addQueryArgs('personalizewp/v1/products-search', _searchArgs);
				break;

			case 'form':
				_searchArgs = {
					search: keyword,
					_embed: true,
					per_page: this.perPage,
					page,
				};
				searchQuery = addQueryArgs('personalizewp/v1/forms-search', _searchArgs);
				break;

			case 'term':
				_searchArgs = {
					search: keyword,
					per_page: this.perPage,
					page,
				};
				searchQuery = addQueryArgs(`wp/v2/${this.restBase}`, _searchArgs);
				break;

			case 'user':
				_searchArgs = {
					search: keyword,
					per_page: this.perPage,
					page,
				};
				searchQuery = addQueryArgs('wp/v2/users', _searchArgs);
				break;

			case 'post':
			default:
				_searchArgs = {
					search: keyword,
					type: this.mode,
					_embed: true,
					per_page: this.perPage,
					page,
				};
				if ( 0 !== this.contentTypes.length ) {
					_searchArgs.subtype = this.contentTypes.join(',');
				}
				searchQuery = addQueryArgs('wp/v2/search', _searchArgs);
				break;
		}
		return searchQuery;
	}

	/**
	 * Process a content search
	 * @param {string} searchQueryString
	 */
	contentSearch( searchQueryString ) {

		apiFetch({
			path: searchQueryString,
			parse: false,
		})
			.then((results) => {
				this.totalPages = parseInt(
					results.headers && results.headers.get('X-WP-TotalPages'),
					10,
				);
				// Parse, because we set parse to false to get the headers.
				results.json().then((results) => {
					const normalizedResults = this.normalizeResults(results);
					this.renderResults(normalizedResults);
				});
			})
			.catch((error) => {
				console.log( 'catching error...' );
				// Just stop here
				this.hideLoading();
			});
	}
});
