/**
 * Copyright (c) 2018 The xterm.js authors. All rights reserved.
 * @license MIT
 */

const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second

import { IRenderDebouncer } from 'browser/Types';

/**
 * Debounces calls to update screen readers to update at most once configurable interval of time.
 */
export class TimeBasedDebouncer implements IRenderDebouncer {
  private _rowStart: number | undefined;
  private _rowEnd: number | undefined;
  private _rowCount: number | undefined;

  // The last moment that the Terminal was refreshed at
  private _lastRefreshMs = 0;
  // Whether a trailing refresh should be triggered due to a refresh request that was throttled
  private _additionalRefreshRequested = false;

  private _refreshTimeoutID: number | undefined;

  constructor(
    private _renderCallback: (start: number, end: number) => void,
    private readonly _debounceThresholdMS = RENDER_DEBOUNCE_THRESHOLD_MS
  ) {
  }

  public dispose(): void {
    if (this._refreshTimeoutID) {
      clearTimeout(this._refreshTimeoutID);
    }
  }

  public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
    this._rowCount = rowCount;
    // Get the min/max row start/end for the arg values
    rowStart = rowStart !== undefined ? rowStart : 0;
    rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
    // Set the properties to the updated values
    this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
    this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;

    // Only refresh if the time since last refresh is above a threshold, otherwise wait for
    // enough time to pass before refreshing again.
    const refreshRequestTime: number = Date.now();
    if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) {
      // Enough time has lapsed since the last refresh; refresh immediately
      this._lastRefreshMs = refreshRequestTime;
      this._innerRefresh();
    } else if (!this._additionalRefreshRequested) {
      // This is the first additional request throttled; set up trailing refresh
      const elapsed = refreshRequestTime - this._lastRefreshMs;
      const waitPeriodBeforeTrailingRefresh = this._debounceThresholdMS - elapsed;
      this._additionalRefreshRequested = true;

      this._refreshTimeoutID = window.setTimeout(() => {
        this._lastRefreshMs = Date.now();
        this._innerRefresh();
        this._additionalRefreshRequested = false;
        this._refreshTimeoutID = undefined; // No longer need to clear the timeout
      }, waitPeriodBeforeTrailingRefresh);
    }
  }

  private _innerRefresh(): void {
    // Make sure values are set
    if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
      return;
    }

    // Clamp values
    const start = Math.max(this._rowStart, 0);
    const end = Math.min(this._rowEnd, this._rowCount - 1);

    // Reset debouncer (this happens before render callback as the render could trigger it again)
    this._rowStart = undefined;
    this._rowEnd = undefined;

    // Run render callback
    this._renderCallback(start, end);
  }
}

