// 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 DiffSides = {
    LEFT: 'left',
    RIGHT: 'right',
  };

  const DiffViewMode = {
    SIDE_BY_SIDE: 'SIDE_BY_SIDE',
    UNIFIED: 'UNIFIED_DIFF',
  };

  const ScrollBehavior = {
    KEEP_VISIBLE: 'keep-visible',
    NEVER: 'never',
  };

  const LEFT_SIDE_CLASS = 'target-side-left';
  const RIGHT_SIDE_CLASS = 'target-side-right';

  Polymer({
    is: 'gr-diff-cursor',

    properties: {
      /**
       * Either DiffSides.LEFT or DiffSides.RIGHT.
       */
      side: {
        type: String,
        value: DiffSides.RIGHT,
      },
      /** @type {!HTMLElement|undefined} */
      diffRow: {
        type: Object,
        notify: true,
        observer: '_rowChanged',
      },

      /**
       * The diff views to cursor through and listen to.
       */
      diffs: {
        type: Array,
        value() { return []; },
      },

      /**
       * If set, the cursor will attempt to move to the line number (instead of
       * the first chunk) the next time the diff renders. It is set back to null
       * when used.
       *
       * @type (?number)
       */
      initialLineNumber: {
        type: Number,
        value: null,
      },

      /**
       * The scroll behavior for the cursor. Values are 'never' and
       * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
       * the viewport.
       */
      _scrollBehavior: {
        type: String,
        value: ScrollBehavior.KEEP_VISIBLE,
      },

      _focusOnMove: {
        type: Boolean,
        value: true,
      },

      _listeningForScroll: Boolean,
    },

    observers: [
      '_updateSideClass(side)',
      '_diffsChanged(diffs.splices)',
    ],

    attached() {
      // Catch when users are scrolling as the view loads.
      this.listen(window, 'scroll', '_handleWindowScroll');
    },

    detached() {
      this.unlisten(window, 'scroll', '_handleWindowScroll');
    },

    moveLeft() {
      this.side = DiffSides.LEFT;
      if (this._isTargetBlank()) {
        this.moveUp();
      }
    },

    moveRight() {
      this.side = DiffSides.RIGHT;
      if (this._isTargetBlank()) {
        this.moveUp();
      }
    },

    moveDown() {
      if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
        this.$.cursorManager.next(this._rowHasSide.bind(this));
      } else {
        this.$.cursorManager.next();
      }
    },

    moveUp() {
      if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
        this.$.cursorManager.previous(this._rowHasSide.bind(this));
      } else {
        this.$.cursorManager.previous();
      }
    },

    moveToNextChunk() {
      this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
          target => {
            return target.parentNode.scrollHeight;
          });
      this._fixSide();
    },

    moveToPreviousChunk() {
      this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this));
      this._fixSide();
    },

    moveToNextCommentThread() {
      this.$.cursorManager.next(this._rowHasThread.bind(this));
      this._fixSide();
    },

    moveToPreviousCommentThread() {
      this.$.cursorManager.previous(this._rowHasThread.bind(this));
      this._fixSide();
    },

    /**
     * @param {number} number
     * @param {string} side
     * @param {string=} opt_path
     */
    moveToLineNumber(number, side, opt_path) {
      const row = this._findRowByNumberAndFile(number, side, opt_path);
      if (row) {
        this.side = side;
        this.$.cursorManager.setCursor(row);
      }
    },

    /**
     * Get the line number element targeted by the cursor row and side.
     * @return {?Element|undefined}
     */
    getTargetLineElement() {
      let lineElSelector = '.lineNum';

      if (!this.diffRow) {
        return;
      }

      if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
        lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right';
      }

      return this.diffRow.querySelector(lineElSelector);
    },

    getTargetDiffElement() {
      // Find the parent diff element of the cursor row.
      for (let diff = this.diffRow; diff; diff = diff.parentElement) {
        if (diff.tagName === 'GR-DIFF') { return diff; }
      }
      return null;
    },

    moveToFirstChunk() {
      this.$.cursorManager.moveToStart();
      this.moveToNextChunk();
    },

    reInitCursor() {
      this._updateStops();
      if (this.initialLineNumber) {
        this.moveToLineNumber(this.initialLineNumber, this.side);
        this.initialLineNumber = null;
      } else {
        this.moveToFirstChunk();
      }
    },

    _handleWindowScroll() {
      if (this._listeningForScroll) {
        this._scrollBehavior = ScrollBehavior.NEVER;
        this._focusOnMove = false;
        this._listeningForScroll = false;
      }
    },

    handleDiffUpdate() {
      this._updateStops();

      if (!this.diffRow) {
        this.reInitCursor();
      }
      this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
      this._focusOnMove = true;
      this._listeningForScroll = false;
    },

    _handleDiffRenderStart() {
      this._listeningForScroll = true;
    },

    /**
     * Get an object describing the location of the cursor. Such as
     * {leftSide: false, number: 123} for line 123 of the revision, or
     * {leftSide: true, number: 321} for line 321 of the base patch.
     * Returns null if an address is not available.
     * @return {?Object}
     */
    getAddress() {
      if (!this.diffRow) { return null; }

      // Get the line-number cell targeted by the cursor. If the mode is unified
      // then prefer the revision cell if available.
      let cell;
      if (this._getViewMode() === DiffViewMode.UNIFIED) {
        cell = this.diffRow.querySelector('.lineNum.right');
        if (!cell) {
          cell = this.diffRow.querySelector('.lineNum.left');
        }
      } else {
        cell = this.diffRow.querySelector('.lineNum.' + this.side);
      }
      if (!cell) { return null; }

      const number = cell.getAttribute('data-value');
      if (!number || number === 'FILE') { return null; }

      return {
        leftSide: cell.matches('.left'),
        number: parseInt(number, 10),
      };
    },

    _getViewMode() {
      if (!this.diffRow) {
        return null;
      }

      if (this.diffRow.classList.contains('side-by-side')) {
        return DiffViewMode.SIDE_BY_SIDE;
      } else {
        return DiffViewMode.UNIFIED;
      }
    },

    _rowHasSide(row) {
      const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') +
          ' + .content';
      return !!row.querySelector(selector);
    },

    _isFirstRowOfChunk(row) {
      const parentClassList = row.parentNode.classList;
      return parentClassList.contains('section') &&
          parentClassList.contains('delta') &&
          !row.previousSibling;
    },

    _rowHasThread(row) {
      return row.querySelector('gr-diff-comment-thread');
    },

    /**
     * If we jumped to a row where there is no content on the current side then
     * switch to the alternate side.
     */
    _fixSide() {
      if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE &&
          this._isTargetBlank()) {
        this.side = this.side === DiffSides.LEFT ?
            DiffSides.RIGHT : DiffSides.LEFT;
      }
    },

    _isTargetBlank() {
      if (!this.diffRow) {
        return false;
      }

      const actions = this._getActionsForRow();
      return (this.side === DiffSides.LEFT && !actions.left) ||
          (this.side === DiffSides.RIGHT && !actions.right);
    },

    _rowChanged(newRow, oldRow) {
      if (oldRow) {
        oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
      }
      this._updateSideClass();
    },

    _updateSideClass() {
      if (!this.diffRow) {
        return;
      }
      this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT,
          this.diffRow);
      this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT,
          this.diffRow);
    },

    _isActionType(type) {
      return type !== 'blank' && type !== 'contextControl';
    },

    _getActionsForRow() {
      const actions = {left: false, right: false};
      if (this.diffRow) {
        actions.left = this._isActionType(
            this.diffRow.getAttribute('left-type'));
        actions.right = this._isActionType(
            this.diffRow.getAttribute('right-type'));
      }
      return actions;
    },

    _getStops() {
      return this.diffs.reduce(
          (stops, diff) => {
            return stops.concat(diff.getCursorStops());
          }, []);
    },

    _updateStops() {
      this.$.cursorManager.stops = this._getStops();
    },

    /**
     * Setup and tear down on-render listeners for any diffs that are added or
     * removed from the cursor.
     * @private
     */
    _diffsChanged(changeRecord) {
      if (!changeRecord) { return; }

      this._updateStops();

      let splice;
      let i;
      for (let spliceIdx = 0;
        changeRecord.indexSplices &&
            spliceIdx < changeRecord.indexSplices.length;
        spliceIdx++) {
        splice = changeRecord.indexSplices[spliceIdx];

        for (i = splice.index;
            i < splice.index + splice.addedCount;
            i++) {
          this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart');
          this.listen(this.diffs[i], 'render-content', 'handleDiffUpdate');
        }

        for (i = 0;
            i < splice.removed && splice.removed.length;
            i++) {
          this.unlisten(splice.removed[i],
              'render-start', '_handleDiffRenderStart');
          this.unlisten(splice.removed[i],
              'render-content', 'handleDiffUpdate');
        }
      }
    },

    _findRowByNumberAndFile(targetNumber, side, opt_path) {
      let stops;
      if (opt_path) {
        const diff = this.diffs.filter(diff => diff.path === opt_path)[0];
        stops = diff.getCursorStops();
      } else {
        stops = this.$.cursorManager.stops;
      }
      let selector;
      for (let i = 0; i < stops.length; i++) {
        selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]';
        if (stops[i].querySelector(selector)) {
          return stops[i];
        }
      }
    },
  });
})();