/**
 * The base JS file from which all other Load Impact JS files inherit namespaces
 * and functionality.
 *
 * Instructions:
 * When creating a new namespace use the following idiom to enable the use
 * of private variables and functions:
 * 
 * LI.NamespaceName = (function() {
 *     var private_var = ...;
 *
 *     function private_function() { ... };
 *
 *     return {
 *         public_function1: function() {
 *         },
 *
 *         public_function2: function() {
 *         },
 *
 *         ...
 *     };
 * })();
 *
 * OR
 *
 * LI.Class = (function() {
 *     function _Class(arg1, ...) {
 *         this.instance_var = ...;
 *     };
 *
 *     function private_function() { ... };
 *
 *     _Class.prototype.public_method1 = function() {
 *         this.instance_var = ...;
 *     };
 *
 *     return _Class;
 * })();
 *
 * OR a mix of both, with classes in a namespace as instantiables or singletons.
 */

// Add browser shims here.
if (!Date.now) {
    // Date.now is from JS 1.5 and hence not supported in older browsers!
    Date.now = function() {
        return +new Date;
    };
}
window.requestAnimFrame = (function() {
    return window.requestAnimationFrame
           || window.webkitRequestAnimationFrame 
           || window.mozRequestAnimationFrame 
           || window.oRequestAnimationFrame 
           || window.msRequestAnimationFrame 
           || function(/* function */ callback, /* DOMElement */ element) {
               window.setTimeout(callback, 1000 / 30);
           };
})();
window.animStartTime = (window.animationStartTime 
                        || window.webkitAnimationStartTime 
                        || window.mozAnimationStartTime 
                        || window.oAnimationStartTime 
                        || window.msAnimationStartTime
                        || Date.now());

// Add jQuery extensions here!
jQuery.fn.reduce = (function(arr, valueInitial, fnReduce) {
    jQuery.each(arr, function(i, value) {
        valueInitial = fnReduce.apply(value, [valueInitial, i, value]);
    });
    return valueInitial;
});

jQuery.fn.serializeObject = (function() {
    var o = {},
        a = this.serializeArray();
    jQuery.each(a, function() {
        if (o[this.name]) {
            if (!o[this.name].push) {
                o[this.name] = [o[this.name]];
            }
            o[this.name].push(this.value || '');
        } else {
            o[this.name] = this.value || '';
        }
    });
    return o;
});

jQuery.fn.sum = (function(arr) {
    var total = 0;
    $.each(arr, function() {
        total += this;
    });
    return total;
});

jQuery.fn.keyDownDelay = function(callback, delay) {
    var self = this;
    delay = (typeof delay == 'undefined') ? 500 : delay;

    this.each(function(k, elem) {
        var timer = null;

        $(this).unbind('keydown').bind('keydown', function(e) {
            window.clearTimeout(timer);

            timer = window.setTimeout(function() { if (callback) callback(e); }, delay);
        });
    });

    return this;
};

jQuery.fn.toggler = function(target, options) {
    var _defaults = {
        state: 'open',

        open_callback: function() {},
        close_callback: function() {},

        animation_done_callback: function(animated_elem, open) {},
    };
    var _options = $.extend({}, _defaults, options);

    var _state_open = _options.state == 'open' ? true : false,
        _target = $(target);

    var _toggle = function(elem, toggle)
    {
        if (_state_open == false)
        {
            $(elem).html('+').qtip('option', 'content.text',
                    LI.Tooltip.tips.click_to_expand.content);
        }
        else
        {
            $(elem).html('-').qtip('option', 'content.text',
                    LI.Tooltip.tips.click_to_close.content);
        }
    }

    this.each(function(k, elem) {
        $(elem).tooltip('click_to_expand');

        _toggle(elem, _state_open);

        $(elem).click(function() {
            if (_state_open == true)
                _options.close_callback();
            else
                _options.open_callback();

            _state_open = !_state_open;
            _target.toggle('slide', { direction: 'up', duration: 500 }, function() {
                _options.animation_done_callback(this, _state_open);
            });

            _toggle(elem, _state_open);

            return false;
        });
    });

    return this;
};

// Extends String
//
// Format strings using: "1 {0} 3 {1}".format("2", "4") => "1 2 3 4"
String.prototype.format = function(args) {
    args = args || {};
    return this.replace(/{([a-zA-Z0-9-_])+}/g, function(m, n) {
        return typeof args[n] != 'undefined' ? args[n] : m;
    });
}

// Create root namespace LI.
if (typeof LI === "undefined") {
    var LI = {};
}

// Set some default values.
LI_LOGGER_LEVEL = undefined;

// Function used to implement inheritence between prototype classes.
var __extends__ = (function(child, parent) {
    var has_prop = Object.prototype.hasOwnProperty;
    for (var key in parent) {
        if (has_prop.call(parent, key)) {
            child[key] = parent[key];
        }
    }
    function ctor() {
        this.constructor = child;
    }
    ctor.prototype = parent.prototype;
    child.prototype = new ctor;
    child.__super__ = parent.prototype;
    return child;
});

