// Copyright (c) 2015, InfluxFramework LLC
// MIT License. See license.txt

import deep_equal from "fast-deep-equal";
import number_systems from "./number_systems";
import cloneDeepWith from "lodash/cloneDeepWith";

influxframework.provide("influxframework.utils");

// Array de duplicate
if (!Array.prototype.uniqBy) {
	Object.defineProperty(Array.prototype, "uniqBy", {
		value: function (key) {
			var seen = {};
			return this.filter(function (item) {
				var k = key(item);
				return k in seen ? false : (seen[k] = true);
			});
		},
	});
	Object.defineProperty(Array.prototype, "move", {
		value: function (from, to) {
			this.splice(to, 0, this.splice(from, 1)[0]);
		},
	});
}

// Python's dict.setdefault ported for JS objects
Object.defineProperty(Object.prototype, "setDefault", {
	value: function (key, default_value) {
		if (!(key in this)) this[key] = default_value;
		return this[key];
	},
	writable: true,
});

// Pluralize
String.prototype.plural = function (revert) {
	const plural = {
		"(quiz)$": "$1zes",
		"^(ox)$": "$1en",
		"([m|l])ouse$": "$1ice",
		"(matr|vert|ind)ix|ex$": "$1ices",
		"(x|ch|ss|sh)$": "$1es",
		"([^aeiouy]|qu)y$": "$1ies",
		"(hive)$": "$1s",
		"(?:([^f])fe|([lr])f)$": "$1$2ves",
		"(shea|lea|loa|thie)f$": "$1ves",
		sis$: "ses",
		"([ti])um$": "$1a",
		"(tomat|potat|ech|her|vet)o$": "$1oes",
		"(bu)s$": "$1ses",
		"(alias)$": "$1es",
		"(octop)us$": "$1i",
		"(ax|test)is$": "$1es",
		"(us)$": "$1es",
		"([^s]+)$": "$1s",
	};

	const singular = {
		"(quiz)zes$": "$1",
		"(matr)ices$": "$1ix",
		"(vert|ind)ices$": "$1ex",
		"^(ox)en$": "$1",
		"(alias)es$": "$1",
		"(octop|vir)i$": "$1us",
		"(cris|ax|test)es$": "$1is",
		"(shoe)s$": "$1",
		"(o)es$": "$1",
		"(bus)es$": "$1",
		"([m|l])ice$": "$1ouse",
		"(x|ch|ss|sh)es$": "$1",
		"(m)ovies$": "$1ovie",
		"(s)eries$": "$1eries",
		"([^aeiouy]|qu)ies$": "$1y",
		"([lr])ves$": "$1f",
		"(tive)s$": "$1",
		"(hive)s$": "$1",
		"(li|wi|kni)ves$": "$1fe",
		"(shea|loa|lea|thie)ves$": "$1f",
		"(^analy)ses$": "$1sis",
		"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$": "$1$2sis",
		"([ti])a$": "$1um",
		"(n)ews$": "$1ews",
		"(h|bl)ouses$": "$1ouse",
		"(corpse)s$": "$1",
		"(us)es$": "$1",
		s$: "",
	};

	const irregular = {
		move: "moves",
		foot: "feet",
		goose: "geese",
		sex: "sexes",
		child: "children",
		man: "men",
		tooth: "teeth",
		person: "people",
	};

	const uncountable = [
		"sheep",
		"fish",
		"deer",
		"moose",
		"series",
		"species",
		"money",
		"rice",
		"information",
		"equipment",
	];

	// save some time in the case that singular and plural are the same
	if (uncountable.indexOf(this.toLowerCase()) >= 0) return this;

	// check for irregular forms
	let word;
	let pattern;
	let replace;
	for (word in irregular) {
		if (revert) {
			pattern = new RegExp(irregular[word] + "$", "i");
			replace = word;
		} else {
			pattern = new RegExp(word + "$", "i");
			replace = irregular[word];
		}
		if (pattern.test(this)) return this.replace(pattern, replace);
	}

	let array;
	if (revert) array = singular;
	else array = plural;

	// check for matches using regular expressions
	let reg;
	for (reg in array) {
		pattern = new RegExp(reg, "i");

		if (pattern.test(this)) return this.replace(pattern, array[reg]);
	}

	return this;
};

