interface Datum {
	id:    string;
	value: string;
}


export class Mention {
	static Keys = {
		TAB:    9,
		ENTER:  13,
		ESCAPE: 27,
		UP:     38,
		DOWN:   40,
	};
	static numberIsNaN = (x: any) => x !== x;
	private isOpen            = false;
	/**
	 * index of currently selected item.
	 */
	private itemIndex         = 0;
	private mentionCharPos: number | undefined = undefined;
	private cursorPos:      number | undefined = undefined;
	private values            = [] as Datum[];
	private suspendMouseEnter = false;
	private options = {
		source: (searchTerm: string, renderList: Function, mentionChar: string) => {},
		renderItem: (item: Datum, searchTerm: string) => {
			return `${item.value}`;
		},
		onSelect: (item: DOMStringMap, insertItem: (item: DOMStringMap) => void) => {
			insertItem(item);
		},
		mentionDenotationChars: ['@'],
		showDenotationChar: true,
		allowedChars:           /^[a-zA-Z0-9_]*$/,
		minChars:               0,
		maxChars:               31,
		offsetTop:              2,
		offsetLeft:             0,
		/**
		 * Whether or not the denotation character(s) should be isolated. For example, to avoid mentioning in an email.
		 */
		isolateCharacter:       false,
		fixMentionsToQuill:     false,
		defaultMenuOrientation: 'bottom',
		dataAttributes:         ['id', 'value', 'denotationChar', 'link', 'target'],
		linkTarget:             '_blank',
		onOpen:  () => true,
		onClose: () => true,
		// Style options
		listItemClass:         'ql-mention-list-item',
		mentionContainerClass: 'ql-mention-list-container',
		mentionListClass:      'ql-mention-list',
	};
	/// HTML elements
	private mentionContainer = document.createElement('div');
	private mentionList      = document.createElement('ul');
	
	
	constructor(
		private quill: Quill,
	) {
		this.mentionContainer.className = this.options.mentionContainerClass;
		this.mentionContainer.style.cssText = 'display: none; position: absolute;';
		this.mentionContainer.onmousemove = this.onContainerMouseMove.bind(this);
		
		if (this.options.fixMentionsToQuill) {
			this.mentionContainer.style.width = 'auto';
		}
		
		this.mentionList.className = this.options.mentionListClass;
		this.mentionContainer.appendChild(this.mentionList);
		
		this.quill.container.appendChild(this.mentionContainer);
		
		quill.on('text-change', this.onTextChange.bind(this));
		quill.on('selection-change', this.onSelectionChange.bind(this));
		
		quill.keyboard.addBinding({
			key: Mention.Keys.ENTER,
		}, this.selectHandler.bind(this));
		quill.keyboard.bindings[Mention.Keys.ENTER].unshift(
			quill.keyboard.bindings[Mention.Keys.ENTER].pop()
		);
		/// ^^ place it at beginning of bindings.
		
		quill.keyboard.addBinding({
			key: Mention.Keys.ESCAPE,
		}, this.escapeHandler.bind(this));
		
		quill.keyboard.addBinding({
			key: Mention.Keys.UP,
		}, this.upHandler.bind(this));
		
		quill.keyboard.addBinding({
			key: Mention.Keys.DOWN,
		}, this.downHandler.bind(this));
		
		document.addEventListener("keypress", e => {
			/// Quick’n’dirty hack.
			if (! this.quill.hasFocus()) {
				return ;
			}
			setTimeout(() => {
				this.setCursorPos();
				this.quill.removeFormat(this.cursorPos! - 1, 1, 'silent');
			}, 0);
		});
	}
	
	selectHandler() {
		if (this.isOpen) {
			this.selectItem();
			return false;
		}
		return true;
	}
	
	escapeHandler() {
		if (this.isOpen) {
			this.hideMentionList();
			return false;
		}
		return true;
	}
	
	upHandler() {
		if (this.isOpen) {
			this.prevItem();
			return false;
		}
		return true;
	}
	
	downHandler() {
		if (this.isOpen) {
			this.nextItem();
			return false;
		}
		return true;
	}
	
	showMentionList() {
		this.mentionContainer.style.visibility = 'hidden';
		this.mentionContainer.style.display = '';
		this.setMentionContainerPosition();
		this.setIsOpen(true);
	}
	
