text: /**
   * @license
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  (function() {
    'use strict';
  
    const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
    const MSG_LOADING_BLAME = 'Loading blame...';
    const MSG_LOADED_BLAME = 'Blame loaded';
  
    const PARENT = 'PARENT';
  
    const DiffSides = {
      LEFT: 'left',
      RIGHT: 'right',
    };
  
    const DiffViewMode = {
      SIDE_BY_SIDE: 'SIDE_BY_SIDE',
      UNIFIED: 'UNIFIED_DIFF',
    };
  
    /**
      * @appliesMixin Gerrit.FireMixin
      * @appliesMixin Gerrit.KeyboardShortcutMixin
      * @appliesMixin Gerrit.PatchSetMixin
      * @appliesMixin Gerrit.PathListMixin
      * @appliesMixin Gerrit.RESTClientMixin
      */
    class GrDiffView extends Polymer.mixinBehaviors( [
      Gerrit.FireBehavior,
      Gerrit.KeyboardShortcutBehavior,
      Gerrit.PatchSetBehavior,
      Gerrit.PathListBehavior,
      Gerrit.RESTClientBehavior,
    ], Polymer.GestureEventListeners(
        Polymer.LegacyElementMixin(
            Polymer.Element))) {
      static get is() { return 'gr-diff-view'; }
      /**
       * Fired when the title of the page should change.
       *
       * @event title-change
       */
  
      /**
       * Fired when user tries to navigate away while comments are pending save.
       *
       * @event show-alert
       */
  
      static get properties() {
        return {
        /**
         * URL params passed from the router.
         */
          params: {
            type: Object,
            observer: '_paramsChanged',
          },
          keyEventTarget: {
            type: Object,
            value() { return document.body; },
          },
          /**
         * @type {{ diffMode: (string|undefined) }}
         */
          changeViewState: {
            type: Object,
            notify: true,
            value() { return {}; },
            observer: '_changeViewStateChanged',
          },
          disableDiffPrefs: {
            type: Boolean,
            value: false,
          },
          _diffPrefsDisabled: {
            type: Boolean,
            computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
          },
          /** @type {?} */
          _patchRange: Object,
          /** @type {?} */
          _commitRange: Object,
          /**
         * @type {{
         *  subject: string,
         *  project: string,
         *  revisions: string,
         * }}
         */
          _change: Object,
          /** @type {?} */
          _changeComments: Object,
          _changeNum: String,
          /**
         * This is a DiffInfo object.
         * This is retrieved and owned by a child component.
         */
          _diff: Object,
          // An array specifically formatted to be used in a gr-dropdown-list
          // element for selected a file to view.
          _formattedFiles: {
            type: Array,
            computed: '_formatFilesForDropdown(_fileList, ' +
              '_patchRange.patchNum, _changeComments)',
          },
          // An sorted array of files, as returned by the rest API.
          _fileList: {
            type: Array,
            value() { return []; },
          },
          _path: {
            type: String,
            observer: '_pathChanged',
          },
          _fileNum: {
            type: Number,
            computed: '_computeFileNum(_path, _formattedFiles)',
          },
          _loggedIn: {
            type: Boolean,
            value: false,
          },
          _loading: {
            type: Boolean,
            value: true,
          },
          _prefs: Object,
          _localPrefs: Object,
          _projectConfig: Object,
          _userPrefs: Object,
          _diffMode: {
            type: String,
            computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)',
          },
          _isImageDiff: Boolean,
          _filesWeblinks: Object,
  
          /**
         * Map of paths in the current change and patch range that have comments
         * or drafts or robot comments.
         */
          _commentMap: Object,
  
          _commentsForDiff: Object,
  
          /**
         * Object to contain the path of the next and previous file in the current
         * change and patch range that has comments.
         */
          _commentSkips: {
            type: Object,
            computed: '_computeCommentSkips(_commentMap, _fileList, _path)',
          },
          _panelFloatingDisabled: {
            type: Boolean,
            value: () => { return window.PANEL_FLOATING_DISABLED; },
          },
          _editMode: {
            type: Boolean,
            computed: '_computeEditMode(_patchRange.*)',
          },
          _isBlameLoaded: Boolean,
          _isBlameLoading: {
            type: Boolean,
            value: false,
          },
          _allPatchSets: {
            type: Array,
            computed: 'computeAllPatchSets(_change, _change.revisions.*)',
          },
          _revisionInfo: {
            type: Object,
            computed: '_getRevisionInfo(_change)',
          },
          _reviewedFiles: {
            type: Object,
            value: () => new Set(),
          },
        };
      }
  
      static get observers() {
        return [
          '_getProjectConfig(_change.project)',
          '_getFiles(_changeNum, _patchRange.*)',
          '_setReviewedObserver(_loggedIn, params.*, _prefs)',
        ];
      }
  
      get keyBindings() {
        return {
          esc: '_handleEscKey',
        };
      }
  
      keyboardShortcuts() {
        return {
          [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
          [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
          [this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
          [this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
          [this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
              '_handleNextLineOrFileWithComments',
          [this.Shortcut.PREV_FILE_WITH_COMMENTS]:
              '_handlePrevLineOrFileWithComments',
          [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
          [this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding
          [this.Shortcut.NEXT_FILE]: '_handleNextFile',
          [this.Shortcut.PREV_FILE]: '_handlePrevFile',
          [this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread',
          [this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread',
          [this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread',
          [this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread',
          [this.Shortcut.OPEN_REPLY_DIALOG]:
              '_handleOpenReplyDialogOrToggleLeftPane',
          [this.Shortcut.TOGGLE_LEFT_PANE]:
              '_handleOpenReplyDialogOrToggleLeftPane',
          [this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
          [this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
          [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
          [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
          [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
          [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
  
          // Final two are actually handled by gr-comment-thread.
          [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
          [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
        };
      }
  
      attached() {
        super.attached();
        this._getLoggedIn().then(loggedIn => {
          this._loggedIn = loggedIn;
        });
  
        this.$.cursor.push('diffs', this.$.diffHost);
      }
  
      _getLoggedIn() {
        return this.$.restAPI.getLoggedIn();
      }
  
      _getProjectConfig(project) {
        return this.$.restAPI.getProjectConfig(project).then(
            config => {
              this._projectConfig = config;
            });
      }
  
      _getChangeDetail(changeNum) {
        return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
          this._change = change;
          return change;
        });
      }
  
      _getChangeEdit(changeNum) {
        return this.$.restAPI.getChangeEdit(this._changeNum);
      }
  
      _getFiles(changeNum, patchRangeRecord) {
        // Polymer 2: check for undefined
        if ([changeNum, patchRangeRecord, patchRangeRecord.base]
            .some(arg => arg === undefined)) {
          return;
        }
  
        const patchRange = patchRangeRecord.base;
        return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray(
            changeNum, patchRange).then(files => {
          this._fileList = files;
        });
      }
  
      _getDiffPreferences() {
        return this.$.restAPI.getDiffPreferences().then(prefs => {
          this._prefs = prefs;
        });
      }
  
      _getPreferences() {
        return this.$.restAPI.getPreferences();
      }
  
      _getWindowWidth() {
        return window.innerWidth;
      }
  
      _handleReviewedChange(e) {
        this._setReviewed(Polymer.dom(e).rootTarget.checked);
      }
  
      _setReviewed(reviewed) {
        if (this._editMode) { return; }
        this.$.reviewed.checked = reviewed;
        this._saveReviewedState(reviewed).catch(err => {
          this.fire('show-alert', {message: ERR_REVIEW_STATUS});
          throw err;
        });
      }
  
      _saveReviewedState(reviewed) {
        return this.$.restAPI.saveFileReviewed(this._changeNum,
            this._patchRange.patchNum, this._path, reviewed);
      }
  
      _handleToggleFileReviewed(e) {
        if (this.shouldSuppressKeyboardShortcut(e) ||
            this.modifierPressed(e)) { return; }
  
        e.preventDefault();
        this._setReviewed(!this.$.reviewed.checked);
      }
  
      _handleEscKey(e) {
        if (this.shouldSuppressKeyboardShortcut(e) ||
            this.modifierPressed(e)) { return; }
  
        e.preventDefault();
        this.$.diffHost.displayLine = false;
      }
  
      _handleLeftPane(e) {
        if (this.shouldSuppressKeyboardShortcut(e)) { return; }
  
        e.preventDefault();
        this.$.cursor.moveLeft();
      }
  
      _handleRightPane(e) {
        if (this.shouldSuppressKeyboardShortcut(e)) { return; }
  
        e.preventDefault();
        this.$.cursor.moveRight();
      }
  
      _handlePrevLineOrFileWithComments(e) {
        if (this.shouldSuppressKeyboardShortcut(e)) { return; }
        if (e.detail.keyboardEvent.shiftKey &&
            e.detail.keyboardEvent.keyCode === 75) { // 'K'
          this._moveToPreviousFileWithComment();
          return;
        }
        if (this.modifierPressed(e)) { return; }
  
        e.preventDefault();
        this.$.diffHost.displayLine = true;
        this.$.cursor.moveUp();
      }
  
      _handleNextLineOrFileWithComments(e) {
        if (this.shouldSuppressKeyboardShortcut(e)) { return; }
        if (e.detail.keyboardEvent.shiftKey &&
            e.detail.keyboardEvent.keyCode === 74) { // 'J'
          this._moveToNextFileWithComment();
          return;
        }
        if (this.modifierPressed(e)) { return; }
  
        e.preventDefault();
        this.$.diffHost.displayLine = true;
        this.$.cursor.moveDown();
      }
  
      _moveToPreviousFileWithComment() {
        if (!this._commentSkips) { return; }
  
        // If there is no previous diff with comments, then return to the change
        // view.
        if (!this._commentSkips.previous) {
          this._navToChangeView();
          return;
        }
  
        Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous,
            this._patchRange.patchNum, this._patchRange.basePatchNum);
      }
  
      _moveToNextFileWithComment() {
        if (!this._commentSkips) { return; }
  
        // If there is no next diff with comments, then return to the change view.
        if (!this._commentSkips.next) {
          this._navToChangeView();
          return;
        }
  
        Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next,
            this._patchRange.patchNum, this._patchRange.basePatchNum);
      }
  
      _handleNewComment(e) {
        if (this.shouldSuppressKeyboardShortcut(e) ||
            this.modifierPressed(e)) { return; }
        e.preventDefault();
        this.$.cursor.createCommentInPlace();
      }
  
      _handlePrevFile(e) {
        // Check for meta key to avoid overriding native chrome shortcut.
        if (this.shouldSuppressKeyboardShortcut(e) ||
            this.getKeyboardEvent(e).metaKey) { return; }
  
        e.preventDefault();
        this._navToFile(this._path, this._fileList, -1);
      }
  
      _handleNextFile(e) {
        // Check for meta key to avoid overriding native chrome shortcut.
        if (this.shouldSuppressKeyboardShortcut(e) ||
            this.getKeyboardEvent(e).metaKey) { return; }
  
        e.preventDefault();
        this._navToFile(this._path, this._fileList, 1);
      }
  
      _handleNextChunkOrCommentThread(e) {
        if (this.shouldSuppressKeyboardShortcut(e)) { return; }
  
        e.preventDefault();
        if (e.detail.keyboardEvent.shiftKey) {
          this.$.cursor.moveToNextCommentThread();
        } else {
          if (this.modifierPressed(e)) { return; }
          this.$.cursor.moveToNextChunk();
        }
      }
  
      _handlePrevChunkOrCommentThread(e) {
        if (this.shouldSuppressKeyboardShortcut(e)) { return; }
  
        e.preventDefault();
        if (e.detail.keyboardEvent.shiftKey) {
          this.$.cursor.moveToPreviousCommentThread();
        } else {
          if (this.modifierPressed(e)) { return; }
          this.$.cursor.moveToPreviousChunk();
        }
      }
  
      _handleOpenReplyDialogOrToggleLeftPane(e) {
        if (this.shouldSuppressKeyboardShortcut(e)) { return; }
  
        if (e.detail.keyboardEvent.shiftKey) { // Hide left diff.
          e.preventDefault();
          this.$.diffHost.toggleLeftDiff();
          return;
        }
  
        if (this.modifierPressed(e)) { return; }
  
        if (!this._loggedIn) { return; }
  
        this.set('changeViewState.showReplyDialog', true);
        e.preventDefault();
        this._navToChangeView();
      }
  
      _handleUpToChange(e) {
        if (this.shouldSuppressKeyboardShortcut(e) ||
            this.modifierPressed(e)) { return; }
  
        e.preventDefault();
        this._navToChangeView();
      }
  
      _handleCommaKey(e) {
        if (this.shouldSuppressKeyboardShortcut(e) ||
            this.modifierPressed(e)) { return; }
        if (this._diffPrefsDisabled) { return; }
  
        e.preventDefault();
        this.$.diffPreferencesDialog.open();
      }
  
      _handleToggleDiffMode(e) {
        if (this.shouldSuppressKeyboardShortcut(e) ||
            this.modifierPressed(e)) { return; }
  
        e.preventDefault();
        if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) {
          this.$.modeSelect.setMode(DiffViewMode.UNIFIED);
        } else {
          this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE);
        }
      }
  
      _navToChangeView() {
        if (!this._changeNum || !this._patchRange.patchNum) { return; }
        this._navigateToChange(
            this._change,
            this._patchRange,
            this._change && this._change.revisions);
      }
  
      _navToFile(path, fileList, direction) {
        const newPath = this._getNavLinkPath(path, fileList, direction);
        if (!newPath) { return; }
  
        if (newPath.up) {
          this._navigateToChange(
              this._change,
              this._patchRange,
              this._change && this._change.revisions);
          return;
        }
  
        Gerrit.Nav.navigateToDiff(this._change, newPath.path,
            this._patchRange.patchNum, this._patchRange.basePatchNum);
      }
  
      /**
       * @param {?string} path The path of the current file being shown.
       * @param {!Array<string>} fileList The list of files in this change and
       *     patch range.
       * @param {number} direction Either 1 (next file) or -1 (prev file).
       * @param {(number|boolean)} opt_noUp Whether to return to the change view
       *     when advancing the file goes outside the bounds of fileList.
       *
       * @return {?string} The next URL when proceeding in the specified
       *     direction.
       */
      _computeNavLinkURL(change, path, fileList, direction, opt_noUp) {
        const newPath = this._getNavLinkPath(path, fileList, direction, opt_noUp);
        if (!newPath) { return null; }
  
        if (newPath.up) {
          return this._getChangePath(
              this._change,
              this._patchRange,
              this._change && this._change.revisions);
        }
        return this._getDiffUrl(this._change, this._patchRange, newPath.path);
      }
  
      /**
       * Gives an object representing the target of navigating either left or
       * right through the change. The resulting object will have one of the
       * following forms:
       *   * {path: "<target file path>"} - When another file path should be the
       *     result of the navigation.
       *   * {up: true} - When the result of navigating should go back to the
       *     change view.
       *   * null - When no navigation is possible for the given direction.
       *
       * @param {?string} path The path of the current file being shown.
       * @param {!Array<string>} fileList The list of files in this change and
       *     patch range.
       * @param {number} direction Either 1 (next file) or -1 (prev file).
       * @param {?number|boolean=} opt_noUp Whether to return to the change view
       *     when advancing the file goes outside the bounds of fileList.
       * @return {?Object}
       */
      _getNavLinkPath(path, fileList, direction, opt_noUp) {
        if (!path || !fileList || fileList.length === 0) { return null; }
  
        let idx = fileList.indexOf(path);
        if (idx === -1) {
          const file = direction > 0 ?
            fileList[0] :
            fileList[fileList.length - 1];
          return {path: file};
        }
  
        idx += direction;
        // Redirect to the change view if opt_noUp isn’t truthy and idx falls
        // outside the bounds of [0, fileList.length).
        if (idx < 0 || idx > fileList.length - 1) {
          if (opt_noUp) { return null; }
          return {up: true};
        }
  
        return {path: fileList[idx]};
      }
  
      _getReviewedFiles(changeNum, patchNum) {
        return this.$.restAPI.getReviewedFiles(changeNum, patchNum)
            .then(files => {
              this._reviewedFiles = new Set(files);
              return this._reviewedFiles;
            });
      }
  
      _getReviewedStatus(editMode, changeNum, patchNum, path) {
        if (editMode) { return Promise.resolve(false); }
        return this._getReviewedFiles(changeNum, patchNum)
            .then(files => files.has(path));
      }
  
      _paramsChanged(value) {
        if (value.view !== Gerrit.Nav.View.DIFF) { return; }
  
        if (value.changeNum && value.project) {
          this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
        }
  
        this.$.diffHost.lineOfInterest = this._getLineOfInterest(this.params);
        this._initCursor(this.params);
  
        this._changeNum = value.changeNum;
        this._path = value.path;
        this._patchRange = {
          patchNum: value.patchNum,
          basePatchNum: value.basePatchNum || PARENT,
        };
  
        // NOTE: This may be called before attachment (e.g. while parentElement is
        // null). Fire title-change in an async so that, if attachment to the DOM
        // has been queued, the event can bubble up to the handler in gr-app.
        this.async(() => {
          this.fire('title-change',
              {title: this.computeTruncatedPath(this._path)});
        });
  
        // When navigating away from the page, there is a possibility that the
        // patch number is no longer a part of the URL (say when navigating to
        // the top-level change info view) and therefore undefined in `params`.
        if (!this._patchRange.patchNum) {
          return;
        }
  
        const promises = [];
  
        promises.push(this._getDiffPreferences());
  
        promises.push(this._getPreferences().then(prefs => {
          this._userPrefs = prefs;
        }));
  
        promises.push(this._getChangeDetail(this._changeNum).then(change => {
          let commit;
          let baseCommit;
          if (change) {
            for (const commitSha in change.revisions) {
              if (!change.revisions.hasOwnProperty(commitSha)) continue;
              const revision = change.revisions[commitSha];
              const patchNum = revision._number.toString();
              if (patchNum === this._patchRange.patchNum) {
                commit = commitSha;
                const commitObj = revision.commit || {};
                const parents = commitObj.parents || [];
                if (this._patchRange.basePatchNum === PARENT && parents.length) {
                  baseCommit = parents[parents.length - 1].commit;
                }
              } else if (patchNum === this._patchRange.basePatchNum) {
                baseCommit = commitSha;
              }
            }
            this._commitRange = {commit, baseCommit};
          }
        }));
  
        promises.push(this._loadComments());
  
        promises.push(this._getChangeEdit(this._changeNum));
  
        this._loading = true;
        return Promise.all(promises).then(r => {
          const edit = r[4];
          if (edit) {
            this.set('_change.revisions.' + edit.commit.commit, {
              _number: this.EDIT_NAME,
              basePatchNum: edit.base_patch_set_number,
              commit: edit.commit,
            });
          }
          this._loading = false;
          this.$.diffHost.comments = this._commentsForDiff;
          return this.$.diffHost.reload(true);
        }).then(() => {
          this.$.reporting.diffViewFullyLoaded();
          // If diff view displayed has not ended yet, it ends here.
          this.$.reporting.diffViewDisplayed();
        });
      }
  
      _changeViewStateChanged(changeViewState) {
        if (changeViewState.diffMode === null) {
          // If screen size is small, always default to unified view.
          this.$.restAPI.getPreferences().then(prefs => {
            this.set('changeViewState.diffMode', prefs.default_diff_view);
          });
        }
      }
  
      _setReviewedObserver(_loggedIn, paramsRecord, _prefs) {
        // Polymer 2: check for undefined
        if ([_loggedIn, paramsRecord, _prefs].some(arg => arg === undefined)) {
          return;
        }
  
        const params = paramsRecord.base || {};
        if (!_loggedIn) { return; }
  
        if (_prefs.manual_review) {
          // Checkbox state needs to be set explicitly only when manual_review
          // is specified.
          this._getReviewedStatus(this.editMode, this._changeNum,
              this._patchRange.patchNum, this._path).then(status => {
            this.$.reviewed.checked = status;
          });
          return;
        }
  
        if (params.view === Gerrit.Nav.View.DIFF) {
          this._setReviewed(true);
        }
      }
  
      /**
       * If the params specify a diff address then configure the diff cursor.
       */
      _initCursor(params) {
        if (params.lineNum === undefined) { return; }
        if (params.leftSide) {
          this.$.cursor.side = DiffSides.LEFT;
        } else {
          this.$.cursor.side = DiffSides.RIGHT;
        }
        this.$.cursor.initialLineNumber = params.lineNum;
      }
  
      _getLineOfInterest(params) {
        // If there is a line number specified, pass it along to the diff so that
        // it will not get collapsed.
        if (!params.lineNum) { return null; }
        return {number: params.lineNum, leftSide: params.leftSide};
      }
  
      _pathChanged(path) {
        if (path) {
          this.fire('title-change',
              {title: this.computeTruncatedPath(path)});
        }
  
        if (this._fileList.length == 0) { return; }
  
        this.set('changeViewState.selectedFileIndex',
            this._fileList.indexOf(path));
      }
  
      _getDiffUrl(change, patchRange, path) {
        if ([change, patchRange, path].some(arg => arg === undefined)) {
          return '';
        }
        return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
            patchRange.basePatchNum);
      }
  
      _patchRangeStr(patchRange) {
        let patchStr = patchRange.patchNum;
        if (patchRange.basePatchNum != null &&
            patchRange.basePatchNum != PARENT) {
          patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
        }
        return patchStr;
      }
  
      /**
       * When the latest patch of the change is selected (and there is no base
       * patch) then the patch range need not appear in the URL. Return a patch
       * range object with undefined values when a range is not needed.
       *
       * @param {!Object} patchRange
       * @param {!Object} revisions
       * @return {!Object}
       */
      _getChangeUrlRange(patchRange, revisions) {
        let patchNum = undefined;
        let basePatchNum = undefined;
        let latestPatchNum = -1;
        for (const rev of Object.values(revisions || {})) {
          latestPatchNum = Math.max(latestPatchNum, rev._number);
        }
        if (patchRange.basePatchNum !== PARENT ||
            parseInt(patchRange.patchNum, 10) !== latestPatchNum) {
          patchNum = patchRange.patchNum;
          basePatchNum = patchRange.basePatchNum;
        }
        return {patchNum, basePatchNum};
      }
  
      _getChangePath(change, patchRange, revisions) {
        if ([change, patchRange].some(arg => arg === undefined)) {
          return '';
        }
        const range = this._getChangeUrlRange(patchRange, revisions);
        return Gerrit.Nav.getUrlForChange(change, range.patchNum,
            range.basePatchNum);
      }
  
      _navigateToChange(change, patchRange, revisions) {
        const range = this._getChangeUrlRange(patchRange, revisions);
        Gerrit.Nav.navigateToChange(change, range.patchNum, range.basePatchNum);
      }
  
      _computeChangePath(change, patchRangeRecord, revisions) {
        return this._getChangePath(change, patchRangeRecord.base, revisions);
      }
  
      _formatFilesForDropdown(fileList, patchNum, changeComments) {
        // Polymer 2: check for undefined
        if ([
          fileList,
          patchNum,
          changeComments,
        ].some(arg => arg === undefined)) {
          return;
        }
  
        if (!fileList) { return; }
        const dropdownContent = [];
        for (const path of fileList) {
          dropdownContent.push({
            text: this.computeDisplayPath(path),
            mobileText: this.computeTruncatedPath(path),
            value: path,
            bottomText: this._computeCommentString(changeComments, patchNum,
                path),
          });
        }
        return dropdownContent;
      }
  
      _computeCommentString(changeComments, patchNum, path) {
        const unresolvedCount = changeComments.computeUnresolvedNum(patchNum,
            path);
        const commentCount = changeComments.computeCommentCount(patchNum, path);
        const commentString = GrCountStringFormatter.computePluralString(
            commentCount, 'comment');
        const unresolvedString = GrCountStringFormatter.computeString(
            unresolvedCount, 'unresolved');
  
        return commentString +
            // Add a space if both comments and unresolved
            (commentString && unresolvedString ? ', ' : '') +
            // Add parentheses around unresolved if it exists.
            (unresolvedString ? `${unresolvedString}` : '');
      }
  
      _computePrefsButtonHidden(prefs, prefsDisabled) {
        return prefsDisabled || !prefs;
      }
  
      _handleFileChange(e) {
        // This is when it gets set initially.
        const path = e.detail.value;
        if (path === this._path) {
          return;
        }
  
        Gerrit.Nav.navigateToDiff(this._change, path, this._patchRange.patchNum,
            this._patchRange.basePatchNum);
      }
  
      _handleFileTap(e) {
        // async is needed so that that the click event is fired before the
        // dropdown closes (This was a bug for touch devices).
        this.async(() => {
          this.$.dropdown.close();
        }, 1);
      }
  
      _handlePatchChange(e) {
        const {basePatchNum, patchNum} = e.detail;
        if (this.patchNumEquals(basePatchNum, this._patchRange.basePatchNum) &&
            this.patchNumEquals(patchNum, this._patchRange.patchNum)) { return; }
        Gerrit.Nav.navigateToDiff(
            this._change, this._path, patchNum, basePatchNum);
      }
  
      _handlePrefsTap(e) {
        e.preventDefault();
        this.$.diffPreferencesDialog.open();
      }
  
      /**
       * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
       * the current state.
       *
       * The expected behavior is to use the mode specified in the user's
       * preferences unless they have manually chosen the alternative view or they
       * are on a mobile device. If the user navigates up to the change view, it
       * should clear this choice and revert to the preference the next time a
       * diff is viewed.
       *
       * Use side-by-side if the user is not logged in.
       *
       * @return {string}
       */
      _getDiffViewMode() {
        if (this.changeViewState.diffMode) {
          return this.changeViewState.diffMode;
        } else if (this._userPrefs) {
          this.set('changeViewState.diffMode', this._userPrefs.default_diff_view);
          return this._userPrefs.default_diff_view;
        } else {
          return 'SIDE_BY_SIDE';
        }
      }
  
      _computeModeSelectHideClass(isImageDiff) {
        return isImageDiff ? 'hide' : '';
      }
  
      _onLineSelected(e, detail) {
        this.$.cursor.moveToLineNumber(detail.number, detail.side);
        if (!this._change) { return; }
        const cursorAddress = this.$.cursor.getAddress();
        const number = cursorAddress ? cursorAddress.number : undefined;
        const leftSide = cursorAddress ? cursorAddress.leftSide : undefined;
        const url = Gerrit.Nav.getUrlForDiffById(this._changeNum,
            this._change.project, this._path, this._patchRange.patchNum,
            this._patchRange.basePatchNum, number, leftSide);
        history.replaceState(null, '', url);
      }
  
      _computeDownloadDropdownLinks(
          project, changeNum, patchRange, path, diff) {
        if (!patchRange || !patchRange.patchNum) { return []; }
  
        const links = [
          {
            url: this._computeDownloadPatchLink(
                project, changeNum, patchRange, path),
            name: 'Patch',
          },
        ];
  
        if (diff && diff.meta_a) {
          let leftPath = path;
          if (diff.change_type === 'RENAMED') {
            leftPath = diff.meta_a.name;
          }
          links.push(
              {
                url: this._computeDownloadFileLink(
                    project, changeNum, patchRange, leftPath, true),
                name: 'Left Content',
              }
          );
        }
  
        if (diff && diff.meta_b) {
          links.push(
              {
                url: this._computeDownloadFileLink(
                    project, changeNum, patchRange, path, false),
                name: 'Right Content',
              }
          );
        }
  
        return links;
      }
  
      _computeDownloadFileLink(
          project, changeNum, patchRange, path, isBase) {
        let patchNum = patchRange.patchNum;
  
        const comparedAgainsParent = patchRange.basePatchNum === 'PARENT';
  
        if (isBase && !comparedAgainsParent) {
          patchNum = patchRange.basePatchNum;
        }
  
        let url = this.changeBaseURL(project, changeNum, patchNum) +
            `/files/${encodeURIComponent(path)}/download`;
  
        if (isBase && comparedAgainsParent) {
          url += '?parent=1';
        }
  
        return url;
      }
  
      _computeDownloadPatchLink(project, changeNum, patchRange, path) {
        let url = this.changeBaseURL(project, changeNum, patchRange.patchNum);
        url += '/patch?zip&path=' + encodeURIComponent(path);
        return url;
      }
  
      _loadComments() {
        return this.$.commentAPI.loadAll(this._changeNum).then(comments => {
          this._changeComments = comments;
          this._commentMap = this._getPaths(this._patchRange);
  
          this._commentsForDiff = this._getCommentsForPath(this._path,
              this._patchRange, this._projectConfig);
        });
      }
  
      _getPaths(patchRange) {
        return this._changeComments.getPaths(patchRange);
      }
  
      _getCommentsForPath(path, patchRange, projectConfig) {
        return this._changeComments.getCommentsBySideForPath(path, patchRange,
            projectConfig);
      }
  
      _getDiffDrafts() {
        return this.$.restAPI.getDiffDrafts(this._changeNum);
      }
  
      _computeCommentSkips(commentMap, fileList, path) {
        // Polymer 2: check for undefined
        if ([
          commentMap,
          fileList,
          path,
        ].some(arg => arg === undefined)) {
          return undefined;
        }
  
        const skips = {previous: null, next: null};
        if (!fileList.length) { return skips; }
        const pathIndex = fileList.indexOf(path);
  
        // Scan backward for the previous file.
        for (let i = pathIndex - 1; i >= 0; i--) {
          if (commentMap[fileList[i]]) {
            skips.previous = fileList[i];
            break;
          }
        }
  
        // Scan forward for the next file.
        for (let i = pathIndex + 1; i < fileList.length; i++) {
          if (commentMap[fileList[i]]) {
            skips.next = fileList[i];
            break;
          }
        }
  
        return skips;
      }
  
      _computeDiffClass(panelFloatingDisabled) {
        if (panelFloatingDisabled) {
          return 'noOverflow';
        }
      }
  
      /**
       * @param {!Object} patchRangeRecord
       */
      _computeEditMode(patchRangeRecord) {
        const patchRange = patchRangeRecord.base || {};
        return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
      }
  
      /**
       * @param {boolean} editMode
       */
      _computeContainerClass(editMode) {
        return editMode ? 'editMode' : '';
      }
  
      _computeBlameToggleLabel(loaded, loading) {
        if (loaded) { return 'Hide blame'; }
        return 'Show blame';
      }
  
      /**
       * Load and display blame information if it has not already been loaded.
       * Otherwise hide it.
       */
      _toggleBlame() {
        if (this._isBlameLoaded) {
          this.$.diffHost.clearBlame();
          return;
        }
  
        this._isBlameLoading = true;
        this.fire('show-alert', {message: MSG_LOADING_BLAME});
        this.$.diffHost.loadBlame()
            .then(() => {
              this._isBlameLoading = false;
              this.fire('show-alert', {message: MSG_LOADED_BLAME});
            })
            .catch(() => {
              this._isBlameLoading = false;
            });
      }
  
      _computeBlameLoaderClass(isImageDiff, path) {
        return !this.isMagicPath(path) && !isImageDiff ? 'show' : '';
      }
  
      _getRevisionInfo(change) {
        return new Gerrit.RevisionInfo(change);
      }
  
      _computeFileNum(file, files) {
        // Polymer 2: check for undefined
        if ([file, files].some(arg => arg === undefined)) {
          return undefined;
        }
  
        return files.findIndex(({value}) => value === file) + 1;
      }
  
      /**
       * @param {number} fileNum
       * @param {!Array<string>} files
       * @return {string}
       */
      _computeFileNumClass(fileNum, files) {
        if (files && fileNum > 0) {
          return 'show';
        }
        return '';
      }
  
      _handleExpandAllDiffContext(e) {
        if (this.shouldSuppressKeyboardShortcut(e)) { return; }
        this.$.diffHost.expandAllContext();
      }
  
      _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
        return disableDiffPrefs || !loggedIn;
      }
  
      _handleNextUnreviewedFile(e) {
        if (this.shouldSuppressKeyboardShortcut(e)) { return; }
        this._setReviewed(true);
        // Ensure that the currently viewed file always appears in unreviewedFiles
        // so we resolve the right "next" file.
        const unreviewedFiles = this._fileList
            .filter(file =>
              (file === this._path || !this._reviewedFiles.has(file)));
        this._navToFile(this._path, unreviewedFiles, 1);
      }
  
      _handleReloadingDiffPreference() {
        this._getDiffPreferences();
      }
    }
  
    customElements.define(GrDiffView.is, GrDiffView);
  })();
  