/*!
 * jquery.fancytree.dnd5.js
 *
 * Drag-and-drop support (native HTML5).
 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
 *
 * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
 *
 * Released under the MIT license
 * https://github.com/mar10/fancytree/wiki/LicenseInfo
 *
 * @version @VERSION
 * @date @DATE
 */


 /*
 #TODO
	Compatiblity when dragging between *separate* windows:

		   Drag from Chrome   Edge    FF    IE11    Safari
	  To Chrome      ok       ok      ok    NO      ?
		 Edge        ok       ok      ok    NO      ?
		 FF          ok       ok      ok    NO      ?
		 IE 11       ok       ok      ok    ok      ?
		 Safari      ?        ?       ?     ?       ok

 */

;(function($, window, document, undefined) {

"use strict";

/* *****************************************************************************
 * Private functions and variables
 */
var
	classDragSource = "fancytree-drag-source",
	classDragRemove = "fancytree-drag-remove",
	classDropAccept = "fancytree-drop-accept",
	classDropAfter = "fancytree-drop-after",
	classDropBefore = "fancytree-drop-before",
	classDropOver = "fancytree-drop-over",
	classDropReject = "fancytree-drop-reject",
	classDropTarget = "fancytree-drop-target",
	nodeMimeType = "application/x-fancytree-node",
	$dropMarker = null,
	SOURCE_NODE = null,
	DRAG_ENTER_RESPONSE = null,
	LAST_HIT_MODE = null;

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

/* Convert a dragEnter() or dragOver() response to a canonical form.
 * Return false or plain object
 * @param {string|object|boolean} r
 * @return {object|false}
 */
function normalizeDragEnterResponse(r) {
	var res;

	if( !r ){
		return false;
	}
	if ( $.isPlainObject(r) ) {
		res = {
			over: !!r.over,
			before: !!r.before,
			after: !!r.after
		};
	}else if ( $.isArray(r) ) {
		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"))
		};
	}
	if( Object.keys(res).length === 0 ) {
		return false;
	}
	// if( Object.keys(res).length === 1 ) {
	// 	res.unique = res[0];
	// }
	return res;
}

/* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
function autoScroll(tree, event) {
	var spOfs, scrollTop, delta,
		dndOpts = tree.options.dnd5,
		sp = tree.$scrollParent[0],
		sensitivity = dndOpts.scrollSensitivity,
		speed = dndOpts.scrollSpeed,
		scrolled = 0;

	if ( sp !== document && sp.tagName !== "HTML" ) {
		spOfs = tree.$scrollParent.offset();
		scrollTop = sp.scrollTop;
		if ( spOfs.top + sp.offsetHeight - event.pageY < sensitivity ) {
			delta = (sp.scrollHeight - tree.$scrollParent.innerHeight() - scrollTop);
			// console.log ("sp.offsetHeight: " + sp.offsetHeight
			// 	+ ", spOfs.top: " + spOfs.top
			// 	+ ", scrollTop: " + scrollTop
			// 	+ ", innerHeight: " + tree.$scrollParent.innerHeight()
			// 	+ ", scrollHeight: " + sp.scrollHeight
			// 	+ ", delta: " + delta
			// 	);
			if( delta > 0 ) {
				sp.scrollTop = scrolled = scrollTop + speed;
			}
		} else if ( scrollTop > 0 && event.pageY - spOfs.top < sensitivity ) {
			sp.scrollTop = scrolled = scrollTop - speed;
		}
	} else {
		scrollTop = $(document).scrollTop();
		if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) {
			scrolled = scrollTop - speed;
			$(document).scrollTop(scrolled);
		} else if ($(window).height() - (event.pageY - scrollTop) < sensitivity) {
			scrolled = scrollTop + speed;
			$(document).scrollTop(scrolled);
		}
	}
	if( scrolled ) {
		tree.debug("autoScroll: " + scrolled + "px");
	}
	return scrolled;
}

/* Handle dragover event (fired every x ms) and return hitMode. */
function handleDragOver(event, data) {
	// Implement auto-scrolling
	if ( data.options.dnd5.scroll ) {
		autoScroll(data.tree, event);
	}
	// Bail out with previous response if we get an invalid dragover
	if( !data.node ) {
		data.tree.warn("Ignore dragover for non-node");  //, event, data);
		return LAST_HIT_MODE;
	}

	var markerOffsetX, nodeOfs, relPosY, //res,
		// eventHash = getEventHash(event),
		hitMode = null,
		tree = data.tree,
		options = tree.options,
		dndOpts = options.dnd5,
		targetNode = data.node,
		sourceNode = data.otherNode,
		markerAt = "center",
		// $source = sourceNode ? $(sourceNode.span) : null,
		$target = $(targetNode.span),
		$targetTitle = $target.find("span.fancytree-title");

	if(DRAG_ENTER_RESPONSE === false){
		tree.warn("Ignore dragover, since dragenter returned false");  //, event, data);
		// $.error("assert failed: dragenter returned false");
		return false;
	} else if(typeof DRAG_ENTER_RESPONSE === "string") {
		$.error("assert failed: dragenter returned string");
		// Use hitMode from onEnter if provided.
		// hitMode = DRAG_ENTER_RESPONSE;
	} else {
		// Calculate hitMode from relative cursor position.
		nodeOfs = $target.offset();
		relPosY = (event.pageY - nodeOfs.top) / $target.height();

		if( DRAG_ENTER_RESPONSE.after && relPosY > 0.75 ){
			hitMode = "after";
		} else if(!DRAG_ENTER_RESPONSE.over && DRAG_ENTER_RESPONSE.after && relPosY > 0.5 ){
			hitMode = "after";
		} else if(DRAG_ENTER_RESPONSE.before && relPosY <= 0.25) {
			hitMode = "before";
		} else if(!DRAG_ENTER_RESPONSE.over && DRAG_ENTER_RESPONSE.before && relPosY <= 0.5) {
			hitMode = "before";
		} else if(DRAG_ENTER_RESPONSE.over) {
			hitMode = "over";
		}
		// Prevent no-ops like 'before source node'
		// TODO: these are no-ops when moving nodes, but not in copy mode
		if( dndOpts.preventVoidMoves ){
			if(targetNode === sourceNode){
				targetNode.debug("drop over source node prevented");
				hitMode = null;
			}else if(hitMode === "before" && sourceNode && targetNode === sourceNode.getNextSibling()){
				targetNode.debug("drop after source node prevented");
				hitMode = null;
			}else if(hitMode === "after" && sourceNode && targetNode === sourceNode.getPrevSibling()){
				targetNode.debug("drop before source node prevented");
				hitMode = null;
			}else if(hitMode === "over" && sourceNode && sourceNode.parent === targetNode && sourceNode.isLastSibling() ){
				targetNode.debug("drop last child over own parent prevented");
				hitMode = null;
			}
		}
	}
	// Let callback modify the calculated hitMode
	data.hitMode = hitMode;
	if(hitMode && dndOpts.dragOver){
		// TODO: http://code.google.com/p/dynatree/source/detail?r=625
		dndOpts.dragOver(targetNode, data);
		hitMode = data.hitMode;
	}
	// LAST_DROP_EFFECT = data.dataTransfer.dropEffect;
	// LAST_EFFECT_ALLOWED = data.dataTransfer.effectAllowed;
	LAST_HIT_MODE = hitMode;
	//
	if( hitMode === "after" || hitMode === "before" || hitMode === "over" ){
		markerOffsetX = dndOpts.dropMarkerOffsetX || 0;
		switch(hitMode){
		case "before":
			markerAt = "top";
			markerOffsetX += (dndOpts.dropMarkerInsertOffsetX || 0);
			break;
		case "after":
			markerAt = "bottom";
			markerOffsetX += (dndOpts.dropMarkerInsertOffsetX || 0);
			break;
		}

		$dropMarker
			.toggleClass(classDropAfter, hitMode === "after")
			.toggleClass(classDropOver, hitMode === "over")
			.toggleClass(classDropBefore, hitMode === "before")
			.show()
			.position($.ui.fancytree.fixPositionOptions({
				my: "left" + offsetString(markerOffsetX) + " center",
				at: "left " + markerAt,
				of: $targetTitle
				}));
	} else {
		$dropMarker.hide();
		// console.log("hide dropmarker")
	}
	// if( $source ){
	// 	$source.toggleClass(classDragRemove, isMove);
	// }
	$(targetNode.span)
		.toggleClass(classDropTarget, hitMode === "after" || hitMode === "before" || hitMode === "over")
		.toggleClass(classDropAfter, hitMode === "after")
		.toggleClass(classDropBefore, hitMode === "before")
		.toggleClass(classDropAccept, hitMode === "over")
		.toggleClass(classDropReject, hitMode === false);

	return hitMode;
}

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

$.ui.fancytree.registerExtension({
	name: "dnd5",
	version: "@VERSION",
	// Default options for this extension.
	options: {
		autoExpandMS: 1500,          // Expand nodes after n milliseconds of hovering
		setTextTypeJson: false,      // Allow dragging of nodes to different IE windows
		preventForeignNodes: false,  // Prevent dropping nodes from different Fancytrees
		preventNonNodes: false,      // Prevent dropping items other than Fancytree nodes
		preventRecursiveMoves: true, // Prevent dropping nodes on own descendants
		preventVoidMoves: true,      // Prevent dropping nodes 'before self', etc.
		scroll: true,                // Enable auto-scrolling while dragging
		scrollSensitivity: 20,       // Active top/bottom margin in pixel
		scrollSpeed: 5,              // Pixel per event
		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 drag
		dragDrag: $.noop,      // Callback(sourceNode, data)
		dragEnd: $.noop,       // Callback(sourceNode, data)
		// Events (drop support)
		dragEnter: null,       // Callback(targetNode, data), return true, to enable dnd drop
		dragOver: $.noop,      // Callback(targetNode, data)
		dragExpand: $.noop,    // Callback(targetNode, data), return false to prevent autoExpand
		dragDrop: $.noop,      // Callback(targetNode, data)
		dragLeave: $.noop      // Callback(targetNode, data)
	},

	treeInit: function(ctx){
		var tree = ctx.tree,
			opts = ctx.options,
			glyph = opts.glyph || null,
			dndOpts = opts.dnd5,
			getNode = $.ui.fancytree.getNode;

		if( $.inArray("dnd", opts.extensions) >= 0 ) {
			$.error("Extensions 'dnd' and 'dnd5' are mutually exclusive.");
		}
		if( dndOpts.dragStop ) {
			$.error("dragStop is not used by ext-dnd5. Use dragEnd instead.");
		}

		// Implement `opts.createNode` event to add the 'draggable' attribute
		// #680: this must happen before calling super.treeInit()
		if( dndOpts.dragStart ) {
			$.ui.fancytree.overrideMethod(ctx.options, "createNode", function(event, data) {
				// Default processing if any
				this._super.apply(this, arguments);

				data.node.span.draggable = true;
			});
		}
		this._superApply(arguments);

		this.$container.addClass("fancytree-ext-dnd5");

		// Store the current scroll parent, which may be the tree
		// container, any enclosing div, or the document
		this.$scrollParent = this.$container.children(":first").scrollParent();

		$dropMarker = $("#fancytree-drop-marker");
		if( !$dropMarker.length ) {
			$dropMarker = $("<div id='fancytree-drop-marker'></div>")
				.hide()
				.css({
					"z-index": 1000,
					// Drop marker should not steal dragenter/dragover events:
					"pointer-events": "none"
				}).prependTo("body");
			if( glyph ) {
				$dropMarker.addClass(glyph.map.dropMarker);
			}
		}
		// Enable drag support if dragStart() is specified:
		if( dndOpts.dragStart ) {
			// Bind drag event handlers
			tree.$container.on("dragstart drag dragend", function(event){
				var json,
					node = getNode(event),
					dataTransfer = event.dataTransfer || event.originalEvent.dataTransfer,
					isMove = dataTransfer.dropEffect === "move",
					$source = node ? $(node.span) : null,
					data = {
						node: node,
						tree: tree,
						options: tree.options,
						originalEvent: event,
						dataTransfer: dataTransfer,
//						dropEffect: undefined,  // set by dragend
						isCancelled: undefined  // set by dragend
					};

				switch( event.type ) {

				case "dragstart":
					$(node.span).addClass(classDragSource);

					// Store current source node in different formats
					SOURCE_NODE = node;

					// Set payload
					// Note:
					// Transfer data is only accessible on dragstart and drop!
					// For all other events the formats and kinds in the drag
					// data store list of items representing dragged data can be
					// enumerated, but the data itself is unavailable and no new
					// data can be added.
					json = JSON.stringify(node.toDict());
					try {
						dataTransfer.setData(nodeMimeType, json);
						dataTransfer.setData("text/html", $(node.span).html());
						dataTransfer.setData("text/plain", node.title);
					} catch(ex) {
						// IE only accepts 'text' type
						tree.warn("Could not set data (IE only accepts 'text') - " + ex);
					}
					// We always need to set the 'text' type if we want to drag
					// Because IE 11 only accepts this single type.
					// If we pass JSON here, IE can can access all node properties,
					// even when the source lives in another window. (D'n'd inside
					// the same window will always work.)
					// The drawback is, that in this case ALL browsers will see
					// the JSON representation as 'text', so dragging
					// to a text field will insert the JSON string instead of
					// the node title.
					if( dndOpts.setTextTypeJson ) {
						dataTransfer.setData("text", json);
					} else {
						dataTransfer.setData("text", node.title);
					}

					// Set the allowed and current drag mode (move, copy, or link)
					dataTransfer.effectAllowed = "all";  // "copyMove"
					// dataTransfer.dropEffect = "move";

					// Set the title as drag image (otherwise it would contain the expander)
					if( dataTransfer.setDragImage ) {
						// IE 11 does not support this
						dataTransfer.setDragImage($(node.span).find(".fancytree-title")[0], -10, -10);
						// dataTransfer.setDragImage($(node.span)[0], -10, -10);
					}
					// Let user modify above settings
					return dndOpts.dragStart(node, data) !== false;

				case "drag":
					// Called every few miliseconds
					$source.toggleClass(classDragRemove, isMove);
					dndOpts.dragDrag(node, data);
					break;

				case "dragend":
					$(node.span).removeClass(classDragSource + " " + classDragRemove);
					SOURCE_NODE = null;
					DRAG_ENTER_RESPONSE = null;
//					data.dropEffect = dataTransfer.dropEffect;
					data.isCancelled = (dataTransfer.dropEffect === "none");
					$dropMarker.hide();
					dndOpts.dragEnd(node, data);
					break;
				}
			});
		}
		// Enable drop support if dragEnter() is specified:
		if( dndOpts.dragEnter ) {
			// Bind drop event handlers
			tree.$container.on("dragenter dragover dragleave drop", function(event){
				var json, nodeData, r, res,
					allowDrop = null,
					node = getNode(event),
					dataTransfer = event.dataTransfer || event.originalEvent.dataTransfer,
					data = {
						node: node,
						tree: tree,
						options: tree.options,
						hitMode: DRAG_ENTER_RESPONSE,
						originalEvent: event,
						dataTransfer: dataTransfer,
						otherNode: SOURCE_NODE || null,
						otherNodeData: null,    // set by drop event
						dropEffect: undefined,  // set by drop event
						isCancelled: undefined  // set by drop event
					};

				switch( event.type ) {

				case "dragenter":
					// The dragenter event is fired when a dragged element or
					// text selection enters a valid drop target.

					if( !node ) {
						// Sometimes we get dragenter for the container element
						tree.debug("Ignore non-node " + event.type + ": " + event.target.tagName + "." + event.target.className);
						DRAG_ENTER_RESPONSE = false;
						break;
					}

					$(node.span)
						.addClass(classDropOver)
						.removeClass(classDropAccept + " " + classDropReject);

					if( dndOpts.preventNonNodes && !nodeData ) {
						node.debug("Reject dropping a non-node");
						DRAG_ENTER_RESPONSE = false;
						break;
					} else if( dndOpts.preventForeignNodes && (!SOURCE_NODE || SOURCE_NODE.tree !== node.tree ) ) {
						node.debug("Reject dropping a foreign node");
						DRAG_ENTER_RESPONSE = false;
						break;
					}

					// NOTE: dragenter is fired BEFORE the dragleave event
					// of the previous element!
					// https://www.w3.org/Bugs/Public/show_bug.cgi?id=19041
					setTimeout(function(){
						// node.info("DELAYED " + event.type, event.target, DRAG_ENTER_RESPONSE);
						// Auto-expand node (only when 'over' the node, not 'before', or 'after')
						if( dndOpts.autoExpandMS &&
							node.hasChildren() !== false && !node.expanded &&
							(!dndOpts.dragExpand || dndOpts.dragExpand(node, data) !== false)
							) {
							node.scheduleAction("expand", dndOpts.autoExpandMS);
						}
					}, 0);

					$dropMarker.show();

					// Call dragEnter() to figure out if (and where) dropping is allowed
					if( dndOpts.preventRecursiveMoves && node.isDescendantOf(data.otherNode) ){
						res = false;
					}else{
						r = dndOpts.dragEnter(node, data);
						res = normalizeDragEnterResponse(r);
					}
					DRAG_ENTER_RESPONSE = res;

					allowDrop = res && ( res.over || res.before || res.after );
					break;

				case "dragover":
					// The dragover event is fired when an element or text
					// selection is being dragged over a valid drop target
					// (every few hundred milliseconds).
					LAST_HIT_MODE = handleDragOver(event, data);
					allowDrop = !!LAST_HIT_MODE;
					break;

				case "dragleave":
					// NOTE: dragleave is fired AFTER the dragenter event of the
					// FOLLOWING element.
					if( !node ) {
						tree.debug("Ignore non-node " + event.type + ": " + event.target.tagName + "." + event.target.className);
						break;
					}
					if( !$(node.span).hasClass(classDropOver) ) {
						node.debug("Ignore dragleave (multi)"); //, event.currentTarget);
						break;
					}
					$(node.span).removeClass(classDropOver + " " + classDropAccept + " " + classDropReject);
					node.scheduleAction("cancel");
					dndOpts.dragLeave(node, data);
					$dropMarker.hide();
					break;

				case "drop":
					// Data is only readable in the (dragenter and) drop event:

					if( $.inArray(nodeMimeType, dataTransfer.types) >= 0 ) {
						nodeData = dataTransfer.getData(nodeMimeType);
						tree.info(event.type + ": getData('application/x-fancytree-node'): '" + nodeData + "'");
					}
					if( !nodeData ) {
						// 1. Source is not a Fancytree node, or
						// 2. If the FT mime type was set, but returns '', this
						//    is probably IE 11 (which only supports 'text')
						nodeData = dataTransfer.getData("text");
						tree.info(event.type + ": getData('text'): '" + nodeData + "'");
					}
					if( nodeData ) {
						try {
							// 'text' type may contain JSON if IE is involved
							// and setTextTypeJson option was set
							json = JSON.parse(nodeData);
							if( json.title !== undefined ) {
								data.otherNodeData = json;
							}
						} catch(ex) {
							// assume 'text' type contains plain text, so `otherNodeData`
							// should not be set
						}
					}
					tree.debug(event.type + ": nodeData: '" + nodeData + "', otherNodeData: ", data.otherNodeData);

					$(node.span).removeClass(classDropOver + " " + classDropAccept + " " + classDropReject);
					$dropMarker.hide();

					data.hitMode = LAST_HIT_MODE;
					data.dropEffect = dataTransfer.dropEffect;
					data.isCancelled = data.dropEffect === "none";

					// Let user implement the actual drop operation
					dndOpts.dragDrop(node, data);

					// Prevent browser's default drop handling
					event.preventDefault();
					break;
				}
				// Dnd API madness: we must PREVENT default handling to enable dropping
				if( allowDrop ) {
					event.preventDefault();
					return false;
				}
			});
		}
	}
});
}(jQuery, window, document));
