text: define([
    'materialnote/base/core/func',
    'materialnote/base/core/list',
    'materialnote/base/core/dom',
    'materialnote/base/core/range',
    'materialnote/base/core/key'
  ], function (func, list, dom, range, key) {
    var HintPopover = function (context) {
      var self = this;
      var ui = $.materialnote.ui;
  
      var POPOVER_DIST = 5;
      var hint = context.options.hint || [];
      var direction = context.options.hintDirection || 'bottom';
      var hints = $.isArray(hint) ? hint : [hint];
  
      this.events = {
        'materialnote.keyup': function (we, e) {
          if (!e.isDefaultPrevented()) {
            self.handleKeyup(e);
          }
        },
        'materialnote.keydown': function (we, e) {
          self.handleKeydown(e);
        },
        'materialnote.disable materialnote.dialog.shown': function () {
          self.hide();
        }
      };
  
      this.shouldInitialize = function () {
        return hints.length > 0;
      };
  
      this.initialize = function () {
        this.lastWordRange = null;
        this.$popover = ui.popover({
          className: 'note-hint-popover',
          hideArrow: true,
          direction: ''
        }).render().appendTo('body');
  
        this.$popover.hide();
  
        this.$content = this.$popover.find('.popover-content');
  
        this.$content.on('click', '.note-hint-item', function () {
          self.$content.find('.active').removeClass('active');
          $(this).addClass('active');
          self.replace();
        });
      };
  
      this.destroy = function () {
        this.$popover.remove();
      };
  
      this.selectItem = function ($item) {
        this.$content.find('.active').removeClass('active');
        $item.addClass('active');
  
        this.$content[0].scrollTop = $item[0].offsetTop - (this.$content.innerHeight() / 2);
      };
  
      this.moveDown = function () {
        var $current = this.$content.find('.note-hint-item.active');
        var $next = $current.next();
  
        if ($next.length) {
          this.selectItem($next);
        } else {
          var $nextGroup = $current.parent().next();
  
          if (!$nextGroup.length) {
            $nextGroup = this.$content.find('.note-hint-group').first();
          }
  
          this.selectItem($nextGroup.find('.note-hint-item').first());
        }
      };
  
      this.moveUp = function () {
        var $current = this.$content.find('.note-hint-item.active');
        var $prev = $current.prev();
  
        if ($prev.length) {
          this.selectItem($prev);
        } else {
          var $prevGroup = $current.parent().prev();
  
          if (!$prevGroup.length) {
            $prevGroup = this.$content.find('.note-hint-group').last();
          }
  
          this.selectItem($prevGroup.find('.note-hint-item').last());
        }
      };
  
      this.replace = function () {
        var $item = this.$content.find('.note-hint-item.active');
  
        if ($item.length) {
          var node = this.nodeFromItem($item);
          // XXX: consider to move codes to editor for recording redo/undo.
          this.lastWordRange.insertNode(node);
          range.createFromNode(node).collapse().select();
  
          this.lastWordRange = null;
          this.hide();
          context.triggerEvent('change', context.layoutInfo.editable.html(), context.layoutInfo.editable);
          context.invoke('editor.focus');
        }
  
      };
  
      this.nodeFromItem = function ($item) {
        var hint = hints[$item.data('index')];
        var item = $item.data('item');
        var node = hint.content ? hint.content(item) : item;
        if (typeof node === 'string') {
          node = dom.createText(node);
        }
        return node;
      };
  
      this.createItemTemplates = function (hintIdx, items) {
        var hint = hints[hintIdx];
        return items.map(function (item, idx) {
          var $item = $('<div class="note-hint-item"/>');
          $item.append(hint.template ? hint.template(item) : item + '');
          $item.data({
            'index': hintIdx,
            'item': item
          });
  
          if (hintIdx === 0 && idx === 0) {
            $item.addClass('active');
          }
          return $item;
        });
      };
  
      this.handleKeydown = function (e) {
        if (!this.$popover.is(':visible')) {
          return;
        }
  
        if (e.keyCode === key.code.ENTER) {
          e.preventDefault();
          this.replace();
        } else if (e.keyCode === key.code.UP) {
          e.preventDefault();
          this.moveUp();
        } else if (e.keyCode === key.code.DOWN) {
          e.preventDefault();
          this.moveDown();
        }
      };
  
      this.searchKeyword = function (index, keyword, callback) {
        var hint = hints[index];
        if (hint && hint.match.test(keyword) && hint.search) {
          var matches = hint.match.exec(keyword);
          hint.search(matches[1], callback);
        } else {
          callback();
        }
      };
  
      this.createGroup = function (idx, keyword) {
        var $group = $('<div class="note-hint-group note-hint-group-' + idx + '"/>');
        this.searchKeyword(idx, keyword, function (items) {
          items = items || [];
          if (items.length) {
            $group.html(self.createItemTemplates(idx, items));
            self.show();
          }
        });
  
        return $group;
      };
  
      this.handleKeyup = function (e) {
        if (list.contains([key.code.ENTER, key.code.UP, key.code.DOWN], e.keyCode)) {
          if (e.keyCode === key.code.ENTER) {
            if (this.$popover.is(':visible')) {
              return;
            }
          }
        } else {
          var wordRange = context.invoke('editor.createRange').getWordRange();
          var keyword = wordRange.toString();
          if (hints.length && keyword) {
            this.$content.empty();
  
            var bnd = func.rect2bnd(list.last(wordRange.getClientRects()));
            if (bnd) {
  
              this.$popover.hide();
  
              this.lastWordRange = wordRange;
  
              hints.forEach(function (hint, idx) {
                if (hint.match.test(keyword)) {
                  self.createGroup(idx, keyword).appendTo(self.$content);
                }
              });
  
              // set position for popover after group is created
              if (direction === 'top') {
                this.$popover.css({
                  left: bnd.left,
                  top: bnd.top - this.$popover.outerHeight() - POPOVER_DIST
                });
              } else {
                this.$popover.css({
                  left: bnd.left,
                  top: bnd.top + bnd.height + POPOVER_DIST
                });
              }
  
            }
          } else {
            this.hide();
          }
        }
      };
  
      this.show = function () {
        this.$popover.show();
      };
  
      this.hide = function () {
        this.$popover.hide();
      };
    };
  
    return HintPopover;
  });
  