Object.assign(influxframework.utils, {
	get_random: function (len) {
		var text = "";
		var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

		for (var i = 0; i < len; i++)
			text += possible.charAt(Math.floor(Math.random() * possible.length));

		return text;
	},
	get_file_link: function (filename) {
		filename = cstr(filename);
		if (influxframework.utils.is_url(filename)) {
			return filename;
		} else if (filename.indexOf("/") === -1) {
			return "files/" + filename;
		} else {
			return filename;
		}
	},
	replace_newlines(t) {
		return t ? t.replace(/\n/g, "<br>") : "";
	},
	is_html: function (txt) {
		if (!txt) return false;

		if (
			txt.indexOf("<br>") == -1 &&
			txt.indexOf("<p") == -1 &&
			txt.indexOf("<img") == -1 &&
			txt.indexOf("<div") == -1 &&
			!txt.includes("<span")
		) {
			return false;
		}
		return true;
	},
	is_mac: function () {
		return window.navigator.platform === "MacIntel";
	},
	is_xs: function () {
		return $(document).width() < 768;
	},
	is_sm: function () {
		return $(document).width() < 991 && $(document).width() >= 768;
	},
	is_md: function () {
		return $(document).width() < 1199 && $(document).width() >= 991;
	},
	is_json: function (str) {
		try {
			JSON.parse(str);
		} catch (e) {
			return false;
		}
		return true;
	},
	parse_json: function (str) {
		let parsed_json = "";
		try {
			parsed_json = JSON.parse(str);
		} catch (e) {
			return str;
		}
		return parsed_json;
	},
	strip_whitespace: function (html) {
		return (html || "").replace(/<p>\s*<\/p>/g, "").replace(/<br>(\s*<br>\s*)+/g, "<br><br>");
	},
	encode_tags: function (html) {
		var tagsToReplace = {
			"&": "&amp;",
			"<": "&lt;",
			">": "&gt;",
		};

		function replaceTag(tag) {
			return tagsToReplace[tag] || tag;
		}

		return html.replace(/[&<>]/g, replaceTag);
	},
	strip_original_content: function (txt) {
		var out = [],
			part = [],
			newline = txt.indexOf("<br>") === -1 ? "\n" : "<br>";

		$.each(txt.split(newline), function (i, t) {
			var tt = strip(t);
			if (tt && (tt.substr(0, 1) === ">" || tt.substr(0, 4) === "&gt;")) {
				part.push(t);
			} else {
				out = out.concat(part);
				out.push(t);
				part = [];
			}
		});
		return out.join(newline);
	},

	escape_html: function (txt) {
		let escape_html_mapping = {
			"&": "&amp;",
			"<": "&lt;",
			">": "&gt;",
			'"': "&quot;",
			"'": "&#39;",
			"/": "&#x2F;",
			"`": "&#x60;",
			"=": "&#x3D;",
		};

		return String(txt).replace(/[&<>"'`=/]/g, (char) => escape_html_mapping[char] || char);
	},

	unescape_html: function (txt) {
		let unescape_html_mapping = {
			"&amp;": "&",
			"&lt;": "<",
			"&gt;": ">",
			"&quot;": '"',
			"&#39;": "'",
			"&#x2F;": "/",
			"&#x60;": "`",
			"&#x3D;": "=",
		};

		return String(txt).replace(
			/&amp;|&lt;|&gt;|&quot;|&#39;|&#x2F;|&#x60;|&#x3D;/g,
			(char) => unescape_html_mapping[char] || char
		);
	},

	html2text: function (html) {
		let d = document.createElement("div");
		d.innerHTML = html;
		return d.textContent;
	},

	is_url: function (txt) {
		return (
			txt.toLowerCase().substr(0, 7) == "http://" ||
			txt.toLowerCase().substr(0, 8) == "https://"
		);
	},
	to_title_case: function (string, with_space = false) {
		let titlecased_string = string.toLowerCase().replace(/(?:^|[\s-/])\w/g, function (match) {
			return match.toUpperCase();
		});

		let replace_with = with_space ? " " : "";

		return titlecased_string.replace(/-|_/g, replace_with);
	},
	toggle_blockquote: function (txt) {
		if (!txt) return txt;

		var content = $("<div></div>").html(txt);
		content
			.find("blockquote")
			.parent("blockquote")
			.addClass("hidden")
			.before(
				'<p><a class="text-muted btn btn-default toggle-blockquote" style="padding: 2px 7px 0px; line-height: 1;"> \
					• • • \
				</a></p>'
			);
		return content.html();
	},
	scroll_to: function (
		element,
		animate = true,
		additional_offset,
		element_to_be_scrolled,
		callback,
		highlight_element = false
	) {
		if (influxframework.flags.disable_auto_scroll) return;

		element_to_be_scrolled = element_to_be_scrolled || $("html, body");
		let scroll_top = 0;
		if (element) {
			// If a number is passed, just subtract the offset,
			// otherwise calculate scroll position from element
			scroll_top =
				typeof element == "number"
					? element - cint(additional_offset)
					: this.get_scroll_position(element, additional_offset);
		}

		if (scroll_top < 0) {
			scroll_top = 0;
		}

		// already there
		if (scroll_top == element_to_be_scrolled.scrollTop()) {
			return;
		}

		if (animate) {
			element_to_be_scrolled
				.animate({
					scrollTop: scroll_top,
				})
				.promise()
				.then(() => {
					if (highlight_element) {
						$(element).addClass("highlight");
						document.addEventListener(
							"click",
							function () {
								$(element).removeClass("highlight");
							},
							{ once: true }
						);
					}
					callback && callback();
				});
		} else {
			element_to_be_scrolled.scrollTop(scroll_top);
		}
	},
	get_scroll_position: function (element, additional_offset) {
		let header_offset =
			$(".navbar").height() + $(".page-head:visible").height() || $(".navbar").height();
		let scroll_top = $(element).offset().top - header_offset - cint(additional_offset);
		return scroll_top;
	},
	filter_dict: function (dict, filters) {
		var ret = [];
		if (typeof filters == "string") {
			return [dict[filters]];
		}
		$.each(dict, function (i, d) {
			for (var key in filters) {
				if ($.isArray(filters[key])) {
					if (filters[key][0] == "in") {
						if (filters[key][1].indexOf(d[key]) == -1) return;
					} else if (filters[key][0] == "not in") {
						if (filters[key][1].indexOf(d[key]) != -1) return;
					} else if (filters[key][0] == "<") {
						if (!(d[key] < filters[key])) return;
					} else if (filters[key][0] == "<=") {
						if (!(d[key] <= filters[key])) return;
					} else if (filters[key][0] == ">") {
						if (!(d[key] > filters[key])) return;
					} else if (filters[key][0] == ">=") {
						if (!(d[key] >= filters[key])) return;
					}
				} else {
					if (d[key] != filters[key]) return;
				}
			}
			ret.push(d);
		});
		return ret;
	},
	comma_or: function (list) {
		return influxframework.utils.comma_sep(list, " " + __("or") + " ");
	},
	comma_and: function (list) {
		return influxframework.utils.comma_sep(list, " " + __("and") + " ");
	},
	comma_sep: function (list, sep) {
		if (list instanceof Array) {
			if (list.length == 0) {
				return "";
			} else if (list.length == 1) {
				return list[0];
			} else {
				return list.slice(0, list.length - 1).join(", ") + sep + list.slice(-1)[0];
			}
		} else {
			return list;
		}
	},
	set_footnote: function (footnote_area, wrapper, txt) {
		if (!footnote_area) {
			footnote_area = $('<div class="text-muted footnote-area level">').appendTo(wrapper);
		}

		if (txt) {
			footnote_area.html(txt);
		} else {
			footnote_area.remove();
			footnote_area = null;
		}
		return footnote_area;
	},
	get_args_dict_from_url: function (txt) {
		var args = {};
		$.each(decodeURIComponent(txt).split("&"), function (i, arg) {
			arg = arg.split("=");
			args[arg[0]] = arg[1];
		});
		return args;
	},
	get_url_from_dict: function (args) {
		return (
			$.map(args, function (val, key) {
				if (val !== null) return encodeURIComponent(key) + "=" + encodeURIComponent(val);
				else return null;
			}).join("&") || ""
		);
	},
	validate_type: function (val, type) {
		// from https://github.com/guillaumepotier/Parsley.js/blob/master/parsley.js#L81
		var regExp;

		switch (type) {
			case "phone":
				regExp = /^([0-9 +_\-,.*#()]){1,20}$/;
				break;
			case "name":
				regExp = /^[\w][\w'-]*([ \w][\w'-]+)*$/;
				break;
			case "number":
				regExp = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
				break;
			case "digits":
				regExp = /^\d+$/;
				break;
			case "alphanum":
				regExp = /^\w+$/;
				break;
			case "email":
				// from https://emailregex.com/
				regExp =
					/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
				break;
			case "url":
				regExp =
					/^((([A-Za-z0-9.+-]+:(?:\/\/)?)(?:[-;:&=\+\,\w]@)?[A-Za-z0-9.-]+(:[0-9]+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/i;
				break;
			case "dateIso":
				regExp = /^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$/;
				break;
			default:
				return false;
		}

		// test regExp if not null
		return "" !== val ? regExp.test(val) : false;
	},
	guess_style: function (text, default_style, _colour) {
		var style = default_style || "default";
		var colour = "gray";
		if (text) {
			if (has_words(["Pending", "Review", "Medium", "Not Approved"], text)) {
				style = "warning";
				colour = "orange";
			} else if (
				has_words(["Open", "Urgent", "High", "Failed", "Rejected", "Error"], text)
			) {
				style = "danger";
				colour = "red";
			} else if (
				has_words(
					[
						"Closed",
						"Finished",
						"Converted",
						"Completed",
						"Complete",
						"Confirmed",
						"Approved",
						"Yes",
						"Active",
						"Available",
						"Paid",
						"Success",
					],
					text
				)
			) {
				style = "success";
				colour = "green";
			} else if (has_words(["Submitted"], text)) {
				style = "info";
				colour = "blue";
			}
		}
		return _colour ? colour : style;
	},

	guess_colour: function (text) {
		return influxframework.utils.guess_style(text, null, true);
	},

	get_indicator_color: function (state) {
		return influxframework.db
			.get_list("Workflow State", { filters: { name: state }, fields: ["name", "style"] })
			.then((res) => {
				const state = res[0];
				if (!state.style) {
					return influxframework.utils.guess_colour(state.name);
				}
				const style = state.style;
				const colour_map = {
					Success: "green",
					Warning: "orange",
					Danger: "red",
					Primary: "blue",
				};

				return colour_map[style];
			});
	},

	sort: function (list, key, compare_type, reverse) {
		if (!list || list.length < 2) return list || [];

		var sort_fn = {
			string: function (a, b) {
				return cstr(a[key]).localeCompare(cstr(b[key]));
			},
			number: function (a, b) {
				return flt(a[key]) - flt(b[key]);
			},
		};

		if (!compare_type) compare_type = typeof list[0][key] === "string" ? "string" : "number";

		list.sort(sort_fn[compare_type]);

		if (reverse) {
			list.reverse();
		}

		return list;
	},

	unique: function (list) {
		var dict = {},
			arr = [];
		for (var i = 0, l = list.length; i < l; i++) {
			if (!(list[i] in dict)) {
				dict[list[i]] = null;
				arr.push(list[i]);
			}
		}
		return arr;
	},

	remove_nulls: function (list) {
		var new_list = [];
		for (var i = 0, l = list.length; i < l; i++) {
			if (!is_null(list[i])) {
				new_list.push(list[i]);
			}
		}
		return new_list;
	},

	all: function (lst) {
		for (var i = 0, l = lst.length; i < l; i++) {
			if (!lst[i]) {
				return false;
			}
		}
		return true;
	},

	dict: function (keys, values) {
		// make dictionaries from keys and values
		var out = [];
		$.each(values, function (row_idx, row) {
			var new_row = {};
			$.each(keys, function (key_idx, key) {
				new_row[key] = row[key_idx];
			});
			out.push(new_row);
		});
		return out;
	},

	sum: function (list) {
		return list.reduce(function (previous_value, current_value) {
			return flt(previous_value) + flt(current_value);
		}, 0.0);
	},

	arrays_equal: function (arr1, arr2) {
		if (!arr1 || !arr2) {
			return false;
		}
		if (arr1.length != arr2.length) {
			return false;
		}
		for (var i = 0; i < arr1.length; i++) {
			if ($.isArray(arr1[i])) {
				if (!influxframework.utils.arrays_equal(arr1[i], arr2[i])) {
					return false;
				}
			} else if (arr1[i] !== arr2[i]) {
				return false;
			}
		}
		return true;
	},

	intersection: function (a, b) {
		// from stackoverflow: http://stackoverflow.com/questions/1885557/simplest-code-for-array-intersection-in-javascript
		/* finds the intersection of
		 * two arrays in a simple fashion.
		 *
		 * PARAMS
		 *  a - first array, must already be sorted
		 *  b - second array, must already be sorted
		 *
		 * NOTES
		 *
		 *  Should have O(n) operations, where n is
		 *    n = MIN(a.length(), b.length())
		 */
		var ai = 0,
			bi = 0;
		var result = new Array();

		// sorted copies
		a = [].concat(a).sort();
		b = [].concat(b).sort();

		while (ai < a.length && bi < b.length) {
			if (a[ai] < b[bi]) {
				ai++;
			} else if (a[ai] > b[bi]) {
				bi++;
			} else {
				/* they're equal */
				result.push(a[ai]);
				ai++;
				bi++;
			}
		}

		return result;
	},

	resize_image: function (reader, callback, max_width, max_height) {
		var tempImg = new Image();
		if (!max_width) max_width = 600;
		if (!max_height) max_height = 400;
		tempImg.src = reader.result;

		tempImg.onload = function () {
			var tempW = tempImg.width;
			var tempH = tempImg.height;
			if (tempW > tempH) {
				if (tempW > max_width) {
					tempH *= max_width / tempW;
					tempW = max_width;
				}
			} else {
				if (tempH > max_height) {
					tempW *= max_height / tempH;
					tempH = max_height;
				}
			}

			var canvas = document.createElement("canvas");
			canvas.width = tempW;
			canvas.height = tempH;
			var ctx = canvas.getContext("2d");
			ctx.drawImage(this, 0, 0, tempW, tempH);
			var dataURL = canvas.toDataURL("image/jpeg");
			setTimeout(function () {
				callback(dataURL);
			}, 10);
		};
	},

	csv_to_array: function (strData, strDelimiter) {
		// Check to see if the delimiter is defined. If not,
		// then default to comma.
		strDelimiter = strDelimiter || ",";

		// Create a regular expression to parse the CSV values.
		var objPattern = new RegExp(
			// Delimiters.
			"(\\" +
				strDelimiter +
				"|\\r?\\n|\\r|^)" +
				// Quoted fields.
				'(?:"([^"]*(?:""[^"]*)*)"|' +
				// Standard fields.
				'([^"\\' +
				strDelimiter +
				"\\r\\n]*))",
			"gi"
		);

		// Create an array to hold our data. Give the array
		// a default empty first row.
		var arrData = [[]];

		// Create an array to hold our individual pattern
		// matching groups.
		var arrMatches = null;

		// Keep looping over the regular expression matches
		// until we can no longer find a match.
		while ((arrMatches = objPattern.exec(strData))) {
			// Get the delimiter that was found.
			var strMatchedDelimiter = arrMatches[1];

			// Check to see if the given delimiter has a length
			// (is not the start of string) and if it matches
			// field delimiter. If id does not, then we know
			// that this delimiter is a row delimiter.
			if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter) {
				// Since we have reached a new row of data,
				// add an empty row to our data array.
				arrData.push([]);
			}

			var strMatchedValue;

			// Now that we have our delimiter out of the way,
			// let's check to see which kind of value we
			// captured (quoted or unquoted).
			if (arrMatches[2]) {
				// We found a quoted value. When we capture
				// this value, unescape any double quotes.
				strMatchedValue = arrMatches[2].replace(new RegExp('""', "g"), '"');
			} else {
				// We found a non-quoted value.
				strMatchedValue = arrMatches[3];
			}

			// Now that we have our value string, let's add
			// it to the data array.
			arrData[arrData.length - 1].push(strMatchedValue);
		}

		// Return the parsed data.
		return arrData;
	},

	warn_page_name_change: function () {
		influxframework.msgprint(__("Note: Changing the Page Name will break previous URL to this page."));
	},

	notify: function (subject, body, route, onclick) {
		console.log("push notifications are evil and deprecated");
	},

	set_title: function (title) {
		influxframework._original_title = title;
		if (influxframework._title_prefix) {
			title = influxframework._title_prefix + " " + title.replace(/<[^>]*>/g, "");
		}
		document.title = title;

		// save for re-routing
		const sub_path = influxframework.router.get_sub_path();
		influxframework.route_titles[sub_path] = title;
	},

	set_title_prefix: function (prefix) {
		influxframework._title_prefix = prefix;

		// reset the original title
		influxframework.utils.set_title(influxframework._original_title);
	},

	is_image_file: function (filename) {
		if (!filename) return false;
		// url can have query params
		filename = filename.split("?")[0];
		return /\.(gif|jpg|jpeg|tiff|png|svg)$/i.test(filename);
	},

	play_sound: function (name) {
		try {
			if (influxframework.boot.user.mute_sounds) {
				return;
			}

			var audio = $("#sound-" + name)[0];
			audio.volume = audio.getAttribute("volume");
			audio.play();
		} catch (e) {
			console.log("Cannot play sound", name, e);
			// pass
		}
	},
	split_emails: function (txt) {
		var email_list = [];

		if (!txt) {
			return email_list;
		}

		// emails can be separated by comma or newline
		txt.split(/[,\n](?=(?:[^"]|"[^"]*")*$)/g).forEach(function (email) {
			email = email.trim();
			if (email) {
				email_list.push(email);
			}
		});

		return email_list;
	},
	supportsES6: (function () {
		try {
			new Function("(a = 0) => a");
			return true;
		} catch (err) {
			return false;
		}
	})(),
	throttle: function (func, wait, options) {
		var context, args, result;
		var timeout = null;
		var previous = 0;
		if (!options) options = {};

		let later = function () {
			previous = options.leading === false ? 0 : Date.now();
			timeout = null;
			result = func.apply(context, args);
			if (!timeout) context = args = null;
		};

		return function () {
			var now = Date.now();
			if (!previous && options.leading === false) previous = now;
			let remaining = wait - (now - previous);
			context = this;
			args = arguments;
			if (remaining <= 0 || remaining > wait) {
				if (timeout) {
					clearTimeout(timeout);
					timeout = null;
				}
				previous = now;
				result = func.apply(context, args);
				if (!timeout) context = args = null;
			} else if (!timeout && options.trailing !== false) {
				timeout = setTimeout(later, remaining);
			}
			return result;
		};
	},
	debounce: function (func, wait, immediate) {
		var timeout;
		return function () {
			var context = this,
				args = arguments;
			var later = function () {
				timeout = null;
				if (!immediate) func.apply(context, args);
			};
			var callNow = immediate && !timeout;
			clearTimeout(timeout);
			timeout = setTimeout(later, wait);
			if (callNow) func.apply(context, args);
		};
	},
	get_form_link: function (
		doctype,
		name,
		html = false,
		display_text = null,
		query_params_obj = null
	) {
		display_text = display_text || name;
		name = encodeURIComponent(name);
		let route = `/app/${encodeURIComponent(doctype.toLowerCase().replace(/ /g, "-"))}/${name}`;
		if (query_params_obj) {
			route += influxframework.utils.make_query_string(query_params_obj);
		}
		if (html) {
			return `<a href="${route}">${display_text}</a>`;
		}
		return route;
	},
	get_route_label(route_str) {
		let route = route_str.split("/");

		if (route[2] === "Report" || route[0] === "query-report") {
			return __("{0} Report", [route[3] || route[1]]);
		}
		if (route[0] === "List") {
			return __("{0} List", [route[1]]);
		}
		if (route[0] === "modules") {
			return __("{0} Modules", [route[1]]);
		}
		if (route[0] === "dashboard") {
			return __("{0} Dashboard", [route[1]]);
		}
		return __(influxframework.utils.to_title_case(route[0], true));
	},
	report_column_total: function (values, column, type) {
		if (column.column.disable_total) {
			return "";
		} else if (values.length > 0) {
			if (column.column.fieldtype == "Percent" || type === "mean") {
				return values.reduce((a, b) => a + flt(b)) / values.length;
			} else if (column.column.fieldtype == "Int") {
				return values.reduce((a, b) => a + cint(b));
			} else if (influxframework.model.is_numeric_field(column.column.fieldtype)) {
				return values.reduce((a, b) => a + flt(b));
			} else {
				return null;
			}
		} else {
			return null;
		}
	},
	setup_search($wrapper, el_class, text_class, data_attr) {
		const $search_input = $wrapper.find('[data-element="search"]').show();
		$search_input.focus().val("");
		const $elements = $wrapper.find(el_class).show();

		$search_input.off("keyup").on("keyup", () => {
			let text_filter = $search_input.val().toLowerCase();
			// Replace trailing and leading spaces
			text_filter = text_filter.replace(/^\s+|\s+$/g, "");
			for (let i = 0; i < $elements.length; i++) {
				const text_element = $elements.eq(i).find(text_class);
				const text = text_element.text().toLowerCase();

				let name = "";
				if (data_attr && text_element.attr(data_attr)) {
					name = text_element.attr(data_attr).toLowerCase();
				}

				if (text.includes(text_filter) || name.includes(text_filter)) {
					$elements.eq(i).css("display", "");
				} else {
					$elements.eq(i).css("display", "none");
				}
			}
		});
	},
	setup_timer(start, end, $element) {
		const increment = end > start;
		let counter = start;

		let interval = setInterval(() => {
			increment ? counter++ : counter--;
			if (increment ? counter > end : counter < end) {
				clearInterval(interval);
				return;
			}
			$element.text(counter);
		}, 1000);
	},

	deep_equal(a, b) {
		return deep_equal(a, b);
	},

	deep_clone(obj, customizer) {
		return cloneDeepWith(obj, customizer);
	},

	file_name_ellipsis(filename, length) {
		let first_part_length = (length * 2) / 3;
		let last_part_length = length - first_part_length;
		let parts = filename.split(".");
		let extn = parts.pop();
		let name = parts.join("");
		let first_part = name.slice(0, first_part_length);
		let last_part = name.slice(-last_part_length);
		if (name.length > length) {
			return `${first_part}...${last_part}.${extn}`;
		} else {
			return filename;
		}
	},
	get_decoded_string(dataURI) {
		// decodes base64 to string
		let parts = dataURI.split(",");
		const encoded_data = parts[1];
		let decoded = atob(encoded_data);
		try {
			const escaped = escape(decoded);
			decoded = decodeURIComponent(escaped);
		} catch (e) {
			// pass decodeURIComponent failure
			// just return atob response
		}
		return decoded;
	},
	copy_to_clipboard(string) {
		const show_success_alert = () => {
			influxframework.show_alert({
				indicator: "green",
				message: __("Copied to clipboard."),
			});
		};
		if (navigator.clipboard && window.isSecureContext) {
			navigator.clipboard.writeText(string).then(show_success_alert);
		} else {
			let input = $("<textarea>");
			$("body").append(input);
			input.val(string).select();

			document.execCommand("copy");
			show_success_alert();
			input.remove();
		}
	},
	is_rtl(lang = null) {
		return ["ar", "he", "fa", "ps"].includes(lang || influxframework.boot.lang);
	},
	bind_actions_with_object($el, object) {
		// remove previously bound event
		$($el).off("click.class_actions");
		// attach new event
		$($el).on("click.class_actions", "[data-action]", (e) => {
			let $target = $(e.currentTarget);
			let action = $target.data("action");
			let method = object[action];
			method ? object[action](e, $target) : null;
		});

		return $el;
	},

	eval(code, context = {}) {
		let variable_names = Object.keys(context);
		let variables = Object.values(context);
		code = `let out = ${code}; return out`;
		try {
			let expression_function = new Function(...variable_names, code);
			return expression_function(...variables);
		} catch (error) {
			console.log("Error evaluating the following expression:"); // eslint-disable-line no-console
			console.error(code); // eslint-disable-line no-console
			throw error;
		}
	},

	get_browser() {
		let ua = navigator.userAgent;
		let tem;
		let M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];

		if (/trident/i.test(M[1])) {
			tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
			return { name: "IE", version: tem[1] || "" };
		}
		if (M[1] === "Chrome") {
			tem = ua.match(/\bOPR|Edge\/(\d+)/);
			if (tem != null) {
				return { name: "Opera", version: tem[1] };
			}
		}
		M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"];
		if ((tem = ua.match(/version\/(\d+)/i)) != null) {
			M.splice(1, 1, tem[1]);
		}
		return {
			name: M[0],
			version: M[1],
		};
	},

	get_formatted_duration(value, duration_options = null) {
		let duration = "";
		if (!duration_options) {
			duration_options = {
				hide_days: 0,
				hide_seconds: 0,
			};
		}
		if (value) {
			let total_duration = influxframework.utils.seconds_to_duration(value, duration_options);

			if (total_duration.days) {
				duration += total_duration.days + __("d", null, "Days (Field: Duration)");
			}
			if (total_duration.hours) {
				duration += duration.length ? " " : "";
				duration += total_duration.hours + __("h", null, "Hours (Field: Duration)");
			}
			if (total_duration.minutes) {
				duration += duration.length ? " " : "";
				duration += total_duration.minutes + __("m", null, "Minutes (Field: Duration)");
			}
			if (total_duration.seconds) {
				duration += duration.length ? " " : "";
				duration += total_duration.seconds + __("s", null, "Seconds (Field: Duration)");
			}
		}
		return duration;
	},

	seconds_to_duration(seconds, duration_options) {
		const round = seconds > 0 ? Math.floor : Math.ceil;
		const total_duration = {
			days: round(seconds / 86400), // 60 * 60 * 24
			hours: round((seconds % 86400) / 3600),
			minutes: round((seconds % 3600) / 60),
			seconds: round(seconds % 60),
		};

		if (duration_options && duration_options.hide_days) {
			total_duration.hours = round(seconds / 3600);
			total_duration.days = 0;
		}

		return total_duration;
	},

	duration_to_seconds(days = 0, hours = 0, minutes = 0, seconds = 0) {
		let value = 0;
		if (days) {
			value += days * 24 * 60 * 60;
		}
		if (hours) {
			value += hours * 60 * 60;
		}
		if (minutes) {
			value += minutes * 60;
		}
		if (seconds) {
			value += seconds;
		}
		return value;
	},

	get_duration_options: function (docfield) {
		let duration_options = {
			hide_days: docfield.hide_days,
			hide_seconds: docfield.hide_seconds,
		};
		return duration_options;
	},

	get_number_system: function (country) {
		if (["Bangladesh", "India", "Myanmar", "Pakistan"].includes(country)) {
			return number_systems.indian;
		} else {
			return number_systems.default;
		}
	},

	map_defaults: {
		center: [19.08, 72.8961],
		zoom: 13,
		tiles: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
		options: {
			attribution:
				'&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
		},
	},

	icon(icon_name, size = "sm", icon_class = "", icon_style = "", svg_class = "") {
		let size_class = "";

		if (typeof size == "object") {
			icon_style += ` width: ${size.width}; height: ${size.height}`;
		} else {
			size_class = `icon-${size}`;
		}
		return `<svg class="icon ${svg_class} ${size_class}" style="${icon_style}">
			<use class="${icon_class}" href="#icon-${icon_name}"></use>
		</svg>`;
	},

	flag(country_code) {
		return `<img
		src="https://flagcdn.com/${country_code}.svg"
		width="20" height="15">`;
	},

	make_chart(wrapper, custom_options = {}) {
		let chart_args = {
			type: "bar",
			colors: ["light-blue"],
			axisOptions: {
				xIsSeries: 1,
				shortenYAxisNumbers: 1,
				xAxisMode: "tick",
				numberFormatter: influxframework.utils.format_chart_axis_number,
			},
		};

		for (let key in custom_options) {
			if (typeof chart_args[key] === "object" && typeof custom_options[key] === "object") {
				chart_args[key] = Object.assign(chart_args[key], custom_options[key]);
			} else {
				chart_args[key] = custom_options[key];
			}
		}

		return new influxframework.Chart(wrapper, chart_args);
	},

	format_chart_axis_number(label, country) {
		const default_country = influxframework.sys_defaults.country;
		return influxframework.utils.shorten_number(label, country || default_country, 3);
	},

	generate_route(item) {
		const type = item.type.toLowerCase();
		if (type === "doctype") {
			item.doctype = item.name;
		}
		let route = "";
		if (!item.route) {
			if (item.link) {
				route = strip(item.link, "#");
			} else if (type === "doctype") {
				let doctype_slug = influxframework.router.slug(item.doctype);

				if (influxframework.model.is_single(item.doctype)) {
					route = doctype_slug;
				} else {
					switch (item.doc_view) {
						case "List":
							if (item.filters) {
								influxframework.route_options = item.filters;
							}
							route = `${doctype_slug}/view/list`;
							break;
						case "Tree":
							route = `${doctype_slug}/view/tree`;
							break;
						case "Report Builder":
							route = `${doctype_slug}/view/report`;
							break;
						case "Dashboard":
							route = `${doctype_slug}/view/dashboard`;
							break;
						case "New":
							route = `${doctype_slug}/new`;
							break;
						case "Calendar":
							route = `${doctype_slug}/view/calendar/default`;
							break;
						case "Kanban":
							route = `${doctype_slug}/view/kanban`;
							break;
						default:
							route = doctype_slug;
					}
				}
			} else if (type === "report") {
				if (item.is_query_report) {
					route = "query-report/" + item.name;
				} else if (!item.doctype) {
					route = "/report/" + item.name;
				} else {
					route = influxframework.router.slug(item.doctype) + "/view/report/" + item.name;
				}
			} else if (type === "page") {
				route = item.name;
			} else if (type === "dashboard") {
				route = `dashboard-view/${item.name}`;
			}
		} else {
			route = item.route;
		}

		if (item.route_options) {
			route +=
				"?" +
				$.map(item.route_options, function (value, key) {
					return encodeURIComponent(key) + "=" + encodeURIComponent(value);
				}).join("&");
		}

		// if(type==="page" || type==="help" || type==="report" ||
		// (item.doctype && influxframework.model.can_read(item.doctype))) {
		//     item.shown = true;
		// }
		return `/app/${route}`;
	},

	shorten_number: function (number, country, min_length = 4, max_no_of_decimals = 2) {
		/* returns the number as an abbreviated string
		 * PARAMS
		 *  number - number to be shortened
		 *  country - country that determines the numnber system to be used
		 *  min_length - length below which the number will not be shortened
		 *	max_no_of_decimals - max number of decimals of the shortened number
		 */

		// return number if total digits is lesser than min_length
		const len = String(number).match(/\d/g).length;
		if (len < min_length) return number.toString();

		const number_system = this.get_number_system(country);
		let x = Math.abs(Math.round(number));
		for (const map of number_system) {
			if (x >= map.divisor) {
				let result = number / map.divisor;
				const no_of_decimals = this.get_number_of_decimals(result);
				/*
					If no_of_decimals is greater than max_no_of_decimals,
					round the number to max_no_of_decimals
				*/
				result =
					no_of_decimals > max_no_of_decimals
						? result.toFixed(max_no_of_decimals)
						: result;
				return result + " " + map.symbol;
			}
		}

		return number.toFixed(max_no_of_decimals);
	},

	get_number_of_decimals: function (number) {
		if (Math.floor(number) === number) return 0;
		return number.toString().split(".")[1].length || 0;
	},

	build_summary_item(summary) {
		if (summary.type == "separator") {
			return $(`<div class="summary-separator">
				<div class="summary-value ${summary.color ? summary.color.toLowerCase() : "text-muted"}">${
				summary.value
			}</div>
			</div>`);
		}
		let df = { fieldtype: summary.datatype };
		let doc = null;
		if (summary.datatype == "Currency") {
			df.options = "currency";
			doc = { currency: summary.currency };
		}

		let value = influxframework.format(summary.value, df, { only_value: true }, doc);
		let color = summary.indicator
			? summary.indicator.toLowerCase()
			: summary.color
			? summary.color.toLowerCase()
			: "";

		return $(`<div class="summary-item">
			<span class="summary-label">${summary.label}</span>
			<div class="summary-value ${color}">${value}</div>
		</div>`);
	},

	print(doctype, docname, print_format, letterhead, lang_code) {
		let w = window.open(
			influxframework.urllib.get_full_url(
				"/printview?doctype=" +
					encodeURIComponent(doctype) +
					"&name=" +
					encodeURIComponent(docname) +
					"&trigger_print=1" +
					"&format=" +
					encodeURIComponent(print_format) +
					"&no_letterhead=" +
					(letterhead ? "0" : "1") +
					"&letterhead=" +
					encodeURIComponent(letterhead) +
					(lang_code ? "&_lang=" + lang_code : "")
			)
		);

		if (!w) {
			influxframework.msgprint(__("Please enable pop-ups"));
			return;
		}
	},

	get_clipboard_data(clipboard_paste_event) {
		let e = clipboard_paste_event;
		let clipboard_data =
			e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
		return clipboard_data.getData("Text");
	},

	add_custom_button(html, action, class_name = "", title = "", btn_type, wrapper, prepend) {
		if (!btn_type) btn_type = "btn-secondary";
		let button = $(
			`<button class="btn ${btn_type} btn-xs ${class_name}" title="${title}">${html}</button>`
		);
		button.click((event) => {
			event.stopPropagation();
			action && action(event);
		});
		!prepend && button.appendTo(wrapper);
		prepend && wrapper.prepend(button);
	},

	sleep(time) {
		return new Promise((resolve) => setTimeout(resolve, time));
	},

	parse_array(array) {
		if (array && array.length !== 0) {
			return array;
		}
		return undefined;
	},

	// simple implementation of python's range
	range(start, end) {
		if (!end) {
			end = start;
			start = 0;
		}
		let arr = [];
		for (let i = start; i < end; i++) {
			arr.push(i);
		}
		return arr;
	},

	get_link_title(doctype, name) {
		if (!doctype || !name || !influxframework._link_titles) {
			return;
		}

		return influxframework._link_titles[doctype + "::" + name];
	},

	add_link_title(doctype, name, value) {
		if (!doctype || !name) {
			return;
		}

		if (!influxframework._link_titles) {
			// for link titles
			influxframework._link_titles = {};
		}

		influxframework._link_titles[doctype + "::" + name] = value;
	},

	fetch_link_title(doctype, name) {
		try {
			return influxframework
				.xcall("influxframework.desk.search.get_link_title", {
					doctype: doctype,
					docname: name,
				})
				.then((title) => {
					influxframework.utils.add_link_title(doctype, name, title);
					return title;
				});
		} catch (error) {
			console.log("Error while fetching link title."); // eslint-disable-line
			console.log(error); // eslint-disable-line
			return Promise.resolve(name);
		}
	},

	only_allow_num_decimal(input) {
		input.on("input", (e) => {
			let self = $(e.target);
			self.val(self.val().replace(/[^0-9.]/g, ""));
			if (
				(e.which != 46 || self.val().indexOf(".") != -1) &&
				(e.which < 48 || e.which > 57)
			) {
				e.preventDefault();
			}
		});
	},

	string_to_boolean(string) {
		switch (string.toLowerCase().trim()) {
			case "t":
			case "true":
			case "y":
			case "yes":
			case "1":
				return true;
			case "f":
			case "false":
			case "n":
			case "no":
			case "0":
			case null:
				return false;
			default:
				return string;
		}
	},

	get_filter_as_json(filters) {
		// convert filter array to json
		let filter = null;

		if (filters.length) {
			filter = {};
			filters.forEach((arr) => {
				filter[arr[1]] = [arr[2], arr[3]];
			});
			filter = JSON.stringify(filter);
		}

		return filter;
	},

	get_filter_from_json(filter_json, doctype) {
		// convert json to filter array
		if (filter_json) {
			if (!filter_json.length) {
				return [];
			}

			const filters_json = new Function(`return ${filter_json}`)();
			if (!doctype) {
				// e.g. return {
				//    priority: (2) ['=', 'Medium'],
				//    status: (2) ['=', 'Open']
				// }
				return filters_json || [];
			}

			// e.g. return [
			//    ['ToDo', 'status', '=', 'Open', false],
			//    ['ToDo', 'priority', '=', 'Medium', false]
			// ]
			return Object.keys(filters_json).map((filter) => {
				let val = filters_json[filter];
				return [doctype, filter, val[0], val[1], false];
			});
		}
	},

	load_video_player() {
		return influxframework.require("video_player.bundle.js");
	},

	is_current_user(user) {
		return user === influxframework.session.user;
	},

	debug: {
		watch_property(obj, prop, callback = console.trace) {
			if (!influxframework.boot.developer_mode) {
				return;
			}
			console.warn("Adding property watcher, make sure to remove it after debugging.");

			// Adapted from https://stackoverflow.com/a/11658693
			// Reused under CC-BY-SA 4.0
			// changes: variable names are changed for consistency with our codebase
			const private_prop = "$_" + prop + "_$";
			obj[private_prop] = obj[prop];

			Object.defineProperty(obj, prop, {
				get: function () {
					return obj[private_prop];
				},
				set: function (value) {
					callback();
					obj[private_prop] = value;
				},
			});
		},
	},
});