LI.Ajax = (function() {
    // Public API:
    return {
        // This function provides a fault-tolerant version of jQuery's $.ajax
        // function. The $.ajax call will, in case of error, be retried a 
        // configurable number of times using truncated binary exponential
        // backoff when calculating the inter-request times.
        ajax_with_exponential_backoff: function(ajax_opts, backoff_opts) {
            var bopts = $.extend({
                'error': function(retries) {},
                'retry_callback': function(retry, secs_to_retry) {},
                'retries': 7
            }, backoff_opts || {});
            var f = (function(retry) {
                if (retry < bopts.retries) {
                    $.ajax(ajax_opts).error(function() {
                        var sleep = LI.Utils.truncated_binary_exponential_backoff(retry);
                        LI.Logger.warning("Retrying AJAX request in " + sleep + 's.');
                        if (bopts.retry_callback) {
                            bopts.retry_callback(retry, sleep);
                        }
                        setTimeout((function() { f(retry + 1) }), sleep * 1000);
                    });
                } else if (bopts.error) {
                    bopts.error(bopts.retries);
                }
            });
            f(0);
        },

        fill_form: function(form_id, get_path, extra_callback) {
            $.getJSON(get_path, function (data) {
                if (data.result == 'ok')
                {
                    $.each(data.fields, function(key, val) {
                        if ($('#' + key).attr('type') == 'checkbox')
                        {
                            if (val == 't')
                            {
                                $('#' + key).attr('checked', 'checked');
                            }
                        }
                        else
                        {
                            $('#' + key).val(val);
                        }
                    });
                }
                else
                {
                    // teleport goats?
                    // TODO: do something useful on failure.
                }

                if (extra_callback != undefined)
                {
                    extra_callback(data);
                }
            });
        },

        // A wrapper around LI.Ajax.ajax_with_exponential_backoff(...) that
        // mimics jQuery's $.getJSON API.
        getJSON_with_exponential_backoff: function(url, data, success,
                                                   opts, backoff_opts) {
            LI.Ajax.ajax_with_exponential_backoff($.extend({
                'url': url,
                'dataType': 'json',
                'data': data,
                'cache': false,
                'success': success
            }, opts || {}), backoff_opts);
        },

        postJSON: function(url, data, callback) {
            $.post(url, data, callback, "json");
        },

        postJSON_with_exponential_backoff: function(url, data, success,
                                                   opts, backoff_opts) {
            LI.Ajax.ajax_with_exponential_backoff($.extend({
                'type': 'POST',
                'url': url,
                'dataType': 'json',
                'data': data,
                'success': success
            }, opts || {}), backoff_opts);
        },

        submitJSON: function(form_id, post_path, extra_callback) {
            var p = $('#' + form_id).serialize();
            //$("#flash-message").html("").hide();
            $("#" + form_id + " span.error").html("");
            $("#" + form_id + " input.error").removeClass("error");
            LI.Ajax.postJSON(post_path, p, function(data) {
                if (data.result == 'ok')
                {
                    if (data.messages)
                    {
                        $.each(data.messages, function(key, val) {
                            //$("#flash-message").html(val).show();
                        });
                    }
                }
                else
                {
                    if (data.errors)
                    {
                        $.each(data.errors, function(key, val) {
                            $("#" + key).addClass('error');
                            $("#error-" + key).html(val);
                        });
                    }
                }

                if (extra_callback != undefined)
                {
                    extra_callback(data);
                }
            });
            return false;
        },

        poll: function(options)
        {
            var _defaults = {
                // Polling interval in ms
                interval: 2000,
                // Max number of polls. 0 is no limit.
                max_polls: 0,
                // Max running time in seconds. 0 is no limit.
                max_seconds: 300,
                // True if "interval" is waited before first poll, else poll is instant
                wait_first_poll: true,

                // Called every successful poll, should return true for polling to stop.
                // Args:
                //   save as $.ajax.success
                success_callback: null,
                // Allows for options to $.ajax to be changed. Return object with new settings.
                // Args:
                //   options: Object with current settnigs
                options_callback: null,
                // Called when exponential back off stops
                error_callback: null,
                // Called when either timeout has been reached
                // Args:
                //   save as $.ajax success
                timeout_callback: null,

                type: 'GET',
                cache: false
            };

            var self = this;
            var timeout = null;
            var poll_count = 0;
            var start = (new Date()).getTime();
            var _options = $.extend({}, _defaults, options);

            _options.success = function(data, status, xhr) {
                poll_count += 1;

                // Check max poll count
                if (_options.max_polls > 0 && poll_count >= _options.max_polls)
                {
                    if (_options.timeout_callback)
                        _options.timeout_callback(data, status, xhr);
                    clearTimeout(timeout);
                    return;
                }

                var now = (new Date()).getTime();
                if (_options.max_seconds > 0 && (now - start) > _options.max_seconds * 1000)
                {
                    if (_options.timeout_callback)
                        _options.timeout_callback(data, status, xhr);
                    clearTimeout(timeout);
                    return;
                }

                // Check if we have met the success condition
                if (_options.success_callback)
                {
                    if (_options.success_callback(data, status, xhr) == true)
                    {
                        clearTimeout(timeout);
                        return;
                    }
                }

                // Check for changes in options
                if (_options.options_callback)
                {
                    var o = _options.options_callback(_options);
                    _options = $.extend(_options, o);
                }
                    
                timeout = setTimeout(_poll, _options.interval);
            };

            var _poll = function() {
                LI.Ajax.ajax_with_exponential_backoff(_options, {
                    retries: 5,
                    error: function(e) {
                        if (_options.error_callback)
                            _options.error_callback();
                        clearTimeout(timeout);
                    }
                });
            }

            // Initial poll
            if (_options.wait_first_poll == true)
                setTimeout(function(){ _poll();}, _options.interval);
            else
                _poll();
        }
    };
})();

