;!function(exports, undefined) {

  var isArray = Array.isArray ? Array.isArray : function _isArray(obj) {
    return Object.prototype.toString.call(obj) === "[object Array]";
  };
  var defaultMaxListeners = 10;

  function init() {
    this._events = new Object;
  }

  function configure(conf) {
    if (conf) {
      conf.delimiter && (this.delimiter = conf.delimiter);
      conf.wildcard && (this.wildcard = conf.wildcard);
      if (this.wildcard) {
        this.listenerTree = new Object;
      }
    }
  }

  function EventEmitter(conf) {
    this._events = new Object;
    configure.call(this, conf);
  }

  //
  // Attention, function return type now is array, always !
  // It has zero elements if no any matches found and one or more
  // elements (leafs) if there are matches
  //
  function searchListenerTree(handlers, type, tree, i) {
    if (!tree) {
      return [];
    }
    var listeners=[], leaf, len, branch, xTree, xxTree, isolatedBranch, endReached,
        typeLength = type.length, currentType = type[i], nextType = type[i+1];
    if (i === typeLength && tree._listeners) {
      //
      // If at the end of the event(s) list and the tree has listeners
      // invoke those listeners.
      //
      if (typeof tree._listeners === 'function') {
        handlers && handlers.push(tree._listeners);
        return [tree];
      } else {
        for (leaf = 0, len = tree._listeners.length; leaf < len; leaf++) {
          handlers && handlers.push(tree._listeners[leaf]);
        }
        return [tree];
      }
    }

    if ((currentType === '*' || currentType === '**') || tree[currentType]) {
      //
      // If the event emitted is '*' at this part
      // or there is a concrete match at this patch
      //
      if (currentType === '*') {
        for (branch in tree) {
          if (branch !== '_listeners' && tree.hasOwnProperty(branch)) {
            listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+1));
          }
        }
        return listeners;
      } else if(currentType === '**') {
        endReached = (i+1 === typeLength || (i+2 === typeLength && nextType === '*'));
        if(endReached && tree._listeners) {
          // The next element has a _listeners, add it to the handlers.
          listeners = listeners.concat(searchListenerTree(handlers, type, tree, typeLength));
        }

        for (branch in tree) {
          if (branch !== '_listeners' && tree.hasOwnProperty(branch)) {
            if(branch === '*' || branch === '**') {
              if(tree[branch]._listeners && !endReached) {
                listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], typeLength));
              }
              listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i));
            } else if(branch === nextType) {
              listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+2));
            } else {
              // No match on this one, shift into the tree but not in the type array.
              listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i));
            }
          }
        }
        return listeners;
      }

      listeners = listeners.concat(searchListenerTree(handlers, type, tree[currentType], i+1));
    }

    xTree = tree['*'];
    if (xTree) {
      //
      // If the listener tree will allow any match for this part,
      // then recursively explore all branches of the tree
      //
      searchListenerTree(handlers, type, xTree, i+1);
    }

    xxTree = tree['**'];
    if(xxTree) {
      if(i < typeLength) {
        if(xxTree._listeners) {
          // If we have a listener on a '**', it will catch all, so add its handler.
          searchListenerTree(handlers, type, xxTree, typeLength);
        }

        // Build arrays of matching next branches and others.
        for(branch in xxTree) {
          if(branch !== '_listeners' && xxTree.hasOwnProperty(branch)) {
            if(branch === nextType) {
              // We know the next element will match, so jump twice.
              searchListenerTree(handlers, type, xxTree[branch], i+2);
            } else if(branch === currentType) {
              // Current node matches, move into the tree.
              searchListenerTree(handlers, type, xxTree[branch], i+1);
            } else {
              isolatedBranch = {};
              isolatedBranch[branch] = xxTree[branch];
              searchListenerTree(handlers, type, { '**': isolatedBranch }, i+1);
            }
          }
        }
      } else if(xxTree._listeners) {
        // We have reached the end and still on a '**'
        searchListenerTree(handlers, type, xxTree, typeLength);
      } else if(xxTree['*'] && xxTree['*']._listeners) {
        searchListenerTree(handlers, type, xxTree['*'], typeLength);
      }
    }

    return listeners;
  }

  function growListenerTree(type, listener) {

    type = typeof type === 'string' ? type.split(this.delimiter) : type.slice();

    //
    // Looks for two consecutive '**', if so, don't add the event at all.
    //
    for(var i = 0, len = type.length; i+1 < len; i++) {
      if(type[i] === '**' && type[i+1] === '**') {
        return;
      }
    }

    var tree = this.listenerTree;
    var name = type.shift();

    while (name) {

      if (!tree[name]) {
        tree[name] = new Object;
      }

      tree = tree[name];

      if (type.length === 0) {

        if (!tree._listeners) {
          tree._listeners = listener;
        }
        else if(typeof tree._listeners === 'function') {
          tree._listeners = [tree._listeners, listener];
        }
        else if (isArray(tree._listeners)) {

          tree._listeners.push(listener);

          if (!tree._listeners.warned) {

            var m = defaultMaxListeners;

            if (typeof this._events.maxListeners !== 'undefined') {
              m = this._events.maxListeners;
            }

            if (m > 0 && tree._listeners.length > m) {

              tree._listeners.warned = true;
              console.error('(node) warning: possible EventEmitter memory ' +
                            'leak detected. %d listeners added. ' +
                            'Use emitter.setMaxListeners() to increase limit.',
                            tree._listeners.length);
              console.trace();
            }
          }
        }
        return true;
      }
      name = type.shift();
    }
    return true;
  };

  // By default EventEmitters will print a warning if more than
  // 10 listeners are added to it. This is a useful default which
  // helps finding memory leaks.
  //
  // Obviously not all Emitters should be limited to 10. This function allows
  // that to be increased. Set to zero for unlimited.

  EventEmitter.prototype.delimiter = '.';

  EventEmitter.prototype.setMaxListeners = function(n) {
    this._events || init.call(this);
    this._events.maxListeners = n;
  };

  EventEmitter.prototype.event = '';

  EventEmitter.prototype.once = function(event, fn) {
    this.many(event, 1, fn);
    return this;
  };

  EventEmitter.prototype.many = function(event, ttl, fn) {
    var self = this;

    if (typeof fn !== 'function') {
      throw new Error('many only accepts instances of Function');
    }

    function listener() {
      if (--ttl === 0) {
        self.off(event, listener);
      }
      fn.apply(this, arguments);
    };

    listener._origin = fn;

    this.on(event, listener);

    return self;
  };

  EventEmitter.prototype.emit = function() {
    this._events || init.call(this);

    var type = arguments[0];

    if (type === 'newListener') {
      if (!this._events.newListener) { return false; }
    }

    // Loop through the *_all* functions and invoke them.
    if (this._all) {
      var l = arguments.length;
      var args = new Array(l - 1);
      for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
      for (i = 0, l = this._all.length; i < l; i++) {
        this.event = type;
        this._all[i].apply(this, args);
      }
    }

    // If there is no 'error' event listener then throw.
    if (type === 'error') {

      if (!this._all &&
        !this._events.error &&
        !(this.wildcard && this.listenerTree.error)) {

        if (arguments[1] instanceof Error) {
          throw arguments[1]; // Unhandled 'error' event
        } else {
          throw new Error("Uncaught, unspecified 'error' event.");
        }
        return false;
      }
    }

    var handler;

    if(this.wildcard) {
      handler = [];
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      searchListenerTree.call(this, handler, ns, this.listenerTree, 0);
    }
    else {
      handler = this._events[type];
    }

    if (typeof handler === 'function') {
      this.event = type;
      if (arguments.length === 1) {
        handler.call(this);
      }
      else if (arguments.length > 1)
        switch (arguments.length) {
          case 2:
            handler.call(this, arguments[1]);
            break;
          case 3:
            handler.call(this, arguments[1], arguments[2]);
            break;
          // slower
          default:
            var l = arguments.length;
            var args = new Array(l - 1);
            for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
            handler.apply(this, args);
        }
      return true;
    }
    else if (handler) {
      var l = arguments.length;
      var args = new Array(l - 1);
      for (var i = 1; i < l; i++) args[i - 1] = arguments[i];

      var listeners = handler.slice();
      for (var i = 0, l = listeners.length; i < l; i++) {
        this.event = type;
        listeners[i].apply(this, args);
      }
      return (listeners.length > 0) || this._all;
    }
    else {
      return this._all;
    }

  };

  EventEmitter.prototype.on = function(type, listener) {

    if (typeof type === 'function') {
      this.onAny(type);
      return this;
    }

    if (typeof listener !== 'function') {
      throw new Error('on only accepts instances of Function');
    }
    this._events || init.call(this);

    // To avoid recursion in the case that type == "newListeners"! Before
    // adding it to the listeners, first emit "newListeners".
    this.emit('newListener', type, listener);

    if(this.wildcard) {
      growListenerTree.call(this, type, listener);
      return this;
    }

    if (!this._events[type]) {
      // Optimize the case of one listener. Don't need the extra array object.
      this._events[type] = listener;
    }
    else if(typeof this._events[type] === 'function') {
      // Adding the second element, need to change to array.
      this._events[type] = [this._events[type], listener];
    }
    else if (isArray(this._events[type])) {
      // If we've already got an array, just append.
      this._events[type].push(listener);

      // Check for listener leak
      if (!this._events[type].warned) {

        var m = defaultMaxListeners;

        if (typeof this._events.maxListeners !== 'undefined') {
          m = this._events.maxListeners;
        }

        if (m > 0 && this._events[type].length > m) {

          this._events[type].warned = true;
          console.error('(node) warning: possible EventEmitter memory ' +
                        'leak detected. %d listeners added. ' +
                        'Use emitter.setMaxListeners() to increase limit.',
                        this._events[type].length);
          console.trace();
        }
      }
    }
    return this;
  };

  EventEmitter.prototype.onAny = function(fn) {

    if(!this._all) {
      this._all = [];
    }

    if (typeof fn !== 'function') {
      throw new Error('onAny only accepts instances of Function');
    }

    // Add the function to the event listener collection.
    this._all.push(fn);
    return this;
  };

  EventEmitter.prototype.addListener = EventEmitter.prototype.on;

  EventEmitter.prototype.off = function(type, listener) {
    if (typeof listener !== 'function') {
      throw new Error('removeListener only takes instances of Function');
    }

    var handlers,leafs=[];

    if(this.wildcard) {
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0);
    }
    else {
      // does not use listeners(), so no side effect of creating _events[type]
      if (!this._events[type]) return this;
      handlers = this._events[type];
      leafs.push({_listeners:handlers});
    }

    for (var iLeaf=0; iLeaf<leafs.length; iLeaf++) {
      var leaf = leafs[iLeaf];
      handlers = leaf._listeners;
      if (isArray(handlers)) {

        var position = -1;

        for (var i = 0, length = handlers.length; i < length; i++) {
          if (handlers[i] === listener ||
            (handlers[i].listener && handlers[i].listener === listener) ||
            (handlers[i]._origin && handlers[i]._origin === listener)) {
            position = i;
            break;
          }
        }

        if (position < 0) {
          return this;
        }

        if(this.wildcard) {
          leaf._listeners.splice(position, 1)
        }
        else {
          this._events[type].splice(position, 1);
        }

        if (handlers.length === 0) {
          if(this.wildcard) {
            delete leaf._listeners;
          }
          else {
            delete this._events[type];
          }
        }
      }
      else if (handlers === listener ||
        (handlers.listener && handlers.listener === listener) ||
        (handlers._origin && handlers._origin === listener)) {
        if(this.wildcard) {
          delete leaf._listeners;
        }
        else {
          delete this._events[type];
        }
      }
    }

    return this;
  };

  EventEmitter.prototype.offAny = function(fn) {
    var i = 0, l = 0, fns;
    if (fn && this._all && this._all.length > 0) {
      fns = this._all;
      for(i = 0, l = fns.length; i < l; i++) {
        if(fn === fns[i]) {
          fns.splice(i, 1);
          return this;
        }
      }
    } else {
      this._all = [];
    }
    return this;
  };

  EventEmitter.prototype.removeListener = EventEmitter.prototype.off;

  EventEmitter.prototype.removeAllListeners = function(type) {
    if (arguments.length === 0) {
      !this._events || init.call(this);
      return this;
    }

    if(this.wildcard) {
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      var leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0);

      for (var iLeaf=0; iLeaf<leafs.length; iLeaf++) {
        var leaf = leafs[iLeaf];
        leaf._listeners = null;
      }
    }
    else {
      if (!this._events[type]) return this;
      this._events[type] = null;
    }
    return this;
  };

  EventEmitter.prototype.listeners = function(type) {
    if(this.wildcard) {
      var handlers = [];
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      searchListenerTree.call(this, handlers, ns, this.listenerTree, 0);
      return handlers;
    }

    this._events || init.call(this);

    if (!this._events[type]) this._events[type] = [];
    if (!isArray(this._events[type])) {
      this._events[type] = [this._events[type]];
    }
    return this._events[type];
  };

  EventEmitter.prototype.listenersAny = function() {

    if(this._all) {
      return this._all;
    }
    else {
      return [];
    }

  };

  if (typeof define === 'function' && define.amd) {
    define(function() {
      return EventEmitter;
    });
  } else {
    exports.EventEmitter2 = EventEmitter;
  }

}(typeof process !== 'undefined' && typeof process.title !== 'undefined' && typeof exports !== 'undefined' ? exports : window);


