
var EDGY_STOPWATCH = {};

EDGY_STOPWATCH.new_google_graph = function (points, prefs) {
    if (! (points && prefs.title)) {
        return null;
    }

    var alt_base64_enc = function (num) {
        if (num >= Math.pow(64, 2)) {
            return "__";                // treat it as missing
        }
        var translate = function (num) {
            var c;
            var n = num;
            var int = function (s) { return s.charCodeAt(0); };
            if (n < 26) {
                c = int("A") + n;
            } else if (n < 52) {
                c = int("a") + (n % 26);
            } else if (n < 62) {
                c = int("0") + (n % 52);
            } else if (n === 62) {
                c = int("-");
            } else {  
                c = int(".");
            }
            return String.fromCharCode(c);
        };
        var low_num = num & 63;
        var high_num = num >> 6;
        return translate(high_num) + translate(low_num);
    };

    var encode_graph_points = function (spec) {
        // Google's absolute range is 0-4095 
        var scale_factor = 4095 / spec.scale_max;
        var s = "";
        for (var i = 0; i < spec.points.length; i++) {
            // We always want to be inside the target range - hence floor()
            var n = Math.floor(spec.points[i] * scale_factor);
            s += alt_base64_enc(n);
        }
        return s;
    };

    var y_axis_interval = function (max) {
        if (max < 11) {
            return 1; 
        } else if (max < 22) {
            return 2; 
        } else if (max < 55) {
            return 5; 
        } else if (max < 110) {
            return 10; 
        } else if (max < 220) {
            return 20; 
        } else if (max < 550) {
            return 50; 
        } else if (max < 1100) {
            return 100; 
        } else if (max < 2200) {
            return 200; 
        } else {
            return 500; 
        }
    };

    var axis_range = function (spec) {
        var y_interval = y_axis_interval(spec.scale_max);
        var y_axis_range = "0,0," + spec.scale_max + "," + y_interval;
        return spec.x_axis_range ?
                "chxt=y,x&chxr=" + y_axis_range + "|1," + spec.x_axis_range :
                "chxt=y&chxr=" + y_axis_range;
    };

    var define_graph = function (graph, spec) {
        var charts_api_url = "http://chart.apis.google.com/chart";
        var chart_type = "cht=" + spec.type; 
        var chart_size = "chs=" + spec.width + "x" + spec.height;
        var chart_title = "chtt=" + encodeURIComponent(spec.title);
        var chart_range = axis_range(spec);
        var chart_data = "chd=e:" + encode_graph_points(spec);
        graph.url = charts_api_url + 
                        "?" + chart_type + 
                        "&" + chart_size + 
                        "&" + chart_title +
                        "&" + chart_range +
                        "&" + chart_data +
                        spec.extra_url_args;
        return graph;
    };

    var max = function (points) {
        var max = 0;
        for (var i in points) {
            var n = points[i];
            max = (n > max) ? n : max;
        }
        return max;
    };

    var STANDARD_SCALE_MAX = 4095;     // We default to Google's scale (0-4095)

    var spec = {
        points: points,
        type: prefs.type || "lc",
        title: prefs.title,
        scale_max: prefs.zoom ? max(points) : STANDARD_SCALE_MAX,
        x_axis_range: prefs.x_axis_range,
        extra_url_args: prefs.extra_url_args || "",
        height: 200,
        width: 400
    };

    var graph = {
        url: null,
        title: spec.title,
        height: spec.height,
        width: spec.width
    };

    return define_graph(graph, spec);
};

