/* Simple Ad Server written in javascript.
 * http://staff.wikia-inc.com/wiki/Athena/Javascript
 */

Athena		 = {};
Athena.configUrl = "http://athena-ads.wikia.com/athena/config/";
Athena.beaconUrl = "http://athena-ads.wikia.com/athena/beacon/";
Athena.geoUrl	 = "http://geoiplookup.wikia.com/";
Athena.pageVars  = [];
Athena.chain     = [];
Athena.statMax   = 25;
Athena.now       = new Date();
Athena.day       = Athena.now.getDay();

/* Constructor/Setup 
 * 1) Pull configuration (list of ads to try)
 * 2) Pull Geo Targeting data
 */
Athena.init = function (){
	Athena.startTime = Athena.now.getTime();
	Athena.debugLevel = Athena.getRequestVal('athena_debug', 0);
	if (Athena.debugLevel > 0 && Athena.empty(YAHOO.env.ua.gecko)){
		// If not in Firefox, dump debug at the bottom
		this.myLogReader = new YAHOO.widget.LogReader("", {newestOnTop:false}); 
	}

	Athena.debug("Loading geo data from " + Athena.geoUrl, 3);
	Athena.loadScript(Athena.geoUrl);


	Athena.rand = Math.random() * 100;
	Athena.loadTagStats();

	// hack to get the scripts to finish loading
	document.write("<script>Athena.noop()<\/script>");
};


/* Pull the configuration data from our servers */
Athena.pullConfig = function (){

	// Allow for us to work in a dev environment
	if (Athena.getRequestVal('athena_dev_hosts') != ''){
		Athena.configUrl = "http://athena.dev.wikia-inc.com/athena/config/";
		Athena.beaconUrl = "http://athena.dev.wikia-inc.com/athena/beacon/";
	}

	var url = Athena.configUrl + '?skin=' + Athena.getPageVar('skin');
	Athena.debug("Loading config from " + url, 2);
	Athena.loadScript(url);

	// hack to get the scripts to finish loading
	document.write("<script>Athena.noop();<\/script>");
};


/* Public method for users to call an ad for a slot */
Athena.callAd = function(slotname){
	Athena.debug("Athena.callAd() called for " + slotname, 3); 

	return Athena._executeAd(slotname);

};


Athena._executeAd = function(slotname) {
	var thisTag = Athena.getNextTag(slotname);
	Athena.debug("Ad for " + thisTag['network_name'] + " being called in " + slotname);
	Athena.debug("Config for ad being executed is " + Athena.print_r(thisTag), 6);

	// Print the tag, wrapped in a try/catch so that errors don't prevent the rest of
	// our javascript from loading.
	try {
		document.write(thisTag['tag']);
	} catch (e) {

		Athena.reportError("Error loading ads = " + Athena.print_r(e));
		return false;
	}

	return true;
};


/*  Set up chain for slot
 *    a) See if there is a "sampled ad" to try
 *    b) Check against targeting criteria
 *    c) Check against frequency/rejection capping criteria
 *    d) Consider the maximum number of hops, have the last one be a garaunteed fill
 */
Athena.buildChain = function(slotname) {
	// Sampled ad
	var networks = [];
	var sampledAd = Athena.getSampledAd(slotname);
	Athena.chain[slotname] = [];
	if (sampledAd !== false && Athena.isValidCriteria(sampledAd)){
		Athena.chain[slotname].push(sampledAd);	
		networks.push("Sampled: " + sampledAd["network_name"] + ", #" + sampledAd["tag_id"]);
}


	// Fill in the middle
	for (var i = Athena.chain[slotname].length; i < Athena.config['slots'][slotname].length; i++){
		var thisTag = Athena.config['slots'][slotname][i];

		// skip over if it is sampled
		if (thisTag['tag_id'] == sampledAd['tag_id']){
			continue;
		}
		
		if (Athena.isValidCriteria(thisTag)){
			Athena.chain[slotname].push(thisTag);	
			networks.push(thisTag["network_name"] + ", #" + thisTag["tag_id"]);
		}

		if (Athena.chain[slotname].length == Athena.config.maxHops - 1){
			// Chain is full
			break;
		}
	}
	
	// Guaranteed ad.
	var guaranteedAd = Athena.getGuaranteedAd(slotname);
	Athena.chain[slotname].push(guaranteedAd);
	networks.push("Guaranteed: " + guaranteedAd["network_name"] + ", #" + guaranteedAd["tag_id"]);

	// Set up a listener for the slot to keep track of loads
        YAHOO.util.Event.onContentReady(slotname + "_load", Athena.adLoadHandler);


	Athena.debug("Chain for " + slotname + " = " + Athena.print_r(networks), 4);
	return true;
};



