// TODO(fancycode): Should load through AMD if possible.
/* global SimpleWebRTC, OC, OCA: false */

var webrtc;
var guestNamesTable = {};
var spreedMappingTable = {};
var spreedPeerConnectionTable = [];

(function(OCA, OC) {
	'use strict';

	OCA.SpreedMe = OCA.SpreedMe || {};

	var previousUsersInRoom = [];
	var usersInCallMapping = {};
	var ownPeer = null;
	var ownScreenPeer = null;
	var hasLocalMedia = false;
	var selfInCall = 0;  // OCA.SpreedMe.app.FLAG_DISCONNECTED, not available yet.
	var delayedCreatePeer = [];

	function updateParticipantsUI(currentUsersNo) {
		'use strict';
		if (!currentUsersNo) {
			currentUsersNo = 1;
		}

		var $appContentElement = $(OCA.SpreedMe.app.mainCallElementSelector),
			participantsClass = 'participants-' + currentUsersNo,
			hadScreensharing = $appContentElement.hasClass('screensharing'),
			hadSidebar = $appContentElement.hasClass('with-app-sidebar');
		if (!$appContentElement.hasClass(participantsClass)) {
			$appContentElement.attr('class', '').addClass(participantsClass);
			if (currentUsersNo > 1) {
				$appContentElement.addClass('incall');
			} else {
				$appContentElement.removeClass('incall');
			}

			if (hadScreensharing) {
				$appContentElement.addClass('screensharing');
			}

			if (hadSidebar) {
				$appContentElement.addClass('with-app-sidebar');
			}
		}
	}

	function createScreensharingPeer(signaling, sessionId) {
		var currentSessionId = signaling.getSessionid();
		var useMcu = signaling.hasFeature("mcu");

		if (useMcu && !webrtc.webrtc.getPeers(currentSessionId, 'screen').length) {
			if (ownScreenPeer) {
				ownScreenPeer.end();
			}

			// Create own publishing stream.
			ownScreenPeer = webrtc.webrtc.createPeer({
				id: currentSessionId,
				type: 'screen',
				sharemyscreen: true,
				enableDataChannels: false,
				receiveMedia: {
					offerToReceiveAudio: 0,
					offerToReceiveVideo: 0
				},
				broadcaster: currentSessionId,
			});
			webrtc.emit('createdPeer', ownScreenPeer);
			ownScreenPeer.start();
		}

		if (sessionId === currentSessionId) {
			return;
		}

		if (useMcu) {
			// TODO(jojo): Already create peer object to avoid duplicate offers.
			// TODO(jojo): We should use "requestOffer" as with regular
			// audio/video peers. Not possible right now as there is no way
			// for clients to know that screensharing is active and an offer
			// from the MCU should be requested.
			webrtc.connection.sendOffer(sessionId, "screen");
		} else if (!useMcu) {
			var screenPeers = webrtc.webrtc.getPeers(sessionId, 'screen');
			var screenPeerSharedTo = screenPeers.find(function(screenPeer) {
				return screenPeer.sharemyscreen === true;
			});
			if (!screenPeerSharedTo) {
				var peer = webrtc.webrtc.createPeer({
					id: sessionId,
					type: 'screen',
					sharemyscreen: true,
					enableDataChannels: false,
					receiveMedia: {
						offerToReceiveAudio: 0,
						offerToReceiveVideo: 0
					},
					broadcaster: currentSessionId,
				});
				webrtc.emit('createdPeer', peer);
				peer.start();
			}
		}
	}

	function checkStartPublishOwnPeer(signaling) {
		'use strict';
		var currentSessionId = signaling.getSessionid();
		if (!hasLocalMedia || webrtc.webrtc.getPeers(currentSessionId, 'video').length) {
			// No media yet or already publishing.
			return;
		}

		if (ownPeer) {
			OCA.SpreedMe.webrtc.removePeers(ownPeer.id);
			OCA.SpreedMe.speakers.remove(ownPeer.id, true);
			OCA.SpreedMe.videos.remove(ownPeer.id);
			delete spreedMappingTable[ownPeer.id];
			ownPeer.end();
		}

		// Create own publishing stream.
		ownPeer = webrtc.webrtc.createPeer({
			id: currentSessionId,
			type: "video",
			enableDataChannels: true,
			receiveMedia: {
				offerToReceiveAudio: 0,
				offerToReceiveVideo: 0
			},
			sendVideoIfAvailable: signaling.getSendVideoIfAvailable()
		});
		webrtc.emit('createdPeer', ownPeer);
		ownPeer.start();
	}

	function userHasStreams(user) {
		var flags = user;
		if (flags.hasOwnProperty('inCall')) {
			flags = flags.inCall;
		}
		flags = flags || OCA.SpreedMe.app.FLAG_DISCONNECTED;
		var REQUIRED_FLAGS = OCA.SpreedMe.app.FLAG_WITH_AUDIO | OCA.SpreedMe.app.FLAG_WITH_VIDEO;
		return (flags & REQUIRED_FLAGS) !== 0;
	}

	function usersChanged(signaling, newUsers, disconnectedSessionIds) {
		'use strict';
		var currentSessionId = signaling.getSessionid();

		var useMcu = signaling.hasFeature("mcu");
		if (useMcu && newUsers.length) {
			checkStartPublishOwnPeer(signaling);
		}

		newUsers.forEach(function(user) {
			if (!user.inCall) {
				return;
			}

			// TODO(fancycode): Adjust property name of internal PHP backend to be all lowercase.
			var sessionId = user.sessionId || user.sessionid;
			if (!sessionId || sessionId === currentSessionId || previousUsersInRoom.indexOf(sessionId) !== -1) {
				return;
			}

			previousUsersInRoom.push(sessionId);

			// TODO(fancycode): Adjust property name of internal PHP backend to be all lowercase.
			spreedMappingTable[sessionId] = user.userId || user.userid;

			var videoView = OCA.SpreedMe.videos.videoViews[sessionId];
			if (!videoView) {
				OCA.SpreedMe.videos.add(sessionId);
			}

			var createPeer = function() {
				var peer = webrtc.webrtc.createPeer({
					id: sessionId,
					type: "video",
					enableDataChannels: true,
					receiveMedia: {
						offerToReceiveAudio: 1,
						offerToReceiveVideo: 1
					},
					sendVideoIfAvailable: signaling.getSendVideoIfAvailable()
				});
				webrtc.emit('createdPeer', peer);
				peer.start();
			};

			if (!webrtc.webrtc.getPeers(sessionId, 'video').length) {
				if (useMcu) {
					// TODO(jojo): Already create peer object to avoid duplicate offers.
					webrtc.connection.requestOffer(user, "video");
				} else if (userHasStreams(selfInCall) && (!userHasStreams(user) || sessionId < currentSessionId)) {
					// To avoid overloading the user joining a room (who previously called
					// all the other participants), we decide who calls who by comparing
					// the session ids of the users: "larger" ids call "smaller" ones.
					console.log("Starting call with", user);
					createPeer();
				} else if (userHasStreams(selfInCall) && userHasStreams(user) && sessionId > currentSessionId) {
					// If the remote peer is not aware that it was disconnected
					// from the current peer the remote peer will not send a new
					// offer; thus, if the current peer does not receive a new
					// offer in a reasonable time, the current peer calls the
					// remote peer instead of waiting to be called to
					// reestablish the connection.
					delayedCreatePeer[sessionId] = setTimeout(function() {
						createPeer();
					}, 10000);
				}
			}

			//Send shared screen to new participants
			if (webrtc.getLocalScreen()) {
				createScreensharingPeer(signaling, sessionId);
			}
		});

		disconnectedSessionIds.forEach(function(sessionId) {
			console.log('XXX Remove peer', sessionId);
			OCA.SpreedMe.webrtc.removePeers(sessionId);
			OCA.SpreedMe.speakers.remove(sessionId, true);
			OCA.SpreedMe.videos.remove(sessionId);
			delete spreedMappingTable[sessionId];
			delete guestNamesTable[sessionId];
			if (delayedCreatePeer[sessionId]) {
				clearTimeout(delayedCreatePeer[sessionId]);
				delete delayedCreatePeer[sessionId];
			}
		});

		previousUsersInRoom = previousUsersInRoom.diff(disconnectedSessionIds);
		updateParticipantsUI(previousUsersInRoom.length + 1);
	}

	function usersInCallChanged(signaling, users) {
		// The passed list are the users that are currently in the room,
		// i.e. that are in the call and should call each other.
		var currentSessionId = signaling.getSessionid();
		var currentUsersInRoom = [];
		var userMapping = {};
		selfInCall = OCA.SpreedMe.app.FLAG_DISCONNECTED;
		var sessionId;
		for (sessionId in users) {
			if (!users.hasOwnProperty(sessionId)) {
				continue;
			}
			var user = users[sessionId];
			if (!user.inCall) {
				continue;
			}

			if (sessionId === currentSessionId) {
				selfInCall = user.inCall;
				continue;
			}

			currentUsersInRoom.push(sessionId);
			userMapping[sessionId] = user;
		}

		if (!selfInCall) {
			// Own session is no longer in the call, disconnect from all others.
			usersChanged(signaling, [], previousUsersInRoom);
			return;
		}

		var newSessionIds = currentUsersInRoom.diff(previousUsersInRoom);
		var disconnectedSessionIds = previousUsersInRoom.diff(currentUsersInRoom);
		var newUsers = [];
		newSessionIds.forEach(function(sessionId) {
			newUsers.push(userMapping[sessionId]);
		});
		if (newUsers.length || disconnectedSessionIds.length) {
			usersChanged(signaling, newUsers, disconnectedSessionIds);
		}
	}

	/**
	 * @param {OCA.Talk.Application} app
	 */
	function initWebRTC(app) {
		Array.prototype.diff = function(a) {
			return this.filter(function(i) {
				return a.indexOf(i) < 0;
			});
		};

		var signaling = app.signaling;
		signaling.on('usersLeft', function(users) {
			users.forEach(function(user) {
				delete usersInCallMapping[user];
			});
			usersChanged(signaling, [], users);
		});
		signaling.on('usersChanged', function(users) {
			users.forEach(function(user) {
				var sessionId = user.sessionId || user.sessionid;
				usersInCallMapping[sessionId] = user;
			});
			usersInCallChanged(signaling, usersInCallMapping);
		});
		signaling.on('usersInRoom', function(users) {
			usersInCallMapping = {};
			users.forEach(function(user) {
				var sessionId = user.sessionId || user.sessionid;
				usersInCallMapping[sessionId] = user;
			});
			usersInCallChanged(signaling, usersInCallMapping);
		});
		signaling.on('leaveCall', function (token, reconnect) {
			// When the MCU is used and there is a connection error the call is
			// left and then joined again to perform the reconnection. In those
			// cases the call should be kept active from the point of view of
			// WebRTC.
			if (reconnect) {
				return;
			}

			webrtc.leaveCall();
		});

		signaling.on('message', function (message) {
			if (message.type !== 'offer') {
				return;
			}

			var peers = OCA.SpreedMe.webrtc.webrtc.peers;
			var stalePeer = peers.find(function(peer) {
				if (peer.sharemyscreen) {
					return false;
				}

				return peer.id === message.from && peer.type === message.roomType && peer.sid !== message.sid;
			});

			if (stalePeer) {
				stalePeer.end();

				if (message.roomType === 'video') {
					OCA.SpreedMe.speakers.remove(stalePeer.id, true);
					OCA.SpreedMe.videos.remove(stalePeer.id);
				}
			}

			if (message.roomType === 'video' && delayedCreatePeer[message.from]) {
				clearTimeout(delayedCreatePeer[message.from]);
				delete delayedCreatePeer[message.from];
			}

			if (!selfInCall) {
				console.log('Offer received when not in the call, ignore');

				message.type = 'offer-to-ignore';
			}

			// MCU screen offers do not include the "broadcaster" property,
			// which is expected by SimpleWebRTC in screen offers from a remote
			// peer, so it needs to be explicitly added.
			if (signaling.hasFeature("mcu") && message.roomType === 'screen') {
				message.broadcaster = message.from;
			}
		});

		webrtc = new SimpleWebRTC({
			localVideoEl: 'localVideo',
			remoteVideosEl: '',
			autoRequestMedia: true,
			debug: false,
			media: {
				audio: true,
				video: true
			},
			autoAdjustMic: false,
			audioFallback: true,
			detectSpeakingEvents: true,
			connection: signaling,
			enableDataChannels: true,
			nick: OC.getCurrentUser().displayName
		});
		if (signaling.hasFeature('mcu')) {
			// Force "Plan-B" semantics if the MCU is used, which doesn't support
			// "Unified Plan" with SimpleWebRTC yet.
			webrtc.webrtc.config.peerConnectionConfig.sdpSemantics = 'plan-b';
		}
		OCA.SpreedMe.webrtc = webrtc;

		signaling.on('pullMessagesStoppedOnFail', function() {
			// Force leaving the call in WebRTC; when pulling messages stops due
			// to failures the room is left, and leaving the room indirectly
			// runs signaling.leaveCurrentCall(), but if the signaling fails to
			// leave the call (which is likely due to the messages failing to be
			// received) no event will be triggered and the call will not be
			// left from WebRTC point of view.
			webrtc.leaveCall();
		});

		OCA.SpreedMe.webrtc.startMedia = function (token) {
			webrtc.joinCall(token);
		};

		var spreedListofSpeakers = {};
		var spreedListofSharedScreens = {};
		var latestSpeakerId = null;
		var unpromotedSpeakerId = null;
		var latestScreenId = null;
		var screenSharingActive = false;

		window.addEventListener('resize', function() {
			if (screenSharingActive) {
				$('#screens').children('video').each(function() {
					$(this).width('100%');
					$(this).height($('#screens').height());
				});
			}
		});

		var sendDataChannelToAll = function(channel, message, payload) {
			// If running with MCU, the message must be sent through the
			// publishing peer and will be distributed by the MCU to subscribers.
			var conn = OCA.SpreedMe.webrtc.connection;
			if (ownPeer && conn.hasFeature && conn.hasFeature('mcu')) {
				ownPeer.sendDirectly(channel, message, payload);
				return;
			}
			OCA.SpreedMe.webrtc.sendDirectlyToAll(channel, message, payload);
		};

		OCA.SpreedMe.videos = {
			videoViews: [],
			add: function(id) {
				if (!(typeof id === 'string' || id instanceof String)) {
					return;
				}

				var user = usersInCallMapping[id];
				if (user && !userHasStreams(user)) {
					console.log("User has no stream", id);
				}

				var userId = spreedMappingTable[id];

				var videoView = new OCA.Talk.Views.VideoView({
					peerId: id
				});

				// When the MCU is used and the other participant has no streams
				// or when no MCU is used and neither the local participant nor
				// the other one has no streams there will be no Peer for that
				// other participant, so the VideoView status will not be
				// modified later and thus it needs to be fully set now.
				if ((signaling.hasFeature('mcu') && user && !userHasStreams(user)) ||
						(!signaling.hasFeature('mcu') && user && !userHasStreams(user) && !hasLocalMedia)) {
					videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.COMPLETED);
					videoView.setAudioAvailable(false);
					videoView.setVideoAvailable(false);
				}

				videoView.setParticipant(userId);
				videoView.setScreenAvailable(!!spreedListofSharedScreens[id]);

				OCA.SpreedMe.videos.videoViews[id] = videoView;

				videoView.$el.prependTo($('#videos'));

				return videoView;
			},
			remove: function(id) {
				if (!(typeof id === 'string' || id instanceof String)) {
					return;
				}

				if (!OCA.SpreedMe.videos.videoViews[id]) {
					return;
				}

				OCA.SpreedMe.videos.videoViews[id].$el.remove();

				delete OCA.SpreedMe.videos.videoViews[id];
			},
			addPeer: function(peer) {
				var signaling = OCA.SpreedMe.app.signaling;
				if (peer.id === webrtc.connection.getSessionid()) {
					console.log("Not adding video for own peer", peer);
					OCA.SpreedMe.videos.startSendingNick(peer);
					return;
				}

				var videoView = OCA.SpreedMe.videos.videoViews[peer.id];
				if (!videoView) {
					videoView = OCA.SpreedMe.videos.add(peer.id);
				}

				// Initialize ice restart counter for peer
				spreedPeerConnectionTable[peer.id] = 0;

				peer.pc.addEventListener('iceconnectionstatechange', function () {
					var userId = spreedMappingTable[peer.id];

					switch (peer.pc.iceConnectionState) {
						case 'checking':
							console.log('Connecting to peer...');

							videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.CHECKING);
							break;
						case 'connected':
						case 'completed': // on caller side
							console.log('Connection established.');

							if (peer.pc.iceConnectionState === 'connected') {
								videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.CONNECTED);
							} else {
								videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.COMPLETED);
							}

							// Ensure that the peer name is shown, as the name
							// indicator for registered users without microphone
							// nor camera will not be updated later.
							if (userId && userId.length) {
								videoView.setParticipant(userId, peer.nick);
							}

							// Send the current information about the video and microphone state
							if (!OCA.SpreedMe.webrtc.webrtc.isVideoEnabled()) {
								OCA.SpreedMe.webrtc.emit('videoOff');
							} else {
								OCA.SpreedMe.webrtc.emit('videoOn');
							}
							if (!OCA.SpreedMe.webrtc.webrtc.isAudioEnabled()) {
								OCA.SpreedMe.webrtc.emit('audioOff');
							} else {
								OCA.SpreedMe.webrtc.emit('audioOn');
							}
							if (!OC.getCurrentUser()['uid']) {
								var currentGuestNick = localStorage.getItem("nick");
								sendDataChannelToAll('status', 'nickChanged', currentGuestNick);
							}

							// Reset ice restart counter for peer
							if (spreedPeerConnectionTable[peer.id] > 0) {
								spreedPeerConnectionTable[peer.id] = 0;
							}
							break;
						case 'disconnected':
							console.log('Disconnected.');

							videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.DISCONNECTED);

							setTimeout(function() {
								if (peer.pc.iceConnectionState !== 'disconnected') {
									return;
								}

								videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.DISCONNECTED_LONG);

								if (!signaling.hasFeature("mcu")) {
									// ICE failures will be handled in "iceFailed"
									// below for MCU installations.

									// If the peer is still disconnected after 5 seconds we try ICE restart.
									if (spreedPeerConnectionTable[peer.id] < 5) {
										if (peer.pc.localDescription.type === 'offer' &&
											peer.pc.signalingState === 'stable') {
											spreedPeerConnectionTable[peer.id] ++;
											console.log('ICE restart.');
											peer.icerestart();
										}
									}
								}
							}, 5000);
							break;
						case 'failed':
							console.log('Connection failed.');

							videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.FAILED);

							if (!signaling.hasFeature("mcu")) {
								// ICE failures will be handled in "iceFailed"
								// below for MCU installations.
								if (spreedPeerConnectionTable[peer.id] < 5) {
									if (peer.pc.localDescription.type === 'offer' &&
										peer.pc.signalingState === 'stable') {
										spreedPeerConnectionTable[peer.id] ++;
										console.log('ICE restart.');
										peer.icerestart();
									}
								} else {
									console.log('ICE failed after 5 tries.');

									videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.FAILED_NO_RESTART);
								}
							}
							break;
						case 'closed':
							console.log('Connection closed.');

							videoView.setConnectionStatus(OCA.Talk.Views.VideoView.ConnectionStatus.CLOSED);
							break;
					}

					OCA.SpreedMe.speakers.updateVideoContainerDummyIfLatestSpeaker(peer.id);
				});
			},
			// The nick name below the avatar is distributed through the
			// DataChannel of the PeerConnection and only sent once during
			// establishment. For the MCU case, the sending PeerConnection
			// is created once and then never changed when more participants
			// join. For this, we periodically send the nick to all other
			// participants through the sending PeerConnection.
			//
			// TODO: The name for the avatar should come from the participant
			// list which already has all information and get rid of using the
			// DataChannel for this.
			startSendingNick: function(peer) {
				if (!signaling.hasFeature("mcu")) {
					return;
				}

				OCA.SpreedMe.videos.stopSendingNick(peer);
				peer.nickInterval = setInterval(function() {
					var payload;
					var user = OC.getCurrentUser();
					if (!user.uid) {
						payload = localStorage.getItem("nick");
					} else {
						payload = {
							"name": user.displayName,
							"userid": user.uid
						};
					}
					peer.sendDirectly('status', "nickChanged", payload);
				}, 1000);
			},
			stopSendingNick: function(peer) {
				if (!peer.nickInterval) {
					return;
				}

				clearInterval(peer.nickInterval);
				peer.nickInterval = null;
			}
		};

		OCA.SpreedMe.speakers = {
			switchVideoToId: function(id) {
				if (screenSharingActive || latestSpeakerId === id) {
					return;
				}

				var videoView = OCA.SpreedMe.videos.videoViews[id];
				if (!videoView) {
					console.warn('promote: no video found for ID', id);
					return;
				}

				var oldVideoView = OCA.SpreedMe.videos.videoViews[latestSpeakerId];
				if (oldVideoView) {
					oldVideoView.setPromoted(false);
				}

				videoView.setPromoted(true);
				OCA.SpreedMe.speakers.updateVideoContainerDummy(id);

				latestSpeakerId = id;
			},
			unpromoteLatestSpeaker: function() {
				if (latestSpeakerId) {
					var oldVideoView = OCA.SpreedMe.videos.videoViews[latestSpeakerId];
					if (oldVideoView) {
						oldVideoView.setPromoted(false);
					}

					unpromotedSpeakerId = latestSpeakerId;
					latestSpeakerId = null;
					$('.videoContainer-dummy').remove();
				}
			},
			updateVideoContainerDummyIfLatestSpeaker: function(id) {
				if (latestSpeakerId !== id) {
					return;
				}

				OCA.SpreedMe.speakers.updateVideoContainerDummy(id);
			},
			updateVideoContainerDummy: function(id) {
				$('.videoContainer-dummy').remove();

				var videoView = OCA.SpreedMe.videos.videoViews[id];
				if (videoView) {
					videoView.$el.after(videoView.newDummyVideoContainer());
				}
			},
			add: function(id, notPromote) {
				if (!(typeof id === 'string' || id instanceof String)) {
					return;
				}

				if (notPromote) {
					spreedListofSpeakers[id] = 1;
					return;
				}

				spreedListofSpeakers[id] = (new Date()).getTime();

				var videoView = OCA.SpreedMe.videos.videoViews[id];
				if (videoView) {
					videoView.setSpeaking(true);
				}

				if (latestSpeakerId === id) {
					return;
				}

				OCA.SpreedMe.speakers.switchVideoToId(id);
			},
			remove: function(id, enforce) {
				if (!(typeof id === 'string' || id instanceof String)) {
					return;
				}

				if (enforce) {
					delete spreedListofSpeakers[id];
				}

				var videoView = OCA.SpreedMe.videos.videoViews[id];
				if (videoView) {
					videoView.setSpeaking(false);
				}

				if (latestSpeakerId !== id) {
					return;
				}

				var mostRecentTime = 0,
					mostRecentId = null;
				for (var currentId in spreedListofSpeakers) {
					// skip loop if the property is from prototype
					if (!spreedListofSpeakers.hasOwnProperty(currentId)) {
						continue;
					}

					// skip non-string ids
					if (!(typeof currentId === 'string' || currentId instanceof String)) {
						continue;
					}

					var currentTime = spreedListofSpeakers[currentId];
					if (currentTime > mostRecentTime && OCA.SpreedMe.videos.videoViews[currentId]) {
						mostRecentTime = currentTime;
						mostRecentId = currentId;
					}
				}

				if (mostRecentId !== null) {
					OCA.SpreedMe.speakers.switchVideoToId(mostRecentId);
				} else if (enforce === true) {
					// if there is no mostRecentId available, there is no user left in call
					// remove the remaining dummy container then too
					OCA.SpreedMe.speakers.unpromoteLatestSpeaker();
					$('.videoContainer-dummy').remove();
				}
			}
		};

		OCA.SpreedMe.sharedScreens = {
			screenViews: [],
			switchScreenToId: function(id) {
				var screenView = OCA.SpreedMe.sharedScreens.screenViews[id];
				if (!screenView) {
					console.warn('promote: no screen video found for ID', id);
					return;
				}

				if(latestScreenId === id) {
					return;
				}

				for (var currentId in spreedListofSharedScreens) {
					// skip loop if the property is from prototype
					if (!spreedListofSharedScreens.hasOwnProperty(currentId)) {
						continue;
					}

					// skip non-string ids
					if (!(typeof currentId === 'string' || currentId instanceof String)) {
						continue;
					}

					screenView = OCA.SpreedMe.sharedScreens.screenViews[currentId];
					if (currentId === id) {
						screenView.$el.removeClass('hidden');
					} else {
						screenView.$el.addClass('hidden');
					}
				}

				var oldVideoView = OCA.SpreedMe.videos.videoViews[latestScreenId];
				if (oldVideoView) {
					oldVideoView.setScreenVisible(false);
				}
				var videoView = OCA.SpreedMe.videos.videoViews[id];
				if (videoView) {
					videoView.setScreenVisible(true);
				}

				latestScreenId = id;
			},
			add: function(id) {
				if (!(typeof id === 'string' || id instanceof String)) {
					return;
				}

				spreedListofSharedScreens[id] = (new Date()).getTime();

				var currentUser = OCA.SpreedMe.webrtc.connection.getSessionid();
				if (currentUser !== id) {
					var videoView = OCA.SpreedMe.videos.videoViews[id];
					if (videoView) {
						videoView.setScreenAvailable(true);
					}
				}

				OCA.SpreedMe.sharedScreens.switchScreenToId(id);
			},
			remove: function(id) {
				if (!(typeof id === 'string' || id instanceof String)) {
					return;
				}

				var screenView = OCA.SpreedMe.sharedScreens.screenViews[id];
				if (screenView) {
					screenView.$el.remove();

					delete OCA.SpreedMe.sharedScreens.screenViews[id];
				}

				delete spreedListofSharedScreens[id];

				var videoView = OCA.SpreedMe.videos.videoViews[id];
				if (videoView) {
					videoView.setScreenAvailable(false);
				}

				var mostRecentTime = 0,
					mostRecentId = null;
				for (var currentId in spreedListofSharedScreens) {
					// skip loop if the property is from prototype
					if (!spreedListofSharedScreens.hasOwnProperty(currentId)) {
						continue;
					}

					// skip non-string ids
					if (!(typeof currentId === 'string' || currentId instanceof String)) {
						continue;
					}

					var currentTime = spreedListofSharedScreens[currentId];
					if (currentTime > mostRecentTime) {
						mostRecentTime = currentTime;
						mostRecentId = currentId;
					}
				}

				if (mostRecentId !== null) {
					OCA.SpreedMe.sharedScreens.switchScreenToId(mostRecentId);
				}
			}
		};

		OCA.SpreedMe.webrtc.on('createdPeer', function (peer) {
			console.log('PEER CREATED', peer);
			if (peer.type === 'video') {
				OCA.SpreedMe.videos.addPeer(peer);
				// Make sure required data channels exist for all peers. This
				// is required for peers that get created by SimpleWebRTC from
				// received "Offer" messages. Otherwise the "channelMessage"
				// will not be called.
				peer.getDataChannel('status');
			}
		});

		function checkPeerMedia(peer, track, mediaType) {
			var defer = $.Deferred();
			peer.pc.getStats(track).then(function(stats) {
				var result = false;
				stats.forEach(function(statsReport) {
					if (result || statsReport.mediaType !== mediaType || !statsReport.hasOwnProperty('bytesReceived')) {
						return;
					}

					if (statsReport.bytesReceived > 0) {
						OCA.SpreedMe.webrtc.emit('unmute', {
							id: peer.id,
							name: mediaType
						});
						result = true;
					}
				});
				if (result) {
					defer.resolve();
				} else {
					defer.reject();
				}
			});
			return defer;
		}

		function stopPeerCheckMedia(peer) {
			if (peer.check_audio_interval) {
				clearInterval(peer.check_audio_interval);
				peer.check_audio_interval = null;
			}
			if (peer.check_video_interval) {
				clearInterval(peer.check_video_interval);
				peer.check_video_interval = null;
			}
			OCA.SpreedMe.videos.stopSendingNick(peer);
		}

		function startPeerCheckMedia(peer, stream) {
			stopPeerCheckMedia(peer);
			peer.check_video_interval = setInterval(function() {
				stream.getVideoTracks().forEach(function(video) {
					checkPeerMedia(peer, video, 'video').then(function() {
						clearInterval(peer.check_video_interval);
						peer.check_video_interval = null;
					});
				});
			}, 1000);
			peer.check_audio_interval = setInterval(function() {
				stream.getAudioTracks().forEach(function(audio) {
					checkPeerMedia(peer, audio, 'audio').then(function() {
						clearInterval(peer.check_audio_interval);
						peer.check_audio_interval = null;
					});
				});
			}, 1000);
		}

		OCA.SpreedMe.webrtc.on('peerStreamAdded', function (peer) {
			// With the MCU, a newly subscribed stream might not get the
			// "audioOn"/"videoOn" messages as they are only sent when
			// a user starts publishing. Instead wait for initial data
			// and trigger events locally.
			if (!OCA.SpreedMe.app.signaling.hasFeature("mcu")) {
				return;
			}

			startPeerCheckMedia(peer, peer.stream);
		});

		OCA.SpreedMe.webrtc.on('peerStreamRemoved', function (peer) {
			stopPeerCheckMedia(peer);
		});

		OCA.SpreedMe.webrtc.on('localScreenStopped', function() {
			app.disableScreensharingButton();
		});

		var forceReconnect = function(signaling, flags) {
			if (ownPeer) {
				OCA.SpreedMe.webrtc.removePeers(ownPeer.id);
				OCA.SpreedMe.speakers.remove(ownPeer.id, true);
				OCA.SpreedMe.videos.remove(ownPeer.id);
				delete spreedMappingTable[ownPeer.id];
				ownPeer.end();
				ownPeer = null;
			}

			usersChanged(signaling, [], previousUsersInRoom);
			usersInCallMapping = {};
			previousUsersInRoom = [];

			// Reconnects with a new session id will trigger "usersChanged"
			// with the users in the room and that will re-establish the
			// peerconnection streams.
			// If flags are undefined the current call flags are used.
			signaling.forceReconnect(true, flags);
		};

		OCA.SpreedMe.webrtc.webrtc.on('videoOn', function () {
			var signaling = OCA.SpreedMe.app.signaling;
			if (signaling.getSendVideoIfAvailable()) {
				return;
			}

			// When enabling the local video if the video is not being sent a
			// reconnection is forced to start sending it.
			signaling.setSendVideoIfAvailable(true);

			var flags = signaling.getCurrentCallFlags();
			flags |= OCA.SpreedMe.app.FLAG_WITH_VIDEO;

			forceReconnect(signaling, flags);
		});

		OCA.SpreedMe.webrtc.webrtc.on('iceFailed', function (/* peer */) {
			var signaling = OCA.SpreedMe.app.signaling;
			if (!signaling.hasFeature("mcu")) {
				// ICE restarts will be handled by "iceConnectionStateChange"
				// above.
				return;
			}

			// For now assume the connection to the MCU is interrupted on ICE
			// failures and force a reconnection of all streams.
			forceReconnect(signaling);
		});

		var localStreamRequestedTimeout = null;
		var localStreamRequestedTimeoutNotification = null;

		var clearLocalStreamRequestedTimeoutAndHideNotification = function() {
			clearTimeout(localStreamRequestedTimeout);
			localStreamRequestedTimeout = null;

			if (localStreamRequestedTimeoutNotification) {
				OC.Notification.hide(localStreamRequestedTimeoutNotification);
				localStreamRequestedTimeoutNotification = null;
			}
		};

		// In some cases the browser may enter in a faulty state in which
		// "getUserMedia" does not return neither successfully nor with an
		// error. It is not possible to detect this except by guessing when some
		// time passes and the user has not granted nor rejected the media
		// permissions.
		OCA.SpreedMe.webrtc.on('localStreamRequested', function () {
			clearLocalStreamRequestedTimeoutAndHideNotification();

			localStreamRequestedTimeout = setTimeout(function() {
				// FIXME emit an event and handle it as needed instead of
				// calling UI code from here.
				localStreamRequestedTimeoutNotification = OC.Notification.show(t('spreed', 'This is taking longer than expected. Are the media permissions already granted (or rejected)? If yes please restart your browser, as audio and video are failing'), { type: 'error' });
			}, 10000);
		});

		signaling.on('leaveRoom', function(token) {
			if (signaling.currentRoomToken === token) {
				clearLocalStreamRequestedTimeoutAndHideNotification();
			}
		});

		OCA.SpreedMe.webrtc.on('localMediaStarted', function (configuration) {
			console.log('localMediaStarted');

			clearLocalStreamRequestedTimeoutAndHideNotification();

			app.startLocalMedia(configuration);
			hasLocalMedia = true;
			var signaling = OCA.SpreedMe.app.signaling;
			if (signaling.hasFeature("mcu")) {
				checkStartPublishOwnPeer(signaling);
			}
		});

		OCA.SpreedMe.webrtc.on('localMediaError', function(error) {
			console.log('Access to microphone & camera failed', error);

			clearLocalStreamRequestedTimeoutAndHideNotification();

			hasLocalMedia = false;
			var message;
			if ((error.name === "NotSupportedError" &&
					OCA.SpreedMe.webrtc.capabilities.supportRTCPeerConnection) ||
				(error.name === "NotAllowedError" &&
					error.message && error.message.indexOf("Only secure origins") !== -1)) {
				message = t('spreed', 'Access to microphone & camera is only possible with HTTPS');
				message += ': ' + t('spreed', 'Please move your setup to HTTPS');
			} else if (error.name === "NotAllowedError") {
				message = t('spreed', 'Access to microphone & camera was denied');
			} else if(!OCA.SpreedMe.webrtc.capabilities.support) {
				console.log('WebRTC not supported');

				message = t('spreed', 'WebRTC is not supported in your browser');
				message += ': ' + t('spreed', 'Please use a different browser like Firefox or Chrome');
			} else {
				message = t('spreed', 'Error while accessing microphone & camera');
				console.log('Error while accessing microphone & camera: ', error.message || error.name);
			}

			app.startWithoutLocalMedia({audio: false, video: false});
			OC.Notification.show(message, {
				type: 'error',
				timeout: 15,
			});
		});

		OCA.SpreedMe.webrtc.on('channelOpen', function(channel) {
			console.log('%s datachannel is open', channel.label);
		});

		OCA.SpreedMe.webrtc.on('channelMessage', function (peer, label, data) {
			if (label === 'status') {
				if(data.type === 'speaking') {
					OCA.SpreedMe.speakers.add(peer.id);
				} else if(data.type === 'stoppedSpeaking') {
					OCA.SpreedMe.speakers.remove(peer.id);
				} else if(data.type === 'audioOn') {
					OCA.SpreedMe.webrtc.emit('unmute', {id: peer.id, name:'audio'});
				} else if(data.type === 'audioOff') {
					OCA.SpreedMe.webrtc.emit('mute', {id: peer.id, name:'audio'});
				} else if(data.type === 'videoOn') {
					OCA.SpreedMe.webrtc.emit('unmute', {id: peer.id, name:'video'});
				} else if(data.type === 'videoOff') {
					OCA.SpreedMe.webrtc.emit('mute', {id: peer.id, name:'video'});
				} else if (data.type === 'nickChanged') {
					var payload = data.payload || '';
					if (typeof(payload) === 'string') {
						OCA.SpreedMe.webrtc.emit('nick', {id: peer.id, name:data.payload});
						app._messageCollection.updateGuestName(new Hashes.SHA1().hex(peer.id), data.payload);
					} else {
						OCA.SpreedMe.webrtc.emit('nick', {id: peer.id, name: payload.name, userid: payload.userid});
					}
				}
			} else if (label === 'hark') {
				// Ignore messages from hark datachannel
			} else {
				console.log('Uknown message from %s datachannel', label, data);
			}
		});

		OCA.SpreedMe.webrtc.on('videoAdded', function(video, peer) {
			console.log('VIDEO ADDED', peer);
			if (peer.type === 'screen') {
				OCA.SpreedMe.webrtc.emit('screenAdded', video, peer);
				return;
			}

			var videoView = OCA.SpreedMe.videos.videoViews[peer.id];
			if (videoView) {
				var userId = spreedMappingTable[peer.id];
				var guestName = guestNamesTable[peer.id];

				var participantName = peer.nick;
				if (!userId || !userId.length) {
					participantName = peer.nick || guestName;
				}

				videoView.setParticipant(userId, participantName);

				videoView.setVideoElement(video);
			}

			var otherSpeakerPromoted = false;
			for (var key in spreedListofSpeakers) {
				if (spreedListofSpeakers.hasOwnProperty(key) && spreedListofSpeakers[key] > 1) {
					otherSpeakerPromoted = true;
					break;
				}
			}
			if (!otherSpeakerPromoted) {
				OCA.SpreedMe.speakers.add(peer.id);
			} else {
				OCA.SpreedMe.speakers.add(peer.id, true);
			}
		});

		OCA.SpreedMe.webrtc.on('speaking', function(){
			sendDataChannelToAll('status', 'speaking');
			OCA.SpreedMe.app._localVideoView.setSpeaking(true);
		});

		OCA.SpreedMe.webrtc.on('stoppedSpeaking', function(){
			sendDataChannelToAll('status', 'stoppedSpeaking');
			OCA.SpreedMe.app._localVideoView.setSpeaking(false);
		});

		// a peer was removed
		OCA.SpreedMe.webrtc.on('videoRemoved', function(video, peer) {
			var screens;

			if (peer) {
				if (peer.type === 'video') {
					// a removed peer can't speak anymore ;)
					OCA.SpreedMe.speakers.remove(peer.id, true);

					var videoView = OCA.SpreedMe.videos.videoViews[peer.id];
					if (videoView) {
						videoView.setVideoElement(null);
					}
				} else if (peer.type === 'screen') {
					OCA.SpreedMe.sharedScreens.remove(peer.id);
				}
			} else if (video.id === 'localScreen') {
				// SimpleWebRTC notifies about stopped screensharing through
				// the generic "videoRemoved" API, but the stream must be
				// handled differently.
				OCA.SpreedMe.webrtc.emit('localScreenStopped');

				OCA.SpreedMe.sharedScreens.remove(OCA.SpreedMe.webrtc.connection.getSessionid());
			}

			// Check if there are still some screens
			screens = document.getElementById('screens');
			if (!screens || !screens.hasChildNodes()) {
				screenSharingActive = false;
				$(OCA.SpreedMe.app.mainCallElementSelector).removeClass('screensharing');
				if (unpromotedSpeakerId) {
					OCA.SpreedMe.speakers.switchVideoToId(unpromotedSpeakerId);
					unpromotedSpeakerId = null;
				}
			}
		});

		// Send the audio on and off events via data channel
		OCA.SpreedMe.webrtc.on('audioOn', function() {
			sendDataChannelToAll('status', 'audioOn');
		});
		OCA.SpreedMe.webrtc.on('audioOff', function() {
			sendDataChannelToAll('status', 'audioOff');
		});
		OCA.SpreedMe.webrtc.on('videoOn', function() {
			sendDataChannelToAll('status', 'videoOn');
		});
		OCA.SpreedMe.webrtc.on('videoOff', function() {
			sendDataChannelToAll('status', 'videoOff');
		});

		OCA.SpreedMe.webrtc.on('screenAdded', function(video, peer) {
			OCA.SpreedMe.speakers.unpromoteLatestSpeaker();

			screenSharingActive = true;
			$(OCA.SpreedMe.app.mainCallElementSelector).addClass('screensharing');

			var screens = document.getElementById('screens');
			if (screens) {
				var screenView = new OCA.Talk.Views.ScreenView({
					peerId: peer? peer.id: undefined
				});
				screenView.setVideoElement(video);

				if (peer) {
					var participantName = peer.nick || guestNamesTable[peer.id];
					screenView.setParticipantName(participantName);
				}

				screenView.$el.prependTo($('#screens'));

				if (peer) {
					OCA.SpreedMe.sharedScreens.screenViews[peer.id] = screenView;

					OCA.SpreedMe.sharedScreens.add(peer.id);
				} else {
					OCA.SpreedMe.sharedScreens.screenViews[OCA.SpreedMe.webrtc.connection.getSessionid()] = screenView;

					OCA.SpreedMe.sharedScreens.add(OCA.SpreedMe.webrtc.connection.getSessionid());
				}
			}
		});

		// Local screen added.
		OCA.SpreedMe.webrtc.on('localScreenAdded', function(video) {
			OCA.SpreedMe.webrtc.emit('screenAdded', video, null);
			var signaling = OCA.SpreedMe.app.signaling;

			var currentSessionId = signaling.getSessionid();
			for (var sessionId in usersInCallMapping) {
				if (!usersInCallMapping.hasOwnProperty(sessionId)) {
					continue;
				} else if (!usersInCallMapping[sessionId].inCall) {
					continue;
				} else if (sessionId === currentSessionId) {
					// Running with MCU, no need to create screensharing
					// subscriber for client itself.
					continue;
				}

				createScreensharingPeer(signaling, sessionId);
			}
		});

		OCA.SpreedMe.webrtc.on('localScreenStopped', function() {
			var signaling = OCA.SpreedMe.app.signaling;
			if (!signaling.hasFeature('mcu')) {
				// Only need to notify clients here if running with MCU.
				// Otherwise SimpleWebRTC will notify each client on its own.
				return;
			}

			var currentSessionId = signaling.getSessionid();
			OCA.SpreedMe.webrtc.getPeers().forEach(function(existingPeer) {
				if (ownScreenPeer && existingPeer.type === 'screen' && existingPeer.id === currentSessionId) {
					ownScreenPeer = null;
					existingPeer.end();
					signaling.sendRoomMessage({
						roomType: 'screen',
						type: 'unshareScreen'
					});
				}
			});
		});

		// Peer changed nick
		OCA.SpreedMe.webrtc.on('nick', function(data) {
			// Video
			var videoView = OCA.SpreedMe.videos.videoViews[data.id];
			if (videoView) {
				videoView.setParticipant(data.userid, data.name);
			}

			//Screen
			var screenView = OCA.SpreedMe.sharedScreens.screenViews[data.id];
			if (screenView) {
				screenView.setParticipantName(data.name);
			}

			if (!data.userid && data.name) {
				guestNamesTable[data.id] = data.name;
			}

			OCA.SpreedMe.speakers.updateVideoContainerDummyIfLatestSpeaker(data.id);
		});

		// Peer is muted
		OCA.SpreedMe.webrtc.on('mute', function(data) {
			var videoView = OCA.SpreedMe.videos.videoViews[data.id];
			if (!videoView) {
				return;
			}

			if (data.name === 'video') {
				videoView.setVideoAvailable(false);
			} else {
				videoView.setAudioAvailable(false);
			}

			OCA.SpreedMe.speakers.updateVideoContainerDummyIfLatestSpeaker(data.id);
		});

		// Peer is umuted
		OCA.SpreedMe.webrtc.on('unmute', function(data) {
			var videoView = OCA.SpreedMe.videos.videoViews[data.id];
			if (!videoView) {
				return;
			}

			if (data.name === 'video') {
				videoView.setVideoAvailable(true);
			} else {
				videoView.setAudioAvailable(true);
			}

			OCA.SpreedMe.speakers.updateVideoContainerDummyIfLatestSpeaker(data.id);
		});
	}

	OCA.SpreedMe.initWebRTC = initWebRTC;

})(OCA, OC);