	hideMentionList() {
		this.mentionContainer.style.display = 'none';
		this.setIsOpen(false);
	}
	
	
	private highlightItem(scrollItemInView = true) {
		const childNodes = Array.from(this.mentionList.childNodes) as HTMLLIElement[];
		for (const node of childNodes) {
			node.classList.remove('selected');
		}
		childNodes[this.itemIndex].classList.add('selected');
		
		if (scrollItemInView) {
			const itemHeight      = childNodes[this.itemIndex].offsetHeight;
			const itemPos         = this.itemIndex * itemHeight;
			const containerTop    = this.mentionContainer.scrollTop;
			const containerBottom = containerTop + this.mentionContainer.offsetHeight;
			
			if (itemPos < containerTop) {
				// Scroll up if the item is above the top of the container
				this.mentionContainer.scrollTop = itemPos;
			} else if (itemPos > (containerBottom - itemHeight)) {
				// scroll down if any part of the element is below the bottom of the container
				this.mentionContainer.scrollTop += (itemPos - containerBottom) + itemHeight;
			}
		}
	}
	
	private getItemData(): DOMStringMap {
		const node = this.mentionList.childNodes[this.itemIndex] as HTMLElement;
		const { link }   = node.dataset;
		const itemTarget = node.dataset.target;
		if (link !== undefined) {
			node.dataset.value = `<a href="${link}" target=${itemTarget || this.options.linkTarget}>${node.dataset.value}`;
		}
		return node.dataset;
	}
	
	onContainerMouseMove() {
		this.suspendMouseEnter = false;
	}
	
	selectItem() {
		const data = this.getItemData();
		this.options.onSelect(data, (asyncData) => {
			this.insertItem(asyncData);
		});
		this.hideMentionList();
	}
	
	insertItem(data: DOMStringMap) {
		const render = data;
		if (render === null) {
			return ;
		}
		if (!this.options.showDenotationChar) {
			render.denotationChar = '';
		}
		if (this.cursorPos === undefined) {
			throw new Error(`Invalid this.cursorPos`);
		}
		if (!render.value) {
			throw new Error(`Didn't receive value from server.`);
		}
		
		this.quill.insertText(this.cursorPos, render.value, 'bold', Quill.sources.USER);
		this.quill.setSelection(this.cursorPos + render.value.length, 0);
		this.setCursorPos();
		this.hideMentionList();
	}
	
	onItemMouseEnter(e: MouseEvent) {
		if (this.suspendMouseEnter) {
			return ;
		}
		const index = Number(
			(e.target as HTMLLIElement).dataset.index
		);
		if (! Mention.numberIsNaN(index) && index !== this.itemIndex) {
			this.itemIndex = index;
			this.highlightItem(false);
		}
	}
	
	onItemClick(e: MouseEvent) {
		e.stopImmediatePropagation();
		e.preventDefault();
		this.itemIndex = Number(
			(e.currentTarget as HTMLElement).dataset.index
		);
		this.highlightItem();
		this.selectItem();
	}
	
	private attachDataValues(element: HTMLLIElement, data: Datum): HTMLLIElement {
		for (const [key, value] of Object.entries(data)) {
			if (this.options.dataAttributes.includes(key)) {
				element.dataset[key] = value;
			} else {
				delete element.dataset[key];
			}
		}
		return element;
	}
	
	renderList(mentionChar: string, data: Datum[], searchTerm: string = "") {
		if (data.length > 0) {
			this.values = data;
			this.mentionList.innerHTML = '';
			
			for (const [i, datum] of data.entries()) {
				const li = document.createElement('li');
				li.className = this.options.listItemClass;
				li.dataset.index = `${i}`;
				// li.innerHTML = this.options.renderItem(datum, searchTerm);
				li.innerText = datum.value.replace(/\n/g, "↵");
				/// ^^
				li.onmouseenter = this.onItemMouseEnter.bind(this);
				li.dataset.denotationChar = mentionChar;
				li.onclick = this.onItemClick.bind(this);
				this.mentionList.appendChild(
					this.attachDataValues(li, datum)
				);
			}
			this.itemIndex = 0;
			this.highlightItem();
			this.showMentionList();
		} else {
			this.hideMentionList();
		}
	}
	
	nextItem() {
		this.itemIndex = (this.itemIndex + 1) % this.values.length;
		this.suspendMouseEnter = true;
		this.highlightItem();
	}
	
	prevItem() {
		this.itemIndex = ((this.itemIndex + this.values.length) - 1) % this.values.length;
		this.suspendMouseEnter = true;
		this.highlightItem();
	}
	
	private hasValidChars(s: string) {
		return this.options.allowedChars.test(s);
	}
	
