(function (window, factory) {
	if (typeof module == 'object' && module.exports) {
		module.exports = lazySizes;
	} else {
		window.lazySizes = factory(window, window.document);
	}
}(window, function l (window, document) {
	'use strict';

	/*jshint eqnull:true */
	if (!window.IntersectionObserver || !document.getElementsByClassName || !window.MutationObserver) {return;}

	var lazysizes, lazySizesCfg;

	var docElem = document.documentElement;

	var Date = window.Date;

	var supportPicture = window.HTMLPictureElement;

	var _addEventListener = 'addEventListener';

	var _getAttribute = 'getAttribute';

	var addEventListener = window[ _addEventListener ];

	var setTimeout = window.setTimeout;

	var requestAnimationFrame = window.requestAnimationFrame || setTimeout;

	var requestIdleCallback = window.requestIdleCallback || setTimeout;

	var regPicture = /^picture$/i;

	var loadEvents = [ 'load', 'error', 'lazyincluded', '_lazyloaded' ];

	var forEach = Array.prototype.forEach;

	var hasClass = function (ele, cls) {
		return ele.classList.contains(cls);
	};

	var addClass = function (ele, cls) {
		ele.classList.add(cls);
	};

	var removeClass = function (ele, cls) {
		ele.classList.remove(cls);
	};

	var addRemoveLoadEvents = function (dom, fn, add) {
		var action = add ? _addEventListener : 'removeEventListener';
		if (add) {
			addRemoveLoadEvents(dom, fn);
		}
		loadEvents.forEach(function (evt) {
			dom[ action ](evt, fn);
		});
	};

	var triggerEvent = function (elem, name, detail, noBubbles, noCancelable) {
		var event = document.createEvent('CustomEvent');

		if (!detail) {
			detail = {};
		}

		detail.instance = lazysizes;

		event.initCustomEvent(name, !noBubbles, !noCancelable, detail);

		elem.dispatchEvent(event);
		return event;
	};

	var updatePolyfill = function (el, full) {
		var polyfill;
		if (!supportPicture && (polyfill = (window.picturefill || lazySizesCfg.pf))) {
			polyfill({ reevaluate: true, elements: [ el ] });
		} else if (full && full.src) {
			el.src = full.src;
		}
	};

	var getWidth = function (elem, parent, width) {
		width = width || elem.offsetWidth;

		while (width < lazySizesCfg.minSize && parent && !elem._lazysizesWidth) {
			width = parent.offsetWidth;
			parent = parent.parentNode;
		}

		return width;
	};

	var rAF = (function () {
		var running, waiting;
		var fns = [];

		var run = function () {
			var fn;
			running = true;
			waiting = false;
			while (fns.length) {
				fn = fns.shift();
				fn[ 0 ].apply(fn[ 1 ], fn[ 2 ]);
			}
			running = false;
		};

		return function (fn) {
			if (running) {
				fn.apply(this, arguments);
			} else {
				fns.push([ fn, this, arguments ]);

				if (!waiting) {
					waiting = true;
					(document.hidden ? setTimeout : requestAnimationFrame)(run);
				}
			}
		};
	})();

	var rAFIt = function (fn, simple) {
		return simple ?
			function () {
				rAF(fn);
			} :
			function () {
				var that = this;
				var args = arguments;
				rAF(function () {
					fn.apply(that, args);
				});
			}
			;
	};

	//based on http://modernjavascript.blogspot.de/2013/08/building-better-debounce.html
	var debounce = function (func) {
		var timeout, timestamp;
		var wait = 99;
		var run = function () {
			timeout = null;
			func();
		};
		var later = function () {
			var last = Date.now() - timestamp;

			if (last < wait) {
				setTimeout(later, wait - last);
			} else {
				(requestIdleCallback || run)(run);
			}
		};

		return function () {
			timestamp = Date.now();

			if (!timeout) {
				timeout = setTimeout(later, wait);
			}
		};
	};

	var loader = (function () {
		var inviewObserver, preloadObserver;

		var lazyloadElems, isCompleted, resetPreloadingTimer, started;

		var regImg = /^img$/i;
		var regIframe = /^iframe$/i;

		var supportScroll = ('onscroll' in window) && !(/glebot/.test(navigator.userAgent));

		var isLoading = 0;
		var isPreloadLoading = 0;

		var resetPreloading = function (e) {
			isLoading--;

			if (isPreloadLoading) {
				isPreloadLoading--;
			}

			if (e && e.target) {
				addRemoveLoadEvents(e.target, resetPreloading);
			}

			if (!e || isLoading < 0 || !e.target) {
				isLoading = 0;
				isPreloadLoading = 0;
			}

			if (lazyQuedElements.length && (isLoading - isPreloadLoading) < 1 && isLoading < 3) {
				setTimeout(function () {
					while (lazyQuedElements.length && (isLoading - isPreloadLoading) < 1 && isLoading < 4) {
						lazyUnveilElement({ target: lazyQuedElements.shift() });
					}
				});
			}
		};

		var switchLoadingClass = function (e) {
			addClass(e.target, lazySizesCfg.loadedClass);
			removeClass(e.target, lazySizesCfg.loadingClass);
			addRemoveLoadEvents(e.target, rafSwitchLoadingClass);
		};
		var rafedSwitchLoadingClass = rAFIt(switchLoadingClass);
		var rafSwitchLoadingClass = function (e) {
			rafedSwitchLoadingClass({ target: e.target });
		};

		var changeIframeSrc = function (elem, src) {
			try {
				elem.contentWindow.location.replace(src);
			} catch (e) {
				elem.src = src;
			}
		};

		var handleSources = function (source) {
			var customMedia;

			var sourceSrcset = source[ _getAttribute ](lazySizesCfg.srcsetAttr);

			if ((customMedia = lazySizesCfg.customMedia[ source[ _getAttribute ]('data-media') || source[ _getAttribute ]('media') ])) {
				source.setAttribute('media', customMedia);
			}

			if (sourceSrcset) {
				source.setAttribute('srcset', sourceSrcset);
			}
		};

		var lazyUnveil = rAFIt(function (elem, detail, isAuto, sizes, isImg) {
			var src, srcset, parent, isPicture, event, firesLoad;

			if (!(event = triggerEvent(elem, 'lazybeforeunveil', detail)).defaultPrevented) {

				if (sizes) {
					if (isAuto) {
						addClass(elem, lazySizesCfg.autosizesClass);
					} else {
						elem.setAttribute('sizes', sizes);
					}
				}

				srcset = elem[ _getAttribute ](lazySizesCfg.srcsetAttr);
				src = elem[ _getAttribute ](lazySizesCfg.srcAttr);

				if (isImg) {
					parent = elem.parentNode;
					isPicture = parent && regPicture.test(parent.nodeName || '');
				}

				firesLoad = detail.firesLoad || (('src' in elem) && (srcset || src || isPicture));

				event = { target: elem };

				if (firesLoad) {
					addRemoveLoadEvents(elem, resetPreloading, true);
					clearTimeout(resetPreloadingTimer);
					resetPreloadingTimer = setTimeout(resetPreloading, 2500);

					addClass(elem, lazySizesCfg.loadingClass);
					addRemoveLoadEvents(elem, rafSwitchLoadingClass, true);
				}

				if (isPicture) {
					forEach.call(parent.getElementsByTagName('source'), handleSources);
				}

				if (srcset) {
					elem.setAttribute('srcset', srcset);
				} else if (src && !isPicture) {
					if (regIframe.test(elem.nodeName)) {
						changeIframeSrc(elem, src);
					} else {
						elem.src = src;
					}
				}

				if (srcset || isPicture) {
					updatePolyfill(elem, { src: src });
				}
			}

			rAF(function () {
				if (elem._lazyRace) {
					delete elem._lazyRace;
				}
				removeClass(elem, lazySizesCfg.lazyWaitClass);

				if (!firesLoad || elem.complete) {
					if (firesLoad) {
						resetPreloading(event);
					} else {
						isLoading--;
					}
					switchLoadingClass(event);
				}
			});
		});

		var unveilElement = function (elem) {
			var detail, index;
			var isImg = regImg.test(elem.nodeName);

			//allow using sizes="auto", but don't use. it's invalid. Use data-sizes="auto" or a valid value for sizes instead (i.e.: sizes="80vw")
			var sizes = isImg && (elem[ _getAttribute ](lazySizesCfg.sizesAttr) || elem[ _getAttribute ]('sizes'));
			var isAuto = sizes == 'auto';

			if ((isAuto || !isCompleted) && isImg && (elem.src || elem.srcset) && !elem.complete && !hasClass(elem, lazySizesCfg.errorClass)) {return;}

			detail = triggerEvent(elem, 'lazyunveilread').detail;

			if (isAuto) {
				autoSizer.updateElem(elem, true, elem.offsetWidth);
			}

			isLoading++;

			if ((index = lazyQuedElements.indexOf(elem)) != -1) {
				lazyQuedElements.splice(index, 1);
			}

			inviewObserver.unobserve(elem);

			lazyUnveil(elem, detail, isAuto, sizes, isImg);
		};
		var unveilElements = function (change) {
			var i, len;
			for (i = 0, len = change.length; i < len; i++) {
				if (change[ i ].isIntersecting === false) {
					continue;
				}
				unveilElement(change[ i ].target);
			}
		};

		var lazyQuedElements = [];

		var lazyUnveilElement = function (change) {
			var index, i, len, element;

			for (i = 0, len = change.length; i < len; i++) {
				element = change[ i ].target;
				if ((isLoading - isPreloadLoading) < 1 && isLoading < 4) {
					isPreloadLoading++;
					unveilElement(element);
				} else if ((index = lazyQuedElements.indexOf(element)) == -1) {
					lazyQuedElements.push(element);
				} else {
					lazyQuedElements.splice(index, 1);
				}
			}
		};

		var removeLazyClassElements = [];

		var removeLazyClass = rAFIt(function () {
			var element;

			while (removeLazyClassElements.length) {
				element = removeLazyClassElements.shift();
				addClass(element, lazySizesCfg.lazyWaitClass);
				removeClass(element, lazySizesCfg.lazyClass);

				if (element._lazyAdd) {
					delete element._lazyAdd;
				}
			}
		}, true);

		var addElements = function () {
			var i, len, runLazyRemove;
			for (i = 0, len = lazyloadElems.length; i < len; i++) {
				if (!lazyloadElems[ i ]._lazyAdd) {
					lazyloadElems[ i ]._lazyAdd = true;

					inviewObserver.observe(lazyloadElems[ i ]);

					removeLazyClassElements.push(lazyloadElems[ i ]);
					runLazyRemove = true;

					if (!supportScroll) {
						unveilElement(lazyloadElems[ i ]);
					}
				}
			}

			if (runLazyRemove) {
				removeLazyClass();
			}
		};

		return {
			_: function () {
				started = Date.now();

				lazyloadElems = document.getElementsByClassName(lazySizesCfg.lazyClass);

				inviewObserver = new IntersectionObserver(unveilElements);

				new MutationObserver(addElements).observe(docElem, { childList: true, subtree: true, attributes: true });

				addElements();
			},
			unveil: unveilElement
		};
	})();

	var autoSizer = (function () {
		var autosizesElems;

		var sizeElement = rAFIt(function (elem, parent, event, width) {
			var sources, i, len;
			elem._lazysizesWidth = width;
			width += 'px';

			elem.setAttribute('sizes', width);

			if (regPicture.test(parent.nodeName || '')) {
				sources = parent.getElementsByTagName('source');
				for (i = 0, len = sources.length; i < len; i++) {
					sources[ i ].setAttribute('sizes', width);
				}
			}

			if (!event.detail.dataAttr) {
				updatePolyfill(elem, event.detail);
			}
		});
		var getSizeElement = function (elem, dataAttr, width) {
			var event;
			var parent = elem.parentNode;

			if (parent) {
				width = getWidth(elem, parent, width);
				event = triggerEvent(elem, 'lazybeforesizes', { width: width, dataAttr: !!dataAttr });

				if (!event.defaultPrevented) {
					width = event.detail.width;

					if (width && width !== elem._lazysizesWidth) {
						sizeElement(elem, parent, event, width);
					}
				}
			}
		};

		var updateElementsSizes = function () {
			var i;
			var len = autosizesElems.length;
			if (len) {
				i = 0;

				for (; i < len; i++) {
					getSizeElement(autosizesElems[ i ]);
				}
			}
		};

		var debouncedUpdateElementsSizes = debounce(updateElementsSizes);

		return {
			_: function () {
				autosizesElems = document.getElementsByClassName(lazySizesCfg.autosizesClass);
				addEventListener('resize', debouncedUpdateElementsSizes);
			},
			checkElems: debouncedUpdateElementsSizes,
			updateElem: getSizeElement
		};
	})();

	var init = function () {
		if (!init.i) {
			init.i = true;
			autoSizer._();
			loader._();
		}
	};

	(function () {
		var prop;

		var lazySizesDefaults = {
			lazyClass: 'lazyload',
			lazyWaitClass: 'lazyloadwait',
			loadedClass: 'lazyloaded',
			loadingClass: 'lazyloading',
			preloadClass: 'lazypreload',
			errorClass: 'lazyerror',
			//strictClass: 'lazystrict',
			autosizesClass: 'lazyautosizes',
			srcAttr: 'data-src',
			srcsetAttr: 'data-srcset',
			sizesAttr: 'data-sizes',
			minSize: 40,
			customMedia: {},
			init: true,
			hFac: 0.8,
			loadMode: 1,
			expand: 0,
		};

		lazySizesCfg = window.lazySizesConfig || window.lazysizesConfig || {};

		for (prop in lazySizesDefaults) {
			if (!(prop in lazySizesCfg)) {
				lazySizesCfg[ prop ] = lazySizesDefaults[ prop ];
			}
		}

		setTimeout(function () {
			if (lazySizesCfg.init) {
				init();
			}
		});
	})();

	lazysizes = {
		cfg: lazySizesCfg,
		autoSizer: autoSizer,
		loader: loader,
		init: init,
		uP: updatePolyfill,
		aC: addClass,
		rC: removeClass,
		hC: hasClass,
		fire: triggerEvent,
		gW: getWidth,
		rAF: rAF,
		rAFIt: rAFIt,
		debounce: debounce,
	};

	return lazysizes;
}));
