/* eslint-disable
    camelcase,
    max-len,
    no-return-assign,
    no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS101: Remove unnecessary use of Array.from
 * DS102: Remove unnecessary code created because of implicit returns
 * DS205: Consider reworking code to avoid use of IIFEs
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
import RangesTracker from '@overleaf/ranges-tracker'
import App from '../../../base'
import EventEmitter from '../../../utils/EventEmitter'
import ColorManager from '../../colors/ColorManager'
import getMeta from '../../../utils/meta'

export default App.controller(
  'ReviewPanelController',
  function (
    $scope,
    $element,
    ide,
    $timeout,
    $http,
    $modal,
    eventTracking,
    localStorage
  ) {
    let UserTCSyncState
    const $reviewPanelEl = $element.find('#review-panel')

    const 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
    }

    const currentUserType = function () {
      if ((ide.$scope.user != null ? ide.$scope.user.id : undefined) == null) {
        return UserTypes.ANONYMOUS
      } else {
        const user_id = ide.$scope.user.id
        const { project } = ide.$scope
        if (
          (project.owner != null ? project.owner.id : undefined) === user_id
        ) {
          return UserTypes.MEMBER
        }
        for (const member of Array.from(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',
    }

    ide.$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}`)
          ) || {},
      },
      dropdown: {
        loading: false,
      },
      commentThreads: {},
      resolvedThreadIds: {},
      layoutToLeft: false,
      rendererData: {},
      formattedProjectMembers: {},
      fullTCStateCollapsed: true,
      // 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,
      entryHover: false,
      isReact: getMeta('ol-splitTestVariants')?.['review-panel'] === 'react',
    }

    ide.$scope.loadingThreads = true

    window.addEventListener('beforeunload', function () {
      const collapsedStates = {}
      for (const doc in ide.$scope.reviewPanel.overview.docsCollapsedState) {
        const state = ide.$scope.reviewPanel.overview.docsCollapsedState[doc]
        if (state) {
          collapsedStates[doc] = state
        }
      }
      const valToStore =
        Object.keys(collapsedStates).length > 0
          ? JSON.stringify(collapsedStates)
          : null
      return localStorage(
        `docs_collapsed_state:${$scope.project_id}`,
        valToStore
      )
    })

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

    $scope.$on('layout:pdf:resize', (event, state) => {
      ide.$scope.reviewPanel.layoutToLeft =
        state.east?.size < 220 || state.east?.initClosed
      ide.$scope.$broadcast('review-panel:layout', false)
    })

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

    $scope.$on('review-panel:sizes', (e, sizes) => {
      $scope.$broadcast('editor:set-scroll-size', sizes)
      dispatchReviewPanelEvent('sizes', sizes)
    })

    $scope.$watch('project.features.trackChangesVisible', function (visible) {
      if (visible == null) {
        return
      }
      if (!visible) {
        return ($scope.ui.reviewPanelOpen = false)
      }
    })

    $scope.$watch('project.members', function (members) {
      ide.$scope.reviewPanel.formattedProjectMembers = {}
      if (($scope.project != null ? $scope.project.owner : undefined) != null) {
        ide.$scope.reviewPanel.formattedProjectMembers[
          $scope.project.owner._id
        ] = formatUser($scope.project.owner)
      }
      if (
        ($scope.project != null ? $scope.project.members : undefined) != null
      ) {
        return (() => {
          const result = []
          for (const member of Array.from(members)) {
            if (member.privileges === 'readAndWrite') {
              if (
                ide.$scope.reviewPanel.trackChangesState[member._id] == null
              ) {
                // An added member will have track changes enabled if track changes is on for everyone
                _setUserTCState(
                  member._id,
                  ide.$scope.reviewPanel.trackChangesOnForEveryone,
                  true
                )
              }
              result.push(
                (ide.$scope.reviewPanel.formattedProjectMembers[member._id] =
                  formatUser(member))
              )
            } else {
              result.push(undefined)
            }
          }
          return result
        })()
      }
    })

    $scope.commentState = {
      adding: false,
      content: '',
    }

    ide.$scope.users = $scope.users = {}

    ide.$scope.reviewPanelEventsBridge = new EventEmitter()

    ide.socket.on('new-comment', function (thread_id, comment) {
      const thread = getThread(thread_id)
      delete thread.submitting
      thread.messages.push(formatComment(comment))
      $scope.$apply()
      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
    })

    ide.socket.on('accept-changes', function (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)
        dispatchReviewPanelEvent('changes:accept', change_ids)
      }
      updateEntries(doc_id)
      return $scope.$apply(function () {})
    })

    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', function (thread_id) {
      _onThreadDeleted(thread_id)
      return $scope.$apply(function () {})
    })

    ide.socket.on('edit-message', function (thread_id, message_id, content) {
      _onCommentEdited(thread_id, message_id, content)
      return $scope.$apply(function () {})
    })

    ide.socket.on('delete-message', function (thread_id, message_id) {
      _onCommentDeleted(thread_id, message_id)
      return $scope.$apply(function () {})
    })

    const rangesTrackers = {}

    const getDocEntries = function (doc_id) {
      if (ide.$scope.reviewPanel.entries[doc_id] == null) {
        ide.$scope.reviewPanel.entries[doc_id] = {}
      }
      return ide.$scope.reviewPanel.entries[doc_id]
    }

    const getDocResolvedComments = function (doc_id) {
      if (ide.$scope.reviewPanel.resolvedComments[doc_id] == null) {
        ide.$scope.reviewPanel.resolvedComments[doc_id] = {}
      }
      return ide.$scope.reviewPanel.resolvedComments[doc_id]
    }

    function getThread(thread_id) {
      if (ide.$scope.reviewPanel.commentThreads[thread_id] == null) {
        ide.$scope.reviewPanel.commentThreads[thread_id] = { messages: [] }
      }
      return ide.$scope.reviewPanel.commentThreads[thread_id]
    }

    function getChangeTracker(doc_id) {
      if (rangesTrackers[doc_id] == null) {
        rangesTrackers[doc_id] = new RangesTracker()
        rangesTrackers[doc_id].resolvedThreadIds =
          ide.$scope.reviewPanel.resolvedThreadIds
      }
      return rangesTrackers[doc_id]
    }

    let scrollbar = {}
    ide.$scope.reviewPanelEventsBridge.on(
      'aceScrollbarVisibilityChanged',
      function (isVisible, scrollbarWidth) {
        scrollbar = { isVisible, scrollbarWidth }
        return updateScrollbar()
      }
    )

    function updateScrollbar() {
      if (
        scrollbar.isVisible &&
        ide.$scope.reviewPanel.subView === $scope.SubViews.CUR_FILE &&
        !$scope.editor.showRichText
      ) {
        return $reviewPanelEl.css('right', `${scrollbar.scrollbarWidth}px`)
      } else {
        return $reviewPanelEl.css('right', '0')
      }
    }

    $scope.$watch(
      '!ui.reviewPanelOpen && reviewPanel.hasEntries',
      function (open, prevVal) {
        if (open == null) {
          return
        }
        $scope.ui.miniReviewPanelVisible = open
        if (open !== prevVal) {
          return $timeout(() => $scope.$broadcast('review-panel:toggle'))
        }
      }
    )

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

    $scope.$watch('reviewPanel.subView', function (view, oldView) {
      if (view == null) {
        return
      }
      updateScrollbar()
      if (view === $scope.SubViews.OVERVIEW) {
        return refreshOverviewPanel()
      } else if (oldView === $scope.SubViews.OVERVIEW) {
        dispatchReviewPanelEvent('overview-closed', view)
      }
    })

    $scope.$watch('editor.sharejs_doc', function (doc, old_doc) {
      if (doc == null) {
        return
      }
      // 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 =
        ide.$scope.reviewPanel.resolvedThreadIds
      ide.$scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
      if (old_doc != null) {
        old_doc.off('flipped_pending_to_inflight')
      }
      doc.on('flipped_pending_to_inflight', () => regenerateTrackChangesId(doc))
      return regenerateTrackChangesId(doc)
    })

    $scope.$watch(
      function () {
        const entries =
          ide.$scope.reviewPanel.entries[$scope.editor.open_doc_id] || {}
        const permEntries = {}
        for (const entry in entries) {
          const entryData = entries[entry]
          if (!['add-comment', 'bulk-actions'].includes(entry)) {
            permEntries[entry] = entryData
          }
        }
        return Object.keys(permEntries).length
      },
      nEntries =>
        (ide.$scope.reviewPanel.hasEntries =
          nEntries > 0 && $scope.project.features.trackChangesVisible)
    )

    function regenerateTrackChangesId(doc) {
      const old_id = getChangeTracker(doc.doc_id).getIdSeed()
      const new_id = RangesTracker.generateIdSeed()
      getChangeTracker(doc.doc_id).setIdSeed(new_id)
      return doc.setTrackChangesIdSeeds({ pending: new_id, inflight: old_id })
    }

    const refreshRanges = () =>
      $http
        .get(`/project/${$scope.project_id}/ranges`)
        .then(function (response) {
          const docs = response.data
          return (() => {
            const result = []
            for (const doc of Array.from(docs)) {
              if (
                ide.$scope.reviewPanel.overview.docsCollapsedState[doc.id] ==
                null
              ) {
                ide.$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
                const rangesTracker = getChangeTracker(doc.id)
                rangesTracker.comments =
                  (doc.ranges != null ? doc.ranges.comments : undefined) || []
                rangesTracker.changes =
                  (doc.ranges != null ? doc.ranges.changes : undefined) || []
              }
              result.push(updateEntries(doc.id))
            }
            return result
          })()
        })

    function refreshOverviewPanel() {
      ide.$scope.reviewPanel.overview.loading = true
      return refreshRanges()
        .then(() => (ide.$scope.reviewPanel.overview.loading = false))
        .catch(() => (ide.$scope.reviewPanel.overview.loading = false))
    }

    ide.$scope.refreshResolvedCommentsDropdown = function () {
      ide.$scope.reviewPanel.dropdown.loading = true
      const q = refreshRanges()
      q.then(() => (ide.$scope.reviewPanel.dropdown.loading = false))
      q.catch(() => (ide.$scope.reviewPanel.dropdown.loading = false))
      return q
    }

    function updateEntries(doc_id) {
      const rangesTracker = getChangeTracker(doc_id)
      const entries = getDocEntries(doc_id)
      const resolvedComments = getDocResolvedComments(doc_id)

      let changed = false

      // Assume we'll delete everything until we see it, then we'll remove it from this object
      const delete_changes = {}
      for (const id in entries) {
        const change = entries[id]
        if (!['add-comment', 'bulk-actions'].includes(id)) {
          for (const entry_id of Array.from(change.entry_ids)) {
            delete_changes[entry_id] = true
          }
        }
      }
      for (const id in resolvedComments) {
        const change = resolvedComments[id]
        for (const entry_id of Array.from(change.entry_ids)) {
          delete_changes[entry_id] = true
        }
      }

      let potential_aggregate = false
      let prev_insertion = null

      for (const change of Array.from(rangesTracker.changes)) {
        changed = true

        if (
          potential_aggregate &&
          change.op.d &&
          change.op.p === prev_insertion.op.p + prev_insertion.op.i.length &&
          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 {
          if (entries[change.id] == null) {
            entries[change.id] = {}
          }
          delete delete_changes[change.id]
          const new_entry = {
            type: change.op.i ? 'insert' : 'delete',
            entry_ids: [change.id],
            content: change.op.i || change.op.d,
            offset: change.op.p,
            metadata: change.metadata,
          }
          for (const key in new_entry) {
            entries[change.id][key] = new_entry[key]
          }
        }

        if (change.op.i) {
          potential_aggregate = true
          prev_insertion = change
        } else {
          potential_aggregate = false
          prev_insertion = null
        }

        if ($scope.users[change.metadata.user_id] == null) {
          if (!window.isRestrictedTokenMember) {
            refreshChangeUsers(change.metadata.user_id)
          }
        }
      }

      if (!window.isRestrictedTokenMember) {
        if (rangesTracker.comments.length > 0) {
          ensureThreadsAreLoaded()
        } else if (ide.$scope.loadingThreads === true) {
          // ensure that tracked changes are highlighted even if no comments are loaded
          ide.$scope.loadingThreads = false
          dispatchReviewPanelEvent('loaded_threads')
        }
      }

      for (const comment of Array.from(rangesTracker.comments)) {
        let new_comment
        changed = true
        delete delete_changes[comment.id]
        if (ide.$scope.reviewPanel.resolvedThreadIds[comment.op.t]) {
          new_comment =
            resolvedComments[comment.id] != null
              ? resolvedComments[comment.id]
              : (resolvedComments[comment.id] = {})
          delete entries[comment.id]
        } else {
          new_comment =
            entries[comment.id] != null
              ? entries[comment.id]
              : (entries[comment.id] = {})
          delete resolvedComments[comment.id]
        }
        const new_entry = {
          type: 'comment',
          thread_id: comment.op.t,
          entry_ids: [comment.id],
          content: comment.op.c,
          offset: comment.op.p,
        }
        for (const key in new_entry) {
          new_comment[key] = new_entry[key]
        }
      }

      for (const change_id in delete_changes) {
        const _ = delete_changes[change_id]
        changed = true
        delete entries[change_id]
        delete resolvedComments[change_id]
      }

      if (changed) {
        // TODO: unused?
        $scope.$broadcast('entries:changed')
      }

      return entries
    }

    $scope.$on('editor:track-changes:changed', function () {
      const doc_id = $scope.editor.open_doc_id
      const entries = updateEntries(doc_id)

      // For now, not worrying about entry panels for rich text
      if (!$scope.editor.showRichText) {
        $scope.$broadcast('review-panel:recalculate-screen-positions')
        dispatchReviewPanelEvent('recalculate-screen-positions', entries)

        return ide.$scope.$broadcast('review-panel:layout')
      }
    })

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

    $scope.$on(
      'editor:focus:changed',
      function (e, selection_offset_start, selection_offset_end, selection) {
        const doc_id = $scope.editor.open_doc_id
        const entries = getDocEntries(doc_id)
        // All selected changes will be added to this array.
        ide.$scope.reviewPanel.selectedEntryIds = []
        // Count of user-visible changes, i.e. an aggregated change will count as one.
        ide.$scope.reviewPanel.nVisibleSelectedChanges = 0

        const offset = selection_offset_start
        const length = selection_offset_end - selection_offset_start

        // Recreate the add comment and bulk actions entries only when
        // necessary. This is to avoid the UI thinking that these entries have
        // changed and getting into an infinite loop.
        if (selection) {
          const existingAddComment = entries['add-comment']
          if (
            !existingAddComment ||
            existingAddComment.offset !== offset ||
            existingAddComment.length !== length
          ) {
            entries['add-comment'] = {
              type: 'add-comment',
              offset,
              length,
            }
          }
          const existingBulkActions = entries['bulk-actions']
          if (
            !existingBulkActions ||
            existingBulkActions.offset !== offset ||
            existingBulkActions.length !== length
          ) {
            entries['bulk-actions'] = {
              type: 'bulk-actions',
              offset,
              length,
            }
          }
        } else {
          delete entries['add-comment']
          delete entries['bulk-actions']
        }

        for (const id in entries) {
          const entry = entries[id]
          let isChangeEntryAndWithinSelection = false
          if (
            entry.type === 'comment' &&
            !ide.$scope.reviewPanel.resolvedThreadIds[entry.thread_id]
          ) {
            entry.focused =
              entry.offset <= selection_offset_start &&
              selection_offset_start <= entry.offset + entry.content.length
          } else if (entry.type === 'insert') {
            isChangeEntryAndWithinSelection =
              entry.offset >= selection_offset_start &&
              entry.offset + entry.content.length <= selection_offset_end
            entry.focused =
              entry.offset <= selection_offset_start &&
              selection_offset_start <= entry.offset + entry.content.length
          } else if (entry.type === 'delete') {
            isChangeEntryAndWithinSelection =
              selection_offset_start <= entry.offset &&
              entry.offset <= selection_offset_end
            entry.focused = entry.offset === selection_offset_start
          } else if (entry.type === 'aggregate-change') {
            isChangeEntryAndWithinSelection =
              entry.offset >= selection_offset_start &&
              entry.offset + entry.content.length <= selection_offset_end
            entry.focused =
              entry.offset <= selection_offset_start &&
              selection_offset_start <= entry.offset + entry.content.length
          } else if (
            ['add-comment', 'bulk-actions'].includes(entry.type) &&
            selection
          ) {
            entry.focused = true
          }

          if (isChangeEntryAndWithinSelection) {
            for (const entry_id of Array.from(entry.entry_ids)) {
              ide.$scope.reviewPanel.selectedEntryIds.push(entry_id)
            }
            ide.$scope.reviewPanel.nVisibleSelectedChanges++
          }
        }

        $scope.$broadcast('review-panel:recalculate-screen-positions')

        dispatchReviewPanelEvent('recalculate-screen-positions', entries)

        // Ensure that watchers, such as the React-based review panel component,
        // are informed of the changes to entries
        ide.$scope.$apply()

        return ide.$scope.$broadcast('review-panel:layout')
      }
    )

    ide.$scope.acceptChanges = function (change_ids) {
      _doAcceptChanges(change_ids)
      eventTracking.sendMB('rp-changes-accepted', {
        view: $scope.ui.reviewPanelOpen
          ? ide.$scope.reviewPanel.subView
          : 'mini',
      })
    }

    ide.$scope.rejectChanges = function (change_ids) {
      _doRejectChanges(change_ids)
      eventTracking.sendMB('rp-changes-rejected', {
        view: $scope.ui.reviewPanelOpen
          ? ide.$scope.reviewPanel.subView
          : 'mini',
      })
    }

    // The next two functions control a class on the review panel that affects
    // the overflow-y rule on the panel. This is necessary so that an entry in
    // the review panel is visible when hovering over its indicator when the
    // review panel is minimized. See issue #8057.
    $scope.mouseEnterIndicator = function () {
      ide.$scope.reviewPanel.entryHover = true
    }

    $scope.mouseLeaveIndicator = function () {
      ide.$scope.reviewPanel.entryHover = false
    }

    function _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)
      dispatchReviewPanelEvent('changes:accept', change_ids)
    }

    const _doRejectChanges = change_ids => {
      $scope.$broadcast('changes:reject', change_ids)
      dispatchReviewPanelEvent('changes:reject', change_ids)
    }

    ide.$scope.bulkAcceptActions = function () {
      _doAcceptChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
      eventTracking.sendMB('rp-bulk-accept', {
        view: $scope.ui.reviewPanelOpen
          ? ide.$scope.reviewPanel.subView
          : 'mini',
        nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
      })
    }

    ide.$scope.bulkRejectActions = function () {
      _doRejectChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
      eventTracking.sendMB('rp-bulk-reject', {
        view: $scope.ui.reviewPanelOpen
          ? ide.$scope.reviewPanel.subView
          : 'mini',
        nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
      })
    }

    ide.$scope.showBulkAcceptDialog = () => showBulkActionsDialog(true)

    ide.$scope.showBulkRejectDialog = () => showBulkActionsDialog(false)

    const showBulkActionsDialog = isAccept =>
      $modal
        .open({
          templateUrl: 'bulkActionsModalTemplate',
          controller: 'BulkActionsModalController',
          resolve: {
            isAccept() {
              return isAccept
            },
            nChanges() {
              return ide.$scope.reviewPanel.nVisibleSelectedChanges
            },
          },
          scope: $scope.$new(),
        })
        .result.then(function (isAccept) {
          if (isAccept) {
            return ide.$scope.bulkAcceptActions()
          } else {
            return ide.$scope.bulkRejectActions()
          }
        })

    $scope.handleTogglerClick = function (e) {
      e.target.blur()
      return $scope.toggleReviewPanel()
    }

    ide.$scope.addNewComment = function (e) {
      e.preventDefault()
      $scope.$broadcast('comment:start_adding')
      return $scope.toggleReviewPanel()
    }

    $scope.addNewCommentFromKbdShortcut = function () {
      if (!$scope.project.features.trackChangesVisible) {
        return
      }
      $scope.$broadcast('comment:select_line')
      dispatchReviewPanelEvent('comment:select_line')

      if (!$scope.ui.reviewPanelOpen) {
        $scope.toggleReviewPanel()
      }
      return $timeout(function () {
        ide.$scope.$broadcast('review-panel:layout')
        return $scope.$broadcast('comment:start_adding')
      })
    }

    $scope.startNewComment = function () {
      $scope.$broadcast('comment:select_line')
      dispatchReviewPanelEvent('comment:select_line')
      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
    }

    ide.$scope.submitNewComment = function (content) {
      if (content == null || content === '') {
        return
      }
      const doc_id = $scope.editor.open_doc_id
      const entries = getDocEntries(doc_id)
      if (entries['add-comment'] == null) {
        return
      }
      const { offset, length } = entries['add-comment']
      const thread_id = RangesTracker.generateId()
      const thread = getThread(thread_id)
      thread.submitting = true
      $scope.$broadcast('comment:add', thread_id, offset, length)
      dispatchReviewPanelEvent('comment:add', {
        threadId: 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'
          )
        )
      // TODO: unused?
      $scope.$broadcast('editor:clearSelection')
      $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
      eventTracking.sendMB('rp-new-comment', { size: content.length })
    }

    $scope.cancelNewComment = entry =>
      $timeout(() => ide.$scope.$broadcast('review-panel:layout'))

    $scope.startReply = function (entry) {
      entry.replying = true
      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
    }

    ide.$scope.submitReply = function (entry, entry_id) {
      const { thread_id } = entry
      const 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'
          )
        )

      const trackingMetadata = {
        view: $scope.ui.reviewPanelOpen
          ? ide.$scope.reviewPanel.subView
          : 'mini',
        size: entry.replyContent.length,
        thread: thread_id,
      }

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

    $scope.cancelReply = function (entry) {
      entry.replying = false
      entry.replyContent = ''
      return ide.$scope.$broadcast('review-panel:layout')
    }

    ide.$scope.resolveComment = function (doc_id, entry_id) {
      const entry = getDocEntries(doc_id)[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)
      eventTracking.sendMB('rp-comment-resolve', {
        view: $scope.ui.reviewPanelOpen
          ? ide.$scope.reviewPanel.subView
          : 'mini',
      })
    }

    ide.$scope.unresolveComment = function (thread_id) {
      _onCommentReopened(thread_id)
      $http.post(`/project/${$scope.project_id}/thread/${thread_id}/reopen`, {
        _csrf: window.csrfToken,
      })
      eventTracking.sendMB('rp-comment-reopen')
    }

    function _onCommentResolved(thread_id, user) {
      const thread = getThread(thread_id)
      if (thread == null) {
        return
      }
      thread.resolved = true
      thread.resolved_by_user = formatUser(user)
      thread.resolved_at = new Date().toISOString()
      ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
      $scope.$broadcast('comment:resolve_threads', [thread_id])
      dispatchReviewPanelEvent('comment:resolve_threads', [thread_id])
    }

    function _onCommentReopened(thread_id) {
      const thread = getThread(thread_id)
      if (thread == null) {
        return
      }
      delete thread.resolved
      delete thread.resolved_by_user
      delete thread.resolved_at
      delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
      $scope.$broadcast('comment:unresolve_thread', thread_id)
      dispatchReviewPanelEvent('comment:unresolve_thread', thread_id)
    }

    function _onThreadDeleted(thread_id) {
      delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
      delete ide.$scope.reviewPanel.commentThreads[thread_id]
      $scope.$broadcast('comment:remove', thread_id)
      dispatchReviewPanelEvent('comment:remove', thread_id)
    }

    function _onCommentEdited(thread_id, comment_id, content) {
      const thread = getThread(thread_id)
      if (thread == null) {
        return
      }
      for (const message of Array.from(thread.messages)) {
        if (message.id === comment_id) {
          message.content = content
        }
      }
      return updateEntries()
    }

    function _onCommentDeleted(thread_id, comment_id) {
      const thread = getThread(thread_id)
      if (thread == null) {
        return
      }
      thread.messages = thread.messages.filter(m => m.id !== comment_id)
      return updateEntries()
    }

    ide.$scope.deleteThread = function (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,
        },
      })
      eventTracking.sendMB('rp-comment-delete')
    }

    ide.$scope.saveEdit = function (thread_id, comment_id, content) {
      $http.post(
        `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}/edit`,
        {
          content,
          _csrf: window.csrfToken,
        }
      )
      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
    }

    ide.$scope.deleteComment = function (thread_id, comment_id) {
      _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,
        },
      })
      return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
    }

    $scope.setSubView = function (subView) {
      ide.$scope.reviewPanel.subView = subView
      eventTracking.sendMB('rp-subview-change', { subView })
    }

    ide.$scope.gotoEntry = (doc_id, entry_offset) =>
      ide.editorManager.openDocId(doc_id, { gotoOffset: entry_offset })

    $scope.toggleFullTCStateCollapse = function () {
      if ($scope.project.features.trackChanges) {
        return (ide.$scope.reviewPanel.fullTCStateCollapsed =
          !ide.$scope.reviewPanel.fullTCStateCollapsed)
      } else {
        _sendAnalytics()
        return $scope.openTrackChangesUpgradeModal()
      }
    }

    const _sendAnalytics = () => {
      eventTracking.send(
        'subscription-funnel',
        'editor-click-feature',
        'real-time-track-changes'
      )
      eventTracking.sendMB('paywall-prompt', {
        'paywall-type': 'track-changes',
      })
    }

    const _setUserTCState = function (userId, newValue, isLocal) {
      if (isLocal == null) {
        isLocal = false
      }
      if (ide.$scope.reviewPanel.trackChangesState[userId] == null) {
        ide.$scope.reviewPanel.trackChangesState[userId] = {}
      }
      const state = ide.$scope.reviewPanel.trackChangesState[userId]

      if (
        state.syncState == null ||
        state.syncState === UserTCSyncState.SYNCED
      ) {
        state.value = newValue
        state.syncState = UserTCSyncState.SYNCED
      } else if (
        state.syncState === UserTCSyncState.PENDING &&
        state.value === newValue
      ) {
        state.syncState = UserTCSyncState.SYNCED
      } else if (isLocal) {
        state.value = newValue
        state.syncState = UserTCSyncState.PENDING
      }

      if (userId === ide.$scope.user.id) {
        return ($scope.editor.wantTrackChanges = newValue)
      }
    }

    const _setEveryoneTCState = function (newValue, isLocal) {
      if (isLocal == null) {
        isLocal = false
      }
      ide.$scope.reviewPanel.trackChangesOnForEveryone = newValue
      const { project } = $scope
      for (const member of Array.from(project.members)) {
        _setUserTCState(member._id, newValue, isLocal)
      }
      _setGuestsTCState(newValue, isLocal)
      return _setUserTCState(project.owner._id, newValue, isLocal)
    }

    function _setGuestsTCState(newValue, isLocal) {
      if (isLocal == null) {
        isLocal = false
      }
      ide.$scope.reviewPanel.trackChangesOnForGuests = newValue
      if (
        currentUserType() === UserTypes.GUEST ||
        currentUserType() === UserTypes.ANONYMOUS
      ) {
        return ($scope.editor.wantTrackChanges = newValue)
      }
    }

    const applyClientTrackChangesStateToServer = function () {
      const data = {}
      if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
        data.on = true
      } else {
        data.on_for = {}
        for (const userId in ide.$scope.reviewPanel.trackChangesState) {
          const userState = ide.$scope.reviewPanel.trackChangesState[userId]
          data.on_for[userId] = userState.value
        }
        if (ide.$scope.reviewPanel.trackChangesOnForGuests) {
          data.on_for_guests = true
        }
      }
      data._csrf = window.csrfToken
      return $http.post(`/project/${$scope.project_id}/track_changes`, data)
    }

    const applyTrackChangesStateToClient = function (state) {
      if (typeof state === 'boolean') {
        _setEveryoneTCState(state)
        return _setGuestsTCState(state)
      } else {
        const { project } = $scope
        ide.$scope.reviewPanel.trackChangesOnForEveryone = false
        _setGuestsTCState(state.__guests__ === true)
        for (const member of Array.from(project.members)) {
          _setUserTCState(
            member._id,
            state[member._id] != null ? state[member._id] : false
          )
        }
        return _setUserTCState(
          $scope.project.owner._id,
          state[$scope.project.owner._id] != null
            ? state[$scope.project.owner._id]
            : false
        )
      }
    }

    ide.$scope.toggleTrackChangesForEveryone = function (onForEveryone) {
      _setEveryoneTCState(onForEveryone, true)
      _setGuestsTCState(onForEveryone, true)
      return applyClientTrackChangesStateToServer()
    }

    ide.$scope.toggleTrackChangesForGuests = function (onForGuests) {
      _setGuestsTCState(onForGuests, true)
      return applyClientTrackChangesStateToServer()
    }

    ide.$scope.toggleTrackChangesForUser = function (onForUser, userId) {
      _setUserTCState(userId, onForUser, true)
      return applyClientTrackChangesStateToServer()
    }

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

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

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

    const onToggleTrackChangesForGuestsAvailability = function (available) {
      // If the feature is no longer available we need to turn off the guest flag
      if (available) {
        return
      }
      if (!ide.$scope.reviewPanel.trackChangesOnForGuests) {
        return
      } // Already turned off
      if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
        return
      } // Overrides guest setting
      return $scope.toggleTrackChangesForGuests(false)
    }

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

    $scope.$watch(
      'reviewPanel.trackChangesForGuestsAvailable',
      function (available) {
        if (available != null) {
          return onToggleTrackChangesForGuestsAvailability(available)
        }
      }
    )

    let _inited = false
    ide.$scope.$on('project:joined', function () {
      if (_inited) {
        return
      }
      const { project } = ide.$scope
      if (project.features.trackChanges) {
        applyTrackChangesStateToClient(project.trackChangesState)
      } else {
        applyTrackChangesStateToClient(false)
      }
      setGuestFeatureBasedOnProjectAccessLevel(project.publicAccesLevel)
      return (_inited = true)
    })

    let _refreshingRangeUsers = false
    const _refreshedForUserIds = {}
    function refreshChangeUsers(refresh_for_user_id) {
      if (refresh_for_user_id != null) {
        if (_refreshedForUserIds[refresh_for_user_id] != null) {
          // 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

      return $http
        .get(`/project/${$scope.project_id}/changes/users`)
        .then(function (response) {
          const users = response.data
          _refreshingRangeUsers = false
          ide.$scope.users = $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 != null ? ide.$scope.user.id : undefined) != null
          ) {
            $scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user)
          }
          return (() => {
            const result = []
            for (const user of Array.from(users)) {
              if (user.id != null) {
                result.push(($scope.users[user.id] = formatUser(user)))
              } else {
                result.push(undefined)
              }
            }
            return result
          })()
        })
        .catch(() => (_refreshingRangeUsers = false))
    }

    let _threadsLoaded = false
    function ensureThreadsAreLoaded() {
      if (_threadsLoaded) {
        // We get any updates in real time so only need to load them once.
        return
      }
      _threadsLoaded = true
      ide.$scope.loadingThreads = true
      return $http
        .get(`/project/${$scope.project_id}/threads`)
        .then(function (response) {
          const threads = response.data
          ide.$scope.loadingThreads = false
          for (const thread_id in ide.$scope.reviewPanel.resolvedThreadIds) {
            delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
          }
          for (const thread_id in threads) {
            const thread = threads[thread_id]
            for (const comment of Array.from(thread.messages)) {
              formatComment(comment)
            }
            if (thread.resolved_by_user != null) {
              thread.resolved_by_user = formatUser(thread.resolved_by_user)
              ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
              $scope.$broadcast('comment:resolve_threads', [thread_id])
            }
          }
          ide.$scope.reviewPanel.commentThreads = threads
          dispatchReviewPanelEvent('loaded_threads')
          return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
        })
    }

    function formatComment(comment) {
      comment.user = formatUser(comment.user)
      comment.timestamp = new Date(comment.timestamp)
      return comment
    }

    function formatUser(user) {
      let isSelf, name
      const id =
        (user != null ? user._id : undefined) ||
        (user != null ? user.id : undefined)

      if (id == null) {
        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 != null && n !== '')
          .join(' ')
        if (name === '') {
          name =
            (user.email != null ? user.email.split('@')[0] : undefined) ||
            'Unknown'
        }
        isSelf = false
      }
      return {
        id,
        email: user.email,
        name,
        isSelf,
        hue: ColorManager.getHueForUserId(id),
        avatar_text: [user.first_name, user.last_name]
          .filter(n => n != null)
          .map(n => n[0])
          .join(''),
      }
    }

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

    // listen for events from the CodeMirror 6 track changes extension
    window.addEventListener('editor:event', event => {
      const { type, payload } = event.detail

      switch (type) {
        case 'line-height': {
          ide.$scope.reviewPanel.rendererData.lineHeight = payload
          ide.$scope.$broadcast('review-panel:layout')
          break
        }

        case 'track-changes:changed': {
          $scope.$broadcast('editor:track-changes:changed')
          break
        }

        case 'track-changes:visibility_changed': {
          $scope.$broadcast('editor:track-changes:visibility_changed')
          break
        }

        case 'focus:changed': {
          const { from, to, empty } = payload
          $scope.$broadcast('editor:focus:changed', from, to, !empty)
          break
        }

        case 'add-new-comment': {
          $scope.addNewCommentFromKbdShortcut()
          break
        }

        case 'toggle-track-changes': {
          $scope.toggleTrackChangesFromKbdShortcut()
          break
        }

        case 'toggle-review-panel': {
          $scope.toggleReviewPanel()
          break
        }
      }
    })

    // Add methods somewhere that React can see them
    $scope.reviewPanel.saveEdit = $scope.saveEdit
  }
)

// send events to the CodeMirror 6 track changes extension
const dispatchReviewPanelEvent = (type, payload) => {
  window.dispatchEvent(
    new CustomEvent('review-panel:event', {
      detail: { type, payload },
    })
  )
}
