
// Translations for en_US
i18n_register({"catalog": {}, "plural": function(n) { return n == 1 ? 0 : 1; }});


Splunk.namespace("util");

Splunk.util = {


    /**
     * Assign empty handlers for logger calls. Overriden by Splunk.Logger if it is imported.
     */
    logger : {
        "info":function(){},
        "log":function(){},
        "debug":function(){},
        "warn":function(){},
        "error":function(){}
    },
    
    /**
     * Converts an object literal to an encoded querystring key/value string.
     *
     */
    propToQueryString: function(dictionary) {
        var o = [];
        var val;
        for (var prop in dictionary) {
            val = '' + dictionary[prop];
            o.push(encodeURIComponent(prop) + '=' + encodeURIComponent(dictionary[prop]));
        }
        return o.join('&');
    },

    /**
     * Converts a flat querystring into an object literal
     *
     */
    queryStringToProp: function(args) {
        args = this.trim(args, '&\?#');

        var parts = args.split('&');
        var output = {};

        var key;
        var value;
        var equalsSegments
        var lim = parts.length;
        for (var i=0,l=lim; i<l; i++) {
            equalsSegments = parts[i].split('=');
            key = decodeURIComponent(equalsSegments.shift());
            value = equalsSegments.join("=");
            output[key] = decodeURIComponent(value);
        }
        return output;
    },

    /**
     * This was ported, rewritten a bit and greatly simplified from the
     * same method in the old Calendar object we used to use.
     * TODO - it is only here temporarily, and we should continue trying to
     * kill it.
     */
    parseDate : function(str, fmt) {

        if ((!str) || (!str.indexOf) || (str.indexOf("mm")==0)) return null;

        var y = 0;
        var m = -1;
        var d = 0;
        var a = str.split(/\W+/);
        var b = fmt.match(/%./g);
        var i = 0, j = 0;
        var hr = 0;
        var min = 0;
        var sec = 0;

        for (i = 0; i < a.length; ++i) {
            if (!a[i])
                continue;
            switch (b[i]) {
                case "%d":
                d = parseInt(a[i], 10);
                break;

                case "%m":
                m = parseInt(a[i], 10) - 1;
                break;

                case "%Y":
                case "%y":
                y = parseInt(a[i], 10);
                (y < 100) && (y += (y > 29) ? 1900 : 2000);
                break;

                case "%H":
                hr = parseInt(a[i], 10);
                break;

                case "%M":
                min = parseInt(a[i], 10);
                break;

                case "%S":
                sec = parseInt(a[i], 10);
                break;
            }
        }
        if (y != 0 && m != -1 && d != 0) {
            var ourDate = new Date(y, m, d, hr, min, sec);
            return ourDate
        } else {
            //this.logger.warn('unable to parse date "' + str + '" into "' + fmt + '"');
            return false
        }
    },
    /**
     * Given a timezone offset in minutes, and  a JS Date object, 
     * returns the delta in milliseconds, of the two timezones.
     * Note that this will include the offset contributions from DST for both.
     */
    getTimezoneOffsetDelta: function(serverOffsetThen, d) {
        if (!Splunk.util.isInt(serverOffsetThen)) {
            return 0;
        }
        // what JS thinks the timezone offset is at the time given by d. This WILL INCLUDE DST
        var clientOffsetThen = d.getTimezoneOffset() * 60;
        // what splunkd told is the actual timezone offset.
        serverOffsetThen     = serverOffsetThen * -60;

        return 1000 * (serverOffsetThen - clientOffsetThen);
    },

    getEpochTimeFromISO: function(isoStr) {
        // lazily init the regex so we only do it only if necessary and only once. 
        if (!this._isoTimeRegex) {
            // Nobody doesnt like ISO.
            this._isoTimeRegex = /([\+\-])?(\d{4,})(?:(?:\-(\d{2}))(?:(?:\-(\d{2}))(?:(?:[T ](\d{2}))(?:(?:\:(\d{2}))(?:(?:\:(\d{2}(?:\.\d+)?)))?)?(?:(Z)|([\+\-])(\d{2})(?:\:(\d{2}))?)?)?)?)?/;
        }
        var m = this._isoTimeRegex.exec(isoStr);
        // put it into a string form that JS Date constructors can actually deal with.
        
        // Being Super Careful: calling substring on undefined variable
        // here throws an exception that kills the stack but doesnt 
        // appear in firebug nor even in the Error Console. 
        var seconds, milliseconds;
        if (m[7]) {
            seconds = m[7].substring(0,2);
            // Note this includes the period.  ie ".003"
            millisecondsStr = m[7].substring(2);
        } else {
            millisecondsStr = "";
        }
        var offset = eval(m[9] + (60*m[10] + parseInt(m[11], 10)));

        var str = sprintf("%s/%s/%s %s:%s:%s", m[3], m[4], m[2], m[5], m[6], seconds);
        // its still wrong, because JS will interpret this time in localtime, 
        // AND if you give IE the timezone part of the string, it passes out in its own vomit. 
        var t = new Date(str);

        // so we patch it.
        t.setTime(t.getTime() + this.getTimezoneOffsetDelta(offset, t));
        var startTime = t.getTime() / 1000;
        
        return startTime + millisecondsStr;
    },

    getConfigValue: function(configKey, optionalDefault) {
        if (window.$C && window.$C.hasOwnProperty(configKey)) return window.$C[configKey];
        else {
            if (typeof optionalDefault != 'undefined') { // ensure optionalDefault can be set to 'false'
                // util.logger will have been swapped out by the Logger when Logger
                // has already been setup, but still works when its not.

                //this.logger.debug('getConfigValue - ' + configKey + ' not set, defaulting to ' + optionalDefault);
                return optionalDefault;
            }

            throw new Error('getConfigValue - ' + configKey + ' not set, no default provided');
        }
    },

    /**
     * Returns a proper path that is relative to the current appserver location.
     * This is critical to ensure that we are proxy compatible. This method
     * takes 1 or more arguments, which will all be stiched together in sequence.
     *
     * Ex: make_url('search/job'); // "/splunk/search/job"
     * Ex: make_url('/search/job'); // "/splunk/search/job"
     * Ex: make_url('/search', '/job'); // "/splunk/search/job"
     * Ex: make_url('/search', '/job', 1234); // "/splunk/search/job/1234"
     *
     * Static paths are augmented with a cache defeater
     *
     * Ex: make_url('/static/js/foo.js'); // "/splunk/static/@12345/js/foo.js"
     * Ex: make_url('/static/js/foo.js'); // "/splunk/static/@12345.1/js/foo.js"
     *
     * @param path {String} The relative path to extend
     *
     * TODO: lots of fancy URL munging
     *
     */
    make_url: function() {
        var output = '', seg, len, output;
        for (var i=0,l=arguments.length; i<l; i++) {
            seg = arguments[i].toString();
            len = seg.length;
            if (len > 1 && seg.charAt(len-1) == '/') {
                seg = seg.substring(0, len-1);
            }
            if (seg.charAt(0) != '/') {
                output += '/' + seg;
            } else {
                output += seg;
            }
        }

        // augment static dirs with build number
        if (output!='/') {
            var segments = output.split('/');
            var firstseg = segments[1];
            if (firstseg=='static' || firstseg=='modules') {
                var postfix = output.substring(firstseg.length+2, output.length);
                output = '/'+firstseg+'/@' + window.$C['BUILD_NUMBER'];
                if (window.$C['BUILD_PUSH_NUMBER']) output += '.' + window.$C['BUILD_PUSH_NUMBER'];
                if (segments[2] == 'app') 
                    output += ':'+this.getConfigValue('APP_BUILD', 0);
                output += '/' + postfix;
            }
        }

        var root = Splunk.util.getConfigValue('MRSPARKLE_ROOT_PATH', '/');
        var locale = Splunk.util.getConfigValue('LOCALE', 'en-US');
        if (root == '' || root == '/') {
            return '/' + locale + output;
        } else {
            return root + '/' + locale + output;
        }
    },

    /**
     * Given a path and a dictionary of options, builds a qualified query string.
     *
     * @param uri {String} required; path to endpoint. eg. "search/jobs"
     * @param options {Object} key / value par of query params eg. {'foo': 'bar'}
     */
    make_full_url: function(url, options) {
        url = this.make_url(url);
        if (options) url = url + '?' + this.propToQueryString(options);
        return url;
    },

    /**
     * Redirects user to a new page.
     *
     * @param uri {String} required
     * @param options {Object} containing parameters like:
     *         sid => attaches optional sid in valid format
     *         s => attaches optional saved search name
     *         q => attaches optional search string in valid format
     *
     *         Example:
     *             util.redirect_to('app/core/search', {
     *                 'sid' : 1234,
     *                 'foo' : 'bar'
     *             });
     *
     *             redirects to 'splunk/app/core/search?sid=1234&foo=bar'
     * @param windowObj {Window Object} an optional window object to target the location change 
     * @param focus {Boolean} if true, focus is called on windowObj
     */
    redirect_to: function(uri, options, windowObj, focus) {
        uri = this.make_full_url(uri, options);
        if (!windowObj) windowObj = window;
        windowObj.document.location = uri
        if (focus && windowObj.focus) windowObj.focus();
        return;
    },

    /**
     * Returns the current app name (not label).
     */
    getCurrentApp: function() {
        return $(document.body).attr("s:app") || 'UNKNOWN_APP';
    },

    /**
     * Returns the current view name (not label).
     */
    getCurrentView: function() {
        return $(document.body).attr("s:view") || 'UNKNOWN_VIEW';
    },
    /**
     * Returns the current 'displayView' name if it differs from the view name, else returns the current view name.
     */
    getCurrentDisplayView: function() {
        return $(document.body).attr("s:displayview") || this.getCurrentView();
    },
    getAutoCancelInterval: function() {
        var interval = $(document.body).attr("s:autoCancelInterval");
        if (!interval) {
            this.logger.error("no autoCancelInterval found. Returning 0");
            interval = 0;
        }
        return interval;
    },
    /**
     * Returns the current viewstate ID as requested via the URI parameter
     * 'vs'.  This is embedded in the <body> tag.
     *
     * If no viewstate has been requested, then all parameter writes will
     * go to the default sticky state, keyed by the reserved token '_current'.
     *
     * NOTE: viewstate is also provided to the modules through context resurrection, 
     * And that being the case, the value of this is marginal.
     */
    //getCurrentViewState: function() {
    //    return $(document.body).attr("s:viewstateid") || null;
    //},

    /**
     * Returns a dictionary of all the app, view, and saved search config
     * data that is specified in the current view.  Ex:
     * {
     *    'view': {"template": "builder.html", "displayView": "report_builder_display", "refresh": null, "label": "Display Report", "viewstateId": "*:ft10i02z", "onunloadCancelJobs": false, "id": "report_builder_display"},
     *    'app': {"id": "search", "label": "Search"},
     *    'savedSearch': {"search": "johnvey | timechart count", "name": "jvreport3", "vsid": "*:ft10i02z", "qualifiedSearch": "search  johnvey | timechart count"}
     * }
     */
    getCurrentViewConfig: function() {
        return $.extend({}, Splunk.ViewConfig);
    },
    
    /**
     * Return the path without the localization segment.
     */
    getPath: function() {
        var locale = this.getConfigValue('LOCALE').toString(),
            path = document.location.pathname;

        // if there is no way to figure out the locale, just return pathname
        if (!this.getConfigValue('LOCALE') || path.indexOf(locale) == -1) {
            return document.location.pathname;
        }
        var start = locale.length + path.indexOf(locale);
        return path.slice(start);
    },

    /**
     * Get the cumulative offsetTop for an element.
     *
     * @param {Object} element A DOM element.
     */
    getCumlativeOffsetTop: function(element){
        if(!element) return 0;
        return element.offsetTop + this.getCumlativeOffsetTop(element.offsetParent);
    },

    /**
     * Get the cumulative offsetLeft for an element.
     *
     * @param {Object} element A DOM element.
     */
    getCumlativeOffsetLeft: function(element){
        if(!element) return 0;
        return element.offsetLeft + this.getCumlativeOffsetLeft(element.offsetParent);
    },

    /**
     * Retrieve the amount of content that has been hidden by scrolling down.
     *
     * @type Number
     * @return 0-n value.
     */
    getPageYOffset: function(){
        var pageYOffset = 0;
        if(window.pageYOffset){
            pageYOffset = window.pageYOffset
        }else if(document.documentElement && document.documentElement.scrollTop){
            pageYOffset = document.documentElement.scrollTop
        }
        return pageYOffset;
    },

    /**
     * Retrieve the inner dimensions of the window. This does not work in jQuery.
     * 
     * @type Object
     * @return An object literal having width and height attributes.
     */
    getWindowDimensions: function(){
        return {
            width:(!isNaN(window.innerWidth))?window.innerWidth:document.documentElement.clientWidth||0,
            height:(!isNaN(window.innerHeight))?window.innerHeight:document.documentElement.clientHeight||0
        };
    },
    
    /**
     * Retrieve the computed style from a specified element.
     *
     * @param el
     * @param styleProperty
     * @return The computed style value.
     * @type String
     */
    getComputedStyleHelper: function(el, styleProperty){
        if(el.currentStyle){
            return el.currentStyle[styleProperty];
        }else if(window.getComputedStyle){
            var cssProperty = styleProperty.replace(/([A-Z])/g, "-$1").toLowerCase();
            var computedStyle = window.getComputedStyle(el, "");
            return computedStyle.getPropertyValue(cssProperty)
        }else{
            return "";
        }
    },

    /**
     * Retrieve a GET parameter from the window.location. Type casting is not performed.
     * @param {String} p The param value to retrieve.
     * @param {String} s Optional string to search through instead of window.location.search
     * @return {String || null} The string value or null if it does not exist.
     */
    getParameter: function(p, s){
        s = s || window.location.search;
        if(!s){
            return null;
        }
        if(!(s.indexOf(p+'=')+1)){
            return null;
        }
        return s.split(p+'=')[1].split('&')[0];
    },

    /**
     * Take an RGB value and convert to HEX equivalent.
     * 
     * @param {String} rgb A RGB value following rgb(XXX, XXX, XXX) convention.
     * @type String
     * @return A HEX equivalent for a given RGB value with a leading '#' character.
     */
    getHEX: function(rgb){
        var parts = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
        var hex = (parts[1]<<16|parts[2]<<8|parts[3]).toString(16);
        return "#"+Array(6-hex.length).concat([hex]).toString().replace(/,/g, 0);
    },
    
    /**
     * Take an arbitrary RGB or HEX in long or shorthand notation and normalize to standard long HEX form with leading '#' character.
     * 
     * @param {String} color A RGB or HEX color value in long or short notation.
     * @type String or null
     * @return A fully qualified 6 character hexadecimal value or with leading '#' character or null if it can't be processed.
     */
    normalizeColor: function(color){
        normalizedColor = null
        if(color.charAt(0)==="#"){
            if(color.length===4){
                normalizedColor = color + color.charAt(1) + color.charAt(2) + color.charAt(3);
            }else{
                normalizedColor = color;
            }
        }else{
            try{
                normalizedColor = this.getHEX(color);
            }catch(e){}
        }
        return normalizedColor;
    },
    
    /**
     * innerHTML substitute when it is not fast enough.
     * @param {HTMLObject} target The target DOM element to replace innerHTML content with.
     * @param {String} innerHTML The innerHTML string to add.
     * @return {HTMLObject} The reference to the target DOM element as it may have been cloned and removed.
     */
    turboInnerHTML: function(target, innerHTML) {
        /*@cc_on //innerHTML is faster for IE
            target.innerHTML = innerHTML;
            return target;
        @*/
        var targetClone = target.cloneNode(false);
        targetClone.innerHTML = innerHTML;
        target.parentNode.replaceChild(targetClone, target);
        return targetClone;
    },
    normalizeBoolean: function(test, strictMode) {

        if (typeof(test) == 'string') {
            test = test.toLowerCase();
        }

        switch (test) {
            case true:
            case 1:
            case '1':
            case 'yes':
            case 'on':
            case 'true':
                return true;

            case false:
            case 0:
            case '0':
            case 'no':
            case 'off':
            case 'false':
                return false;

            default:
                if (strictMode) throw TypeError("Unable to cast value into boolean: " + test);
                return test;
        }
    },
    getCommaFormattedNumber: function(nStr) {
        nStr += '';
        var x = nStr.split('.');
        var x1 = x[0];
        var x2 = x.length > 1 ? '.' + x[1] : '';
        var rgx = /(\d+)(\d{3})/;
        while (rgx.test(x1)) {
            x1 = x1.replace(rgx, '$1' + ',' + '$2');
        }
        return x1 + x2;
    },
    
    
    reLTrim: /^[\s\t\r\n]+/,
    reLTrimCommand: /^[\s\t\r\n\|]+/,
    reRNormalize: /[\s\t\r\n]+$/,
    
    /**
     * Returns a fully qualified search string by prepending the 'search'
     * command of unqualified searches.  This method deems strings as unqualified
     * if it does not start with a | or 'search '
     * 
     * @param {boolean} isUserEntered Indicates if 'q' is expected to be unqualified
     */
    addLeadingSearchCommand: function(q, isUserEntered) {
        var workingQ = '' + q;
        workingQ = workingQ.replace(this.reLTrim, '').replace(this.reRNormalize, ' ');
        if (workingQ.substring(0, 1) == '|') {
            return q;
        }
        
        // this is specific to the case where searchstring = 'search ',
        // which we conservatively assume does not constitute a search command
        if (!isUserEntered
            && (workingQ.substring(0, 7) == 'search ' && workingQ.length > 7)) 
        {
            return q;
        }
        return 'search ' + workingQ;
    },
    
    /**
     * Returns an unqualified search string by removing any leading 'search '
     * command.  This method does a simple search at the beginning of the
     * search.
     */
    stripLeadingSearchCommand: function(q) {
        var workingQ = '' + q;
        workingQ = workingQ.replace(this.reLTrimCommand, '');
        if (workingQ.substring(0, 7) == 'search ') {
            return workingQ.substring(7);
        }
        return q;
    },
    
    /**
     * Deserializes a string into a field list.
     */
    stringToFieldList: function(strList) {
        if (typeof(strList) != 'string' || !strList) return [];
        var items = [];
        var field_name_buffer = [];
        var inquote = false;
        var str = $.trim(strList);
        for (var i=0,j=str.length; i<j; i++) {
            if (str.charAt(i) == '\\') {
                var nextidx = i+1;
                if (j > nextidx && (str.charAt(nextidx) == '\\' || str.charAt(nextidx) == '"')) {
                    field_name_buffer.push(str.charAt(nextidx));
                    i++;
                    continue;
                } else {
                    field_name_buffer.push(str.charAt(i));
                    continue;
                }
            }

            if (str.charAt(i) == '"') {
                if (!inquote) {
                    inquote = true;
                    continue;
                } else {
                    inquote = false;
                    items.push(field_name_buffer.join(''));
                    field_name_buffer = [];
                    continue;
                }
            }

            if ((str.charAt(i) == ' ' || str.charAt(i) == ',') && !inquote) {
                if (field_name_buffer.length > 0) {
                    items.push(field_name_buffer.join(''));
                }
                field_name_buffer = [];
                continue;
            }
            field_name_buffer.push(str.charAt(i));
        }
        if (field_name_buffer.length > 0) items.push(field_name_buffer.join(''));
        return items;
    },


    /**
     * Serializes a field list array into a string.
     */
    _sflQuotable: /([\\",\s])/,
    _sflEscapable: /([\\"])/g,
    fieldListToString: function(fieldArray) {
        if (!fieldArray) return '';
        var output = [];
        for (var i=0,L=fieldArray.length; i<L; i++) {
            var v = $.trim(fieldArray[i]);
            if (v != '') {
                // Escape any char with the backslash.
                if (v.search(this._sflEscapable) > -1) {
                    v = v.replace(this._sflEscapable, "\\$1");
                }

                // Quote the entire string if a backslash, comma, space
                // or double quote is present.
                if (v.search(this._sflQuotable) > -1) {
                    v = ['"', v, '"'].join('');
                }

                output.push(v);
            }
        }
        return output.join(',');
    },


    /**
     * Compare the likeness of two objects. Please use with discretion.
     */
    objectSimilarity: function(obj1, obj2){
            if(obj1 instanceof Array && obj2 instanceof Array){
                    if(obj1.length!==obj2.length){
                       return false;
                    }else{
                        for(var i=0; i<obj1.length; i++){
                            if(!this.objectSimilarity(obj1[i], obj2[i])){
                                return false;
                            }
                        }
                    }
            }else if(obj1 instanceof Object && obj2 instanceof Object){
                if(obj1!=obj2){
                    for(var i in obj2){
                        if(!obj1.hasOwnProperty(i)){
                            return false;
                        }
                    }
                    for(var i in obj1){
                        if(obj1.hasOwnProperty(i)){
                            if(obj2.hasOwnProperty(i)){
                                if(!this.objectSimilarity(obj1[i], obj2[i])){
                                    return false;
                                }
                            }else{
                                return false;
                            }
                        }
                    }
                }
            }else if(typeof(obj1)==="function" && typeof(obj2)==="function"){
                if(obj1.toString()!==obj2.toString()){
                    return false;
                }
            }else if(obj1!==obj2){
                return false;
            }
            return true;
    },
    /**
     * Stop watch class.
     */
    StopWatch: function(){
        var self = this,
            startTime = null,
            stopTime = null,
            times = [];
        var isSet = function(prop){
            return (prop==null)?false:true;
        };
        var isStarted = function(){
            return isSet(startTime);
        };
        var isStopped = function(){
            return isSet(stopTime);
        };
        var softReset = function(){
            startTime = null;
            stopTime = null;
        };
        self.start = function(){
            if(isStarted()){
               throw new Error("cannot call start, start already invoked.");
            }
            startTime = new Date();
        };
        self.stop = function(){
           if(!isStarted()){
               throw new Error("cannot call stop, start not invoked.");
           }
           if(isStopped()){
               throw new Error("cannot call stop, stop already invoked.");
           }
           stopTime = new Date();
           time = stopTime - startTime;
           times.push(time);
        };
        self.pause = function(){
            if(!isStarted()){
               throw new Error("cannot call pause, start not invoked.");
            }
            if(isStopped()){
               throw new Error("cannot call pause, stop already invoked.");
            }
            self.stop();
            softReset();
        };
        self.reset = function(){
            softReset();
            times = [];
        };
        self.time = function(){
            var total = 0;
            for(i=0; i<times.length; i++){
                total += times[i];
            }
            if(isStarted() && !isStopped()){
                total += (new Date() - startTime);
            }
            return total/1000;
        };
    },

    isInt: function(num) {
        return num!=='' && !isNaN(parseInt(num)) && parseInt(num)==(num/1);
    },

    /**
     * Returns a string trimmed to maxLength by removing characters from the
     * middle of the string and replacing with ellipses.
     *
     * Ex: Splunk.util.smartTrim('1234567890', 5) ==> '12...890'
     *
     */
    smartTrim: function(string, maxLength) {
        if (!string) return string;
        if (maxLength < 1) return string;
        if (string.length <= maxLength) return string;
        if (maxLength == 1) return string.substring(0,1) + '...';

        var midpoint = Math.ceil(string.length / 2);
        var toremove = string.length - maxLength;
        var lstrip = Math.ceil(toremove/2);
        var rstrip = toremove - lstrip;
        return string.substring(0, midpoint-lstrip) + '...' + string.substring(midpoint+rstrip);
    },
    _tokenDiscoverer : /\$([^$]+)\$/g,

    /**
     * Finds all instances of any string looking like "$foo$" anywhere in the given object literal.
     * returns an array of all the distinct values it found, eg 'foo'. 
     * if a single string value in the struct has two, like "$foo$ $bar$", duplicates are removed.
     * This will also discover any number of "$foo$" substrings that are found within the 
     * keys of object literals, not just the values.
     */
    discoverReplacementTokens: function(fragment) {
        var keys = [];
        var tokenDiscoverer = Splunk.util._tokenDiscoverer;
        
        if (typeof fragment == 'string') {
            if (fragment.match(tokenDiscoverer)) {
                var keysToAdd = fragment.match(tokenDiscoverer);
                // TODO - im sure there's a way to write the re so that it doesnt include the '$' chars but im moving on.
                for (var i=0; i<keysToAdd.length; i++ ) {
                    keysToAdd[i] = keysToAdd[i].substring(1, keysToAdd[i].length-1);
                }
                return keysToAdd;
            }
            return [];
        }
        else if (typeof fragment == "function") {
            return [];
        }

        // then fragment is not a string. 
        for (var key in fragment) {
            var keysToAdd = [];
            keysToAdd = Splunk.util.discoverReplacementTokens(fragment[key]);
            
            // up until now we've only looked at values. We have to also discover keys in the key itself..
            var matchesInTheKeyItself = key.match(tokenDiscoverer) || [];
            for (var i=0; i<matchesInTheKeyItself.length; i++) {
                // TODO - im sure there's a way to write the re so that it doesnt include the '$' chars but im moving on.
                keysToAdd.push(matchesInTheKeyItself[i].substring(1, matchesInTheKeyItself[i].length-1));
            }
            // check against duplicates.
            for (var i=0; i<keysToAdd.length; i++) {
                if (keys.indexOf(keysToAdd[i]) ==-1) {
                    keys.push(keysToAdd[i]);
                }
            }
        }
        return keys;
    },

    /** 
     * walked through the entirety of fragment to all levels of nesting
     *  and will replace all matches of the given single regex with the given 
     *  single value.
     *  replacement will occur in both keys and values.
     */
    replaceTokens: function(fragment, reg, value) {
        if (typeof fragment == 'string') {
            if (fragment.match(reg)) {
                fragment = fragment.replace(reg, value);
            }
            return fragment;
        }
        else if (typeof fragment == "function") {
            return fragment;
        }
        // watch out for infinite loops.  We make all changes to the array after iteration.
        
        var keysToRename = {};
        for (var key in fragment) {
            // recurse
            if (typeof fragment[key] == 'object') {
                Splunk.util.replaceTokens(fragment[key], reg, value);
            }
            // we have hit a string value.
            else if (typeof fragment[key] == 'string' && fragment[key].match(reg)) {
                fragment[key] = fragment[key].replace(reg, value);
            }
            // now that the value is changed we check the key itself
            if (key.match(reg)) {
                // mark this to be changed after we're out of the iterator
                keysToRename[key] = key.replace(reg, value);
            }
        }
        for (oldKey in keysToRename) {
            var newKey = keysToRename[oldKey];
            fragment[newKey] = fragment[oldKey];
            delete(fragment[oldKey]);
        }
        return fragment;
    },


    getServerTimezoneOffset: function() {
        return Splunk.util.getConfigValue('SERVER_TIMEZONE_OFFSET');
    },

    // constants used by Modules as well as ModuleLoader, to denote runtime states
    // WAITING_FOR_INITIALIZATION and WAITING_FOR_HIERARCHY mean that the Modules 
    // are still being loaded by ModuleLoader. 
    // the remaining two states are relevant BOTH during page load, and in general 
    // at runtime thereafter.  
    // whether or not the page is still loading is an orthogonal piece of information, 
    // and modules can check it on demand by calling Module.isPageLoadComplete().
    moduleLoadStates: {
        WAITING_FOR_INITIALIZATION   : 1,  // waiting for INITIALIZATION
        WAITING_FOR_HIERARCHY   : 2,  // waiting for HIERARCHY
        WAITING_FOR_CONTEXT: 6,
        HAS_CONTEXT         : 7
    },

    /**
     * Returns a wait time (sec) based on the current time elapsed, as mapped
     * onto a cubic easing function.
     *
     * elapsed_time: number of seconds that have elapsed since the first
     *     call to getRetryInterval()
     *
     * min_interval: minimum return value of this method; also the interval
     *     returned when elapsed_time = 0
     *
     * max_interval: maximum return value of this method; also the interval
     *     returned when elapsed_time >= clamp_time
     *
     * clamp_time: total duration over which to calculate a wait time; while
     *     elapsed_time < clamp_time, the return value will be less than
     *     max_interval; when elapsed_time >= clamp_time, the return value will
     *     always be max_interval
     *
     */
    getRetryInterval: function(elapsed_time, min_interval, max_interval, clamp_time) {
        if (elapsed_time >= clamp_time) return parseFloat(max_interval);
        return Math.min(max_interval * Math.pow(elapsed_time/parseFloat(clamp_time), 3) + min_interval, max_interval);
    },
    
    
    /**
     * Returns a string with HTML entities escaped.
     * NOTE: IE will not interpret ""&apos;", opting to just render it encoded
     *      we use the alternate decimal version instead
     *
     */
    escapeHtml: function(input) {
        return input.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    },
    
    /**
     * jQuery closest compat layer, broken for event.target on MSIE (http://tinyurl.com/nbmv8c).
     * 
     * @param {Object} node a jQuery casted element reference.
     * @param {String} expression Standard jQuery expression syntax.
     * @type Object
     * @return A jQuery casted matching element set.
     */
    closest: function(node, expression) {
        if ($.browser.msie) {
            if (node.is(expression)) {
                return node;
            } else {
                while (node.parent().length>0) {
                    var parent = node.parent(expression);
                    if (parent.length>0) {
                        return parent;
                    }
                    node = node.parent();
                }
                return $([]);
            }
        } else {
            return node.closest(expression);
        }
    },
    
    /**
     * From http://blog.stevenlevithan.com/archives/faster-trim-javascript
     * profiler shows this is much faster than the previous implementation in both IE and Firefox.
     * 
     * @param {String} str The string to trim.
     * @param {String} (Optional) delim The characters to remove from the start/end of the string.
     * 
     * @type String
     * @return A trimmed string.
     */
    trim: function(str, delim) {
        if (delim) return str.replace(new RegExp("^[\\s" + delim + "]+"),'').replace(new RegExp("[\\s" + delim + "]+$"), '');
        else return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
    }

}

/**
 * ----------------------
 * Black magic for Prototype's bind() method which we're still using.
 *
 */
var $A = function(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0, length = iterable.length; i < length; i++)
      results.push(iterable[i]);
    return results;
  }
}

Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}
/**
 * ----------------------
 * Prototype augmentation.
 * TODO - find another way.
 *
 */

if (!String.prototype.repeat) {
    String.prototype.repeat = function(count) {
        return new Array(count+1).join(this);
    }
}

if (!String.prototype.reverse) {
    String.prototype.reverse = function() {
        return this.split('').reverse().join('');
    }
}

if (!String.prototype.rsplit) {
    String.prototype.rsplit = function(sep, limit) {
        var sp = this.split(sep);
        if (limit && sp.length > limit) {
            var r = [];
            for(var i=0; i<limit; i++)
                r[i] = sp[sp.length-limit+i];
            return r;
        }
        return sp;
    }
}

if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(search, fromIndex) {
        if (!fromIndex) fromIndex = 0;
        for(var i=0; i<this.length; i++) {
            if (this[i] === search)
                return i;
        }
        return -1;
    }
}

