/**
 * eGroupWare egw_action framework - egw action framework
 *
 * @link http://www.egroupware.org
 * @author Andreas Stöckel <as@stylite.de>
 * @copyright 2011 by Andreas Stöckel
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package egw_action
 */

/*egw:uses
	egw_action;
	egw_action_common;
	egw_action_popup;
	vendor.bower-asset.jquery.dist.jquery;
*/

import {egwAction,egwActionImplementation, egw_getObjectManager} from "./egw_action.js";
import {getPopupImplementation} from "./egw_action_popup.js";
import {EGW_AI_DRAG_OUT, EGW_AI_DRAG_OVER, EGW_AO_EXEC_THIS, EGW_AI_DRAG_ENTER} from "./egw_action_constants.js";

/**
 * Register the drag and drop handlers
 */
if (typeof window._egwActionClasses == "undefined")
	window._egwActionClasses = {};
_egwActionClasses["drag"] = {
	"actionConstructor": egwDragAction,
	"implementation": getDragImplementation
};
_egwActionClasses["drop"] = {
	"actionConstructor": egwDropAction,
	"implementation": getDropImplementation
};

/**
 * The egwDragAction class overwrites the egwAction class and adds the new
 * "dragType" propery. The "onExecute" event of the drag action will be called
 * whenever dragging starts. The onExecute JS handler should return the
 * drag-drop helper object - otherwise an default helper will be generated.
 *
 * @param {egwAction} _id
 * @param {string} _handler
 * @param {string} _caption
 * @param {string} _icon
 * @param {(string|function)} _onExecute
 * @param {bool} _allowOnMultiple
 * @returns {egwDragAction}
 */
export function egwDragAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple)
{
	var action = new egwAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple);

	action.type = "drag";
	action.dragType = "default";
	action.hideOnDisabled = true;

	action.set_dragType = function(_value) {
		action.dragType = _value;
	};

	return action;
}


var
	_dragActionImpl = null;

export function getDragImplementation()
{
	if (!_dragActionImpl)
	{
		_dragActionImpl = new egwDragActionImplementation();
	}
	return _dragActionImpl;
}