/* Look through the list of ads in the potential chain, and find one that is sample-able */
Athena.getSampledAd = function(slotname){
	// Build up an array of the sample stats.
	var sampleArray = [], total = 0;
	for (var i = 0; i < Athena.config['slots'][slotname].length; i++){
		var samplerate =  Athena.config['slots'][slotname][i]['sample_rate'];
		if (samplerate == 0){
			continue;
		}
		total += samplerate * 1; // Multiply by 1 to convert from string

		Athena.debug("Sample Rate for " + Athena.config['slots'][slotname][i]['tag_id'] + " is " + samplerate, 7);
		var thisArray = new Array();
		thisArray["upper_bound"] = total;
		thisArray["index"] = i;
		sampleArray.push(thisArray);
	}
	Athena.debug("Athena.rand = " + Athena.rand + ", Sample Array = " + Athena.print_r(sampleArray), 8);

	// Now check to see if the random number is in sampleArray
	for (var j = 0; j < sampleArray.length; j++){
		if (Athena.rand < sampleArray[j]["upper_bound"]){
			var found = sampleArray[j]["index"];
			return Athena.config['slots'][slotname][found];
		}
	}

	return false;
};


/* Look through the list of ads in the potential chain, and return the best guaranteed fill,
 * for the last hop */
Athena.getGuaranteedAd = function(slotname){

	for (var i = 0; i < Athena.config['slots'][slotname].length; i++){
		var thisTag = Athena.config['slots'][slotname][i];

		if (thisTag['guaranteed_fill'] == 'Yes' && Athena.isValidCriteria(thisTag)){
			return thisTag;
		}
	}

	// Rut roh
	return false;
};


/* Iterate through the chain and deliver the next ad tag to be called */
Athena.getNextTag = function(slotname){
	// Do we need to build the chain?
	if (Athena.empty(Athena.chain[slotname])){
		Athena.buildChain(slotname);
	}

	Athena.lastSlot = slotname;
	var now = new Date();
	for (var i = 0; i < Athena.chain[slotname].length; i++){
		if (Athena.empty(Athena.chain[slotname][i]['started'])){
			// Win nah!
			Athena.chain[slotname][i]['started'] = now.getTime();
			return Athena.chain[slotname][i];
		} else {
			Athena.chain[slotname][i]['rejected'] = true;
			Athena.chain[slotname][i]['load_time'] = now.getTime() - Athena.chain[slotname][i]['started'];
			continue;
		}
			
	}
	// Rut roh
	return false;
};


/* This is the backup tag used to go to the next ad in the configuration */
Athena.hop = function (slotname){
	if (Athena.empty(slotname)){
		slotname = Athena.lastSlot;
	}

	Athena.debug("hop() called for " + slotname);

	return Athena._executeAd(slotname);
};


/* Store accepts/rejections in a cookie
 * Keep this as small as possible! 
 */
Athena.storeTagStats = function (){
	Athena.debug("Stored Tag Stats  = " + Athena.tagStats);
        YAHOO.util.Cookie.set("ATS", Athena.tagStats, {
                  domain: Athena.getCookieDomain(),
                  path: "/",
		  expires: Athena.now.getTime() + 86400,
                  secure: ""
                 }
        );
};

Athena.getCookieDomain = function (){
	var host = Athena.getPageVar('hostname', 'www.wikia.com');
	if (host.indexOf('wikia.com') > 0){
		return 'wikia.com';
	} else {
		return host;
	}
};

