// 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',
  };

  Polymer({
    is: '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
     */

    properties: {
      /**
       * 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',
      },
      /** @type {?} */
      _patchRange: Object,
      /** @type {?} */
      _commitRange: Object,
      /**
       * @type {{
       *  subject: string,
       *  project: string,
       *  revisions: string,
       * }}
       */
      _change: Object,
      /** @type {?} */
      _changeComments: Object,
      _changeNum: String,
      _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',
      },
      _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.*)',
      },
      _isBlameSupported: {
        type: Boolean,
        value: false,
      },
      _isBlameLoaded: Boolean,
      _isBlameLoading: {
        type: Boolean,
        value: false,
      },
      _allPatchSets: {
        type: Array,
        computed: 'computeAllPatchSets(_change, _change.revisions.*)',
      },
      _revisionInfo: {
        type: Object,
        computed: '_getRevisionInfo(_change)',
      },
    },

    behaviors: [
      Gerrit.KeyboardShortcutBehavior,
      Gerrit.PatchSetBehavior,
      Gerrit.PathListBehavior,
      Gerrit.RESTClientBehavior,
    ],

    observers: [
      '_getProjectConfig(_change.project)',
      '_getFiles(_changeNum, _patchRange.*)',
      '_setReviewedObserver(_loggedIn, params.*)',
    ],

    keyBindings: {
      'esc': '_handleEscKey',
      'shift+left': '_handleShiftLeftKey',
      'shift+right': '_handleShiftRightKey',
      'up k': '_handleUpKey',
      'down j': '_handleDownKey',
      'c': '_handleCKey',
      '[': '_handleLeftBracketKey',
      ']': '_handleRightBracketKey',
      'n shift+n': '_handleNKey',
      'p shift+p': '_handlePKey',
      'a shift+a': '_handleAKey',
      'u': '_handleUKey',
      ',': '_handleCommaKey',
      'v': '_handleVKey',
    },

    attached() {
      this._getLoggedIn().then(loggedIn => {
        this._loggedIn = loggedIn;
      });

      this.$.restAPI.getConfig().then(config => {
        this._isBlameSupported = config.change.allow_blame;
      });

      this.$.cursor.push('diffs', this.$.diff);
    },

    _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) {
      const patchRange = patchRangeRecord.base;
      return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray(
          changeNum, patchRange).then(files => {
            this._fileList = files;
          });
    },

    _getDiffPreferences() {
      return this.$.restAPI.getDiffPreferences();
    },

    _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);
    },

    _handleEscKey(e) {
      if (this.shouldSuppressKeyboardShortcut(e) ||
          this.modifierPressed(e)) { return; }

      e.preventDefault();
      this.$.diff.displayLine = false;
    },

    _handleShiftLeftKey(e) {
      if (this.shouldSuppressKeyboardShortcut(e)) { return; }

      e.preventDefault();
      this.$.cursor.moveLeft();
    },

    _handleShiftRightKey(e) {
      if (this.shouldSuppressKeyboardShortcut(e)) { return; }

      e.preventDefault();
      this.$.cursor.moveRight();
    },

    _handleUpKey(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.$.diff.displayLine = true;
      this.$.cursor.moveUp();
    },

    _handleDownKey(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.$.diff.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);
    },

    _handleCKey(e) {
      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
      if (this.$.diff.isRangeSelected()) { return; }
      if (this.modifierPressed(e)) { return; }

      e.preventDefault();
      const line = this.$.cursor.getTargetLineElement();
      if (line) {
        this.$.diff.addDraftAtLine(line);
      }
    },

    _handleLeftBracketKey(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);
    },

    _handleRightBracketKey(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);
    },

    _handleNKey(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();
      }
    },

    _handlePKey(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();
      }
    },

    _handleAKey(e) {
      if (this.shouldSuppressKeyboardShortcut(e)) { return; }

      if (e.detail.keyboardEvent.shiftKey) { // Hide left diff.
        e.preventDefault();
        this.$.diff.toggleLeftDiff();
        return;
      }

      if (this.modifierPressed(e)) { return; }

      if (!this._loggedIn) { return; }

      this.set('changeViewState.showReplyDialog', true);
      e.preventDefault();
      this._navToChangeView();
    },

    _handleUKey(e) {
      if (this.shouldSuppressKeyboardShortcut(e) ||
          this.modifierPressed(e)) { return; }

      e.preventDefault();
      this._navToChangeView();
    },

    _handleCommaKey(e) {
      if (this.shouldSuppressKeyboardShortcut(e) ||
          this.modifierPressed(e)) { return; }

      e.preventDefault();
      this.$.diffPreferences.open();
    },

    _handleVKey(e) {
      if (this.shouldSuppressKeyboardShortcut(e) ||
          this.modifierPressed(e)) { return; }

      e.preventDefault();
      if (this.changeViewState.diffMode=='SIDE_BY_SIDE') {
        this.set('changeViewState.diffMode', 'UNIFIED_DIFF');
      } else {
        this.set('changeViewState.diffMode', '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.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]};
    },

    _paramsChanged(value) {
      if (value.view !== Gerrit.Nav.View.DIFF) { return; }

      this.$.diff.lineOfInterest = this._getLineOfInterest(this.params);
      this._initCursor(this.params);

      this._changeNum = value.changeNum;
      this._patchRange = {
        patchNum: value.patchNum,
        basePatchNum: value.basePatchNum || PARENT,
      };
      this._path = value.path;

      // 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 = [];

      this._localPrefs = this.$.storage.getPreferences();
      promises.push(this._getDiffPreferences().then(prefs => {
        this._prefs = prefs;
      }));

      promises.push(this._getPreferences().then(prefs => {
        this._userPrefs = prefs;
      }));

      promises.push(this._getChangeDetail(this._changeNum).then(change => {
        let commit;
        let baseCommit;
        for (const k in change.revisions) {
          if (!change.revisions.hasOwnProperty(k)) continue;
          const patchNum = change.revisions[k]._number.toString();
          if (patchNum === this._patchRange.patchNum) {
            commit = k;
          } else if (patchNum === this._patchRange.basePatchNum) {
            baseCommit = k;
          }
        }
        this._commitRange = {commit, baseCommit};
      }));

      promises.push(this._loadComments());

      promises.push(this._getChangeEdit(this._changeNum));

      this._loading = true;
      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.$.diff.comments = this._commentsForDiff;
        this.$.diff.reload();
      });
    },

    _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) {
      const params = paramsRecord.base || {};
      if (_loggedIn && 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) {
      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) {
      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) {
      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, loggedIn) {
      return !loggedIn || !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.$.diffPreferences.open();
    },

    _handlePrefsSave(e) {
      e.stopPropagation();
      const el = Polymer.dom(e).rootTarget;
      el.disabled = true;
      this.$.storage.savePreferences(this._localPrefs);
      this._saveDiffPreferences().then(response => {
        el.disabled = false;
        if (!response.ok) { return response; }

        this.$.prefsOverlay.close();
      }).catch(err => {
        el.disabled = false;
      });
    },

    /**
     * _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) {
        return this.changeViewState.diffMode =
            this._userPrefs.default_diff_view;
      } else {
        return 'SIDE_BY_SIDE';
      }
    },

    _computeModeSelectHidden() {
      return this._isImageDiff;
    },

    _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);
    },

    _computeDownloadLink(changeNum, patchRange, path) {
      let url = this.changeBaseURL(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) {
      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.$.diff.clearBlame();
        return;
      }

      this._isBlameLoading = true;
      this.fire('show-alert', {message: MSG_LOADING_BLAME});
      this.$.diff.loadBlame()
          .then(() => {
            this._isBlameLoading = false;
            this.fire('show-alert', {message: MSG_LOADED_BLAME});
          })
          .catch(() => {
            this._isBlameLoading = false;
          });
    },

    _computeBlameLoaderClass(isImageDiff, supported) {
      return !isImageDiff && supported ? 'show' : '';
    },

    _getRevisionInfo(change) {
      return new Gerrit.RevisionInfo(change);
    },
  });
})();