export function egwDragActionImplementation()
{
	var ai = new egwActionImplementation();

	ai.type = "drag";

	ai.helper = null;
	ai.ddTypes = [];
	ai.selected = [];

	// Define default helper DOM
	// default helper also can be called later in application code in order to customization
	ai.defaultDDHelper = function (_selected)
	{
		// Table containing clone of rows
		var table = jQuery(document.createElement("table")).addClass('egwGridView_grid et2_egw_action_ddHelper_row');
		// tr element to use as last row to show lable more ...
		var moreRow = jQuery(document.createElement('tr')).addClass('et2_egw_action_ddHelper_moreRow');
		// Main div helper container
		var div = jQuery(document.createElement("div")).append(table);

		var rows = [];
		// Maximum number of rows to show
		var maxRows = 3;
		// item label
		var itemLabel = egw.lang(egw.link_get_registry(egw.app_name(),_selected.length > 1?'entries':'entry')||egw.app_name());

		var index = 0;

		// Take select all into account when counting number of rows, because they may not be
		// in _selected object
		var pseudoNumRows = (_selected[0] && _selected[0]._context && _selected[0]._context._selectionMgr &&
				_selected[0]._context._selectionMgr._selectAll) ?
				_selected[0]._context._selectionMgr._total : _selected.length;

		for (var i = 0; i < _selected.length;i++)
		{
			var row = jQuery(_selected[i].iface.getDOMNode()).clone();
			if (row)
			{
				rows.push(row);
				table.append(row);
			}
			index++;
			if (index == maxRows)
			{
				// Lable to show number of items
				var spanCnt = jQuery(document.createElement('span'))
						.addClass('et2_egw_action_ddHelper_itemsCnt')
						.appendTo(div);

				spanCnt.text(pseudoNumRows +' '+ itemLabel);
				// Number of not shown rows
				var restRows = pseudoNumRows - maxRows;
				if (restRows)
				{
					moreRow.text(egw.lang("%1 more %2 selected ...", (pseudoNumRows - maxRows), itemLabel));
				}
				table.append(moreRow);
				break;
			}
		}

		var text = jQuery(document.createElement('div')).addClass('et2_egw_action_ddHelper_tip');
		div.append(text);

		// Add notice of Ctrl key, if supported
		if('draggable' in document.createElement('span') &&
			navigator && navigator.userAgent.indexOf('Chrome') >= 0 && egw.app_name() == 'filemanager') // currently only filemanager supports drag out
		{
			var key = ["Mac68K","MacPPC","MacIntel"].indexOf(window.navigator.platform) < 0 ?
				egw.lang('Alt') : egw.lang('Command ⌘');
			text.text(egw.lang('Hold [%1] and [%2] key to drag %3 to your desktop', key, egw.lang('Shift ⇧'), itemLabel));
		}
		// Final html DOM return as helper structor
		return div;
	};

	ai.doRegisterAction = function(_aoi, _callback, _context)
	{
		var node = _aoi.getDOMNode() && _aoi.getDOMNode()[0] ? _aoi.getDOMNode()[0] : _aoi.getDOMNode();

		if (node)
		{
			// Prevent selection
			node.onselectstart = function () {
				return false;
			};
			if (!(window.FileReader && 'draggable' in document.createElement('span')) )
			{
				// No DnD support
				return;
			}

			// It shouldn't be so hard to get the action...
			var action = null;
			var groups = _context.getActionImplementationGroups();
			if(!groups.drag) return;
			for(var i = 0; i < groups.drag.length; i++)
			{
				// dragType 'file' says it can be dragged as a file
				if(groups.drag[i].link.actionObj.dragType == 'file' || groups.drag[i].link.actionObj.dragType.indexOf('file') > -1)
				{
					action = groups.drag[i].link.actionObj;
					break;
				}
			}

			if(!action)
			{
				// Use Ctrl key in order to select content
				jQuery(node).off("mousedown")
						.on({
							mousedown: function(event){
								if (_context.isSelection(event)){
									node.setAttribute("draggable", false);
								}
								else if(event.which != 3)
								{
									document.getSelection().removeAllRanges();
								}
							},
							mouseup: function (event){
								if (_context.isSelection(event)){
									// TODO: save and retrive selected range
									node.setAttribute("draggable", true);
								}
								else
								{
									node.setAttribute("draggable", true);
								}

								// Set cursor back to auto. Seems FF can't handle cursor reversion
								jQuery('body').css({cursor:'auto'});
							}
				});
			}

			node.setAttribute('draggable', true);
			const dragstart = function(event) {
				if (action) {
					if (_context.isSelection(event)) return;

					// Get all selected
					// Multiples aren't supported by event.dataTransfer, yet, so
					// select only the row they clicked on.
					var selected = [_context];
					_context.parent.setAllSelected(false);
					_context.setSelected(true);

					// Set file data
					for (let i = 0; i < selected.length; i++) {
						let d = selected[i].data || egw.dataGetUIDdata(selected[i].id).data || {};
						if (d && d.mime && d.download_url) {
							var url = d.download_url;

							// NEED an absolute URL
							if (url[0] == '/') url = egw.link(url);
							// egw.link adds the webserver, but that might not be an absolute URL - try again
							if (url[0] == '/') url = window.location.origin + url;

							// Unfortunately, dragging files is currently only supported by Chrome
							if (navigator && navigator.userAgent.indexOf('Chrome')) {
								event.dataTransfer.setData("DownloadURL", d.mime + ':' + d.name + ':' + url);
							} else {
								// Include URL as a fallback
								event.dataTransfer.setData("text/uri-list", url);
							}
						}
					}
					event.dataTransfer.effectAllowed = 'copy';

					if (event.dataTransfer.types.length == 0) {
						// No file data? Abort: drag does nothing
						event.preventDefault();
						return;
					}
				} else {
					event.dataTransfer.effectAllowed = 'linkMove';
				}
				// The helper function is called before the start function
				// is evoked. Call the given callback function. The callback
				// function will gather the selected elements and action links
				// and call the doExecuteImplementation function. This
				// will call the onExecute function of the first action
				// in order to obtain the helper object (stored in ai.helper)
				// and the multiple dragDropTypes (ai.ddTypes)
				_callback.call(_context, false, ai);

				const data = {
					ddTypes: ai.ddTypes,
					selected: ai.selected.map((item) => {
						return {id: item.id}
					})
				};

				if (!ai.helper) {
					ai.helper = ai.defaultDDHelper(ai.selected);
				}
				// Add a basic class to the helper in order to standardize the background layout
				ai.helper[0].classList.add('et2_egw_action_ddHelper', 'ui-draggable-dragging');
				document.body.append(ai.helper[0]);
				this.classList.add('drag--moving');

				event.dataTransfer.setData('application/json', JSON.stringify(data))

				event.dataTransfer.setDragImage(ai.helper[0], 12, 12);

				this.setAttribute('data-egwActionObjID', JSON.stringify(data.selected));
			};

			const dragend = function(event){
				const helper = document.querySelector('.et2_egw_action_ddHelper');
				if (helper) helper.remove();
				const draggable = document.querySelector('.drag--moving');
				if (draggable) draggable.classList.remove('drag--moving');
			};

			// Drag Event listeners
			node.addEventListener('dragstart', dragstart , false);
			node.addEventListener('dragend', dragend, false);


			return true;
		}
		return false;
	};

	ai.doUnregisterAction = function(_aoi)
	{
		var node = _aoi.getDOMNode();

		if (node){
			node.setAttribute('draggable', false);
		}
	};

	/**
	 * Builds the context menu and shows it at the given position/DOM-Node.
	 *
	 * @param {string} _context
	 * @param {array} _selected
	 * @param {object} _links
	 */
	ai.doExecuteImplementation = function(_context, _selected, _links)
	{
		// Reset the helper object of the action implementation
		this.helper = null;
		var hasLink = false;

		// Store the drag-drop types
		this.ddTypes = [];
		this.selected = _selected;

		// Call the onExecute event of the first actionObject
		for (var k in _links)
		{
			if (_links[k].visible)
			{
				hasLink = true;

				// Only execute the following code if a JS function is registered
				// for the action and this is the first action link
				if (!this.helper && _links[k].actionObj.onExecute.hasHandler())
				{
					this.helper = _links[k].actionObj.execute(_selected);
				}

				// Push the dragType of the associated action object onto the
				// drag type list - this allows an element to support multiple
				// drag/drop types.
				var type = jQuery.isArray(_links[k].actionObj.dragType) ? _links[k].actionObj.dragType : [_links[k].actionObj.dragType];
				for(var i = 0; i < type.length; i++)
				{
					if (this.ddTypes.indexOf(type[i]) == -1)
					{
						this.ddTypes.push(type[i]);
					}
				}
			}
		}

		// If no helper has been defined, create an default one
		if (!this.helper && hasLink)
		{
			this.helper = ai.defaultDDHelper(_selected);
		}

		return true;
	};

	return ai;
}



