// The session model contains information about the music
// sessions that the current client has joined.
(function(context,$) {

    "use strict";

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

    // screen can be null
    context.JK.SessionModel = function(app, server, client, sessionScreen) {
        var ALERT_TYPES = context.JK.ALERT_TYPES;
        var EVENTS = context.JK.EVENTS;
        var MIX_MODES = context.JK.MIX_MODES;
        var gearUtils = context.JK.GearUtilsInstance;

        var userTracks = null; // comes from the backend
        var clientId = client.clientID;
        var currentSessionId = null; // Set on join, prior to setting currentSession.
        var currentSession = null;
        var currentOrLastSession = null;
        var subscribers = {};
        var users = {}; // User info for session participants
        var rest = context.JK.Rest();
        var requestingSessionRefresh = false;
        var pendingSessionRefresh = false;
        var recordingModel = new context.JK.RecordingModel(app, this, rest, context.jamClient);
        var currentTrackChanges = 0;
        var backendMixerAlertThrottleTimer = null;
        // we track all the clientIDs of all the participants ever seen by this session, so that we can reliably convert a clientId from the backend into a username/avatar
        var participantsEverSeen = {};
        var currentParticipants = {};
        var $self = $(this);
        var sessionPageEnterDeferred = null;
        var sessionPageEnterTimeout = null;
        var startTime = null;
        var joinDeferred = null;
        var previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []};
        var openBackingTrack = null;
        var shownAudioMediaMixerHelp = false;
        var controlsLockedForJamTrackRecording = false;

        var mixerMode = MIX_MODES.PERSONAL;

        server.registerOnSocketClosed(onWebsocketDisconnected);

        function id() {
            return currentSessionId;
        }

        function start(sessionId) {
          currentSessionId = sessionId;
          startTime = new Date().getTime();
        }

        function setUserTracks(_userTracks) {
          userTracks = _userTracks;
        }

        function inSession() {
            return !!currentSessionId;
        }

        function participants() {
            if (currentSession) {
                return currentSession.participants;
            } else {
                return [];
            }
        }

        // if any participant has the metronome open, then we say this session has the metronome open
        function isMetronomeOpen() {
          var metronomeOpen = false;
          context._.each(participants(), function(participant) {
            if(participant.metronome_open) {
              metronomeOpen = true;
              return false;
            }
          })

          return metronomeOpen;
        }

        function isPlayingRecording() {
            // this is the server's state; there is no guarantee that the local tracks
            // requested from the backend will have corresponding track information
            return !!(currentSession && currentSession.claimed_recording);
        }

        function recordedTracks() {
            if(currentSession && currentSession.claimed_recording) {
                return currentSession.claimed_recording.recording.recorded_tracks
            }
            else {
                return null;
            }
        }

        function recordedBackingTracks() {
          if(currentSession && currentSession.claimed_recording) {
            return currentSession.claimed_recording.recording.recorded_backing_tracks
          }
          else {
            return null;
          }
        }

        function backingTracks() {
          var backingTracks = []
          // this may be wrong if we loosen the idea that only one person can have a backing track open.
          // but for now, the 1st person we find with a backing track open is all there is to find...
          context._.each(participants(), function(participant) {
            if(participant.backing_tracks.length > 0) {
              backingTracks = participant.backing_tracks;
              return false; // break
            }
          })
          return backingTracks;
        }

        function jamTracks() {
          if(currentSession && currentSession.jam_track) {
            return currentSession.jam_track.tracks.filter(function(track) { return track.track_type == 'Track'})
          }
          else {
            return null;
          }
        }

        function recordedJamTracks() {
          if(currentSession && currentSession.claimed_recording) {
            return currentSession.claimed_recording.recording.recorded_jam_track_tracks
          }
          else {
            return null;
          }
        }

      function jamTrackName() {
        if (currentSession && currentSession.jam_track) {
          return currentSession.jam_track.name;
        }
        else {
          return null;
        }
      }

      function recordedJamTrackName() {
        if(currentSession && currentSession.claimed_recording && currentSession.claimed_recording.recording.jam_track) {
          return currentSession.claimed_recording.recording.jam_track.name;
        }
        else {
          return null;
        }
      }
        // did I open up the current JamTrack?
        function selfOpenedJamTracks() {
          logger.debug("currentSession", currentSession)
          return currentSession && (currentSession.jam_track_initiator_id == context.JK.currentUserId)
        }

        function backingTrack() {
          if(currentSession) {
            // TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668
            return {
              path: currentSession.backing_track_path
            }
          }
          else {
            return null;
          }
        }

        function creatorId() {
            if(!currentSession) {
                throw "creator is not known"
            }
            return currentSession.user_id;
        }

        function alreadyInSession() {
            var inSession = false;
            $.each(participants(), function(index, participant) {
                 if(participant.user.id == context.JK.currentUserId) {
                     inSession = true;
                     return false;
                 }
            });
            return inSession;
        }

        function lockControlsforJamTrackRecording() {
          controlsLockedForJamTrackRecording = true;
        }

        function unlockControlsforJamTrackRecording() {
          controlsLockedForJamTrackRecording = false;
        }

        function areControlsLockedForJamTrackRecording() {
          return controlsLockedForJamTrackRecording;
        }

        function onMixerModeChanged(newMixerMode)
        {
          mixerMode = newMixerMode;
          var mode = newMixerMode == MIX_MODES.MASTER ? "master" : "personal";
          logger.debug("onMixerModeChanged:" + mode);
          $(document).triggerHandler(EVENTS.MIXER_MODE_CHANGED, {mode:mode});
        }

        function waitForSessionPageEnterDone() {
          sessionPageEnterDeferred = $.Deferred();

          // see if we already have tracks; if so, we need to run with these
          var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);

          logger.debug("isNoInputProfile", gearUtils.isNoInputProfile())
          if(inputTracks.length > 0 || gearUtils.isNoInputProfile() ) {
            logger.debug("on page enter, tracks are already available")
            sessionPageEnterDeferred.resolve(inputTracks);
            var deferred = sessionPageEnterDeferred;
            sessionPageEnterDeferred = null;
            return deferred;
          }

          sessionPageEnterTimeout = setTimeout(function() {
            if(sessionPageEnterTimeout) {
              if(sessionPageEnterDeferred) {
                sessionPageEnterDeferred.reject('timeout');
                sessionPageEnterDeferred = null;
              }
              sessionPageEnterTimeout = null;
            }
          }, 5000);

          return sessionPageEnterDeferred;
        }
        /**
        * Join the session specified by the provided id.
        */
        function joinSession(sessionId) {
            logger.debug("SessionModel.joinSession(" + sessionId + ")");
            joinDeferred  = joinSessionRest(sessionId);

          joinDeferred
            .done(function(response){

                    if(!inSession()) {
                      // the user has left the session before they got joined.  We need to issue a leave again to the server to make sure they are out
                      logger.debug("user left before fully joined to session.  telling server again that they have left")
                      leaveSessionRest(response.id);
                      return;
                    }

                    logger.debug("calling jamClient.JoinSession");
                    // on temporary disconnect scenarios, a user may already be in a session when they enter this path
                    // so we avoid double counting
                    if(!alreadyInSession()) {
                      if(response.music_session.participant_count == 1) {
                        context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create);
                      }
                      else {
                        context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join);
                      }
                    }

                    recordingModel.reset();
                    client.JoinSession({ sessionID: sessionId });
                    refreshCurrentSession(true);
                    server.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, trackChanges);
                    server.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, trackChanges);
                    server.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, trackChanges);
                    server.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, trackChanges);

                    $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId, lesson_session: session.lesson_session}});
                })
                .fail(function() {
                  updateCurrentSession(null);
                });

            return joinDeferred;
        }

        function performLeaveSession(deferred) {

            logger.debug("SessionModel.leaveCurrentSession()");
            // TODO - sessionChanged will be called with currentSession = null\
            server.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, trackChanges);
            server.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, trackChanges);
            server.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, trackChanges);
            server.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, trackChanges);

            //server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession);
            // leave the session right away without waiting on REST.  Why?  If you can't contact the server, or if it takes a long
            // time, for that entire duration you'll still be sending voice data to the other users.
            // this may be bad if someone decides to badmouth others in the left-session during this time
            //console.trace();
            logger.debug("performLeaveSession: calling jamClient.LeaveSession for clientId=" + clientId);
            client.LeaveSession({ sessionID: currentSessionId });
            leaveSessionRest(currentSessionId)
                .done(function() {
                    sessionChanged();
                    deferred.resolve(arguments[0], arguments[1], arguments[2]);
                })
                .fail(function() {
                    deferred.reject(arguments[0], arguments[1], arguments[2]);
                });

            // 'unregister' for callbacks
            context.jamClient.SessionRegisterCallback("");
            //context.jamClient.SessionSetAlertCallback("");
            context.jamClient.SessionSetConnectionStatusRefreshRate(0);
            updateCurrentSession(null);
        }
        /**
        * Leave the current session, if there is one.
        * callback: called in all conditions; either after an attempt is made to tell the server that we are leaving,
        * or immediately if there is no session
        */
        function leaveCurrentSession() {
            var deferred = new $.Deferred();

            recordingModel.stopRecordingIfNeeded()
                .always(function(){
                    performLeaveSession(deferred);
                });

            return deferred;
        }

        function trackChanges(header, payload) {
          if(currentTrackChanges < payload.track_changes_counter) {
            // we don't have the latest info. try and go get it
            logger.debug("track_changes_counter = stale. refreshing...")
            refreshCurrentSession();
          }
          else {
            if(header.type != 'HEARTBEAT_ACK') {
              // don't log if HEARTBEAT_ACK, or you will see this log all the time
              logger.info("track_changes_counter = fresh. skipping refresh...", header, payload)
            }
          }
        }

        /**
        * Refresh the current session, and participants.
        */
        function refreshCurrentSession(force) {
            // XXX use backend instead: https://jamkazam.atlassian.net/browse/VRFS-854
            //logger.debug("SessionModel.refreshCurrentSession(" + currentSessionId +")");
            if(force) {
              logger.debug("refreshCurrentSession(force=true)")
            }
          else {

            }
            refreshCurrentSessionRest(sessionChanged, force);
        }

        /**
        * Subscribe for sessionChanged events. Provide a subscriberId
        * and a callback to be invoked on session changes.
        */
        function subscribe(subscriberId, sessionChangedCallback) {
            logger.debug("SessionModel.subscribe(" + subscriberId + ", [callback])");
            subscribers[subscriberId] = sessionChangedCallback;
        }

        /**
        * Notify subscribers that the current session has changed.
        */
        function sessionChanged() {
            for (var subscriberId in subscribers) {
                subscribers[subscriberId](currentSession);
            }
        }

        // universal place to clean up, reset items
        // fullyJoined means the user stayed in the screen until they had a successful rest.joinSession
        // you might not get a fully joined if you join/leave really quickly before the REST API completes
        function sessionEnded(fullyJoined) {
          //cleanup

          server.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN,   trackChanges);
          server.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, trackChanges);
          server.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, trackChanges);
          server.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, trackChanges);

          if(sessionPageEnterDeferred != null) {
            sessionPageEnterDeferred.reject('session_over');
            sessionPageEnterDeferred = null;
          }
          userTracks = null;
          startTime = null;
          joinDeferred = null;
          mixerMode = MIX_MODES.PERSONAL;
          if(fullyJoined) {
            $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionId}});
          }
          currentSessionId = null;
          currentParticipants = {}
          previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []}
          openBackingTrack = null
          shownAudioMediaMixerHelp = false
          controlsLockedForJamTrackRecording = false;
        }

        // you should only update currentSession with this function
        function updateCurrentSession(sessionData) {
            if(sessionData != null) {
                currentOrLastSession = sessionData;
            }

            var beforeUpdate = currentSession;

            currentSession = sessionData;

            // the 'beforeUpdate != null' makes sure we only do a clean up one time internally
            if(sessionData == null) {
              sessionEnded(beforeUpdate != null);
            }
        }

        function updateSession(response) {
          updateSessionInfo(response, null, true);
        }

        function updateSessionInfo(response, callback, force) {
          if(force === true || currentTrackChanges < response.track_changes_counter) {
            logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter)
            currentTrackChanges = response.track_changes_counter;
            sendClientParticipantChanges(currentSession, response);
            updateCurrentSession(response);
            if(callback != null) {
              callback();
            }
          }
          else {
            logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
          }
        }

        /**
        * Reload the session data from the REST server, calling
        * the provided callback when complete.
        */
        function refreshCurrentSessionRest(callback, force) {

            if(!inSession()) {
              logger.debug("refreshCurrentSession skipped: ")
              return;
            }

            var url = "/api/sessions/" + currentSessionId;
            if(requestingSessionRefresh) {
                // if someone asks for a refresh while one is going on, we ask for another to queue up
                logger.debug("queueing refresh");
                pendingSessionRefresh = true;
            }
            else {
                requestingSessionRefresh = true;
                $.ajax({
                    type: "GET",
                    url: url,
                    success: function(response) {
                        updateSessionInfo(response, callback, force);
                    },
                    error: function(jqXHR) {
                      if(jqXHR.status != 404) {
                        app.notifyServerError(jqXHR, "Unable to refresh session data")
                      }
                      else {
                        logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone")
                      }
                    },
                    complete: function() {
                        requestingSessionRefresh = false;
                        if(pendingSessionRefresh) {
                            // and when the request is done, if we have a pending, fire it off again
                            pendingSessionRefresh = false;
                            refreshCurrentSessionRest(sessionChanged, force);
                        }
                    }
                });
            }
        }

        /**
        * Seems silly. We should just have the bridge take sessionId, clientId
        */
        function _toJamClientParticipant(participant) {
            return {
                userID : "",
                clientID : participant.client_id,
                tcpPort : 0,
                udpPort : 0,
                localIPAddress : participant.ip_address,  // ?
                globalIPAddress : participant.ip_address, // ?
                latency : 0,
                natType : ""
            };
        }

        function ParticipantJoined(newSession, participant) {
          client.ParticipantJoined(newSession, _toJamClientParticipant(participant));
          currentParticipants[participant.client_id] = {server:participant, client: {audio_established:null}}
        }

        function ParticipantLeft(newSession, participant) {
          client.ParticipantLeft(newSession, _toJamClientParticipant(participant));
          delete currentParticipants[participant.client_id]
        }

        function sendClientParticipantChanges(oldSession, newSession) {
            var joins = [], leaves = [], leaveJoins = []; // Will hold JamClientParticipants

            var oldParticipants = []; // will be set to session.participants if session
            var oldParticipantIds = {};
            var newParticipants = [];
            var newParticipantIds = {};

            if (oldSession && oldSession.participants) {
                oldParticipants = oldSession.participants;
                $.each(oldParticipants, function() {
                    oldParticipantIds[this.client_id] = this;
                });
            }
            if (newSession && newSession.participants) {
                newParticipants = newSession.participants;
                $.each(newParticipants, function() {
                    newParticipantIds[this.client_id] = this;
                });
            }

            $.each(newParticipantIds, function(client_id, participant) {
                // grow the 'all participants seen' list
                if(!(client_id in participantsEverSeen)) {
                  participantsEverSeen[client_id] = participant;
                }

                if(client_id in oldParticipantIds) {
                  // if the participant is here now, and here before, there is still a chance we missed a
                  // very fast leave/join. So check if joined_session_at is different
                  if(oldParticipantIds[client_id].joined_session_at != participant.joined_session_at) {
                    leaveJoins.push(participant)
                  }
                }
                else {
                  // new participant id that's not in old participant ids: Join
                  joins.push(participant);
                }
            });
            $.each(oldParticipantIds, function(client_id, participant) {
                if (!(client_id in newParticipantIds)) {
                    // old participant id that's not in new participant ids: Leave
                    leaves.push(participant);
                }
            });

            $.each(joins, function(i,v) {
                if (v.client_id != clientId) {
                    logger.debug("jamClient.ParticipantJoined", v.client_id)
                    ParticipantJoined(newSession, v)
                }
            });
            $.each(leaves, function(i,v) {
                if (v.client_id != clientId) {
                    logger.debug("jamClient.ParticipantLeft", v.client_id)
                    ParticipantLeft(newSession, v)
                }
            });
            $.each(leaveJoins, function(i,v) {
              if (v.client_id != clientId) {
                logger.debug("participant had a rapid leave/join")
                logger.debug("jamClient.ParticipantLeft", v.client_id)
                ParticipantLeft(newSession, v)

                logger.debug("jamClient.ParticipantJoined", v.client_id)
                ParticipantJoined(newSession, v)
              }
            });
        }


        function participantForClientId(clientId) {
            var foundParticipant = null;
            $.each(currentSession.participants, function(index, participant) {
                if (participant.client_id === clientId) {
                    foundParticipant = participant;
                    return false;
                }
            });
            return foundParticipant;
        }

        function deleteTrack(sessionId, trackId) {

            if (trackId) {
                client.TrackSetCount(1);
                client.TrackSaveAssignments();
            }
        }

        /**
        * Make the server calls to join the current user to
        * the session provided.
        */
        function joinSessionRest(sessionId) {
            var data = {
                client_id: clientId,
                ip_address: server.publicIP,
                as_musician: true,
                tracks: userTracks,
                session_id: sessionId,
                audio_latency: context.jamClient.FTUEGetExpectedLatency().latency
            };

            return rest.joinSession(data);
        }

        function leaveSessionRest(sessionId) {
            var url = "/api/participants/" + clientId;
            return $.ajax({
                type: "DELETE",
                url: url
            });
        }

        function setMixerMode(newMixerMode) {
          if(mixerMode != newMixerMode) {
            onMixerModeChanged(newMixerMode);
          }
        }

        function isMasterMixMode() {
          return mixerMode == MIX_MODES.MASTER;
        }

        function isPersonalMixMode() {
          return mixerMode == MIX_MODES.PERSONAL;
        }

        function getMixMode() {
          return mixerMode;
        }

        function syncTracks(allTracks) {
          // double check that we are in session, since a bunch could have happened since then
          if(!inSession()) {
            logger.debug("dropping queued up sync tracks because no longer in session");
            return null;
          }

          if(allTracks === undefined) {
            allTracks = context.JK.TrackHelpers.getTrackInfo(context.jamClient);
          }

          var inputTracks = allTracks.userTracks;
          var backingTracks = allTracks.backingTracks;
          var metronomeTracks = allTracks.metronomeTracks;

          // create a trackSync request based on backend data
          var syncTrackRequest = {};
          syncTrackRequest.client_id = app.clientId;
          syncTrackRequest.tracks = inputTracks;
          syncTrackRequest.backing_tracks = backingTracks;
          syncTrackRequest.metronome_open = metronomeTracks.length > 0;
          syncTrackRequest.id = id();

          return rest.putTrackSyncChange(syncTrackRequest)
            .done(function() {
            })
            .fail(function(jqXHR) {
              if(jqXHR.status != 404) {
                app.notify({
                  "title": "Can't Sync Local Tracks",
                  "text": "The client is unable to sync local track information with the server.  You should rejoin the session to ensure a good experience.",
                  "icon_url": "/assets/content/icon_alert_big.png"
                });
              }
              else {
                logger.debug("Unable to sync local tracks because session is gone.")
              }

            })
        }

        function onWebsocketDisconnected(in_error) {
          // kill the streaming of the session immediately
          if(currentSessionId) {
            logger.debug("onWebsocketDisconnect: calling jamClient.LeaveSession for clientId=" + clientId);
            client.LeaveSession({ sessionID: currentSessionId });
          }
        }

        // returns a deferred object
        function findUserBy(finder) {
            if(finder.clientId) {
                var foundParticipant = null;
                $.each(participants(), function(index, participant) {
                    if(participant.client_id == finder.clientId) {
                        foundParticipant = participant;
                        return false;
                    }
                });

                if(foundParticipant) {
                    return $.Deferred().resolve(foundParticipant.user).promise();
                }
            }

            // TODO: find it via some REST API if not found?
            return $.Deferred().reject().promise();
        }

        function onDeadUserRemove(type, text) {
          if(!inSession()) return;
          var clientId = text;
          var participant = participantsEverSeen[clientId];
          if(participant) {
            app.notify({
              "title": ALERT_TYPES[type].title,
              "text": participant.user.name + " is no longer sending audio.",
              "icon_url": context.JK.resolveAvatarUrl(participant.user.photo_url)
            });
            var $track = $('div.track[client-id="' + clientId + '"]');
            $('.disabled-track-overlay', $track).show();
          }
        }

        function onWindowBackgrounded(type, text) {
          app.user()
            .done(function(userProfile) {
              if(userProfile.show_whats_next &&
                window.location.pathname.indexOf(gon.client_path) == 0 &&
                !app.layout.isDialogShowing('getting-started')) {
                app.layout.showDialog('getting-started');
              }
            })

          if(!inSession()) return;

          // the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen
          if(sessionScreen) {
            sessionScreen.setPromptLeave(false);
            context.location = '/client#/home'
          }
        }

        function onBroadcastSuccess(type, text) {
          logger.debug("SESSION_LIVEBROADCAST_ACTIVE alert. reason:" + text);

          if(currentSession && currentSession.mount) {
            rest.createSourceChange({
              mount_id: currentSession.mount.id,
                source_direction: true,
              success: true,
              reason: text,
              client_id: app.clientId
            })
          }
          else {
            logger.debug("unable to report source change because no mount seen on session")
          }
        }

        function onBroadcastFailure(type, text) {
          logger.debug("SESSION_LIVEBROADCAST_FAIL alert. reason:" + text);

          if(currentSession && currentSession.mount) {
            rest.createSourceChange({
              mount_id: currentSession.mount.id,
              source_direction: true,
              success: false,
              reason: text,
              client_id: app.clientId
            })
          }
          else {
            logger.debug("unable to report source change because no mount seen on session")
          }
        }

        function onBroadcastStopped(type, text) {
          logger.debug("SESSION_LIVEBROADCAST_STOPPED alert. reason:" + text);

          if(currentSession && currentSession.mount) {
            rest.createSourceChange({
              mount_id: currentSession.mount.id,
              source_direction: false,
              success: true,
              reason: text,
              client_id: app.clientId
            })
          }
          else {
            logger.debug("unable to report source change because no mount seen on session")
          }
        }

        function onPlaybackStateChange(type, text) {

          // if text is play_start or play_stop, tell the play_controls

          if(sessionScreen) {
            sessionScreen.onPlaybackStateChange(text);
          }
        }
        function onBackendMixerChanged(type, text) {
          logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text);

          if(inSession() && text == "RebuildAudioIoControl") {

            // the backend will send these events rapid-fire back to back.
            // the server can still perform correctly, but it is nicer to wait 100 ms to let them all fall through
            if(backendMixerAlertThrottleTimer) {clearTimeout(backendMixerAlertThrottleTimer);}

            backendMixerAlertThrottleTimer = setTimeout(function() {

              if(sessionPageEnterDeferred) {
                // this means we are still waiting for the BACKEND_MIXER_CHANGE that indicates we have user tracks built-out/ready

                // we will get at least one BACKEND_MIXER_CHANGE that corresponds to the backend doing a 'audio pause', which won't matter much
                // so we need to check that we actaully have userTracks before considering ourselves done
                var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
                if(inputTracks.length > 0) {
                  logger.debug("obtained tracks at start of session")
                  sessionPageEnterDeferred.resolve(inputTracks);
                  sessionPageEnterDeferred = null;
                }

                return;
              }

              // wait until we are fully in session before trying to sync tracks to server
              if(joinDeferred) {
                joinDeferred.done(function() {
                  syncTracks();
                })
              }

            }, 100);
          }
          else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) {

            var allTracks = context.JK.TrackHelpers.getTrackInfo(context.jamClient);
            var backingTracks = allTracks.backingTracks;
            var previousBackingTracks = previousAllTracks.backingTracks;
            var metronomeTracks = allTracks.metronomeTracks;
            var previousMetronomeTracks = previousAllTracks.metronomeTracks;

            // the way we know if backing tracks changes, or recordings are opened, is via this event.
            // but we want to report to the user when backing tracks change; so we need to detect change on our own
            if(!(previousBackingTracks.length == 0 && backingTracks.length == 0) && previousBackingTracks != backingTracks) {
              logger.debug("backing tracks changed", previousBackingTracks, backingTracks)
              syncTracks(allTracks);
            }
            else if(!(previousMetronomeTracks.length == 0 && metronomeTracks.length == 0) && previousMetronomeTracks != metronomeTracks) {
              logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks)
              syncTracks(allTracks);
            }
            else {
              refreshCurrentSession(true);
            }

            previousAllTracks = allTracks;
          }
          else if(inSession() && (text == 'Global Peer Input Mixer Mode')) {
            setMixerMode(MIX_MODES.MASTER);
          }
          else if(inSession() && (text == 'Local Peer Stream Mixer Mode')) {
            setMixerMode(MIX_MODES.PERSONAL);
          }
        }

        // Public interface
        this.id = id;
        this.start = start;
        this.backingTrack = backingTrack;
        this.backingTracks = backingTracks;
        this.recordedBackingTracks = recordedBackingTracks;
        this.recordedJamTracks = recordedJamTracks;
        this.setUserTracks = setUserTracks;
        this.recordedTracks = recordedTracks;
        this.jamTrackName = jamTrackName;
        this.recordedJamTrackName = recordedJamTrackName;
        this.jamTracks = jamTracks;
        this.participants = participants;
        this.joinSession = joinSession;
        this.leaveCurrentSession = leaveCurrentSession;
        this.refreshCurrentSession = refreshCurrentSession;
        this.updateSession = updateSession;
        this.subscribe = subscribe;
        this.participantForClientId = participantForClientId;
        this.isPlayingRecording = isPlayingRecording;
        this.deleteTrack = deleteTrack;
        this.onWebsocketDisconnected = onWebsocketDisconnected;
        this.recordingModel =  recordingModel;
        this.findUserBy = findUserBy;
        this.inSession = inSession;
        this.setMixerMode = setMixerMode;
        this.isMasterMixMode = isMasterMixMode;
        this.isPersonalMixMode = isPersonalMixMode;
        this.getMixMode = getMixMode;
        this.selfOpenedJamTracks = selfOpenedJamTracks;
        this.isMetronomeOpen = isMetronomeOpen;
        this.areControlsLockedForJamTrackRecording = areControlsLockedForJamTrackRecording;
        this.lockControlsforJamTrackRecording = lockControlsforJamTrackRecording;
        this.unlockControlsforJamTrackRecording = unlockControlsforJamTrackRecording;

        // ALERT HANDLERS
        this.onBackendMixerChanged =  onBackendMixerChanged;
        this.onDeadUserRemove = onDeadUserRemove;
        this.onWindowBackgrounded = onWindowBackgrounded;
        this.waitForSessionPageEnterDone = waitForSessionPageEnterDone;
        this.onBroadcastStopped = onBroadcastStopped;
        this.onBroadcastSuccess = onBroadcastSuccess;
        this.onBroadcastFailure = onBroadcastFailure;
        this.onPlaybackStateChange = onPlaybackStateChange;

        this.getCurrentSession = function() {
            return currentSession;
        };
        this.getCurrentOrLastSession = function() {
            return currentOrLastSession;
        };
        this.getParticipant = function(clientId) {
            return participantsEverSeen[clientId]
        };
        this.setBackingTrack = function(backingTrack) {
          openBackingTrack = backingTrack;
        };
        this.getBackingTrack = function() {
          return openBackingTrack;
        };
        this.hasShownAudioMediaMixerHelp = function() {
          return shownAudioMediaMixerHelp;
        }
        this.markShownAudioMediaMixerHelp = function() {
          shownAudioMediaMixerHelp = true;
        }


        // call to report if the current user was able to establish audio with the specified clientID
        this.setAudioEstablished = function(clientId, audioEstablished) {
          var participant = currentParticipants[clientId]
          if(participant) {
            if(participant.client.audio_established === null) {
              participant.client.audio_established = audioEstablished;
              context.stats.write('client.audio_established.' + (audioEstablished ? 'success':  'failure'), {user_id: context.JK.currentUserId, name: context.JK.currentUserName, remote_user_id: participant.server.user.id, remote_name: participant.server.user.name, value: 1})
            }
            else if(participant.client.audio_established === false && audioEstablished === true) {
              // this means the frontend had declared this audio failed, but later says it works.
              // this could mean our threshold of time to wait before audio is considered failed is too low, and needs tweaking
              context.stats.write('client.audio_established.delayed', {user_id: context.JK.currentUserId, name: context.JK.currentUserName, remote_user_id: participant.server.user.id, remote_name: participant.server.user.name, value: 1})
            }
          }
        }

        this.ensureEnded = function() {
          updateCurrentSession(null);
        }

    };

 })(window,jQuery);