LI.ConfigurationManager = (function() {
    function _ConfigurationManager(defaults, change_handlers) {
        this.config = defaults || {};
        this.change_handlers = change_handlers || {};
    };

    // Public API:
    _ConfigurationManager.prototype.get = function(key, default_value) {
        if (key in this.config) {
            return this.config[key];
        }
        return default_value;
    };

    _ConfigurationManager.prototype.getJSON = function() {
        return JSON.stringify(this.config);
    };

    _ConfigurationManager.prototype.set = function(key, value) {
        var old = this.config[key];
        if (JSON.stringify(old) != JSON.stringify(value)) {
            this.config[key] = value;
            if (key in this.change_handlers) {
                this.change_handlers[key](key, value, old);
            }
        }
    };

    return _ConfigurationManager;
})();

LI.LoadtestConfigurationParser = (function() {
    function _LoadtestConfigurationParser(config) {
        this.config = JSON.parse(config);
    }

    // Public API:
    _LoadtestConfigurationParser.prototype.load_zones = function() {
        var load_zones = [];
        $.each(this.tracks(), function(i, t) {
            $.each(t['clips'], function(j, c) {
                load_zones.push(c['loadzone']);
            });
        });
        return load_zones;
    };

    _LoadtestConfigurationParser.prototype.repetitions = function() {
        return this.config['repetitions'] || 8;
    };

    _LoadtestConfigurationParser.prototype.total_duration = function() {
        if ('continuous' != this.type()) {
            return 0;
        }
        var duration = 0,
            track_duration = 0;
        $.each(this.tracks(), function(i, t) {
            track_duration = 0;
            $.each(t['load_schedule'], function(j, l) {
                track_duration += l['duration'];
            });
            if (track_duration > duration) {
                duration = track_duration;
            }
        });
        return duration;
    };

    _LoadtestConfigurationParser.prototype.title = function() {
        return this.config['tracks'] ? this.config['tracks'][0]['title'] : '';
    };

    _LoadtestConfigurationParser.prototype.tracks = function() {
        return this.config['tracks'] || [];
    };

    _LoadtestConfigurationParser.prototype.type = function() {
        return this.config['type'] || 'continuous';
    };

    _LoadtestConfigurationParser.prototype.user_type = function() {
        return this.config['user_type'] || 'sbu';
    };

    return _LoadtestConfigurationParser;
})();

