'use strict';

/**
 * Copyright Marc J. Schmidt. See the LICENSE file at the top-level
 * directory of this distribution and at
 * https://github.com/marcj/css-element-queries/blob/master/LICENSE.
 */
(function (root, factory) {
	if (typeof define === 'function' && define.amd) {
		define(factory);
	} else if (typeof exports === 'object') {
		module.exports = factory();
	} else {
		root.ModulaResizeSensor = factory();
	}
})(typeof window !== 'undefined' ? window : this, function () {
	// Make sure it does not throw in a SSR (Server Side Rendering) situation
	if (typeof window === 'undefined') {
		return null;
	}
	// Only used for the dirty checking, so the event callback count is limited to max 1 call per fps per sensor.
	// In combination with the event based resize sensor this saves cpu time, because the sensor is too fast and
	// would generate too many unnecessary events.
	var requestAnimationFrame =
		window.requestAnimationFrame ||
		window.mozRequestAnimationFrame ||
		window.webkitRequestAnimationFrame ||
		function (fn) {
			return window.setTimeout(fn, 20);
		};

	/**
	 * Iterate over each of the provided element(s).
	 *
	 * @param {HTMLElement|HTMLElement[]} elements
	 * @param {Function}                  callback
	 */
	function forEachElement(elements, callback) {
		var elementsType = Object.prototype.toString.call(elements);
		var isCollectionTyped =
			'[object Array]' === elementsType ||
			'[object NodeList]' === elementsType ||
			'[object HTMLCollection]' === elementsType ||
			'[object Object]' === elementsType ||
			('undefined' !== typeof jQuery && elements instanceof jQuery) || //jquery
			('undefined' !== typeof Elements && elements instanceof Elements); //mootools
		var i = 0,
			j = elements.length;
		if (isCollectionTyped) {
			for (; i < j; i++) {
				callback(elements[i]);
			}
		} else {
			callback(elements);
		}
	}

	/**
	 * Get element size
	 * @param {HTMLElement} element
	 * @returns {Object} {width, height}
	 */
	function getElementSize(element) {
		if (!element.getBoundingClientRect) {
			return {
				width: element.offsetWidth,
				height: element.offsetHeight,
			};
		}

		var rect = element.getBoundingClientRect();
		return {
			width: Math.round(rect.width),
			height: Math.round(rect.height),
		};
	}

	/**
	 * Class for dimension change detection.
	 *
	 * @param {Element|Element[]|Elements|jQuery} element
	 * @param {Function} callback
	 *
	 * @constructor
	 */
	var ModulaResizeSensor = function (element, callback) {
		/**
		 *
		 * @constructor
		 */
		function EventQueue() {
			var q = [];
			this.add = function (ev) {
				q.push(ev);
			};

			var i, j;
			this.call = function () {
				for (i = 0, j = q.length; i < j; i++) {
					q[i].call();
				}
			};

			this.remove = function (ev) {
				var newQueue = [];
				for (i = 0, j = q.length; i < j; i++) {
					if (q[i] !== ev) newQueue.push(q[i]);
				}
				q = newQueue;
			};

			this.length = function () {
				return q.length;
			};
		}

		/**
		 *
		 * @param {HTMLElement} element
		 * @param {Function}    resized
		 */
		function attachResizeEvent(element, resized) {
			if (!element) return;
			if (element.resizedAttached) {
				element.resizedAttached.add(resized);
				return;
			}

			element.resizedAttached = new EventQueue();
			element.resizedAttached.add(resized);

			element.modularesizeSensor = document.createElement('div');
			element.modularesizeSensor.dir = 'ltr';
			element.modularesizeSensor.className = 'modula-resize-sensor';
			var style =
				'position: absolute; left: -10px; top: -10px; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;';
			var styleChild =
				'position: absolute; left: 0; top: 0; transition: 0s;';

			element.modularesizeSensor.style.cssText = style;
			element.modularesizeSensor.innerHTML =
				'<div class="modula-resize-sensor-expand" style="' +
				style +
				'">' +
				'<div style="' +
				styleChild +
				'"></div>' +
				'</div>' +
				'<div class="modula-resize-sensor-shrink" style="' +
				style +
				'">' +
				'<div style="' +
				styleChild +
				' width: 200%; height: 200%"></div>' +
				'</div>';
			element.appendChild(element.modularesizeSensor);

			var position = window
				.getComputedStyle(element)
				.getPropertyPriority('position');
			if (
				'absolute' !== position &&
				'relative' !== position &&
				'fixed' !== position
			) {
				element.style.position = 'relative';
			}

			var expand = element.modularesizeSensor.childNodes[0];
			var expandChild = expand.childNodes[0];
			var shrink = element.modularesizeSensor.childNodes[1];
			var dirty, rafId, newWidth, newHeight;
			var size = getElementSize(element);
			var lastWidth = size.width;
			var lastHeight = size.height;

			var reset = function () {
				//set display to block, necessary otherwise hidden elements won't ever work
				var invisible =
					element.offsetWidth === 0 && element.offsetHeight === 0;

				if (invisible) {
					var saveDisplay = element.style.display;
					element.style.display = 'block';
				}

				expandChild.style.width = '100000px';
				expandChild.style.height = '100000px';

				expand.scrollLeft = 100000;
				expand.scrollTop = 100000;

				shrink.scrollLeft = 100000;
				shrink.scrollTop = 100000;

				if (invisible) {
					element.style.display = saveDisplay;
				}
			};
			element.modularesizeSensor.resetSensor = reset;

			var onResized = function () {
				rafId = 0;

				if (!dirty) return;

				lastWidth = newWidth;
				lastHeight = newHeight;

				if (element.resizedAttached) {
					element.resizedAttached.call();
				}
			};

			var onScroll = function () {
				var size = getElementSize(element);
				var newWidth = size.width;
				var newHeight = size.height;
				dirty = newWidth != lastWidth || newHeight != lastHeight;

				if (dirty && !rafId) {
					rafId = requestAnimationFrame(onResized);
				}

				reset();
			};

			var addEvent = function (el, name, cb) {
				if (el.attachEvent) {
					el.attachEvent('on' + name, cb);
				} else {
					el.addEventListener(name, cb);
				}
			};

			addEvent(expand, 'scroll', onScroll);
			addEvent(shrink, 'scroll', onScroll);

			// Fix for custom Elements
			requestAnimationFrame(reset);
		}

		forEachElement(element, function (elem) {
			attachResizeEvent(elem, callback);
		});

		this.detach = function (ev) {
			ModulaResizeSensor.detach(element, ev);
		};

		this.reset = function () {
			element.modularesizeSensor.resetSensor();
		};
	};

	ModulaResizeSensor.reset = function (element, ev) {
		forEachElement(element, function (elem) {
			elem.modularesizeSensor.resetSensor();
		});
	};

	ModulaResizeSensor.detach = function (element, ev) {
		forEachElement(element, function (elem) {
			if (!elem) return;
			if (elem.resizedAttached && typeof ev === 'function') {
				elem.resizedAttached.remove(ev);
				if (elem.resizedAttached.length()) return;
			}
			if (elem.modularesizeSensor) {
				if (elem.contains(elem.modularesizeSensor)) {
					elem.removeChild(elem.modularesizeSensor);
				}
				delete elem.modularesizeSensor;
				delete elem.resizedAttached;
			}
		});
	};

	return ModulaResizeSensor;
});
