| define(
  'tinymce.core.selection.SelectionBookmark',
  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.sand.api.PlatformDetection',
    'ephox.sugar.api.dom.Compare',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.node.Node',
    'ephox.sugar.api.node.Text',
    'ephox.sugar.api.search.Traverse',
    'ephox.sugar.api.selection.Selection',
    'global!document'
  ],
  function (Fun, Option, PlatformDetection, Compare, Element, Node, Text, Traverse, Selection, document) {
    var browser = PlatformDetection.detect().browser;
    var clamp = function (offset, element) {
      var max = Node.isText(element) ? Text.get(element).length : Traverse.children(element).length + 1;
      if (offset > max) {
        return max;
      } else if (offset < 0) {
        return 0;
      }
      return offset;
    };
    var normalizeRng = function (rng) {
      return Selection.range(
        rng.start(),
        clamp(rng.soffset(), rng.start()),
        rng.finish(),
        clamp(rng.foffset(), rng.finish())
      );
    };
    var isOrContains = function (root, elm) {
      return Compare.contains(root, elm) || Compare.eq(root, elm);
    };
    var isRngInRoot = function (root) {
      return function (rng) {
        return isOrContains(root, rng.start()) && isOrContains(root, rng.finish());
      };
    };
    // var dumpRng = function (rng) {
    //   console.log('start', rng.start().dom());
    //   console.log('soffset', rng.soffset());
    //   console.log('finish', rng.finish().dom());
    //   console.log('foffset', rng.foffset());
    //   return rng;
    // };
    var shouldStore = function (editor) {
      return editor.inline === true || browser.isIE();
    };
    var nativeRangeToSelectionRange = function (r) {
      return Selection.range(Element.fromDom(r.startContainer), r.startOffset, Element.fromDom(r.endContainer), r.endOffset);
    };
    var readRange = function (win) {
      var selection = win.getSelection();
      var rng = !selection || selection.rangeCount === 0 ? Option.none() : Option.from(selection.getRangeAt(0));
      return rng.map(nativeRangeToSelectionRange);
    };
    var getBookmark = function (root) {
      var win = Traverse.defaultView(root);
      return readRange(win.dom())
        .filter(isRngInRoot(root));
    };
    var validate = function (root, bookmark) {
      return Option.from(bookmark)
          .filter(isRngInRoot(root))
          .map(normalizeRng);
    };
    var bookmarkToNativeRng = function (bookmark) {
      var rng = document.createRange();
      rng.setStart(bookmark.start().dom(), bookmark.soffset());
      rng.setEnd(bookmark.finish().dom(), bookmark.foffset());
      return Option.some(rng);
    };
    var store = function (editor) {
      var newBookmark = shouldStore(editor) ? getBookmark(Element.fromDom(editor.getBody())) : Option.none();
      editor.bookmark = newBookmark.isSome() ? newBookmark : editor.bookmark;
    };
    var storeNative = function (editor, rng) {
      var root = Element.fromDom(editor.getBody());
      var range = shouldStore(editor) ? Option.from(rng) : Option.none();
      var newBookmark = range.map(nativeRangeToSelectionRange)
        .filter(isRngInRoot(root));
      editor.bookmark = newBookmark.isSome() ? newBookmark : editor.bookmark;
    };
    var getRng = function (editor) {
      var bookmark = editor.bookmark ? editor.bookmark : Option.none();
      return bookmark
        .bind(Fun.curry(validate, Element.fromDom(editor.getBody())))
        .bind(bookmarkToNativeRng);
    };
    var restore = function (editor) {
      getRng(editor).each(function (rng) {
        editor.selection.setRng(rng);
      });
    };
    return {
      store: store,
      storeNative: storeNative,
      readRange: readRange,
      restore: restore,
      getRng: getRng,
      getBookmark: getBookmark,
      validate: validate
    };
  }
);
 |