/**
 * Profile statistical tracker. Sends tracking data to the server for stats
 * logging.
 * 
 * This module is completely self contained and does not rely on external
 * libraries, nor does it pollute the global namespace.
 *
 * @author Christopher Nadeau <chris@nadeau.ws>
 */

// Set up namespaces
if (!window.Cmn) window.Cmn = {};
if (!window.Cmn.Profile) window.Cmn.Profile = {};
if (!window.Cmn.Profile.Tracker) window.Cmn.Profile.Tracker = {};

// Init a null console object if it isn't defined by safari/firebug
if(!window.console) {
	(function() {
		window.console={};
		var names= ["log","debug","info","warn","error","assert","dir","dirxml","group","groupEnd","time","timeEnd","count","trace","profile","profileEnd"];
		for (var i=0; i < names.length; ++i) {
			window.console[names[i]] = function(){};
		}
	})();
}





// #############################################################################
// # Worker
// #############################################################################

/**
 * The actual tracker works to collect data and submit it to the server.
 *
 * @option {String}    server_script            The full URL/path to the server where we will send data to
 * @option {Array}     ignoreClasses            ([no-track]) Links with this class won't be tracked when auto clicks are enabeld
 * @option {Object}    customData               Custom data to give to the server
 * @option {Boolean}   autoLogPage              (true) Register an ondomready event and automatically log the page visit. False means you have to call the method yourself.
 * @option {Boolean}   autoLogClicks            (true) Register a click event on all links, to log them. False means you'd have to take care of that yourself.
 */
Cmn.Profile.Tracker.Worker = function(options, docinfo) {
	
	var Util = this.Util = Cmn.Profile.Tracker.Util;
	var Helper = this.Helper = Cmn.Profile.Tracker.Helper;
	var self = this;


	//------------------------------
	// Set options
	//------------------------------
	
	this.options = {
		server_script: '/track',
		ignoreClasses: ['no-track'],
		customData: false,
		autoLogPage: true,
		autoLogClicks: true
	};
	
	Util.mergeObj(this.options, options || {});
	
	
	//------------------------------
	// Get some info
	//------------------------------
	
	this.docinfo = {
		url: document.location.href,
		title: document.title,
		tabId: 0
	};
	
	Util.mergeObj(this.docinfo, options || {});
	
	this.url_referrer = Helper.getReferrer();
	this.cookies_enabled = Helper.cookiesEnabled();
	this.plugins_status = Helper.detectBrowserPlugins();
	
	
	//------------------------------
	// Some methods
	//------------------------------
	
	/**
	 * Logs the page view with the server
	 */
	this.logPageView = function() {
		var url = self.getServerRequest() + '&act=page';
		return this.sendRequest(url);
	},
	
	
	/**
	 * Log when a link is clicked
	 *
	 * @param {String} link_url    The URL of the link clicked
	 * @param {Object} customData  A hash of custom data to send
	 */
	this.logLinkClick = function(link_url, customData) {
		var url = self.getServerRequest();

		var extra = [];
		extra.push('link[url]=' + link_url);
		
		if (customData) {
			for (var i in customData) {
				extra.push(i + '=' + self.Util.escape(customData[i]));
			}
			
		}
		
		extra.push('act=click');
		
		extra = extra.join('&');
		url += '&' + extra;
		
		return this.sendRequest(url);
	},
	
	
	/**
	 * Handles link click events and properly logs them.
	 */
	this.handleLinkClickEvent = function(ev) {
		
		if (!Util.isDefined(ev)) {
			ev = window.event;
		}
		
		var sourceEl;
		if (Util.isDefined(ev.target)) {
			sourceEl = ev.target;
		} else if (Util.isDefined(ev.srcElement)) {
			sourceEl = ev.srcElement;
		} else {
			return;
		}
		
		var parentEl;
		var tag;
		while ((parentEl = sourceEl.parentNode) && ((tag = sourceEl.tagName) != 'A' && tag != 'AREA')) {
			sourceEl = parentEl;
		}
		
		if (Util.isDefined(sourceEl.href)) {
			if (sourceEl.href.toLowerCase.match(/^https?:/)) P
			self.logLinkClick(sourceEl.href.toLowerCase);
		}
	};
	
	
	/**
	 * Gets the basic URL to the tracker script.
	 *
	 * @return {String}
	 */
	this.getServerRequest = function() {
		var now = new Date();
		
		var url = [self.options.server_script + '?__=' + Math.random()];
		url.push('doc[url]=' + self.Util.escape(self.docinfo.url));
		url.push('doc[title]=' + self.Util.escape(self.docinfo.title));
		url.push('doc[tab_id]=' + self.Util.escape(self.docinfo.tab_id));
		url.push('urlref=' + self.url_referrer);
		url.push('screen[width]=' + screen.width);
		url.push('screen[height]=' + screen.height);
		url.push('usertime=' + parseInt(now.getTime()/1000));
		url.push('user[cookies_enabled]=' + (self.cookies_enabled ? 1 : 0));
		
		for (var i in self.plugins_status) {
			if (self.plugins_status[i]) {
				url.push('user[' + i + '_enabled]=1');
			}
		}
		
		if (self.options.customData) {
			for (var i in self.options.customData) {
				url.push(i + '=' . self.Util.escape(self.options.customData[i]));
			}
		}
		
		url = url.join('&');
		
		return url;
	};
	
	
	/**
	 * Sends a GET request by loading up a new Image.
	 *
	 * @param {String} url
	 * @return {Image}
	 */
	this.sendRequest = function(url) {
		var image = new Image(1, 1);
		image.onLoad = function () { };
		image.src = url;
		
		console.log('Sending request: %s', url);
		
		return image;
	};
	
	
	//------------------------------
	// Set up the event listeners
	//------------------------------
	
	if (this.options.autoLogPage || this.options.autoLogClicks) {
		Util.addEventListener(document, 'ondomready', function() {
			
			if (this.options.autoLogPage) {
				self.logPageView();
			}
			
			if (this.options.autoLogClicks && document.links) {
				var ignoreClassRegex = false;
				if (this.options.ignoreClasses.length) {
					ignoreClassRegex = new RegExp('(^| )' + this.options.ignoreClasses.join('|') + '( |$)');
				}

				for (var i = 0; i < document.links.length; i++) {
					if (!ignoreClassRegex || ignoreClassRegex.test(document.links[i].className)) {
						Util.addEventListener(document.links[i], 'click', Util.funcBind(self.handleLinkClickEvent, self));
					}
				}
			}
		});
	}
};





