/**
 * DeleteElement.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.delete.DeleteElement',
  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Options',
    'ephox.sugar.api.dom.Insert',
    'ephox.sugar.api.dom.Remove',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.search.PredicateFind',
    'ephox.sugar.api.search.Traverse',
    'tinymce.core.caret.CaretCandidate',
    'tinymce.core.caret.CaretFinder',
    'tinymce.core.caret.CaretPosition',
    'tinymce.core.dom.Empty',
    'tinymce.core.dom.NodeType'
  ],
  function (Fun, Option, Options, Insert, Remove, Element, Node, PredicateFind, Traverse, CaretCandidate, CaretFinder, CaretPosition, Empty, NodeType) {
    var needsReposition = function (pos, elm) {
      var container = pos.container();
      var offset = pos.offset();
      return CaretPosition.isTextPosition(pos) === false && container === elm.parentNode && offset > CaretPosition.before(elm).offset();
    };

    var reposition = function (elm, pos) {
      return needsReposition(pos, elm) ? new CaretPosition(pos.container(), pos.offset() - 1) : pos;
    };

    var beforeOrStartOf = function (node) {
      return NodeType.isText(node) ? new CaretPosition(node, 0) : CaretPosition.before(node);
    };

    var afterOrEndOf = function (node) {
      return NodeType.isText(node) ? new CaretPosition(node, node.data.length) : CaretPosition.after(node);
    };

    var getPreviousSiblingCaretPosition = function (elm) {
      if (CaretCandidate.isCaretCandidate(elm.previousSibling)) {
        return Option.some(afterOrEndOf(elm.previousSibling));
      } else {
        return elm.previousSibling ? CaretFinder.lastPositionIn(elm.previousSibling) : Option.none();
      }
    };

    var getNextSiblingCaretPosition = function (elm) {
      if (CaretCandidate.isCaretCandidate(elm.nextSibling)) {
        return Option.some(beforeOrStartOf(elm.nextSibling));
      } else {
        return elm.nextSibling ? CaretFinder.firstPositionIn(elm.nextSibling) : Option.none();
      }
    };

    var findCaretPositionBackwardsFromElm = function (rootElement, elm) {
      var startPosition = CaretPosition.before(elm.previousSibling ? elm.previousSibling : elm.parentNode);
      return CaretFinder.prevPosition(rootElement, startPosition).fold(
        function () {
          return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm));
        },
        Option.some
      );
    };

    var findCaretPositionForwardsFromElm = function (rootElement, elm) {
      return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm)).fold(
        function () {
          return CaretFinder.prevPosition(rootElement, CaretPosition.before(elm));
        },
        Option.some
      );
    };

    var findCaretPositionBackwards = function (rootElement, elm) {
      return getPreviousSiblingCaretPosition(elm).orThunk(function () {
        return getNextSiblingCaretPosition(elm);
      }).orThunk(function () {
        return findCaretPositionBackwardsFromElm(rootElement, elm);
      });
    };

    var findCaretPositionForward = function (rootElement, elm) {
      return getNextSiblingCaretPosition(elm).orThunk(function () {
        return getPreviousSiblingCaretPosition(elm);
      }).orThunk(function () {
        return findCaretPositionForwardsFromElm(rootElement, elm);
      });
    };

    var findCaretPosition = function (forward, rootElement, elm) {
      return forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm);
    };

    var findCaretPosOutsideElmAfterDelete = function (forward, rootElement, elm) {
      return findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm));
    };

    var setSelection = function (editor, forward, pos) {
      pos.fold(
        function () {
          editor.focus();
        },
        function (pos) {
          editor.selection.setRng(pos.toRange(), forward);
        }
      );
    };

    var eqRawNode = function (rawNode) {
      return function (elm) {
        return elm.dom() === rawNode;
      };
    };

    var isBlock = function (editor, elm) {
      return elm && editor.schema.getBlockElements().hasOwnProperty(Node.name(elm));
    };

    var paddEmptyBlock = function (elm) {
      if (Empty.isEmpty(elm)) {
        var br = Element.fromHtml('<br data-mce-bogus="1">');
        Remove.empty(elm);
        Insert.append(elm, br);
        return Option.some(CaretPosition.before(br.dom()));
      } else {
        return Option.none();
      }
    };

    // When deleting an element between two text nodes IE 11 doesn't automatically merge the adjacent text nodes
    var deleteNormalized = function (elm, afterDeletePosOpt) {
      return Options.liftN([Traverse.prevSibling(elm), Traverse.nextSibling(elm), afterDeletePosOpt], function (prev, next, afterDeletePos) {
        var offset, prevNode = prev.dom(), nextNode = next.dom();

        if (NodeType.isText(prevNode) && NodeType.isText(nextNode)) {
          offset = prevNode.data.length;
          prevNode.appendData(nextNode.data);
          Remove.remove(next);
          Remove.remove(elm);
          if (afterDeletePos.container() === nextNode) {
            return new CaretPosition(prevNode, offset);
          } else {
            return afterDeletePos;
          }
        } else {
          Remove.remove(elm);
          return afterDeletePos;
        }
      }).orThunk(function () {
        Remove.remove(elm);
        return afterDeletePosOpt;
      });
    };

    var deleteElement = function (editor, forward, elm) {
      var afterDeletePos = findCaretPosOutsideElmAfterDelete(forward, editor.getBody(), elm.dom());
      var parentBlock = PredicateFind.ancestor(elm, Fun.curry(isBlock, editor), eqRawNode(editor.getBody()));
      var normalizedAfterDeletePos = deleteNormalized(elm, afterDeletePos);

      parentBlock.bind(paddEmptyBlock).fold(
        function () {
          setSelection(editor, forward, normalizedAfterDeletePos);
        },
        function (paddPos) {
          setSelection(editor, forward, Option.some(paddPos));
        }
      );
    };

    return {
      deleteElement: deleteElement
    };
  }
);