/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Iterator between two DOM text range positions.
 */


// TODO(user): We're trying to migrate all ES5 subclasses of Closure
// Library to ES6. In ES6 this cannot be referenced before super is called. This
// file has at least one this before a super call (in ES5) and cannot be
// automatically upgraded to ES6 as a result. Please fix this if you have a
// chance. Note: This can sometimes be caused by not calling the super
// constructor at all. You can run the conversion tool yourself to see what it
// does on this file: blaze run //javascript/refactoring/es6_classes:convert.

goog.provide('goog.dom.TextRangeIterator');

goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.RangeIterator');
goog.require('goog.dom.TagName');
goog.require('goog.iter');



/**
 * Subclass of goog.dom.TagIterator that iterates over a DOM range.  It
 * adds functions to determine the portion of each text node that is selected.
 *
 * @param {Node} startNode The starting node position.
 * @param {number} startOffset The offset in to startNode.  If startNode is
 *     an element, indicates an offset in to childNodes.  If startNode is a
 *     text node, indicates an offset in to nodeValue.
 * @param {Node} endNode The ending node position.
 * @param {number} endOffset The offset in to endNode.  If endNode is
 *     an element, indicates an offset in to childNodes.  If endNode is a
 *     text node, indicates an offset in to nodeValue.
 * @param {boolean=} opt_reverse Whether to traverse nodes in reverse.
 * @constructor
 * @extends {goog.dom.RangeIterator}
 * @final
 */
goog.dom.TextRangeIterator = function(
    startNode, startOffset, endNode, endOffset, opt_reverse) {
  'use strict';
  /**
   * The first node in the selection.
   * @private {?Node}
   */
  this.startNode_ = null;

  /**
   * The last node in the selection.
   * @private {?Node}
   */
  this.endNode_ = null;

  /**
   * The offset within the first node in the selection.
   * @private {number}
   */
  this.startOffset_ = 0;

  /**
   * The offset within the last node in the selection.
   * @private {number}
   */
  this.endOffset_ = 0;

  /**
   * Whether the node iterator is moving in reverse.
   * @private {boolean}
   */
  this.isReversed_ = !!opt_reverse;

  var goNext;

  if (startNode) {
    this.startNode_ = startNode;
    this.startOffset_ = startOffset;
    this.endNode_ = endNode;
    this.endOffset_ = endOffset;

    // Skip to the offset nodes - being careful to special case BRs since these
    // have no children but still can appear as the startContainer of a range.
    if (startNode.nodeType == goog.dom.NodeType.ELEMENT &&
        /** @type {!Element} */ (startNode).tagName != goog.dom.TagName.BR) {
      var startChildren = startNode.childNodes;
      var candidate = startChildren[startOffset];
      if (candidate) {
        this.startNode_ = candidate;
        this.startOffset_ = 0;
      } else {
        if (startChildren.length) {
          this.startNode_ =
              /** @type {Node} */ (goog.array.peek(startChildren));
        }
        goNext = true;
      }
    }

    if (endNode.nodeType == goog.dom.NodeType.ELEMENT) {
      this.endNode_ = endNode.childNodes[endOffset];
      if (this.endNode_) {
        this.endOffset_ = 0;
      } else {
        // The offset was past the last element.
        this.endNode_ = endNode;
      }
    }
  }

  goog.dom.TextRangeIterator.base(
      this, 'constructor', this.isReversed_ ? this.endNode_ : this.startNode_,
      this.isReversed_);

  if (goNext) {
    this.next();
  }
};
goog.inherits(goog.dom.TextRangeIterator, goog.dom.RangeIterator);

/** @private {boolean} */
goog.dom.TextRangeIterator.prototype.hasSkippedPastLast_ = false;

/** @override */
goog.dom.TextRangeIterator.prototype.getStartTextOffset = function() {
  'use strict';
  // Offsets only apply to text nodes.  If our current node is the start node,
  // return the saved offset.  Otherwise, return 0.
  return this.node.nodeType != goog.dom.NodeType.TEXT ?
      -1 :
      this.node == this.startNode_ ? this.startOffset_ : 0;
};


