define [
	"base",
	"utils/EventEmitter"
	"ide/colors/ColorManager"
	"ide/review-panel/RangesTracker"
], (App, EventEmitter, ColorManager, RangesTracker) ->
	App.controller "ReviewPanelController", ($scope, $element, ide, $timeout, $http, $modal, event_tracking, localStorage) ->
		$reviewPanelEl = $element.find "#review-panel"

		UserTypes =
			MEMBER:    'member'    # Invited, listed in project.members
			GUEST:     'guest'     # Not invited, but logged in so has a user_id
			ANONYMOUS: 'anonymous' # No user_id

		currentUserType = () ->
			if !ide.$scope.user?.id?
				return UserTypes.ANONYMOUS
			else
				user_id = ide.$scope.user.id
				project = ide.$scope.project
				if project.owner?.id == user_id
					return UserTypes.MEMBER
				for member in project.members
					if member._id == user_id
						return UserTypes.MEMBER
				return UserTypes.GUEST

		$scope.SubViews =
			CUR_FILE : "cur_file"
			OVERVIEW : "overview"

		$scope.UserTCSyncState = UserTCSyncState =
			SYNCED  : "synced"
			PENDING : "pending"

		$scope.reviewPanel =
			trackChangesState: {}
			trackChangesOnForEveryone: false
			trackChangesOnForGuests: false
			trackChangesForGuestsAvailable: false
			entries: {}
			resolvedComments: {}
			hasEntries: false
			subView: $scope.SubViews.CUR_FILE
			openSubView: $scope.SubViews.CUR_FILE
			overview:
				loading: false
				docsCollapsedState: JSON.parse(localStorage("docs_collapsed_state:#{$scope.project_id}")) or {}
			dropdown:
				loading: false
			commentThreads: {}
			resolvedThreadIds: {}
			rendererData: {}
			formattedProjectMembers: {}
			fullTCStateCollapsed: true
			loadingThreads: false
			# All selected changes. If a aggregated change (insertion + deletion) is selection, the two ids
			# will be present. The length of this array will differ from the count below (see explanation).
			selectedEntryIds: [] 
			# A count of user-facing selected changes. An aggregated change (insertion + deletion) will count
			# as only one.
			nVisibleSelectedChanges: 0

		window.addEventListener "beforeunload", () ->
			collapsedStates = {}
			for doc, state of $scope.reviewPanel.overview.docsCollapsedState
				if state
					collapsedStates[doc] = state 
			valToStore = if Object.keys(collapsedStates).length > 0 then JSON.stringify(collapsedStates) else null
			localStorage("docs_collapsed_state:#{$scope.project_id}", valToStore)

		$scope.$on "layout:pdf:linked", (event, state) ->
			$scope.$broadcast "review-panel:layout"

		$scope.$on "layout:pdf:resize", (event, state) ->
			$scope.$broadcast "review-panel:layout", false

		$scope.$on "expandable-text-area:resize", (event) ->
			$timeout () ->
				$scope.$broadcast "review-panel:layout"

		$scope.$on "review-panel:sizes", (e, sizes) ->
			$scope.$broadcast "editor:set-scroll-size", sizes
		
		$scope.$watch "project.features.trackChangesVisible", (visible) ->
			return if !visible?
			if !visible
				$scope.ui.reviewPanelOpen = false

		$scope.$watch "project.members", (members) ->
			$scope.reviewPanel.formattedProjectMembers = {}
			if $scope.project?.owner?
				$scope.reviewPanel.formattedProjectMembers[$scope.project.owner._id] = formatUser($scope.project.owner)
			if $scope.project?.members?
				for member in members
					if member.privileges == "readAndWrite"
						$scope.reviewPanel.formattedProjectMembers[member._id] = formatUser(member)

		$scope.commentState =
			adding: false
			content: ""

		$scope.users = {}

		$scope.reviewPanelEventsBridge = new EventEmitter()
		
		ide.socket.on "new-comment", (thread_id, comment) ->
			thread = getThread(thread_id)
			delete thread.submitting
			thread.messages.push(formatComment(comment))
			$scope.$apply()
			$timeout () ->
				$scope.$broadcast "review-panel:layout"

		ide.socket.on "accept-changes", (doc_id, change_ids) ->
			if doc_id != $scope.editor.open_doc_id
				getChangeTracker(doc_id).removeChangeIds(change_ids)
			else
				$scope.$broadcast "changes:accept", change_ids
			updateEntries(doc_id)
			$scope.$apply () ->
		
		ide.socket.on "resolve-thread", (thread_id, user) ->
			_onCommentResolved(thread_id, user)
			
		ide.socket.on "reopen-thread", (thread_id) ->
			_onCommentReopened(thread_id)
		
		ide.socket.on "delete-thread", (thread_id) ->
			_onThreadDeleted(thread_id)
			$scope.$apply () ->
		
		ide.socket.on "edit-message", (thread_id, message_id, content) ->
			_onCommentEdited(thread_id, message_id, content)
			$scope.$apply () ->
		
		ide.socket.on "delete-message", (thread_id, message_id) ->
			_onCommentDeleted(thread_id, message_id)
			$scope.$apply () ->

		rangesTrackers = {}

		getDocEntries = (doc_id) ->
			$scope.reviewPanel.entries[doc_id] ?= {}
			return $scope.reviewPanel.entries[doc_id]

		getDocResolvedComments = (doc_id) ->
			$scope.reviewPanel.resolvedComments[doc_id] ?= {}
			return $scope.reviewPanel.resolvedComments[doc_id]
		
		getThread = (thread_id) ->
			$scope.reviewPanel.commentThreads[thread_id] ?= { messages: [] }
			return $scope.reviewPanel.commentThreads[thread_id]

		getChangeTracker = (doc_id) ->
			if !rangesTrackers[doc_id]?
				rangesTrackers[doc_id] = new RangesTracker()
				rangesTrackers[doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds
			return rangesTrackers[doc_id]

		scrollbar = {}
		$scope.reviewPanelEventsBridge.on "aceScrollbarVisibilityChanged", (isVisible, scrollbarWidth) ->
			scrollbar = {isVisible, scrollbarWidth}
			updateScrollbar()

		updateScrollbar = () ->
			if scrollbar.isVisible and $scope.reviewPanel.subView == $scope.SubViews.CUR_FILE
				$reviewPanelEl.css "right", "#{ scrollbar.scrollbarWidth }px"
			else
				$reviewPanelEl.css "right", "0"

		$scope.$watch "!ui.reviewPanelOpen && reviewPanel.hasEntries", (open, prevVal) ->
			return if !open?
			$scope.ui.miniReviewPanelVisible = open
			if open != prevVal
				$timeout () -> $scope.$broadcast "review-panel:toggle"

		$scope.$watch "ui.reviewPanelOpen", (open) ->
			return if !open?
			if !open
				# Always show current file when not open, but save current state
				$scope.reviewPanel.openSubView = $scope.reviewPanel.subView
				$scope.reviewPanel.subView = $scope.SubViews.CUR_FILE
			else
				# Reset back to what we had when previously open
				$scope.reviewPanel.subView = $scope.reviewPanel.openSubView
			$timeout () ->
				$scope.$broadcast "review-panel:toggle"
				$scope.$broadcast "review-panel:layout", false

		$scope.$watch "reviewPanel.subView", (view) ->
			return if !view?
			updateScrollbar()
			if view == $scope.SubViews.OVERVIEW
				refreshOverviewPanel()

		$scope.$watch "editor.sharejs_doc", (doc, old_doc) ->
			return if !doc?
			# The open doc range tracker is kept up to date in real-time so
			# replace any outdated info with this
			rangesTrackers[doc.doc_id] = doc.ranges
			rangesTrackers[doc.doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds
			$scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
			if old_doc?
				old_doc.off "flipped_pending_to_inflight"
			doc.on "flipped_pending_to_inflight", () ->
				regenerateTrackChangesId(doc)
			regenerateTrackChangesId(doc)

		$scope.$watch (() ->
			entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}
			permEntries = {}
			for entry, entryData of entries
				if entry not in [ "add-comment", "bulk-actions" ]
					permEntries[entry] = entryData 
			Object.keys(permEntries).length
		), (nEntries) ->
			$scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChangesVisible

		regenerateTrackChangesId = (doc) ->
			old_id = getChangeTracker(doc.doc_id).getIdSeed()
			new_id = RangesTracker.generateIdSeed()
			getChangeTracker(doc.doc_id).setIdSeed(new_id)
			doc.setTrackChangesIdSeeds({pending: new_id, inflight: old_id})
		
		refreshRanges = () ->
			$http.get "/project/#{$scope.project_id}/ranges"
				.then (response) ->
					docs = response.data
					for doc in docs
						if !$scope.reviewPanel.overview.docsCollapsedState[doc.id]?
							$scope.reviewPanel.overview.docsCollapsedState[doc.id] = false
						if doc.id != $scope.editor.open_doc_id # this is kept up to date in real-time, don't overwrite
							rangesTracker = getChangeTracker(doc.id)
							rangesTracker.comments = doc.ranges?.comments or []
							rangesTracker.changes = doc.ranges?.changes or []
						updateEntries(doc.id)

		refreshOverviewPanel = () ->
			$scope.reviewPanel.overview.loading = true
			refreshRanges()
				.then () ->
					$scope.reviewPanel.overview.loading = false
				.catch () ->
					$scope.reviewPanel.overview.loading = false

		$scope.refreshResolvedCommentsDropdown = () ->
			$scope.reviewPanel.dropdown.loading = true
			q = refreshRanges()
			q.then () ->
				$scope.reviewPanel.dropdown.loading = false
			q.catch () ->
				$scope.reviewPanel.dropdown.loading = false
			return q

		updateEntries = (doc_id) ->
			rangesTracker = getChangeTracker(doc_id)
			entries = getDocEntries(doc_id)
			resolvedComments = getDocResolvedComments(doc_id)
			
			changed = false

			# Assume we'll delete everything until we see it, then we'll remove it from this object
			delete_changes = {}
			for id, change of entries
				if id not in [ "add-comment", "bulk-actions" ]
					for entry_id in change.entry_ids
						delete_changes[entry_id] = true 
			for id, change of resolvedComments
				for entry_id in change.entry_ids
					delete_changes[entry_id] = true 

			potential_aggregate = false
			prev_insertion = null

			for change in rangesTracker.changes
				changed = true
				
				if (
					potential_aggregate and 
					change.op.d and 
					change.op.p == prev_insertion.op.p + prev_insertion.op.i.length and
					change.metadata.user_id == prev_insertion.metadata.user_id
				)
					# An actual aggregate op.
					entries[prev_insertion.id].type = "aggregate-change"
					entries[prev_insertion.id].metadata.replaced_content = change.op.d
					entries[prev_insertion.id].entry_ids.push change.id
				else
					entries[change.id] ?= {}
					delete delete_changes[change.id]
					new_entry = {
						type: if change.op.i then "insert" else "delete"
						entry_ids: [ change.id ]
						content: change.op.i or change.op.d
						offset: change.op.p
						metadata: change.metadata
					}
					for key, value of new_entry
						entries[change.id][key] = value

				if change.op.i
					potential_aggregate = true
					prev_insertion = change
				else
					potential_aggregate = false
					prev_insertion = null

				if !$scope.users[change.metadata.user_id]?
					refreshChangeUsers(change.metadata.user_id)

			if rangesTracker.comments.length > 0
				ensureThreadsAreLoaded()

			for comment in rangesTracker.comments
				changed = true
				delete delete_changes[comment.id]
				if $scope.reviewPanel.resolvedThreadIds[comment.op.t]
					new_comment = resolvedComments[comment.id] ?= {}
					delete entries[comment.id]
				else
					new_comment = entries[comment.id] ?= {}
					delete resolvedComments[comment.id]
				new_entry = {
					type: "comment"
					thread_id: comment.op.t
					entry_ids: [ comment.id ]
					content: comment.op.c
					offset: comment.op.p
				}
				for key, value of new_entry
					new_comment[key] = value

			for change_id, _ of delete_changes
				changed = true
				delete entries[change_id]
				delete resolvedComments[change_id]
			
			if changed
				$scope.$broadcast "entries:changed"

		$scope.$on "editor:track-changes:changed", () ->
			doc_id = $scope.editor.open_doc_id
			updateEntries(doc_id)
			$scope.$broadcast "review-panel:recalculate-screen-positions"
			$scope.$broadcast "review-panel:layout"

		$scope.$on "editor:track-changes:visibility_changed", () ->
			$timeout () ->
				$scope.$broadcast "review-panel:layout", false

		$scope.$on "editor:focus:changed", (e, selection_offset_start, selection_offset_end, selection) ->
			doc_id = $scope.editor.open_doc_id
			entries = getDocEntries(doc_id)
			# All selected changes will be added to this array.
			$scope.reviewPanel.selectedEntryIds = []
			# Count of user-visible changes, i.e. an aggregated change will count as one.
			$scope.reviewPanel.nVisibleSelectedChanges = 0
			delete entries["add-comment"]
			delete entries["bulk-actions"]

			if selection
				entries["add-comment"] = {
					type: "add-comment"
					offset: selection_offset_start
					length: selection_offset_end - selection_offset_start
				}
				entries["bulk-actions"] = {
					type: "bulk-actions"
					offset: selection_offset_start
					length: selection_offset_end - selection_offset_start
				}
			
			for id, entry of entries
				isChangeEntryAndWithinSelection = false
				if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
					entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
				else if entry.type == "insert"
					isChangeEntryAndWithinSelection = entry.offset >= selection_offset_start and entry.offset + entry.content.length <= selection_offset_end
					entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
				else if entry.type == "delete"
					isChangeEntryAndWithinSelection = selection_offset_start <= entry.offset <= selection_offset_end
					entry.focused = (entry.offset == selection_offset_start)
				else if entry.type == "aggregate-change"
					isChangeEntryAndWithinSelection = entry.offset >= selection_offset_start and entry.offset + entry.content.length <= selection_offset_end
					entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
				else if entry.type in [ "add-comment", "bulk-actions" ] and selection
					entry.focused = true

				if isChangeEntryAndWithinSelection
					for entry_id in entry.entry_ids
						$scope.reviewPanel.selectedEntryIds.push entry_id
					$scope.reviewPanel.nVisibleSelectedChanges++
			
			$scope.$broadcast "review-panel:recalculate-screen-positions"
			$scope.$broadcast "review-panel:layout"

		$scope.acceptChanges = (change_ids) ->
			_doAcceptChanges change_ids
			event_tracking.sendMB "rp-changes-accepted", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }

		$scope.rejectChanges = (change_ids) ->
			_doRejectChanges change_ids
			event_tracking.sendMB "rp-changes-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }

		_doAcceptChanges = (change_ids) ->
			$http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/accept", { change_ids, _csrf: window.csrfToken}
			$scope.$broadcast "changes:accept", change_ids

		_doRejectChanges = (change_ids) ->
			$scope.$broadcast "changes:reject", change_ids

		bulkAccept = () ->
			_doAcceptChanges $scope.reviewPanel.selectedEntryIds.slice()
			event_tracking.sendMB "rp-bulk-accept", { 
				view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini',  
				nEntries: $scope.reviewPanel.nVisibleSelectedChanges
			}

		bulkReject = () ->
			_doRejectChanges $scope.reviewPanel.selectedEntryIds.slice()
			event_tracking.sendMB "rp-bulk-reject", { 
				view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini',  
				nEntries: $scope.reviewPanel.nVisibleSelectedChanges
			}

		$scope.showBulkAcceptDialog = () ->
			showBulkActionsDialog true

		$scope.showBulkRejectDialog = () -> 
			showBulkActionsDialog false

		showBulkActionsDialog = (isAccept) ->
			$modal.open({
				templateUrl: "bulkActionsModalTemplate"
				controller: "BulkActionsModalController"
				resolve:
					isAccept: () -> isAccept
					nChanges: () -> $scope.reviewPanel.nVisibleSelectedChanges
				scope: $scope.$new()
			}).result.then (isAccept) ->
				if isAccept
					bulkAccept()
				else
					bulkReject()

		$scope.handleTogglerClick = (e) ->
			e.target.blur()
			$scope.toggleReviewPanel()

		$scope.addNewComment = () ->
			$scope.$broadcast "comment:start_adding"
			$scope.toggleReviewPanel()

		$scope.addNewCommentFromKbdShortcut = () ->
			if !$scope.project.features.trackChangesVisible
				return
			$scope.$broadcast "comment:select_line"
			if !$scope.ui.reviewPanelOpen
				$scope.toggleReviewPanel()
			$timeout () ->
				$scope.$broadcast "review-panel:layout"	
				$scope.$broadcast "comment:start_adding"

		$scope.startNewComment = () ->
			$scope.$broadcast "comment:select_line"
			$timeout () ->
				$scope.$broadcast "review-panel:layout"

		$scope.submitNewComment = (content) ->
			return if !content? or content == ""
			doc_id = $scope.editor.open_doc_id
			entries = getDocEntries(doc_id)
			return if !entries["add-comment"]?
			{offset, length} = entries["add-comment"]
			thread_id = RangesTracker.generateId()
			thread = getThread(thread_id)
			thread.submitting = true
			$scope.$broadcast "comment:add", thread_id, offset, length
			$http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken})
				.catch () ->
					ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment")
			$scope.$broadcast "editor:clearSelection"
			$timeout () ->
				$scope.$broadcast "review-panel:layout"
			event_tracking.sendMB "rp-new-comment", { size: content.length }

		$scope.cancelNewComment = (entry) ->
			$timeout () ->
				$scope.$broadcast "review-panel:layout"
		
		$scope.startReply = (entry) ->
			entry.replying = true
			$timeout () ->
				$scope.$broadcast "review-panel:layout"

		$scope.submitReply = (entry, entry_id) ->			
			thread_id = entry.thread_id
			content   = entry.replyContent
			$http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken})
				.catch () ->
					ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment")
			
			trackingMetadata =
				view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini'
				size: entry.replyContent.length
				thread: thread_id

			thread = getThread(thread_id)
			thread.submitting = true
			entry.replyContent = ""
			entry.replying = false
			$timeout () ->
				$scope.$broadcast "review-panel:layout"
			event_tracking.sendMB "rp-comment-reply", trackingMetadata

		$scope.cancelReply = (entry) ->
			entry.replying = false
			entry.replyContent = ""
			$scope.$broadcast "review-panel:layout"

		$scope.resolveComment = (entry, entry_id) ->
			entry.focused = false
			$http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken}
			_onCommentResolved(entry.thread_id, ide.$scope.user)
			event_tracking.sendMB "rp-comment-resolve", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }

		$scope.unresolveComment = (thread_id) ->
			_onCommentReopened(thread_id)
			$http.post "/project/#{$scope.project_id}/thread/#{thread_id}/reopen", {_csrf: window.csrfToken}
			event_tracking.sendMB "rp-comment-reopen"
		
		_onCommentResolved = (thread_id, user) ->
			thread = getThread(thread_id)
			return if !thread?
			thread.resolved = true
			thread.resolved_by_user = formatUser(user)
			thread.resolved_at = new Date().toISOString()
			$scope.reviewPanel.resolvedThreadIds[thread_id] = true
			$scope.$broadcast "comment:resolve_threads", [thread_id]
		
		_onCommentReopened = (thread_id) ->
			thread = getThread(thread_id)
			return if !thread?
			delete thread.resolved
			delete thread.resolved_by_user
			delete thread.resolved_at
			delete $scope.reviewPanel.resolvedThreadIds[thread_id]
			$scope.$broadcast "comment:unresolve_thread", thread_id

		_onThreadDeleted = (thread_id) ->
			delete $scope.reviewPanel.resolvedThreadIds[thread_id]
			delete $scope.reviewPanel.commentThreads[thread_id]
			$scope.$broadcast "comment:remove", thread_id
		
		_onCommentEdited = (thread_id, comment_id, content) ->
			thread = getThread(thread_id)
			return if !thread?
			for message in thread.messages
				if message.id == comment_id
					message.content = content
			updateEntries()
		
		_onCommentDeleted = (thread_id, comment_id) ->
			thread = getThread(thread_id)
			return if !thread?
			thread.messages = thread.messages.filter (m) -> m.id != comment_id
			updateEntries()
		
		$scope.deleteThread = (entry_id, doc_id, thread_id) ->
			_onThreadDeleted(thread_id)
			$http({
				method: "DELETE"
				url: "/project/#{$scope.project_id}/doc/#{doc_id}/thread/#{thread_id}",
				headers: {
					'X-CSRF-Token': window.csrfToken
				}
			})
			event_tracking.sendMB "rp-comment-delete"
		
		$scope.saveEdit = (thread_id, comment) ->
			$http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}/edit", {
				content: comment.content
				_csrf: window.csrfToken
			})
			$timeout () ->
				$scope.$broadcast "review-panel:layout"

		$scope.deleteComment = (thread_id, comment) ->
			_onCommentDeleted(thread_id, comment.id)
			$http({
				method: "DELETE"
				url: "/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}",
				headers: {
					'X-CSRF-Token': window.csrfToken
				}
			})
			$timeout () ->
				$scope.$broadcast "review-panel:layout"

		$scope.setSubView = (subView) -> 
			$scope.reviewPanel.subView = subView
			event_tracking.sendMB "rp-subview-change", { subView }
						
		$scope.gotoEntry = (doc_id, entry) ->
			ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset })

		$scope.toggleFullTCStateCollapse = () ->
			if $scope.project.features.trackChanges
				$scope.reviewPanel.fullTCStateCollapsed = !$scope.reviewPanel.fullTCStateCollapsed
			else
				$scope.openTrackChangesUpgradeModal()

		_setUserTCState = (userId, newValue, isLocal = false) ->
			$scope.reviewPanel.trackChangesState[userId] ?= {}
			state = $scope.reviewPanel.trackChangesState[userId]

			if !state.syncState? or state.syncState == UserTCSyncState.SYNCED
				state.value = newValue
				state.syncState = UserTCSyncState.SYNCED
			else if state.syncState == UserTCSyncState.PENDING and state.value == newValue
				state.syncState = UserTCSyncState.SYNCED
			else if isLocal
				state.value = newValue
				state.syncState = UserTCSyncState.PENDING

			if userId == ide.$scope.user.id
				$scope.editor.wantTrackChanges = newValue

		_setEveryoneTCState = (newValue, isLocal = false) ->
			$scope.reviewPanel.trackChangesOnForEveryone = newValue
			project = $scope.project
			for member in project.members
				_setUserTCState(member._id, newValue, isLocal)
			_setGuestsTCState(newValue, isLocal)
			_setUserTCState(project.owner._id, newValue, isLocal)

		_setGuestsTCState = (newValue, isLocal = false) ->
			$scope.reviewPanel.trackChangesOnForGuests = newValue
			if currentUserType() == UserTypes.GUEST or currentUserType() == UserTypes.ANONYMOUS
				$scope.editor.wantTrackChanges = newValue

		applyClientTrackChangesStateToServer = () ->
			data = {}
			if $scope.reviewPanel.trackChangesOnForEveryone
				data.on = true
			else
				data.on_for = {}
				for userId, userState of $scope.reviewPanel.trackChangesState
					data.on_for[userId] = userState.value
				if $scope.reviewPanel.trackChangesOnForGuests
					data.on_for_guests = true
			data._csrf = window.csrfToken
			$http.post "/project/#{$scope.project_id}/track_changes", data

		applyTrackChangesStateToClient = (state) ->
			if typeof state is "boolean"
				_setEveryoneTCState state
				_setGuestsTCState state
			else
				project = $scope.project
				$scope.reviewPanel.trackChangesOnForEveryone = false
				_setGuestsTCState(state.__guests__ == true)
				for member in project.members
					_setUserTCState(member._id, state[member._id] ? false)
				_setUserTCState($scope.project.owner._id, state[$scope.project.owner._id] ? false)

		$scope.toggleTrackChangesForEveryone = (onForEveryone) ->
			_setEveryoneTCState onForEveryone, true
			_setGuestsTCState onForEveryone, true
			applyClientTrackChangesStateToServer()

		$scope.toggleTrackChangesForGuests = (onForGuests) ->
			_setGuestsTCState onForGuests, true
			applyClientTrackChangesStateToServer()

		$scope.toggleTrackChangesForUser = (onForUser, userId) ->
			_setUserTCState userId, onForUser, true
			applyClientTrackChangesStateToServer()				

		ide.socket.on "toggle-track-changes", (state) ->
			$scope.$apply () ->
				applyTrackChangesStateToClient(state)

		$scope.toggleTrackChangesFromKbdShortcut = () ->
			if !($scope.project.features.trackChangesVisible && $scope.project.features.trackChanges)
				return
			$scope.toggleTrackChangesForUser !$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value, ide.$scope.user.id

		setGuestFeatureBasedOnProjectAccessLevel = (projectPublicAccessLevel) ->
			$scope.reviewPanel.trackChangesForGuestsAvailable = (projectPublicAccessLevel == 'tokenBased')

		onToggleTrackChangesForGuestsAvailability = (available) ->
			# If the feature is no longer available we need to turn off the guest flag
			return if available
			return if !$scope.reviewPanel.trackChangesOnForGuests # Already turned off
			return if $scope.reviewPanel.trackChangesOnForEveryone # Overrides guest setting
			$scope.toggleTrackChangesForGuests(false)

		$scope.$watch 'project.publicAccesLevel', setGuestFeatureBasedOnProjectAccessLevel

		$scope.$watch 'reviewPanel.trackChangesForGuestsAvailable', (available) ->
			if available?
				onToggleTrackChangesForGuestsAvailability(available)

		_inited = false
		ide.$scope.$on "project:joined", () ->
			return if _inited
			project = ide.$scope.project
			if project.features.trackChanges
				window.trackChangesState ?= false
				applyTrackChangesStateToClient(window.trackChangesState)
			else
				applyTrackChangesStateToClient(false)
			setGuestFeatureBasedOnProjectAccessLevel(project.publicAccesLevel)
			_inited = true

		_refreshingRangeUsers = false
		_refreshedForUserIds = {}
		refreshChangeUsers = (refresh_for_user_id) ->
			if refresh_for_user_id?
				if _refreshedForUserIds[refresh_for_user_id]?
					# We've already tried to refresh to get this user id, so stop it looping
					return
				_refreshedForUserIds[refresh_for_user_id] = true

			# Only do one refresh at once
			if _refreshingRangeUsers
				return
			_refreshingRangeUsers = true

			$http.get "/project/#{$scope.project_id}/changes/users"
				.then (response) ->
					users = response.data
					_refreshingRangeUsers = false
					$scope.users = {}
					# Always include ourself, since if we submit an op, we might need to display info
					# about it locally before it has been flushed through the server
					if ide.$scope.user?.id?
						$scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user)
					for user in users
						if user.id?
							$scope.users[user.id] = formatUser(user)
				.catch () ->
					_refreshingRangeUsers = false

		_threadsLoaded = false
		ensureThreadsAreLoaded = () ->
			if _threadsLoaded
				# We get any updates in real time so only need to load them once.
				return
			_threadsLoaded = true
			$scope.reviewPanel.loadingThreads = true
			$http.get "/project/#{$scope.project_id}/threads"
				.then (response) ->
					threads = response.data
					$scope.reviewPanel.loadingThreads = false
					for thread_id, _ of $scope.reviewPanel.resolvedThreadIds
						delete $scope.reviewPanel.resolvedThreadIds[thread_id]
					for thread_id, thread of threads
						for comment in thread.messages
							formatComment(comment)
						if thread.resolved_by_user?
							thread.resolved_by_user = formatUser(thread.resolved_by_user)
							$scope.reviewPanel.resolvedThreadIds[thread_id] = true
							$scope.$broadcast "comment:resolve_threads", [thread_id]
					$scope.reviewPanel.commentThreads = threads
					$timeout () ->
						$scope.$broadcast "review-panel:layout"

		formatComment = (comment) ->
			comment.user = formatUser(comment.user)
			comment.timestamp = new Date(comment.timestamp)
			return comment

		formatUser = (user) ->
			id = user?._id or user?.id

			if !id?
				return {
					email: null
					name: "Anonymous"
					isSelf: false
					hue: ColorManager.ANONYMOUS_HUE
					avatar_text: "A"
				}
			if id == window.user_id
				name = "You"
				isSelf = true
			else
				name = [user.first_name, user.last_name].filter((n) -> n? and n != "").join(" ")
				if name == ""
					name = user.email?.split("@")[0] or "Unknown"
				isSelf = false
			return {
				id: id
				email: user.email
				name: name
				isSelf: isSelf
				hue: ColorManager.getHueForUserId(id)
				avatar_text: [user.first_name, user.last_name].filter((n) -> n?).map((n) -> n[0]).join ""
			}

		$scope.openTrackChangesUpgradeModal = () ->
			$modal.open {
				templateUrl: "trackChangesUpgradeModalTemplate"
				controller: "TrackChangesUpgradeModalController"
				scope: $scope.$new()
			}