// #############################################################################
// # Helper
// #############################################################################

/**
 * Tracker-related methods.
 */
Cmn.Profile.Tracker.Helper = {
	
	/**
	 * Map of browser plugins.
	 * plugin: [mimetypes]
	 * @var {Object}
	 */
	pluginMap: {
		// document types
		pdf:         ['application/pdf'],
		// media players
		quicktime:   ['video/quicktime'],
		realplayer:  ['audio/x-pn-realaudio-plugin'],
		wma:         ['application/x-mplayer2'],
		// interactive multimedia 
		director:    ['application/x-director'],
		flash:       ['application/x-shockwave-flash'],
		// RIA
		java:        ['application/x-java-vm'],
		gears:       ['application/x-googlegears'],
		silverlight: ['application/x-silverlight']
	},
	
	
	/**
	 * Fetches the page referrer.
	 * 
	 * @return {String}
	 */
	getReferrer: function() {
		var referrer = '';
		try {
			referrer = top.document.referrer;
		} catch (e) {
			if (parent) {
				try {
					referrer = parent.document.referrer;
				} catch (e2) {
					referrer = '';
				}
			}
		}
		if (referrer === '') {
			referrer = document.referrer;
		}

		return referrer;
	},
	
	
	/**
	 * Detects browser plugins.
	 *
	 * @return {Object} Hash of plugin:status
	 */
	detectBrowserPlugins: function() {
		
		var pluginMap = Cmn.Profile.Tracker.Helper.pluginMap;
		var pluginStatus = {};

		if (navigator.mimeTypes && navigator.mimeTypes.length) {
			var testPluginName;
			var testMimeType;
			var mimeType;
			
			for (testPluginName in pluginMap) {
				for (testMimeType in pluginMap[testPluginName]) {
					mimeType = navigator.mimeTypes[pluginMap[testPluginName][testMimeType]];
					if (mimeType && mimeType.enabledPlugin) {
						pluginStatus[testPluginName] = true;
						break;
					} else {
						pluginStatus[testPluginName] = false;
					}
				}
			}
		} else {
			// Just initializing values to false
			for (var i in pluginMap) {
				pluginStatus[i] = false;
			}
		}
		
		// Safari and Opera
		// IE6: typeof navigator.javaEnabled == 'unknown'
		if (typeof navigator.javaEnabled !== 'undefined' && navigator.javaEnabled()) {
			pluginStatus['java'] = true;
		}

		// Firefox
		if (typeof window.GearsFactory === 'function') {
			pluginStatus['gears'] = true;
		}
		
		return pluginStatus;
	},
	
	
	
	/**
	 * Checks to see if we can set cookies in this browser.
	 *
	 * @return {Boolean}
	 */
	cookiesEnabled: function(testCookieName) {
		testCookieName = testCookieName || '__cmn_t_tc';
		var Util = Cmn.Profile.Tracker.Util;
		
		if (Util.isDefined(navigator.cookieEnabled)) {
			return navigator.cookieEnabled ? true : false;
		} else {
			Util.setCookie(testCookieName, '1');
			return Util.getCookie(testCookieName) == '1' ? true : false;
		}
	}
};




