type Point = [number, number]; // [x, y] or here [time, value]

export class TimeSeries {
  private points: Point[] = [];
  private startTimeSec: number;
  private maxPoints: number;

  constructor(opts?: {maxPoints?: number}) {
    this.maxPoints = opts?.maxPoints ?? 1000;
    this.startTimeSec = Date.now() / 1000;
  }

  /** Add TimeSeries entry for value at current time */
  addPoint(value: number, timeSec = Math.floor(Date.now() / 1000)): void {
    // Substract initial time so x values are not big and cause rounding errors
    const time = timeSec - this.startTimeSec;
    this.points.push([time, value]);

    // Limit length by removing old entries
    while (this.points.length > this.maxPoints) {
      this.points.shift();
    }
  }

  /** Compute the slope of all registered points assuming linear regression */
  computeLinearSpeed(): number {
    return linearRegression(this.points).m;
  }

  /**
   * Compute x point at which y = 0.
   * From eq `y = b + m*x` then solve for `0 = b + m*x`
   */
  computeY0Point(): number {
    const {m, b} = linearRegression(this.points);
    // The X cordinate system has been shifted left by startTimeSec, so return the
    // projection in original coordinated system
    return -b / m + this.startTimeSec;
  }

  /** Remove all entries */
  clear(): void {
    this.points = [];
  }

  numPoints(): number {
    return this.points.length;
  }
}

/**
 * From https://github.com/simple-statistics/simple-statistics/blob/d0d177baf74976a2421638bce98ab028c5afb537/src/linear_regression.js
 *
 * [Simple linear regression](http://en.wikipedia.org/wiki/Simple_linear_regression)
 * is a simple way to find a fitted line between a set of coordinates.
 * This algorithm finds the slope and y-intercept of a regression line
 * using the least sum of squares.
 *
 * @param data an array of two-element of arrays,
 * like `[[0, 1], [2, 3]]`
 * @returns object containing slope and intersect of regression line
 * @example
 * linearRegression([[0, 0], [1, 1]]); // => { m: 1, b: 0 }
 */
function linearRegression(data: Point[]): {m: number; b: number} {
  let m: number, b: number;

  // Store data length in a local variable to reduce
  // repeated object property lookups
  const dataLength = data.length;

  //if there's only one point, arbitrarily choose a slope of 0
  //and a y-intercept of whatever the y of the initial point is
  if (dataLength === 1) {
    m = 0;
    b = data[0][1];
  } else {
    // Initialize our sums and scope the `m` and `b`
    // variables that define the line.
    let sumX = 0,
      sumY = 0,
      sumXX = 0,
      sumXY = 0;

    // Use local variables to grab point values
    // with minimal object property lookups
    let point: Point, x: number, y: number;

    // Gather the sum of all x values, the sum of all
    // y values, and the sum of x^2 and (x*y) for each
    // value.
    //
    // In math notation, these would be SS_x, SS_y, SS_xx, and SS_xy
    for (let i = 0; i < dataLength; i++) {
      point = data[i];
      x = point[0];
      y = point[1];

      sumX += x;
      sumY += y;

      sumXX += x * x;
      sumXY += x * y;
    }

    // `m` is the slope of the regression line
    m = (dataLength * sumXY - sumX * sumY) / (dataLength * sumXX - sumX * sumX);

    // `b` is the y-intercept of the line.
    b = sumY / dataLength - (m * sumX) / dataLength;
  }

  // Return both values as an object.
  return {
    m: m,
    b: b,
  };
}
