/*
 This script provides access to the Adchemy Variation Service.

 Copyright(C) 2009 Adchemy, Inc.

 */


/**
 * class for debugging purposes to accumulate debug message and display them
 *
 * @private
 */
var adchemyDebugInfo = new function() {
    this.info = "";
    this.firstTime = true;
    this.append = function(str) {
        this.info += str + "<br/>";
    };

    this.print = function() {
        var body = document.body;
        if (body) {
            var str = body.innerHTML;
            if(this.firstTime) {
                this.firstTime = false;
                str += "<div style='font-weight:bold; font-family:serif; font-style:normal; color:black; background-color:white'><h3>Debug Info:  </h3><hr></div>";
            }
            str += "<div style='font-weight:normal; font-family:serif; font-style:normal; color:black; background-color:white'><br>" + this.info + "<br/><hr></div>";
            body.innerHTML = str;
            this.info = "";
        }
    };
};

//////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * class to request json and call callback function to pass responseData.  Not for end programmer
 * use.  Used internally by adchemyVariationFetcher.
 * @private
 */
var adchemyJsonHelper = {
    timeout: 200,
    callback: null,
    errorFunction: null,
    requestIsDone: false,
    timeoutId: null,
    isTimeout: false,
    /**
     * sends request to url with possible data and if successfull - call callBackFunction
     * @param {String} actUrl url, to which to send a request, sans protocol prefix
     * @param {String} data name value pairs separated by "&"
     * @param {Function} callBackFunction reference to the function to call when request is completed
     * @param {Function} errorFunction reference to the function to call when request is failed
     * @private
     */
    sendData: function(url, data, callBackFunction, errorFunction) {
        adchemyDebugInfo.append("Sending request to variation service, timeout: " + this.timeout);
        this.callback = callBackFunction;
        this.errorFunction = errorFunction;

        var protocol = (("https:" == document.location.protocol) ? "https://" : "http://");
        var actUrl = protocol + url;
        try {
            actUrl += (actUrl.match(/\?/) ? "&" : "?") + "alps-json-callback=adchemyJsonHelper.complete";
            if (data) {
                actUrl += "&" + data;
            }
            adchemyDebugInfo.append("Request URL: " + actUrl);

            var head = document.getElementsByTagName("head")[0];
            var script = document.createElement("script");
            script.src = actUrl;
            script.type = 'text/javascript';
            // What is the point of this?   Even if it works, it doesn't seem like it's
            // going to do the right thing.  Whatever we get back should really be UTF-8.
            // If the charset for server code and variation content is not UTF-8, it should be.
            if (this.scriptCharset) {
                script.charset = this.scriptCharset;
            }

            adchemyJsonHelper.requestIsDone = false;
            // Attach handlers for all browsers
            script.onload = script.onreadystatechange = function() {
                if ((!adchemyJsonHelper.requestIsDone) && (!adchemyJsonHelper.isTimeout) && (!this.readyState || this.readyState == "loaded" || this.readyState == "complete")) {
                    adchemyDebugInfo.append("Load complete.");

                    adchemyJsonHelper.success();

                    adchemyJsonHelper.removeScript(head, script);
                }
            };

            head.appendChild(script);


            // Timeout checker
            if (this.timeout > 0) {
                this.timeoutId = window.setTimeout(function() {
                    // Check to see if the request is still happening
                    if (!adchemyJsonHelper.requestIsDone) {
                        adchemyDebugInfo.append("WARNING: Load error occurred");

                        adchemyJsonHelper.isTimeout = true;

                        adchemyJsonHelper.removeScript(head, script);

                        adchemyJsonHelper.handleError(actUrl, new Error("load error"));
                    }
                }, this.timeout);
            }
        } catch(e) {
            this.handleError(actUrl, e);
        }
    },
    removeScript: function(parentObj, scriptObj) {
        // Handle memory leak in IE
        scriptObj.onload = scriptObj.onreadystatechange = null;
        parentObj.removeChild(scriptObj);
    },
    complete: function(responseData) {
        // TODO:  Update this to process the JSONP format returned by the ALPS server
        if (!responseData || responseData["error"]) {
            this.handleError("", new Error(((responseData["error"]) ? responseData["error"] : "")));
        } else if (!adchemyJsonHelper.isTimeout) { //catch situation in firefox, it continues to load script even when we delete script tag
            this.success();
            this.callback.call(this, responseData);
        } else {
            adchemyDebugInfo.append("Timout detected by complete() callback.");
        }
    },
    handleError: function(url, e) {
        this.requestIsDone = true;
        if (this.timeoutId) {
            window.clearTimeout(this.timeoutId);
        }
        adchemyDebugInfo.append("ERROR: in handleError: " + url + " Exception: " + e.message);

        if (adchemyJsonHelper.errorFunction) {
            adchemyJsonHelper.errorFunction.call(this, "");
        }
    },
    success: function() {
        this.requestIsDone = true;
        adchemyDebugInfo.append("Successfully processed request");
    }
};

///////////////////////////////////////////////////////////////////////////////////

/**
 * class to work with visitor context getting from url string.  Not for end programmer use.
 * Used internally by adchemyVariationFetcher.
 *
 * @private
 */
