LI.Animation = (function() {
    // Animation queues.
    var _queues = {},
        _render_queue = [],
        _loop_scheduled = false;

    // Private API:
    function _loop(time) {
        var t = Date.now(),
            new_render_queue = [],
            q = null;
        $.each(_render_queue, function(i, n) {
            q = _queues[n];
            if (0 < q.queue.length) {
                var step = q.queue[0];
                if (undefined === step.start) {
                    step.start = Date.now();
                }
                var elapsed = t - step.start,
                    percent = elapsed / step.duration * 100.0;
                step.step(percent, step.step_callback);
                if (elapsed >= step.duration) {
                    q.queue.shift();
                    if (step.done_callback) {
                        step.done_callback();
                    }
                }
                if (q.running && 0 < q.queue.length) {
                    new_render_queue.push(n);
                }
            }
        });
        _render_queue = [];
        if (0 < new_render_queue.length) {
            _render_queue = new_render_queue;
            requestAnimFrame(_loop);
            _loop_scheduled = true;
        } else {
            _loop_scheduled = false;
        }
    };

    // Public API:
    return {
        animate: function(name, step_fun, duration, easing, done_callback,
                          step_callback) {
            var step = {
                'done_callback': undefined === done_callback ? null
                                                               : done_callback,
                'duration': undefined === duration ? 1000 : duration,
                'easing': undefined === easing ? 'linear' : easing,
                'start': undefined,
                'step': step_fun,
                'step_callback': undefined === step_callback ? null
                                                               : step_callback
            };
            if (undefined === _queues[name]) {
                _queues[name] = {
                    'queue': [step],
                    'running': true
                };
            } else {
                _queues[name].queue.push(step);
                _queues[name].running = true;
            }
            _render_queue.push(name);
            if (!_loop_scheduled) {
                requestAnimFrame(_loop);
            }
        },

        stop: function(name, clear, jump_to_end) {
            if (_queues[name]) {
                var clear = undefined === clear ? false : clear;
                var jump_to_end = undefined === jump_to_end ? false
                                                              : jump_to_end;
                _queues[name].running = false;
                _render_queue.splice($.inArray(name, _render_queue), 1);
                if (jump_to_end) {
                    $.each(_queues[name].queue, function(i, s) {
                        s.step(100);
                    });
                }
                if (clear) {
                    _queues[name].queue = [];
                }
            }
        },

        flash_field: function(selector, speed, color) {
            if (speed == undefined)
                speed = 150;
            if (color == undefined)
                color = '#F78B83';

            var org_color = $(selector).css('backgroundColor');
            $(selector).animate({'backgroundColor': color}, {'duration': speed})
                        .animate({'backgroundColor': org_color}, {'duration': speed})
                        .animate({'backgroundColor': color}, {'duration': speed})
                        .animate({'backgroundColor': org_color}, {'duration': speed});
        }
    };
})();

LI.CountDown = (function() {
    function _CountDown(container_id, duration, options) {
        this.container_id = container_id;
        this.duration = duration;
        this.time_left = 0;
        this.running = false;
        this.e = null;
        this.options = $.extend({
            'formatter': null
        }, options || {});
    };

    _CountDown.prototype.set_time_left = function(time_left) {
        this.time_left = time_left;
    };

    _CountDown.prototype.start = function() {
        if (!this.running) {
            this.running = true;
            this.time_left = this.duration;
            if (0 < this.time_left) {
                var self = this;
                setTimeout(function() { self.update(); }, 1000);
            }
        }
    };

    _CountDown.prototype.stop = function() {
        this.running = false;
    };

    _CountDown.prototype.update = function() {
        if (!this.e) {
            this.e = $('#' + this.container_id);
        }
        this.time_left -= 1;
        var time_left = this.time_left,
            self = this;
        if (this.options.formatter) {
            time_left = this.options.formatter(time_left);
        }
        this.e.text(time_left);
        if (0 < this.time_left && this.running) {
            setTimeout(function() { self.update(); }, 1000);
        }
    };

    return _CountDown;
})();