/* Pull the stats from a cookie, clean up the old ones if there are any */
Athena.loadTagStats = function (){

	// Url override
	if (Athena.getRequestVal('athena_tag_stats') != ''){
		Athena.debug("Using tag stats from url = " + Athena.getRequestVal('athena_tag_stats'));
		Athena.tagStats = Athena.getRequestVal('athena_tag_stats');
	} else {

		// Pull from cookie
		var cookieData = YAHOO.util.Cookie.get("ATS");
		if (Athena.empty(cookieData)){
			Athena.tagStats = '';
			return Athena.tagStats;
		} else {
			Athena.tagStats = cookieData;
		}
	}
	
	// Split based on comma and rebuild the array
	var pieces = Athena.tagStats.split(',');
	var holder = [];
	for (var i = 0; i < pieces.length; i++){
		// Build up a temporary array with items from the same day (wipe out old)
		if (pieces[i].substring(0, 1) == Athena.day){
			holder.push(pieces[i]);	
		}
		if (i > Athena.statMax){ break; }  // Sanity check, should be caught with "set"
	}
	Athena.tagStats = holder.join(',');

	Athena.debug("Tag Stats Loaded = " + Athena.tagStats, 5);

	return Athena.tagStats;
};


/* Get loads/rejects for a tag. type is "l" or "r" */
Athena.getTagStat = function (tag_id, type){
	var stat = 0;

	try {
		var statReg = new RegExp(Athena.day + '_' + tag_id + 'l([0-9]+)r*([0-9]*)');
		var statMatch = Athena.tagStats.match(statReg);
		if (!Athena.empty(statMatch)){
			if (type == "l" && !Athena.empty(statMatch[1])){
				stat = statMatch[1];
			} else if (type == "r" && !Athena.empty(statMatch[2])){
				stat = statMatch[2];
			}
		}
	} catch(e)  {
		// If something goes wrong, quietly return 0
		Athena.debug("Error getting stats = " + Athena.print_r(e));
	}

	Athena.debug("Stats for " + tag_id + " type " + type + " = " + stat, 7);
	return stat;
};


/* Set loads/rejects for a tag. type is "l" or "r" */
Athena.setTagStat = function (tag_id, type){
	Athena.debug("Setting a " + type + " stat for " + tag_id, 6);

	var pieces = Athena.tagStats.split(',');
	var holder = [], found=false;
	if (pieces.length > Athena.statMax){
		// If too may, take off the first one
		pieces.shift();
	}

	// Get the current stats, if any, and rebuild
	var loads = Athena.getTagStat(tag_id, "l");
	var rejects = Athena.getTagStat(tag_id, "r");
	if (type == "l"){ loads++; }
	if (type == "r"){ rejects++; }
	var piece = Athena.day + '_' + tag_id + "l" + loads;
	if (rejects > 0){
		piece = piece + "r" + rejects;
	}

	var statReg = new RegExp(Athena.day + '_' + tag_id + 'l[0-9]+r*[0-9]*');
	var tmp = Athena.tagStats.replace(statReg, piece);
	if (tmp == Athena.tagStats){
		// Append it to the end.
		Athena.tagStats = Athena.tagStats + ',' + piece;
	} else {
		Athena.tagStats = tmp;
	}

	Athena.debug("Tag Stats After Set = " + Athena.tagStats, 5);
};




