/*
 * This file is part of OsmInEdit, released under ISC license (see LICENSE.md)
 *
 * Copyright (c) Adrien Pavie 2019
 * Copyright (c) Daimler AG 2019
 *
 * Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

import request from 'request-promise-native';
import { parseString } from 'xml2js';
import { mergeDeep } from '../utils';
import Fuse from 'fuse.js';
import I18n from '../config/locales/ui';

/**
 * Presets manager handles loading, listing and filtering of presets for OSM features.
 */
class PresetsManager {
	constructor() {
		this._presetsURL = [ './presets/indoor.xml', './presets/default.xml' ];
		this._presets = null;
		this._loading = false;
	}

	/**
	 * Make preset data available in memory.
	 */
	loadPresets() {
		this._loading = true;

		// Download presets XML files using promises
		const loaders = Promise.all(
			this._presetsURL.map(filepath => {
				return request(window.EDITOR_URL + filepath)                     // Download
				.then(this._xmlToJson)                                           // Convert XML into raw JSON
				.then(xml => this._simplifyJSONPreset(xml.presets, "presets"))   // Clean JSON
				.catch(() => null);
			})
		);

		// Once everything is downloaded
		loaders.then(presets => {
			// Merge all presets into a single one
			this._presets = mergeDeep({}, ...presets.filter(p => p !== null));

			/*
			 * Generate search index
			 */

			// Generate flat list of items
			const getItems = item => {
				let matches = [];

				if(item.items) {
					matches = matches.concat(item.items.map(it => {
						if(it.tags) {
							it.kv = Object.entries(it.tags).flat().map(v => v.replace(/_/g, " "));
						}
						return it;
					}));
				}

				if(item.groups) {
					matches = matches.concat(item.groups.map(it => getItems(it)).flat());
				}

				return matches;
			};

			// Set options for search
			this._searcher = new Fuse(
				getItems(this._presets),
				{
					shouldSort: true,
					threshold: 0.4,
					maxPatternLength: 32,
					minMatchCharLength: 3,
					keys: [
						{ name: "name", weight: 0.7 },
						{ name: "kv", weight: 0.3 }
					]
				}
			);

			// Set loading done
			this._loading = false;
		});
	}

	/**
	 * Get the list of available presets
	 * @param {string} path The access path (list of groups separated by /, example "/root/highways/unpaved")
	 * @param {function} [filter] Function filtering presets
	 * @return {Object[]} The presets, possibly organised by groups
	 */
	getPresets(path, filter) {
		if(this._presets) {
			let result;

			if(path === "/") {
				result = this._presets;
			}
			else {
				const entries = path.split("/").filter(p => p !== "");
				let last = this._presets;

				// Look up each entry in path
				for(let e of entries) {
					let sub = (last.groups && last.items) ? last.groups.concat(last.items) : (last.items ? last.items : last.groups);
					sub = sub.filter(g => g.name === e);

					if(sub.length === 1) {
						last = sub[0];
					}
					else {
						last = null;
						break;
					}
				}

				result = last;
			}

			return this._applyFiltering(result, filter);
		}
		else {
			return null;
		}
	}

	/**
	 * Search for presets by text.
	 * @param {string} text The search text
	 * @param {function} [filter] Function filtering presets
	 * @return {Object[]} List of matching presets
	 */
	findPresetsByName(text, filter) {
		if(!text || text.trim().length === 0) {
			return null;
		}
		else if(this._presets) {
			const res = this._searcher.search(text);
			return this._applyFiltering({ items: res }, filter);
		}
		else {
			return null;
		}
	}

	/**
	 * @private
	 */
	_applyFiltering(presets, filter) {
		// Filter presets if necessary
		if(presets && filter) {
			// Function to process one preset/group at a time
			const recursiveFilter = g => {
				let res = Object.assign({}, g);

				// Filter list of items
				if(g.items) { res.items = g.items.filter(filter); }

				// Filter list of groups (recursively)
				if(g.groups) { res.groups = g.groups.map(gr => recursiveFilter(gr)).filter(g => g !== null); }

				// If empty list of groups and items, send null
				if((!res.groups || res.groups.length === 0) && (!res.items || res.items.length === 0)) { res = null; }

				// If entry has a single group and no items, just send the group itself
				if(res && res.groups && res.groups.length === 1 && (!res.items || res.items.length === 0)) { res = res.groups[0]; }

				return res;
			};

			presets = recursiveFilter(presets);
		}

		return presets;
	}

