/**
 * @file Application controller.
 * @copyright Chad Phillips 2016
 * @module Controller
 */

define([
  "openhangout.logging",
  "faye",
  "js.cookie",
  "fullscreen",
  "jquery.hotkeys",
  "openhangout.plugin.room.licode",
  "openhangout.plugin.room.janus",
  "openhangout.plugin.room.janus-freeswitch",
], function(logging, Faye, Cookies) {

  var ControllerFactory = function(RoomApp) {
    RoomApp.module("Controller", function(MODULE, APP, Backbone, Marionette, $, _) {

      var INSTANCES = {};

      /**
       *
       * Create a new instance of the room app.
       *
       * @constructor
       * @alias Controller.makeInstance
       *
       * @param {string} name
       *   Name for the instance.
       */
      MODULE.makeInstance = function(name) {
        name = name ? name : 'default';
        if (INSTANCES[name]) {
          return INSTANCES[name];
        }
        else {
          var instance = makeNewInstance(name);
          return instance;
        }
      }

      var makeNewInstance = function(name) {

        var log = logging(name);

        var INSTANCE = {};
        INSTANCE.inited = false;
        INSTANCE._instance = name;
        INSTANCE.EventBus = Backbone.Wreqr.radio.channel('openhangout-' + name);
        INSTANCE.Vent = INSTANCE.EventBus.vent;
        INSTANCE.reqres = new Backbone.Wreqr.RequestResponse();

        INSTANCE.Audio = APP.Audio.makeInstance(name);

        INSTANCE.settings = {};
        INSTANCE.configuration = _.extend({}, APP.Constants);

        INSTANCE.RegionManager = new Marionette.RegionManager();
        INSTANCE.Regions = INSTANCE.RegionManager.addRegions({
          headerButtons: '#header-buttons',
          fullscreenButtons: '#fullscreen-buttons',
          chat: '#chat',
          feedSelector: '#multiselect-feed-selector-container',
        });
        INSTANCE.refreshRegions = function() {
          INSTANCE.Regions = INSTANCE.RegionManager.getRegions();
        }

        INSTANCE.getLocalAudioMuted = function() {
          return INSTANCE.AppState.get('localAudioMuted');
        }

        INSTANCE.setLocalAudioMuted = function(muted) {
          INSTANCE.AppState.set('localAudioMuted', muted);
        }

        INSTANCE.muteIfLocalUser = function(userId) {
          if (userId == INSTANCE.settings.userId) {
            var muted = INSTANCE.getLocalAudioMuted();
            if (!muted) {
              log.debug('AUDIO', 'setting localAudioMuted for local user');
              INSTANCE.setLocalAudioMuted(true);
            }
          }
        };

        INSTANCE.changeLocalAudioMute = function() {
          var muted = INSTANCE.getLocalAudioMuted();
          var result = INSTANCE.plugin.room.changeLocalAudioMute(muted);
          // Verify that all streams are active before allowing button press.
          if (result) {
            INSTANCE.changeAudioMute(INSTANCE.settings.userId, muted);
            if (muted) {
              INSTANCE.setVolumeToSilence();
            }
            INSTANCE.volumeEventsEnabled = !muted;
            INSTANCE.Vent.trigger('audio:mute:state', muted);
          }
        }

        INSTANCE.mediaUp = function(userId) {
          var videoFeed = INSTANCE.getUserVideoFeed(userId);
          if (videoFeed) {
            log.debug('FEED', 'audio and video up for feed:', userId);
            videoFeed.set('mediaUp', true);
          }
        }

        INSTANCE.changeAudioMute = function(userId, muted) {
          var audioFeed = INSTANCE.AudioFeeds.get(userId);
          var videoFeed = INSTANCE.getUserVideoFeed(userId);
          if (audioFeed || videoFeed) {
            log.debug('AUDIO', 'setting isAudioMuted for feed:', userId, muted);
            audioFeed && audioFeed.set('isAudioMuted', muted);
            videoFeed && videoFeed.set('isAudioMuted', muted);
          }
        }

        INSTANCE.getLocalVideoMuted = function() {
          return INSTANCE.AppState.get('localVideoMuted');
        }

        INSTANCE.setLocalVideoMuted = function(muted) {
          INSTANCE.AppState.set('localVideoMuted', muted);
        }

        INSTANCE.changeLocalVideoMute = function() {
          var muted = INSTANCE.getLocalVideoMuted();
          var result = INSTANCE.plugin.room.changeLocalVideoMute(muted);
          if (result) {
            INSTANCE.changeVideoMute(INSTANCE.settings.userId, muted);
            INSTANCE.Vent.trigger('video:mute:state', muted);
          }
        }

        INSTANCE.changeVideoMute = function(userId, muted) {
          var audioFeed = INSTANCE.AudioFeeds.get(userId);
          var videoFeed = INSTANCE.getUserVideoFeed(userId);
          if (audioFeed || videoFeed) {
            log.debug('VIDEO', 'setting isVideoMuted for feed:', userId, muted);
            audioFeed && audioFeed.set('isVideoMuted', muted);
            videoFeed && videoFeed.set('isVideoMuted', muted);
          }
        }

        INSTANCE.clearFocusUser = function(feed) {
          log.debug('AUTO FOCUS', 'clearing focus user');
          var attrs = {
            active: false,
            hasFocus: false,
          };
          INSTANCE.VideoFeeds.each(function(f) {
            if (f.id !== feed.id) {
              f.set(attrs);
            }
          });
        }

        INSTANCE.clearFloorUser = function(feed) {
          log.debug('FLOOR', 'clearing users');
          var attrs = {
            hasFloor: false,
          };
          INSTANCE.VideoFeeds.each(function(f) {
            if (f.id !== feed.id) {
              f.set(attrs);
            }
          });
        }

        INSTANCE.userSetFocusUser = function(feed) {
          INSTANCE.clearFocusUser(feed);
          // Examine state just prior to flipping.
          var autoFocus;
          var active = feed.get('active');
          if (active) {
            autoFocus = true;
          }
          else {
            autoFocus = false;
            feed.set('hasFocus', true);
          }
          log.debug('USER FOCUS', 'set focus user, active:', !active, 'autoFocus:', autoFocus);
          feed.set('active', !active);
          INSTANCE.setFloorToFocus();
          INSTANCE.AppState.set('autoFocus', autoFocus);
        }
        INSTANCE.Vent.on('focus:user', INSTANCE.userSetFocusUser);

        INSTANCE.findFloorUser = function() {
          var feed = INSTANCE.VideoFeeds.findWhere({hasFloor: true});
          return feed;
        }

        INSTANCE.findFocusUser = function() {
          var feed = INSTANCE.VideoFeeds.findWhere({hasFocus: true});
          return feed;
        }

        INSTANCE.clearAllFocusUsers = function() {
          log.debug('AUTO FOCUS', 'clearing all focus users');
          INSTANCE.clearFocusUser({});
          INSTANCE.AppState.set('autoFocus', true);
          INSTANCE.setFloorToFocus();
        }
        INSTANCE.Vent.on('focus:user:clear-all', INSTANCE.clearAllFocusUsers);

        INSTANCE.getView = function(view) {
          return INSTANCE.AppState.get('view');
        };

        INSTANCE.setView = function(view) {
          Cookies.set('view', view, { expires: 1 });
          INSTANCE.AppState.set('view', view);
        };

        INSTANCE.switchView = function(newView) {
          if (INSTANCE.getSubscriberMode() == 'single') {
            return;
          }
          if (!newView) {
            var oldView = INSTANCE.getView();
            newView = oldView == 'tile' ? 'focus' : 'tile';
          }
          log.debug('VIEW', 'switch view to:', newView);
          INSTANCE.setView(newView);
        };
        INSTANCE.Vent.on('view:switch', INSTANCE.switchView);

        INSTANCE.setChatDisplay = function(isOpen) {
          INSTANCE.AppState.set('chat', isOpen);
          INSTANCE.AppState.set('chatUnread', 0);
          log.debug('CHAT', 'set chat display:', isOpen);
          INSTANCE.resizeLayout();
        }

        INSTANCE.toggleChat = function() {
          INSTANCE.setChatDisplay(!INSTANCE.AppState.get('chat'));
        };
        INSTANCE.Vent.on('chat:toggle', INSTANCE.toggleChat);

        INSTANCE.setFocusCropDisplay = function(crop) {
          INSTANCE.AppState.set('focusCrop', crop);
          log.debug('VIEW', 'set focus crop:', crop);
          INSTANCE.resizeLayout();
        }

        INSTANCE.toggleFocusCrop = function() {
          INSTANCE.setFocusCropDisplay(!INSTANCE.AppState.get('focusCrop'));
        };
        INSTANCE.Vent.on('view:focus:crop', INSTANCE.toggleFocusCrop);

        INSTANCE.openChat = function() {
          var isOpen = INSTANCE.AppState.get('chat');
          if (!isOpen) {
            INSTANCE.setChatDisplay(true);
          }
        };
        INSTANCE.Vent.on('chat:open', INSTANCE.openChat);

        INSTANCE.toggleLocalAudioMute = function() {
          INSTANCE.setLocalAudioMuted(!INSTANCE.getLocalAudioMuted());
        };
        INSTANCE.Vent.on('audio:local:toggle', INSTANCE.toggleLocalAudioMute);

        INSTANCE.toggleLocalVideoMute = function() {
          INSTANCE.setLocalVideoMuted(!INSTANCE.getLocalVideoMuted());
        };
        INSTANCE.Vent.on('video:local:toggle', INSTANCE.toggleLocalVideoMute);

        INSTANCE.muteUser = function(model) {
          // For now, only allow muting other users for publishers, doesn't
          // seem to make sense to allow someone only listening to do that.
          if (INSTANCE.role == 'publisher') {
            var data = {
              type: 'mute-user',
              userId: model.id,
            }
            log.debug('AUDIO', 'broadcast mute user:', data.userId);
            INSTANCE.RoomSocket.publish('/' + INSTANCE.settings.roomName + '/command', data);
          }
        };
        INSTANCE.Vent.on('audio:mute:user', INSTANCE.muteUser);

        INSTANCE.refreshFeedsUser = function(model) {
          log.info('FEED', 'refresh feeds user:', model.id);
          var postUnsubscribeCallback = function() {
            INSTANCE.removeUserFeeds(model.id);
          }
          INSTANCE.plugin.room.resubscribeStreams(model.id, postUnsubscribeCallback);
        };
        INSTANCE.Vent.on('refresh:feeds:user', INSTANCE.refreshFeedsUser);

        INSTANCE.setFullscreenListener = function() {
          var listener = function(jqEvt, active, el) {
            INSTANCE.setFullscreen(active);
          }
          $(document).on('fscreenclose', listener);
        };

        INSTANCE.fullscreenStateChanged = function(fullscreen) {
        }

        INSTANCE.getFullscreenState = function() {
          var fullscreen = $.fullscreen.isFullScreen();
          return fullscreen;
        };

        INSTANCE.setFullscreen = function(active) {
          INSTANCE.AppState.set('fullscreen', active);
          INSTANCE.Vent.trigger('fullscreen:state', active);
        };

        INSTANCE.toggleFullscreen = function() {
          INSTANCE.setFullscreen(!INSTANCE.getFullscreenState());
        };
        INSTANCE.Vent.on('fullscreen:toggle', INSTANCE.toggleFullscreen);

        INSTANCE.hangupConnection = function() {
          // NOTE: This will fail unless the window was originally opened by the
          // user via javascript.
          log.info('APP', 'hangup');
          window.close();
        };
        INSTANCE.Vent.on('connection:hangup', INSTANCE.hangupConnection);

        INSTANCE.reloadConnection = function() {
          log.info('APP', 'reload');
          location.reload();
        };
        INSTANCE.Vent.on('connection:reload', INSTANCE.reloadConnection);

        // TODO: Remove this if mulitple gUM requests are ever allowed on iOS.
        INSTANCE.iOSLocalVideoHack = function(data) {
          var localFeed = INSTANCE.VideoFeeds.get(INSTANCE.settings.userId);
          if (localFeed && INSTANCE.isiOSLocalSubscribeFeed(data.attributes.userId) && !localFeed.get('iOSLocalVideoHack')) {
            log.info('FEED', 'activating iOS local video hack');
            localFeed.set('iOSLocalVideoHack', data);
          }
        };
        INSTANCE.Vent.on('plugin:room:janus:video:subscribed', INSTANCE.iOSLocalVideoHack);

        INSTANCE.addChatToMessages = function(userId, data) {
          var audioFeed = INSTANCE.AudioFeeds.get(userId);
          var videoFeed = INSTANCE.findVisibleVideoFeed(userId);
          if (audioFeed) {
            var chatData = {
              username: data.username,
              message: data.message,
              feed: audioFeed,
            }
            if (videoFeed.get('multiselect')) {
              chatData.displayUsernameInPopover = true;
            }
            log.debug('CHAT', 'adding chat to messages, username:', chatData.username, 'message:', chatData.message);
            var model = new APP.Model.ChatMessage(chatData);
            INSTANCE.ChatMessages.add(model);
            INSTANCE.storeChatMessage(model);
            var chatIsOpen = INSTANCE.AppState.get('chat');
            if (data.userId != INSTANCE.settings.userId) {
              if (INSTANCE.configuration.chatAlwaysShowPopups || !chatIsOpen) {
                videoFeed && videoFeed.set('chatPopoverMessage', model);
              }
            }
            if (!chatIsOpen) {
              var unread = INSTANCE.AppState.get('chatUnread');
              unread++;
              INSTANCE.AppState.set('chatUnread', unread);
            }
          }
        };

        INSTANCE.sendChatMessage = function(message) {
          var data = {
            userId: INSTANCE.settings.userId,
            username: INSTANCE.settings.username,
            message: message,
          }
          log.debug('CHAT', 'broadcasting chat message, username:', data.username, 'message:', data.message);
          INSTANCE.RoomSocket.publish('/' + INSTANCE.settings.roomName + '/chat', data);
        };

        INSTANCE.Vent.on('chat:message:send', INSTANCE.sendChatMessage);

        /**
         * Configure the application prior to connecting.
         *
         * @param {object} settings
         *   Settings to specify the user, published streams, etc.
         * @param {object} configuration
         *   Override core configuration values.
         */
        INSTANCE.configure = function(settings, configuration) {
          configuration = configuration ? configuration : {};
          log.info('APP', 'configuring with settings', settings);
          INSTANCE.settings = settings;
          _.extend(INSTANCE.configuration, configuration);
        };

        INSTANCE.initApp = function() {
          if (_.isObject(INSTANCE.settings)) {
            log.info('APP', 'init');
            INSTANCE.initAppState();
            INSTANCE.initPlugins();
            INSTANCE.initRoomSocket();
            INSTANCE.initUserInfo();
            INSTANCE.initDisplay();
            INSTANCE.initChat();
            INSTANCE.initFeeds();
            if (INSTANCE.getSubscriberMode() == 'single') {
              INSTANCE.initFeedSelector();
            }
            INSTANCE.initAudioView();
            if (INSTANCE.getSubscriberMode() == 'multiple') {
              INSTANCE.initTileView();
            }
            INSTANCE.initFilmstripLayoutView();
            INSTANCE.initFocusView();
            INSTANCE.initLayoutView();
            INSTANCE.configLocalAudioStream();
            INSTANCE.configLocalDisplayStream();
            INSTANCE.configLocalPublishedVideoStreams();
            INSTANCE.connect('publisher');
            INSTANCE.inited = true;
            if (INSTANCE.configuration.makeFakeFilmstripsCount > 0) {
              INSTANCE.addFakeVideoFeeds(INSTANCE.configuration.makeFakeFilmstripsCount, INSTANCE.configuration.makeFakeFilmstripsDelay);
            }
          }
          else {
            var message = "ERROR: missing application-level settings, call configure() to configure";
            log.error('APP', message);
            throw(message);
          }
        };

        INSTANCE.buildRoomPluginConfigs = function() {
          var subscribeStreamGroup = INSTANCE.settings.subscribeStreamGroup || INSTANCE.configuration.streamGroupDefault;
          var configs = {
            roomName: INSTANCE.settings.roomName,
            subscribeStreamGroup: subscribeStreamGroup,
          };
          if (INSTANCE.localAudioStreamConfig) {
            configs.audio = INSTANCE.localAudioStreamConfig;
          }
          if (INSTANCE.localDisplayStreamConfig) {
            configs.displayVideo = INSTANCE.localDisplayStreamConfig;
          }
          if (INSTANCE.localPublishedVideoStreamsConfig) {
            configs.publishedVideo = INSTANCE.localPublishedVideoStreamsConfig;
          }
          return configs;
        }

        INSTANCE.connect = function(role) {
          INSTANCE.role = role ? role : 'publisher';
          var configs = INSTANCE.buildRoomPluginConfigs();
          INSTANCE.plugin.room.connect(INSTANCE.role, configs);
        }

        INSTANCE.connectError = function(err) {
          var errorType = err.type;
          var errorMessage = err.message;
          var mediaError = err.mediaError;
          switch(errorType) {
            case 'stream':
              var message = "Error accessing local mic/camera: ";
              var name = 'unknown';
              var constraintName = 'unknown';
              var mediaErrorMessage;
              try {
                name = mediaError.name || name;
                constraintName = _.isUndefined(mediaError.constraint) ? constraintName : mediaError.constraint;
                mediaErrorMessage = name + ": " + constraintName;
              }
              catch (e) {
                mediaError = mediaError || 'unknown reason';
                mediaErrorMessage = mediaError;
              }
              log.error('STREAM', message, errorMessage, mediaError);
              alert(message + errorMessage + ", " + mediaErrorMessage);
              break;
            case 'room':
              alert(errorMessage);
              break;
          }
          //INSTANCE.reloadConnection();
        }

        // TODO: Implement this by blocking the UI while perms are being
        // requested.
        INSTANCE.consentDialog = function(open) {
        }

        INSTANCE.disconnect = function() {
          INSTANCE.disconnectRoom();
          INSTANCE.disconnectRoomSocket();
        }

        INSTANCE.roomDisconnected = function() {
          INSTANCE.Audio.teardown();
          INSTANCE.VideoFeeds.reset([]);
          INSTANCE.AudioFeeds.reset([]);
          INSTANCE.Room = null;
          INSTANCE.Vent.trigger('room:disconnected');
        }

        INSTANCE.getHeaderHeight = function() {
          if (INSTANCE.AppState.get('fullscreen')) {
            return 0;
          }
          else {
            return INSTANCE.configuration.headerHeight;
          }
        }

        INSTANCE.initButtons = function() {
          log.debug('DISPLAY', 'init buttons');
          INSTANCE.ButtonsHeaderInstance = new APP.View.ButtonsHeader({
            model: INSTANCE.AppState,
            region: 'headerButtons',
            vent: INSTANCE.Vent,
            settings: INSTANCE.settings,
            configuration: INSTANCE.configuration,
            instance: INSTANCE,
          });
          INSTANCE.Regions.headerButtons.show(INSTANCE.ButtonsHeaderInstance);
          INSTANCE.ButtonsFullscreenInstance = new APP.View.ButtonsFullscreen({
            model: INSTANCE.AppState,
            region: 'fullscreenButtons',
            vent: INSTANCE.Vent,
            settings: INSTANCE.settings,
            configuration: INSTANCE.configuration,
            instance: INSTANCE,
          });
          INSTANCE.Regions.fullscreenButtons.show(INSTANCE.ButtonsFullscreenInstance);
        };

        INSTANCE.initAppState = function() {
          log.info('APP', 'init state');
          INSTANCE.AppState = new APP.Model.App({
            view: INSTANCE.configuration.defaultLayout,
            // TODO: Convert this to detect mode set by user (or default) in
            // the future.
            subscriberMode: INSTANCE.setSubscriberMode(),
          });
          INSTANCE.volumeEventsEnabled = !INSTANCE.configuration.audioMuteDefault;
        };

        INSTANCE.initPlugins = function() {
          log.info('APP', 'init plugins');
          INSTANCE.plugin = {};
          var room = require('openhangout.plugin.room.' + INSTANCE.configuration.plugin.room);
          INSTANCE.plugin.room = new room(INSTANCE);
          log.info('APP', 'Room plugin init complete: ', INSTANCE._instance, INSTANCE.plugin.room.name);
        };

        INSTANCE.setSubscriberMode = function() {
          var subscriberMode;
          if (INSTANCE.settings.subscriberMode) {
            subscriberMode = INSTANCE.settings.subscriberMode;
          }
          else if (INSTANCE.settings.mobile) {
            subscriberMode = 'single';
          }
          else {
            subscriberMode = 'multiple';
          }
          return subscriberMode;
        }

        INSTANCE.roomSocketCommandCallback = function(data) {
          switch (data.type) {
            case 'mute-user':
              INSTANCE.muteIfLocalUser(data.userId);
              break;
          }
        }

        INSTANCE.roomSocketChatCallback = function(data) {
          INSTANCE.addChatToMessages(data.userId, data);
        }

        INSTANCE.roomSocketVolumeCallback = function(data) {
          INSTANCE.setFeedVolume(data.userId, data.volume);
        }

        INSTANCE.initRoomSocket = function() {
          var hostname = INSTANCE.settings.videoServerHostname || INSTANCE.configuration.defaultVideoServerHostname;
          var port = INSTANCE.settings.videoServerPort || INSTANCE.configuration.defaultVideoServerPort;
          var roomName = INSTANCE.settings.roomName;
          INSTANCE.RoomSocket = new Faye.Client('https://' + hostname + ':' + port + '/faye', {retry: 5});
          INSTANCE.RoomSocket.subscribe('/' + roomName + '/command', INSTANCE.roomSocketCommandCallback);
          INSTANCE.RoomSocket.subscribe('/' + roomName + '/chat', INSTANCE.roomSocketChatCallback);
          if (INSTANCE.plugin.room.enableAudioManagement) {
            INSTANCE.RoomSocket.subscribe('/' + roomName + '/volume', INSTANCE.roomSocketVolumeCallback);
          }
        }

        INSTANCE.disconnectRoom = function() {
          INSTANCE.Room && INSTANCE.plugin.room.disconnect(INSTANCE.Room);
        }

        INSTANCE.disconnectRoomSocket = function() {
          INSTANCE.RoomSocket && INSTANCE.RoomSocket.disconnect();
        }

        INSTANCE.getChatMessagesFromStorage = function() {
          var roomName = INSTANCE.settings.roomName;
          var chatData = localStorage.getItem('chatData');
          var roomData;
          try {
            roomData = JSON.parse(chatData) || {};
          }
          catch(e) {
            roomData = {};
          }
          var chatMessages = roomData[roomName] || [];
          log.debug('CHAT', 'retrieved messages from storage', chatMessages);
          return chatMessages;
        };

        INSTANCE.storeChatData = function(chatMessages) {
          var roomName = INSTANCE.settings.roomName;
          var newRoomData = {}
          newRoomData[roomName] = chatMessages;
          log.debug('CHAT', 'storing messages', newRoomData);
          localStorage.setItem('chatData', JSON.stringify(newRoomData));
        }

        INSTANCE.storeChatMessage = function(model) {
          var chatMessages = INSTANCE.getChatMessagesFromStorage();
          var messageData = model.toJSON();
          // Extract only the feed data needed, the full model won't store.
          var feedData = messageData.feed.toJSON();
          messageData.feed = {
            chatColor: feedData.chatColor,
          }
          log.debug('CHAT', 'storing message', messageData);
          chatMessages.push(messageData);
          INSTANCE.storeChatData(chatMessages);
        };

        INSTANCE.initChat = function() {
          log.info('CHAT', 'init');
          // This function is simplistic for now, reading stored chat messages for
          // the current room from an array keyed by room, then writing back only
          // that array key to storage to clear out messages from old rooms.
          var chatMessages = INSTANCE.getChatMessagesFromStorage();
          INSTANCE.storeChatData(chatMessages);
          // Must reconstitute as models, the rest of the code is expecting it.
          for (var key in chatMessages) {
            var feedData = chatMessages[key].feed;
            chatMessages[key] = new APP.Model.ChatMessage(chatMessages[key]);
            chatMessages[key].set('feed', new APP.Model.AudioFeed(feedData));
          }
          INSTANCE.ChatMessages = new APP.Collection.ChatMessages(chatMessages);
          INSTANCE.ChatInstance = new APP.View.Chat({
            model: INSTANCE.AppState,
            collection: INSTANCE.ChatMessages,
            vent: INSTANCE.Vent,
            settings: INSTANCE.settings,
            configuration: INSTANCE.configuration,
            instance: INSTANCE,
          });
          INSTANCE.Regions.chat.show(INSTANCE.ChatInstance);
        };

        INSTANCE.initUserInfo = function() {
          if (_.isEmpty(INSTANCE.settings.userId) || _.isEmpty(INSTANCE.settings.username)) {
            log.info('USER INFO', 'init');
            var userId = Cookies.get('userId');
            var name = Cookies.get('username');
            if (!userId || !name) {
              var defaultUserId = String(Math.random()).substring(2);
              var defaultName = 'Anonymous user';
              Cookies.set('userId', defaultUserId, { expires: 1 });
              name = prompt('Please enter your name', defaultName);
              name && Cookies.set('username', name, { expires: 1 });
            }
            INSTANCE.settings.username = name ? name : defaultName;
            INSTANCE.settings.userId = userId ? userId : defaultUserId;
          }
        }

        INSTANCE.initDisplay = function() {
          log.info('DISPLAY', 'init');
          INSTANCE.setFullscreenListener();
          INSTANCE.AppState.on('change:fullscreen', INSTANCE.fullscreenStateChanged);
          var fullscreen = INSTANCE.getFullscreenState();
          INSTANCE.AppState.set('fullscreen', fullscreen);
          INSTANCE.initButtons();

          INSTANCE.AppState.on('change:view', INSTANCE.setLayout);
          INSTANCE.AppState.on('change:localAudioMuted', INSTANCE.changeLocalAudioMute);
          INSTANCE.AppState.on('change:localVideoMuted', INSTANCE.changeLocalVideoMute);

          INSTANCE.hotkeys = INSTANCE.initHotkeys();
          if (INSTANCE.configuration.hotkeys) {
            INSTANCE.hotkeys.activate($(document));
          }
          $(window).resize(function() {
            INSTANCE.resizeLayout();
          });
        };

        INSTANCE.initHotkeys = function() {
          log.info('HOTKEYS', 'init');

          // These allow the hotkeys to work even when an input has the
          // focus. Given the ctrl+shift prefix, this seems reasonable
          // and safe, and avoids inconsistencies when using the hotkeys.
          $.hotkeys.options.filterInputAcceptingElements = false;
          $.hotkeys.options.filterContentEditable = false;
          $.hotkeys.options.filterTextInputs = false;

          var makeHotkey = function(key) {
            var hotkey = 'ctrl+shift+' + key;
            return hotkey;
          };

          var hotkeysLog = function(event, funcName) {
            log.debug('HOTKEY', event.data.keys + ' hotkey pressed, calling: ' + funcName);
          };

          var toggleLocalAudioMute = function(data) {
            hotkeysLog(data, 'toggleLocalAudioMute');
            INSTANCE.toggleLocalAudioMute();
          };

          var toggleLocalVideoMute = function(data) {
            hotkeysLog(data, 'toggleLocalVideoMute');
            INSTANCE.toggleLocalVideoMute();
          };

          var toggleChat = function(data) {
            hotkeysLog(data, 'toggleChat');
            INSTANCE.toggleChat();
          };

          var switchView = function(data) {
            hotkeysLog(data, 'switchView');
            INSTANCE.switchView();
          };

          var toggleFullscreen = function(data) {
            hotkeysLog(data, 'toggleFullscreen');
            INSTANCE.toggleFullscreen();
          };

          var bindings = {
              m: toggleLocalAudioMute,
              v: toggleLocalVideoMute,
              c: toggleChat,
              s: switchView,
              f: toggleFullscreen,
          }

          var boundFunctions = {};

          var bindingsCallback = function(func, key) {
              var boundFunc = _.bind(func, this);
              boundFunctions[key] = boundFunc;
          }
          _.each(bindings, _.bind(bindingsCallback, this));

          var activate = function(el) {
              log.debug('HOTKEYS', 'activate');
              var callback = function(func, key) {
                  el.bind('keyup', makeHotkey(key), func);
              }
              _.each(boundFunctions, callback);
          }

          var deactivate = function(el) {
              log.debug('HOTKEYS', 'deactivate');
              var callback = function(func, key) {
                  el.unbind('keyup', func);
              }
              _.each(boundFunctions, callback);
          }

          log.debug('HOTKEYS', 'bindings', bindings);

          return {
            activate: activate,
            deactivate: deactivate,
          }
        };

        INSTANCE.setDefaultView = function() {
          var view = Cookies.get('view');
          log.debug('DISPLAY', 'set default view:', view);
          if (view) {
            INSTANCE.AppState.set('view', view);
          }
          INSTANCE.setSubscriberModeViews();
          INSTANCE.setLayout();
        };

        INSTANCE.setSubscriberModeViews = function() {
          var subscriberMode = INSTANCE.getSubscriberMode();
          switch (subscriberMode) {
            case 'single':
              log.debug('DISPLAY', 'set layout: subscriber mode single');
              INSTANCE.LayoutInstance.showUserVideosFilmstripSingleContainer(true);
              break;
            case 'multiple':
              log.debug('DISPLAY', 'set layout: subscriber mode multiple');
              INSTANCE.LayoutInstance.showUserVideosFilmstripMultipleContainer(true);
              break;
          }
        }

        INSTANCE.setLayoutTileView = function() {
          var tileContainer = $(INSTANCE.configuration.tileViewContainer);
          var focusContainer = $(INSTANCE.configuration.focusViewContainer);
          var filmstripLayoutContainer = $(INSTANCE.configuration.filmstripLayoutViewContainer);
          focusContainer.hide();
          filmstripLayoutContainer.hide();
          // Clears all from focus.
          INSTANCE.clearFocusUser({});
          tileContainer.show();
          INSTANCE.UserVideosTileInstance.setupStartLocalVideo();
        };

        INSTANCE.setLayoutFocusView = function() {
          var tileContainer = $(INSTANCE.configuration.tileViewContainer);
          var focusContainer = $(INSTANCE.configuration.focusViewContainer);
          var filmstripLayoutContainer = $(INSTANCE.configuration.filmstripLayoutViewContainer);
          tileContainer.hide();
          INSTANCE.setFloorToFocus();
          focusContainer.show();
          filmstripLayoutContainer.show();
          INSTANCE.UserVideosFilmstripLayoutInstance.setupStartLocalVideo();
        };

        INSTANCE.setLayout = function() {
          var view = INSTANCE.getView();
          var subscriberMode = INSTANCE.getSubscriberMode();
          switch (subscriberMode) {
            case 'single':
              INSTANCE.setLayoutFocusView();
              log.debug('DISPLAY', 'set layout: single subscriber');
              break;
            case 'multiple':
              switch (view) {
                case 'tile':
                  INSTANCE.setLayoutTileView();
                  break;
                case 'focus':
                  INSTANCE.setLayoutFocusView();
                  break;
              }
              log.debug('DISPLAY', 'set layout:', view);
              break;
          }
          INSTANCE.resizeLayout();
        };

        INSTANCE.userFocusChanged = function(feed, hasFocus) {
          if (hasFocus) {
            INSTANCE.switchUser(feed);
          }
        };

        INSTANCE.switchUser = function(feed) {
          log.info('FEED', 'switching user to: ', feed.get('username'));
          INSTANCE.plugin.room.switchUser(feed.id);
        };

        INSTANCE.switchToActiveFocusUser = function() {
          var feed = INSTANCE.VideoFeeds.findWhere({hasFocus: true});
          if (feed) {
            INSTANCE.switchUser(feed);
          }
        };

        INSTANCE.getUserVideoFeed = function(userId) {
          return INSTANCE.VideoFeeds.get(userId);
        };

        INSTANCE.initFeeds = function() {
          log.info('FEED', 'init');
          INSTANCE.VideoFeeds = new APP.Collection.VideoFeeds();
          INSTANCE.VideoFeeds.on('add', INSTANCE.videoFeedAdded);
          INSTANCE.VideoFeeds.on('remove', INSTANCE.videoFeedRemoved);
          if (INSTANCE.getSubscriberMode() == 'single') {
            INSTANCE.VideoFeeds.on('change:hasFocus', INSTANCE.userFocusChanged);
          }
          INSTANCE.AudioFeeds = new APP.Collection.AudioFeeds();
          INSTANCE.AudioFeeds.on('remove', INSTANCE.audioFeedRemoved);
        }

        INSTANCE.initFeedSelector = function() {
          log.info('FEED', 'init feed selector');
          INSTANCE.UserVideoFilmstripMultiSelectFeedSelectorInstance = new APP.View.UserVideoFilmstripMultiSelectFeedSelector({
            collection: INSTANCE.VideoFeeds,
            settings: INSTANCE.settings,
            vent: INSTANCE.Vent,
            instance: INSTANCE,
          });
          INSTANCE.Regions.feedSelector.show(INSTANCE.UserVideoFilmstripMultiSelectFeedSelectorInstance);
        }

        INSTANCE.initAudioView = function() {
          log.info('DISPLAY', 'init audio view');
          INSTANCE.RegionManager.addRegion('audioContainer', INSTANCE.configuration.audioViewContainer);
          INSTANCE.refreshRegions();
          INSTANCE.UserAudioStreamsInstance = new APP.View.UserAudioStreams({
            collection: INSTANCE.AudioFeeds,
            vent: INSTANCE.Vent,
            settings: INSTANCE.settings,
            configuration: INSTANCE.configuration,
            instance: INSTANCE,
          });
          INSTANCE.Regions.audioContainer.show(INSTANCE.UserAudioStreamsInstance);
        };

        INSTANCE.initTileView = function() {
          log.info('DISPLAY', 'init tile view');
          INSTANCE.RegionManager.addRegion('tileContainer', INSTANCE.configuration.tileViewContainer);
          INSTANCE.refreshRegions();
          INSTANCE.UserVideosTileInstance = new APP.View.UserVideosTile({
            collection: INSTANCE.VideoFeeds,
            videoType: 'tile',
            vent: INSTANCE.Vent,
            settings: INSTANCE.settings,
            configuration: INSTANCE.configuration,
            instance: INSTANCE,
          });
          INSTANCE.Regions.tileContainer.show(INSTANCE.UserVideosTileInstance);
        };

        INSTANCE.initFilmstripLayoutView = function() {
          log.info('DISPLAY', 'init filmstrip layout view');
          INSTANCE.RegionManager.addRegion('filmstripLayoutContainer', INSTANCE.configuration.filmstripLayoutViewContainer);
          INSTANCE.refreshRegions();
          INSTANCE.UserVideosFilmstripLayoutInstance = new APP.View.UserVideosFilmstripLayout({
            collection: INSTANCE.VideoFeeds,
            videoType: 'filmstrip',
            vent: INSTANCE.Vent,
            appState: INSTANCE.AppState,
            settings: INSTANCE.settings,
            configuration: INSTANCE.configuration,
            instance: INSTANCE,
          });
          INSTANCE.Regions.filmstripLayoutContainer.show(INSTANCE.UserVideosFilmstripLayoutInstance);
        };

        INSTANCE.initFocusView = function() {
          log.info('DISPLAY', 'init focus view');
          INSTANCE.RegionManager.addRegion('focusContainer', INSTANCE.configuration.focusViewContainer);
          INSTANCE.refreshRegions();
          INSTANCE.UserVideosFocusInstance = new APP.View.UserVideosFocus({
            collection: INSTANCE.VideoFeeds,
            videoType: 'focus',
            appState: INSTANCE.AppState,
            vent: INSTANCE.Vent,
            settings: INSTANCE.settings,
            configuration: INSTANCE.configuration,
            instance: INSTANCE,
          });
          INSTANCE.Regions.focusContainer.show(INSTANCE.UserVideosFocusInstance);
        };

        INSTANCE.initLayoutView = function() {
          INSTANCE.LayoutInstance = new APP.View.Layout({
            model: INSTANCE.AppState,
            vent: INSTANCE.Vent,
            settings: INSTANCE.settings,
            configuration: INSTANCE.configuration,
            instance: INSTANCE,
          });
          var displayFullscreenButtons = function() {
            if (INSTANCE.AppState.get('fullscreen')) {
              INSTANCE.LayoutInstance.fullscreenButtonsShow();
            }
          }
          INSTANCE.Vent.on('fullscreen:buttons:display', displayFullscreenButtons);
          INSTANCE.setDefaultView();
        };

        INSTANCE.configLocalPublishedVideoStream = function(config, streamGroup) {
          log.info('STREAM', 'configure video stream for publish to:', streamGroup, config);
          INSTANCE.localPublishedVideoStreamsConfig = INSTANCE.localPublishedVideoStreamsConfig ? INSTANCE.localPublishedVideoStreamsConfig : {};
          var videoStreamConfig = INSTANCE.videoStreamConfig(config, streamGroup);
          INSTANCE.localPublishedVideoStreamsConfig[streamGroup] = videoStreamConfig;
          return videoStreamConfig;
        }

        INSTANCE.configLocalPublishedVideoStreams = function() {
          log.info('STREAM', 'config video streams');
          INSTANCE.localPublishedVideoStreams = {};
          var publishStreamGroups = INSTANCE.settings.publishStreamGroups;
          if (!publishStreamGroups) {
            publishStreamGroups = {};
            publishStreamGroups[INSTANCE.configuration.streamGroupDefault] = { publishedFor: [INSTANCE.configuration.streamGroupDefault] };
          }
          for (var key in publishStreamGroups) {
            INSTANCE.configLocalPublishedVideoStream(publishStreamGroups[key], key);
          }
          INSTANCE.publishingForGroups = INSTANCE.calculatePublishedFor(publishStreamGroups);
        }

        INSTANCE.videoStreamAttrs = function(videoAttrs, streamAttrs) {
          // TODO: Figure out screen sharing? Disabled for now.
          // var screen = APP.Util.getParameterByName("screen");
          var screen = false;
          var config = {
            audio: false,
            video: {
              minWidth: videoAttrs.width,
              minHeight: videoAttrs.height,
              maxWidth: videoAttrs.width,
              maxHeight: videoAttrs.height,
            },
            data: false,
            screen: screen,
            attributes: streamAttrs,
          };
          if (videoAttrs.bitrate) {
            config.bitrate = videoAttrs.bitrate;
          }
          if (INSTANCE.settings.videoDeviceId) {
            config.video.deviceId = {
              exact: INSTANCE.settings.videoDeviceId,
            };
          }
          // For screen sharing a Chrome extension ID is required.
          // If not using Chrome, the creation of the stream will fail.
          if (screen && INSTANCE.settings.chromeScreenshareExtensionId){
            config.extensionId = INSTANCE.settings.chromeScreenshareExtensionId;
          }
          log.debug('STREAM', 'config', config);
          return config;
        };

        INSTANCE.configLocalAudioStream = function(config) {
          if (!config) {
            config = {
              audio: {
                advanced: [
                  {
                    googEchoCancellation: INSTANCE.configuration.googleEchoCancellation,
                  },
                  {
                    googAutoGainControl: INSTANCE.configuration.googleAutoGainControl,
                  },
                  {
                    googNoiseSuppression: INSTANCE.configuration.googleNoiseSuppression,
                  },
                  {
                    googHighpassFilter: INSTANCE.configuration.googleHighpassFilter,
                  },
                  {
                    googTypingNoiseDetection: INSTANCE.configuration.googleTypingNoiseDetection,
                  },
                ],
              },
              video: false,
              data: false,
              screen: false,
              attributes: {
                streamGroup: 'audio',
                isAudioStream: true,
                username: INSTANCE.settings.username,
                userId: INSTANCE.settings.userId,
                isAudioMuted: INSTANCE.configuration.audioMuteDefault,
                isVideoMuted: INSTANCE.configuration.videoMuteDefault,
              },
            };
          }
          if (INSTANCE.settings.audioInDeviceId) {
            config.audio.deviceId = {
              exact: INSTANCE.settings.audioInDeviceId
            }
          }
          log.debug('STREAM', 'config local audio stream', config);
          INSTANCE.localAudioStreamConfig = config;
          return config;
        };

        INSTANCE.configLocalDisplayStream = function(config) {
          if (!config) {
            var videoAttrs;
            if (INSTANCE.settings.localVideo) {
              videoAttrs = {
                width: INSTANCE.settings.localVideo.width,
                height: INSTANCE.settings.localVideo.height,
              };
            }
            else {
              videoAttrs = {
                width: INSTANCE.configuration.localDisplayVideoWidthDefault,
                height: INSTANCE.configuration.localDisplayVideoHeightDefault,
              };
            }
            var streamAttrs = {
              streamGroup: 'local',
              isVideoStream: true,
              username: INSTANCE.settings.username,
              userId: INSTANCE.settings.userId,
              isAudioMuted: INSTANCE.configuration.audioMuteDefault,
              isVideoMuted: INSTANCE.configuration.videoMuteDefault,
            };
            config = INSTANCE.videoStreamAttrs(videoAttrs, streamAttrs);
          }
          log.debug('STREAM', 'config local display stream', config);
          INSTANCE.localDisplayStreamConfig = config;
          return config;
        };


        // TODO: Find some sane way to integrate plugin specific settings,
        // and/or additional settings, like bitrate.
        INSTANCE.videoStreamConfig = function(config, streamGroup) {
          streamGroup = streamGroup ? streamGroup : INSTANCE.configuration.streamGroupDefault;
          var videoAttrs;
          if (config && config.videoSize) {
            videoAttrs = {
              width: config.videoSize.width,
              height: config.videoSize.height,
            };
            if (config.bitrate) {
              videoAttrs.bitrate = config.bitrate;
            }
          }
          else {
            videoAttrs = {
              width: INSTANCE.configuration.videoStreamWidthDefault,
              height: INSTANCE.configuration.videoStreamHeightDefault,
            };
          }
          var publishedFor;
          if (config && config.publishedFor) {
            publishedFor = config.publishedFor;
          }
          else {
            publishedFor = [INSTANCE.configuration.streamGroupDefault];
          }
          var streamAttrs = {
            streamGroup: streamGroup,
            publishedFor: publishedFor,
            isVideoStream: true,
            username: INSTANCE.settings.username,
            userId: INSTANCE.settings.userId,
            isAudioMuted: INSTANCE.configuration.audioMuteDefault,
            isVideoMuted: INSTANCE.configuration.videoMuteDefault,
          };
          if (config.iOSLocal) {
            streamAttrs.iOSLocal = config.iOSLocal;
          }
          return INSTANCE.videoStreamAttrs(videoAttrs, streamAttrs);
        };

        INSTANCE.createUserToken = function(role, callback) {
          log.debug('TOKEN', 'start');
          callback = callback ? callback : function() {};
          var userId = INSTANCE.settings.userId;
          var count = 1;
          var timeout = INSTANCE.configuration.serverRetryInterval;
          var createUserTokenCallback = function (err, token) {
            log.debug('TOKEN', token);
            if (err) {
              log.error('TOKEN', 'error', err);
              count++;
              if (count > INSTANCE.configuration.serverRetries) {
                log.error('TOKEN', 'error, maximum server retries exceeded', INSTANCE.configuration.serverRetries);
                callback(err);
              }
              else {
                timeout += INSTANCE.configuration.serverRetryInterval;
                setTimeout(function() {
                  INSTANCE._createUserToken(userId, role, createUserTokenCallback);
                }, timeout);
              }
            }
            else {
              log.info('TOKEN', 'complete');
              callback(null, token);
            }
          }
          INSTANCE._createUserToken(userId, role, createUserTokenCallback);
        };

        INSTANCE._createUserToken = function(userId, role, callback) {
          log.info('TOKEN', 'creating');

          var url = '/room/' + INSTANCE.settings.roomName + '/create-token/';
          var body = {
            userId: userId,
            role: role,
            videoServerHostname: INSTANCE.settings.videoServerHostname || INSTANCE.configuration.defaultVideoServerHostname,
          };

          var settings = {
            cache: false,
            contentType: 'application/json; charset=UTF-8',
            data: JSON.stringify(body),
            dataType: 'text',
            type: 'POST',
          }

          $.ajax(url, settings)
            .done(function(data) {
              callback(null, data);
            })
            .fail(function(jqXHR, textStatus, errorThrown) {
              var message = 'Error fetching token';
              log.error('TOKEN', message, textStatus, errorThrown);
              callback(message);
            });
        };

        INSTANCE.autoFocusNeeded = function() {
          return INSTANCE.findIndividualVideoFeeds().length > 2;
        }

        // This ensures that the first two users get a sensible focus state, and
        // that somebody gets the focus if the view is switched to before anyone has
        // the focus. All others coming in should respond to talking events.
        INSTANCE.setBestFocus = function() {
          var active = INSTANCE.VideoFeeds.findWhere({active: true});
          var hasFocus = INSTANCE.VideoFeeds.findWhere({hasFocus: true});
          if ((!active && !INSTANCE.autoFocusNeeded()) || !hasFocus) {
            INSTANCE.focusLast();
          }
        };

        INSTANCE.focusLast = function() {
          // Take the last joined for focus.
          var feeds = INSTANCE.findIndividualVideoFeeds();
          if (INSTANCE.isFocusState() && feeds.length > 0) {
            var feed = feeds.pop();
            log.debug('DISPLAY', 'focus last:', feed.get('username'));
            INSTANCE.clearFocusUser(feed);
            feed.set('hasFocus', true);
          }
        };

        INSTANCE.setFloorUserByUserId = function(userId) {
          var floorUser = INSTANCE.getUserVideoFeed(userId);
          INSTANCE.setFloorUser(floorUser);
        }

        INSTANCE.setFloorUser = function(floorUser) {
          var isLocalUser = _.isObject(floorUser) && (floorUser.id === INSTANCE.settings.userId);
          if (floorUser && !isLocalUser) {
            log.debug('FLOOR', 'user candidate', floorUser.get('username'));
            INSTANCE.clearFloorUser(floorUser);
            floorUser.set('hasFloor', true);
            INSTANCE.setAutoFocus();
          }
        }

        INSTANCE.setAutoFocus = function() {
          var autoFocus = INSTANCE.AppState.get('autoFocus');
          if (INSTANCE.isFocusState() && INSTANCE.autoFocusNeeded() && autoFocus) {
            INSTANCE.setFloorToFocus();
          }
        }

        INSTANCE.setFocusUser = function(focusUser) {
          if (focusUser) {
            log.debug('FOCUS', 'set user', focusUser.get('username'));
            INSTANCE.clearFocusUser(focusUser);
            focusUser.set('hasFocus', true);
          }
        }

        INSTANCE.setFloorToFocus = function() {
          var active = INSTANCE.VideoFeeds.findWhere({active: true});
          var floorUser = INSTANCE.VideoFeeds.findWhere({hasFloor: true});
          if (active || !floorUser || !INSTANCE.autoFocusNeeded()) {
            INSTANCE.setBestFocus();
          }
          else {
            INSTANCE.setFocusUser(floorUser);
          }
          if (!active) {
            INSTANCE.AppState.set('autoFocus', true);
          }
        }

        INSTANCE.autoFloorUser = function() {
          var threshold = INSTANCE.configuration.audioSpeakingThreshold;
          var highestVol = threshold;
          var floorReducer = function(currFeed, nextFeed) {
            var vol = nextFeed.get('speakingVolume');
            if (vol > threshold) {
              if (vol > highestVol) {
                highestVol = vol;
                return nextFeed;
              }
              else {
                return currFeed;
              }
            }
            else {
              return currFeed;
            }
          }
          var floorUser = INSTANCE.VideoFeeds.reduce(floorReducer, null);
          INSTANCE.setFloorUser(floorUser);
        }

        INSTANCE.audioFeedRemoved = function(feed) {
          log.info('FEED', 'audio feed removed', feed.get('username'));
          var color = feed.get('chatColor');
          if (color) {
            APP.Util.returnColor(INSTANCE._instance, color);
          }
        }

        INSTANCE.videoFeedAdded = function(feed) {
          log.info('FEED', 'video feed added', feed.get('username'));
          INSTANCE.setFloorToFocus();
          INSTANCE.updateUserInfoSlots();
          INSTANCE.Vent.trigger("feed:video:add", feed);
        }

        INSTANCE.videoFeedRemoved = function(feed) {
          log.info('FEED', 'video feed removed', feed.get('username'));
          INSTANCE.setFloorToFocus();
          INSTANCE.updateUserInfoSlots();
          INSTANCE.Vent.trigger("feed:video:remove", feed);
        }

        INSTANCE.setVolumeToSilence = function() {
          log.debug('AUDIO', 'setting local audio volume to silent');
          INSTANCE.setLocalVolume(-100);
        }

        INSTANCE.isSpeaking = function(volume) {
          return volume > INSTANCE.configuration.audioSpeakingThreshold;
        }

        INSTANCE.setFeedVolume = function(userId, volume) {
          var audioFeed = INSTANCE.AudioFeeds.get(userId);
          var videoFeed = INSTANCE.getUserVideoFeed(userId);
          if (audioFeed || videoFeed) {
            var speaking = INSTANCE.isSpeaking(volume);
            var attrs = {
              speaking: speaking,
              speakingVolume: volume,
            }
            audioFeed && audioFeed.set(attrs);
            videoFeed && videoFeed.set(attrs);
            if (audioFeed && INSTANCE.settings.userId === INSTANCE.configuration.verboseDebuggingUserId) {
              log.debug('FEED VOLUME', audioFeed.get('username') + ':', volume, speaking);
            }
          }
          INSTANCE.autoFloorUser();
        }

        INSTANCE.setLocalVolume = function(volume) {
          INSTANCE.sendVolumeData(volume);
        }

        INSTANCE.handleVolumeEvents = function(volume) {
          var speaking = !INSTANCE.getLocalAudioMuted() && INSTANCE.isSpeaking(volume);
          if (speaking) {
            INSTANCE.volumeEventsEnabled = true;
          }
          else if (!speaking && INSTANCE.volumeEventsEnabled) {
            INSTANCE.volumeEventsEnabled = false;
            INSTANCE.setVolumeToSilence();
          }
          if (INSTANCE.volumeEventsEnabled) {
            INSTANCE.setLocalVolume(volume);
          }
        };

        INSTANCE.isFocusState = function() {
          var view = INSTANCE.getView();
          var subscriberMode = INSTANCE.getSubscriberMode();
          if (subscriberMode == 'multiple' && view == 'focus') {
            return true;
          }
          else if (subscriberMode == 'single') {
            return true;
          }
          return false;
        };

        INSTANCE.sendVolumeData = function(volume) {
          var data = {
            userId: INSTANCE.settings.userId,
            volume: volume,
          }
          INSTANCE.RoomSocket.publish('/' + INSTANCE.settings.roomName + '/volume', data);
        };

        INSTANCE.calculatePublishedFor = function(publishStreamGroups) {
          var publishingForGroups = {};
          for (var key in publishStreamGroups) {
            var streamGroup = publishStreamGroups[key];
            if (streamGroup.publishedFor) {
              for (var i in streamGroup.publishedFor) {
                publishingForGroups[streamGroup.publishedFor[i]] = true;
              }
            }
          }
          return publishingForGroups;
        }

        INSTANCE.audioMonitoring = function(stream) {
          INSTANCE.Audio.setup(stream, INSTANCE.configuration.audioPollingInterval, INSTANCE.handleVolumeEvents);
        }

        INSTANCE.stopAudioMonitoring = function() {
          INSTANCE.Audio.teardown();
        }

        INSTANCE.setAudioPlaybackDevice = function(sinkId, element, callback) {
          callback = callback ? callback : function() {};

          if (typeof element.sinkId !== 'undefined') {
            if (sinkId) {
              log.info('AUDIO', 'Setting speaker:', sinkId, element);

              element.setSinkId(sinkId)
                .then(function() {
                  var message = 'Success, audio output device attached';
                  log.debug('AUDIO', message, sinkId);
                  callback(null, message);
                })
                .catch(function(error) {
                  log.error('AUDIO', error);
                  callback(error);
                });
            }
            else {
              var message = 'No audio output device ID found';
              log.warn('AUDIO', message);
              callback(null, message);
            }
          } else {
            var message = 'Browser does not support output device selection.';
            log.warn('AUDIO', message);
            callback(message);
          }
        }

        INSTANCE.audioStreamAttached = function(element) {
          if (INSTANCE.settings.audioOutDeviceId) {
            INSTANCE.setAudioPlaybackDevice(INSTANCE.settings.audioOutDeviceId, element);
          }
        }

        INSTANCE.buildSubscribeStreamOptions = function(attributes) {
          var streamSubscribeOptions;
          if (attributes.isAudioStream) {
            streamSubscribeOptions = {
              audio: true,
              data: false,
              video: false,
            };
          }
          else if (attributes.isVideoStream) {
            streamSubscribeOptions = {
              audio: false,
              data: false,
              video: true,
            };
          }
          return streamSubscribeOptions;
        }

        INSTANCE.subscribeToStream = function(attributes) {
          var streamGroup = attributes.streamGroup;
          var publishedFor = attributes.publishedFor;
          // This logic expresses these rules:
          // 1. Only subscribe to a stream group that's also being published to
          //    (exception for connected users not publishing any streams, these
          //    are view only).
          // 2. Don't subscribe to any stream we're publishing in the group,
          //    unless we're specifically subscribing to our own stream, or
          //    on iOS and subscribing to the special 'iOSLocal' stream.
          // 3. Don't subscribe to any streams from a group we're not
          //    subscribing to.
          var subscribeStreamGroup = INSTANCE.settings.subscribeStreamGroup || INSTANCE.configuration.streamGroupDefault;
          if (!_.isEmpty(INSTANCE.settings.publishStreamGroups) && !INSTANCE.publishingForGroups[subscribeStreamGroup]) {
            var message = "Configuration error: cannot subscribe to group '" + subscribeStreamGroup + "', no streams published to group";
            log.error('STREAM', message);
            alert(message);
            throw message;
          }
          if (INSTANCE.isiOSLocalSubscribeFeed(attributes.userId)) {
            log.info('STREAM', "iOS user subscribing to iOSLocal stream:", attributes);
            return true;
          }
          else if (INSTANCE.settings.subscribeToOwnStream) {
            if (!INSTANCE.isSelfSubscribeFeed(attributes.userId)) {
              log.info('STREAM', "skip stream, only subscribing to user's own stream:", attributes);
              return false;
            }
          }
          else if (attributes.userId == INSTANCE.settings.userId) {
            log.info('STREAM', "skip subscribing to user's own stream:", attributes);
            return false;
          }
          else if (publishedFor && publishedFor.indexOf(subscribeStreamGroup) == -1) {
            log.info('STREAM', 'skip subscribing to video stream not in', subscribeStreamGroup, 'published for', publishedFor);
            return false;
          }
          return true;
        }

        INSTANCE.getVisibleVideoFeedsCount = function() {
          var count = INSTANCE.VideoFeeds.reduce(function(count, feed) {
            if (INSTANCE.isSelfSubscribeFeed(feed.id) || INSTANCE.isiOSLocalSubscribeFeed(feed.id)) {
              return count;
            }
            else {
              return count + 1;
            }
          }, 0);
          return count;
        }

        INSTANCE.getOtherVideoFeedsCount = function() {
          var count = INSTANCE.VideoFeeds.reduce(function(count, feed) {
            if (feed.id == INSTANCE.settings.userId || INSTANCE.isSelfSubscribeFeed(feed.id) || INSTANCE.isiOSLocalSubscribeFeed(feed.id)) {
              return count;
            }
            else {
              return count + 1;
            }
          }, 0);
          return count;
        }

        INSTANCE.updateUserInfoSlots = function() {
          var slot = 1;
          var count = INSTANCE.VideoFeeds.each(function(feed) {
            if (feed.id != INSTANCE.settings.userId && !INSTANCE.isSelfSubscribeFeed(feed.id) && !INSTANCE.isiOSLocalSubscribeFeed(feed.id)) {
              feed.set('userInfoSlot', slot);
              slot++;
            }
          });
        }

        INSTANCE.isSelfSubscribeFeed = function(userId) {
          return userId == INSTANCE.makeSelfSubscribeFeedUserId();
        }

        INSTANCE.makeSelfSubscribeFeedUserId = function() {
          return INSTANCE.settings.userId + '-self';
        }

        INSTANCE.isiOSLocalSubscribeFeed = function(userId) {
          return INSTANCE.settings.iOS && userId == INSTANCE.makeiOSLocalSubscribeFeedUserId();
        }

        INSTANCE.makeiOSLocalSubscribeFeedUserId = function() {
          return INSTANCE.settings.userId + '-self-ios-local';
        }

        INSTANCE.addLocalOrRemoteAttributes = function(attrs, isLocal) {
          var data = {
            id: attrs.userId,
            elementId: APP.Util.buildStreamElementId(attrs.userId),
          };
          if (isLocal) {
            data.username = INSTANCE.settings.username;
            data.isAudioMuted = INSTANCE.configuration.audioMuteDefault;
            data.isVideoMuted = INSTANCE.configuration.videoMuteDefault;
            data.isLocal = true;
          }
          else {
            data.username = attrs.username;
            data.isAudioMuted = attrs.isAudioMuted;
            data.isVideoMuted = attrs.isVideoMuted;
          }
          if (attrs.hasOwnProperty("multiselect")) {
            data.multiselect = attrs.multiselect;
          }
          return data;
        }

        INSTANCE.addAudioFeed = function(streamId, streamAttributes, streamAttachFunc) {
          var isLocal = streamAttributes.userId == INSTANCE.settings.userId;
          var model = {
            streamAttachFunc: streamAttachFunc,
            audioStreamId: streamId,
          };
          var modelData = INSTANCE.addLocalOrRemoteAttributes(streamAttributes, isLocal);
          _.extend(model, modelData);
          var color;
          if (color = APP.Util.getColor(INSTANCE._instance)) {
            model.chatColor = color;
          }
          INSTANCE.AudioFeeds.add(model);
          log.info("FEED", "adding audio feed", model);
        };

        INSTANCE.addVideoFeed = function(streamId, streamAttributes, streamAttachFunc) {
          var isLocal = streamAttributes.userId == INSTANCE.settings.userId;
          var tileSize = INSTANCE.getTileVideoFeedSize();
          var focusSize = INSTANCE.getFocusVideoFeedSize();
          var filmstripWidth = INSTANCE.getFilmstripVideoFeedWidth();
          var model = {
            streamAttachFunc: streamAttachFunc,
            videoStreamId: streamId,
            tileSize: tileSize,
            focusSize: focusSize,
            filmstripWidth: filmstripWidth,
          };
          var modelData = INSTANCE.addLocalOrRemoteAttributes(streamAttributes, isLocal);
          _.extend(model, modelData);
          INSTANCE.VideoFeeds.add(model);
          log.info("FEED", "adding video feed", model);
          INSTANCE.resizeLayout();
        };

        INSTANCE.addFakeVideoFeed = function(userId, name, isAudioMuted, isVideoMuted) {
          var attrs = {
            userId: userId,
            username: name,
            isAudioMuted: isAudioMuted,
            isVideoMuted: isVideoMuted,
          }
          INSTANCE.addVideoFeed(null, attrs, null);
        }

        INSTANCE.addFakeVideoFeeds = function(count, delay) {
          var counter = 0;
          var add = function() {
            var isAudioMuted = Math.random() >= 0.5;
            var isVideoMuted = Math.random() >= 0.5;
            INSTANCE.addFakeVideoFeed(counter, 'Fake User #' + counter, isAudioMuted, isVideoMuted);
          }
          var createVideoFeedDelay = function() {
            counter++;
            if (counter <= count) {
              setTimeout(function() {
                  add();
                  createVideoFeedDelay();
              }, delay);
            }
          }
          createVideoFeedDelay();
        }

        INSTANCE.findVisibleVideoFeed = function(userId) {
          var videoFeed = INSTANCE.getUserVideoFeed(userId);
          if (INSTANCE.getSubscriberMode() == 'single' && (!videoFeed || !videoFeed.get('multiselect'))) {
            videoFeed = INSTANCE.VideoFeeds.findWhere({multiselect: true});
          }
          return videoFeed;
        }

        INSTANCE.findIndividualVideoFeeds = function() {
          var feeds = INSTANCE.VideoFeeds.where({multiselect: false});
          return feeds;
        }

        INSTANCE.removeUserAudioFeed = function(userId) {
          var audioFeed = INSTANCE.AudioFeeds.get(userId);
          if (audioFeed) {
            log.info("FEED", "removing audio feed", userId);
            audioFeed.trigger('destroy', audioFeed);
          }
        }

        INSTANCE.removeUserVideoFeed = function(userId) {
          var videoFeed = INSTANCE.getUserVideoFeed(userId);
          if (videoFeed) {
            log.info("FEED", "removing video feed", userId);
            videoFeed.trigger('destroy', videoFeed);
            INSTANCE.resizeLayout();
          }
        }

        INSTANCE.removeUserFeeds = function(userId) {
          INSTANCE.removeUserAudioFeed(userId);
          INSTANCE.removeUserVideoFeed(userId);
        }

        INSTANCE.getChatWidth = function() {
          return INSTANCE.settings.mobile ? INSTANCE.configuration.chatWidthMobile : INSTANCE.configuration.chatWidth;
        }

        INSTANCE.getDisplaySize = function(absolute) {
          var chatWidth = (!absolute && INSTANCE.AppState.get('chat')) ? INSTANCE.getChatWidth() : 0;
          var height;
          var width;
          if (INSTANCE.settings.displayContainer) {
            // Must reselect in case it's been dynamically added.
            var container = $(INSTANCE.settings.displayContainer);
            height = container.height();
            width = container.width();
            if (!INSTANCE.settings.videoOnlyDisplay) {
              height -= INSTANCE.getHeaderHeight();
              width -= chatWidth;
            }
          }
          else {
            height = window.innerHeight - INSTANCE.getHeaderHeight();
            width = window.innerWidth - chatWidth;
          }
          var size = {
            height: height,
            width: width,
            area: height * width,
          }
          //log.debug("DISPLAY", "display size", size);
          return size;
        }

        INSTANCE.calculateTileSize = function(tiles, area, tileAreaPercentage) {
          var feedArea = area * tileAreaPercentage / 100 / tiles;
          var sqrt = Math.sqrt(feedArea / INSTANCE.configuration.horizontalDisplayRatio / INSTANCE.configuration.verticalDisplayRatio);
          var feedHeight = Math.floor((sqrt * INSTANCE.configuration.verticalDisplayRatio) - (INSTANCE.configuration.feedTileMargin / 2));
          var feedWidth = Math.floor((sqrt * INSTANCE.configuration.horizontalDisplayRatio) - (INSTANCE.configuration.feedTileMargin / 2));
          var size = {
            height: feedHeight,
            width: feedWidth,
          };
          //log.debug("DISPLAY", "calculated tile size", size);
          return size;
        }

        INSTANCE.testMedianTileAreaPercentage = function(tiles, size, tileAreaPercentage) {
          var tileSize = INSTANCE.calculateTileSize(tiles, size.area, tileAreaPercentage);

          var hTiles = Math.floor(size.width / tileSize.width);
          var vTiles = Math.floor(size.height / tileSize.height);
          var fittedTiles = hTiles * vTiles;

          if (fittedTiles < tiles) {
            return false;
          }
          else {
            return tileSize;
          }
        }

        INSTANCE.getTileVideoFeedSize = function() {
          var tiles = INSTANCE.getVisibleVideoFeedsCount();
          var size = INSTANCE.getDisplaySize();
          // It's possible for there to be zero tiles, or for the display area to
          // be zero-sized, and all of the subsequent math depends on having at
          // least one tile and a valid display area, so bail here in this case.
          if (tiles < 1 || size.height < 1 || size.width < 1) {
            return {
              height: 0,
              width: 0,
            };
          }
          var reducer = function(successTileAreaPercentage, failTileAreaPercentage, tileSize) {
            // No need for perfection, this is close enough, and is achieved by
            // about 8 iterations.
            if (Math.abs(successTileAreaPercentage - failTileAreaPercentage) < 1) {
              // Theoretically, the calculated size should fit into the browser
              // window, but in practice sometimes it's a hair too big, the shrink
              // multiplier is meant to compensate.
              var shrink = INSTANCE.settings.mobile ? .96 : .98;
              var finalSize = {
                height: Math.floor(tileSize.height * shrink),
                width: Math.floor(tileSize.width * shrink),
              };
              //log.debug("DISPLAY", "final calculated tile size", finalSize);
              return finalSize;
            }
            else {
              var median = (successTileAreaPercentage + failTileAreaPercentage) / 2;
              var medianResult = INSTANCE.testMedianTileAreaPercentage(tiles, size, median);
              if (medianResult) {
                return reducer(median, failTileAreaPercentage, medianResult);
              }
              else {
                return reducer(successTileAreaPercentage, median, tileSize);
              }
            }
          }
          return reducer(0, 100);
        }

        INSTANCE.getFocusVideoFeedSize = function() {
          var size = INSTANCE.getDisplaySize(true);
          if (INSTANCE.AppState.get('chat') || INSTANCE.AppState.get('focusCrop')) {
            // TODO: No idea why the last addition is needed.
            var height = size.height - INSTANCE.getHeaderHeight() - INSTANCE.configuration.filmstripHeight + 30;
            var width = Math.floor(height * INSTANCE.configuration.horizontalDisplayRatio / INSTANCE.configuration.verticalDisplayRatio);
            var focusSize = {
              height: height,
              width: width,
            }
            //log.debug('DISPLAY', 'focus size', focusSize);
            return focusSize;
          }
          else {
            return size;
          }
        }

        INSTANCE.getFilmstripVideoFeedWidth = function() {
          if (INSTANCE.getSubscriberMode() == 'single') {
            return INSTANCE.configuration.filmstripSize.width;
          }
          var size = INSTANCE.getDisplaySize(true);
          var count = INSTANCE.getVisibleVideoFeedsCount();
          var totalWidth = (INSTANCE.configuration.filmstripSize.width * count) + (INSTANCE.configuration.filmstripMargin * count * 2);
          var width;
          if ((totalWidth + (INSTANCE.configuration.filmstripMargin * 4)) > size.width) {
            width = INSTANCE.configuration.filmstripSize.height;
          }
          else {
            width = INSTANCE.configuration.filmstripSize.width;
          }
          //log.debug('DISPLAY', 'filmstrip width', width);
          return width;
        }

        INSTANCE.reqres.setHandler(INSTANCE._instance + ":feed:width", function() {
          return INSTANCE.getFilmstripVideoFeedWidth();
        });

        INSTANCE.reqres.setHandler("feed:count", function() {
          return INSTANCE.getVisibleVideoFeedsCount();
        });

        INSTANCE.reqres.setHandler("feed:count:other", function(userId) {
          return INSTANCE.getOtherVideoFeedsCount(userId);
        });

        INSTANCE.reqres.setHandler("chat:width", function() {
          return INSTANCE.getChatWidth();
        });

        INSTANCE.reqres.setHandler("focus:state", function() {
          return INSTANCE.isFocusState();
        });

        INSTANCE.reqres.setHandler("focus:can", function(feed) {
          return INSTANCE.isFocusState() && INSTANCE.getSubscriberMode() == 'multiple';
        });

        INSTANCE.reqres.setHandler("focus:find", function() {
          return INSTANCE.findFocusUser();
        });

        INSTANCE.reqres.setHandler("floor:find", function() {
          return INSTANCE.findFloorUser();
        });

        INSTANCE.reqres.setHandler("subscriber:mode", function() {
          return INSTANCE.getSubscriberMode();
        });

        INSTANCE.reqres.setHandler("feed:self-subscribe", function(userId) {
          return INSTANCE.isSelfSubscribeFeed(userId)
        });

        INSTANCE.getChatSize = function() {
          var size = INSTANCE.getDisplaySize(true);
          var margin = INSTANCE.isFocusState() ? INSTANCE.configuration.filmstripHeight : 0;
          var props = {
            chatHeight: size.height - margin - (INSTANCE.configuration.chatMargin * 2),
          }
          return props;
        }

        INSTANCE.resizeLayout = function() {
          var displaySize = INSTANCE.getDisplaySize();
          var absDisplaySize = INSTANCE.getDisplaySize(true);
          var tileSize = INSTANCE.getTileVideoFeedSize();
          var focusSize = INSTANCE.getFocusVideoFeedSize();
          var filmstripWidth = INSTANCE.getFilmstripVideoFeedWidth();
          var isChatOpen = INSTANCE.AppState.get('chat');
          var isFocusCropped = INSTANCE.AppState.get('focusCrop');
          var attrs = {
            tileSize: tileSize,
            focusSize: focusSize,
            filmstripWidth: filmstripWidth,
          }
          INSTANCE.VideoFeeds.each(function(feed) {
            feed.set(attrs);
          });
          var chatProps = INSTANCE.getChatSize();
          var focusAttrs = {};
          var chatAttrs = {
            height: chatProps.chatHeight,
          };
          var filmstripAttrs = {};
          switch (INSTANCE.configuration.filmstripPlacement) {
            case 'top':
              filmstripAttrs.top = 0;
              break;
            default:
              filmstripAttrs.bottom = 0;
              break;
          }
          if (isChatOpen || isFocusCropped) {
            var chatWidthAdjustment = isChatOpen ? INSTANCE.getChatWidth() : 0;
            var marginSize = (absDisplaySize.width - chatWidthAdjustment - focusSize.width) / 2;
            focusAttrs['margin-left'] = marginSize;
            focusAttrs['margin-right'] = marginSize;
            var paddingTop;
            switch (INSTANCE.configuration.filmstripPlacement) {
              case 'top':
                paddingTop = INSTANCE.configuration.filmstripHeight;
                paddingBottom = 0;
                break;
              default:
                paddingTop = 0;
                paddingBottom = INSTANCE.configuration.filmstripHeight;
                break;
            }
            focusAttrs['padding-top'] = paddingTop;
            focusAttrs['padding-bottom'] = paddingBottom;

            if (INSTANCE.AppState.get('chat')) {
              var view = INSTANCE.getView();
              if (INSTANCE.isFocusState()) {
                switch (INSTANCE.configuration.filmstripPlacement) {
                  case 'top':
                    chatAttrs['margin-top'] = INSTANCE.configuration.filmstripHeight + INSTANCE.configuration.feedTileMargin;
                    break;
                  default:
                    chatAttrs['margin-top'] = INSTANCE.configuration.feedTileMargin;
                    break;
                }
              }
              else {
                chatAttrs['margin-top'] = INSTANCE.configuration.feedTileMargin;
              }
            }
          }
          else {
            focusAttrs['margin-left'] = 'auto';
            focusAttrs['margin-right'] = 'auto';
            focusAttrs['padding-top'] = 0;
          }
          var attrs = {
            filmstripAttrs: filmstripAttrs,
            focusCssAttrs: focusAttrs,
            chatCssAttrs: chatAttrs,
            displaySize: displaySize,
          }
          INSTANCE.AppState.set('layout', attrs);
          if (INSTANCE.UserVideosFilmstripLayoutInstance) {
            INSTANCE.UserVideosFilmstripLayoutInstance.updateLayout();
          }
          // TODO: This should be handled by the Chat view.
          if (INSTANCE.ChatInstance) {
            INSTANCE.ChatInstance.scrollMessages();
          }
          INSTANCE.Vent.trigger("layout:resize");
        };

        INSTANCE.getSubscriberMode = function() {
          return INSTANCE.AppState.get('subscriberMode');
        }

        return INSTANCE;
      }

    });
  }

  return ControllerFactory;

});