if (!Array.prototype.extend) {
    Array.prototype.extend = function(arr) {
        for(var i=0; i<arr.length; i++)
            this.push(arr[i]);
    }
}

/**
* sprintf routine borrowed from http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_sprintf/
* Licensed under GPL and MIT licenses
*
* Modified by Gareth to add support for Python style argument specifiers:
* sprintf("Hi %(name)s, welcome to %(application)s", { name: 'Gareth', app: 'Splunk })
* Objects holding named arguments can also implement a python style __getitem__ method to return dynamic values
*/
function sprintf( ) {
    // Return a formatted string
    //
    // +    discuss at: http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_sprintf/
    // +       version: 810.1015
    // +   original by: Ash Searle (http://hexmen.com/blog/)
    // + namespaced by: Michael White (http://getsprink.com)
    // +    tweaked by: Jack
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    // *     example 1: sprintf("%01.2f", 123.1);
    // *     returns 1: 123.10

    var regex = /%%|%(\d+\$)?(\([^)]+\))?([-+#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuidfegEG])/g;
    var a = arguments, i = 0, format = a[i++];

    // pad()
    var pad = function(str, len, chr, leftJustify) {
        var padding = (str.length >= len) ? '' : Array(1 + len - str.length >>> 0).join(chr);
        return leftJustify ? str + padding : padding + str;
    };

    // justify()
    var justify = function(value, prefix, leftJustify, minWidth, zeroPad) {
        var diff = minWidth - value.length;
        if (diff > 0) {
            if (leftJustify || !zeroPad) {
                value = pad(value, minWidth, ' ', leftJustify);
            } else {
                value = value.slice(0, prefix.length) + pad('', diff, '0', true) + value.slice(prefix.length);
            }
        }
        return value;
    };

    // formatBaseX()
    var formatBaseX = function(value, base, prefix, leftJustify, minWidth, precision, zeroPad) {
        // Note: casts negative numbers to positive ones
        var number = value >>> 0;
        prefix = prefix && number && {'2': '0b', '8': '0', '16': '0x'}[base] || '';
        value = prefix + pad(number.toString(base), precision || 0, '0', false);
        return justify(value, prefix, leftJustify, minWidth, zeroPad);
    };

    // formatString()
    var formatString = function(value, leftJustify, minWidth, precision, zeroPad) {
        if (precision != null) {
            value = value.slice(0, precision);
        }
        return justify(value, '', leftJustify, minWidth, zeroPad);
    };

    // finalFormat()
    var doFormat = function(substring, valueIndex, valueName, flags, minWidth, _, precision, type) {
        if (substring == '%%') return '%';

        // parse flags
        var leftJustify = false, positivePrefix = '', zeroPad = false, prefixBaseX = false;
        var flagsl = flags.length;
        for (var j = 0; flags && j < flagsl; j++) switch (flags.charAt(j)) {
            case ' ': positivePrefix = ' '; break;
            case '+': positivePrefix = '+'; break;
            case '-': leftJustify = true; break;
            case '0': zeroPad = true; break;
            case '#': prefixBaseX = true; break;
        }

        // parameters may be null, undefined, empty-string or real valued
        // we want to ignore null, undefined and empty-string values
        if (!minWidth) {
            minWidth = 0;
        } else if (minWidth == '*') {
            minWidth = +a[i++];
        } else if (minWidth.charAt(0) == '*') {
            minWidth = +a[minWidth.slice(1, -1)];
        } else {
            minWidth = +minWidth;
        }

        // Note: undocumented perl feature:
        if (minWidth < 0) {
            minWidth = -minWidth;
            leftJustify = true;
        }

        if (!isFinite(minWidth)) {
            throw new Error('sprintf: (minimum-)width must be finite');
        }

        if (!precision) {
            precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type == 'd') ? 0 : void(0);
        } else if (precision == '*') {
            precision = +a[i++];
        } else if (precision.charAt(0) == '*') {
            precision = +a[precision.slice(1, -1)];
        } else {
            precision = +precision;
        }

        // grab value using valueIndex if required?
        if (valueName) {
            valueName = valueName.substr(1, valueName.length-2);
            var value = a[1].__getitem__ ? a[1].__getitem__(valueName) : a[1][valueName];
        } else {
            var value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++];
        }

        switch (type) {
            case 's': return formatString(String(value), leftJustify, minWidth, precision, zeroPad);
            case 'c': return formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, zeroPad);
            case 'b': return formatBaseX(value, 2, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
            case 'o': return formatBaseX(value, 8, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
            case 'x': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
            case 'X': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad).toUpperCase();
            case 'u': return formatBaseX(value, 10, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
            case 'i':
            case 'd': {
                        var number = parseInt(+value);
                        var prefix = number < 0 ? '-' : positivePrefix;
                        value = prefix + pad(String(Math.abs(number)), precision, '0', false);
                        return justify(value, prefix, leftJustify, minWidth, zeroPad);
                    }
            case 'e':
            case 'E':
            case 'f':
            case 'F':
            case 'g':
            case 'G':
                        {
                        var number = +value;
                        var prefix = number < 0 ? '-' : positivePrefix;
                        var method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())];
                        var textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(type) % 2];
                        value = prefix + Math.abs(number)[method](precision);
                        return justify(value, prefix, leftJustify, minWidth, zeroPad)[textTransform]();
                    }
            default: return substring;
        }
    };

    return format.replace(regex, doFormat);
}// }}}