var adchemyVisitorContext = new function() {

    this.convertToQueryString = function(queryMap) {
        var urlStr = "";
        for (var key in queryMap) {
            if (urlStr != "") {
                urlStr += "&";
            }
            urlStr += key + "=" + queryMap[key];
        }
        return urlStr;
    }

    this.createVisitorContextFromQueryString = function(queryString, mapping, restrictToMap) {
        var queryMap = this.getQueryStringInternal(queryString);
        var vcMap = {};

        for (var key in queryMap) {
            var value = queryMap[key];

            if (this.isAlpsParameter(key)) {
                vcMap[key] = value;
                continue;
            }

            if (mapping && mapping[key]) {
                vcMap[mapping[key]] = value;
                continue;
            }

            if (!restrictToMap) {
                vcMap[key] = value;
            }
        }
        return vcMap;
    }

    this.isAlpsParameter = function(key) {
        return key.toLowerCase().indexOf("alps") == 0;
    }

    this.getUrlParametersAsString = function(urlParams2alpsParams, restrict) {
        var query = window.location.search.substring(1);
        var vc = this.createVisitorContextFromQueryString(query, urlParams2alpsParams, restrict);
        return this.convertToQueryString(vc);
    }

    this.getQueryStringVariablesMap = function() {
        var query = window.location.search.substring(1);
        return this.getQueryStringInternal(query);
    };

    this.getQueryStringInternal = function(query) {
        var vars = query.split('&');
        var varsMap = {};
        for (var i = 0, len = vars.length; i < len; i++) {
            var pair = vars[i].split('=');
            if (pair.length == 2) {
                varsMap[pair[0]] = pair[1];
            }
        }
        return varsMap;

    }
}
///////

/**
 * Class used to manage DOM element traversal and filtering.  Not for end programmer use.
 * Used internally by adchemyVariationFetcher.
 * @private
 */
var ElementFilter = new function() {

    var filterFn = null;

    /**
     * Check if the current element is valid. This will invalidate document and textnodes.
     *
     * @method _isValidElement
     * @private
     * @param el {HTMLElement} the element to be checked
     */
    var _isValidElement = function(el) {
        return el && el.tagName && el.getAttribute;
    };

    /**
     * Traverses the given HTMLElement and collects accepted elements.
     *
     * @method _traverse
     * @private
     *
     * @param el {HTMLElement} the element to be traversed
     * @param collection {array} all accepted elements are collected here
	 * @param filterFn {function} the function should return <code>true</code> if accepted, <code>false</code> otherwise.
     * @return collection {array} contains all accepted elements
     */
    var _traverse = function (el, collection) {
        collection = collection || [];

        // el.children is only available for ie and ff3.5, but ff2 and ff3 does not have it
        // el.childNodes is available to all browsers but includes text nodes
        var nodes = el.children || el.childNodes;

        if(nodes && nodes.length > 0) {
            for(var i = 0, len = nodes.length; i < len; i++) {
                _traverse(nodes[i], collection);
            }
        }

        // no not pass to filterFn if el is document
        // since document has not an html element (no attribute and tagName)
        if(_isValidElement(el) && filterFn(el)) {
            collection.push(el);
        }

        return collection;
    };


    /**
     * Start collecting accepted HTML Elements starting from the document, if startEl is not defined.
     * @private
     * @method collect
	 * @param filterFn {function} the function should return <code>true</code> if accepted, <code>false</code> otherwise.
     * @param startEl {element} (optional) start html element, document otherwise.
     * @return the array of all accepted elements
     */
    this.collect = function(filterFunc, startEl) {
        startEl = startEl || document;

        filterFn = filterFunc;

        return _traverse(startEl);
    };
};



//////////////////////////////////////////////////////////////////////////////////////////
/**
 * The main class to use to update content with alps variations.
 * The functions made available by this class are the only ones intended for end programmer
 * use.
 *
 * <p>The general approach for using this class is to add some Adchemy-specific attributes
 * to html elements that will have dynamic content, then to call this class to retrieve
 * the dynamic content.  This class automatically populates tagged HTML elements with
 * their content.
 *
 * <p>A simple example fragment is shown below:
 * <pre>
 * &lt;script language="JavaScript" type="text/javascript"&gt;
 *
 *   function callVariationService() {
 *       adchemyVariationFetcher.setAccountId(myacctnumber);
 *       adchemyVariationFetcher.getVariations("mypagegroup", null, null, null);
 *   }
 *   window.onload = callVariationService;
 *   &lt;script&gt;
 *
 *
 *   &lt;p alps-component-name="mycomp" alps-variation-target="text"&gt;&lt;/p&gt;
 * </pre>
 *
 * @class adchemyVariationFetcher
 */