/* Does the criteria match for this tag? */
Athena.isValidCriteria = function (thisTag){
	if (!Athena.empty(thisTag['isValidCriteria'])){
		return thisTag['isValidCriteria']; 
	} else if (Athena.empty(thisTag['criteria'])){ 
		thisTag['isValidCriteria'] = true; 
		return thisTag['isValidCriteria']; 
	} 
 
	try { 
 
	  for (var keyname in thisTag.criteria){ 
		Athena.debug("Checking " + keyname + " criteria", 8); 
		switch (keyname){ 
		  case 'Geography':  
			if ( ! Athena.isValidCountry(thisTag.criteria.Geography)){ 
				Athena.debug("Ad #" + thisTag["tag_id"] + " rejected because of Invalid Geography", 8);
				thisTag['isValidCriteria'] = false;
				return thisTag['isValidCriteria']; 
			} 
			break; 
		  default: 
			// If it is not predefined, assume it is a page var. 
			var list = eval("thisTag.criteria." + keyname); 
			if (! Athena.in_array(Athena.getPageVar(keyname), list)){ 
				
				Athena.debug("Ad #" + thisTag["tag_id"] + " rejected because " + keyname + " not found in " + Athena.print_r(list), 8);
				thisTag['isValidCriteria'] = false;
				return thisTag['isValidCriteria']; 
			} 
			break; // Shouldn't be necessary, but silences a jslint error 
		} 
	  } 
 
	} catch(e){ 
		Athena.reportError("Javascript Error while checking criteria: " + Athena.print_r(e)); 
		return false; 
	} 


	// Frequency
	if (!Athena.empty(thisTag["freq_cap"])){
		var loads = Athena.getTagStat(thisTag["tag_id"], "l");
		if (loads >= thisTag["freq_cap"]){
			Athena.debug("Ad #" + thisTag["tag_id"] +
				" rejected because " + loads + " loads is greater than" + 
				" freq_cap of " + thisTag["freq_cap"],  2);
			thisTag['isValidCriteria'] = false;
			return thisTag['isValidCriteria']; 
		}
		
	}

	// Rejection capping
	if (!Athena.empty(thisTag["rej_cap"])){
		var rejects = Athena.getTagStat(thisTag["tag_id"], "r");
		if (rejects >= thisTag["rej_cap"]){
			Athena.debug("Ad #" + thisTag["tag_id"] +
				" rejected because " + rejects + " rejects is greater than" + 
				" rej_cap of " + thisTag["rej_cap"],  2);
			thisTag['isValidCriteria'] = false;
			return thisTag['isValidCriteria']; 
		}
		
	}
 
	// All criteria passed 
	Athena.debug("Targeting criteria passed for tag #" + thisTag["tag_id"], 8);
	thisTag['isValidCriteria'] = true;
	return thisTag['isValidCriteria']; 

};


/* Check to see if the user from the right geography */
Athena.isValidCountry = function (countryList){

	if (Athena.getRequestVal('athena_country') != ''){
		ac = Athena.getRequestVal('athena_country');
		Athena.debug("Using athena_country for geo targeting (" + ac + ")");
	} else {
		try { 
			// sometimes Geo isn't available because geoiplookup hasn't returned
			ac = Geo.country.toLowerCase();
		} catch (e){
			ac = "us"; // Default to us. Better to over-send than under
		}
	}

	if (ac == "gb"){
		// Wankers.
		ac = "uk";
	}

	Athena.debug("Checking if " + ac + " is in " + Athena.print_r(countryList), 8);

	if (Athena.in_array("row", countryList) &&
		  !Athena.in_array(ac, Array('us', 'uk', 'ca'))){
		Athena.debug("ROW targetted, and country not in us, uk, canada", 6);
		return true;
	}
	for (var i = 0; i < countryList.length; i++){
		if (ac == countryList[i]){
			return true;
		}
	}

	return false;
};


Athena.in_array = function (needle, haystack){
    var found = false, key;

    for (key in haystack) {
	if (haystack[key] == needle) {
	    found = true;
	    break;
	}
    }
 
    return found;
};

// http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_empty/
// Thank you.
Athena.empty = function ( mixed_var ) {
    
    if (mixed_var === "" || 
	mixed_var === 0 ||
	mixed_var === "0" ||
	mixed_var === null ||
	mixed_var === false ||
	mixed_var === undefined){
        return true;
    }
    if (typeof mixed_var == 'object') {
        for (var key in mixed_var) {
            if (typeof mixed_var[key] !== 'function' ) {
              return false;
            }
        }
        return true;
    }
    return false;
};

Athena.adLoadHandler = function (){
        var slotname = this.id.replace(/_load/, '');
	/* Someday we will use this to keep track of how long it took the ad to load.
	   Unfortunately, YUI's onLoad handler for a div doesn't consider when iframes load,
	   so this event is fired too early, and the times are off. I've heard that jQuery's
	   onload handler can handle this better. Someday. 
	*/
	return true;
};