EDGY_STOPWATCH.new_stats = function (mps, received_ticks, expected_count) { 
    var stats = {
            mps: mps,
            min: NaN,
            avg: NaN,
            median: NaN,
            max: NaN,
            pkt_loss: "100%",
            standard_graph: null,
            zoomed_graph: null
    };

    if (received_ticks.length === 0) {
        return stats;   
    }

    var sort = function (orig_nums) {
        var nums = orig_nums.slice(0);
        return nums.sort(function (a, b) { return a - b; });
    };

    var average = function (nums) {
        var total = 0;
        for (var i = 0; i < nums.length; i++) {
            total += nums[i];
        }
        return Math.round(total / nums.length);
    };

    var median = function (nums) {
        var is_odd = function (x) { return x % 2; };
        // Take account of zero-indexing
        if (is_odd(nums.length)) {
            var i = (nums.length - 1) / 2;
            return nums[i];
        } 
        var i = (nums.length / 2) - 1;
        return average([nums[i], nums[i+1]]);
    };

    var pkt_loss = function (received, expected) {
        if (received === expected) {
            return "-";
        }
        var n = ((expected - received) / expected) * 100;
        return n.toFixed() + "%";
    };

    var define_standard_graph = function (part_title, nums) {
        var title = part_title + " - Standard";
        var extra_url_args = "&chm=B,C4FD3D,0,0,0";
        return EDGY_STOPWATCH.new_google_graph(nums, {
            title: title,
            extra_url_args: extra_url_args
        });
    };

    var define_zoomed_graph = function (part_title, nums) {
        var title = part_title + " - Zoomed";
        return EDGY_STOPWATCH.new_google_graph(nums, {
            title: title,
            zoom: true
        });
    };

    var sorted_ticks = sort(received_ticks);
    stats.avg = average(sorted_ticks);
    stats.median = median(sorted_ticks);
    stats.min = sorted_ticks.shift();
    stats.max = sorted_ticks.pop() || stats.min;

    stats.pkt_loss = pkt_loss(received_ticks.length, expected_count);
    var part_title = "RTTs @" + stats.mps + " Msg/Sec";
    stats.standard_graph = define_standard_graph(part_title, received_ticks);
    stats.zoomed_graph = define_zoomed_graph(part_title, received_ticks);
    return stats;
};

