/**
 * TODO(chris): This is only a prototype!!!
 */

const TYPE_DEFINITION = {
	lehreinheit: {
		id: "lehreinheit_id",
		dragIcon: "fa-solid fa-chalkboard-user",
		extras: [
			"stundenblockung"
		]
	},
	vevent: {
		id: "uid",
		dragIcon: "fa-solid fa-calendar",
		extras: [
			"dtstart",
			"dtend",
			"summary"
		]
	},
	person: {
		id: "person_id",
		dragIcon: "fa-solid fa-user"
	},
	student: {
		id: "student_uid",
		dragIcon: "fa-solid fa-user-graduate"
	},
	prestudent: {
		id: "prestudent_id",
		dragIcon: "fa-solid fa-user-graduate text-muted"
	}
	// TODO: IMPLEMENT OTHER TYPES
};

const VALID_TYPES = Object.keys(TYPE_DEFINITION);

const TYPE_CONSTANTS = Object.keys(TYPE_DEFINITION).reduce((res, type) => {
	res['TYPE_' + type.toUpperCase()] = type;
	return res;
}, {});

function isValidDragObject(value) {
	if (!value)
		return false;
	if (Array.isArray(value))
		return value.every(isValidDragObject);
	if (!value.type)
		return false;

	if (value.type.substr(-11) == '-collection') {
		if (!Object.prototype.hasOwnProperty.call(value, 'values'))
			return false;

		if (!VALID_TYPES.includes(value.type.substr(0, value.type.length-11)))
			return false;
	} else {
		if (!Object.prototype.hasOwnProperty.call(value, 'id'))
			return false;

		if (!VALID_TYPES.includes(value.type))
			return false;

		if (TYPE_DEFINITION[value.type].extras) {
			if (!TYPE_DEFINITION[value.type].extras.every(extra => Object.prototype.hasOwnProperty.call(value, extra)))
				return false;
		}
	}

	return true;
}

function getValidTransferData(event, allowedTypes, strict) {
	let obj = null;

	try {
		obj = getTransferData(event, strict);
		if (!obj)
			return null;

		if (!strict && Array.isArray(obj)) {
			obj = obj.filter(isValidDragObject);
			if (!obj.length)
				return null;
		} else if (!isValidDragObject(obj))
			return null;

		if (allowedTypes && allowedTypes.length) {
			if (Array.isArray(obj)) {
				if (strict && !obj.every(v => allowedTypes.includes(v.type))) {
					return null;
				} else if (!strict) {
					obj = obj.filter(v => allowedTypes.includes(v.type));
					if (!obj.length)
						return null;
				}
			} else if (!allowedTypes.includes(obj.type)) {
				return null;
			}
		}
	} catch(_error) {
		return null;
	}

	if (Array.isArray(obj) && obj.length == 1)
		return obj.find(Boolean);

	return obj;
}

function isValidTransferData(event, allowedTypes, strict) {
	return getValidTransferData(event, allowedTypes, strict) ? true : false;
}

function getTransferData(event, strict) {
	const result = [];

	for (const type of event.dataTransfer.types) {
		if (type.substr(0, 16) != 'application/fhc-') {
			if (strict)
				return null;
			continue;
		}
		let base_type = type.substr(16);
		let collection = false;
		if (base_type.substr(-11) == '-collection') {
			base_type = base_type.substr(0, base_type.length-11);
			collection = true;
		}
		if (!VALID_TYPES.includes(base_type)) {
			if (strict)
				return null;
			continue;
		}
		let data = JSON.parse(event.dataTransfer.getData(type));
		if (collection)
			result.push(...data.values);
		else
			result.push(data);
	}

	if (!result.length)
		return null;

	if (result.length == 1)
		return result[0];

	return result;
}

function convertToValidDragObject(data, strict) {
	if (Array.isArray(data)) {
		const converted = data.map(convertToValidDragObject).filter(Boolean);
		if (!converted.length)
			return undefined;
		if (strict && converted.length != data.length)
			return undefined;

		const sorted = converted.reduce((res, item) => {
			if (!res[item.type])
				res[item.type] = [];
			res[item.type].push(item);
			return res;
		}, {});

		return Object.entries(sorted).map(([type, values]) => {
			if (values.length > 1) {
				return {
					type: type + '-collection',
					values
				};
			}
			return values[0];
		});
	}

	if (Object.prototype.hasOwnProperty.call(data, 'type') && isValidDragObject(data)) {
		return data;
	}

	const found = Object.entries(TYPE_DEFINITION).find(([ , typedef ]) => {
		if (!Object.prototype.hasOwnProperty.call(data, typedef.id))
			return false;
		if (typedef.extras) {
			if (!typedef.extras.every(extra => Object.prototype.hasOwnProperty.call(data, extra)))
				return false;
		}
		return true;
	});

	if (!found) {
		return undefined;
	}

	const [ type, typedef ] = found;

	const newData = {};
	newData.type = type;
	newData.id = data[typedef.id];
	if (typedef.extras)
		typedef.extras.forEach(extra => newData[extra] = data[extra]);

	return newData;
}