LI.Dialogs = (function() {
    function _default_dialog_options(options) {
        options = options || {};
        // Default options for a model dialog
        var _options = {
            content: {
                text: '',
                title: ''
            },
            position: {
                // Center dialog in window.
                my: 'center',
                at: 'center',
                target: $(window)
            },
            show: {
                effect: function(offset) {
                    $(this).effect('drop', {
                        'direction': 'up',
                        'mode': 'show'
                    });
                },
                ready: true, // Show it straight away.
                modal: {
                    on: true, // Make it modal (darken the rest of the page).
                    blur: false // Don't close the tooltip when clicked.
                }
            },
            hide: false, // We'll hide it manually so disable hide events.
            style: 'ui-tooltip-light ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-dialogue dialog',
            events: {
                render: function(event, api) {
                    // Hide the tooltip when any of the buttons in the dialog
                    // are clicked.
                    $('a.button', api.elements.content).click(api.hide);
                },
                hide: function(event, api) {
                    // Destroy the tooltip once it's hidden as we no longer
                    // need it!
                    api.destroy();
                }
            }
        };

        var o = $.extend(true, {}, _options, options);
        return o;
    }

    function _create_modal(content, title, options) {
        options = options || {};

        var standard = {
            content:
            {
                text: content,
                title: title
            }
        };

        var opt = $.extend(true, {}, standard, options);
        var o = _default_dialog_options(opt);

        /*
        * Since the dialogue isn't really a tooltip as such, we'll use a dummy
        * out-of-DOM element as our target instead of an actual element like
        * document.body.
        */
        return $('<div />').qtip(o);
    };

    var AlertDialog = (function() {
        function _AlertDialog(question, options) {
            this.options = $.extend({
                'callback': null,
                'title': 'Alert',
                'ok_button_text': 'Ok'
            }, options || {});

            // Content will consist of the question and ok/cancel buttons.
            var content = $('<div />'),
                self = this,
                message = $('<p />', { html: question }),
                ok = $('<a />', { 
                    text: this.options.ok_button_text,
                    click: function() {
                        if (self.options.callback) {
                            self.options.callback();
                        }
                        $(document.documentElement).unbind('keypress');
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-last');
            _create_modal(content.append(message).append(ok),
                          this.options.title);
            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    ok.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        return _AlertDialog;
    })();

    var ConfirmDialog = (function() {
        function _ConfirmDialog(question, options, qtip_overrides) {
            this.options = $.extend({
                'callback': null,
                'title': 'Please confirm',
                'ok_button_text': 'Ok',
                'ok_button_extra_class': '',
                'cancel_button_text': 'Cancel',
                'cancel_button_extra_class': ''
            }, options || {});

            qtip_overrides = qtip_overrides || {};
            // Content will consist of the question and ok/cancel buttons.
            var content = $('<div />'),
                self = this,
                message = $('<p />', { html: question }),
                ok = $('<a />', { 
                    text: this.options.ok_button_text,
                    click: function() {
                        if (self.options.callback) {
                            self.options.callback(true);
                        }
                        $(document.documentElement).unbind('keypress');
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-first ' + self.options.ok_button_extra_class),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        if (self.options.callback) {
                            self.options.callback(false);
                        }
                        $(document.documentElement).unbind('keypress');
                        return false;
                    }
                }).addClass('button button-inline button-inline-last ' + self.options.cancel_button_extra_class);
            _create_modal(content.append(message).append(ok).append(cancel),
                          this.options.title, qtip_overrides);
            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    ok.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        return _ConfirmDialog;
    })();

    var PromptDialog = (function() {
        function _PromptDialog(question, options, qtip_overrides) {
            this.options = $.extend({
                'callback': null,
                'initial': '',
                'title': 'Please confirm',
                'ok_button_text': 'Ok',
                'cancel_button_text': 'Cancel'
            }, options || {});
            qtip_overrides = $.extend({
                    events: {
                        visible: function(e,a) {
                            input.focus();
                        }
                    }
            }, qtip_overrides || {});

            // Content will consist of the question and ok/cancel buttons.
            var content = $('<div />'),
                self = this,
                message = $('<p />', { text: question }),
                input = $('<input />', {
                    val: self.options.initial
                }).addClass('input input-large').css('width', '90%').attr('id', 'sdf'),
                message2 = $('<p />', { text: '' }),
                ok = $('<a />', { 
                    text: this.options.ok_button_text,
                    click: function() {
                        if (self.options.callback) {
                            self.options.callback(input.val());
                        }
                        $(document.documentElement).unbind('keypress');
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-first'),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        if (self.options.callback) {
                            self.options.callback(null);
                        }
                        $(document.documentElement).unbind('keypress');
                        return false;
                    }
                }).addClass('button button-inline button-inline-last');
            
            _create_modal(
                content.append(message).append(input).append(message2).append(ok).append(cancel),
                this.options.title,qtip_overrides );
        
            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    ok.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        return _PromptDialog;
    })();

    var ProgressDialog = (function() {
        function _ProgressDialog(message, options) {
            this.options = $.extend({
                'callback': null,
                'id': 'dialog-progressbar',
                'initial': '',
                'title': 'Progress',
                'cancel_button_text': 'Cancel'
            }, options || {});

            // Content will consist of the question and ok/cancel buttons.
            var content = $('<div />'),
                self = this,
                message = message ? $('<p />', { text: message }) : null,
                progressbar = $('<div />').attr('id', this.options.id)
                                          .addClass('progressbar progressbar-single')
                                          .append($('<div />').addClass('progressbar-inner progressbar-single-inner')),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        if (self.options.callback) {
                            self.options.callback();
                        }
                        $(document.documentElement).unbind('keypress');
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-first ie-hack-disable-filter');
            var msie = /(msie) ([\w.]+)/i.exec(navigator.userAgent) == null ? false : true,
                chrome = /(chrome)\/([\w.]+)/i.exec(navigator.userAgent) == null ? false : true,
                safari = /(safari)\/([\w.]+)/i.exec(navigator.userAgent) == null ? false : !chrome;
            this.dialog = _create_modal(
                message ? content.append(message).append(progressbar).append(cancel)
                          : content.append(progressbar).append(cancel),
                this.options.title,
                msie || safari ? { show: { modal: { on: false } } } : {}
            );

            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    cancel.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        _ProgressDialog.prototype.close = function() {
            this.dialog.qtip('destroy');
        };

        _ProgressDialog.prototype.set_progress = function(percent) {
            $('#' + this.options.id + ' div').stop();
            $('#' + this.options.id + ' div').animate({
                    width: Math.min(100, Math.max(0, percent)) + '%'
                }, 1000);
        };

        return _ProgressDialog;
    })();

    var IndeterminateProgressDialog = (function() {
        function _IndeterminateProgressDialog(message, options) {
            this.options = $.extend({
                'callback': null,
                'initial': '',
                'title': 'Progress',
                'cancel_button_text': 'Cancel'
            }, options || {});

            // Content will consist of the question and ok/cancel buttons.
            var content = $('<div />'),
                self = this,
                message = message ? $('<p />', { text: message }) : null,
                progressbar = $('<div />').css('text-align', 'center')
                                          .append('<img src="/static/images/loader.gif" alt="Please wait..." width="24" height="24" />'),
                cancel = $('<a />', {
                    text: this.options.cancel_button_text,
                    click: function() {
                        if (self.options.callback) {
                            self.options.callback();
                        }
                        $(document.documentElement).unbind('keypress');
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-first ie-hack-disable-filter');
            
            this.dialog = _create_modal(
                message ? content.append(message).append(progressbar).append(cancel)
                          : content.append(progressbar).append(cancel),
                this.options.title
            );

            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    cancel.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        _IndeterminateProgressDialog.prototype.close = function() {
            this.dialog.qtip('destroy');
        };

        return _IndeterminateProgressDialog;
    })();

    var EditorChoiceDialog = (function() {
        function _EditorChoiceDialog(options) {
            this.options = $.extend({
                'lua_callback': null,
                'lile_callback': null,

                'title': 'Select editor',
                'lua_button_text': 'Text editor',
                'lile_button_text': 'Graphical editor'
            }, options || {});

            var content = $('<div />'),
                self = this,
                message = $('<p />', {
                    html: "Which script editor would you like to use?<br /><br />"
                        + "We offer a <a href='/learning-center/faq/textedit-intro'>"
                        + "text editor</a> where you type your script manually with your keyboard. "
                        + "You might prefer this if you are a programmer or used to do scripting.<br /><br />" 
                        + "We also offer a <a href='/learning-center/faq/lile-intro'>graphical logic editor</a> "
                        + "where you select the actions to be performed by the simulated clients from menus instead."
                        + "You might prefer this if you are not used to do scripting or if you are new to Load Impact."
                        + "<br /><br />Both editors offer the option to auto-generate a script from a given URL. "
                        + "The <a href='/learning-center/'>recorder</a> is also available in both editors.<br /><br />"
                        + "Feel free to try both before you decide as you can not switch between the two in existing scripts."
                }).addClass('justify'),
                lua = $('<a />', { 
                    html: '<img class="editor-select-button-image" src="/static/images/icon_lua.png" alt="" />' + this.options.lua_button_text,
                    click: function() {
                        if (self.options.lua_callback)
                            self.options.lua_callback();
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-last ie-hack-disable-filter').tooltip('user_scenario_editor_text'),
                lile = $('<a />', {
                    html: '<img class="editor-select-button-image" src="/static/images/icon_lile.png" alt="" />' + this.options.lile_button_text,
                    click: function() {
                        if (self.options.lile_callback)
                            self.options.lile_callback();
                        return false;
                    }
                }).addClass('button button-inline button-inline-last ie-hack-disable-filter').tooltip('user_scenario_editor_lile');

            _create_modal(content.append(message).append(lua).append(lile),
                        this.options.title);

            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    lua.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        return _EditorChoiceDialog;
    })();

    var FreetestDialog = (function() {
        function _FreetestDialog(upper_text, options, qtip_overrides) {
            this.dialog = null;
            this.options = $.extend({
                'callback': null,
                'title': 'Start a free load test',
                'ok_button_text': 'Start free test',
                'ok_button_queue_text': 'Queue free test',
                'cancel_button_text': 'Cancel',
            }, options || {});

            qtip_overrides = qtip_overrides || {
                events: {
                    render: function(event, api) {
                        // Dont close dialog on all button clicks. 
                    },
                }
            };
            // Content will consist of the question and ok/cancel buttons.
            var content = $('<div />'),
                self = this,
                message = $('<p />', { html: upper_text }),
                footer = $('<p />', { html: 
                      '<div id="autogen-status" class="hidden">'
                    + '<img src="/static/images/loader.gif" width="24" height="24" alt="Creating test..." /> <strong><span id="autogen-progress">Creating test...</span></strong>'
                    + '</div>'
                    + '<span class="freetest-terms-text">Running a test means you accept our <a href="/info/user-agreement/" target="_blank">terms of use</a>.</span><br />'
                    + '<span class="freetest-safe">Load Impact is safe - <a href="/learning-center/faq/is-load-impact-safe">read more</a></span>'

                }),
                ok = $('<a />', { 
                    text: this.options.ok_button_text,
                    click: function() {
                        if ($('.button-primary').text() == self.options.ok_button_text)
                        {
                            $.getJSON('/test/list-freetests/1', function (data) {
                                if (data.length >= FREETEST_CAP)
                                {
                                    var queued = 0;
                                    var running = 0;
                                    $.each(data, function(n, test) {
                                        var status_running = 2; 
                                        if (running < 8 && test.status == status_running)
                                        {
                                            var host = test.url.split(/\/+/g)[1];
                                            var row = '<span class="started">Started ' + test.started + ' ago</span>'
                                                    + '<a href="/load-test/' + host + '-' + test.public_id + '">' + host + '</a>';
                                            $('#pick-a-freetest-holder').append(row);
                                            running++;
                                        }
                                        else if (test.status < status_running)
                                        {
                                            queued++;
                                        }
                                    });
                                    var estimated_queue_time = Math.round((queued * FREETEST_DURATION) / FREETEST_CAP);
                                    $('.button-primary').text(self.options.ok_button_queue_text);
                                    $('#queue-wait-time').text(estimated_queue_time);
                                    $('#pick-a-freetest-holder').slideDown(600);
                                }
                                else
                                {
                                    $('#freetest-url').attr('disabled', 'disabled');
                                    $('.button-primary').hide();
                                    $('#autogen-status').show();
                                    auto_creator.create($('#freetest-url').val());
                                }
                            });
                        }
                        else
                        {
                            $('#freetest-url').attr('disabled', 'disabled');
                            $('.button-primary').hide();
                            $('#autogen-status').show();
                            auto_creator.create($('#freetest-url').val());
                        }

                        if (self.options.callback) {
                            self.options.callback(true);
                        }
                        $(document.documentElement).unbind('keypress');
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-first freetest-start button-large'),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        $('#freetest-url').removeAttr('disabled');
                        if (self.options.callback) {
                            self.options.callback(false);
                        }
                        $(document.documentElement).unbind('keypress');
                        self.dialog.qtip('api').hide();
                        return false;
                    }
                }).addClass('button button-inline button-inline-last freetest-cancel right');
            this.dialog = _create_modal(content.append(message).append(ok).append(footer).append(cancel),
                          this.options.title, qtip_overrides);
            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    ok.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        return _FreetestDialog;
    })();

    var StartTestDialog = (function() {
        function _ST(options) {
            
            this.options = $.extend({
                'start_callback': null,
                'content_id': 'start-allowed',
                'buttons': ['start', 'cancel'],

                'credits': 0,
                'total_credits': 0,

                'title': 'Start test',
                'start_button_text': 'Start test',
                'cancel_button_text': 'Cancel'
            }, options || {});

            var content = $('<div />'),
                self = this,
                message = $('<p />').html(
                    $('#' + this.options.content_id).html()
                ),
                start = $('<a />', { 
                    text: this.options.start_button_text,
                    click: function() {
                        if (self.options.start_callback)
                            self.options.start_callback();
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-last')
                  .html('<img src="/static/images/control_play_blue.png" alt="Start test"> ' + this.options.start_button_text),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        return false;
                    }
                }).addClass('button button-inline button-inline-last')
                ;
            
            content.append(message)
            if ($.inArray('start', self.options.buttons) > -1)
                content.append(start)
            if ($.inArray('cancel', self.options.buttons) > -1)
                content.append(cancel);


            _create_modal(content, this.options.title, {
                events: {
                    show: function() {
                        $('.credit-amount').html(self.options.credits);
                        $('.credit-total').html(self.options.total_credits);
                    }
                }
            });

            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    start.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        return _ST;
    })();

    var PurchaseDialog = (function() {
        function _Purchase(options) {
            this.dialog = null;
            this.data = {};
            this.options = $.extend({
                'content_id': 'start-not-allowed',

                'start_test_callback': null,

                'title': 'Start test',
                'start_button_text': 'Start test',
                'cancel_button_text': 'Cancel'
            }, options || {});

            var content = $('<div />'),
                self = this,
                message = $('<p />').html(
                    $('#' + this.options.content_id).html()
                ),
                start = $('<a />', { 
                    text: this.options.start_button_text,
                    click: function(e) {
                        if ($(start).hasClass('button-disabled')) {
                            e.preventDefault();
                            return false;
                        }

                        var id = self.data.id;

                        self.dialog.qtip('api').hide();

                        if (self.options.start_test_callback)
                            self.options.start_test_callback(id);
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-last ie-hack-disable-filter')
                  .html('<img src="/static/images/control_play_blue.png" alt="Start test"> ' + this.options.start_button_text)
                  .attr('id', 'dialog-start-test-button'),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        self.dialog.qtip('api').hide();
                        return false;
                    }
                }).addClass('button button-inline button-inline-last ie-hack-disable-filter')
                ;

            content.append( $('#'+this.options.content_id).show()).append(start).append(cancel);

            this.dialog = _create_modal(content, this.options.title, {
                show: {
                    solo: true,
                    ready: false
                },
                style: 'ui-tooltip-light ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-dialogue dialog dialog-800',
                events: {
                    render: function(event, api) {
                    },
                    hide: function(event, api) {
                        $(document.documentElement).unbind('keypress');
                        self.step_one().hide();
                        self.step_two().hide();

                        self.data = {};
                    }
                }
            });

            $('#cancel-purchase-button').live('click', function() {
                self.step_one().show({slide: true});
                return false;
            });

            $('#buy-button-per-test').live('click', function() {
                var credits = self.data.credits_needed;

                self._update_selected_price(credits);
                self.step_two().show({slide: true});
                return false;
            });
            $('#buy-package-button').live('click', function() {

                var credits = $('input[name="package"]:checked').val();

                self._update_selected_price(credits);

                self.step_two().show({slide: true});
                return false;
            });

            $('#confirm-button').live('click', function() {
                if ($(this).hasClass('button-green3-disabled'))
                    return false;

                $(this).addClass('button-green3-disabled').removeClass('button-green3');
                $('#cancel-purchase-button').hide();
                
                $('#purchase-result').html('<img src="/static/images/loader.gif" alt="Processing purchase" /> Processing payment...');
           
                var current_url = window.location.pathname; //+ window.location.search;
                if (self.data.url != undefined && self.data.url != null)
                {
                    current_url = self.data.url;
                }

                var csrf = $('input[name="csrf"]').val();
                var data = {
                    csrf: csrf,
                    credits: self.data.credits_selected,
                    total_credits: self.data.total_credits,
                    id: self.data.id,
                    price: self.data.price_selected,
                    url: current_url
                };

                var credits = $('#credit-to-purchase').val();
                var url = '/payment/buy-now/' + credits + '?inline='+self.options.id;

                LI.Ajax.postJSON(url, data, function(json) {
                    if (json.status == 'ok')
                    {
                        if (json.form)
                        {
                            $('#hidden-form').html(json.form);
                            $('#payment-form').submit();
                        }
                        else
                        {
                            self.purchase_complete({
                                id: json.payment_id,
                                status: 'ok'
                            });
                        }
                    }
                    else if (json.status == 'failed')
                    {
                        $('#purchase-result').html('<h4>' + json.message + '<h4>');
                    }
                });
                return false;
            });
        };

        _Purchase.prototype.step_one = function()
        {
            var self = this;

            return {
                show: function(options) {

                    if (options.slide && options.slide == true) {
                        $('#step2').hide("slide", { direction: "left" }, "fast", function() {
                            $('#step1').show("slide", { direction: "left" }, "fast");
                        });
                    } else {
                        $('#step1').show();
                        $('#step2').hide();
                    }
                },

                hide: function() {
                }
            };
        }

        _Purchase.prototype.step_two = function()
        {
            var self = this;
            
            return {
                show: function(options) {
                    $('#cancel-purchase-button').show();
                    $('#card-images').show();

                    $('#purchase-result').html('');

                    if (options.slide && options.slide == true) {
                        $('#step1').hide("slide", { direction: "left" }, "fast", function() {
                            $('#step2').show("slide", { direction: "left" }, "fast");
                        });
                    } else {
                        $('#step2').show();
                        $('#step1').hide();
                    }
                },

                hide: function() {
                    $('#confirm-button').removeClass('button-green3-disabled').addClass('button-green3');
                    $('#cancel-purchase-button').show();
                }
            };
        }

        _Purchase.prototype.showDialog = function(data)
        {
            var self = this;

            this.data = data;
            var events = this.dialog.qtip('option', 'events');

            if (events.show == null)
            {
                this.dialog.qtip('option', 'events.show', function(e,a) {
                    // Default button
                    $(document.documentElement).keypress(function(e) {
                        if (13 == e.which) {
                            start.trigger('click');
                            e.preventDefault();
                        }
                    });
                    // Disable start button
                    $('#dialog-start-test-button')
                        .removeClass('button-primary')
                        .addClass('button-disabled');

                    if (self.data.payment != undefined)
                    {
                        self.purchase_complete(self.data.payment);
                    }

                    if (self.data.credits != undefined)
                    {
                        $('#credit_to_buy').html(self.data.credits_needed);
                        $('.credit_to_buy').html(self.data.credits_needed);
                        $('.credit-amount').html(self.data.credits);
                    }

                    if (self.data.total_credits != undefined)
                    {
                        if (self.data.credits > self.data.total_credits)
                            $('.credit-total').css('color', 'red');
                        $('.credit-total').html(self.data.total_credits);
                    }

                    if (self.data.price != undefined)
                    {
                        $('.credit-price').html('$' + self.data.price.price);
                        $('.price-price').html('$' + self.data.price.price);
                        $('.price-vat').html('$' + self.data.price.vat);
                        $('.price-total').html('$' + self.data.price.total);
                    }
                });
            }

            this.dialog.qtip('api').show();
        };

        // Private API
        _Purchase.prototype._update_selected_price = function(credits)
        {
            this.data.credits_selected = credits;
            $('#credit-to-purchase').val(credits);

            var price = this.data.price_table[credits];
            this.data.price_selected = price;
            $('.price-price').html('$' + price.price);
            $('.price-vat').html('$' + price.vat);
            $('.price-total').html('$' + price.total);

            var c = credits;
            $('#credits-buy-desc').html('You have selected <span id="credit_to_buy">'+c+'</span> credits');
        }

        _Purchase.prototype.purchase_complete = function(options)
        {
            this.step_two().show({slide:false});

            if (options.status && options.status == 'ok')
            {
                $('#cancel-purchase-button').hide();
                $('#card-images').hide();

                var c = $('#credit_to_buy').html();
                $('#credits-buy-desc').html('You have purchased <span id="credit_to_buy">'+c+'</span> credits which are available in your account now.');

                $('#purchase-result').html(
                    '<div class="success">Purchase successful!</div><div>You can view the <a href="/payment/receipt/'+options.id+'" target="_blank">'
                    +'receipt here</a> or on the <a href="/account/credits" target="_blank">credits page</a>.</div>'
                    );

                // Analytics Goal completion
                if (typeof _gaq != 'undefined') {
                    _gaq.push(['_trackPageview', '/credit/purchase/complete/']);
                }

                // Enable start button
                $('#dialog-start-test-button')
                    .addClass('button-primary')
                    .removeClass('button-disabled');

                $('#confirm-button').addClass('button-green3-disabled').removeClass('button-green3');
            }
            else
            {
                $('#confirm-button').removeClass('button-green3-disabled').addClass('button-green3');
                $('#purchase-result').html('<h4 class="failed">Payment failed!<h4>');
            }

           $('#cancel-purchase-button').hide();

        }

        return _Purchase;
    })();

    // Pricing dialog
    var PricingDialog = (function() {
        function _Pricing(options) {
            this.dialog = null;
            this.data = {};
            this.options = $.extend({
                'content_id': 'start-not-allowed',


                'title': 'Confirm purchase',
                'start_button_text': 'Start test',
                'cancel_button_text': 'Cancel'
            }, options || {});

            var content = $('<div />'),
                self = this,
                message = $('<p />').html(
                    $('#' + this.options.content_id).html()
                ),
                start = $('<a />', { 
                    text: this.options.start_button_text,
                    click: function(e) {
                        self.dialog.qtip('api').hide();
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-last')
                  .attr('id', 'dialog-start-test-button'),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        self.dialog.qtip('api').hide();
                        return false;
                    }
                }).addClass('button button-inline button-inline-last')
                ;

            content.append( $('#'+this.options.content_id).show());//.append(start).append(cancel);

            this.dialog = _create_modal(content, this.options.title, {
                show: {
                    solo: true,
                    ready: false
                },
                style: 'ui-tooltip-light ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-dialogue dialog dialog-800',
                events: {
                    render: function(event, api) { },
                    hide: function(event, api) {

                        self.data = {};
                    }
                }
            });

            $('#cancel-purchase-button').live('click', function() {
                self.dialog.qtip('api').hide();
                return false;
            });
            $('#confirm-button').live('click', function() {
                if ($(this).hasClass('button-done'))
                {
                    self.dialog.qtip('api').hide();
                    return false;
                }

                if ($(this).hasClass('button-green3-disabled'))
                    return false;

                $(this).addClass('button-green3-disabled').removeClass('button-green3');
                $('#purchase-result').html('<img src="/static/images/loader.gif" alt="Processing purchase" /> Processing payment...');

                var current_url = window.location.pathname;
                var credits = self.data.credits;
                var csrf = $('input[name="csrf"]').val();
                var data = {csrf: csrf, credits: credits, price: self.data.price_table[credits], url: current_url};

                var url = '/payment/buy-now/' + credits;

                LI.Ajax.postJSON(url, data, function(json) {
                    if (json.status == 'ok')
                    {
                        if (json.form)
                        {
                            $('#hidden-form').html(json.form);
                            $('#payment-form').submit();
                        }
                        else
                        {
                            self.purchase_complete({
                                id: json.payment_id,
                                status: 'ok'
                            });
                        }
                    }
                    else if (json.status == 'failed')
                    {
                        $('#purchase-result').html('<h4>' + json.message + '<h4>');
                    }
                });

                return false;
            });
        }

        _Pricing.prototype.purchase_complete = function(options)
        {
            if (options.status && options.status == 'ok')
            {
                $('#cancel-purchase-button').hide();
                $('#card-images').hide();

                var c = $('#credit_to_buy').html();
                $('#credits-buy-desc').html('You have purchased <span id="credit_to_buy">'+c+'</span> credits which are available in your account now.');

                $('#purchase-result').html(
                    '<div class="success">Purchase successful!</div><div>You can view the <a href="/payment/receipt/'+options.id+'" target="_blank">'
                    +'receipt here</a> or on the <a href="/account/credits" target="_blank">credits page</a>.</div>'
                    );

                $('#confirm-button').removeClass('button-green3 button-green3-disabled')
                    .addClass('button-wide button-done')
                    .html('Done');
            }
            else
            {
                $('#confirm-button').removeClass('button-green3-disabled').addClass('button-green3');
                $('#purchase-result').html('<h4 class="failed">Payment failed!<h4>');
            }
        }

        _Pricing.prototype.show_dialog = function(options)
        {
            this.reset();

            var self = this,
                events = this.dialog.qtip('option', 'events');

            self.data = options;
            if (events.show == null)
            {
                this.dialog.qtip('option', 'events.show', function(e,a) {
                    if (self.data.price_table != undefined)
                    {
                        $('#credit_to_buy').html(self.data.credits);
                        var p = self.data.price_table[self.data.credits];
                        $('.price-price').html('$' + p.price);
                        $('.price-vat').html('$' + p.vat);
                        $('.price-total').html('$' + p.total);
                    }

                    if (self.data.payment != undefined)
                    {
                        self.purchase_complete(self.data.payment);
                    }

                    $('#step1').hide();
                    $('#step2').show();
                });
            }
            this.dialog.qtip('api').show();
        }

        _Pricing.prototype.reset = function() {
            $('#cancel-purchase-button').show();
            $('#card-images').show();
            $('#credits-buy-desc').html('You have selected <span id="credit_to_buy"></span> credits');

            $('#confirm-button').addClass('button-green3')
                .removeClass('button-wide button-done button-green3-disabled')
                .html('Purchase');
            $('#purchase-result').html('');
            this.data = null;
        }


        return _Pricing;
    })();

    var UnsavedScriptDialog = (function() {
        function _USD(options) {
            
            this.options = $.extend({
                'discard_callback': null,

                'title': 'Discard changes?',
                'text': 'Your script has been modified. Are you sure you want to discard the changes?',
                'exit_button_text': 'Discard changes',
                'cancel_button_text': 'Cancel'
            }, options || {});

            var content = $('<div />'),
                self = this,
                message = $('<p />', {
                    text: self.options.text
                }),
                exit = $('<a />', { 
                    text: this.options.exit_button_text,
                    click: function() {
                        if (self.options.discard_callback)
                            self.options.discard_callback();
                        return false;
                    }
                }).addClass('button button-inline button-inline-last'),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-last')
                ;
            
            content.append(message).append(exit).append(cancel);

            _create_modal(content, this.options.title);

            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    cancel.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        return _USD;
    })();

    var ValidationResultDialog = (function() {
        function _Valid(options) {
            
            this.dialog = null;
            this.options = $.extend({
                'ok_callback': null,
                'show_callback': null,
                'title': 'Validation',
                'text': '',
                'ok_button_text': 'OK'
            }, options || {});

            var content = $('<div />'),
                self = this,
                ok = $('<a />', { 
                    text: this.options.ok_button_text,
                    click: function() {
                        if (self.options.ok_callback)
                            self.options.ok_callback();
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-last')
                ;
            
            content.append($('#test-table').show()).append(ok);

            this.dialog = _create_modal(content, this.options.title, {
                show: {
                    solo: true,
                    ready: true
                },
                style: 'ui-tooltip-light ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-dialogue dialog dialog-800',
                events: {
                    show: function(event, api) {
                        if (self.options.show_callback)
                            self.options.show_callback();

                        $(document.documentElement).keypress(function(e) {
                            if (13 == e.which) {
                                ok.trigger('click');
                                $(document.documentElement).unbind('keypress');
                                e.preventDefault();
                            }
                        });
                    },
                    render: function(event, api) {
                        $('a.button-primary', api.elements.content).click(api.hide);
                    },
                    hide: function(event, api) {
                        //api.destroy();
                    }
                }
            });


        };

        _Valid.prototype.dialog = function() {
            return this.dialog;
        }

        return _Valid;
    })();

    // Replace script
    var ReplaceScriptDialog = (function() {
        function _Replace(options) {
            
            this.options = $.extend({
                'overwrite_callback': null,
                'append_callback': null,

                'append_enabled': true,

                'title': 'Overwrite script?',
                'text': 'Your script has been modified. Are you sure you want to discard the changes?',

                'overwrite_button_text': 'Overwrite script',
                'append_button_text': 'Append script',
                'cancel_button_text': 'Cancel'
            }, options || {});

            var content = $('<div />'),
                self = this,
                message = $('<p />', {
                    text: self.options.text
                }),
                overwrite = $('<a />', { 
                    text: this.options.overwrite_button_text,
                    click: function() {
                        if (self.options.overwrite_callback)
                            self.options.overwrite_callback();
                        return false;
                    }
                }).addClass('button button-primary button-inline button-inline-last'),
                append = $('<a />', { 
                    text: this.options.append_button_text,
                    click: function() {
                        if (self.options.append_callback)
                            self.options.append_callback();
                        return false;
                    }
                }).addClass('button button-inline button-inline-last'),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        return false;
                    }
                }).addClass('button button-inline button-inline-last')
                ;
            
            content.append(message).append(overwrite);
            if (this.options.append_enabled)
                content.append(append)
            content.append(cancel);

            _create_modal(content, this.options.title);

            $(document.documentElement).keypress(function(e) {
                if (13 == e.which) {
                    overwrite.trigger('click');
                    $(document.documentElement).unbind('keypress');
                    e.preventDefault();
                }
            });
        };

        return _Replace;
    })();

    var DataStoreUploadDialog = (function() {
        function _DataStoreUploadDialog(options) {
            
            this.dialog = null;
            this.options = $.extend({
                'ok_callback': null,
                'show_callback': null,
                'title': 'Add new data store',
                'text': '',
                'ok_button_text': 'OK',
                'cancel_button_text': 'Cancel'
            }, options || {});

            var content = $('<div />'),
                self = this,
                ok = $('<a />', { 
                    text: this.options.ok_button_text,
                    click: function() {
                        if (self.options.ok_callback) {
                            if (self.options.ok_callback()) {
                                return false;
                            }
                        }
                        self.dialog.qtip('api').hide();
                        return false;
                    }
                }).addClass('button button-primary button-inline ie-hack-disable-filter'),
                cancel = $('<a />', { 
                    text: this.options.cancel_button_text,
                    click: function() {
                        self.dialog.qtip('api').hide();
                        return false;
                    }
                }).addClass('button button-inline button-inline-last ie-hack-disable-filter');

            // Reset form and add it to dialog.
            $('#data-store-name').val('');
            $('#data-store-file').val('');
            $('#data-store-file').removeClass('error');
            $('#data-store-file-error span').text('');
            $('#data-store-file-error').hide();
            $('#data-store-file-info').hide();
            $('#data-store-upload-form').get(0).reset();
            $('#data-store-csv-preview-table > thead th').remove();
            $('#data-store-csv-preview-table > thead > tr').append('<th class="listing-th">Row</th>');
            $('#data-store-csv-preview-table > tbody tr').remove();
            $('#data-store-csv-preview-table > tbody').append(
                $('<tr/>').append('<td class="listing-td" style="text-align: center; font-style: italic;">CSV content preview. No CSV file has been chosen.</th>'));
            $('#section-toggler-data-stores-advanced').unbind('click');
            $('#section-toggler-data-stores-advanced').toggler('#data-stores-advanced-container', {
                state: 'closed',
                open_callback: function() {},
                close_callback: function() {}
            });
            content.append($('#new-data-store-container').show()).append(ok).append(cancel);

            this.dialog = _create_modal(content, this.options.title, {
                show: {
                    solo: true,
                    ready: true
                },
                style: 'ui-tooltip-light ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-dialogue dialog dialog-800',
                events: {
                    show: function(event, api) {
                        if (self.options.show_callback)
                            self.options.show_callback();

                        $(document.documentElement).keypress(function(e) {
                            if (13 == e.which) {
                                ok.trigger('click');
                                $(document.documentElement).unbind('keypress');
                                e.preventDefault();
                            }
                        });
                    },
                    render: function(event, api) {
                        /// We override this handler not to hide dialog when
                        // user presses "Upload" and there are errors!
                    },
                    hide: function(event, api) {
                        //api.destroy();
                    }
                }
            });
        };

        _DataStoreUploadDialog.prototype.dialog = function() {
            return this.dialog;
        }

        return _DataStoreUploadDialog;
    })();

    // Public API:
    return {
        default_dialog_options: _default_dialog_options, 
        create_modal: _create_modal,
        AlertDialog: AlertDialog,
        ConfirmDialog: ConfirmDialog,
        PromptDialog: PromptDialog,
        ProgressDialog: ProgressDialog,
        IndeterminateProgressDialog: IndeterminateProgressDialog,
        EditorChoiceDialog: EditorChoiceDialog,
        FreetestDialog: FreetestDialog,
        StartTestDialog: StartTestDialog,
        UnsavedScriptDialog: UnsavedScriptDialog,
        ValidationResultDialog: ValidationResultDialog,
        ReplaceScriptDialog: ReplaceScriptDialog,
        PurchaseDialog: PurchaseDialog,
        PricingDialog: PricingDialog,
        DataStoreUploadDialog: DataStoreUploadDialog
    };
})();

LI.Drawer = (function() {
    var _Drawer = function(container_selector, button_selector, options) {
        this.container_selector = container_selector;
        this.button_selector = button_selector;
        this.options = $.extend({
            'close_callback': null,
            'duration': 500,
            'easing': 'easeOutExpo',
            'minimized': false,
            'open_callback': null
        }, options || {});
        this.container_width = $(container_selector).outerWidth();
        this.container_height = $(container_selector).outerHeight();
        this.button_height = $(button_selector).outerHeight();
        this.content_height = this.container_height - this.button_height;
    };

    _Drawer.prototype.close = function() {
        var self = this;
        $(this.container_selector).animate({
                top: '-' + this.content_height + 'px'
            }, this.options.duration, this.options.easing, function() {
                $(self.container_selector).parent().css({
                    'height': self.button_height + 'px'
                });
            }).removeClass('open');
        if (this.options.close_callback) {
            this.options.close_callback();
        }
    };

    _Drawer.prototype.open = function() {
        $(this.container_selector).parent().css({
            'height': this.container_height + 'px'
        });
        $(this.container_selector).animate({
                top:'0'
            }, this.options.duration, this.options.easing).addClass('open');
        if (this.options.open_callback) {
            this.options.open_callback();
        }
    };

    _Drawer.prototype.minimize = function() {
        $(this.container_selector).css({
            height: this.container_height + 'px',
            position: 'relative',
            top: '-' + this.content_height + 'px'
        }).removeClass('open');
        $(this.container_selector).parent().css({
            'height': this.button_height + 'px',
            'overflow': 'hidden'
        });
    };

    _Drawer.prototype.maximize = function() {
        $(this.container_selector).css({
            height: this.container_height + 'px',
            position: 'relative',
            top: '0'
        }).addClass('open');
        $(this.container_selector).parent().css({
            'height': this.container_height + 'px',
            'overflow': 'hidden'
        });
    };

    _Drawer.prototype.init = function() {
        var content_height = this.container_height - this.button_height;
        if (this.options.minimized) {
            this.minimize();
        } else {
            this.maximize();
        }
        var self = this;
        $(this.button_selector).click(function(e) {
            if ($(self.container_selector).hasClass('open')) {
                self.close();
            } else {
                self.open();
            }
            e.preventDefault();
        });
    };

    return _Drawer;
})();

LI.Editor = function() 
{
    // Validation handler
    var _Validate = (function()
    {
        function _Validate()
        {
            this.button = '#validate-script-button';
            this.dialog = null;
            this.log_table = null;
            this.error = null; // last recorded error
        }

        _Validate.prototype.init = function()
        {
            $(this.button).click(function(e) {
                LI.Editor.EditorManager.save_scenario(true);

                e.preventDefault();
            }).tooltip('validation');
        }

        _Validate.prototype.show = function()
        {
            var self = this;
            if (self.dialog == null) {
                self.dialog = new LI.Dialogs.ValidationResultDialog({
                    'show_callback': function() {
                        if (self.log_table == null)
                            self.init_table();
                        self.start();
                    },
                    'ok_callback': function() {
                        if (self.error)
                        {
                            var ed = LI.Editor.EditorManager.get_current_editor();
                            ed.show_error(self.error);
                        }
                    }
                });

                $('.script-error-link').live('click', function(e) {
                    if (self.error)
                    {
                        var ed = LI.Editor.EditorManager.get_current_editor();
                        ed.show_error(self.error);
                    }

                    self.dialog.dialog.qtip('api').toggle(false);

                    return false;
                });
            }
            else
            {
                self.dialog.dialog.qtip('api').toggle(true);
            }
            return false;
        };

        _Validate.prototype.init_table = function()
        {
            this.log_table = $("#log-entries-table").dataTable({
                'aaSorting': [[0, 'desc']],
                "bPaginate": true,
                "bProcessing": false,
                "bServerSide": false,
                "bLengthChange": false,
                "bFilter": false,
                "bSort": false,
                "bInfo": true,
                "iDisplayLength": 8,
                "sPaginationType": "full_numbers",

                "fnRowCallback": function(nRow, aaData, iDisplayIndex) {
                    if ('error' == aaData[1]) {
                        $(nRow).addClass('row-error');
                    } else if ('warning' == aaData[1]) {
                        $(nRow).addClass('row-warning');
                    } else if ('info' == aaData[1]) {
                        $(nRow).addClass('row-info');
                    } else if ('debug' == aaData[1]) {
                        $(nRow).addClass('row-debug');
                    }
                    return nRow;
                },
                "aoColumns": [
                    {
                        "bSearchable": false,
                        "bUseRendered": false,
                        "bVisible": true,
                        "sClass": 'listing-td',
                        "sType": "string",
                        "sWidth": '20%'
                    },
                    {
                        "bSearchable": true,
                        "bVisible": true,
                        "sClass": 'listing-td',
                        "sType": "string",
                        "sWidth": '20%'
                    },
                    {
                        "bSearchable": true,
                        "bVisible": true,
                        "sClass": 'listing-td',
                        "sType": "string",
                        "sWidth": '60%'
                    }
                ]
            });

        }

        _Validate.prototype.start = function()
        {
            this.reset();
            $('#validation-result').html(
'<img src="/static/images/loader.gif" alt="Running validation..." /> Running validation...'
                );

            var self = this;
            var editor = LI.Editor.EditorManager.get_current_editor();

            var data = { type: editor.get_type()};
            self.id = LI.Editor.EditorManager.get_current_scenario();
            LI.Ajax.postJSON_with_exponential_backoff('/api/v1/validation/'+self.id, data, function(data) {
                if (data.message)
                {
                    self.reset();
                    new LI.Dialogs.AlertDialog(data.message);
                }
                else
                {
                    editor.remove_error();
                    self.poll();
                }
            });
        };

        _Validate.prototype.poll = function()
        {
            var self = this;
            LI.Ajax.poll({
                url: '/api/v1/validation/'+self.id,
                dataTable: 'json',
                type: 'GET',

                timeout_callback: function() { new LI.Dialogs.AlertDialog('Failed to get validation result from server');},
                error_callback: function() { new LI.Dialogs.AlertDialog('Failed to queue scenario for validation');},
                success_callback: function(data) {
                    if (data.logs)
                    {
                        var log_table = $("#log-entries-table").dataTable();
                        $.each(data.logs, function(k,v) {
                            var message = v.message;
                            if (v.level == 'error')
                            {
                                message += ' <a href="#" class="script-error-link">[Show error]</a>';
                            }

                            log_table.fnAddData([
                                v.created,
                                v.level,
                                message
                                ], false);

                        });
                        log_table.fnDraw();
                    }

                    if (data.status)
                    {
                        if (data.status == LI.Enums.ValidationStatus.FINISHED)
                        {
                            $('#validation-result').html('Validation <span style="color: green">successful</span>!');
                            return true;
                        }
                        else if (data.status == LI.Enums.ValidationStatus.FAILED)
                        {
                            if (data.general_error)
                            {
                                new LI.Dialogs.AlertDialog(data.general_error.message);
                            }
                            else
                                self.error = data.error;

                            $('#validation-result').html('Validation <span style="color: red">failed</span>!');
                            return true;
                        }
                    }

                    return false;
                }
            });
        }
   
        _Validate.prototype.reset = function()
        {
            $('#validation-result').html('Validation aborted');
            this.id = null;
            this.error = null;
            this.log_table.fnClearTable();
        };

        return _Validate;
    })();

    // Analyze handler
    var _Analyze = (function()
    {
        function _Analyze()
        {
            this.button = '#analyze-button';
            this.spinner = '#analyze-spinner';

            this.id = 0;
            this.url = '';
        }

        _Analyze.prototype.init = function()
        {
            var self = this;
            $(this.button).click(function(e) {
                if ($(this).hasClass('running'))
                {
                    return false;
                }

                self.reset();

                self.start();
                return false;
            }).tooltip('analyze');
        }

        _Analyze.prototype.start = function()
        {
            $(this.button).addClass('running');
            $(this.spinner).show();
            var url = $('#target-url').val();
            this.url = url;

            var self = this;
            $.post('/test/user-scenario/analyze', {url: url}, function(json) {
                if (json.result == 'ok' && json.page_analysis_id)
                {
                    self.id = json.page_analysis_id;
                    self.poll();
                }
                else if (json.result == 'error')
                {
                    self.reset();

                    self.target_error_message(json.message);
                }
            });
        }

        _Analyze.prototype.poll = function()
        {
            var checked = $('input[name="only-domain"]').is(':checked');

            var self = this;
            var script_type = LI.Editor.EditorManager.get_current_editor().get_type();
            var data = {
                only_domain: checked,
                url: self.url,
                script_type: script_type
            };

            LI.Ajax.poll({
                url: '/test/user-scenario/poll/'+self.id,
                data: data,
                type: 'POST',
                dataType: 'json',

                timeout_callback: function(data) { self.reset(); new LI.Dialogs.AlertDialog('Analyze timed out'); }, 
                error_callback: function() { self.reset(); new LI.Dialogs.AlertDialog('Failed to get result from the server');}, 
                success_callback: function(data) {
                    if (data.result)
                    {
                        var manager = LI.Editor.EditorManager;
                        var editor = manager.get_current_editor();
                        if (data.result == 'ok')
                        {
                            if (editor.get_empty_script() == editor.get_value())
                            {
                                manager.set_value(data.script, true);
                            }
                            else
                            {
                                var app = editor.get_type() == 'lile' ? false : true;
                                new LI.Dialogs.ReplaceScriptDialog({
                                    append_enabled: app,
                                    overwrite_callback: function() {
                                        manager.set_value(data.script, true);
                                    },

                                    append_callback: function() {
                                        var script = editor.get_value();
                                        script += "\n\n" + data.script;

                                        manager.set_value(script, true);
                                    }
                                });
                            }
                        }
                        else if (data.result == 'error')
                        {
                            new LI.Dialogs.AlertDialog(data.message);
                        }
                        else if (data.result == 'working')
                        {
                            return false;
                        }

                        self.reset();
                        return true;
                    }
                }
            });
        }

        _Analyze.prototype.reset = function()
        {
            this.id = 0;
            this.url = '';
            $(this.spinner).hide();
            $(this.button).removeClass('running');
            this.target_error_message();
        }

        _Analyze.prototype.target_error_message = function(message)
        {
            if (undefined != message)
            {
                var o = {content: message };
                $('#target-url').addClass('error').removeData('qtip').errortip(o);
            }
            else
            {
                $('#target-url').removeClass('error').qtip('hide');
            }
        }

        return _Analyze;
    })();

    // Proxy recorder handler
    var _Proxy = (function()
    {
        function _Proxy()
        {
            this.button = '#proxy-button';
            this.test_button = '#proxy-test-button';
            this.start_button = '#proxy-start-button';
            this.stop_button = '#proxy-stop-button';
            this.url = '';
        }

        _Proxy.prototype.init = function()
        {
            var self = this;

            $(this.button).click(function(e) {
                self.reserve_port();

                return false;
            }).tooltip('proxy_recorder');

            $(this.start_button).click(function() {
                if ($(this).hasClass('button-disabled'))
                    return false;
                
                self.start();

                return false;
            });
            $(this.stop_button).click(function() {
                if ($(this).hasClass('button-disabled'))
                    return false;

                    self.stop();
                    self.reset();
                $('#step2').hide("slide", { direction: "left" }, "fast", function() {
                    $(self.button).removeClass('button-disabled');
                });
                
                return false;
            });

            $(this.test_button).click(function(e) {
                self.test(); 
            }).tooltip('proxy_test');

            $('#proxy-cancel-button').live('click', function() {
                $('#proxy-info-box').slideUp(300);
                self.reset();
                $(self.button).removeClass('button-disabled');

                return false;
            });
        }

        _Proxy.prototype.reserve_port = function()
        {
            var self = this;
            var url = $('#target-url').val();

            $('#proxy-step1').show();
            $('#proxy-step2').hide();

            $.ajax({
                type: 'POST',
                url: '/proxy/start',
                data: {url: url},
                // async: false, // ASYNC false is needed here to allow the window.open popup. Browser will block it otherwise.
                success: function(json) {
                    if (json.result == 'ok')
                    {
                        $('#proxy-info-box').slideDown(300);
                        
                        self.url = url;

                        $(self.button).addClass('button-disabled');
                        $('#proxy-port').addClass('bold').html(json.port);
                        $('#proxy-status').html('');
                    }
                    else if (json.result == 'error')
                    {
                        self.reset();
                        self.target_error_message(json.message);
                    }
                }
            });
        }

        _Proxy.prototype.start = function()
        {
            var url = $('#target-url').val();
            var self = this;
            var li_url = '/proxy/redirect?url=' + url;
            window.open(li_url, 'Recording');

            $(self.start_button).addClass('button-disabled');
            $(self.stop_button).removeClass('button-disabled');

            $('#proxy-status').html('');
        }

        _Proxy.prototype.test = function()
        {
            $('#proxy-status').html('');
            var self = this;
            $.ajax({
                type: 'GET',
                url: '/proxy/test',
                success: function(json) {
                    if (json.result == 'ok')
                    {
                        $('#proxy-step1').hide("slide", { direction: "left" }, "fast", function() {
                            $('#proxy-step2').show("slide", { direction: "left" }, "fast");

                            $(self.stop_button).addClass('button-disabled');
                            $(self.start_button).removeClass('button-disabled');
                        });
                    }
                    else if (json.result == 'error')
                    {
                        $('#proxy-status').html('<img src="/static/images/cross.png" alt="Invalid" /> ' + json.message);
                    }
                }
            }); 
        }

        _Proxy.prototype.target_error_message = function(message)
        {
            if (undefined != message)
            {
                var o = {content: message };
                $('#target-url').addClass('error').removeData('qtip').errortip(o);
            }
            else
            {
                $('#target-url').removeClass('error').qtip('hide');
            }
        }

        _Proxy.prototype.reset = function()
        {
            $('#proxy-info-box').slideUp(300);
            $(this.button).removeClass('button-disabled');
            this.url = '';
            this.target_error_message();
        }

        _Proxy.prototype.stop = function()
        {
            var checked = $('input[name="only-domain"]').is(':checked');
            var url = this.url;
            var script_type = LI.Editor.EditorManager.get_current_editor().get_type();
            var data = {
                only_domain: checked,
                url: url,
                script_type: script_type
            };
            
            $('#proxy-info-box').slideUp(300);
            LI.Ajax.postJSON('/proxy/stop', data, function(json) {
                var manager = LI.Editor.EditorManager;
                var editor = manager.get_current_editor();
                if (json.result == 'ok')
                {
                    var gen_script = json.script;
                    if (gen_script == null)
                        gen_script = '';
                    if (editor.get_empty_script() == editor.get_value())
                    {
                        manager.set_value(gen_script, true);
                    }
                    else
                    {
                        var app = editor.get_type() == 'lile' ? false : true;
                        new LI.Dialogs.ReplaceScriptDialog({
                            append_enabled: app,
                            overwrite_callback: function() {
                                manager.set_value(gen_script, true);
                            },

                            append_callback: function() {
                                var script = editor.get_value();
                                script += "\n\n" + json.script;

                                manager.set_value(script, true);
                            }
                        });
                    }
                }
            });
        }

        return _Proxy;
    })();


    var _Calculator = (function()
    {
        var _default = {
            'trigger_class': '.open-calculator',
            'calc_button': '.calculate-btn',
            'recalc_button': '.recalculate-btn',
            'calculate_callback': this.calculate_callback,
            'clients_bar': '#calculator-bar-clients',
            'clients_field': '#clients',
            'clients_step': 125,
            'clients_min': 125,
            'clients_default': 500,
            'clients_max': 50000,
            'duration_bar': '#calculator-bar-duration',
            'duration_field': '#duration',
            'duration_step': 15,
            'duration_min': 15,
            'duration_default': 15,
            'duration_max': 180,
            'tests_bar': '#calculator-bar-tests',
            'tests_field': '#tests',
            'tests_min': 1,
            'tests_default': 3,
            'tests_max': 50,
            'result_holder': '.calc-result',
            'result_credits_holder': '#calc-credits-needed',
            'user_type': 'sbu',
            'user_type_field': 'input[name="user-type"]',
            'user_type_multiples': {
                'vu': 2, 
                'sbu': 1
            },
        };

        function _Calculator()
        {
        
        }

        _Calculator.prototype.init = function(opts)
        {
            var self = this;

            if (opts === undefined)
            {
                opts = {}; 
            }
            self.options = $.extend({}, _default, opts);
            self.bind_fancybox(self.options.trigger_class);    

            $(self.options.clients_bar).slider({
                range: "min",
                value: self.options.clients_default,
                min: self.options.clients_min,
                max: 10625, // TODO: use clients_max to figure this one out instead.
                // max: 32500,
                step: self.options.clients_step,
                slide: function(e, ui) { 
                    self.update_client_field_from_slider(self, ui.value); 
                }
            });
            $(self.options.duration_bar).slider({
                range: "min",
                value: self.options.duration_default,
                min: self.options.duration_min,
                max: self.options.duration_max,
                step: self.options.duration_step,
                slide: function(e, ui) {
                    $(self.options.duration_field).val(ui.value);
                    $(self.options.duration_field).removeClass("error");
                }
            });
            $(self.options.tests_bar).slider({
                range: "min",
                value: self.options.tests_default,
                min: self.options.tests_min,
                max: self.options.tests_max,
                slide: function(e, ui) {
                    $(self.options.tests_field).val(ui.value);
                    $(self.options.tests_field).removeClass("error");
                }
            });
            
            $(self.options.clients_field).val($(self.options.clients_bar).slider("value"));
            $(self.options.duration_field).val($(self.options.duration_bar).slider("value"));
            $(self.options.tests_field).val($(self.options.tests_bar).slider("value"));
            
            $(self.options.clients_field).bind("keyup", function(e) {
                $(e.target).removeClass("error");
                var val = e.target.value;
                var multiple = self.options.user_type_multiples[$(self.options.user_type_field + ':checked').val()];

                if ( ! isNaN(val) && self.options.clients_min <= val && val <= self.options.clients_max * multiple) 
                {
                    self.update_slider_from_client_field(self, val);
                }
                else
                {
                    $(e.target).addClass("error");
                }
            });
            $(self.options.duration_field).bind("keyup", function(e) {
                $(e.target).removeClass("error");
                var val = e.target.value;
                if ( ! isNaN(val) && self.options.duration_min <= val && val  <= self.options.duration_max) 
                {
                    $(self.options.duration_bar).slider("value", val);
                }
                else
                {
                    $(e.target).addClass("error");
                }
            });
            $(self.options.tests_field).bind("keyup", function(e) {
                $(e.target).removeClass("error");
                var val = e.target.value;
                if ( ! isNaN(val) && self.options.tests_min <= val && val  <= self.options.tests_max) 
                {
                    $(self.options.tests_bar).slider("value", val);
                }
                else
                {
                    $(e.target).addClass("error");
                }
            });
            $(self.options.user_type_field).bind("change", function(e) {
                var new_val = $(self.options.clients_field).val();
                if (new_val > self.options.clients_max)
                {
                    new_val = self.options.clients_max;
                    $(self.options.clients_field).val(new_val);
                }
                
                self.update_slider_from_client_field(self, new_val);
            });
            $(self.options.calc_button + ',' + self.options.recalc_button).bind("click", function() { self.calculate() });
            return self;
        }

        // Pass along the slider value from the slide-trigger as it gets odd values if fetched from here.
        _Calculator.prototype.update_client_field_from_slider = function(self, slider_value)
        {
            var a = slider_value / self.options.clients_step;
            var v = a > 40 ? a + ((a-40) * ((1000/self.options.clients_step) - 1)) : a;
            var multiple = self.options.user_type_multiples[$(self.options.user_type_field + ':checked').val()];
            $(self.options.clients_field).val(v * self.options.clients_step * multiple);
            $(self.options.clients_field).removeClass("error");
            return false;
        }

        // Don't relly need to pass along the field value, but do it anyway for consistency.
        _Calculator.prototype.update_slider_from_client_field = function(self, field_value)
        {
            var multiple = self.options.user_type_multiples[$(self.options.user_type_field + ':checked').val()];
            var val = field_value / multiple;
            var v = val > 40 * self.options.clients_step 
                ? (((val - (40 * self.options.clients_step)) / (1000/self.options.clients_step))) + (40 * self.options.clients_step)
                : Math.round(val);
            $(self.options.clients_bar).slider("value", Math.round(v));
            return false;
        }

        _Calculator.prototype.calculate = function()
        {
            var self = this;
            var clients = $(self.options.clients_field).val();
            var minutes = $(self.options.duration_field).val();

            if ($(self.options.calc_button + ':visible').size())
            {
                $(self.options.calc_button).fadeTo(50, 0.5); 
            }
            $(self.options.calc_button).after('<img src="/static/images/loader.gif" alt="Calculating..." />');
            $(self.options.recalc_button).after('<img src="/static/images/loader.gif" alt="Calculating..." />');
            $(self.options.result_holder).addClass('calc-result-disabled');
            
            if (self.options.user_type_multiples[$(self.options.user_type_field + ':checked').val()] != undefined)
            {
                self.options.user_type = $(self.options.user_type_field + ':checked').val();
            }

            var config_data = {
                "type": "continuous",
                "user_type": self.options.user_type,
                "user_agent": {},
                "tracks": [{
                    "title":"calculator",
                    "loadzone":"amazon:us:ashburn",
                    "clips": [{
                        "loadscript": 0,
                        "percent": 100,
                        "start": 0,
                        "duration": minutes
                    }],
                    "load_schedule":[{
                        "type": "ramp",
                        "users": clients,
                        "duration": minutes
                    }]
                }]
            };
            var post_data = { config: JSON.stringify(config_data) };
            
            LI.Ajax.postJSON('/test/config/credits/', post_data, self.calculate_callback);

            return false;
        }

        _Calculator.prototype.calculate_callback = function(data)
        {
            var self = LI.Editor.Calculator;
            var credits = "?";

            if (data.credits)
            {
                credits = data.credits;

                // Multiply with number of tests
                var number_of_tests = $(self.options.tests_field).val();
                if (isNaN(number_of_tests))
                {
                    $(self.options.tests_field).addClass('error'); 
                    return false;
                }
                else
                {
                    credits *= number_of_tests;
                }
            }

            self.reset_result();

            var options = $(self.options.result_holder + " input[name='package']");
            var packages_too_small = true;
            $(options).each(function(i,obj) {
                if ($(obj).val() > credits)
                {
                    $(obj).parent().parent().next().next().after('<div class="recommended"><span>Recommended</span></div>');
                    $(obj).attr("checked", "checked");
                    packages_too_small = false;
                    return false;
                }
            });

            if (packages_too_small)
            {
                    $(self.options.result_holder + ' div.custom-package-holder > span').after('<div class="recommended"><span>Recommended</span></div>');
            }

            $(self.options.result_holder).removeClass('calc-result-disabled');
            $(self.options.result_credits_holder).html(credits);
            $(self.options.calc_button).next().hide();
            $(self.options.calc_button).hide();
            $(self.options.recalc_button).next().remove();
            $(self.options.result_holder).slideDown(300);
            return false;
        }

        // Reset the entire lightbox
        _Calculator.prototype.reset = function()
        {
            var self = this;
            self.reset_result();
            $(self.options.result_holder).hide();
            $(self.options.calc_button).show();
            $(self.options.calc_button).fadeTo(50, 1.0);
            
            $(self.options.clients_bar).slider("value", self.options.clients_default);
            $(self.options.duration_bar).slider("value", self.options.duration_default);
            $(self.options.tests_bar).slider("value", self.options.tests_default);
            
            $(self.options.clients_field).val($(self.options.clients_bar).slider("value"));
            $(self.options.duration_field).val($(self.options.duration_bar).slider("value"));
            $(self.options.tests_field).val($(self.options.tests_bar).slider("value"));

            return false;
        }

        // Reset the result form
        _Calculator.prototype.reset_result = function()
        {
            var self = this;
            $(self.options.result_credits_holder).html("0");
            $(self.options.result_holder + "input").removeAttr("checked");
            $(self.options.result_holder + ' .recommended').remove();
            return false;
        }

        _Calculator.prototype.bind_fancybox = function(e)
        {
            var self = this;
            var trig = $(self.options.trigger_class);
            if (trig.length <= 0)
            {
                return;
            }
            if (self.options.bind_trigger_callback != null)
            {
                self.options.bind_trigger_callback();
            }

            $(e).fancybox({
                'type': 'inline',
                'overlayColor': '#000',
                'transitionIn': 'none',
                'transitionOut': 'none',
                'centerOnScroll': true,
                'hideOnContentClick': false,
                'onStart': function(t) {
                    // self.reset();
                },
                'onCleanup': function(t) {
                    self.reset();
                }
            });

            return false;
        }

        return _Calculator;
    })();



    //
    // Schedule handler
    //
    var _Schedule = (function()
    {
        var _default = {
            'trigger_class': '.edit-schedule-button',

            'schedule_id_callback': null,
            // Called when bind_trigger is called. No params provided
            'bind_trigger_callback': null,
            // Called after a save/create call.
            // params:
            //   action    save/create
            //   user_scenario_id
            //   name      scenario name
            //   data      json result from server
            //             
        };


        function _Schedule()
        {
            this.schedule_form = '#schedule-form';
            this.trigger_buttons = '.edit-schedule-button';
            this.save_button = '#schedule-save-button';
            this.spinner = '#schedule-spinner';

            this.id = 0;
            this.schedule_id = null;
            this.enabled = true;
        }

        _Schedule.prototype.init = function(opts)
        {
            var self = this;

            if (opts === undefined)
            {
                opts = {}; 
            }
            self.errorbox = new LI.ErrorBox('errorbox');
            self.options = $.extend({}, _default, opts);
            self.bind_triggers();

            this.table = LI.Utils.load_data_table({ 
                'sDom': 'lfrt<"actions">ip',
                'sTableId': 'schedule-table', 
                'bServerSide': false, 
                "sPaginationType": "full_numbers",
                'aaSorting': [[3, "asc"]],
                'oLanguage': {
                    'sZeroRecords': "You don't have any scheduled test"
                },
                'aoColumns': [
                    {
                        'bSearchable': false,
                        'bSortable': false,
                        'bVisible': true,
                        'sClass': 'listing-td',
                        'sType': 'html',
                        'sWidth': '1%',
                        'fnRender': this.render_checkbox
                    },
                    {
                        'sClass': 'listing-td',
                        'fnRender': this.render_config_name
                    },
                    {
                        'fnRender': this.render_target
                    },
                    {
                        // next
                    },
                    {
                        // last
                    },
                    {
                        'bSortable': true,
                        'fnRender': this.render_repeat
                    },
                    {
                        'bSortable': false,
                        'bSearchable': false,
                        'fnRender': this.render_actions,
                    },
                    {
                        'bVisible': false
                    }
                ]
            }).fnSetFilteringDelay(300).fnActionButtons({
                buttons: [
                    {
                        text: '<img src="/static/images/cross.png" alt="Delete" /> Delete',
                        tooltip: 'test_schedule_list_table_delete_button',
                        click: function() {
                            var t = $(self.table).dataTable(),
                                ids = $('input[name="id[]"]:checked', $(self.table).parent());

                            if (ids.length == 0) {
                                new LI.Dialogs.AlertDialog('Please select a scheduled test first');
                                return false;
                            }

                            d = new LI.Dialogs.ConfirmDialog('Are you sure you want to remove this scheduled test?', {
                                'callback': function(confimed) {
                                    if (!confimed) {
                                        return false;
                                    }

                                    ids = $('input[name="id[]"]:checked', $(self.table).parent());
                                    $.each(ids, function(k, v) {
                                        var id = $(v).val(),
                                            tr = $(v).closest('tr');
                                        $.getJSON('/test/schedule/delete/' + id, function(data) {
                                            var old = tr.hasClass('odd') ? '#fff' : '#f1f1f1';
                                            if (data.result == 'ok')  
                                            {
                                                tr.css({
                                                    'text-decoration': 'line-through'
                                                }).animate(
                                                    { backgroundColor: '#fdd' },
                                                    400
                                                ).animate(
                                                    { backgroundColor: old },
                                                    800,
                                                    function() {
                                                        self.table.fnDeleteRow(tr.get(0), null, true);
                                                    }
                                                );
                                            }
                                            else
                                            {
                                                window.scrollTo(0, 0);
                                                self.errorbox.error("Failed to delete scheduled test!");
                                            }
                                        });
                                    });
                                }
                            });
                            
                            return false;
                        }
                    }
                ]
            }); // end fnActionButton

            $('#schedule-table thead th:eq(1)').tooltip('test_schedule_list_table_name');
            $('#schedule-table thead th:eq(2)').tooltip('test_schedule_list_table_target');
            $('#schedule-table thead th:eq(3)').tooltip('test_schedule_list_table_next');
            $('#schedule-table thead th:eq(4)').tooltip('test_schedule_list_table_last');
            $('#schedule-table thead th:eq(5)').tooltip('test_schedule_list_table_repeated');
            $('.edit-schedule-button').tooltip('test_schedule_list_table_edit_button');
        }

        _Schedule.prototype.load = function(schedule_id)
        {
            $('#schedule-wrapper h2').html('Edit test schedule');
            $.getJSON('/test/schedule/get_with_list/' + schedule_id, function(data) {
                if (data.aaData)
                {
                    var options = '';
                    $.each(data.aaData, function(i, config) {
                        options += '<option value="' + config[4] + '">' + config[0] + ' (' + config[1] + ')</option>';
                    });
                    $('#schedule-config').html(options);
                }
                if (data.time)
                {
                   $('#schedule-currenttime').html(data.time); 
                }

                // TODO: use Date.parse below instead.
                if (data.schedule.next_run.length >= 16)
                {
                    $('#schedule-id').val(data.schedule.id);
                    $('#schedule-config').val(data.schedule.config_id);
                    $('#schedule-date').val(data.schedule.next_run.substr(0,10));
                    $('#schedule-hour').val(data.schedule.next_run.substr(11,2));
                    $('#schedule-minute').val(data.schedule.next_run.substr(14,2));
                    $('#schedule-repeat').val(data.schedule.repeat);
                    if (data.schedule.email == 't')
                    {
                        $('#schedule-email').attr('checked', 'checked');
                    }
                }
            });
        }

        _Schedule.prototype.save_callback = function(result)
        {
            if (result.result == 'ok')
            {
                $.fancybox.close();
                if (result.is_new == false)
                {
                    var tr = $('#schedule-edit-row-' + result.id + '').closest('tr');
                    var foo = LI.Editor.Schedule.table_update(result, tr, 0);
                }
                else
                {
                    LI.Editor.Schedule.table_add(result);
                }
                LI.Editor.Schedule.errorbox.info("Scheduled test has been saved successfully!");
                LI.Editor.Schedule.reset();
            }
            else
            {
                if (result.errors.next_run)
                {
                    $('#schedule-date').errortip_top({content: result.errors.next_run});
                }

                if (result.errors['schedule-config'])
                {
                    $('#schedule-config').errortip_top({content: result.errors['schedule-config']});
                }
                else if (result.errors['schedule-id'])
                {
                    $('#schedule-config').errortip_top({content: result.errors['schedule-id']});
                }
            }
            $('#schedule-save-button > img').attr('src', '/static/images/clock_add.png');
            $('.edit-schedule-button').tooltip('test_schedule_list_table_edit_button');
        }

        _Schedule.prototype.save = function()
        {
            var data = $('#schedule-form').serialize();
            LI.Ajax.postJSON('/test/schedule/save/', data, LI.Editor.Schedule.save_callback);
            return false;
        }


        _Schedule.prototype.reset = function()
        {
            var self = this;
            self.enabled = true;
            $('#schedule-wrapper h2').html('Schedule new test');
            $('#schedule-id').val('');
            $('#schedule-config').show();
            $('#schedule-config').next('.error').hide();
            $(self.schedule_form + ' #schedule-date').val('');
            $(self.schedule_form + ' select').val(0);
            $(self.schedule_form + ' input[type=checkbox]').removeAttr('checked');
            $(self.schedule_form + ' input.error').removeClass('error');
            $(self.schedule_form + ' select.error').removeClass('error');
            $(self.schedule_form + ' span.error').html('');
            $(self.save_button).removeAttr('disabled');
            $(self.save_button).removeClass('button-disabled');
            $(self.save_button + ' > img').attr('src', '/static/images/clock_add.png');
        }

        _Schedule.prototype.bind_triggers = function()
        {
            var self = this;

            var trig = $(self.options.trigger_class);
            if (trig.length <= 0)
            {
                return;
            }

            if (self.options.bind_trigger_callback != null)
            {
                self.options.bind_trigger_callback();
            }

            $(self.save_button).click(function(e) {
                if (self.enabled)
                {
                    $('.qtip.ui-tooltip').qtip('hide');// hide errortips
                    self.save()
                    $(self.save_button + ' > img').attr('src', '/static/images/loader.gif');
                }
                return false; 
            });

            $(self.trigger_buttons).live('mouseenter', function() {
                self.bind_fancybox(this);
            });
            $('#cancel-schedule-button').click(function() {
                    $.fancybox.close();
                    return false;
            });
            $('#schedule-date').datepicker({ dateFormat: 'yy-mm-dd', firstDay: 1 , minDate: +0});
        }

        _Schedule.prototype.bind_fancybox = function(e)
        {
            var self = this;
            var trig = $(self.options.trigger_class);
            if (trig.length <= 0)
            {
                return;
            }

            if (self.options.bind_trigger_callback != null)
            {
                self.options.bind_trigger_callback();
            }

            $(e).fancybox({
                'type': 'inline',
                'transitionIn': 'none',
                'transitionOut': 'none',
                'centerOnScroll': true,
                'hideOnContentClick': false,
                'onStart': function(t) {
                    self.reset();

                    var schedule_id = self.options.schedule_id_callback(t);
                    if (schedule_id)
                    {
                        self.load(schedule_id);
                    }
                    else
                    {
                        $.getJSON('/test/schedule/get_with_list/' + new Date().getTime(), function(data){
                            var options = '';
                            if (data.aaData && data.aaData.length > 0)
                            {
                                $.each(data.aaData, function(i, config){
                                    options += '<option value="' + config[4] + '">' + config[0] + ' (' + config[1] + ')</option>';
                                });
                                $('#schedule-config').html(options);
                            }
                            else
                            {
                                $('#schedule-config').hide();
                                $('#schedule-config').next().fadeIn(500);
                                $(self.save_button).addClass('button-disabled');
                                $(self.save_button).attr('disabled', 'disabled');
                                self.enabled = false;
                            }
                            if (data.time)
                            {
                               $('#schedule-currenttime').html(data.time); 
                            }
                        });
                    }
                },
                'onCleanup': function(t) {
                    $('.qtip.ui-tooltip').qtip('hide');// hide errortips
                }
            });
        }

        _Schedule.prototype.table_add = function(data)
        {
            return this.table.fnAddData([
                data.id,
                data.title,
                data.url,
                data.next_run,
                data.last_run,
                data.repeat,
                data.id,
                data.config_id
            ], true);
        }

        _Schedule.prototype.table_update = function(data, tr)
        {
            return this.table.fnUpdate([
                data.id,
                data.title,
                data.url,
                data.next_run,
                data.last_run,
                data.repeat,
                data.id,
                data.config_id
            ], this.table.fnGetPosition(tr.get(0)), 0, true, true);
        }

        _Schedule.prototype.render_checkbox = function(row)
        {
            var id = row.aData[6];
            return '<input type="checkbox" name="id[]" value="'+id+'"/>';
        }

        // Previous tab functions
        _Schedule.prototype.render_config_name = function(row)
        {
            var id = row.aData[7];
            var name = row.aData[1];
            return '<a href="/test/config/edit/' + id + '"><img src="/static/images/pencil.png" alt="Edit" /> ' + name + '</a>';
        }

        _Schedule.prototype.render_target = function(row)
        {
            var target = row.aData[2];
            return '<a href="' + target + '" rel="nofollow" target="_blank">' + LI.Utils.smart_url_truncate(target, 32) + '</a>';
        }

        _Schedule.prototype.render_repeat = function(row)
        {
            var repeat = row.aData[5];
            switch (repeat)
            {
                case 'm':
                    repeat = 'Monthly';
                    break;
                case 'w':
                    repeat = 'Weekly';
                    break;
                case 'd':
                    repeat = 'Daily';
                    break;
                case '-':
                    repeat = 'Never';
                    break;
            }

            return repeat;
        }

        _Schedule.prototype.render_actions = function(row)
        {
            var id = row.aData[6];
            var html = '<span id="schedule-edit-row-' +id+ '" style="display:none">' + id + '</span>' +
                '<a href="#edit-schedule" class="button edit-schedule-button"><img src="/static/images/pencil.png" alt="Edit" /> Edit</a>' 
                ;
            return html;
        }


        return _Schedule;
    })();


    DataStoreFileParser = (function() {
        var _DataStoreFileParser = function() {
            this.file = null;
            this.fileData = "";
            this.sepToChar = {
                'tab': '\t',
                'comma': ',',
                'semicolon': ';',
                'space': ' '
            };
            this.delimToChar = {
                'double': '"',
                'single': '\'',
                'none': ''
            };
            $('#data-store-from-line').keydown(LI.Forms.validate_numeric_field);
            $('#data-store-from-line').keyup(_.bind(this._reparse, this));
            $('#data-store-from-line').change(_.bind(this._reparse, this));
            $('#data-stores-advanced-container input:radio[name=separator]').change(_.bind(this._reparse, this));
            $('#data-store-delimiter').change(_.bind(this._reparse, this));
        };

        _DataStoreFileParser.prototype._reparse = function() {
            var fromLineText = $('#data-store-from-line').val(),
                fromLine = '' == fromLineText ? 1 : Math.max(1, parseInt(fromLineText)),
                rows = $.csv(this.sepToChar[$('#data-stores-advanced-container input:radio[name=separator]:checked').val()],
                             this.delimToChar[$('#data-store-delimiter').val()])(this.fileData)
                        .slice(Math.max(0, fromLine - 1)),
                nrCols = rows.length ? rows[0].length : 0;
            $('#data-store-csv-preview-table > thead th').remove();
            $('#data-store-csv-preview-table > thead > tr').append('<th class="listing-th">Row</th>');
            for (var i = 0; i < nrCols; ++i) {
                $('#data-store-csv-preview-table > thead > tr').append('<th class="listing-th">Column ' + (i + 1) + '</th>');
            }
            $('#data-store-csv-preview-table > tbody tr').remove();
            $.each(rows, function(i, row) {
                var columns = '<td class="listing-td listing-td-full-border">' + (fromLine + i) + '</td>';
                $.each(row, function(j, col) {
                    columns += '<td class="listing-td listing-td-full-border">' + LI.Utils.escape_html(col) + '</td>';
                    if (j == (nrCols - 1)) {
                        return false;
                    }
                });
                $('#data-store-csv-preview-table > tbody:last').append('<tr>' + columns + '</tr>');
                if (4 == i) {
                    return false;
                }
            });
        };

        _DataStoreFileParser.prototype.parse = function(file) {
            if (file.name) {
                $('#data-store-file-name').text(file.name);
            } else {
                $('#data-store-file-name').text('Uknown');
            }
            if (file.fileSize) {
                $('#data-store-file-size').text(LI.Formatter.bytes(file.fileSize));
            } else {
                $('#data-store-file-size').text('Uknown');
            }
            if (file.lastModifiedDate) {
                $('#data-store-file-lastmod').text(file.lastModifiedDate.toLocaleDateString());
            } else {
                $('#data-store-file-lastmod').text('Unknown');
            }
            $('#data-store-file').removeClass('error');
            $('#data-store-file-error').hide();
            $('#data-store-file-info').show();
            if (file.fileSize && 52428800 < file.fileSize) {
                $('#data-store-file').addClass('error');
                $('#data-store-file-error span').text("The chosen file is too large, max is 50 MiB.");
                $('#data-store-file-error').show();
                $('#data-store-file-info').hide();
                return;
            }
            this.file = file;
            if (typeof FileReader !== 'undefined' /*&& (/text\/(csv|comma\-separated\-values)/i).test(file.type)*/) {
                var reader = new FileReader(),
                    self = this,
                    blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice,
                    previewEnd = Math.min(10240, file.size);
                reader.onload = function(e) {      
                    self.fileData = e.target.result;
                    self._reparse();
                };
                if (blobSlice) {
                    reader.readAsText(blobSlice.call(file, 0, 10240), 'utf-8');
                } else {
                    reader.readAsText(file, 'utf-8');
                }
            } else {
                $('#data-store-csv-preview-table tbody tr').remove();
                var tr = $('<tr/>');
                tr.append($('<td/>').addClass('listing-td error')
                                    .attr('colspan', '1')
                                    .css({ 'color': 'red', 'text-align': 'center', 'font-style': 'italic' })
                                    .text('Unfortunately your browser doesn\'t support file previewing.'));
                $('#data-store-csv-preview-table tbody').append(tr);
            }
        };

        return _DataStoreFileParser;
    })();

    //
    // Editor Manager
    //
    function _EditorManager()
    {
        var self = this;
        var _default = {
            'editor_type': 'lua', // or lile
            'container': 'edit-script',
            'trigger_class': '.edit-user-scenario-button',

            'script_id_callback': null,
            // Called when bind_trigger is called. No params provided
            'bind_trigger_callback': null,
            // Called after a save/create call.
            // params:
            //   action    save/create
            //   user_scenario_id
            //   name      scenario name
            //   data      json result from server
            //             
            'post_save_callback': null
        };

        this.default_editor = 'lua';
        this.editors = {'lile': null, 'lua': null};
        this.editor = null; 
        this.user_scenario_id = null;
        this.last_new_trigger = null;
        this.discard = false;

        // Private API:
        this._load_scenario = function(user_scenario_id)
        {
            $.ajax({
                url: '/test/user-scenario/get/' + user_scenario_id,
                cache: false,
                dataType: 'json',
                data: {},
                success: function(data) {
                    if (data.result == 'ok')
                    {
                        // Change active editor depending on script type
                        self._switch_editor(data.script_type);
                        self.editor.show();

                        self.user_scenario_id = user_scenario_id;
                        $('#load-script-editor-name').val(data.name);
                        self._set_value(data.load_script, false, data.error);

                        $('#data-store-selected-input').val(data.data_store_id);
                        $('#data-store-selected-name').text(data.data_store_name);
                        if (-1 == data.data_store_id) {
                            $('#data-store-deselect-button').hide();
                        } else {
                            $('#data-store-deselect-button').show();
                        }
                        if ($('#data-stores-container').is(':visible')) {
                            $('#section-toggler-data-stores').trigger('click');
                        }

                        self.editor.refresh();

                        $.fancybox.hideActivity();
                    }
                }
            });
        };

        this._save_scenario = function(validation)
        {
            validation = typeof(validation) == 'undefined' ? false : validation;
            var name = $('#load-script-editor-name').val();
            if ('' == name) {
                 $.fancybox.hideActivity();
                 var text = 'The user scenario must be given a name';
                 $('#load-script-editor-name')
                    .addClass('error')
                    .errortip({content: text})
                    ;
                return;
            }

            var script_type = LI.Editor.EditorManager.get_current_editor().get_type();
            var action = 'create';
            var data = {
                name: name,
                data_store_id: $('#data-store-selected-input').val(),
                load_script: self.editor.get_value(),
                script_type: script_type,
                csrf: self.options.csrf_token
            };
            var id = '';
            if (self.user_scenario_id) {
                action = 'save';
                id = self.user_scenario_id;
            }
            
            self._reset_error_messages();

            var that = self;
            $.ajax({
                type: 'POST',
                cache: false,
                dataType: 'json',
                contentType: 'application/x-www-form-urlencoded',
                url: '/test/user-scenario/' + action + '/' + id,
                data: data,
                success: function(data, status, xhr) {
                    $.fancybox.hideActivity();
                    if (data.result == 'error')
                    {
                        if (data.errors && data.errors.name)
                        {
                             $('#load-script-editor-name')
                                .addClass('error')
                                .errortip({content: data.errors.name});
                        }
                        new LI.Dialogs.AlertDialog(data.message);
                        return;
                    }

                    if (action == 'create' && data.result == 'ok')
                    {
                        self.user_scenario_id = data.user_scenario_id;
                    }

                    self.editor.set_modified_flag(false);

                    self._update_script_dropdown(action, self.user_scenario_id, name, data);
                    
                    if (validation == true)
                    {
                        LI.Editor.Validate.show();
                    }
                    else
                    {
                        $.fancybox.close();
                    }
                }
            });
        };

        this._update_script_dropdown = function(action, user_scenario_id, name, data)
        {
            if (self.options.post_save_callback != null)
            {
                self.options.post_save_callback(action, user_scenario_id, name, data);
            }
        };

        this._reset = function(save_reset)
        {
            // Reset fields
            $('#load-script-editor-name').val('');
            $('#target-url').val('http://');
            $('#data-store-selected-input').val('-1');
            $('#data-store-selected-name').text('None');
            $('#data-store-deselect-button').hide();
            if ($('#data-stores-container').is(':visible')) {
                $('#section-toggler-data-stores').trigger('click');
            }

            this._reset_error_messages();

            this.editor.reset();

            this.user_scenario_id = null;
        };

        this._reset_error_messages = function() 
        {
            $('#load-script-editor-name')
                .removeClass('error')
                .qtip('hide')
                ;

            LI.Editor.Analyze.reset();
            LI.Editor.Proxy.reset();
        }

        this._bind_trigger = function()
        {
            var trig = $(self.options.trigger_class);
            if (trig.length <= 0)
                return;

            if (self.options.bind_trigger_callback != null)
            {
                self.options.bind_trigger_callback();
            }

            trig.fancybox({
                'type': 'inline',
                //'autoScale': false,
                //'autoDimensions': false,
                'width': 900,
                'height': 628,
                'transitionIn': 'none',
                'transitionOut': 'none',
                'centerOnScroll': true,
                'hideOnContentClick': false,
                'onComplete': function() {
                },
                'onStart': function(t) {
                    self.discard = false;

                    self.editor.show();

                    $.fancybox.showActivity();
                    // Load url from test config, if possible
                    var url = $('#url');
                    if (url.length > 0) {
                        $('#target-url').val(url.val());
                    }
                    var user_scenario_id = self.options.script_id_callback(t);
                    if (user_scenario_id)
                    {
                        self._load_scenario(user_scenario_id);
                    }
                    else
                    {
                        // Setup default editor
                        self._switch_editor(self.default_editor);
                        self.editor.show();
                        // HACK: timeout hack used to get around CM render bug
                        setTimeout(function() {
                            self.editor.refresh();
                        }, 100);
                    }
                },
                'onCleanup': function() {
                    if (self.discard == false &&
                            self.editor.is_modified())
                    {
                        new LI.Dialogs.UnsavedScriptDialog({discard_callback: function() {
                            self.discard = true;
                            $.fancybox.close();
                        }});

                        return false;
                    }

                    self._reset();
                    self.editor.hide();
                    return true;
                }
            });
        };
            
        this._set_value = function(script, mark_as_dirty, error)
        {
            if (mark_as_dirty == true)
                self.editor.set_value(script);
            else
                self.editor.load(script, error);
        };

        this._get_current_editor = function()
        {
            return self.editor;
        };

        this._switch_editor = function(editor)
        {
            if (self.editor)
            {
                self.editor.hide();
            }

            if (editor == 'lile')
            {
                self.editor = self.editors['lile'];
                $('#editor-help-link').attr('href', 
                    '/learning-center/faq/lile-intro');
            }
            else
            {
                self.editor = self.editors['lua'];
                $('#editor-help-link').attr('href', 
                    '/learning-center/faq/textedit-intro');
            }
        };

        this._get_last_new_trigger = function()
        {
            return self.last_new_trigger;
        }

        // Public API:
        return {
            init: function(opts)
            {
                if (opts === undefined) opts = {}; 
                self.options = $.extend({}, _default, opts);

                // Create editor
                self.editors['lua'] = new LI.Editor.CM();
                self.editors['lile'] = new LI.Editor.LILE();

                $.each(self.editors, function(k, v) {
                    v.init();
                });

                // Setup default editor
                self._switch_editor(self.default_editor);

                // Bind fancybox
                self._bind_trigger();
               
                // Hook up save and cancel buttons.
                $('#save-script-button').click(function() {
                    $.fancybox.showActivity();
                    self._save_scenario();
                    return false;
                }).tooltip('editor_save_script');
                $('#cancel-script-button').click(function() {
                    $.fancybox.close();
                    return false;
                });

                // Hook up new script button
                $('.new-script-button').live('click', function(e) {
                    self.last_new_trigger = e.target;

                    var a = new LI.Dialogs.EditorChoiceDialog({
                        lua_callback: function() {
                            self._switch_editor('lua');
                            self.editor.show();
                            self.editor.refresh();
                        },
                        lile_callback: function() {
                            self._switch_editor('lile');
                            self.editor.show();
                            self.editor.refresh();
                        }
                    });
                    return false;
                });

                // Hook up data stores related stuff.
                var fileParser = new DataStoreFileParser(),
                    progDlg = null,
                    uploadStart = function() {
                        $('#data-store-name').prop('disabled', true);
                        $('#data-store-file').prop('disabled', true);
                    },
                    uploadEnd = function() {
                        $('#data-store-name').prop('disabled', false);
                        $('#data-store-file').prop('disabled', false);
                    },
                    loadDataStores = function() {
                        var data_stores = new LI.Models.DataStoreCollection();
                        data_stores.fetch({
                            cache: false,
                            success: function(collection, response) {
                                $('#data-stores-table tbody tr').remove();
                                if (collection.length) {
                                    var pending_count = 0;
                                    collection.each(function(ds, index) {
                                        var tr = $('<tr/>');
                                        if (LI.Enums.DataStoreStatus.FAILED == ds.get("status")) {
                                            tr.addClass('row-error');
                                            var failed = $('<img/>').attr('src', '/static/images/exclamation.png')
                                                                    .attr('width', '16')
                                                                    .attr('height', '16')
                                                                    .attr('alt', 'Failed')
                                                                    .css('vertical-align', 'middle');
                                            var failed_test = $('<span/>').css('color', 'red')     
                                                                          .append('(')
                                                                          .append(failed)
                                                                          .append(' Conversion failed)');
                                            tr.append($('<td/>').addClass('listing-td')
                                                                .append(ds.get('name'))
                                                                .append(' ')
                                                                .append(failed_test));
                                        } else {
                                            tr.append($('<td/>').addClass('listing-td').text(ds.get('name')));
                                        }
                                        if (0 == index % 2) {
                                            tr.addClass('even');
                                        } else {
                                            tr.addClass('odd');
                                        }
                                        tr.append($('<td/>').addClass('listing-td').text(ds.get('rows')));
                                        tr.append($('<td/>').addClass('listing-td').text(LI.Formatter.gmdate('Y-m-d H:i', ds.get('created') + USER_TIMEZONE_OFFSET_SECS)));
                                        if (-1 != $.inArray(ds.get("status"),
                                                            [LI.Enums.DataStoreStatus.QUEUED,
                                                             LI.Enums.DataStoreStatus.CONVERTING])) {
                                            tr.append($('<td/>').addClass('listing-td')
                                                                .append($('<img/>').attr('src', '/static/images/loader.gif')
                                                                                   .attr('width', '16')
                                                                                   .attr('height', '16')
                                                                                   .attr('alt', 'Converting...'))
                                                                .append(' Converting...'));
                                            ++pending_count;
                                        } else if (LI.Enums.DataStoreStatus.FINISHED == ds.get("status")) {
                                            var select_button = $('<a/>').attr('href', '#' + ds.get('id'))
                                                                         .addClass('button select-data-store-button')
                                                                         .append($('<img/>').attr('src', '/static/images/database_go.png')
                                                                                            .attr('alt', 'Select'))
                                                                         .append(' Select');
                                            var delete_button = $('<a/>').attr('href', '#' + ds.get('id'))
                                                                         .addClass('button delete-data-store-button')
                                                                         .append($('<img/>').attr('src', '/static/images/database_delete.png')
                                                                                            .attr('alt', 'Delete'))
                                                                         .append(' Delete');
                                            tr.append($('<td/>').addClass('listing-td')
                                                                .append(select_button)
                                                                .append(' ')
                                                                .append(delete_button));
                                        } else if (LI.Enums.DataStoreStatus.FAILED == ds.get("status")) {
                                            var delete_button = $('<a/>').attr('href', '#' + ds.get('id'))
                                                                         .addClass('button delete-data-store-button')
                                                                         .append($('<img/>').attr('src', '/static/images/database_delete.png')
                                                                                            .attr('alt', 'Delete')
                                                                                            .css('vertical-align', 'middle'))
                                                                         .append(' Delete');
                                            
                                            tr.append($('<td/>').addClass('listing-td')
                                                                .append(delete_button));
                                        }
                                        $('#data-stores-table tbody').append(tr);
                                    });
                                    if (0 < pending_count) {
                                        setTimeout(loadDataStores, 3000);
                                    }
                                } else {
                                    var tr = $('<tr/>');
                                    tr.append($('<td/>').addClass('listing-td')
                                                        .attr('colspan', '4')
                                                        .css({ 'text-align': 'center', 'font-style': 'italic' })
                                                        .text('No data stores uploaded.'));
                                    $('#data-stores-table tbody').append(tr);
                                }
                            },
                            error: function(collection, response) {
                                new LI.Dialogs.AlertDialog('Failed loading data stores, for further assistance please contact support.');
                            }
                        });
                    };
                $('#data-store-deselect-button').click(function() {
                    $('#data-store-selected-name').text('None');
                    $('#data-store-selected-input').val('-1');
                    $('#data-store-deselect-button').hide();
                    return false;
                });
                $('.data-stores-help').tooltip('user_scenario_data_stores_info_link');
                $('#section-toggler-data-stores').toggler('#data-stores-container', {
                    state: 'closed',
                    open_callback: function() {
                        if ($('#data-stores-table .not-loaded').length) {
                            loadDataStores();
                        }
                        $("#load-script-container").hide();
                    },
                    close_callback: function() {
                    },
                    animation_done_callback: function(animated_elem, open) {
                        if (!open) {
                            $("#load-script-container").show();
                        }
                    }
                });
                $('.select-data-store-button').live('click', function(e) {
                    var id = $(this).attr('href').substr(1),
                        name = $('td:eq(0)', $(e.target).closest('tr')).text();
                    $('#data-store-selected-name').text(name);
                    $('#data-store-selected-input').val(id);
                    $('#data-store-deselect-button').show();
                    return false;
                });
                $('.delete-data-store-button').live('click', function(e) {
                    var self = this,
                        d = new LI.Dialogs.ConfirmDialog('Are you sure you want to delete this data store?', {
                        'callback': function(confimed) {
                            if (!confimed) {
                                return false;
                            }
                            var id = $(self).attr('href').substr(1),
                                model = new LI.Models.DataStore({ 'id': id }),
                                tr = $(e.target).closest('tr');
                            model.destroy({
                                error: function(model, response) {
                                    var j = JSON.parse(response.responseText);
                                    if (j && j.message) {
                                        new LI.Dialogs.AlertDialog(j.message);
                                    } else {
                                        new LI.Dialogs.AlertDialog("Failed to delete data store!");
                                    }
                                },
                                success: function(model, response) {
                                    var old = tr.hasClass('odd') ? '#fff' : '#f1f1f1';
                                    tr.css({
                                        'text-decoration': 'line-through'
                                    }).animate(
                                        { backgroundColor: '#fdd' },
                                        400
                                    ).animate(
                                        { backgroundColor: old },
                                        800,
                                        function() {
                                            tr.remove();
                                        }
                                    );
                                    if (id == $('#data-store-selected-input').val()) {
                                        $('#data-store-selected-name').text('None');
                                        $('#data-store-selected-input').val('-1');
                                        $('#data-store-deselect-button').hide();
                                    }
                                }
                            });
                        }
                    });
                    return false;
                });
                var chosenFileData = null;
                $('.new-data-store-button').click(function() {
                    var d = new LI.Dialogs.DataStoreUploadDialog({
                        ok_button_text: "Upload",
                        ok_callback: function() {
                            if (chosenFileData) {
                                progDlg = new LI.Dialogs.ProgressDialog();
                                if (typeof File !== 'undefined'
                                    && typeof FileList !== 'undefined'
                                    && typeof XMLHttpRequestUpload !== 'undefined') {
                                    $('#data-store-file').fileupload('send', {
                                        files: [fileParser.file]
                                    });
                                } else {
                                    $('#data-store-file').fileupload('send', chosenFileData);
                                }
                                uploadStart();
                            } else {
                                $('#data-store-file').addClass('error');
                                $('#data-store-file-error span').text("You must choose a CSV file to upload");
                                $('#data-store-file-error').show();
                                $('#data-store-file-info').hide();
                                return true; // Don't hide data store upload dialog!
                            }
                        }
                    });
                    return false;
                });
                $('#data-store-file').fileupload({
                    dataType: 'json',
                    url: '/api/v1/data-store/',
                    limitMultiFileUploads: 1,
                    replaceFileInput: true,
                    forceIframeTransport: false,
                    done: function(e, data) {
                        progDlg.close();
                        uploadEnd();
                        loadDataStores();
                    },
                    fail: function(e, data) {
                        var msg = 'Failed uploading data store. ',
                            j = JSON.parse(data.jqXHR.responseText);
                        if (j && j.message) {
                            msg += j.message;
                        }
                        if (j && j.errors) {
                            msg += '<ul>';
                            $.each(j.errors, function(field, error) {
                                msg += '<li><strong>' + error + '</strong></li>';
                            });
                            msg += '</ul>';
                        }
                        new LI.Dialogs.AlertDialog(msg);
                        progDlg.close();
                        uploadEnd();
                    },
                    progressall: function(e, data) {
                        var progress = parseInt(data.loaded / data.total * 100, 10);
                        progDlg.set_progress(progress);
                    },
                    add: function(e, data) {
                        // NOTE: Prevent file upload on select!
                    },
                    change: function(e, data) {
                        chosenFileData = data;
                        fileParser.parse(data.files[0]);
                    }
                });
                var spinner = $("#data-store-from-line").spinner({
                    min: 1,
                    value: 1
                })
                $('.ui-spinner-button').click(function() {
                    $(this).siblings('input').change();
                });

                LI.Editor.Analyze.init();
                LI.Editor.Proxy.init();
                LI.Editor.Validate.init();

            }, // End init

            get_current_scenario: function()
            {
                return self.user_scenario_id;
            },
            
            get_last_new_trigger: self._get_last_new_trigger,
            get_current_editor: self._get_current_editor,
            switch_editor: self._switch_editor,
            set_value: self._set_value,
            bind_trigger: self._bind_trigger,
            load: self._load_scenario,
            save_scenario: self._save_scenario
        };
    };

    // CodeMirror editor
    var _CodeMirror = (function()
    {
        function _CodeMirror()
        {
            this.editor = null;
        };

        _CodeMirror.prototype.init = function()
        {
            var self = this;
            var textarea = document.getElementById('load-script-editor-textarea');
            this.editor = CodeMirror.fromTextArea(textarea, {
                content: '',
                matchBrackets: true,
                lineNumbers: true,
                theme: "neat",
                mode: 'lua',

                onChange: function(e) { self.on_change(e); },
                onCursorActivity: function() { self._change_highlighed_line(); }
            });

            this.hlLine = this.editor.setLineClass(0, "activeline");
            this.error = null;
            this.errorline = null; // handle for line marker
            this.error_tooltip = null;
            
            this.hide();
        };

        _CodeMirror.prototype.show = function()
        {
            $('.CodeMirror').show();
        };
        _CodeMirror.prototype.hide = function()
        {
            $('.CodeMirror').hide();
        };

        _CodeMirror.prototype.refresh = function()
        {
            this.editor.refresh();

            if ($('.CodeMirror').is(':visible') == false)
                return;

            // HACK: avoids js-loop in chrome when focus is requested and
            // choose-editor-dialog is still visible.
            var self = this;
            setTimeout(function() {
                self.editor.focus();
            }, 0);
        };

        _CodeMirror.prototype.get_type = function()
        {
            return 'lua';
        };

        _CodeMirror.prototype.reset = function()
        {
            this.load(this.get_empty_script());
        };

        _CodeMirror.prototype.load = function(v, error)
        {
            this.editor.setValue(v);
            this.modified = false;

            if (error)
            {
                this.show_error({message: error.message, line: error.line});
            }
        };

        _CodeMirror.prototype.set_value = function(v)
        {
            this.editor.setValue(v);
        }

        _CodeMirror.prototype.get_value = function(v)
        {
            return this.editor.getValue();
        };

        _CodeMirror.prototype.get_empty_script = function()
        {
            return '';
        }

        _CodeMirror.prototype.is_modified = function()
        {
            return this.modified;
        }

        _CodeMirror.prototype.set_modified_flag = function(flag)
        {
            flag = typeof(flag) == 'undefined' ? true : flag;
            this.modified = flag;
        }

        _CodeMirror.prototype.on_change = function(e)
        {
            this.modified = true;
            if (this.error)
                this.remove_error();
        }

        _CodeMirror.prototype.remove_error = function()
        {
            if (this.errorline)
            {
                this.editor.clearMarker(this.errorline);
                this.editor.setLineClass(this.errorline, null);
            }
            if (this.error_tooltip)
                this.error_tooltip.qtip().hide();
            this.error = null;
        }

        _CodeMirror.prototype.show_error = function(error)
        {
            var line = error.line - 1; // CM starts counting at 0

            // Move cursor up/down to the error
            this.editor.setCursor({line: line+8, ch:0});
            this.editor.setCursor({line: line, ch:0});

            // Mark error
            this.error = {line: line, message: error.message};
            this.errorline = this.editor.setMarker(line, '●', 'errorline error-bullet');
            this.editor.setLineClass(this.errorline, 'errorline');

            var self = this;

            function bind()
            {
                self.error_tooltip = $('.error-bullet').errortip({
                    content: {
                        text: error.message
                    },
                    position: {
                        my: 'top left',
                        adjust: {
                            x: -4,
                            y: 4
                        }
                    },
                    hide: {
                        event: 'unfocus mouseleave'
                    },
                    show: {
                        event: 'mouseenter'
                    },
                    style: {
                        classes: 'ui-tooltip-red ui-tooltip-rounded ui-tooltip-shadow ui-error-tooltip'
                    },
                    events: {
                        show: function(e,api) {
                            var row_pos = self.editor.charCoords({line: self.error.line, ch: 0}),
                                ed_pos = $(self.editor.getWrapperElement()).offset();

                            var pos = row_pos.y - ed_pos.top;
                            if (pos > 50)
                            {
                                api.set({
                                    'position.adjust.x': -4,
                                    'position.adjust.y': -4,
                                    'position.my': 'bottom left'
                                });
                            }
                        }
                    }
                });
                return $('.error-bullet');
            }

            // Create tooltip. We also create it on mouseenter since the
            // tooltip will be deleted when CM is scrolling
            bind().live('mouseenter', function() {
                bind();
            });
        }

        // Private API
        _CodeMirror.prototype._change_highlighed_line = function()
        {
            var new_line = this.editor.lineInfo(this.editor.getCursor().line);
            var old_line = this.editor.lineInfo(this.hlLine);

            this.editor.setLineClass(this.hlLine, null);
            this.hlLine = this.editor.setLineClass(new_line.line, "activeline");

            if (this.error && old_line && old_line.line == this.error.line)
            {
                this.editor.setLineClass(this.errorline, "errorline");
            }
        }

        return _CodeMirror;
    })();

    // LILE editor
    var _LILE = (function()
    {
        function _LILE()
        {
            this.initialized = false; 
            this.editor = null;
            this.modified = false;

        };

        _LILE.prototype.init = function()
        {
        };

        _LILE.prototype._init_lazy = function()
        {
            $(window).trigger('start-lile');
            this.initialized = true;

            this.editor = $('#lile-div');

            var self = this;

            // Setup LILE script modification callback
            PKLEInstance.on_logic_editor_data_change = function() {
                self.modified = true;
            };
        }

        _LILE.prototype.show = function()
        {
            if (this.initialized == false)
            {
                this._init_lazy();
                this.initialized = true;
            }
            this.editor.show(); 
        };
        _LILE.prototype.hide = function()
        {
            if (this.initialized == false)
                return;

            this.editor.hide(); 
        };

        _LILE.prototype.refresh = function()
        {
            PK.WidgetMgr.reinit_layout();
        };

        _LILE.prototype.reset = function()
        {
            if (this.initialized == false)
                return;

            this.load(this.get_empty_script());
            this.modified = false;
        };

        _LILE.prototype.load = function(v, error)
        {
            if (this.initialized == false)
                return;

            PK.load_user_script(v, error);
            this.modified = false;
        };

        _LILE.prototype.set_value = function(v)
        {
            if (this.initialized == false)
                return;

            PK.load_user_script(v);
            this.modified = true;
        };

        _LILE.prototype.get_type = function()
        {
            return 'lile';
        };

        _LILE.prototype.get_value = function(v)
        {
            if (this.initialized == false)
                return this.get_empty_script();

                var lile_code = logic_editor.serialize_object_data_to_json(logic_editor.get_object_data());
                lile_code = JSON.stringify(lile_code, function(key, value){
                    if (this[key] == undefined)
                        return null;
                    return value;
                });
            return lile_code;
        };

        _LILE.prototype.is_modified = function()
        {
            return this.modified;
        }
        
        _LILE.prototype.set_modified_flag = function(flag)
        {
            flag = typeof(flag) == 'undefined' ? true : flag;
            this.modified = flag;
        }

        _LILE.prototype.get_empty_script = function()
        {
            return PKLEInstance.empty_script;
        }

        _LILE.prototype.show_error = function(error)
        {
            var object_data = PKLEInstance.current_script_data.get();
            PKLEInstance.Show_script_with_execution_result(
                object_data,
                error['message'],
                error['line'],
                Ext.decode(error['table'])
            );
        }

        _LILE.prototype.remove_error = function()
        {
            PKLEInstance.refresh_script("my-script");
        }
        
        return _LILE;
    })();
    //---------------

    // Public API:
    return {
        EditorManager: new _EditorManager,
        CM: _CodeMirror,
        LILE: _LILE,
        Analyze: new _Analyze,
        Proxy: new _Proxy,
        Validate: new _Validate,
        Schedule: new _Schedule,
        Calculator: new _Calculator
    };
}();

LI.ErrorBox = (function() {
    function _ErrorBox(container_id, timeout) {
        this.container_id = container_id;
        this.timeout = timeout == undefined ? 5000 : timeout; 
        this.c = $('#' + this.container_id).addClass('errorbox');
        this.u = null;
        this.add_message = function(classname, msg) {
            var n = null,
                self = this;
            if (!this.u) {
                this.u = $('<ul />');
                this.c.append(this.u);
            }
            n = $('<li />', { html: msg }).addClass(
                    ['message', classname].join(' '));
            this.u.append(n);
            if (!$(this.c).is(':visible')) {
                this.show();
            }

            // Skip timeout
            if (this.timeout == 0)
                return;
            setTimeout(function() {
                n.animate(
                    { opacity: 0.1 },
                    1000,
                    function() {
                        n.remove();
                        if (0 >= self.count()) {
                            self.hide();
                        }
                    }
                );
            }, self.timeout);
        };
    };

    _ErrorBox.prototype.clear = function() {
        $('.message', this.e).remove();
        $('ul', this.e).remove();
    };

    _ErrorBox.prototype.count = function() {
        if (this.u) {
            return $('li', this.u).length;
        } else {
            return $('div', this.c).length;
        }
    };

    _ErrorBox.prototype.error = function(msg) {
        this.add_message('error', msg);
    };

    _ErrorBox.prototype.info = function(msg) {
        this.add_message('info', msg);
    };

    _ErrorBox.prototype.hide = function() {
        this.c.hide();
    };

    _ErrorBox.prototype.show = function() {
        this.c.show('slide', { 'direction': 'down' }, 500);
    };

    return _ErrorBox;
})();

LI.Forms = (function() {
    function _flash_background(id, options) {
        var field = $(id),
            old = field.css('backgroundColor'),
            options = $.extend({
                color: '#F78B83',
                fade_in_duration: 400,
                fade_out_duration: 1200,
                complete: null
            }, options || {});
        field.stop().animate(
            { backgroundColor: options.color },
            options.fade_in_duration,
            function() {
                field.animate(
                    { backgroundColor: old },
                    options.fade_out_duration,
                    function() {
                        field.stop().attr('style', '');
                        if (options.complete) {
                            options.complete();
                        }
                    }
                );
            }
        );
    };

    function _validate_numeric_field(e) {
        // We only care about digits 0-9 and some special keys.
        var code = e.charCode || e.keyCode || e.which || 0;

        // Allow Ctrl|Cmd+A, Ctrl|Cmd+X (cut), Ctrl|Cmd+C (copy),
        // Ctrl|Cmd+Z (undo), Ctrl|Cmd+V (paste) and Shift+Ins.
        if (((e.ctrlKey || -1 != $.inArray(code, [17, 91, 93, 224]))
             && -1 != $.inArray(code, [65, 67, 86, 88, 90, 91, 97, 99, 118,
                                       120, 122]))
            || (e.shiftKey && code == 45)) {
            return true;
        }
        if (-1 == $.inArray(code, [8, 9, 13, 35, 36, 37, 39, 46]) 
            && !(48 <= code && 57 >= code) && !(96 <= code && 105 >= code)) {
            e.preventDefault(); // Don't allow any other characters!
        }
    };

    // Public API:
    return {
        flash_background: _flash_background,
        validate_numeric_field: _validate_numeric_field
    };
})();

LI.Map = (function() {
    function _Map(container_id, options) {
        this.container_id = container_id;
        this.options = $.extend({
            'center_lat': 0.0,
            'center_lng': 0.0,
            'init': null,
            'map_type': 'roadmap',
            'max_auto_zoom': 8,
            'zoom': 1
        }, options || {});
        this.markers = {};
        this.labels = {};
        this.lines = {};
        this.line_options = {};
        this.map = null;
        this.lazy_init_funs = [];
    }

    // Public API:

    // Add a label to the map.
    _Map.prototype.add_label = function(name, content, track) {
        var self = this;
        var lazy_fun = function() {
            var label = new TR.MapLabel();
            label.set_content(content);
            if (track) {
                label.track(track);
            }
            label.setMap(self.map);
            self.labels[name] = label;
        };
        if (!this.map) {
            this.lazy_init_funs.push(lazy_fun);
        } else {
            lazy_fun();
        }
    };

    // Add a line to the map.
    _Map.prototype.add_line = function(name, src, dst, options) {
        var self = this;
        var lazy_fun = function() {
            var line_options = $.extend({
                'append': true,
                'clickable': false,
                'color': '#00cc00',
                'geodesic': false,
                'opacity': 1.0,
                'stroke_weight': 3
            }, options || {});
            if (line_options.append && self.lines[name]) {
                var p = self.lines[name].getPath();
                p.push(dst);
                self.lines[name].setPath(p);
                self.line_options[name].path = p;
            } else {
                if (self.lines[name]) {
                    self.lines[name].setMap(null);
                    delete self.lines[name];
                }
                var gm_options = {
                    clickable: line_options.clickable,
                    geodesic: line_options.geodesic,
                    path: [src, dst],
                    strokeColor: line_options.color,
                    strokeOpacity: line_options.opacity,
                    strokeWeight: line_options.stroke_weight
                };
                var line = new google.maps.Polyline(gm_options);
                line.setMap(self.map);
                self.lines[name] = line;
                self.line_options[name] = gm_options;
            }
        };
        if (!this.map) {
            this.lazy_init_funs.push(lazy_fun);
        } else {
            lazy_fun();
        }
    };

    // Add a line to the map between two markers.
    _Map.prototype.add_line_between_markers = function(name, src_name, dst_name,
                                                       options) {
        var self = this;
        var lazy_fun = function() {
            if (self.markers[src_name] && self.markers[dst_name]) {
                self.add_line(name, self.markers[src_name].getPosition(),
                              self.markers[dst_name].getPosition(), options);
            } else {
                LI.Logger.warning("At least one of the markers (\"" + src_name
                                  + "\" and \"" + dst_name + "\") were not "
                                  + "found.");
            }
        };
        if (!this.map) {
            this.lazy_init_funs.push(lazy_fun);
        } else {
            lazy_fun();
        }
    };

    // Add a marker to the map.
    _Map.prototype.add_marker = function(name, lat, lng, options) {
        var self = this;
        var lazy_fun = function() {
            var marker_options = $.extend({
                'animation': google.maps.Animation.DROP,
                'auto_zoom_to_fit': true,
                'image': null,
                'tooltip': null,
                'click': null // Parameters passed in: (event, marker, map)
            }, options || {});
            var position = new google.maps.LatLng(lat, lng);
            var icon = null;
            if (marker_options.image) {
                icon = new google.maps.MarkerImage(
                    marker_options.image,
                    new google.maps.Size(24, 24),
                    new google.maps.Point(0, 0),
                    new google.maps.Point(12, 12)
                );
            }
            var marker = new google.maps.Marker({
                position: position,
                icon: icon,
                map: self.map,
                animation: marker_options.animation,
                title: marker_options.tooltip
            });
            if (marker_options.click) {
                google.maps.event.addListener(marker, 'click', function(e) {
                    return marker_options.click(e, this, self);
                });
            }
            self.markers[name] = marker;
            if (marker_options.auto_zoom_to_fit) {
                self.auto_zoom_to_fit();
            }
        };
        if (!this.map) {
            this.lazy_init_funs.push(lazy_fun);
        } else {
            lazy_fun();
        }
    };

    // Auto-zoom map to fit bounds of all markers.
    _Map.prototype.auto_zoom_to_fit = function(include_lines, draw_bounding_box) {
        var self = this;
        var lazy_fun = function() {
            var lat_sorted = [],
                lng_sorted = [];
            $.each(self.markers, function(n, m) {
                lat_sorted.push(m.getPosition().lat());
                lng_sorted.push(m.getPosition().lng());
            });
            lat_sorted.sort(function(a, b) {
                return a - b;
            });
            lng_sorted.sort(function(a, b) {
                return a - b;
            });
            var ne = new google.maps.LatLng(lat_sorted[0], lng_sorted[0]),
                sw = new google.maps.LatLng(lat_sorted[lat_sorted.length - 1],
                                            lng_sorted[lng_sorted.length - 1]),
                bounds = new google.maps.LatLngBounds(ne, sw);
            if (include_lines) {
                $.each(self.lines, function(n, l) {
                    l.getPath().forEach(function(p, i) {
                        bounds.extend(p);
                    });
                });
            }
            self.map.fitBounds(bounds);
            if (draw_bounding_box) {
                var ne = bounds.getNorthEast();
                var sw = bounds.getSouthWest();
                var boundingBox = new google.maps.Polyline({
                    path: [
                      ne, new google.maps.LatLng(ne.lat(), sw.lng()),
                      sw, new google.maps.LatLng(sw.lat(), ne.lng()), ne
                    ],
                    strokeColor: '#FF0000',
                    strokeOpacity: 1.0,
                    strokeWeight: 2
                });
                boundingBox.setMap(self.map);
            }

            // Since the map will only respond to similar actions like pan/zoom
            // when ready to do so we wait for an idle event before zooming
            // after bound fitting.
            var listener_zoom = google.maps.event.addListener(self.map, "idle", function() {
                if (self.options.max_auto_zoom < self.map.getZoom()) {
                    self.map.setZoom(self.options.max_auto_zoom);
                }
                google.maps.event.removeListener(listener_zoom);
            });
        };
        if (!this.map) {
            this.lazy_init_funs.push(lazy_fun);
        } else {
            lazy_fun();
        }
    };

    // Clear map of all overlays (markers and lines).
    _Map.prototype.clear = function() {
        this.clear_labels();
        this.clear_lines();
        this.clear_markers();
    };

    // Clear map of all markers.
    _Map.prototype.clear_markers = function() {
        $.each(this.markers, function(n, m) {
            m.setMap(null);
            delete m;
        });
        this.markers = {};
    };

    // Clear map of all labels.
    _Map.prototype.clear_labels = function() {
        $.each(this.labels, function(n, l) {
            l.setMap(null);
            delete l;
        });
        this.labels = {};
    };

    // Clear map of all lines.
    _Map.prototype.clear_lines = function() {
        $.each(this.lines, function(n, l) {
            l.setMap(null);
            delete l;
        });
        this.lines = {};
    };

    // Get a marker given its name.
    _Map.prototype.get_marker_from_name = function(name) {
        return this.markers[name];
    };

    // Get a label given its name.
    _Map.prototype.get_label_from_name = function(name) {
        return this.labels[name];
    };

    // Get a line given its name.
    _Map.prototype.get_line_from_name = function(name) {
        return this.lines[name];
    };

    // Get a line given its name.
    _Map.prototype.get_line_options_from_name = function(name) {
        return this.line_options[name];
    };

    // Initialize map.
    _Map.prototype.init = function() {
        if (!this.map) {
            var map_options = {
                zoom: this.options.zoom,
                center: new google.maps.LatLng(this.options.center_lat,
                                               this.options.center_lng),
                mapTypeId: this.options.map_type == 'roadmap'
                           ? google.maps.MapTypeId.ROADMAP
                             : google.maps.MapTypeId.ROADMAP,
                scrollwheel: false,
                streetViewControl: false
            };
            this.map = new google.maps.Map(document.getElementById(this.container_id),
                                           map_options);
            $.each(this.lazy_init_funs, function(i, f) {
                f();
            });
            if (this.options.init) {
                this.options.init();
            }
        }
    };

    // Resize map.
    _Map.prototype.resize = function() {
        var self = this;
        var lazy_fun = function() {
            google.maps.event.trigger(self.map, 'resize');
            self.auto_zoom_to_fit();
        };
        if (!this.map) {
            this.lazy_init_funs.push(lazy_fun);
        } else {
            lazy_fun();
        }
    };

    return _Map;
})();

// NOTE: LI.MapLabel must be initialized after Google Maps JS has been fully
// loaded and intialized.
LI.MapLabel = (function() {
    function _MapLabel() {
        // Create label root element and set it's content HTML/text and also
        // set a class of "map-label" so that we can style all labels using
        // CSS.
        this.div = document.createElement('div');
        $(this.div).addClass('map-label').css({
            'position': 'absolute',
            'display': 'none'
        });

        // A list of event listeners.
        this.listeners = [];

        // Set prototype.
        _MapLabel.prototype = new google.maps.OverlayView;
    };

    // Public API:

    // Implement draw from the OverlayView interface.
    _MapLabel.prototype.draw = function() {
        // Using the bound position calculate where to place the label.
        var projection = this.getProjection(),
            position = projection.fromLatLngToDivPixel(this.get('position'));

        // Update CSS of label div and set content.
        $(this.div).css({
            'top': position.y + 'px',
            'left': position.x + 'px',
            'display': 'block'
        }).html(this.get('content').toString());
    };

    // Implement onAdd from the OverlayView interface.
    _MapLabel.prototype.onAdd = function() {
        // Add label DOM elements to the overlay layer.
        var pane = this.getPanes().overlayLayer;
        $(pane).append(this.div);

        // Ensures the label is redrawn if the text or position is changed.
        var self = this;
        this.listeners = [
            google.maps.event.addListener(this, 'content_changed', function() {
                self.draw();
            }),
            google.maps.event.addListener(this, 'position_changed', function() {
                self.draw();
            })
        ];
    };

    // Implement onRemove from the OverlayView interface.
    _MapLabel.prototype.onRemove = function() {
        $(this.div).remove();

        // Label is removed from the map, stop updating its content and
        // position.
        $.each(this.listeners, function(i, l) {
            google.maps.event.removeListener(l);
        });
    };

    // Set the HTML or text content of the label.
    _MapLabel.prototype.set_content = function(content) {
        this.set('content', '<div>' + content + '</div>');
    };

    // Function which sets an overlay object to track.
    _MapLabel.prototype.track = function(overlay) {
        // Bind this label to specified overlay on the map. Naming the value
        // as "position" in the MVCObject key-value store will let us listen to
        // the event "position_changed" to know when the position is updated.
        this.bindTo('position', overlay, 'position');
    };

    return _MapLabel;
})();

LI.MapAnimator = (function() {
    function _MapAnimator(map, options) {
        this.map = map;
        this.options = $.extend({
            'max_zoom': 8,
            'min_zoom': 6,
            'parallel': true,
            'interval': 0.1
        }, options || {});
        this.queue = [];
        this.running = false;
        this.lazy_start_funs = [];
    }

    // Public API:

    // Add step in animation.
    _MapAnimator.prototype.add_step = function(f, options) {
        var step_options = $.extend({
            'data': null,
            'step_start': null,
            'step_end': null
        }, options || {});
        var step = {
            'options': step_options,
            'run': f
        };
        this.queue.push(step);
    };

    // Add a line to the map between two markers.
    _MapAnimator.prototype.add_line_between_markers = function(name, src_name,
                                                               dst_name,
                                                               options) {
        var self = this;
        var lazy_fun = function() {
            var line_options = $.extend({
                    'append': true,
                    'done_callback': null,
                    'duration': 1000,
                    'easing': 'linear',
                    'geodesic': false,
                    'quadratic_bezier': true,
                    'step_callback': null
                }, options || {}),
                src_m = self.map.get_marker_from_name(src_name),
                dst_m = self.map.get_marker_from_name(dst_name);
            if (src_m && dst_m) {
                var src = self.map.get_marker_from_name(src_name).getPosition(),
                    dst = self.map.get_marker_from_name(dst_name).getPosition(),
                    gmgs = google.maps.geometry.spherical,
                    distance = gmgs.computeDistanceBetween(src, dst),
                    step_fun = null;
                if (line_options.quadratic_bezier) {
                    var qb = new LI.MapQuadraticBezierCurve(src, dst, -0.5);
                    step_fun = function(percent, step_callback) {
                        // Step function that gradually draws the line across the map
                        // as the animation percent progress increases.
                        if (distance > 1000) {
                            var step = null;
                            if (100 <= percent) {
                                step = dst;
                            } else {
                                step = qb.get_position(percent / 100.0);
                            }
                            self.map.add_line(name, src, step, line_options);
                            if (step_callback) {
                                step_callback(src, step);
                            }
                        }
                    };
                } else {
                    step_fun = function(percent, step_callback) {
                        // Step function that gradually draws the line across the map
                        // as the animation percent progress increases.
                        if (distance > 1000) {
                            var step = null;
                            if (100 <= percent) {
                                step = dst;
                            } else {
                                step = gmgs.interpolate(src, dst, percent / 100.0);
                            }
                            self.map.add_line(name, src, step, line_options);
                            if (step_callback) {
                                step_callback(src, step);
                            }
                        }
                    };
                }
                LI.Animation.animate(name, step_fun, line_options.duration,
                                     line_options.easing,
                                     line_options.done_callback,
                                     line_options.step_callback);
            }
        };
        if (!this.map.map) {
            this.lazy_start_funs.push(lazy_fun);
        } else {
            lazy_fun();
        }
    };

    // Animate a line on the map.
    _MapAnimator.prototype.animate_line = function(line_name, queue_name, props,
                                                   duration, easing, done) {
        var self = this;
        var lazy_fun = function() {
            var line_props_to_animate = $.extend({
                    'color': null,
                    'opacity': null,
                    'stroke_weight': null
                }, props || {}),
                line = self.map.get_line_from_name(line_name);
            if (line) {
                var line_options = self.map.get_line_options_from_name(line_name),
                    start_color = line_options.strokeColor,
                    start_opacity= line_options.strokeOpacity,
                    start_stroke_weight = line_options.strokeWeight,
                    step_fun = function(percent) {
                        if (line_props_to_animate.color) {
                            // TODO: We need color interpolation here!!!
                            /*line_options.strokeColor += (line_props_to_animate.color
                                                         - start_color)
                                                        * (percent / 100.0);*/
                        }
                        if (line_props_to_animate.opacity) {
                            line_options.strokeOpacity = start_opacity +
                                                         (line_props_to_animate.opacity
                                                          - start_opacity)
                                                          * (percent / 100.0);
                        }
                        if (line_props_to_animate.stroke_weight) {
                            line_options.strokeWeight = start_stroke_weight +
                                                        (line_props_to_animate.stroke_weight
                                                          - start_stroke_weight)
                                                         * (percent / 100.0);
                        }
                        line.setOptions(line_options);
                    };
                LI.Animation.animate(queue_name, step_fun, duration, easing, done);
            }
        };
        if (!this.map.map) {
            this.lazy_start_funs.push(lazy_fun);
        } else {
            lazy_fun();
        }
    };

    // Start animator.
    _MapAnimator.prototype.start = function() {
        $.each(this.lazy_start_funs, function(i, f) {
            f();
        });
    };

    return _MapAnimator;
})();

LI.MapQuadraticBezierCurve = (function() {
    function _MapQuadraticBezierCurve(src, dst, options) {
        this.src = src;
        this.dst = dst;
        this.middle = {
            lat: (this.src.lat() + this.dst.lat()) / 2.0,
            lng: (this.src.lng() + this.dst.lng()) / 2.0
        };
        this.options = $.extend({
            'curvedness': 0.5,
        }, options || {});
        this.ctrl = new google.maps.LatLng((this.middle.lng - this.dst.lng())
                                           * this.options.curvedness
                                           + this.middle.lat,
                                           (this.dst.lat() - this.middle.lat)
                                           * this.options.curvedness
                                           + this.middle.lng);
    }

    // Public API:

    _MapQuadraticBezierCurve.prototype.quadratic_bezier = function(t, f) {
        return (((1 - t) * (1 - t)) * (f(this.src)))
               + ((2 * t) * (1 - t) * (f(this.ctrl)))
               + ((t * t) * f(this.dst));
    };

    _MapQuadraticBezierCurve.prototype.get_position = function(t) {
        return new google.maps.LatLng(this.quadratic_bezier(t, function(pt) {
                                          return pt.lat();
                                      }),
                                      this.quadratic_bezier(t, function(pt) {
                                          return pt.lng();
                                      }));
    };

    return _MapQuadraticBezierCurve;
})();