LI.Formatter = (function() {
    // Public API:
    return {
        bps: function(bits_per_sec, display_value, display_unit, decimals) {
            bits_per_sec = Math.abs(bits_per_sec); // Converts exponential values.
            var value = bits_per_sec,
                unit = "bit/s";
            if (display_value == undefined) {
                display_value = true;
            }
            if (display_unit == undefined) {
                display_unit = true;
            }
            if (decimals == undefined || isNaN(decimals)) {
                decimals = 0;
            }
            if (bits_per_sec > 5000000000) {
                value = bits_per_sec / 1000000000;
                unit = "Gbit/s";
            } else if (bits_per_sec > 5000000) {
                value = bits_per_sec / 1000000;
                unit = "Mbit/s";
            } else if (bits_per_sec > 5000) {
                value = bits_per_sec / 1000;
                unit = "Kbit/s";
            }
            value = value.toFixed(decimals);
            var result = "";
            if (display_value) {
                result += value;
            }
            if (result != "" && display_unit) {
                result += " ";
            }
            if (display_unit) {
                result += unit;
            }
            return result;
        },

        bytes: function(bytes, display_value, display_unit, decimals) {
            bytes = Math.abs(bytes); // Converts exponential values.
            var value = bytes,
                unit = "bytes",
                result = "";
            if (display_value == undefined) {
                display_value = true;
            }
            if (display_unit == undefined) {
                display_unit = true;
            }
            if (decimals == undefined || isNaN(decimals)) {
                if (bytes > 1024) {
                    decimals = 2;
                } else {
                    decimals = 0;
                }
            }
            if (bytes > 1099511627776) {
                value = bytes / 1099511627776;
                unit = "TiB";
            } else if (bytes > 1073741824) {
                value = bytes / 1073741824;
                unit = "GiB";
            } else if (bytes > 1048576) {
                value = bytes / 1048576;
                unit = "MiB";
            } else if (bytes > 1024) {
                value = bytes / 1024;
                unit = "KiB";
            }
            value = value.toFixed(decimals);
            if (display_value) {
                result += value;
            }
            if (result != "" && display_unit) {
                result += " ";
            }
            if (display_unit) {
                result += unit;
            }
            return result;
        },

        // Code that simulates PHP's date function from (MIT licensed):
        // http://jacwright.com/projects/javascript/date_format/
        DateTimeReplaceChars: {
            shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
            longMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
            shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
            longDays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],

            // Day
            d: function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); },
            D: function() { return LI.Formatter.DateTimeReplaceChars.shortDays[this.getDay()]; },
            j: function() { return this.getDate(); },
            l: function() { return LI.Formatter.DateTimeReplaceChars.longDays[this.getDay()]; },
            N: function() { return this.getDay() + 1; },
            S: function() { return (this.getDate() % 10 == 1 && this.getDate() != 11 ? 'st' : (this.getDate() % 10 == 2 && this.getDate() != 12 ? 'nd' : (this.getDate() % 10 == 3 && this.getDate() != 13 ? 'rd' : 'th'))); },
            w: function() { return this.getDay(); },
            z: function() { var d = new Date(this.getFullYear(),0,1); return Math.ceil((this - d) / 86400000); }, // Fixed now
            // Week
            W: function() { var d = new Date(this.getFullYear(), 0, 1); return Math.ceil((((this - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now
            // Month
            F: function() { return LI.Formatter.DateTimeReplaceChars.longMonths[this.getMonth()]; },
            m: function() { return (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1); },
            M: function() { return LI.Formatter.DateTimeReplaceChars.shortMonths[this.getMonth()]; },
            n: function() { return this.getMonth() + 1; },
            t: function() { var d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 0).getDate() }, // Fixed now, gets #days of date
            // Year
            L: function() { var year = this.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); },   // Fixed now
            o: function() { var d  = new Date(this.valueOf());  d.setDate(d.getDate() - ((this.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now
            Y: function() { return this.getFullYear(); },
            y: function() { return ('' + this.getFullYear()).substr(2); },
            // Time
            a: function() { return this.getHours() < 12 ? 'am' : 'pm'; },
            A: function() { return this.getHours() < 12 ? 'AM' : 'PM'; },
            B: function() { return Math.floor((((this.getUTCHours() + 1) % 24) + this.getUTCMinutes() / 60 + this.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
            g: function() { return this.getHours() % 12 || 12; },
            G: function() { return this.getHours(); },
            h: function() { return ((this.getHours() % 12 || 12) < 10 ? '0' : '') + (this.getHours() % 12 || 12); },
            H: function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); },
            i: function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); },
            s: function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); },
            u: function() { var m = this.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ?
            '0' : '')) + m; },
            // Timezone
            e: function() { return "Not Yet Supported"; },
            I: function() { return "Not Yet Supported"; },
            O: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + '00'; },
            P: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now
            T: function() { var m = this.getMonth(); this.setMonth(0); var result = this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); this.setMonth(m); return result;},
            Z: function() { return -this.getTimezoneOffset() * 60; },
            // Full Date/Time
            c: function() { return this.format("Y-m-d\\TH:i:sP"); }, // Fixed now
            r: function() { return this.toString(); },
            U: function() { return this.getTime() / 1000; }
        },

        date: function(format, timestamp) {
            var d = null,
                s = '',
                c = null,
                replace = LI.Formatter.DateTimeReplaceChars;
            if ('number' == (typeof timestamp).toLowerCase()) {
                d = new Date(timestamp * 1000)
            } else {
                d = new Date(new Date().getTime() + USER_TIMEZONE_OFFSET_SECS);
            }
            for (var i = 0, l = format.length; i < l; ++i) {
                c = format.charAt(i);
                if (i - 1 >= 0 && format.charAt(i - 1) == "\\") {
                    s += c;
                } else if (replace[c]) {
                    s += replace[c].call(d);
                } else if (c != "\\"){
                    s += c;
                }
            }
            return s;
        },

        // Code that simulates PHP's gmdate function modified from (MIT licensed):
        // http://jacwright.com/projects/javascript/date_format/
        GMDateTimeReplaceChars: {
            shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
            longMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
            shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
            longDays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],

            // Day
            d: function() { return (this.getUTCDate() < 10 ? '0' : '') + this.getUTCDate(); },
            D: function() { return LI.Formatter.GMDateTimeReplaceChars.shortDays[this.getUTCDay()]; },
            j: function() { return this.getUTCDate(); },
            l: function() { return LI.Formatter.GMDateTimeReplaceChars.longDays[this.getUTCDay()]; },
            N: function() { return this.getUTCDay() + 1; },
            S: function() { return (this.getUTCDate() % 10 == 1 && this.getUTCDate() != 11 ? 'st' : (this.getUTCDate() % 10 == 2 && this.getUTCDate() != 12 ? 'nd' : (this.getUTCDate() % 10 == 3 && this.getUTCDate() != 13 ? 'rd' : 'th'))); },
            w: function() { return this.getUTCDay(); },
            z: function() { var d = new Date(this.getUTCFullYear(),0,1); return Math.ceil((this - d) / 86400000); }, // Fixed now
            // Week
            W: function() { var d = new Date(this.getUTCFullYear(), 0, 1); return Math.ceil((((this - d) / 86400000) + d.getUTCDay() + 1) / 7); }, // Fixed now
            // Month
            F: function() { return LI.Formatter.GMDateTimeReplaceChars.longMonths[this.getUTCMonth()]; },
            m: function() { return (this.getUTCMonth() < 9 ? '0' : '') + (this.getUTCMonth() + 1); },
            M: function() { return LI.Formatter.GMDateTimeReplaceChars.shortMonths[this.getUTCMonth()]; },
            n: function() { return this.getUTCMonth() + 1; },
            t: function() { var d = new Date(); return new Date(d.getUTCFullYear(), d.getUTCMonth(), 0).getUTCDate() }, // Fixed now, gets #days of date
            // Year
            L: function() { var year = this.getUTCFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); },   // Fixed now
            o: function() { var d  = new Date(this.valueOf());  d.setDate(d.getUTCDate() - ((this.getUTCDay() + 6) % 7) + 3); return d.getUTCFullYear();}, //Fixed now
            Y: function() { return this.getUTCFullYear(); },
            y: function() { return ('' + this.getUTCFullYear()).substr(2); },
            // Time
            a: function() { return this.getUTCHours() < 12 ? 'am' : 'pm'; },
            A: function() { return this.getUTCHours() < 12 ? 'AM' : 'PM'; },
            B: function() { return Math.floor((((this.getUTCHours() + 1) % 24) + this.getUTCMinutes() / 60 + this.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
            g: function() { return this.getUTCHours() % 12 || 12; },
            G: function() { return this.getUTCHours(); },
            h: function() { return ((this.getUTCHours() % 12 || 12) < 10 ? '0' : '') + (this.getUTCHours() % 12 || 12); },
            H: function() { return (this.getUTCHours() < 10 ? '0' : '') + this.getUTCHours(); },
            i: function() { return (this.getUTCMinutes() < 10 ? '0' : '') + this.getUTCMinutes(); },
            s: function() { return (this.getUTCSeconds() < 10 ? '0' : '') + this.getUTCSeconds(); },
            u: function() { var m = this.getUTCMilliseconds(); return (m < 10 ? '00' : (m < 100 ?
            '0' : '')) + m; },
            // Timezone
            e: function() { return "Not Yet Supported"; },
            I: function() { return "Not Yet Supported"; },
            O: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + '00'; },
            P: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now
            T: function() { var m = this.getUTCMonth(); this.setMonth(0); var result = this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); this.setMonth(m); return result;},
            Z: function() { return -this.getTimezoneOffset() * 60; },
            // Full Date/Time
            c: function() { return this.format("Y-m-d\\TH:i:sP"); }, // Fixed now
            r: function() { return this.toString(); },
            U: function() { return this.getTime() / 1000; }
        },

        gmdate: function(format, timestamp) {
            var d = null,
                s = '',
                c = null,
                replace = LI.Formatter.GMDateTimeReplaceChars;
            if ('number' == (typeof timestamp).toLowerCase()) {
                d = new Date(timestamp * 1000)
            } else {
                d = new Date(new Date().getTime());
            }
            for (var i = 0, l = format.length; i < l; ++i) {
                c = format.charAt(i);
                if (i - 1 >= 0 && format.charAt(i - 1) == "\\") {
                    s += c;
                } else if (replace[c]) {
                    s += replace[c].call(d);
                } else if (c != "\\"){
                    s += c;
                }
            }
            return s;
        },

        datetime_from_epoch: function(timestamp, utc) {
            if (utc) {
                return (new Date(timestamp)).toUTCString();
            } else {
                return (new Date(timestamp)).toLocaleString();
            }
        },

        datetime_relative: function(datetime) {
            return LI.Formatter.time_since_epoch(Math.round(Date.parse(datetime)
                                                            / 1000.0));
        },

        percent: function(sub, total) {
            sub = parseInt(sub);
            total = parseInt(total);
            if (sub == 0 || total == 0) {
                return "0%"
            }
            return (sub / total * 100.0).toFixed(2) + "%";
        },

        rps: function(req_per_sec, decimals) {
            req_per_sec = Math.abs(req_per_sec); // Converts exponential values.
            var value = req_per_sec,
                unit = ' req/s';
            if (value > 1000) {
                value = (value / 1000.0).toFixed(2);
                unit = 'k req/s';
            } else {
                if (decimals == undefined || isNaN(decimals)) {
                    decimals = 0;
                }
                value = value.toFixed(decimals)
            }
            return value + unit;
        },

        time: function(time_ms) {
            time_ms = Math.abs(time_ms); // Converts exponential values.
            var value = time_ms;
            if (60000 <= value) {
                return (value / 60000).toPrecision(2) + 'm';
            } else if (value >= 1000) {
                return (value / 1000).toFixed(2) + 's';
            }
            return value.toFixed(2) + 'ms';
        },

        time_of_day: function(timestamp) {
            var d = new Date(timestamp),
                hours = d.getHours(),
                minutes = d.getMinutes(),
                value = '';
            if (10 > hours) {
                value += '0';
            }
            value += hours + ':';
            if (10 > minutes) {
                value += '0';
            }
            value += minutes;
            return value;
        },

        time_since_epoch: function(timestamp, verbose) {
            var days = Math.floor(timestamp / 86400),
                hours = Math.floor((timestamp % 86400) / 3600),
                minutes = Math.floor(((timestamp % 86400) % 3600) / 60),
                seconds = Math.floor(((timestamp % 86400) % 3600) % 60),
                suffixes = [
                    {
                        'd': [' days', ' day'],
                        'h': [' hours', ' hour'],
                        'm': [' minutes', ' minute'],
                        's': [' seconds', ' second']
                    },
                    {
                        'd': ['d', 'd'],
                        'h': ['h', 'h'],
                        'm': ['m', 'm'],
                        's': ['s', 's']
                    }
                ],
                use_suffixes = suffixes[verbose === false ? 1 : 0],
                value = '';
            if (0 < days) {
                value += ' ' + days + (days > 1
                                       ? use_suffixes['d'][0]
                                       : use_suffixes['d'][1]);
            }
            if (0 < hours) {
                value += ' ' + hours + (hours > 1
                                        ? use_suffixes['h'][0]
                                        : use_suffixes['h'][1]);
            }
            if (0 < minutes) {
                value += ' ' + minutes + (1 < minutes
                                          ? use_suffixes['m'][0]
                                          : use_suffixes['m'][1]);
            }
            if (0 < seconds) {
                value += ' ' + seconds + (1 < seconds
                                          ? use_suffixes['s'][0]
                                          : use_suffixes['s'][1]);
            } else if (0 >= seconds && 0 >= minutes && 0 >= hours && 0 >= days) {
                value += ' 0' + use_suffixes['s'][0];
            }
            return value;
        },

        timezone_to_offset_seconds: function(tz) {
            if (0 == tz) {
                return 0;
            }
            var tzs = tz + '', // Convert to string.
                sign = '-' == tzs[0] ? -1 : 1,
                h = -1 == sign ? tzs[1] : tzs[0],
                m = -1 == sign ? tzs[2] : tzs[1];
            return sign * parseInt(h) * 3600 + ('3' == m ? 1800 : 0);
        }
    }
})();

