<script lang="ts">
import {defineComponent, PropType} from 'vue';
import SearchIcon from './icon/SearchIcon.vue';
import LogInIcon from './icon/LogInIcon.vue';
import InfoIcon from './icon/InfoIcon.vue';
import {TolMap, SearchSugg, SearchSuggResponse, UiOptions} from '../lib';
import {LayoutNode} from '../layout';

// Displays a search box, and sends search requests
export default defineComponent({
	data(){
		return {
			searchSuggs: [] as SearchSugg[],
			searchHasMoreSuggs: false,
			focusedSuggIdx: null as null | number, // Denotes a search-suggestion selected using the arrow keys
			lastSuggReqTime: 0, // Set when a search-suggestions request is initiated
			pendingSuggReqUrl: '', // Used by a pendingSuggReq callback to use the latest user input
			pendingDelayedSuggReq: 0, // Set via setTimeout() for a non-initial search-suggestions request
		};
	},
	props: {
		tolMap: {type: Object as PropType<TolMap>, required: true},
		uiOpts: {type: Object as PropType<UiOptions>, required: true},
	},
	computed: {
		infoIconStyles(): Record<string,string> {
			let size = '18px';
			return {
				width: size,
				height: size,
				minWidth: size,
				minHeight: size,
				margin: '2px',
			};
		},
		suggDisplayStrings(): [string, string, string][] {
			let result: [string, string, string][] = [];
			let input = (this.$refs.searchInput as HTMLInputElement).value;
			// For each SearchSugg
			for (let sugg of this.searchSuggs){
				let idx = sugg.name.indexOf(input);
				// Split suggestion text into parts before/within/after an input match
				let strings: [string, string, string];
				if (idx != -1){
					strings = [sugg.name.substring(0, idx), input, sugg.name.substring(idx + input.length)];
				} else {
					strings = [input, '', ''];
				}
				// Indicate a distinct canonical-name
				if (sugg.canonicalName != null){
					strings[2] += ` (aka ${sugg.canonicalName})`;
				}
				//
				result.push(strings);
			}
			return result;
		},
	},
	methods: {
		onCloseClick(evt: Event){
			if (evt.target == this.$el){
				this.$emit('close');
			}
		},
		onSearch(){
			if (this.focusedSuggIdx == null){
				this.resolveSearch((this.$refs.searchInput as HTMLInputElement).value.toLowerCase())
			} else {
				let sugg = this.searchSuggs[this.focusedSuggIdx]
				this.resolveSearch(sugg.canonicalName || sugg.name);
			}
		},
		onSearchModeChg(){
			this.uiOpts.searchJumpMode = !this.uiOpts.searchJumpMode;
			this.$emit('setting-chg', 'searchJumpMode');
		},
		resolveSearch(tolNodeName: string){
			if (tolNodeName == ''){
				return;
			}
			// Asks server for nodes in parent-chain, updates tolMap, then emits search event
			let url = new URL(window.location.href);
			url.pathname = '/data/chain';
			url.search = '?name=' + encodeURIComponent(tolNodeName);
			url.search += (this.uiOpts.useReducedTree ? '&tree=reduced' : '');
			fetch(url.toString())
				.then(response => response.json())
				.then(obj => {
					let keys = Object.getOwnPropertyNames(obj);
					if (keys.length > 0){
						keys.forEach(key => {
							if (!this.tolMap.has(key)){
								this.tolMap.set(key, obj[key])
							}
						});
						this.$emit('search', tolNodeName);
					} else {
						// Trigger failure animation
						let input = this.$refs.searchInput as HTMLInputElement;
						input.classList.remove('animate-red-then-fade');
						input.offsetWidth; // Triggers reflow
						input.classList.add('animate-red-then-fade');
					}
				})
				.catch(error => {
					console.log('ERROR loading tolnode chain', error);
				});
		},
		focusInput(){
			(this.$refs.searchInput as HTMLInputElement).focus();
		},
		onInput(){
			let input = this.$refs.searchInput as HTMLInputElement;
			// Check for empty input
			if (input.value.length == 0){
				this.searchSuggs = [];
				this.searchHasMoreSuggs = false;
				this.focusedSuggIdx = null;
				return;
			}
			// Get URL to use for querying search-suggestions
			let url = new URL(window.location.href);
			url.pathname = '/data/search';
			url.search = '?name=' + encodeURIComponent(input.value);
			url.search += this.uiOpts.useReducedTree ? '&tree=reduced' : '';
			url.search += '&limit=' + this.uiOpts.searchSuggLimit;
			// Query server, delaying/ignoring if a request was recently sent
			this.pendingSuggReqUrl = url.toString();
			let doReq = () => {
				return fetch(this.pendingSuggReqUrl)
					.then(response => {
						if (!response.ok){
							throw new Error('Server response not OK')
						}
						return response.json()
					})
					.then((results: SearchSuggResponse) => {
						this.searchSuggs = results.suggs;
						this.searchHasMoreSuggs = results.hasMore;
						this.focusedSuggIdx = null;
					})
					.catch(error => {
						console.error('Error encountered during fetch operation', error);
					});
			};
			let currentTime = new Date().getTime();
			if (this.lastSuggReqTime == 0){
				this.lastSuggReqTime = currentTime;
				doReq().finally(() => {
					if (this.lastSuggReqTime == currentTime){
						this.lastSuggReqTime = 0;
					}
				});
			} else if (this.pendingDelayedSuggReq == 0){
				this.lastSuggReqTime = currentTime;
				this.pendingDelayedSuggReq = setTimeout(() => {
					this.pendingDelayedSuggReq = 0;
					doReq().finally(() => {
						if (this.lastSuggReqTime == currentTime){
							this.lastSuggReqTime = 0;
						}
					});
				}, 300);
			}
		},
		onDownKey(){
			// Select next search-suggestion, if any
			if (this.searchSuggs.length > 0){
				if (this.focusedSuggIdx == null){
					this.focusedSuggIdx = 0;
				} else {
					this.focusedSuggIdx = Math.min(this.focusedSuggIdx + 1, this.searchSuggs.length - 1);
				}
			}
		},
		onUpKey(){
			// Select previous search-suggestion, or cancel selection
			if (this.focusedSuggIdx != null){
				if (this.focusedSuggIdx == 0){
					this.focusedSuggIdx = null;
				} else {
					this.focusedSuggIdx -= 1;
				}
			}
		},
		onInfoIconClick(nodeName: string){
			this.$emit('info-click', nodeName);
		},
	},
	mounted(){
		(this.$refs.searchInput as HTMLInputElement).focus();
	},
	components: {SearchIcon, InfoIcon, LogInIcon, },
	emits: ['search', 'close', 'info-click', 'setting-chg', ],
});
</script>

