
// ------------------------------------------
// Rellax.js
// Buttery smooth parallax library
// Copyright (c) 2016 Moe Amaya (@moeamaya)
// MIT license
//
// Thanks to Paraxify.js and Jaime Cabllero
// for parallax concepts
// ------------------------------------------

(function (root, factory) {
	if (typeof define === 'function' && define.amd) {
		// AMD. Register as an anonymous module.
		define([], factory);
	} else if (typeof module === 'object' && module.exports) {
		// Node. Does not work with strict CommonJS, but
		// only CommonJS-like environments that support module.exports,
		// like Node.
		module.exports = factory();
	} else {
		// Browser globals (root is window)
		root.Rellax = factory();
	}
}(typeof window !== "undefined" ? window : global, function () {
	var Rellax = function(el, options){
		"use strict";

		var self = Object.create(Rellax.prototype);

		var posY = 0;
		var screenY = 0;
		var posX = 0;
		var screenX = 0;
		var blocks = [];
		var pause = true;

		// check what requestAnimationFrame to use, and if
		// it's not supported, use the onscroll event
		var loop = window.requestAnimationFrame ||
			window.webkitRequestAnimationFrame ||
			window.mozRequestAnimationFrame ||
			window.msRequestAnimationFrame ||
			window.oRequestAnimationFrame ||
			function(callback){ return setTimeout(callback, 1000 / 60); };

		// store the id for later use
		var loopId = null;

		// Test via a getter in the options object to see if the passive property is accessed
		var supportsPassive = false;
		try {
			var opts = Object.defineProperty({}, 'passive', {
				get: function() {
					supportsPassive = true;
				}
			});
			window.addEventListener("testPassive", null, opts);
			window.removeEventListener("testPassive", null, opts);
		} catch (e) {}

		// check what cancelAnimation method to use
		var clearLoop = window.cancelAnimationFrame || window.mozCancelAnimationFrame || clearTimeout;

		// check which transform property to use
		var transformProp = window.transformProp || (function(){
				var testEl = document.createElement('div');
				if (testEl.style.transform === null) {
					var vendors = ['Webkit', 'Moz', 'ms'];
					for (var vendor in vendors) {
						if (testEl.style[ vendors[vendor] + 'Transform' ] !== undefined) {
							return vendors[vendor] + 'Transform';
						}
					}
				}
				return 'transform';
			})();

		// Default Settings
		self.options = {
			speed: -2,
			verticalSpeed: null,
			horizontalSpeed: null,
			breakpoints: [576, 768, 1201],
			center: false,
			wrapper: null,
			relativeToWrapper: false,
			round: true,
			vertical: true,
			horizontal: false,
			verticalScrollAxis: "y",
			horizontalScrollAxis: "x",
			callback: function() {},
		};

		// User defined options (might have more in the future)
		if (options){
			Object.keys(options).forEach(function(key){
				self.options[key] = options[key];
			});
		}

		function validateCustomBreakpoints () {
			if (self.options.breakpoints.length === 3 && Array.isArray(self.options.breakpoints)) {
				var isAscending = true;
				var isNumerical = true;
				var lastVal;
				self.options.breakpoints.forEach(function (i) {
					if (typeof i !== 'number') isNumerical = false;
					if (lastVal !== null) {
						if (i < lastVal) isAscending = false;
					}
					lastVal = i;
				});
				if (isAscending && isNumerical) return;
			}
			// revert defaults if set incorrectly
			self.options.breakpoints = [576, 768, 1201];
			console.warn("Rellax: You must pass an array of 3 numbers in ascending order to the breakpoints option. Defaults reverted");
		}

		if (options && options.breakpoints) {
			validateCustomBreakpoints();
		}

		// By default, rellax class
		if (!el) {
			el = '.rellax';
		}

		// check if el is a className or a node
		var elements = typeof el === 'string' ? document.querySelectorAll(el) : [el];

		// Now query selector
		if (elements.length > 0) {
			self.elems = elements;
		}

		// The elements don't exist
		else {
			console.warn("Rellax: The elements you're trying to select don't exist.");
			return;
		}

		// Has a wrapper and it exists
		if (self.options.wrapper) {
			if (!self.options.wrapper.nodeType) {
				var wrapper = document.querySelector(self.options.wrapper);

				if (wrapper) {
					self.options.wrapper = wrapper;
				} else {
					console.warn("Rellax: The wrapper you're trying to use doesn't exist.");
					return;
				}
			}
		}

		// set a placeholder for the current breakpoint
		var currentBreakpoint;

		// helper to determine current breakpoint
		var getCurrentBreakpoint = function (w) {
			var bp = self.options.breakpoints;
			if (w < bp[0]) return 'xs';
			if (w >= bp[0] && w < bp[1]) return 'sm';
			if (w >= bp[1] && w < bp[2]) return 'md';
			return 'lg';
		};

		// Get and cache initial position of all elements
		var cacheBlocks = function() {
			for (var i = 0; i < self.elems.length; i++){
				var block = createBlock(self.elems[i]);
				blocks.push(block);
			}
		};


		// Let's kick this script off
		// Build array for cached element values
		var init = function() {
			for (var i = 0; i < blocks.length; i++){
				self.elems[i].style.cssText = blocks[i].style;
			}

			blocks = [];

			screenY = window.innerHeight;
			screenX = window.innerWidth;
			currentBreakpoint = getCurrentBreakpoint(screenX);

			setPosition();

			cacheBlocks();

			animate();

			// If paused, unpause and set listener for window resizing events
			if (pause) {
				window.addEventListener('resize', init);
				pause = false;
				// Start the loop
				update();
			}
		};

		// We want to cache the parallax blocks'
		// values: base, top, height, speed
		// el: is dom object, return: el cache values
		var createBlock = function(el) {
			var dataPercentage = el.getAttribute( 'data-rellax-percentage' );
			var dataSpeed = el.getAttribute( 'data-rellax-speed' );
			var dataXsSpeed = el.getAttribute( 'data-rellax-xs-speed' );
			var dataMobileSpeed = el.getAttribute( 'data-rellax-mobile-speed' );
			var dataTabletSpeed = el.getAttribute( 'data-rellax-tablet-speed' );
			var dataDesktopSpeed = el.getAttribute( 'data-rellax-desktop-speed' );
			var dataVerticalSpeed = el.getAttribute('data-rellax-vertical-speed');
			var dataHorizontalSpeed = el.getAttribute('data-rellax-horizontal-speed');
			var dataVericalScrollAxis = el.getAttribute('data-rellax-vertical-scroll-axis');
			var dataHorizontalScrollAxis = el.getAttribute('data-rellax-horizontal-scroll-axis');
			var dataZindex = el.getAttribute( 'data-rellax-zindex' ) || 0;
			var dataMin = el.getAttribute( 'data-rellax-min' );
			var dataMax = el.getAttribute( 'data-rellax-max' );
			var dataMinX = el.getAttribute('data-rellax-min-x');
			var dataMaxX = el.getAttribute('data-rellax-max-x');
			var dataMinY = el.getAttribute('data-rellax-min-y');
			var dataMaxY = el.getAttribute('data-rellax-max-y');
			var mapBreakpoints;
			var breakpoints = true;

			if (!dataXsSpeed && !dataMobileSpeed && !dataTabletSpeed && !dataDesktopSpeed) {
				breakpoints = false;
			} else {
				mapBreakpoints = {
					'xs': dataXsSpeed,
					'sm': dataMobileSpeed,
					'md': dataTabletSpeed,
					'lg': dataDesktopSpeed
				};
			}

			// initializing at scrollY = 0 (top of browser), scrollX = 0 (left of browser)
			// ensures elements are positioned based on HTML layout.
			//
			// If the element has the percentage attribute, the posY and posX needs to be
			// the current scroll position's value, so that the elements are still positioned based on HTML layout
			var wrapperPosY = self.options.wrapper ? self.options.wrapper.scrollTop : (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
			// If the option relativeToWrapper is true, use the wrappers offset to top, subtracted from the current page scroll.
			if (self.options.relativeToWrapper) {
				var scrollPosY = (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
				wrapperPosY = scrollPosY - self.options.wrapper.offsetTop;
			}
			var posY = self.options.vertical ? ( dataPercentage || self.options.center ? wrapperPosY : 0 ) : 0;
			var posX = self.options.horizontal ? ( dataPercentage || self.options.center ? self.options.wrapper ? self.options.wrapper.scrollLeft : (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft) : 0 ) : 0;

			var blockTop = posY + el.getBoundingClientRect().top;
			var blockHeight = el.clientHeight || el.offsetHeight || el.scrollHeight;

			var blockLeft = posX + el.getBoundingClientRect().left;
			var blockWidth = el.clientWidth || el.offsetWidth || el.scrollWidth;

			// apparently parallax equation everyone uses
			var percentageY = dataPercentage ? dataPercentage : (posY - blockTop + screenY) / (blockHeight + screenY);
			var percentageX = dataPercentage ? dataPercentage : (posX - blockLeft + screenX) / (blockWidth + screenX);
			if(self.options.center){ percentageX = 0.5; percentageY = 0.5; }

			// Optional individual block speed as data attr, otherwise global speed
			var speed = (breakpoints && mapBreakpoints[currentBreakpoint] !== null) ? Number(mapBreakpoints[currentBreakpoint]) : (dataSpeed ? dataSpeed : self.options.speed);
			var verticalSpeed = dataVerticalSpeed ? dataVerticalSpeed : self.options.verticalSpeed;
			var horizontalSpeed = dataHorizontalSpeed ? dataHorizontalSpeed : self.options.horizontalSpeed;

			// Optional individual block movement axis direction as data attr, otherwise global movement direction
			var verticalScrollAxis = dataVericalScrollAxis ? dataVericalScrollAxis : self.options.verticalScrollAxis;
			var horizontalScrollAxis = dataHorizontalScrollAxis ? dataHorizontalScrollAxis : self.options.horizontalScrollAxis;

			var bases = updatePosition(percentageX, percentageY, speed, verticalSpeed, horizontalSpeed);

			// ~~Store non-translate3d transforms~~
			// Store inline styles and extract transforms
			var style = el.style.cssText;
			var transform = '';

			// Check if there's an inline styled transform
			var searchResult = /transform\s*:/i.exec(style);
			if (searchResult) {
				// Get the index of the transform
				var index = searchResult.index;

				// Trim the style to the transform point and get the following semi-colon index
				var trimmedStyle = style.slice(index);
				var delimiter = trimmedStyle.indexOf(';');

				// Remove "transform" string and save the attribute
				if (delimiter) {
					transform = " " + trimmedStyle.slice(11, delimiter).replace(/\s/g,'');
				} else {
					transform = " " + trimmedStyle.slice(11).replace(/\s/g,'');
				}
			}

			return {
				baseX: bases.x,
				baseY: bases.y,
				top: blockTop,
				left: blockLeft,
				height: blockHeight,
				width: blockWidth,
				speed: speed,
				verticalSpeed: verticalSpeed,
				horizontalSpeed: horizontalSpeed,
				verticalScrollAxis: verticalScrollAxis,
				horizontalScrollAxis: horizontalScrollAxis,
				style: style,
				transform: transform,
				zindex: dataZindex,
				min: dataMin,
				max: dataMax,
				minX: dataMinX,
				maxX: dataMaxX,
				minY: dataMinY,
				maxY: dataMaxY
			};
		};

		// set scroll position (posY, posX)
		// side effect method is not ideal, but okay for now
		// returns true if the scroll changed, false if nothing happened
		var setPosition = function() {
			var oldY = posY;
			var oldX = posX;

			posY = self.options.wrapper ? self.options.wrapper.scrollTop : (document.documentElement || document.body.parentNode || document.body).scrollTop || window.pageYOffset;
			posX = self.options.wrapper ? self.options.wrapper.scrollLeft : (document.documentElement || document.body.parentNode || document.body).scrollLeft || window.pageXOffset;
			// If option relativeToWrapper is true, use relative wrapper value instead.
			if (self.options.relativeToWrapper) {
				var scrollPosY = (document.documentElement || document.body.parentNode || document.body).scrollTop || window.pageYOffset;
				posY = scrollPosY - self.options.wrapper.offsetTop;
			}


			if (oldY != posY && self.options.vertical) {
				// scroll changed, return true
				return true;
			}

			if (oldX != posX && self.options.horizontal) {
				// scroll changed, return true
				return true;
			}

			// scroll did not change
			return false;
		};

		// Ahh a pure function, gets new transform value
		// based on scrollPosition and speed
		// Allow for decimal pixel values
		var updatePosition = function(percentageX, percentageY, speed, verticalSpeed, horizontalSpeed) {
			var result = {};
			var valueX = ((horizontalSpeed ? horizontalSpeed : speed) * (100 * (1 - percentageX)));
			var valueY = ((verticalSpeed ? verticalSpeed : speed) * (100 * (1 - percentageY)));

			result.x = self.options.round ? Math.round(valueX) : Math.round(valueX * 100) / 100;
			result.y = self.options.round ? Math.round(valueY) : Math.round(valueY * 100) / 100;

			return result;
		};

		// Remove event listeners and loop again
		var deferredUpdate = function() {
			window.removeEventListener('resize', deferredUpdate);
			window.removeEventListener('orientationchange', deferredUpdate);
			(self.options.wrapper ? self.options.wrapper : window).removeEventListener('scroll', deferredUpdate);
			(self.options.wrapper ? self.options.wrapper : document).removeEventListener('touchmove', deferredUpdate);

			// loop again
			loopId = loop(update);
		};

		// Loop
		var update = function() {
			if (setPosition() && pause === false) {
				animate();

				// loop again
				loopId = loop(update);
			} else {
				loopId = null;

				// Don't animate until we get a position updating event
				window.addEventListener('resize', deferredUpdate);
				window.addEventListener('orientationchange', deferredUpdate);
				(self.options.wrapper ? self.options.wrapper : window).addEventListener('scroll', deferredUpdate, supportsPassive ? { passive: true } : false);
				(self.options.wrapper ? self.options.wrapper : document).addEventListener('touchmove', deferredUpdate, supportsPassive ? { passive: true } : false);
			}
		};

		// Transform3d on parallax element
		var animate = function() {
			var positions;
			for (var i = 0; i < self.elems.length; i++){
				// Determine relevant movement directions
				var verticalScrollAxis = blocks[i].verticalScrollAxis.toLowerCase();
				var horizontalScrollAxis = blocks[i].horizontalScrollAxis.toLowerCase();
				var verticalScrollX = verticalScrollAxis.indexOf("x") != -1 ? posY : 0;
				var verticalScrollY = verticalScrollAxis.indexOf("y") != -1 ? posY : 0;
				var horizontalScrollX = horizontalScrollAxis.indexOf("x") != -1 ? posX : 0;
				var horizontalScrollY = horizontalScrollAxis.indexOf("y") != -1 ? posX : 0;

				var percentageY = ((verticalScrollY + horizontalScrollY - blocks[i].top + screenY) / (blocks[i].height + screenY));
				var percentageX = ((verticalScrollX + horizontalScrollX - blocks[i].left + screenX) / (blocks[i].width + screenX));

				// Subtracting initialize value, so element stays in same spot as HTML
				positions = updatePosition(percentageX, percentageY, blocks[i].speed, blocks[i].verticalSpeed, blocks[i].horizontalSpeed);
				var positionY = positions.y - blocks[i].baseY;
				var positionX = positions.x - blocks[i].baseX;

				// The next two "if" blocks go like this:
				// Check if a limit is defined (first "min", then "max");
				// Check if we need to change the Y or the X
				// (Currently working only if just one of the axes is enabled)
				// Then, check if the new position is inside the allowed limit
				// If so, use new position. If not, set position to limit.

				// Check if a min limit is defined
				if (blocks[i].min !== null) {
					if (self.options.vertical && !self.options.horizontal) {
						positionY = positionY <= blocks[i].min ? blocks[i].min : positionY;
					}
					if (self.options.horizontal && !self.options.vertical) {
						positionX = positionX <= blocks[i].min ? blocks[i].min : positionX;
					}
				}

				// Check if directional min limits are defined
				if (blocks[i].minY != null) {
						positionY = positionY <= blocks[i].minY ? blocks[i].minY : positionY;
				}
				if (blocks[i].minX != null) {
						positionX = positionX <= blocks[i].minX ? blocks[i].minX : positionX;
				}

				// Check if a max limit is defined
				if (blocks[i].max !== null) {
					if (self.options.vertical && !self.options.horizontal) {
						positionY = positionY >= blocks[i].max ? blocks[i].max : positionY;
					}
					if (self.options.horizontal && !self.options.vertical) {
						positionX = positionX >= blocks[i].max ? blocks[i].max : positionX;
					}
				}

				// Check if directional max limits are defined
				if (blocks[i].maxY != null) {
						positionY = positionY >= blocks[i].maxY ? blocks[i].maxY : positionY;
				}
				if (blocks[i].maxX != null) {
						positionX = positionX >= blocks[i].maxX ? blocks[i].maxX : positionX;
				}

				var zindex = blocks[i].zindex;

				// Move that element
				// (Set the new translation and append initial inline transforms.)
				var translate = 'translate3d(' + (self.options.horizontal ? positionX : '0') + 'px,' + (self.options.vertical ? positionY : '0') + 'px,' + zindex + 'px) ' + blocks[i].transform;
				self.elems[i].style[transformProp] = translate;
			}
			self.options.callback(positions);
		};

		self.destroy = function() {
			for (var i = 0; i < self.elems.length; i++){
				self.elems[i].style.cssText = blocks[i].style;
			}

			// Remove resize event listener if not pause, and pause
			if (!pause) {
				window.removeEventListener('resize', init);
				pause = true;
			}

			// Clear the animation loop to prevent possible memory leak
			clearLoop(loopId);
			loopId = null;
		};

		// Init
		init();

		// Allow to recalculate the initial values whenever we want
		self.refresh = init;

		return self;
	};
	return Rellax;
}));