LI.Logger = (function() {
    var SEVERITY_TO_STRING = {
        0: 'CRITICAL',
        1: 'ERROR',
        2: 'WARNING',
        3: 'INFO',
        4: 'DEBUG'
    };

    function log(severity, msg) {
        if (window.console) {
            /**
             * The LI_LOGGER_LEVEL variable can be defined in global scope (not
             * using "var" infront of the declaration) to specify the logging
             * level of the logger.
             *
             * Example:
             * LI_LOGGER_LEVEL = LI.Logger.ERROR;
             */
            if (LI_LOGGER_LEVEL == undefined || 
                (LI_LOGGER_LEVEL != undefined && LI_LOGGER_LEVEL >= severity)) {
                window.console.log(LI.Formatter.datetime_from_epoch(new Date().getTime())
                                   + ' - [' + SEVERITY_TO_STRING[severity] + ']: '
                                   + msg);
            }
        }
    };

    // Public API:
    return {
        CRITICAL: 0,
        ERROR: 1,
        WARNING: 2,
        INFO: 3,
        DEBUG: 4,

        critical: function(msg) {
            log(LI.Logger.CRITICAL, msg);
        },

        error: function(msg) {
            log(LI.Logger.ERROR, msg);
        },

        warning: function(msg) {
            log(LI.Logger.WARNING, msg);
        },

        info: function(msg) {
            log(LI.Logger.INFO, msg);
        },

        debug: function(msg) {
            log(LI.Logger.DEBUG, msg);
        }
    };
})();

