import {
	toggleClass,
	getRect,
	index,
	closest,
	on,
	off,
	clone,
	css,
	setRect,
	unsetRect,
	matrix,
	expando
} from '../../src/utils.js';

import dispatchEvent from '../../src/EventDispatcher.js';

let multiDragElements = [],
	multiDragClones = [],
	lastMultiDragSelect, // for selection with modifier key down (SHIFT)
	multiDragSortable,
	initialFolding = false, // Initial multi-drag fold when drag started
	folding = false, // Folding any other time
	dragStarted = false,
	dragEl,
	clonesFromRect,
	clonesHidden;

function MultiDragPlugin() {
	function MultiDrag(sortable) {
		// Bind all private methods
		for (let fn in this) {
			if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
				this[fn] = this[fn].bind(this);
			}
		}

		if (!sortable.options.avoidImplicitDeselect) {
			if (sortable.options.supportPointer) {
				on(document, 'pointerup', this._deselectMultiDrag);
			} else {
				on(document, 'mouseup', this._deselectMultiDrag);
				on(document, 'touchend', this._deselectMultiDrag);
			}
		}

		on(document, 'keydown', this._checkKeyDown);
		on(document, 'keyup', this._checkKeyUp);

		this.defaults = {
			selectedClass: 'sortable-selected',
			multiDragKey: null,
			avoidImplicitDeselect: false,
			setData(dataTransfer, dragEl) {
				let data = '';
				if (multiDragElements.length && multiDragSortable === sortable) {
					multiDragElements.forEach((multiDragElement, i) => {
						data += (!i ? '' : ', ') + multiDragElement.textContent;
					});
				} else {
					data = dragEl.textContent;
				}
				dataTransfer.setData('Text', data);
			}
		};
	}

	MultiDrag.prototype = {
		multiDragKeyDown: false,
		isMultiDrag: false,


		delayStartGlobal({ dragEl: dragged }) {
			dragEl = dragged;
		},

		delayEnded() {
			this.isMultiDrag = ~multiDragElements.indexOf(dragEl);
		},

		setupClone({ sortable, cancel }) {
			if (!this.isMultiDrag) return;
			for (let i = 0; i < multiDragElements.length; i++) {
				multiDragClones.push(clone(multiDragElements[i]));

				multiDragClones[i].sortableIndex = multiDragElements[i].sortableIndex;

				multiDragClones[i].draggable = false;
				multiDragClones[i].style['will-change'] = '';

				toggleClass(multiDragClones[i], this.options.selectedClass, false);
				multiDragElements[i] === dragEl && toggleClass(multiDragClones[i], this.options.chosenClass, false);
			}

			sortable._hideClone();
			cancel();
		},

		clone({ sortable, rootEl, dispatchSortableEvent, cancel }) {
			if (!this.isMultiDrag) return;
			if (!this.options.removeCloneOnHide) {
				if (multiDragElements.length && multiDragSortable === sortable) {
					insertMultiDragClones(true, rootEl);
					dispatchSortableEvent('clone');

					cancel();
				}
			}
		},

		showClone({ cloneNowShown, rootEl, cancel }) {
			if (!this.isMultiDrag) return;
			insertMultiDragClones(false, rootEl);
			multiDragClones.forEach(clone => {
				css(clone, 'display', '');
			});

			cloneNowShown();
			clonesHidden = false;
			cancel();
		},

		hideClone({ sortable, cloneNowHidden, cancel }) {
			if (!this.isMultiDrag) return;
			multiDragClones.forEach(clone => {
				css(clone, 'display', 'none');
				if (this.options.removeCloneOnHide && clone.parentNode) {
					clone.parentNode.removeChild(clone);
				}
			});

			cloneNowHidden();
			clonesHidden = true;
			cancel();
		},

		dragStartGlobal({ sortable }) {
			if (!this.isMultiDrag && multiDragSortable) {
				multiDragSortable.multiDrag._deselectMultiDrag();
			}

			multiDragElements.forEach(multiDragElement => {
				multiDragElement.sortableIndex = index(multiDragElement);
			});

			// Sort multi-drag elements
			multiDragElements = multiDragElements.sort(function(a, b) {
				return a.sortableIndex - b.sortableIndex;
			});
			dragStarted = true;
		},

		dragStarted({ sortable }) {
			if (!this.isMultiDrag) return;
			if (this.options.sort) {
				// Capture rects,
				// hide multi drag elements (by positioning them absolute),
				// set multi drag elements rects to dragRect,
				// show multi drag elements,
				// animate to rects,
				// unset rects & remove from DOM

				sortable.captureAnimationState();

				if (this.options.animation) {
					multiDragElements.forEach(multiDragElement => {
						if (multiDragElement === dragEl) return;
						css(multiDragElement, 'position', 'absolute');
					});

					let dragRect = getRect(dragEl, false, true, true);

					multiDragElements.forEach(multiDragElement => {
						if (multiDragElement === dragEl) return;
						setRect(multiDragElement, dragRect);
					});

					folding = true;
					initialFolding = true;
				}
			}

			sortable.animateAll(() => {
				folding = false;
				initialFolding = false;

				if (this.options.animation) {
					multiDragElements.forEach(multiDragElement => {
						unsetRect(multiDragElement);
					});
				}

				// Remove all auxiliary multidrag items from el, if sorting enabled
				if (this.options.sort) {
					removeMultiDragElements();
				}
			});
		},

		dragOver({ target, completed, cancel }) {
			if (folding && ~multiDragElements.indexOf(target)) {
				completed(false);
				cancel();
			}
		},

		revert({ fromSortable, rootEl, sortable, dragRect }) {
			if (multiDragElements.length > 1) {
				// Setup unfold animation
				multiDragElements.forEach(multiDragElement => {
					sortable.addAnimationState({
						target: multiDragElement,
						rect: folding ? getRect(multiDragElement) : dragRect
					});

					unsetRect(multiDragElement);

					multiDragElement.fromRect = dragRect;

					fromSortable.removeAnimationState(multiDragElement);
				});
				folding = false;
				insertMultiDragElements(!this.options.removeCloneOnHide, rootEl);
			}
		},

		dragOverCompleted({ sortable, isOwner, insertion, activeSortable, parentEl, putSortable }) {
			let options = this.options;
			if (insertion) {
				// Clones must be hidden before folding animation to capture dragRectAbsolute properly
				if (isOwner) {
					activeSortable._hideClone();
				}

				initialFolding = false;
				// If leaving sort:false root, or already folding - Fold to new location
				if (options.animation && multiDragElements.length > 1 && (folding || !isOwner && !activeSortable.options.sort && !putSortable)) {
					// Fold: Set all multi drag elements's rects to dragEl's rect when multi-drag elements are invisible
					let dragRectAbsolute = getRect(dragEl, false, true, true);

					multiDragElements.forEach(multiDragElement => {
						if (multiDragElement === dragEl) return;
						setRect(multiDragElement, dragRectAbsolute);

						// Move element(s) to end of parentEl so that it does not interfere with multi-drag clones insertion if they are inserted
						// while folding, and so that we can capture them again because old sortable will no longer be fromSortable
						parentEl.appendChild(multiDragElement);
					});

					folding = true;
				}

				// Clones must be shown (and check to remove multi drags) after folding when interfering multiDragElements are moved out
				if (!isOwner) {
					// Only remove if not folding (folding will remove them anyways)
					if (!folding) {
						removeMultiDragElements();
					}

					if (multiDragElements.length > 1) {
						let clonesHiddenBefore = clonesHidden;
						activeSortable._showClone(sortable);

						// Unfold animation for clones if showing from hidden
						if (activeSortable.options.animation && !clonesHidden && clonesHiddenBefore) {
							multiDragClones.forEach(clone => {
								activeSortable.addAnimationState({
									target: clone,
									rect: clonesFromRect
								});

								clone.fromRect = clonesFromRect;
								clone.thisAnimationDuration = null;
							});
						}
					} else {
						activeSortable._showClone(sortable);
					}
				}
			}
		},

		dragOverAnimationCapture({ dragRect, isOwner, activeSortable }) {
			multiDragElements.forEach(multiDragElement => {
				multiDragElement.thisAnimationDuration = null;
			});

			if (activeSortable.options.animation && !isOwner && activeSortable.multiDrag.isMultiDrag) {
				clonesFromRect = Object.assign({}, dragRect);
				let dragMatrix = matrix(dragEl, true);
				clonesFromRect.top -= dragMatrix.f;
				clonesFromRect.left -= dragMatrix.e;
			}
		},

		dragOverAnimationComplete() {
			if (folding) {
				folding = false;
				removeMultiDragElements();
			}
		},

		drop({ originalEvent: evt, rootEl, parentEl, sortable, dispatchSortableEvent, oldIndex, putSortable }) {
			let toSortable = (putSortable || this.sortable);

			if (!evt) return;

			let options = this.options,
				children = parentEl.children;

			// Multi-drag selection
			if (!dragStarted) {
				if (options.multiDragKey && !this.multiDragKeyDown) {
					this._deselectMultiDrag();
				}
				toggleClass(dragEl, options.selectedClass, !~multiDragElements.indexOf(dragEl));

				if (!~multiDragElements.indexOf(dragEl)) {
					multiDragElements.push(dragEl);
					dispatchEvent({
						sortable,
						rootEl,
						name: 'select',
						targetEl: dragEl,
						originalEvent: evt
					});

					// Modifier activated, select from last to dragEl
					if (evt.shiftKey && lastMultiDragSelect && sortable.el.contains(lastMultiDragSelect)) {
						let lastIndex = index(lastMultiDragSelect),
							currentIndex = index(dragEl);

						if (~lastIndex && ~currentIndex && lastIndex !== currentIndex) {
							// Must include lastMultiDragSelect (select it), in case modified selection from no selection
							// (but previous selection existed)
							let n, i;
							if (currentIndex > lastIndex) {
								i = lastIndex;
								n = currentIndex;
							} else {
								i = currentIndex;
								n = lastIndex + 1;
							}

							const filter = options.filter;

							for (; i < n; i++) {
								if (~multiDragElements.indexOf(children[i])) continue;
								// Check if element is draggable
								if (!closest(children[i], options.draggable, parentEl, false)) continue;
								// Check if element is filtered
								const filtered = filter && (typeof filter === 'function' ?
									filter.call(sortable, evt, children[i], sortable) :
									filter.split(',').some((criteria) => {
										return closest(children[i], criteria.trim(), parentEl, false);
									}));
								if (filtered) continue;
								toggleClass(children[i], options.selectedClass, true);
								multiDragElements.push(children[i]);

								dispatchEvent({
									sortable,
									rootEl,
									name: 'select',
									targetEl: children[i],
									originalEvent: evt
								});
							}
						}
					} else {
						lastMultiDragSelect = dragEl;
					}

					multiDragSortable = toSortable;
				} else {
					multiDragElements.splice(multiDragElements.indexOf(dragEl), 1);
					lastMultiDragSelect = null;
					dispatchEvent({
						sortable,
						rootEl,
						name: 'deselect',
						targetEl: dragEl,
						originalEvent: evt
					});
				}
			}

			// Multi-drag drop
			if (dragStarted && this.isMultiDrag) {
				folding = false;
				// Do not "unfold" after around dragEl if reverted
				if ((parentEl[expando].options.sort || parentEl !== rootEl) && multiDragElements.length > 1) {
					let dragRect = getRect(dragEl),
						multiDragIndex = index(dragEl, ':not(.' + this.options.selectedClass + ')');

					if (!initialFolding && options.animation) dragEl.thisAnimationDuration = null;

					toSortable.captureAnimationState();

					if (!initialFolding) {
						if (options.animation) {
							dragEl.fromRect = dragRect;
							multiDragElements.forEach(multiDragElement => {
								multiDragElement.thisAnimationDuration = null;
								if (multiDragElement !== dragEl) {
									let rect = folding ? getRect(multiDragElement) : dragRect;
									multiDragElement.fromRect = rect;

									// Prepare unfold animation
									toSortable.addAnimationState({
										target: multiDragElement,
										rect: rect
									});
								}
							});
						}

						// Multi drag elements are not necessarily removed from the DOM on drop, so to reinsert
						// properly they must all be removed
						removeMultiDragElements();

						multiDragElements.forEach(multiDragElement => {
							if (children[multiDragIndex]) {
								parentEl.insertBefore(multiDragElement, children[multiDragIndex]);
							} else {
								parentEl.appendChild(multiDragElement);
							}
							multiDragIndex++;
						});

						// If initial folding is done, the elements may have changed position because they are now
						// unfolding around dragEl, even though dragEl may not have his index changed, so update event
						// must be fired here as Sortable will not.
						if (oldIndex === index(dragEl)) {
							let update = false;
							multiDragElements.forEach(multiDragElement => {
								if (multiDragElement.sortableIndex !== index(multiDragElement)) {
									update = true;
									return;
								}
							});

							if (update) {
								dispatchSortableEvent('update');
								dispatchSortableEvent('sort');
							}
						}
					}

					// Must be done after capturing individual rects (scroll bar)
					multiDragElements.forEach(multiDragElement => {
						unsetRect(multiDragElement);
					});

					toSortable.animateAll();
				}

				multiDragSortable = toSortable;
			}

			// Remove clones if necessary
			if (rootEl === parentEl || (putSortable && putSortable.lastPutMode !== 'clone')) {
				multiDragClones.forEach(clone => {
					clone.parentNode && clone.parentNode.removeChild(clone);
				});
			}
		},

		nullingGlobal() {
			this.isMultiDrag =
			dragStarted = false;
			multiDragClones.length = 0;
		},

		destroyGlobal() {
			this._deselectMultiDrag();
			off(document, 'pointerup', this._deselectMultiDrag);
			off(document, 'mouseup', this._deselectMultiDrag);
			off(document, 'touchend', this._deselectMultiDrag);

			off(document, 'keydown', this._checkKeyDown);
			off(document, 'keyup', this._checkKeyUp);
		},

		_deselectMultiDrag(evt) {
			if (typeof dragStarted !== "undefined" && dragStarted) return;

			// Only deselect if selection is in this sortable
			if (multiDragSortable !== this.sortable) return;

			// Only deselect if target is not item in this sortable
			if (evt && closest(evt.target, this.options.draggable, this.sortable.el, false)) return;

			// Only deselect if left click
			if (evt && evt.button !== 0) return;

			while (multiDragElements.length) {
				let el = multiDragElements[0];
				toggleClass(el, this.options.selectedClass, false);
				multiDragElements.shift();
				dispatchEvent({
					sortable: this.sortable,
					rootEl: this.sortable.el,
					name: 'deselect',
					targetEl: el,
					originalEvent: evt
				});
			}
		},

		_checkKeyDown(evt) {
			if (evt.key === this.options.multiDragKey) {
				this.multiDragKeyDown = true;
			}
		},

		_checkKeyUp(evt) {
			if (evt.key === this.options.multiDragKey) {
				this.multiDragKeyDown = false;
			}
		}
	};

	return Object.assign(MultiDrag, {
		// Static methods & properties
		pluginName: 'multiDrag',
		utils: {
			/**
			 * Selects the provided multi-drag item
			 * @param  {HTMLElement} el    The element to be selected
			 */
			select(el) {
				let sortable = el.parentNode[expando];
				if (!sortable || !sortable.options.multiDrag || ~multiDragElements.indexOf(el)) return;
				if (multiDragSortable && multiDragSortable !== sortable) {
					multiDragSortable.multiDrag._deselectMultiDrag();
					multiDragSortable = sortable;
				}
				toggleClass(el, sortable.options.selectedClass, true);
				multiDragElements.push(el);
			},
			/**
			 * Deselects the provided multi-drag item
			 * @param  {HTMLElement} el    The element to be deselected
			 */
			deselect(el) {
				let sortable = el.parentNode[expando],
					index = multiDragElements.indexOf(el);
				if (!sortable || !sortable.options.multiDrag || !~index) return;
				toggleClass(el, sortable.options.selectedClass, false);
				multiDragElements.splice(index, 1);
			}
		},
		eventProperties() {
			const oldIndicies = [],
				newIndicies = [];

			multiDragElements.forEach(multiDragElement => {
				oldIndicies.push({
					multiDragElement,
					index: multiDragElement.sortableIndex
				});

				// multiDragElements will already be sorted if folding
				let newIndex;
				if (folding && multiDragElement !== dragEl) {
					newIndex = -1;
				} else if (folding) {
					newIndex = index(multiDragElement, ':not(.' + this.options.selectedClass + ')');
				} else {
					newIndex = index(multiDragElement);
				}
				newIndicies.push({
					multiDragElement,
					index: newIndex
				});
			});
			return {
				items: [...multiDragElements],
				clones: [...multiDragClones],
				oldIndicies,
				newIndicies
			};
		},
		optionListeners: {
			multiDragKey(key) {
				key = key.toLowerCase();
				if (key === 'ctrl') {
					key = 'Control';
				} else if (key.length > 1) {
					key = key.charAt(0).toUpperCase() + key.substr(1);
				}
				return key;
			}
		}
	});
}

function insertMultiDragElements(clonesInserted, rootEl) {
	multiDragElements.forEach((multiDragElement, i) => {
		let target = rootEl.children[multiDragElement.sortableIndex + (clonesInserted ? Number(i) : 0)];
		if (target) {
			rootEl.insertBefore(multiDragElement, target);
		} else {
			rootEl.appendChild(multiDragElement);
		}
	});
}

/**
 * Insert multi-drag clones
 * @param  {[Boolean]} elementsInserted  Whether the multi-drag elements are inserted
 * @param  {HTMLElement} rootEl
 */
function insertMultiDragClones(elementsInserted, rootEl) {
	multiDragClones.forEach((clone, i) => {
		let target = rootEl.children[clone.sortableIndex + (elementsInserted ? Number(i) : 0)];
		if (target) {
			rootEl.insertBefore(clone, target);
		} else {
			rootEl.appendChild(clone);
		}
	});
}

function removeMultiDragElements() {
	multiDragElements.forEach(multiDragElement => {
		if (multiDragElement === dragEl) return;
		multiDragElement.parentNode && multiDragElement.parentNode.removeChild(multiDragElement);
	});
}

export default MultiDragPlugin;
