export class RetryOperation {
  _originalTimeouts: any;
  _timeouts: any;
  _options: any;
  _maxRetryTime: any;
  _fn: any | null = null;
  _errors: any[] = [];
  _attempts: number = 1;
  _operationTimeout: any | null = null;
  _operationTimeoutCb: any | null = null;
  _timeout: any | null = null;
  _operationStart: any | null = null;
  _timer: any | null = null;
  _cachedTimeouts: any;

  constructor(timeouts: any, options: any) {
    // Compatibility for the old (timeouts, retryForever) signature
    if (typeof options === 'boolean') {
      options = { forever: options };
    }

    this._originalTimeouts = JSON.parse(JSON.stringify(timeouts));
    this._timeouts = timeouts;
    this._options = options || {};
    this._maxRetryTime = options && options.maxRetryTime || Infinity;

    if (this._options.forever) {
      this._cachedTimeouts = this._timeouts.slice(0);
    }
  }

  reset() {
    this._attempts = 1;
    this._timeouts = this._originalTimeouts.slice(0);
  }

  stop() {
    if (this._timeout) {
      clearTimeout(this._timeout);
    }
    if (this._timer) {
      clearTimeout(this._timer);
    }

    this._timeouts = [];
    this._cachedTimeouts = null;
  }

  retry(err: any) {
    if (this._timeout) {
      clearTimeout(this._timeout);
    }

    if (!err) {
      return false;
    }
    const currentTime = new Date().getTime();
    if (err && currentTime - this._operationStart >= this._maxRetryTime) {
      this._errors.push(err);
      this._errors.unshift(new Error('RetryOperation timeout occurred'));
      return false;
    }

    this._errors.push(err);

    let timeout = this._timeouts.shift();
    if (timeout === undefined) {
      if (this._cachedTimeouts) {
        // retry forever, only keep last error
        this._errors.splice(0, this._errors.length - 1);
        timeout = this._cachedTimeouts.slice(-1);
      } else {
        return false;
      }
    }

    const self = this;
    this._timer = setTimeout(function () {
      self._attempts++;

      if (self._operationTimeoutCb) {
        self._timeout = setTimeout(function () {
          self._operationTimeoutCb(self._attempts);
        }, self._operationTimeout);

        if (self._options.unref) {
          self._timeout.unref();
        }
      }

      self._fn(self._attempts);
    }, timeout);

    if (this._options.unref) {
      this._timer.unref();
    }

    return true;
  }

  attempt(fn: any, timeoutOps?: any) {
    this._fn = fn;

    if (timeoutOps) {
      if (timeoutOps.timeout) {
        this._operationTimeout = timeoutOps.timeout;
      }
      if (timeoutOps.cb) {
        this._operationTimeoutCb = timeoutOps.cb;
      }
    }

    const self = this;
    if (this._operationTimeoutCb) {
      this._timeout = setTimeout(function () {
        self._operationTimeoutCb();
      }, self._operationTimeout);
    }

    this._operationStart = new Date().getTime();

    this._fn(this._attempts);
  };

  errors() {
    return this._errors;
  }

  attempts() {
    return this._attempts;
  }

  mainError() {
    if (this._errors.length === 0) {
      return null;
    }

    const counts: any = {};
    let mainError: any = null;
    let mainErrorCount = 0;

    for (let i = 0; i < this._errors.length; i++) {
      const error = this._errors[i];
      const message = error.message;
      const count = (counts[message] || 0) + 1;

      counts[message] = count;

      if (count >= mainErrorCount) {
        mainError = error;
        mainErrorCount = count;
      }
    }

    return mainError;
  }
}