keep keep keep keep replace replace replace replace replace replace keep

for leaf in @leaves.toArray()
if offset <= leaf.length
return [leaf, offset]

ScribeDOM =
  canEdit: (node) ->
    ancestor = Scribe.Utils.findAncestor(node, (node) =>
      this.isExternal(node)
    )
    return !ancestor?

  filterUneditable: (nodeList) ->
    return _.filter(nodeList, (node) =>
      return this.canEdit(node)
    )

  isExternal: (node) ->
    return node?.classList?.contains(Scribe.Constants.SPECIAL_CLASSES.EXTERNAL)

  isTextNodeParent: (node) ->
    childNodes = Scribe.DOM.filterUneditable(node.childNodes)
    return childNodes.length == 1 && childNodes[0].nodeType == node.TEXT_NODE

  mergeNodes: (node1, node2) ->
    return node2 if !node1?
    return node1 if !node2?
    this.moveChildren(node1, node2)
    node2.parentNode.removeChild(node2)
    if (node1.tagName == 'OL' || node1.tagName == 'UL') && node1.childNodes.length == 2
      Scribe.DOM.mergeNodes(node1.firstChild, node1.lastChild)
    return node1

  moveChildren: (newParent, oldParent) ->
    _.each(_.clone(oldParent.childNodes), (child) ->
      newParent.appendChild(child)
    )

  splitAfter: (node, root) ->
    return false if node == root or node.parentNode == root
    parentNode = node.parentNode
    parentClone = parentNode.cloneNode(false)
    parentNode.parentNode.insertBefore(parentClone, parentNode.nextSibling)
    while node.nextSibling?
      parentClone.appendChild(node.nextSibling)
    Scribe.DOM.splitAfter(parentNode, root)

  splitNode: (node, offset, force = false) ->
    if offset > Scribe.Utils.getNodeLength(node)
      throw new Error('Splitting at offset greater than node length')
    if node.nodeType == node.TEXT_NODE
      node = this.wrap(node.ownerDocument.createElement('span'), node)
    # Check if split necessary
    if !force
      if offset == 0
        return [node.previousSibling, node, false]
      if offset == Scribe.Utils.getNodeLength(node)
        return [node, node.nextSibling, false]
    left = node
    right = node.cloneNode(false)
    node.parentNode.insertBefore(right, left.nextSibling)
    if Scribe.DOM.isTextNodeParent(node)
      # Text split
      beforeText = node.textContent.substring(0, offset)
      afterText = node.textContent.substring(offset)
      left.textContent = beforeText
      right.textContent = afterText
      return [left, right, true]
    else
      # Node split
      [child, offset] = Scribe.Utils.getChildAtOffset(node, offset)
      [childLeft, childRight] = Scribe.DOM.splitNode(child, offset)
      while childRight != null
        nextRight = childRight.nextSibling
        right.appendChild(childRight)
        childRight = nextRight
      return [left, right, true]

  switchTag: (node, newTag) ->
    return if node.tagName == newTag
    newNode = node.ownerDocument.createElement(newTag)
    this.moveChildren(newNode, node)
    node.parentNode.replaceChild(newNode, node)
    newNode.className = node.className if node.className
    newNode.id = node.id if node.id
    return newNode

  traversePostorder: (root, fn, context = fn) ->
    return unless root?
    cur = root.firstChild
    while cur?
      Scribe.DOM.traversePostorder.call(context, cur, fn)
      cur = fn.call(context, cur)
      cur = cur.nextSibling if cur?

  traversePreorder: (root, offset, fn, context = fn, args...) ->
    return unless root?
    cur = root.firstChild
    while cur?
      if Scribe.DOM.canEdit(cur)
        nextOffset = offset + Scribe.Utils.getNodeLength(cur)
        curHtml = cur.innerHTML
        cur = fn.apply(context, [cur, offset].concat(args))
        Scribe.DOM.traversePreorder.apply(null, [cur, offset, fn, context].concat(args))
        if cur? && cur.innerHTML == curHtml
          cur = cur.nextSibling
          offset = nextOffset
      else
        cur = cur.nextSibling

  traverseSiblings: (startNode, endNode, fn) ->
    while startNode?
      nextSibling = startNode.nextSibling
      fn(startNode)
      break if startNode == endNode
      startNode = nextSibling

  unwrap: (node) ->
    next = node.firstChild
    _.each(_.clone(node.childNodes), (child) ->
      node.parentNode.insertBefore(child, node)
    )
    node.parentNode.removeChild(node)
    return next

  wrap: (wrapper, node) ->
    node.parentNode.insertBefore(wrapper, node)
    wrapper.appendChild(node)
    return wrapper

window.Scribe ||= {}
window.Scribe.DOM = ScribeDOM


