/* 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/. */

const CHECK_RTC_STATS_COLLECTION = [
  "inboundRtpStreamStats",
  "outboundRtpStreamStats",
  "remoteInboundRtpStreamStats",
  "remoteOutboundRtpStreamStats",
];

const DEFAULT_PROPS = {
  avgPoints: 10,
  histSecs: 15,
  toRate: false,
  noAvg: false,
  fixedPointDecimals: 2,
  toHuman: false,
};

const REMOTE_RTP_PROPS = "avgPoints=2;histSecs=90";
const GRAPH_KEYS = [
  "inbound-rtp.framesPerSecond",
  "inbound-rtp.packetsReceived;toRate",
  "inbound-rtp.packetsLost;toRate",
  "inbound-rtp.jitter;fixedPointDecimals=4",
  `remote-inbound-rtp.roundTripTime;${REMOTE_RTP_PROPS}`,
  `remote-inbound-rtp.packetsReceived;toRate;${REMOTE_RTP_PROPS}`,
  "outbound-rtp.packetsSent;toRate",
  "outbound-rtp.framesSent;toRate",
  "outbound-rtp.frameHeight;noAvg",
  "outbound-rtp.frameWidth;noAvg",
  "outbound-rtp.nackCount",
  "outbound-rtp.pliCount",
  "outbound-rtp.firCount",
  `remote-outbound-rtp.bytesSent;toHuman;toRate;${REMOTE_RTP_PROPS}`,
  `remote-outbound-rtp.packetsSent;toRate;${REMOTE_RTP_PROPS}`,
]
  .map(k => k.split(".", 2))
  .reduce((mapOfArr, [k, rest]) => {
    mapOfArr[k] ??= [];
    const [subKey, ...conf] = rest.split(";");
    let config = conf.reduce((c, v) => {
      let [configName, ...configVal] = v.split("=", 2);
      c[configName] = !configVal.length ? true : configVal[0];
      return c;
    }, {});
    mapOfArr[k].push({ subKey, config });
    return mapOfArr;
  }, {});

// Sliding window iterator of size n (where: n >= 1) over the array.
// Only returns full windows.
// Returns [] if n > array.length.
// eachN(['a','b','c','d','e'], 3) will yield the following values:
//   ['a','b','c'], ['b','c','d'], and ['c','d','e']
const eachN = (array, n) => {
  return {
    // Index state
    index: 0,
    // Iteration function
    next() {
      let slice = array.slice(this.index, this.index + n);
      this.index++;
      // Done is true _AFTER_ the last value has returned.
      // When done is true, value is ignored.
      return { value: slice, done: slice.length < n };
    },
    [Symbol.iterator]() {
      return this;
    },
  };
};

const msToSec = ms => 1000 * ms;

//
// A subset of the graph data
//
class GraphDataSet {
  constructor(dataPoints) {
    this.dataPoints = dataPoints;
  }

  // The latest
  latest = () => (this.dataPoints ? this.dataPoints.slice(-1)[0] : undefined);

  earliest = () => (this.dataPoints ? this.dataPoints[0] : undefined);

  // The returns the earliest time to graph
  startTime = () => (this.earliest() || { time: 0 }).time;

  // Returns the latest time to graph
  stopTime = () => (this.latest() || { time: 0 }).time;

  // Elapsed time within the display window
  elapsed = () =>
    this.dataPoints ? this.latest().time - this.earliest().time : 0;

  // Return a new data set that has been filtered
  filter = fn => new GraphDataSet([...this.dataPoints].filter(fn));

  // The range of values in the set or or undefined if the set is empty
  dataRange = () =>
    this.dataPoints.reduce(
      ({ min, max }, { value }) => ({
        min: Math.min(min, value),
        max: Math.max(max, value),
      }),
      this.dataPoints.length
        ? { min: this.dataPoints[0].value, max: this.dataPoints[0].value }
        : undefined
    );

  // Get the rates between points. By definition the rates will have
  // one fewer data points.
  toRateDataSet = () =>
    new GraphDataSet(
      [...eachN(this.dataPoints, 2)].map(([a, b]) => ({
        // Time mid point
        time: (b.time + a.time) / 2,
        value: msToSec(b.value - a.value) / (b.time - a.time),
      }))
    );

  average = samples =>
    samples.reduce(
      ({ time, value }, { time: t, value: v }) => ({
        time: time + t / samples.length,
        value: value + v / samples.length,
      }),
      { time: 0, value: 0 }
    );

  toRollingAverageDataSet = sampleSize =>
    new GraphDataSet([...eachN(this.dataPoints, sampleSize)].map(this.average));
}

class GraphData {
  constructor(id, key, subKey, config) {
    this.id = id;
    this.key = key;
    this.subKey = subKey;
    this.data = [];
    this.config = Object.assign({}, DEFAULT_PROPS, config);
  }

  setValueForTime(dataPoint) {
    this.data = this.data.filter(({ time: t }) => t != dataPoint.time);
    this.data.push(dataPoint);
  }

  getValuesSince = time => this.data.filter(dp => dp.time > time);

  getDataSetSince = time =>
    new GraphDataSet(this.data.filter(dp => dp.time > time));

  getConfig = () => this.config;

  // Cull old data, but keep twice the window size for average computation
  cullData = timeNow =>
    (this.data = this.data.filter(
      ({ time }) => time + msToSec(this.config.histSecs * 2) > timeNow
    ));
}

class GraphDb {
  constructor(report) {
    this.graphDatas = new Map();
    this.insertReportData(report);
  }

  mkStoreKey = ({ id, key, subKey }) => `${key}.${id}.${subKey}`;

  insertDataPoint(id, key, subKey, config, time, value) {
    let storeKey = this.mkStoreKey({ id, key, subKey });
    let data =
      this.graphDatas.get(storeKey) || new GraphData(id, key, subKey, config);
    data.setValueForTime({ time, value });
    data.cullData(time);
    this.graphDatas.set(storeKey, data);
  }

  insertReportData(report) {
    if (report.timestamp == this.lastReportTimestamp) {
      return;
    }
    this.lastReportTimestamp = report.timestamp;
    CHECK_RTC_STATS_COLLECTION.forEach(listName => {
      (report[listName] || []).forEach(stats => {
        (GRAPH_KEYS[stats.type] || []).forEach(({ subKey, config }) => {
          if (stats[subKey] !== undefined) {
            this.insertDataPoint(
              stats.id,
              stats.type,
              subKey,
              config,
              stats.timestamp,
              stats[subKey]
            );
          }
        });
      });
    });
  }

  getGraphDataById = id =>
    [...this.graphDatas.values()].filter(gd => gd.id == id);
}

export { GraphDb };