/**
 * The egwDropAction class overwrites the egwAction class and adds the "acceptedTypes"
 * property. This array should contain all "dragTypes" the drop action is allowed to
 *
 * @param {egwAction} _id
 * @param {string} _handler
 * @param {string} _caption
 * @param {string} _icon
 * @param {(string|function)} _onExecute
 * @param {bool} _allowOnMultiple
 * @returns {egwDropAction}
 */
export function egwDropAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple)
{
	var action = new egwAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple);

	action.type = "drop";
	action.acceptedTypes = ["default"];
	action.canHaveChildren = ["drag","popup"];
	action["default"] = false;
	action.order = 0;
	action.group = 0;

	action.set_default = function(_value) {
		action["default"] = _value;
	};

	action.set_order = function(_value) {
		action.order = _value;
	};

	action.set_group = function(_value) {
		action.group = _value;
	};

	/**
	 * The acceptType property allows strings as well as arrays - strings are
	 * automatically included in an array.
	 *
	 * @param {(string|array)} _value
	 */
	action.set_acceptedTypes = function(_value) {
		if (_value instanceof Array)
		{
			action.acceptedTypes = _value;
		}
		else
		{
			action.acceptedTypes = [_value];
		}
	};

	return action;
}

var
	_dropActionImpl = null;

