/**
 * NormalizeRange.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */
define(
  'tinymce.core.selection.NormalizeRange',
  [
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Struct',
    'tinymce.core.caret.CaretContainer',
    'tinymce.core.dom.NodeType',
    'tinymce.core.dom.TreeWalker',
    'tinymce.core.fmt.CaretFormat',
    'tinymce.core.selection.RangeCompare'
  ],
  function (Option, Struct, CaretContainer, NodeType, TreeWalker, CaretFormat, RangeCompare) {
    var position = Struct.immutable('container', 'offset');
    var findParent = function (node, rootNode, predicate) {
      while (node && node !== rootNode) {
        if (predicate(node)) {
          return node;
        }
        node = node.parentNode;
      }
      return null;
    };
    var hasParent = function (node, rootNode, predicate) {
      return findParent(node, rootNode, predicate) !== null;
    };
    var hasParentWithName = function (node, rootNode, name) {
      return hasParent(node, rootNode, function (node) {
        return node.nodeName === name;
      });
    };
    var isTable = function (node) {
      return node && node.nodeName === 'TABLE';
    };
    var isTableCell = function (node) {
      return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
    };
    var isCeFalseCaretContainer = function (node, rootNode) {
      return CaretContainer.isCaretContainer(node) && hasParent(node, rootNode, CaretFormat.isCaretNode) === false;
    };
    var hasBrBeforeAfter = function (dom, node, left) {
      var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || dom.getRoot());
      while ((node = walker[left ? 'prev' : 'next']())) {
        if (NodeType.isBr(node)) {
          return true;
        }
      }
    };
    var isPrevNode = function (node, name) {
      return node.previousSibling && node.previousSibling.nodeName === name;
    };
    var hasContentEditableFalseParent = function (body, node) {
      while (node && node !== body) {
        if (NodeType.isContentEditableFalse(node)) {
          return true;
        }
        node = node.parentNode;
      }
      return false;
    };
    // Walks the dom left/right to find a suitable text node to move the endpoint into
    // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
    var findTextNodeRelative = function (dom, isAfterNode, collapsed, left, startNode) {
      var walker, lastInlineElement, parentBlockContainer, body = dom.getRoot(), node;
      var nonEmptyElementsMap = dom.schema.getNonEmptyElements();
      parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;
      // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
      // This: <p><br>|</p> becomes <p>|<br></p>
      if (left && NodeType.isBr(startNode) && isAfterNode && dom.isEmpty(parentBlockContainer)) {
        return Option.some(position(startNode.parentNode, dom.nodeIndex(startNode)));
      }
      // Walk left until we hit a text node we can move to or a block/br/img
      walker = new TreeWalker(startNode, parentBlockContainer);
      while ((node = walker[left ? 'prev' : 'next']())) {
        // Break if we hit a non content editable node
        if (dom.getContentEditableParent(node) === "false" || isCeFalseCaretContainer(node, body)) {
          return Option.none();
        }
        // Found text node that has a length
        if (NodeType.isText(node) && node.nodeValue.length > 0) {
          if (hasParentWithName(node, body, 'A') === false) {
            return Option.some(position(node, left ? node.nodeValue.length : 0));
          }
          return Option.none();
        }
        // Break if we find a block or a BR/IMG/INPUT etc
        if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
          return Option.none();
        }
        lastInlineElement = node;
      }
      // Only fetch the last inline element when in caret mode for now
      if (collapsed && lastInlineElement) {
        return Option.some(position(lastInlineElement, 0));
      }
      return Option.none();
    };
    var normalizeEndPoint = function (dom, collapsed, start, rng) {
      var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
      var directionLeft, isAfterNode, normalized = false;
      container = rng[(start ? 'start' : 'end') + 'Container'];
      offset = rng[(start ? 'start' : 'end') + 'Offset'];
      isAfterNode = NodeType.isElement(container) && offset === container.childNodes.length;
      nonEmptyElementsMap = dom.schema.getNonEmptyElements();
      directionLeft = start;
      if (CaretContainer.isCaretContainer(container)) {
        return Option.none();
      }
      if (NodeType.isElement(container) && offset > container.childNodes.length - 1) {
        directionLeft = false;
      }
      // If the container is a document move it to the body element
      if (NodeType.isDocument(container)) {
        container = body;
        offset = 0;
      }
      // If the container is body try move it into the closest text node or position
      if (container === body) {
        // If start is before/after a image, table etc
        if (directionLeft) {
          node = container.childNodes[offset > 0 ? offset - 1 : 0];
          if (node) {
            if (CaretContainer.isCaretContainer(node)) {
              return Option.none();
            }
            if (nonEmptyElementsMap[node.nodeName] || isTable(node)) {
              return Option.none();
            }
          }
        }
        // Resolve the index
        if (container.hasChildNodes()) {
          offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
          container = container.childNodes[offset];
          offset = NodeType.isText(container) && isAfterNode ? container.data.length : 0;
          // Don't normalize non collapsed selections like <p>[a</p><table></table>]
          if (!collapsed && container === body.lastChild && isTable(container)) {
            return Option.none();
          }
          if (hasContentEditableFalseParent(body, container) || CaretContainer.isCaretContainer(container)) {
            return Option.none();
          }
          // Don't walk into elements that doesn't have any child nodes like a IMG
          if (container.hasChildNodes() && isTable(container) === false) {
            // Walk the DOM to find a text node to place the caret at or a BR
            node = container;
            walker = new TreeWalker(container, body);
            do {
              if (NodeType.isContentEditableFalse(node) || CaretContainer.isCaretContainer(node)) {
                normalized = false;
                break;
              }
              // Found a text node use that position
              if (NodeType.isText(node) && node.nodeValue.length > 0) {
                offset = directionLeft ? 0 : node.nodeValue.length;
                container = node;
                normalized = true;
                break;
              }
              // Found a BR/IMG/PRE element that we can place the caret before
              if (nonEmptyElementsMap[node.nodeName.toLowerCase()] && !isTableCell(node)) {
                offset = dom.nodeIndex(node);
                container = node.parentNode;
                // Put caret after image and pre tag when moving the end point
                if ((node.nodeName === 'IMG' || node.nodeName === 'PRE') && !directionLeft) {
                  offset++;
                }
                normalized = true;
                break;
              }
            } while ((node = (directionLeft ? walker.next() : walker.prev())));
          }
        }
      }
      // Lean the caret to the left if possible
      if (collapsed) {
        // So this: <b>x</b><i>|x</i>
        // Becomes: <b>x|</b><i>x</i>
        // Seems that only gecko has issues with this
        if (NodeType.isText(container) && offset === 0) {
          findTextNodeRelative(dom, isAfterNode, collapsed, true, container).each(function (pos) {
            container = pos.container();
            offset = pos.offset();
            normalized = true;
          });
        }
        // Lean left into empty inline elements when the caret is before a BR
        // So this: <i><b></b><i>|<br></i>
        // Becomes: <i><b>|</b><i><br></i>
        // Seems that only gecko has issues with this.
        // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
        if (NodeType.isElement(container)) {
          node = container.childNodes[offset];
          // Offset is after the containers last child
          // then use the previous child for normalization
          if (!node) {
            node = container.childNodes[offset - 1];
          }
          if (node && NodeType.isBr(node) && !isPrevNode(node, 'A') &&
            !hasBrBeforeAfter(dom, node, false) && !hasBrBeforeAfter(dom, node, true)) {
            findTextNodeRelative(dom, isAfterNode, collapsed, true, node).each(function (pos) {
              container = pos.container();
              offset = pos.offset();
              normalized = true;
            });
          }
        }
      }
      // Lean the start of the selection right if possible
      // So this: x[<b>x]</b>
      // Becomes: x<b>[x]</b>
      if (directionLeft && !collapsed && NodeType.isText(container) && offset === container.nodeValue.length) {
        findTextNodeRelative(dom, isAfterNode, collapsed, false, container).each(function (pos) {
          container = pos.container();
          offset = pos.offset();
          normalized = true;
        });
      }
      return normalized ? Option.some(position(container, offset)) : Option.none();
    };
    var normalize = function (dom, rng) {
      var collapsed = rng.collapsed, normRng = rng.cloneRange();
      normalizeEndPoint(dom, collapsed, true, normRng).each(function (pos) {
        normRng.setStart(pos.container(), pos.offset());
      });
      if (!collapsed) {
        normalizeEndPoint(dom, collapsed, false, normRng).each(function (pos) {
          normRng.setEnd(pos.container(), pos.offset());
        });
      }
      // If it was collapsed then make sure it still is
      if (collapsed) {
        normRng.collapse(true);
      }
      return RangeCompare.isEq(rng, normRng) ? Option.none() : Option.some(normRng);
    };
    return {
      normalize: normalize
    };
  }
); 
  |