import { sleep, randomToken } from './util.js';
import unload from 'unload';

var LeaderElection = function LeaderElection(channel, options) {
  this._channel = channel;
  this._options = options;
  this.isLeader = false;
  this.isDead = false;
  this.token = randomToken();
  this._isApl = false; // _isApplying

  this._reApply = false; // things to clean up

  this._unl = []; // _unloads

  this._lstns = []; // _listeners

  this._invs = []; // _intervals

  this._dpL = function () {}; // onduplicate listener


  this._dpLC = false; // true when onduplicate called
};

LeaderElection.prototype = {
  applyOnce: function applyOnce() {
    var _this = this;

    if (this.isLeader) return Promise.resolve(false);
    if (this.isDead) return Promise.resolve(false); // do nothing if already running

    if (this._isApl) {
      this._reApply = true;
      return Promise.resolve(false);
    }

    this._isApl = true;
    var stopCriteria = false;
    var recieved = [];

    var handleMessage = function handleMessage(msg) {
      if (msg.context === 'leader' && msg.token != _this.token) {
        recieved.push(msg);

        if (msg.action === 'apply') {
          // other is applying
          if (msg.token > _this.token) {
            // other has higher token, stop applying
            stopCriteria = true;
          }
        }

        if (msg.action === 'tell') {
          // other is already leader
          stopCriteria = true;
        }
      }
    };

    this._channel.addEventListener('internal', handleMessage);

    var ret = _sendMessage(this, 'apply') // send out that this one is applying
    .then(function () {
      return sleep(_this._options.responseTime);
    }) // let others time to respond
    .then(function () {
      if (stopCriteria) return Promise.reject(new Error());else return _sendMessage(_this, 'apply');
    }).then(function () {
      return sleep(_this._options.responseTime);
    }) // let others time to respond
    .then(function () {
      if (stopCriteria) return Promise.reject(new Error());else return _sendMessage(_this);
    }).then(function () {
      return beLeader(_this);
    }) // no one disagreed -> this one is now leader
    .then(function () {
      return true;
    })["catch"](function () {
      return false;
    }) // apply not successfull
    .then(function (success) {
      _this._channel.removeEventListener('internal', handleMessage);

      _this._isApl = false;

      if (!success && _this._reApply) {
        _this._reApply = false;
        return _this.applyOnce();
      } else return success;
    });

    return ret;
  },
  awaitLeadership: function awaitLeadership() {
    if (
    /* _awaitLeadershipPromise */
    !this._aLP) {
      this._aLP = _awaitLeadershipOnce(this);
    }

    return this._aLP;
  },

  set onduplicate(fn) {
    this._dpL = fn;
  },

  die: function die() {
    var _this2 = this;

    if (this.isDead) return;
    this.isDead = true;

    this._lstns.forEach(function (listener) {
      return _this2._channel.removeEventListener('internal', listener);
    });

    this._invs.forEach(function (interval) {
      return clearInterval(interval);
    });

    this._unl.forEach(function (uFn) {
      uFn.remove();
    });

    return _sendMessage(this, 'death');
  }
};

function _awaitLeadershipOnce(leaderElector) {
  if (leaderElector.isLeader) return Promise.resolve();
  return new Promise(function (res) {
    var resolved = false;

    function finish() {
      if (resolved) {
        return;
      }

      resolved = true;
      clearInterval(interval);

      leaderElector._channel.removeEventListener('internal', whenDeathListener);

      res(true);
    } // try once now


    leaderElector.applyOnce().then(function () {
      if (leaderElector.isLeader) {
        finish();
      }
    }); // try on fallbackInterval

    var interval = setInterval(function () {
      leaderElector.applyOnce().then(function () {
        if (leaderElector.isLeader) {
          finish();
        }
      });
    }, leaderElector._options.fallbackInterval);

    leaderElector._invs.push(interval); // try when other leader dies


    var whenDeathListener = function whenDeathListener(msg) {
      if (msg.context === 'leader' && msg.action === 'death') {
        leaderElector.applyOnce().then(function () {
          if (leaderElector.isLeader) finish();
        });
      }
    };

    leaderElector._channel.addEventListener('internal', whenDeathListener);

    leaderElector._lstns.push(whenDeathListener);
  });
}
/**
 * sends and internal message over the broadcast-channel
 */


function _sendMessage(leaderElector, action) {
  var msgJson = {
    context: 'leader',
    action: action,
    token: leaderElector.token
  };
  return leaderElector._channel.postInternal(msgJson);
}

export function beLeader(leaderElector) {
  leaderElector.isLeader = true;
  var unloadFn = unload.add(function () {
    return leaderElector.die();
  });

  leaderElector._unl.push(unloadFn);

  var isLeaderListener = function isLeaderListener(msg) {
    if (msg.context === 'leader' && msg.action === 'apply') {
      _sendMessage(leaderElector, 'tell');
    }

    if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) {
      /**
       * another instance is also leader!
       * This can happen on rare events
       * like when the CPU is at 100% for long time
       * or the tabs are open very long and the browser throttles them.
       * @link https://github.com/pubkey/broadcast-channel/issues/414
       * @link https://github.com/pubkey/broadcast-channel/issues/385
       */
      leaderElector._dpLC = true;

      leaderElector._dpL(); // message the lib user so the app can handle the problem


      _sendMessage(leaderElector, 'tell'); // ensure other leader also knows the problem

    }
  };

  leaderElector._channel.addEventListener('internal', isLeaderListener);

  leaderElector._lstns.push(isLeaderListener);

  return _sendMessage(leaderElector, 'tell');
}

function fillOptionsWithDefaults(options, channel) {
  if (!options) options = {};
  options = JSON.parse(JSON.stringify(options));

  if (!options.fallbackInterval) {
    options.fallbackInterval = 3000;
  }

  if (!options.responseTime) {
    options.responseTime = channel.method.averageResponseTime(channel.options);
  }

  return options;
}

export function createLeaderElection(channel, options) {
  if (channel._leaderElector) {
    throw new Error('BroadcastChannel already has a leader-elector');
  }

  options = fillOptionsWithDefaults(options, channel);
  var elector = new LeaderElection(channel, options);

  channel._befC.push(function () {
    return elector.die();
  });

  channel._leaderElector = elector;
  return elector;
}