LI.PubSub = (function() {
    function _PubSub() {
        this.subscribers = {};
    };

    // Public API:
    _PubSub.prototype.publish = function(topic, data) {
        if (this.subscribers[topic]) {
            $.each(this.subscribers[topic], function(i, f) {
                f(topic, data);
            });
        }
    };

    _PubSub.prototype.subscribe = function(topic, f) {
        if (!this.subscribers[topic]) {
            this.subscribers[topic] = [];
        }
        this.subscribers[topic].push(f);
    };

    return _PubSub;
})();

LI.ObservableHash = (function() {
    function _ObservableHash(defaults) {
        _ObservableHash.__super__.constructor.apply(this);
        this.hash = defaults || {};
    };
    __extends__(_ObservableHash, LI.PubSub);

    // Public API:
    _ObservableHash.prototype.clear = function() {
        delete this.hash;
        this.hash = {};
    };

    _ObservableHash.prototype.get = function(key, default_value) {
        if (key in this.hash) {
            return this.hash[key];
        }
        return default_value;
    };

    _ObservableHash.prototype.getJSON = function() {
        return JSON.stringify(this.hash);
    };

    _ObservableHash.prototype.set = function(key, value) {
        var old = this.hash[key];
        if (JSON.stringify(old) != JSON.stringify(value)) {
            this.hash[key] = value;
            this.publish(key, value);
        }
    };

    return _ObservableHash;
})();