/** @override */
goog.dom.TextRangeIterator.prototype.getEndTextOffset = function() {
  'use strict';
  // Offsets only apply to text nodes.  If our current node is the end node,
  // return the saved offset.  Otherwise, return the length of the node.
  return this.node.nodeType != goog.dom.NodeType.TEXT ?
      -1 :
      this.node == this.endNode_ ? this.endOffset_ : this.node.nodeValue.length;
};


/** @override */
goog.dom.TextRangeIterator.prototype.getStartNode = function() {
  'use strict';
  return this.startNode_;
};


/**
 * Change the start node of the iterator.
 * @param {Node} node The new start node.
 */
goog.dom.TextRangeIterator.prototype.setStartNode = function(node) {
  'use strict';
  if (!this.isStarted()) {
    this.setPosition(node);
  }

  this.startNode_ = node;
  this.startOffset_ = 0;
};


/** @override */
goog.dom.TextRangeIterator.prototype.getEndNode = function() {
  'use strict';
  return this.endNode_;
};


/**
 * Change the end node of the iterator.
 * @param {Node} node The new end node.
 */
goog.dom.TextRangeIterator.prototype.setEndNode = function(node) {
  'use strict';
  this.endNode_ = node;
  this.endOffset_ = 0;
};

/** @override */
goog.dom.TextRangeIterator.prototype.isLast = function() {
  'use strict';
  return this.isStarted() && this.isLastTag_();
};

/**
 * Returns true if the iterator is on the last step before iteration finishes,
 * false otherwise.
 * @return {boolean}
 * @private
 */
goog.dom.TextRangeIterator.prototype.isLastTag_ = function() {
  'use strict';
  if (this.node != this.lastNode_()) {
    return false;
  }
  // For a reverse iterator, this function will return true if the end offset is
  // > 0 and the iterator is not currently on an end tag OR the end offset = 0
  // and the iterator is currently on a start tag.
  if (this.isReversed_) {
    return this.startOffset_ ? !this.isEndTag() : this.isStartTag();
  }
  // For a forward-iterating iterator, this function will return true if the end
  // offset is 0 or the iterator is not currently on a start tag.
  return !this.endOffset_ || !this.isStartTag();
};

/**
 * Move to the next position in the selection. Returns `{done: true}` when it
 * passes the end of the range.
 * @return {!IIterableResult<!Node>} The node at the next position.
 * @override
 */
goog.dom.TextRangeIterator.prototype.next = function() {
  'use strict';
  if (this.isLast() || this.hasSkippedPastLast_) {
    return goog.iter.ES6_ITERATOR_DONE;
  }

  // Call the super function.
  return goog.dom.TextRangeIterator.superClass_.next.call(this);
};


/**
 * Get the last node the iterator will hit.
 * @return {?Node} The last node the iterator will hit.
 * @private
 */
goog.dom.TextRangeIterator.prototype.lastNode_ = function() {
  'use strict';
  return this.isReversed_ ? this.startNode_ : this.endNode_;
};

/** @override */
goog.dom.TextRangeIterator.prototype.skipTag = function() {
  'use strict';
  goog.dom.TextRangeIterator.superClass_.skipTag.apply(this);

  // If the node we are skipping contains the end node, we just skipped past
  // the end, so we stop the iteration.
  if (goog.dom.contains(this.node, this.lastNode_())) {
    this.hasSkippedPastLast_ = true;
  }
};


/**
 * @override
 * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
 */
goog.dom.TextRangeIterator.prototype.copyFrom = function(other) {
  'use strict';
  this.startNode_ = other.startNode_;
  this.endNode_ = other.endNode_;
  this.startOffset_ = other.startOffset_;
  this.endOffset_ = other.endOffset_;
  this.isReversed_ = other.isReversed_;

  goog.dom.TextRangeIterator.superClass_.copyFrom.call(this, other);
};


/**
 * @return {!goog.dom.TextRangeIterator} An identical iterator.
 * @override
 */
goog.dom.TextRangeIterator.prototype.clone = function() {
  'use strict';
  var copy = new goog.dom.TextRangeIterator(
      this.startNode_, this.startOffset_, this.endNode_, this.endOffset_,
      this.isReversed_);
  copy.copyFrom(this);
  return copy;
};