/* Send a beacon back to our server so we know if it worked */
Athena.sendBeacon = function (){
	Athena.debug("sendBeacon() called");
	var events = '', numSlots = 0;
	for(var slotname in Athena.chain){

		numSlots++;

		// TODO: Account for slippage?
		for (var i = 0; i < Athena.chain[slotname].length; i++){
			var thisTag = Athena.chain[slotname][i];
			var loads = Athena.getTagStat(thisTag['tag_id'], "l");

			// Reject
			if (! Athena.empty(thisTag['rejected'])){
				events += ',r' + thisTag['tag_id'] + 'pl' + loads;
				Athena.setTagStat(thisTag['tag_id'], "r");
				Athena.debug("Recording Reject for " + thisTag["network_name"] + " in " + slotname, 5);
			// Garaunteed ads have to fill!
			} else if ( !Athena.empty(thisTag['started']) && 
				thisTag['guaranteed_fill'] == 'Yes' ){
				events += ',l' + thisTag['tag_id'] + 'pl' + loads;
				Athena.setTagStat(thisTag['tag_id'], "l");
				Athena.debug("Recording Load for " + thisTag["network_name"] + " in " + slotname, 4);
			// Check to see if this one started, and the next one didn't
			} else if ( !Athena.empty(thisTag['started']) && 
				      Athena.empty(Athena.chain[slotname][i+1]['started'])) {
				events += ',l' + thisTag['tag_id'] + 'pl' + loads;
				Athena.setTagStat(thisTag['tag_id'], "l");
				Athena.debug("Recording Load for " + thisTag["network_name"] + " in " + slotname, 4);
			}
		}
	}
	Athena.storeTagStats();

	Athena.debug("sendBeacon() events = " + Athena.print_r(events), 6);

	Athena.beaconData = {};

	var now = new Date();
	var ms = (now.getTime() - Athena.startTime) / 1000;
	Athena.beaconData.adTime = Math.floor(ms * 10) / 10;
	Athena.debug ("Athena done, ads loaded in " + Athena.beaconData.adTime + " seconds");
	Athena.beaconData.numSlots = numSlots;
	Athena.beaconData.events = events;
	var beacon = YAHOO.Tools.JSONEncode(Athena.beaconData);

	var img = document.createElement("img");
	img.src = Athena.beaconUrl + '?beacon=' + encodeURIComponent(beacon);
	img.id = "beacon_image";
	img.width = 0;
	img.height = 0;
	document.body.appendChild(img);		
        YAHOO.util.Dom.setStyle('beacon_image', 'display', 'none'); 


	return true;
};

// Listener to send the beacon when the page loads
// YAHOO.util.Event.onDOMReady(Athena.sendBeacon);
YAHOO.util.Event.on(window, "load", Athena.sendBeacon);

/* Set / get page variables */
Athena.setPageVar = function (key, value){
	Athena.pageVars[key.toLowerCase()]=value;
	Athena.debug("Page var '" + key + "' set to '" + Athena.pageVars[key.toLowerCase()] + "'", 5);
	return true;
};


Athena.getPageVar = function (key){
	if (!Athena.empty(Athena.pageVars[key.toLowerCase()])){
		return Athena.pageVars[key.toLowerCase()];
	} else {
		return '';
	}
};


/* Send a message to the debug console if available, otherwise alert */
Athena.debug = function (msg, level){
	if (Athena.debugLevel == 0){
		return false;
	} else if (level > Athena.debugLevel){
		return false;
	// FIXME: Do we have to use YUI here, or can I just call console.log directly?
	} else if (typeof YAHOO.log != "undefined"){
		YAHOO.log(msg, "info", "Athena");
	} else {
		alert("Athena debug: " + msg);
	}
};



Athena.reportError = function (error){
	Athena.debug("ERROR = " + error);
	// TODO: Send a request back to the server with errors
};


/* Load the supplied url inside a script tag  */
Athena.loadScript = function(url, method) {
	document.write('<script type="text/javascript" src="' + url + '"><\/script>');
};


/* Javascript equivalent of php's print_r. 
 * http://www.openjs.com/scripts/others/dump_function_php_print_r.php
 */
Athena.print_r = function (arr,level) {
	var dumped_text = "\n";
	if(!level) level = 0;

	// saving a crash if you try to do something silly like print_r(top);
	if (level > 6) return false; 
	
	//The padding given at the beginning of the line.
	var level_padding = "";
	for(var j=0;j<level+1;j++) level_padding += "	";
	
	if(typeof(arr) == 'object') { //Array/Hashes/Objects 
		for(var item in arr) {
			var value = arr[item];
			
			if(typeof(value) == 'object') { //If it is an array,
				dumped_text += level_padding + "'" + item + "' ...";
				dumped_text += Athena.print_r(value,level+1);
			} else {
				dumped_text += level_padding + "'" + item + "' => \"" + value + "\"\n";
			}
		}
	} else { //Stings/Chars/Numbers etc.
		dumped_text = "===>"+arr+"<===("+typeof(arr)+")";
	}
	return dumped_text;
};


