(function(context,$) {

    "use strict";

    context.JK = context.JK || {};
    context.JK.SessionScreen = function(app) {
        var logger = context.JK.logger;
        var sessionModel = null;
        var sessionId;
        var tracks = {};
        var myTracks = [];
        var mixers = [];

        var configureTrackDialog;
        var addTrackDialog;
        var addNewGearDialog;

        var screenActive = false;

        var currentMixerRangeMin = null;
        var currentMixerRangeMax = null;

        var lookingForMixersCount = 0;
        var lookingForMixersTimer = null;
        var lookingForMixers = {};

        var RENDER_SESSION_DELAY = 750;  // When I need to render a session, I have to wait a bit for the mixers to be there.

        var defaultParticipant = {
            tracks: [{
                instrument_id: "unknown"
            }],
            user: {
                first_name: 'Unknown',
                last_name: 'User',
                photo_url: null
            }
        };

        // Be sure to copy/extend these instead of modifying in place
        var trackVuOpts = {
            vuType: "vertical",
            lightCount: 13,
            lightWidth: 3,
            lightHeight: 17
        };
        // Must add faderId key to this
        var trackFaderOpts = {
            faderType: "vertical",
            height: 83
        };

        // Recreate ChannelGroupIDs ENUM from C++
        var ChannelGroupIds = {
            "MasterGroup": 0,
            "MonitorGroup": 1,
            "AudioInputMusicGroup": 2,
            "AudioInputChatGroup": 3,
            "MediaTrackGroup": 4,
            "StreamOutMusicGroup": 5,
            "StreamOutChatGroup": 6,
            "UserMusicInputGroup": 7,
            "UserChatInputGroup": 8,
            "PeerAudioInputMusicGroup": 9,
            "PeerMediaTrackGroup": 10
        };

        // recreate eThresholdType enum from MixerDialog.h
        var alert_type = {
            0: {"title": "", "message": ""}, // NO_EVENT,
            1: {"title": "", "message": ""}, // BACKEND_ERROR: generic error - eg P2P message error
            2: {"title": "", "message": ""}, // BACKEND_MIXER_CHANGE, - event that controls have been regenerated
            3: {"title": "", "message": ""}, // PACKET_JTR,
            4: { "title": "Packet Loss", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click here." }, // PACKET_LOSS
            5: {"title": "", "message": ""}, // PACKET_LATE,
            6: {"title": "", "message": ""}, // JTR_QUEUE_DEPTH,
            7: {"title": "", "message": ""}, // NETWORK_JTR,
            8: { "title": "Session Latency", "message": "The latency of your audio device combined with your Internet connection has become high enough to impact your session quality. For troubleshooting tips, click here." }, // NETWORK_PING,
            9: {"title": "", "message": ""}, // BITRATE_THROTTLE_WARN,
            10: { "title": "Low Bandwidth", "message": "The available bandwidth on your network has become too low,and this may impact your audio quality. For troubleshooting tips, click here." }, // BANDWIDTH_LOW
            //IO related events
            11: { "title": "Input Rate", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here." }, // INPUT_IO_RATE
            12: {"title": "", "message": ""}, // INPUT_IO_JTR,
            13: { "title": "Output Rate", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here." }, // OUTPUT_IO_RATE
            14: {"title": "", "message": ""}, // OUTPUT_IO_JTR,
            // CPU load related
            15: { "title": "CPU Utilization High", "message": "The CPU of your computer is unable to keep up with the current processing load, and this may impact your audio quality. For troubleshooting tips, click here." }, // CPU_LOAD
            16: {"title": "", "message": ""}, // DECODE_VIOLATIONS,
            17: {"title": "", "message": ""} // LAST_THRESHOLD
        };


        function beforeShow(data) {
            sessionId = data.id;
            $('#session-mytracks-container').empty();
        }

        function alertCallback(type, text) {
            if (type === 2) { // BACKEND_MIXER_CHANGE
                sessionModel.refreshCurrentSession();
            } else {
                context.setTimeout(function() {
                    app.notify({
                      "title": alert_type[type].title,
                      "text": text,
                      "icon_url": "/assets/content/icon_alert_big.png"
                }); }, 1);
            }
        }

        function afterShow(data) {

            // indicate that the screen is active, so that
            // body-scoped drag handlers can go active
            screenActive = true;

            // Subscribe for callbacks on audio events
            context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback");
            context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback");
            context.jamClient.SessionSetAlertCallback("JK.AlertCallback");

            // If you load this page directly, the loading of the current user
            // is happening in parallel. We can't join the session until the
            // current user has been completely loaded. Poll for the current user
            // before proceeding with session joining.
            function checkForCurrentUser() {
                if (context.JK.userMe) {
                    afterCurrentUserLoaded();
                } else {
                    context.setTimeout(checkForCurrentUser, 100);
                }
            }
            checkForCurrentUser();
        }

        function afterCurrentUserLoaded() {
            logger.debug("afterCurrentUserLoaded");
            // It seems the SessionModel should be a singleton.
            // a client can only be in one session at a time,
            // and other parts of the code want to know at any certain times
            // about the current session, if any (for example, reconnect logic)
            context.JK.CurrentSessionModel = sessionModel = new context.JK.SessionModel(
                context.JK.app,
                context.JK.JamServer,
                context.jamClient
            );

            sessionModel.subscribe('sessionScreen', sessionChanged);
            logger.debug("sessionId=" + sessionId);
            sessionModel.joinSession(sessionId)
                .fail(function(xhr, textStatus, errorMessage) {
                    if(xhr.status == 404) {
                        // we tried to join the session, but it's already gone.  kick user back to join session screen
                        context.window.location = "#/findSession";
                        app.notify(
                            {   title: "Unable to Join Session",
                                text: "The session you attempted to join is over."
                            },
                            { no_cancel: true });
                    }else {
                        app.ajaxError(xhr, textStatus, errorMessage);
                    }
                });
        }

        function beforeHide(data) {
            // track that the screen is inactive, to disable body-level handlers
            screenActive = false;
            sessionModel.leaveCurrentSession()
                .fail(app.ajaxError);
        }

        function sessionChanged() {
            logger.debug("sessionChanged()");

            // TODO - in the specific case of a user changing their tracks using the configureTrack dialog,
            // this event appears to fire before the underlying mixers have updated. I have no event to
            // know definitively when the underlying mixers are up to date, so for now, we just delay slightly.
            // This obviously has the possibility of introducing time-based bugs.
            context.setTimeout(renderSession, RENDER_SESSION_DELAY);
        }

        /**
        * the mixers object is a list. In order to find one by key,
        * you must iterate. Convenience method to locate a particular
        * mixer by id.
        */
        function getMixer(mixerId) {
            var foundMixer = null;
            $.each(mixers, function(index, mixer) {
                if (mixer.id === mixerId) {
                    foundMixer = mixer;
                }
            });
            return foundMixer;
        }

        function renderSession() {
            $('#session-mytracks-container').empty();
            $('.session-track').remove(); // Remove previous tracks
            var $voiceChat = $('#voice-chat');
            $voiceChat.hide();
            _updateMixers();
            _renderTracks();
            _wireTopVolume();
            _wireTopMix();
            _addVoiceChat();
            _initDialogs();
            if ($('.session-livetracks .track').length === 0) {
                $('.session-livetracks .when-empty').show();
            }
        }

        function _initDialogs() {
            logger.debug("Calling _initDialogs");
            configureTrackDialog.initialize();
            addTrackDialog.initialize();
            addNewGearDialog.initialize();
        }

        // Get the latest list of underlying audio mixer channels
        function _updateMixers() {
            var mixerIds = context.jamClient.SessionGetIDs();
            var holder = $.extend(true, {}, {mixers: context.jamClient.SessionGetControlState(mixerIds)});
            mixers = holder.mixers;
            // Always add a hard-coded simplified 'mixer' for the L2M mix
            var l2m_mixer = {
                id: '__L2M__',
                range_low: -80,
                range_high: 20,
                volume_left: context.jamClient.SessionGetMasterLocalMix()
            };
            mixers.push(l2m_mixer);
        }

        // TODO FIXME - This needs to support multiple tracks for an individual
        // client id and group.
        function _mixerForClientId(clientId, groupIds, usedMixers) {
            var foundMixer = null;
            $.each(mixers, function(index, mixer) {
                if (mixer.client_id === clientId) {
                    for (var i=0; i<groupIds.length; i++) {
                        if (mixer.group_id === groupIds[i]) {
                            if (!(mixer.id in usedMixers)) {
                                foundMixer = mixer;
                                return false;
                            }
                        }
                    }
                }
            });
            return foundMixer;
        }

        function _wireTopVolume() {
            var gainPercent = 0;
            var mixerIds = [];
            $.each(mixers, function(index, mixer) {
                if (mixer.group_id === ChannelGroupIds.MasterGroup) {
                    mixerIds.push(mixer.id);
                    gainPercent = percentFromMixerValue(
                        mixer.range_low, mixer.range_high, mixer.volume_left);
                }
                if (mixer.group_id === ChannelGroupIds.MonitorGroup) {
                    mixerIds.push(mixer.id);
                }
            });
            var faderId = mixerIds.join(',');
            $('#volume').attr('mixer-id', faderId);
            var faderOpts = {
                faderId: faderId,
                faderType: "horizontal",
                width: 50,
                style: {
                    "background-image": "none",
                    "background-repeat":"no-repeat",
                    "height": "24px"
                }
            };
            context.JK.FaderHelpers.renderFader("#volume", faderOpts);
            context.JK.FaderHelpers.subscribe(faderId, faderChanged);
            // Visually update fader to underlying mixer start value.
            // Always do this, even if gainPercent is zero.
            context.JK.FaderHelpers.setFaderValue(faderId, gainPercent);
        }

        /**
        * This control has it's own Set/Get methods, so we don't need to
        * line it up with some mixer later. We'll use a special mixer-id value
        * to let us know we're dealing with the mix control.
        */
        function _wireTopMix() {
            var $mixSlider = $('#l2m');
            var l2m_mixer = {
                range_low: -80,
                range_high: 20,
                volume_left: context.jamClient.SessionGetMasterLocalMix()
            };
            // var gainPercent = percentFromMixerValue(
            //     l2m_mixer.range_low, l2m_mixer.range_high, l2m_mixer.volume_left);

            var faderId = '#l2m'; // also the selector for renderFader

            var faderOpts = {
                faderId: faderId,
                faderType: "horizontal",
                width: 70,
                style: {
                    "background-image": "none",
                    "background-repeat":"no-repeat",
                    "height": "24px"
                }
            };
            context.JK.FaderHelpers.renderFader(faderId, faderOpts);
            context.JK.FaderHelpers.subscribe(faderId, l2mChanged);

            // initialize to middle (50%, 0dB) per Peter's request
            context.JK.FaderHelpers.setFaderValue(faderId, 50);
            context.jamClient.SessionSetMasterLocalMix(0);
        }

        /**
        * This has a specialized jamClient call, so custom handler.
        */
        function l2mChanged(faderId, newValue, dragging) {
            var dbValue = context.JK.FaderHelpers.convertLinearToDb(newValue);
            context.jamClient.SessionSetMasterLocalMix(dbValue);
        }

        function _addVoiceChat() {
            // If, and only if, there is a mixer in group 3 (voice chat)
            // Add the voice chat controls below my tracks, and hook up the mixer.
            // Assumption is that there is only ever one, so we just take the first one.
            $.each(mixers, function(index, mixer) {
                if (mixer.group_id === ChannelGroupIds.AudioInputChatGroup) {
                    var $voiceChat = $('#voice-chat');
                    $voiceChat.show();
                    $voiceChat.attr('mixer-id', mixer.id);
                    $('#voice-chat .voicechat-mute').attr('mixer-id', mixer.id);
                    var gainPercent = percentFromMixerValue(
                        mixer.range_low, mixer.range_high, mixer.volume_left);
                    var faderOpts = {
                        faderId: mixer.id,
                        faderType: "horizontal",
                        width: 50
                    };
                    context.JK.FaderHelpers.renderFader("#voice-chat .voicechat-gain", faderOpts);
                    context.JK.FaderHelpers.subscribe(mixer.id, faderChanged);
                    context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent);
                    if (mixer.mute) {
                        var $mute = $voiceChat.find('.voicechat-mute');
                        _toggleVisualMuteControl($mute, true);
                    }
                }
            });
        }

        function _renderTracks() {
            myTracks = [];

            // Participants are here now, but the mixers don't update right away.
            // Draw tracks from participants, then setup timers to look for the
            // mixers that go with those participants, if they're missing.
            lookingForMixersCount = 0;
            $.each(sessionModel.participants(), function(index, participant) {

                var name = participant.user.name;
                if (!(name)) {
                    name = participant.user.first_name + ' ' + participant.user.last_name;
                }

                var usedMixers = {}; // Once we use a mixer, we add it here to allow us to find 'second' tracks

                // loop through all tracks for each participant
                $.each(participant.tracks, function(index, track) {
                    var instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id);
                    var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url);

                    var myTrack = false;

                    // Default trackData to participant + no Mixer state.
                    var trackData = {
                        trackId: track.id,
                        connection_id: track.connection_id,
                        clientId: participant.client_id,
                        name: name,
                        instrumentIcon: instrumentIcon,
                        avatar: photoUrl,
                        latency: "good",
                        gainPercent: 0,
                        muteClass: 'muted',
                        mixerId: ""
                    };

                    // This is the likely cause of multi-track problems.
                    // This should really become _mixersForClientId and return a list.
                    // With multiple tracks, there will be more than one mixer for a
                    // particular client, in a particular group, and I'll need to further
                    // identify by track id or something similar.
                    var mixer = _mixerForClientId(
                        participant.client_id,
                        [
                            ChannelGroupIds.AudioInputMusicGroup,
                            ChannelGroupIds.PeerAudioInputMusicGroup
                        ],
                        usedMixers);
                    if (mixer) {
                        usedMixers[mixer.id] = true;
                        myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup);
                        var gainPercent = percentFromMixerValue(
                            mixer.range_low, mixer.range_high, mixer.volume_left);
                        var muteClass = "enabled";
                        if (mixer.mute) {
                            muteClass = "muted";
                        }
                        trackData.gainPercent = gainPercent;
                        trackData.muteClass = muteClass;
                        trackData.mixerId = mixer.id;
                        context.jamClient.SessionSetUserName(participant.client_id,name);
                    } else { // No mixer to match, yet
                        lookingForMixers[track.id] = participant.client_id;
                        if (!(lookingForMixersTimer)) {
                            lookingForMixersTimer = context.setInterval(lookForMixers, 500);
                        }
                    }

                    _addTrack(index, trackData);

                    // Show settings icons only for my tracks
                    if (myTrack) {
                        var $trackSettings = $('div[mixer-id="' + mixer.id + '"].track-icon-settings');
                        $trackSettings.show();
                        myTracks.push(trackData);
                    }
                    // TODO: UNCOMMENT THIS WHEN TESTING LOCALLY IN BROWSER
                    //myTracks.push(trackData);
                });
            });

            logger.debug("416-myTracks.length=" + myTracks.length);

            configureTrackDialog = new context.JK.ConfigureTrackDialog(app, myTracks, sessionId, sessionModel);
            addTrackDialog = new context.JK.AddTrackDialog(app, myTracks, sessionId, sessionModel);
            addNewGearDialog = new context.JK.AddNewGearDialog(app, ftueCallback);

            // # NO LONGER HIDING ADD TRACK even when there are 2 tracks (VRFS-537)
            $('#div-add-track').click(function() {
                if (myTracks.length === 2) {
                    $('#btn-error-ok').click(function() {
                        app.layout.closeDialog('error-dialog');
                    });
                    context.JK.showErrorDialog(app, "You can only have a maximum of 2 personal tracks per session.", "max # of tracks");
                }
                else {
                    app.layout.showDialog('add-track');
                    addTrackDialog.showDialog();
                }
            });
        }

        function ftueCallback() {
            context.location = "#/home";
            app.layout.showDialog('ftue');
        }

        function connectTrackToMixer(trackSelector, clientId, mixerId, gainPercent) {
            var vuOpts = $.extend({}, trackVuOpts);
            var faderOpts = $.extend({}, trackFaderOpts);
            faderOpts.faderId = mixerId;
            var vuLeftSelector = trackSelector + " .track-vu-left";
            var vuRightSelector = trackSelector + " .track-vu-right";
            var faderSelector = trackSelector + " .track-gain";
            var $track = $(trackSelector);
            // Set mixer-id attributes and render VU/Fader
            context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts);
            $track.find('.track-vu-left').attr('mixer-id', mixerId + '_vul');
            context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts);
            $track.find('.track-vu-right').attr('mixer-id', mixerId + '_vur');
            context.JK.FaderHelpers.renderFader(faderSelector, faderOpts);
            // Set gain position
            context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent);
            context.JK.FaderHelpers.subscribe(mixerId, faderChanged);
        }

        // Function called on an interval when participants change. Mixers seem to
        // show up later, so we render the tracks from participants, but keep track
        // of the ones there weren't any mixers for, and continually try to find them
        // and get them connected to the mixers underneath.
        function lookForMixers() {
            lookingForMixersCount++;
            _updateMixers();
            var usedMixers = {};
            var keysToDelete = [];
            for (var key in lookingForMixers) {
                var clientId = lookingForMixers[key];
                var mixer = _mixerForClientId(
                    clientId,
                    [
                        ChannelGroupIds.AudioInputMusicGroup,
                        ChannelGroupIds.PeerAudioInputMusicGroup
                    ],
                    usedMixers);
                if (mixer) {
                    usedMixers[mixer.id] = true;
                    keysToDelete.push(key);
                    var gainPercent = percentFromMixerValue(
                        mixer.range_low, mixer.range_high, mixer.volume_left);
                    var trackSelector = 'div.track[track-id="' + key + '"]';
                    connectTrackToMixer(trackSelector, key, mixer.id, gainPercent);
                    var $track = $('div.track[client-id="' + key + '"]');
                    $track.find('.track-icon-mute').attr('mixer-id', mixer.id);
                    $track.find('.track-icon-settings').attr('mixer-id', mixer.id);
                    // Set mute state
                    _toggleVisualMuteControl($track.find('.track-icon-mute'), mixer.mute);
                }
            }

            for (var i=0; i<keysToDelete.length; i++) {
                delete lookingForMixers[keysToDelete[i]];
            }

            if (context.JK.dlen(lookingForMixers) === 0 ||
               lookingForMixersCount > 20) {
                lookingForMixersCount = 0;
                lookingForMixers = {};
                context.clearTimeout(lookingForMixersTimer);
                lookingForMixersTimer = null;
            }
        }

        // Given a mixerID and a value between 0.0-1.0,
        // light up the proper VU lights.
        function _updateVU(mixerId, value) {

            // Special-case for mono tracks. If mono, and it's a _vul id,
            // update both sides, otherwise do nothing.
            // If it's a stereo track, just do the normal thing.
            var selector;
            var pureMixerId = mixerId.replace("_vul", "");
            pureMixerId = pureMixerId.replace("_vur", "");
            var mixer = getMixer(pureMixerId);
            if (mixer) {
                if (!(mixer.stereo)) { // mono track
                    if (mixerId.substr(-4) === "_vul") {
                        // Do the left
                        selector = '#tracks [mixer-id="' + pureMixerId + '_vul"]';
                        context.JK.VuHelpers.updateVU(selector, value);
                        // Do the right
                        selector = '#tracks [mixer-id="' + pureMixerId + '_vur"]';
                        context.JK.VuHelpers.updateVU(selector, value);
                    } // otherwise, it's a mono track, _vur event - ignore.
                } else { // stereo track
                    selector = '#tracks [mixer-id="' + mixerId + '"]';
                    context.JK.VuHelpers.updateVU(selector, value);
                }
            }
        }

        function _addTrack(index, trackData) {
            var parentSelector = '#session-mytracks-container';
            var $destination = $(parentSelector);
            if (trackData.clientId !== app.clientId) {
                parentSelector = '#session-livetracks-container';
                $destination = $(parentSelector);
                $('.session-livetracks .when-empty').hide();
            }
            var template = $('#template-session-track').html();
            var newTrack = context.JK.fillTemplate(template, trackData);
            $destination.append(newTrack);

            // Render VU meters and gain fader
            var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]';
            var gainPercent = trackData.gainPercent || 0;
            connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent);

            var $closeButton = $('#div-track-close', 'div[track-id="' + trackData.trackId + '"]');
            if (index === 0) {
                $closeButton.hide();
            }
            else {
                $closeButton.click(deleteTrack);
            }

            var $trackSettings = $('div[mixer-id="' + trackData.mixerId + '"].track-icon-settings');
            $trackSettings.click(function() {
                // call this to initialize Voice Chat tab
                configureTrackDialog.showVoiceChatPanel(true);
                configureTrackDialog.showMusicAudioPanel(true);
            });

            tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
        }

        /**
        * Will be called when fader changes. The fader id (provided at subscribe time),
        * the new value (0-100) and whether the fader is still being dragged are passed.
        */
        function faderChanged(faderId, newValue, dragging) {
            var mixerIds = faderId.split(',');
            $.each(mixerIds, function(i,v) {
                var broadcast = !(dragging);  // If fader is still dragging, don't broadcast
                fillTrackVolumeObject(v, broadcast);
                setMixerVolume(v, newValue);
            });
        }

        function handleVolumeChangeCallback(mixerId, isLeft, value) {
            // Visually update mixer
            // There is no need to actually set the back-end mixer value as the
            // back-end will already have updated the audio mixer directly prior to sending
            // me this event. I simply need to visually show the new fader position.
            // TODO: Use mixer's range
            var faderValue = percentFromMixerValue(-80, 20, value);
            context.JK.FaderHelpers.setFaderValue(mixerId, faderValue);
        }

        function handleBridgeCallback() {
            var eventName = null;
            var mixerId = null;
            var value = null;
            var tuples = arguments.length / 3;
            for (var i=0; i<tuples; i++) {
                eventName = arguments[3*i];
                mixerId = arguments[(3*i)+1];
                value = arguments[(3*i)+2];
                var vuVal = 0.0;
                if (eventName === 'left_vu' || eventName === 'right_vu') {
                    // TODO - no guarantee range will be -80 to 20. Get from the
                    // GetControlState for this mixer which returns min/max
                    // value is a DB value from -80 to 20. Convert to float from 0.0-1.0
                    vuVal = (value + 80) / 100;
                    if (eventName === 'left_vu') {
                        mixerId = mixerId + "_vul";
                    } else {
                        mixerId = mixerId + "_vur";
                    }
                    _updateVU(mixerId, vuVal);
                } else if (eventName === 'add' || eventName === 'remove') {
                    //logger.dbg('non-vu event: ' + eventName + ',' + mixerId + ',' + value);

                    // TODO - _renderSession. Note I get streams of these in
                    // sequence, so have Nat fix, or buffer/spam protect
                    // Note - this is already handled from websocket events.
                    // However, there may be use of these two events to avoid
                    // the polling-style check for when a mixer has been added
                    // to match a participant track.
                } else {
                    // Examples of other events
                    // Add media file track: "add", "The_Abyss_4T", 0
                    logger.debug('non-vu event: ' + eventName + ',' + mixerId + ',' + value);
                }
            }
        }

        function deleteSession(evt) {
            var sessionId = $(evt.currentTarget).attr("action-id");
            if (sessionId) {
                $.ajax({
                    type: "DELETE",
                    url: "/api/sessions/" + sessionId,
                    success: function(response) {
                        context.location="#/home";
                    },
                    error: function(jqXHR, textStatus, errorThrown) {
                        logger.error("Error deleting session " + sessionId);
                    }
                });
            }
        }

        function deleteTrack(evt) {
            var trackId = $(evt.currentTarget).attr("track-id");
            sessionModel.deleteTrack(sessionId, trackId);
            logger.debug("643-myTracks.length=" + myTracks.length);
        }

        function _toggleVisualMuteControl($control, muting) {
            if (muting) {
                $control.removeClass('enabled');
                $control.addClass('muted');
            } else {
                $control.removeClass('muted');
                $control.addClass('enabled');
            }
        }

        function _toggleAudioMute(mixerId, muting) {
            fillTrackVolumeObject(mixerId);
            context.trackVolumeObject.mute = muting;
            context.jamClient.SessionSetControlState(mixerId);
        }

        function toggleMute(evt) {
            var $control = $(evt.currentTarget);
            var muting = ($control.hasClass('enabled'));
            var mixerIds = $control.attr('mixer-id').split(',');
            $.each(mixerIds, function(i,v) {
                _toggleAudioMute(v, muting);
            });
            _toggleVisualMuteControl($control, muting);
        }

        function fillTrackVolumeObject(mixerId, broadcast) {
            _updateMixers();
            var mixer = null;
            var _broadcast = true;
            if (broadcast !== undefined) {
                _broadcast = broadcast;
            }
            for (var i=0; i<mixers.length; i++) {
                mixer = mixers[i];
                if (mixer.id === mixerId) {
                    context.trackVolumeObject.clientID = mixer.client_id;
                    context.trackVolumeObject.broadcast = _broadcast;
                    context.trackVolumeObject.master = mixer.master;
                    context.trackVolumeObject.monitor = mixer.monitor;
                    context.trackVolumeObject.mute = mixer.mute;
                    context.trackVolumeObject.name = mixer.name;
                    context.trackVolumeObject.record = mixer.record;
                    context.trackVolumeObject.volL = mixer.volume_left;
                    context.trackVolumeObject.volR = mixer.volume_right;
                    // trackVolumeObject doesn't have a place for range min/max
                    currentMixerRangeMin = mixer.range_low;
                    currentMixerRangeMax = mixer.range_high;
                    break;
                }
            }
        }

        // Given a mixer's min/max and current value, return it as
        // a percent from 0-100. Return an integer.
        function percentFromMixerValue(min, max, value) {
            try {
                var range = Math.abs(max - min);
                var magnitude = value - min;
                var percent = Math.round(100*(magnitude/range));
                return percent;
            } catch(err) {
                return 0;
            }
        }

        // Given a mixer's min/max and a percent value, return it as
        // the mixer's value. Returns an integer.
        function percentToMixerValue(min, max, percent) {
            var range = Math.abs(max - min);
            var multiplier = percent/100; // Change 85 into 0.85
            var value = min + (multiplier * range);
            // Protect against percents < 0 and > 100
            if (value < min) {
                value = min;
            }
            if (value > max) {
                value = max;
            }
            return value;
        }

        // Given a volume percent (0-100), set the underlying
        // audio volume level of the passed mixerId to the correct
        // value.
        function setMixerVolume(mixerId, volumePercent) {
            // The context.trackVolumeObject has been filled with the mixer values
            // that go with mixerId, and the range of that mixer
            // has been set in currentMixerRangeMin-Max.
            // All that needs doing is to translate the incoming percent
            // into the real value ont the sliders range. Set Left/Right
            // volumes on trackVolumeObject, and call SetControlState to stick.
            var sliderValue = percentToMixerValue(
                currentMixerRangeMin, currentMixerRangeMax, volumePercent);
            context.trackVolumeObject.volL = sliderValue;
            context.trackVolumeObject.volR = sliderValue;
            // Special case for L2M mix:
            if (mixerId === '__L2M__') {
                logger.debug("L2M volumePercent=" + volumePercent);
                var dbValue = context.JK.FaderHelpers.convertLinearToDb(volumePercent);
                context.jamClient.SessionSetMasterLocalMix(dbValue);
                // context.jamClient.SessionSetMasterLocalMix(sliderValue);
            } else {
                context.jamClient.SessionSetControlState(mixerId);
            }
        }

        function sessionResync(evt) {
            evt.preventDefault();
            var response = context.jamClient.SessionAudioResync();
            if (response) {
                app.notify({
                  "title": "Error",
                  "text": response,
                  "icon_url": "/assets/content/icon_alert_big.png"});
            }
            return false;
        }

        function events() {
            $('#session-resync').on('click', sessionResync);
            $('#session-contents').on("click", '[action="delete"]', deleteSession);
            $('#tracks').on('click', 'div[control="mute"]', toggleMute);

            $('.voicechat-settings').click(function() {
                // call this to initialize Music Audio tab
                configureTrackDialog.showMusicAudioPanel(true);
                configureTrackDialog.showVoiceChatPanel(true);
            });
        }

        this.initialize = function() {
            context.jamClient.SetVURefreshRate(150);
            events();
            var screenBindings = {
                'beforeShow': beforeShow,
                'afterShow': afterShow,
                'beforeHide': beforeHide
            };
            app.bindScreen('session', screenBindings);
        };

        this.tracks = tracks;

        this.getCurrentSession = function() {
            return sessionModel.getCurrentSession();
        };

        this.refreshCurrentSession = function() {
            sessionModel.refreshCurrentSession();
        };

        context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback;
        context.JK.HandleBridgeCallback = handleBridgeCallback;
        context.JK.AlertCallback = alertCallback;

    };

 })(window,jQuery);