// Minimum time denominator in ms to prevent division by zero for instantaneous
// downloads (e.g., from cache), maintaining a high bandwidth estimate so the
// ABR doesn't downshift.
const MIN_TIME_DIFF_MS = 1;

export class BandwidthCalculator {
  private loadingsCount = 0;
  private readonly bytes: number[] = [];
  private readonly loadingOnlyTimestamps: number[] = [];
  private readonly timestamps: number[] = [];
  private noLoadingsTime = 0;
  private loadingsStoppedAt = 0;

  constructor(private readonly clearThresholdMs = 20000) {}

  addBytes(bytesLength: number, now = performance.now()) {
    this.bytes.push(bytesLength);
    this.loadingOnlyTimestamps.push(now - this.noLoadingsTime);
    this.timestamps.push(now);
  }

  startLoading(now = performance.now()) {
    this.clearStale();
    if (this.loadingsCount === 0 && this.loadingsStoppedAt !== 0) {
      this.noLoadingsTime += now - this.loadingsStoppedAt;
    }
    this.loadingsCount++;
  }

  stopLoading(now = performance.now()) {
    if (this.loadingsCount > 0) {
      this.loadingsCount--;
      if (this.loadingsCount === 0) this.loadingsStoppedAt = now;
    }
  }

  getBandwidthLoadingOnly(
    seconds: number,
    ignoreThresholdTimestamp = Number.NEGATIVE_INFINITY,
  ) {
    if (!this.loadingOnlyTimestamps.length) return 0;
    const milliseconds = seconds * 1000;
    const lastItemTimestamp =
      this.loadingOnlyTimestamps[this.loadingOnlyTimestamps.length - 1];
    let lastCountedTimestamp = lastItemTimestamp;
    const threshold = lastItemTimestamp - milliseconds;
    let totalBytes = 0;

    for (let i = this.bytes.length - 1; i >= 0; i--) {
      const timestamp = this.loadingOnlyTimestamps[i];
      if (
        timestamp < threshold ||
        this.timestamps[i] < ignoreThresholdTimestamp
      ) {
        break;
      }
      lastCountedTimestamp = timestamp;
      totalBytes += this.bytes[i];
    }

    const timeDiff = Math.max(
      lastItemTimestamp - lastCountedTimestamp,
      MIN_TIME_DIFF_MS,
    );
    return (totalBytes * 8000) / timeDiff;
  }

  getBandwidth(
    seconds: number,
    ignoreThresholdTimestamp = Number.NEGATIVE_INFINITY,
    now = performance.now(),
  ) {
    if (!this.timestamps.length) return 0;
    const milliseconds = seconds * 1000;
    const threshold = now - milliseconds;
    let lastCountedTimestamp = now;
    let totalBytes = 0;

    for (let i = this.bytes.length - 1; i >= 0; i--) {
      const timestamp = this.timestamps[i];
      if (timestamp < threshold || timestamp < ignoreThresholdTimestamp) break;
      lastCountedTimestamp = timestamp;
      totalBytes += this.bytes[i];
    }

    const timeDiff = Math.max(now - lastCountedTimestamp, MIN_TIME_DIFF_MS);
    return (totalBytes * 8000) / timeDiff;
  }

  clearStale() {
    if (!this.loadingOnlyTimestamps.length) return;
    const threshold =
      this.loadingOnlyTimestamps[this.loadingOnlyTimestamps.length - 1] -
      this.clearThresholdMs;

    let samplesToRemove = 0;
    for (const timestamp of this.loadingOnlyTimestamps) {
      if (timestamp > threshold) break;
      samplesToRemove++;
    }

    this.bytes.splice(0, samplesToRemove);
    this.loadingOnlyTimestamps.splice(0, samplesToRemove);
    this.timestamps.splice(0, samplesToRemove);
  }

  clear() {
    this.bytes.length = 0;
    this.loadingOnlyTimestamps.length = 0;
    this.timestamps.length = 0;
    this.loadingsCount = 0;
    this.noLoadingsTime = 0;
    this.loadingsStoppedAt = 0;
  }
}
