import Menu from "./menu.js";
import {ITEM_SELECTED_STYLE} from "./search/search.js";

export const MENU_FILTER_ENABLED_STYLE = 'filtered';
export const MENU_NO_RESULTS_STYLE = 'has-no-results';
export const ITEM_FILTERED_STYLE = 'filter-item';

/**
 * Filters the site menu down so only MenuItems whose title/name matches the search query are visible (and 
 * necessary ancestor menu items too).
 * @see Search
 */
export default class Filter {
	/**
	 * @param {Menu}        menu
	 * @param {Set<string>} wordSet Search query. Should be a Set of *lowercase* words. 
	 */
	constructor(menu, wordSet) {
		this.menu = menu;
		this.wordSet = wordSet;
		/**
		 * @type {MenuItem[]}
		 */
		this.highlighted = [];
		/**
		 * @type {MenuItem[]}
		 */
		this.bottomItems = [];
	}
	getWordSet() {
		return this.wordSet;
	}
	clear() {
		this.clearHighlight();
		this.bottomItems = [];
		for (let item of this.menu.getElement().querySelectorAll('.'+ITEM_FILTERED_STYLE)) {
			item.classList.remove(ITEM_FILTERED_STYLE);
		}
		this.menu.getElement().classList.remove(MENU_FILTER_ENABLED_STYLE);
		this.menu.getElement().classList.remove(MENU_NO_RESULTS_STYLE);
		document.querySelectorAll('dt.'+ITEM_SELECTED_STYLE).forEach(elem => {
			elem.classList.remove(ITEM_SELECTED_STYLE);
		});
	}
	/**
	 * Remove word highlighting from menu items in the DOM.
	 */
	clearHighlight() {
		this.highlighted.forEach(item=>{
			 let nameElem = item.element.querySelector('dt > a > span');
			 nameElem.innerHTML = nameElem.innerText;
		}); 
		this.highlighted = [];
	}

	/**
	 * Apply filter based on supplied word array. 
	 * Update the DOM so menu items are labelled with FILTERED_ITEM_STYLE and matched words are highlighted. 
	 */
	apply() {
		// For each root/top-level menu item:
		for (let item of this.menu.getItems()) {
			this.classifyMatch(item, this.wordSet);	
		}
		// If there's only one bottom item (in the filter tree) and it's not a navigation tree leaf item: 
		if (this.bottomItems.length === 1 && !this.bottomItems[0].isLeafNode()) {
			// Show its children:
			this.bottomItems[0].expand();
			let children = this.bottomItems[0].children;
			for (let child of children) {
				child.setClass(ITEM_FILTERED_STYLE);
				child.setVisible(true);
			}
			children[0].setClass(ITEM_SELECTED_STYLE);
			// If the filter tree is empty:
		} else if (this.bottomItems.length === 0) {
			this.menu.getElement().classList.add(MENU_NO_RESULTS_STYLE);
		} else {
			let item = this.bottomItems[0];
			// Mark the first item as being "Selected". Used for keyboard navigation i.e. hit Enter to open this page.
			item.setClass(ITEM_SELECTED_STYLE);
		}
		
		// Indicate the menu is in "Filtered" mode:
		this.menu.getElement().classList.add(MENU_FILTER_ENABLED_STYLE);
		this.menu.scrollToTop();
	}

	/**
	 * Recursively dive into each menu tree branch and classify items matching the keywords.
	 * Later instances of a word will be prioritised when highlighting in a single branch.
	 * @param {MenuItem} item
	 * @param {Set<string>} parentChecklist
	 * @return {Set<string>} Consumed/used-up words, provided the menu-item branch fully matches the search. 
	 * @private
	 */
	classifyMatch(item, parentChecklist) {
		// Clone to ensure the checklist is not shared between sibling branches.
		let branchChecklist = new Set(parentChecklist);
		// Check-off words upto this point in this branch
		for (let word of branchChecklist) {
			if (Filter.match(item, word)) {
				branchChecklist.delete(word);
			}
		}
		// Check child items for matches
		let childrenConsumedWords = new Set();
		// Do any child items match the search query?
		for (let child of item.children) {
			let currConsWords = this.classifyMatch(child, branchChecklist);
			// Add all to `consumedWords`:
			currConsWords.forEach(word => childrenConsumedWords.add(word));
		}
		// Cannot proceed if child items do not complete the match (e.g. if this is a leaf node) and there are still 
		// words unaccounted for at this point.
		if (childrenConsumedWords.size === 0 && branchChecklist.size > 0) {
			return new Set(); // Return nothing as no words are used-up.
		}
		// Else: This item is part of a branch forming a match. Set filtered style:
		item.setClass(ITEM_FILTERED_STYLE);
		// Make sure this is visible:
		item.setVisible(true);
		
		// If child items complete a match, expand this in the menu: 
		if (childrenConsumedWords.size > 0) {
			item.expand();
		}
		// Highlight words not otherwise highlighted in child items.
		let availableWords = setDifference(this.wordSet, childrenConsumedWords);
		let itemConsumedWords = new Set();
		for (let word of availableWords) {
			if (Filter.match(item, word)) {
				itemConsumedWords.add(word);
			}
		}
		this.highlightItem(item, itemConsumedWords);
		
		// Record this if it is a bottom item (i.e. a *filter* leaf node):
		if (itemConsumedWords.size > 0 && childrenConsumedWords.size === 0) {
			this.bottomItems.push(item);
		}
		
		return setUnion(itemConsumedWords, childrenConsumedWords);
	}
	highlightItem(item, wordSet) {
		this.highlighted.push(item);
		let nameElem = item.element.querySelector('dt > a > span');
		let text = nameElem.innerText; // innerText deliberately used to avoid <mark> wrapping multiple times.
		for (let word of wordSet) {
			let wordPosition = text.toLowerCase().indexOf(word);
			let wordInOriginalCase = text.substr(wordPosition, word.length);
			text = text.replace(wordInOriginalCase, '<'+wordInOriginalCase+'>');
		}
		let i = 0;
		while(text[i] !== undefined) { 
			// Replace markers as necessary with <mark> tags.
			if (text[i] === '<') {
				text = text.substring(0, i+1) + 'mark>' + text.substring(i+1);
				i += 5;
			} else if (text[i] === '>') {
				text = text.substring(0, i) + '</mark' + text.substring(i);
				i += 6;
			}
			i++;
		}
		nameElem.innerHTML = text;
	}
	/**
	 * @param {MenuItem} item
	 * @param {string}   word
	 * @return {boolean}
	 */
	static match(item, word) {
		return item.name.toLowerCase().includes(word);
	}

	/**
	 * Show an additional item (e.g. sibling items) in a filtered menu view, after a filter has already been applied.
	 * Grants the filtered style and reveals via MenuEssential's slide transition. 
	 * @param {Element} itemElem <dt> element for the item.
	 */
	static showAdditionalItem(itemElem) {
		if (itemElem.classList.contains(ITEM_FILTERED_STYLE)) {
			return;
		}
		// Use MenuEssential's slide transition to animate reveal of additional items.
		$(itemElem).hide();
		itemElem.classList.add(ITEM_FILTERED_STYLE);
		$(itemElem).slideDown(MenuEssential.slideOptions);
	}
}