function setTransferData(event, validDragObject, setDragImage = false) {
	if (setDragImage) {
		const dragItems = Array.isArray(validDragObject) ? validDragObject : [ validDragObject ];
		const dragElements = dragItems.map(item => {
			const icon = document.createElement('i');
			const label = document.createElement('span');
			const iconContainer = document.createElement('span');

			iconContainer.className = 'btn btn-outline-dark bg-light';
			label.className = 'small';

			if (TYPE_DEFINITION[item.type]) {
				icon.className = TYPE_DEFINITION[item.type].dragIcon || 'fa-solid fa-question';
				label.textContent = item.id;
			} else if (item.type.substr(-11) == '-collection' && TYPE_DEFINITION[item.type.substr(0, item.type.length-11)]) {
				iconContainer.style.boxShadow = '3px 3px var(--bs-btn-border-color)';
				icon.className = TYPE_DEFINITION[item.type.substr(0, item.type.length-11)].dragIcon || 'fa-solid fa-question';
				label.textContent = 'x' + item.values.length;
			} else {
				icon.className = 'fa-solid fa-question';
				label.textContent = item.id || '';
			}
			
			iconContainer.append(icon);

			const itemContainer = document.createElement('div');
			itemContainer.className = 'd-flex flex-column align-items-center gap-2 small';
			itemContainer.append(iconContainer, label);
			return itemContainer;
		});

		const container = document.createElement('div');
		container.className = 'd-flex flex-row gap-2 small';
		container.append(...dragElements);

		document.body.append(container);
		event.dataTransfer.setDragImage(container, -25, 0);
		requestAnimationFrame(() => {
			document.body.removeChild(container);
		});
	}
	if (Array.isArray(validDragObject)) {
		return validDragObject.forEach(data => setTransferData(event, data));
	}
	
	event.dataTransfer.setData('application/fhc-' + validDragObject.type, JSON.stringify(validDragObject));
}

/**
 * check if the dataTransfer types are in the allowed types array
 * if strict is disabled at least one type must be the allowed array
 * otherwise all types have to be in the allowed array
 *
 * @param Event		event
 * @param Array		allowedTypes
 * @param Boolean	strict
 */
function eventHasTypes(event, allowedTypes, strict) {
	if (!allowedTypes || !allowedTypes.length)
		allowedTypes = VALID_TYPES;
	allowedTypes = allowedTypes.map(type => 'application/fhc-' + type);

	const dataTypes = [...event.dataTransfer.types];
	
	// NOTE(chris): if dragging across browsers the dataTransfer object is
	// set to a default one without data. Since we do not support dragging
	// across browsers (yet) we return false which will disallow dropping.
	if (!dataTypes.length)
		return false;
	
	if (!strict)
		return allowedTypes.some(type => [...event.dataTransfer.types].includes(type));
	
	return [...event.dataTransfer.types].every(type => allowedTypes.includes(type));
}

function bindDragEnterLeave(el, onEnter, onLeave) {
	// NOTE(chris): add save dragenter and dragleave events
	// that won't fire when hovering over child elements

	let skipLeave = false;
	let skipLeaveParent = true;

	function init(evt) {
		skipLeave = false;
		skipLeaveParent = true;
		// add global listeners
		window.addEventListener('dragenter', globalDragenter, true);
		window.addEventListener('dragleave', globalDragleave, true);
		window.addEventListener('drop', globalDrop, true);
		// call enter
		onEnter(evt);
		// remove self
		el.removeEventListener('dragenter', init);
	}

	function cleanup(evt, wasDropped) {
		// remove global listeners
		window.removeEventListener('dragenter', globalDragenter, true);
		window.removeEventListener('dragleave', globalDragleave, true);
		window.removeEventListener('drop', globalDrop, true);
		// call leave
		onLeave(evt, wasDropped);
		// add init
		el.addEventListener('dragenter', init);
	}

	function globalDragenter(evt) {
		skipLeaveParent = false;
		if (el != evt.target && !el.contains(evt.target)) {
			cleanup(evt);
		} else {
			skipLeave = true;
		}
	}
	function globalDragleave(evt) {
		if (el != evt.target && !el.contains(evt.target)) {
			if (skipLeaveParent) {
				skipLeaveParent = false;
				return;
			}
		} else {
			if (skipLeave) {
				skipLeave = false;
				return;
			}
		}
		cleanup(evt);
	}
	function globalDrop(evt) {
		cleanup(evt, true);
	}

	el.addEventListener('dragenter', init);

	return () => {
		// cleanup
		el.removeEventListener('dragenter', init);
	}
}

export {
	isValidDragObject,
	getValidTransferData,
	isValidTransferData,
	getTransferData,
	convertToValidDragObject,
	setTransferData,
	eventHasTypes,
	bindDragEnterLeave
};
export default {
	...TYPE_CONSTANTS,
	isValidDragObject,
	getValidTransferData,
	isValidTransferData,
	getTransferData,
	convertToValidDragObject,
	setTransferData,
	eventHasTypes,
	bindDragEnterLeave
};