export function getDropImplementation()
{
	if (!_dropActionImpl)
	{
		_dropActionImpl = new egwDropActionImplementation();
	}
	return _dropActionImpl;
}

export function egwDropActionImplementation()
{
	var ai = new egwActionImplementation();

	//keeps track of current drop element where dragged item's entered.
	// it's necessary for dragenter/dragleave issue correction.
	var currentDropEl = null;

	ai.type = "drop";

	ai.doRegisterAction = function(_aoi, _callback, _context)
	{
		var node = _aoi.getDOMNode() && _aoi.getDOMNode()[0] ? _aoi.getDOMNode()[0] : _aoi.getDOMNode();
		var self = this;
		if (node)
		{
			node.classList.add('et2dropzone');
			const dragover = function (event) {
				if (event.preventDefault) {
					event.preventDefault();
				}
				if (!self.getTheDraggedDOM()) return ;

					const data = {
					event: event,
					ui: self.getTheDraggedData()
				};
				_aoi.triggerEvent(EGW_AI_DRAG_OVER, data);

				return true;

			};

			const dragenter = function (event) {
				event.stopImmediatePropagation();
				// don't trigger dragenter if we are entering the drag element
				// don't go further if the dragged element is no there (happens when a none et2 dragged element is being dragged)
				if (!self.getTheDraggedDOM() || self.isTheDraggedDOM(this) || this == currentDropEl) return;

				currentDropEl = event.currentTarget;
				event.dataTransfer.dropEffect = 'link';

				const data = {
					event: event,
					ui: self.getTheDraggedData()
				};

				_aoi.triggerEvent(EGW_AI_DRAG_ENTER, data);

				this.classList.add('drop-hover');

				// stop the event from being fired for its children
				event.preventDefault();
				return false;
			};

			const drop = function (event) {
				event.preventDefault();
				// don't go further if the dragged element is no there (happens when a none et2 dragged element is being dragged)
				if (!self.getTheDraggedDOM()) return ;

				// remove the hover class
				this.classList.remove('drop-hover');

				const helper = self.getHelperDOM();
				let ui = self.getTheDraggedData();
				ui.position = {top: event.clientY, left: event.clientX};
				ui.offset = {top: event.offsetY, left: event.offsetX};


				let data = JSON.parse(event.dataTransfer.getData('application/json'));

				if (!self.isAccepted(data, _context, _callback) || self.isTheDraggedDOM(this))
				{
					// clean up the helper dom
					if (helper) helper.remove();
					return;
				}

				let selected = data.selected.map((item) => {
					return egw_getObjectManager(item.id, false)
				});


				var links = _callback.call(_context, "links", self, EGW_AO_EXEC_THIS);

				// Disable all links which only accept types which are not
				// inside ddTypes
				for (var k in links) {
					var accepted = links[k].actionObj.acceptedTypes;

					var enabled = false;
					for (var i = 0; i < data.ddTypes.length; i++) {
						if (accepted.indexOf(data.ddTypes[i]) != -1) {
							enabled = true;
							break;
						}
					}
					// Check for allowing multiple selected
					if (!links[k].actionObj.allowOnMultiple && selected.length > 1) {
						enabled = false;
					}
					if (!enabled) {
						links[k].enabled = false;
						links[k].visible = !links[k].actionObj.hideOnDisabled;
					}
				}

				// Check whether there is only one link
				var cnt = 0;
				var lnk = null;
				for (var k in links) {
					if (links[k].enabled && links[k].visible) {
						lnk = links[k];
						cnt += 1 + links[k].actionObj.children.length;

						// Add ui, so you know what happened where
						lnk.actionObj.ui = ui;

					}
				}

				if (cnt == 1) {
					window.setTimeout(function () {
						lnk.actionObj.execute(selected, _context);
					}, 0);
				}

				if (cnt > 1) {
					// More than one drop action link is associated
					// to the drop event - show those as a popup menu
					// and let the user decide which one to use.
					// This is possible as the popup and the popup action
					// object and the drop action object share same
					// set of properties.
					var popup = getPopupImplementation();
					var pos = popup._getPageXY(event);

					// Don't add paste actions, this is a drop
					popup.auto_paste = false;

					window.setTimeout(function () {
						popup.doExecuteImplementation(pos, selected, links,
							_context);
						// Reset, popup is reused
						popup.auto_paste = true;
					}, 0); // Timeout is needed to have it working in IE
				}
				// Set cursor back to auto. Seems FF can't handle cursor reversion
				jQuery('body').css({cursor: 'auto'});

				_aoi.triggerEvent(EGW_AI_DRAG_OUT, {event: event, ui: self.getTheDraggedData()});

				// clean up the helper dom
				if (helper) helper.remove();
				self.getTheDraggedDOM().classList.remove('drag--moving');
			};

			const dragleave = function (event) {
				event.stopImmediatePropagation();

				// don't trigger dragleave if we are leaving the drag element
				// don't go further if the dragged element is no there (happens when a none et2 dragged element is being dragged)
				if (!self.getTheDraggedDOM() || self.isTheDraggedDOM(this) || this == currentDropEl) return;

				const data = {
					event: event,
					ui: self.getTheDraggedData()
				};

				_aoi.triggerEvent(EGW_AI_DRAG_OUT, data);

				this.classList.remove('drop-hover');

				event.preventDefault();
				return false;
			};

			// DND Event listeners
			node.addEventListener('dragover', dragover, false);

			node.addEventListener('dragenter', dragenter, false);

			node.addEventListener('drop', drop, false);

			node.addEventListener('dragleave', dragleave, false);

			return true;
		}
		return false;
	};

	ai.isTheDraggedDOM = function (_dom)
	{
		return _dom.classList.contains('drag--moving');
	}

	ai.getTheDraggedDOM = function ()
	{
		return document.querySelector('.drag--moving');
	}

	ai.getHelperDOM = function ()
	{
		return document.querySelector('.et2_egw_action_ddHelper');
	}

	ai.getTheDraggedData = function()
	{
		let data = this.getTheDraggedDOM().dataset.egwactionobjid;
		let selected = [];
		if (data)
		{
			data = JSON.parse(data);
			selected = data.map((item)=>{return egw_getObjectManager(item.id, false)});
		}
		return {
			draggable: this.getTheDraggedDOM(),
			helper: this.getHelperDOM(),
			selected: selected

		}
	}

	// check if given draggable is accepted for drop
	ai.isAccepted = function(_data, _context, _callback, _node)
	{
		if (_node && !_node.classList.contains('et2dropzone')) return false;
		if (typeof _data.ddTypes != "undefined")
		{
			const accepted = this._fetchAccepted(
				_callback.call(_context, "links", this, EGW_AO_EXEC_THIS));

			// Check whether all drag types of the selected objects
			// are accepted
			var ddTypes = _data.ddTypes;

			for (let i = 0; i < ddTypes.length; i++)
			{
				if (accepted.indexOf(ddTypes[i]) != -1)
				{
					return true;
				}
			}
		}
		return false;
	};

	ai.doUnregisterAction = function(_aoi)
	{
		var node = _aoi.getDOMNode();

		if (node) {
			node.classList.remove('et2dropzone');
		}
	};

	ai._fetchAccepted = function(_links)
	{
		// Accumulate the accepted types
		var accepted = [];
		for (var k in _links)
		{
			for (var i = 0; i < _links[k].actionObj.acceptedTypes.length; i++)
			{
				var type = _links[k].actionObj.acceptedTypes[i];

				if (accepted.indexOf(type) == -1)
				{
					accepted.push(type);
				}
			}
		}

		return accepted;
	};

	/**
	 * Builds the context menu and shows it at the given position/DOM-Node.
	 *
	 * @param {string} _context
	 * @param {array} _selected
	 * @param {object} _links
	 */
	ai.doExecuteImplementation = function(_context, _selected, _links)
	{
		if (_context == "links")
		{
			return _links;
		}
	};

	return ai;
}