(function() {
  'use strict';
  window.addEventListener('beforeprint', function() {
    for (var id in Chart.instances) {
      Chart.instances[id].resize();
    }
  }, false);

  var errorBarPlugin = (function () {
    function drawErrorBar(chart, ctx, low, high, y, height, color) {
      ctx.save();
      ctx.lineWidth = 3;
      ctx.strokeStyle = color;
      var area = chart.chartArea;
      ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top);
      ctx.clip();
      ctx.beginPath();
      ctx.moveTo(low, y - height);
      ctx.lineTo(low, y + height);
      ctx.moveTo(low, y);
      ctx.lineTo(high, y);
      ctx.moveTo(high, y - height);
      ctx.lineTo(high, y + height);
      ctx.stroke();
      ctx.restore();
    }
    // Avoid sudden jumps in error bars when switching
    // between linear and logarithmic scale
    function conservativeError(vx, mx, now, final, scale) {
      var finalDiff = Math.abs(mx - final);
      var diff = Math.abs(vx - now);
      return (diff > finalDiff) ? vx + scale * finalDiff : now;
    }
    return {
      afterDatasetDraw: function(chart, easingOptions) {
        var ctx = chart.ctx;
        var easing = easingOptions.easingValue;
        chart.data.datasets.forEach(function(d, i) {
          var bars = chart.getDatasetMeta(i).data;
          var axis = chart.scales[chart.options.scales.xAxes[0].id];
          bars.forEach(function(b, j) {
            var value = axis.getValueForPixel(b._view.x);
            var final = axis.getValueForPixel(b._model.x);
            var errorBar = d.errorBars[j];
            var low = axis.getPixelForValue(value - errorBar.minus);
            var high = axis.getPixelForValue(value + errorBar.plus);
            var finalLow = axis.getPixelForValue(final - errorBar.minus);
            var finalHigh = axis.getPixelForValue(final + errorBar.plus);
            var l = easing === 1 ? finalLow :
              conservativeError(b._view.x, b._model.x, low,
                finalLow, -1.0);
            var h = easing === 1 ? finalHigh :
              conservativeError(b._view.x, b._model.x,
                high, finalHigh, 1.0);
            drawErrorBar(chart, ctx, l, h, b._view.y, 4, errorBar.color);
          });
        });
      },
    };
  })();

  // Formats the ticks on the X-axis on the scatter plot
  var iterFormatter = function() {
    var denom = 0;
    return function(iters, index, values) {
      if (iters == 0) {
        return '';
      }
      if (index == values.length - 1) {
        return '';
      }
      var power;
      if (iters >= 1e9) {
        denom = 1e9;
        power = '⁹';
      } else if (iters >= 1e6) {
        denom = 1e6;
        power = '⁶';
      } else if (iters >= 1e3) {
        denom = 1e3;
        power = '³';
      } else {
        denom = 1;
      }
      if (denom > 1) {
        var value = (iters / denom).toFixed();
        return String(value) + '×10' + power;
      } else {
        return String(iters);
      }
    };
  };

  var colors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
  var errorColors = ["#cda220", "#8fb8d8", "#ab2b2b", "#2d872d", "#7420cd"];


  // Positions tooltips at cursor. Required for overview since the bars may
  // extend past the canvas width.
  Chart.Tooltip.positioners.cursor = function(_elems, position) {
    return position;
  }

  function axisType(logaxis) {
    return logaxis ? 'logarithmic' : 'linear';
  }

  function reportSort(a, b) {
    return a.reportNumber - b.reportNumber;
  }

  // adds groupNumber and group fields to reports;
  // returns list of list of reports, grouped by group
  function groupReports(reports) {

    function reportGroup(report) {
      var parts = report.groups.slice();
      parts.pop();
      return parts.join('/');
    }

    var groups = [];
    reports.forEach(function(report) {
      report.group = reportGroup(report);
      if (groups.length === 0) {
        groups.push([report]);
      } else {
        var prevGroup = groups[groups.length - 1];
        var prevGroupName = prevGroup[0].group;
        if (prevGroupName === report.group) {
          prevGroup.push(report);
        } else {
          groups.push([report]);
        }
      }
      report.groupNumber = groups.length - 1;
    });
    return groups;
  }

  // compares 2 arrays lexicographically
  function lex(aParts, bParts) {
    for(var i = 0; i < aParts.length && i < bParts.length; i++) {
      var x = aParts[i];
      var y = bParts[i];
      if (x < y) {
        return -1;
      }
      if (y < x) {
        return 1;
      }
    }
    return aParts.length - bParts.length;
  }
  function lexicalSort(a, b) {
    return lex(a.groups, b.groups);
  }

  function reverseLexicalSort(a, b) {
    return lex(a.groups.slice().reverse(), b.groups.slice().reverse());
  }

  function durationSort(a, b) {
    return a.reportAnalysis.anMean.estPoint - b.reportAnalysis.anMean.estPoint;
  }
  function reverseDurationSort(a,b) {
    return -durationSort(a,b);
  }

  function timeUnits(secs) {
    if (secs < 0)
      return timeUnits(-secs);
    else if (secs >= 1e9)
      return [1e-9, "Gs"];
    else if (secs >= 1e6)
      return [1e-6, "Ms"];
    else if (secs >= 1)
      return [1, "s"];
    else if (secs >= 1e-3)
      return [1e3, "ms"];
    else if (secs >= 1e-6)
      return [1e6, "\u03bcs"];
    else if (secs >= 1e-9)
      return [1e9, "ns"];
    else if (secs >= 1e-12)
      return [1e12, "ps"];
    return [1, "s"];
  }

  function formatUnit(raw, unit, precision) {
    var v = precision ? raw.toPrecision(precision) : Math.round(raw);
    var label = String(v) + ' ' + unit;
    return label;
  }

  function formatTime(value, precision) {
    var units = timeUnits(value);
    var scale = units[0];
    return formatUnit(value * scale, units[1], precision);
  }

  // pure function that produces the 'data' object of the overview chart
  function overviewData(state, reports) {
    var order = state.order;
    var sorter = order === 'report-index' ? reportSort
               : order === 'lex'          ? lexicalSort
               : order === 'colex'        ? reverseLexicalSort
               : order === 'duration'     ? durationSort
               : order === 'rev-duration' ? reverseDurationSort
               : reportSort;
    var sortedReports = reports.filter(function(report) {
      return !state.hidden[report.groupNumber];
    }).slice().sort(sorter);
    var data = sortedReports.map(function(report) {
      return report.reportAnalysis.anMean.estPoint;
    });
    var labels = sortedReports.map(function(report) {
      return report.groups.join(' / ');
    });
    var upperBound = function(report) {
      var est = report.reportAnalysis.anMean;
      return est.estPoint + est.estError.confIntUDX;
    };
    var errorBars = sortedReports.map(function(report) {
      var est = report.reportAnalysis.anMean;
      return {
        minus: est.estError.confIntLDX,
        plus: est.estError.confIntUDX,
        color: errorColors[report.groupNumber % errorColors.length]
      };
    });
    var top = sortedReports.map(upperBound).reduce(function(a, b) {
      return Math.max(a, b);
    }, 0);
    var scale = top;
    if(state.activeReport !== null) {
      reports.forEach(function(report) {
        if(report.reportNumber === state.activeReport) {
          scale = upperBound(report);
        }
      });
    }

    return {
      labels: labels,
      top: top,
      max: scale * 1.1,
      reports: sortedReports,
      datasets: [{
        borderWidth: 1,
        backgroundColor: sortedReports.map(function(report) {
          var active = report.reportNumber === state.activeReport;
          var alpha = active ? 'ff' : 'a0';
          var color = colors[report.groupNumber % colors.length] + alpha;
          if (active) {
            return Chart.helpers.getHoverColor(color);
          } else {
            return color;
          }
        }),
        barThickness: 16,
        barPercentage: 0.8,
        data: data,
        errorBars: errorBars,
        minBarLength: 2,
      }]
    };
  }

  function inside(box, point) {
    return (point.x >= box.left && point.x <= box.right && point.y >= box.top &&
      point.y <= box.bottom);
  }

  function overviewHover(event, elems) {
    var chart = this;
    var xAxis = chart.scales[chart.options.scales.xAxes[0].id];
    var yAxis = chart.scales[chart.options.scales.yAxes[0].id];
    var point = Chart.helpers.getRelativePosition(event, chart);
    var over =
      (inside(xAxis, point) || inside(yAxis, point) || elems.length > 0);
    if (over) {
      chart.canvas.style.cursor = "pointer";
    } else {
      chart.canvas.style.cursor = "default";
    }
  }

  // Re-renders the overview after clicking/sorting
  function renderOverview(state, reports, chart) {
    var data = overviewData(state, reports);
    var xaxis = chart.options.scales.xAxes[0];
    xaxis.ticks.max = data.max;
    chart.config.data.datasets[0].backgroundColor = data.datasets[0].backgroundColor;
    chart.config.data.datasets[0].errorBars = data.datasets[0].errorBars;
    chart.config.data.datasets[0].data = data.datasets[0].data;
    chart.options.scales.xAxes[0].type = axisType(state.logaxis);
    chart.options.legend.display = state.legend;
    chart.data.labels = data.labels;
    chart.update();
  }

  function overviewClick(state, reports) {
    return function(event, elems) {
      var chart = this;
      var xAxis = chart.scales[chart.options.scales.xAxes[0].id];
      var yAxis = chart.scales[chart.options.scales.yAxes[0].id];
      var point = Chart.helpers.getRelativePosition(event, chart);
      var sorted = overviewData(state, reports).reports;

      function activateBar(index) {
        // Trying to activate active bar disables instead
        if (sorted[index].reportNumber === state.activeReport) {
          state.activeReport = null;
        } else {
          state.activeReport = sorted[index].reportNumber;
        }
      }

      if (inside(xAxis, point)) {
        state.activeReport = null;
        state.logaxis = !state.logaxis;
        renderOverview(state, reports, chart);
      } else if (inside(yAxis, point)) {
        var index = yAxis.getValueForPixel(point.y);
        activateBar(index);
        renderOverview(state, reports, chart);
      } else if (elems.length > 0) {
        var elem = elems[0];
        var index = elem._index;
        activateBar(index);
        state.logaxis = false;
        renderOverview(state, reports, chart);
      } else if(inside(chart.chartArea, point)) {
        state.activeReport = null;
        renderOverview(state, reports, chart);
      }
    };
  }

  // listener for sort drop-down
  function overviewSort(state, reports, chart) {
    return function(event) {
      state.order = event.currentTarget.value;
      renderOverview(state, reports, chart);
    };
  }

  // Returns a formatter for the ticks on the X-axis of the overview
  function overviewTick(state) {
    return function(value, index, values) {
      var label = formatTime(value);
      if (state.logaxis) {
        const remain = Math.round(value /
          (Math.pow(10, Math.floor(Chart.helpers.log10(value)))));
        if (index === values.length - 1) {
          // Draw endpoint if we don't span a full order of magnitude
          if (values[index] / values[1] < 10) {
            return label;
          } else {
            return '';
          }
        }
        if (remain === 1) {
          return label;
        }
        return '';
      } else {
        // Don't show the right endpoint
        if (index === values.length - 1) {
          return '';
        }
        return label;
      }
    }
  }

  function mkOverview(reports) {
    var canvas = document.createElement('canvas');

    var state = {
      logaxis: false,
      activeReport: null,
      order: 'index',
      hidden: {},
      legend: false,
    };


    var data = overviewData(state, reports);
    var chart = new Chart(canvas.getContext('2d'), {
      type: 'horizontalBar',
      data: data,
      plugins: [errorBarPlugin],
      options: {
        onHover: overviewHover,
        onClick: overviewClick(state, reports),
        onResize: function(chart, size) {
          if (size.width < 800) {
            chart.options.scales.yAxes[0].ticks.mirror = true;
            chart.options.scales.yAxes[0].ticks.padding = -10;
            chart.options.scales.yAxes[0].ticks.fontColor = '#000';
          } else {
            chart.options.scales.yAxes[0].ticks.fontColor = '#666';
            chart.options.scales.yAxes[0].ticks.mirror = false;
            chart.options.scales.yAxes[0].ticks.padding = 0;
          }
        },
        elements: {
          rectangle: {
            borderWidth: 2,
          },
        },
        scales: {
          yAxes: [{
            ticks: {
              // make sure we draw the ticks above the error bars
              z: 2,
            }
          }],
          xAxes: [{
            display: true,
            type: axisType(state.logaxis),
            ticks: {
              autoSkip: false,
              min: 0,
              max: data.top * 1.1,
              minRotation: 0,
              maxRotation: 0,
              callback: overviewTick(state),
            }
          }]
        },
        responsive: true,
        maintainAspectRatio: false,
        legend: {
          display: state.legend,
          position: 'right',
          onLeave: function() {
            chart.canvas.style.cursor = 'default';
          },
          onHover: function() {
            chart.canvas.style.cursor = 'pointer';
          },
          onClick: function(_event, item) {
            // toggle hidden
            state.hidden[item.groupNumber] = !state.hidden[item.groupNumber];
            renderOverview(state, reports, chart);
          },
          labels: {
            boxWidth: 12,
            generateLabels: function() {
              var groups = [];
              var groupNames = [];
              reports.forEach(function(report) {
                var index = groups.indexOf(report.groupNumber);
                if (index === -1) {
                  groups.push(report.groupNumber);
                  var groupName = report.groups.slice(0,report.groups.length-1).join(' / ');
                  groupNames.push(groupName);
                }
              });
              return groups.map(function(groupNumber, index) {
                var color = colors[groupNumber % colors.length];
                return {
                  text: groupNames[index],
                  fillStyle: color,
                  hidden: state.hidden[groupNumber],
                  groupNumber: groupNumber,
                };
              });
            },
          },
        },
        tooltips: {
          position: 'cursor',
          callbacks: {
            label: function(item) {
              return formatTime(item.xLabel, 3);
            },
          },
        },
        title: {
          display: false,
          text: 'Chart.js Horizontal Bar Chart'
        }
      }
    });
    document.getElementById('sort-overview')
      .addEventListener('change', overviewSort(state, reports, chart));
    var toggle = document.getElementById('legend-toggle');
    toggle.addEventListener('mouseup', function () {
      state.legend = !state.legend;
      if(state.legend) {
        toggle.classList.add('right');
      } else {
        toggle.classList.remove('right');
      }
      renderOverview(state, reports, chart);
    })
    return canvas;
  }

  function mkKDE(report) {
    var canvas = document.createElement('canvas');
    var mean = report.reportAnalysis.anMean.estPoint;
    var units = timeUnits(mean);
    var scale = units[0];
    var reportKDE = report.reportKDEs[0];
    var data = reportKDE.kdeValues.map(function(time, index) {
      var pdf = reportKDE.kdePDF[index];
      return {
        x: time * scale,
        y: pdf
      };
    });
    var chart = new Chart(canvas.getContext('2d'), {
      type: 'line',
      data: {
        datasets: [{
          label: 'KDE',
          borderColor: colors[0],
          borderWidth: 2,
          backgroundColor: '#00000000',
          data: data,
          hoverBorderWidth: 1,
          pointHitRadius: 8,
        },
          {
            label: 'mean'
          }
        ],
      },
      plugins: [{
        afterDraw: function(chart) {
          var ctx = chart.ctx;
          var area = chart.chartArea;
          var axis = chart.scales[chart.options.scales.xAxes[0].id];
          var value = axis.getPixelForValue(mean * scale);
          ctx.save();
          ctx.strokeStyle = colors[1];
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(value, area.top);
          ctx.lineTo(value, area.bottom);
          ctx.stroke();
          ctx.restore();
        },
      }],
      options: {
        title: {
          display: true,
          text: report.groups.join(' / ') + ' — time densities',
        },
        elements: {
          point: {
            radius: 0,
            hitRadius: 0
          }
        },
        scales: {
          xAxes: [{
            display: true,
            type: 'linear',
            scaleLabel: {
              display: false,
              labelString: 'Time'
            },
            ticks: {
              min: reportKDE.kdeValues[0] * scale,
              max: reportKDE.kdeValues[reportKDE.kdeValues.length - 1] * scale,
              callback: function(value, index, values) {
                // Don't show endpoints
                if (index === 0 || index === values.length - 1) {
                  return '';
                }
                var str = String(value) + ' ' + units[1];
                return str;
              },
            }
          }],
          yAxes: [{
            display: true,
            type: 'linear',
            ticks: {
              min: 0,
              callback: function() {
                return '';
              },
            },
          }]
        },
        responsive: true,
        legend: {
          display: false,
          position: 'right',
        },
        tooltips: {
          mode: 'nearest',
          callbacks: {
            title: function() {
              return '';
            },
            label: function(
              item) {
              return formatUnit(item.xLabel, units[1], 3);
            },
          },
        },
        hover: {
          intersect: false
        },
      }
    });
    return canvas;
  }

  function mkScatter(report) {

    // collect the measured value for a given regression
    function getMeasured(key) {
      var ix = report.reportKeys.indexOf(key);
      return report.reportMeasured.map(function(x) {
        return x[ix];
      });
    }

    var canvas = document.createElement('canvas');
    var times = getMeasured("time");
    var iters = getMeasured("iters");
    var lastIter = iters[iters.length - 1];
    var olsTime = report.reportAnalysis.anRegress[0].regCoeffs.iters;
    var dataPoints = times.map(function(time, i) {
      return {
        x: iters[i],
        y: time
      }
    });
    var formatter = iterFormatter();
    var chart = new Chart(canvas.getContext('2d'), {
      type: 'scatter',
      data: {
        datasets: [{
          data: dataPoints,
          label: 'scatter',
          borderWidth: 2,
          pointHitRadius: 8,
          borderColor: colors[1],
          backgroundColor: '#fff',
        },
          {
            data: [
              {x: 0, y: 0 },
              { x: lastIter, y: olsTime.estPoint * lastIter }
            ],
            label: 'regression',
            type: 'line',
            backgroundColor: "#00000000",
            borderColor: colors[0],
            pointRadius: 0,
          },
          {
            data: [{
              x: 0,
              y: 0
            }, {
              x: lastIter,
              y: (olsTime.estPoint - olsTime.estError.confIntLDX) * lastIter,
            }],
            label: 'lower',
            type: 'line',
            fill: 1,
            borderWidth: 0,
            pointRadius: 0,
            borderColor: '#00000000',
            backgroundColor: colors[0] + '33',
          },
          {
            data: [{
              x: 0,
              y: 0
            }, {
              x: lastIter,
              y: (olsTime.estPoint + olsTime.estError.confIntUDX) * lastIter,
            }],
            label: 'upper',
            type: 'line',
            fill: 1,
            borderWidth: 0,
            borderColor: '#00000000',
            pointRadius: 0,
            backgroundColor: colors[0] + '33',
          },
        ],
      },
      options: {
        title: {
          display: true,
          text: report.groups.join(' / ') + ' — time per iteration',
        },
        scales: {
          yAxes: [{
            display: true,
            type: 'linear',
            scaleLabel: {
              display: false,
              labelString: 'Time'
            },
            ticks: {
              callback: function(value, index, values) {
                return formatTime(value);
              },
            }
          }],
          xAxes: [{
            display: true,
            type: 'linear',
            scaleLabel: {
              display: false,
              labelString: 'Iterations'
            },
            ticks: {
              callback: formatter,
              max: lastIter,
            }
          }],
        },
        legend: {
          display: false,
        },
        tooltips: {
          callbacks: {
            label: function(ttitem, ttdata) {
              var iters = ttitem.xLabel;
              var duration = ttitem.yLabel;
              return formatTime(duration, 3) + ' / ' +
                iters.toLocaleString() + ' iters';
            },
          },
        },
      }
    });
    return canvas;
  }

  // Create an HTML Element with attributes and child nodes
  function elem(tag, props, children) {
    var node = document.createElement(tag);
    if (children) {
      children.forEach(function(child) {
        if (typeof child === 'string') {
          var txt = document.createTextNode(child);
          node.appendChild(txt);
        } else {
          node.appendChild(child);
        }
      });
    }
    Object.assign(node, props);
    return node;
  }

  function bounds(analysis) {
    var mean = analysis.estPoint;
    return {
      low: mean - analysis.estError.confIntLDX,
      mean: mean,
      high: mean + analysis.estError.confIntUDX
    };
  }

  function confidence(level) {
    return String(1 - level) + ' confidence level';
  }

  function mkOutliers(report) {
    var outliers = report.reportAnalysis.anOutlierVar;
    return elem('div', {className: 'outliers'}, [
      elem('p', {}, [
        'Outlying measurements have ',
        outliers.ovDesc,
        ' (', String((outliers.ovFraction * 100).toPrecision(3)), '%)',
        ' effect on estimated standard deviation.'
      ])
    ]);
  }

  function mkTable(report) {
    var analysis = report.reportAnalysis;
    var timep4 = function(t) {
      return formatTime(t, 3)
    };
    var idformatter = function(t) {
      return t.toPrecision(3)
    };
    var rows = [
      Object.assign({
        label: 'OLS regression',
        formatter: timep4
      },
        bounds(analysis.anRegress[0].regCoeffs.iters)),
      Object.assign({
        label: 'R² goodness-of-fit',
        formatter: idformatter
      },
        bounds(analysis.anRegress[0].regRSquare)),
      Object.assign({
        label: 'Mean execution time',
        formatter: timep4
      },
        bounds(analysis.anMean)),
      Object.assign({
        label: 'Standard deviation',
        formatter: timep4
      },
        bounds(analysis.anStdDev)),
    ];
    return elem('table', {
      className: 'analysis'
    }, [
      elem('thead', {}, [
        elem('tr', {}, [
          elem('th'),
          elem('th', {
            className: 'low',
            title: confidence(analysis.anRegress[0].regCoeffs.iters.estError.confIntCL)
          }, ['lower bound']),
          elem('th', {}, ['estimate']),
          elem('th', {
            className: 'high',
            title: confidence(analysis.anRegress[0].regCoeffs.iters.estError.confIntCL)
          }, ['upper bound']),
        ])
      ]),
      elem('tbody', {}, rows.map(function(row) {
        return elem('tr', {}, [
          elem('td', {}, [row.label]),
          elem('td', {className: 'low'}, [row.formatter(row.low, 4)]),
          elem('td', {}, [row.formatter(row.mean)]),
          elem('td', {className: 'high'}, [row.formatter(row.high, 4)]),
        ]);
      }))
    ]);
  }
  document.addEventListener('DOMContentLoaded', function() {
    var rawJSON = document.getElementById('report-data').text;
    var reportData = JSON.parse(rawJSON)
      .map(function(report) {
        report.groups = report.reportName.split('/');
        return report;
      });
    groupReports(reportData);
    var overview = document.getElementById('overview-chart');
    var overviewLineHeight = 16 * 1.25;
    overview.style.height =
      String(overviewLineHeight * reportData.length + 36) + 'px';
    overview.appendChild(mkOverview(reportData.slice()));
    var reports = document.getElementById('reports');
    reportData.forEach(function(report, i) {
      var id = 'report_' + String(i);
      reports.appendChild(
        elem('div', {id: id, className: 'report-details'}, [
          elem('h1', {}, [elem('a', {href: '#' + id}, [report.groups.join(' / ')])]),
          elem('div', {className: 'kde'}, [mkKDE(report)]),
          elem('div', {className: 'scatter'}, [mkScatter(report)]),
          mkTable(report), mkOutliers(report)
        ]));
    });
  }, false);
})();
