Utilities.extendObject(window.benchmarkController, {
    updateGraphData: function(testResult, testData, options)
    {
        var element = document.getElementById("test-graph-data");
        element.innerHTML = "";
        element._testResult = testResult;
        element._options = options;

        var margins = new Insets(30, 30, 30, 40);
        var size = Point.elementClientSize(element);
        size.y = window.innerHeight - element.offsetTop;
        size = size.subtract(margins.size);

        // Convert from compact JSON output to propertied data
        var samplesWithProperties = {};
        [Strings.json.controller, Strings.json.complexity, Strings.json.complexityAverage].forEach(function(seriesName) {
            var series = testData[Strings.json.samples][seriesName];
            samplesWithProperties[seriesName] = series.toArray();
        })

        this.createTimeGraph(testResult, samplesWithProperties[Strings.json.controller], testData[Strings.json.marks], testData[Strings.json.controller], options, margins, size);
        this.onTimeGraphOptionsChanged();

        this._showOrHideNodes(true, "form[name=graph-type]");
        document.forms["graph-type"].elements["type"] = "complexity";
        this.createComplexityGraph(testResult, testData[Strings.json.controller], samplesWithProperties, options, margins, size);
        this.onComplexityGraphOptionsChanged();

        this.onGraphTypeChanged();
    },

    _addRegressionLine: function(parent, xScale, yScale, points, range, isAlongYAxis)
    {
        var polygon = [];
        var line = []
        var xRange = isAlongYAxis ? range : 0;
        var yRange = isAlongYAxis ? 0 : range;
        for (var i = 0; i < points.length; ++i) {
            var point = points[i];
            var x;
            if (xRange instanceof Array)
                x = xRange[0];
            else
                x = point[0] + xRange;
            polygon.push(xScale(x), yScale(point[1] + yRange));
            line.push(xScale(point[0]), yScale(point[1]));
        }
        for (var i = points.length - 1; i >= 0; --i) {
            var point = points[i];
            var x;
            if (xRange instanceof Array)
                x = xRange[1];
            else
                x = point[0] - xRange;
            polygon.push(xScale(x), yScale(point[1] - yRange));
        }
        parent.append("polygon")
            .attr("points", polygon.join(","));
        parent.append("line")
            .attr("x1", line[0])
            .attr("y1", line[1])
            .attr("x2", line[2])
            .attr("y2", line[3]);
    },

    _addRegression: function(data, svg, xScale, yScale)
    {
        svg.append("circle")
            .attr("cx", xScale(data.segment1[1][0]))
            .attr("cy", yScale(data.segment1[1][1]))
            .attr("r", 5);
        this._addRegressionLine(svg, xScale, yScale, data.segment1, data.stdev);
        this._addRegressionLine(svg, xScale, yScale, data.segment2, data.stdev);
    },

    createComplexityGraph: function(result, timeRegressions, data, options, margins, size)
    {
        var svg = d3.select("#test-graph-data").append("svg")
            .attr("id", "complexity-graph")
            .attr("class", "hidden")
            .attr("width", size.width + margins.left + margins.right)
            .attr("height", size.height + margins.top + margins.bottom)
            .append("g")
                .attr("transform", "translate(" + margins.left + "," + margins.top + ")");

        var timeSamples = data[Strings.json.controller];

        var xMin = 100000, xMax = 0;
        if (timeRegressions) {
            timeRegressions.forEach(function(regression) {
                for (var i = regression.startIndex; i <= regression.endIndex; ++i) {
                    xMin = Math.min(xMin, timeSamples[i].complexity);
                    xMax = Math.max(xMax, timeSamples[i].complexity);
                }
            });
        } else {
            xMin = d3.min(timeSamples, function(s) { return s.complexity; });
            xMax = d3.max(timeSamples, function(s) { return s.complexity; });
        }

        var xScale = d3.scale.linear()
            .range([0, size.width])
            .domain([xMin, xMax]);
        var yScale = d3.scale.linear()
            .range([size.height, 0])
            .domain([1000/20, 1000/60]);

        var xAxis = d3.svg.axis()
            .scale(xScale)
            .orient("bottom");
        var yAxis = d3.svg.axis()
            .scale(yScale)
            .tickValues([1000/20, 1000/25, 1000/30, 1000/35, 1000/40, 1000/45, 1000/50, 1000/55, 1000/60])
            .tickFormat(function(d) { return (1000 / d).toFixed(0); })
            .orient("left");

        // x-axis
        svg.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + size.height + ")")
            .call(xAxis);

        // y-axis
        svg.append("g")
            .attr("class", "y axis")
            .call(yAxis);

        // time result
        var mean = svg.append("g")
            .attr("class", "mean complexity");
        var timeResult = result[Strings.json.controller];
        var yMin = yScale.domain()[0], yMax = yScale.domain()[1];
        this._addRegressionLine(mean, xScale, yScale, [[timeResult.average, yMin], [timeResult.average, yMax]], timeResult.stdev, true);

        // regression
        this._addRegression(result[Strings.json.complexity], svg.append("g").attr("class", "regression raw"), xScale, yScale);
        this._addRegression(result[Strings.json.complexityAverage], svg.append("g").attr("class", "regression average"), xScale, yScale);

        var bootstrapResult = result[Strings.json.complexity][Strings.json.bootstrap];
        if (bootstrapResult) {
            var histogram = d3.layout.histogram()
                .bins(xScale.ticks(100))(bootstrapResult.data);
            var yBootstrapScale = d3.scale.linear()
                .range([size.height/2, 0])
                .domain([0, d3.max(histogram, function(d) { return d.y; })]);
            group = svg.append("g").attr("class", "bootstrap");
            var bar = group.selectAll(".bar")
                .data(histogram)
                .enter().append("g")
                    .attr("class", "bar")
                    .attr("transform", function(d) { return "translate(" + xScale(d.x) + "," + yBootstrapScale(d.y) + ")"; });
            bar.append("rect")
                .attr("x", 1)
                .attr("y", size.height/2)
                .attr("width", xScale(histogram[1].x) - xScale(histogram[0].x) - 1)
                .attr("height", function(d) { return size.height/2 - yBootstrapScale(d.y); });
            group = group.append("g").attr("class", "median");
            this._addRegressionLine(group, xScale, yScale, [[bootstrapResult.median, yMin], [bootstrapResult.median, yMax]], [bootstrapResult.confidenceLow, bootstrapResult.confidenceHigh], true);
            group.append("circle")
                .attr("cx", xScale(bootstrapResult.median))
                .attr("cy", yScale(1000/60))
                .attr("r", 5);
        }

        // series
        group = svg.append("g")
            .attr("class", "series raw")
            .selectAll("line")
                .data(data[Strings.json.complexity])
                .enter();
        group.append("line")
            .attr("x1", function(d) { return xScale(d.complexity) - 3; })
            .attr("x2", function(d) { return xScale(d.complexity) + 3; })
            .attr("y1", function(d) { return yScale(d.frameLength) - 3; })
            .attr("y2", function(d) { return yScale(d.frameLength) + 3; });
        group.append("line")
            .attr("x1", function(d) { return xScale(d.complexity) - 3; })
            .attr("x2", function(d) { return xScale(d.complexity) + 3; })
            .attr("y1", function(d) { return yScale(d.frameLength) + 3; })
            .attr("y2", function(d) { return yScale(d.frameLength) - 3; });

        group = svg.append("g")
            .attr("class", "series average")
            .selectAll("circle")
                .data(data[Strings.json.complexityAverage])
                .enter();
        group.append("circle")
            .attr("cx", function(d) { return xScale(d.complexity); })
            .attr("cy", function(d) { return yScale(d.frameLength); })
            .attr("r", 3)
        group.append("line")
            .attr("x1", function(d) { return xScale(d.complexity); })
            .attr("x2", function(d) { return xScale(d.complexity); })
            .attr("y1", function(d) { return yScale(d.frameLength - d.stdev); })
            .attr("y2", function(d) { return yScale(d.frameLength + d.stdev); });

        // Cursor
        var cursorGroup = svg.append("g").attr("class", "cursor hidden");
        cursorGroup.append("line")
            .attr("class", "x")
            .attr("x1", 0)
            .attr("x2", 0)
            .attr("y1", yScale(yAxis.scale().domain()[0]) + 10)
            .attr("y2", yScale(yAxis.scale().domain()[1]));
        cursorGroup.append("line")
            .attr("class", "y")
            .attr("x1", xScale(xAxis.scale().domain()[0]) - 10)
            .attr("x2", xScale(xAxis.scale().domain()[1]))
            .attr("y1", 0)
            .attr("y2", 0)
        cursorGroup.append("text")
            .attr("class", "label x")
            .attr("x", 0)
            .attr("y", yScale(yAxis.scale().domain()[0]) + 15)
            .attr("baseline-shift", "-100%")
            .attr("text-anchor", "middle");
        cursorGroup.append("text")
            .attr("class", "label y")
            .attr("x", xScale(xAxis.scale().domain()[0]) - 15)
            .attr("y", 0)
            .attr("baseline-shift", "-30%")
            .attr("text-anchor", "end");
        // Area to handle mouse events
        var area = svg.append("rect")
            .attr("fill", "transparent")
            .attr("x", 0)
            .attr("y", 0)
            .attr("width", size.width)
            .attr("height", size.height);

        area.on("mouseover", function() {
            document.querySelector("#complexity-graph .cursor").classList.remove("hidden");
        }).on("mouseout", function() {
            document.querySelector("#complexity-graph .cursor").classList.add("hidden");
        }).on("mousemove", function() {
            var location = d3.mouse(this);
            var location_domain = [xScale.invert(location[0]), yScale.invert(location[1])];
            cursorGroup.select("line.x")
                .attr("x1", location[0])
                .attr("x2", location[0]);
            cursorGroup.select("text.x")
                .attr("x", location[0])
                .text(location_domain[0].toFixed(1));
            cursorGroup.select("line.y")
                .attr("y1", location[1])
                .attr("y2", location[1]);
            cursorGroup.select("text.y")
                .attr("y", location[1])
                .text((1000 / location_domain[1]).toFixed(1));
        });
    },

    createTimeGraph: function(result, samples, marks, regressions, options, margins, size)
    {
        var svg = d3.select("#test-graph-data").append("svg")
            .attr("id", "time-graph")
            .attr("width", size.width + margins.left + margins.right)
            .attr("height", size.height + margins.top + margins.bottom)
            .append("g")
                .attr("transform", "translate(" + margins.left + "," + margins.top + ")");

        // Axis scales
        var x = d3.scale.linear()
                .range([0, size.width])
                .domain([
                    Math.min(d3.min(samples, function(s) { return s.time; }), 0),
                    d3.max(samples, function(s) { return s.time; })]);
        var complexityMax = d3.max(samples, function(s) {
            if (s.time > 0)
                return s.complexity;
            return 0;
        });

        var yLeft = d3.scale.linear()
                .range([size.height, 0])
                .domain([0, complexityMax]);
        var yRight = d3.scale.linear()
                .range([size.height, 0])
                .domain([1000/20, 1000/60]);

        // Axes
        var xAxis = d3.svg.axis()
                .scale(x)
                .orient("bottom")
                .tickFormat(function(d) { return (d/1000).toFixed(0); });
        var yAxisLeft = d3.svg.axis()
                .scale(yLeft)
                .orient("left");
        var yAxisRight = d3.svg.axis()
                .scale(yRight)
                .tickValues([1000/20, 1000/25, 1000/30, 1000/35, 1000/40, 1000/45, 1000/50, 1000/55, 1000/60])
                .tickFormat(function(d) { return (1000/d).toFixed(0); })
                .orient("right");

        // x-axis
        svg.append("g")
            .attr("class", "x axis")
            .attr("fill", "rgb(235, 235, 235)")
            .attr("transform", "translate(0," + size.height + ")")
            .call(xAxis)
            .append("text")
                .attr("class", "label")
                .attr("x", size.width)
                .attr("y", -6)
                .attr("fill", "rgb(235, 235, 235)")
                .style("text-anchor", "end")
                .text("time");

        // yLeft-axis
        svg.append("g")
            .attr("class", "yLeft axis")
            .attr("fill", "#7ADD49")
            .call(yAxisLeft)
            .append("text")
                .attr("class", "label")
                .attr("transform", "rotate(-90)")
                .attr("y", 6)
                .attr("fill", "#7ADD49")
                .attr("dy", ".71em")
                .style("text-anchor", "end")
                .text(Strings.text.complexity);

        // yRight-axis
        svg.append("g")
            .attr("class", "yRight axis")
            .attr("fill", "#FA4925")
            .attr("transform", "translate(" + size.width + ", 0)")
            .call(yAxisRight)
            .append("text")
                .attr("class", "label")
                .attr("x", 9)
                .attr("y", -20)
                .attr("fill", "#FA4925")
                .attr("dy", ".71em")
                .style("text-anchor", "start")
                .text(Strings.text.frameRate);

        // marks
        var yMin = yRight(yAxisRight.scale().domain()[0]);
        var yMax = yRight(yAxisRight.scale().domain()[1]);
        for (var markName in marks) {
            var mark = marks[markName];
            var xLocation = x(mark.time);

            var markerGroup = svg.append("g")
                .attr("class", "marker")
                .attr("transform", "translate(" + xLocation + ", 0)");
            markerGroup.append("text")
                .attr("transform", "translate(10, " + (yMin - 10) + ") rotate(-90)")
                .style("text-anchor", "start")
                .text(markName)
            markerGroup.append("line")
                .attr("x1", 0)
                .attr("x2", 0)
                .attr("y1", yMin)
                .attr("y2", yMax);
        }

        if (Strings.json.controller in result) {
            var complexity = result[Strings.json.controller];
            var regression = svg.append("g")
                .attr("class", "complexity mean");
            this._addRegressionLine(regression, x, yLeft, [[samples[0].time, complexity.average], [samples[samples.length - 1].time, complexity.average]], complexity.stdev);
        }
        if (Strings.json.frameLength in result) {
            var frameLength = result[Strings.json.frameLength];
            var regression = svg.append("g")
                .attr("class", "fps mean");
            this._addRegressionLine(regression, x, yRight, [[samples[0].time, 1000/frameLength.average], [samples[samples.length - 1].time, 1000/frameLength.average]], frameLength.stdev);
        }

        // right-target
        if (options["controller"] == "adaptive") {
            var targetFrameLength = 1000 / options["frame-rate"];
            svg.append("line")
                .attr("x1", x(0))
                .attr("x2", size.width)
                .attr("y1", yRight(targetFrameLength))
                .attr("y2", yRight(targetFrameLength))
                .attr("class", "target-fps marker");
        }

        // Cursor
        var cursorGroup = svg.append("g").attr("class", "cursor");
        cursorGroup.append("line")
            .attr("x1", 0)
            .attr("x2", 0)
            .attr("y1", yMin)
            .attr("y2", yMin);

        // Data
        var allData = samples;
        var filteredData = samples.filter(function (sample) {
            return "smoothedFrameLength" in sample;
        });

        function addData(name, data, yCoordinateCallback, pointRadius, omitLine) {
            var svgGroup = svg.append("g").attr("id", name);
            if (!omitLine) {
                svgGroup.append("path")
                    .datum(data)
                    .attr("d", d3.svg.line()
                        .x(function(d) { return x(d.time); })
                        .y(yCoordinateCallback));
            }
            svgGroup.selectAll("circle")
                .data(data)
                .enter()
                .append("circle")
                .attr("cx", function(d) { return x(d.time); })
                .attr("cy", yCoordinateCallback)
                .attr("r", pointRadius);

            cursorGroup.append("circle")
                .attr("class", name)
                .attr("r", pointRadius + 2);
        }

        addData("complexity", allData, function(d) { return yLeft(d.complexity); }, 2);
        addData("rawFPS", allData, function(d) { return yRight(d.frameLength); }, 1);
        addData("filteredFPS", filteredData, function(d) { return yRight(d.smoothedFrameLength); }, 2);

        // regressions
        var regressionGroup = svg.append("g")
            .attr("id", "regressions");
        if (regressions) {
            var complexities = [];
            regressions.forEach(function (regression) {
                if (!isNaN(regression.segment1[0][1]) && !isNaN(regression.segment1[1][1])) {
                    regressionGroup.append("line")
                        .attr("x1", x(regression.segment1[0][0]))
                        .attr("x2", x(regression.segment1[1][0]))
                        .attr("y1", yRight(regression.segment1[0][1]))
                        .attr("y2", yRight(regression.segment1[1][1]));
                }
                if (!isNaN(regression.segment2[0][1]) && !isNaN(regression.segment2[1][1])) {
                    regressionGroup.append("line")
                        .attr("x1", x(regression.segment2[0][0]))
                        .attr("x2", x(regression.segment2[1][0]))
                        .attr("y1", yRight(regression.segment2[0][1]))
                        .attr("y2", yRight(regression.segment2[1][1]));
                }
                // inflection point
                regressionGroup.append("circle")
                    .attr("cx", x(regression.segment1[1][0]))
                    .attr("cy", yLeft(regression.complexity))
                    .attr("r", 5);
                complexities.push(regression.complexity);
            });
            if (complexities.length) {
                var yLeftComplexities = d3.svg.axis()
                    .scale(yLeft)
                    .tickValues(complexities)
                    .tickSize(10)
                    .orient("left");
                svg.append("g")
                    .attr("class", "complexity yLeft axis")
                    .call(yLeftComplexities);
            }
        }

        // Area to handle mouse events
        var area = svg.append("rect")
            .attr("fill", "transparent")
            .attr("x", 0)
            .attr("y", 0)
            .attr("width", size.width)
            .attr("height", size.height);

        var timeBisect = d3.bisector(function(d) { return d.time; }).right;
        var statsToHighlight = ["complexity", "rawFPS", "filteredFPS"];
        area.on("mouseover", function() {
            document.querySelector("#time-graph .cursor").classList.remove("hidden");
            document.querySelector("#test-graph nav").classList.remove("hide-data");
        }).on("mouseout", function() {
            document.querySelector("#time-graph .cursor").classList.add("hidden");
            document.querySelector("#test-graph nav").classList.add("hide-data");
        }).on("mousemove", function() {
            var form = document.forms["time-graph-options"].elements;

            var mx_domain = x.invert(d3.mouse(this)[0]);
            var index = Math.min(timeBisect(allData, mx_domain), allData.length - 1);
            var data = allData[index];
            var cursor_x = x(data.time);
            var cursor_y = yAxisRight.scale().domain()[1];
            var ys = [yRight(yAxisRight.scale().domain()[0]), yRight(yAxisRight.scale().domain()[1])];

            document.querySelector("#test-graph nav .time").textContent = (data.time / 1000).toFixed(4) + "s (" + index + ")";
            statsToHighlight.forEach(function(name) {
                var element = document.querySelector("#test-graph nav ." + name);
                var content = "";
                var data_y = null;
                switch (name) {
                case "complexity":
                    content = data.complexity;
                    data_y = yLeft(data.complexity);
                    break;
                case "rawFPS":
                    content = (1000/data.frameLength).toFixed(2);
                    data_y = yRight(data.frameLength);
                    break;
                case "filteredFPS":
                    if ("smoothedFrameLength" in data) {
                        content = (1000/data.smoothedFrameLength).toFixed(2);
                        data_y = yRight(data.smoothedFrameLength);
                    }
                    break;
                }

                element.textContent = content;

                if (form[name].checked && data_y !== null) {
                    ys.push(data_y);
                    cursorGroup.select("." + name)
                        .attr("cx", cursor_x)
                        .attr("cy", data_y);
                    document.querySelector("#time-graph .cursor ." + name).classList.remove("hidden");
                } else
                    document.querySelector("#time-graph .cursor ." + name).classList.add("hidden");
            });

            if (form["rawFPS"].checked)
                cursor_y = Math.max(cursor_y, data.frameLength);
            cursorGroup.select("line")
                .attr("x1", cursor_x)
                .attr("x2", cursor_x)
                .attr("y1", Math.min.apply(null, ys))
                .attr("y2", Math.max.apply(null, ys));

        });
    },

    _showOrHideNodes: function(isShown, selector) {
        var nodeList = document.querySelectorAll(selector);
        if (isShown) {
            for (var i = 0; i < nodeList.length; ++i)
                nodeList[i].classList.remove("hidden");
        } else {
            for (var i = 0; i < nodeList.length; ++i)
                nodeList[i].classList.add("hidden");
        }
    },

    onComplexityGraphOptionsChanged: function() {
        var form = document.forms["complexity-graph-options"].elements;
        benchmarkController._showOrHideNodes(form["series-raw"].checked, "#complexity-graph .series.raw");
        benchmarkController._showOrHideNodes(form["series-average"].checked, "#complexity-graph .series.average");
        benchmarkController._showOrHideNodes(form["regression-time-score"].checked, "#complexity-graph .mean.complexity");
        benchmarkController._showOrHideNodes(form["bootstrap-score"].checked, "#complexity-graph .bootstrap");
        benchmarkController._showOrHideNodes(form["complexity-regression-aggregate-raw"].checked, "#complexity-graph .regression.raw");
        benchmarkController._showOrHideNodes(form["complexity-regression-aggregate-average"].checked, "#complexity-graph .regression.average");
    },

    onTimeGraphOptionsChanged: function() {
        var form = document.forms["time-graph-options"].elements;
        benchmarkController._showOrHideNodes(form["markers"].checked, ".marker");
        benchmarkController._showOrHideNodes(form["averages"].checked, "#test-graph-data .mean");
        benchmarkController._showOrHideNodes(form["complexity"].checked, "#complexity");
        benchmarkController._showOrHideNodes(form["rawFPS"].checked, "#rawFPS");
        benchmarkController._showOrHideNodes(form["filteredFPS"].checked, "#filteredFPS");
        benchmarkController._showOrHideNodes(form["regressions"].checked, "#regressions");
    },

    onGraphTypeChanged: function() {
        var form = document.forms["graph-type"].elements;
        var testResult = document.getElementById("test-graph-data")._testResult;
        var isTimeSelected = form["graph-type"].value == "time";

        benchmarkController._showOrHideNodes(isTimeSelected, "#time-graph");
        benchmarkController._showOrHideNodes(isTimeSelected, "form[name=time-graph-options]");
        benchmarkController._showOrHideNodes(!isTimeSelected, "#complexity-graph");
        benchmarkController._showOrHideNodes(!isTimeSelected, "form[name=complexity-graph-options]");

        var score = "", mean = "";
        if (isTimeSelected) {
            score = testResult[Strings.json.score].toFixed(2);

            var regression = testResult[Strings.json.controller];
            mean = [
                "mean: ",
                regression.average.toFixed(2),
                " ± ",
                regression.stdev.toFixed(2),
                " (",
                regression.percent.toFixed(2),
                "%)"];
            if (regression.concern) {
                mean = mean.concat([
                    ", worst 5%: ",
                    regression.concern.toFixed(2)]);
            }
            mean = mean.join("");
        } else {
            var complexityRegression = testResult[Strings.json.complexity];
            var complexityAverageRegression = testResult[Strings.json.complexityAverage];

            document.getElementById("complexity-regression-aggregate-raw").textContent = complexityRegression.complexity.toFixed(2) + ", ±" + complexityRegression.stdev.toFixed(2) + "ms";
            document.getElementById("complexity-regression-aggregate-average").textContent = complexityAverageRegression.complexity.toFixed(2) + ", ±" + complexityAverageRegression.stdev.toFixed(2) + "ms";

            var bootstrap = complexityRegression[Strings.json.bootstrap];
            if (bootstrap) {
                score = bootstrap.median.toFixed(2);
                mean = [
                    (bootstrap.confidencePercentage * 100).toFixed(0),
                    "% CI: ",
                    bootstrap.confidenceLow.toFixed(2),
                    "–",
                    bootstrap.confidenceHigh.toFixed(2)
                ].join("");
            }
        }

        sectionsManager.setSectionScore("test-graph", score, mean);
    }
});