coverage:
	@rm -rf tmp
	@mkdir tmp
	@mv bin/src/* tmp/
	@jscoverage tmp/ bin/src/
	@./node_modules/.bin/mocha-phantomjs bin/tests/unit.html --reporter json-cov | node scripts/jsoncovtohtmlcov > coverage.html
	@rm bin/src/*
	@mv tmp/* bin/src/
	@rm -rf tmp

test: unit

unit:
	@./node_modules/.bin/mocha-phantomjs bin/tests/unit.html

.PHONY: test


keep replace replace replace keep keep keep keep

offset -= leaf.length
return [leaf, offset]

class ScribeLine extends LinkedList.Node
  @CLASS_NAME : 'line'
  @DIRTY_CLASS: 'dirty'
  @ID_PREFIX  : 'line-'

  @BLOCK_TAGS: [
    'ADDRESS'
    'BLOCKQUOTE'
    'DD'
    'DIV'
    'DL'
    'H1', 'H2', 'H3', 'H4', 'H5', 'H6'
    'LI'
    'OL'
    'P'
    'PRE'
    'TABLE'
    'TBODY'
    'TD'
    'TFOOT'
    'TH'
    'THEAD'
    'TR'
    'UL'
  ]

  @BREAK_TAGS: [
    'BR'
    'HR'
  ]

  @FORMATS:
    align: ['center', 'justify', 'left', 'right']

  @isLineNode: (node) ->
    return node? && node.classList? && node.classList.contains(Scribe.Line.CLASS_NAME)

  constructor: (@doc, @node) ->
    @id = _.uniqueId(Scribe.Line.ID_PREFIX)
    @node.id = @id
    @node.classList.add(Scribe.Line.CLASS_NAME)
    @trailingNewline = true
    this.rebuild()
    super(@node)

  buildLeaves: (node, formats) ->
    _.each(Scribe.DOM.filterUneditable(node.childNodes), (node) =>
      nodeFormats = _.clone(formats)
      [formatName, formatValue] = Scribe.Utils.getFormatForContainer(node)
      nodeFormats[formatName] = formatValue if formatName?
      if Scribe.Leaf.isLeafNode(node)
        @leaves.append(new Scribe.Leaf(this, node, nodeFormats))
      if Scribe.Leaf.isLeafParent(node)
        this.buildLeaves(node, nodeFormats)
    )

  applyToContents: (offset, length, fn) ->
    return if length <= 0
    [prevNode, startNode] = this.splitContents(offset)
    [endNode, nextNode] = this.splitContents(offset + length)
    Scribe.DOM.traverseSiblings(startNode, endNode, fn)

  deleteText: (offset, length) ->
    this.applyToContents(offset, length, (node) ->
      Scribe.Utils.removeNode(node)
    )
    @trailingNewline = false if @length == offset + length
    this.rebuild()

  findLeaf: (leafNode) ->
    curLeaf = @leaves.first
    while curLeaf?
      return curLeaf if curLeaf.node == leafNode
      curLeaf = curLeaf.next
    return null

  findLeafAtOffset: (offset) ->
    retLeaf = @leaves.first
    _.all(@leaves.toArray(), (leaf) ->
      retLeaf = leaf
      if offset > leaf.length && leaf.next?
        offset -= leaf.length
        return true
      else
        return false
    )
    return [retLeaf, offset]

  format: (name, value) ->
    console.warn "Unimplemented"

  formatText: (offset, length, name, value) ->
    return if length <= 0
    if value && Scribe.Utils.getFormatDefault(name) != value
      refNode = null
      formatNode = Scribe.Utils.createContainerForFormat(@doc.root.ownerDocument, name, value)
      this.applyToContents(offset, length, (node) =>
        refNode = node.nextSibling
        formatNode.appendChild(node)
        Scribe.Utils.removeFormatFromSubtree(node, name)
      )
      @node.insertBefore(formatNode, refNode)
    else
      this.applyToContents(offset, length, (node) ->
        Scribe.Utils.removeFormatFromSubtree(node, name)
      )
    this.rebuild()

  insertText: (offset, text, formats = {}) ->
    [leaf, leafOffset] = this.findLeafAtOffset(offset)
    if _.isEqual(leaf.formats, formats) and @length > 1
      leaf.insertText(leafOffset, text)
      @length += text.length
      @outerHTML = @node.outerHTML
      @delta = this.toDelta()
    else
      [prevNode, nextNode] = this.splitContents(offset)
      span = @node.ownerDocument.createElement('span')
      span.textContent = text
      parentNode = prevNode?.parentNode or nextNode?.parentNode
      parentNode.insertBefore(span, nextNode)
      _.each(formats, (value, name) =>
        this.formatText(offset, text.length, name, value)
      )
      this.rebuild()

  rebuild: ->
    if @node.parentNode == @doc.root
      return false if @outerHTML? && @outerHTML == @node.outerHTML && !@node.classList.contains(Scribe.Line.DIRTY_CLASS)
      while @leaves? && @leaves.length > 0
        @leaves.remove(@leaves.first)
      @leaves = new LinkedList()
      Scribe.Normalizer.normalizeLine(@node)
      Scribe.Normalizer.optimizeLine(@node)
      this.buildLeaves(@node, {})
      this.resetContent()
    else
      @doc.removeLine(this)
    return true

  resetContent: ->
    @length = _.reduce(@leaves.toArray(), ((length, leaf) -> leaf.length + length), 0)
    @length += 1 if @trailingNewline
    @outerHTML = @node.outerHTML
    @formats = {}
    [formatName, formatValue] = Scribe.Utils.getFormatForContainer(@node)
    @formats[formatName] = formatValue if formatName?
    this.setDirty(false)
    @delta = this.toDelta()

  setDirty: (isDirty = true) ->
    if isDirty
      @node.classList.add(Scribe.Line.DIRTY_CLASS)
    else
      @node.classList.remove(Scribe.Line.DIRTY_CLASS)

  splitContents: (offset) ->
    this.setDirty()
    [node, offset] = Scribe.Utils.getChildAtOffset(@node, offset)
    if @node.tagName == 'OL' || @node.tagName == 'UL'
      [node, offset] = Scribe.Utils.getChildAtOffset(node, offset)
    return Scribe.DOM.splitNode(node, offset)

  toDelta: ->
    ops = _.map(@leaves.toArray(), (leaf) ->
      return new Tandem.InsertOp(leaf.text, leaf.getFormats(true))
    )
    ops.push(new Tandem.InsertOp("\n", @formats)) if @trailingNewline
    delta = new Tandem.Delta(0, @length, ops)
    return delta

window.Scribe or= {}
window.Scribe.Line = ScribeLine


describe('Undo manager', ->
  describe('computeUndo', ->
    tests =
      'insert':
        original: new Tandem.Delta(0, 12, [new Tandem.InsertOp("The lazy fox")])
        change: new Tandem.Delta(12, 18, [new Tandem.RetainOp(0, 9), new Tandem.InsertOp('hairy '), new Tandem.RetainOp(9, 12)])
        expected: new Tandem.Delta(18, 12, [new Tandem.RetainOp(0, 9), new Tandem.RetainOp(15, 18)])
      'delete':
        original: new Tandem.Delta(0, 12, [new Tandem.InsertOp("The lazy fox")])
        change: new Tandem.Delta(12, 7, [new Tandem.RetainOp(0, 4), new Tandem.RetainOp(9, 12)])
        expected: new Tandem.Delta(7, 12, [new Tandem.RetainOp(0, 4), new Tandem.InsertOp('lazy '), new Tandem.RetainOp(4, 7)])
      'insert and delete':
        original: new Tandem.Delta(0, 12, [new Tandem.InsertOp("The lazy fox")])
        change: new Tandem.Delta(12, 13, [new Tandem.RetainOp(0, 4), new Tandem.InsertOp('hairy '), new Tandem.RetainOp(9, 12)])
        expected: new Tandem.Delta(13, 12, [new Tandem.RetainOp(0, 4), new Tandem.InsertOp('lazy '), new Tandem.RetainOp(10, 13)])
      'delete at end':
        original: new Tandem.Delta(0, 12, [new Tandem.InsertOp("The lazy fox")])
        change: new Tandem.Delta(12, 9, [new Tandem.RetainOp(0, 9)])
        expected: new Tandem.Delta(9, 12, [new Tandem.RetainOp(0, 9), new Tandem.InsertOp('fox')])
      'append':
        original: new Tandem.Delta(0, 8, [new Tandem.InsertOp("The lazy")])
        change: new Tandem.Delta(8, 12, [new Tandem.RetainOp(0, 8), new Tandem.InsertOp(' fox')])
        expected: new Tandem.Delta(12, 8, [new Tandem.RetainOp(0, 8)])
      'replace':
        original: new Tandem.Delta(0, 3, [new Tandem.InsertOp("fox")])
        change: new Tandem.Delta(3, 4, [new Tandem.InsertOp("lazy")])
        expected: new Tandem.Delta(4, 3, [new Tandem.InsertOp("fox")])

    _.each(tests, (test, name) ->
      it(name, ->
        undoDelta = Scribe.UndoManager.computeUndo(test.change, test.original)
        expect(undoDelta).to.deep.equal(test.expected)
      )
    )
  )
)


// Inspired by http://blog.jcoglan.com/2007/07/23/writing-a-linked-list-in-javascript/

function LinkedList() {}
LinkedList.prototype = {
  length: 0,
  first: null,
  last: null
};

LinkedList.prototype.append = function(node) {
  if (typeof this.first === 'undefined' || this.first === null) {
    this.first = node;
  } else {
    node.next = null;
    this.last.next = node;
  }
  node.prev = this.last;
  this.last = node;
  this.length++;
};

LinkedList.prototype.insertAfter = function(node, newNode) {
  newNode.prev = node;
  if (node) {
    newNode.next = node.next;
    if (typeof node.next != 'undefined' && node.next !== null) {
      node.next.prev = newNode;
    }
    node.next = newNode;
    if (node === this.last) {
      this.last = newNode;
    }
  }
  else {
    // Insert after null implies inserting at position 0
    newNode.next = this.first;
    this.first.prev = newNode;
    this.first = newNode;
  }
  this.length++;
};

LinkedList.prototype.remove = function(node) {
  if (this.length > 1) {
    if (typeof node.prev !== 'undefined' && node.prev !== null) {
      node.prev.next = node.next;
    }
    if (typeof node.next !== 'undefined' && node.next !== null) {
      node.next.prev = node.prev;
    }
    if (node == this.first) { this.first = node.next; }
    if (node == this.last) { this.last = node.prev; }
  } else {
    this.first = null;
    this.last = null;
  }
  node.prev = null;
  node.next = null;
  this.length--;
};

LinkedList.prototype.toArray = function() {
  var arr = [];
  var cur = this.first;
  while (cur) {
    arr.push(cur);
    cur = cur.next;
  }
  return arr;
}

LinkedList.Node = function(data) {
  this.prev = null; this.next = null;
  this.data = data;
};


GIT
  remote: git://github.com/manufaktor/guard-haml.git
  revision: ee19e704c5afface900d18ba7f9ad90993712d02
  branch: development
  specs:
    guard-haml (0.4)
      guard (>= 1.1)
      haml (>= 3.0)

GEM
  remote: http://rubygems.org/
  specs:
    coffee-script (2.2.0)
      coffee-script-source
      execjs
    coffee-script-source (1.3.3)
    execjs (1.4.0)
      multi_json (~> 1.0)
    ffi (1.1.5)
    guard (1.3.0)
      listen (>= 0.4.2)
      thor (>= 0.14.6)
    guard-coffeescript (1.2.0)
      coffee-script (>= 2.2.0)
      guard (>= 1.1.0)
    guard-sass (1.0.0)
      guard (>= 1.1.0)
      sass (>= 3.1)
    haml (3.1.7)
    listen (0.4.7)
      rb-fchange (~> 0.0.5)
      rb-fsevent (~> 0.9.1)
      rb-inotify (~> 0.8.8)
    multi_json (1.3.6)
    rb-fchange (0.0.5)
      ffi
    rb-fsevent (0.9.1)
    rb-inotify (0.8.8)
      ffi (>= 0.5.0)
    sass (3.1.20)
    thor (0.15.4)

PLATFORMS
  ruby

DEPENDENCIES
  guard
  guard-coffeescript
  guard-haml!
  guard-sass


if _.isEqual(leaf.formats, formats) and @length > 1

[prevNode, nextNode] = this.splitContents(offset)

keep keep keep keep keep replace replace keep keep


# Arrays must be alphabetized, so we can use binary search

ScribeConstants =

  DEFAULT_LEAF_FORMATS:
    'background' : 'white'
    'color'      : 'black'
    'family'     : 'san-serif'
    'size'       : 'normal'

  FONT_BACKGROUNDS: [
    'black'
    'blue'
    'green'
    'orange'
    'purple'
    'red'
    'white'
    'yellow'
  ]

  FONT_COLORS: [
    'black'
    'blue'
    'green'
    'orange'
    'red'
    'white'
    'yellow'
  ]

  FONT_FAMILIES: [
    'monospace'
    'serif'
  ]

  FONT_SIZES: [
    'huge'
    'large'
    'small'
  ]

  INDENT_FORMATS:
    'bullet'  : [0..8]
    'indent'  : [0..8]
    'list'    : [0..8]

  # Missing rule implies removal
  LINE_RULES: {
    'A'         : {}
    'B'         : {}
    'BR'        : {}
    'BIG'       : {rename: 'span'}
    'CENTER'    : {rename: 'span'}
    'DEL'       : {rename: 's'}
    'EM'        : {rename: 'i'}
    'H1'        : {rename: 'div'}
    'H2'        : {rename: 'div'}
    'H3'        : {rename: 'div'}
    'H4'        : {rename: 'div'}
    'H5'        : {rename: 'div'}
    'H6'        : {rename: 'div'}
    'I'         : {}
    'INS'       : {rename: 'span'}
    'LI'        : {}
    'OL'        : {}
    'S'         : {}
    'SMALL'     : {rename: 'span'}
    'SPAN'      : {}
    'STRIKE'    : {rename: 's'}
    'STRONG'    : {rename: 'b'}
    'U'         : {}
    'UL'        : {}
  }

  LIST_TAGS: [
    'OL'
    'UL'
  ]

  MAX_INDENT: 9
  MIN_INDENT: 1     # Smallest besides not having an indent at all

  NOBREAK_SPACE: "\uFEFF"

  SPECIAL_CLASSES:
    EXTERNAL: 'ext'

ScribeConstants.SPAN_FORMATS =
  'background' : ScribeConstants.FONT_BACKGROUNDS
  'color'      : ScribeConstants.FONT_COLORS
  'family'     : ScribeConstants.FONT_FAMILIES
  'size'       : ScribeConstants.FONT_SIZES

# Array of possbile values mostly for consistency with SPAN_FORMATS, not actually used in codebase
ScribeConstants.TAG_FORMATS =
  'bold'      : [true, false]
  'italic'    : [true, false]
  'link'      : [true, false]     # Link value could actually also be any string representing the href
  'strike'    : [true, false]
  'underline' : [true, false]

ScribeConstants.LEAF_FORMATS = _.extend({}, ScribeConstants.SPAN_FORMATS, ScribeConstants.TAG_FORMATS)

window.Scribe or= {}
window.Scribe.Constants = ScribeConstants


parentNode = prevNode?.parentNode or nextNode?.parentNode

this.formatText(offset, text.length, name, value)

keep keep replace replace keep keep keep keep

if offset == 0    # Special case for remote cursor preservation

[prevNode, nextNode] = this.splitContents(offset)


class ScribeSelection
  constructor: (@editor) ->
    @range = null
    this.initListeners()

  initListeners: ->
    checkUpdate = =>
      this.update()
    keyUpdate = (event) =>
      checkUpdate() if Scribe.Keyboard.KEYS.LEFT <= event.which and event.which <= Scribe.Keyboard.KEYS.DOWN
    @editor.root.addEventListener('keyup', keyUpdate)
    @editor.root.addEventListener('mouseup', checkUpdate)

  format: (name, value) ->
    this.update()
    return unless @range
    start = @range.start.index
    end = @range.end.index
    formats = @range.getFormats()
    if end > start
      @editor.formatAt(start, end - start, name, value)
    formats[name] = value
    @range.formats = formats
    @editor.emit(Scribe.Editor.events.SELECTION_CHANGE, @range)
    this.setRange(new Scribe.Range(@editor, start, end))

  deleteRange: ->
    this.update()
    return false if @range.isCollapsed()
    @editor.deleteAt(@range.start.index, @range.end.index - @range.start.index)
    this.update()
    return @range

  getNative: ->
    rangySel = rangy.getSelection(@editor.contentWindow)
    selection = window.getSelection()
    return null unless rangySel.anchorNode? && rangySel.focusNode?
    if !rangySel.isBackwards()
      [anchorNode, anchorOffset, focusNode, focusOffset] = [rangySel.anchorNode, rangySel.anchorOffset, rangySel.focusNode, rangySel.focusOffset]
    else
      [focusNode, focusOffset, anchorNode, anchorOffset] = [rangySel.anchorNode, rangySel.anchorOffset, rangySel.focusNode, rangySel.focusOffset]
    return {
      anchorNode    : anchorNode
      anchorOffset  : anchorOffset
      focusNode     : focusNode
      focusOffset   : focusOffset
    }

  getRange: ->
    nativeSel = this.getNative()
    return null unless nativeSel?
    start = new Scribe.Position(@editor, nativeSel.anchorNode, nativeSel.anchorOffset)
    end = new Scribe.Position(@editor, nativeSel.focusNode, nativeSel.focusOffset)
    return new Scribe.Range(@editor, start, end)

  preserve: (fn) ->
    if @range?
      savedSel = this.save()
      fn.call(null)
      this.restore(savedSel)
    else
      fn.call(null)

  restore: (savedSel) ->
    rangy.restoreSelection(savedSel, false)
    this.update(true)

  save: ->
    savedSel = rangy.saveSelection(@editor.contentWindow)
    _.each(savedSel.rangeInfos, (rangeInfo) ->
      _.each([rangeInfo.startMarkerId, rangeInfo.endMarkerId, rangeInfo.markerId], (markerId) ->
        marker = rangeInfo.document.getElementById(markerId)
        marker.classList.add(Scribe.Constants.SPECIAL_CLASSES.EXTERNAL) if marker?.classList?
      )
    )
    return savedSel

  setRange: (@range, silent = false) ->
    rangySel = rangy.getSelection(@editor.contentWindow)
    if @range?
      rangySelRange = @range.getRangy()
      rangySel.setSingleRange(rangySelRange)
    else
      rangySel.removeAllRanges()
    @editor.emit(Scribe.Editor.events.SELECTION_CHANGE, @range) unless silent

  setRangeNative: (nativeSel) ->
    rangySel = rangy.getSelection(@editor.contentWindow)
    range = rangy.createRangyRange(@editor.contentWindow)
    range.setStart(nativeSel.anchorNode, nativeSel.anchorOffset)
    range.setEnd(nativeSel.focusNode, nativeSel.focusOffset)
    rangySel.setSingleRange(range)

  update: (silent = false) ->
    range = this.getRange()
    unless (range == @range) || (@range?.equals(range))
      @editor.emit(Scribe.Editor.events.SELECTION_CHANGE, range) unless silent
      @range = range

window.Scribe or= {}
window.Scribe.Selection = ScribeSelection


initButtonFormats = ->
  _.each(Scribe.Toolbar.BUTTON_FORMATS, (format) =>
    button = @container.querySelector(".#{format}")
    return unless button?
    button.addEventListener('click', =>
      value = !button.classList.contains('active')
      @editor.selection.format(format, value)
      this.emit(Scribe.Toolbar.events.FORMAT, format, value)
    )
  )

initLinkFormat = ->
  linkButton = @container.querySelector('.link')
  return unless linkButton?
  linkButton.addEventListener('click', =>
    value = false
    if !linkButton.classList.contains('active')
      range = @editor.selection.getRange()
      value = range.getText()
    @editor.selection.format('link', value)
    this.emit(Scribe.Toolbar.events.FORMAT, 'link', value)
  )

initSelectFormats = ->
  _.each(Scribe.Toolbar.SELECT_FORMATS, (format) =>
    select = @container.querySelector(".#{format}")
    return unless select?
    select.addEventListener('change', =>
      value = select.options[select.selectedIndex].value
      @editor.selection.format(format, value)
      this.emit(Scribe.Toolbar.events.FORMAT, format, value)
    )
  )

initSelectionListener = ->
  @editor.on(Scribe.Editor.events.SELECTION_CHANGE, (selection) =>
    formats = selection.getFormats()
    _.each(@container.querySelectorAll('.active'), (button) =>
      button.classList.remove('active')
    )
    _.each(formats, (value, key) =>
      if value?
        elem = @container.querySelector(".#{key}")
        return unless elem?
        if elem.tagName == 'SELECT'
          value = '' if _.isArray(value)
          elem.value = value
        else
          elem.classList.add('active')
    )
  )

class ScribeToolbar extends EventEmitter2
  @BUTTON_FORMATS: ['bold', 'italic', 'strike', 'underline', 'bullet', 'list', 'indent', 'outdent']
  @SELECT_FORMATS: ['background', 'color', 'family', 'size']

  @events:
    FORMAT: 'format'

  constructor: (@container, @editor) ->
    @container = document.getElementById(@container) if _.isString(@container)
    initButtonFormats.call(this)
    initLinkFormat.call(this)
    initSelectFormats.call(this)
    initSelectionListener.call(this)

window.Scribe or= {}
window.Scribe.Toolbar = ScribeToolbar


keep keep keep keep replace keep keep keep

termRegex = new RegExp s.escapeRegExp(searchTerm), "i"

RocketChat.models.Users = new class extends RocketChat.models._Base
	constructor: ->
		@model = Meteor.users

		@tryEnsureIndex { 'roles': 1 }, { sparse: 1 }
		@tryEnsureIndex { 'name': 1 }
		@tryEnsureIndex { 'lastLogin': 1 }
		@tryEnsureIndex { 'status': 1 }
		@tryEnsureIndex { 'active': 1 }, { sparse: 1 }
		@tryEnsureIndex { 'statusConnection': 1 }, { sparse: 1 }
		@tryEnsureIndex { 'type': 1 }

	# FIND ONE
	findOneById: (_id, options) ->
		return @findOne _id, options

	findOneByImportId: (_id, options) ->
		return @findOne { importIds: _id }, options

	findOneByUsername: (username, options) ->
		query =
			username: username

		return @findOne query, options

	findOneByEmailAddress: (emailAddress, options) ->
		query =
			'emails.address': new RegExp("^" + s.escapeRegExp(emailAddress) + "$", 'i')

		return @findOne query, options

	findOneAdmin: (admin, options) ->
		query =
			admin: admin

		return @findOne query, options

	findOneByIdAndLoginToken: (_id, token, options) ->
		query =
			_id: _id
			'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token)

		return @findOne query, options

	# FIND
	findById: (userId) ->
		query =
			_id: userId

		return @find query

	findUsersNotOffline: (options) ->
		query =
			username:
				$exists: 1
			status:
				$in: ['online', 'away', 'busy']

		return @find query, options

	findByUsername: (username, options) ->
		query =
			username: username

		return @find query, options

	findUsersByUsernamesWithHighlights: (usernames, options) ->
		query =
			username: { $in: usernames }
			'settings.preferences.highlights.0':
				$exists: true

		return @find query, options

	findActiveByUsernameOrNameRegexWithExceptions: (searchTerm, exceptions = [], options = {}) ->
		if not _.isArray exceptions
			exceptions = [ exceptions ]

		termRegex = new RegExp searchTerm, "i"
		query =
			$and: [
				{ active: true }
				{'$or': [
					{'$and': [
						{ username: { $nin: exceptions } }
						{ username: termRegex }
					]}
					{'$and': [
						{ name: { $nin: exceptions } }
						{ name: termRegex }
					]}
				]}
			]
			type:
				$in: ['user', 'bot']

		return @find query, options

	findByActiveUsersUsernameExcept: (username, except, options) ->
		query =
			active: true
			$and: [
				{username: {$nin: except}}
				{username: username}
			]

		return @find query, options

	findUsersByNameOrUsername: (nameOrUsername, options) ->
		query =
			username:
				$exists: 1

			$or: [
				{name: nameOrUsername}
				{username: nameOrUsername}
			]

			type:
				$in: ['user']

		return @find query, options

	findByUsernameNameOrEmailAddress: (usernameNameOrEmailAddress, options) ->
		query =
			$or: [
				{name: usernameNameOrEmailAddress}
				{username: usernameNameOrEmailAddress}
				{'emails.address': usernameNameOrEmailAddress}
			]
			type:
				$in: ['user', 'bot']

		return @find query, options

	findLDAPUsers: (options) ->
		query =
			ldap: true

		return @find query, options

	getLastLogin: (options = {}) ->
		query = { lastLogin: { $exists: 1 } }
		options.sort = { lastLogin: -1 }
		options.limit = 1

		return @find(query, options)?.fetch?()?[0]?.lastLogin

	findUsersByUsernames: (usernames, options) ->
		query =
			username:
				$in: usernames

		return @find query, options

	# UPDATE
	updateLastLoginById: (_id) ->
		update =
			$set:
				lastLogin: new Date

		return @update _id, update

	setServiceId: (_id, serviceName, serviceId) ->
		update =
			$set: {}

		serviceIdKey = "services.#{serviceName}.id"
		update.$set[serviceIdKey] = serviceId

		return @update _id, update

	setUsername: (_id, username) ->
		update =
			$set: username: username

		return @update _id, update

	setEmail: (_id, email) ->
		update =
			$set:
				emails: [
					address: email
					verified: false
				]

		return @update _id, update

	setEmailVerified: (_id, email) ->
		query =
			_id: _id
			emails:
				$elemMatch:
					address: email
					verified: false

		update =
			$set:
				'emails.$.verified': true

		return @update query, update

	setName: (_id, name) ->
		update =
			$set:
				name: name

		return @update _id, update

	setAvatarOrigin: (_id, origin) ->
		update =
			$set:
				avatarOrigin: origin

		return @update _id, update

	unsetAvatarOrigin: (_id) ->
		update =
			$unset:
				avatarOrigin: 1

		return @update _id, update

	setUserActive: (_id, active=true) ->
		update =
			$set:
				active: active

		return @update _id, update

	setAllUsersActive: (active) ->
		update =
			$set:
				active: active

		return @update {}, update, { multi: true }

	unsetLoginTokens: (_id) ->
		update =
			$set:
				"services.resume.loginTokens" : []

		return @update _id, update

	unsetRequirePasswordChange: (_id) ->
		update =
			$unset:
				"requirePasswordChange" : true
				"requirePasswordChangeReason" : true

		return @update _id, update

	resetPasswordAndSetRequirePasswordChange: (_id, requirePasswordChange, requirePasswordChangeReason) ->
		update =
			$unset:
				"services.password": 1
			$set:
				"requirePasswordChange" : requirePasswordChange,
				"requirePasswordChangeReason": requirePasswordChangeReason

		return @update _id, update

	setLanguage: (_id, language) ->
		update =
			$set:
				language: language

		return @update _id, update

	setProfile: (_id, profile) ->
		update =
			$set:
				"settings.profile": profile

		return @update _id, update

	setPreferences: (_id, preferences) ->
		update =
			$set:
				"settings.preferences": preferences

		return @update _id, update

	setUtcOffset: (_id, utcOffset) ->
		query =
			_id: _id
			utcOffset:
				$ne: utcOffset

		update =
			$set:
				utcOffset: utcOffset

		return @update query, update

	saveUserById: (_id, data) ->
		setData = {}
		unsetData = {}

		if data.name?
			if not _.isEmpty(s.trim(data.name))
				setData.name = s.trim(data.name)
			else
				unsetData.name = 1

		if data.email?
			if not _.isEmpty(s.trim(data.email))
				setData.emails = [
					address: s.trim(data.email)
				]
			else
				unsetData.name = 1

		if data.phone?
			if not _.isEmpty(s.trim(data.phone))
				setData.phone = [
					phoneNumber: s.trim(data.phone)
				]
			else
				unsetData.phone = 1

		update = {}

		if not _.isEmpty setData
			update.$set = setData

		if not _.isEmpty unsetData
			update.$unset = unsetData

		if _.isEmpty update
			return true

		return @update { _id: _id }, update

	# INSERT
	create: (data) ->
		user =
			createdAt: new Date
			avatarOrigin: 'none'

		_.extend user, data

		return @insert user

	# REMOVE
	removeById: (_id) ->
		return @remove _id

	###
	Find users to send a message by email if:
	- he is not online
	- has a verified email
	- has not disabled email notifications
	###
	getUsersToSendOfflineEmail: (usersIds) ->
		query =
			_id:
				$in: usersIds
			status: 'offline'
			statusConnection:
				$ne: 'online'
			'emails.verified': true

		return @find query, { fields: { name: 1, username: 1, emails: 1, 'settings.preferences.emailNotificationMode': 1 } }


Template.videoButtons.helpers
	videoAvaliable: ->
		return WebRTC.getInstanceByRoomId(Session.get('openedRoom'))?

	videoActive: ->
		return WebRTC.getInstanceByRoomId(Session.get('openedRoom')).localUrl.get()? or WebRTC.getInstanceByRoomId(Session.get('openedRoom')).remoteItems.get()?.length > 0

	callInProgress: ->
		return WebRTC.getInstanceByRoomId(Session.get('openedRoom')).callInProgress.get()

Template.videoButtons.events
	'click .start-video-call': ->
		WebRTC.getInstanceByRoomId(Session.get('openedRoom')).startCall({audio: true, video: true})

	'click .start-audio-call': ->
		WebRTC.getInstanceByRoomId(Session.get('openedRoom')).startCall({audio: true})

	'click .join-video-call': ->
		WebRTC.getInstanceByRoomId(Session.get('openedRoom')).joinCall({audio: true, video: true})

	'click .join-audio-call': ->
		WebRTC.getInstanceByRoomId(Session.get('openedRoom')).joinCall({audio: true})


.flex-tab {
  .video-chat {
    ul {
      li {
        margin-bottom: 20px;
      }
    }
  }
}

.video-chat {
  .main-video {
    iframe {
      width: 100%;
      min-height: 299px;
    }
  }
}

.message.jitsi_call_started .actionLinks {
    text-align: left;

    .action-link {
        background-color: #13679a;
        padding: 5px;
        border-radius: 7px;
        color: rgba(255,255,255,0.85);
    }
}


TempSettings = new Meteor.Collection null
Template.admin.onCreated ->
	if not RocketChat.settings.cachedCollectionPrivate?
		RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ name: 'private-settings', eventType: 'onAll' })
		RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection
		RocketChat.settings.cachedCollectionPrivate.init()

	RocketChat.settings.collectionPrivate.find().observe
		added: (data) ->
			TempSettings.insert data
		changed: (data) ->
			TempSettings.update data._id, data
		removed: (data) ->
			TempSettings.remove data._id

Template.admin.onDestroyed ->
	TempSettings.remove {}

Template.admin.helpers
	languages: ->
		languages = TAPi18n.getLanguages()
		result = []
		for key, language of languages
			result.push _.extend(language, { key: key })
		result = _.sortBy(result, 'key')
		result.unshift {
			"name": "Default",
			"en": "Default",
			"key": ""
		}
		return result;

	appLanguage: (key) ->
		if !key
			return !RocketChat.settings.get('Language')
		selected = (RocketChat.settings.get('Language'))?.split('-').shift().toLowerCase() is key
		return selected

	group: ->
		group = FlowRouter.getParam('group')
		group ?= TempSettings.findOne({ type: 'group' })?._id
		return TempSettings.findOne { _id: group, type: 'group' }

	sections: ->
		group = FlowRouter.getParam('group')
		group ?= TempSettings.findOne({ type: 'group' })?._id
		settings = TempSettings.find({ group: group }, {sort: {section: 1, sorter: 1, i18nLabel: 1}}).fetch()

		sections = {}
		for setting in settings
			if setting.i18nDefaultQuery?
				if _.isString(setting.i18nDefaultQuery)
					i18nDefaultQuery = JSON.parse(setting.i18nDefaultQuery)
				else
					i18nDefaultQuery = setting.i18nDefaultQuery

				if not _.isArray(i18nDefaultQuery)
					i18nDefaultQuery = [i18nDefaultQuery]

				found = 0
				for item in i18nDefaultQuery
					if TempSettings.findOne(item)?
						setting.value = TAPi18n.__(setting._id + '_Default')

			sections[setting.section or ''] ?= []
			sections[setting.section or ''].push setting

		sectionsArray = []
		for key, value of sections
			sectionsArray.push
				section: key
				settings: value

		return sectionsArray

	i18nDefaultValue: ->
		return TAPi18n.__(@_id + '_Default')

	isDisabled: ->
		if @blocked
			return { disabled: 'disabled' }

		if not @enableQuery?
			return {}

		if _.isString(@enableQuery)
			enableQuery = JSON.parse(@enableQuery)
		else
			enableQuery = @enableQuery

		if not _.isArray(enableQuery)
			enableQuery = [enableQuery]

		found = 0
		for item in enableQuery
			if TempSettings.findOne(item)?
				found++

		return if found is enableQuery.length then {} else {disabled: 'disabled'}

	isReadonly: ->
		if @readonly is true
			return { readonly: 'readonly' }

	hasChanges: (section) ->
		group = FlowRouter.getParam('group')

		query =
			group: group
			changed: true

		if section?
			if section is ''
				query.$or = [
					{section: ''}
					{section: {$exists: false}}
				]
			else
				query.section = section

		return TempSettings.find(query).count() > 0

	translateSection: (section) ->
		if section.indexOf(':') > -1
			return section

		return t(section)

	flexOpened: ->
		return 'opened' if RocketChat.TabBar.isFlexOpen()

	arrowPosition: ->
		console.log 'room.helpers arrowPosition' if window.rocketDebug
		return 'left' unless RocketChat.TabBar.isFlexOpen()

	label: ->
		label = @i18nLabel or @_id
		return TAPi18n.__ label if label

	description: ->
		description = TAPi18n.__ @i18nDescription if @i18nDescription
		if description? and description isnt @i18nDescription
			return description

	sectionIsCustomOAuth: (section) ->
		return /^Custom OAuth:\s.+/.test section

	callbackURL: (section) ->
		id = s.strRight(section, 'Custom OAuth: ').toLowerCase()
		return Meteor.absoluteUrl('_oauth/' + id)

	relativeUrl: (url) ->
		return Meteor.absoluteUrl(url)

	selectedOption: (_id, val) ->
		return RocketChat.settings.get(_id) is val

	random: ->
		return Random.id()

	getEditorOptions: (readOnly = false) ->
		return {} =
			lineNumbers: true
			mode: this.code or "javascript"
			gutters: [
				"CodeMirror-linenumbers"
				"CodeMirror-foldgutter"
			]
			foldGutter: true
			matchBrackets: true
			autoCloseBrackets: true
			matchTags: true,
			showTrailingSpace: true
			highlightSelectionMatches: true
			readOnly: readOnly

	setEditorOnBlur: (_id) ->
		Meteor.defer ->
			codeMirror = $('.code-mirror-box[data-editor-id="'+_id+'"] .CodeMirror')[0].CodeMirror
			if codeMirror.changeAdded is true
				return

			onChange = ->
				value = codeMirror.getValue()
				TempSettings.update {_id: _id},
					$set:
						value: value
						changed: RocketChat.settings.collectionPrivate.findOne(_id).value isnt value

			onChangeDelayed = _.debounce onChange, 500

			codeMirror.on 'change', onChangeDelayed
			codeMirror.changeAdded = true

		return

	assetAccept: (fileConstraints) ->
		if fileConstraints.extensions?.length > 0
			return '.' + fileConstraints.extensions.join(', .')

Template.admin.events
	"change .input-monitor": (e, t) ->
		value = _.trim $(e.target).val()

		switch @type
			when 'int'
				value = parseInt(value)
			when 'boolean'
				value = value is "1"

		TempSettings.update {_id: @_id},
			$set:
				value: value
				changed: RocketChat.settings.collectionPrivate.findOne(@_id).value isnt value

	"click .submit .save": (e, t) ->
		group = FlowRouter.getParam('group')

		query =
			group: group
			changed: true

		if @section is ''
			query.$or = [
				{section: ''}
				{section: {$exists: false}}
			]
		else
			query.section = @section

		settings = TempSettings.find(query, {fields: {_id: 1, value: 1}}).fetch()

		if not _.isEmpty settings
			RocketChat.settings.batchSet settings, (err, success) ->
				return handleError(err) if err
				TempSettings.update({changed: true}, {$unset: {changed: 1}})
				toastr.success TAPi18n.__ 'Settings_updated'

	"click .submit .refresh-clients": (e, t) ->
		Meteor.call 'refreshClients', ->
			toastr.success TAPi18n.__ 'Clients_will_refresh_in_a_few_seconds'

	"click .submit .add-custom-oauth": (e, t) ->
		config =
			title: TAPi18n.__ 'Add_custom_oauth'
			text: TAPi18n.__ 'Give_a_unique_name_for_the_custom_oauth'
			type: "input",
			showCancelButton: true,
			closeOnConfirm: true,
			inputPlaceholder: TAPi18n.__ 'Custom_oauth_unique_name'

		swal config, (inputValue) ->
			if inputValue is false
				return false

			if inputValue is ""
				swal.showInputError TAPi18n.__ 'Name_cant_be_empty'
				return false

			Meteor.call 'addOAuthService', inputValue, (err) ->
				if err
					handleError(err)

	"click .submit .remove-custom-oauth": (e, t) ->
		name = this.section.replace('Custom OAuth: ', '')
		config =
			title: TAPi18n.__ 'Are_you_sure'
			type: "input",
			type: 'warning'
			showCancelButton: true
			confirmButtonColor: '#DD6B55'
			confirmButtonText: TAPi18n.__ 'Yes_delete_it'
			cancelButtonText: TAPi18n.__ 'Cancel'
			closeOnConfirm: true

		swal config, ->
			Meteor.call 'removeOAuthService', name

	"click .delete-asset": ->
		Meteor.call 'unsetAsset', @asset

	"change input[type=file]": (ev) ->
		e = ev.originalEvent or ev
		files = e.target.files
		if not files or files.length is 0
			files = e.dataTransfer?.files or []

		for blob in files
			toastr.info TAPi18n.__ 'Uploading_file'

			# if @fileConstraints.contentType isnt blob.type
			# 	toastr.error blob.type, TAPi18n.__ 'Invalid_file_type'
			# 	return

			reader = new FileReader()
			reader.readAsBinaryString(blob)
			reader.onloadend = =>
				Meteor.call 'setAsset', reader.result, blob.type, @asset, (err, data) ->
					if err?
						handleError(err)
						# toastr.error err.reason, TAPi18n.__ err.error
						console.log err
						return

					toastr.success TAPi18n.__ 'File_uploaded'

	"click .expand": (e) ->
		$(e.currentTarget).closest('.section').removeClass('section-collapsed')
		$(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__ "Collapse")
		$('.CodeMirror').each (index, codeMirror) ->
			codeMirror.CodeMirror.refresh()

	"click .collapse": (e) ->
		$(e.currentTarget).closest('.section').addClass('section-collapsed')
		$(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__ "Expand")

	"click button.action": (e) ->
		if @type isnt 'action'
			return

		Meteor.call @value, (err, data) ->
			if err?
				err.details = _.extend(err.details || {}, errorTitle: 'Error')
				handleError(err)
				return

			args = [data.message].concat data.params

			toastr.success TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success')

	"click .button-fullscreen": ->
		codeMirrorBox = $('.code-mirror-box[data-editor-id="'+this._id+'"]')
		codeMirrorBox.addClass('code-mirror-box-fullscreen')
		codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh()

	"click .button-restore": ->
		codeMirrorBox = $('.code-mirror-box[data-editor-id="'+this._id+'"]')
		codeMirrorBox.removeClass('code-mirror-box-fullscreen')
		codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh()

Template.admin.onRendered ->
	Tracker.afterFlush ->
		SideNav.setFlex "adminFlex"
		SideNav.openFlex()

	Meteor.setTimeout ->
		$('input.minicolors').minicolors({theme: 'rocketchat'})
	, 1000

	Tracker.autorun ->
		FlowRouter.watchPathChange()
		Meteor.setTimeout ->
			$('input.minicolors').minicolors({theme: 'rocketchat'})
		, 400


filterReg = new RegExp s.escapeRegExp(filter), "i"

Template.adminRooms.helpers
	isReady: ->
		return Template.instance().ready?.get()
	rooms: ->
		return Template.instance().rooms()
	flexOpened: ->
		return 'opened' if RocketChat.TabBar.isFlexOpen()
	arrowPosition: ->
		return 'left' unless RocketChat.TabBar.isFlexOpen()
	isLoading: ->
		return 'btn-loading' unless Template.instance().ready?.get()
	hasMore: ->
		return Template.instance().limit?.get() is Template.instance().rooms?().count()
	roomCount: ->
		return Template.instance().rooms?().count()
	name: ->
		if @t is 'c' or @t is 'p'
			return @name
		else if @t is 'd'
			return @usernames.join ' x '
	type: ->
		if @t is 'c'
			return TAPi18n.__ 'Channel'
		else if @t is 'd'
			return TAPi18n.__ 'Direct Message'
		if @t is 'p'
			return TAPi18n.__ 'Private Group'

	flexTemplate: ->
		return RocketChat.TabBar.getTemplate()
	flexData: ->
		return RocketChat.TabBar.getData()

	default: ->
		if this.default
			return t('True')
		else
			return t('False')

Template.adminRooms.onCreated ->
	instance = @
	@limit = new ReactiveVar 50
	@filter = new ReactiveVar ''
	@types = new ReactiveVar []
	@ready = new ReactiveVar true

	RocketChat.TabBar.addButton({
		groups: ['adminrooms'],
		id: 'admin-room',
		i18nTitle: 'Room_Info',
		icon: 'icon-info',
		template: 'adminRoomInfo',
		order: 1
	});

	RocketChat.ChannelSettings.addOption
		id: 'make-default'
		template: 'channelSettingsDefault'
		data: ->
			return Session.get('adminRoomsSelected')
		validation: ->
			return RocketChat.authz.hasAllPermission('view-room-administration')

	@autorun ->
		filter = instance.filter.get()
		types = instance.types.get()

		if types.length is 0
			types = ['c', 'd', 'p']

		limit = instance.limit.get()
		subscription = instance.subscribe 'adminRooms', filter, types, limit
		instance.ready.set subscription.ready()

	@rooms = ->
		filter = _.trim instance.filter?.get()
		types = instance.types?.get()

		unless _.isArray types
			types = []

		query = {}

		filter = _.trim filter
		if filter
			filterReg = new RegExp filter, "i"
			query = { $or: [ { name: filterReg }, { t: 'd', usernames: filterReg } ] }

		if types.length
			query['t'] = { $in: types }

		return ChatRoom.find(query, { limit: instance.limit?.get(), sort: { default: -1, name: 1 } })

	@getSearchTypes = ->
		return _.map $('[name=room-type]:checked'), (input) -> return $(input).val()

Template.adminRooms.onRendered ->
	Tracker.afterFlush ->
		SideNav.setFlex "adminFlex"
		SideNav.openFlex()

Template.adminRooms.events
	'keydown #rooms-filter': (e) ->
		if e.which is 13
			e.stopPropagation()
			e.preventDefault()

	'keyup #rooms-filter': (e, t) ->
		e.stopPropagation()
		e.preventDefault()
		t.filter.set e.currentTarget.value

	'click .room-info': (e) ->
		e.preventDefault()

		Session.set('adminRoomsSelected', { rid: @_id });

		RocketChat.TabBar.setTemplate('adminRoomInfo')

	'click .load-more': (e, t) ->
		e.preventDefault()
		e.stopPropagation()
		t.limit.set t.limit.get() + 50

	'change [name=room-type]': (e, t) ->
		t.types.set t.getSearchTypes()


/* globals localforage */