EDGY_STOPWATCH.create_ui = function () { 
    var sequence = function (n1, n2) {
        var list = [];
        var i = n1;
        while (i != n2) {
            list.push(i);
            i = (i < n2) ? i + 1 : i - 1;
        }
        list.push(n2);
        return list;
    };

    var create_control_panel = function () {
        var auto_mps = function (tester, mps_start, mps_end) {
            return function () {
                var mps_list = sequence(mps_start, mps_end);
                tester.do_mps(mps_list);
                return false;
            };
        };
        var add_sting_test = function (tester, container) {
            var html = 
                "<a href='' id='sting-test-button' class='test-button'>Go!</a> \n" +
                "<p class='description'>Quick, strenuous, test.</p> \n" +
                "<table class='details'> \n" +
                "    <tr><td>Messages:</td><td>200</td></tr> \n" +
                "    <tr><td>Msgs/Sec:</td><td>30</td></tr> \n" +
                "    <tr><td>Duration:</td><td>~ 10 secs</td></tr> \n" +
                "</table> \n";
            var button = $(html);
            container.append(button);
            button.click(auto_mps(tester, 30, 30));
        };
        var add_hammock_test = function (tester, container) {
            var html = 
                "<a href='' id='hammock-test-button' class='test-button'>Go!</a> \n" +
                "<p class='description'>Complete test set.</p> \n" +
                "<table class='details'> \n" +
                "    <tr><td>Messages:</td><td>6000</td></tr> \n" +
                "    <tr><td>Msgs/Sec:</td><td>1...30</td></tr> \n" +
                "    <tr><td>Duration:</td><td>~ 15 mins</td></tr> \n" +
                "</table> \n";
            var button = $(html);
            container.append(button);
            button.click(auto_mps(tester, 1, 30));
        };
        var add_diy_test = function (tester, container) {
            var num_selector_options = function (selected) {
                var tag = "";
                for (var i = 1; i <= 30; i++) {
                    if (i === selected) {
                        tag += "<option selected='selected'>" + i + "</option> \n";
                    } else {
                        tag += "<option>" + i + "</option> \n";
                    }
                }
                return tag;
            };
            var mps_start_selector = 
                "<select id='mps_start'> \n" +
                num_selector_options(1) + 
                "</select> \n";
            var mps_end_selector = 
                "<select id='mps_end'> \n" +
                num_selector_options(30) + 
                "</select> \n";
            var form = 
                "<p class='description'>200 Msgs/Interval.</p> \n" +
                "<form> \n" +
                "    <label>Msgs/Sec Start: \n" + 
                mps_start_selector +
                "    </label> \n" +
                "    <br/> \n" +
                "    <label>Msgs/Sec End: \n" + 
                mps_end_selector +
                "    </label> \n" +
                "</form> \n";
            var button_html = "<a href='' id='diy-test-button' class='test-button'>Go!</a>";
            var button = $(button_html);
            container.append(button);
            container.append($(form));
            button.click(function () {
                var mps_start = Number($("#mps_start").val());
                var mps_end = Number($("#mps_end").val());
                var task = auto_mps(tester, mps_start, mps_end);
                task();
                return false;
            });
        };
        var control_panel = {
            init: function (tester) {
                var html =
                    "<div id='control_panel' class='page_segment'> \n" +
                    "    <h2>Control Panel</h2> \n" +
                    "    <div class='exper'> \n" + 
                    "    <div id='the_sting' class='test_option'> \n" +
                    "        <span class='opt_num'>Test Option #1</span> \n" +
                    "        <h3>The Sting</h3> \n" +
                    "    </div> \n" +
                    "    <div id='the_hammock' class='test_option'> \n" +
                    "        <span class='opt_num'>Test Option #2</span> \n" +
                    "        <h3>The Hammock</h3> \n" +
                    "    </div> \n" +
                    "    <div id='the_diy' class='test_option'> \n" +
                    "        <span class='opt_num'>Test Option #3</span> \n" +
                    "        <h3>DIY</h3> \n" +
                    "    </div> \n" +
                    "    </div> \n" +
                    "</div> \n";
                var panel = $(html);
                $("#test_main").append(panel);
                add_sting_test(tester, panel.find("#the_sting"));
                add_hammock_test(tester, panel.find("#the_hammock"));
                add_diy_test(tester, panel.find("#the_diy"));
                // CSS Cheat
                var highest = 0;
                $(".test_option").each(function () {
                    var h = $(this).height();
                    highest = h > highest ? h : highest;
                });
                $(".test_option").height(highest);
            }
        };
        return control_panel;
    };

    var create_stats_table = function () {
        var stats_table = {
            col_grapher: null,
            all_stats: [],
            init: function () {
                var html =
                    "<div class='page_segment'> \n" +
                    "    <h2>Statistics</h2> \n" +
                    "    <table id='stats'> \n" +
                    "        <tr> \n" +
                    "            <th>Messages Per/Sec:</th> \n" +
                    "            <th>Packet Loss:</th> \n" +
                    "            <th>Minimum:</th> \n" +
                    "            <th>Average:</th> \n" +
                    "            <th>Median:</th> \n" +
                    "            <th>Maximum:</th> \n" +
                    "            <th class='graph_actions'>Graph:</th> \n" +
                    "        </tr> \n" +
                    "    </table> \n" +
                    "    <div id='col_graph_viewer'/> \n" +
                    "</div> \n";
                $("#test_main").append($(html));
            },
            update: function (stats) {
                this.all_stats.push(stats);
                if (! this.col_grapher) {
                    this.init_col_grapher();
                }
                var tag = 
                    "<tr> \n" +
                    "    <td>" + stats.mps + "</td> \n" +
                    "    <td>" + stats.pkt_loss + "</td> \n" +
                    "    <td>" + stats.min + "</td> \n" +
                    "    <td>" + stats.avg + "</td> \n" +
                    "    <td>" + stats.median + "</td> \n" +
                    "    <td>" + stats.max + "</td> \n" +
                    "    <td><a class='graph_launcher' href=''>draw graph</a></td> \n" +
                    "</tr> + \n";
                var row = $(tag);
                this.col_grapher.before(row);
                row.find(".graph_launcher").toggle(
                    function () {
                        stats_table.draw_row_graph($(this), stats);
                        $(this).text("hide graph");
                        return false;
                    },
                    function () {
                        stats_table.remove_row_graph($(this));
                        $(this).text("draw graph");
                        return false;
                    }
                );
            },
            init_col_grapher: function () {
                var tag = 
                    "<tr id='col_grapher'> \n" +
                    "    <td class='graph_actions'>Graph:</td> \n" +
                    "    <td>-</td> \n" +
                    "    <td><a class='min_grapher' href=''>draw graph</a></td> \n" +
                    "    <td><a class='avg_grapher' href=''>draw graph</a></td> \n" +
                    "    <td><a class='median_grapher' href=''>draw graph</a></td> \n" +
                    "    <td><a class='max_grapher' href=''>draw graph</a></td> \n" +
                    "    <td>-</td> \n" +
                    "</tr> + \n";
                var col_grapher = $(tag);
                $("#stats").append(col_grapher);
                var stat_types = ["min", "avg", "median", "max"];
                var on_col_graph_handler = function (stat_type) {
                    return function () {
                        stats_table.draw_col_graph(stat_type);
                        return false;
                    }
                };
                for (var i in stat_types) {
                    var stat_type = stat_types[i];
                    var class_name = "." + stat_type + "_grapher";
                    var handler = on_col_graph_handler(stat_type);
                    col_grapher.find(class_name).click(handler);
                }
                this.col_grapher = col_grapher;
            },
            draw_col_graph: function (stat_type) {
                var points = [];
                for (var i = 0; i < 30; i++) {
                    points[i] = 0;
                    var mps = i + 1;
                    for (var j in this.all_stats) {
                        if (this.all_stats[j].mps === mps) {
                            points[i] = this.all_stats[j][stat_type];
                        }
                    }
                }
                var name = 
                        (stat_type === "min") ? "Minimum" :
                        (stat_type === "avg") ? "Average" :
                        (stat_type === "median") ? "Median" :
                        (stat_type === "max") ? "Maximum" : null;
                var part_title = name + " RTTs";
                var graph_spec = {
                    type: "bvs",
                    title: part_title + " - Standard",
                    x_axis_range: "0,30,2",
                    extra_url_args: "&chco=4D89F9&chbh=a"
                }
                var standard_graph = EDGY_STOPWATCH.new_google_graph(points, graph_spec);
                graph_spec.title = part_title + " - Zoomed";
                graph_spec.zoom = true;
                var zoomed_graph = EDGY_STOPWATCH.new_google_graph(points, graph_spec);
                var tags = 
                        this.image_tag_for(standard_graph) +
                        this.image_tag_for(zoomed_graph);
                $("#col_graph_viewer").empty().append($(tags));
            },
            draw_row_graph: function (jnode, stats) {
                var row =
                    "<tr class='graph_view'> \n" +
                    "    <td colspan='7'> \n" +
                    this.image_tag_for(stats.standard_graph) + "\n" +
                    this.image_tag_for(stats.zoomed_graph) + "\n" +
                    "    </td> \n" +
                    "</tr> \n";
                var tr = $("<tr class='graph_view'></td></tr>");
                jnode.parents("tr").after($(row));
            },
            remove_row_graph: function (jnode, stats) {
                jnode.parents("tr").next().remove();
            },
            image_tag_for: function (graph) {
                return "<img src='" + graph.url + "' \n" +
                       "     alt='" + graph.title + "' \n" +
                       "     height='" + graph.height + "' \n" +
                       "     width='" + graph.width + "' /> \n";
            }
        };
        return stats_table;
    };

    var create_replies_table = function () {
        var replies_table = {
            pending_write: [],
            init: function () {
                var html = 
                    "<div class='page_segment'> \n" +
                    "    <h2>Replies</h2> \n" +
                    "    <table id='replies'> \n" +
                    "        <tr> \n" +
                    "            <th>Reply:</th> \n" + 
                    "            <th>Messages Per/Sec:</th> \n" +
                    "            <th>Round Trip Time (millisecs):</th> \n" +
                    "        </tr> \n" +
                    "    </table> \n" +
                    "</div> \n";
                $("#test_main").append($(html));
                var flush = function () {
                    replies_table.flush();
                };
                window.setInterval(flush, 1000);
            },
            update: function (reply) {
                this.pending_write.push(reply);
            },
            flush: function () {
                var html = "";
                var reply;
                while (reply = this.pending_write.shift()) {
                    html += "<tr>";
                    for (var i in reply) {
                        html += "<td>" + reply[i] + "</td>";
                    }
                    html += "</tr> \n";
                }
                if (html) {
                    $("#replies").append($(html));
                }
            }
        };
        return replies_table;
    };

    var start_load_status_indicator = function () {
        var indicator = {
            timer_ref: null,
            init: function () {
                var first_call = function () {
                    $("#load-status").show();
                    var add_dot = function () {
                        var s = $("#load-status").text();
                        $("#load-status").text(s + ".");
                    };
                    window.clearInterval(indicator.timer_ref);
                    indicator.timer_ref = window.setInterval(add_dot, 1000);
                };
                // Should be a timeout, but we use interval to make stop() simpler
                indicator.timer_ref = window.setInterval(first_call, 1000);
            },
            stop: function () {
                window.clearInterval(this.timer_ref);
            },
            remove: function () {
                $("#load-status").remove();
                this.stop();
            }
        };
        indicator.init();
        return indicator;
    };

    var ui = {
        tester: null,
        load_status_indicator: start_load_status_indicator(),
        control_panel: create_control_panel(),
        stats_table: create_stats_table(),
        replies_table: create_replies_table(),
        init: function (tester) {
            this.tester = tester;
            this.load_status_indicator.remove();
            this.load_status_indicator = null;
            this.control_panel.init(tester);
            this.stats_table.init();
            this.replies_table.init();
        },
        fail_init: function () {
            this.load_status_indicator.stop();
            Err = $("<p id='fail-result'>! Failed to Initialise</p>");
            $("#test_main").append(Err);
        },
        update_replies: function (reply) {
            this.replies_table.update(reply);
        },
        update_stats: function (stats) {
            this.stats_table.update(stats);
        }
    };

    return ui;
};