// #############################################################################
// # Util
// #############################################################################

/**
 * General utility methods.
 */
Cmn.Profile.Tracker.Util = {
	
	/**
	 * Checks to see if a value is defined.
	 *
	 * @param {Object} obj  The value to check
	 * @return {Boolean}
	 */
	isDefined: function(obj) {
		return (obj != undefined);
	},
	
	
	/**
	 * Get the type of a value.
	 *
	 * @param {Object} obj The object to inspect
	 * @return {String}
	 */
	getType: function(obj) {
		if (obj == undefined) return false;
		if (obj.$family) return (obj.$family.name == 'number' && !isFinite(obj)) ? false : obj.$family.name;
		if (obj.nodeName){
			switch (obj.nodeType){
				case 1: return 'element';
				case 3: return (/\S/).test(obj.nodeValue) ? 'textnode' : 'whitespace';
			}
		} else if (typeof obj.length == 'number'){
			if (obj.callee) return 'arguments';
			else if (obj.item) return 'collection';
		}
		return typeof obj;
	},
	
	
	/**
	 * Get an element by an ID.
	 *
	 * @param {String} id
	 * @return {DOMElement}
	 */
	getEl: function(id) {
		// Assume its already an element ref
		if (typeof id != 'string') {
			return id;
		}
		
		return document.getElementById(id);
	},
	
	
	/**
	 * Attach an event listener to an element
	 */ 
	addEventListener: function(element, eventType, eventHandler, useCapture) {
		
		if (element == document && eventType == 'ondomready') {
			// If we're already loaded, call the func now
			if (this._hasOnDomReadyHandlerFired) {
				eventHandler();
				return;
			}
			
			this._initOnDomReadyListener();			
			this._onDomReadyEvents.push(eventHandler);
			return;
		}
		
		if (element.addEventListener) {
			element.addEventListener(eventType, eventHandler, useCapture);
			return true;
		} else if (element.attachEvent) {
			return element.attachEvent('on' + eventType, eventHandler);
		}
		element['on' + eventType] = eventHandler;
	},
	
	_hasInitOnDomReadyListener: false,
	_onDomReadyEvents: [],
	_hasOnDomReadyHandlerFired: false,
	
	_initOnDomReadyListener: function() {
		
		if (this._hasInitOnReadyListener) return;
		this._hasInitOnReadyListener = true;
		
		var self = this;
		
		// Maybe we have a jquery source on this site
		if (window.jQuery) {
			window.jQuery(document).ready(function() {
				self._onDomReadyHandler();
			});
			return;
		}
		
		if (documentAlias.addEventListener) {
			this.addEventListener(document, "DOMContentLoaded", function () {
                    document.removeEventListener("DOMContentLoaded", arguments.callee, false);
					self._onDomReadyHandler();
				});
		} else if (document.attachEvent) {
			document.attachEvent("onreadystatechange", function () {
				if (document.readyState === "complete") {
					document.detachEvent("onreadystatechange", arguments.callee);
					self._onDomReadyHandler();
				}
			});

			if (document.documentElement.doScroll && window == window.top) {
				(function () {
					if (self._hasOnDomReadyHandlerFired) {
						return;
					}
					try {
						document.documentElement.doScroll("left");
					} catch (error) {
						setTimeout(arguments.callee, 0);
						return;
					}
					self._onDomReadyHandler();
				}());
			}
		}
		// fallback
		addEventListener(window, 'load', this._onDomReadyHandler, false);
	},
	
	_onDomReadyHandler: function() {
		if (this._hasOnDomReadyHandlerFired) return;
		this._hasOnDomReadyHandlerFired = true;
		
		for (var i = 0; i < this._onDomReadyEvents.length; i++) {
			this._onDomReadyEvents[i]();
		}
	},
	
	
	/**
	 * Merge obj2 into the base object. If a value exists in both base and obj2, obj2's value is used.
	 *
	 * @param {Object}  base  The base object that we will merge into
	 * @param {Object}  obj2  The second object we'll pull more values form
	 * @return {Object} base
	 */
	mergeObj: function(base, obj2) {
		for (var prop in obj2) {
			base[prop] = obj2[prop];
		}
		
		return base;
	},
	
	
	/**
	 * Bind a function to a particular scope.
	 *
	 * @param {Function} fn     The function to bind
	 * @param {Object}   scope  The object to bind to
	 * @return {Function}
	 */
	funcBind: function(fn, scope) {
		return function() {
			fn.apply(scope, arguments);
		}
	},


	/**
	 * Sets a browser cookie.
	 *
	 * @param {String}  cookieName     The name of the cookie to set
	 * @param {String}  value          The value of the cookie
	 * @param {Integer} daysToExpire   How long the cookie should last
	 * @param {String}  path           Cookie path
	 * @param {String}  domain         Cookie domain
	 * @param {Boolean} secure         Set a secure cookie?
	 */
	setCookie: function(cookieName, value, daysToExpire, path, domain, secure) {
		var expiryDate;

		if (daysToExpire) {
			// time is in milliseconds
			expiryDate = new Date();
			// there are 1000 * 60 * 60 * 24 milliseconds in a day (i.e., 86400000 or 8.64e7)
			expiryDate.setTime(expiryDate.getTime() + daysToExpire * 8.64e7);
		}

		document.cookie = cookieName + '=' + escapeWrapper(value) +
			(daysToExpire ? ';expires=' + expiryDate.toGMTString() : '') +
			';path=' + (path ? path : '/') +
			(domain ? ';domain=' + domain : '') +
			(secure ? ';secure' : '');
	},
	
	
	/**
	 * Reads a browser cookie.
	 *
	 * @param {String} cookieName  The name of the cookie to read
	 * @return {String} The cookie value, or null if no such cookie exists
	 */
	getCookie: function(cookieName) {
		
		var cookiePattern = new RegExp('(^|;)[ ]*' + cookieName + '=([^;]*)'),

		cookieMatch = cookiePattern.exec(document.cookie);

		return cookieMatch ? Cmn.Profile.Tracker.Util(cookieMatch[2]) : 0;
	},
	
	
	/**
	 * URL encode a string.
	 *
	 * @param {String} str The string to encode
	 * @return {String}
	 */
	escape: function(str) {
		return (window.encodeURIComponent || window.escape)(str);
	},
	
	
	/**
	 * URL decode a string
	 *
	 * @param {String} str The string to decode
	 * @return {String}
	 */
	unescape: function(str) {
		return (window.decodeURIComponent || window.unescape)(str);
	}
};