class CachedCollectionManager {
	constructor() {
		this.items = [];
		this._syncEnabled = false;

		const _unstoreLoginToken = Accounts._unstoreLoginToken;
		Accounts._unstoreLoginToken = (...args) => {
			_unstoreLoginToken.apply(Accounts, args);
			this.clearAllCache();
		};
	}

	register(cachedCollection) {
		this.items.push(cachedCollection);
	}

	clearAllCache() {
		for (const item of this.items) {
			item.clearCache();
		}
	}

	countQueries() {
		for (const item of this.items) {
			item.countQueries();
		}
	}

	set syncEnabled(value) {
		check(value, Boolean);
		this._syncEnabled = value;
	}

	get syncEnabled() {
		return this._syncEnabled;
	}
}

RocketChat.CachedCollectionManager = new CachedCollectionManager;

class CachedCollection {
	constructor({
		collection,
		name,
		methodName,
		syncMethodName,
		eventName,
		eventType = 'onUser',
		useSync = true,
		useCache = true,
		debug = true,
		version = 1,
		maxCacheTime = 60*60*24*30
	}) {
		this.collection = collection || new Meteor.Collection(null);

		this.ready = new ReactiveVar(false);
		this.name = name;
		this.methodName = methodName || `${name}/get`;
		this.syncMethodName = syncMethodName || `${name}/sync`;
		this.eventName = eventName || `${name}-changed`;
		this.eventType = eventType;
		this.useSync = useSync;
		this.useCache = useCache;
		this.debug = debug;
		this.version = version;
		this.updatedAt = new Date(0);
		this.maxCacheTime = maxCacheTime;

		RocketChat.CachedCollectionManager.register(this);
	}

