/*!
 * jquery.fancytree.dnd.js
 *
 * Drag-and-drop support (jQuery UI draggable/droppable).
 * (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-ui/ui/widgets/draggable",
			"jquery-ui/ui/widgets/droppable",
			"./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
	 */
	var didRegisterDnd = false,
		classDropAccept = "fancytree-drop-accept",
		classDropAfter = "fancytree-drop-after",
		classDropBefore = "fancytree-drop-before",
		classDropOver = "fancytree-drop-over",
		classDropReject = "fancytree-drop-reject",
		classDropTarget = "fancytree-drop-target";

	/* Convert number to string and prepend +/-; return empty string for 0.*/
	function offsetString(n) {
		// eslint-disable-next-line no-nested-ternary
		return n === 0 ? "" : n > 0 ? "+" + n : "" + n;
	}

	//--- Extend ui.draggable event handling --------------------------------------

	function _registerDnd() {
		if (didRegisterDnd) {
			return;
		}

		// Register proxy-functions for draggable.start/drag/stop

		$.ui.plugin.add("draggable", "connectToFancytree", {
			start: function(event, ui) {
				// 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10
				var draggable =
						$(this).data("ui-draggable") ||
						$(this).data("draggable"),
					sourceNode = ui.helper.data("ftSourceNode") || null;

				if (sourceNode) {
					// Adjust helper offset, so cursor is slightly outside top/left corner
					draggable.offset.click.top = -2;
					draggable.offset.click.left = +16;
					// Trigger dragStart event
					// TODO: when called as connectTo..., the return value is ignored(?)
					return sourceNode.tree.ext.dnd._onDragEvent(
						"start",
						sourceNode,
						null,
						event,
						ui,
						draggable
					);
				}
			},
			drag: function(event, ui) {
				var ctx,
					isHelper,
					logObject,
					// 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10
					draggable =
						$(this).data("ui-draggable") ||
						$(this).data("draggable"),
					sourceNode = ui.helper.data("ftSourceNode") || null,
					prevTargetNode = ui.helper.data("ftTargetNode") || null,
					targetNode = $.ui.fancytree.getNode(event.target),
					dndOpts = sourceNode && sourceNode.tree.options.dnd;

				// logObject = sourceNode || prevTargetNode || $.ui.fancytree;
				// logObject.debug("Drag event:", event, event.shiftKey);
				if (event.target && !targetNode) {
					// We got a drag event, but the targetNode could not be found
					// at the event location. This may happen,
					// 1. if the mouse jumped over the drag helper,
					// 2. or if a non-fancytree element is dragged
					// We ignore it:
					isHelper =
						$(event.target).closest(
							"div.fancytree-drag-helper,#fancytree-drop-marker"
						).length > 0;
					if (isHelper) {
						logObject =
							sourceNode || prevTargetNode || $.ui.fancytree;
						logObject.debug("Drag event over helper: ignored.");
						return;
					}
				}
				ui.helper.data("ftTargetNode", targetNode);

				if (dndOpts && dndOpts.updateHelper) {
					ctx = sourceNode.tree._makeHookContext(sourceNode, event, {
						otherNode: targetNode,
						ui: ui,
						draggable: draggable,
						dropMarker: $("#fancytree-drop-marker"),
					});
					dndOpts.updateHelper.call(sourceNode.tree, sourceNode, ctx);
				}

				// Leaving a tree node
				if (prevTargetNode && prevTargetNode !== targetNode) {
					prevTargetNode.tree.ext.dnd._onDragEvent(
						"leave",
						prevTargetNode,
						sourceNode,
						event,
						ui,
						draggable
					);
				}
				if (targetNode) {
					if (!targetNode.tree.options.dnd.dragDrop) {
						// not enabled as drop target
					} else if (targetNode === prevTargetNode) {
						// Moving over same node
						targetNode.tree.ext.dnd._onDragEvent(
							"over",
							targetNode,
							sourceNode,
							event,
							ui,
							draggable
						);
					} else {
						// Entering this node first time
						targetNode.tree.ext.dnd._onDragEvent(
							"enter",
							targetNode,
							sourceNode,
							event,
							ui,
							draggable
						);
						targetNode.tree.ext.dnd._onDragEvent(
							"over",
							targetNode,
							sourceNode,
							event,
							ui,
							draggable
						);
					}
				}
				// else go ahead with standard event handling
			},
			stop: function(event, ui) {
				var logObject,
					// 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10:
					draggable =
						$(this).data("ui-draggable") ||
						$(this).data("draggable"),
					sourceNode = ui.helper.data("ftSourceNode") || null,
					targetNode = ui.helper.data("ftTargetNode") || null,
					dropped = event.type === "mouseup" && event.which === 1;

				if (!dropped) {
					logObject = sourceNode || targetNode || $.ui.fancytree;
					logObject.debug("Drag was cancelled");
				}
				if (targetNode) {
					if (dropped) {
						targetNode.tree.ext.dnd._onDragEvent(
							"drop",
							targetNode,
							sourceNode,
							event,
							ui,
							draggable
						);
					}
					targetNode.tree.ext.dnd._onDragEvent(
						"leave",
						targetNode,
						sourceNode,
						event,
						ui,
						draggable
					);
				}
				if (sourceNode) {
					sourceNode.tree.ext.dnd._onDragEvent(
						"stop",
						sourceNode,
						null,
						event,
						ui,
						draggable
					);
				}
			},
		});

		didRegisterDnd = true;
	}

	/******************************************************************************
	 * Drag and drop support
	 */
	function _initDragAndDrop(tree) {
		var dnd = tree.options.dnd || null,
			glyph = tree.options.glyph || null;

		// Register 'connectToFancytree' option with ui.draggable
		if (dnd) {
			_registerDnd();
		}
		// Attach ui.draggable to this Fancytree instance
		if (dnd && dnd.dragStart) {
			tree.widget.element.draggable(
				$.extend(
					{
						addClasses: false,
						// DT issue 244: helper should be child of scrollParent:
						appendTo: tree.$container,
						//			appendTo: "body",
						containment: false,
						//			containment: "parent",
						delay: 0,
						distance: 4,
						revert: false,
						scroll: true, // to disable, also set css 'position: inherit' on ul.fancytree-container
						scrollSpeed: 7,
						scrollSensitivity: 10,
						// Delegate draggable.start, drag, and stop events to our handler
						connectToFancytree: true,
						// Let source tree create the helper element
						helper: function(event) {
							var $helper,
								$nodeTag,
								opts,
								sourceNode = $.ui.fancytree.getNode(
									event.target
								);

							if (!sourceNode) {
								// #405, DT issue 211: might happen, if dragging a table *header*
								return "<div>ERROR?: helper requested but sourceNode not found</div>";
							}
							opts = sourceNode.tree.options.dnd;
							$nodeTag = $(sourceNode.span);
							// Only event and node argument is available
							$helper = $(
								"<div class='fancytree-drag-helper'><span class='fancytree-drag-helper-img' /></div>"
							)
								.css({ zIndex: 3, position: "relative" }) // so it appears above ext-wide selection bar
								.append(
									$nodeTag
										.find("span.fancytree-title")
										.clone()
								);

							// Attach node reference to helper object
							$helper.data("ftSourceNode", sourceNode);

							// Support glyph symbols instead of icons
							if (glyph) {
								$helper
									.find(".fancytree-drag-helper-img")
									.addClass(
										glyph.map._addClass +
											" " +
											glyph.map.dragHelper
									);
							}
							// Allow to modify the helper, e.g. to add multi-node-drag feedback
							if (opts.initHelper) {
								opts.initHelper.call(
									sourceNode.tree,
									sourceNode,
									{
										node: sourceNode,
										tree: sourceNode.tree,
										originalEvent: event,
										ui: { helper: $helper },
									}
								);
							}
							// We return an unconnected element, so `draggable` will add this
							// to the parent specified as `appendTo` option
							return $helper;
						},
						start: function(event, ui) {
							var sourceNode = ui.helper.data("ftSourceNode");
							return !!sourceNode; // Abort dragging if no node could be found
						},
					},
					tree.options.dnd.draggable
				)
			);
		}
		// Attach ui.droppable to this Fancytree instance
		if (dnd && dnd.dragDrop) {
			tree.widget.element.droppable(
				$.extend(
					{
						addClasses: false,
						tolerance: "intersect",
						greedy: false,
						/*
			activate: function(event, ui) {
				tree.debug("droppable - activate", event, ui, this);
			},
			create: function(event, ui) {
				tree.debug("droppable - create", event, ui);
			},
			deactivate: function(event, ui) {
				tree.debug("droppable - deactivate", event, ui);
			},
			drop: function(event, ui) {
				tree.debug("droppable - drop", event, ui);
			},
			out: function(event, ui) {
				tree.debug("droppable - out", event, ui);
			},
			over: function(event, ui) {
				tree.debug("droppable - over", event, ui);
			}
*/
					},
					tree.options.dnd.droppable
				)
			);
		}
	}

	/******************************************************************************
	 *
	 */

	$.ui.fancytree.registerExtension({
		name: "dnd",
		version: "@VERSION",
		// Default options for this extension.
		options: {
			// Make tree nodes accept draggables
			autoExpandMS: 1000, // Expand nodes after n milliseconds of hovering.
			draggable: null, // Additional options passed to jQuery draggable
			droppable: null, // Additional options passed to jQuery droppable
			focusOnClick: false, // Focus, although draggable cancels mousedown event (#270)
			preventVoidMoves: true, // Prevent dropping nodes 'before self', etc.
			preventRecursiveMoves: true, // Prevent dropping nodes on own descendants
			smartRevert: true, // set draggable.revert = true if drop was rejected
			dropMarkerOffsetX: -24, // absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop)
			dropMarkerInsertOffsetX: -16, // additional offset for drop-marker with hitMode = "before"/"after"
			// Events (drag support)
			dragStart: null, // Callback(sourceNode, data), return true, to enable dnd
			dragStop: null, // Callback(sourceNode, data)
			initHelper: null, // Callback(sourceNode, data)
			updateHelper: null, // Callback(sourceNode, data)
			// Events (drop support)
			dragEnter: null, // Callback(targetNode, data)
			dragOver: null, // Callback(targetNode, data)
			dragExpand: null, // Callback(targetNode, data), return false to prevent autoExpand
			dragDrop: null, // Callback(targetNode, data)
			dragLeave: null, // Callback(targetNode, data)
		},

		treeInit: function(ctx) {
			var tree = ctx.tree;
			this._superApply(arguments);
			// issue #270: draggable eats mousedown events
			if (tree.options.dnd.dragStart) {
				tree.$container.on("mousedown", function(event) {
					//				if( !tree.hasFocus() && ctx.options.dnd.focusOnClick ) {
					if (ctx.options.dnd.focusOnClick) {
						// #270
						var node = $.ui.fancytree.getNode(event);
						if (node) {
							node.debug(
								"Re-enable focus that was prevented by jQuery UI draggable."
							);
							// node.setFocus();
							// $(node.span).closest(":tabbable").focus();
							// $(event.target).trigger("focus");
							// $(event.target).closest(":tabbable").trigger("focus");
						}
						setTimeout(function() {
							// #300
							$(event.target)
								.closest(":tabbable")
								.focus();
						}, 10);
					}
				});
			}
			_initDragAndDrop(tree);
		},
		/* Display drop marker according to hitMode ('after', 'before', 'over'). */
		_setDndStatus: function(
			sourceNode,
			targetNode,
			helper,
			hitMode,
			accept
		) {
			var markerOffsetX,
				pos,
				markerAt = "center",
				instData = this._local,
				dndOpt = this.options.dnd,
				glyphOpt = this.options.glyph,
				$source = sourceNode ? $(sourceNode.span) : null,
				$target = $(targetNode.span),
				$targetTitle = $target.find("span.fancytree-title");

			if (!instData.$dropMarker) {
				instData.$dropMarker = $(
					"<div id='fancytree-drop-marker'></div>"
				)
					.hide()
					.css({ "z-index": 1000 })
					.prependTo($(this.$div).parent());
				//                .prependTo("body");

				if (glyphOpt) {
					instData.$dropMarker.addClass(
						glyphOpt.map._addClass + " " + glyphOpt.map.dropMarker
					);
				}
			}
			if (
				hitMode === "after" ||
				hitMode === "before" ||
				hitMode === "over"
			) {
				markerOffsetX = dndOpt.dropMarkerOffsetX || 0;
				switch (hitMode) {
					case "before":
						markerAt = "top";
						markerOffsetX += dndOpt.dropMarkerInsertOffsetX || 0;
						break;
					case "after":
						markerAt = "bottom";
						markerOffsetX += dndOpt.dropMarkerInsertOffsetX || 0;
						break;
				}

				pos = {
					my: "left" + offsetString(markerOffsetX) + " center",
					at: "left " + markerAt,
					of: $targetTitle,
				};
				if (this.options.rtl) {
					pos.my = "right" + offsetString(-markerOffsetX) + " center";
					pos.at = "right " + markerAt;
				}
				instData.$dropMarker
					.toggleClass(classDropAfter, hitMode === "after")
					.toggleClass(classDropOver, hitMode === "over")
					.toggleClass(classDropBefore, hitMode === "before")
					.toggleClass("fancytree-rtl", !!this.options.rtl)
					.show()
					.position($.ui.fancytree.fixPositionOptions(pos));
			} else {
				instData.$dropMarker.hide();
			}
			if ($source) {
				$source
					.toggleClass(classDropAccept, accept === true)
					.toggleClass(classDropReject, accept === false);
			}
			$target
				.toggleClass(
					classDropTarget,
					hitMode === "after" ||
						hitMode === "before" ||
						hitMode === "over"
				)
				.toggleClass(classDropAfter, hitMode === "after")
				.toggleClass(classDropBefore, hitMode === "before")
				.toggleClass(classDropAccept, accept === true)
				.toggleClass(classDropReject, accept === false);

			helper
				.toggleClass(classDropAccept, accept === true)
				.toggleClass(classDropReject, accept === false);
		},

		/*
		 * Handles drag'n'drop functionality.
		 *
		 * A standard jQuery drag-and-drop process may generate these calls:
		 *
		 * start:
		 *     _onDragEvent("start", sourceNode, null, event, ui, draggable);
		 * drag:
		 *     _onDragEvent("leave", prevTargetNode, sourceNode, event, ui, draggable);
		 *     _onDragEvent("over", targetNode, sourceNode, event, ui, draggable);
		 *     _onDragEvent("enter", targetNode, sourceNode, event, ui, draggable);
		 * stop:
		 *     _onDragEvent("drop", targetNode, sourceNode, event, ui, draggable);
		 *     _onDragEvent("leave", targetNode, sourceNode, event, ui, draggable);
		 *     _onDragEvent("stop", sourceNode, null, event, ui, draggable);
		 */
		_onDragEvent: function(
			eventName,
			node,
			otherNode,
			event,
			ui,
			draggable
		) {
			// if(eventName !== "over"){
			// 	this.debug("tree.ext.dnd._onDragEvent(%s, %o, %o) - %o", eventName, node, otherNode, this);
			// }
			var accept,
				nodeOfs,
				parentRect,
				rect,
				relPos,
				relPos2,
				enterResponse,
				hitMode,
				r,
				opts = this.options,
				dnd = opts.dnd,
				ctx = this._makeHookContext(node, event, {
					otherNode: otherNode,
					ui: ui,
					draggable: draggable,
				}),
				res = null,
				self = this,
				$nodeTag = $(node.span);

			if (dnd.smartRevert) {
				draggable.options.revert = "invalid";
			}

			switch (eventName) {
				case "start":
					if (node.isStatusNode()) {
						res = false;
					} else if (dnd.dragStart) {
						res = dnd.dragStart(node, ctx);
					}
					if (res === false) {
						this.debug("tree.dragStart() cancelled");
						//draggable._clear();
						// NOTE: the return value seems to be ignored (drag is not cancelled, when false is returned)
						// TODO: call this._cancelDrag()?
						ui.helper.trigger("mouseup").hide();
					} else {
						if (dnd.smartRevert) {
							// #567, #593: fix revert position
							// rect = node.li.getBoundingClientRect();
							rect = node[
								ctx.tree.nodeContainerAttrName
							].getBoundingClientRect();
							parentRect = $(
								draggable.options.appendTo
							)[0].getBoundingClientRect();
							draggable.originalPosition.left = Math.max(
								0,
								rect.left - parentRect.left
							);
							draggable.originalPosition.top = Math.max(
								0,
								rect.top - parentRect.top
							);
						}
						$nodeTag.addClass("fancytree-drag-source");
						// Register global handlers to allow cancel
						$(document).on(
							"keydown.fancytree-dnd,mousedown.fancytree-dnd",
							function(event) {
								// node.tree.debug("dnd global event", event.type, event.which);
								if (
									event.type === "keydown" &&
									event.which === $.ui.keyCode.ESCAPE
								) {
									self.ext.dnd._cancelDrag();
								} else if (event.type === "mousedown") {
									self.ext.dnd._cancelDrag();
								}
							}
						);
					}
					break;

				case "enter":
					if (
						dnd.preventRecursiveMoves &&
						node.isDescendantOf(otherNode)
					) {
						r = false;
					} else {
						r = dnd.dragEnter ? dnd.dragEnter(node, ctx) : null;
					}
					if (!r) {
						// convert null, undefined, false to false
						res = false;
					} else if ($.isArray(r)) {
						// TODO: also accept passing an object of this format directly
						res = {
							over: $.inArray("over", r) >= 0,
							before: $.inArray("before", r) >= 0,
							after: $.inArray("after", r) >= 0,
						};
					} else {
						res = {
							over: r === true || r === "over",
							before: r === true || r === "before",
							after: r === true || r === "after",
						};
					}
					ui.helper.data("enterResponse", res);
					// this.debug("helper.enterResponse: %o", res);
					break;

				case "over":
					enterResponse = ui.helper.data("enterResponse");
					hitMode = null;
					if (enterResponse === false) {
						// Don't call dragOver if onEnter returned false.
						//                break;
					} else if (typeof enterResponse === "string") {
						// Use hitMode from onEnter if provided.
						hitMode = enterResponse;
					} else {
						// Calculate hitMode from relative cursor position.
						nodeOfs = $nodeTag.offset();
						relPos = {
							x: event.pageX - nodeOfs.left,
							y: event.pageY - nodeOfs.top,
						};
						relPos2 = {
							x: relPos.x / $nodeTag.width(),
							y: relPos.y / $nodeTag.height(),
						};

						if (enterResponse.after && relPos2.y > 0.75) {
							hitMode = "after";
						} else if (
							!enterResponse.over &&
							enterResponse.after &&
							relPos2.y > 0.5
						) {
							hitMode = "after";
						} else if (enterResponse.before && relPos2.y <= 0.25) {
							hitMode = "before";
						} else if (
							!enterResponse.over &&
							enterResponse.before &&
							relPos2.y <= 0.5
						) {
							hitMode = "before";
						} else if (enterResponse.over) {
							hitMode = "over";
						}
						// Prevent no-ops like 'before source node'
						// TODO: these are no-ops when moving nodes, but not in copy mode
						if (dnd.preventVoidMoves) {
							if (node === otherNode) {
								this.debug(
									"    drop over source node prevented"
								);
								hitMode = null;
							} else if (
								hitMode === "before" &&
								otherNode &&
								node === otherNode.getNextSibling()
							) {
								this.debug(
									"    drop after source node prevented"
								);
								hitMode = null;
							} else if (
								hitMode === "after" &&
								otherNode &&
								node === otherNode.getPrevSibling()
							) {
								this.debug(
									"    drop before source node prevented"
								);
								hitMode = null;
							} else if (
								hitMode === "over" &&
								otherNode &&
								otherNode.parent === node &&
								otherNode.isLastSibling()
							) {
								this.debug(
									"    drop last child over own parent prevented"
								);
								hitMode = null;
							}
						}
						//                this.debug("hitMode: %s - %s - %s", hitMode, (node.parent === otherNode), node.isLastSibling());
						ui.helper.data("hitMode", hitMode);
					}
					// Auto-expand node (only when 'over' the node, not 'before', or 'after')
					if (
						hitMode !== "before" &&
						hitMode !== "after" &&
						dnd.autoExpandMS &&
						node.hasChildren() !== false &&
						!node.expanded &&
						(!dnd.dragExpand || dnd.dragExpand(node, ctx) !== false)
					) {
						node.scheduleAction("expand", dnd.autoExpandMS);
					}
					if (hitMode && dnd.dragOver) {
						// TODO: http://code.google.com/p/dynatree/source/detail?r=625
						ctx.hitMode = hitMode;
						res = dnd.dragOver(node, ctx);
					}
					accept = res !== false && hitMode !== null;
					if (dnd.smartRevert) {
						draggable.options.revert = !accept;
					}
					this._local._setDndStatus(
						otherNode,
						node,
						ui.helper,
						hitMode,
						accept
					);
					break;

				case "drop":
					hitMode = ui.helper.data("hitMode");
					if (hitMode && dnd.dragDrop) {
						ctx.hitMode = hitMode;
						dnd.dragDrop(node, ctx);
					}
					break;

				case "leave":
					// Cancel pending expand request
					node.scheduleAction("cancel");
					ui.helper.data("enterResponse", null);
					ui.helper.data("hitMode", null);
					this._local._setDndStatus(
						otherNode,
						node,
						ui.helper,
						"out",
						undefined
					);
					if (dnd.dragLeave) {
						dnd.dragLeave(node, ctx);
					}
					break;

				case "stop":
					$nodeTag.removeClass("fancytree-drag-source");
					$(document).off(".fancytree-dnd");
					if (dnd.dragStop) {
						dnd.dragStop(node, ctx);
					}
					break;

				default:
					$.error("Unsupported drag event: " + eventName);
			}
			return res;
		},

		_cancelDrag: function() {
			var dd = $.ui.ddmanager.current;
			if (dd) {
				dd.cancel();
			}
		},
	});
	// Value returned by `require('jquery.fancytree..')`
	return $.ui.fancytree;
}); // End of closure