LI.Utils = (function() {
    // Public API:
    return {
        escape_html: function(html) {
            return html.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
        },

        plural: function(string, count) {
            if (count == 1)
                return string;

            // NOTE: really stupid plural-function
            return string + "s";
        },

        get_url_parameter: function(name) {
            return decodeURIComponent(
                (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,''])[1]
            );
        },

        load_data_table: function(options) {
            var default_settings = {
                'bPaginate': true,
                'bSort': true,
                'iDisplayLength': 25,
                'aLengthMenu': [[10, 25, 50, 100, 500, -1], [10, 25, 50, 100, 500, "All"]],

                'bServerSide': true,

                'aaSorting': [[3, "desc"]],
                'aoColumnDefs': [
                    {
                        'sClass': 'listing-td',
                        'aTargets': ["_all"]
                    },
                    {
                        'bSortable': false,
                        'aTargets': [4]
                    },
                    { 'sName': 'name', 'aTargets': [0] },
                    { 'sName': 'description', 'aTargets': [1] },
                    { 'sName': 'created', 'aTargets': [2] },
                    { 'sName': 'updated', 'aTargets': [3] }
                ],

                "oLanguage": {
                    "sLoadingRecords": "Loading...",
                    "sZeroRecords" : "Empty."
                },
                "oSearch": {"sSearch": ''},
                "sPaginationType": "two_button"
            };

            var settings = $.extend({}, default_settings, options);
            return $('#' + options.sTableId).dataTable(settings);
        },

        resource_type_from_url: function(url) {
            if (url.match(/\.(bmp|gif|ico|jp(e)?g|png|tiff|webp)(\?.*)?$/i)) {
                return 'image';
            } else if (url.match(/\.css(\?.*)?$/i)) {
                return 'stylesheet';
            } else if (url.match(/\.(js|vb)(\?.*)?$/i)) {
                return 'script';
            } else if (url.match(/((\.(asp[x]?|exe|jsp|[s]?htm[l]?|p(hp[1-9]?|html)|py|rb))|\/)(\?.*)?$/i)) {
                return 'document';
            } else if (url.match(/\.(aac|applet|asf|avi|flv|jar|mov|mp[34]|mp[e]?g|swf|wav|xap)(\?.*)?$/i)) {
                return 'embed';
            } else if (url.match(/\.(otf|ttf|woff)(\?.*)?$/i)) {
                return 'font';
            } else if (url.match(/\.(zip|ace|rar|7z|gz|bz2|arc)(\?.*)?$/i)) {
                return 'compressed-archive';
            } else if (url.match(/\.(iso|img|dmg)(\?.*)?$/i)) {
                return 'iso-image';
            } else {
                return 'other';
            }
        },

        resource_type_to_title: function(type) {
            if ('image' == type) {
                return 'Images';
            } else if ('stylesheet' == type) {
                return 'Stylesheets';
            } else if ('script' == type) {
                return 'Scripts';
            } else if ('document' == type) {
                return 'Documents';
            } else if ('embed' == type) {
                return 'Embeds';
            } else if ('font' == type) {
                return 'Fonts';
            } else if ('compressed-archive' == type) {
                return 'Compressed Archives';
            } else if ('iso-image' == type) {
                return 'ISO images';
            } else {
                return 'Other';
            }
        },

        smart_url_truncate: function(url, length) {
            if (!url) {
                return '';
            }
            var r = /(http|https):\/\/(?:\w+:{0,1}\w*@)?([^\/:]+)(:[0-9]+)?((?:\/$)|(?:\/(?:[\w#!:.?+=&%@!\-\/]+)))?/,
                l = length || 50;
            if (r.test(url)) {
                // Truncates in the middle of the URL (the both ends are more
                // interesting than the middle). But prefer to show as much as
                // possible from the last slash to the end.
                if (l < 1) {
                    return url;
                }
                if (url.length <= l) {
                    return url;
                }
                if (l == 1) {
                    return url.substring(0, 1) + '...';
                }
                var mid = Math.ceil(url.length / 2),
                    diff = url.length - l,
                    slash = url.lastIndexOf('/'),
                    end = url.length - slash;
                    lstrip = Math.ceil(diff / 2),
                    rstrip = diff - lstrip,
                    rend = url.length - (mid + rstrip);
                if (end >= l) {
                    return '...' + url.substring(slash, slash + l);
                } else if (end > rend) {
                    rstrip -= end - rend;
                    lstrip += end - rend;
                }
                return url.substring(0, mid - lstrip) + '...'
                       + url.substring(mid + rstrip);
            } else {
                return url.substr(0, l);
            }
        },

        string_ends_with: function(str, suffix) {
            return str.indexOf(suffix, str.length - suffix.length) !== -1;
        },

        string_starts_with: function(str, prefix) {
            return str.indexOf(prefix) === 0;
        },

        truncated_binary_exponential_backoff: function(c) {
            return (Math.pow(2, c) - 1) / 2;
        },

        truncate_middle: function(s, length) {
            if (!s) {
                return '';
            }
            // Truncates in the middle of the string (the both ends are more
            // interesting than the middle).
            var l = length || 50;
            if (l < 1) {
                return s;
            }
            if (s.length <= l) {
                return s;
            }
            if (l == 1) {
                return s.substring(0, 1) + '...';
            }
            var mid = Math.ceil(s.length / 2),
                diff = s.length - l,
                lstrip = Math.ceil(diff / 2),
                rstrip = diff - lstrip;
            return s.substring(0, mid - lstrip) + '...'
                   + s.substring(mid + rstrip);
        },

        code_mirrorify_code_tags: function() {
            $(document).ready(function() {
                $('code').each(function() {
                    var code = $(this).html();

                    $(this).addClass('cm-s-neat');
                    CodeMirror.runMode(code, "lua" , this);
                });
            });
        }
    };
})();

LI.StartTest = (function() {

    var self = null;
    var _Start = function(options)
    {
        options = options || {
            start_test_callback: null
        };
        self = this;

        var callback = null;
        if (options.start_test_callback != undefined)
            callback = options.start_test_callback;

        this.purchase_dialog = null;
        this.purchase_dialog = new LI.Dialogs.PurchaseDialog({
            start_test_callback: callback
        });
    }

    _Start.prototype.init = function(options)
    {
        self.purchase_dialog.showDialog(options);
    }

    _Start.prototype.show_dialog = function(start_dialog, show_options)
    {
        if (start_dialog == true)
        {
            var d = new LI.Dialogs.StartTestDialog(show_options);
        }
        else
        {
            this.purchase_dialog.showDialog(show_options);
        }
    }

    _Start.prototype.check_credits = function(id, url)
    {
        this.id = id;
        var self = this;
        $.ajax({
            url: '/test/config/credits/' + id,
            cache: false,
            dataType: 'json',
            success: function(json) {
                if (json.result == 'ok')
                {
                    $('.credit-total').css('color', 'black').html(json.total_credits);

                    if (json.credits_needed > 0)
                    {
                        var options = {
                            id: id,
                            credits: json.credits,
                            credits_needed: json.credits_needed,
                            total_credits: json.total_credits,
                            url: url != undefined ? url : null
                        };
                        if (json.price) {
                            options.price = json.price;
                        }
                        if (json.price_table)
                            options.price_table = json.price_table;

                        self.purchase_dialog.showDialog(options);
                        self.purchase_dialog.step_one().show({slide:false});
                    }
                    else
                    {
                        var d = new LI.Dialogs.StartTestDialog({
                            credits: json.credits,
                            total_credits: json.total_credits,

                            start_callback: self.on_start
                        });
                    }
                }
            }
        });
    }

    _Start.prototype.on_start = function() {
        self.start(self.id);
    }

    _Start.prototype.start = function(config_id)
    {
        $.getJSON('/test/config/start_test/' + config_id, function(json) {
            if (json.result == 'ok')
            {
                window.location = '/test/view/' + json.test_id;
            }
            else if (json.result == 'failed')
            {
                var d = new LI.Dialogs.AlertDialog(json.message);
            }
        });
    }

    return _Start;
})();

LI.TestAutoCreator = (function() {
    function _TestAutoCreator(csrf_token, options) {
        this.csrf_token = csrf_token;
        this.options = $.extend({
            duration: 15,
            error: null,
            feedback: null,
            only_create_user_scenario: false,
            success: null,
            user_type: 'sbu',
            users: 50,
            start_test: null,
            hidden: false // should script be hidden by default or not?
        }, options || {});
        this.page_analysis_id = null;
        this.url = null;
    };

    // Public API:
    _TestAutoCreator.prototype.create = function(url) {
        this.url = url;
        var self = this;
        $.post('/test/user-scenario/analyze', { url: this.url }, function(json) {
            if (json.result == 'ok' && json.page_analysis_id) {
                self.page_analysis_id = json.page_analysis_id;
                self.poll();
            } else if (json.result == 'error') {
                if (self.options.error) {
                    self.options.error(json.message);
                }
            }
        });
    };

    _TestAutoCreator.prototype.poll = function() {
        var data = {
                only_domain: true,
                url: this.url,
                script_type: 'lua'
            },
            self = this,
            autogen_template = 'Auto-generated ('
                               + (new Date()).toLocaleString() + ')';
        if (self.options.feedback) {
            self.options.feedback('Analyzing target website...');
        }
        LI.Ajax.postJSON('/test/user-scenario/poll/' + self.page_analysis_id, data, function(json) {
            if (json.result == 'ok') {
                if (self.options.feedback) {
                    self.options.feedback('Creating user scenario...');
                }
                self.page_analysis_id = 0;
                data = {
                    autogen: self.options.hidden,
                    name: autogen_template,
                    script_type: 'lua',
                    data_store_id: -1,
                    load_script: json.script,
                    csrf: self.csrf_token
                };
                LI.Ajax.postJSON('/test/user-scenario/create', data, function(json) {
                    if (json.result == 'ok') {
                        if (self.options.only_create_user_scenario) {
                            if (self.options.feedback) {
                                self.options.feedback('User scenario created.');
                            }
                            if (self.options.success) {
                                self.options.success(autogen_template,
                                                     parseInt(json.user_scenario_id),
                                                     null);
                            }
                        } else {
                            if (self.options.feedback) {
                                self.options.feedback('Creating test configuration...');
                            }

                            // Create test configuration based on created user
                            // scenario id.
                            var config = {
                                'type': 'continuous',
                                'user_type': self.options.user_type,
                                'tracks': [{
                                    'title': autogen_template,
                                    'loadzone': 'amazon:us:ashburn',
                                    'clips': [{
                                        'start': 0,
                                        'duration': self.options.duration,
                                        'loadscript': parseInt(json.user_scenario_id),
                                        'percent': 100
                                    }],
                                    'load_schedule': [{
                                        'type': 'ramp',
                                        'duration': self.options.duration,
                                        'users': self.options.users
                                    }]
                                }]
                            };

                            // Create test configuration and a test based on that
                            // test configuration.
                            var j = JSON.stringify(config);
                            $.ajax({
                                type: 'POST',
                                cache: false,
                                dataType: 'json',
                                contentType: 'application/x-www-form-urlencoded',
                                url: '/test/config/create',
                                data: {
                                    autogen: true,
                                    csrf: self.csrf_token,
                                    title: autogen_template,
                                    url: self.url,
                                    config: j
                                },
                                error: function(data, status, xhr) {
                                    if (self.options.error) {
                                        self.options.error(data.message);
                                    }
                                },
                                success: function(data, status, xhr) {
                                    if (self.options.feedback) {
                                        self.options.feedback('Ready to start test.');
                                    }

                                    if (self.options.start_test == null)
                                    {
                                        var s = new LI.StartTest();
                                        s.check_credits(data.config_id);
                                    }
                                    else
                                    {
                                        var s = self.options.start_test();
                                        s.check_credits(data.config_id);
                                    }

                                    self.url = '';
                                    if (self.options.success) {
                                        self.options.success(autogen_template,
                                                             parseInt(json.user_scenario_id),
                                                             parseInt(data.config_id));
                                    }
                                }
                            });
                        }
                    } else if (data.result == 'error') {
                        self.options.error(data.message);
                    } else {
                        if (self.options.error) {
                            self.options.error(json.message);
                        }
                    }
                });
            } else if (json.result == 'error') {
                self.options.error(json.message);
            } else if (json.result == 'working') {
                setTimeout(function() { self.poll(); }, 2000);
            } else {
                if (self.options.error) {
                    self.options.error();
                }
            }
        });
    };

    return _TestAutoCreator;
})();