	log(...args) {
		if (this.debug === true) {
			console.log(`CachedCollection ${this.name} =>`, ...args);
		}
	}

	countQueries() {
		this.log(`${Object.keys(this.collection._collection.queries).length} queries`);
	}

	recomputeCollectionQueries() {
		this.log(`recomputing ${Object.keys(this.collection._collection.queries).length} queries`);
		_.each(this.collection._collection.queries, (query) => {
			this.collection._collection._recomputeResults(query);
		});
	}

	loadFromCache(callback = () => {}) {
		if (this.useCache === false) {
			return callback(false);
		}

		localforage.getItem(this.name, (error, data) => {
			if (data && data.version < this.version) {
				this.clearCache();
				callback(false);
				return;
			}

			const now = new Date();
			if (data && now - data.updatedAt >= 1000*this.maxCacheTime) {
				this.clearCache();
				callback(false);
				return;
			}

			if (data && data.records && data.records.length > 0) {
				this.log(`${data.records.length} records loaded from cache`);
				data.records.forEach((record) => {
					record.__cache__ = true;
					this.collection.upsert({ _id: record._id }, _.omit(record, '_id'));

					if (record._updatedAt) {
						const _updatedAt = new Date(record._updatedAt);
						if (_updatedAt > this.updatedAt) {
							this.updatedAt = _updatedAt;
						}
					}
				});

				callback(true);
			} else {
				callback(false);
			}
		});
	}

