(function() {
window.clockSyncWorker = function() {
/*
 * @overview es6-promise - a tiny implementation of Promises/A+.
 * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
 * @license   Licensed under MIT license
 *            See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
 * @version   v4.2.8+1e68dce6
 */

// eslint-disable-next-line
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.ES6Promise=e()}(this,function(){"use strict";function t(t){var e=typeof t;return null!==t&&("object"===e||"function"===e)}function e(t){return"function"==typeof t}function n(t){W=t}function r(t){z=t}function o(){return function(){return process.nextTick(a)}}function i(){return"undefined"!=typeof U?function(){U(a)}:c()}function s(){var t=0,e=new H(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){return t.port2.postMessage(0)}}function c(){var t=setTimeout;return function(){return t(a,1)}}function a(){for(var t=0;t<N;t+=2){var e=Q[t],n=Q[t+1];e(n),Q[t]=void 0,Q[t+1]=void 0}N=0}function f(){try{var t=Function("return this")().require("vertx");return U=t.runOnLoop||t.runOnContext,i()}catch(e){return c()}}function l(t,e){var n=this,r=new this.constructor(p);void 0===r[V]&&x(r);var o=n._state;if(o){var i=arguments[o-1];z(function(){return T(o,r,i,n._result)})}else j(n,r,t,e);return r}function h(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(p);return w(n,t),n}function p(){}function v(){return new TypeError("You cannot resolve a promise with itself")}function d(){return new TypeError("A promises callback cannot return that same promise.")}function _(t,e,n,r){try{t.call(e,n,r)}catch(o){return o}}function y(t,e,n){z(function(t){var r=!1,o=_(n,e,function(n){r||(r=!0,e!==n?w(t,n):A(t,n))},function(e){r||(r=!0,S(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&o&&(r=!0,S(t,o))},t)}function m(t,e){e._state===Z?A(t,e._result):e._state===$?S(t,e._result):j(e,void 0,function(e){return w(t,e)},function(e){return S(t,e)})}function b(t,n,r){n.constructor===t.constructor&&r===l&&n.constructor.resolve===h?m(t,n):void 0===r?A(t,n):e(r)?y(t,n,r):A(t,n)}function w(e,n){if(e===n)S(e,v());else if(t(n)){var r=void 0;try{r=n.then}catch(o){return void S(e,o)}b(e,n,r)}else A(e,n)}function g(t){t._onerror&&t._onerror(t._result),E(t)}function A(t,e){t._state===X&&(t._result=e,t._state=Z,0!==t._subscribers.length&&z(E,t))}function S(t,e){t._state===X&&(t._state=$,t._result=e,z(g,t))}function j(t,e,n,r){var o=t._subscribers,i=o.length;t._onerror=null,o[i]=e,o[i+Z]=n,o[i+$]=r,0===i&&t._state&&z(E,t)}function E(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r=void 0,o=void 0,i=t._result,s=0;s<e.length;s+=3)r=e[s],o=e[s+n],r?T(n,r,o,i):o(i);t._subscribers.length=0}}function T(t,n,r,o){var i=e(r),s=void 0,u=void 0,c=!0;if(i){try{s=r(o)}catch(a){c=!1,u=a}if(n===s)return void S(n,d())}else s=o;n._state!==X||(i&&c?w(n,s):c===!1?S(n,u):t===Z?A(n,s):t===$&&S(n,s))}function M(t,e){try{e(function(e){w(t,e)},function(e){S(t,e)})}catch(n){S(t,n)}}function P(){return tt++}function x(t){t[V]=tt++,t._state=void 0,t._result=void 0,t._subscribers=[]}function C(){return new Error("Array Methods must be provided an Array")}function O(t){return new et(this,t).promise}function k(t){var e=this;return new e(L(t)?function(n,r){for(var o=t.length,i=0;i<o;i++)e.resolve(t[i]).then(n,r)}:function(t,e){return e(new TypeError("You must pass an array to race."))})}function F(t){var e=this,n=new e(p);return S(n,t),n}function Y(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function q(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function D(){var t=void 0;if("undefined"!=typeof global)t=global;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;if(n){var r=null;try{r=Object.prototype.toString.call(n.resolve())}catch(e){}if("[object Promise]"===r&&!n.cast)return}t.Promise=nt}var K=void 0;K=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var L=K,N=0,U=void 0,W=void 0,z=function(t,e){Q[N]=t,Q[N+1]=e,N+=2,2===N&&(W?W(a):R())},B="undefined"!=typeof window?window:void 0,G=B||{},H=G.MutationObserver||G.WebKitMutationObserver,I="undefined"==typeof self&&"undefined"!=typeof process&&"[object process]"==={}.toString.call(process),J="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,Q=new Array(1e3),R=void 0;R=I?o():H?s():J?u():void 0===B&&"function"==typeof require?f():c();var V=Math.random().toString(36).substring(2),X=void 0,Z=1,$=2,tt=0,et=function(){function t(t,e){this._instanceConstructor=t,this.promise=new t(p),this.promise[V]||x(this.promise),L(e)?(this.length=e.length,this._remaining=e.length,this._result=new Array(this.length),0===this.length?A(this.promise,this._result):(this.length=this.length||0,this._enumerate(e),0===this._remaining&&A(this.promise,this._result))):S(this.promise,C())}return t.prototype._enumerate=function(t){for(var e=0;this._state===X&&e<t.length;e++)this._eachEntry(t[e],e)},t.prototype._eachEntry=function(t,e){var n=this._instanceConstructor,r=n.resolve;if(r===h){var o=void 0,i=void 0,s=!1;try{o=t.then}catch(u){s=!0,i=u}if(o===l&&t._state!==X)this._settledAt(t._state,e,t._result);else if("function"!=typeof o)this._remaining--,this._result[e]=t;else if(n===nt){var c=new n(p);s?S(c,i):b(c,t,o),this._willSettleAt(c,e)}else this._willSettleAt(new n(function(e){return e(t)}),e)}else this._willSettleAt(r(t),e)},t.prototype._settledAt=function(t,e,n){var r=this.promise;r._state===X&&(this._remaining--,t===$?S(r,n):this._result[e]=n),0===this._remaining&&A(r,this._result)},t.prototype._willSettleAt=function(t,e){var n=this;j(t,void 0,function(t){return n._settledAt(Z,e,t)},function(t){return n._settledAt($,e,t)})},t}(),nt=function(){function t(e){this[V]=P(),this._result=this._state=void 0,this._subscribers=[],p!==e&&("function"!=typeof e&&Y(),this instanceof t?M(this,e):q())}return t.prototype["catch"]=function(t){return this.then(null,t)},t.prototype["finally"]=function(t){var n=this,r=n.constructor;return e(t)?n.then(function(e){return r.resolve(t()).then(function(){return e})},function(e){return r.resolve(t()).then(function(){throw e})}):n.then(t,t)},t}();return nt.prototype.then=l,nt.all=O,nt.race=k,nt.resolve=h,nt.reject=F,nt._setScheduler=n,nt._setAsap=r,nt._asap=z,nt.polyfill=D,nt.Promise=nt,nt.polyfill(),nt});

// eslint-disable-next-line no-unused-vars
var workerHelpers = function() {
   // Helpers for web worker and window communication
   // eslint-disable-next-line no-empty-function
   var noop = function() {};

   var ret = {};

   var workerEventType = 'worker-event';

   ret.events = {
      START: 'start',
      STOP: 'stop',
      UPDATE: 'update',
      MASTER_OFFLINE: 'master_offline',
      DELTA_UPDATED: 'delta_updated',
      MASTER_ONLINE: 'master_online',
      WORKER_STOPPED: 'worker_stopped',
      PING: 'ping',
      PONG: 'pong',
      LOG: 'log',
      SIMILAR_DELTAS: 'similar_deltas',
      PING_SENT: 'pingsent'
   };

   ret.logTypes = {
      LOG: 'log',
      INFO: 'info',
      DEBUG: 'debug',
      WARN: 'warn',
      ERROR: 'error'
   };

   ret.callbacks = {};

   var getGlobal = function() {
      return self || window || {postMessage: noop};
   };

   var tryCleanObject = function(obj) {
      if (obj === undefined || typeof obj === 'function') {
         return undefined;
      }

      try {
         /* eslint-disable function-paren-newline */
         return JSON.parse(
            JSON.stringify(obj, function(key, value) {
               // NaN is the only value that does not equal itself
               // eslint-disable-next-line no-self-compare
               if (value !== value) {
                  return 'NaN';
               }

               if (value === Infinity) {
                  return 'Infinity';
               }

               if (value === -Infinity) {
                  return '-Infinity';
               }

               return value;
            })
         );
      } catch (e) {
         return obj;
      }
   };

   ret.emit = function(event, values, target) {
      if (!event) {
         return;
      }

      (target || getGlobal()).postMessage({
         type: tryCleanObject(workerEventType),
         payload: {
            event: tryCleanObject(event),
            values: tryCleanObject(values)
         }
      });
   };

   ret.on = function(event, callback) {
      if (!event || !callback || typeof callback !== 'function') {
         return;
      }

      ret.callbacks[event] = callback;
   };

   ret.onMessage = function(ev) {
      if (
         !ev ||
         !ev.data ||
         ev.data.type !== workerEventType ||
         !ev.data.payload ||
         !ev.data.payload.event ||
         !ret.callbacks[ev.data.payload.event]
      ) {
         return;
      }

      ret.callbacks[ev.data.payload.event](ev.data.payload.values);
   };

   ret.callbacks[ret.events.START] = noop;
   ret.callbacks[ret.events.STOP] = noop;
   ret.callbacks[ret.events.UPDATE] = noop;
   ret.callbacks[ret.events.PING_SENT] = noop;
   ret.callbacks[ret.events.PING] = function() {
      ret.emit(ret.events.PONG);
   };

   ret.ping = function(target) {
      ret.emit(ret.events.PING, undefined, target || getGlobal());
      ret.callbacks[ret.events.PING_SENT]();
   };

   var sendLog = function(type, message) {
      ret.emit(ret.events.LOG, {
         type: type || ret.logTypes.INFO,
         message: message
      });
   };

   ret.logger = {
      log: function(message) {
         sendLog(ret.logTypes.INFO, message);
      },
      debug: function(message) {
         sendLog(ret.logTypes.DEBUG, message);
      },
      info: function(message) {
         sendLog(ret.logTypes.INFO, message);
      },
      warn: function(message) {
         sendLog(ret.logTypes.WARN, message);
      },
      error: function(message) {
         sendLog(ret.logTypes.ERROR, message);
      }
   };

   return ret;
};

// eslint-disable-next-line no-unused-vars
var webSocketsService = function(url, slaveId, port, wss, timeoutInterval, logger) {
   if (!url) {
      throw new Error('url is required');
   }

   var ret = {};
   var opened = false;
   var defaultMessage = 'ping';
   var ws;
   var connectingThread;
   var getTimestampTimeoutThread;
   var pingTimeoutThread;

   var constructWebSocketUrl = function() {
      return (wss ? 'wss' : 'ws') + '://' + url + (port ? ':' + port : '');
   };

   ret.isSocketOpen = function() {
      return opened && ws && ws instanceof WebSocket && ws.readyState == ws.OPEN;
   };

   var onDisconnect = function() {
      opened = false;
      ws = undefined;
   };

   ret.closeSocketConnection = function() {
      if (getTimestampTimeoutThread) {
         clearTimeout(getTimestampTimeoutThread);
      }

      if (pingTimeoutThread) {
         clearTimeout(getTimestampTimeoutThread);
      }

      if (connectingThread) {
         clearTimeout(getTimestampTimeoutThread);
      }

      if (ret.isSocketOpen()) {
         ws.close();
      }

      onDisconnect();
   };

   ret.openSocketConnection = function() {
      return new Promise(function(resolve, reject) {
         if (ret.isSocketOpen()) {
            ret.closeSocketConnection();
         }

         ws = new WebSocket(constructWebSocketUrl());

         connectingThread = setTimeout(function() {
            reject('could not connect to websocket server');
         }, timeoutInterval);

         ws.onerror = reject;
         ws.onclose = onDisconnect;
         ws.onopen = function() {
            if (connectingThread) {
               clearTimeout(connectingThread);
            }

            opened = true;
            resolve('connected');
         };
      });
   };

   ret.getMasterTime = function() {
      if (!ret.isSocketOpen()) {
         return Promise.reject('socket not connected');
      }

      return new Promise(function(resolve, reject) {
         if (getTimestampTimeoutThread) {
            clearTimeout(getTimestampTimeoutThread);
            getTimestampTimeoutThread = undefined;
         }

         getTimestampTimeoutThread = setTimeout(function() {
            ret.closeSocketConnection();
            logger.warn('WebSocket - Web Socket connection timeout.');
            reject('request timedout');
         }, timeoutInterval);

         ws.onmessage = function(messageEvent) {
            if (getTimestampTimeoutThread) {
               clearTimeout(getTimestampTimeoutThread);
               getTimestampTimeoutThread = undefined;
            }

            if (!messageEvent || !messageEvent.data) {
               logger.warn('WebSocket - Web Socket response is empty.');
               reject('No Data in response');
            }

            var response = messageEvent.data.split('_');
            // Support for legacy app responses
            var altResponse = messageEvent.data.split(' ');

            if (response && response.length > 1 && !isNaN(response[1])) {
               resolve(parseInt(response[1]));
            } else if (altResponse && altResponse.length > 1 && !isNaN(altResponse[1])) {
               resolve(parseInt(altResponse[1]));
            } else {
               reject(messageEvent);
            }
         };

         ws.send(slaveId || defaultMessage);
      });
   };

   ret.isSocketObject = true;

   var init = function() {
      timeoutInterval = timeoutInterval || 500;
   };

   init();

   return ret;
};

// eslint-disable-next-line no-unused-vars
var slaveWorkerService = function(
   helpers,
   masterIp,
   port,
   playerGuid,
   refreshInterval,
   maxAcceptedDelay,
   failedRetryInterval,
   webSocketConnectionTimeoutInterval,
   maxAcceptedDeltasRange,
   isPerformanceApiSupported
) {
   /* eslint-disable function-paren-newline */
   /* eslint-disable prefer-object-spread */
   var websockets;
   var getTimestampThread;
   var stopped = true;
   var lastClockValues = {};
   var delta = null;
   var logger = helpers.logger;

   refreshInterval = refreshInterval || 20000;
   port = port || 55537;
   masterIp = masterIp || '';

   var ret = {};

   var isNullOrNotANumber = function(value) {
      return (!value || isNaN(value)) && value !== 0;
   };

   var closeWebsocket = function() {
      if (!websockets || !websockets.isSocketObject || !websockets.isSocketOpen()) {
         return;
      }

      websockets.closeSocketConnection();
   };

   var openWebsocket = function() {
      return new Promise(function(resolve, reject) {
         closeWebsocket();
         websockets.openSocketConnection().then(resolve, reject);
      });
   };

   var isWithinRange = function(newVal, oldVal, range) {
      if (isNullOrNotANumber(newVal)) {
         return false;
      }

      if (isNullOrNotANumber(oldVal)) {
         return true;
      }

      return Math.abs(newVal - oldVal) <= (range || 10);
   };

   var clearTimeoutIfActive = function(timeoutThread) {
      if (timeoutThread) {
         clearTimeout(timeoutThread);
      }
   };

   var calculateDeltaAndDelay = function(originalTimestamp, masterTimestamp, currentTimestamp) {
      var retValues = {
         websocketDelay: 0,
         websocketDelta: 0,
         isAccepted: false
      };

      if (!originalTimestamp || !masterTimestamp || !currentTimestamp) {
         return retValues;
      }

      retValues.websocketDelay = currentTimestamp - originalTimestamp;
      retValues.websocketDelta = masterTimestamp - originalTimestamp;
      // if round-trip is bigger then maximum accepted value then we reject it
      retValues.isAccepted = retValues.websocketDelay >= 0 && retValues.websocketDelay <= maxAcceptedDelay;

      return retValues;
   };

   var isSimilarToCurrentDelta = function(data) {
      if (!data) {
         return false;
      }

      return delta && isWithinRange(data.websocketDelta, delta, maxAcceptedDeltasRange);
   };

   var updateDeltaAndDelayValues = function(data) {
      if (stopped || !data || !data.isAccepted || isSimilarToCurrentDelta(data)) {
         logger.debug('Worker - Current Delta is similar to the previous one, ignoring');
         return;
      }

      delta = data.websocketDelta;
      helpers.emit(helpers.events.DELTA_UPDATED, data);
   };

   var retrySocketOnError = function(error, callback) {
      logger.warn(error.message || error);
      logger.info('Worker - Retrying to connect websocket now');
      if (!callback || typeof callback !== 'function') {
         return;
      }

      getTimestampThread = setTimeout(callback, failedRetryInterval);
   };

   var getTimeOrigin = function() {
      var timeOrigin = self.performance.timeOrigin;
      return timeOrigin == undefined ? self.performance.timing.navigationStart : timeOrigin;
   };

   var getCurrentTime = function() {
      return isPerformanceApiSupported ? getTimeOrigin() + self.performance.now() : Date.now();
   };

   var requestTimestampFromMaster = function() {
      if (!masterIp || !websockets || stopped) {
         return;
      }

      clearTimeoutIfActive(getTimestampThread);
      getTimestampThread = undefined;

      openWebsocket().then(
         function onConnected() {
            var startTimestamp = getCurrentTime();
            websockets.getMasterTime().then(
               function(masterTime) {
                  closeWebsocket();
                  helpers.emit(helpers.events.MASTER_ONLINE);

                  var currentClockValues = calculateDeltaAndDelay(startTimestamp, masterTime, getCurrentTime());

                  if (!currentClockValues.isAccepted) {
                     logger.info('Worker - roundtrip took more than minimum accepted value, retrying soon.');
                     retrySocketOnError('roundtrip too large', requestTimestampFromMaster);
                     return;
                  }

                  // Only update delta if it is different from the current delta
                  if (
                     isWithinRange(
                        currentClockValues.websocketDelta,
                        lastClockValues.websocketDelta,
                        maxAcceptedDeltasRange
                     )
                  ) {
                     logger.debug(
                        'Worker - Response from Controller with Delta: ' +
                           currentClockValues.websocketDelta +
                           ' and roundtrip:' +
                           currentClockValues.websocketDelay
                     );
                     updateDeltaAndDelayValues(currentClockValues);
                  } else {
                     logger.debug(
                        'Worker - Got an out of ordinary delta, waiting for next cycle before sending it'
                     );
                     helpers.emit(helpers.events.SIMILAR_DELTAS);
                  }

                  lastClockValues = Object.assign({}, currentClockValues);
                  getTimestampThread = setTimeout(requestTimestampFromMaster, refreshInterval);
               },
               function(error) {
                  logger.warn('Worker - Received wrong response from Controller.');
                  closeWebsocket();
                  retrySocketOnError(error, requestTimestampFromMaster);
                  helpers.emit(helpers.events.MASTER_OFFLINE);
               }
            );
         },
         function onNotConnected(error) {
            logger.warn('Worker - Web Socket could not connect.');
            retrySocketOnError(error, requestTimestampFromMaster);
            helpers.emit(helpers.events.MASTER_OFFLINE);
         }
      );
   };

   var init = function() {
      logger.debug('Worker - Starting worker and Initializing WebSocket service');

      // eslint-disable-next-line no-undef
      websockets = webSocketsService(
         masterIp,
         playerGuid,
         port,
         false,
         webSocketConnectionTimeoutInterval,
         logger
      );

      logger.debug('Worker - WebSocket Initialized');

      logger.debug('Worker - Will start syncing clock after 10 seconds');
      // Delay start by 10 seconds at start
      setTimeout(requestTimestampFromMaster, 10000);
   };

   ret.stop = function() {
      stopped = true;

      clearTimeoutIfActive(getTimestampThread);
      getTimestampThread = undefined;

      closeWebsocket();

      websockets = undefined;

      delta = undefined;
   };

   ret.start = function() {
      stopped = false;
      init();
   };

   ret.isRunning = function() {
      return !stopped;
   };

   return ret;
};

var slave;
// eslint-disable-next-line no-undef
var helpers = workerHelpers();

self.onmessage = helpers.onMessage;

helpers.on(helpers.events.START, function(payload) {
   // eslint-disable-next-line no-undef
   if (!payload) {
      return;
   }

   // eslint-disable-next-line no-undef
   slave = slaveWorkerService(
      helpers,
      payload.masterIp,
      payload.port,
      payload.playerGuid,
      payload.refreshInterval,
      payload.maxAcceptedDelay,
      payload.failedRetryInterval,
      payload.webSocketConnectionTimeoutInterval,
      payload.maxAcceptedDeltasRange,
      payload.isPerformanceApiSupported
   );
   slave.start();
   helpers.logger.debug('Worker - Worker Started.');
});

helpers.on(helpers.events.STOP, function() {
   if (!slave) {
      return;
   }

   helpers.logger.debug('Worker - Stopping Worker.');

   slave.stop();
   slave = undefined;
   helpers.emit(helpers.events.WORKER_STOPPED);
});
};
}());
