/*
 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
 *
 */
var adchemyDebugInfo = new function() {
    this.info = "";
    this.firstTime = true;
    this.append = function(str) {
        this.info += str + "<br/>";
    };

    this.print = function() {
        var body = document.getElementsByTagName("body")[0];
        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
 *
 */
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
     */
    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
 *
 */
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 = new Object();

        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 = new Object();
        for (var i = 0; i < vars.length; i++) {
            var pair = vars[i].split('=');
            if (pair.length == 2) {
                varsMap[pair[0]] = pair[1];
            }
        }
        return varsMap;

    }
}

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

/**
* Class responsible for getting elements from the current document tree and
* collect all accepted HTML Elements depeding on the filter function.
*
* @class GetElementByFilter
* @param filterFn {function} the function should return <code>true</code> if accepted, <code>false</code> otherwise.
*/
var GetElementByFilter = function(filterFn) {
    
    /**
     * 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
     * @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; i < nodes.length; 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.
     *
     * @method collect
     * @param startEl {element} (optional) start html element, document otherwise.
     * @return the array of all accepted elements
     */
    this.collect = function(startEl) {
        startEl = startEl || document;
        
        return _traverse(startEl);
    };
};

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

/**
 * main class to use to update content with alps variations
 *
 */
var adchemyVariationFetcher = new function() {
    var defaultComponentServerHost = "alps.adchemy.com";
    var defaultTimeout = 300;
    var defaultRestrictToMappedParms = false;
    var alpsSessionClearStr = "alps-session=new";
    var defaultAlpsTracking = true;

    this.accountId = -1;
    this.componentServerHost = defaultComponentServerHost;
    this.timeout = defaultTimeout;
    this.restrictToMappedParms = defaultRestrictToMappedParms;
    this.isDebug = false;
    this.alpsTracking = defaultAlpsTracking;

    this.urlParams2VisitorContext = null;
    this.startNewSession = false;
    this.sessionId = null;
    this.userCallback = this.variationHandler;

    this.setVisitorContextMapping = function(urlParams2VisitorContext) {
        // Should we do some error checking here?
        this.urlParams2VisitorContext = urlParams2VisitorContext;
    };

    this.setComponentServerHost = function(componentServerHost) {
        // Should we do some error checking here?
        this.componentServerHost = componentServerHost;
    }

    this.setAccountId = function(accountId) {
        // validation
        if(!accountId) {
            throw new Error("Null AccountID");
        }
        if(accountId <= 0) {
            throw new Error("Invalid AccountId");
        }
        this.accountId = accountId;
    }

    /** Retrieve Variations from the server and call the client's callback function
     *
     * adchemyVariationFetcher.getVariations(<page group>, <page>, explicitVisitorContext, cb_variations});
     *
     * @param pagegroup
     * @param page
     * @param explicitVisitorContext
     * @param cb_variations
     */
    this.getVariations = function(pagegroup, page, explicitVisitorContext, cb_variations) {

        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();
            }
        }

    };

    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 error handler is a public function so that client can change it if they want to.
     */
    this.errorHandler = function() {
        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;
        }
        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();
        }
    }

    /**
     * Default variation handler.  Can be overridden.
     */
    this.variationHandler = function(csResponse) {
        // do our populating magic here.  Called in the context of our JSON helper
        // So "this" in this function represents the JSON Helper, not the Variation Fetcher
        // Javascript is goofy.
        if (adchemyVariationFetcher.isDebug) {
            adchemyDebugInfo.append("Variation Handler - begin updating elements on the page");
            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;
                // Loop through the components we got back
                for(var i=0; i < components.length; i++) {
                    var cur = this.getElementsToReplace(components[i].referenceId);
                    this.replaceElements(cur, components[i]);
                }
            }
        }
        if (adchemyVariationFetcher.isDebug) {
            adchemyDebugInfo.print();
        }

    };

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

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

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

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

        }
    }

    this.doSingleReplacement = function(replacementType, component, htmlElement) {
        if(replacementType) {
            replacementType = replacementType.toLowerCase();
        }
        var success = true;
        switch (replacementType) {
            case "text":
                // Change some text
                htmlElement.innerHTML = component.value;
                break;
            case "class":
                // Set a named style
                htmlElement.className = component.value;
                break;
            case null:
                adchemyDebugInfo.append("Cannot populate DOM element: no adchemy-variation-target attribute on "
                        + " element with name/id '" + component.referenceId + "' and type " + htmlElement.tagName);
                success = false;
            break;
            default:
                htmlElement.setAttribute(replacementType, component.value);
                break;
        }
        if (adchemyVariationFetcher.isDebug && success) {
            adchemyDebugInfo.append("Populated " + replacementType + " variation for component " + component.referenceId + " on element of type " + htmlElement.tagName);
        }

    }

    /**
     * Private function to build the url string we'll pass to component server
     * @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;

        if (this.startNewSession) {
            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 && key.length > 0 && value && value.length > 0) {
                    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 && key.length > 0 && value && value.length > 0) {
                    url += "&" + key + "=" + value;
                }
            }
        }

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

        return url;
    }

};