	loadFromServer(callback = () => {}) {
		Meteor.call(this.methodName, (error, data) => {
			this.log(`${data.length} records loaded from server`);
			data.forEach((record) => {
				this.collection.upsert({ _id: record._id }, _.omit(record, '_id'));

				if (record._updatedAt && record._updatedAt > this.updatedAt) {
					this.updatedAt = record._updatedAt;
				}
			});
			this.recomputeCollectionQueries();

			if (this.updatedAt < new Date) {
				this.updatedAt = new Date;
			}

			callback(data);
		});
	}

	loadFromServerAndPopulate() {
		this.loadFromServer((loadedData) => {
			this.ready.set(true);
			this.saveCache(loadedData);
		});
	}

	sync() {
		if (RocketChat.CachedCollectionManager.syncEnabled === false || Meteor.connection._outstandingMethodBlocks.length !== 0) {
			return false;
		}

		this.log(`syncing from ${this.updatedAt}`);

		Meteor.call(this.syncMethodName, this.updatedAt, (error, data) => {
			if (data.update && data.update.length > 0) {
				this.log(`${data.update.length} records updated in sync`);

				for (const record of data.update) {
					this.collection.upsert({ _id: record._id }, _.omit(record, '_id'));

					if (record._updatedAt && record._updatedAt > this.updatedAt) {
						this.updatedAt = record._updatedAt;
					}
				}
			}

			if (data.remove && data.remove.length > 0) {
				this.log(`${data.remove.length} records removed in sync`);

				for (const record of data.remove) {
					this.collection.remove({ _id: record._id });

					if (record._deletedAt && record._deletedAt > this.updatedAt) {
						this.updatedAt = record._deletedAt;
					}
				}
			}

			this.saveCache();
		});

		return true;
	}

