/**!
 * PJAX- Standalone
 *
 * A standalone implementation of Pushstate AJAX, for non-jQuery web pages.
 * jQuery are recommended to use the original implementation at: http://github.com/defunkt/jquery-pjax
 * 
 * @version 0.6.1
 * @author Carl
 * @source https://github.com/thybag/PJAX-Standalone
 * @license MIT
 */
(function(){ 

	// Object to store private values/methods.
	var internal = {
		// Is this the first usage of PJAX? (Ensure history entry has required values if so.)
		"firstrun": true,
		// Borrowed wholesale from https://github.com/defunkt/jquery-pjax
		// Attempt to check that a device supports pushstate before attempting to use it.
		"is_supported": window.history && window.history.pushState && window.history.replaceState && !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/),
		// Track which scripts have been included in to the page. (used if e)
		"loaded_scripts": []
	};
	
	// If PJAX isn't supported we can skip setting up the library all together
	// So as not to break any code expecting PJAX to be there, return a shell object containing
	// IE7 + compatible versions of connect (which needs to do nothing) and invoke ( which just changes the page)
	if(!internal.is_supported) {
		// PJAX shell, so any code expecting PJAX will work
		var pjax_shell = {
			"connect": function() { return; },
			"invoke": function() {
				var url = (arguments.length === 2) ? arguments[0] : arguments.url;
				document.location = url;
				return;	
			} 
		};
		// AMD support
		if (typeof define === 'function' && define.amd) { 
			define( function() { return pjax_shell; }); 
		} else { 
			window.pjax = pjax_shell; 
		}
		return;
	}

	/**
	 * AddEvent
	 *
	 * @scope private
	 * @param obj Object to listen on
	 * @param event Event to listen for.
	 * @param callback Method to run when event is detected.
	 */
	internal.addEvent = function(obj, event, callback) {
		obj.addEventListener(event, callback, false);
	};

	/**
	 * Clone
	 * Util method to create copies of the options object (so they do not share references)
	 * This allows custom settings on different links.
	 *
	 * @scope private
	 * @param obj
	 * @return obj
	 */
	internal.clone = function(obj) {
		var object = {};
		// For every option in object, create it in the duplicate.
		for (var i in obj) {
			object[i] = obj[i];
		}
		return object;
	};

	/**
	 * triggerEvent
	 * Fire an event on a given object (used for callbacks)
	 *
	 * @scope private
	 * @param node. Objects to fire event on
	 * @return event_name. type of event
	 */
	internal.triggerEvent = function(node, event_name, data) {
		// Good browsers
		var evt = document.createEvent("HTMLEvents");
		evt.initEvent(event_name, true, true);
		// If additional data was provided, add it to event
		if(typeof data !== 'undefined') evt.data = data;
		node.dispatchEvent(evt);
	};

	/**
	 * popstate listener
	 * Listens for back/forward button events and updates page accordingly.
	 */
	internal.addEvent(window, 'popstate', function(st) {
		if(st.state !== null) {

			var opt = {	
				'url': st.state.url, 
				'container': st.state.container, 
				'title' : st.state.title,
				'history': false
			};

			// Merge original in original connect options
			if(typeof internal.options !== 'undefined'){
				for(var a in internal.options){ 
					if(typeof opt[a] === 'undefined') opt[a] = internal.options[a];
				}
			}

			// Convert state data to PJAX options
			var options = internal.parseOptions(opt);
			// If something went wrong, return.
			if(options === false) return;
			// If there is a state object, handle it as a page load.
			internal.handle(options);
		}
	});

	/**
	 * attach
	 * Attach PJAX listeners to a link.
	 * @scope private
	 * @param link_node. link that will be clicked.
	 * @param content_node. 
	 */
	internal.attach = function(node, options) {

		// Ignore external links.
		if ( node.protocol !== document.location.protocol ||
			node.host !== document.location.host ) {
			return;
		}

		// Ignore anchors on the same page
		if(node.pathname === location.pathname && node.hash.length > 0) {
			return;
		}

		// Ignore common non-PJAX loadable media types (pdf/doc/zips & images) unless user provides alternate array
		var ignoreFileTypes = ['pdf','doc','docx','zip','rar','7z','gif','jpeg','jpg','png'];
		if(typeof options.ignoreFileTypes === 'undefined') options.ignoreFileTypes = ignoreFileTypes;
		// Skip link if file type is within ignored types array
		if(options.ignoreFileTypes.indexOf( node.pathname.split('.').pop().toLowerCase() ) !== -1){
			return;
		}

		// Add link HREF to object
		options.url = node.href;

		// If PJAX data is specified, use as container
		if(node.getAttribute('data-pjax')) {
			options.container = node.getAttribute('data-pjax');
		}

		// If data-title is specified, use as title.
		if(node.getAttribute('data-title')) {
			options.title = node.getAttribute('data-title');
		}

		// Check options are valid.
		options = internal.parseOptions(options);
		if(options === false) return;

		// Attach event.
		internal.addEvent(node, 'click', function(event) {
			// Allow middle click (pages in new windows)
			if ( event.which > 1 || event.metaKey || event.ctrlKey ) return;
			// Don't fire normal event
			if(event.preventDefault){ event.preventDefault(); }else{ event.returnValue = false; }
			// Take no action if we are already on said page?
			if(document.location.href === options.url) return false;
			// handle the load.
			internal.handle(options);
		});
	};

	/**
	 * parseLinks
	 * Parse all links within a DOM node, using settings provided in options.
	 * @scope private
	 * @param dom_obj. Dom node to parse for links.
	 * @param options. Valid Options object.
	 */
	internal.parseLinks = function(dom_obj, options) {

		var nodes;

		if(typeof options.useClass !== 'undefined'){
			// Get all nodes with the provided class name.
			nodes = dom_obj.getElementsByClassName(options.useClass);
		}else{
			// If no class was provided, just get all the links
			nodes = dom_obj.getElementsByTagName('a');
		}

		// For all returned nodes
		for(var i=0,tmp_opt; i < nodes.length; i++) {
			var node = nodes[i];
			if(typeof options.excludeClass !== 'undefined') {
				if(node.className.indexOf(options.excludeClass) !== -1) continue;
			}
			// Override options history to true, else link parsing could be triggered by back button (which runs in no-history mode)
			tmp_opt = internal.clone(options);
			tmp_opt.history = true;
			internal.attach(node, tmp_opt);
		}

		if(internal.firstrun) {
			// Store array or all currently included script src's to avoid PJAX accidentally reloading existing libraries
			var scripts = document.getElementsByTagName('script');
			for(var c=0; c < scripts.length; c++) {
				if(scripts[c].src && internal.loaded_scripts.indexOf(scripts[c].src) === -1){
					internal.loaded_scripts.push(scripts[c].src);
				}
			}

			// Fire ready event once all links are connected
			internal.triggerEvent(internal.get_container_node(options.container), 'ready');
			
		}
	};

	/**
	 * SmartLoad
	 * Smartload checks the returned HTML to ensure PJAX ready content has been provided rather than
	 * a full HTML page. If a full HTML has been returned, it will attempt to scan the page and extract
	 * the correct HTML to update our container with in order to ensure PJAX still functions as expected.
	 *
	 * @scope private
	 * @param HTML (HTML returned from AJAX)
	 * @param options (Options object used to request page)
	 * @return HTML to append to our page.
	 */
	internal.smartLoad = function(html, options) {
		// Grab the title if there is one
		var title = html.getElementsByTagName('title')[0];
		if(title)
			document.title = title.innerHTML;

		// Going by caniuse all browsers that support the pushstate API also support querySelector's
		// see: http://caniuse.com/#search=push
		// see: http://caniuse.com/#search=querySelector
		var container = html.querySelector("#" + options.container.id);
		if(container !== null) return container;

		// If our container was not found, HTML will be returned as is.
		return html;
	};

	/**
	 * Update Content
	 * Updates DOM with content loaded via PJAX
	 *
	 * @param html DOM fragment of loaded container
	 * @param options PJAX configuration options
	 * return options
	 */
	internal.updateContent = function(html, options){
		// Create in memory DOM node, to make parsing returned data easier
		var tmp = document.createElement('div');
		tmp.innerHTML = html; 

		// Ensure we have the correct HTML to apply to our container.
		if(options.smartLoad) tmp = internal.smartLoad(tmp, options);

		// If no title was provided, extract it
		if(typeof options.title === 'undefined'){
			// Use current doc title (this will be updated via smart load if its enabled)
			options.title = document.title;

			// Attempt to grab title from non-smart loaded page contents 
			if(!options.smartLoad){
				var tmpTitle = tmp.getElementsByTagName('title');
				if(tmpTitle.length !== 0) options.title = tmpTitle[0].innerHTML;
			}
		}

		// Update the DOM with the new content
		options.container.innerHTML = tmp.innerHTML;

		// Run included JS?
		if(options.parseJS) internal.runScripts(tmp);
		
		// Send data back to handle
		return options;
	};

	/**
	 * runScripts
	 * Execute JavaScript on pages loaded via PJAX
	 *
	 * Note: In-line JavaScript is run each time a page is hit, while external JavaScript
	 *		is only loaded once (Although remains loaded while the user continues browsing)
	 *
	 * @param html DOM fragment of loaded container
	 * return void
	 */
	internal.runScripts = function(html){
		// Extract JavaScript & eval it (if enabled)
		var scripts = html.getElementsByTagName('script');
		for(var sc=0; sc < scripts.length;sc++) {
			// If has an src & src isn't in "loaded_scripts", load the script.
			if(scripts[sc].src && internal.loaded_scripts.indexOf(scripts[sc].src) === -1){
				// Append to head to include
				var s = document.createElement("script"); 
				s.src = scripts[sc].src;
				document.head.appendChild(s);
				// Add to loaded list
				internal.loaded_scripts.push(scripts[sc].src);
			}else{
				// If raw JS, eval it. 
				eval(scripts[sc].innerHTML);
			}
		}
	};

	/**
	 * handle
	 * Handle requests to load content via PJAX.
	 * @scope private
	 * @param url. Page to load.
	 * @param node. Dom node to add returned content in to.
	 * @param addtohistory. Does this load require a history event.
	 */
	internal.handle = function(options) {
		
		// Fire beforeSend Event.
		internal.triggerEvent(options.container, 'beforeSend', options);

		// Do the request
		internal.request(options.url, function(html) {

			// Fail if unable to load HTML via AJAX
			if(html === false){
				internal.triggerEvent(options.container,'complete', options);
				internal.triggerEvent(options.container,'error', options);
				return;
			}

			// Parse page & update DOM
			options = internal.updateContent(html, options);
			
			// Do we need to add this to the history?
			if(options.history) {
				// If this is the first time pjax has run, create a state object for the current page.
				if(internal.firstrun){
					window.history.replaceState({'url': document.location.href, 'container':  options.container.id, 'title': document.title}, document.title);
					internal.firstrun = false;
				}
				// Update browser history
				window.history.pushState({'url': options.url, 'container': options.container.id, 'title': options.title }, options.title , options.url);
			}

			// Initialize any new links found within document (if enabled).
			if(options.parseLinksOnload){
				internal.parseLinks(options.container, options);
			}

			// Fire Events
			internal.triggerEvent(options.container,'complete', options);
			internal.triggerEvent(options.container,'success', options);
			
			// Don't track if page isn't part of history, or if autoAnalytics is disabled
			if(options.autoAnalytics && options.history) {
				// If autoAnalytics is enabled and a Google analytics tracker is detected push 
				// a trackPageView, so PJAX loaded pages can be tracked successfully.
				if(window._gaq) _gaq.push(['_trackPageview']);
				if(window.ga) ga('send', 'pageview', {'page': options.url, 'title': options.title});
			}

			// Set new title
			document.title = options.title;

			// Scroll page to top on new page load
			if(options.returnToTop) {
				window.scrollTo(0, 0);
			} 
		});
	};

	/**
	 * Request
	 * Performs AJAX request to page and returns the result..
	 *
	 * @scope private
	 * @param location. Page to request.
	 * @param callback. Method to call when a page is loaded.
	 */
	internal.request = function(location, callback) {
		// Create xmlHttpRequest object.
		var xmlhttp;
		try { 
			xmlhttp = window.XMLHttpRequest? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); 
		}  catch (e) { 
			console.log("Unable to create XMLHTTP Request");
			return; 
		}
		// Add state listener.
		xmlhttp.onreadystatechange = function() {
			if ((xmlhttp.readyState === 4) && (xmlhttp.status === 200)) {
				// Success, Return HTML
				callback(xmlhttp.responseText);
			}else if((xmlhttp.readyState === 4) && (xmlhttp.status === 404 || xmlhttp.status === 500)){
				// error (return false)
				callback(false);
			}
		};
		// Secret pjax ?get param so browser doesn't return pjax content from cache when we don't want it to
		// Switch between ? and & so as not to break any URL params (Based on change by zmasek https://github.com/zmasek/)
		xmlhttp.open("GET", location + ((!/[?&]/.test(location)) ? '?_pjax' : '&_pjax'), true);
		// Add headers so things can tell the request is being performed via AJAX.
		xmlhttp.setRequestHeader('X-PJAX', 'true'); // PJAX header
		xmlhttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');// Standard AJAX header.

		xmlhttp.send(null);
	};

	/**
	 * parseOptions
	 * Validate and correct options object while connecting up any listeners.
	 *
	 * @scope private
	 * @param options
	 * @return false | valid options object
	 */
	internal.parseOptions = function(options) {

		/**  Defaults parse options. (if something isn't provided)
		 *
		 * - history: track event to history (on by default, set to off when performing back operation)
		 * - parseLinksOnload: Enabled by default. Process pages loaded via PJAX and setup PJAX on any links found.
		 * - smartLoad: Tries to ensure the correct HTML is loaded. If you are certain your back end 
		 *		will only return PJAX ready content this can be disabled for a slight performance boost.
		 * - autoAnalytics: Automatically attempt to log events to Google analytics (if tracker is available)
		 * - returnToTop: Scroll user back to top of page, when new page is opened by PJAX
		 * - parseJS: Disabled by default, when enabled PJAX will automatically run returned JavaScript
		 */
		var defaults = {
			"history": true,
			"parseLinksOnload": true,
			"smartLoad" : true,
			"autoAnalytics": true,
			"returnToTop": true,
			"parseJS": false
		};

		// Ensure a URL and container have been provided.
		if(typeof options.url === 'undefined' || typeof options.container === 'undefined' || options.container === null) {
			console.log("URL and Container must be provided.");
			return false;
		}

		// Check required options are defined, if not, use default
		for(var o in defaults) {
			if(typeof options[o] === 'undefined') options[o] = defaults[o];
		}

		// Ensure history setting is a boolean.
		options.history = (options.history === false) ? false : true;

		// Get container (if its an id, convert it to a DOM node.)
		options.container = internal.get_container_node(options.container);

		// Events
		var events = ['ready', 'beforeSend', 'complete', 'error', 'success'];

		// If everything went okay thus far, connect up listeners
		for(var e in events){
			var evt = events[e];
			if(typeof options[evt] === 'function'){
				internal.addEvent(options.container, evt, options[evt]);
			}
		}

		// Return valid options
		return options;
	};

	/**
	 * get_container_node
	 * Returns container node
	 *
	 * @param container - (string) container ID | container DOM node.
	 * @return container DOM node | false
	 */
	internal.get_container_node = function(container) {
		if(typeof container === 'string') {
			container = document.getElementById(container);
			if(container === null){
				console.log("Could not find container with id:" + container);
				return false;
			}
		}
		return container;
	};

	/**
	 * connect
	 * Attach links to PJAX handlers.
	 * @scope public
	 *
	 * Can be called in 3 ways.
	 * Calling as connect(); 
	 *		Will look for links with the data-pjax attribute.
	 *
	 * Calling as connect(container_id)
	 *		Will try to attach to all links, using the container_id as the target.
	 *
	 * Calling as connect(container_id, class_name)
	 *		Will try to attach any links with the given class name, using container_id as the target.
	 *
	 * Calling as connect({	
	 *						'url':'somepage.php',
	 *						'container':'somecontainer',
	 *						'beforeSend': function(){console.log("sending");}
	 *					})
	 *		Will use the provided JSON to configure the script in full (including callbacks)
	 */
	this.connect = function(/* options */) {
		// connect();
		var options = {};
		// connect(container, class_to_apply_to)
		if(arguments.length === 2){
			options.container = arguments[0];
			options.useClass = arguments[1];
		}
		// Either JSON or container id
		if(arguments.length === 1){
			if(typeof arguments[0] === 'string' ) {
				//connect(container_id)
				options.container = arguments[0];
			}else{
				//Else connect({url:'', container: ''});
				options = arguments[0];
			}
		}
		// Delete history and title if provided. These options should only be provided via invoke();
		delete options.title;
		delete options.history;
		
		internal.options = options;
		if(document.readyState === 'complete') {
			internal.parseLinks(document, options);
		} else {
			//Don't run until the window is ready.
			internal.addEvent(window, 'load', function(){	
				//Parse links using specified options
				internal.parseLinks(document, options);
			});
		}
	};
	
	/**
	 * invoke
	 * Directly invoke a pjax page load.
	 * invoke({url: 'file.php', 'container':'content'});
	 *
	 * @scope public
	 * @param options  
	 */
	this.invoke = function(/* options */) {

		var options = {};
		// url, container
		if(arguments.length === 2){
			options.url = arguments[0];
			options.container = arguments[1];
		}else{
			options = arguments[0];
		}

		// Process options
		options = internal.parseOptions(options);
		// If everything went okay, activate pjax.
		if(options !== false) internal.handle(options);
	};

	// Make object usable
	var pjax_obj = this;
	if (typeof define === 'function' && define.amd) {
		// Register pjax as AMD module
		define( function() {
			return pjax_obj;
		});
	}else{
		// Make PJAX object accessible in global name space
		window.pjax = pjax_obj;
	}


}).call({});
