/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/** ****************************************************************
 * window.talosDebug provides some statistical functions
 * (sum, average, median, stddev) and a tpRecordTime method which
 * reports some statistics about the data set, including detected
 * stability point of the data (detecting first few noisy elements).
 * If tpRecordTime doesn't exist globally (i.e. running outside of
 * talos, e.g. in a browser), then it points it to window.talosDebug.tpRecordTime
 * Can be controlled by few properties (disable display, hardcoded
 * stability point, etc)
 *
 * talos-debug.js: Bug 849558
 *****************************************************************/
window.talosDebug = {
  // Optional config properties
  disabled: false,
  ignore: -1, // Number of items to ignore at the begining of the set. -1 for auto-detect.
  displayData: false, // If true, will also display all the data points.
  fixed: 2, // default floating point digits for display.
  // End of config

  sum(values) {
    return values.reduce(function(a, b) {
      return a + b;
    });
  },

  average(values) {
    var d = window.talosDebug;
    return values.length ? d.sum(values) / values.length : 999999;
  },

  median(values) {
    var clone = values.slice(0);
    var sorted = clone.sort(function(a, b) {
      // eslint-disable-next-line no-nested-ternary
      return a > b ? 1 : a < b ? -1 : 0;
    });
    var len = values.length;
    if (!len) {
      return 999999;
    }
    if (len % 2) {
      // We have a middle number
      return sorted[(len - 1) / 2];
    }
    return (sorted[len / 2] + sorted[len / 2 - 1]) / 2; // Average value of the two middle items.
  },

  stddev(values, avg) {
    if (values.length <= 1) {
      return 0;
    }

    return Math.sqrt(
      values
        .map(function(v) {
          return Math.pow(v - avg, 2);
        })
        .reduce(function(a, b) {
          return a + b;
        }) /
        (values.length - 1)
    );
  },

  // Estimate the number of warmup iterations of this data set (in a completely unscrientific way).
  // returns -1 if could not find a stability point (supposedly meaning that the values
  // are still in the process of convergence).
  // Algorithm (not based on a scientific method which I know of):
  // Target: Find a locally noisy value after the values show stability
  //         (stddev of a moving window stops decreasing), while trying to make sure
  //         that it wasn't a glitch and the next 2 floating window stddev are, indeed, not
  //         still decreasing.
  //         Do the above for few different window widths (3-7), get the maximum result of those.
  //         Now we should have an index which is at least the 2nd value within the stable part.
  //         We get stddev for that index..end (baseStd), and then go backwards as long as stddev is
  //         decreasing or within ~1% of baseStd, and return the earliest index for which it is.
  detectWarmup(values) {
    var MIN_WIDTH = 3;
    var MAX_WIDTH = 7;
    var d = window.talosDebug;

    function windowStd(from, winSize) {
      var win = values.slice(from, from + winSize);
      return d.stddev(win, d.median(values));
    }

    var stableFrom = -1;
    var overallAverage = d.median(values);
    // var overallStd = d.stddev(values, overallAverage);
    for (var winWidth = MIN_WIDTH; winWidth < MAX_WIDTH + 1; winWidth++) {
      var prevStd = windowStd(0, winWidth);
      for (var i = 1; i < values.length - winWidth - 3; i++) {
        var w0 = windowStd(i + 0, winWidth);
        var w1 = windowStd(i + 1, winWidth);
        var w2 = windowStd(i + 2, winWidth);
        // var currWindow = values.slice(i, i + winWidth);
        if (w0 >= prevStd && !(w1 < w0 && w2 < w1)) {
          if (i > stableFrom) {
            stableFrom = i;
          }
          break;
        }
        prevStd = w0;
      }
    }

    function withinPercentage(base, value, percentage) {
      return Math.abs(base - value) < (base * percentage) / 100;
    }
    // Now go backwards as long as stddev decreases or doesn't increase in more than 1%.
    var baseStd = d.stddev(values.slice(stableFrom), overallAverage);
    var len = values.length;
    while (true) {
      var current = d.stddev(values.slice(stableFrom - 1), overallAverage);
      // 100/len : the more items we have, the more sensitively we compare:
      //   for 100 items, we're allowing 2% over baseStd.
      if (
        stableFrom > 0 &&
        (current < baseStd ||
          withinPercentage(baseStd, current, 200 / (len ? len : 100)))
      ) {
        stableFrom--;
      } else {
        break;
      }
    }

    return stableFrom;
  },

  statsDisplay(collection) {
    var d = window.talosDebug;
    var std = d.stddev(collection, d.average(collection));
    var avg = d.average(collection);
    var med = d.median(collection);
    return (
      "Count: " +
      collection.length +
      "\nAverage: " +
      avg.toFixed(d.fixed) +
      "\nMedian: " +
      med.toFixed(d.fixed) +
      "\nStdDev: " +
      std.toFixed(d.fixed) +
      " (" +
      ((100 * std) / (avg ? avg : 1)).toFixed(d.fixed) +
      "% of average)"
    );
  },

  tpRecordTime(dataCSV) {
    var d = window.talosDebug;
    if (d.disabled) {
      return;
    }

    var collection = ("" + dataCSV).split(",").map(function(item) {
      return parseFloat(item);
    });
    var res = d.statsDisplay(collection);

    var warmup = d.ignore >= 0 ? d.ignore : d.detectWarmup(collection);
    if (warmup >= 0) {
      res +=
        "\n\nWarmup " +
        (d.ignore >= 0 ? "requested: " : "auto-detected: ") +
        warmup;
      var warmedUp = collection.slice(warmup);
      if (warmup) {
        res +=
          "\nAfter ignoring first " +
          (warmup > 1 ? warmup + " items" : "item") +
          ":\n";
        res += d.statsDisplay(warmedUp);
      }
    } else {
      res += "\n\nWarmup auto-detection: Failed.";
    }

    if (d.displayData) {
      var disp = collection.map(function(item) {
        return item.toFixed(d.fixed);
      });
      if (warmup >= 0) {
        disp.splice(warmup, 0, "[warmed-up:]");
      }
      res += "\n\nRecorded:\n" + disp.join(", ");

      res += "\n\nStddev from item NN to last:\n";
      disp = collection.map(function(value, index) {
        return d
          .stddev(collection.slice(index), d.average(collection.slice(index)))
          .toFixed(d.fixed);
      });
      if (warmup >= 0) {
        disp.splice(warmup, 0, "[warmed-up:]");
      }
      res += disp.join(", ");
    } else {
      res += "\n\n[set window.talosDebug.displayData=true for data]";
    }

    alert(res);
  },
};

// Enable testing outside of talos by providing an alternative report function.
if (typeof tpRecordTime === "undefined") {
  tpRecordTime = window.talosDebug.tpRecordTime;
}