<template>
<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onCloseClick">
	<div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2
		bg-stone-50 rounded-md shadow shadow-black flex">
		<div class="relative">
			<input type="text" class="block border p-1 m-2" ref="searchInput"
				@keyup.enter="onSearch" @keyup.esc="onCloseClick"
				@input="onInput" @keydown.down.prevent="onDownKey" @keydown.up.prevent="onUpKey"/>
			<div class="absolute top-[100%] w-full">
				<div v-for="(sugg, idx) of searchSuggs"
					:style="{backgroundColor: idx == focusedSuggIdx ? '#a3a3a3' : 'white'}"
					class="border p-1 hover:underline hover:cursor-pointer"
					@click="resolveSearch(sugg.canonicalName || sugg.name)">
					<span>{{suggDisplayStrings[idx][0]}}</span>
					<span class="font-bold">{{suggDisplayStrings[idx][1]}}</span>
					<span>{{suggDisplayStrings[idx][2]}}</span>
					<info-icon :style="infoIconStyles"
						class="float-right text-stone-500 hover:text-stone-900 hover:cursor-pointer"
						@click.stop="onInfoIconClick(sugg.canonicalName || sugg.name)"/>
				</div>
				<div v-if="searchHasMoreSuggs" class="bg-white px-1 text-center border">...</div>
			</div>
		</div>
		<div class="my-auto hover:cursor-pointer hover:brightness-75 rounded border shadow">
			<search-icon @click.stop="onSearch" class="block w-8 h-8"/>
		</div>
		<div class="my-auto mx-2 hover:cursor-pointer hover:brightness-75 rounded">
			<log-in-icon @click.stop="onSearchModeChg" class="block w-8 h-8"
				:class="uiOpts.searchJumpMode ? 'text-stone-500' : 'text-stone-300'"/>
		</div>
	</div>
</div>
</template>

<style>
.animate-red-then-fade {
	animation-name: red-then-fade;
	animation-duration: 500ms;
	animation-timing-function: ease-in;
}
@keyframes red-then-fade {
	from {
		background-color: rgba(255,0,0,0.2);
	}
	to {
		background-color: transparent;
	}
}
</style>
