text: define([
      'summernote/core/agent',
      'summernote/core/func',
      'summernote/core/list',
      'summernote/core/dom'
  ], function (agent, func, list, dom) {
  
      var range = (function () {
  
          /**
           * return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js
           *
           * @param {TextRange} textRange
           * @param {Boolean} isStart
           * @return {BoundaryPoint}
           *
           * @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
           */
          var textRangeToPoint = function (textRange, isStart) {
              var container = textRange.parentElement(), offset;
  
              var tester = document.body.createTextRange(), prevContainer;
              var childNodes = list.from(container.childNodes);
              for (offset = 0; offset < childNodes.length; offset++) {
                  if (dom.isText(childNodes[offset])) {
                      continue;
                  }
                  tester.moveToElementText(childNodes[offset]);
                  if (tester.compareEndPoints('StartToStart', textRange) >= 0) {
                      break;
                  }
                  prevContainer = childNodes[offset];
              }
  
              if (offset !== 0 && dom.isText(childNodes[offset - 1])) {
                  var textRangeStart = document.body.createTextRange(), curTextNode = null;
                  textRangeStart.moveToElementText(prevContainer || container);
                  textRangeStart.collapse(!prevContainer);
                  curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild;
  
                  var pointTester = textRange.duplicate();
                  pointTester.setEndPoint('StartToStart', textRangeStart);
                  var textCount = pointTester.text.replace(/[\r\n]/g, '').length;
  
                  while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) {
                      textCount -= curTextNode.nodeValue.length;
                      curTextNode = curTextNode.nextSibling;
                  }
  
                  /* jshint ignore:start */
                  var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack
                  /* jshint ignore:end */
  
                  if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) &&
                      textCount === curTextNode.nodeValue.length) {
                      textCount -= curTextNode.nodeValue.length;
                      curTextNode = curTextNode.nextSibling;
                  }
  
                  container = curTextNode;
                  offset = textCount;
              }
  
              return {
                  cont: container,
                  offset: offset
              };
          };
  
          /**
           * return TextRange from boundary point (inspired by google closure-library)
           * @param {BoundaryPoint} point
           * @return {TextRange}
           */
          var pointToTextRange = function (point) {
              var textRangeInfo = function (container, offset) {
                  var node, isCollapseToStart;
  
                  if (dom.isText(container)) {
                      var prevTextNodes = dom.listPrev(container, func.not(dom.isText));
                      var prevContainer = list.last(prevTextNodes).previousSibling;
                      node = prevContainer || container.parentNode;
                      offset += list.sum(list.tail(prevTextNodes), dom.nodeLength);
                      isCollapseToStart = !prevContainer;
                  } else {
                      node = container.childNodes[offset] || container;
                      if (dom.isText(node)) {
                          return textRangeInfo(node, 0);
                      }
  
                      offset = 0;
                      isCollapseToStart = false;
                  }
  
                  return {
                      node: node,
                      collapseToStart: isCollapseToStart,
                      offset: offset
                  };
              };
  
              var textRange = document.body.createTextRange();
              var info = textRangeInfo(point.node, point.offset);
  
              textRange.moveToElementText(info.node);
              textRange.collapse(info.collapseToStart);
              textRange.moveStart('character', info.offset);
              return textRange;
          };
  
          /**
           * Wrapped Range
           *
           * @constructor
           * @param {Node} sc - start container
           * @param {Number} so - start offset
           * @param {Node} ec - end container
           * @param {Number} eo - end offset
           */
          var WrappedRange = function (sc, so, ec, eo) {
              this.sc = sc;
              this.so = so;
              this.ec = ec;
              this.eo = eo;
  
              // nativeRange: get nativeRange from sc, so, ec, eo
              var nativeRange = function () {
                  if (agent.isW3CRangeSupport) {
                      var w3cRange = document.createRange();
                      w3cRange.setStart(sc, so);
                      w3cRange.setEnd(ec, eo);
  
                      return w3cRange;
                  } else {
                      var textRange = pointToTextRange({
                          node: sc,
                          offset: so
                      });
  
                      textRange.setEndPoint('EndToEnd', pointToTextRange({
                          node: ec,
                          offset: eo
                      }));
  
                      return textRange;
                  }
              };
  
              this.getPoints = function () {
                  return {
                      sc: sc,
                      so: so,
                      ec: ec,
                      eo: eo
                  };
              };
  
              this.getStartPoint = function () {
                  return {
                      node: sc,
                      offset: so
                  };
              };
  
              this.getEndPoint = function () {
                  return {
                      node: ec,
                      offset: eo
                  };
              };
  
              /**
               * select update visible range
               */
              this.select = function () {
                  var nativeRng = nativeRange();
                  if (agent.isW3CRangeSupport) {
                      var selection = document.getSelection();
                      if (selection.rangeCount > 0) {
                          selection.removeAllRanges();
                      }
                      selection.addRange(nativeRng);
                  } else {
                      nativeRng.select();
                  }
  
                  return this;
              };
  
              /**
               * @return {WrappedRange}
               */
              this.normalize = function () {
  
                  /**
                   * @param {BoundaryPoint} point
                   * @param {Boolean} isLeftToRight
                   * @return {BoundaryPoint}
                   */
                  var getVisiblePoint = function (point, isLeftToRight) {
                      if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) ||
                          (dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) ||
                          (dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) ||
                          (dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) {
                          return point;
                      }
  
                      // point on block's edge
                      var block = dom.ancestor(point.node, dom.isBlock);
                      if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) ||
                          ((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) {
  
                          // returns point already on visible point
                          if (dom.isVisiblePoint(point)) {
                              return point;
                          }
                          // reverse direction
                          isLeftToRight = !isLeftToRight;
                      }
  
                      var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) :
                          dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint);
                      return nextPoint || point;
                  };
  
                  var endPoint = getVisiblePoint(this.getEndPoint(), false);
                  var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true);
  
                  return new WrappedRange(
                      startPoint.node,
                      startPoint.offset,
                      endPoint.node,
                      endPoint.offset
                  );
              };
  
              /**
               * returns matched nodes on range
               *
               * @param {Function} [pred] - predicate function
               * @param {Object} [options]
               * @param {Boolean} [options.includeAncestor]
               * @param {Boolean} [options.fullyContains]
               * @return {Node[]}
               */
              this.nodes = function (pred, options) {
                  pred = pred || func.ok;
  
                  var includeAncestor = options && options.includeAncestor;
                  var fullyContains = options && options.fullyContains;
  
                  // TODO compare points and sort
                  var startPoint = this.getStartPoint();
                  var endPoint = this.getEndPoint();
  
                  var nodes = [];
                  var leftEdgeNodes = [];
  
                  dom.walkPoint(startPoint, endPoint, function (point) {
                      if (dom.isEditable(point.node)) {
                          return;
                      }
  
                      var node;
                      if (fullyContains) {
                          if (dom.isLeftEdgePoint(point)) {
                              leftEdgeNodes.push(point.node);
                          }
                          if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) {
                              node = point.node;
                          }
                      } else if (includeAncestor) {
                          node = dom.ancestor(point.node, pred);
                      } else {
                          node = point.node;
                      }
  
                      if (node && pred(node)) {
                          nodes.push(node);
                      }
                  }, true);
  
                  return list.unique(nodes);
              };
  
              /**
               * returns commonAncestor of range
               * @return {Element} - commonAncestor
               */
              this.commonAncestor = function () {
                  return dom.commonAncestor(sc, ec);
              };
  
              /**
               * returns expanded range by pred
               *
               * @param {Function} pred - predicate function
               * @return {WrappedRange}
               */
              this.expand = function (pred) {
                  var startAncestor = dom.ancestor(sc, pred);
                  var endAncestor = dom.ancestor(ec, pred);
  
                  if (!startAncestor && !endAncestor) {
                      return new WrappedRange(sc, so, ec, eo);
                  }
  
                  var boundaryPoints = this.getPoints();
  
                  if (startAncestor) {
                      boundaryPoints.sc = startAncestor;
                      boundaryPoints.so = 0;
                  }
  
                  if (endAncestor) {
                      boundaryPoints.ec = endAncestor;
                      boundaryPoints.eo = dom.nodeLength(endAncestor);
                  }
  
                  return new WrappedRange(
                      boundaryPoints.sc,
                      boundaryPoints.so,
                      boundaryPoints.ec,
                      boundaryPoints.eo
                  );
              };
  
              /**
               * @param {Boolean} isCollapseToStart
               * @return {WrappedRange}
               */
              this.collapse = function (isCollapseToStart) {
                  if (isCollapseToStart) {
                      return new WrappedRange(sc, so, sc, so);
                  } else {
                      return new WrappedRange(ec, eo, ec, eo);
                  }
              };
  
              /**
               * splitText on range
               */
              this.splitText = function () {
                  var isSameContainer = sc === ec;
                  var boundaryPoints = this.getPoints();
  
                  if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) {
                      ec.splitText(eo);
                  }
  
                  if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) {
                      boundaryPoints.sc = sc.splitText(so);
                      boundaryPoints.so = 0;
  
                      if (isSameContainer) {
                          boundaryPoints.ec = boundaryPoints.sc;
                          boundaryPoints.eo = eo - so;
                      }
                  }
  
                  return new WrappedRange(
                      boundaryPoints.sc,
                      boundaryPoints.so,
                      boundaryPoints.ec,
                      boundaryPoints.eo
                  );
              };
  
              /**
               * delete contents on range
               * @return {WrappedRange}
               */
              this.deleteContents = function () {
                  if (this.isCollapsed()) {
                      return this;
                  }
  
                  var rng = this.splitText();
                  var nodes = rng.nodes(null, {
                      fullyContains: true
                  });
  
                  // find new cursor point
                  var point = dom.prevPointUntil(rng.getStartPoint(), function (point) {
                      return !list.contains(nodes, point.node);
                  });
  
                  var emptyParents = [];
                  $.each(nodes, function (idx, node) {
                      // find empty parents
                      var parent = node.parentNode;
                      if (point.node !== parent && dom.nodeLength(parent) === 1) {
                          emptyParents.push(parent);
                      }
                      dom.remove(node, false);
                  });
  
                  // remove empty parents
                  $.each(emptyParents, function (idx, node) {
                      dom.remove(node, false);
                  });
  
                  return new WrappedRange(
                      point.node,
                      point.offset,
                      point.node,
                      point.offset
                  ).normalize();
              };
  
              /**
               * makeIsOn: return isOn(pred) function
               */
              var makeIsOn = function (pred) {
                  return function () {
                      var ancestor = dom.ancestor(sc, pred);
                      return !!ancestor && (ancestor === dom.ancestor(ec, pred));
                  };
              };
  
              // isOnEditable: judge whether range is on editable or not
              this.isOnEditable = makeIsOn(dom.isEditable);
              // isOnList: judge whether range is on list node or not
              this.isOnList = makeIsOn(dom.isList);
              // isOnAnchor: judge whether range is on anchor node or not
              this.isOnAnchor = makeIsOn(dom.isAnchor);
              // isOnAnchor: judge whether range is on cell node or not
              this.isOnCell = makeIsOn(dom.isCell);
  
              /**
               * @param {Function} pred
               * @return {Boolean}
               */
              this.isLeftEdgeOf = function (pred) {
                  if (!dom.isLeftEdgePoint(this.getStartPoint())) {
                      return false;
                  }
  
                  var node = dom.ancestor(this.sc, pred);
                  return node && dom.isLeftEdgeOf(this.sc, node);
              };
  
              /**
               * returns whether range was collapsed or not
               */
              this.isCollapsed = function () {
                  return sc === ec && so === eo;
              };
  
              /**
               * wrap inline nodes which children of body with paragraph
               *
               * @return {WrappedRange}
               */
              this.wrapBodyInlineWithPara = function () {
                  if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) {
                      sc.innerHTML = dom.emptyPara;
                      return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0);
                  }
  
                  /**
                   * [workaround] firefox often create range on not visible point. so normalize here.
                   *  - firefox: |<p>text</p>|
                   *  - chrome: <p>|text|</p>
                   */
                  var rng = this.normalize();
                  if (dom.isParaInline(sc) || dom.isPara(sc)) {
                      return rng;
                  }
  
                  // find inline top ancestor
                  var topAncestor;
                  if (dom.isInline(rng.sc)) {
                      var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline));
                      topAncestor = list.last(ancestors);
                      if (!dom.isInline(topAncestor)) {
                          topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so];
                      }
                  } else {
                      topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0];
                  }
  
                  // siblings not in paragraph
                  var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse();
                  inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline));
  
                  // wrap with paragraph
                  if (inlineSiblings.length) {
                      var para = dom.wrap(list.head(inlineSiblings), 'p');
                      dom.appendChildNodes(para, list.tail(inlineSiblings));
                  }
  
                  return this.normalize();
              };
  
              /**
               * insert node at current cursor
               *
               * @param {Node} node
               * @return {Node}
               */
              this.insertNode = function (node) {
                  var rng = this.wrapBodyInlineWithPara().deleteContents();
                  var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node));
  
                  if (info.rightNode) {
                      info.rightNode.parentNode.insertBefore(node, info.rightNode);
                  } else {
                      info.container.appendChild(node);
                  }
  
                  return node;
              };
  
              /**
               * insert html at current cursor
               */
              this.pasteHTML = function (markup) {
                  var contentsContainer = $('<div></div>').html(markup)[0];
                  var childNodes = list.from(contentsContainer.childNodes);
  
                  var rng = this.wrapBodyInlineWithPara().deleteContents();
  
                  return childNodes.reverse().map(function (childNode) {
                      return rng.insertNode(childNode);
                  }).reverse();
              };
  
              /**
               * returns text in range
               *
               * @return {String}
               */
              this.toString = function () {
                  var nativeRng = nativeRange();
                  return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text;
              };
  
              /**
               * returns range for word before cursor
               *
               * @param {Boolean} [findAfter] - find after cursor, default: false
               * @return {WrappedRange}
               */
              this.getWordRange = function (findAfter) {
                  var endPoint = this.getEndPoint();
  
                  if (!dom.isCharPoint(endPoint)) {
                      return this;
                  }
  
                  var startPoint = dom.prevPointUntil(endPoint, function (point) {
                      return !dom.isCharPoint(point);
                  });
  
                  if (findAfter) {
                      endPoint = dom.nextPointUntil(endPoint, function (point) {
                          return !dom.isCharPoint(point);
                      });
                  }
  
                  return new WrappedRange(
                      startPoint.node,
                      startPoint.offset,
                      endPoint.node,
                      endPoint.offset
                  );
              };
  
              /**
               * create offsetPath bookmark
               *
               * @param {Node} editable
               */
              this.bookmark = function (editable) {
                  return {
                      s: {
                          path: dom.makeOffsetPath(editable, sc),
                          offset: so
                      },
                      e: {
                          path: dom.makeOffsetPath(editable, ec),
                          offset: eo
                      }
                  };
              };
  
              /**
               * create offsetPath bookmark base on paragraph
               *
               * @param {Node[]} paras
               */
              this.paraBookmark = function (paras) {
                  return {
                      s: {
                          path: list.tail(dom.makeOffsetPath(list.head(paras), sc)),
                          offset: so
                      },
                      e: {
                          path: list.tail(dom.makeOffsetPath(list.last(paras), ec)),
                          offset: eo
                      }
                  };
              };
  
              /**
               * getClientRects
               * @return {Rect[]}
               */
              this.getClientRects = function () {
                  var nativeRng = nativeRange();
                  return nativeRng.getClientRects();
              };
          };
  
          /**
           * @class core.range
           *
           * Data structure
           *  * BoundaryPoint: a point of dom tree
           *  * BoundaryPoints: two boundaryPoints corresponding to the start and the end of the Range
           *
           * See to http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position
           *
           * @singleton
           * @alternateClassName range
           */
          return {
              /**
               * @method
               *
               * create Range Object From arguments or Browser Selection
               *
               * @param {Node} sc - start container
               * @param {Number} so - start offset
               * @param {Node} ec - end container
               * @param {Number} eo - end offset
               * @return {WrappedRange}
               */
              create: function (sc, so, ec, eo) {
                  if (!arguments.length) { // from Browser Selection
                      if (agent.isW3CRangeSupport) {
                          var selection = document.getSelection();
                          if (!selection || selection.rangeCount === 0) {
                              return null;
                          } else if (dom.isBody(selection.anchorNode)) {
                              // Firefox: returns entire body as range on initialization. We won't never need it.
                              return null;
                          }
  
                          var nativeRng = selection.getRangeAt(0);
                          sc = nativeRng.startContainer;
                          so = nativeRng.startOffset;
                          ec = nativeRng.endContainer;
                          eo = nativeRng.endOffset;
                      } else { // IE8: TextRange
                          var textRange = document.selection.createRange();
                          var textRangeEnd = textRange.duplicate();
                          textRangeEnd.collapse(false);
                          var textRangeStart = textRange;
                          textRangeStart.collapse(true);
  
                          var startPoint = textRangeToPoint(textRangeStart, true),
                              endPoint = textRangeToPoint(textRangeEnd, false);
  
                          // same visible point case: range was collapsed.
                          if (dom.isText(startPoint.node) && dom.isLeftEdgePoint(startPoint) &&
                              dom.isTextNode(endPoint.node) && dom.isRightEdgePoint(endPoint) &&
                              endPoint.node.nextSibling === startPoint.node) {
                              startPoint = endPoint;
                          }
  
                          sc = startPoint.cont;
                          so = startPoint.offset;
                          ec = endPoint.cont;
                          eo = endPoint.offset;
                      }
                  } else if (arguments.length === 2) { //collapsed
                      ec = sc;
                      eo = so;
                  }
                  return new WrappedRange(sc, so, ec, eo);
              },
  
              /**
               * @method
               *
               * create WrappedRange from node
               *
               * @param {Node} node
               * @return {WrappedRange}
               */
              createFromNode: function (node) {
                  var sc = node;
                  var so = 0;
                  var ec = node;
                  var eo = dom.nodeLength(ec);
  
                  // browsers can't target a picture or void node
                  if (dom.isVoid(sc)) {
                      so = dom.listPrev(sc).length - 1;
                      sc = sc.parentNode;
                  }
                  if (dom.isBR(ec)) {
                      eo = dom.listPrev(ec).length - 1;
                      ec = ec.parentNode;
                  } else if (dom.isVoid(ec)) {
                      eo = dom.listPrev(ec).length;
                      ec = ec.parentNode;
                  }
  
                  return this.create(sc, so, ec, eo);
              },
  
              /**
               * create WrappedRange from node after position
               *
               * @param {Node} node
               * @return {WrappedRange}
               */
              createFromNodeBefore: function (node) {
                  return this.createFromNode(node).collapse(true);
              },
  
              /**
               * create WrappedRange from node after position
               *
               * @param {Node} node
               * @return {WrappedRange}
               */
              createFromNodeAfter: function (node) {
                  return this.createFromNode(node).collapse();
              },
  
              /**
               * @method
               *
               * create WrappedRange from bookmark
               *
               * @param {Node} editable
               * @param {Object} bookmark
               * @return {WrappedRange}
               */
              createFromBookmark: function (editable, bookmark) {
                  var sc = dom.fromOffsetPath(editable, bookmark.s.path);
                  var so = bookmark.s.offset;
                  var ec = dom.fromOffsetPath(editable, bookmark.e.path);
                  var eo = bookmark.e.offset;
                  return new WrappedRange(sc, so, ec, eo);
              },
  
              /**
               * @method
               *
               * create WrappedRange from paraBookmark
               *
               * @param {Object} bookmark
               * @param {Node[]} paras
               * @return {WrappedRange}
               */
              createFromParaBookmark: function (bookmark, paras) {
                  var so = bookmark.s.offset;
                  var eo = bookmark.e.offset;
                  var sc = dom.fromOffsetPath(list.head(paras), bookmark.s.path);
                  var ec = dom.fromOffsetPath(list.last(paras), bookmark.e.path);
  
                  return new WrappedRange(sc, so, ec, eo);
              }
          };
      })();
  
      return range;
  });
  