	saveCache(data) {
		this.log('saving cache');
		if (!data) {
			data = this.collection.find().fetch();
		}

		localforage.setItem(this.name, {
			updatedAt: new Date,
			version: this.version,
			records: data
		});
		this.log('saving cache (done)');
	}

	clearCache() {
		this.log('clearing cache');
		localforage.removeItem(this.name);
	}

	setupListener(eventType, eventName) {
		RocketChat.Notifications[eventType || this.eventType](eventName || this.eventName, (t, record) => {
			this.log('record received', t, record);
			if (t === 'remove') {
				this.collection.remove(record._id);
			} else {
				this.collection.upsert({ _id: record._id }, _.omit(record, '_id'));
			}

			this.saveCache();
		});
	}

	trySync() {
		// Wait for an empty queue to load data again and sync
		const interval = Meteor.setInterval(() => {
			if (this.sync()) {
				Meteor.clearInterval(interval);
			}
		}, 200);
	}

	init() {
		if (this.initiated === true) {
			return;
		}

		this.initiated = true;
		this.loadFromCache((cacheLoaded) => {
			this.ready.set(cacheLoaded);

			if (cacheLoaded === false) {
				// If there is no cache load data immediately
				this.loadFromServerAndPopulate();
			} else if (this.useSync === true) {
				this.trySync();
			}

			if (this.useSync === true) {
				let connectionWasOnline = true;
				Tracker.autorun(() => {
					const connected = Meteor.connection.status().connected;

					if (connected === true && connectionWasOnline === false) {
						this.trySync();
					}

					connectionWasOnline = connected;
				});
			}

			this.setupListener();
		});
	}
}

