/*!
 *
 * jquery.fancytree.clones.js
 * Support faster lookup of nodes by key and shared ref-ids.
 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
 *
 * Copyright (c) 2008-2019, Martin Wendt (https://wwWendt.de)
 *
 * Released under the MIT license
 * https://github.com/mar10/fancytree/wiki/LicenseInfo
 *
 * @version @VERSION
 * @date @DATE
 */

(function(factory) {
	if (typeof define === "function" && define.amd) {
		// AMD. Register as an anonymous module.
		define(["jquery", "./jquery.fancytree"], factory);
	} else if (typeof module === "object" && module.exports) {
		// Node/CommonJS
		require("./jquery.fancytree");
		module.exports = factory(require("jquery"));
	} else {
		// Browser globals
		factory(jQuery);
	}
})(function($) {
	"use strict";

	/*******************************************************************************
	 * Private functions and variables
	 */
	function _assert(cond, msg) {
		// TODO: see qunit.js extractStacktrace()
		if (!cond) {
			msg = msg ? ": " + msg : "";
			$.error("Assertion failed" + msg);
		}
	}

	/* Return first occurrence of member from array. */
	function _removeArrayMember(arr, elem) {
		// TODO: use Array.indexOf for IE >= 9
		var i;
		for (i = arr.length - 1; i >= 0; i--) {
			if (arr[i] === elem) {
				arr.splice(i, 1);
				return true;
			}
		}
		return false;
	}

	/**
	 * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
	 *
	 * @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
	 * @see http://github.com/garycourt/murmurhash-js
	 * @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
	 * @see http://sites.google.com/site/murmurhash/
	 *
	 * @param {string} key ASCII only
	 * @param {boolean} [asString=false]
	 * @param {number} seed Positive integer only
	 * @return {number} 32-bit positive integer hash
	 */
	function hashMurmur3(key, asString, seed) {
		/*eslint-disable no-bitwise */
		var h1b,
			k1,
			remainder = key.length & 3,
			bytes = key.length - remainder,
			h1 = seed,
			c1 = 0xcc9e2d51,
			c2 = 0x1b873593,
			i = 0;

		while (i < bytes) {
			k1 =
				(key.charCodeAt(i) & 0xff) |
				((key.charCodeAt(++i) & 0xff) << 8) |
				((key.charCodeAt(++i) & 0xff) << 16) |
				((key.charCodeAt(++i) & 0xff) << 24);
			++i;

			k1 =
				((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
				0xffffffff;
			k1 = (k1 << 15) | (k1 >>> 17);
			k1 =
				((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
				0xffffffff;

			h1 ^= k1;
			h1 = (h1 << 13) | (h1 >>> 19);
			h1b =
				((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) &
				0xffffffff;
			h1 =
				(h1b & 0xffff) +
				0x6b64 +
				((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
		}

		k1 = 0;

		switch (remainder) {
			case 3:
				k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
			// fall through
			case 2:
				k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
			// fall through
			case 1:
				k1 ^= key.charCodeAt(i) & 0xff;

				k1 =
					((k1 & 0xffff) * c1 +
						((((k1 >>> 16) * c1) & 0xffff) << 16)) &
					0xffffffff;
				k1 = (k1 << 15) | (k1 >>> 17);
				k1 =
					((k1 & 0xffff) * c2 +
						((((k1 >>> 16) * c2) & 0xffff) << 16)) &
					0xffffffff;
				h1 ^= k1;
		}

		h1 ^= key.length;

		h1 ^= h1 >>> 16;
		h1 =
			((h1 & 0xffff) * 0x85ebca6b +
				((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
			0xffffffff;
		h1 ^= h1 >>> 13;
		h1 =
			((h1 & 0xffff) * 0xc2b2ae35 +
				((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
			0xffffffff;
		h1 ^= h1 >>> 16;

		if (asString) {
			// Convert to 8 digit hex string
			return ("0000000" + (h1 >>> 0).toString(16)).substr(-8);
		}
		return h1 >>> 0;
		/*eslint-enable no-bitwise */
	}

	/*
	 * Return a unique key for node by calculating the hash of the parents refKey-list.
	 */
	function calcUniqueKey(node) {
		var key,
			h1,
			path = $.map(node.getParentList(false, true), function(e) {
				return e.refKey || e.key;
			});

		path = path.join("/");
		// 32-bit has a high probability of collisions, so we pump up to 64-bit
		// https://security.stackexchange.com/q/209882/207588

		h1 = hashMurmur3(path, true);
		key = "id_" + h1 + hashMurmur3(h1 + path, true);

		return key;
	}

	/**
	 * [ext-clones] Return a list of clone-nodes (i.e. same refKey) or null.
	 * @param {boolean} [includeSelf=false]
	 * @returns {FancytreeNode[] | null}
	 *
	 * @alias FancytreeNode#getCloneList
	 * @requires jquery.fancytree.clones.js
	 */
	$.ui.fancytree._FancytreeNodeClass.prototype.getCloneList = function(
		includeSelf
	) {
		var key,
			tree = this.tree,
			refList = tree.refMap[this.refKey] || null,
			keyMap = tree.keyMap;

		if (refList) {
			key = this.key;
			// Convert key list to node list
			if (includeSelf) {
				refList = $.map(refList, function(val) {
					return keyMap[val];
				});
			} else {
				refList = $.map(refList, function(val) {
					return val === key ? null : keyMap[val];
				});
				if (refList.length < 1) {
					refList = null;
				}
			}
		}
		return refList;
	};

	/**
	 * [ext-clones] Return true if this node has at least another clone with same refKey.
	 * @returns {boolean}
	 *
	 * @alias FancytreeNode#isClone
	 * @requires jquery.fancytree.clones.js
	 */
	$.ui.fancytree._FancytreeNodeClass.prototype.isClone = function() {
		var refKey = this.refKey || null,
			refList = (refKey && this.tree.refMap[refKey]) || null;
		return !!(refList && refList.length > 1);
	};

	/**
	 * [ext-clones] Update key and/or refKey for an existing node.
	 * @param {string} key
	 * @param {string} refKey
	 * @returns {boolean}
	 *
	 * @alias FancytreeNode#reRegister
	 * @requires jquery.fancytree.clones.js
	 */
	$.ui.fancytree._FancytreeNodeClass.prototype.reRegister = function(
		key,
		refKey
	) {
		key = key == null ? null : "" + key;
		refKey = refKey == null ? null : "" + refKey;
		// this.debug("reRegister", key, refKey);

		var tree = this.tree,
			prevKey = this.key,
			prevRefKey = this.refKey,
			keyMap = tree.keyMap,
			refMap = tree.refMap,
			refList = refMap[prevRefKey] || null,
			//		curCloneKeys = refList ? node.getCloneList(true),
			modified = false;

		// Key has changed: update all references
		if (key != null && key !== this.key) {
			if (keyMap[key]) {
				$.error(
					"[ext-clones] reRegister(" +
						key +
						"): already exists: " +
						this
				);
			}
			// Update keyMap
			delete keyMap[prevKey];
			keyMap[key] = this;
			// Update refMap
			if (refList) {
				refMap[prevRefKey] = $.map(refList, function(e) {
					return e === prevKey ? key : e;
				});
			}
			this.key = key;
			modified = true;
		}

		// refKey has changed
		if (refKey != null && refKey !== this.refKey) {
			// Remove previous refKeys
			if (refList) {
				if (refList.length === 1) {
					delete refMap[prevRefKey];
				} else {
					refMap[prevRefKey] = $.map(refList, function(e) {
						return e === prevKey ? null : e;
					});
				}
			}
			// Add refKey
			if (refMap[refKey]) {
				refMap[refKey].append(key);
			} else {
				refMap[refKey] = [this.key];
			}
			this.refKey = refKey;
			modified = true;
		}
		return modified;
	};

	/**
	 * [ext-clones] Define a refKey for an existing node.
	 * @param {string} refKey
	 * @returns {boolean}
	 *
	 * @alias FancytreeNode#setRefKey
	 * @requires jquery.fancytree.clones.js
	 * @since 2.16
	 */
	$.ui.fancytree._FancytreeNodeClass.prototype.setRefKey = function(refKey) {
		return this.reRegister(null, refKey);
	};

	/**
	 * [ext-clones] Return all nodes with a given refKey (null if not found).
	 * @param {string} refKey
	 * @param {FancytreeNode} [rootNode] optionally restrict results to descendants of this node
	 * @returns {FancytreeNode[] | null}
	 * @alias Fancytree#getNodesByRef
	 * @requires jquery.fancytree.clones.js
	 */
	$.ui.fancytree._FancytreeClass.prototype.getNodesByRef = function(
		refKey,
		rootNode
	) {
		var keyMap = this.keyMap,
			refList = this.refMap[refKey] || null;

		if (refList) {
			// Convert key list to node list
			if (rootNode) {
				refList = $.map(refList, function(val) {
					var node = keyMap[val];
					return node.isDescendantOf(rootNode) ? node : null;
				});
			} else {
				refList = $.map(refList, function(val) {
					return keyMap[val];
				});
			}
			if (refList.length < 1) {
				refList = null;
			}
		}
		return refList;
	};

	/**
	 * [ext-clones] Replace a refKey with a new one.
	 * @param {string} oldRefKey
	 * @param {string} newRefKey
	 * @alias Fancytree#changeRefKey
	 * @requires jquery.fancytree.clones.js
	 */
	$.ui.fancytree._FancytreeClass.prototype.changeRefKey = function(
		oldRefKey,
		newRefKey
	) {
		var i,
			node,
			keyMap = this.keyMap,
			refList = this.refMap[oldRefKey] || null;

		if (refList) {
			for (i = 0; i < refList.length; i++) {
				node = keyMap[refList[i]];
				node.refKey = newRefKey;
			}
			delete this.refMap[oldRefKey];
			this.refMap[newRefKey] = refList;
		}
	};

	/*******************************************************************************
	 * Extension code
	 */
	$.ui.fancytree.registerExtension({
		name: "clones",
		version: "@VERSION",
		// Default options for this extension.
		options: {
			highlightActiveClones: true, // set 'fancytree-active-clone' on active clones and all peers
			highlightClones: false, // set 'fancytree-clone' class on any node that has at least one clone
		},

		treeCreate: function(ctx) {
			this._superApply(arguments);
			ctx.tree.refMap = {};
			ctx.tree.keyMap = {};
		},
		treeInit: function(ctx) {
			this.$container.addClass("fancytree-ext-clones");
			_assert(ctx.options.defaultKey == null);
			// Generate unique / reproducible default keys
			ctx.options.defaultKey = function(node) {
				return calcUniqueKey(node);
			};
			// The default implementation loads initial data
			this._superApply(arguments);
		},
		treeClear: function(ctx) {
			ctx.tree.refMap = {};
			ctx.tree.keyMap = {};
			return this._superApply(arguments);
		},
		treeRegisterNode: function(ctx, add, node) {
			var refList,
				len,
				tree = ctx.tree,
				keyMap = tree.keyMap,
				refMap = tree.refMap,
				key = node.key,
				refKey = node && node.refKey != null ? "" + node.refKey : null;

			//		ctx.tree.debug("clones.treeRegisterNode", add, node);

			if (node.isStatusNode()) {
				return this._super(ctx, add, node);
			}

			if (add) {
				if (keyMap[node.key] != null) {
					var other = keyMap[node.key],
						msg =
							"clones.treeRegisterNode: duplicate key '" +
							node.key +
							"': /" +
							node.getPath(true) +
							" => " +
							other.getPath(true);
					// Sometimes this exception is not visible in the console,
					// so we also write it:
					tree.error(msg);
					$.error(msg);
				}
				keyMap[key] = node;

				if (refKey) {
					refList = refMap[refKey];
					if (refList) {
						refList.push(key);
						if (
							refList.length === 2 &&
							ctx.options.clones.highlightClones
						) {
							// Mark peer node, if it just became a clone (no need to
							// mark current node, since it will be rendered later anyway)
							keyMap[refList[0]].renderStatus();
						}
					} else {
						refMap[refKey] = [key];
					}
					// node.debug("clones.treeRegisterNode: add clone =>", refMap[refKey]);
				}
			} else {
				if (keyMap[key] == null) {
					$.error(
						"clones.treeRegisterNode: node.key not registered: " +
							node.key
					);
				}
				delete keyMap[key];
				if (refKey) {
					refList = refMap[refKey];
					// node.debug("clones.treeRegisterNode: remove clone BEFORE =>", refMap[refKey]);
					if (refList) {
						len = refList.length;
						if (len <= 1) {
							_assert(len === 1);
							_assert(refList[0] === key);
							delete refMap[refKey];
						} else {
							_removeArrayMember(refList, key);
							// Unmark peer node, if this was the only clone
							if (
								len === 2 &&
								ctx.options.clones.highlightClones
							) {
								//							node.debug("clones.treeRegisterNode: last =>", node.getCloneList());
								keyMap[refList[0]].renderStatus();
							}
						}
						// node.debug("clones.treeRegisterNode: remove clone =>", refMap[refKey]);
					}
				}
			}
			return this._super(ctx, add, node);
		},
		nodeRenderStatus: function(ctx) {
			var $span,
				res,
				node = ctx.node;

			res = this._super(ctx);

			if (ctx.options.clones.highlightClones) {
				$span = $(node[ctx.tree.statusClassPropName]);
				// Only if span already exists
				if ($span.length && node.isClone()) {
					//				node.debug("clones.nodeRenderStatus: ", ctx.options.clones.highlightClones);
					$span.addClass("fancytree-clone");
				}
			}
			return res;
		},
		nodeSetActive: function(ctx, flag, callOpts) {
			var res,
				scpn = ctx.tree.statusClassPropName,
				node = ctx.node;

			res = this._superApply(arguments);

			if (ctx.options.clones.highlightActiveClones && node.isClone()) {
				$.each(node.getCloneList(true), function(idx, n) {
					// n.debug("clones.nodeSetActive: ", flag !== false);
					$(n[scpn]).toggleClass(
						"fancytree-active-clone",
						flag !== false
					);
				});
			}
			return res;
		},
	});
	// Value returned by `require('jquery.fancytree..')`
	return $.ui.fancytree;
}); // End of closure
