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';
  
    /**
     * Possible CSS classes indicating the state of selection. Dynamically added/
     * removed based on where the user clicks within the diff.
     */
    const SelectionClass = {
      COMMENT: 'selected-comment',
      LEFT: 'selected-left',
      RIGHT: 'selected-right',
      BLAME: 'selected-blame',
    };
  
    const getNewCache = () => { return {left: null, right: null}; };
  
    Polymer({
      is: 'gr-diff-selection',
  
      properties: {
        diff: Object,
        /** @type {?Object} */
        _cachedDiffBuilder: Object,
        _linesCache: {
          type: Object,
          value: getNewCache(),
        },
      },
  
      observers: [
        '_diffChanged(diff)',
      ],
  
      listeners: {
        copy: '_handleCopy',
        down: '_handleDown',
      },
  
      behaviors: [
        Gerrit.DomUtilBehavior,
      ],
  
      attached() {
        this.classList.add(SelectionClass.RIGHT);
      },
  
      get diffBuilder() {
        if (!this._cachedDiffBuilder) {
          this._cachedDiffBuilder =
              Polymer.dom(this).querySelector('gr-diff-builder');
        }
        return this._cachedDiffBuilder;
      },
  
      _diffChanged() {
        this._linesCache = getNewCache();
      },
  
      _handleDownOnRangeComment(node) {
        if (node &&
            node.nodeName &&
            node.nodeName.toLowerCase() === 'gr-comment-thread') {
          this._setClasses([
            SelectionClass.COMMENT,
            node.commentSide === 'left' ?
              SelectionClass.LEFT :
              SelectionClass.RIGHT,
          ]);
          return true;
        }
        return false;
      },
  
      _handleDown(e) {
        // Handle the down event on comment thread in Polymer 2
        const handled = this._handleDownOnRangeComment(e.target);
        if (handled) return;
  
        const lineEl = this.diffBuilder.getLineElByChild(e.target);
        const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
        if (!lineEl && !blameSelected) { return; }
  
        const targetClasses = [];
  
        if (blameSelected) {
          targetClasses.push(SelectionClass.BLAME);
        } else {
          const commentSelected =
              this._elementDescendedFromClass(e.target, 'gr-comment');
          const side = this.diffBuilder.getSideByLineEl(lineEl);
  
          targetClasses.push(side === 'left' ?
            SelectionClass.LEFT :
            SelectionClass.RIGHT);
  
          if (commentSelected) {
            targetClasses.push(SelectionClass.COMMENT);
          }
        }
  
        this._setClasses(targetClasses);
      },
  
      /**
       * Set the provided list of classes on the element, to the exclusion of all
       * other SelectionClass values.
       *
       * @param {!Array<!string>} targetClasses
       */
      _setClasses(targetClasses) {
        // Remove any selection classes that do not belong.
        for (const key in SelectionClass) {
          if (SelectionClass.hasOwnProperty(key)) {
            const className = SelectionClass[key];
            if (!targetClasses.includes(className)) {
              this.classList.remove(SelectionClass[key]);
            }
          }
        }
        // Add new selection classes iff they are not already present.
        for (const _class of targetClasses) {
          if (!this.classList.contains(_class)) {
            this.classList.add(_class);
          }
        }
      },
  
      _getCopyEventTarget(e) {
        return Polymer.dom(e).rootTarget;
      },
  
      /**
       * Utility function to determine whether an element is a descendant of
       * another element with the particular className.
       *
       * @param {!Element} element
       * @param {!string} className
       * @return {boolean}
       */
      _elementDescendedFromClass(element, className) {
        return this.descendedFromClass(element, className,
            this.diffBuilder.diffElement);
      },
  
      _handleCopy(e) {
        let commentSelected = false;
        const target = this._getCopyEventTarget(e);
        if (target.type === 'textarea') { return; }
        if (!this._elementDescendedFromClass(target, 'diff-row')) { return; }
        if (this.classList.contains(SelectionClass.COMMENT)) {
          commentSelected = true;
        }
        const lineEl = this.diffBuilder.getLineElByChild(target);
        if (!lineEl) {
          return;
        }
        const side = this.diffBuilder.getSideByLineEl(lineEl);
        const text = this._getSelectedText(side, commentSelected);
        if (text) {
          e.clipboardData.setData('Text', text);
          e.preventDefault();
        }
      },
  
      /**
       * For Polymer 2, use shadowRoot.getSelection instead.
       */
      _getSelection() {
        const diffHost = util.querySelector(document.body, 'gr-diff');
        const selection = diffHost &&
          diffHost.shadowRoot &&
          diffHost.shadowRoot.getSelection();
        return selection ? selection: window.getSelection();
      },
  
      /**
       * Get the text of the current selection. If commentSelected is
       * true, it returns only the text of comments within the selection.
       * Otherwise it returns the text of the selected diff region.
       *
       * @param {!string} side The side that is selected.
       * @param {boolean} commentSelected Whether or not a comment is selected.
       * @return {string} The selected text.
       */
      _getSelectedText(side, commentSelected) {
        const sel = this._getSelection();
        if (sel.rangeCount != 1) {
          return ''; // No multi-select support yet.
        }
        if (commentSelected) {
          return this._getCommentLines(sel, side);
        }
        const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
        const startLineEl =
            this.diffBuilder.getLineElByChild(range.startContainer);
        const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
        // Happens when triple click in side-by-side mode with other side empty.
        const endsAtOtherEmptySide = !endLineEl &&
            range.endOffset === 0 &&
            range.endContainer.nodeName === 'TD' &&
            (range.endContainer.classList.contains('left') ||
             range.endContainer.classList.contains('right'));
        const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10);
        let endLineNum;
        if (endsAtOtherEmptySide) {
          endLineNum = startLineNum + 1;
        } else if (endLineEl) {
          endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10);
        }
  
        return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum,
            range.endOffset, side);
      },
  
      /**
       * Query the diff object for the selected lines.
       *
       * @param {number} startLineNum
       * @param {number} startOffset
       * @param {number|undefined} endLineNum Use undefined to get the range
       *     extending to the end of the file.
       * @param {number} endOffset
       * @param {!string} side The side that is currently selected.
       * @return {string} The selected diff text.
       */
      _getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) {
        const lines =
            this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
        if (lines.length) {
          lines[lines.length - 1] = lines[lines.length - 1]
              .substring(0, endOffset);
          lines[0] = lines[0].substring(startOffset);
        }
        return lines.join('\n');
      },
  
      /**
       * Query the diff object for the lines from a particular side.
       *
       * @param {!string} side The side that is currently selected.
       * @return {!Array<string>} An array of strings indexed by line number.
       */
      _getDiffLines(side) {
        if (this._linesCache[side]) {
          return this._linesCache[side];
        }
        let lines = [];
        const key = side === 'left' ? 'a' : 'b';
        for (const chunk of this.diff.content) {
          if (chunk.ab) {
            lines = lines.concat(chunk.ab);
          } else if (chunk[key]) {
            lines = lines.concat(chunk[key]);
          }
        }
        this._linesCache[side] = lines;
        return lines;
      },
  
      /**
       * Query the diffElement for comments and check whether they lie inside the
       * selection range.
       *
       * @param {!Selection} sel The selection of the window.
       * @param {!string} side The side that is currently selected.
       * @return {string} The selected comment text.
       */
      _getCommentLines(sel, side) {
        const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
        const content = [];
        // Query the diffElement for comments.
        const messages = this.diffBuilder.diffElement.querySelectorAll(
            `.side-by-side [data-side="${side
            }"] .message *, .unified .message *`);
  
        for (let i = 0; i < messages.length; i++) {
          const el = messages[i];
          // Check if the comment element exists inside the selection.
          if (sel.containsNode(el, true)) {
            // Padded elements require newlines for accurate spacing.
            if (el.parentElement.id === 'container' ||
                el.parentElement.nodeName === 'BLOCKQUOTE') {
              if (content.length && content[content.length - 1] !== '') {
                content.push('');
              }
            }
  
            if (el.id === 'output' &&
                !this._elementDescendedFromClass(el, 'collapsed')) {
              content.push(this._getTextContentForRange(el, sel, range));
            }
          }
        }
  
        return content.join('\n');
      },
  
      /**
       * Given a DOM node, a selection, and a selection range, recursively get all
       * of the text content within that selection.
       * Using a domNode that isn't in the selection returns an empty string.
       *
       * @param {!Node} domNode The root DOM node.
       * @param {!Selection} sel The selection.
       * @param {!Range} range The normalized selection range.
       * @return {string} The text within the selection.
       */
      _getTextContentForRange(domNode, sel, range) {
        if (!sel.containsNode(domNode, true)) { return ''; }
  
        let text = '';
        if (domNode instanceof Text) {
          text = domNode.textContent;
          if (domNode === range.endContainer) {
            text = text.substring(0, range.endOffset);
          }
          if (domNode === range.startContainer) {
            text = text.substring(range.startOffset);
          }
        } else {
          for (const childNode of domNode.childNodes) {
            text += this._getTextContentForRange(childNode, sel, range);
          }
        }
        return text;
      },
    });
  })();
  