'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.ResizeSensor = 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;
	}
	// https://github.com/Semantic-Org/Semantic-UI/issues/3855
	// https://github.com/marcj/css-element-queries/issues/257
	var globalWindow = typeof window != 'undefined' && window.Math == Math
		? window
		: typeof self != 'undefined' && self.Math == Math
			? self
			: Function( 'return this' )();
	// 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 = globalWindow.requestAnimationFrame ||
		globalWindow.mozRequestAnimationFrame ||
		globalWindow.webkitRequestAnimationFrame ||
		function (fn) {
			return globalWindow.setTimeout( fn, 20 );
		};

	var cancelAnimationFrame = globalWindow.cancelAnimationFrame ||
		globalWindow.mozCancelAnimationFrame ||
		globalWindow.webkitCancelAnimationFrame ||
		function (timer) {
			globalWindow.clearTimeout( timer );
		};

	/**
	 * 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 )
		}
	}

	/**
	 * Apply CSS styles to element.
	 *
	 * @param {HTMLElement} element
	 * @param {Object} style
	 */
	function setStyle(element, style) {
		Object.keys( style ).forEach(
			function(key) {
				element.style[key] = style[key];
			}
		);
	}

	/**
	 * Class for dimension change detection.
	 *
	 * @param {Element|Element[]|Elements|jQuery} element
	 * @param {Function} callback
	 *
	 * @constructor
	 */
	var ResizeSensor = function(element, callback) {
		// Is used when checking in reset() only for invisible elements
		var lastAnimationFrameForInvisibleCheck = 0;

		/**
		 *
		 * @constructor
		 */
		function EventQueue() {
			var q    = [];
			this.add = function(ev) {
				q.push( ev );
			};

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

			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.resizeSensor           = document.createElement( 'div' );
			element.resizeSensor.dir       = 'ltr';
			element.resizeSensor.className = 'resize-sensor';

			var style      = {
				pointerEvents: 'none',
				position: 'absolute',
				left: '0px',
				top: '0px',
				right: '0px',
				bottom: '0px',
				overflow: 'hidden',
				zIndex: '-1',
				visibility: 'hidden',
				maxWidth: '100%'
			};
			var styleChild = {
				position: 'absolute',
				left: '0px',
				top: '0px',
				transition: '0s',
			};

			setStyle( element.resizeSensor, style );

			var expand       = document.createElement( 'div' );
			expand.className = 'resize-sensor-expand';
			setStyle( expand, style );

			var expandChild = document.createElement( 'div' );
			setStyle( expandChild, styleChild );
			expand.appendChild( expandChild );

			var shrink       = document.createElement( 'div' );
			shrink.className = 'resize-sensor-shrink';
			setStyle( shrink, style );

			var shrinkChild = document.createElement( 'div' );
			setStyle( shrinkChild, styleChild );
			setStyle( shrinkChild, { width: '200%', height: '200%' } );
			shrink.appendChild( shrinkChild );

			element.resizeSensor.appendChild( expand );
			element.resizeSensor.appendChild( shrink );
			element.appendChild( element.resizeSensor );

			var computedStyle = window.getComputedStyle( element );
			var position      = computedStyle ? computedStyle.getPropertyValue( 'position' ) : null;
			if ('absolute' !== position && 'relative' !== position && 'fixed' !== position && 'sticky' !== position) {
				element.style.position = 'relative';
			}

			var dirty = false;

			// last request animation frame id used in onscroll event
			var rafId                           = 0;
			var size                            = getElementSize( element );
			var lastWidth                       = 0;
			var lastHeight                      = 0;
			var initialHiddenCheck              = true;
			lastAnimationFrameForInvisibleCheck = 0;

			var resetExpandShrink = function () {
				var width  = element.offsetWidth;
				var height = element.offsetHeight;

				expandChild.style.width  = (width + 10) + 'px';
				expandChild.style.height = (height + 10) + 'px';

				expand.scrollLeft = width + 10;
				expand.scrollTop  = height + 10;

				shrink.scrollLeft = width + 10;
				shrink.scrollTop  = height + 10;
			};

			var reset                        = function() {
				// Check if element is hidden
				if (initialHiddenCheck) {
					var invisible = element.offsetWidth === 0 && element.offsetHeight === 0;
					if (invisible) {
						// Check in next frame
						if ( ! lastAnimationFrameForInvisibleCheck) {
							lastAnimationFrameForInvisibleCheck = requestAnimationFrame(
								function(){
									lastAnimationFrameForInvisibleCheck = 0;
									reset();
								}
							);
						}

						return;
					} else {
						// Stop checking
						initialHiddenCheck = false;
					}
				}

				resetExpandShrink();
			};
			element.resizeSensor.resetSensor = reset;

			var onResized = function() {
				rafId = 0;

				if ( ! dirty) {
					return;
				}

				lastWidth  = size.width;
				lastHeight = size.height;

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

			var onScroll = function() {
				size  = getElementSize( element );
				dirty = size.width !== lastWidth || size.height !== 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 and invisible elements
			lastAnimationFrameForInvisibleCheck = requestAnimationFrame(
				function(){
					lastAnimationFrameForInvisibleCheck = 0;
					reset();
				}
			);
		}

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

		this.detach = function(ev) {
			// clean up the unfinished animation frame to prevent a potential endless requestAnimationFrame of reset
			if (lastAnimationFrameForInvisibleCheck) {
				cancelAnimationFrame( lastAnimationFrameForInvisibleCheck );
				lastAnimationFrameForInvisibleCheck = 0;
			}
			ResizeSensor.detach( element, ev );
		};

		this.reset = function() {
			// To prevent invoking element.resizeSensor.resetSensor if it's undefined
			if (element.resizeSensor.resetSensor) {
				element.resizeSensor.resetSensor();
			}
		};
	};

	ResizeSensor.reset = function(element) {
		forEachElement(
			element,
			function(elem){
				// To prevent invoking element.resizeSensor.resetSensor if it's undefined
				if (element.resizeSensor.resetSensor) {
					elem.resizeSensor.resetSensor();
				}
			}
		);
	};

	ResizeSensor.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.resizeSensor) {
					if (elem.contains( elem.resizeSensor )) {
						elem.removeChild( elem.resizeSensor );
					}
					delete elem.resizeSensor;
					delete elem.resizedAttached;
				}
			}
		);
	};

	if (typeof MutationObserver !== "undefined") {
		var observer = new MutationObserver(
			function (mutations) {
				for (var i in mutations) {
					if (mutations.hasOwnProperty( i )) {
						var items = mutations[i].addedNodes;
						for (var j = 0; j < items.length; j++) {
							if (items[j].resizeSensor) {
								ResizeSensor.reset( items[j] );
							}
						}
					}
				}
			}
		);

		document.addEventListener(
			"DOMContentLoaded",
			function (event) {
				observer.observe(
					document.body,
					{
						childList: true,
						subtree: true,
					}
				);
			}
		);
	}

	return ResizeSensor;

}));