var adchemyVariationFetcher = new function() {
    var defaultComponentServerHost = "alps.adchemy.com";
    var defaultTimeout = 800;
    var defaultRestrictToMappedParms = false;
    var alpsSessionClearStr = "alps-session=new";
    var defaultAlpsTracking = true;
    var lastResponse = null;
    
    /**
     * Identifies the default div to make visible on error in a multiple-div page.
     *
     * <p>If the library fails to load variations because of timeout or other error,
     * the div whose id is specified by this variable is made visible.
     *
     * <p>Note that calling the setMultiDiv() method is preferred to setting this variable
     * directly.
     *
     */
	this.defaultDiv = "";
	/**
	 * Map of div id's to Variation Service template aliases.  See setMultiDiv for
     * more information.
     *
     * <p>This variable should not be set directly by the programmer.
	 */
    this.divMap = null;
    /**
     * Boolean that indicates whether the current page uses Multi Div page mapping.
     * See setMultiDiv for more information.
     */
    this.isMultiDiv = false;
    /**
     * In some template groups, one or more of the templates can be a redirect to some other url.  This is
     * often the case when routing some traffic to an existing control page.
     *
     * <p>This property allows the programmer to identify a particular component name which, if present in the response,
     * indicates that the page should redirect to the value returned for that component.
     *
     * <p>Assume there exists a template group with two templates in it, one of which contains real components with content
     * that should be shown to users, and the other of which contains only a single component whose value is an url
     * to which to redirect the user.  The value of this property is the name of that redirect component.
     *
     */
    this.redirectComponentName = "";
    /**
     * The account id to use when fetching the variations
     */
    this.accountId = -1;
    /**
     * The host name to contact when fetching variations. If this variable is not
     * set then it defaults to alps.adchemy.com
     */
    this.componentServerHost = defaultComponentServerHost;
    /**
     * The number in milliseconds that the API should wait after calling the componentServerHost before timing out.
     * After this period, the variationFetcher displays any specified default variation values, and ignores any
     * subsequent response from the server.
     *
     * <p>The default is 800 ms.
     */
    this.timeout = defaultTimeout;
    /**
     * Specifies whether the variationFetcher should pass along all url parameters to the server, or whether it
     * should pass only those that are specified by a call to setVisitorContextMapping.
     *
     * <p>The default behavior is to forward all url parameters to the variation service.
     */
    this.restrictToMappedParms = defaultRestrictToMappedParms;
    /**
     * A boolean property that appends a debugging log display at the bottom of the page
     */
    this.isDebug = false;
    this.alpsTracking = defaultAlpsTracking;
    this.urlParams2VisitorContext = null;
    /**
     * A map of default variations for use if the server request fails or times out.
     * For more information, see setDefaultComponentVariations.
     */
    this.defaultComponentVariations = null;
    /**
     * A boolean setting that determines whether to force a new session or not.  If true, a call to getVariations will
     * start a new session, regardless of whether one already exists.  If false, then if an existing session id exists
     * locally, it will be used.
     *
     * <p>Starting a new session forces the server to make new variation decisions.
     */
    this.startNewSession = false;
    this.sessionId = null;
    this.userCallback = this.variationHandler;
    this.sessionUrl = null;
	this.sessionParams = {};
    this.cookieDays;
    this.cookieDomain;
    this.cookieName;
    this.useCookies = false;
    /**
     * A map that associates a particular component to a success and an error handler to be run
     * before or after the default handler
     */
    this.variationComponentHandlers = {};
    /**
     * Indicates whether the component handler type is to be run before or after the default handler. The value of this
     * should be either 'BEFORE' or 'AFTER'
     */
    this.componentHandlerType = '';
    
    var createCookie = function(name,value,params) {
        if (params !== undefined && params.days !== undefined) {
            var date = new Date();
            date.setTime(date.getTime()+(params.days*24*60*60*1000));
            var expires = "; expires="+date.toGMTString();
        }
        else var expires = "";

		var cookieVal = "";
		if (value.constructor.name === "Object") {
			for (var key in value) {
				if (cookieVal !== "") {
					cookieVal += '&' + key + '=' + escape(value[key]);
				}
				else {
					cookieVal = key + '=' + escape(value[key]);
				}
			}
		}
		else {
			cookieVal = escape(value);
		}

        var val = name+"="+cookieVal+expires+"; path=" + ((params !== undefined && params.path !== undefined) ? params.path : "/") + ((params !== undefined && params.domain !== undefined) ? ("; domain=" + params.domain) : "");
        document.cookie = val;
    };

    var readCookie = function(name) {

        var nameEQ = name + "=";
        var ca = document.cookie.split(';');
        for(var i=0, len=ca.length;i < len;i++) {
            var c = ca[i];
            while (c.charAt(0)==' ') c = c.substring(1,c.length);
            if (c.indexOf(nameEQ) == 0) return unescape(c.substring(nameEQ.length,c.length));
        }
        return null;
    };

    var eraseCookie = function(name) {
        createCookie(name,"",{days: -1});
    };

    var queryStrToObjLiteral = function(str) {
        //check if there are anchors in the end and trash it
        str = str.split('#')[0];

        var paramArr = str.split('&');
        var objLiteral = {};
        for (var i=0, len=paramArr.length; i<len; i++) {
            var keyValArr = paramArr[i].split('=');
            objLiteral[keyValArr[0]] = keyValArr[1];
        }

        return objLiteral;
    };

    var objLiteralToQueryStr = function(obj) {
        var str = ""
        for (var key in obj) {
            if (str === "") {
                str += key + '=' + obj[key];
            }
            else {
                str += '&' + key + '=' + obj[key];
            }
        }
        return str;
    };

	var checkSessionConfig = function() {

		//this setting will override the cookie values
		if (this.startNewSession === true) {
			return true;
		}
        //if useCookies is false it means we never set the cookie configuration and
        //if startNewSession was not set to false, then we should not start a new session
        else if (this.startNewSession === false && this.useCookies === false) {
            return false;
        }

		var cookieVal = readCookie(this.cookieName);

		if (cookieVal !== null && cookieVal !== "") {


            var urlParam = cookieVal.split('&', 1)[0];
			var url = urlParam.split('=')[1];
            var cookieParams = cookieVal.replace(urlParam + '&', '');
			var currentUrl = window.location.href;

			//completely different url so create new session

			if (currentUrl.match(url) === null) {
                adchemyDebugInfo.append("Different url.  New session.");
				return true;
			}

			if (cookieParams !== "") {

                //remove anchor
                currentUrl = currentUrl.split('#')[0];
                var results = currentUrl.split('?');
                if (results.length > 1) {
                    var params = results[1];
                }
                else {
                    //there's params to remember set in the cookie, but the
                    //url doesn't have any
                    adchemyDebugInfo.append("Params don't match.  New session.");
                    return true;
                }

                var prefMap = queryStrToObjLiteral(cookieVal);
                var currMap = queryStrToObjLiteral(params);

                var hasChanged = false;
                for (var key in prefMap) {
                    //check if such a key exist in the current paramaters
                    if (typeof currMap[key] !== "undefined") {
                        if (prefMap[key] !== currMap[key]) {
                            hasChanged = true;
                            prefMap[key] = currMap[key];
                        }
                    }
                    //the preferred parameter is not in the url at all
                    //excluding key url
                    else if(key !== 'url'){
                    	hasChanged = true;
                    }

                }

                if (hasChanged === true) {
                    eraseCookie(this.cookieName);
                    createCookie(this.cookieName, prefMap, {days:this.cookieDays, domain: this.cookieDomain});
                    adchemyDebugInfo.append("Params don't match.  New session.");
                    return true;
                }

				return false;
			}
            else {
                // No params on current url, but params in the cookie.
                adchemyDebugInfo.append("No params in cookie.  New session.");
                return true;
            }

		}
		else {
            var cookieVals = {
                url : this.sessionUrl
            };

            for (var key in this.sessionParams) {
                cookieVals[key] = this.sessionParams[key];
            }
            createCookie(this.cookieName, cookieVals, {days:this.cookieDays, domain: this.cookieDomain});

			return true;
		}

		return false;
	};
	
	//from: http://www.nczonline.net/blog/2009/07/28/the-best-way-to-load-external-javascript/ and made the callback optional
	var loadScript = function(url, callback) {

	    var script = document.createElement("script")
	    script.type = "text/javascript";

	    if (script.readyState){  //IE
	        script.onreadystatechange = function(){
	            if (script.readyState == "loaded" ||
	                    script.readyState == "complete"){
	                script.onreadystatechange = null;
	                if (callback !== undefined) {
		                callback();
	                }
	            }
	        };
	    } else {  //Others
	        script.onload = function(){
	    		if (callback !== undefined) {
		            callback();
	    		}
	        };
	    }

	    script.src = url;
	    document.getElementsByTagName("head")[0].appendChild(script);
	};
	/**
	 * Sets the mapping of the handlers for a particular component to be run before or after the default handler
	 * @param {String} handlerType A string that indicates whether the handler is to be run before or after. This should be set as 'BEFORE' or 'AFTER'
	 * @param {Object} componentHandlerMap This is the mapping of the component to the custom handlers. The format is as follows:
	 *     <pre>
	 *         var customHandlerMap = {
	 *             'component1' : {
	 *                 success: successHandler,
	 *                 error: errorHandler     
	 *             },
	 *             'component2' : {
	 *                 success: successHandler,
	 *                 error: errorHandler     
	 *             }
	 *         };
	 *     </pre>
	 */
	this.setVariationComponentHandlers = function(handlerType, componentHandlerMap) {
	    this.componentHandlerType = handlerType;
	    this.variationComponentHandlers = componentHandlerMap;
	};
	/**
	 * Sets the configuration for context-sensitive session management.  A page can specify the conditions under which
     * a new session should be established, based on url and/or on request parameters.  For example, a single account
     * may have several pages, each of which may be arrived at with various parameters.
     *
     * <p>Say the user goes to the page
     * foo.html, with parameter of baz='bee' and this is their first visit.  They get a new session.
     *
     * <p>Now they happen to see the same ad again, and navigate again to foo.html with parameter baz='bee'.  If the
     * account is tracking unique ad impressions, then the user should
     * use the same session, not create a new one.
     *
     * <p>Now they see a different ad that happens to point to the same page.  They navigate to foo.html with parameter
     * baz='bop'.  They are navigating to the same landing page, but they saw a different ad, so if the account is tracking
     * ad impressions, then they should start a new session.
     *
	 * @param {String} url The url to match to determine if a new session is to be created.  If the url for a new visit
     * matches this one, the existing session is used if it exists.
	 * @param {Object} params A map of url parameters and their values.
	 * This object literal represents the query string to match in the url. For example, an parameter value
	 * of {'foo' : 'bar'} matches a query string of http://<some url>?foo=bar
	 * @param {Object} cookieParams A map that specifies settings for the context-aware session cookie.
	 * The object can contain three keys: 'days' - The number of days the cookie (and therefore the session) should
     * persist (required), 'domain' - the
	 * domain under which the cookie should be saved (required), and 'suffix' - a suffix for the cookie name (optional).
     * If no suffix is specified then the account id is used.
	 */
	this.setSessionHandlingConfig = function(url, params, cookieParams) {
        this.cookieDays = cookieParams.days;
        this.cookieDomain = cookieParams.domain;
        this.cookieName = 'adchemy_js_' + ((cookieParams.suffix === undefined || cookieParams.suffix === null || cookieParams.suffix === '') ? this.accountId : cookieParams.suffix);
        this.sessionUrl = url;
        this.sessionParams = params;
        this.useCookies = true;
	};
	/**
	 * A single html page can be used to show all the templates in a template group by mapping a different div section
     * to each template.  This function controls that mapping.  If a template group tg1 has three templates in it temp1,
     * temp2 and temp3, then one html page can display any of the three.  Create three div's with id's temp1, temp2, and
     * temp3 and call this function with null parameters.  The VariationFetcher will automatically make visible the div
     * with the id that matches the template chosen by the variation service.
     *
     * <p>If you don't want to make the div id's the same as the template aliases, then you can pass a map to this function,
     * specifying the relationships between div id's and tempalte aliases.  Something like the following:
     * <pre>setMultiDiv({'myDiv1' : 'temp1', 'myDivB' : 'temp2', 'myDivFoo' : 'temp3'}, 'myDiv1');</pre>
     *
	 * @param {Object} divMapping An object literal that represents a mapping between page template aliases and div id's.
	 * @param {String} defDiv The id of the default div id that should be displayed if a load error occurs. If this parameter
	 * is not set then the variationFetcher uses the div Id specified by the first element of the divMapping parameter.
	 */
    this.setMultiDiv = function(divMapping, defDiv) {
    	this.divMap = divMapping;
		if (defDiv === undefined) {
			//create a default div
			for (var key in this.divMap) {
				this.defaultDiv = this.divMap[key];
				break;
			}
		}
		else {
			this.defaultDiv = defDiv;
		}

        this.isMultiDiv = true;
    };
    /**
     * Sets the mapping between the url parameters and visitor context attributes. If restrictToMappedParams is false then the
     * entire query string of the url is passed, but if it is true then only the mapped parameters specified to
     * this method are forwarded on when fetching the variations.
     *
     * @param {Object} urlParams2VisitorContext An map of parameter names to visitor context attribute names that is
     * matched against the url and sent with variation request. For example:
     * <pre>setVisitorContextMapping({'urlParam1': 'VC1', 'urlParam2':'VC2'}); </pre>
     */
    this.setVisitorContextMapping = function(urlParams2VisitorContext) {
        // Should we do some error checking here?
        this.urlParams2VisitorContext = urlParams2VisitorContext;
    };
    /**
     * Sets the default variations to be used when an error occurs fetching 
     * variations
     *
     * @param {Object} defaultVariations A map of component names and the default value for each.  For example:
     * <pre>setDefaultComponentVariations({'c1' : 'This is the default Text for c1', 'c2' : 'This is the default Text for c2' });</pre>
     */
    this.setDefaultComponentVariations = function(defaultVariations) {
    	this.defaultComponentVariations = defaultVariations;
    };
    /**
     * Sets the hostname to be called when requesting variations
     *
     * @param {String} compenentServerHost The component server host
     */
    this.setComponentServerHost = function(componentServerHost) {
        // Should we do some error checking here?
        this.componentServerHost = componentServerHost;
    };
    /**
     * Sets the account id to be used when fetching variations
     *
     * @param {Number} accountId The account id
     */
    this.setAccountId = function(accountId) {
        // validation
        if(!accountId) {
            throw new Error("Null AccountID");
        }
        if(accountId <= 0) {
            throw new Error("Invalid AccountId");
        }
        this.accountId = accountId;
    };

    this.findResponseElementValue = function (components, referenceId) {
        if(components == null) return null;
        for(var i=0, len = components.length; i < len; i++) {
            if (components[i].referenceId == referenceId) {
                return components[i].value;
            }
        }
        return null;
    };

    /**
     * Retrieves Variations from the server and call the client's callback function. Depending on the values specified
     * for startNewSession and setSessionHandlingConfig, this call may start a new session.  Starting a new session
     * forces the server to make new variation decisions.
     *
     * <pre>adchemyVariationFetcher.getVariations(&lt;page group&gt;, &lt;page&gt;, explicitVisitorContext, cb_variations}); </pre>
     *
     * @param {String} pagegroup The page group for which this page is the landing page
     * @param {String} page The page alias of a specific page in the page group.  This parameter is optional.
     * @param {Object} explicitVisitorContext A map of visitor context name/value pairs.  This parameter is optional.
     * @param {Function} cb_variations The callback function to be called after the variations have been fetched.
     * This parameter is optional and should only be used if the page requires custom variation handling/population.
     */
    this.getVariations = function(pagegroup, page, explicitVisitorContext, cb_variations) {
        
        //check if debugging is on through the url
        if (/_avf_debug=true/.test(window.location.href) === true) {
           this.isDebug = true;
        }

        adchemyJsonHelper.timeout = this.timeout;
        if (cb_variations) {
            this.userCallback = cb_variations;
        } else {
            // default variation handler
            this.userCallback = this.variationHandler;
        }

        try {
            adchemyJsonHelper.sendData(this.buildUrl(this.accountId, pagegroup, page, explicitVisitorContext), null, this.completionHandler, this.errorHandler);
        } catch(e) {
            if (adchemyVariationFetcher.isDebug) {
                adchemyDebugInfo.append("ERROR: Exception detected while sending request to variation service: " + e.message);
                adchemyDebugInfo.print();
            }
        }

    };
    /**
     * Sends a request which represents a conversion or other specific event
     * @param {String} eventName The name of the event
     * @param {Object} explicitVisitorContext A map of visitor context name/value pairs.  This parameter is optional.
     */
    this.postEvent = function(eventName, explicitVisitorContext) {

        try {
            adchemyJsonHelper.sendData(this.buildEventUrl(this.accountId, eventName, explicitVisitorContext), null, this.statusHandler, this.errorHandler);
        } catch(e) {
            if (adchemyVariationFetcher.isDebug) {
                adchemyDebugInfo.append("ERROR: Exception detected while sending request to variation service: " + e.message);
                adchemyDebugInfo.print();
            }
        }
    };
    /**
     * This function is called when retrieving variations fails for some reason. Upon failure the default variations are used
     * to populate the page. In the case of a multiple div template the default div id is used to display 
     * the proper div. This method can be overridden.
     */
    this.errorHandler = function() {
        // Call the default variation handler so that the page will show the default variations
        adchemyVariationFetcher.defaultVariationHandler();

        if (adchemyVariationFetcher.isDebug) {
            adchemyDebugInfo.print();
        }
    };

    this.completionHandler = function(CsResponse) {
        // There really shouldn't be any response.  We're just keeping the signature consistent.
        if (adchemyVariationFetcher.isDebug) {
            adchemyDebugInfo.append("Successful post to component server.");
            adchemyDebugInfo.print();
        }
        // Set the session ID so we always have it
        if(CsResponse) {
            adchemyVariationFetcher.sessionId = CsResponse.sessionId;
            adchemyVariationFetcher.lastResponse = CsResponse;
        }
        if(adchemyVariationFetcher.userCallback) {
            adchemyVariationFetcher.userCallback(CsResponse);
        }
    };

    this.statusHandler = function(CsResponse) {
        // There really shouldn't be any response.  We're just keeping the signature consistent.
        if (adchemyVariationFetcher.isDebug) {
            adchemyDebugInfo.append("Successful post to component server.");
            adchemyDebugInfo.print();
        }
    };
    /**
     * Called upon return of the variations from the server, this function performs auto-population of content.
     * The following types of auto-population are supported:
     * <p>
     * <sl>
     * <li>text - the variation contains text that appears on a page. The callback applies the variation by setting innerHTML on the element.
	 * <li>src - the variation contains the url of an image or whatever. The callback applies the variation by setting the src attribute on the element.
	 * <li>class - the variation contains the name of a CSS style. The callback applies the variation by setting the class attribute of the element.
	 * (note that this action is a special case because the way to set the class is not by setting the class attribute but by setting the className attribute.)
     * <li>href - the variation contains a destination url. The callback applies the variation by setting the href attribute of the element.
     * <li>eval - only used on a script tag. It creates a variable named after the alps-component-name and sets it equal to value of the variation which should be a JSON object
     * <li>load - only used on a script tag. It dynamically adds a script tag with the src equal to the variation.
     * </sl>
     * <p>The usage of these properties are follows:
     * <pre>
     * &lt;p id='myparagraph' alps-component-name='textcomponent' alps-variation-target='text'&gt;default content&lt;/p&gt;
     * &lt;img id='myimage' alps-component-name='imageComponent' alps-variation-target='src' src='defaultimagelocation'/&gt;
     * &lt;div id='stylediv' alps-component-name='styleComponent' alps-variation-target='class' class='defaultstylename'&gt;Foo&lt;/div&gt;
     * &lt;a id='myanchor' alps-component-name='linkComponent' alps-variation-target='href' href='defaultlinklocation'&gt;foo&lt;/a&gt;
     * &lt;script type='text/javascript' alps-component-name='myVar' alps-variation-target='eval'&gt;&lt;/script&gt;
     * &lt;script type='text/javascript' alps-component-name='myScript' alps-variation-target='load'&gt;&lt;/script&gt; 
     * </pre>
     * 
     * <p>The method checks whether a component name associated with a redirect is present in the response. If it is, then the user is redirected
     * to the value returned for that component.
     *
     * <p>This method also performs multiple div template logic to determine which
     * div should be displayed for multiDiv pages.  See setMultiDiv for more information.
     * If a mapping is set then the specified values are used to display the proper div. If the mapping is not set then the
     * page reference id of the response from requesting the variations is used as the div id.
     * 
     * <p>If a callbackHandler is specified in the call to getVariations, then this method is not called.  Instead, the
     * specified callback is called.  If you need a custom callback handler but still desire to use the population behavior
     * of this method, you can call variationHandler from within your custom callback. You may also define a callback that 
     * can be called before or after this default handler. These handlers maybe defined using setBeforeVariationComponentHandlers
     * and setAfterVariationComponentHandlers.
     *
     * @param {Object} csResponse the response object from the variation service
     */
    this.variationHandler = function(csResponse) {
        if (adchemyVariationFetcher.isDebug) {
            adchemyDebugInfo.append("Variation Handler - begin updating elements on the page");
            if (typeof csResponse.toSource !== "undefined") {
                adchemyDebugInfo.append("Response object: " + csResponse.toSource());
            }
        }


        // We get called with an object returned from the server in JSON format, so it's now a javascript object
        // This object has the same structure as the CsResponseType structure in XML.  We may want to simplify this
        // for the Javascript environment.  It's probably more information and more nesting than we need here.

        // Check if we got a parameter.  Should always be there.
        if(csResponse)  {
            // Check if there is a page instance returned, and whether it has components.  Should always be there.
            if(csResponse.pageInstance && csResponse.pageInstance.components) {
                var components = csResponse.pageInstance.components;
                var afterComponentVals = {}
                
                //run component handlers defined to run before the default handler
                if (this.componentHandlerType === 'BEFORE') {
	                for(var i=0, len = components.length; i < len; i++) {
	                   var componentName = components[i].referenceId;
	                   if (this.variationComponentHandlers[componentName] !== undefined) {
	                       this.variationComponentHandlers[componentName].success.call(window, csResponse.pageInstance.pageReferenceId, componentName, components[i].value);
	                   }
	                }
                }
                
                var afterComponentVals = {}
                // Loop through the components we got back
                for(var i=0, len = components.length; i < len; i++) {
                    var componentName = components[i].referenceId;
                	if (componentName === this.redirectComponentName) {
                        adchemyDebugInfo.append("Variation Handler - found redirect url.  Redirecting.");
                		window.location = components[i].value;
                		return false;
                	}
                	else {
                	    //check if this component needs to be processed after
	                	if (this.componentHandlerType === 'AFTER' && this.variationComponentHandlers[componentName] !== undefined) {
	                	    afterComponentVals[componentName] = components[i].value;
	                	}
	                	//make  sure this component hasn't already been processed before
	                	else if(this.variationComponentHandlers[componentName] === undefined) {
		                    var cur = this.getElementsToReplace(componentName);
		                    this.replaceElements(cur, componentName, components[i].value);
	                	}
                	}
                }
                
                //run component handlers defined to run after the default handler
                if (this.componentHandlerType === 'AFTER') {
	                for (var key in this.variationComponentHandlers) {
	                    if (this.variationComponentHandlers.hasOwnProperty(key) === true) {
		                    this.variationComponentHandlers[key].success.call(window, csResponse.pageInstance.pageReferenceId, key, afterComponentVals[key])
	                    }
	                }
                }
                

                //Do multi div logic if needed
                if (this.isMultiDiv === true) {

	                var pageRefId = csResponse.pageInstance.pageReferenceId;
                	if (this.divMap === null) {
                		document.getElementById(pageRefId).style.display = 'block';
                	}
                	else {
                		document.getElementById(this.divMap[pageRefId]).style.display = 'block';
                	}
                }
            }
        }
        if (adchemyVariationFetcher.isDebug) {
            adchemyDebugInfo.print();
        }
        return true;
    };

    this.defaultVariationHandler = function() {
        if (this.isDebug) {
            adchemyDebugInfo.append("Applying default Variations");
        }
        
        if (this.componentHandlerType === 'BEFORE') {
	        for (var key in this.defaultComponentVariations) {
	            if (this.variationComponentHandlers[key] !== undefined && this.variationComponentHandlers.hasOwnProperty(key) === true) {
	                this.variationComponentHandlers[key].error.call(window, key, this.defaultComponentVariations[key]);
	            }
	        }
        }
        
        var afterComponentVals = {}
        
        // Loop through the components we got back
        for (var key in this.defaultComponentVariations) {
        	var value = this.defaultComponentVariations[key];

	        if (adchemyVariationFetcher.isDebug) {
	            adchemyDebugInfo.append("Default variation for " + key + " is " + value);
	        }
	        
            //check if this component needs to be processed after
            if (this.componentHandlerType === 'AFTER' && this.variationComponentHandlers[key] !== undefined) {
                afterComponentVals[key] = value;
            }
            //make sure this component hasn't already been processed before
            else if(this.variationComponentHandlers[key] === undefined) {
		        var cur = this.getElementsToReplace(key);
		        this.replaceElements(cur, key, value);
            }
        }
        
        if (this.componentHandlerType === 'AFTER') {
	        for (var key in this.variationComponentHandlers) {
                if (this.variationComponentHandlers.hasOwnProperty(key) === true) {
		            this.variationComponentHandlers[key].error.call(window, key, afterComponentVals[key])
                }
	        }
        }

        //Do multi div logic if needed
        if (this.isMultiDiv === true) {
            if (this.defaultDiv === "") {
                // Nothing to do.  Log an error.
                adchemyDebugInfo.append("Can't enable display in error case for multi-div without a default div id");
            }
			else {
				//defaultDiv will always be set at this point now, either through the map or as a passed in value
				document.getElementById(this.defaultDiv).style.display = 'block';
            }
        }

    };

    /**
     * Retrieves all the elements to replace given the component name mapped
     * @param componentName the Component name mapping
     * @private
     */
    this.getElementsToReplace = function(componentName) {
        var filterFn = function(el) {
            return el.getAttribute("alps-component-name") == componentName;
        };

        //return new GetElementByFilter(filterFn).collect();
        return ElementFilter.collect(filterFn);
    };

    /**
     * Replace the element attribute value with the component value mapped
     * @private
     * @param elements the element to be replace
     * @param component the Component mapping
     * @private
     */
    this.replaceElements = function(elements, componentId, componentValue) {
        if (!elements) {
            adchemyDebugInfo.append("No matching DOM element name or id found for componentid: " + componentId)
            return;
        }

        for (var i=0, len = elements.length; i < len; i++) {
            var replacementType = elements[i].getAttribute("alps-variation-target");
            this.doSingleReplacement(replacementType, componentId, componentValue, elements[i]);

        }
    };

    this.doSingleReplacement = function(replacementType, componentId, componentValue, htmlElement) {
        if(replacementType) {
            replacementType = replacementType.toLowerCase();
        }
        var success = true;
        switch (replacementType) {
            case "text":
                // Change some text
                if (htmlElement.nodeName !== 'TITLE') {
	                htmlElement.innerHTML = componentValue;
		        }
                else {
                	document.title = componentValue;
                }
                break;
            case "class":
                // Set a named style
                htmlElement.className = componentValue;
                break;
            case "eval":
                // evaluate some javascript and set a javascript component name variable to the result.
                try {
                    window[componentId] = eval('(' + componentValue + ')');
                }  catch(err) {
                    adchemyDebugInfo.append("Unable to evaluate javascript component value.  ComponentID: " + componentId);
                }
                break;
            case "load": 
            	//dynamic loading of a js file
            	loadScript(componentValue);
            	break
            case null:
                adchemyDebugInfo.append("Cannot populate DOM element: no adchemy-variation-target attribute on "
                        + " element with name/id '" + componentId + "' and type " + htmlElement.tagName);
                success = false;
	            break;
            default:
                htmlElement.setAttribute(replacementType, componentValue);
                break;
        }
        if (adchemyVariationFetcher.isDebug && success) {
            adchemyDebugInfo.append("Populated " + replacementType + " variation for component " + componentId + " on element of type " + htmlElement.tagName);
        }

    };

    /**
     * Private function to build the url string we'll pass to component server
     * @private
     * @param baseUrl
     * @param accountId
     * @param pagegroup
     * @param page
     * @param explicitVisitorContext
     */
    this.buildUrl = function(accountId, pagegroup, page, explicitVisitorContext) {

        // validation
        if(!accountId) {
            throw new Error("Null AccountID");
        }
        if(accountId <= 0) {
            throw new Error("AccountId must be set");
        }
        if(!pagegroup) {
            throw new Error("Null PageGroup");
        }

        // check to ensure no protocol on host name
        if(!this.componentServerHost || this.componentServerHost.indexOf("://") != -1) {
            throw new Error("Illegal host name: " + this.componentServerHost);
        }

        var urlParamsStr = adchemyVisitorContext.getUrlParametersAsString(this.urlParams2VisitorContext, this.restrictToMappedParms);

        var url = this.componentServerHost + "/alps-cs/content/" + accountId + "/groups/" + pagegroup;

        if (page && page.length > 0) {
            url += "/pages/" + page;
        }
        url += "/pi?alps-output=json&jstracking=" + this.alpsTracking;

        //add host url and referrer
        url += "&hostURL=" + encodeURIComponent(window.location.href) + ((document.referrer !==  "") ? ("&REFERER=" + encodeURIComponent(document.referrer)) : "");

        var isNewSession = checkSessionConfig.call(this);

        if (isNewSession === true) {
            url += "&" + alpsSessionClearStr;
        }

        if (urlParamsStr !== null && urlParamsStr.length > 1) {
            url += "&" + urlParamsStr;
        }

        // Now any extra vc they gave us
        if(explicitVisitorContext) {
            for (var key in explicitVisitorContext) {
                var value = explicitVisitorContext[key];
                if(key !== null && key !== "" && key !== undefined && value !== null && value !== undefined && value !== "") {
                    url += "&" + key + "=" + (value + '');
                }
            }
        }

        if (this.isDebug) {
            adchemyDebugInfo.append("Variations url with visitor context: " + url);
        }

        return url;
    };

    this.buildEventUrl = function(accountId, eventName, explicitVisitorContext) {
        if(!accountId) {
            throw new Error("Null AccountID");
        }
        if(accountId < 0) {
            throw new Error("AccountId must be set");
        }
        if(!eventName) {
            throw new Error("Null EventName");
        }
        // check to ensure no protocol on host name
        if(!this.componentServerHost || this.componentServerHost.indexOf("://") != -1) {
            throw new Error("Illegal host name: " + this.componentServerHost);
        }

        var urlParamsStr = adchemyVisitorContext.getUrlParametersAsString(this.urlParams2VisitorContext, this.restrictToMappedParms);

        var url = this.componentServerHost + "/alps-cs/event/" + accountId + "/" + eventName + "?alps-output=json&jstracking=" + this.alpsTracking;

        if(!this.sessionId) {
            adchemyDebugInfo.append("Null Session ID found when building event posting URL.");
        }
        else {
            url += "&alps-session=" + this.sessionId;
        }

        if (urlParamsStr !== null && urlParamsStr.length > 1) {
            url += "&" + urlParamsStr;
        }

        // Now any extra vc they gave us
        if(explicitVisitorContext) {
            for (var key in explicitVisitorContext) {
                var value = explicitVisitorContext[key];
                if(key !== null && key !== "" && key !== undefined && value !== null && value !== undefined && value !== "") {
                    url += "&" + key + "=" + (value + '');
                }
            }
        }

        if (this.isDebug) {
            adchemyDebugInfo.append("Event url with visitor context: " + url);
        }

        return url;
    }

};

