import { RetryOperation } from './operation';

interface RetryOptions {
  retries: number;
  factor: number;
  forever: boolean;
  minTimeout: number;
  maxTimeout: number;
  randomize: boolean;
  maxRetryTime: number;
  unref: any;
}
export function operation(options: Partial<RetryOptions>) {
  const timeouts = exports.timeouts(options);
  return new RetryOperation(timeouts, {
    forever: options && (options.forever || options.retries === Infinity),
    unref: options && options.unref,
    maxRetryTime: options && options.maxRetryTime
  });
};

export function timeouts(options: RetryOptions) {
  if (options instanceof Array) {
    return Object.assign([], options);
  }

  const opts: Pick<
    RetryOptions,
    'retries' | 'factor' | 'minTimeout' | 'maxTimeout' | 'randomize'
  > & Partial<Pick<RetryOptions, 'unref'>> = {
    retries: 10,
    factor: 2,
    minTimeout: 1 * 1000,
    maxTimeout: Infinity,
    randomize: false
  };
  for (const key in options) {
    opts[key] = options[key];
  }

  if (opts.minTimeout > opts.maxTimeout) {
    throw new Error('minTimeout is greater than maxTimeout');
  }

  const timeouts: number[] = [];
  for (let i: number = 0; i < opts.retries; i++) {
    timeouts.push(createTimeout(i, opts));
  }

  if (options && options.forever && !timeouts.length) {
    timeouts.push(createTimeout(opts.retries, opts));
  }

  // sort the array numerically ascending
  timeouts.sort(function (a, b) {
    return a - b;
  });

  return timeouts;
}

export function createTimeout(attempt: number, opts: Pick<
  RetryOptions,
  'factor' | 'minTimeout' | 'maxTimeout' | 'randomize'
>) {
  const random = (opts.randomize)
    ? (Math.random() + 1)
    : 1;

  let timeout = Math.round(random * Math.max(opts.minTimeout, 1) * Math.pow(opts.factor, attempt));
  timeout = Math.min(timeout, opts.maxTimeout);

  return timeout;
};

export function wrap(obj: any, options: any, methods: any) {
  if (options instanceof Array) {
    methods = options;
    options = null;
  }

  if (!methods) {
    methods = [];
    for (const key in obj) {
      if (typeof obj[key] === 'function') {
        methods.push(key);
      }
    }
  }

  for (let i = 0; i < methods.length; i++) {
    const method = methods[i];
    const original = obj[method];

    obj[method] = function retryWrapper(original: any) {
      const op = exports.operation(options);
      const args = Array.prototype.slice.call(arguments, 1);
      const callback = args.pop();

      args.push(function (err: any) {
        if (op.retry(err)) {
          return;
        }
        if (err) {
          arguments[0] = op.mainError();
        }
        callback.apply(obj, arguments);
      });

      op.attempt(function () {
        original.apply(obj, args);
      });
    }.bind(obj, original);
    obj[method].options = options;
  }
}