
// Copyright 2011 Splunk, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

(function() {
    var Splunk = require('./splunk');

    Splunk.namespace("util");

    /****** DON'T CHANGE ANYTHING ABOVE THIS LINE ***********/

    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;
        },

        /**
         * Extracts the fragment identifier value.
         */
        getHash: function(){
        var hashPos = window.location.href.indexOf('#');

        if (hashPos == -1) {
            return "";
        }

        var qPos = window.location.href.indexOf('?', hashPos);

        if (qPos != -1)
            return window.location.href.substr(qPos);

        return window.location.href.substr(hashPos);
        },

        /**
         * 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;

                    default:
                        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;
            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(path) {
            if (path === undefined) {
                path = document.location.pathname;
            }
            var locale = this.getConfigValue('LOCALE').toString();

            // if there is no way to figure out the locale, just return pathname
            if (!this.getConfigValue('LOCALE') || path.indexOf(locale) == -1) {
                return path;
            }
            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).replace(this.reLTrimCommand, '');
            }
            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(',');
        },
        searchEscape: function(str) {
        if (!str.match(/[\s\,=|\[\]\"]/))
            return str;

        return '"' + str.replace(/(\"|\\)/g, "\\$1") + '"';
        },

        /**
         * 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 j in obj2){
                            if(!obj1.hasOwnProperty(j)){
                                return false;
                            }
                        }
                        for(var k in obj1){
                            if(obj1.hasOwnProperty(k)){
                                if(obj2.hasOwnProperty(k)){
                                    if(!this.objectSimilarity(obj1[k], obj2[k])){
                                        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, 10)) && parseInt(num, 10)==(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;
            var keysToAdd;

            if (typeof fragment == 'string') {
                if (fragment.match(tokenDiscoverer)) {
                    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) {
                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 j=0; j<matchesInTheKeyItself.length; j++) {
                    // 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[j].substring(1, matchesInTheKeyItself[j].length-1));
                }
                // check against duplicates.
                for (var k=0; k<keysToAdd.length; k++) {
                    if (keys.indexOf(keysToAdd[k]) ==-1) {
                        keys.push(keysToAdd[k]);
                    }
                }
            }
            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;');
        },

        /**
         * Returns a string with backslashes escaped
         */
        escapeBackslash: function(input) {
            return (""+input).replace(/\\/g, '\\\\');
        },

        /**
         * 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*$/, '');
        },

        focusFirstField: function(popup){ //this puts the focus on the first form element whether an input or select dropdown
            var firstInput = $(":input:visible:enabled:first",popup),
            firstSelect = $("select:visible:enabled:first",popup),
            firstInputOffset = (firstInput.length) ? firstInput.offset().top : false,
            firstSelectOffset = (firstSelect.length) ? firstSelect.offset().top : false,
            firstElem = firstInput;

            if(firstInputOffset && firstSelectOffset){
                if(firstSelectOffset < firstInputOffset){
                    firstElem = firstSelect;
                }
            }
            firstElem.focus();
        }

    };

    /**
     * ----------------------
     * 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
    */
    exports.sprintf = sprintf;
    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;
        var i = 0;
        var format = a[i];
        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;
                default: 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];
                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];
                i++;
            } else if (precision.charAt(0) == '*') {
                precision = +a[precision.slice(1, -1)];
            } else {
                precision = +precision;
            }

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

            var number;
            var prefix;
            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': {
                            number = parseInt(+value, 10);
                            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':
                            {
                            number = +value;
                            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);
    }// }}}
})();