/* Nick wrote: This code looks at the query string supplied in the url and parses it.
 * It returns an associative array of url decoded name value pairs
 */
Athena.parseQueryString = function (qstring){
  var ret = new Array();
  var varName, varValue, nvpairs, intIndex;
  
  if (qstring.charAt(0) == '?') qstring = qstring.substr(1);
  if (qstring.length == 0) return ret;
  
  qstring=qstring.replace(/\;/g, '&', qstring);
  
  nvpairs=qstring.split('&');

  for (var i = 0; i < nvpairs.length; i++){
    if (nvpairs[i].length == 0){
      continue;
    }

    if ((intIndex = nvpairs[i].indexOf('=')) != -1) {
      varName = decodeURIComponent(nvpairs[i].substr(0, intIndex));
      varValue = decodeURIComponent(nvpairs[i].substr(intIndex + 1)); 
    } else {
      varName = decodeURIComponent(nvpairs[i].substr(0, intIndex));
      varValue = '';
    }

    if (varName == '' || varValue == ''){
      continue;
    }

    ret[varName]=varValue;
  }

  return ret;
};


/* Look for a particular value in the query string, and return it. Optional second arg is a default. */
Athena.getRequestVal = function(varname, defaultval){
	if (Athena.empty(Athena.nvpairs)){
		Athena.nvpairs = Athena.parseQueryString(document.location.search);
	}
	if (! Athena.empty(Athena.nvpairs[varname])){
		return Athena.nvpairs[varname];
	} else if (! Athena.empty(defaultval)) {
		return defaultval;
	} else {
		return '';
	}
};


/* Target based on the minute of the hour */
Athena.getMinuteTargeting = function (){
	var myDate;
	myDate = new Date();
	return myDate.getMinutes() % 15;
};


/* Get the current slotname by looking at the DOM.
 * Supported scenarios
 * 1) NO starting element. If this is the case, then a startign element is created for you
 * 2) Starting Element passed as a string
 *
 * Tip: If you are inside an iframe, assign an "id" to the current 
 * 	iframe with window.frameElement.id, then pass this id in as a string
 *
 * Nested iframes are not currently supported. Soon.
 */ 
Athena.getSlotnameFromDom = function (e){
	var tempElement, slotdiv = '';

	// Determine the starting point.
	if (typeof e == "string"){
		// Get the id from a string that is passed in
		Athena.debug("Determined slotname for hop using string method", 7);
		tempElement = document.getElementById(e);
	} else if (typeof e == "undefined" || typeof e.parentNode == undefined){
		// HACK: couldn't determine e's parent (or it wasn't passed in)
		// Write out a dummy div as a starting point. 
		var dummydivid = "dummydiv_" + Math.random();
		Athena.debug("Determined slotname for hop by writing fake div", 7);
		document.write('<div style="display:none" id="' + dummydivid + '"></div>');
		tempElement = document.getElementById(dummydivid);
	} else {
		Athena.debug("Determined slotname for hop by using element", 7);
		tempElement = e;
	}

	while (tempElement != null) {
		if (typeof tempElement.id != "undefined" && tempElement.id.match(/_load$/)) {
			slotdiv = tempElement.id;
			break;
		}
		tempElement = tempElement.parentNode;
	}

	if (slotdiv == ""){
		return false;
	} else {
		var s = slotdiv.replace(/_load$/, '');
		Athena.debug("Current slotname = " + s, 5);
		return s;
	}
};


Athena.noop = function (){
	return true;
};

/* Generate random number to use as a cache buster during the call for ad (OpenX and DART) 
 * AdsCB is referenced in a lot of places, but I'd like for us to use Athena.random() long term
 */
var AdsCB = Math.floor(Math.random()*99999999);
Athena.random = function (){
	return AdsCB;
};

// Init
Athena.init();
