/*!
 * jquery.fancytree.fixed.js
 *
 * Add fixed colums and headers to ext.table.
 * (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
 */

// Allow to use multiple var statements inside a function

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

	/******************************************************************************
	 * Private functions and variables
	 */

	$.ui.fancytree.registerExtension({
		name: "fixed",
		version: "0.0.1",
		// Default options for this extension.
		options: {
			fixCol: 1,
			fixColWidths: null,
			fixRows: true,
			scrollSpeed: 50,
			resizable: true,
			classNames: {
				table: "fancytree-ext-fixed",
				wrapper: "fancytree-ext-fixed-wrapper",
				topLeft: "fancytree-ext-fixed-wrapper-tl",
				topRight: "fancytree-ext-fixed-wrapper-tr",
				bottomLeft: "fancytree-ext-fixed-wrapper-bl",
				bottomRight: "fancytree-ext-fixed-wrapper-br",
				hidden: "fancytree-ext-fixed-hidden",
				counterpart: "fancytree-ext-fixed-node-counterpart",
				scrollBorderBottom: "fancytree-ext-fixed-scroll-border-bottom",
				scrollBorderRight: "fancytree-ext-fixed-scroll-border-right",
				hover: "fancytree-ext-fixed-hover",
			},
		},
		// Overide virtual methods for this extension.
		// `this`	   : is this extension object
		// `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree)
		treeInit: function(ctx) {
			this._requireExtension("table", true, true);
			// 'fixed' requires the table extension to be loaded before itself

			var res = this._superApply(arguments),
				tree = ctx.tree,
				options = this.options.fixed,
				fcn = this.options.fixed.classNames,
				$table = tree.widget.element,
				fixedColCount = options.fixCols,
				fixedRowCount = options.fixRows,
				$tableWrapper = $table.parent(),
				$topLeftWrapper = $("<div>").addClass(fcn.topLeft),
				$topRightWrapper = $("<div>").addClass(fcn.topRight),
				$bottomLeftWrapper = $("<div>").addClass(fcn.bottomLeft),
				$bottomRightWrapper = $("<div>").addClass(fcn.bottomRight),
				tableStyle = $table.attr("style"),
				tableClass = $table.attr("class"),
				$topLeftTable = $("<table>")
					.attr("style", tableStyle)
					.attr("class", tableClass),
				$topRightTable = $("<table>")
					.attr("style", tableStyle)
					.attr("class", tableClass),
				$bottomLeftTable = $table,
				$bottomRightTable = $("<table>")
					.attr("style", tableStyle)
					.attr("class", tableClass),
				$head = $table.find("thead"),
				$colgroup = $table.find("colgroup"),
				headRowCount = $head.find("tr").length;

			this.$fixedWrapper = $tableWrapper;
			$table.addClass(fcn.table);
			$tableWrapper.addClass(fcn.wrapper);
			$bottomRightTable.append($("<tbody>"));

			if ($colgroup.length) {
				$colgroup.remove();
			}

			if (typeof fixedRowCount === "boolean") {
				fixedRowCount = fixedRowCount ? headRowCount : 0;
			} else {
				fixedRowCount = Math.max(
					0,
					Math.min(fixedRowCount, headRowCount)
				);
			}

			if (fixedRowCount) {
				$topLeftTable.append($head.clone(true));
				$topRightTable.append($head.clone(true));
				$head.remove();
			}

			$topLeftTable.find("tr").each(function(idx) {
				$(this)
					.find("th")
					.slice(fixedColCount)
					.remove();
			});

			$topRightTable.find("tr").each(function(idx) {
				$(this)
					.find("th")
					.slice(0, fixedColCount)
					.remove();
			});

			this.$fixedWrapper = $tableWrapper;

			$tableWrapper.append(
				$topLeftWrapper.append($topLeftTable),
				$topRightWrapper.append($topRightTable),
				$bottomLeftWrapper.append($bottomLeftTable),
				$bottomRightWrapper.append($bottomRightTable)
			);

			$bottomRightTable.on("keydown", function(evt) {
				var node = tree.focusNode,
					ctx = tree._makeHookContext(node || tree, evt),
					res = tree._callHook("nodeKeydown", ctx);
				return res;
			});

			$bottomRightTable.on("click dblclick", "tr", function(evt) {
				var $trLeft = $(this),
					$trRight = $trLeft.data(fcn.counterpart),
					node = $.ui.fancytree.getNode($trRight),
					ctx = tree._makeHookContext(node, evt),
					et = $.ui.fancytree.getEventTarget(evt),
					prevPhase = tree.phase;

				try {
					tree.phase = "userEvent";
					switch (evt.type) {
						case "click":
							ctx.targetType = et.type;
							if (node.isPagingNode()) {
								return (
									tree._triggerNodeEvent(
										"clickPaging",
										ctx,
										evt
									) === true
								);
							}
							return tree._triggerNodeEvent("click", ctx, evt) ===
								false
								? false
								: tree._callHook("nodeClick", ctx);
						case "dblclick":
							ctx.targetType = et.type;
							return tree._triggerNodeEvent(
								"dblclick",
								ctx,
								evt
							) === false
								? false
								: tree._callHook("nodeDblclick", ctx);
					}
				} finally {
					tree.phase = prevPhase;
				}
			});

			$tableWrapper
				.on(
					"mouseenter",
					"." +
						fcn.bottomRight +
						" table tr, ." +
						fcn.bottomLeft +
						" table tr",
					function(evt) {
						var $tr = $(this),
							$trOther = $tr.data(fcn.counterpart);
						$tr.addClass(fcn.hover);
						$trOther.addClass(fcn.hover);
					}
				)
				.on(
					"mouseleave",
					"." +
						fcn.bottomRight +
						" table tr, ." +
						fcn.bottomLeft +
						" table tr",
					function(evt) {
						var $tr = $(this),
							$trOther = $tr.data(fcn.counterpart);
						$tr.removeClass(fcn.hover);
						$trOther.removeClass(fcn.hover);
					}
				);

			$bottomLeftWrapper.on("mousewheel DOMMouseScroll", function(event) {
				var $this = $(this),
					newScroll = $this.scrollTop(),
					scrollUp =
						event.originalEvent.wheelDelta > 0 ||
						event.originalEvent.detail < 0;

				newScroll += scrollUp
					? -options.scrollSpeed
					: options.scrollSpeed;
				$this.scrollTop(newScroll);
				$bottomRightWrapper.scrollTop(newScroll);
				event.preventDefault();
			});

			$bottomRightWrapper.scroll(function() {
				var $this = $(this),
					scrollLeft = $this.scrollLeft(),
					scrollTop = $this.scrollTop();

				$topLeftWrapper
					.toggleClass(fcn.scrollBorderBottom, scrollTop > 0)
					.toggleClass(fcn.scrollBorderRight, scrollLeft > 0);
				$topRightWrapper
					.toggleClass(fcn.scrollBorderBottom, scrollTop > 0)
					.scrollLeft(scrollLeft);
				$bottomLeftWrapper
					.toggleClass(fcn.scrollBorderRight, scrollLeft > 0)
					.scrollTop(scrollTop);
			});

			$.ui.fancytree.overrideMethod(
				$.ui.fancytree._FancytreeNodeClass.prototype,
				"scrollIntoView",
				function(effects, options) {
					var $prevContainer = tree.$container;
					tree.$container = $bottomRightWrapper;
					return this._super
						.apply(this, arguments)
						.always(function() {
							tree.$container = $prevContainer;
						});
				}
			);
			return res;
		},

		treeLoad: function(ctx) {
			var self = this,
				res = this._superApply(arguments);

			res.done(function() {
				self.ext.fixed._adjustLayout.call(self);
				if (self.options.fixed.resizable) {
					self.ext.fixed._makeTableResizable();
				}
			});
			return res;
		},

		_makeTableResizable: function() {
			var $wrapper = this.$fixedWrapper,
				fcn = this.options.fixed.classNames,
				$topLeftWrapper = $wrapper.find("div." + fcn.topLeft),
				$topRightWrapper = $wrapper.find("div." + fcn.topRight),
				$bottomLeftWrapper = $wrapper.find("div." + fcn.bottomLeft),
				$bottomRightWrapper = $wrapper.find("div." + fcn.bottomRight);

			function _makeResizable($table) {
				$table.resizable({
					handles: "e",
					resize: function(evt, ui) {
						var width = Math.max($table.width(), ui.size.width);
						$bottomLeftWrapper.css("width", width);
						$topLeftWrapper.css("width", width);
						$bottomRightWrapper.css("left", width);
						$topRightWrapper.css("left", width);
					},
					stop: function() {
						$table.css("width", "100%");
					},
				});
			}

			_makeResizable($topLeftWrapper.find("table"));
			_makeResizable($bottomLeftWrapper.find("table"));
		},

		/* Called by nodeRender to sync node order with tag order.*/
		//	nodeFixOrder: function(ctx) {
		//	},

		nodeLoadChildren: function(ctx, source) {
			return this._superApply(arguments);
		},

		nodeRemoveChildMarkup: function(ctx) {
			var node = ctx.node;

			function _removeChild(elem) {
				var i,
					child,
					children = elem.children;
				if (children) {
					for (i = 0; i < children.length; i++) {
						child = children[i];
						if (child.trRight) {
							$(child.trRight).remove();
						}
						_removeChild(child);
					}
				}
			}

			_removeChild(node);
			return this._superApply(arguments);
		},

		nodeRemoveMarkup: function(ctx) {
			var node = ctx.node;

			if (node.trRight) {
				$(node.trRight).remove();
			}
			return this._superApply(arguments);
		},

		nodeSetActive: function(ctx, flag, callOpts) {
			var node = ctx.node,
				cn = this.options._classNames;

			if (node.trRight) {
				$(node.trRight)
					.toggleClass(cn.active, flag)
					.toggleClass(cn.focused, flag);
			}
			return this._superApply(arguments);
		},

		nodeKeydown: function(ctx) {
			return this._superApply(arguments);
		},

		nodeSetFocus: function(ctx, flag) {
			var node = ctx.node,
				cn = this.options._classNames;

			if (node.trRight) {
				$(node.trRight).toggleClass(cn.focused, flag);
			}
			return this._superApply(arguments);
		},

		nodeRender: function(ctx, force, deep, collapsed, _recursive) {
			var res = this._superApply(arguments),
				node = ctx.node,
				isRootNode = !node.parent;

			if (!isRootNode) {
				var $trLeft = $(node.tr),
					fcn = this.options.fixed.classNames,
					$trRight = $trLeft.data(fcn.counterpart);

				if (!$trRight && $trLeft.length) {
					var idx = $trLeft.index(),
						fixedColCount = this.options.fixed.fixCols,
						$blTableBody = $(
							"div." + fcn.bottomLeft + " table tbody"
						),
						$brTableBody = $(
							"div." + fcn.bottomRight + " table tbody"
						),
						$prevLeftNode = $blTableBody
							.find("tr")
							.eq(Math.max(idx + 1, 0)),
						prevRightNode = $prevLeftNode.data(fcn.counterpart);

					$trRight = $trLeft.clone(true);
					var trRight = $trRight.get(0);

					if (prevRightNode) {
						$(prevRightNode).before($trRight);
					} else {
						$brTableBody.append($trRight);
					}
					$trRight.show();
					trRight.ftnode = node;
					node.trRight = trRight;

					$trLeft
						.find("td")
						.slice(fixedColCount)
						.remove();
					$trRight
						.find("td")
						.slice(0, fixedColCount)
						.remove();
					$trLeft.data(fcn.counterpart, $trRight);
					$trRight.data(fcn.counterpart, $trLeft);
				}
			}

			return res;
		},

		nodeRenderTitle: function(ctx, title) {
			return this._superApply(arguments);
		},

		nodeRenderStatus: function(ctx) {
			var res = this._superApply(arguments),
				node = ctx.node;

			if (node.trRight) {
				var $trRight = $(node.trRight),
					$trLeft = $(node.tr),
					fcn = this.options.fixed.classNames,
					hovering = $trRight.hasClass(fcn.hover),
					trClasses = $trLeft.attr("class");

				$trRight.attr("class", trClasses);
				if (hovering) {
					$trRight.addClass(fcn.hover);
					$trLeft.addClass(fcn.hover);
				}
			}
			return res;
		},

		nodeSetExpanded: function(ctx, flag, callOpts) {
			var res,
				self = this,
				node = ctx.node,
				$leftTr = $(node.tr),
				fcn = this.options.fixed.classNames,
				cn = this.options._classNames,
				$rightTr = $leftTr.data(fcn.counterpart);

			flag = typeof flag === "undefined" ? true : flag;

			if (!$rightTr) {
				return this._superApply(arguments);
			}
			$rightTr.toggleClass(cn.expanded, !!flag);
			if (flag && !node.isExpanded()) {
				res = this._superApply(arguments);
				res.done(function() {
					node.visit(function(child) {
						var $trLeft = $(child.tr),
							$trRight = $trLeft.data(fcn.counterpart);

						self.ext.fixed._adjustRowHeight($trLeft, $trRight);
						if (!child.expanded) {
							return "skip";
						}
					});

					self.ext.fixed._adjustColWidths();
					self.ext.fixed._adjustWrapperLayout();
				});
			} else if (!flag && node.isExpanded()) {
				node.visit(function(child) {
					var $trLeft = $(child.tr),
						$trRight = $trLeft.data(fcn.counterpart);
					if ($trRight) {
						if (!child.expanded) {
							return "skip";
						}
					}
				});

				self.ext.fixed._adjustColWidths();
				self.ext.fixed._adjustWrapperLayout();
				res = this._superApply(arguments);
			} else {
				res = this._superApply(arguments);
			}
			return res;
		},

		nodeSetStatus: function(ctx, status, message, details) {
			return this._superApply(arguments);
		},

		treeClear: function(ctx) {
			var tree = ctx.tree,
				$table = tree.widget.element,
				$wrapper = this.$fixedWrapper,
				fcn = this.options.fixed.classNames;

			$table
				.find("tr, td, th, thead")
				.removeClass(fcn.hidden)
				.css({
					"min-width": "auto",
					height: "auto",
				});
			$wrapper.empty().append($table);
			return this._superApply(arguments);
		},

		treeRegisterNode: function(ctx, add, node) {
			return this._superApply(arguments);
		},

		treeDestroy: function(ctx) {
			var tree = ctx.tree,
				$table = tree.widget.element,
				$wrapper = this.$fixedWrapper,
				fcn = this.options.fixed.classNames;

			$table
				.find("tr, td, th, thead")
				.removeClass(fcn.hidden)
				.css({
					"min-width": "auto",
					height: "auto",
				});
			$wrapper.empty().append($table);
			return this._superApply(arguments);
		},

		_adjustColWidths: function() {
			if (this.options.fixed.adjustColWidths) {
				this.options.fixed.adjustColWidths.call(this);
				return;
			}

			var $wrapper = this.$fixedWrapper,
				fcn = this.options.fixed.classNames,
				$tlWrapper = $wrapper.find("div." + fcn.topLeft),
				$blWrapper = $wrapper.find("div." + fcn.bottomLeft),
				$trWrapper = $wrapper.find("div." + fcn.topRight),
				$brWrapper = $wrapper.find("div." + fcn.bottomRight);

			function _adjust($topWrapper, $bottomWrapper) {
				var $trTop = $topWrapper.find("thead tr").first(),
					$trBottom = $bottomWrapper.find("tbody tr").first();

				$trTop.find("th").each(function(idx) {
					var $thTop = $(this),
						$tdBottom = $trBottom.find("td").eq(idx),
						thTopWidth = $thTop.width(),
						thTopOuterWidth = $thTop.outerWidth(),
						tdBottomWidth = $tdBottom.width(),
						tdBottomOuterWidth = $tdBottom.outerWidth(),
						newWidth = Math.max(
							thTopOuterWidth,
							tdBottomOuterWidth
						);

					$thTop.css(
						"min-width",
						newWidth - (thTopOuterWidth - thTopWidth)
					);
					$tdBottom.css(
						"min-width",
						newWidth - (tdBottomOuterWidth - tdBottomWidth)
					);
				});
			}

			_adjust($tlWrapper, $blWrapper);
			_adjust($trWrapper, $brWrapper);
		},

		_adjustRowHeight: function($tr1, $tr2) {
			var fcn = this.options.fixed.classNames;
			if (!$tr2) {
				$tr2 = $tr1.data(fcn.counterpart);
			}
			$tr1.css("height", "auto");
			$tr2.css("height", "auto");
			var row1Height = $tr1.outerHeight(),
				row2Height = $tr2.outerHeight(),
				newHeight = Math.max(row1Height, row2Height);
			$tr1.css("height", newHeight + 1);
			$tr2.css("height", newHeight + 1);
		},

		_adjustWrapperLayout: function() {
			var $wrapper = this.$fixedWrapper,
				fcn = this.options.fixed.classNames,
				$topLeftWrapper = $wrapper.find("div." + fcn.topLeft),
				$topRightWrapper = $wrapper.find("div." + fcn.topRight),
				$bottomLeftWrapper = $wrapper.find("div." + fcn.bottomLeft),
				$bottomRightWrapper = $wrapper.find("div." + fcn.bottomRight),
				$topLeftTable = $topLeftWrapper.find("table"),
				$topRightTable = $topRightWrapper.find("table"),
				//			$bottomLeftTable = $bottomLeftWrapper.find("table"),
				wrapperWidth = $wrapper.width(),
				wrapperHeight = $wrapper.height(),
				fixedWidth = Math.min(wrapperWidth, $topLeftTable.width()),
				fixedHeight = Math.min(
					wrapperHeight,
					Math.max($topLeftTable.height(), $topRightTable.height())
				);
			//			vScrollbar = $bottomRightWrapper.get(0).scrollHeight > (wrapperHeight - fixedHeight),
			//			hScrollbar = $bottomRightWrapper.get(0).scrollWidth > (wrapperWidth - fixedWidth);

			$topLeftWrapper.css({
				width: fixedWidth,
				height: fixedHeight,
			});
			$topRightWrapper.css({
				//			width: wrapperWidth - fixedWidth - (vScrollbar ? 17 : 0),
				//			width: "calc(100% - " + (fixedWidth + (vScrollbar ? 17 : 0)) + "px)",
				width: "calc(100% - " + (fixedWidth + 17) + "px)",
				height: fixedHeight,
				left: fixedWidth,
			});
			$bottomLeftWrapper.css({
				width: fixedWidth,
				//			height: vScrollbar ? wrapperHeight - fixedHeight - (hScrollbar ? 17 : 0) : "auto",
				//			height: vScrollbar ? ("calc(100% - " + (fixedHeight + (hScrollbar ? 17 : 0)) + "px)") : "auto",
				//			height: vScrollbar ? ("calc(100% - " + (fixedHeight + 17) + "px)") : "auto",
				height: "calc(100% - " + (fixedHeight + 17) + "px)",
				top: fixedHeight,
			});
			$bottomRightWrapper.css({
				//			width: wrapperWidth - fixedWidth,
				//			height: vScrollbar ? wrapperHeight - fixedHeight : "auto",
				width: "calc(100% - " + fixedWidth + "px)",
				//			height: vScrollbar ? ("calc(100% - " + fixedHeight + "px)") : "auto",
				height: "calc(100% - " + fixedHeight + "px)",
				top: fixedHeight,
				left: fixedWidth,
			});
		},

		_adjustLayout: function() {
			var self = this,
				$wrapper = this.$fixedWrapper,
				fcn = this.options.fixed.classNames,
				$topLeftWrapper = $wrapper.find("div." + fcn.topLeft),
				$topRightWrapper = $wrapper.find("div." + fcn.topRight),
				$bottomLeftWrapper = $wrapper.find("div." + fcn.bottomLeft);
			// $bottomRightWrapper = $wrapper.find("div." + fcn.bottomRight)

			$topLeftWrapper.find("table tr").each(function(idx) {
				var $trRight = $topRightWrapper.find("tr").eq(idx);
				self.ext.fixed._adjustRowHeight($(this), $trRight);
			});

			$bottomLeftWrapper
				.find("table tbody")
				.find("tr")
				.each(function(idx) {
					// var $trRight = $bottomRightWrapper.find("tbody").find("tr").eq(idx);
					self.ext.fixed._adjustRowHeight($(this));
				});

			self.ext.fixed._adjustColWidths.call(this);
			self.ext.fixed._adjustWrapperLayout.call(this);
		},

		//	treeSetFocus: function(ctx, flag) {
		////			alert("treeSetFocus" + ctx.tree.$container);
		//		ctx.tree.$container.focus();
		//		$.ui.fancytree.focusTree = ctx.tree;
		//	}
	});
	// Value returned by `require('jquery.fancytree..')`
	return $.ui.fancytree;
}); // End of closure
