import * as Collections from '../utils/collections';
import Util from '../util';
import {OneOffTimer as Timer} from '../utils/timers';
import Strategy from "./strategy";
import StrategyOptions from "./strategy_options";

/** Loops through strategies with optional timeouts.
 *
 * Options:
 * - loop - whether it should loop through the substrategy list
 * - timeout - initial timeout for a single substrategy
 * - timeoutLimit - maximum timeout
 *
 * @param {Strategy[]} strategies
 * @param {Object} options
 */
export default class SequentialStrategy implements Strategy {

  strategies: Strategy[];
  loop: boolean;
  failFast: boolean;
  timeout: number;
  timeoutLimit: number;

  constructor(strategies : Strategy[], options : StrategyOptions) {
    this.strategies = strategies;
    this.loop = Boolean(options.loop);
    this.failFast = Boolean(options.failFast);
    this.timeout = options.timeout;
    this.timeoutLimit = options.timeoutLimit;
  }

  isSupported() : boolean {
    return Collections.any(this.strategies, Util.method("isSupported"));
  }

  connect(minPriority : number, callback : Function) {
    var strategies = this.strategies;
    var current = 0;
    var timeout = this.timeout;
    var runner = null;

    var tryNextStrategy = (error, handshake)=> {
      if (handshake) {
        callback(null, handshake);
      } else {
        current = current + 1;
        if (this.loop) {
          current = current % strategies.length;
        }

        if (current < strategies.length) {
          if (timeout) {
            timeout = timeout * 2;
            if (this.timeoutLimit) {
              timeout = Math.min(timeout, this.timeoutLimit);
            }
          }
          runner = this.tryStrategy(
            strategies[current],
            minPriority,
            { timeout, failFast: this.failFast },
            tryNextStrategy
          );
        } else {
          callback(true);
        }
      }
    };

    runner = this.tryStrategy(
      strategies[current],
      minPriority,
      { timeout: timeout, failFast: this.failFast },
      tryNextStrategy
    );

    return {
      abort: function() {
        runner.abort();
      },
      forceMinPriority: function(p) {
        minPriority = p;
        if (runner) {
          runner.forceMinPriority(p);
        }
      }
    };
  }

  private tryStrategy(strategy : Strategy, minPriority : number, options : StrategyOptions, callback : Function) {
    var timer = null;
    var runner = null;

    if (options.timeout > 0) {
      timer = new Timer(options.timeout, function() {
        runner.abort();
        callback(true);
      });
    }

    runner = strategy.connect(minPriority, function(error, handshake) {
      if (error && timer && timer.isRunning() && !options.failFast) {
        // advance to the next strategy after the timeout
        return;
      }
      if (timer) {
        timer.ensureAborted();
      }
      callback(error, handshake);
    });

    return {
      abort: function() {
        if (timer) {
          timer.ensureAborted();
        }
        runner.abort();
      },
      forceMinPriority: function(p) {
        runner.forceMinPriority(p);
      }
    };
  }
}