	/**
	 * Search presets available for given feature
	 * @param {Object} feature The feature to use
	 * @return {Object[]} List of matching presets
	 */
	findPresetsForFeature(feature) {
		if(this._presets) {
			const matches = [];
			const fTags = feature.properties.tags;

			// Recursive function for checking one preset at a time
			const check = preset => {
				if(preset.groups) {
					preset.groups.forEach(check);
				}
				if(preset.items) {
					preset.items
					.filter(item => item.tags)
					.forEach(item => {
						let valid = true;

						for(const k in item.tags) {
							if(item.tagMatch[k] !== "none" && fTags[k] !== item.tags[k]) {
								if(fTags[k] === undefined || item.tagMatch[k] !== "key") {
									valid = false;
									break;
								}
							}
						}

						if(valid) {
							matches.push(item);
						}
					});
				}
			};

			check(this._presets);
			return matches;
		}
		else {
			return null;
		}
	}

	/**
	 * Make JSON preset more easily readable
	 * @private
	 */
	_simplifyJSONPreset(p, type) {
		let res = {};

		// Convert various fields as simpler arrays
		const transformArray = (variable, types) => {
			if(types.includes(type) && p[variable]) {
				res[variable+"s"] = p[variable].map(obj => this._simplifyJSONPreset(obj, variable));
			}
		};

		// Optionals
		if([ "item", "chunk" ].includes(type) && p.optional) {
			res.optionals = mergeDeep({}, ...p.optional.map(obj => this._simplifyJSONPreset(obj, "optional")));
		}

		// Checkgroups
		if(p.checkgroup) {
			p.check = (p.check || []);
			p.checkgroup.forEach(cg => {
				p.check = p.check.concat(cg.check);
			});
		}

		transformArray("group",       [ "presets", "group" ]);
		transformArray("item",        [ "group" ]);
		transformArray("link",        [ "item", "chunk", "optional" ]);
		transformArray("text",        [ "item", "chunk", "optional" ]);
		transformArray("combo",       [ "item", "chunk", "optional" ]);
		transformArray("reference",   [ "item", "chunk", "optional" ]);
		transformArray("multiselect", [ "item", "chunk", "optional" ]);
		transformArray("list_entry",  [ "combo", "multiselect" ]);
		transformArray("check", [ "item", "chunk", "optional" ]);

		// Chunks
		if(type === "presets" && p.chunk) {
			const chunks = {};
			p.chunk.map(c => this._simplifyJSONPreset(c, "chunk")).forEach(c => { chunks[c.id] = c; });
			res.chunks = chunks;
		}

		// Keys
		if([ "item", "chunk", "optional" ].includes(type) && p.key) {
			const tags = {};
			const tagMatch = {};
			p.key.map(k => this._simplifyJSONPreset(k, "key")).forEach(t => {
				tags[t.key] = t.value;
				if(t.match && t.match !== "keyvalue") {
					tagMatch[t.key] = t.match;
				}
			});
			res.tags = tags;
			res.tagMatch = tagMatch;
		}

		// Copy properties
		[
			"icon", "name", "key", "value", "type", "wiki", "id", "ref", "href",
			"text", "default", "use_last_as_default", "auto_increment", "length",
			"alternative_autocomplete_keys", "match", "values", "editable", "delimiter", "indoor_structure",
			"values_from", "display_values", "short_descriptions", "values_searchable", "use_last_as_default",
			"values_no_i18n", "values_sort", "rows", "short_description", "icon_size", "display_value"
		].forEach(prop => {
			if(p.$ && p.$[prop]) { res[prop] = p.$[prop]; }
		});

		// Replace some properties by their translation (if available)
		if(
			I18n.locale && I18n.locale !== "en"
			&& [ "group", "item", "label", "text", "combo", "multiselect", "list_entry", "check" ].includes(type)
		) {
			const i18nFields = [ "name", "text", "display_values", "display_value" ];

			i18nFields.forEach(field => {
				if(p.$[I18n.locale+"."+field]) {
					res[field] = p.$[I18n.locale+"."+field];
				}
			});
		}

		// Object types
		if(res.type) {
			res.type = res.type.split(",");
		}

		// Replace references by actual style
		if(type === "presets" && res.chunks) {
			const replaceByChunk = (entry) => {
				[ "groups", "items" ].forEach(v => {
					if(entry[v]) {
						entry[v] = entry[v].map(replaceByChunk);
					}
				});

				if(entry.optionals) {
					entry.optionals = replaceByChunk(entry.optionals);
				}

				if(entry.references) {
					entry = mergeDeep({}, entry, ...entry.references.map(r => res.chunks[r.ref]));
					delete entry.references;
				}
				return entry;
			};

			res = replaceByChunk(res);
		}

		return res;
	}

	/**
	 * Convert XML into JSON
	 * @private
	 */
	_xmlToJson(xml) {
		return new Promise((resolve, reject) => {
			parseString(xml, (err, result) => {
				if (err) { reject(err); }
				else { resolve(result); }
			});
		});
	}
}

export default PresetsManager;