RocketChat.CachedCollection = CachedCollection;


//! moment.js locale configuration
//! locale : Armenian (hy-am)
//! author : Armendarabyan : https://github.com/armendarabyan

(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('../moment')) :
   typeof define === 'function' && define.amd ? define(['moment'], factory) :
   factory(global.moment)
}(this, function (moment) { 'use strict';

    function monthsCaseReplace(m, format) {
        var months = {
            'nominative': 'հունվար_փետրվար_մարտ_ապրիլ_մայիս_հունիս_հուլիս_օգոստոս_սեպտեմբեր_հոկտեմբեր_նոյեմբեր_դեկտեմբեր'.split('_'),
            'accusative': 'հունվարի_փետրվարի_մարտի_ապրիլի_մայիսի_հունիսի_հուլիսի_օգոստոսի_սեպտեմբերի_հոկտեմբերի_նոյեմբերի_դեկտեմբերի'.split('_')
        },
        nounCase = (/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/).test(format) ?
            'accusative' :
            'nominative';
        return months[nounCase][m.month()];
    }
    function monthsShortCaseReplace(m, format) {
        var monthsShort = 'հնվ_փտր_մրտ_ապր_մյս_հնս_հլս_օգս_սպտ_հկտ_նմբ_դկտ'.split('_');
        return monthsShort[m.month()];
    }
    function weekdaysCaseReplace(m, format) {
        var weekdays = 'կիրակի_երկուշաբթի_երեքշաբթի_չորեքշաբթի_հինգշաբթի_ուրբաթ_շաբաթ'.split('_');
        return weekdays[m.day()];
    }

    var hy_am = moment.defineLocale('hy-am', {
        months : monthsCaseReplace,
        monthsShort : monthsShortCaseReplace,
        weekdays : weekdaysCaseReplace,
        weekdaysShort : 'կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ'.split('_'),
        weekdaysMin : 'կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ'.split('_'),
        longDateFormat : {
            LT : 'HH:mm',
            LTS : 'LT:ss',
            L : 'DD.MM.YYYY',
            LL : 'D MMMM YYYY թ.',
            LLL : 'D MMMM YYYY թ., LT',
            LLLL : 'dddd, D MMMM YYYY թ., LT'
        },
        calendar : {
            sameDay: '[այսօր] LT',
            nextDay: '[վաղը] LT',
            lastDay: '[երեկ] LT',
            nextWeek: function () {
                return 'dddd [օրը ժամը] LT';
            },
            lastWeek: function () {
                return '[անցած] dddd [օրը ժամը] LT';
            },
            sameElse: 'L'
        },
        relativeTime : {
            future : '%s հետո',
            past : '%s առաջ',
            s : 'մի քանի վայրկյան',
            m : 'րոպե',
            mm : '%d րոպե',
            h : 'ժամ',
            hh : '%d ժամ',
            d : 'օր',
            dd : '%d օր',
            M : 'ամիս',
            MM : '%d ամիս',
            y : 'տարի',
            yy : '%d տարի'
        },
        meridiemParse: /գիշերվա|առավոտվա|ցերեկվա|երեկոյան/,
        isPM: function (input) {
            return /^(ցերեկվա|երեկոյան)$/.test(input);
        },
        meridiem : function (hour) {
            if (hour < 4) {
                return 'գիշերվա';
            } else if (hour < 12) {
                return 'առավոտվա';
            } else if (hour < 17) {
                return 'ցերեկվա';
            } else {
                return 'երեկոյան';
            }
        },
        ordinalParse: /\d{1,2}|\d{1,2}-(ին|րդ)/,
        ordinal: function (number, period) {
            switch (period) {
            case 'DDD':
            case 'w':
            case 'W':
            case 'DDDo':
                if (number === 1) {
                    return number + '-ին';
                }
                return number + '-րդ';
            default:
                return number;
            }
        },
        week : {
            dow : 1, // Monday is the first day of the week.
            doy : 7  // The week that contains Jan 1st is the first week of the year.
        }
    });

    return hy_am;

}));


/* globals FileUploadBase, UploadFS, FileUpload:true, FileSystemStore:true */

FileSystemStore = new UploadFS.store.Local({
	collection: RocketChat.models.Uploads.model,
	name: 'fileSystem',
	filter: new UploadFS.Filter({
		onCheck: FileUpload.validateFileUpload
	})
});

FileUpload.FileSystem = class FileUploadFileSystem extends FileUploadBase {
	constructor(meta, file, data) {
		super(meta, file, data);
		this.handler = new UploadFS.Uploader({
			store: FileSystemStore,
			data: data,
			file: meta,
			onError: (err) => {
				var uploading = Session.get('uploading');
				if (uploading != null) {
					let item = _.findWhere(uploading, {
						id: this.id
					});
					if (item != null) {
						item.error = err.reason;
						item.percentage = 0;
					}
					return Session.set('uploading', uploading);
				}
			},
			onComplete: (fileData) => {
				var file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify');

				file.url = fileData.url.replace(Meteor.absoluteUrl(), '/');

				Meteor.call('sendFileMessage', this.meta.rid, null, file, () => {
					Meteor.setTimeout(() => {
						var uploading = Session.get('uploading');
						if (uploading != null) {
							let item = _.findWhere(uploading, {
								id: this.id
							});
							return Session.set('uploading', _.without(uploading, item));
						}
					}, 2000);
				});
			}
		});
	}
	start() {
		return this.handler.start();
	}

	getProgress() {
		return this.handler.getProgress();
	}

	stop() {
		return this.handler.stop();
	}
};


query = { $or: [ { username: filterReg }, { name: filterReg }, { "emails.address": filterReg } ] }

keep keep keep keep keep replace keep keep keep keep keep

Meteor.publish 'filteredUsers', (filter) ->
	unless this.userId
		return this.ready()

	if not _.isObject filter
		return this.ready()

	exp = new RegExp(filter.name, 'i')

	options =
		fields:
			username: 1
		sort:
			username: 1
		limit: 5

	pub = this

	cursorHandle = RocketChat.models.Users.findByActiveUsersUsernameExcept(exp, filter.except, options).observeChanges
		added: (_id, record) ->
			pub.added('filtered-users', _id, record)

		changed: (_id, record) ->
			pub.changed('filtered-users', _id, record)

		removed: (_id, record) ->
			pub.removed('filtered-users', _id, record)

	@ready()
	@onStop ->
		cursorHandle.stop()
	return