	private containerBottomIsNotVisible(topPos: number, containerPos: ClientRect | DOMRect) {
		const mentionContainerBottom = topPos + this.mentionContainer.offsetHeight + containerPos.top;
		return mentionContainerBottom > window.pageYOffset + window.innerHeight;
	}
	
	private containerRightIsNotVisible(leftPos: number, containerPos: ClientRect | DOMRect) {
		if (this.options.fixMentionsToQuill) {
			return false;
		}
		const rightPos = leftPos + this.mentionContainer.offsetWidth + containerPos.left;
		const browserWidth = window.pageXOffset + document.documentElement.clientWidth;
		return rightPos > browserWidth;
	}
	
	private setIsOpen(isOpen: boolean) {
		if (this.isOpen !== isOpen) {
			if (isOpen) {
				this.options.onOpen();
			} else {
				this.options.onClose();
			}
			this.isOpen = isOpen;
		}
	}
	
	private setMentionContainerPosition() {
		const containerPos = this.quill.container.getBoundingClientRect();
		/// vv Here we always trigger from the cursor.
		if (this.cursorPos === undefined) {
			throw new Error(`Invalid this.cursorPos`);
		}
		const mentionCharPos = this.quill.getBounds(this.cursorPos);
		const containerHeight = this.mentionContainer.offsetHeight;
		
		let topPos  = this.options.offsetTop;
		let leftPos = this.options.offsetLeft;
		
		// handle horizontal positioning
		if (this.options.fixMentionsToQuill) {
			const rightPos = 0;
			this.mentionContainer.style.right = `${rightPos}px`;
		} else {
			leftPos += mentionCharPos.left;
		}
		
		if (this.containerRightIsNotVisible(leftPos, containerPos)) {
			const containerWidth = this.mentionContainer.offsetWidth + this.options.offsetLeft;
			const quillWidth = containerPos.width;
			leftPos = quillWidth - containerWidth;
		}
		
		// handle vertical positioning
		if (this.options.defaultMenuOrientation === 'top') {
			// Attempt to align the mention container with the top of the quill editor
			if (this.options.fixMentionsToQuill) {
				topPos = -1 * (containerHeight + this.options.offsetTop);
			} else {
				topPos = mentionCharPos.top - (containerHeight + this.options.offsetTop);
			}
			
			// default to bottom if the top is not visible
			if (topPos + containerPos.top <= 0) {
				let overMentionCharPos = this.options.offsetTop;
				
				if (this.options.fixMentionsToQuill) {
					overMentionCharPos += containerPos.height;
				} else {
					overMentionCharPos += mentionCharPos.bottom;
				}
				
				topPos = overMentionCharPos;
			}
		} else {
			// Attempt to align the mention container with the bottom of the quill editor
			if (this.options.fixMentionsToQuill) {
				topPos += containerPos.height;
			} else {
				topPos += mentionCharPos.bottom;
			}
			
			// default to the top if the bottom is not visible
			if (this.containerBottomIsNotVisible(topPos, containerPos)) {
				let overMentionCharPos = this.options.offsetTop * -1;
				
				if (!this.options.fixMentionsToQuill) {
					overMentionCharPos += mentionCharPos.top;
				}
				
				topPos = overMentionCharPos - containerHeight;
			}
		}
		
		this.mentionContainer.style.top  = `${topPos}px`;
		this.mentionContainer.style.left = `${leftPos}px`;
		this.mentionContainer.style.visibility = 'visible';
	}
	
	
	/**
	 * HF Helpers for manual trigger
	 */
	setCursorPos() {
		const range = this.quill.getSelection();
		if (range) {
			this.cursorPos = range.index;
		} else {
			this.quill.setSelection(this.quill.getLength(), 0);
			/// ^^ place cursor at the end of input by default.
			this.cursorPos = this.quill.getLength();
		}
	}
	getCursorPos(): number {
		return this.cursorPos!;
	}
	trigger(values: string[]) {
		this.renderList("", values.map(x => {
			return { id: x, value: x };
		}), "");
	}
	
	onSomethingChange() {
		/// We trigger manually so here we can _probably_ just always close.
		this.hideMentionList();
	}
	
	onTextChange(delta: Delta, oldDelta: Delta, source: Sources) {
		if (source === 'user') {
			this.onSomethingChange();
		}
	}
	
	onSelectionChange(range: RangeStatic) {
		if (range && range.length === 0) {
			this.onSomethingChange();
		} else {
			this.hideMentionList();
		}
	}
}


Quill.register('modules/mention', Mention);