// The wrapper around the web-socket connection to the server
// manages the connection, heartbeats, and reconnect logic.
// presents itself as a dialog, or in-situ banner (_jamServer.html.haml)
(function (context, $) {

  "use strict";

  context.JK = context.JK || {};

  var logger = context.JK.logger;
  var msg_factory = context.JK.MessageFactory;
  var rest = context.JK.Rest();
  var EVENTS = context.JK.EVENTS;

  // Let socket.io know where WebSocketMain.swf is
  context.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf";

  context.JK.JamServer = function (app, activeElementEvent) {

    // uniquely identify the websocket connection
    var channelId = null;
    var clientType = null;
    var mode = null;
    var isLatencyTesterMode = false;

    // heartbeat
    var startHeartbeatTimeout = null;
    var heartbeatInterval = null;
    var heartbeatMS = null;
    var connection_expire_time = null;
    var lastHeartbeatSentTime = null;
    var lastHeartbeatAckTime = null;
    var lastHeartbeatFound = false;
    var lastDisconnectedReason = null;
    var heartbeatAckCheckInterval = null;
    var notificationLastSeenAt = undefined;
    var notificationLastSeen = undefined;
    var clientClosedConnection = false;
    var initialConnectAttempt = true;
    var active = true;

    // reconnection logic
    var connectDeferred = null;
    var freezeInteraction = false;
    var countdownInterval = null;
    var reconnectAttemptLookup = [2, 2, 2, 4, 8, 15, 30];
    var reconnectAttempt = 0;
    var reconnectingWaitPeriodStart = null;
    var reconnectDueTime = null;
    var connectTimeout = null;
    var activityTimeout;

    // elements
    var $inSituBanner = null;
    var $inSituBannerHolder = null;
    var $messageContents = null;
    var $dialog = null;
    var $templateServerConnection = null;
    var $templateNoLogin = null;
    var $templateDisconnected = null;
    var $currentDisplay = null;

    var $self = $(this);

    var server = {};
    server.socket = {};
    server.signedIn = false;
    server.clientID = "";
    server.publicIP = "";
    server.dispatchTable = {};
    server.socketClosedListeners = [];
    server.connecting = false; // is the websocket connection being opened?
    server.connected = false; // is the websocket connection opened AND logged in?
    server.reconnecting = false; // are we beginning the reconnect sequence (which includes an internet health check)

    function heartbeatStateReset() {
      lastHeartbeatSentTime = null;
      lastHeartbeatAckTime = null;
      lastHeartbeatFound = false;
    }

    // if activeElementVotes is null, then we are assuming this is the initial connect sequence
    function initiateReconnect(activeElementVotes, in_error) {
      var initialConnect = !!activeElementVotes;

      freezeInteraction = activeElementVotes && ((activeElementVotes.dialog && activeElementVotes.dialog.freezeInteraction === true) || (activeElementVotes.screen && activeElementVotes.screen.freezeInteraction === true));

      if (in_error) {
        reconnectAttempt = 0;
        $currentDisplay = renderDisconnected();
        beginReconnectPeriod();
      }
    }

    // handles logic if the websocket connection closes, and if it was in error then also prompt for    reconnect
    function closedCleanup(in_error) {

      if(isLatencyTester()) {
        logger.info("latency-tester: websocket connection lost")
      }

      if(server.connected) {
        $self.triggerHandler(EVENTS.CONNECTION_DOWN);
      }

      server.connected = false;
      server.connecting = false;

      // stop future heartbeats
      if (heartbeatInterval != null) {
        clearInterval(heartbeatInterval);
        heartbeatInterval = null;
      }

      // stop the heartbeat start delay from happening
      if (startHeartbeatTimeout != null) {
        clearTimeout(startHeartbeatTimeout);
        startHeartbeatTimeout = null;
      }

      // stop checking for heartbeat acks
      if (heartbeatAckCheckInterval != null) {
        clearTimeout(heartbeatAckCheckInterval);
        heartbeatAckCheckInterval = null;
      }

      clearConnectTimeout();

      // noReconnect is a global to suppress reconnect behavior, so check it first

      // we don't show any reconnect dialog on the initial connect; so we have this one-time flag
      // to cause reconnects in the case that the websocket is down on the initially connect
      if(server.noReconnect) {
        renderLoginRequired();
      }
      else if ((initialConnectAttempt || !server.reconnecting)) {
        server.reconnecting = true;
        initialConnectAttempt = false;

        var result = activeElementEvent('beforeDisconnect');

        initiateReconnect(result, in_error);

        activeElementEvent('afterDisconnect');

        // notify anyone listening that the socket closed
        var len = server.socketClosedListeners.length;
        for (var i = 0; i < len; i++) {
          try {
            server.socketClosedListeners[i](in_error);
          } catch (ex) {
            logger.warn('exception in callback for websocket closed event:' + ex);
          }
        }
      }
    }

    ////////////////////
    //// HEARTBEAT /////
    ////////////////////
    function _heartbeatAckCheck() {

      // if we've seen an ack to the latest heartbeat, don't bother with checking again
      // this makes us resilient to front-end hangs
      if (lastHeartbeatFound) {
        return;
      }

      // check if the server is still sending heartbeat acks back down
      // this logic equates to 'if we have not received a heartbeat within heartbeatMissedMS, then get upset
      if (new Date().getTime() - lastHeartbeatAckTime.getTime() > connection_expire_time) {
        logger.error("no heartbeat ack received from server after ", connection_expire_time, " seconds . giving up on socket connection");
        lastDisconnectedReason = 'NO_HEARTBEAT_ACK';
        context.JK.JamServer.close(true);
      }
      else {
        lastHeartbeatFound = true;
      }
    }

    function _heartbeat() {

      if(isLatencyTester()) {
        logger.info("latency-tester debug: heartbeat" + app.heartbeatActive)
      }
      if (app.heartbeatActive) {
        //console.log("heartbeat active?: " + active)
        var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt, active);
        notificationLastSeenAt = undefined;
        notificationLastSeen = undefined;
        // for debugging purposes, see if the last time we've sent a heartbeat is way off (500ms) of the target interval
        var now = new Date();


        if(lastHeartbeatSentTime) {
          var drift = new Date().getTime() - lastHeartbeatSentTime.getTime() - heartbeatMS;
          if (drift > 500) {
            logger.warn("significant drift between heartbeats: " + drift + 'ms beyond target interval')
          }
        }
        lastHeartbeatSentTime = now;
        context.JK.JamServer.send(message);
        lastHeartbeatFound = false;
      }
    }

    function isClientMode() {
      return mode == "client";
    }

    function isLatencyTester() {
      return isLatencyTesterMode;
    }

    function clearConnectTimeout() {
      if (connectTimeout) {
        clearTimeout(connectTimeout);
        connectTimeout = null;
      }
    }

    function loggedIn(header, payload) {

      // reason for setTimeout:
      // loggedIn causes an absolute ton of initialization to happen, and errors sometimes happen
      // but because loggedIn(header,payload) is a callback from a websocket, the browser doesn't show a stack trace...

      setTimeout(function() {
        server.signedIn = true;
        server.clientID = payload.client_id;
        server.publicIP = payload.public_ip;

        if (context.jamClient !== undefined) {
          context.jamClient.connected = true;
          context.jamClient.clientID = server.clientID;
        }

        clearConnectTimeout();

        heartbeatStateReset();

        app.clientId = payload.client_id;

        if (isClientMode() && context.jamClient) {
          // tell the backend that we have logged in
          try {
            var msg = {user_id: payload.user_id, token: payload.token, username: payload.username, arses: payload.arses, client_id_int: payload.client_id_int, subscription: payload.subscription}
            if(payload.connection_policy) {
              try {
                msg.policy= JSON.parse(payload.connection_policy)
              }
              catch(e) {
                msg.policy = null
                console.log("unable to parse connection policy", e)
              }
            }
            console.log("logged with new msg", msg)
            context.jamClient.OnLoggedIn(msg); // ACTS AS CONTINUATION
          }
          catch(e) {
            console.log("fallback to old callback", e)
            context.jamClient.OnLoggedIn(payload.user_id, payload.token, payload.username); // ACTS AS CONTINUATION
          }


          $.cookie('client_id', payload.client_id);
        }

        // this has to be after context.jamclient.OnLoggedIn, because it hangs in scenarios
        // where there is no device on startup for the current profile.
        // So, in that case, it's possible that a reconnect loop will attempt, but we *do not want*
        // it to go through unless we've passed through .OnLoggedIn
        server.connected = true;
        server.reconnecting = false;
        server.connecting = false;
        initialConnectAttempt = false;

        heartbeatMS = payload.heartbeat_interval * 1000;
        connection_expire_time = payload.connection_expire_time * 1000;
        logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's');

        // add some randomness to help move heartbeats apart from each other

        // send 1st heartbeat somewhere between 0 - 0.5 of the connection expire time
        var randomStartTime = connection_expire_time * (Math.random() / 2)

        if (startHeartbeatTimeout) {
          logger.warn("start heartbeat timeout is active; should be null")
          clearTimeout(startHeartbeatTimeout)
        }

        if (heartbeatInterval != null) {
          logger.warn("heartbeatInterval is active; should be null")
          clearInterval(heartbeatInterval);
          heartbeatInterval = null;
        }

        if (heartbeatAckCheckInterval != null) {
          logger.warn("heartbeatAckCheckInterval is active; should be null")
          clearInterval(heartbeatAckCheckInterval);
          heartbeatAckCheckInterval = null;
        }

        startHeartbeatTimeout = setTimeout(function() {
          if(server.connected) {
            heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS);
            heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000);
            lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat
          }
        }, randomStartTime)

        logger.info("starting heartbeat timer in " + randomStartTime/1000 + 's')



        connectDeferred.resolve();
        $self.triggerHandler(EVENTS.CONNECTION_UP)

        activeElementEvent('afterConnect', payload);

        if (payload.client_update && context.JK.ClientUpdateInstance) {
          context.JK.ClientUpdateInstance.runCheck(payload.client_update.product, payload.client_update.version, payload.client_update.uri, payload.client_update.size)
        }
      }, 0)
    }

    function setActive(active) {
      if(context.UserActivityActions) {
        context.UserActivityActions.setActive(active)
      }
    }
    function markAway() {
      logger.debug("sleep again!")
      active = false;
      setActive(active)
      var userStatus = msg_factory.userStatus(false, null);
      server.send(userStatus);
    }

    function activityCheck() {
      var timeoutTime = 300000; // 5 * 1000 * 60 , 5 minutes
      active = true;
      setActive(active)
      activityTimeout = setTimeout(markAway, timeoutTime);
      $(document).ready(function() {
        $('body').bind('mousedown keydown touchstart focus', function(event) {
          if (activityTimeout) {
            clearTimeout(activityTimeout);
            activityTimeout = null;
          }

          if (!active) {
            if(server && server.connected) {
              logger.debug("awake again!")
              var userStatus = msg_factory.userStatus(true, null);
              server.send(userStatus);
            }
          }
          active = true;
          setActive(active)
          activityTimeout = setTimeout(markAway, timeoutTime);
        });
      });
    }

    function heartbeatAck(header, payload) {
      lastHeartbeatAckTime = new Date();
    }

    function registerLoginAck() {
      logger.debug("register for loggedIn to set clientId");
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.LOGIN_ACK, loggedIn);
    }

    function registerHeartbeatAck() {
      logger.debug("register for heartbeatAck");
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, heartbeatAck);
    }

    function registerSocketClosed() {
      logger.debug("register for socket closed");
      context.JK.JamServer.registerOnSocketClosed(socketClosed);
    }

    function registerServerRejection() {
      logger.debug("register for server rejection");
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_REJECTION_ERROR, serverRejection);
    }

    /**
     * Called whenever the websocket closes; this gives us a chance to cleanup things that should be stopped/cleared
     * @param in_error did the socket close abnormally?
     */
    function socketClosed(in_error) {

      // tell the backend that we have logged out
      if (context.jamClient) context.jamClient.OnLoggedOut();

    }

    function serverRejection(header, payload) {
      logger.warn("server rejected our websocket connection. reason=" + payload.error_msg)

      if(payload.error_code == 'max_user_connections') {
        context.JK.Banner.showAlert("Too Many Connections", "You have too many connections to the server. If you believe this is in error, please contact support.")
      }
      else if(payload.error_code == 'invalid_login' || payload.error_code == 'empty_login') {
        logger.debug(payload.error_code + ": no longer reconnecting")
        server.noReconnect = true; // stop trying to log in!!
      }
      else if (payload.error_code == 'no_reconnect') {
        logger.debug(payload.error_code + ": no longer reconnecting")
        server.noReconnect = true; // stop trying to log in!!
        context.JK.Banner.showAlert("Misbehaved Client", "Please restart your application in order to continue using JamKazam.")
      }
    }

    ///////////////////
    /// RECONNECT /////
    ///////////////////
    function internetUp() {
      var start = new Date().getTime();
      server.connect()
        .done(function () {
          guardAgainstRapidTransition(start, finishReconnect);
        })
        .fail(function () {
          guardAgainstRapidTransition(start, closedOnReconnectAttempt);
        });
    }

    // websocket couldn't connect.  let's try again soon
    function closedOnReconnectAttempt() {
      failedReconnect();
    }

    function finishReconnect() {

      if(!clientClosedConnection) {
        lastDisconnectedReason = 'WEBSOCKET_CLOSED_REMOTELY'
        clientClosedConnection = false;
      }
      else if(!lastDisconnectedReason) {
        // let's have at least some sort of type, however generci
        lastDisconnectedReason = 'WEBSOCKET_CLOSED_LOCALLY'
      }

      if ($currentDisplay.is('.no-websocket-connection')) {
        // this path is the 'not in session path'; so there is nothing else to do
        $currentDisplay.removeClass('active');

        // TODO: tell certain elements that we've reconnected
      }
      else {
        window.location.reload();
      }
    }

    function buildOptions() {
      return {};
    }

    function renderLoginRequired() {
      var $inSituContent = $(context._.template($templateNoLogin.html(), buildOptions(), { variable: 'data' }));
      $inSituContent.find('a.disconnected-login').click(function() {
        var redirectPath= '?redirect-to=' + encodeURIComponent(context.JK.locationPath());
        if(gon.isNativeClient) {
          window.location.href = '/signin' + redirectPath;
        }
        else {
          window.location.href = '/' + redirectPath;
        }
        return false;
      })
      $messageContents.empty();
      $messageContents.append($inSituContent);
      $inSituBannerHolder.addClass('active');
      return $inSituBannerHolder;
    }
    function renderDisconnected() {

      var content = null;
      if (freezeInteraction) {
        var template = $templateDisconnected.html();
        var templateHtml = $(context.JK.fillTemplate(template, buildOptions()));
        templateHtml.find('.reconnect-countdown').html(formatDelaySecs(reconnectDelaySecs()));
        content = context.JK.Banner.show({
          html: templateHtml,
          type: 'reconnect'
        });
      }
      else {
        var $inSituContent = $(context._.template($templateServerConnection.html(), buildOptions(), { variable: 'data' }));
        $inSituContent.find('.reconnect-countdown').html(formatDelaySecs(reconnectDelaySecs()));
        $messageContents.empty();
        $messageContents.append($inSituContent);
        $inSituBannerHolder.addClass('active');
        content = $inSituBannerHolder;
      }

      return content;
    }

    function formatDelaySecs(secs) {
        return $('<span class="countdown-seconds"><span class="countdown">' + secs + '</span> ' + (secs == 1 ? ' second.<span style="visibility:hidden">s</span>' : 'seconds.') + '</span>');
    }

    function setCountdown($parent) {
      $parent.find('.reconnect-countdown').html(formatDelaySecs(reconnectDelaySecs()));
    }

    function renderCouldNotReconnect() {
      return renderDisconnected();
    }

    function renderReconnecting() {
      $currentDisplay.find('.reconnect-progress-msg').text('Attempting to reconnect...')

      if ($currentDisplay.is('.no-websocket-connection')) {
        $currentDisplay.find('.disconnected-reconnect').removeClass('reconnect-enabled').addClass('reconnect-disabled');
      }
      else {
        $currentDisplay.find('.disconnected-reconnect').removeClass('button-orange').addClass('button-grey');
      }
    }

    function failedReconnect() {
      reconnectAttempt += 1;
      $currentDisplay = renderCouldNotReconnect();
      beginReconnectPeriod();
    }

    function guardAgainstRapidTransition(start, nextStep) {
      var now = new Date().getTime();

      if ((now - start) < 1500) {
        setTimeout(function () {
          nextStep();
        }, 1500 - (now - start))
      }
      else {
        nextStep();
      }
    }

    function attemptReconnect() {

      if(server.connecting) {
        logger.warn("attemptReconnect called when already connecting");
        return;
      }

      if(server.connected) {
        logger.warn("attemptReconnect called when already connected");
        return;
      }

      var start = new Date().getTime();

      renderReconnecting();

      rest.serverHealthCheck()
        .done(function () {
          guardAgainstRapidTransition(start, internetUp);
        })
        .fail(function (xhr, textStatus, errorThrown) {

          if (xhr && xhr.status >= 100) {
            // we could connect to the server, and it's alive
            guardAgainstRapidTransition(start, internetUp);
          }
          else {
            guardAgainstRapidTransition(start, failedReconnect);
          }
        });

      return false;
    }

    function clearReconnectTimers() {
      if (countdownInterval) {
        clearInterval(countdownInterval);
        countdownInterval = null;
      }
    }

    function beginReconnectPeriod() {
      // allow user to force reconnect
      $currentDisplay.find('a.disconnected-reconnect').unbind('click').click(function () {
        if ($(this).is('.button-orange') || $(this).is('.reconnect-enabled')) {
          logger.debug("user initiated reconnect")
          clearReconnectTimers();
          attemptReconnect();
        }
        return false;
      });

      reconnectingWaitPeriodStart = new Date().getTime();
      reconnectDueTime = reconnectingWaitPeriodStart + reconnectDelaySecs() * 1000;

      // update count down timer periodically
      countdownInterval = setInterval(function () {
        var now = new Date().getTime();
        if (now > reconnectDueTime) {
          clearReconnectTimers();
          attemptReconnect();
        }
        else {
          var secondsUntilReconnect = Math.ceil((reconnectDueTime - now) / 1000);
          $currentDisplay.find('.reconnect-countdown').html(formatDelaySecs(secondsUntilReconnect));
        }
      }, 333);
    }

    function reconnectDelaySecs() {
      if (reconnectAttempt > reconnectAttemptLookup.length - 1) {
        return reconnectAttemptLookup[reconnectAttemptLookup.length - 1];
      }
      else {
        return reconnectAttemptLookup[reconnectAttempt];
      }
    }


    server.registerOnSocketClosed = function (callback) {
      server.socketClosedListeners.push(callback);
    }

    server.registerMessageCallback = function (messageType, callback) {
      if (server.dispatchTable[messageType] === undefined) {
        server.dispatchTable[messageType] = [];
      }

      server.dispatchTable[messageType].push(callback);
    };

    server.unregisterMessageCallback = function (messageType, callback) {
      if (server.dispatchTable[messageType] !== undefined) {
        for (var i = server.dispatchTable[messageType].length; i--;) {
          if (server.dispatchTable[messageType][i] === callback) {
            server.dispatchTable[messageType].splice(i, 1);
            break;
          }
        }

        if (server.dispatchTable[messageType].length === 0) {
          delete server.dispatchTable[messageType];
        }
      }
    };

    server.connect = function () {

      if(server.connecting) {
        logger.error("server.connect should never be called if we are already connecting. cancelling.")
        // XXX should return connectPromise, but needs to be tested/vetted
        return;
      }
      if(server.connected) {
        logger.error("server.connect should never be called if we are already connected. cancelling.")
        // XXX should return connectPromise, but needs to be tested/vetted
        return;
      }

      if(!clientType) {
        clientType = context.JK.clientType();
      }
      if(!mode) {
        mode = 'client'
        if (context.jamClient && context.jamClient.getOperatingMode) {
          mode = context.jamClient.getOperatingMode()
        }
        isLatencyTesterMode = mode == 'server';
      }

      connectDeferred = new $.Deferred();
      channelId = context.JK.generateUUID(); // create a new channel ID for every websocket connection

      var rememberToken = $.cookie("remember_token");

      if(isClientMode() && !rememberToken) {
        server.noReconnect = true;
        logger.debug("no login info; shutting down websocket");
        renderLoginRequired();
        connectDeferred.reject();
        return connectDeferred;
      }
      // we will log in one of 3 ways:
      // browser: use session cookie, and auth with token
      // native: use session cookie, and use the token
      // latency_tester: ask for client ID from backend; no token (trusted)


      if (isClientMode()) {
        var client_type = context.JK.clientType()
        var client_id = (gon.global.env == "development" ? $.cookie('client_id') : null)
        var machine = context.jamClient.SessionGetMacHash()
        if (machine) {
          machine = machine.all
        }
      }
      else {
        var client_type = 'latency_tester'
        var client_id = context.jamClient.clientID
        var machine = null
      }
      var params = {
        channel_id: channelId,
        token: rememberToken,
        client_type: client_type, // isClientMode() ? context.JK.clientType() : 'latency_tester',
        client_id: client_id,
        machine: machine,
        os: context.JK.GetOSAsString(),
        //jamblaster_serial_no: context.PlatformStore.jamblasterSerialNo(),
        udp_reachable: context.JK.StunInstance ? !context.JK.StunInstance.sync() : null // latency tester doesn't have the stun class loaded
      }
      var uri = context.gon.websocket_gateway_uri + '?' + $.param(params);  // Set in index.html.erb.

      logger.debug("connecting websocket: " + uri);

      server.connecting = true;
      server.socket = new context.WebSocket(uri);
      server.socket.channelId = channelId;
      server.socket.onopen = server.onOpen;
      server.socket.onmessage = server.onMessage;
      server.socket.onclose = server.onClose;

      connectTimeout = setTimeout(function () {
        logger.debug("connection timeout fired")
        connectTimeout = null;

        if(connectDeferred.state() === 'pending') {
          server.close(true);
          connectDeferred.reject();
        }
      }, 4000);

      return connectDeferred;
    };

    server.close = function (in_error) {
      logger.info("closing websocket");

      clientClosedConnection = true;
      server.socket.close();

      closedCleanup(in_error);
    }

    server.rememberLogin = function () {
      var token, loginMessage;
      token = $.cookie("remember_token");

      loginMessage = msg_factory.login_with_token(token, null, clientType);
      server.send(loginMessage);
    };

    server.latencyTesterLogin = function() {
      var loginMessage = msg_factory.login_with_client_id(context.jamClient.clientID, 'latency_tester');
      server.send(loginMessage);
    }

    server.onOpen = function () {
      logger.debug("server.onOpen");

      // we should receive LOGIN_ACK very soon.  we already set a timer elsewhere to give 4 seconds to receive it
    };

    server.onMessage = function (e) {
      var message = JSON.parse(e.data),
        messageType = message.type.toLowerCase(),
        payload = message[messageType],
        callbacks = server.dispatchTable[message.type];

      if (message.type == context.JK.MessageType.PEER_MESSAGE) {
        //logger.info("server.onMessage:" + messageType);
      }
      else if (message.type != context.JK.MessageType.HEARTBEAT_ACK && message.type != context.JK.MessageType.PEER_MESSAGE) {
        logger.info("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload));
      }

      if (callbacks !== undefined) {
        var len = callbacks.length;
        for (var i = 0; i < len; i++) {
          try {
            callbacks[i](message, payload);
          } catch (ex) {
            logger.warn('exception in callback for websocket message:' + ex);
            throw ex;
          }
        }
      }
      else {
        logger.info("Unexpected message type %s.", message.type);
      }
    };

    // onClose is called if either client or server closes connection
    server.onClose = function () {
      logger.info("Socket to server closed.");

      var disconnectedSocket = this;

      if(disconnectedSocket.channelId != server.socket.channelId) {
        logger.debug(" ignoring disconnect for non-current socket. current=" + server.socket.channelId + ", disc=" + disconnectedSocket.channelId)
        return;
      }

      if (connectDeferred.state() === "pending") {
        connectDeferred.reject();
      }

      closedCleanup(true);
    };

    server.send = function (message) {

      var jsMessage = JSON.stringify(message);

      if( isLatencyTester() && (message.type == context.JK.MessageType.HEARTBEAT  || message.type == context.JK.MessageType.PEER_MESSAGE)) {
        logger.info("latency-tester: server.send(" + jsMessage + ")")
      }
      else if (message.type != context.JK.MessageType.HEARTBEAT && message.type != context.JK.MessageType.PEER_MESSAGE) {
        logger.info("server.send(" + jsMessage + ")");
      }
      if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) {
        try {
          server.socket.send(jsMessage);
        }
        catch(err) {
          logger.warn("error when sending on websocket: " + err)
        }
      } else {
        logger.warn("Dropped message because server connection is closed.");
      }
    };

    server.loginSession = function (sessionId) {
      var loginMessage;

      if (!server.signedIn) {
        logger.warn("Not signed in!");
        // TODO: surface the error
        return;
      }

      loginMessage = msg_factory.login_jam_session(sessionId);
      server.send(loginMessage);
    };

    /** with the advent of the reliable UDP channel, this is no longer how messages are sent from client-to-clent
     * however, the mechanism still exists and is useful in test contexts; and maybe in the future
     * @param receiver_id  client ID of message to send
     * @param message the actual message
     */
    server.sendP2PMessage = function (receiver_id, message) {
      //logger.log("P2P message from [" + server.clientID + "] to [" + receiver_id + "]: " + message);
      //console.time('sendP2PMessage');
      var outgoing_msg = msg_factory.client_p2p_message(server.clientID, receiver_id, message);
      server.send(outgoing_msg);
      //console.timeEnd('sendP2PMessage');
    };

    server.sendLogin = function (token) {
      logger.debug("sending login message on behalf of client")
      var outgoing_msg = msg_factory.login_with_token(token, null, null);
      server.send(outgoing_msg);
    };

    server.sendLogout = function () {
      logger.debug("sending logout message on behalf of client")
      var outgoing_msg = msg_factory.logout();
      server.send(outgoing_msg);
    };

    server.sendChatMessage = function(channel, message) {

      if (server.connected) {

        var chatMsg = msg_factory.chatMessage(channel, message)
        server.send(chatMsg)
        return true;
      }
      else {
        return false;
      }
    }

    server.updateNotificationSeen = function (notificationId, notificationCreatedAt) {
      var time = new Date(notificationCreatedAt);

      if (!notificationCreatedAt) {
        throw 'invalid value passed to updateNotificationSeen'
      }

      if (!notificationLastSeenAt) {
        notificationLastSeenAt = notificationCreatedAt;
        notificationLastSeen = notificationId;
        logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
      }
      else if (time.getTime() > new Date(notificationLastSeenAt).getTime()) {
        notificationLastSeenAt = notificationCreatedAt;
        notificationLastSeen = notificationId;
        logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
      }
      else {
        logger.debug("ignored notificationLastSeenAt for: " + notificationCreatedAt);
      }
    }

    server.registerMessageCallback(context.JK.MessageType.PEER_MESSAGE, function (header, payload) {
      if (context.jamClient !== undefined) {
        context.jamClient.P2PMessageReceived(header.from, payload.message);
      }
    });

    server.get$Server = function() {
      return $self;
    }

    context.JK.JamServer = server;

    // Callbacks from jamClient
    if (context.jamClient !== undefined && context.jamClient.IsNativeClient()) {
      context.jamClient.SendP2PMessage.connect(server.sendP2PMessage);

      if (context.jamClient.SendLogin) {
        context.jamClient.SendLogin.connect(server.sendLogin);
        context.jamClient.SendLogin.connect(server.sendLogout);
      }
    }

    function initialize() {

      registerLoginAck();
      registerHeartbeatAck();
      registerServerRejection();
      registerSocketClosed();
      activityCheck();

      $inSituBanner = $('.server-connection');
      $inSituBannerHolder = $('.no-websocket-connection');
      $messageContents = $inSituBannerHolder.find('.message-contents');
      $dialog = $('#banner');
      $templateServerConnection = $('#template-server-connection');
      $templateNoLogin = $('#template-no-login');
      $templateDisconnected = $('#template-disconnected');

      if ($inSituBanner.length != 1) {
        throw "found wrong number of .server-connection: " + $inSituBanner.length;
      }
      if ($inSituBannerHolder.length != 1) {
        throw "found wrong number of .no-websocket-connection: " + $inSituBannerHolder.length;
      }
      if ($messageContents.length != 1) {
        throw "found wrong number of .message-contents: " + $messageContents.length;
      }
      if ($dialog.length != 1) {
        throw "found wrong number of #banner: " + $dialog.length;
      }
      if ($templateServerConnection.length != 1) {
        throw "found wrong number of #template-server-connection: " + $templateServerConnection.length;
      }
      if ($templateDisconnected.length != 1) {
        throw "found wrong number of #template-disconnected: " + $templateDisconnected.length;
      }
    }

    this.initialize = initialize;

    return this;
  }
})(window, jQuery);