EDGY_STOPWATCH.init_mps_tester = function (transport_type) { 

    var timer_ref = null;

    var tester = {
        ITERATIONS: 200,
        ticks: [],
        pubsub: null,
        input_channel: null,
        ui: EDGY_STOPWATCH.create_ui(),
        init: function (transport_type) {
            this.pubsub = EDGY.new_pubsub(transport_type); 
            var cb = function (client_id) {
                tester.on_connect(client_id);
            };
            this.pubsub.init(cb);
        },
        on_connect: function (client_id) {
            if (client_id) {
                var chan_in = "/echo/" + client_id + "/in"; 
                var chan_out = "/echo/" + client_id + "/out";
                this.input_channel = chan_in;
                this.pubsub.subscribe(chan_out, this, this.recv);
                this.ui.init(this);
            } else {
                this.ui.fail_init();
            }
        },
        send: function (s) {
            var millisecs = (new Date()).getTime();
            s = s + " (" + millisecs + ")";
            if (this.input_channel) {
                this.pubsub.publish(this.input_channel, s);
            }
        },
        recv: function (message) {
            var s = message.data;
            var t1 = s.replace(/^.*\(([^\)]*)\)$/, "$1");
            if (t1 !== s) {
                var now = (new Date()).getTime();
                var time = now - t1;
                this.ticks.push(time);
                var parts = s.split(/\s+/);
                this.ui.update_replies([parts[0], parts[1], time]);
            } 
        },
        print_stats: function (mps) {
            var stats = EDGY_STOPWATCH.new_stats(mps, this.ticks, this.ITERATIONS);
            this.ui.update_stats(stats);
        },
        do_mps: function (mps_list) {
            if ((! timer_ref) && mps_list.length > 0) {
                var mps = mps_list.shift();
                this.ticks = [];
                var i = 1;
                var send_next = function () {
                    tester.send(i + " " + mps);
                    i++;
                    if (i > tester.ITERATIONS) {
                        // Allow four secs for last packets to arrive
                        // back and then continue with next batch.
                        window.clearInterval(timer_ref);
                        var task = function () {
                            tester.print_stats(mps);
                            timer_ref = null;
                            tester.do_mps(mps_list);
                        };
                        window.setTimeout(task, 4000);
                    }
                };
                var interval = Math.round(1000 / mps);
                timer_ref = window.setInterval(send_next, interval);
            }
        }
    };

    tester.init(transport_type);
};

