librejs
authork054 <k@fsf.org>
Fri, 18 Mar 2016 18:18:55 +0000 (14:18 -0400)
committerk054 <k@fsf.org>
Fri, 18 Mar 2016 18:18:55 +0000 (14:18 -0400)
2016/assets/js/engine.io.bundle.js [new file with mode: 0644]
2016/assets/js/engine.io.bundle.min.js [new file with mode: 0644]
2016/assets/js/kiwi.js [new file with mode: 0644]
2016/assets/js/kiwi.min.js [new file with mode: 0644]
2016/assets/js/lodash.js [new file with mode: 0644]
2016/assets/js/lodash.min.js [new file with mode: 0644]

diff --git a/2016/assets/js/engine.io.bundle.js b/2016/assets/js/engine.io.bundle.js
new file mode 100644 (file)
index 0000000..9dcbc53
--- /dev/null
@@ -0,0 +1,3910 @@
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.eio=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+
+module.exports =  _dereq_('./lib/');
+
+},{"./lib/":2}],2:[function(_dereq_,module,exports){
+
+module.exports = _dereq_('./socket');
+
+/**
+ * Exports parser
+ *
+ * @api public
+ *
+ */
+module.exports.parser = _dereq_('engine.io-parser');
+
+},{"./socket":3,"engine.io-parser":15}],3:[function(_dereq_,module,exports){
+(function (global){
+/**
+ * Module dependencies.
+ */
+
+var transports = _dereq_('./transports');
+var Emitter = _dereq_('component-emitter');
+var debug = _dereq_('debug')('engine.io-client:socket');
+var index = _dereq_('indexof');
+var parser = _dereq_('engine.io-parser');
+var parseuri = _dereq_('parseuri');
+var parsejson = _dereq_('parsejson');
+var parseqs = _dereq_('parseqs');
+
+/**
+ * Module exports.
+ */
+
+module.exports = Socket;
+
+/**
+ * Noop function.
+ *
+ * @api private
+ */
+
+function noop(){}
+
+/**
+ * Socket constructor.
+ *
+ * @param {String|Object} uri or options
+ * @param {Object} options
+ * @api public
+ */
+
+function Socket(uri, opts){
+  if (!(this instanceof Socket)) return new Socket(uri, opts);
+
+  opts = opts || {};
+
+  if (uri && 'object' == typeof uri) {
+    opts = uri;
+    uri = null;
+  }
+
+  if (uri) {
+    uri = parseuri(uri);
+    opts.host = uri.host;
+    opts.secure = uri.protocol == 'https' || uri.protocol == 'wss';
+    opts.port = uri.port;
+    if (uri.query) opts.query = uri.query;
+  }
+
+  this.secure = null != opts.secure ? opts.secure :
+    (global.location && 'https:' == location.protocol);
+
+  if (opts.host) {
+    var pieces = opts.host.split(':');
+    opts.hostname = pieces.shift();
+    if (pieces.length) opts.port = pieces.pop();
+  }
+
+  this.agent = opts.agent || false;
+  this.hostname = opts.hostname ||
+    (global.location ? location.hostname : 'localhost');
+  this.port = opts.port || (global.location && location.port ?
+       location.port :
+       (this.secure ? 443 : 80));
+  this.query = opts.query || {};
+  if ('string' == typeof this.query) this.query = parseqs.decode(this.query);
+  this.upgrade = false !== opts.upgrade;
+  this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/';
+  this.forceJSONP = !!opts.forceJSONP;
+  this.jsonp = false !== opts.jsonp;
+  this.forceBase64 = !!opts.forceBase64;
+  this.enablesXDR = !!opts.enablesXDR;
+  this.timestampParam = opts.timestampParam || 't';
+  this.timestampRequests = opts.timestampRequests;
+  this.transports = opts.transports || ['polling', 'websocket'];
+  this.readyState = '';
+  this.writeBuffer = [];
+  this.callbackBuffer = [];
+  this.policyPort = opts.policyPort || 843;
+  this.rememberUpgrade = opts.rememberUpgrade || false;
+  this.open();
+  this.binaryType = null;
+  this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades;
+}
+
+Socket.priorWebsocketSuccess = false;
+
+/**
+ * Mix in `Emitter`.
+ */
+
+Emitter(Socket.prototype);
+
+/**
+ * Protocol version.
+ *
+ * @api public
+ */
+
+Socket.protocol = parser.protocol; // this is an int
+
+/**
+ * Expose deps for legacy compatibility
+ * and standalone browser access.
+ */
+
+Socket.Socket = Socket;
+Socket.Transport = _dereq_('./transport');
+Socket.transports = _dereq_('./transports');
+Socket.parser = _dereq_('engine.io-parser');
+
+/**
+ * Creates transport of the given type.
+ *
+ * @param {String} transport name
+ * @return {Transport}
+ * @api private
+ */
+
+Socket.prototype.createTransport = function (name) {
+  debug('creating transport "%s"', name);
+  var query = clone(this.query);
+
+  // append engine.io protocol identifier
+  query.EIO = parser.protocol;
+
+  // transport name
+  query.transport = name;
+
+  // session id if we already have one
+  if (this.id) query.sid = this.id;
+
+  var transport = new transports[name]({
+    agent: this.agent,
+    hostname: this.hostname,
+    port: this.port,
+    secure: this.secure,
+    path: this.path,
+    query: query,
+    forceJSONP: this.forceJSONP,
+    jsonp: this.jsonp,
+    forceBase64: this.forceBase64,
+    enablesXDR: this.enablesXDR,
+    timestampRequests: this.timestampRequests,
+    timestampParam: this.timestampParam,
+    policyPort: this.policyPort,
+    socket: this
+  });
+
+  return transport;
+};
+
+function clone (obj) {
+  var o = {};
+  for (var i in obj) {
+    if (obj.hasOwnProperty(i)) {
+      o[i] = obj[i];
+    }
+  }
+  return o;
+}
+
+/**
+ * Initializes transport to use and starts probe.
+ *
+ * @api private
+ */
+Socket.prototype.open = function () {
+  var transport;
+  if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') != -1) {
+    transport = 'websocket';
+  } else if (0 == this.transports.length) {
+    // Emit error on next tick so it can be listened to
+    var self = this;
+    setTimeout(function() {
+      self.emit('error', 'No transports available');
+    }, 0);
+    return;
+  } else {
+    transport = this.transports[0];
+  }
+  this.readyState = 'opening';
+
+  // Retry with the next transport if the transport is disabled (jsonp: false)
+  var transport;
+  try {
+    transport = this.createTransport(transport);
+  } catch (e) {
+    this.transports.shift();
+    this.open();
+    return;
+  }
+
+  transport.open();
+  this.setTransport(transport);
+};
+
+/**
+ * Sets the current transport. Disables the existing one (if any).
+ *
+ * @api private
+ */
+
+Socket.prototype.setTransport = function(transport){
+  debug('setting transport %s', transport.name);
+  var self = this;
+
+  if (this.transport) {
+    debug('clearing existing transport %s', this.transport.name);
+    this.transport.removeAllListeners();
+  }
+
+  // set up transport
+  this.transport = transport;
+
+  // set up transport listeners
+  transport
+  .on('drain', function(){
+    self.onDrain();
+  })
+  .on('packet', function(packet){
+    self.onPacket(packet);
+  })
+  .on('error', function(e){
+    self.onError(e);
+  })
+  .on('close', function(){
+    self.onClose('transport close');
+  });
+};
+
+/**
+ * Probes a transport.
+ *
+ * @param {String} transport name
+ * @api private
+ */
+
+Socket.prototype.probe = function (name) {
+  debug('probing transport "%s"', name);
+  var transport = this.createTransport(name, { probe: 1 })
+    , failed = false
+    , self = this;
+
+  Socket.priorWebsocketSuccess = false;
+
+  function onTransportOpen(){
+    if (self.onlyBinaryUpgrades) {
+      var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary;
+      failed = failed || upgradeLosesBinary;
+    }
+    if (failed) return;
+
+    debug('probe transport "%s" opened', name);
+    transport.send([{ type: 'ping', data: 'probe' }]);
+    transport.once('packet', function (msg) {
+      if (failed) return;
+      if ('pong' == msg.type && 'probe' == msg.data) {
+        debug('probe transport "%s" pong', name);
+        self.upgrading = true;
+        self.emit('upgrading', transport);
+        Socket.priorWebsocketSuccess = 'websocket' == transport.name;
+
+        debug('pausing current transport "%s"', self.transport.name);
+        self.transport.pause(function () {
+          if (failed) return;
+          if ('closed' == self.readyState || 'closing' == self.readyState) {
+            return;
+          }
+          debug('changing transport and sending upgrade packet');
+
+          cleanup();
+
+          self.setTransport(transport);
+          transport.send([{ type: 'upgrade' }]);
+          self.emit('upgrade', transport);
+          transport = null;
+          self.upgrading = false;
+          self.flush();
+        });
+      } else {
+        debug('probe transport "%s" failed', name);
+        var err = new Error('probe error');
+        err.transport = transport.name;
+        self.emit('upgradeError', err);
+      }
+    });
+  }
+
+  function freezeTransport() {
+    if (failed) return;
+
+    // Any callback called by transport should be ignored since now
+    failed = true;
+
+    cleanup();
+
+    transport.close();
+    transport = null;
+  }
+
+  //Handle any error that happens while probing
+  function onerror(err) {
+    var error = new Error('probe error: ' + err);
+    error.transport = transport.name;
+
+    freezeTransport();
+
+    debug('probe transport "%s" failed because of error: %s', name, err);
+
+    self.emit('upgradeError', error);
+  }
+
+  function onTransportClose(){
+    onerror("transport closed");
+  }
+
+  //When the socket is closed while we're probing
+  function onclose(){
+    onerror("socket closed");
+  }
+
+  //When the socket is upgraded while we're probing
+  function onupgrade(to){
+    if (transport && to.name != transport.name) {
+      debug('"%s" works - aborting "%s"', to.name, transport.name);
+      freezeTransport();
+    }
+  }
+
+  //Remove all listeners on the transport and on self
+  function cleanup(){
+    transport.removeListener('open', onTransportOpen);
+    transport.removeListener('error', onerror);
+    transport.removeListener('close', onTransportClose);
+    self.removeListener('close', onclose);
+    self.removeListener('upgrading', onupgrade);
+  }
+
+  transport.once('open', onTransportOpen);
+  transport.once('error', onerror);
+  transport.once('close', onTransportClose);
+
+  this.once('close', onclose);
+  this.once('upgrading', onupgrade);
+
+  transport.open();
+
+};
+
+/**
+ * Called when connection is deemed open.
+ *
+ * @api public
+ */
+
+Socket.prototype.onOpen = function () {
+  debug('socket open');
+  this.readyState = 'open';
+  Socket.priorWebsocketSuccess = 'websocket' == this.transport.name;
+  this.emit('open');
+  this.flush();
+
+  // we check for `readyState` in case an `open`
+  // listener already closed the socket
+  if ('open' == this.readyState && this.upgrade && this.transport.pause) {
+    debug('starting upgrade probes');
+    for (var i = 0, l = this.upgrades.length; i < l; i++) {
+      this.probe(this.upgrades[i]);
+    }
+  }
+};
+
+/**
+ * Handles a packet.
+ *
+ * @api private
+ */
+
+Socket.prototype.onPacket = function (packet) {
+  if ('opening' == this.readyState || 'open' == this.readyState) {
+    debug('socket receive: type "%s", data "%s"', packet.type, packet.data);
+
+    this.emit('packet', packet);
+
+    // Socket is live - any packet counts
+    this.emit('heartbeat');
+
+    switch (packet.type) {
+      case 'open':
+        this.onHandshake(parsejson(packet.data));
+        break;
+
+      case 'pong':
+        this.setPing();
+        break;
+
+      case 'error':
+        var err = new Error('server error');
+        err.code = packet.data;
+        this.emit('error', err);
+        break;
+
+      case 'message':
+        this.emit('data', packet.data);
+        this.emit('message', packet.data);
+        break;
+    }
+  } else {
+    debug('packet received with socket readyState "%s"', this.readyState);
+  }
+};
+
+/**
+ * Called upon handshake completion.
+ *
+ * @param {Object} handshake obj
+ * @api private
+ */
+
+Socket.prototype.onHandshake = function (data) {
+  this.emit('handshake', data);
+  this.id = data.sid;
+  this.transport.query.sid = data.sid;
+  this.upgrades = this.filterUpgrades(data.upgrades);
+  this.pingInterval = data.pingInterval;
+  this.pingTimeout = data.pingTimeout;
+  this.onOpen();
+  // In case open handler closes socket
+  if  ('closed' == this.readyState) return;
+  this.setPing();
+
+  // Prolong liveness of socket on heartbeat
+  this.removeListener('heartbeat', this.onHeartbeat);
+  this.on('heartbeat', this.onHeartbeat);
+};
+
+/**
+ * Resets ping timeout.
+ *
+ * @api private
+ */
+
+Socket.prototype.onHeartbeat = function (timeout) {
+  clearTimeout(this.pingTimeoutTimer);
+  var self = this;
+  self.pingTimeoutTimer = setTimeout(function () {
+    if ('closed' == self.readyState) return;
+    self.onClose('ping timeout');
+  }, timeout || (self.pingInterval + self.pingTimeout));
+};
+
+/**
+ * Pings server every `this.pingInterval` and expects response
+ * within `this.pingTimeout` or closes connection.
+ *
+ * @api private
+ */
+
+Socket.prototype.setPing = function () {
+  var self = this;
+  clearTimeout(self.pingIntervalTimer);
+  self.pingIntervalTimer = setTimeout(function () {
+    debug('writing ping packet - expecting pong within %sms', self.pingTimeout);
+    self.ping();
+    self.onHeartbeat(self.pingTimeout);
+  }, self.pingInterval);
+};
+
+/**
+* Sends a ping packet.
+*
+* @api public
+*/
+
+Socket.prototype.ping = function () {
+  this.sendPacket('ping');
+};
+
+/**
+ * Called on `drain` event
+ *
+ * @api private
+ */
+
+Socket.prototype.onDrain = function() {
+  for (var i = 0; i < this.prevBufferLen; i++) {
+    if (this.callbackBuffer[i]) {
+      this.callbackBuffer[i]();
+    }
+  }
+
+  this.writeBuffer.splice(0, this.prevBufferLen);
+  this.callbackBuffer.splice(0, this.prevBufferLen);
+
+  // setting prevBufferLen = 0 is very important
+  // for example, when upgrading, upgrade packet is sent over,
+  // and a nonzero prevBufferLen could cause problems on `drain`
+  this.prevBufferLen = 0;
+
+  if (this.writeBuffer.length == 0) {
+    this.emit('drain');
+  } else {
+    this.flush();
+  }
+};
+
+/**
+ * Flush write buffers.
+ *
+ * @api private
+ */
+
+Socket.prototype.flush = function () {
+  if ('closed' != this.readyState && this.transport.writable &&
+    !this.upgrading && this.writeBuffer.length) {
+    debug('flushing %d packets in socket', this.writeBuffer.length);
+    this.transport.send(this.writeBuffer);
+    // keep track of current length of writeBuffer
+    // splice writeBuffer and callbackBuffer on `drain`
+    this.prevBufferLen = this.writeBuffer.length;
+    this.emit('flush');
+  }
+};
+
+/**
+ * Sends a message.
+ *
+ * @param {String} message.
+ * @param {Function} callback function.
+ * @return {Socket} for chaining.
+ * @api public
+ */
+
+Socket.prototype.write =
+Socket.prototype.send = function (msg, fn) {
+  this.sendPacket('message', msg, fn);
+  return this;
+};
+
+/**
+ * Sends a packet.
+ *
+ * @param {String} packet type.
+ * @param {String} data.
+ * @param {Function} callback function.
+ * @api private
+ */
+
+Socket.prototype.sendPacket = function (type, data, fn) {
+  var packet = { type: type, data: data };
+  this.emit('packetCreate', packet);
+  this.writeBuffer.push(packet);
+  this.callbackBuffer.push(fn);
+  this.flush();
+};
+
+/**
+ * Closes the connection.
+ *
+ * @api private
+ */
+
+Socket.prototype.close = function () {
+  if ('opening' == this.readyState || 'open' == this.readyState) {
+    this.onClose('forced close');
+    debug('socket closing - telling transport to close');
+    this.transport.close();
+  }
+
+  return this;
+};
+
+/**
+ * Called upon transport error
+ *
+ * @api private
+ */
+
+Socket.prototype.onError = function (err) {
+  debug('socket error %j', err);
+  Socket.priorWebsocketSuccess = false;
+  this.emit('error', err);
+  this.onClose('transport error', err);
+};
+
+/**
+ * Called upon transport close.
+ *
+ * @api private
+ */
+
+Socket.prototype.onClose = function (reason, desc) {
+  if ('opening' == this.readyState || 'open' == this.readyState) {
+    debug('socket close with reason: "%s"', reason);
+    var self = this;
+
+    // clear timers
+    clearTimeout(this.pingIntervalTimer);
+    clearTimeout(this.pingTimeoutTimer);
+
+    // clean buffers in next tick, so developers can still
+    // grab the buffers on `close` event
+    setTimeout(function() {
+      self.writeBuffer = [];
+      self.callbackBuffer = [];
+      self.prevBufferLen = 0;
+    }, 0);
+
+    // stop event from firing again for transport
+    this.transport.removeAllListeners('close');
+
+    // ensure transport won't stay open
+    this.transport.close();
+
+    // ignore further transport communication
+    this.transport.removeAllListeners();
+
+    // set ready state
+    this.readyState = 'closed';
+
+    // clear session id
+    this.id = null;
+
+    // emit close event
+    this.emit('close', reason, desc);
+  }
+};
+
+/**
+ * Filters upgrades, returning only those matching client transports.
+ *
+ * @param {Array} server upgrades
+ * @api private
+ *
+ */
+
+Socket.prototype.filterUpgrades = function (upgrades) {
+  var filteredUpgrades = [];
+  for (var i = 0, j = upgrades.length; i<j; i++) {
+    if (~index(this.transports, upgrades[i])) filteredUpgrades.push(upgrades[i]);
+  }
+  return filteredUpgrades;
+};
+
+}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./transport":4,"./transports":5,"component-emitter":12,"debug":14,"engine.io-parser":15,"indexof":23,"parsejson":24,"parseqs":25,"parseuri":26}],4:[function(_dereq_,module,exports){
+/**
+ * Module dependencies.
+ */
+
+var parser = _dereq_('engine.io-parser');
+var Emitter = _dereq_('component-emitter');
+
+/**
+ * Module exports.
+ */
+
+module.exports = Transport;
+
+/**
+ * Transport abstract constructor.
+ *
+ * @param {Object} options.
+ * @api private
+ */
+
+function Transport (opts) {
+  this.path = opts.path;
+  this.hostname = opts.hostname;
+  this.port = opts.port;
+  this.secure = opts.secure;
+  this.query = opts.query;
+  this.timestampParam = opts.timestampParam;
+  this.timestampRequests = opts.timestampRequests;
+  this.readyState = '';
+  this.agent = opts.agent || false;
+  this.socket = opts.socket;
+  this.enablesXDR = opts.enablesXDR;
+}
+
+/**
+ * Mix in `Emitter`.
+ */
+
+Emitter(Transport.prototype);
+
+/**
+ * A counter used to prevent collisions in the timestamps used
+ * for cache busting.
+ */
+
+Transport.timestamps = 0;
+
+/**
+ * Emits an error.
+ *
+ * @param {String} str
+ * @return {Transport} for chaining
+ * @api public
+ */
+
+Transport.prototype.onError = function (msg, desc) {
+  var err = new Error(msg);
+  err.type = 'TransportError';
+  err.description = desc;
+  this.emit('error', err);
+  return this;
+};
+
+/**
+ * Opens the transport.
+ *
+ * @api public
+ */
+
+Transport.prototype.open = function () {
+  if ('closed' == this.readyState || '' == this.readyState) {
+    this.readyState = 'opening';
+    this.doOpen();
+  }
+
+  return this;
+};
+
+/**
+ * Closes the transport.
+ *
+ * @api private
+ */
+
+Transport.prototype.close = function () {
+  if ('opening' == this.readyState || 'open' == this.readyState) {
+    this.doClose();
+    this.onClose();
+  }
+
+  return this;
+};
+
+/**
+ * Sends multiple packets.
+ *
+ * @param {Array} packets
+ * @api private
+ */
+
+Transport.prototype.send = function(packets){
+  if ('open' == this.readyState) {
+    this.write(packets);
+  } else {
+    throw new Error('Transport not open');
+  }
+};
+
+/**
+ * Called upon open
+ *
+ * @api private
+ */
+
+Transport.prototype.onOpen = function () {
+  this.readyState = 'open';
+  this.writable = true;
+  this.emit('open');
+};
+
+/**
+ * Called with data.
+ *
+ * @param {String} data
+ * @api private
+ */
+
+Transport.prototype.onData = function(data){
+  var packet = parser.decodePacket(data, this.socket.binaryType);
+  this.onPacket(packet);
+};
+
+/**
+ * Called with a decoded packet.
+ */
+
+Transport.prototype.onPacket = function (packet) {
+  this.emit('packet', packet);
+};
+
+/**
+ * Called upon close.
+ *
+ * @api private
+ */
+
+Transport.prototype.onClose = function () {
+  this.readyState = 'closed';
+  this.emit('close');
+};
+
+},{"component-emitter":12,"engine.io-parser":15}],5:[function(_dereq_,module,exports){
+(function (global){
+/**
+ * Module dependencies
+ */
+
+var XMLHttpRequest = _dereq_('xmlhttprequest');
+var XHR = _dereq_('./polling-xhr');
+var JSONP = _dereq_('./polling-jsonp');
+var websocket = _dereq_('./websocket');
+
+/**
+ * Export transports.
+ */
+
+exports.polling = polling;
+exports.websocket = websocket;
+
+/**
+ * Polling transport polymorphic constructor.
+ * Decides on xhr vs jsonp based on feature detection.
+ *
+ * @api private
+ */
+
+function polling(opts){
+  var xhr;
+  var xd = false;
+  var xs = false;
+  var jsonp = false !== opts.jsonp;
+
+  if (global.location) {
+    var isSSL = 'https:' == location.protocol;
+    var port = location.port;
+
+    // some user agents have empty `location.port`
+    if (!port) {
+      port = isSSL ? 443 : 80;
+    }
+
+    xd = opts.hostname != location.hostname || port != opts.port;
+    xs = opts.secure != isSSL;
+  }
+
+  opts.xdomain = xd;
+  opts.xscheme = xs;
+  xhr = new XMLHttpRequest(opts);
+
+  if ('open' in xhr && !opts.forceJSONP) {
+    return new XHR(opts);
+  } else {
+    if (!jsonp) throw new Error('JSONP disabled');
+    return new JSONP(opts);
+  }
+}
+
+}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./polling-jsonp":6,"./polling-xhr":7,"./websocket":9,"xmlhttprequest":10}],6:[function(_dereq_,module,exports){
+(function (global){
+
+/**
+ * Module requirements.
+ */
+
+var Polling = _dereq_('./polling');
+var inherit = _dereq_('component-inherit');
+
+/**
+ * Module exports.
+ */
+
+module.exports = JSONPPolling;
+
+/**
+ * Cached regular expressions.
+ */
+
+var rNewline = /\n/g;
+var rEscapedNewline = /\\n/g;
+
+/**
+ * Global JSONP callbacks.
+ */
+
+var callbacks;
+
+/**
+ * Callbacks count.
+ */
+
+var index = 0;
+
+/**
+ * Noop.
+ */
+
+function empty () { }
+
+/**
+ * JSONP Polling constructor.
+ *
+ * @param {Object} opts.
+ * @api public
+ */
+
+function JSONPPolling (opts) {
+  Polling.call(this, opts);
+
+  this.query = this.query || {};
+
+  // define global callbacks array if not present
+  // we do this here (lazily) to avoid unneeded global pollution
+  if (!callbacks) {
+    // we need to consider multiple engines in the same page
+    if (!global.___eio) global.___eio = [];
+    callbacks = global.___eio;
+  }
+
+  // callback identifier
+  this.index = callbacks.length;
+
+  // add callback to jsonp global
+  var self = this;
+  callbacks.push(function (msg) {
+    self.onData(msg);
+  });
+
+  // append to query string
+  this.query.j = this.index;
+
+  // prevent spurious errors from being emitted when the window is unloaded
+  if (global.document && global.addEventListener) {
+    global.addEventListener('beforeunload', function () {
+      if (self.script) self.script.onerror = empty;
+    });
+  }
+}
+
+/**
+ * Inherits from Polling.
+ */
+
+inherit(JSONPPolling, Polling);
+
+/*
+ * JSONP only supports binary as base64 encoded strings
+ */
+
+JSONPPolling.prototype.supportsBinary = false;
+
+/**
+ * Closes the socket.
+ *
+ * @api private
+ */
+
+JSONPPolling.prototype.doClose = function () {
+  if (this.script) {
+    this.script.parentNode.removeChild(this.script);
+    this.script = null;
+  }
+
+  if (this.form) {
+    this.form.parentNode.removeChild(this.form);
+    this.form = null;
+  }
+
+  Polling.prototype.doClose.call(this);
+};
+
+/**
+ * Starts a poll cycle.
+ *
+ * @api private
+ */
+
+JSONPPolling.prototype.doPoll = function () {
+  var self = this;
+  var script = document.createElement('script');
+
+  if (this.script) {
+    this.script.parentNode.removeChild(this.script);
+    this.script = null;
+  }
+
+  script.async = true;
+  script.src = this.uri();
+  script.onerror = function(e){
+    self.onError('jsonp poll error',e);
+  };
+
+  var insertAt = document.getElementsByTagName('script')[0];
+  insertAt.parentNode.insertBefore(script, insertAt);
+  this.script = script;
+
+  var isUAgecko = 'undefined' != typeof navigator && /gecko/i.test(navigator.userAgent);
+  
+  if (isUAgecko) {
+    setTimeout(function () {
+      var iframe = document.createElement('iframe');
+      document.body.appendChild(iframe);
+      document.body.removeChild(iframe);
+    }, 100);
+  }
+};
+
+/**
+ * Writes with a hidden iframe.
+ *
+ * @param {String} data to send
+ * @param {Function} called upon flush.
+ * @api private
+ */
+
+JSONPPolling.prototype.doWrite = function (data, fn) {
+  var self = this;
+
+  if (!this.form) {
+    var form = document.createElement('form');
+    var area = document.createElement('textarea');
+    var id = this.iframeId = 'eio_iframe_' + this.index;
+    var iframe;
+
+    form.className = 'socketio';
+    form.style.position = 'absolute';
+    form.style.top = '-1000px';
+    form.style.left = '-1000px';
+    form.target = id;
+    form.method = 'POST';
+    form.setAttribute('accept-charset', 'utf-8');
+    area.name = 'd';
+    form.appendChild(area);
+    document.body.appendChild(form);
+
+    this.form = form;
+    this.area = area;
+  }
+
+  this.form.action = this.uri();
+
+  function complete () {
+    initIframe();
+    fn();
+  }
+
+  function initIframe () {
+    if (self.iframe) {
+      try {
+        self.form.removeChild(self.iframe);
+      } catch (e) {
+        self.onError('jsonp polling iframe removal error', e);
+      }
+    }
+
+    try {
+      // ie6 dynamic iframes with target="" support (thanks Chris Lambacher)
+      var html = '<iframe src="javascript:0" name="'+ self.iframeId +'">';
+      iframe = document.createElement(html);
+    } catch (e) {
+      iframe = document.createElement('iframe');
+      iframe.name = self.iframeId;
+      iframe.src = 'javascript:0';
+    }
+
+    iframe.id = self.iframeId;
+
+    self.form.appendChild(iframe);
+    self.iframe = iframe;
+  }
+
+  initIframe();
+
+  // escape \n to prevent it from being converted into \r\n by some UAs
+  // double escaping is required for escaped new lines because unescaping of new lines can be done safely on server-side
+  data = data.replace(rEscapedNewline, '\\\n');
+  this.area.value = data.replace(rNewline, '\\n');
+
+  try {
+    this.form.submit();
+  } catch(e) {}
+
+  if (this.iframe.attachEvent) {
+    this.iframe.onreadystatechange = function(){
+      if (self.iframe.readyState == 'complete') {
+        complete();
+      }
+    };
+  } else {
+    this.iframe.onload = complete;
+  }
+};
+
+}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./polling":8,"component-inherit":13}],7:[function(_dereq_,module,exports){
+(function (global){
+/**
+ * Module requirements.
+ */
+
+var XMLHttpRequest = _dereq_('xmlhttprequest');
+var Polling = _dereq_('./polling');
+var Emitter = _dereq_('component-emitter');
+var inherit = _dereq_('component-inherit');
+var debug = _dereq_('debug')('engine.io-client:polling-xhr');
+
+/**
+ * Module exports.
+ */
+
+module.exports = XHR;
+module.exports.Request = Request;
+
+/**
+ * Empty function
+ */
+
+function empty(){}
+
+/**
+ * XHR Polling constructor.
+ *
+ * @param {Object} opts
+ * @api public
+ */
+
+function XHR(opts){
+  Polling.call(this, opts);
+
+  if (global.location) {
+    var isSSL = 'https:' == location.protocol;
+    var port = location.port;
+
+    // some user agents have empty `location.port`
+    if (!port) {
+      port = isSSL ? 443 : 80;
+    }
+
+    this.xd = opts.hostname != global.location.hostname ||
+      port != opts.port;
+    this.xs = opts.secure != isSSL;
+  }
+}
+
+/**
+ * Inherits from Polling.
+ */
+
+inherit(XHR, Polling);
+
+/**
+ * XHR supports binary
+ */
+
+XHR.prototype.supportsBinary = true;
+
+/**
+ * Creates a request.
+ *
+ * @param {String} method
+ * @api private
+ */
+
+XHR.prototype.request = function(opts){
+  opts = opts || {};
+  opts.uri = this.uri();
+  opts.xd = this.xd;
+  opts.xs = this.xs;
+  opts.agent = this.agent || false;
+  opts.supportsBinary = this.supportsBinary;
+  opts.enablesXDR = this.enablesXDR;
+  return new Request(opts);
+};
+
+/**
+ * Sends data.
+ *
+ * @param {String} data to send.
+ * @param {Function} called upon flush.
+ * @api private
+ */
+
+XHR.prototype.doWrite = function(data, fn){
+  var isBinary = typeof data !== 'string' && data !== undefined;
+  var req = this.request({ method: 'POST', data: data, isBinary: isBinary });
+  var self = this;
+  req.on('success', fn);
+  req.on('error', function(err){
+    self.onError('xhr post error', err);
+  });
+  this.sendXhr = req;
+};
+
+/**
+ * Starts a poll cycle.
+ *
+ * @api private
+ */
+
+XHR.prototype.doPoll = function(){
+  debug('xhr poll');
+  var req = this.request();
+  var self = this;
+  req.on('data', function(data){
+    self.onData(data);
+  });
+  req.on('error', function(err){
+    self.onError('xhr poll error', err);
+  });
+  this.pollXhr = req;
+};
+
+/**
+ * Request constructor
+ *
+ * @param {Object} options
+ * @api public
+ */
+
+function Request(opts){
+  this.method = opts.method || 'GET';
+  this.uri = opts.uri;
+  this.xd = !!opts.xd;
+  this.xs = !!opts.xs;
+  this.async = false !== opts.async;
+  this.data = undefined != opts.data ? opts.data : null;
+  this.agent = opts.agent;
+  this.isBinary = opts.isBinary;
+  this.supportsBinary = opts.supportsBinary;
+  this.enablesXDR = opts.enablesXDR;
+  this.create();
+}
+
+/**
+ * Mix in `Emitter`.
+ */
+
+Emitter(Request.prototype);
+
+/**
+ * Creates the XHR object and sends the request.
+ *
+ * @api private
+ */
+
+Request.prototype.create = function(){
+  var xhr = this.xhr = new XMLHttpRequest({ agent: this.agent, xdomain: this.xd, xscheme: this.xs, enablesXDR: this.enablesXDR });
+  var self = this;
+
+  try {
+    debug('xhr open %s: %s', this.method, this.uri);
+    xhr.open(this.method, this.uri, this.async);
+    if (this.supportsBinary) {
+      // This has to be done after open because Firefox is stupid
+      // http://stackoverflow.com/questions/13216903/get-binary-data-with-xmlhttprequest-in-a-firefox-extension
+      xhr.responseType = 'arraybuffer';
+    }
+
+    if ('POST' == this.method) {
+      try {
+        if (this.isBinary) {
+          xhr.setRequestHeader('Content-type', 'application/octet-stream');
+        } else {
+          xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8');
+        }
+      } catch (e) {}
+    }
+
+    // ie6 check
+    if ('withCredentials' in xhr) {
+      xhr.withCredentials = true;
+    }
+
+    if (this.hasXDR()) {
+      xhr.onload = function(){
+        self.onLoad();
+      };
+      xhr.onerror = function(){
+        self.onError(xhr.responseText);
+      };
+    } else {
+      xhr.onreadystatechange = function(){
+        if (4 != xhr.readyState) return;
+        if (200 == xhr.status || 1223 == xhr.status) {
+          self.onLoad();
+        } else {
+          // make sure the `error` event handler that's user-set
+          // does not throw in the same tick and gets caught here
+          setTimeout(function(){
+            self.onError(xhr.status);
+          }, 0);
+        }
+      };
+    }
+
+    debug('xhr data %s', this.data);
+    xhr.send(this.data);
+  } catch (e) {
+    // Need to defer since .create() is called directly fhrom the constructor
+    // and thus the 'error' event can only be only bound *after* this exception
+    // occurs.  Therefore, also, we cannot throw here at all.
+    setTimeout(function() {
+      self.onError(e);
+    }, 0);
+    return;
+  }
+
+  if (global.document) {
+    this.index = Request.requestsCount++;
+    Request.requests[this.index] = this;
+  }
+};
+
+/**
+ * Called upon successful response.
+ *
+ * @api private
+ */
+
+Request.prototype.onSuccess = function(){
+  this.emit('success');
+  this.cleanup();
+};
+
+/**
+ * Called if we have data.
+ *
+ * @api private
+ */
+
+Request.prototype.onData = function(data){
+  this.emit('data', data);
+  this.onSuccess();
+};
+
+/**
+ * Called upon error.
+ *
+ * @api private
+ */
+
+Request.prototype.onError = function(err){
+  this.emit('error', err);
+  this.cleanup();
+};
+
+/**
+ * Cleans up house.
+ *
+ * @api private
+ */
+
+Request.prototype.cleanup = function(){
+  if ('undefined' == typeof this.xhr || null === this.xhr) {
+    return;
+  }
+  // xmlhttprequest
+  if (this.hasXDR()) {
+    this.xhr.onload = this.xhr.onerror = empty;
+  } else {
+    this.xhr.onreadystatechange = empty;
+  }
+
+  try {
+    this.xhr.abort();
+  } catch(e) {}
+
+  if (global.document) {
+    delete Request.requests[this.index];
+  }
+
+  this.xhr = null;
+};
+
+/**
+ * Called upon load.
+ *
+ * @api private
+ */
+
+Request.prototype.onLoad = function(){
+  var data;
+  try {
+    var contentType;
+    try {
+      contentType = this.xhr.getResponseHeader('Content-Type');
+    } catch (e) {}
+    if (contentType === 'application/octet-stream') {
+      data = this.xhr.response;
+    } else {
+      if (!this.supportsBinary) {
+        data = this.xhr.responseText;
+      } else {
+        data = 'ok';
+      }
+    }
+  } catch (e) {
+    this.onError(e);
+  }
+  if (null != data) {
+    this.onData(data);
+  }
+};
+
+/**
+ * Check if it has XDomainRequest.
+ *
+ * @api private
+ */
+
+Request.prototype.hasXDR = function(){
+  return 'undefined' !== typeof global.XDomainRequest && !this.xs && this.enablesXDR;
+};
+
+/**
+ * Aborts the request.
+ *
+ * @api public
+ */
+
+Request.prototype.abort = function(){
+  this.cleanup();
+};
+
+/**
+ * Aborts pending requests when unloading the window. This is needed to prevent
+ * memory leaks (e.g. when using IE) and to ensure that no spurious error is
+ * emitted.
+ */
+
+if (global.document) {
+  Request.requestsCount = 0;
+  Request.requests = {};
+  if (global.attachEvent) {
+    global.attachEvent('onunload', unloadHandler);
+  } else if (global.addEventListener) {
+    global.addEventListener('beforeunload', unloadHandler);
+  }
+}
+
+function unloadHandler() {
+  for (var i in Request.requests) {
+    if (Request.requests.hasOwnProperty(i)) {
+      Request.requests[i].abort();
+    }
+  }
+}
+
+}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./polling":8,"component-emitter":12,"component-inherit":13,"debug":14,"xmlhttprequest":10}],8:[function(_dereq_,module,exports){
+/**
+ * Module dependencies.
+ */
+
+var Transport = _dereq_('../transport');
+var parseqs = _dereq_('parseqs');
+var parser = _dereq_('engine.io-parser');
+var inherit = _dereq_('component-inherit');
+var debug = _dereq_('debug')('engine.io-client:polling');
+
+/**
+ * Module exports.
+ */
+
+module.exports = Polling;
+
+/**
+ * Is XHR2 supported?
+ */
+
+var hasXHR2 = (function() {
+  var XMLHttpRequest = _dereq_('xmlhttprequest');
+  var xhr = new XMLHttpRequest({ agent: this.agent, xdomain: false });
+  return null != xhr.responseType;
+})();
+
+/**
+ * Polling interface.
+ *
+ * @param {Object} opts
+ * @api private
+ */
+
+function Polling(opts){
+  var forceBase64 = (opts && opts.forceBase64);
+  if (!hasXHR2 || forceBase64) {
+    this.supportsBinary = false;
+  }
+  Transport.call(this, opts);
+}
+
+/**
+ * Inherits from Transport.
+ */
+
+inherit(Polling, Transport);
+
+/**
+ * Transport name.
+ */
+
+Polling.prototype.name = 'polling';
+
+/**
+ * Opens the socket (triggers polling). We write a PING message to determine
+ * when the transport is open.
+ *
+ * @api private
+ */
+
+Polling.prototype.doOpen = function(){
+  this.poll();
+};
+
+/**
+ * Pauses polling.
+ *
+ * @param {Function} callback upon buffers are flushed and transport is paused
+ * @api private
+ */
+
+Polling.prototype.pause = function(onPause){
+  var pending = 0;
+  var self = this;
+
+  this.readyState = 'pausing';
+
+  function pause(){
+    debug('paused');
+    self.readyState = 'paused';
+    onPause();
+  }
+
+  if (this.polling || !this.writable) {
+    var total = 0;
+
+    if (this.polling) {
+      debug('we are currently polling - waiting to pause');
+      total++;
+      this.once('pollComplete', function(){
+        debug('pre-pause polling complete');
+        --total || pause();
+      });
+    }
+
+    if (!this.writable) {
+      debug('we are currently writing - waiting to pause');
+      total++;
+      this.once('drain', function(){
+        debug('pre-pause writing complete');
+        --total || pause();
+      });
+    }
+  } else {
+    pause();
+  }
+};
+
+/**
+ * Starts polling cycle.
+ *
+ * @api public
+ */
+
+Polling.prototype.poll = function(){
+  debug('polling');
+  this.polling = true;
+  this.doPoll();
+  this.emit('poll');
+};
+
+/**
+ * Overloads onData to detect payloads.
+ *
+ * @api private
+ */
+
+Polling.prototype.onData = function(data){
+  var self = this;
+  debug('polling got data %s', data);
+  var callback = function(packet, index, total) {
+    // if its the first message we consider the transport open
+    if ('opening' == self.readyState) {
+      self.onOpen();
+    }
+
+    // if its a close packet, we close the ongoing requests
+    if ('close' == packet.type) {
+      self.onClose();
+      return false;
+    }
+
+    // otherwise bypass onData and handle the message
+    self.onPacket(packet);
+  };
+
+  // decode payload
+  parser.decodePayload(data, this.socket.binaryType, callback);
+
+  // if an event did not trigger closing
+  if ('closed' != this.readyState) {
+    // if we got data we're not polling
+    this.polling = false;
+    this.emit('pollComplete');
+
+    if ('open' == this.readyState) {
+      this.poll();
+    } else {
+      debug('ignoring poll - transport state "%s"', this.readyState);
+    }
+  }
+};
+
+/**
+ * For polling, send a close packet.
+ *
+ * @api private
+ */
+
+Polling.prototype.doClose = function(){
+  var self = this;
+
+  function close(){
+    debug('writing close packet');
+    self.write([{ type: 'close' }]);
+  }
+
+  if ('open' == this.readyState) {
+    debug('transport open - closing');
+    close();
+  } else {
+    // in case we're trying to close while
+    // handshaking is in progress (GH-164)
+    debug('transport not open - deferring close');
+    this.once('open', close);
+  }
+};
+
+/**
+ * Writes a packets payload.
+ *
+ * @param {Array} data packets
+ * @param {Function} drain callback
+ * @api private
+ */
+
+Polling.prototype.write = function(packets){
+  var self = this;
+  this.writable = false;
+  var callbackfn = function() {
+    self.writable = true;
+    self.emit('drain');
+  };
+
+  var self = this;
+  parser.encodePayload(packets, this.supportsBinary, function(data) {
+    self.doWrite(data, callbackfn);
+  });
+};
+
+/**
+ * Generates uri for connection.
+ *
+ * @api private
+ */
+
+Polling.prototype.uri = function(){
+  var query = this.query || {};
+  var schema = this.secure ? 'https' : 'http';
+  var port = '';
+
+  // cache busting is forced
+  if (false !== this.timestampRequests) {
+    query[this.timestampParam] = +new Date + '-' + Transport.timestamps++;
+  }
+
+  if (!this.supportsBinary && !query.sid) {
+    query.b64 = 1;
+  }
+
+  query = parseqs.encode(query);
+
+  // avoid port if default for schema
+  if (this.port && (('https' == schema && this.port != 443) ||
+     ('http' == schema && this.port != 80))) {
+    port = ':' + this.port;
+  }
+
+  // prepend ? to query
+  if (query.length) {
+    query = '?' + query;
+  }
+
+  return schema + '://' + this.hostname + port + this.path + query;
+};
+
+},{"../transport":4,"component-inherit":13,"debug":14,"engine.io-parser":15,"parseqs":25,"xmlhttprequest":10}],9:[function(_dereq_,module,exports){
+/**
+ * Module dependencies.
+ */
+
+var Transport = _dereq_('../transport');
+var parser = _dereq_('engine.io-parser');
+var parseqs = _dereq_('parseqs');
+var inherit = _dereq_('component-inherit');
+var debug = _dereq_('debug')('engine.io-client:websocket');
+
+/**
+ * `ws` exposes a WebSocket-compatible interface in
+ * Node, or the `WebSocket` or `MozWebSocket` globals
+ * in the browser.
+ */
+
+var WebSocket = _dereq_('ws');
+
+/**
+ * Module exports.
+ */
+
+module.exports = WS;
+
+/**
+ * WebSocket transport constructor.
+ *
+ * @api {Object} connection options
+ * @api public
+ */
+
+function WS(opts){
+  var forceBase64 = (opts && opts.forceBase64);
+  if (forceBase64) {
+    this.supportsBinary = false;
+  }
+  Transport.call(this, opts);
+}
+
+/**
+ * Inherits from Transport.
+ */
+
+inherit(WS, Transport);
+
+/**
+ * Transport name.
+ *
+ * @api public
+ */
+
+WS.prototype.name = 'websocket';
+
+/*
+ * WebSockets support binary
+ */
+
+WS.prototype.supportsBinary = true;
+
+/**
+ * Opens socket.
+ *
+ * @api private
+ */
+
+WS.prototype.doOpen = function(){
+  if (!this.check()) {
+    // let probe timeout
+    return;
+  }
+
+  var self = this;
+  var uri = this.uri();
+  var protocols = void(0);
+  var opts = { agent: this.agent };
+
+  this.ws = new WebSocket(uri, protocols, opts);
+
+  if (this.ws.binaryType === undefined) {
+    this.supportsBinary = false;
+  }
+
+  this.ws.binaryType = 'arraybuffer';
+  this.addEventListeners();
+};
+
+/**
+ * Adds event listeners to the socket
+ *
+ * @api private
+ */
+
+WS.prototype.addEventListeners = function(){
+  var self = this;
+
+  this.ws.onopen = function(){
+    self.onOpen();
+  };
+  this.ws.onclose = function(){
+    self.onClose();
+  };
+  this.ws.onmessage = function(ev){
+    self.onData(ev.data);
+  };
+  this.ws.onerror = function(e){
+    self.onError('websocket error', e);
+  };
+};
+
+/**
+ * Override `onData` to use a timer on iOS.
+ * See: https://gist.github.com/mloughran/2052006
+ *
+ * @api private
+ */
+
+if ('undefined' != typeof navigator
+  && /iPad|iPhone|iPod/i.test(navigator.userAgent)) {
+  WS.prototype.onData = function(data){
+    var self = this;
+    setTimeout(function(){
+      Transport.prototype.onData.call(self, data);
+    }, 0);
+  };
+}
+
+/**
+ * Writes data to socket.
+ *
+ * @param {Array} array of packets.
+ * @api private
+ */
+
+WS.prototype.write = function(packets){
+  var self = this;
+  this.writable = false;
+  // encodePacket efficient as it uses WS framing
+  // no need for encodePayload
+  for (var i = 0, l = packets.length; i < l; i++) {
+    parser.encodePacket(packets[i], this.supportsBinary, function(data) {
+      //Sometimes the websocket has already been closed but the browser didn't
+      //have a chance of informing us about it yet, in that case send will
+      //throw an error
+      try {
+        self.ws.send(data);
+      } catch (e){
+        debug('websocket closed before onclose event');
+      }
+    });
+  }
+
+  function ondrain() {
+    self.writable = true;
+    self.emit('drain');
+  }
+  // fake drain
+  // defer to next tick to allow Socket to clear writeBuffer
+  setTimeout(ondrain, 0);
+};
+
+/**
+ * Called upon close
+ *
+ * @api private
+ */
+
+WS.prototype.onClose = function(){
+  Transport.prototype.onClose.call(this);
+};
+
+/**
+ * Closes socket.
+ *
+ * @api private
+ */
+
+WS.prototype.doClose = function(){
+  if (typeof this.ws !== 'undefined') {
+    this.ws.close();
+  }
+};
+
+/**
+ * Generates uri for connection.
+ *
+ * @api private
+ */
+
+WS.prototype.uri = function(){
+  var query = this.query || {};
+  var schema = this.secure ? 'wss' : 'ws';
+  var port = '';
+
+  // avoid port if default for schema
+  if (this.port && (('wss' == schema && this.port != 443)
+    || ('ws' == schema && this.port != 80))) {
+    port = ':' + this.port;
+  }
+
+  // append timestamp to URI
+  if (this.timestampRequests) {
+    query[this.timestampParam] = +new Date;
+  }
+
+  // communicate binary support capabilities
+  if (!this.supportsBinary) {
+    query.b64 = 1;
+  }
+
+  query = parseqs.encode(query);
+
+  // prepend ? to query
+  if (query.length) {
+    query = '?' + query;
+  }
+
+  return schema + '://' + this.hostname + port + this.path + query;
+};
+
+/**
+ * Feature detection for WebSocket.
+ *
+ * @return {Boolean} whether this transport is available.
+ * @api public
+ */
+
+WS.prototype.check = function(){
+  return !!WebSocket && !('__initialize' in WebSocket && this.name === WS.prototype.name);
+};
+
+},{"../transport":4,"component-inherit":13,"debug":14,"engine.io-parser":15,"parseqs":25,"ws":27}],10:[function(_dereq_,module,exports){
+// browser shim for xmlhttprequest module
+var hasCORS = _dereq_('has-cors');
+
+module.exports = function(opts) {
+  var xdomain = opts.xdomain;
+
+  // scheme must be same when usign XDomainRequest
+  // http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx
+  var xscheme = opts.xscheme;
+
+  // XDomainRequest has a flow of not sending cookie, therefore it should be disabled as a default.
+  // https://github.com/Automattic/engine.io-client/pull/217
+  var enablesXDR = opts.enablesXDR;
+
+  // Use XDomainRequest for IE8 if enablesXDR is true
+  // because loading bar keeps flashing when using jsonp-polling
+  // https://github.com/yujiosaka/socke.io-ie8-loading-example
+  try {
+    if ('undefined' != typeof XDomainRequest && !xscheme && enablesXDR) {
+      return new XDomainRequest();
+    }
+  } catch (e) { }
+
+  // XMLHttpRequest can be disabled on IE
+  try {
+    if ('undefined' != typeof XMLHttpRequest && (!xdomain || hasCORS)) {
+      return new XMLHttpRequest();
+    }
+  } catch (e) { }
+
+  if (!xdomain) {
+    try {
+      return new ActiveXObject('Microsoft.XMLHTTP');
+    } catch(e) { }
+  }
+}
+
+},{"has-cors":21}],11:[function(_dereq_,module,exports){
+(function (global){
+/**
+ * Create a blob builder even when vendor prefixes exist
+ */
+
+var BlobBuilder = global.BlobBuilder
+  || global.WebKitBlobBuilder
+  || global.MSBlobBuilder
+  || global.MozBlobBuilder;
+
+/**
+ * Check if Blob constructor is supported
+ */
+
+var blobSupported = (function() {
+  try {
+    var b = new Blob(['hi']);
+    return b.size == 2;
+  } catch(e) {
+    return false;
+  }
+})();
+
+/**
+ * Check if BlobBuilder is supported
+ */
+
+var blobBuilderSupported = BlobBuilder
+  && BlobBuilder.prototype.append
+  && BlobBuilder.prototype.getBlob;
+
+function BlobBuilderConstructor(ary, options) {
+  options = options || {};
+
+  var bb = new BlobBuilder();
+  for (var i = 0; i < ary.length; i++) {
+    bb.append(ary[i]);
+  }
+  return (options.type) ? bb.getBlob(options.type) : bb.getBlob();
+};
+
+module.exports = (function() {
+  if (blobSupported) {
+    return global.Blob;
+  } else if (blobBuilderSupported) {
+    return BlobBuilderConstructor;
+  } else {
+    return undefined;
+  }
+})();
+
+}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],12:[function(_dereq_,module,exports){
+
+/**
+ * Expose `Emitter`.
+ */
+
+module.exports = Emitter;
+
+/**
+ * Initialize a new `Emitter`.
+ *
+ * @api public
+ */
+
+function Emitter(obj) {
+  if (obj) return mixin(obj);
+};
+
+/**
+ * Mixin the emitter properties.
+ *
+ * @param {Object} obj
+ * @return {Object}
+ * @api private
+ */
+
+function mixin(obj) {
+  for (var key in Emitter.prototype) {
+    obj[key] = Emitter.prototype[key];
+  }
+  return obj;
+}
+
+/**
+ * Listen on the given `event` with `fn`.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+Emitter.prototype.on =
+Emitter.prototype.addEventListener = function(event, fn){
+  this._callbacks = this._callbacks || {};
+  (this._callbacks[event] = this._callbacks[event] || [])
+    .push(fn);
+  return this;
+};
+
+/**
+ * Adds an `event` listener that will be invoked a single
+ * time then automatically removed.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+Emitter.prototype.once = function(event, fn){
+  var self = this;
+  this._callbacks = this._callbacks || {};
+
+  function on() {
+    self.off(event, on);
+    fn.apply(this, arguments);
+  }
+
+  on.fn = fn;
+  this.on(event, on);
+  return this;
+};
+
+/**
+ * Remove the given callback for `event` or all
+ * registered callbacks.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+Emitter.prototype.off =
+Emitter.prototype.removeListener =
+Emitter.prototype.removeAllListeners =
+Emitter.prototype.removeEventListener = function(event, fn){
+  this._callbacks = this._callbacks || {};
+
+  // all
+  if (0 == arguments.length) {
+    this._callbacks = {};
+    return this;
+  }
+
+  // specific event
+  var callbacks = this._callbacks[event];
+  if (!callbacks) return this;
+
+  // remove all handlers
+  if (1 == arguments.length) {
+    delete this._callbacks[event];
+    return this;
+  }
+
+  // remove specific handler
+  var cb;
+  for (var i = 0; i < callbacks.length; i++) {
+    cb = callbacks[i];
+    if (cb === fn || cb.fn === fn) {
+      callbacks.splice(i, 1);
+      break;
+    }
+  }
+  return this;
+};
+
+/**
+ * Emit `event` with the given args.
+ *
+ * @param {String} event
+ * @param {Mixed} ...
+ * @return {Emitter}
+ */
+
+Emitter.prototype.emit = function(event){
+  this._callbacks = this._callbacks || {};
+  var args = [].slice.call(arguments, 1)
+    , callbacks = this._callbacks[event];
+
+  if (callbacks) {
+    callbacks = callbacks.slice(0);
+    for (var i = 0, len = callbacks.length; i < len; ++i) {
+      callbacks[i].apply(this, args);
+    }
+  }
+
+  return this;
+};
+
+/**
+ * Return array of callbacks for `event`.
+ *
+ * @param {String} event
+ * @return {Array}
+ * @api public
+ */
+
+Emitter.prototype.listeners = function(event){
+  this._callbacks = this._callbacks || {};
+  return this._callbacks[event] || [];
+};
+
+/**
+ * Check if this emitter has `event` handlers.
+ *
+ * @param {String} event
+ * @return {Boolean}
+ * @api public
+ */
+
+Emitter.prototype.hasListeners = function(event){
+  return !! this.listeners(event).length;
+};
+
+},{}],13:[function(_dereq_,module,exports){
+
+module.exports = function(a, b){
+  var fn = function(){};
+  fn.prototype = b.prototype;
+  a.prototype = new fn;
+  a.prototype.constructor = a;
+};
+},{}],14:[function(_dereq_,module,exports){
+
+/**
+ * Expose `debug()` as the module.
+ */
+
+module.exports = debug;
+
+/**
+ * Create a debugger with the given `name`.
+ *
+ * @param {String} name
+ * @return {Type}
+ * @api public
+ */
+
+function debug(name) {
+  if (!debug.enabled(name)) return function(){};
+
+  return function(fmt){
+    fmt = coerce(fmt);
+
+    var curr = new Date;
+    var ms = curr - (debug[name] || curr);
+    debug[name] = curr;
+
+    fmt = name
+      + ' '
+      + fmt
+      + ' +' + debug.humanize(ms);
+
+    // This hackery is required for IE8
+    // where `console.log` doesn't have 'apply'
+    window.console
+      && console.log
+      && Function.prototype.apply.call(console.log, console, arguments);
+  }
+}
+
+/**
+ * The currently active debug mode names.
+ */
+
+debug.names = [];
+debug.skips = [];
+
+/**
+ * Enables a debug mode by name. This can include modes
+ * separated by a colon and wildcards.
+ *
+ * @param {String} name
+ * @api public
+ */
+
+debug.enable = function(name) {
+  try {
+    localStorage.debug = name;
+  } catch(e){}
+
+  var split = (name || '').split(/[\s,]+/)
+    , len = split.length;
+
+  for (var i = 0; i < len; i++) {
+    name = split[i].replace('*', '.*?');
+    if (name[0] === '-') {
+      debug.skips.push(new RegExp('^' + name.substr(1) + '$'));
+    }
+    else {
+      debug.names.push(new RegExp('^' + name + '$'));
+    }
+  }
+};
+
+/**
+ * Disable debug output.
+ *
+ * @api public
+ */
+
+debug.disable = function(){
+  debug.enable('');
+};
+
+/**
+ * Humanize the given `ms`.
+ *
+ * @param {Number} m
+ * @return {String}
+ * @api private
+ */
+
+debug.humanize = function(ms) {
+  var sec = 1000
+    , min = 60 * 1000
+    , hour = 60 * min;
+
+  if (ms >= hour) return (ms / hour).toFixed(1) + 'h';
+  if (ms >= min) return (ms / min).toFixed(1) + 'm';
+  if (ms >= sec) return (ms / sec | 0) + 's';
+  return ms + 'ms';
+};
+
+/**
+ * Returns true if the given mode name is enabled, false otherwise.
+ *
+ * @param {String} name
+ * @return {Boolean}
+ * @api public
+ */
+
+debug.enabled = function(name) {
+  for (var i = 0, len = debug.skips.length; i < len; i++) {
+    if (debug.skips[i].test(name)) {
+      return false;
+    }
+  }
+  for (var i = 0, len = debug.names.length; i < len; i++) {
+    if (debug.names[i].test(name)) {
+      return true;
+    }
+  }
+  return false;
+};
+
+/**
+ * Coerce `val`.
+ */
+
+function coerce(val) {
+  if (val instanceof Error) return val.stack || val.message;
+  return val;
+}
+
+// persist
+
+try {
+  if (window.localStorage) debug.enable(localStorage.debug);
+} catch(e){}
+
+},{}],15:[function(_dereq_,module,exports){
+(function (global){
+/**
+ * Module dependencies.
+ */
+
+var keys = _dereq_('./keys');
+var sliceBuffer = _dereq_('arraybuffer.slice');
+var base64encoder = _dereq_('base64-arraybuffer');
+var after = _dereq_('after');
+var utf8 = _dereq_('utf8');
+
+/**
+ * Check if we are running an android browser. That requires us to use
+ * ArrayBuffer with polling transports...
+ *
+ * http://ghinda.net/jpeg-blob-ajax-android/
+ */
+
+var isAndroid = navigator.userAgent.match(/Android/i);
+
+/**
+ * Current protocol version.
+ */
+
+exports.protocol = 3;
+
+/**
+ * Packet types.
+ */
+
+var packets = exports.packets = {
+    open:     0    // non-ws
+  , close:    1    // non-ws
+  , ping:     2
+  , pong:     3
+  , message:  4
+  , upgrade:  5
+  , noop:     6
+};
+
+var packetslist = keys(packets);
+
+/**
+ * Premade error packet.
+ */
+
+var err = { type: 'error', data: 'parser error' };
+
+/**
+ * Create a blob api even for blob builder when vendor prefixes exist
+ */
+
+var Blob = _dereq_('blob');
+
+/**
+ * Encodes a packet.
+ *
+ *     <packet type id> [ <data> ]
+ *
+ * Example:
+ *
+ *     5hello world
+ *     3
+ *     4
+ *
+ * Binary is encoded in an identical principle
+ *
+ * @api private
+ */
+
+exports.encodePacket = function (packet, supportsBinary, utf8encode, callback) {
+  if ('function' == typeof supportsBinary) {
+    callback = supportsBinary;
+    supportsBinary = false;
+  }
+
+  if ('function' == typeof utf8encode) {
+    callback = utf8encode;
+    utf8encode = null;
+  }
+
+  var data = (packet.data === undefined)
+    ? undefined
+    : packet.data.buffer || packet.data;
+
+  if (global.ArrayBuffer && data instanceof ArrayBuffer) {
+    return encodeArrayBuffer(packet, supportsBinary, callback);
+  } else if (Blob && data instanceof global.Blob) {
+    return encodeBlob(packet, supportsBinary, callback);
+  }
+
+  // Sending data as a utf-8 string
+  var encoded = packets[packet.type];
+
+  // data fragment is optional
+  if (undefined !== packet.data) {
+    encoded += utf8encode ? utf8.encode(String(packet.data)) : String(packet.data);
+  }
+
+  return callback('' + encoded);
+
+};
+
+/**
+ * Encode packet helpers for binary types
+ */
+
+function encodeArrayBuffer(packet, supportsBinary, callback) {
+  if (!supportsBinary) {
+    return exports.encodeBase64Packet(packet, callback);
+  }
+
+  var data = packet.data;
+  var contentArray = new Uint8Array(data);
+  var resultBuffer = new Uint8Array(1 + data.byteLength);
+
+  resultBuffer[0] = packets[packet.type];
+  for (var i = 0; i < contentArray.length; i++) {
+    resultBuffer[i+1] = contentArray[i];
+  }
+
+  return callback(resultBuffer.buffer);
+}
+
+function encodeBlobAsArrayBuffer(packet, supportsBinary, callback) {
+  if (!supportsBinary) {
+    return exports.encodeBase64Packet(packet, callback);
+  }
+
+  var fr = new FileReader();
+  fr.onload = function() {
+    packet.data = fr.result;
+    exports.encodePacket(packet, supportsBinary, true, callback);
+  };
+  return fr.readAsArrayBuffer(packet.data);
+}
+
+function encodeBlob(packet, supportsBinary, callback) {
+  if (!supportsBinary) {
+    return exports.encodeBase64Packet(packet, callback);
+  }
+
+  if (isAndroid) {
+    return encodeBlobAsArrayBuffer(packet, supportsBinary, callback);
+  }
+
+  var length = new Uint8Array(1);
+  length[0] = packets[packet.type];
+  var blob = new Blob([length.buffer, packet.data]);
+
+  return callback(blob);
+}
+
+/**
+ * Encodes a packet with binary data in a base64 string
+ *
+ * @param {Object} packet, has `type` and `data`
+ * @return {String} base64 encoded message
+ */
+
+exports.encodeBase64Packet = function(packet, callback) {
+  var message = 'b' + exports.packets[packet.type];
+  if (Blob && packet.data instanceof Blob) {
+    var fr = new FileReader();
+    fr.onload = function() {
+      var b64 = fr.result.split(',')[1];
+      callback(message + b64);
+    };
+    return fr.readAsDataURL(packet.data);
+  }
+
+  var b64data;
+  try {
+    b64data = String.fromCharCode.apply(null, new Uint8Array(packet.data));
+  } catch (e) {
+    // iPhone Safari doesn't let you apply with typed arrays
+    var typed = new Uint8Array(packet.data);
+    var basic = new Array(typed.length);
+    for (var i = 0; i < typed.length; i++) {
+      basic[i] = typed[i];
+    }
+    b64data = String.fromCharCode.apply(null, basic);
+  }
+  message += global.btoa(b64data);
+  return callback(message);
+};
+
+/**
+ * Decodes a packet. Changes format to Blob if requested.
+ *
+ * @return {Object} with `type` and `data` (if any)
+ * @api private
+ */
+
+exports.decodePacket = function (data, binaryType, utf8decode) {
+  // String data
+  if (typeof data == 'string' || data === undefined) {
+    if (data.charAt(0) == 'b') {
+      return exports.decodeBase64Packet(data.substr(1), binaryType);
+    }
+
+    if (utf8decode) {
+      try {
+        data = utf8.decode(data);
+      } catch (e) {
+        return err;
+      }
+    }
+    var type = data.charAt(0);
+
+    if (Number(type) != type || !packetslist[type]) {
+      return err;
+    }
+
+    if (data.length > 1) {
+      return { type: packetslist[type], data: data.substring(1) };
+    } else {
+      return { type: packetslist[type] };
+    }
+  }
+
+  var asArray = new Uint8Array(data);
+  var type = asArray[0];
+  var rest = sliceBuffer(data, 1);
+  if (Blob && binaryType === 'blob') {
+    rest = new Blob([rest]);
+  }
+  return { type: packetslist[type], data: rest };
+};
+
+/**
+ * Decodes a packet encoded in a base64 string
+ *
+ * @param {String} base64 encoded message
+ * @return {Object} with `type` and `data` (if any)
+ */
+
+exports.decodeBase64Packet = function(msg, binaryType) {
+  var type = packetslist[msg.charAt(0)];
+  if (!global.ArrayBuffer) {
+    return { type: type, data: { base64: true, data: msg.substr(1) } };
+  }
+
+  var data = base64encoder.decode(msg.substr(1));
+
+  if (binaryType === 'blob' && Blob) {
+    data = new Blob([data]);
+  }
+
+  return { type: type, data: data };
+};
+
+/**
+ * Encodes multiple messages (payload).
+ *
+ *     <length>:data
+ *
+ * Example:
+ *
+ *     11:hello world2:hi
+ *
+ * If any contents are binary, they will be encoded as base64 strings. Base64
+ * encoded strings are marked with a b before the length specifier
+ *
+ * @param {Array} packets
+ * @api private
+ */
+
+exports.encodePayload = function (packets, supportsBinary, callback) {
+  if (typeof supportsBinary == 'function') {
+    callback = supportsBinary;
+    supportsBinary = null;
+  }
+
+  if (supportsBinary) {
+    if (Blob && !isAndroid) {
+      return exports.encodePayloadAsBlob(packets, callback);
+    }
+
+    return exports.encodePayloadAsArrayBuffer(packets, callback);
+  }
+
+  if (!packets.length) {
+    return callback('0:');
+  }
+
+  function setLengthHeader(message) {
+    return message.length + ':' + message;
+  }
+
+  function encodeOne(packet, doneCallback) {
+    exports.encodePacket(packet, supportsBinary, true, function(message) {
+      doneCallback(null, setLengthHeader(message));
+    });
+  }
+
+  map(packets, encodeOne, function(err, results) {
+    return callback(results.join(''));
+  });
+};
+
+/**
+ * Async array map using after
+ */
+
+function map(ary, each, done) {
+  var result = new Array(ary.length);
+  var next = after(ary.length, done);
+
+  var eachWithIndex = function(i, el, cb) {
+    each(el, function(error, msg) {
+      result[i] = msg;
+      cb(error, result);
+    });
+  };
+
+  for (var i = 0; i < ary.length; i++) {
+    eachWithIndex(i, ary[i], next);
+  }
+}
+
+/*
+ * Decodes data when a payload is maybe expected. Possible binary contents are
+ * decoded from their base64 representation
+ *
+ * @param {String} data, callback method
+ * @api public
+ */
+
+exports.decodePayload = function (data, binaryType, callback) {
+  if (typeof data != 'string') {
+    return exports.decodePayloadAsBinary(data, binaryType, callback);
+  }
+
+  if (typeof binaryType === 'function') {
+    callback = binaryType;
+    binaryType = null;
+  }
+
+  var packet;
+  if (data == '') {
+    // parser error - ignoring payload
+    return callback(err, 0, 1);
+  }
+
+  var length = ''
+    , n, msg;
+
+  for (var i = 0, l = data.length; i < l; i++) {
+    var chr = data.charAt(i);
+
+    if (':' != chr) {
+      length += chr;
+    } else {
+      if ('' == length || (length != (n = Number(length)))) {
+        // parser error - ignoring payload
+        return callback(err, 0, 1);
+      }
+
+      msg = data.substr(i + 1, n);
+
+      if (length != msg.length) {
+        // parser error - ignoring payload
+        return callback(err, 0, 1);
+      }
+
+      if (msg.length) {
+        packet = exports.decodePacket(msg, binaryType, true);
+
+        if (err.type == packet.type && err.data == packet.data) {
+          // parser error in individual packet - ignoring payload
+          return callback(err, 0, 1);
+        }
+
+        var ret = callback(packet, i + n, l);
+        if (false === ret) return;
+      }
+
+      // advance cursor
+      i += n;
+      length = '';
+    }
+  }
+
+  if (length != '') {
+    // parser error - ignoring payload
+    return callback(err, 0, 1);
+  }
+
+};
+
+/**
+ * Encodes multiple messages (payload) as binary.
+ *
+ * <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
+ * 255><data>
+ *
+ * Example:
+ * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers
+ *
+ * @param {Array} packets
+ * @return {ArrayBuffer} encoded payload
+ * @api private
+ */
+
+exports.encodePayloadAsArrayBuffer = function(packets, callback) {
+  if (!packets.length) {
+    return callback(new ArrayBuffer(0));
+  }
+
+  function encodeOne(packet, doneCallback) {
+    exports.encodePacket(packet, true, true, function(data) {
+      return doneCallback(null, data);
+    });
+  }
+
+  map(packets, encodeOne, function(err, encodedPackets) {
+    var totalLength = encodedPackets.reduce(function(acc, p) {
+      var len;
+      if (typeof p === 'string'){
+        len = p.length;
+      } else {
+        len = p.byteLength;
+      }
+      return acc + len.toString().length + len + 2; // string/binary identifier + separator = 2
+    }, 0);
+
+    var resultArray = new Uint8Array(totalLength);
+
+    var bufferIndex = 0;
+    encodedPackets.forEach(function(p) {
+      var isString = typeof p === 'string';
+      var ab = p;
+      if (isString) {
+        var view = new Uint8Array(p.length);
+        for (var i = 0; i < p.length; i++) {
+          view[i] = p.charCodeAt(i);
+        }
+        ab = view.buffer;
+      }
+
+      if (isString) { // not true binary
+        resultArray[bufferIndex++] = 0;
+      } else { // true binary
+        resultArray[bufferIndex++] = 1;
+      }
+
+      var lenStr = ab.byteLength.toString();
+      for (var i = 0; i < lenStr.length; i++) {
+        resultArray[bufferIndex++] = parseInt(lenStr[i]);
+      }
+      resultArray[bufferIndex++] = 255;
+
+      var view = new Uint8Array(ab);
+      for (var i = 0; i < view.length; i++) {
+        resultArray[bufferIndex++] = view[i];
+      }
+    });
+
+    return callback(resultArray.buffer);
+  });
+};
+
+/**
+ * Encode as Blob
+ */
+
+exports.encodePayloadAsBlob = function(packets, callback) {
+  function encodeOne(packet, doneCallback) {
+    exports.encodePacket(packet, true, true, function(encoded) {
+      var binaryIdentifier = new Uint8Array(1);
+      binaryIdentifier[0] = 1;
+      if (typeof encoded === 'string') {
+        var view = new Uint8Array(encoded.length);
+        for (var i = 0; i < encoded.length; i++) {
+          view[i] = encoded.charCodeAt(i);
+        }
+        encoded = view.buffer;
+        binaryIdentifier[0] = 0;
+      }
+
+      var len = (encoded instanceof ArrayBuffer)
+        ? encoded.byteLength
+        : encoded.size;
+
+      var lenStr = len.toString();
+      var lengthAry = new Uint8Array(lenStr.length + 1);
+      for (var i = 0; i < lenStr.length; i++) {
+        lengthAry[i] = parseInt(lenStr[i]);
+      }
+      lengthAry[lenStr.length] = 255;
+
+      if (Blob) {
+        var blob = new Blob([binaryIdentifier.buffer, lengthAry.buffer, encoded]);
+        doneCallback(null, blob);
+      }
+    });
+  }
+
+  map(packets, encodeOne, function(err, results) {
+    return callback(new Blob(results));
+  });
+};
+
+/*
+ * Decodes data when a payload is maybe expected. Strings are decoded by
+ * interpreting each byte as a key code for entries marked to start with 0. See
+ * description of encodePayloadAsBinary
+ *
+ * @param {ArrayBuffer} data, callback method
+ * @api public
+ */
+
+exports.decodePayloadAsBinary = function (data, binaryType, callback) {
+  if (typeof binaryType === 'function') {
+    callback = binaryType;
+    binaryType = null;
+  }
+
+  var bufferTail = data;
+  var buffers = [];
+
+  var numberTooLong = false;
+  while (bufferTail.byteLength > 0) {
+    var tailArray = new Uint8Array(bufferTail);
+    var isString = tailArray[0] === 0;
+    var msgLength = '';
+
+    for (var i = 1; ; i++) {
+      if (tailArray[i] == 255) break;
+
+      if (msgLength.length > 310) {
+        numberTooLong = true;
+        break;
+      }
+
+      msgLength += tailArray[i];
+    }
+
+    if(numberTooLong) return callback(err, 0, 1);
+
+    bufferTail = sliceBuffer(bufferTail, 2 + msgLength.length);
+    msgLength = parseInt(msgLength);
+
+    var msg = sliceBuffer(bufferTail, 0, msgLength);
+    if (isString) {
+      try {
+        msg = String.fromCharCode.apply(null, new Uint8Array(msg));
+      } catch (e) {
+        // iPhone Safari doesn't let you apply to typed arrays
+        var typed = new Uint8Array(msg);
+        msg = '';
+        for (var i = 0; i < typed.length; i++) {
+          msg += String.fromCharCode(typed[i]);
+        }
+      }
+    }
+
+    buffers.push(msg);
+    bufferTail = sliceBuffer(bufferTail, msgLength);
+  }
+
+  var total = buffers.length;
+  buffers.forEach(function(buffer, i) {
+    callback(exports.decodePacket(buffer, binaryType, true), i, total);
+  });
+};
+
+}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./keys":16,"after":17,"arraybuffer.slice":18,"base64-arraybuffer":19,"blob":11,"utf8":20}],16:[function(_dereq_,module,exports){
+
+/**
+ * Gets the keys for an object.
+ *
+ * @return {Array} keys
+ * @api private
+ */
+
+module.exports = Object.keys || function keys (obj){
+  var arr = [];
+  var has = Object.prototype.hasOwnProperty;
+
+  for (var i in obj) {
+    if (has.call(obj, i)) {
+      arr.push(i);
+    }
+  }
+  return arr;
+};
+
+},{}],17:[function(_dereq_,module,exports){
+module.exports = after
+
+function after(count, callback, err_cb) {
+    var bail = false
+    err_cb = err_cb || noop
+    proxy.count = count
+
+    return (count === 0) ? callback() : proxy
+
+    function proxy(err, result) {
+        if (proxy.count <= 0) {
+            throw new Error('after called too many times')
+        }
+        --proxy.count
+
+        // after first error, rest are passed to err_cb
+        if (err) {
+            bail = true
+            callback(err)
+            // future error callbacks will go to error handler
+            callback = err_cb
+        } else if (proxy.count === 0 && !bail) {
+            callback(null, result)
+        }
+    }
+}
+
+function noop() {}
+
+},{}],18:[function(_dereq_,module,exports){
+/**
+ * An abstraction for slicing an arraybuffer even when
+ * ArrayBuffer.prototype.slice is not supported
+ *
+ * @api public
+ */
+
+module.exports = function(arraybuffer, start, end) {
+  var bytes = arraybuffer.byteLength;
+  start = start || 0;
+  end = end || bytes;
+
+  if (arraybuffer.slice) { return arraybuffer.slice(start, end); }
+
+  if (start < 0) { start += bytes; }
+  if (end < 0) { end += bytes; }
+  if (end > bytes) { end = bytes; }
+
+  if (start >= bytes || start >= end || bytes === 0) {
+    return new ArrayBuffer(0);
+  }
+
+  var abv = new Uint8Array(arraybuffer);
+  var result = new Uint8Array(end - start);
+  for (var i = start, ii = 0; i < end; i++, ii++) {
+    result[ii] = abv[i];
+  }
+  return result.buffer;
+};
+
+},{}],19:[function(_dereq_,module,exports){
+/*
+ * base64-arraybuffer
+ * https://github.com/niklasvh/base64-arraybuffer
+ *
+ * Copyright (c) 2012 Niklas von Hertzen
+ * Licensed under the MIT license.
+ */
+(function(chars){
+  "use strict";
+
+  exports.encode = function(arraybuffer) {
+    var bytes = new Uint8Array(arraybuffer),
+    i, len = bytes.length, base64 = "";
+
+    for (i = 0; i < len; i+=3) {
+      base64 += chars[bytes[i] >> 2];
+      base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
+      base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
+      base64 += chars[bytes[i + 2] & 63];
+    }
+
+    if ((len % 3) === 2) {
+      base64 = base64.substring(0, base64.length - 1) + "=";
+    } else if (len % 3 === 1) {
+      base64 = base64.substring(0, base64.length - 2) + "==";
+    }
+
+    return base64;
+  };
+
+  exports.decode =  function(base64) {
+    var bufferLength = base64.length * 0.75,
+    len = base64.length, i, p = 0,
+    encoded1, encoded2, encoded3, encoded4;
+
+    if (base64[base64.length - 1] === "=") {
+      bufferLength--;
+      if (base64[base64.length - 2] === "=") {
+        bufferLength--;
+      }
+    }
+
+    var arraybuffer = new ArrayBuffer(bufferLength),
+    bytes = new Uint8Array(arraybuffer);
+
+    for (i = 0; i < len; i+=4) {
+      encoded1 = chars.indexOf(base64[i]);
+      encoded2 = chars.indexOf(base64[i+1]);
+      encoded3 = chars.indexOf(base64[i+2]);
+      encoded4 = chars.indexOf(base64[i+3]);
+
+      bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
+      bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
+      bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
+    }
+
+    return arraybuffer;
+  };
+})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
+
+},{}],20:[function(_dereq_,module,exports){
+(function (global){
+/*! http://mths.be/utf8js v2.0.0 by @mathias */
+;(function(root) {
+
+  // Detect free variables `exports`
+  var freeExports = typeof exports == 'object' && exports;
+
+  // Detect free variable `module`
+  var freeModule = typeof module == 'object' && module &&
+    module.exports == freeExports && module;
+
+  // Detect free variable `global`, from Node.js or Browserified code,
+  // and use it as `root`
+  var freeGlobal = typeof global == 'object' && global;
+  if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
+    root = freeGlobal;
+  }
+
+  /*--------------------------------------------------------------------------*/
+
+  var stringFromCharCode = String.fromCharCode;
+
+  // Taken from http://mths.be/punycode
+  function ucs2decode(string) {
+    var output = [];
+    var counter = 0;
+    var length = string.length;
+    var value;
+    var extra;
+    while (counter < length) {
+      value = string.charCodeAt(counter++);
+      if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
+        // high surrogate, and there is a next character
+        extra = string.charCodeAt(counter++);
+        if ((extra & 0xFC00) == 0xDC00) { // low surrogate
+          output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
+        } else {
+          // unmatched surrogate; only append this code unit, in case the next
+          // code unit is the high surrogate of a surrogate pair
+          output.push(value);
+          counter--;
+        }
+      } else {
+        output.push(value);
+      }
+    }
+    return output;
+  }
+
+  // Taken from http://mths.be/punycode
+  function ucs2encode(array) {
+    var length = array.length;
+    var index = -1;
+    var value;
+    var output = '';
+    while (++index < length) {
+      value = array[index];
+      if (value > 0xFFFF) {
+        value -= 0x10000;
+        output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
+        value = 0xDC00 | value & 0x3FF;
+      }
+      output += stringFromCharCode(value);
+    }
+    return output;
+  }
+
+  /*--------------------------------------------------------------------------*/
+
+  function createByte(codePoint, shift) {
+    return stringFromCharCode(((codePoint >> shift) & 0x3F) | 0x80);
+  }
+
+  function encodeCodePoint(codePoint) {
+    if ((codePoint & 0xFFFFFF80) == 0) { // 1-byte sequence
+      return stringFromCharCode(codePoint);
+    }
+    var symbol = '';
+    if ((codePoint & 0xFFFFF800) == 0) { // 2-byte sequence
+      symbol = stringFromCharCode(((codePoint >> 6) & 0x1F) | 0xC0);
+    }
+    else if ((codePoint & 0xFFFF0000) == 0) { // 3-byte sequence
+      symbol = stringFromCharCode(((codePoint >> 12) & 0x0F) | 0xE0);
+      symbol += createByte(codePoint, 6);
+    }
+    else if ((codePoint & 0xFFE00000) == 0) { // 4-byte sequence
+      symbol = stringFromCharCode(((codePoint >> 18) & 0x07) | 0xF0);
+      symbol += createByte(codePoint, 12);
+      symbol += createByte(codePoint, 6);
+    }
+    symbol += stringFromCharCode((codePoint & 0x3F) | 0x80);
+    return symbol;
+  }
+
+  function utf8encode(string) {
+    var codePoints = ucs2decode(string);
+
+    // console.log(JSON.stringify(codePoints.map(function(x) {
+    //  return 'U+' + x.toString(16).toUpperCase();
+    // })));
+
+    var length = codePoints.length;
+    var index = -1;
+    var codePoint;
+    var byteString = '';
+    while (++index < length) {
+      codePoint = codePoints[index];
+      byteString += encodeCodePoint(codePoint);
+    }
+    return byteString;
+  }
+
+  /*--------------------------------------------------------------------------*/
+
+  function readContinuationByte() {
+    if (byteIndex >= byteCount) {
+      throw Error('Invalid byte index');
+    }
+
+    var continuationByte = byteArray[byteIndex] & 0xFF;
+    byteIndex++;
+
+    if ((continuationByte & 0xC0) == 0x80) {
+      return continuationByte & 0x3F;
+    }
+
+    // If we end up here, it’s not a continuation byte
+    throw Error('Invalid continuation byte');
+  }
+
+  function decodeSymbol() {
+    var byte1;
+    var byte2;
+    var byte3;
+    var byte4;
+    var codePoint;
+
+    if (byteIndex > byteCount) {
+      throw Error('Invalid byte index');
+    }
+
+    if (byteIndex == byteCount) {
+      return false;
+    }
+
+    // Read first byte
+    byte1 = byteArray[byteIndex] & 0xFF;
+    byteIndex++;
+
+    // 1-byte sequence (no continuation bytes)
+    if ((byte1 & 0x80) == 0) {
+      return byte1;
+    }
+
+    // 2-byte sequence
+    if ((byte1 & 0xE0) == 0xC0) {
+      var byte2 = readContinuationByte();
+      codePoint = ((byte1 & 0x1F) << 6) | byte2;
+      if (codePoint >= 0x80) {
+        return codePoint;
+      } else {
+        throw Error('Invalid continuation byte');
+      }
+    }
+
+    // 3-byte sequence (may include unpaired surrogates)
+    if ((byte1 & 0xF0) == 0xE0) {
+      byte2 = readContinuationByte();
+      byte3 = readContinuationByte();
+      codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3;
+      if (codePoint >= 0x0800) {
+        return codePoint;
+      } else {
+        throw Error('Invalid continuation byte');
+      }
+    }
+
+    // 4-byte sequence
+    if ((byte1 & 0xF8) == 0xF0) {
+      byte2 = readContinuationByte();
+      byte3 = readContinuationByte();
+      byte4 = readContinuationByte();
+      codePoint = ((byte1 & 0x0F) << 0x12) | (byte2 << 0x0C) |
+        (byte3 << 0x06) | byte4;
+      if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) {
+        return codePoint;
+      }
+    }
+
+    throw Error('Invalid UTF-8 detected');
+  }
+
+  var byteArray;
+  var byteCount;
+  var byteIndex;
+  function utf8decode(byteString) {
+    byteArray = ucs2decode(byteString);
+    byteCount = byteArray.length;
+    byteIndex = 0;
+    var codePoints = [];
+    var tmp;
+    while ((tmp = decodeSymbol()) !== false) {
+      codePoints.push(tmp);
+    }
+    return ucs2encode(codePoints);
+  }
+
+  /*--------------------------------------------------------------------------*/
+
+  var utf8 = {
+    'version': '2.0.0',
+    'encode': utf8encode,
+    'decode': utf8decode
+  };
+
+  // Some AMD build optimizers, like r.js, check for specific condition patterns
+  // like the following:
+  if (
+    typeof define == 'function' &&
+    typeof define.amd == 'object' &&
+    define.amd
+  ) {
+    define(function() {
+      return utf8;
+    });
+  } else if (freeExports && !freeExports.nodeType) {
+    if (freeModule) { // in Node.js or RingoJS v0.8.0+
+      freeModule.exports = utf8;
+    } else { // in Narwhal or RingoJS v0.7.0-
+      var object = {};
+      var hasOwnProperty = object.hasOwnProperty;
+      for (var key in utf8) {
+        hasOwnProperty.call(utf8, key) && (freeExports[key] = utf8[key]);
+      }
+    }
+  } else { // in Rhino or a web browser
+    root.utf8 = utf8;
+  }
+
+}(this));
+
+}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],21:[function(_dereq_,module,exports){
+
+/**
+ * Module dependencies.
+ */
+
+var global = _dereq_('global');
+
+/**
+ * Module exports.
+ *
+ * Logic borrowed from Modernizr:
+ *
+ *   - https://github.com/Modernizr/Modernizr/blob/master/feature-detects/cors.js
+ */
+
+try {
+  module.exports = 'XMLHttpRequest' in global &&
+    'withCredentials' in new global.XMLHttpRequest();
+} catch (err) {
+  // if XMLHttp support is disabled in IE then it will throw
+  // when trying to create
+  module.exports = false;
+}
+
+},{"global":22}],22:[function(_dereq_,module,exports){
+
+/**
+ * Returns `this`. Execute this without a "context" (i.e. without it being
+ * attached to an object of the left-hand side), and `this` points to the
+ * "global" scope of the current JS execution.
+ */
+
+module.exports = (function () { return this; })();
+
+},{}],23:[function(_dereq_,module,exports){
+
+var indexOf = [].indexOf;
+
+module.exports = function(arr, obj){
+  if (indexOf) return arr.indexOf(obj);
+  for (var i = 0; i < arr.length; ++i) {
+    if (arr[i] === obj) return i;
+  }
+  return -1;
+};
+},{}],24:[function(_dereq_,module,exports){
+(function (global){
+/**
+ * JSON parse.
+ *
+ * @see Based on jQuery#parseJSON (MIT) and JSON2
+ * @api private
+ */
+
+var rvalidchars = /^[\],:{}\s]*$/;
+var rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
+var rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
+var rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g;
+var rtrimLeft = /^\s+/;
+var rtrimRight = /\s+$/;
+
+module.exports = function parsejson(data) {
+  if ('string' != typeof data || !data) {
+    return null;
+  }
+
+  data = data.replace(rtrimLeft, '').replace(rtrimRight, '');
+
+  // Attempt to parse using the native JSON parser first
+  if (global.JSON && JSON.parse) {
+    return JSON.parse(data);
+  }
+
+  if (rvalidchars.test(data.replace(rvalidescape, '@')
+      .replace(rvalidtokens, ']')
+      .replace(rvalidbraces, ''))) {
+    return (new Function('return ' + data))();
+  }
+};
+}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],25:[function(_dereq_,module,exports){
+/**
+ * Compiles a querystring
+ * Returns string representation of the object
+ *
+ * @param {Object}
+ * @api private
+ */
+
+exports.encode = function (obj) {
+  var str = '';
+
+  for (var i in obj) {
+    if (obj.hasOwnProperty(i)) {
+      if (str.length) str += '&';
+      str += encodeURIComponent(i) + '=' + encodeURIComponent(obj[i]);
+    }
+  }
+
+  return str;
+};
+
+/**
+ * Parses a simple querystring into an object
+ *
+ * @param {String} qs
+ * @api private
+ */
+
+exports.decode = function(qs){
+  var qry = {};
+  var pairs = qs.split('&');
+  for (var i = 0, l = pairs.length; i < l; i++) {
+    var pair = pairs[i].split('=');
+    qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
+  }
+  return qry;
+};
+
+},{}],26:[function(_dereq_,module,exports){
+/**
+ * Parses an URI
+ *
+ * @author Steven Levithan <stevenlevithan.com> (MIT license)
+ * @api private
+ */
+
+var re = /^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
+
+var parts = [
+    'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
+];
+
+module.exports = function parseuri(str) {
+    var src = str,
+        b = str.indexOf('['),
+        e = str.indexOf(']');
+
+    if (b != -1 && e != -1) {
+        str = str.substring(0, b) + str.substring(b, e).replace(/:/g, ';') + str.substring(e, str.length);
+    }
+
+    var m = re.exec(str || ''),
+        uri = {},
+        i = 14;
+
+    while (i--) {
+        uri[parts[i]] = m[i] || '';
+    }
+
+    if (b != -1 && e != -1) {
+        uri.source = src;
+        uri.host = uri.host.substring(1, uri.host.length - 1).replace(/;/g, ':');
+        uri.authority = uri.authority.replace('[', '').replace(']', '').replace(/;/g, ':');
+        uri.ipv6uri = true;
+    }
+
+    return uri;
+};
+
+},{}],27:[function(_dereq_,module,exports){
+
+/**
+ * Module dependencies.
+ */
+
+var global = (function() { return this; })();
+
+/**
+ * WebSocket constructor.
+ */
+
+var WebSocket = global.WebSocket || global.MozWebSocket;
+
+/**
+ * Module exports.
+ */
+
+module.exports = WebSocket ? ws : null;
+
+/**
+ * WebSocket constructor.
+ *
+ * The third `opts` options object gets ignored in web browsers, since it's
+ * non-standard, and throws a TypeError if passed to the constructor.
+ * See: https://github.com/einaros/ws/issues/227
+ *
+ * @param {String} uri
+ * @param {Array} protocols (optional)
+ * @param {Object) opts (optional)
+ * @api public
+ */
+
+function ws(uri, protocols, opts) {
+  var instance;
+  if (protocols) {
+    instance = new WebSocket(uri, protocols);
+  } else {
+    instance = new WebSocket(uri);
+  }
+  return instance;
+}
+
+if (WebSocket) ws.prototype = WebSocket.prototype;
+
+},{}]},{},[1])
+(1)
+});
+
+
+
+var EngineioTools = {
+    ReconnectingSocket: function ReconnectingSocket(server_uri, socket_options) {
+        var connected = false;
+        var is_reconnecting = false;
+
+        var reconnect_delay = 4000;
+        var reconnect_last_delay = 0;
+        var reconnect_delay_exponential = true;
+        var reconnect_max_attempts = 5;
+        var reconnect_step = 0;
+        var reconnect_tmr = null;
+
+        var original_disconnect;
+        var planned_disconnect = false;
+
+        var socket = eio.apply(eio, arguments);
+        socket.on('open', onOpen);
+        socket.on('close', onClose);
+        socket.on('error', onError);
+
+        original_disconnect = socket.close;
+        socket.close = close;
+
+        // Apply any custom reconnection config
+        if (socket_options) {
+            if (typeof socket_options.reconnect_delay === 'number')
+                reconnect_delay = socket_options.reconnect_delay;
+
+            if (typeof socket_options.reconnect_max_attempts === 'number')
+                reconnect_max_attempts = socket_options.reconnect_max_attempts;
+
+            if (typeof socket_options.reconnect_delay_exponential !== 'undefined')
+                reconnect_delay_exponential = !!socket_options.reconnect_delay_exponential;
+        }
+
+
+        function onOpen() {
+            connected = true;
+            is_reconnecting = false;
+            planned_disconnect = false;
+
+            reconnect_step = 0;
+            reconnect_last_delay = 0;
+
+            clearTimeout(reconnect_tmr);
+        }
+
+
+        function onClose() {
+            connected = false;
+
+            if (!planned_disconnect && !is_reconnecting)
+                reconnect();
+        }
+
+
+        function onError() {
+            // This will be called when a reconnect fails
+            if (is_reconnecting)
+                reconnect();
+        }
+
+
+        function close() {
+            planned_disconnect = true;
+            original_disconnect.call(socket);
+        }
+
+
+        function reconnect() {
+            if (reconnect_step >= reconnect_max_attempts) {
+                socket.emit('reconnecting_failed');
+                return;
+            }
+
+            var delay = reconnect_delay_exponential ?
+                (reconnect_last_delay || reconnect_delay / 2) * 2 :
+                reconnect_delay * reconnect_step;
+
+            is_reconnecting = true;
+
+            reconnect_tmr = setTimeout(function() {
+                socket.open();
+            }, delay);
+
+            reconnect_last_delay = delay;
+
+            socket.emit('reconnecting', {
+                attempt: reconnect_step + 1,
+                max_attempts: reconnect_max_attempts,
+                delay: delay
+            });
+
+            reconnect_step++;
+        }
+
+        return socket;
+    },
+
+
+
+
+    Rpc: (function(){
+        /*
+            TODO:
+            Create a document explaining the protocol
+            Some way to expire unused callbacks? TTL? expireCallback() function?
+        */
+
+        /**
+         * Wrapper around creating a new WebsocketRpcCaller
+         * This lets us use the WebsocketRpc object as a function
+         */
+        function WebsocketRpc(eio_socket) {
+            var caller = new WebsocketRpcCaller(eio_socket);
+            var ret = function WebsocketRpcInstance() {
+                return ret.makeCall.apply(ret, arguments);
+            };
+
+            for(var prop in caller){
+                ret[prop] = caller[prop];
+            }
+
+            ret._mixinEmitter();
+            ret._bindSocketListeners();
+
+            // Keep a reference to the main Rpc object so namespaces can find calling functions
+            ret._rpc = ret;
+
+            return ret;
+        }
+
+
+        function WebsocketRpcCaller(eio_socket) {
+            this._next_id = 0;
+            this._rpc_callbacks = {};
+            this._socket = eio_socket;
+
+            this._rpc = this;
+            this._namespace = '';
+            this._namespaces = [];
+        }
+
+
+        WebsocketRpcCaller.prototype._bindSocketListeners = function() {
+            var self = this;
+
+            // Proxy the onMessage listener
+            this._onMessageProxy = function rpcOnMessageBoundFunction(){
+                self._onMessage.apply(self, arguments);
+            };
+            this._socket.on('message', this._onMessageProxy);
+        };
+
+
+
+        WebsocketRpcCaller.prototype.dispose = function() {
+            if (this._onMessageProxy) {
+                this._socket.removeListener('message', this._onMessageProxy);
+                delete this._onMessageProxy;
+            }
+
+            // Clean up any namespaces
+            for (var idx in this._namespaces) {
+                this._namespaces[idx].dispose();
+            }
+
+            this.removeAllListeners();
+        };
+
+
+
+        WebsocketRpcCaller.prototype.namespace = function(namespace_name) {
+            var complete_namespace, namespace;
+
+            if (this._namespace) {
+                complete_namespace = this._namespace + '.' + namespace_name;
+            } else {
+                complete_namespace = namespace_name;
+            }
+
+            namespace = new this._rpc.Namespace(this._rpc, complete_namespace);
+            this._rpc._namespaces.push(namespace);
+
+            return namespace;
+        };
+
+
+
+        // Find all namespaces that either matches or starts with namespace_name
+        WebsocketRpcCaller.prototype._findRelevantNamespaces = function(namespace_name) {
+            var found_namespaces = [];
+
+            for(var idx in this._namespaces) {
+                if (this._namespaces[idx]._namespace === namespace_name) {
+                    found_namespaces.push(this._namespaces[idx]);
+                }
+
+                if (this._namespaces[idx]._namespace.indexOf(namespace_name + '.') === 0) {
+                    found_namespaces.push(this._namespaces[idx]);
+                }
+            }
+
+            return found_namespaces;
+        };
+
+
+
+        /**
+         * The engine.io socket already has an emitter mixin so steal it from there
+         */
+        WebsocketRpcCaller.prototype._mixinEmitter = function(target_obj) {
+            var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners'];
+
+            target_obj = target_obj || this;
+
+            for (var i=0; i<funcs.length; i++) {
+                if (typeof this._socket[funcs[i]] === 'function')
+                    target_obj[funcs[i]] = this._socket[funcs[i]];
+            }
+        };
+
+
+        /**
+         * Check if a packet is a valid RPC call
+         */
+        WebsocketRpcCaller.prototype._isCall = function(packet) {
+            return (typeof packet.method !== 'undefined' &&
+                    typeof packet.params !== 'undefined');
+        };
+
+
+        /**
+         * Check if a packet is a valid RPC response
+         */
+        WebsocketRpcCaller.prototype._isResponse = function(packet) {
+            return (typeof packet.id !== 'undefined' &&
+                    typeof packet.response !== 'undefined');
+        };
+
+
+
+        /**
+         * Make an RPC call
+         * First argument must be the method name to call
+         * If the last argument is a function, it is used as a callback
+         * All other arguments are passed to the RPC method
+         * Eg. Rpc.makeCall('namespace.method_name', 1, 2, 3, callbackFn)
+         */
+        WebsocketRpcCaller.prototype.makeCall = function(method) {
+            var params, callback, packet;
+
+            // Get a normal array of passed in arguments
+            params = Array.prototype.slice.call(arguments, 1, arguments.length);
+
+            // If the last argument is a function, take it as a callback and strip it out
+            if (typeof params[params.length-1] === 'function') {
+                callback = params[params.length-1];
+                params = params.slice(0, params.length-1);
+            }
+
+            packet = {
+                method: method,
+                params: params
+            };
+
+            if (typeof callback === 'function') {
+                packet.id = this._next_id;
+
+                this._next_id++;
+                this._rpc_callbacks[packet.id] = callback;
+            }
+
+            this.send(packet);
+        };
+
+
+        /**
+         * Encode the packet into JSON and send it over the websocket
+         */
+        WebsocketRpcCaller.prototype.send = function(packet) {
+            if (this._socket)
+                this._socket.send(JSON.stringify(packet));
+        };
+
+
+        /**
+         * Handler for the websocket `message` event
+         */
+        WebsocketRpcCaller.prototype._onMessage = function(message_raw) {
+            var self = this,
+                packet,
+                returnFn,
+                callback,
+                namespace, namespaces, idx;
+
+            try {
+                packet = JSON.parse(message_raw);
+                if (!packet) throw 'Corrupt packet';
+            } catch(err) {
+                return;
+            }
+
+            if (this._isResponse(packet)) {
+                // If we have no callback waiting for this response, don't do anything
+                if (typeof this._rpc_callbacks[packet.id] !== 'function')
+                    return;
+
+                // Delete the callback before calling it. If any exceptions accur within the callback
+                // we don't have to worry about the delete not happening
+                callback = this._rpc_callbacks[packet.id];
+                delete this._rpc_callbacks[packet.id];
+
+                callback.apply(this, packet.response);
+
+            } else if (this._isCall(packet)) {
+                // Calls with an ID may be responded to
+                if (typeof packet.id !== 'undefined') {
+                    returnFn = this._createReturnCallFn(packet.id);
+                } else {
+                    returnFn = this._noop;
+                }
+
+                this.emit.apply(this, ['all', packet.method, returnFn].concat(packet.params));
+                this.emit.apply(this, [packet.method, returnFn].concat(packet.params));
+
+                if (packet.method.indexOf('.') > 0) {
+                    namespace = packet.method.substring(0, packet.method.lastIndexOf('.'));
+                    namespaces = this._findRelevantNamespaces(namespace);
+                    for(idx in namespaces){
+                        packet.method = packet.method.replace(namespaces[idx]._namespace + '.', '');
+                        namespaces[idx].emit.apply(namespaces[idx], [packet.method, returnFn].concat(packet.params));
+                    }
+                }
+            }
+        };
+
+
+        /**
+         * Returns a function used as a callback when responding to a call
+         */
+        WebsocketRpcCaller.prototype._createReturnCallFn = function(packet_id) {
+            var self = this;
+
+            return function returnCallFn() {
+                var value = Array.prototype.slice.call(arguments, 0);
+
+                var ret_packet = {
+                    id: packet_id,
+                    response: value
+                };
+
+                self.send(ret_packet);
+            };
+        };
+
+
+
+        WebsocketRpcCaller.prototype._noop = function() {};
+
+
+
+        WebsocketRpcCaller.prototype.Namespace = function(rpc, namespace) {
+            var ret = function WebsocketRpcNamespaceInstance() {
+                if (typeof arguments[0] === 'undefined') {
+                    return;
+                }
+
+                arguments[0] = ret._namespace + '.' + arguments[0];
+                return ret._rpc.apply(ret._rpc, arguments);
+            };
+
+            ret._rpc = rpc;
+            ret._namespace = namespace;
+
+            ret.dispose = function() {
+                ret.removeAllListeners();
+                ret._rpc = null;
+            };
+
+            rpc._mixinEmitter(ret);
+
+            return ret;
+        };
+
+
+        return WebsocketRpc;
+
+    }())
+};
+
+
diff --git a/2016/assets/js/engine.io.bundle.min.js b/2016/assets/js/engine.io.bundle.min.js
new file mode 100644 (file)
index 0000000..104aac2
--- /dev/null
@@ -0,0 +1,2 @@
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;"undefined"!=typeof window?t=window:"undefined"!=typeof global?t=global:"undefined"!=typeof self&&(t=self),t.eio=e()}}(function(){var e;return function t(e,n,i){function s(o,r){if(!n[o]){if(!e[o]){var c="function"==typeof require&&require;if(!r&&c)return c(o,!0);if(a)return a(o,!0);throw new Error("Cannot find module '"+o+"'")}var l=n[o]={exports:{}};e[o][0].call(l.exports,function(t){var n=e[o][1][t];return s(n?n:t)},l,l.exports,t,e,n,i)}return n[o].exports}for(var a="function"==typeof require&&require,o=0;o<i.length;o++)s(i[o]);return s}({1:[function(e,t){t.exports=e("./lib/")},{"./lib/":2}],2:[function(e,t){t.exports=e("./socket"),t.exports.parser=e("engine.io-parser")},{"./socket":3,"engine.io-parser":15}],3:[function(e,t){(function(n){function i(e,t){if(!(this instanceof i))return new i(e,t);if(t=t||{},e&&"object"==typeof e&&(t=e,e=null),e&&(e=h(e),t.host=e.host,t.secure="https"==e.protocol||"wss"==e.protocol,t.port=e.port,e.query&&(t.query=e.query)),this.secure=null!=t.secure?t.secure:n.location&&"https:"==location.protocol,t.host){var s=t.host.split(":");t.hostname=s.shift(),s.length&&(t.port=s.pop())}this.agent=t.agent||!1,this.hostname=t.hostname||(n.location?location.hostname:"localhost"),this.port=t.port||(n.location&&location.port?location.port:this.secure?443:80),this.query=t.query||{},"string"==typeof this.query&&(this.query=d.decode(this.query)),this.upgrade=!1!==t.upgrade,this.path=(t.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!t.forceJSONP,this.jsonp=!1!==t.jsonp,this.forceBase64=!!t.forceBase64,this.enablesXDR=!!t.enablesXDR,this.timestampParam=t.timestampParam||"t",this.timestampRequests=t.timestampRequests,this.transports=t.transports||["polling","websocket"],this.readyState="",this.writeBuffer=[],this.callbackBuffer=[],this.policyPort=t.policyPort||843,this.rememberUpgrade=t.rememberUpgrade||!1,this.open(),this.binaryType=null,this.onlyBinaryUpgrades=t.onlyBinaryUpgrades}function s(e){var t={};for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var a=e("./transports"),o=e("component-emitter"),r=e("debug")("engine.io-client:socket"),c=e("indexof"),l=e("engine.io-parser"),h=e("parseuri"),p=e("parsejson"),d=e("parseqs");t.exports=i,i.priorWebsocketSuccess=!1,o(i.prototype),i.protocol=l.protocol,i.Socket=i,i.Transport=e("./transport"),i.transports=e("./transports"),i.parser=e("engine.io-parser"),i.prototype.createTransport=function(e){r('creating transport "%s"',e);var t=s(this.query);t.EIO=l.protocol,t.transport=e,this.id&&(t.sid=this.id);var n=new a[e]({agent:this.agent,hostname:this.hostname,port:this.port,secure:this.secure,path:this.path,query:t,forceJSONP:this.forceJSONP,jsonp:this.jsonp,forceBase64:this.forceBase64,enablesXDR:this.enablesXDR,timestampRequests:this.timestampRequests,timestampParam:this.timestampParam,policyPort:this.policyPort,socket:this});return n},i.prototype.open=function(){var e;if(this.rememberUpgrade&&i.priorWebsocketSuccess&&-1!=this.transports.indexOf("websocket"))e="websocket";else{if(0==this.transports.length){var t=this;return void setTimeout(function(){t.emit("error","No transports available")},0)}e=this.transports[0]}this.readyState="opening";var e;try{e=this.createTransport(e)}catch(n){return this.transports.shift(),void this.open()}e.open(),this.setTransport(e)},i.prototype.setTransport=function(e){r("setting transport %s",e.name);var t=this;this.transport&&(r("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=e,e.on("drain",function(){t.onDrain()}).on("packet",function(e){t.onPacket(e)}).on("error",function(e){t.onError(e)}).on("close",function(){t.onClose("transport close")})},i.prototype.probe=function(e){function t(){if(d.onlyBinaryUpgrades){var t=!this.supportsBinary&&d.transport.supportsBinary;p=p||t}p||(r('probe transport "%s" opened',e),h.send([{type:"ping",data:"probe"}]),h.once("packet",function(t){if(!p)if("pong"==t.type&&"probe"==t.data)r('probe transport "%s" pong',e),d.upgrading=!0,d.emit("upgrading",h),i.priorWebsocketSuccess="websocket"==h.name,r('pausing current transport "%s"',d.transport.name),d.transport.pause(function(){p||"closed"!=d.readyState&&"closing"!=d.readyState&&(r("changing transport and sending upgrade packet"),l(),d.setTransport(h),h.send([{type:"upgrade"}]),d.emit("upgrade",h),h=null,d.upgrading=!1,d.flush())});else{r('probe transport "%s" failed',e);var n=new Error("probe error");n.transport=h.name,d.emit("upgradeError",n)}}))}function n(){p||(p=!0,l(),h.close(),h=null)}function s(t){var i=new Error("probe error: "+t);i.transport=h.name,n(),r('probe transport "%s" failed because of error: %s',e,t),d.emit("upgradeError",i)}function a(){s("transport closed")}function o(){s("socket closed")}function c(e){h&&e.name!=h.name&&(r('"%s" works - aborting "%s"',e.name,h.name),n())}function l(){h.removeListener("open",t),h.removeListener("error",s),h.removeListener("close",a),d.removeListener("close",o),d.removeListener("upgrading",c)}r('probing transport "%s"',e);var h=this.createTransport(e,{probe:1}),p=!1,d=this;i.priorWebsocketSuccess=!1,h.once("open",t),h.once("error",s),h.once("close",a),this.once("close",o),this.once("upgrading",c),h.open()},i.prototype.onOpen=function(){if(r("socket open"),this.readyState="open",i.priorWebsocketSuccess="websocket"==this.transport.name,this.emit("open"),this.flush(),"open"==this.readyState&&this.upgrade&&this.transport.pause){r("starting upgrade probes");for(var e=0,t=this.upgrades.length;t>e;e++)this.probe(this.upgrades[e])}},i.prototype.onPacket=function(e){if("opening"==this.readyState||"open"==this.readyState)switch(r('socket receive: type "%s", data "%s"',e.type,e.data),this.emit("packet",e),this.emit("heartbeat"),e.type){case"open":this.onHandshake(p(e.data));break;case"pong":this.setPing();break;case"error":var t=new Error("server error");t.code=e.data,this.emit("error",t);break;case"message":this.emit("data",e.data),this.emit("message",e.data)}else r('packet received with socket readyState "%s"',this.readyState)},i.prototype.onHandshake=function(e){this.emit("handshake",e),this.id=e.sid,this.transport.query.sid=e.sid,this.upgrades=this.filterUpgrades(e.upgrades),this.pingInterval=e.pingInterval,this.pingTimeout=e.pingTimeout,this.onOpen(),"closed"!=this.readyState&&(this.setPing(),this.removeListener("heartbeat",this.onHeartbeat),this.on("heartbeat",this.onHeartbeat))},i.prototype.onHeartbeat=function(e){clearTimeout(this.pingTimeoutTimer);var t=this;t.pingTimeoutTimer=setTimeout(function(){"closed"!=t.readyState&&t.onClose("ping timeout")},e||t.pingInterval+t.pingTimeout)},i.prototype.setPing=function(){var e=this;clearTimeout(e.pingIntervalTimer),e.pingIntervalTimer=setTimeout(function(){r("writing ping packet - expecting pong within %sms",e.pingTimeout),e.ping(),e.onHeartbeat(e.pingTimeout)},e.pingInterval)},i.prototype.ping=function(){this.sendPacket("ping")},i.prototype.onDrain=function(){for(var e=0;e<this.prevBufferLen;e++)this.callbackBuffer[e]&&this.callbackBuffer[e]();this.writeBuffer.splice(0,this.prevBufferLen),this.callbackBuffer.splice(0,this.prevBufferLen),this.prevBufferLen=0,0==this.writeBuffer.length?this.emit("drain"):this.flush()},i.prototype.flush=function(){"closed"!=this.readyState&&this.transport.writable&&!this.upgrading&&this.writeBuffer.length&&(r("flushing %d packets in socket",this.writeBuffer.length),this.transport.send(this.writeBuffer),this.prevBufferLen=this.writeBuffer.length,this.emit("flush"))},i.prototype.write=i.prototype.send=function(e,t){return this.sendPacket("message",e,t),this},i.prototype.sendPacket=function(e,t,n){var i={type:e,data:t};this.emit("packetCreate",i),this.writeBuffer.push(i),this.callbackBuffer.push(n),this.flush()},i.prototype.close=function(){return("opening"==this.readyState||"open"==this.readyState)&&(this.onClose("forced close"),r("socket closing - telling transport to close"),this.transport.close()),this},i.prototype.onError=function(e){r("socket error %j",e),i.priorWebsocketSuccess=!1,this.emit("error",e),this.onClose("transport error",e)},i.prototype.onClose=function(e,t){if("opening"==this.readyState||"open"==this.readyState){r('socket close with reason: "%s"',e);var n=this;clearTimeout(this.pingIntervalTimer),clearTimeout(this.pingTimeoutTimer),setTimeout(function(){n.writeBuffer=[],n.callbackBuffer=[],n.prevBufferLen=0},0),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),this.readyState="closed",this.id=null,this.emit("close",e,t)}},i.prototype.filterUpgrades=function(e){for(var t=[],n=0,i=e.length;i>n;n++)~c(this.transports,e[n])&&t.push(e[n]);return t}}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./transport":4,"./transports":5,"component-emitter":12,debug:14,"engine.io-parser":15,indexof:23,parsejson:24,parseqs:25,parseuri:26}],4:[function(e,t){function n(e){this.path=e.path,this.hostname=e.hostname,this.port=e.port,this.secure=e.secure,this.query=e.query,this.timestampParam=e.timestampParam,this.timestampRequests=e.timestampRequests,this.readyState="",this.agent=e.agent||!1,this.socket=e.socket,this.enablesXDR=e.enablesXDR}var i=e("engine.io-parser"),s=e("component-emitter");t.exports=n,s(n.prototype),n.timestamps=0,n.prototype.onError=function(e,t){var n=new Error(e);return n.type="TransportError",n.description=t,this.emit("error",n),this},n.prototype.open=function(){return("closed"==this.readyState||""==this.readyState)&&(this.readyState="opening",this.doOpen()),this},n.prototype.close=function(){return("opening"==this.readyState||"open"==this.readyState)&&(this.doClose(),this.onClose()),this},n.prototype.send=function(e){if("open"!=this.readyState)throw new Error("Transport not open");this.write(e)},n.prototype.onOpen=function(){this.readyState="open",this.writable=!0,this.emit("open")},n.prototype.onData=function(e){var t=i.decodePacket(e,this.socket.binaryType);this.onPacket(t)},n.prototype.onPacket=function(e){this.emit("packet",e)},n.prototype.onClose=function(){this.readyState="closed",this.emit("close")}},{"component-emitter":12,"engine.io-parser":15}],5:[function(e,t,n){(function(t){function i(e){var n,i=!1,r=!1,c=!1!==e.jsonp;if(t.location){var l="https:"==location.protocol,h=location.port;h||(h=l?443:80),i=e.hostname!=location.hostname||h!=e.port,r=e.secure!=l}if(e.xdomain=i,e.xscheme=r,n=new s(e),"open"in n&&!e.forceJSONP)return new a(e);if(!c)throw new Error("JSONP disabled");return new o(e)}var s=e("xmlhttprequest"),a=e("./polling-xhr"),o=e("./polling-jsonp"),r=e("./websocket");n.polling=i,n.websocket=r}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./polling-jsonp":6,"./polling-xhr":7,"./websocket":9,xmlhttprequest:10}],6:[function(e,t){(function(n){function i(){}function s(e){a.call(this,e),this.query=this.query||{},r||(n.___eio||(n.___eio=[]),r=n.___eio),this.index=r.length;var t=this;r.push(function(e){t.onData(e)}),this.query.j=this.index,n.document&&n.addEventListener&&n.addEventListener("beforeunload",function(){t.script&&(t.script.onerror=i)})}var a=e("./polling"),o=e("component-inherit");t.exports=s;var r,c=/\n/g,l=/\\n/g;o(s,a),s.prototype.supportsBinary=!1,s.prototype.doClose=function(){this.script&&(this.script.parentNode.removeChild(this.script),this.script=null),this.form&&(this.form.parentNode.removeChild(this.form),this.form=null),a.prototype.doClose.call(this)},s.prototype.doPoll=function(){var e=this,t=document.createElement("script");this.script&&(this.script.parentNode.removeChild(this.script),this.script=null),t.async=!0,t.src=this.uri(),t.onerror=function(t){e.onError("jsonp poll error",t)};var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n),this.script=t;var i="undefined"!=typeof navigator&&/gecko/i.test(navigator.userAgent);i&&setTimeout(function(){var e=document.createElement("iframe");document.body.appendChild(e),document.body.removeChild(e)},100)},s.prototype.doWrite=function(e,t){function n(){i(),t()}function i(){if(s.iframe)try{s.form.removeChild(s.iframe)}catch(e){s.onError("jsonp polling iframe removal error",e)}try{var t='<iframe src="javascript:0" name="'+s.iframeId+'">';a=document.createElement(t)}catch(e){a=document.createElement("iframe"),a.name=s.iframeId,a.src="javascript:0"}a.id=s.iframeId,s.form.appendChild(a),s.iframe=a}var s=this;if(!this.form){var a,o=document.createElement("form"),r=document.createElement("textarea"),h=this.iframeId="eio_iframe_"+this.index;o.className="socketio",o.style.position="absolute",o.style.top="-1000px",o.style.left="-1000px",o.target=h,o.method="POST",o.setAttribute("accept-charset","utf-8"),r.name="d",o.appendChild(r),document.body.appendChild(o),this.form=o,this.area=r}this.form.action=this.uri(),i(),e=e.replace(l,"\\\n"),this.area.value=e.replace(c,"\\n");try{this.form.submit()}catch(p){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"==s.iframe.readyState&&n()}:this.iframe.onload=n}}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./polling":8,"component-inherit":13}],7:[function(e,t){(function(n){function i(){}function s(e){if(c.call(this,e),n.location){var t="https:"==location.protocol,i=location.port;i||(i=t?443:80),this.xd=e.hostname!=n.location.hostname||i!=e.port,this.xs=e.secure!=t}}function a(e){this.method=e.method||"GET",this.uri=e.uri,this.xd=!!e.xd,this.xs=!!e.xs,this.async=!1!==e.async,this.data=void 0!=e.data?e.data:null,this.agent=e.agent,this.isBinary=e.isBinary,this.supportsBinary=e.supportsBinary,this.enablesXDR=e.enablesXDR,this.create()}function o(){for(var e in a.requests)a.requests.hasOwnProperty(e)&&a.requests[e].abort()}var r=e("xmlhttprequest"),c=e("./polling"),l=e("component-emitter"),h=e("component-inherit"),p=e("debug")("engine.io-client:polling-xhr");t.exports=s,t.exports.Request=a,h(s,c),s.prototype.supportsBinary=!0,s.prototype.request=function(e){return e=e||{},e.uri=this.uri(),e.xd=this.xd,e.xs=this.xs,e.agent=this.agent||!1,e.supportsBinary=this.supportsBinary,e.enablesXDR=this.enablesXDR,new a(e)},s.prototype.doWrite=function(e,t){var n="string"!=typeof e&&void 0!==e,i=this.request({method:"POST",data:e,isBinary:n}),s=this;i.on("success",t),i.on("error",function(e){s.onError("xhr post error",e)}),this.sendXhr=i},s.prototype.doPoll=function(){p("xhr poll");var e=this.request(),t=this;e.on("data",function(e){t.onData(e)}),e.on("error",function(e){t.onError("xhr poll error",e)}),this.pollXhr=e},l(a.prototype),a.prototype.create=function(){var e=this.xhr=new r({agent:this.agent,xdomain:this.xd,xscheme:this.xs,enablesXDR:this.enablesXDR}),t=this;try{if(p("xhr open %s: %s",this.method,this.uri),e.open(this.method,this.uri,this.async),this.supportsBinary&&(e.responseType="arraybuffer"),"POST"==this.method)try{this.isBinary?e.setRequestHeader("Content-type","application/octet-stream"):e.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(i){}"withCredentials"in e&&(e.withCredentials=!0),this.hasXDR()?(e.onload=function(){t.onLoad()},e.onerror=function(){t.onError(e.responseText)}):e.onreadystatechange=function(){4==e.readyState&&(200==e.status||1223==e.status?t.onLoad():setTimeout(function(){t.onError(e.status)},0))},p("xhr data %s",this.data),e.send(this.data)}catch(i){return void setTimeout(function(){t.onError(i)},0)}n.document&&(this.index=a.requestsCount++,a.requests[this.index]=this)},a.prototype.onSuccess=function(){this.emit("success"),this.cleanup()},a.prototype.onData=function(e){this.emit("data",e),this.onSuccess()},a.prototype.onError=function(e){this.emit("error",e),this.cleanup()},a.prototype.cleanup=function(){if("undefined"!=typeof this.xhr&&null!==this.xhr){this.hasXDR()?this.xhr.onload=this.xhr.onerror=i:this.xhr.onreadystatechange=i;try{this.xhr.abort()}catch(e){}n.document&&delete a.requests[this.index],this.xhr=null}},a.prototype.onLoad=function(){var e;try{var t;try{t=this.xhr.getResponseHeader("Content-Type")}catch(n){}e="application/octet-stream"===t?this.xhr.response:this.supportsBinary?"ok":this.xhr.responseText}catch(n){this.onError(n)}null!=e&&this.onData(e)},a.prototype.hasXDR=function(){return"undefined"!=typeof n.XDomainRequest&&!this.xs&&this.enablesXDR},a.prototype.abort=function(){this.cleanup()},n.document&&(a.requestsCount=0,a.requests={},n.attachEvent?n.attachEvent("onunload",o):n.addEventListener&&n.addEventListener("beforeunload",o))}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./polling":8,"component-emitter":12,"component-inherit":13,debug:14,xmlhttprequest:10}],8:[function(e,t){function n(e){var t=e&&e.forceBase64;(!c||t)&&(this.supportsBinary=!1),i.call(this,e)}var i=e("../transport"),s=e("parseqs"),a=e("engine.io-parser"),o=e("component-inherit"),r=e("debug")("engine.io-client:polling");t.exports=n;var c=function(){var t=e("xmlhttprequest"),n=new t({agent:this.agent,xdomain:!1});return null!=n.responseType}();o(n,i),n.prototype.name="polling",n.prototype.doOpen=function(){this.poll()},n.prototype.pause=function(e){function t(){r("paused"),n.readyState="paused",e()}var n=this;if(this.readyState="pausing",this.polling||!this.writable){var i=0;this.polling&&(r("we are currently polling - waiting to pause"),i++,this.once("pollComplete",function(){r("pre-pause polling complete"),--i||t()})),this.writable||(r("we are currently writing - waiting to pause"),i++,this.once("drain",function(){r("pre-pause writing complete"),--i||t()}))}else t()},n.prototype.poll=function(){r("polling"),this.polling=!0,this.doPoll(),this.emit("poll")},n.prototype.onData=function(e){var t=this;r("polling got data %s",e);var n=function(e){return"opening"==t.readyState&&t.onOpen(),"close"==e.type?(t.onClose(),!1):void t.onPacket(e)};a.decodePayload(e,this.socket.binaryType,n),"closed"!=this.readyState&&(this.polling=!1,this.emit("pollComplete"),"open"==this.readyState?this.poll():r('ignoring poll - transport state "%s"',this.readyState))},n.prototype.doClose=function(){function e(){r("writing close packet"),t.write([{type:"close"}])}var t=this;"open"==this.readyState?(r("transport open - closing"),e()):(r("transport not open - deferring close"),this.once("open",e))},n.prototype.write=function(e){var t=this;this.writable=!1;var n=function(){t.writable=!0,t.emit("drain")},t=this;a.encodePayload(e,this.supportsBinary,function(e){t.doWrite(e,n)})},n.prototype.uri=function(){var e=this.query||{},t=this.secure?"https":"http",n="";return!1!==this.timestampRequests&&(e[this.timestampParam]=+new Date+"-"+i.timestamps++),this.supportsBinary||e.sid||(e.b64=1),e=s.encode(e),this.port&&("https"==t&&443!=this.port||"http"==t&&80!=this.port)&&(n=":"+this.port),e.length&&(e="?"+e),t+"://"+this.hostname+n+this.path+e}},{"../transport":4,"component-inherit":13,debug:14,"engine.io-parser":15,parseqs:25,xmlhttprequest:10}],9:[function(e,t){function n(e){var t=e&&e.forceBase64;t&&(this.supportsBinary=!1),i.call(this,e)}var i=e("../transport"),s=e("engine.io-parser"),a=e("parseqs"),o=e("component-inherit"),r=e("debug")("engine.io-client:websocket"),c=e("ws");t.exports=n,o(n,i),n.prototype.name="websocket",n.prototype.supportsBinary=!0,n.prototype.doOpen=function(){if(this.check()){var e=this.uri(),t=void 0,n={agent:this.agent};this.ws=new c(e,t,n),void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.binaryType="arraybuffer",this.addEventListeners()}},n.prototype.addEventListeners=function(){var e=this;this.ws.onopen=function(){e.onOpen()},this.ws.onclose=function(){e.onClose()},this.ws.onmessage=function(t){e.onData(t.data)},this.ws.onerror=function(t){e.onError("websocket error",t)}},"undefined"!=typeof navigator&&/iPad|iPhone|iPod/i.test(navigator.userAgent)&&(n.prototype.onData=function(e){var t=this;setTimeout(function(){i.prototype.onData.call(t,e)},0)}),n.prototype.write=function(e){function t(){n.writable=!0,n.emit("drain")}var n=this;this.writable=!1;for(var i=0,a=e.length;a>i;i++)s.encodePacket(e[i],this.supportsBinary,function(e){try{n.ws.send(e)}catch(t){r("websocket closed before onclose event")}});setTimeout(t,0)},n.prototype.onClose=function(){i.prototype.onClose.call(this)},n.prototype.doClose=function(){"undefined"!=typeof this.ws&&this.ws.close()},n.prototype.uri=function(){var e=this.query||{},t=this.secure?"wss":"ws",n="";return this.port&&("wss"==t&&443!=this.port||"ws"==t&&80!=this.port)&&(n=":"+this.port),this.timestampRequests&&(e[this.timestampParam]=+new Date),this.supportsBinary||(e.b64=1),e=a.encode(e),e.length&&(e="?"+e),t+"://"+this.hostname+n+this.path+e},n.prototype.check=function(){return!(!c||"__initialize"in c&&this.name===n.prototype.name)}},{"../transport":4,"component-inherit":13,debug:14,"engine.io-parser":15,parseqs:25,ws:27}],10:[function(e,t){var n=e("has-cors");t.exports=function(e){var t=e.xdomain,i=e.xscheme,s=e.enablesXDR;try{if("undefined"!=typeof XDomainRequest&&!i&&s)return new XDomainRequest}catch(a){}try{if("undefined"!=typeof XMLHttpRequest&&(!t||n))return new XMLHttpRequest}catch(a){}if(!t)try{return new ActiveXObject("Microsoft.XMLHTTP")}catch(a){}}},{"has-cors":21}],11:[function(e,t){(function(e){function n(e,t){t=t||{};for(var n=new i,s=0;s<e.length;s++)n.append(e[s]);return t.type?n.getBlob(t.type):n.getBlob()}var i=e.BlobBuilder||e.WebKitBlobBuilder||e.MSBlobBuilder||e.MozBlobBuilder,s=function(){try{var e=new Blob(["hi"]);return 2==e.size}catch(t){return!1}}(),a=i&&i.prototype.append&&i.prototype.getBlob;t.exports=function(){return s?e.Blob:a?n:void 0}()}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],12:[function(e,t){function n(e){return e?i(e):void 0}function i(e){for(var t in n.prototype)e[t]=n.prototype[t];return e}t.exports=n,n.prototype.on=n.prototype.addEventListener=function(e,t){return this._callbacks=this._callbacks||{},(this._callbacks[e]=this._callbacks[e]||[]).push(t),this},n.prototype.once=function(e,t){function n(){i.off(e,n),t.apply(this,arguments)}var i=this;return this._callbacks=this._callbacks||{},n.fn=t,this.on(e,n),this},n.prototype.off=n.prototype.removeListener=n.prototype.removeAllListeners=n.prototype.removeEventListener=function(e,t){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks[e];if(!n)return this;if(1==arguments.length)return delete this._callbacks[e],this;for(var i,s=0;s<n.length;s++)if(i=n[s],i===t||i.fn===t){n.splice(s,1);break}return this},n.prototype.emit=function(e){this._callbacks=this._callbacks||{};var t=[].slice.call(arguments,1),n=this._callbacks[e];if(n){n=n.slice(0);for(var i=0,s=n.length;s>i;++i)n[i].apply(this,t)}return this},n.prototype.listeners=function(e){return this._callbacks=this._callbacks||{},this._callbacks[e]||[]},n.prototype.hasListeners=function(e){return!!this.listeners(e).length}},{}],13:[function(e,t){t.exports=function(e,t){var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},{}],14:[function(e,t){function n(e){return n.enabled(e)?function(t){t=i(t);var s=new Date,a=s-(n[e]||s);n[e]=s,t=e+" "+t+" +"+n.humanize(a),window.console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}:function(){}}function i(e){return e instanceof Error?e.stack||e.message:e}t.exports=n,n.names=[],n.skips=[],n.enable=function(e){try{localStorage.debug=e}catch(t){}for(var i=(e||"").split(/[\s,]+/),s=i.length,a=0;s>a;a++)e=i[a].replace("*",".*?"),"-"===e[0]?n.skips.push(new RegExp("^"+e.substr(1)+"$")):n.names.push(new RegExp("^"+e+"$"))},n.disable=function(){n.enable("")},n.humanize=function(e){var t=1e3,n=6e4,i=60*n;return e>=i?(e/i).toFixed(1)+"h":e>=n?(e/n).toFixed(1)+"m":e>=t?(e/t|0)+"s":e+"ms"},n.enabled=function(e){for(var t=0,i=n.skips.length;i>t;t++)if(n.skips[t].test(e))return!1;for(var t=0,i=n.names.length;i>t;t++)if(n.names[t].test(e))return!0;return!1};try{window.localStorage&&n.enable(localStorage.debug)}catch(s){}},{}],15:[function(e,t,n){(function(t){function i(e,t,i){if(!t)return n.encodeBase64Packet(e,i);var s=e.data,a=new Uint8Array(s),o=new Uint8Array(1+s.byteLength);o[0]=u[e.type];for(var r=0;r<a.length;r++)o[r+1]=a[r];return i(o.buffer)}function s(e,t,i){if(!t)return n.encodeBase64Packet(e,i);var s=new FileReader;return s.onload=function(){e.data=s.result,n.encodePacket(e,t,!0,i)},s.readAsArrayBuffer(e.data)}function a(e,t,i){if(!t)return n.encodeBase64Packet(e,i);if(d)return s(e,t,i);var a=new Uint8Array(1);a[0]=u[e.type];var o=new g([a.buffer,e.data]);return i(o)}function o(e,t,n){for(var i=new Array(e.length),s=h(e.length,n),a=function(e,n,s){t(n,function(t,n){i[e]=n,s(t,i)})},o=0;o<e.length;o++)a(o,e[o],s)}var r=e("./keys"),c=e("arraybuffer.slice"),l=e("base64-arraybuffer"),h=e("after"),p=e("utf8"),d=navigator.userAgent.match(/Android/i);n.protocol=3;var u=n.packets={open:0,close:1,ping:2,pong:3,message:4,upgrade:5,noop:6},f=r(u),m={type:"error",data:"parser error"},g=e("blob");n.encodePacket=function(e,n,s,o){"function"==typeof n&&(o=n,n=!1),"function"==typeof s&&(o=s,s=null);var r=void 0===e.data?void 0:e.data.buffer||e.data;if(t.ArrayBuffer&&r instanceof ArrayBuffer)return i(e,n,o);if(g&&r instanceof t.Blob)return a(e,n,o);var c=u[e.type];return void 0!==e.data&&(c+=s?p.encode(String(e.data)):String(e.data)),o(""+c)},n.encodeBase64Packet=function(e,i){var s="b"+n.packets[e.type];if(g&&e.data instanceof g){var a=new FileReader;return a.onload=function(){var e=a.result.split(",")[1];i(s+e)},a.readAsDataURL(e.data)}var o;try{o=String.fromCharCode.apply(null,new Uint8Array(e.data))}catch(r){for(var c=new Uint8Array(e.data),l=new Array(c.length),h=0;h<c.length;h++)l[h]=c[h];o=String.fromCharCode.apply(null,l)}return s+=t.btoa(o),i(s)},n.decodePacket=function(e,t,i){if("string"==typeof e||void 0===e){if("b"==e.charAt(0))return n.decodeBase64Packet(e.substr(1),t);if(i)try{e=p.decode(e)}catch(s){return m}var a=e.charAt(0);return Number(a)==a&&f[a]?e.length>1?{type:f[a],data:e.substring(1)}:{type:f[a]}:m}var o=new Uint8Array(e),a=o[0],r=c(e,1);return g&&"blob"===t&&(r=new g([r])),{type:f[a],data:r}},n.decodeBase64Packet=function(e,n){var i=f[e.charAt(0)];if(!t.ArrayBuffer)return{type:i,data:{base64:!0,data:e.substr(1)}};var s=l.decode(e.substr(1));return"blob"===n&&g&&(s=new g([s])),{type:i,data:s}},n.encodePayload=function(e,t,i){function s(e){return e.length+":"+e}function a(e,i){n.encodePacket(e,t,!0,function(e){i(null,s(e))})}return"function"==typeof t&&(i=t,t=null),t?g&&!d?n.encodePayloadAsBlob(e,i):n.encodePayloadAsArrayBuffer(e,i):e.length?void o(e,a,function(e,t){return i(t.join(""))}):i("0:")},n.decodePayload=function(e,t,i){if("string"!=typeof e)return n.decodePayloadAsBinary(e,t,i);"function"==typeof t&&(i=t,t=null);var s;if(""==e)return i(m,0,1);for(var a,o,r="",c=0,l=e.length;l>c;c++){var h=e.charAt(c);if(":"!=h)r+=h;else{if(""==r||r!=(a=Number(r)))return i(m,0,1);if(o=e.substr(c+1,a),r!=o.length)return i(m,0,1);if(o.length){if(s=n.decodePacket(o,t,!0),m.type==s.type&&m.data==s.data)return i(m,0,1);var p=i(s,c+a,l);if(!1===p)return}c+=a,r=""}}return""!=r?i(m,0,1):void 0},n.encodePayloadAsArrayBuffer=function(e,t){function i(e,t){n.encodePacket(e,!0,!0,function(e){return t(null,e)})}return e.length?void o(e,i,function(e,n){var i=n.reduce(function(e,t){var n;return n="string"==typeof t?t.length:t.byteLength,e+n.toString().length+n+2},0),s=new Uint8Array(i),a=0;return n.forEach(function(e){var t="string"==typeof e,n=e;if(t){for(var i=new Uint8Array(e.length),o=0;o<e.length;o++)i[o]=e.charCodeAt(o);n=i.buffer}s[a++]=t?0:1;for(var r=n.byteLength.toString(),o=0;o<r.length;o++)s[a++]=parseInt(r[o]);s[a++]=255;for(var i=new Uint8Array(n),o=0;o<i.length;o++)s[a++]=i[o]}),t(s.buffer)}):t(new ArrayBuffer(0))},n.encodePayloadAsBlob=function(e,t){function i(e,t){n.encodePacket(e,!0,!0,function(e){var n=new Uint8Array(1);if(n[0]=1,"string"==typeof e){for(var i=new Uint8Array(e.length),s=0;s<e.length;s++)i[s]=e.charCodeAt(s);e=i.buffer,n[0]=0}for(var a=e instanceof ArrayBuffer?e.byteLength:e.size,o=a.toString(),r=new Uint8Array(o.length+1),s=0;s<o.length;s++)r[s]=parseInt(o[s]);if(r[o.length]=255,g){var c=new g([n.buffer,r.buffer,e]);t(null,c)}})}o(e,i,function(e,n){return t(new g(n))})},n.decodePayloadAsBinary=function(e,t,i){"function"==typeof t&&(i=t,t=null);for(var s=e,a=[],o=!1;s.byteLength>0;){for(var r=new Uint8Array(s),l=0===r[0],h="",p=1;255!=r[p];p++){if(h.length>310){o=!0;break}h+=r[p]}if(o)return i(m,0,1);s=c(s,2+h.length),h=parseInt(h);var d=c(s,0,h);if(l)try{d=String.fromCharCode.apply(null,new Uint8Array(d))}catch(u){var f=new Uint8Array(d);d="";for(var p=0;p<f.length;p++)d+=String.fromCharCode(f[p])}a.push(d),s=c(s,h)}var g=a.length;a.forEach(function(e,s){i(n.decodePacket(e,t,!0),s,g)})}}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./keys":16,after:17,"arraybuffer.slice":18,"base64-arraybuffer":19,blob:11,utf8:20}],16:[function(e,t){t.exports=Object.keys||function(e){var t=[],n=Object.prototype.hasOwnProperty;for(var i in e)n.call(e,i)&&t.push(i);return t}},{}],17:[function(e,t){function n(e,t,n){function s(e,i){if(s.count<=0)throw new Error("after called too many times");--s.count,e?(a=!0,t(e),t=n):0!==s.count||a||t(null,i)}var a=!1;return n=n||i,s.count=e,0===e?t():s}function i(){}t.exports=n},{}],18:[function(e,t){t.exports=function(e,t,n){var i=e.byteLength;if(t=t||0,n=n||i,e.slice)return e.slice(t,n);if(0>t&&(t+=i),0>n&&(n+=i),n>i&&(n=i),t>=i||t>=n||0===i)return new ArrayBuffer(0);for(var s=new Uint8Array(e),a=new Uint8Array(n-t),o=t,r=0;n>o;o++,r++)a[r]=s[o];return a.buffer}},{}],19:[function(e,t,n){!function(e){"use strict";n.encode=function(t){var n,i=new Uint8Array(t),s=i.length,a="";for(n=0;s>n;n+=3)a+=e[i[n]>>2],a+=e[(3&i[n])<<4|i[n+1]>>4],a+=e[(15&i[n+1])<<2|i[n+2]>>6],a+=e[63&i[n+2]];return s%3===2?a=a.substring(0,a.length-1)+"=":s%3===1&&(a=a.substring(0,a.length-2)+"=="),a},n.decode=function(t){var n,i,s,a,o,r=.75*t.length,c=t.length,l=0;"="===t[t.length-1]&&(r--,"="===t[t.length-2]&&r--);var h=new ArrayBuffer(r),p=new Uint8Array(h);for(n=0;c>n;n+=4)i=e.indexOf(t[n]),s=e.indexOf(t[n+1]),a=e.indexOf(t[n+2]),o=e.indexOf(t[n+3]),p[l++]=i<<2|s>>4,p[l++]=(15&s)<<4|a>>2,p[l++]=(3&a)<<6|63&o;return h}}("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")},{}],20:[function(t,n,i){(function(t){!function(s){function a(e){for(var t,n,i=[],s=0,a=e.length;a>s;)t=e.charCodeAt(s++),t>=55296&&56319>=t&&a>s?(n=e.charCodeAt(s++),56320==(64512&n)?i.push(((1023&t)<<10)+(1023&n)+65536):(i.push(t),s--)):i.push(t);return i}function o(e){for(var t,n=e.length,i=-1,s="";++i<n;)t=e[i],t>65535&&(t-=65536,s+=w(t>>>10&1023|55296),t=56320|1023&t),s+=w(t);return s}function r(e,t){return w(e>>t&63|128)}function c(e){if(0==(4294967168&e))return w(e);var t="";return 0==(4294965248&e)?t=w(e>>6&31|192):0==(4294901760&e)?(t=w(e>>12&15|224),t+=r(e,6)):0==(4292870144&e)&&(t=w(e>>18&7|240),t+=r(e,12),t+=r(e,6)),t+=w(63&e|128)}function l(e){for(var t,n=a(e),i=n.length,s=-1,o="";++s<i;)t=n[s],o+=c(t);return o}function h(){if(v>=_)throw Error("Invalid byte index");var e=255&g[v];if(v++,128==(192&e))return 63&e;throw Error("Invalid continuation byte")}function p(){var e,t,n,i,s;if(v>_)throw Error("Invalid byte index");if(v==_)return!1;if(e=255&g[v],v++,0==(128&e))return e;if(192==(224&e)){var t=h();if(s=(31&e)<<6|t,s>=128)return s;throw Error("Invalid continuation byte")}if(224==(240&e)){if(t=h(),n=h(),s=(15&e)<<12|t<<6|n,s>=2048)return s;throw Error("Invalid continuation byte")}if(240==(248&e)&&(t=h(),n=h(),i=h(),s=(15&e)<<18|t<<12|n<<6|i,s>=65536&&1114111>=s))return s;throw Error("Invalid UTF-8 detected")}function d(e){g=a(e),_=g.length,v=0;for(var t,n=[];(t=p())!==!1;)n.push(t);return o(n)}var u="object"==typeof i&&i,f="object"==typeof n&&n&&n.exports==u&&n,m="object"==typeof t&&t;(m.global===m||m.window===m)&&(s=m);var g,_,v,w=String.fromCharCode,b={version:"2.0.0",encode:l,decode:d};
+if("function"==typeof e&&"object"==typeof e.amd&&e.amd)e(function(){return b});else if(u&&!u.nodeType)if(f)f.exports=b;else{var k={},y=k.hasOwnProperty;for(var $ in b)y.call(b,$)&&(u[$]=b[$])}else s.utf8=b}(this)}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],21:[function(e,t){var n=e("global");try{t.exports="XMLHttpRequest"in n&&"withCredentials"in new n.XMLHttpRequest}catch(i){t.exports=!1}},{global:22}],22:[function(e,t){t.exports=function(){return this}()},{}],23:[function(e,t){var n=[].indexOf;t.exports=function(e,t){if(n)return e.indexOf(t);for(var i=0;i<e.length;++i)if(e[i]===t)return i;return-1}},{}],24:[function(e,t){(function(e){var n=/^[\],:{}\s]*$/,i=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,s=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,a=/(?:^|:|,)(?:\s*\[)+/g,o=/^\s+/,r=/\s+$/;t.exports=function(t){return"string"==typeof t&&t?(t=t.replace(o,"").replace(r,""),e.JSON&&JSON.parse?JSON.parse(t):n.test(t.replace(i,"@").replace(s,"]").replace(a,""))?new Function("return "+t)():void 0):null}}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],25:[function(e,t,n){n.encode=function(e){var t="";for(var n in e)e.hasOwnProperty(n)&&(t.length&&(t+="&"),t+=encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t},n.decode=function(e){for(var t={},n=e.split("&"),i=0,s=n.length;s>i;i++){var a=n[i].split("=");t[decodeURIComponent(a[0])]=decodeURIComponent(a[1])}return t}},{}],26:[function(e,t){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,i=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(e){var t=e,s=e.indexOf("["),a=e.indexOf("]");-1!=s&&-1!=a&&(e=e.substring(0,s)+e.substring(s,a).replace(/:/g,";")+e.substring(a,e.length));for(var o=n.exec(e||""),r={},c=14;c--;)r[i[c]]=o[c]||"";return-1!=s&&-1!=a&&(r.source=t,r.host=r.host.substring(1,r.host.length-1).replace(/;/g,":"),r.authority=r.authority.replace("[","").replace("]","").replace(/;/g,":"),r.ipv6uri=!0),r}},{}],27:[function(e,t){function n(e,t){var n;return n=t?new s(e,t):new s(e)}var i=function(){return this}(),s=i.WebSocket||i.MozWebSocket;t.exports=s?n:null,s&&(n.prototype=s.prototype)},{}]},{},[1])(1)});var EngineioTools={ReconnectingSocket:function(e,t){function n(){c=!0,l=!1,g=!1,f=0,p=0,clearTimeout(m)}function i(){c=!1,g||l||o()}function s(){l&&o()}function a(){g=!0,r.call(_)}function o(){if(f>=u)return void _.emit("reconnecting_failed");var e=d?2*(p||h/2):h*f;l=!0,m=setTimeout(function(){_.open()},e),p=e,_.emit("reconnecting",{attempt:f+1,max_attempts:u,delay:e}),f++}var r,c=!1,l=!1,h=4e3,p=0,d=!0,u=5,f=0,m=null,g=!1,_=eio.apply(eio,arguments);return _.on("open",n),_.on("close",i),_.on("error",s),r=_.close,_.close=a,t&&("number"==typeof t.reconnect_delay&&(h=t.reconnect_delay),"number"==typeof t.reconnect_max_attempts&&(u=t.reconnect_max_attempts),"undefined"!=typeof t.reconnect_delay_exponential&&(d=!!t.reconnect_delay_exponential)),_},Rpc:function(){function e(e){var n=new t(e),i=function(){return i.makeCall.apply(i,arguments)};for(var s in n)i[s]=n[s];return i._mixinEmitter(),i._bindSocketListeners(),i._rpc=i,i}function t(e){this._next_id=0,this._rpc_callbacks={},this._socket=e,this._rpc=this,this._namespace="",this._namespaces=[]}return t.prototype._bindSocketListeners=function(){var e=this;this._onMessageProxy=function(){e._onMessage.apply(e,arguments)},this._socket.on("message",this._onMessageProxy)},t.prototype.dispose=function(){this._onMessageProxy&&(this._socket.removeListener("message",this._onMessageProxy),delete this._onMessageProxy);for(var e in this._namespaces)this._namespaces[e].dispose();this.removeAllListeners()},t.prototype.namespace=function(e){var t,n;return t=this._namespace?this._namespace+"."+e:e,n=new this._rpc.Namespace(this._rpc,t),this._rpc._namespaces.push(n),n},t.prototype._findRelevantNamespaces=function(e){var t=[];for(var n in this._namespaces)this._namespaces[n]._namespace===e&&t.push(this._namespaces[n]),0===this._namespaces[n]._namespace.indexOf(e+".")&&t.push(this._namespaces[n]);return t},t.prototype._mixinEmitter=function(e){var t=["on","once","off","removeListener","removeAllListeners","emit","listeners","hasListeners"];e=e||this;for(var n=0;n<t.length;n++)"function"==typeof this._socket[t[n]]&&(e[t[n]]=this._socket[t[n]])},t.prototype._isCall=function(e){return"undefined"!=typeof e.method&&"undefined"!=typeof e.params},t.prototype._isResponse=function(e){return"undefined"!=typeof e.id&&"undefined"!=typeof e.response},t.prototype.makeCall=function(e){var t,n,i;t=Array.prototype.slice.call(arguments,1,arguments.length),"function"==typeof t[t.length-1]&&(n=t[t.length-1],t=t.slice(0,t.length-1)),i={method:e,params:t},"function"==typeof n&&(i.id=this._next_id,this._next_id++,this._rpc_callbacks[i.id]=n),this.send(i)},t.prototype.send=function(e){this._socket&&this._socket.send(JSON.stringify(e))},t.prototype._onMessage=function(e){var t,n,i,s,a,o;try{if(t=JSON.parse(e),!t)throw"Corrupt packet"}catch(r){return}if(this._isResponse(t)){if("function"!=typeof this._rpc_callbacks[t.id])return;i=this._rpc_callbacks[t.id],delete this._rpc_callbacks[t.id],i.apply(this,t.response)}else if(this._isCall(t)&&(n="undefined"!=typeof t.id?this._createReturnCallFn(t.id):this._noop,this.emit.apply(this,["all",t.method,n].concat(t.params)),this.emit.apply(this,[t.method,n].concat(t.params)),t.method.indexOf(".")>0)){s=t.method.substring(0,t.method.lastIndexOf(".")),a=this._findRelevantNamespaces(s);for(o in a)t.method=t.method.replace(a[o]._namespace+".",""),a[o].emit.apply(a[o],[t.method,n].concat(t.params))}},t.prototype._createReturnCallFn=function(e){var t=this;return function(){var n=Array.prototype.slice.call(arguments,0),i={id:e,response:n};t.send(i)}},t.prototype._noop=function(){},t.prototype.Namespace=function(e,t){var n=function(){return"undefined"!=typeof arguments[0]?(arguments[0]=n._namespace+"."+arguments[0],n._rpc.apply(n._rpc,arguments)):void 0};return n._rpc=e,n._namespace=t,n.dispose=function(){n.removeAllListeners(),n._rpc=null},e._mixinEmitter(n),n},e}()};
\ No newline at end of file
diff --git a/2016/assets/js/kiwi.js b/2016/assets/js/kiwi.js
new file mode 100644 (file)
index 0000000..26e026a
--- /dev/null
@@ -0,0 +1,8577 @@
+        /*    
+        @licstart  The following is the entire license notice for the 
+        JavaScript code in this page.
+
+        Copyright (C) 2014  Loic J. Duros
+
+        The JavaScript code in this page is free software: you can
+        redistribute it and/or modify it under the terms of the GNU
+        General Public License (GNU GPL) as published by the Free Software
+        Foundation, either version 3 of the License, or (at your option)
+        any later version.  The code is distributed WITHOUT ANY WARRANTY;
+        without even the implied warranty of MERCHANTABILITY or FITNESS
+        FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
+
+        As additional permission under GNU GPL version 3 section 7, you
+        may distribute non-source (e.g., minimized or compacted) forms of
+        that code without the copy of the GNU GPL normally required by
+        section 4, provided you include this license notice and a URL
+        through which recipients can access the Corresponding Source.   
+
+
+        @licend  The above is the entire license notice
+        for the JavaScript code in this page.
+        */
+
+
+(function (global, undefined) {
+
+// Holds anything kiwi client specific (ie. front, gateway, _kiwi.plugs..)\r
+/**\r
+*   @namespace\r
+*/\r
+var _kiwi = {};\r
+\r
+_kiwi.misc = {};\r
+_kiwi.model = {};\r
+_kiwi.view = {};\r
+_kiwi.applets = {};\r
+_kiwi.utils = {};\r
+\r
+\r
+/**\r
+ * A global container for third party access\r
+ * Will be used to access a limited subset of kiwi functionality\r
+ * and data (think: plugins)\r
+ */\r
+_kiwi.global = {\r
+    build_version: '',  // Kiwi IRC version this is built from (Set from index.html)\r
+    settings: undefined, // Instance of _kiwi.model.DataStore\r
+    plugins: undefined, // Instance of _kiwi.model.PluginManager\r
+    events: undefined, // Instance of PluginInterface\r
+    rpc: undefined, // Instance of WebsocketRpc\r
+    utils: {}, // References to misc. re-usable helpers / functions\r
+\r
+    // Make public some internal utils for plugins to make use of\r
+    initUtils: function() {\r
+        this.utils.randomString = randomString;\r
+        this.utils.secondsToTime = secondsToTime;\r
+        this.utils.parseISO8601 = parseISO8601;\r
+        this.utils.escapeRegex = escapeRegex;\r
+        this.utils.formatIRCMsg = formatIRCMsg;\r
+        this.utils.styleText = styleText;\r
+        this.utils.hsl2rgb = hsl2rgb;\r
+\r
+        this.utils.notifications = _kiwi.utils.notifications;\r
+        this.utils.formatDate = _kiwi.utils.formatDate;\r
+    },\r
+\r
+    addMediaMessageType: function(match, buildHtml) {\r
+        _kiwi.view.MediaMessage.addType(match, buildHtml);\r
+    },\r
+\r
+    // Event managers for plugins\r
+    components: {\r
+        EventComponent: function(event_source, proxy_event_name) {\r
+            /*\r
+             * proxyEvent() listens for events then re-triggers them on its own\r
+             * event emitter. Why? So we can .off() on this emitter without\r
+             * effecting the source of events. Handy for plugins that we don't\r
+             * trust meddling with the core events.\r
+             *\r
+             * If listening for 'all' events the arguments are as follows:\r
+             *     1. Name of the triggered event\r
+             *     2. The event data\r
+             * For all other events, we only have one argument:\r
+             *     1. The event data\r
+             *\r
+             * When this is used via `new kiwi.components.Network()`, this listens\r
+             * for 'all' events so the first argument is the event name which is\r
+             * the connection ID. We don't want to re-trigger this event name so\r
+             * we need to juggle the arguments to find the real event name we want\r
+             * to emit.\r
+             */\r
+            function proxyEvent(event_name, event_data) {\r
+                if (proxy_event_name == 'all') {\r
+                } else {\r
+                    event_data = event_name.event_data;\r
+                    event_name = event_name.event_name;\r
+                }\r
+\r
+                this.trigger(event_name, event_data);\r
+            }\r
+\r
+            // The event we are to proxy\r
+            proxy_event_name = proxy_event_name || 'all';\r
+\r
+            _.extend(this, Backbone.Events);\r
+            this._source = event_source;\r
+\r
+            // Proxy the events to this dispatcher\r
+            event_source.on(proxy_event_name, proxyEvent, this);\r
+\r
+            // Clean up this object\r
+            this.dispose = function () {\r
+                event_source.off(proxy_event_name, proxyEvent);\r
+                this.off();\r
+                delete this.event_source;\r
+            };\r
+        },\r
+\r
+        Network: function(connection_id) {\r
+            var connection_event;\r
+\r
+            // If no connection id given, use all connections\r
+            if (typeof connection_id !== 'undefined') {\r
+                connection_event = 'connection:' + connection_id.toString();\r
+            } else {\r
+                connection_event = 'connection';\r
+            }\r
+\r
+            // Helper to get the network object\r
+            var getNetwork = function() {\r
+                var network = typeof connection_id === 'undefined' ?\r
+                    _kiwi.app.connections.active_connection :\r
+                    _kiwi.app.connections.getByConnectionId(connection_id);\r
+\r
+                return network ?\r
+                    network :\r
+                    undefined;\r
+            };\r
+\r
+            // Create the return object (events proxy from the gateway)\r
+            var obj = new this.EventComponent(_kiwi.gateway, connection_event);\r
+\r
+            // Proxy several gateway functions onto the return object\r
+            var funcs = {\r
+                kiwi: 'kiwi', raw: 'raw', kick: 'kick', topic: 'topic',\r
+                part: 'part', join: 'join', action: 'action', ctcp: 'ctcp',\r
+                ctcpRequest: 'ctcpRequest', ctcpResponse: 'ctcpResponse',\r
+                notice: 'notice', msg: 'privmsg', say: 'privmsg',\r
+                changeNick: 'changeNick', channelInfo: 'channelInfo',\r
+                mode: 'mode', quit: 'quit'\r
+            };\r
+\r
+            _.each(funcs, function(gateway_fn, func_name) {\r
+                obj[func_name] = function() {\r
+                    var fn_name = gateway_fn;\r
+\r
+                    // Add connection_id to the argument list\r
+                    var args = Array.prototype.slice.call(arguments, 0);\r
+                    args.unshift(connection_id);\r
+\r
+                    // Call the gateway function on behalf of this connection\r
+                    return _kiwi.gateway[fn_name].apply(_kiwi.gateway, args);\r
+                };\r
+            });\r
+\r
+            // Now for some network related functions...\r
+            obj.createQuery = function(nick) {\r
+                var network, restricted_keys;\r
+\r
+                network = getNetwork();\r
+                if (!network) {\r
+                    return;\r
+                }\r
+\r
+                return network.createQuery(nick);\r
+            };\r
+\r
+            // Add the networks getters/setters\r
+            obj.get = function(name) {\r
+                var network, restricted_keys;\r
+\r
+                network = getNetwork();\r
+                if (!network) {\r
+                    return;\r
+                }\r
+\r
+                restricted_keys = [\r
+                    'password'\r
+                ];\r
+                if (restricted_keys.indexOf(name) > -1) {\r
+                    return undefined;\r
+                }\r
+\r
+                return network.get(name);\r
+            };\r
+\r
+            obj.set = function() {\r
+                var network = getNetwork();\r
+                if (!network) {\r
+                    return;\r
+                }\r
+\r
+                return network.set.apply(network, arguments);\r
+            };\r
+\r
+            return obj;\r
+        },\r
+\r
+        ControlInput: function() {\r
+            var obj = new this.EventComponent(_kiwi.app.controlbox);\r
+            var funcs = {\r
+                run: 'processInput', addPluginIcon: 'addPluginIcon'\r
+            };\r
+\r
+            _.each(funcs, function(controlbox_fn, func_name) {\r
+                obj[func_name] = function() {\r
+                    var fn_name = controlbox_fn;\r
+                    return _kiwi.app.controlbox[fn_name].apply(_kiwi.app.controlbox, arguments);\r
+                };\r
+            });\r
+\r
+            // Give access to the control input textarea\r
+            obj.input = _kiwi.app.controlbox.$('.inp');\r
+\r
+            return obj;\r
+        }\r
+    },\r
+\r
+    // Entry point to start the kiwi application\r
+    init: function (opts, callback) {\r
+        var locale_promise, theme_promise,\r
+            that = this;\r
+\r
+        opts = opts || {};\r
+\r
+        this.initUtils();\r
+\r
+        // Set up the settings datastore\r
+        _kiwi.global.settings = _kiwi.model.DataStore.instance('kiwi.settings');\r
+        _kiwi.global.settings.load();\r
+\r
+        // Set the window title\r
+        window.document.title = opts.server_settings.client.window_title || 'Kiwi IRC';\r
+\r
+        locale_promise = new Promise(function (resolve) {\r
+            var locale = _kiwi.global.settings.get('locale') || 'magic';\r
+            $.getJSON(opts.base_path + '/assets/locales/' + locale + '.json', function (locale) {\r
+                if (locale) {\r
+                    that.i18n = new Jed(locale);\r
+                } else {\r
+                    that.i18n = new Jed();\r
+                }\r
+                resolve();\r
+            });\r
+        });\r
+\r
+        theme_promise = new Promise(function (resolve) {\r
+            var text_theme = opts.server_settings.client.settings.text_theme || 'default';\r
+            $.getJSON(opts.base_path + '/assets/text_themes/' + text_theme + '.json', function(text_theme) {\r
+                opts.text_theme = text_theme;\r
+                resolve();\r
+            });\r
+        });\r
+\r
+\r
+        Promise.all([locale_promise, theme_promise]).then(function () {\r
+            _kiwi.app = new _kiwi.model.Application(opts);\r
+\r
+            // Start the client up\r
+            _kiwi.app.initializeInterfaces();\r
+\r
+            // Event emitter to let plugins interface with parts of kiwi\r
+            _kiwi.global.events  = new PluginInterface();\r
+\r
+            // Now everything has started up, load the plugin manager for third party plugins\r
+            _kiwi.global.plugins = new _kiwi.model.PluginManager();\r
+\r
+            callback();\r
+\r
+        }).then(null, function(err) {\r
+            console.error(err.stack);\r
+        });\r
+    },\r
+\r
+    start: function() {\r
+        _kiwi.app.showStartup();\r
+    },\r
+\r
+    // Allow plugins to change the startup applet\r
+    registerStartupApplet: function(startup_applet_name) {\r
+        _kiwi.app.startup_applet_name = startup_applet_name;\r
+    },\r
+\r
+    /**\r
+     * Open a new IRC connection\r
+     * @param {Object} connection_details {nick, host, port, ssl, password, options}\r
+     * @param {Function} callback function(err, network){}\r
+     */\r
+    newIrcConnection: function(connection_details, callback) {\r
+        _kiwi.gateway.newConnection(connection_details, callback);\r
+    },\r
+\r
+\r
+    /**\r
+     * Taking settings from the server and URL, extract the default server/channel/nick settings\r
+     */\r
+    defaultServerSettings: function () {\r
+        var parts;\r
+        var defaults = {\r
+            nick: '',\r
+            server: '',\r
+            port: 6667,\r
+            ssl: false,\r
+            channel: '',\r
+            channel_key: ''\r
+        };\r
+        var uricheck;\r
+\r
+\r
+        /**\r
+         * Get any settings set by the server\r
+         * These settings may be changed in the server selection dialog or via URL parameters\r
+         */\r
+        if (_kiwi.app.server_settings.client) {\r
+            if (_kiwi.app.server_settings.client.nick)\r
+                defaults.nick = _kiwi.app.server_settings.client.nick;\r
+\r
+            if (_kiwi.app.server_settings.client.server)\r
+                defaults.server = _kiwi.app.server_settings.client.server;\r
+\r
+            if (_kiwi.app.server_settings.client.port)\r
+                defaults.port = _kiwi.app.server_settings.client.port;\r
+\r
+            if (_kiwi.app.server_settings.client.ssl)\r
+                defaults.ssl = _kiwi.app.server_settings.client.ssl;\r
+\r
+            if (_kiwi.app.server_settings.client.channel)\r
+                defaults.channel = _kiwi.app.server_settings.client.channel;\r
+\r
+            if (_kiwi.app.server_settings.client.channel_key)\r
+                defaults.channel_key = _kiwi.app.server_settings.client.channel_key;\r
+        }\r
+\r
+\r
+\r
+        /**\r
+         * Get any settings passed in the URL\r
+         * These settings may be changed in the server selection dialog\r
+         */\r
+\r
+        // Any query parameters first\r
+        if (getQueryVariable('nick'))\r
+            defaults.nick = getQueryVariable('nick');\r
+\r
+        if (window.location.hash)\r
+            defaults.channel = window.location.hash;\r
+\r
+\r
+        // Process the URL part by part, extracting as we go\r
+        parts = window.location.pathname.toString().replace(_kiwi.app.get('base_path'), '').split('/');\r
+\r
+        if (parts.length > 0) {\r
+            parts.shift();\r
+\r
+            if (parts.length > 0 && parts[0]) {\r
+                // Check to see if we're dealing with an irc: uri, or whether we need to extract the server/channel info from the HTTP URL path.\r
+                uricheck = parts[0].substr(0, 7).toLowerCase();\r
+                if ((uricheck === 'ircs%3a') || (uricheck.substr(0,6) === 'irc%3a')) {\r
+                    parts[0] = decodeURIComponent(parts[0]);\r
+                    // irc[s]://<host>[:<port>]/[<channel>[?<password>]]\r
+                    uricheck = /^irc(s)?:(?:\/\/?)?([^:\/]+)(?::([0-9]+))?(?:(?:\/)([^\?]*)(?:(?:\?)(.*))?)?$/.exec(parts[0]);\r
+                    /*\r
+                        uricheck[1] = ssl (optional)\r
+                        uricheck[2] = host\r
+                        uricheck[3] = port (optional)\r
+                        uricheck[4] = channel (optional)\r
+                        uricheck[5] = channel key (optional, channel must also be set)\r
+                    */\r
+                    if (uricheck) {\r
+                        if (typeof uricheck[1] !== 'undefined') {\r
+                            defaults.ssl = true;\r
+                            if (defaults.port === 6667) {\r
+                                defaults.port = 6697;\r
+                            }\r
+                        }\r
+                        defaults.server = uricheck[2];\r
+                        if (typeof uricheck[3] !== 'undefined') {\r
+                            defaults.port = uricheck[3];\r
+                        }\r
+                        if (typeof uricheck[4] !== 'undefined') {\r
+                            defaults.channel = '#' + uricheck[4];\r
+                            if (typeof uricheck[5] !== 'undefined') {\r
+                                defaults.channel_key = uricheck[5];\r
+                            }\r
+                        }\r
+                    }\r
+                    parts = [];\r
+                } else {\r
+                    // Extract the port+ssl if we find one\r
+                    if (parts[0].search(/:/) > 0) {\r
+                        defaults.port = parts[0].substring(parts[0].search(/:/) + 1);\r
+                        defaults.server = parts[0].substring(0, parts[0].search(/:/));\r
+                        if (defaults.port[0] === '+') {\r
+                            defaults.port = parseInt(defaults.port.substring(1), 10);\r
+                            defaults.ssl = true;\r
+                        } else {\r
+                            defaults.ssl = false;\r
+                        }\r
+\r
+                    } else {\r
+                        defaults.server = parts[0];\r
+                    }\r
+\r
+                    parts.shift();\r
+                }\r
+            }\r
+\r
+            if (parts.length > 0 && parts[0]) {\r
+                defaults.channel = '#' + parts[0];\r
+                parts.shift();\r
+            }\r
+        }\r
+\r
+        // If any settings have been given by the server.. override any auto detected settings\r
+        /**\r
+         * Get any server restrictions as set in the server config\r
+         * These settings can not be changed in the server selection dialog\r
+         */\r
+        if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) {\r
+            if (_kiwi.app.server_settings.connection.server) {\r
+                defaults.server = _kiwi.app.server_settings.connection.server;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.port) {\r
+                defaults.port = _kiwi.app.server_settings.connection.port;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.ssl) {\r
+                defaults.ssl = _kiwi.app.server_settings.connection.ssl;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.channel) {\r
+                defaults.channel = _kiwi.app.server_settings.connection.channel;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.channel_key) {\r
+                defaults.channel_key = _kiwi.app.server_settings.connection.channel_key;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.nick) {\r
+                defaults.nick = _kiwi.app.server_settings.connection.nick;\r
+            }\r
+        }\r
+\r
+        // Set any random numbers if needed\r
+        defaults.nick = defaults.nick.replace('?', Math.floor(Math.random() * 100000).toString());\r
+\r
+        if (getQueryVariable('encoding'))\r
+            defaults.encoding = getQueryVariable('encoding');\r
+\r
+        return defaults;\r
+    },\r
+};\r
+\r
+\r
+\r
+// If within a closure, expose the kiwi globals\r
+if (typeof global !== 'undefined') {\r
+    global.kiwi = _kiwi.global;\r
+} else {\r
+    // Not within a closure so set a var in the current scope\r
+    var kiwi = _kiwi.global;\r
+}\r
+
+
+
+(function () {\r
+\r
+    _kiwi.model.Application = Backbone.Model.extend({\r
+        /** _kiwi.view.Application */\r
+        view: null,\r
+\r
+        /** _kiwi.view.StatusMessage */\r
+        message: null,\r
+\r
+        initialize: function (options) {\r
+            this.app_options = options;\r
+\r
+            if (options.container) {\r
+                this.set('container', options.container);\r
+            }\r
+\r
+            // The base url to the kiwi server\r
+            this.set('base_path', options.base_path ? options.base_path : '');\r
+\r
+            // Path for the settings.json file\r
+            this.set('settings_path', options.settings_path ?\r
+                    options.settings_path :\r
+                    this.get('base_path') + '/assets/settings.json'\r
+            );\r
+\r
+            // Any options sent down from the server\r
+            this.server_settings = options.server_settings || {};\r
+            this.translations = options.translations || {};\r
+            this.themes = options.themes || [];\r
+            this.text_theme = options.text_theme || {};\r
+\r
+            // The applet to initially load\r
+            this.startup_applet_name = options.startup || 'kiwi_startup';\r
+\r
+            // Set any default settings before anything else is applied\r
+            if (this.server_settings && this.server_settings.client && this.server_settings.client.settings) {\r
+                this.applyDefaultClientSettings(this.server_settings.client.settings);\r
+            }\r
+        },\r
+\r
+\r
+        initializeInterfaces: function () {\r
+            // Best guess at where the kiwi server is if not already specified\r
+            var kiwi_server = this.app_options.kiwi_server || this.detectKiwiServer();\r
+\r
+            // Set the gateway up\r
+            _kiwi.gateway = new _kiwi.model.Gateway({kiwi_server: kiwi_server});\r
+            this.bindGatewayCommands(_kiwi.gateway);\r
+\r
+            this.initializeClient();\r
+            this.initializeGlobals();\r
+\r
+            this.view.barsHide(true);\r
+        },\r
+\r
+\r
+        detectKiwiServer: function () {\r
+            // If running from file, default to localhost:7777 by default\r
+            if (window.location.protocol === 'file:') {\r
+                return 'http://localhost:7778';\r
+            } else {\r
+                // Assume the kiwi server is on the same server\r
+                return window.location.protocol + '//' + window.location.host;\r
+            }\r
+        },\r
+\r
+\r
+        showStartup: function() {\r
+            this.startup_applet = _kiwi.model.Applet.load(this.startup_applet_name, {no_tab: true});\r
+            this.startup_applet.tab = this.view.$('.console');\r
+            this.startup_applet.view.show();\r
+\r
+            _kiwi.global.events.emit('loaded');\r
+        },\r
+\r
+\r
+        initializeClient: function () {\r
+            this.view = new _kiwi.view.Application({model: this, el: this.get('container')});\r
+\r
+            // Takes instances of model_network\r
+            this.connections = new _kiwi.model.NetworkPanelList();\r
+\r
+            // If all connections are removed at some point, hide the bars\r
+            this.connections.on('remove', _.bind(function() {\r
+                if (this.connections.length === 0) {\r
+                    this.view.barsHide();\r
+                }\r
+            }, this));\r
+\r
+            // Applets panel list\r
+            this.applet_panels = new _kiwi.model.PanelList();\r
+            this.applet_panels.view.$el.addClass('panellist applets');\r
+            this.view.$el.find('.tabs').append(this.applet_panels.view.$el);\r
+\r
+            /**\r
+             * Set the UI components up\r
+             */\r
+            this.controlbox = (new _kiwi.view.ControlBox({el: $('#kiwi .controlbox')[0]})).render();\r
+            this.client_ui_commands = new _kiwi.misc.ClientUiCommands(this, this.controlbox);\r
+\r
+            this.rightbar = new _kiwi.view.RightBar({el: this.view.$('.right_bar')[0]});\r
+            this.topicbar = new _kiwi.view.TopicBar({el: this.view.$el.find('.topic')[0]});\r
+\r
+            new _kiwi.view.AppToolbar({el: _kiwi.app.view.$el.find('.toolbar .app_tools')[0]});\r
+            new _kiwi.view.ChannelTools({el: _kiwi.app.view.$el.find('.channel_tools')[0]});\r
+\r
+            this.message = new _kiwi.view.StatusMessage({el: this.view.$el.find('.status_message')[0]});\r
+\r
+            this.resize_handle = new _kiwi.view.ResizeHandler({el: this.view.$el.find('.memberlists_resize_handle')[0]});\r
+\r
+            // Rejigg the UI sizes\r
+            this.view.doLayout();\r
+        },\r
+\r
+\r
+        initializeGlobals: function () {\r
+            _kiwi.global.connections = this.connections;\r
+\r
+            _kiwi.global.panels = this.panels;\r
+            _kiwi.global.panels.applets = this.applet_panels;\r
+\r
+            _kiwi.global.components.Applet = _kiwi.model.Applet;\r
+            _kiwi.global.components.Panel =_kiwi.model.Panel;\r
+            _kiwi.global.components.MenuBox = _kiwi.view.MenuBox;\r
+            _kiwi.global.components.DataStore = _kiwi.model.DataStore;\r
+            _kiwi.global.components.Notification = _kiwi.view.Notification;\r
+            _kiwi.global.components.Events = function() {\r
+                return kiwi.events.createProxy();\r
+            };\r
+        },\r
+\r
+\r
+        applyDefaultClientSettings: function (settings) {\r
+            _.each(settings, function (value, setting) {\r
+                if (typeof _kiwi.global.settings.get(setting) === 'undefined') {\r
+                    _kiwi.global.settings.set(setting, value);\r
+                }\r
+            });\r
+        },\r
+\r
+\r
+        panels: (function() {\r
+            var active_panel;\r
+\r
+            var fn = function(panel_type) {\r
+                var app = _kiwi.app,\r
+                    panels;\r
+\r
+                // Default panel type\r
+                panel_type = panel_type || 'connections';\r
+\r
+                switch (panel_type) {\r
+                case 'connections':\r
+                    panels = app.connections.panels();\r
+                    break;\r
+                case 'applets':\r
+                    panels = app.applet_panels.models;\r
+                    break;\r
+                }\r
+\r
+                // Active panels / server\r
+                panels.active = active_panel;\r
+                panels.server = app.connections.active_connection ?\r
+                    app.connections.active_connection.panels.server :\r
+                    null;\r
+\r
+                return panels;\r
+            };\r
+\r
+            _.extend(fn, Backbone.Events);\r
+\r
+            // Keep track of the active panel. Channel/query/server or applet\r
+            fn.bind('active', function (new_active_panel) {\r
+                var previous_panel = active_panel;\r
+                active_panel = new_active_panel;\r
+\r
+                _kiwi.global.events.emit('panel:active', {previous: previous_panel, active: active_panel});\r
+            });\r
+\r
+            return fn;\r
+        })(),\r
+\r
+\r
+        bindGatewayCommands: function (gw) {\r
+            var that = this;\r
+\r
+            // As soon as an IRC connection is made, show the full client UI\r
+            gw.on('connection:connect', function (event) {\r
+                that.view.barsShow();\r
+            });\r
+\r
+\r
+            /**\r
+             * Handle the reconnections to the kiwi server\r
+             */\r
+            (function () {\r
+                // 0 = non-reconnecting state. 1 = reconnecting state.\r
+                var gw_stat = 0;\r
+\r
+                gw.on('disconnect', function (event) {\r
+                    that.view.$el.removeClass('connected');\r
+\r
+                    // Reconnection phase will start to kick in\r
+                    gw_stat = 1;\r
+                });\r
+\r
+\r
+                gw.on('reconnecting', function (event) {\r
+                    var msg = translateText('client_models_application_reconnect_in_x_seconds', [event.delay/1000]) + '...';\r
+\r
+                    // Only need to mention the repeating re-connection messages on server panels\r
+                    _kiwi.app.connections.forEach(function(connection) {\r
+                        connection.panels.server.addMsg('', styleText('quit', {text: msg}), 'action quit');\r
+                    });\r
+                });\r
+\r
+\r
+                // After the socket has connected, kiwi handshakes and then triggers a kiwi:connected event\r
+                gw.on('kiwi:connected', function (event) {\r
+                    var msg;\r
+\r
+                    that.view.$el.addClass('connected');\r
+\r
+                    // Make the rpc globally available for plugins\r
+                    _kiwi.global.rpc = _kiwi.gateway.rpc;\r
+\r
+                    _kiwi.global.events.emit('connected');\r
+\r
+                    // If we were reconnecting, show some messages we have connected back OK\r
+                    if (gw_stat === 1) {\r
+\r
+                        // No longer in the reconnection state\r
+                        gw_stat = 0;\r
+\r
+                        msg = translateText('client_models_application_reconnect_successfully') + ' :)';\r
+                        that.message.text(msg, {timeout: 5000});\r
+\r
+                        // Mention the re-connection on every channel\r
+                        _kiwi.app.connections.forEach(function(connection) {\r
+                            connection.reconnect();\r
+\r
+                            connection.panels.server.addMsg('', styleText('rejoin', {text: msg}), 'action join');\r
+\r
+                            connection.panels.forEach(function(panel) {\r
+                                if (!panel.isChannel())\r
+                                    return;\r
+\r
+                                panel.addMsg('', styleText('rejoin', {text: msg}), 'action join');\r
+                            });\r
+                        });\r
+                    }\r
+\r
+                });\r
+            })();\r
+\r
+\r
+            gw.on('kiwi:reconfig', function () {\r
+                $.getJSON(that.get('settings_path'), function (data) {\r
+                    that.server_settings = data.server_settings || {};\r
+                    that.translations = data.translations || {};\r
+                });\r
+            });\r
+\r
+\r
+            gw.on('kiwi:jumpserver', function (data) {\r
+                var serv;\r
+                // No server set? Then nowhere to jump to.\r
+                if (typeof data.kiwi_server === 'undefined')\r
+                    return;\r
+\r
+                serv = data.kiwi_server;\r
+\r
+                // Strip any trailing slash from the end\r
+                if (serv[serv.length-1] === '/')\r
+                    serv = serv.substring(0, serv.length-1);\r
+\r
+                // Force the jumpserver now?\r
+                if (data.force) {\r
+                    // Get an interval between 5 and 6 minutes so everyone doesn't reconnect it all at once\r
+                    var jump_server_interval = Math.random() * (360 - 300) + 300;\r
+                    jump_server_interval = 1;\r
+\r
+                    // Tell the user we are going to disconnect, wait 5 minutes then do the actual reconnect\r
+                    var msg = _kiwi.global.i18n.translate('client_models_application_jumpserver_prepare').fetch();\r
+                    that.message.text(msg, {timeout: 10000});\r
+\r
+                    setTimeout(function forcedReconnect() {\r
+                        var msg = _kiwi.global.i18n.translate('client_models_application_jumpserver_reconnect').fetch();\r
+                        that.message.text(msg, {timeout: 8000});\r
+\r
+                        setTimeout(function forcedReconnectPartTwo() {\r
+                            _kiwi.gateway.set('kiwi_server', serv);\r
+\r
+                            _kiwi.gateway.reconnect(function() {\r
+                                // Reconnect all the IRC connections\r
+                                that.connections.forEach(function(con){ con.reconnect(); });\r
+                            });\r
+                        }, 5000);\r
+\r
+                    }, jump_server_interval * 1000);\r
+                }\r
+            });\r
+        }\r
+\r
+    });\r
+\r
+})();\r
+
+
+
+_kiwi.model.Gateway = Backbone.Model.extend({\r
+\r
+    initialize: function () {\r
+\r
+        // For ease of access. The socket.io object\r
+        this.socket = this.get('socket');\r
+\r
+        // Used to check if a disconnection was unplanned\r
+        this.disconnect_requested = false;\r
+    },\r
+\r
+\r
+\r
+    reconnect: function (callback) {\r
+        this.disconnect_requested = true;\r
+        this.socket.close();\r
+\r
+        this.socket = null;\r
+        this.connect(callback);\r
+    },\r
+\r
+\r
+\r
+    /**\r
+    *   Connects to the server\r
+    *   @param  {Function}  callback    A callback function to be invoked once Kiwi's server has connected to the IRC server\r
+    */\r
+    connect: function (callback) {\r
+        var that = this;\r
+\r
+        this.connect_callback = callback;\r
+\r
+        this.socket = new EngineioTools.ReconnectingSocket(this.get('kiwi_server'), {\r
+            transports: _kiwi.app.server_settings.transports || ['polling', 'websocket'],\r
+            path: _kiwi.app.get('base_path') + '/transport',\r
+            reconnect_max_attempts: 5,\r
+            reconnect_delay: 2000\r
+        });\r
+\r
+        // If we have an existing RPC object, clean it up before replacing it\r
+        if (this.rpc) {\r
+            rpc.dispose();\r
+        }\r
+        this.rpc = new EngineioTools.Rpc(this.socket);\r
+\r
+        this.socket.on('connect_failed', function (reason) {\r
+            this.socket.disconnect();\r
+            this.trigger("connect_fail", {reason: reason});\r
+        });\r
+\r
+        this.socket.on('error', function (e) {\r
+            console.log("_kiwi.gateway.socket.on('error')", {reason: e});\r
+            if (that.connect_callback) {\r
+                that.connect_callback(e);\r
+                delete that.connect_callback;\r
+            }\r
+\r
+            that.trigger("connect_fail", {reason: e});\r
+        });\r
+\r
+        this.socket.on('connecting', function (transport_type) {\r
+            console.log("_kiwi.gateway.socket.on('connecting')");\r
+            that.trigger("connecting");\r
+        });\r
+\r
+        /**\r
+         * Once connected to the kiwi server send the IRC connect command along\r
+         * with the IRC server details.\r
+         * A `connect` event is sent from the kiwi server once connected to the\r
+         * IRCD and the nick has been accepted.\r
+         */\r
+        this.socket.on('open', function () {\r
+            // Reset the disconnect_requested flag\r
+            that.disconnect_requested = false;\r
+\r
+            // Each minute we need to trigger a heartbeat. Server expects 2min, but to be safe we do it every 1min\r
+            var heartbeat = function() {\r
+                if (!that.rpc) return;\r
+\r
+                that.rpc('kiwi.heartbeat');\r
+                that._heartbeat_tmr = setTimeout(heartbeat, 60000);\r
+            };\r
+\r
+            heartbeat();\r
+\r
+            console.log("_kiwi.gateway.socket.on('open')");\r
+        });\r
+\r
+        this.rpc.on('too_many_connections', function () {\r
+            that.trigger("connect_fail", {reason: 'too_many_connections'});\r
+        });\r
+\r
+        this.rpc.on('irc', function (response, data) {\r
+            that.parse(data.command, data.data);\r
+        });\r
+\r
+        this.rpc.on('kiwi', function (response, data) {\r
+            that.parseKiwi(data.command, data.data);\r
+        });\r
+\r
+        this.socket.on('close', function () {\r
+            that.trigger("disconnect", {});\r
+            console.log("_kiwi.gateway.socket.on('close')");\r
+        });\r
+\r
+        this.socket.on('reconnecting', function (status) {\r
+            console.log("_kiwi.gateway.socket.on('reconnecting')");\r
+            that.trigger("reconnecting", {delay: status.delay, attempts: status.attempts});\r
+        });\r
+\r
+        this.socket.on('reconnecting_failed', function () {\r
+            console.log("_kiwi.gateway.socket.on('reconnect_failed')");\r
+        });\r
+    },\r
+\r
+\r
+    /**\r
+     * Return a new network object with the new connection details\r
+     */\r
+    newConnection: function(connection_info, callback_fn) {\r
+        var that = this;\r
+\r
+        // If not connected, connect first then re-call this function\r
+        if (!this.isConnected()) {\r
+            this.connect(function(err) {\r
+                if (err) {\r
+                    callback_fn(err);\r
+                    return;\r
+                }\r
+\r
+                that.newConnection(connection_info, callback_fn);\r
+            });\r
+\r
+            return;\r
+        }\r
+\r
+        this.makeIrcConnection(connection_info, function(err, server_num) {\r
+            var connection;\r
+\r
+            if (!err) {\r
+                if (!_kiwi.app.connections.getByConnectionId(server_num)){\r
+                    var inf = {\r
+                        connection_id: server_num,\r
+                        nick: connection_info.nick,\r
+                        address: connection_info.host,\r
+                        port: connection_info.port,\r
+                        ssl: connection_info.ssl,\r
+                        password: connection_info.password\r
+                    };\r
+                    connection = new _kiwi.model.Network(inf);\r
+                    _kiwi.app.connections.add(connection);\r
+                }\r
+\r
+                console.log("_kiwi.gateway.socket.on('connect')", connection);\r
+                callback_fn && callback_fn(err, connection);\r
+\r
+            } else {\r
+                console.log("_kiwi.gateway.socket.on('error')", {reason: err});\r
+                callback_fn && callback_fn(err);\r
+            }\r
+        });\r
+    },\r
+\r
+\r
+    /**\r
+     * Make a new IRC connection and return its connection ID\r
+     */\r
+    makeIrcConnection: function(connection_info, callback_fn) {\r
+        var server_info = {\r
+            nick:       connection_info.nick,\r
+            hostname:   connection_info.host,\r
+            port:       connection_info.port,\r
+            ssl:        connection_info.ssl,\r
+            password:   connection_info.password\r
+        };\r
+\r
+        connection_info.options = connection_info.options || {};\r
+\r
+        // A few optional parameters\r
+        if (connection_info.options.encoding)\r
+            server_info.encoding = connection_info.options.encoding;\r
+\r
+        this.rpc('kiwi.connect_irc', server_info, function (err, server_num) {\r
+            if (!err) {\r
+                callback_fn && callback_fn(err, server_num);\r
+\r
+            } else {\r
+                callback_fn && callback_fn(err);\r
+            }\r
+        });\r
+    },\r
+\r
+\r
+    isConnected: function () {\r
+        // TODO: Check this. Might want to use .readyState\r
+        return this.socket;\r
+    },\r
+\r
+\r
+\r
+    parseKiwi: function (command, data) {\r
+        var args;\r
+\r
+        switch (command) {\r
+        case 'connected':\r
+            // Send some info on this client to the server\r
+            args = {\r
+                build_version: _kiwi.global.build_version\r
+            };\r
+            this.rpc('kiwi.client_info', args);\r
+\r
+            this.connect_callback && this.connect_callback();\r
+            delete this.connect_callback;\r
+\r
+            break;\r
+        }\r
+\r
+        this.trigger('kiwi:' + command, data);\r
+        this.trigger('kiwi', data);\r
+    },\r
+\r
+    /**\r
+    *   Parses the response from the server\r
+    */\r
+    parse: function (command, data) {\r
+        var network_trigger = '';\r
+\r
+        // Trigger the connection specific events (used by Network objects)\r
+        if (typeof data.connection_id !== 'undefined') {\r
+            network_trigger = 'connection:' + data.connection_id.toString();\r
+\r
+            this.trigger(network_trigger, {\r
+                event_name: command,\r
+                event_data: data\r
+            });\r
+\r
+            // Some events trigger a more in-depth event name\r
+            if (command == 'message' && data.type) {\r
+                this.trigger('connection ' + network_trigger, {\r
+                    event_name: 'message:' + data.type,\r
+                    event_data: data\r
+                });\r
+            }\r
+\r
+            if (command == 'channel' && data.type) {\r
+                this.trigger('connection ' + network_trigger, {\r
+                    event_name: 'channel:' + data.type,\r
+                    event_data: data\r
+                });\r
+            }\r
+        }\r
+\r
+        // Trigger the global events\r
+        this.trigger('connection', {event_name: command, event_data: data});\r
+        this.trigger('connection:' + command, data);\r
+    },\r
+\r
+    /**\r
+    *   Make an RPC call with the connection_id as the first argument\r
+    *   @param  {String}    method          RPC method name\r
+    *   @param  {Number}    connection_id   Connection ID this call relates to\r
+    */\r
+    rpcCall: function(method, connection_id) {\r
+        var args = Array.prototype.slice.call(arguments, 0);\r
+\r
+        if (typeof args[1] === 'undefined' || args[1] === null)\r
+            args[1] = _kiwi.app.connections.active_connection.get('connection_id');\r
+\r
+        return this.rpc.apply(this.rpc, args);\r
+    },\r
+\r
+    /**\r
+    *   Sends a PRIVMSG message\r
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
+    *   @param  {String}    msg         The message to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    privmsg: function (connection_id, target, msg, callback) {\r
+        var args = {\r
+            target: target,\r
+            msg: msg\r
+        };\r
+\r
+        this.rpcCall('irc.privmsg', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Sends a NOTICE message\r
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
+    *   @param  {String}    msg         The message to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    notice: function (connection_id, target, msg, callback) {\r
+        var args = {\r
+            target: target,\r
+            msg: msg\r
+        };\r
+\r
+        this.rpcCall('irc.notice', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Sends a CTCP message\r
+    *   @param  {Boolean}   request     Indicates whether this is a CTCP request (true) or reply (false)\r
+    *   @param  {String}    type        The type of CTCP message, e.g. 'VERSION', 'TIME', 'PING' etc.\r
+    *   @param  {String}    target      The target of the message, e.g a channel or nick\r
+    *   @param  {String}    params      Additional paramaters\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    ctcp: function (connection_id, is_request, type, target, params, callback) {\r
+        var args = {\r
+            is_request: is_request,\r
+            type: type,\r
+            target: target,\r
+            params: params\r
+        };\r
+\r
+        this.rpcCall('irc.ctcp', connection_id, args, callback);\r
+    },\r
+\r
+    ctcpRequest: function (connection_id, type, target, params, callback) {\r
+        this.ctcp(connection_id, true, type, target, params, callback);\r
+    },\r
+    ctcpResponse: function (connection_id, type, target, params, callback) {\r
+        this.ctcp(connection_id, false, type, target, params, callback);\r
+    },\r
+\r
+    /**\r
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
+    *   @param  {String}    msg         The message to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    action: function (connection_id, target, msg, callback) {\r
+        this.ctcp(connection_id, true, 'ACTION', target, msg, callback);\r
+    },\r
+\r
+    /**\r
+    *   Joins a channel\r
+    *   @param  {String}    channel     The channel to join\r
+    *   @param  {String}    key         The key to the channel\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    join: function (connection_id, channel, key, callback) {\r
+        var args = {\r
+            channel: channel,\r
+            key: key\r
+        };\r
+\r
+        this.rpcCall('irc.join', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Retrieves channel information\r
+    */\r
+    channelInfo: function (connection_id, channel, callback) {\r
+        var args = {\r
+            channel: channel\r
+        };\r
+\r
+        this.rpcCall('irc.channel_info', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Leaves a channel\r
+    *   @param  {String}    channel     The channel to part\r
+    *   @param  {String}    message     Optional part message\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    part: function (connection_id, channel, message, callback) {\r
+        "use strict";\r
+\r
+        // The message param is optional, so juggle args if it is missing\r
+        if (typeof arguments[2] === 'function') {\r
+            callback = arguments[2];\r
+            message = undefined;\r
+        }\r
+        var args = {\r
+            channel: channel,\r
+            message: message\r
+        };\r
+\r
+        this.rpcCall('irc.part', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Queries or modifies a channell topic\r
+    *   @param  {String}    channel     The channel to query or modify\r
+    *   @param  {String}    new_topic   The new topic to set\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    topic: function (connection_id, channel, new_topic, callback) {\r
+        var args = {\r
+            channel: channel,\r
+            topic: new_topic\r
+        };\r
+\r
+        this.rpcCall('irc.topic', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Kicks a user from a channel\r
+    *   @param  {String}    channel     The channel to kick the user from\r
+    *   @param  {String}    nick        The nick of the user to kick\r
+    *   @param  {String}    reason      The reason for kicking the user\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    kick: function (connection_id, channel, nick, reason, callback) {\r
+        var args = {\r
+            channel: channel,\r
+            nick: nick,\r
+            reason: reason\r
+        };\r
+\r
+        this.rpcCall('irc.kick', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Disconnects us from the server\r
+    *   @param  {String}    msg         The quit message to send to the IRC server\r
+    *   @param  {Function}   callback    A callback function\r
+    */\r
+    quit: function (connection_id, msg, callback) {\r
+        msg = msg || "";\r
+\r
+        var args = {\r
+            message: msg\r
+        };\r
+\r
+        this.rpcCall('irc.quit', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Sends a string unmodified to the IRC server\r
+    *   @param  {String}    data        The data to send to the IRC server\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    raw: function (connection_id, data, callback) {\r
+        var args = {\r
+            data: data\r
+        };\r
+\r
+        this.rpcCall('irc.raw', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Changes our nickname\r
+    *   @param  {String}    new_nick    Our new nickname\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    changeNick: function (connection_id, new_nick, callback) {\r
+        var args = {\r
+            nick: new_nick\r
+        };\r
+\r
+        this.rpcCall('irc.nick', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    * Sets a mode for a target\r
+    */\r
+    mode: function (connection_id, target, mode_string, callback) {\r
+        var args = {\r
+            data: 'MODE ' + target + ' ' + mode_string\r
+        };\r
+\r
+        this.rpcCall('irc.raw', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+     *  Sends ENCODING change request to server.\r
+     *  @param  {String}     new_encoding  The new proposed encode\r
+     *  @param  {Fucntion}   callback      A callback function\r
+     */\r
+    setEncoding: function (connection_id, new_encoding, callback) {\r
+        var args = {\r
+            encoding: new_encoding\r
+        };\r
+\r
+        this.rpcCall('irc.encoding', connection_id, args, callback);\r
+    }\r
+});\r
+
+
+
+(function () {
+
+    _kiwi.model.Network = Backbone.Model.extend({
+        defaults: {
+            connection_id: 0,
+            /**
+            *   The name of the network
+            *   @type    String
+            */
+            name: 'Network',
+
+            /**
+            *   The address (URL) of the network
+            *   @type    String
+            */
+            address: '',
+
+            /**
+            *   The port for the network
+            *   @type    Int
+            */
+            port: 6667,
+
+            /**
+            *   If this network uses SSL
+            *   @type    Bool
+            */
+            ssl: false,
+
+            /**
+            *   The password to connect to this network
+            *   @type    String
+            */
+            password: '',
+
+            /**
+            *   The current nickname
+            *   @type   String
+            */
+            nick: '',
+
+            /**
+            *   The channel prefix for this network
+            *   @type    String
+            */
+            channel_prefix: '#',
+
+            /**
+            *   The user prefixes for channel owner/admin/op/voice etc. on this network
+            *   @type   Array
+            */
+            user_prefixes: [
+                {symbol: '~', mode: 'q'},
+                {symbol: '&', mode: 'a'},
+                {symbol: '@', mode: 'o'},
+                {symbol: '%', mode: 'h'},
+                {symbol: '+', mode: 'v'}
+            ],
+
+            /**
+            *   List of nicks we are ignoring
+            *   @type Array
+            */
+            ignore_list: []
+        },
+
+
+        initialize: function () {
+            // If we already have a connection, bind our events
+            if (typeof this.get('connection_id') !== 'undefined') {
+                this.gateway = _kiwi.global.components.Network(this.get('connection_id'));
+                this.bindGatewayEvents();
+            }
+
+            // Create our panel list (tabs)
+            this.panels = new _kiwi.model.PanelList([], this);
+            //this.panels.network = this;
+
+            // Automatically create a server tab
+            var server_panel = new _kiwi.model.Server({name: 'Server', network: this});
+            this.panels.add(server_panel);
+            this.panels.server = this.panels.active = server_panel;
+        },
+
+
+        reconnect: function(callback_fn) {
+            var that = this,
+                server_info = {
+                    nick:       this.get('nick'),
+                    host:       this.get('address'),
+                    port:       this.get('port'),
+                    ssl:        this.get('ssl'),
+                    password:   this.get('password')
+                };
+
+            _kiwi.gateway.makeIrcConnection(server_info, function(err, connection_id) {
+                if (!err) {
+                    that.gateway.dispose();
+
+                    that.set('connection_id', connection_id);
+                    that.gateway = _kiwi.global.components.Network(that.get('connection_id'));
+                    that.bindGatewayEvents();
+
+                    // Reset each of the panels connection ID
+                    that.panels.forEach(function(panel) {
+                        panel.set('connection_id', connection_id);
+                    });
+
+                    callback_fn && callback_fn(err);
+
+                } else {
+                    console.log("_kiwi.gateway.socket.on('error')", {reason: err});
+                    callback_fn && callback_fn(err);
+                }
+            });
+        },
+
+
+        bindGatewayEvents: function () {
+            //this.gateway.on('all', function() {console.log('ALL', this.get('connection_id'), arguments);});
+
+            this.gateway.on('connect', onConnect, this);
+            this.gateway.on('disconnect', onDisconnect, this);
+
+            this.gateway.on('nick', function(event) {
+                if (event.nick === this.get('nick')) {
+                    this.set('nick', event.newnick);
+                }
+            }, this);
+
+            this.gateway.on('options', onOptions, this);
+            this.gateway.on('motd', onMotd, this);
+            this.gateway.on('channel:join', onJoin, this);
+            this.gateway.on('channel:part', onPart, this);
+            this.gateway.on('channel:kick', onKick, this);
+            this.gateway.on('quit', onQuit, this);
+            this.gateway.on('message', onMessage, this);
+            this.gateway.on('nick', onNick, this);
+            this.gateway.on('ctcp_request', onCtcpRequest, this);
+            this.gateway.on('ctcp_response', onCtcpResponse, this);
+            this.gateway.on('topic', onTopic, this);
+            this.gateway.on('topicsetby', onTopicSetBy, this);
+            this.gateway.on('userlist', onUserlist, this);
+            this.gateway.on('userlist_end', onUserlistEnd, this);
+            this.gateway.on('banlist', onBanlist, this);
+            this.gateway.on('mode', onMode, this);
+            this.gateway.on('whois', onWhois, this);
+            this.gateway.on('whowas', onWhowas, this);
+            this.gateway.on('away', onAway, this);
+            this.gateway.on('list_start', onListStart, this);
+            this.gateway.on('irc_error', onIrcError, this);
+            this.gateway.on('unknown_command', onUnknownCommand, this);
+            this.gateway.on('channel_info', onChannelInfo, this);
+            this.gateway.on('wallops', onWallops, this);
+        },
+
+
+        /**
+         * Create panels and join the channel
+         * This will not wait for the join event to create a panel. This
+         * increases responsiveness in case of network lag
+         */
+        createAndJoinChannels: function (channels) {
+            var that = this,
+                panels = [];
+
+            // Multiple channels may come as comma-delimited
+            if (typeof channels === 'string') {
+                channels = channels.split(',');
+            }
+
+            $.each(channels, function (index, channel_name_key) {
+                // We may have a channel key so split it off
+                var spli = channel_name_key.trim().split(' '),
+                    channel_name = spli[0],
+                    channel_key = spli[1] || '';
+
+                // Trim any whitespace off the name
+                channel_name = channel_name.trim();
+
+                // Add channel_prefix in front of the first channel if missing
+                if (that.get('channel_prefix').indexOf(channel_name[0]) === -1) {
+                    // Could be many prefixes but '#' is highly likely the required one
+                    channel_name = '#' + channel_name;
+                }
+
+                // Check if we have the panel already. If not, create it
+                channel = that.panels.getByName(channel_name);
+                if (!channel) {
+                    channel = new _kiwi.model.Channel({name: channel_name, network: that});
+                    that.panels.add(channel);
+                }
+
+                panels.push(channel);
+
+                that.gateway.join(channel_name, channel_key);
+            });
+
+
+            return panels;
+        },
+
+
+        /**
+         * Join all the open channels we have open
+         * Reconnecting to a network would typically call this.
+         */
+        rejoinAllChannels: function() {
+            var that = this;
+
+            this.panels.forEach(function(panel) {
+                if (!panel.isChannel())
+                    return;
+
+                that.gateway.join(panel.get('name'));
+            });
+        },
+
+        isChannelName: function (channel_name) {
+            var channel_prefix = this.get('channel_prefix');
+
+            if (!channel_name || !channel_name.length) return false;
+            return (channel_prefix.indexOf(channel_name[0]) > -1);
+        },
+
+        // Check a nick alongside our ignore list
+        isNickIgnored: function (nick) {
+            var idx, list = this.get('ignore_list');
+            var pattern, regex;
+
+            for (idx = 0; idx < list.length; idx++) {
+                pattern = list[idx].replace(/([.+^$[\]\\(){}|-])/g, "\\$1")
+                    .replace('*', '.*')
+                    .replace('?', '.');
+
+                regex = new RegExp(pattern, 'i');
+                if (regex.test(nick)) return true;
+            }
+
+            return false;
+        },
+
+        // Create a new query panel
+        createQuery: function (nick) {
+            var that = this,
+                query;
+
+            // Check if we have the panel already. If not, create it
+            query = that.panels.getByName(nick);
+            if (!query) {
+                query = new _kiwi.model.Query({name: nick});
+                that.panels.add(query);
+            }
+
+            // In all cases, show the demanded query
+            query.view.show();
+
+            return query;
+        }
+    });
+
+
+
+    function onDisconnect(event) {
+        this.set('connected', false);
+
+        $.each(this.panels.models, function (index, panel) {
+            if (!panel.isApplet()) {
+                panel.addMsg('', styleText('network_disconnected', {text: translateText('client_models_network_disconnected', [])}), 'action quit');
+            }
+        });
+    }
+
+
+
+    function onConnect(event) {
+        var panels, channel_names;
+
+        // Update our nick with what the network gave us
+        this.set('nick', event.nick);
+
+        this.set('connected', true);
+
+        // If this is a re-connection then we may have some channels to re-join
+        this.rejoinAllChannels();
+
+        // Auto joining channels
+        if (this.auto_join && this.auto_join.channel) {
+            panels = this.createAndJoinChannels(this.auto_join.channel + ' ' + (this.auto_join.key || ''));
+
+            // Show the last channel if we have one
+            if (panels)
+                panels[panels.length - 1].view.show();
+
+            delete this.auto_join;
+        }
+    }
+
+
+
+    function onOptions(event) {
+        var that = this;
+
+        $.each(event.options, function (name, value) {
+            switch (name) {
+            case 'CHANTYPES':
+                that.set('channel_prefix', value.join(''));
+                break;
+            case 'NETWORK':
+                that.set('name', value);
+                break;
+            case 'PREFIX':
+                that.set('user_prefixes', value);
+                break;
+            }
+        });
+
+        this.set('cap', event.cap);
+    }
+
+
+
+    function onMotd(event) {
+        this.panels.server.addMsg(this.get('name'), styleText('motd', {text: event.msg}), 'motd');
+    }
+
+
+
+    function onJoin(event) {
+        var c, members, user;
+        c = this.panels.getByName(event.channel);
+        if (!c) {
+            c = new _kiwi.model.Channel({name: event.channel, network: this});
+            this.panels.add(c);
+        }
+
+        members = c.get('members');
+        if (!members) return;
+
+        // Do we already have this member?
+        if (members.getByNick(event.nick)) {
+            return;
+        }
+
+        user = new _kiwi.model.Member({
+            nick: event.nick,
+            ident: event.ident,
+            hostname: event.hostname,
+            user_prefixes: this.get('user_prefixes')
+        });
+
+        _kiwi.global.events.emit('channel:join', {channel: event.channel, user: user, network: this.gateway})
+        .then(function() {
+            members.add(user, {kiwi: event});
+        });
+    }
+
+
+
+    function onPart(event) {
+        var channel, members, user,
+            part_options = {};
+
+        part_options.type = 'part';
+        part_options.message = event.message || '';
+        part_options.time = event.time;
+
+        channel = this.panels.getByName(event.channel);
+        if (!channel) return;
+
+        // If this is us, close the panel
+        if (event.nick === this.get('nick')) {
+            channel.close();
+            return;
+        }
+
+        members = channel.get('members');
+        if (!members) return;
+
+        user = members.getByNick(event.nick);
+        if (!user) return;
+
+        _kiwi.global.events.emit('channel:leave', {channel: event.channel, user: user, type: 'part', message: part_options.message, network: this.gateway})
+        .then(function() {
+            members.remove(user, {kiwi: part_options});
+        });
+    }
+
+
+
+    function onQuit(event) {
+        var member, members,
+            quit_options = {};
+
+        quit_options.type = 'quit';
+        quit_options.message = event.message || '';
+        quit_options.time = event.time;
+
+        $.each(this.panels.models, function (index, panel) {
+            // Let any query panels know they quit
+            if (panel.isQuery() && panel.get('name').toLowerCase() === event.nick.toLowerCase()) {
+                panel.addMsg(' ', styleText('channel_quit', {
+                    nick: event.nick,
+                    text: translateText('client_models_channel_quit', [quit_options.message])
+                }), 'action quit', {time: quit_options.time});
+            }
+
+            // Remove the nick from any channels
+            if (panel.isChannel()) {
+                member = panel.get('members').getByNick(event.nick);
+                if (member) {
+                    _kiwi.global.events.emit('channel:leave', {channel: panel.get('name'), user: member, type: 'quit', message: quit_options.message, network: this.gateway})
+                    .then(function() {
+                        panel.get('members').remove(member, {kiwi: quit_options});
+                    });
+                }
+            }
+        });
+    }
+
+
+
+    function onKick(event) {
+        var channel, members, user,
+            part_options = {};
+
+        part_options.type = 'kick';
+        part_options.by = event.nick;
+        part_options.message = event.message || '';
+        part_options.current_user_kicked = (event.kicked == this.get('nick'));
+        part_options.current_user_initiated = (event.nick == this.get('nick'));
+        part_options.time = event.time;
+
+        channel = this.panels.getByName(event.channel);
+        if (!channel) return;
+
+        members = channel.get('members');
+        if (!members) return;
+
+        user = members.getByNick(event.kicked);
+        if (!user) return;
+
+
+        _kiwi.global.events.emit('channel:leave', {channel: event.channel, user: user, type: 'kick', message: part_options.message, network: this.gateway})
+        .then(function() {
+            members.remove(user, {kiwi: part_options});
+
+            if (part_options.current_user_kicked) {
+                members.reset([]);
+            }
+        });
+    }
+
+
+
+    function onMessage(event) {
+        _kiwi.global.events.emit('message:new', {network: this.gateway, message: event})
+        .then(_.bind(function() {
+            var panel,
+                is_pm = ((event.target || '').toLowerCase() == this.get('nick').toLowerCase());
+
+            // An ignored user? don't do anything with it
+            if (this.isNickIgnored(event.nick)) {
+                return;
+            }
+
+            if (event.type == 'notice') {
+                if (event.from_server) {
+                    panel = this.panels.server;
+
+                } else {
+                    panel = this.panels.getByName(event.target) || this.panels.getByName(event.nick);
+
+                    // Forward ChanServ messages to its associated channel
+                    if (event.nick && event.nick.toLowerCase() == 'chanserv' && event.msg.charAt(0) == '[') {
+                        channel_name = /\[([^ \]]+)\]/gi.exec(event.msg);
+                        if (channel_name && channel_name[1]) {
+                            channel_name = channel_name[1];
+
+                            panel = this.panels.getByName(channel_name);
+                        }
+                    }
+
+                }
+
+                if (!panel) {
+                    panel = this.panels.server;
+                }
+
+            } else if (is_pm) {
+                // If a panel isn't found for this PM, create one
+                panel = this.panels.getByName(event.nick);
+                if (!panel) {
+                    panel = new _kiwi.model.Query({name: event.nick, network: this});
+                    this.panels.add(panel);
+                }
+
+            } else {
+                // If a panel isn't found for this target, reroute to the
+                // server panel
+                panel = this.panels.getByName(event.target);
+                if (!panel) {
+                    panel = this.panels.server;
+                }
+            }
+
+            switch (event.type){
+            case 'message':
+                panel.addMsg(event.nick, styleText('privmsg', {text: event.msg}), 'privmsg', {time: event.time});
+                break;
+
+            case 'action':
+                panel.addMsg('', styleText('action', {nick: event.nick, text: event.msg}), 'action', {time: event.time});
+                break;
+
+            case 'notice':
+                panel.addMsg('[' + (event.nick||'') + ']', styleText('notice', {text: event.msg}), 'notice', {time: event.time});
+
+                // Show this notice to the active panel if it didn't have a set target, but only in an active channel or query window
+                active_panel = _kiwi.app.panels().active;
+
+                if (!event.from_server && panel === this.panels.server && active_panel !== this.panels.server) {
+                    if (active_panel.get('network') === this && (active_panel.isChannel() || active_panel.isQuery()))
+                        active_panel.addMsg('[' + (event.nick||'') + ']', styleText('notice', {text: event.msg}), 'notice', {time: event.time});
+                }
+                break;
+            }
+        }, this));
+    }
+
+
+
+    function onNick(event) {
+        var member;
+
+        $.each(this.panels.models, function (index, panel) {
+            if (panel.get('name') == event.nick)
+                panel.set('name', event.newnick);
+
+            if (!panel.isChannel()) return;
+
+            member = panel.get('members').getByNick(event.nick);
+            if (member) {
+                member.set('nick', event.newnick);
+                panel.addMsg('', styleText('nick_changed', {nick: event.nick, text: translateText('client_models_network_nickname_changed', [event.newnick]), channel: name}), 'action nick', {time: event.time});
+            }
+        });
+    }
+
+
+
+    function onCtcpRequest(event) {
+        // An ignored user? don't do anything with it
+        if (this.isNickIgnored(event.nick)) {
+            return;
+        }
+
+        // Reply to a TIME ctcp
+        if (event.msg.toUpperCase() === 'TIME') {
+            this.gateway.ctcpResponse(event.type, event.nick, (new Date()).toString());
+        } else if(event.type.toUpperCase() === 'PING') { // CTCP PING reply
+            this.gateway.ctcpResponse(event.type, event.nick, event.msg.substr(5));
+        }
+    }
+
+
+
+    function onCtcpResponse(event) {
+        // An ignored user? don't do anything with it
+        if (this.isNickIgnored(event.nick)) {
+            return;
+        }
+
+        this.panels.server.addMsg('[' + event.nick + ']',  styleText('ctcp', {text: event.msg}), 'ctcp', {time: event.time});
+    }
+
+
+
+    function onTopic(event) {
+        var c;
+        c = this.panels.getByName(event.channel);
+        if (!c) return;
+
+        // Set the channels topic
+        c.set('topic', event.topic);
+
+        // If this is the active channel, update the topic bar too
+        if (c.get('name') === this.panels.active.get('name')) {
+            _kiwi.app.topicbar.setCurrentTopic(event.topic);
+        }
+    }
+
+
+
+    function onTopicSetBy(event) {
+        var c, when;
+        c = this.panels.getByName(event.channel);
+        if (!c) return;
+
+        when = new Date(event.when * 1000);
+        c.set('topic_set_by', {nick: event.nick, when: when});
+    }
+
+
+
+    function onChannelInfo(event) {
+        var channel = this.panels.getByName(event.channel);
+        if (!channel) return;
+
+        if (event.url) {
+            channel.set('info_url', event.url);
+        } else if (event.modes) {
+            channel.set('info_modes', event.modes);
+        }
+    }
+
+
+
+    function onUserlist(event) {
+        var that = this,
+            channel = this.panels.getByName(event.channel);
+
+        // If we didn't find a channel for this, may aswell leave
+        if (!channel) return;
+
+        channel.temp_userlist = channel.temp_userlist || [];
+        _.each(event.users, function (item) {
+            var user = new _kiwi.model.Member({
+                nick: item.nick,
+                modes: item.modes,
+                user_prefixes: that.get('user_prefixes')
+            });
+            channel.temp_userlist.push(user);
+        });
+    }
+
+
+
+    function onUserlistEnd(event) {
+        var channel;
+        channel = this.panels.getByName(event.channel);
+
+        // If we didn't find a channel for this, may aswell leave
+        if (!channel) return;
+
+        // Update the members list with the new list
+        channel.get('members').reset(channel.temp_userlist || []);
+
+        // Clear the temporary userlist
+        delete channel.temp_userlist;
+    }
+
+
+
+    function onBanlist(event) {
+        var channel = this.panels.getByName(event.channel);
+        if (!channel)
+            return;
+
+        channel.set('banlist', event.bans || []);
+    }
+
+
+
+    function onMode(event) {
+        var channel, i, prefixes, members, member, find_prefix,
+            request_updated_banlist = false;
+
+        // Build a nicely formatted string to be displayed to a regular human
+        function friendlyModeString (event_modes, alt_target) {
+            var modes = {}, return_string;
+
+            // If no default given, use the main event info
+            if (!event_modes) {
+                event_modes = event.modes;
+                alt_target = event.target;
+            }
+
+            // Reformat the mode object to make it easier to work with
+            _.each(event_modes, function (mode){
+                var param = mode.param || alt_target || '';
+
+                // Make sure we have some modes for this param
+                if (!modes[param]) {
+                    modes[param] = {'+':'', '-':''};
+                }
+
+                modes[param][mode.mode[0]] += mode.mode.substr(1);
+            });
+
+            // Put the string together from each mode
+            return_string = [];
+            _.each(modes, function (modeset, param) {
+                var str = '';
+                if (modeset['+']) str += '+' + modeset['+'];
+                if (modeset['-']) str += '-' + modeset['-'];
+                return_string.push(str + ' ' + param);
+            });
+            return_string = return_string.join(', ');
+
+            return return_string;
+        }
+
+
+        channel = this.panels.getByName(event.target);
+        if (channel) {
+            prefixes = this.get('user_prefixes');
+            find_prefix = function (p) {
+                return event.modes[i].mode[1] === p.mode;
+            };
+            for (i = 0; i < event.modes.length; i++) {
+                if (_.any(prefixes, find_prefix)) {
+                    if (!members) {
+                        members = channel.get('members');
+                    }
+                    member = members.getByNick(event.modes[i].param);
+                    if (!member) {
+                        console.log('MODE command recieved for unknown member %s on channel %s', event.modes[i].param, event.target);
+                        return;
+                    } else {
+                        if (event.modes[i].mode[0] === '+') {
+                            member.addMode(event.modes[i].mode[1]);
+                        } else if (event.modes[i].mode[0] === '-') {
+                            member.removeMode(event.modes[i].mode[1]);
+                        }
+                        members.sort();
+                    }
+                } else {
+                    // Channel mode being set
+                    // TODO: Store this somewhere?
+                    //channel.addMsg('', 'CHANNEL === ' + event.nick + ' set mode ' + event.modes[i].mode + ' on ' + event.target, 'action mode');
+                }
+
+                // TODO: Be smart, remove this specific ban from the banlist rather than request a whole banlist
+                if (event.modes[i].mode[1] == 'b')
+                    request_updated_banlist = true;
+            }
+
+            channel.addMsg('', styleText('mode', {nick: event.nick, text: translateText('client_models_network_mode', [friendlyModeString()]), channel: event.target}), 'action mode', {time: event.time});
+
+            // TODO: Be smart, remove the specific ban from the banlist rather than request a whole banlist
+            if (request_updated_banlist)
+                this.gateway.raw('MODE ' + channel.get('name') + ' +b');
+
+        } else {
+            // This is probably a mode being set on us.
+            if (event.target.toLowerCase() === this.get("nick").toLowerCase()) {
+                this.panels.server.addMsg('', styleText('selfmode', {nick: event.nick, text: translateText('client_models_network_mode', [friendlyModeString()]), channel: event.target}), 'action mode');
+            } else {
+               console.log('MODE command recieved for unknown target %s: ', event.target, event);
+            }
+        }
+    }
+
+
+
+    function onWhois(event) {
+        var logon_date, idle_time = '', panel;
+
+        if (event.end)
+            return;
+
+        if (typeof event.idle !== 'undefined') {
+            idle_time = secondsToTime(parseInt(event.idle, 10));
+            idle_time = idle_time.h.toString().lpad(2, "0") + ':' + idle_time.m.toString().lpad(2, "0") + ':' + idle_time.s.toString().lpad(2, "0");
+        }
+
+        panel = _kiwi.app.panels().active;
+        if (event.ident) {
+            panel.addMsg(event.nick, styleText('whois_ident', {nick: event.nick, ident: event.ident, host: event.hostname, text: event.msg}), 'whois');
+
+        } else if (event.chans) {
+            panel.addMsg(event.nick, styleText('whois_channels', {nick: event.nick, text: translateText('client_models_network_channels', [event.chans])}), 'whois');
+        } else if (event.irc_server) {
+            panel.addMsg(event.nick, styleText('whois_server', {nick: event.nick, text: translateText('client_models_network_server', [event.irc_server, event.server_info])}), 'whois');
+        } else if (event.msg) {
+            panel.addMsg(event.nick, styleText('whois', {text: event.msg}), 'whois');
+        } else if (event.logon) {
+            logon_date = new Date();
+            logon_date.setTime(event.logon * 1000);
+            logon_date = _kiwi.utils.formatDate(logon_date);
+
+            panel.addMsg(event.nick, styleText('whois_idle_and_signon', {nick: event.nick, text: translateText('client_models_network_idle_and_signon', [idle_time, logon_date])}), 'whois');
+        } else if (event.away_reason) {
+            panel.addMsg(event.nick, styleText('whois_away', {nick: event.nick, text: translateText('client_models_network_away', [event.away_reason])}), 'whois');
+        } else {
+            panel.addMsg(event.nick, styleText('whois_idle', {nick: event.nick, text: translateText('client_models_network_idle', [idle_time])}), 'whois');
+        }
+    }
+
+    function onWhowas(event) {
+        var panel;
+
+        if (event.end)
+            return;
+
+        panel = _kiwi.app.panels().active;
+        if (event.hostname) {
+            panel.addMsg(event.nick, styleText('who', {nick: event.nick, ident: event.ident, host: event.hostname, realname: event.real_name, text: event.msg}), 'whois');
+        } else {
+            panel.addMsg(event.nick, styleText('whois_notfound', {nick: event.nick, text: translateText('client_models_network_nickname_notfound', [])}), 'whois');
+        }
+    }
+
+
+    function onAway(event) {
+        $.each(this.panels.models, function (index, panel) {
+            if (!panel.isChannel()) return;
+
+            member = panel.get('members').getByNick(event.nick);
+            if (member) {
+                member.set('away', !(!event.reason));
+            }
+        });
+    }
+
+
+
+    function onListStart(event) {
+        var chanlist = _kiwi.model.Applet.loadOnce('kiwi_chanlist');
+        chanlist.view.show();
+    }
+
+
+
+    function onIrcError(event) {
+        var panel, tmp;
+
+        if (event.channel !== undefined && !(panel = this.panels.getByName(event.channel))) {
+            panel = this.panels.server;
+        }
+
+        switch (event.error) {
+        case 'banned_from_channel':
+            panel.addMsg(' ', styleText('channel_banned', {nick: event.nick, text: translateText('client_models_network_banned', [event.channel, event.reason]), channel: event.channel}), 'status');
+            _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_banned').fetch(event.channel, event.reason));
+            break;
+        case 'bad_channel_key':
+            panel.addMsg(' ', styleText('channel_badkey', {nick: event.nick, text: translateText('client_models_network_channel_badkey', [event.channel]), channel: event.channel}), 'status');
+            _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_channel_badkey').fetch(event.channel));
+            break;
+        case 'invite_only_channel':
+            panel.addMsg(' ', styleText('channel_inviteonly', {nick: event.nick, text: translateText('client_models_network_channel_inviteonly', [event.nick, event.channel]), channel: event.channel}), 'status');
+            _kiwi.app.message.text(event.channel + ' ' + _kiwi.global.i18n.translate('client_models_network_channel_inviteonly').fetch());
+            break;
+        case 'user_on_channel':
+            panel.addMsg(' ', styleText('channel_alreadyin', {nick: event.nick, text: translateText('client_models_network_channel_alreadyin'), channel: event.channel}));
+            break;
+        case 'channel_is_full':
+            panel.addMsg(' ', styleText('channel_limitreached', {nick: event.nick, text: translateText('client_models_network_channel_limitreached', [event.channel]), channel: event.channel}), 'status');
+            _kiwi.app.message.text(event.channel + ' ' + _kiwi.global.i18n.translate('client_models_network_channel_limitreached').fetch(event.channel));
+            break;
+        case 'chanop_privs_needed':
+            panel.addMsg(' ', styleText('chanop_privs_needed', {text: event.reason, channel: event.channel}), 'status');
+            _kiwi.app.message.text(event.reason + ' (' + event.channel + ')');
+            break;
+        case 'cannot_send_to_channel':
+            panel.addMsg(' ', '== ' + _kiwi.global.i18n.translate('Cannot send message to channel, you are not voiced').fetch(event.channel, event.reason), 'status');
+            break;
+        case 'no_such_nick':
+            tmp = this.panels.getByName(event.nick);
+            if (tmp) {
+                tmp.addMsg(' ', styleText('no_such_nick', {nick: event.nick, text: event.reason, channel: event.channel}), 'status');
+            } else {
+                this.panels.server.addMsg(' ', styleText('no_such_nick', {nick: event.nick, text: event.reason, channel: event.channel}), 'status');
+            }
+            break;
+        case 'nickname_in_use':
+            this.panels.server.addMsg(' ', styleText('nickname_alreadyinuse', {nick: event.nick, text: translateText('client_models_network_nickname_alreadyinuse', [event.nick]), channel: event.channel}), 'status');
+            if (this.panels.server !== this.panels.active) {
+                _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_nickname_alreadyinuse').fetch(event.nick));
+            }
+
+            // Only show the nickchange component if the controlbox is open
+            if (_kiwi.app.controlbox.$el.css('display') !== 'none') {
+                (new _kiwi.view.NickChangeBox()).render();
+            }
+
+            break;
+
+        case 'password_mismatch':
+            this.panels.server.addMsg(' ', styleText('channel_badpassword', {nick: event.nick, text: translateText('client_models_network_badpassword', []), channel: event.channel}), 'status');
+            break;
+
+        case 'error':
+            if (event.reason) {
+                this.panels.server.addMsg(' ', styleText('general_error', {text: event.reason}), 'status');
+            }
+            break;
+
+        default:
+            // We don't know what data contains, so don't do anything with it.
+            //_kiwi.front.tabviews.server.addMsg(null, ' ', '== ' + data, 'status');
+        }
+    }
+
+
+    function onUnknownCommand(event) {
+        var display_params = _.clone(event.params);
+
+        // A lot of commands have our nick as the first parameter. This is redundant for us
+        if (display_params[0] && display_params[0] == this.get('nick')) {
+            display_params.shift();
+        }
+
+        this.panels.server.addMsg('', styleText('unknown_command', {text: '[' + event.command + '] ' + display_params.join(', ', '')}));
+    }
+
+
+    function onWallops(event) {
+        var active_panel = _kiwi.app.panels().active;
+
+        // Send to server panel
+        this.panels.server.addMsg('[' + (event.nick||'') + ']', styleText('wallops', {text: event.msg}), 'wallops', {time: event.time});
+
+        // Send to active panel if its a channel/query *and* it's related to this network
+        if (active_panel !== this.panels.server && (active_panel.isChannel() || active_panel.isQuery()) && active_panel.get('network') === this)
+            active_panel.addMsg('[' + (event.nick||'') + ']', styleText('wallops', {text: event.msg}), 'wallops', {time: event.time});
+    }
+
+}
+
+)();
+
+
+
+_kiwi.model.Member = Backbone.Model.extend({\r
+    initialize: function (attributes) {\r
+        var nick, modes, prefix;\r
+\r
+        // The nick may have a mode prefix, we don't want this\r
+        nick = this.stripPrefix(this.get("nick"));\r
+\r
+        // Make sure we have a mode array, and that it's sorted\r
+        modes = this.get("modes");\r
+        modes = modes || [];\r
+        this.sortModes(modes);\r
+\r
+        this.set({"nick": nick, "modes": modes, "prefix": this.getPrefix(modes)}, {silent: true});\r
+\r
+        this.updateOpStatus();\r
+\r
+        this.view = new _kiwi.view.Member({"model": this});\r
+    },\r
+\r
+\r
+    /**\r
+     * Sort modes in order of importance\r
+     */\r
+    sortModes: function (modes) {\r
+        var that = this;\r
+\r
+        return modes.sort(function (a, b) {\r
+            var a_idx, b_idx, i;\r
+            var user_prefixes = that.get('user_prefixes');\r
+\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === a) {\r
+                    a_idx = i;\r
+                }\r
+            }\r
+\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === b) {\r
+                    b_idx = i;\r
+                }\r
+            }\r
+\r
+            if (a_idx < b_idx) {\r
+                return -1;\r
+            } else if (a_idx > b_idx) {\r
+                return 1;\r
+            } else {\r
+                return 0;\r
+            }\r
+        });\r
+    },\r
+\r
+\r
+    addMode: function (mode) {\r
+        var modes_to_add = mode.split(''),\r
+            modes, prefix;\r
+\r
+        modes = this.get("modes");\r
+        $.each(modes_to_add, function (index, item) {\r
+            modes.push(item);\r
+        });\r
+\r
+        modes = this.sortModes(modes);\r
+        this.set({"prefix": this.getPrefix(modes), "modes": modes});\r
+\r
+        this.updateOpStatus();\r
+\r
+        this.view.render();\r
+    },\r
+\r
+\r
+    removeMode: function (mode) {\r
+        var modes_to_remove = mode.split(''),\r
+            modes, prefix;\r
+\r
+        modes = this.get("modes");\r
+        modes = _.reject(modes, function (m) {\r
+            return (_.indexOf(modes_to_remove, m) !== -1);\r
+        });\r
+\r
+        this.set({"prefix": this.getPrefix(modes), "modes": modes});\r
+\r
+        this.updateOpStatus();\r
+\r
+        this.view.render();\r
+    },\r
+\r
+\r
+    /**\r
+     * Figure out a valid prefix given modes.\r
+     * If a user is an op but also has voice, the prefix\r
+     * should be the op as it is more important.\r
+     */\r
+    getPrefix: function (modes) {\r
+        var prefix = '';\r
+        var user_prefixes = this.get('user_prefixes');\r
+\r
+        if (typeof modes[0] !== 'undefined') {\r
+            prefix = _.detect(user_prefixes, function (prefix) {\r
+                return prefix.mode === modes[0];\r
+            });\r
+\r
+            prefix = (prefix) ? prefix.symbol : '';\r
+        }\r
+\r
+        return prefix;\r
+    },\r
+\r
+\r
+    /**\r
+     * Remove any recognised prefix from a nick\r
+     */\r
+    stripPrefix: function (nick) {\r
+        var tmp = nick, i, j, k, nick_char;\r
+        var user_prefixes = this.get('user_prefixes');\r
+\r
+        i = 0;\r
+\r
+        nick_character_loop:\r
+        for (j = 0; j < nick.length; j++) {\r
+            nick_char = nick.charAt(j);\r
+\r
+            for (k = 0; k < user_prefixes.length; k++) {\r
+                if (nick_char === user_prefixes[k].symbol) {\r
+                    i++;\r
+                    continue nick_character_loop;\r
+                }\r
+            }\r
+\r
+            break;\r
+        }\r
+\r
+        return tmp.substr(i);\r
+    },\r
+\r
+\r
+\r
+    /**\r
+     * Format this nick into readable format (eg. nick [ident@hostname])\r
+     */\r
+    displayNick: function (full) {\r
+        var display = this.get('nick');\r
+\r
+        if (full) {\r
+            if (this.get("ident")) {\r
+                display += ' [' + this.get("ident") + '@' + this.get("hostname") + ']';\r
+            }\r
+        }\r
+\r
+        return display;\r
+    },\r
+\r
+\r
+    // Helper to quickly get user mask details\r
+    getMaskParts: function () {\r
+        return {\r
+            nick: this.get('nick') || '',\r
+            ident: this.get('ident') || '',\r
+            hostname: this.get('hostname') || ''\r
+        };\r
+    },\r
+\r
+\r
+    /**\r
+     * With the modes set on the user, make note if we have some sort of op status\r
+     */\r
+    updateOpStatus: function () {\r
+        var user_prefixes = this.get('user_prefixes'),\r
+            modes = this.get('modes'),\r
+            o, max_mode;\r
+\r
+        if (modes.length > 0) {\r
+            o = _.indexOf(user_prefixes, _.find(user_prefixes, function (prefix) {\r
+                return prefix.mode === 'o';\r
+            }));\r
+\r
+            max_mode = _.indexOf(user_prefixes, _.find(user_prefixes, function (prefix) {\r
+                return prefix.mode === modes[0];\r
+            }));\r
+\r
+            if ((max_mode === -1) || (max_mode > o)) {\r
+                this.set({"is_op": false}, {silent: true});\r
+            } else {\r
+                this.set({"is_op": true}, {silent: true});\r
+            }\r
+\r
+        } else {\r
+            this.set({"is_op": false}, {silent: true});\r
+        }\r
+    }\r
+});
+
+
+_kiwi.model.MemberList = Backbone.Collection.extend({\r
+    model: _kiwi.model.Member,\r
+    comparator: function (a, b) {\r
+        var i, a_modes, b_modes, a_idx, b_idx, a_nick, b_nick;\r
+        var user_prefixes = this.channel.get('network').get('user_prefixes');\r
+\r
+        a_modes = a.get("modes");\r
+        b_modes = b.get("modes");\r
+\r
+        // Try to sort by modes first\r
+        if (a_modes.length > 0) {\r
+            // a has modes, but b doesn't so a should appear first\r
+            if (b_modes.length === 0) {\r
+                return -1;\r
+            }\r
+            a_idx = b_idx = -1;\r
+            // Compare the first (highest) mode\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === a_modes[0]) {\r
+                    a_idx = i;\r
+                }\r
+            }\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === b_modes[0]) {\r
+                    b_idx = i;\r
+                }\r
+            }\r
+            if (a_idx < b_idx) {\r
+                return -1;\r
+            } else if (a_idx > b_idx) {\r
+                return 1;\r
+            }\r
+            // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting\r
+\r
+        } else if (b_modes.length > 0) {\r
+            // b has modes but a doesn't so b should appear first\r
+            return 1;\r
+        }\r
+        a_nick = a.get("nick").toLocaleUpperCase();\r
+        b_nick = b.get("nick").toLocaleUpperCase();\r
+        // Lexicographical sorting\r
+        if (a_nick < b_nick) {\r
+            return -1;\r
+        } else if (a_nick > b_nick) {\r
+            return 1;\r
+        } else {\r
+            return 0;\r
+        }\r
+    },\r
+\r
+\r
+    initialize: function (options) {\r
+        this.view = new _kiwi.view.MemberList({"model": this});\r
+        this.initNickCache();\r
+    },\r
+\r
+\r
+    /*\r
+     * Keep a reference to each member by the nick. Speeds up .getByNick()\r
+     * so it doesn't need to loop over every model for each nick lookup\r
+     */\r
+    initNickCache: function() {\r
+        var that = this;\r
+\r
+        this.nick_cache = Object.create(null);\r
+\r
+        this.on('reset', function() {\r
+            this.nick_cache = Object.create(null);\r
+\r
+            this.models.forEach(function(member) {\r
+                that.nick_cache[member.get('nick').toLowerCase()] = member;\r
+            });\r
+        });\r
+\r
+        this.on('add', function(member) {\r
+            that.nick_cache[member.get('nick').toLowerCase()] = member;\r
+        });\r
+\r
+        this.on('remove', function(member) {\r
+            delete that.nick_cache[member.get('nick').toLowerCase()];\r
+        });\r
+\r
+        this.on('change:nick', function(member) {\r
+            that.nick_cache[member.get('nick').toLowerCase()] = member;\r
+            delete that.nick_cache[member.previous('nick').toLowerCase()];\r
+        });\r
+    },\r
+\r
+\r
+    getByNick: function (nick) {\r
+        if (typeof nick !== 'string') return;\r
+        return this.nick_cache[nick.toLowerCase()];\r
+    }\r
+});
+
+
+_kiwi.model.NewConnection = Backbone.Collection.extend({
+    initialize: function() {
+        this.view = new _kiwi.view.ServerSelect({model: this});
+
+        this.view.bind('server_connect', this.onMakeConnection, this);
+
+    },
+
+
+    populateDefaultServerSettings: function() {
+        var defaults = _kiwi.global.defaultServerSettings();
+        this.view.populateFields(defaults);
+    },
+
+
+    onMakeConnection: function(new_connection_event) {
+        var that = this;
+
+        this.connect_details = new_connection_event;
+
+        this.view.networkConnecting();
+
+        _kiwi.gateway.newConnection({
+            nick: new_connection_event.nick,
+            host: new_connection_event.server,
+            port: new_connection_event.port,
+            ssl: new_connection_event.ssl,
+            password: new_connection_event.password,
+            options: new_connection_event.options
+        }, function(err, network) {
+            that.onNewNetwork(err, network);
+        });
+    },
+
+
+    onNewNetwork: function(err, network) {
+        // Show any errors if given
+        if (err) {
+            this.view.showError(err);
+        }
+
+        if (network && this.connect_details) {
+            network.auto_join = {
+                channel: this.connect_details.channel,
+                key: this.connect_details.channel_key
+            };
+
+            this.trigger('new_network', network);
+        }
+    }
+});
+
+
+_kiwi.model.Panel = Backbone.Model.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "";\r
+        this.view = new _kiwi.view.Panel({"model": this, "name": name});\r
+        this.set({\r
+            "scrollback": [],\r
+            "name": name\r
+        }, {"silent": true});\r
+\r
+        _kiwi.global.events.emit('panel:created', {panel: this});\r
+    },\r
+\r
+    close: function () {\r
+        _kiwi.app.panels.trigger('close', this);\r
+        _kiwi.global.events.emit('panel:close', {panel: this});\r
+\r
+        if (this.view) {\r
+            this.view.unbind();\r
+            this.view.remove();\r
+            this.view = undefined;\r
+            delete this.view;\r
+        }\r
+\r
+        var members = this.get('members');\r
+        if (members) {\r
+            members.reset([]);\r
+            this.unset('members');\r
+        }\r
+\r
+        this.get('panel_list').remove(this);\r
+\r
+        this.unbind();\r
+        this.destroy();\r
+    },\r
+\r
+    isChannel: function () {\r
+        return false;\r
+    },\r
+\r
+    isQuery: function () {\r
+        return false;\r
+    },\r
+\r
+    isApplet: function () {\r
+        return false;\r
+    },\r
+\r
+    isServer: function () {\r
+        return false;\r
+    },\r
+\r
+    isActive: function () {\r
+        return (_kiwi.app.panels().active === this);\r
+    }\r
+});
+
+
+_kiwi.model.PanelList = Backbone.Collection.extend({\r
+    model: _kiwi.model.Panel,\r
+\r
+    comparator: function (chan) {\r
+        return chan.get('name');\r
+    },\r
+    initialize: function (elements, network) {\r
+        var that = this;\r
+\r
+        // If this PanelList is associated with a network/connection\r
+        if (network) {\r
+            this.network = network;\r
+        }\r
+\r
+        this.view = new _kiwi.view.Tabs({model: this});\r
+\r
+        // Holds the active panel\r
+        this.active = null;\r
+\r
+        // Keep a tab on the active panel\r
+        this.bind('active', function (active_panel) {\r
+            this.active = active_panel;\r
+        }, this);\r
+\r
+        this.bind('add', function(panel) {\r
+            panel.set('panel_list', this);\r
+        });\r
+    },\r
+\r
+\r
+\r
+    getByCid: function (cid) {\r
+        if (typeof name !== 'string') return;\r
+\r
+        return this.find(function (c) {\r
+            return cid === c.cid;\r
+        });\r
+    },\r
+\r
+\r
+\r
+    getByName: function (name) {\r
+        if (typeof name !== 'string') return;\r
+\r
+        return this.find(function (c) {\r
+            return name.toLowerCase() === c.get('name').toLowerCase();\r
+        });\r
+    }\r
+});\r
+
+
+
+_kiwi.model.NetworkPanelList = Backbone.Collection.extend({
+    model: _kiwi.model.Network,
+
+    initialize: function() {
+        this.view = new _kiwi.view.NetworkTabs({model: this});
+        
+        this.on('add', this.onNetworkAdd, this);
+        this.on('remove', this.onNetworkRemove, this);
+
+        // Current active connection / panel
+        this.active_connection = undefined;
+        this.active_panel = undefined;
+
+        // TODO: Remove this - legacy
+        this.active = undefined;
+    },
+
+    getByConnectionId: function(id) {
+        return this.find(function(connection){
+            return connection.get('connection_id') == id;
+        });
+    },
+
+    panels: function() {
+        var panels = [];
+
+        this.each(function(network) {
+            panels = panels.concat(network.panels.models);
+        });
+
+        return panels;
+    },
+
+
+    onNetworkAdd: function(network) {
+        network.panels.on('active', this.onPanelActive, this);
+
+        // if it's our first connection, set it active
+        if (this.models.length === 1) {
+            this.active_connection = network;
+            this.active_panel = network.panels.server;
+
+            // TODO: Remove this - legacy
+            this.active = this.active_panel;
+        }
+    },
+
+    onNetworkRemove: function(network) {
+        network.panels.off('active', this.onPanelActive, this);
+    },
+
+    onPanelActive: function(panel) {
+        var connection = this.getByConnectionId(panel.tab.data('connection_id'));
+        this.trigger('active', panel, connection);
+
+        this.active_connection = connection;
+        this.active_panel = panel;
+
+        // TODO: Remove this - legacy
+        this.active = panel;
+    }
+});
+
+
+// TODO: Channel modes\r
+// TODO: Listen to gateway events for anythign related to this channel\r
+_kiwi.model.Channel = _kiwi.model.Panel.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "",\r
+            members;\r
+\r
+        this.set({\r
+            "members": new _kiwi.model.MemberList(),\r
+            "name": name,\r
+            "scrollback": [],\r
+            "topic": ""\r
+        }, {"silent": true});\r
+\r
+        this.view = new _kiwi.view.Channel({"model": this, "name": name});\r
+\r
+        members = this.get("members");\r
+        members.channel = this;\r
+        members.bind("add", function (member, members, options) {\r
+            var show_message = _kiwi.global.settings.get('show_joins_parts');\r
+            if (show_message === false) {\r
+                return;\r
+            }\r
+\r
+            this.addMsg(' ', styleText('channel_join', {member: member.getMaskParts(), text: translateText('client_models_channel_join'), channel: name}), 'action join', {time: options.kiwi.time});\r
+        }, this);\r
+\r
+        members.bind("remove", function (member, members, options) {\r
+            var show_message = _kiwi.global.settings.get('show_joins_parts');\r
+            var msg = (options.kiwi.message) ? '(' + options.kiwi.message + ')' : '';\r
+\r
+            if (options.kiwi.type === 'quit' && show_message) {\r
+                this.addMsg(' ', styleText('channel_quit', {member: member.getMaskParts(), text: translateText('client_models_channel_quit', [msg]), channel: name}), 'action quit', {time: options.kiwi.time});\r
+\r
+            } else if (options.kiwi.type === 'kick') {\r
+\r
+                if (!options.kiwi.current_user_kicked) {\r
+                    //If user kicked someone, show the message regardless of settings.\r
+                    if (show_message || options.kiwi.current_user_initiated) {\r
+                        this.addMsg(' ', styleText('channel_kicked', {member: member.getMaskParts(), text: translateText('client_models_channel_kicked', [options.kiwi.by, msg]), channel: name}), 'action kick', {time: options.kiwi.time});\r
+                    }\r
+                } else {\r
+                    this.addMsg(' ', styleText('channel_selfkick', {text: translateText('client_models_channel_selfkick', [options.kiwi.by, msg]), channel: name}), 'action kick', {time: options.kiwi.time});\r
+                }\r
+            } else if (show_message) {\r
+                this.addMsg(' ', styleText('channel_part', {member: member.getMaskParts(), text: translateText('client_models_channel_part', [msg]), channel: name}), 'action part', {time: options.kiwi.time});\r
+\r
+            }\r
+        }, this);\r
+\r
+        _kiwi.global.events.emit('panel:created', {panel: this});\r
+    },\r
+\r
+\r
+    addMsg: function (nick, msg, type, opts) {\r
+        var message_obj, bs, d, members, member,\r
+            scrollback = (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250);\r
+\r
+        opts = opts || {};\r
+\r
+        // Time defaults to now\r
+        if (typeof opts.time === 'number') {\r
+            opts.time = new Date(opts.time);\r
+        } else {\r
+            opts.time = new Date();\r
+        }\r
+\r
+        // CSS style defaults to empty string\r
+        if (!opts || typeof opts.style === 'undefined') {\r
+            opts.style = '';\r
+        }\r
+\r
+        // Create a message object\r
+        message_obj = {"msg": msg, "date": opts.date, "time": opts.time, "nick": nick, "chan": this.get("name"), "type": type, "style": opts.style};\r
+\r
+        // If this user has one, get its prefix\r
+        members = this.get('members');\r
+        if (members) {\r
+            member = members.getByNick(message_obj.nick);\r
+            if (member) {\r
+                message_obj.nick_prefix = member.get('prefix');\r
+            }\r
+        }\r
+\r
+        // The CSS class (action, topic, notice, etc)\r
+        if (typeof message_obj.type !== "string") {\r
+            message_obj.type = '';\r
+        }\r
+\r
+        // Make sure we don't have NaN or something\r
+        if (typeof message_obj.msg !== "string") {\r
+            message_obj.msg = '';\r
+        }\r
+\r
+        // Update the scrollback\r
+        bs = this.get("scrollback");\r
+        if (bs) {\r
+            bs.push(message_obj);\r
+\r
+            // Keep the scrolback limited\r
+            if (bs.length > scrollback) {\r
+                bs = _.last(bs, scrollback);\r
+            }\r
+            this.set({"scrollback": bs}, {silent: true});\r
+        }\r
+\r
+        this.trigger("msg", message_obj);\r
+    },\r
+\r
+\r
+    clearMessages: function () {\r
+        this.set({'scrollback': []}, {silent: true});\r
+        this.addMsg('', 'Window cleared');\r
+\r
+        this.view.render();\r
+    },\r
+\r
+\r
+    setMode: function(mode_string) {\r
+        this.get('network').gateway.mode(this.get('name'), mode_string);\r
+    },\r
+\r
+    isChannel: function() {\r
+        return true;\r
+    }\r
+});\r
+
+
+
+_kiwi.model.Query = _kiwi.model.Channel.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "",\r
+            members;\r
+\r
+        this.view = new _kiwi.view.Channel({"model": this, "name": name});\r
+        this.set({\r
+            "name": name,\r
+            "scrollback": []\r
+        }, {"silent": true});\r
+\r
+        _kiwi.global.events.emit('panel:created', {panel: this});\r
+    },\r
+\r
+    isChannel: function () {\r
+        return false;\r
+    },\r
+\r
+    isQuery: function () {\r
+        return true;\r
+    }\r
+});
+
+
+_kiwi.model.Server = _kiwi.model.Channel.extend({\r
+    initialize: function (attributes) {\r
+        var name = "Server";\r
+        this.view = new _kiwi.view.Channel({"model": this, "name": name});\r
+        this.set({\r
+            "scrollback": [],\r
+            "name": name\r
+        }, {"silent": true});\r
+\r
+        _kiwi.global.events.emit('panel:created', {panel: this});\r
+    },\r
+\r
+    isServer: function () {\r
+        return true;\r
+    },\r
+\r
+    isChannel: function () {\r
+        return false;\r
+    }\r
+});
+
+
+_kiwi.model.Applet = _kiwi.model.Panel.extend({\r
+    initialize: function (attributes) {\r
+        // Temporary name\r
+        var name = "applet_"+(new Date().getTime().toString()) + Math.ceil(Math.random()*100).toString();\r
+        this.view = new _kiwi.view.Applet({model: this, name: name});\r
+\r
+        this.set({\r
+            "name": name\r
+        }, {"silent": true});\r
+\r
+        // Holds the loaded applet\r
+        this.loaded_applet = null;\r
+    },\r
+\r
+\r
+    // Load an applet within this panel\r
+    load: function (applet_object, applet_name) {\r
+        if (typeof applet_object === 'object') {\r
+            // Make sure this is a valid Applet\r
+            if (applet_object.get || applet_object.extend) {\r
+\r
+                // Try find a title for the applet\r
+                this.set('title', applet_object.get('title') || _kiwi.global.i18n.translate('client_models_applet_unknown').fetch());\r
+\r
+                // Update the tabs title if the applet changes it\r
+                applet_object.bind('change:title', function (obj, new_value) {\r
+                    this.set('title', new_value);\r
+                }, this);\r
+\r
+                // If this applet has a UI, add it now\r
+                this.view.$el.html('');\r
+                if (applet_object.view) {\r
+                    this.view.$el.append(applet_object.view.$el);\r
+                }\r
+\r
+                // Keep a reference to this applet\r
+                this.loaded_applet = applet_object;\r
+\r
+                this.loaded_applet.trigger('applet_loaded');\r
+            }\r
+\r
+        } else if (typeof applet_object === 'string') {\r
+            // Treat this as a URL to an applet script and load it\r
+            this.loadFromUrl(applet_object, applet_name);\r
+        }\r
+\r
+        return this;\r
+    },\r
+\r
+\r
+    loadFromUrl: function(applet_url, applet_name) {\r
+        var that = this;\r
+\r
+        this.view.$el.html(_kiwi.global.i18n.translate('client_models_applet_loading').fetch());\r
+        $script(applet_url, function () {\r
+            // Check if the applet loaded OK\r
+            if (!_kiwi.applets[applet_name]) {\r
+                that.view.$el.html(_kiwi.global.i18n.translate('client_models_applet_notfound').fetch());\r
+                return;\r
+            }\r
+\r
+            // Load a new instance of this applet\r
+            that.load(new _kiwi.applets[applet_name]());\r
+        });\r
+    },\r
+\r
+\r
+    close: function () {\r
+        this.view.$el.remove();\r
+        this.destroy();\r
+\r
+        this.view = undefined;\r
+\r
+        // Call the applets dispose method if it has one\r
+        if (this.loaded_applet && this.loaded_applet.dispose) {\r
+            this.loaded_applet.dispose();\r
+        }\r
+\r
+        // Call the inherited close()\r
+        this.constructor.__super__.close.apply(this, arguments);\r
+    },\r
+\r
+    isApplet: function () {\r
+        return true;\r
+    }\r
+},\r
+\r
+\r
+{\r
+    // Load an applet type once only. If it already exists, return that\r
+    loadOnce: function (applet_name) {\r
+\r
+        // See if we have an instance loaded already\r
+        var applet = _.find(_kiwi.app.panels('applets'), function(panel) {\r
+            // Ignore if it's not an applet\r
+            if (!panel.isApplet()) return;\r
+\r
+            // Ignore if it doesn't have an applet loaded\r
+            if (!panel.loaded_applet) return;\r
+\r
+            if (panel.loaded_applet.get('_applet_name') === applet_name) {\r
+                return true;\r
+            }\r
+        });\r
+\r
+        if (applet) return applet;\r
+\r
+\r
+        // If we didn't find an instance, load a new one up\r
+        return this.load(applet_name);\r
+    },\r
+\r
+\r
+    load: function (applet_name, options) {\r
+        var applet, applet_obj;\r
+\r
+        options = options || {};\r
+\r
+        applet_obj = this.getApplet(applet_name);\r
+\r
+        if (!applet_obj)\r
+            return;\r
+\r
+        // Create the applet and load the content\r
+        applet = new _kiwi.model.Applet();\r
+        applet.load(new applet_obj({_applet_name: applet_name}));\r
+\r
+        // Add it into the tab list if needed (default)\r
+        if (!options.no_tab)\r
+            _kiwi.app.applet_panels.add(applet);\r
+\r
+\r
+        return applet;\r
+    },\r
+\r
+\r
+    getApplet: function (applet_name) {\r
+        return _kiwi.applets[applet_name] || null;\r
+    },\r
+\r
+\r
+    register: function (applet_name, applet) {\r
+        _kiwi.applets[applet_name] = applet;\r
+    }\r
+});
+
+
+_kiwi.model.PluginManager = Backbone.Model.extend({\r
+    initialize: function () {\r
+        this.$plugin_holder = $('<div id="kiwi_plugins" style="display:none;"></div>')\r
+            .appendTo(_kiwi.app.view.$el);\r
+\r
+        this.loading_plugins = 0;\r
+        this.loaded_plugins = {};\r
+    },\r
+\r
+    // Load an applet within this panel\r
+    load: function (url) {\r
+        var that = this;\r
+\r
+        if (this.loaded_plugins[url]) {\r
+            this.unload(url);\r
+        }\r
+\r
+        this.loading_plugins++;\r
+\r
+        this.loaded_plugins[url] = $('<div></div>');\r
+        this.loaded_plugins[url].appendTo(this.$plugin_holder)\r
+            .load(url, _.bind(that.pluginLoaded, that));\r
+    },\r
+\r
+\r
+    unload: function (url) {\r
+        if (!this.loaded_plugins[url]) {\r
+            return;\r
+        }\r
+\r
+        this.loaded_plugins[url].remove();\r
+        delete this.loaded_plugins[url];\r
+    },\r
+\r
+\r
+    // Called after each plugin is loaded\r
+    pluginLoaded: function() {\r
+        this.loading_plugins--;\r
+\r
+        if (this.loading_plugins === 0) {\r
+            this.trigger('loaded');\r
+        }\r
+    },\r
+});
+
+
+_kiwi.model.DataStore = Backbone.Model.extend({
+       initialize: function () {
+               this._namespace = '';
+               this.new_data = {};
+       },
+
+       namespace: function (new_namespace) {
+               if (new_namespace) this._namespace = new_namespace;
+               return this._namespace;
+       },
+
+       // Overload the original save() method
+       save: function () {
+               localStorage.setItem(this._namespace, JSON.stringify(this.attributes));
+       },
+
+       // Overload the original load() method
+       load: function () {
+               if (!localStorage) return;
+
+               var data;
+
+               try {
+                       data = JSON.parse(localStorage.getItem(this._namespace)) || {};
+               } catch (error) {
+                       data = {};
+               }
+
+               this.attributes = data;
+       }
+},
+
+{
+       // Generates a new instance of DataStore with a set namespace
+       instance: function (namespace, attributes) {
+               var datastore = new _kiwi.model.DataStore(attributes);
+               datastore.namespace(namespace);
+               return datastore;
+       }
+});
+
+
+_kiwi.model.ChannelInfo = Backbone.Model.extend({
+    initialize: function () {
+        this.view = new _kiwi.view.ChannelInfo({"model": this});
+    }
+});
+
+
+_kiwi.view.Panel = Backbone.View.extend({
+    tagName: "div",
+    className: "panel",
+
+    events: {
+    },
+
+    initialize: function (options) {
+        this.initializePanel(options);
+    },
+
+    initializePanel: function (options) {
+        this.$el.css('display', 'none');
+        options = options || {};
+
+        // Containing element for this panel
+        if (options.container) {
+            this.$container = $(options.container);
+        } else {
+            this.$container = $('#kiwi .panels .container1');
+        }
+
+        this.$el.appendTo(this.$container);
+
+        this.alert_level = 0;
+
+        this.model.set({"view": this}, {"silent": true});
+
+        this.listenTo(this.model, 'change:activity_counter', function(model, new_count) {
+            var $act = this.model.tab.find('.activity');
+
+            if (new_count > 999) {
+                $act.text('999+');
+            } else {
+                $act.text(new_count);
+            }
+
+            if (new_count === 0) {
+                $act.addClass('zero');
+            } else {
+                $act.removeClass('zero');
+            }
+        });
+    },
+
+    render: function () {
+    },
+
+
+    show: function () {
+        var $this = this.$el;
+
+        // Hide all other panels and show this one
+        this.$container.children('.panel').css('display', 'none');
+        $this.css('display', 'block');
+
+        // Show this panels memberlist
+        var members = this.model.get("members");
+        if (members) {
+            _kiwi.app.rightbar.show();
+            members.view.show();
+        } else {
+            _kiwi.app.rightbar.hide();
+        }
+
+        // Remove any alerts and activity counters for this panel
+        this.alert('none');
+        this.model.set('activity_counter', 0);
+
+        _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels().active);
+        this.model.trigger('active', this.model);
+
+        _kiwi.app.view.doLayout();
+
+        if (!this.model.isApplet())
+            this.scrollToBottom(true);
+    },
+
+
+    alert: function (level) {
+        // No need to highlight if this si the active panel
+        if (this.model == _kiwi.app.panels().active) return;
+
+        var types, type_idx;
+        types = ['none', 'action', 'activity', 'highlight'];
+
+        // Default alert level
+        level = level || 'none';
+
+        // If this alert level does not exist, assume clearing current level
+        type_idx = _.indexOf(types, level);
+        if (!type_idx) {
+            level = 'none';
+            type_idx = 0;
+        }
+
+        // Only 'upgrade' the alert. Never down (unless clearing)
+        if (type_idx !== 0 && type_idx <= this.alert_level) {
+            return;
+        }
+
+        // Clear any existing levels
+        this.model.tab.removeClass(function (i, css) {
+            return (css.match(/\balert_\S+/g) || []).join(' ');
+        });
+
+        // Add the new level if there is one
+        if (level !== 'none') {
+            this.model.tab.addClass('alert_' + level);
+        }
+
+        this.alert_level = type_idx;
+    },
+
+
+    // Scroll to the bottom of the panel
+    scrollToBottom: function (force_down) {
+        // If this isn't the active panel, don't scroll
+        if (this.model !== _kiwi.app.panels().active) return;
+
+        // Don't scroll down if we're scrolled up the panel a little
+        if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) {
+            this.$container[0].scrollTop = this.$container[0].scrollHeight;
+        }
+    }
+});
+
+
+_kiwi.view.Channel = _kiwi.view.Panel.extend({
+    events: function(){
+        var parent_events = this.constructor.__super__.events;
+
+        if(_.isFunction(parent_events)){
+            parent_events = parent_events();
+        }
+        return _.extend({}, parent_events, {
+            'click .msg .nick' : 'nickClick',
+            'click .msg .inline-nick' : 'nickClick',
+            "click .chan": "chanClick",
+            'click .media .open': 'mediaClick',
+            'mouseenter .msg .nick': 'msgEnter',
+            'mouseleave .msg .nick': 'msgLeave'
+        });
+    },
+
+    initialize: function (options) {
+        this.initializePanel(options);
+
+        // Container for all the messages
+        this.$messages = $('<div class="messages"></div>');
+        this.$el.append(this.$messages);
+
+        this.model.bind('change:topic', this.topic, this);
+        this.model.bind('change:topic_set_by', this.topicSetBy, this);
+
+        if (this.model.get('members')) {
+            // When we join the memberlist, we have officially joined the channel
+            this.model.get('members').bind('add', function (member) {
+                if (member.get('nick') === this.model.collection.network.get('nick')) {
+                    this.$el.find('.initial_loader').slideUp(function () {
+                        $(this).remove();
+                    });
+                }
+            }, this);
+
+            // Memberlist reset with a new nicklist? Consider we have joined
+            this.model.get('members').bind('reset', function(members) {
+                if (members.getByNick(this.model.collection.network.get('nick'))) {
+                    this.$el.find('.initial_loader').slideUp(function () {
+                        $(this).remove();
+                    });
+                }
+            }, this);
+        }
+
+        // Only show the loader if this is a channel (ie. not a query)
+        if (this.model.isChannel()) {
+            this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;"> ' + _kiwi.global.i18n.translate('client_views_channel_joining').fetch() + ' <span class="loader"></span></div>');
+        }
+
+        this.model.bind('msg', this.newMsg, this);
+        this.msg_count = 0;
+    },
+
+
+    render: function () {
+        var that = this;
+
+        this.$messages.empty();
+        _.each(this.model.get('scrollback'), function (msg) {
+            that.newMsg(msg);
+        });
+    },
+
+
+    newMsg: function(msg) {
+
+        // Parse the msg object into properties fit for displaying
+        msg = this.generateMessageDisplayObj(msg);
+
+        _kiwi.global.events.emit('message:display', {panel: this.model, message: msg})
+        .then(_.bind(function() {
+            var line_msg;
+
+            // Format the nick to the config defined format
+            var display_obj = _.clone(msg);
+            display_obj.nick = styleText('message_nick', {nick: msg.nick, prefix: msg.nick_prefix || ''});
+
+            line_msg = '<div class="msg <%= type %> <%= css_classes %>"><div class="time"><%- time_string %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
+            this.$messages.append($(_.template(line_msg, display_obj)).data('message', msg));
+
+            // Activity/alerts based on the type of new message
+            if (msg.type.match(/^action /)) {
+                this.alert('action');
+
+            } else if (msg.is_highlight) {
+                _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+                _kiwi.app.view.favicon.newHighlight();
+                _kiwi.app.view.playSound('highlight');
+                _kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
+                this.alert('highlight');
+
+            } else {
+                // If this is the active panel, send an alert out
+                if (this.model.isActive()) {
+                    _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+                }
+                this.alert('activity');
+            }
+
+            if (this.model.isQuery() && !this.model.isActive()) {
+                _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+
+                // Highlights have already been dealt with above
+                if (!msg.is_highlight) {
+                    _kiwi.app.view.favicon.newHighlight();
+                }
+
+                _kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
+                _kiwi.app.view.playSound('highlight');
+            }
+
+            // Update the activity counters
+            (function () {
+                // Only inrement the counters if we're not the active panel
+                if (this.model.isActive()) return;
+
+                var count_all_activity = _kiwi.global.settings.get('count_all_activity'),
+                    exclude_message_types, new_count;
+
+                // Set the default config value
+                if (typeof count_all_activity === 'undefined') {
+                    count_all_activity = false;
+                }
+
+                // Do not increment the counter for these message types
+                exclude_message_types = [
+                    'action join',
+                    'action quit',
+                    'action part',
+                    'action kick',
+                    'action nick',
+                    'action mode'
+                ];
+
+                if (count_all_activity || _.indexOf(exclude_message_types, msg.type) === -1) {
+                    new_count = this.model.get('activity_counter') || 0;
+                    new_count++;
+                    this.model.set('activity_counter', new_count);
+                }
+
+            }).apply(this);
+
+            if(this.model.isActive()) this.scrollToBottom();
+
+            // Make sure our DOM isn't getting too large (Acts as scrollback)
+            this.msg_count++;
+            if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
+                $('.msg:first', this.$messages).remove();
+                this.msg_count--;
+            }
+        }, this));
+    },
+
+
+    // Let nicks be clickable + colourise within messages
+    parseMessageNicks: function(word, colourise) {
+        var members, member, style = '';
+
+        members = this.model.get('members');
+        if (!members) {
+            return;
+        }
+
+        member = members.getByNick(word);
+        if (!member) {
+            return;
+        }
+
+        if (colourise !== false) {
+            // Use the nick from the member object so the style matches the letter casing
+            style = this.getNickStyles(member.get('nick')).asCssString();
+        }
+
+        return _.template('<span class="inline-nick" style="<%- style %>;cursor:pointer;" data-nick="<%- nick %>"><%- nick %></span>', {
+            nick: word,
+            style: style
+        });
+
+    },
+
+
+    // Make channels clickable
+    parseMessageChannels: function(word) {
+        var re,
+            parsed = false,
+            network = this.model.get('network');
+
+        if (!network) {
+            return;
+        }
+
+        re = new RegExp('(^|\\s)([' + escapeRegex(network.get('channel_prefix')) + '][^ ,\\007]+)', 'g');
+
+        if (!word.match(re)) {
+            return parsed;
+        }
+
+        parsed = word.replace(re, function (m1, m2) {
+            return m2 + '<a class="chan" data-channel="' + _.escape(m1.trim()) + '">' + _.escape(m1.trim()) + '</a>';
+        });
+
+        return parsed;
+    },
+
+
+    parseMessageUrls: function(word) {
+        var found_a_url = false,
+            parsed_url;
+
+        parsed_url = word.replace(/^(([A-Za-z][A-Za-z0-9\-]*\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w!:.?$'()[\]*,;~+=&%@!\-\/]*)?(#.*)?$/gi, function (url) {
+            var nice = url,
+                extra_html = '';
+
+            // Don't allow javascript execution
+            if (url.match(/^javascript:/)) {
+                return url;
+            }
+
+            found_a_url = true;
+
+            // Add the http if no protoocol was found
+            if (url.match(/^www\./)) {
+                url = 'http://' + url;
+            }
+
+            // Shorten the displayed URL if it's going to be too long
+            if (nice.length > 100) {
+                nice = nice.substr(0, 100) + '...';
+            }
+
+            // Get any media HTML if supported
+            extra_html = _kiwi.view.MediaMessage.buildHtml(url);
+
+            // Make the link clickable
+            return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url.replace(/"/g, '%22') + '">' + _.escape(nice) + '</a>' + extra_html;
+        });
+
+        return found_a_url ? parsed_url : false;
+    },
+
+
+    // Sgnerate a css style for a nick
+    getNickStyles: function(nick) {
+        var ret, colour, nick_int = 0, rgb, nick_lightness;
+
+        // Get a colour from a nick (Method based on IRSSIs nickcolor.pl)
+        _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
+
+        nick_lightness = (_.find(_kiwi.app.themes, function (theme) {
+            return theme.name.toLowerCase() === _kiwi.global.settings.get('theme').toLowerCase();
+        }) || {}).nick_lightness;
+
+        if (typeof nick_lightness !== 'number') {
+            nick_lightness = 35;
+        } else {
+            nick_lightness = Math.max(0, Math.min(100, nick_lightness));
+        }
+
+        rgb = hsl2rgb(nick_int % 255, 70, nick_lightness);
+        rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
+        colour = '#' + rgb.toString(16);
+
+        ret = {color: colour};
+        ret.asCssString = function() {
+            return _.reduce(this, function(result, item, key){
+                return result + key + ':' + item + ';';
+            }, '');
+        };
+
+        return ret;
+    },
+
+
+    // Takes an IRC message object and parses it for displaying
+    generateMessageDisplayObj: function(msg) {
+        var nick_hex, time_difference,
+            message_words,
+            sb = this.model.get('scrollback'),
+            prev_msg = sb[sb.length-2],
+            hour, pm, am_pm_locale_key;
+
+        // Clone the msg object so we dont modify the original
+        msg = _.clone(msg);
+
+        // Defaults
+        msg.css_classes = '';
+        msg.nick_style = '';
+        msg.is_highlight = false;
+        msg.time_string = '';
+
+
+        // Nick highlight detecting
+        var nick = _kiwi.app.connections.active_connection.get('nick');
+        if ((new RegExp('(^|\\W)(' + escapeRegex(nick) + ')(\\W|$)', 'i')).test(msg.msg)) {
+            // Do not highlight the user's own input
+            if (msg.nick.localeCompare(nick) !== 0) {
+                msg.is_highlight = true;
+                msg.css_classes += ' highlight';
+            }
+        }
+
+        message_words = msg.msg.split(' ');
+        message_words = _.map(message_words, function(word) {
+            var parsed_word;
+
+            parsed_word = this.parseMessageUrls(word);
+            if (typeof parsed_word === 'string') return parsed_word;
+
+            parsed_word = this.parseMessageChannels(word);
+            if (typeof parsed_word === 'string') return parsed_word;
+
+            parsed_word = this.parseMessageNicks(word, (msg.type === 'privmsg'));
+            if (typeof parsed_word === 'string') return parsed_word;
+
+            parsed_word = _.escape(word);
+
+            // Replace text emoticons with images
+            if (_kiwi.global.settings.get('show_emoticons')) {
+                parsed_word = emoticonFromText(parsed_word);
+            }
+
+            return parsed_word;
+        }, this);
+
+        msg.unparsed_msg = msg.msg;
+        msg.msg = message_words.join(' ');
+
+        // Convert IRC formatting into HTML formatting
+        msg.msg = formatIRCMsg(msg.msg);
+
+        // Add some style to the nick
+        msg.nick_style = this.getNickStyles(msg.nick).asCssString();
+
+        // Generate a hex string from the nick to be used as a CSS class name
+        nick_hex = '';
+        if (msg.nick) {
+            _.map(msg.nick.split(''), function (char) {
+                nick_hex += char.charCodeAt(0).toString(16);
+            });
+            msg.css_classes += ' nick_' + nick_hex;
+        }
+
+        if (prev_msg) {
+            // Time difference between this message and the last (in minutes)
+            time_difference = (msg.time.getTime() - prev_msg.time.getTime())/1000/60;
+            if (prev_msg.nick === msg.nick && time_difference < 1) {
+                msg.css_classes += ' repeated_nick';
+            }
+        }
+
+        // Build up and add the line
+        if (_kiwi.global.settings.get('use_24_hour_timestamps')) {
+            msg.time_string = msg.time.getHours().toString().lpad(2, "0") + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0");
+        } else {
+            hour = msg.time.getHours();
+            pm = hour > 11;
+
+            hour = hour % 12;
+            if (hour === 0)
+                hour = 12;
+
+            am_pm_locale_key = pm ?
+                'client_views_panel_timestamp_pm' :
+                'client_views_panel_timestamp_am';
+
+            msg.time_string = translateText(am_pm_locale_key, hour + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0"));
+        }
+
+        return msg;
+    },
+
+
+    topic: function (topic) {
+        if (typeof topic !== 'string' || !topic) {
+            topic = this.model.get("topic");
+        }
+
+        this.model.addMsg('', styleText('channel_topic', {text: topic, channel: this.model.get('name')}), 'topic');
+
+        // If this is the active channel then update the topic bar
+        if (_kiwi.app.panels().active === this.model) {
+            _kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
+        }
+    },
+
+    topicSetBy: function (topic) {
+        // If this is the active channel then update the topic bar
+        if (_kiwi.app.panels().active === this.model) {
+            _kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
+        }
+    },
+
+    // Click on a nickname
+    nickClick: function (event) {
+        var $target = $(event.currentTarget),
+            nick,
+            members = this.model.get('members'),
+            member;
+
+        event.stopPropagation();
+
+        // Check this current element for a nick before resorting to the main message
+        // (eg. inline nicks has the nick on its own element within the message)
+        nick = $target.data('nick');
+        if (!nick) {
+            nick = $target.parent('.msg').data('message').nick;
+        }
+
+        // Make sure this nick is still in the channel
+        member = members ? members.getByNick(nick) : null;
+        if (!member) {
+            return;
+        }
+
+        _kiwi.global.events.emit('nick:select', {target: $target, member: member, source: 'message'})
+        .then(_.bind(this.openUserMenuForNick, this, $target, member));
+    },
+
+
+    updateLastSeenMarker: function() {
+        if (this.model.isActive()) {
+            // Remove the previous last seen classes
+            this.$(".last_seen").removeClass("last_seen");
+
+            // Mark the last message the user saw
+            this.$messages.children().last().addClass("last_seen");
+        }
+    },
+
+
+    openUserMenuForNick: function ($target, member) {
+        var members = this.model.get('members'),
+            are_we_an_op = !!members.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op'),
+            userbox, menubox;
+
+        userbox = new _kiwi.view.UserBox();
+        userbox.setTargets(member, this.model);
+        userbox.displayOpItems(are_we_an_op);
+
+        menubox = new _kiwi.view.MenuBox(member.get('nick') || 'User');
+        menubox.addItem('userbox', userbox.$el);
+        menubox.showFooter(false);
+
+        _kiwi.global.events.emit('usermenu:created', {menu: menubox, userbox: userbox, user: member})
+        .then(_.bind(function() {
+            menubox.show();
+
+            // Position the userbox + menubox
+            var target_offset = $target.offset(),
+                t = target_offset.top,
+                m_bottom = t + menubox.$el.outerHeight(),  // Where the bottom of menu will be
+                memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight();
+
+            // If the bottom of the userbox is going to be too low.. raise it
+            if (m_bottom > memberlist_bottom){
+                t = memberlist_bottom - menubox.$el.outerHeight();
+            }
+
+            // Set the new positon
+            menubox.$el.offset({
+                left: target_offset.left,
+                top: t
+            });
+        }, this))
+        .catch(_.bind(function() {
+            userbox = null;
+
+            menu.dispose();
+            menu = null;
+        }, this));
+    },
+
+
+    chanClick: function (event) {
+        var target = (event.target) ? $(event.target).data('channel') : $(event.srcElement).data('channel');
+
+        _kiwi.app.connections.active_connection.gateway.join(target);
+    },
+
+
+    mediaClick: function (event) {
+        var $media = $(event.target).parents('.media');
+        var media_message;
+
+        if ($media.data('media')) {
+            media_message = $media.data('media');
+        } else {
+            media_message = new _kiwi.view.MediaMessage({el: $media[0]});
+
+            // Cache this MediaMessage instance for when it's opened again
+            $media.data('media', media_message);
+        }
+
+        media_message.toggle();
+    },
+
+
+    // Cursor hovers over a message
+    msgEnter: function (event) {
+        var nick_class;
+
+        // Find a valid class that this element has
+        _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
+            if (css_class.match(/^nick_[a-z0-9]+/i)) {
+                nick_class = css_class;
+            }
+        });
+
+        // If no class was found..
+        if (!nick_class) return;
+
+        $('.'+nick_class).addClass('global_nick_highlight');
+    },
+
+
+    // Cursor leaves message
+    msgLeave: function (event) {
+        var nick_class;
+
+        // Find a valid class that this element has
+        _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
+            if (css_class.match(/^nick_[a-z0-9]+/i)) {
+                nick_class = css_class;
+            }
+        });
+
+        // If no class was found..
+        if (!nick_class) return;
+
+        $('.'+nick_class).removeClass('global_nick_highlight');
+    }
+});
+
+
+
+_kiwi.view.Applet = _kiwi.view.Panel.extend({
+    className: 'panel applet',
+    initialize: function (options) {
+        this.initializePanel(options);
+    }
+});
+
+
+_kiwi.view.Application = Backbone.View.extend({
+    initialize: function () {
+        var that = this;
+
+        this.$el = $($('#tmpl_application').html().trim());
+        this.el = this.$el[0];
+
+        $(this.model.get('container') || 'body').append(this.$el);
+
+        this.elements = {
+            panels:        this.$el.find('.panels'),
+            right_bar:     this.$el.find('.right_bar'),
+            toolbar:       this.$el.find('.toolbar'),
+            controlbox:    this.$el.find('.controlbox'),
+            resize_handle: this.$el.find('.memberlists_resize_handle')
+        };
+
+        $(window).resize(function() { that.doLayout.apply(that); });
+        this.elements.toolbar.resize(function() { that.doLayout.apply(that); });
+        this.elements.controlbox.resize(function() { that.doLayout.apply(that); });
+
+        // Change the theme when the config is changed
+        _kiwi.global.settings.on('change:theme', this.updateTheme, this);
+        this.updateTheme(getQueryVariable('theme'));
+
+        _kiwi.global.settings.on('change:channel_list_style', this.setTabLayout, this);
+        this.setTabLayout(_kiwi.global.settings.get('channel_list_style'));
+
+        _kiwi.global.settings.on('change:show_timestamps', this.displayTimestamps, this);
+        this.displayTimestamps(_kiwi.global.settings.get('show_timestamps'));
+
+        this.$el.appendTo($('body'));
+        this.doLayout();
+
+        $(document).keydown(this.setKeyFocus);
+
+        // Confirmation require to leave the page
+        window.onbeforeunload = function () {
+            if (_kiwi.gateway.isConnected()) {
+                return _kiwi.global.i18n.translate('client_views_application_close_notice').fetch();
+            }
+        };
+
+        // Keep tabs on the browser having focus
+        this.has_focus = true;
+
+        $(window).on('focus', function windowOnFocus() {
+            that.has_focus = true;
+        });
+
+        $(window).on('blur', function windowOnBlur() {
+            var active_panel = that.model.panels().active;
+            if (active_panel && active_panel.view.updateLastSeenMarker) {
+                active_panel.view.updateLastSeenMarker();
+            }
+
+            that.has_focus = false;
+        });
+
+        // If we get a touchstart event, make note of it so we know we're using a touchscreen
+        $(window).on('touchstart', function windowOnTouchstart() {
+            that.$el.addClass('touch');
+            $(window).off('touchstart', windowOnTouchstart);
+        });
+
+
+        this.favicon = new _kiwi.view.Favicon();
+        this.initSound();
+
+        this.monitorPanelFallback();
+    },
+
+
+
+    updateTheme: function (theme_name) {
+        // If called by the settings callback, get the correct new_value
+        if (theme_name === _kiwi.global.settings) {
+            theme_name = arguments[1];
+        }
+
+        // If we have no theme specified, get it from the settings
+        if (!theme_name) theme_name = _kiwi.global.settings.get('theme') || 'relaxed';
+
+        theme_name = theme_name.toLowerCase();
+
+        // Clear any current theme
+        $('[data-theme]:not([disabled])').each(function (idx, link) {
+            var $link = $(link);
+            $link.attr('rel', 'alternate ' + $link.attr('rel')).attr('disabled', true)[0].disabled = true;
+        });
+
+        // Apply the new theme
+        var link = $('[data-theme][title=' + theme_name + ']');
+        if (link.length > 0) {
+            link.attr('rel', 'stylesheet').attr('disabled', false)[0].disabled = false;
+        }
+
+        this.doLayout();
+    },
+
+
+    setTabLayout: function (layout_style) {
+        // If called by the settings callback, get the correct new_value
+        if (layout_style === _kiwi.global.settings) {
+            layout_style = arguments[1];
+        }
+
+        if (layout_style == 'list') {
+            this.$el.addClass('chanlist_treeview');
+        } else {
+            this.$el.removeClass('chanlist_treeview');
+        }
+
+        this.doLayout();
+    },
+
+
+    displayTimestamps: function (show_timestamps) {
+        // If called by the settings callback, get the correct new_value
+        if (show_timestamps === _kiwi.global.settings) {
+            show_timestamps = arguments[1];
+        }
+
+        if (show_timestamps) {
+            this.$el.addClass('timestamps');
+        } else {
+            this.$el.removeClass('timestamps');
+        }
+    },
+
+
+    // Globally shift focus to the command input box on a keypress
+    setKeyFocus: function (ev) {
+        // If we're copying text, don't shift focus
+        if (ev.ctrlKey || ev.altKey || ev.metaKey) {
+            return;
+        }
+
+        // If we're typing into an input box somewhere, ignore
+        if ((ev.target.tagName.toLowerCase() === 'input') || (ev.target.tagName.toLowerCase() === 'textarea') || $(ev.target).attr('contenteditable')) {
+            return;
+        }
+
+        $('#kiwi .controlbox .inp').focus();
+    },
+
+
+    doLayout: function () {
+        var $kiwi = this.$el;
+        var $panels = this.elements.panels;
+        var $right_bar = this.elements.right_bar;
+        var $toolbar = this.elements.toolbar;
+        var $controlbox = this.elements.controlbox;
+        var $resize_handle = this.elements.resize_handle;
+
+        if (!$kiwi.is(':visible')) {
+            return;
+        }
+
+        var css_heights = {
+            top: $toolbar.outerHeight(true),
+            bottom: $controlbox.outerHeight(true)
+        };
+
+
+        // If any elements are not visible, full size the panals instead
+        if (!$toolbar.is(':visible')) {
+            css_heights.top = 0;
+        }
+
+        if (!$controlbox.is(':visible')) {
+            css_heights.bottom = 0;
+        }
+
+        // Apply the CSS sizes
+        $panels.css(css_heights);
+        $right_bar.css(css_heights);
+        $resize_handle.css(css_heights);
+
+        // If we have channel tabs on the side, adjust the height
+        if ($kiwi.hasClass('chanlist_treeview')) {
+            this.$el.find('.tabs', $kiwi).css(css_heights);
+        }
+
+        // Determine if we have a narrow window (mobile/tablet/or even small desktop window)
+        if ($kiwi.outerWidth() < 420) {
+            $kiwi.addClass('narrow');
+            if (this.model.rightbar && this.model.rightbar.keep_hidden !== true)
+                this.model.rightbar.toggle(true);
+        } else {
+            $kiwi.removeClass('narrow');
+            if (this.model.rightbar && this.model.rightbar.keep_hidden !== false)
+                this.model.rightbar.toggle(false);
+        }
+
+        // Set the panels width depending on the memberlist visibility
+        if (!$right_bar.hasClass('disabled')) {
+            // Panels to the side of the memberlist
+            $panels.css('right', $right_bar.outerWidth(true));
+            // The resize handle sits overlapping the panels and memberlist
+            $resize_handle.css('left', $right_bar.position().left - ($resize_handle.outerWidth(true) / 2));
+        } else {
+            // Memberlist is hidden so panels to the right edge
+            $panels.css('right', 0);
+            // And move the handle just out of sight to the right
+            $resize_handle.css('left', $panels.outerWidth(true));
+        }
+
+        var input_wrap_width = parseInt($controlbox.find('.input_tools').outerWidth(), 10);
+        $controlbox.find('.input_wrap').css('right', input_wrap_width + 7);
+    },
+
+
+    alertWindow: function (title) {
+        if (!this.alertWindowTimer) {
+            this.alertWindowTimer = new (function () {
+                var that = this;
+                var tmr;
+                var has_focus = true;
+                var state = 0;
+                var default_title = _kiwi.app.server_settings.client.window_title || 'Kiwi IRC';
+                var title = 'Kiwi IRC';
+
+                this.setTitle = function (new_title) {
+                    new_title = new_title || default_title;
+                    window.document.title = new_title;
+                    return new_title;
+                };
+
+                this.start = function (new_title) {
+                    // Don't alert if we already have focus
+                    if (has_focus) return;
+
+                    title = new_title;
+                    if (tmr) return;
+                    tmr = setInterval(this.update, 1000);
+                };
+
+                this.stop = function () {
+                    // Stop the timer and clear the title
+                    if (tmr) clearInterval(tmr);
+                    tmr = null;
+                    this.setTitle();
+
+                    // Some browsers don't always update the last title correctly
+                    // Wait a few seconds and then reset
+                    setTimeout(this.reset, 2000);
+                };
+
+                this.reset = function () {
+                    if (tmr) return;
+                    that.setTitle();
+                };
+
+
+                this.update = function () {
+                    if (state === 0) {
+                        that.setTitle(title);
+                        state = 1;
+                    } else {
+                        that.setTitle();
+                        state = 0;
+                    }
+                };
+
+                $(window).focus(function (event) {
+                    has_focus = true;
+                    that.stop();
+
+                    // Some browsers don't always update the last title correctly
+                    // Wait a few seconds and then reset
+                    setTimeout(that.reset, 2000);
+                });
+
+                $(window).blur(function (event) {
+                    has_focus = false;
+                });
+            })();
+        }
+
+        this.alertWindowTimer.start(title);
+    },
+
+
+    barsHide: function (instant) {
+        var that = this;
+
+        if (!instant) {
+            this.$el.find('.toolbar').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
+            $('#kiwi .controlbox').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
+        } else {
+            this.$el.find('.toolbar').slideUp(0);
+            $('#kiwi .controlbox').slideUp(0);
+            this.doLayout();
+        }
+    },
+
+    barsShow: function (instant) {
+        var that = this;
+
+        if (!instant) {
+            this.$el.find('.toolbar').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
+            $('#kiwi .controlbox').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
+        } else {
+            this.$el.find('.toolbar').slideDown(0);
+            $('#kiwi .controlbox').slideDown(0);
+            this.doLayout();
+        }
+    },
+
+
+    initSound: function () {
+        var that = this,
+            base_path = this.model.get('base_path');
+
+        $script(base_path + '/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js', function() {
+            if (typeof soundManager === 'undefined')
+                return;
+
+            soundManager.setup({
+                url: base_path + '/assets/libs/soundmanager2/',
+                flashVersion: 9, // optional: shiny features (default = 8)// optional: ignore Flash where possible, use 100% HTML5 mode
+                preferFlash: true,
+
+                onready: function() {
+                    that.sound_object = soundManager.createSound({
+                        id: 'highlight',
+                        url: base_path + '/assets/sound/highlight.mp3'
+                    });
+                }
+            });
+        });
+    },
+
+
+    playSound: function (sound_id) {
+        if (!this.sound_object) return;
+
+        if (_kiwi.global.settings.get('mute_sounds'))
+            return;
+
+        soundManager.play(sound_id);
+    },
+
+
+    showNotification: function(title, message) {
+        var icon = this.model.get('base_path') + '/assets/img/ico.png',
+            notifications = _kiwi.utils.notifications;
+
+        if (!this.has_focus && notifications.allowed()) {
+            notifications
+                .create(title, { icon: icon, body: message })
+                .closeAfter(5000)
+                .on('click', _.bind(window.focus, window));
+        }
+    },
+
+    monitorPanelFallback: function() {
+        var panel_access = [];
+
+        this.model.panels.on('active', function() {
+            var panel = _kiwi.app.panels().active,
+                panel_index;
+
+            // If the panel is already open, remove it so we can put it back in first place
+            panel_index = _.indexOf(panel_access, panel.cid);
+
+            if (panel_index > -1) {
+                panel_access.splice(panel_index, 1);
+            }
+
+            //Make this panel the most recently accessed
+            panel_access.unshift(panel.cid);
+        });
+
+        this.model.panels.on('remove', function(panel) {
+            // If closing the active panel, switch to the last-accessed panel
+            if (panel_access[0] === panel.cid) {
+                panel_access.shift();
+
+                //Get the last-accessed panel model now that we removed the closed one
+                var model = _.find(_kiwi.app.panels('applets').concat(_kiwi.app.panels('connections')), {cid: panel_access[0]});
+
+                if (model) {
+                    model.view.show();
+                }
+            }
+        });
+    }
+});
+
+
+
+_kiwi.view.AppToolbar = Backbone.View.extend({
+    events: {
+        'click .settings': 'clickSettings',
+        'click .startup': 'clickStartup'
+    },
+
+    initialize: function () {
+        // Remove the new connection/startup link if the server has disabled server changing
+        if (_kiwi.app.server_settings.connection && !_kiwi.app.server_settings.connection.allow_change) {
+            this.$('.startup').css('display', 'none');
+        }
+    },
+
+    clickSettings: function (event) {
+        event.preventDefault();
+        _kiwi.app.controlbox.processInput('/settings');
+    },
+
+    clickStartup: function (event) {
+        event.preventDefault();
+        _kiwi.app.startup_applet.view.show();
+    }
+});
+
+
+
+_kiwi.view.ControlBox = Backbone.View.extend({
+    events: {
+        'keydown .inp': 'process',
+        'click .nick': 'showNickChange'
+    },
+
+    initialize: function () {
+        var that = this;
+
+        this.buffer = [];  // Stores previously run commands
+        this.buffer_pos = 0;  // The current position in the buffer
+
+        this.preprocessor = new InputPreProcessor();
+        this.preprocessor.recursive_depth = 5;
+
+        // Hold tab autocomplete data
+        this.tabcomplete = {active: false, data: [], prefix: ''};
+
+        // Keep the nick view updated with nick changes
+        _kiwi.app.connections.on('change:nick', function(connection) {
+            // Only update the nick view if it's the active connection
+            if (connection !== _kiwi.app.connections.active_connection)
+                return;
+
+            $('.nick', that.$el).text(connection.get('nick'));
+        });
+
+        // Update our nick view as we flick between connections
+        _kiwi.app.connections.on('active', function(panel, connection) {
+            $('.nick', that.$el).text(connection.get('nick'));
+        });
+
+        // Keep focus on the input box as we flick between panels
+        _kiwi.app.panels.bind('active', function (active_panel) {
+            if (active_panel.isChannel() || active_panel.isServer() || active_panel.isQuery()) {
+                that.$('.inp').focus();
+            }
+        });
+    },
+
+    render: function() {
+        var send_message_text = translateText('client_views_controlbox_message');
+        this.$('.inp').attr('placeholder', send_message_text);
+
+        return this;
+    },
+
+    showNickChange: function (ev) {
+        // Nick box already open? Don't do it again
+        if (this.nick_change)
+            return;
+
+        this.nick_change = new _kiwi.view.NickChangeBox();
+        this.nick_change.render();
+
+        this.listenTo(this.nick_change, 'close', function() {
+            delete this.nick_change;
+        });
+    },
+
+    process: function (ev) {
+        var that = this,
+            inp = $(ev.currentTarget),
+            inp_val = inp.val(),
+            meta;
+
+        if (navigator.appVersion.indexOf("Mac") !== -1) {
+            meta = ev.metaKey;
+        } else {
+            meta = ev.altKey;
+        }
+
+        // If not a tab key, reset the tabcomplete data
+        if (this.tabcomplete.active && ev.keyCode !== 9) {
+            this.tabcomplete.active = false;
+            this.tabcomplete.data = [];
+            this.tabcomplete.prefix = '';
+        }
+
+        switch (true) {
+        case (ev.keyCode === 13):              // return
+            inp_val = inp_val.trim();
+
+            if (inp_val) {
+                $.each(inp_val.split('\n'), function (idx, line) {
+                    that.processInput(line);
+                });
+
+                this.buffer.push(inp_val);
+                this.buffer_pos = this.buffer.length;
+            }
+
+            inp.val('');
+            return false;
+
+            break;
+
+        case (ev.keyCode === 38):              // up
+            if (this.buffer_pos > 0) {
+                this.buffer_pos--;
+                inp.val(this.buffer[this.buffer_pos]);
+            }
+            //suppress browsers default behavior as it would set the cursor at the beginning
+            return false;
+
+        case (ev.keyCode === 40):              // down
+            if (this.buffer_pos < this.buffer.length) {
+                this.buffer_pos++;
+                inp.val(this.buffer[this.buffer_pos]);
+            }
+            break;
+
+        case (ev.keyCode === 219 && meta):            // [ + meta
+            // Find all the tab elements and get the index of the active tab
+            var $tabs = $('#kiwi .tabs').find('li[class!=connection]');
+            var cur_tab_ind = (function() {
+                for (var idx=0; idx<$tabs.length; idx++){
+                    if ($($tabs[idx]).hasClass('active'))
+                        return idx;
+                }
+            })();
+
+            // Work out the previous tab along. Wrap around if needed
+            if (cur_tab_ind === 0) {
+                $prev_tab = $($tabs[$tabs.length - 1]);
+            } else {
+                $prev_tab = $($tabs[cur_tab_ind - 1]);
+            }
+
+            $prev_tab.click();
+            return false;
+
+        case (ev.keyCode === 221 && meta):            // ] + meta
+            // Find all the tab elements and get the index of the active tab
+            var $tabs = $('#kiwi .tabs').find('li[class!=connection]');
+            var cur_tab_ind = (function() {
+                for (var idx=0; idx<$tabs.length; idx++){
+                    if ($($tabs[idx]).hasClass('active'))
+                        return idx;
+                }
+            })();
+
+            // Work out the next tab along. Wrap around if needed
+            if (cur_tab_ind === $tabs.length - 1) {
+                $next_tab = $($tabs[0]);
+            } else {
+                $next_tab = $($tabs[cur_tab_ind + 1]);
+            }
+
+            $next_tab.click();
+            return false;
+
+        case (ev.keyCode === 9     //Check if ONLY tab is pressed
+            && !ev.shiftKey        //(user could be using some browser
+            && !ev.altKey          //keyboard shortcut)
+            && !ev.metaKey
+            && !ev.ctrlKey):
+            this.tabcomplete.active = true;
+            if (_.isEqual(this.tabcomplete.data, [])) {
+                // Get possible autocompletions
+                var ac_data = [],
+                    members = _kiwi.app.panels().active.get('members');
+
+                // If we have a members list, get the models. Otherwise empty array
+                members = members ? members.models : [];
+
+                $.each(members, function (i, member) {
+                    if (!member) return;
+                    ac_data.push(member.get('nick'));
+                });
+
+                ac_data.push(_kiwi.app.panels().active.get('name'));
+
+                ac_data = _.sortBy(ac_data, function (nick) {
+                    return nick.toLowerCase();
+                });
+                this.tabcomplete.data = ac_data;
+            }
+
+            if (inp_val[inp[0].selectionStart - 1] === ' ') {
+                return false;
+            }
+
+            (function () {
+                var tokens,              // Words before the cursor position
+                    val,                 // New value being built up
+                    p1,                  // Position in the value just before the nick
+                    newnick,             // New nick to be displayed (cycles through)
+                    range,               // TextRange for setting new text cursor position
+                    nick,                // Current nick in the value
+                    trailing = ': ';     // Text to be inserted after a tabbed nick
+
+                tokens = inp_val.substring(0, inp[0].selectionStart).split(' ');
+                if (tokens[tokens.length-1] == ':')
+                    tokens.pop();
+
+                // Only add the trailing text if not at the beginning of the line
+                if (tokens.length > 1)
+                    trailing = '';
+
+                nick  = tokens[tokens.length - 1];
+
+                if (this.tabcomplete.prefix === '') {
+                    this.tabcomplete.prefix = nick;
+                }
+
+                this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {
+                    return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);
+                });
+
+                if (this.tabcomplete.data.length > 0) {
+                    // Get the current value before cursor position
+                    p1 = inp[0].selectionStart - (nick.length);
+                    val = inp_val.substr(0, p1);
+
+                    // Include the current selected nick
+                    newnick = this.tabcomplete.data.shift();
+                    this.tabcomplete.data.push(newnick);
+                    val += newnick;
+
+                    if (inp_val.substr(inp[0].selectionStart, 2) !== trailing)
+                        val += trailing;
+
+                    // Now include the rest of the current value
+                    val += inp_val.substr(inp[0].selectionStart);
+
+                    inp.val(val);
+
+                    // Move the cursor position to the end of the nick
+                    if (inp[0].setSelectionRange) {
+                        inp[0].setSelectionRange(p1 + newnick.length + trailing.length, p1 + newnick.length + trailing.length);
+                    } else if (inp[0].createTextRange) { // not sure if this bit is actually needed....
+                        range = inp[0].createTextRange();
+                        range.collapse(true);
+                        range.moveEnd('character', p1 + newnick.length + trailing.length);
+                        range.moveStart('character', p1 + newnick.length + trailing.length);
+                        range.select();
+                    }
+                }
+            }).apply(this);
+            return false;
+        }
+    },
+
+
+    processInput: function (command_raw) {
+        var that = this,
+            command, params, events_data,
+            pre_processed;
+
+        // If sending a message when not in a channel or query window, automatically
+        // convert it into a command
+        if (command_raw[0] !== '/' && !_kiwi.app.panels().active.isChannel() && !_kiwi.app.panels().active.isQuery()) {
+            command_raw = '/' + command_raw;
+        }
+
+        // The default command
+        if (command_raw[0] !== '/' || command_raw.substr(0, 2) === '//') {
+            // Remove any slash escaping at the start (ie. //)
+            command_raw = command_raw.replace(/^\/\//, '/');
+
+            // Prepend the default command
+            command_raw = '/msg ' + _kiwi.app.panels().active.get('name') + ' ' + command_raw;
+        }
+
+        // Process the raw command for any aliases
+        this.preprocessor.vars.server = _kiwi.app.connections.active_connection.get('name');
+        this.preprocessor.vars.channel = _kiwi.app.panels().active.get('name');
+        this.preprocessor.vars.destination = this.preprocessor.vars.channel;
+        command_raw = this.preprocessor.process(command_raw);
+
+        // Extract the command and parameters
+        params = command_raw.split(/\s/);
+        if (params[0][0] === '/') {
+            command = params[0].substr(1).toLowerCase();
+            params = params.splice(1, params.length - 1);
+        } else {
+            // Default command
+            command = 'msg';
+            params.unshift(_kiwi.app.panels().active.get('name'));
+        }
+
+        // Emit a plugin event for any modifications
+        events_data = {command: command, params: params};
+
+        _kiwi.global.events.emit('command', events_data)
+        .then(function() {
+            // Trigger the command events
+            that.trigger('command', {command: events_data.command, params: events_data.params});
+            that.trigger('command:' + events_data.command, {command: events_data.command, params: events_data.params});
+
+            // If we didn't have any listeners for this event, fire a special case
+            // TODO: This feels dirty. Should this really be done..?
+            if (!that._events['command:' + events_data.command]) {
+                that.trigger('unknown_command', {command: events_data.command, params: events_data.params});
+            }
+        });
+    },
+
+
+    addPluginIcon: function ($icon) {
+        var $tool = $('<div class="tool"></div>').append($icon);
+        this.$el.find('.input_tools').append($tool);
+        _kiwi.app.view.doLayout();
+    }
+});
+
+
+
+_kiwi.view.Favicon = Backbone.View.extend({
+    initialize: function () {
+        var that = this,
+            $win = $(window);
+
+        this.has_focus = true;
+        this.highlight_count = 0;
+        // Check for html5 canvas support
+        this.has_canvas_support = !!window.CanvasRenderingContext2D;
+
+        // Store the original favicon
+        this.original_favicon = $('link[rel~="icon"]')[0].href;
+
+        // Create our favicon canvas
+        this._createCanvas();
+
+        // Reset favicon notifications when user focuses window
+        $win.on('focus', function () {
+            that.has_focus = true;
+            that._resetHighlights();
+        });
+        $win.on('blur', function () {
+            that.has_focus = false;
+        });
+    },
+
+    newHighlight: function () {
+        var that = this;
+        if (!this.has_focus) {
+            this.highlight_count++;
+            if (this.has_canvas_support) {
+                this._drawFavicon(function() {
+                    that._drawBubble(that.highlight_count.toString());
+                    that._refreshFavicon(that.canvas.toDataURL());
+                });
+            }
+        }
+    },
+
+    _resetHighlights: function () {
+        var that = this;
+        this.highlight_count = 0;
+        this._refreshFavicon(this.original_favicon);
+    },
+
+    _drawFavicon: function (callback) {
+        var that = this,
+            canvas = this.canvas,
+            context = canvas.getContext('2d'),
+            favicon_image = new Image();
+
+        // Allow cross origin resource requests
+        favicon_image.crossOrigin = 'anonymous';
+        // Trigger the load event
+        favicon_image.src = this.original_favicon;
+
+        favicon_image.onload = function() {
+            // Clear canvas from prevous iteration
+            context.clearRect(0, 0, canvas.width, canvas.height);
+            // Draw the favicon itself
+            context.drawImage(favicon_image, 0, 0, canvas.width, canvas.height);
+            callback();
+        };
+    },
+
+    _drawBubble: function (label) {
+        var letter_spacing,
+            bubble_width = 0, bubble_height = 0,
+            canvas = this.canvas,
+            context = test_context = canvas.getContext('2d'),
+            canvas_width = canvas.width,
+            canvas_height = canvas.height;
+
+        // Different letter spacing for MacOS 
+        if (navigator.appVersion.indexOf("Mac") !== -1) {
+            letter_spacing = -1.5;
+        }
+        else {
+            letter_spacing = -1;
+        }
+
+        // Setup a test canvas to get text width
+        test_context.font = context.font = 'bold 10px Arial';
+        test_context.textAlign = 'right';
+        this._renderText(test_context, label, 0, 0, letter_spacing);
+
+        // Calculate bubble width based on letter spacing and padding
+        bubble_width = test_context.measureText(label).width + letter_spacing * (label.length - 1) + 2;
+        // Canvas does not have any way of measuring text height, so we just do it manually and add 1px top/bottom padding
+        bubble_height = 9;
+
+        // Set bubble coordinates
+        bubbleX = canvas_width - bubble_width;
+        bubbleY = canvas_height - bubble_height;
+
+        // Draw bubble background
+        context.fillStyle = 'red';
+        context.fillRect(bubbleX, bubbleY, bubble_width, bubble_height);
+
+        // Draw the text
+        context.fillStyle = 'white';
+        this._renderText(context, label, canvas_width - 1, canvas_height - 1, letter_spacing);
+    },
+
+    _refreshFavicon: function (url) {
+        $('link[rel~="icon"]').remove();
+        $('<link rel="shortcut icon" href="' + url + '">').appendTo($('head'));
+    },
+
+    _createCanvas: function () {
+        var canvas = document.createElement('canvas');
+            canvas.width = 16;
+            canvas.height = 16;
+        
+        this.canvas = canvas;
+    },
+
+    _renderText: function (context, text, x, y, letter_spacing) {
+        // A hacky solution for letter-spacing, but works well with small favicon text
+        // Modified from http://jsfiddle.net/davidhong/hKbJ4/
+        var current,
+            characters = text.split('').reverse(),
+            index = 0,
+            currentPosition = x;
+
+        while (index < text.length) {
+            current = characters[index++];
+            context.fillText(current, currentPosition, y);
+            currentPosition += (-1 * (context.measureText(current).width + letter_spacing));
+        }
+
+        return context;
+    }
+});
+
+
+
+_kiwi.view.MediaMessage = Backbone.View.extend({
+    events: {
+        'click .media_close': 'close'
+    },
+
+    initialize: function () {
+        // Get the URL from the data
+        this.url = this.$el.data('url');
+    },
+
+    toggle: function () {
+        if (!this.$content || !this.$content.is(':visible')) {
+            this.open();
+        } else {
+            this.close();
+        }
+    },
+
+    // Close the media content and remove it from display
+    close: function () {
+        var that = this;
+        this.$content.slideUp('fast', function () {
+            that.$content.remove();
+        });
+    },
+
+    // Open the media content within its wrapper
+    open: function () {
+        // Create the content div if we haven't already
+        if (!this.$content) {
+            this.$content = $('<div class="media_content"><a class="media_close"><i class="fa fa-chevron-up"></i> ' + _kiwi.global.i18n.translate('client_views_mediamessage_close').fetch() + '</a><br /><div class="content"></div></div>');
+            this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || _kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch() + ' :(');
+        }
+
+        // Now show the content if not already
+        if (!this.$content.is(':visible')) {
+            // Hide it first so the slideDown always plays
+            this.$content.hide();
+
+            // Add the media content and slide it into view
+            this.$el.append(this.$content);
+            this.$content.slideDown();
+        }
+    },
+
+
+
+    // Generate the media content for each recognised type
+    mediaTypes: {
+        twitter: function () {
+            var tweet_id = this.$el.data('tweetid');
+            var that = this;
+
+            $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {
+                that.$content.find('.content').html(data.html);
+            });
+
+            return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_tweet').fetch() + '...</div>');
+        },
+
+
+        image: function () {
+            return $('<a href="' + this.url + '" target="_blank"><img height="100" src="' + this.url + '" /></a>');
+        },
+
+
+        imgur: function () {
+            var that = this;
+
+            $.getJSON('http://api.imgur.com/oembed?url=' + this.url, function (data) {
+                var img_html = '<a href="' + data.url + '" target="_blank"><img height="100" src="' + data.url + '" /></a>';
+                that.$content.find('.content').html(img_html);
+            });
+
+            return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_image').fetch() + '...</div>');
+        },
+
+
+        reddit: function () {
+            var that = this;
+            var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);
+
+            $.getJSON('http://www.' + matches[0] + '.json?jsonp=?', function (data) {
+                console.log('Loaded reddit data', data);
+                var post = data[0].data.children[0].data;
+                var thumb = '';
+
+                // Show a thumbnail if there is one
+                if (post.thumbnail) {
+                    //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
+
+                    // Hide the thumbnail if an over_18 image
+                    if (post.over_18) {
+                        thumb = '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';
+                        thumb += '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';
+                        thumb += '<img src="' + post.thumbnail + '" class="thumbnail" style="visibility:hidden;" />';
+                        thumb += '</span>';
+                    } else {
+                        thumb = '<img src="' + post.thumbnail + '" class="thumbnail" />';
+                    }
+                }
+
+                // Build the template string up
+                var tmpl = '<div>' + thumb + '<b><%- title %></b><br />Posted by <%- author %>. &nbsp;&nbsp; ';
+                tmpl += '<i class="fa fa-arrow-up"></i> <%- ups %> &nbsp;&nbsp; <i class="fa fa-arrow-down"></i> <%- downs %><br />';
+                tmpl += '<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>';
+
+                that.$content.find('.content').html(_.template(tmpl, post));
+            });
+
+            return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_reddit').fetch() + '...</div>');
+        },
+
+
+        youtube: function () {
+            var ytid = this.$el.data('ytid');
+            var that = this;
+            var yt_html = '<iframe width="480" height="270" src="https://www.youtube.com/embed/'+ ytid +'?feature=oembed" frameborder="0" allowfullscreen=""></iframe>';
+            that.$content.find('.content').html(yt_html);
+
+            return $('');
+        },
+
+
+        gist: function () {
+            var that = this,
+                matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(this.url);
+
+            $.getJSON('https://gist.github.com/'+matches[1]+'.json?callback=?' + (matches[2] || ''), function (data) {
+                $('body').append('<link rel="stylesheet" href="' + data.stylesheet + '" type="text/css" />');
+                that.$content.find('.content').html(data.div);
+            });
+
+            return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_gist').fetch() + '...</div>');
+        },
+
+        spotify: function () {
+            var uri = this.$el.data('uri'),
+                method = this.$el.data('method'),
+                spot, html;
+
+            switch (method) {
+                case "track":
+                case "album":
+                    spot = {
+                        url: 'https://embed.spotify.com/?uri=' + uri,
+                        width: 300,
+                        height: 80
+                    };
+                    break;
+                case "artist":
+                    spot = {
+                        url: 'https://embed.spotify.com/follow/1/?uri=' + uri +'&size=detail&theme=dark',
+                        width: 300,
+                        height: 56
+                    };
+                    break;
+            }
+
+            html = '<iframe src="' + spot.url + '" width="' + spot.width + '" height="' + spot.height + '" frameborder="0" allowtransparency="true"></iframe>';
+
+            return $(html);
+        },
+
+        soundcloud: function () {
+            var url = this.$el.data('url'),
+                $content = $('<div></div>').text(_kiwi.global.i18n.translate('client_models_applet_loading').fetch());
+
+            $.getJSON('https://soundcloud.com/oembed', { url: url })
+                .then(function (data) {
+                    $content.empty().append(
+                        $(data.html).attr('height', data.height - 100)
+                    );
+                }, function () {
+                    $content.text(_kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch());
+                });
+
+            return $content;
+        },
+
+        custom: function() {
+            var type = this.constructor.types[this.$el.data('index')];
+
+            if (!type)
+                return;
+
+            return $(type.buildHtml(this.$el.data('url')));
+        }
+
+    }
+    }, {
+
+    /**
+     * Add a media message type to append HTML after a matching URL
+     * match() should return a truthy value if it wants to handle this URL
+     * buildHtml() should return the HTML string to be used within the drop down
+     */
+    addType: function(match, buildHtml) {
+        if (typeof match !== 'function' || typeof buildHtml !== 'function')
+            return;
+
+        this.types = this.types || [];
+        this.types.push({match: match, buildHtml: buildHtml});
+    },
+
+
+    // Build the closed media HTML from a URL
+    buildHtml: function (url) {
+        var html = '', matches;
+
+        _.each(this.types || [], function(type, type_idx) {
+            if (!type.match(url))
+                return;
+
+            // Add which media type should handle this media message. Will be read when it's clicked on
+            html += '<span class="media" title="Open" data-type="custom" data-index="'+type_idx+'" data-url="' + _.escape(url) + '"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        });
+
+        // Is it an image?
+        if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
+            html += '<span class="media image" data-type="image" data-url="' + url + '" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is this an imgur link not picked up by the images regex?
+        matches = (/imgur\.com\/[^/]*(?!=\.[^!.]+($|\?))/ig).exec(url);
+        if (matches && !url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
+            html += '<span class="media imgur" data-type="imgur" data-url="' + url + '" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is it a tweet?
+        matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);
+        if (matches) {
+            html += '<span class="media twitter" data-type="twitter" data-url="' + url + '" data-tweetid="' + matches[2] + '" title="Show tweet information"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is reddit?
+        matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);
+        if (matches) {
+            html += '<span class="media reddit" data-type="reddit" data-url="' + url + '" title="Reddit thread"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is youtube?
+        matches = (/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/gi).exec(url);
+        if (matches) {
+            html += '<span class="media youtube" data-type="youtube" data-url="' + url + '" data-ytid="' + matches[1] + '" title="YouTube Video"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is a github gist?
+        matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(url);
+        if (matches) {
+            html += '<span class="media gist" data-type="gist" data-url="' + url + '" data-gist_id="' + matches[1] + '" title="GitHub Gist"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is this a spotify link?
+        matches = (/http:\/\/(?:play|open\.)?spotify.com\/(album|track|artist)\/([a-zA-Z0-9]+)\/?/i).exec(url);
+        if (matches) {
+            // Make it a Spotify URI! (spotify:<type>:<id>)
+            var method = matches[1],
+                uri = "spotify:" + matches[1] + ":" + matches[2];
+            html += '<span class="media spotify" data-type="spotify" data-uri="' + uri + '" data-method="' + method + '" title="Spotify ' + method + '"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        matches = (/(?:m\.)?(soundcloud\.com(?:\/.+))/i).exec(url);
+        if (matches) {
+            html += '<span class="media soundcloud" data-type="soundcloud" data-url="http://' + matches[1] + '" title="SoundCloud player"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        return html;
+    }
+});
+
+
+
+_kiwi.view.Member = Backbone.View.extend({
+    tagName: "li",
+    initialize: function (options) {
+        this.model.bind('change', this.render, this);
+        this.render();
+    },
+    render: function () {
+        var $this = this.$el,
+            prefix_css_class = (this.model.get('modes') || []).join(' ');
+
+        $this.attr('class', 'mode ' + prefix_css_class);
+        $this.html('<a class="nick"><span class="prefix">' + this.model.get("prefix") + '</span>' + this.model.get("nick") + '</a>');
+
+        return this;
+    }
+});
+
+
+_kiwi.view.MemberList = Backbone.View.extend({
+    tagName: "div",
+    events: {
+        "click .nick": "nickClick",
+        "click .channel_info": "channelInfoClick"
+    },
+
+    initialize: function (options) {
+        this.model.bind('all', this.render, this);
+        this.$el.appendTo('#kiwi .memberlists');
+
+        // Holds meta data. User counts, etc
+        this.$meta = $('<div class="meta"></div>').appendTo(this.$el);
+
+        // The list for holding the nicks
+        this.$list = $('<ul></ul>').appendTo(this.$el);
+    },
+    render: function () {
+        var that = this;
+
+        this.$list.empty();
+        this.model.forEach(function (member) {
+            member.view.$el.data('member', member);
+            that.$list.append(member.view.$el);
+        });
+
+        // User count
+        if(this.model.channel.isActive()) {
+            this.renderMeta();
+        }
+
+        return this;
+    },
+
+    renderMeta: function() {
+        var members_count = this.model.length + ' ' + translateText('client_applets_chanlist_users');
+        this.$meta.text(members_count);
+    },
+
+    nickClick: function (event) {
+        var $target = $(event.currentTarget).parent('li'),
+            member = $target.data('member');
+
+        _kiwi.global.events.emit('nick:select', {target: $target, member: member, source: 'nicklist'})
+        .then(_.bind(this.openUserMenuForItem, this, $target));
+    },
+
+
+    // Open a user menu for the given userlist item (<li>)
+    openUserMenuForItem: function($target) {
+        var member = $target.data('member'),
+            userbox,
+            are_we_an_op = !!this.model.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op');
+
+        userbox = new _kiwi.view.UserBox();
+        userbox.setTargets(member, this.model.channel);
+        userbox.displayOpItems(are_we_an_op);
+
+        var menu = new _kiwi.view.MenuBox(member.get('nick') || 'User');
+        menu.addItem('userbox', userbox.$el);
+        menu.showFooter(false);
+
+        _kiwi.global.events.emit('usermenu:created', {menu: menu, userbox: userbox, user: member})
+        .then(_.bind(function() {
+            menu.show();
+
+            var target_offset = $target.offset(),
+                t = target_offset.top,
+                m_bottom = t + menu.$el.outerHeight(),  // Where the bottom of menu will be
+                memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight(),
+                l = target_offset.left,
+                m_right = l + menu.$el.outerWidth(),  // Where the left of menu will be
+                memberlist_right = this.$el.parent().offset().left + this.$el.parent().outerWidth();
+
+            // If the bottom of the userbox is going to be too low.. raise it
+            if (m_bottom > memberlist_bottom){
+                t = memberlist_bottom - menu.$el.outerHeight();
+            }
+
+            // If the top of the userbox is going to be too high.. lower it
+            if (t < 0){
+                t = 0;
+            }
+
+            // If the right of the userbox is going off screen.. bring it in
+            if (m_right > memberlist_right){
+                l = memberlist_right - menu.$el.outerWidth();
+            }
+
+            // Set the new positon
+            menu.$el.offset({
+                left: l,
+                top: t
+            });
+
+        }, this))
+        .catch(_.bind(function() {
+            userbox = null;
+
+            menu.dispose();
+            menu = null;
+        }, this));
+    },
+
+
+    channelInfoClick: function(event) {
+        new _kiwi.model.ChannelInfo({channel: this.model.channel});
+    },
+
+
+    show: function () {
+        $('#kiwi .memberlists').children().removeClass('active');
+        $(this.el).addClass('active');
+
+        this.renderMeta();
+    }
+});
+
+
+_kiwi.view.MenuBox = Backbone.View.extend({
+    events: {
+        'click .ui_menu_foot .close, a.close_menu': 'dispose'
+    },
+
+    initialize: function(title) {
+        var that = this;
+
+        this.$el = $('<div class="ui_menu"><div class="items"></div></div>');
+
+        this._title = title || '';
+        this._items = {};
+        this._display_footer = true;
+        this._close_on_blur = true;
+    },
+
+
+    render: function() {
+        var that = this,
+            $title,
+            $items = that.$el.find('.items');
+
+        $items.find('*').remove();
+
+        if (this._title) {
+            $title = $('<div class="ui_menu_title"></div>')
+                .text(this._title);
+
+            this.$el.prepend($title);
+        }
+
+        _.each(this._items, function(item) {
+            var $item = $('<div class="ui_menu_content hover"></div>')
+                .append(item);
+
+            $items.append($item);
+        });
+
+        if (this._display_footer)
+            this.$el.append('<div class="ui_menu_foot"><a class="close" onclick="">Close <i class="fa fa-times"></i></a></div>');
+
+    },
+
+
+    setTitle: function(new_title) {
+        this._title = new_title;
+
+        if (!this._title)
+            return;
+
+        this.$el.find('.ui_menu_title').text(this._title);
+    },
+
+
+    onDocumentClick: function(event) {
+        var $target = $(event.target);
+
+        if (!this._close_on_blur)
+            return;
+
+        // If this is not itself AND we don't contain this element, dispose $el
+        if ($target[0] != this.$el[0] && this.$el.has($target).length === 0)
+            this.dispose();
+    },
+
+
+    dispose: function() {
+        _.each(this._items, function(item) {
+            item.dispose && item.dispose();
+            item.remove && item.remove();
+        });
+
+        this._items = null;
+        this.remove();
+
+        if (this._close_proxy)
+            $(document).off('click', this._close_proxy);
+    },
+
+
+    addItem: function(item_name, $item) {
+        if ($item.is('a')) $item.addClass('fa fa-chevron-right');
+        this._items[item_name] = $item;
+    },
+
+
+    removeItem: function(item_name) {
+        delete this._items[item_name];
+    },
+
+
+    showFooter: function(show) {
+        this._display_footer = show;
+    },
+
+
+    closeOnBlur: function(close_it) {
+        this._close_on_blur = close_it;
+    },
+
+
+    show: function() {
+        var that = this,
+            $controlbox, menu_height;
+
+        this.render();
+        this.$el.appendTo(_kiwi.app.view.$el);
+
+        // Ensure the menu doesn't get too tall to overlap the input bar at the bottom
+        $controlbox = _kiwi.app.view.$el.find('.controlbox');
+        $items = this.$el.find('.items');
+        menu_height = this.$el.outerHeight() - $items.outerHeight();
+
+        $items.css({
+            'overflow-y': 'auto',
+            'max-height': $controlbox.offset().top - this.$el.offset().top - menu_height
+        });
+
+        // We add this document click listener on the next javascript tick.
+        // If the current tick is handling an existing click event (such as the nicklist click handler),
+        // the click event bubbles up and hits the document therefore calling this callback to
+        // remove this menubox before it's even shown.
+        setTimeout(function() {
+            that._close_proxy = function(event) {
+                that.onDocumentClick(event);
+            };
+            $(document).on('click', that._close_proxy);
+        }, 0);
+    }
+});
+
+
+
+// Model for this = _kiwi.model.NetworkPanelList
+_kiwi.view.NetworkTabs = Backbone.View.extend({
+    tagName: 'ul',
+    className: 'connections',
+
+    initialize: function() {
+        this.model.on('add', this.networkAdded, this);
+        this.model.on('remove', this.networkRemoved, this);
+
+        this.$el.appendTo(_kiwi.app.view.$el.find('.tabs'));
+    },
+
+    networkAdded: function(network) {
+        $('<li class="connection"></li>')
+            .append(network.panels.view.$el)
+            .appendTo(this.$el);
+    },
+
+    networkRemoved: function(network) {
+        // Remove the containing list element
+        network.panels.view.$el.parent().remove();
+
+        network.panels.view.remove();
+
+        _kiwi.app.view.doLayout();
+    }
+});
+
+
+_kiwi.view.NickChangeBox = Backbone.View.extend({
+    events: {
+        'submit': 'changeNick',
+        'click .cancel': 'close'
+    },
+
+    initialize: function () {
+        var text = {
+            new_nick: _kiwi.global.i18n.translate('client_views_nickchangebox_new').fetch(),
+            change: _kiwi.global.i18n.translate('client_views_nickchangebox_change').fetch(),
+            cancel: _kiwi.global.i18n.translate('client_views_nickchangebox_cancel').fetch()
+        };
+        this.$el = $(_.template($('#tmpl_nickchange').html().trim(), text));
+    },
+
+    render: function () {
+        // Add the UI component and give it focus
+        _kiwi.app.controlbox.$el.prepend(this.$el);
+        this.$el.find('input').focus();
+
+        this.$el.css('bottom', _kiwi.app.controlbox.$el.outerHeight(true));
+    },
+
+    close: function () {
+        this.$el.remove();
+        this.trigger('close');
+    },
+
+    changeNick: function (event) {
+        event.preventDefault();
+
+        var connection = _kiwi.app.connections.active_connection;
+        this.listenTo(connection, 'change:nick', function() {
+            this.close();
+        });
+
+        connection.gateway.changeNick(this.$('input').val());
+    }
+});
+
+
+_kiwi.view.ResizeHandler = Backbone.View.extend({
+    events: {
+        'mousedown': 'startDrag',
+        'mouseup': 'stopDrag'
+    },
+
+    initialize: function () {
+        this.dragging = false;
+        this.starting_width = {};
+
+        $(window).on('mousemove', $.proxy(this.onDrag, this));
+    },
+
+    startDrag: function (event) {
+        this.dragging = true;
+    },
+
+    stopDrag: function (event) {
+        this.dragging = false;
+    },
+
+    onDrag: function (event) {
+        if (!this.dragging) return;
+
+        var offset = $('#kiwi').offset().left;
+
+        this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2) - offset);
+        $('#kiwi .right_bar').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));
+        _kiwi.app.view.doLayout();
+    }
+});
+
+
+_kiwi.view.ServerSelect = Backbone.View.extend({
+    events: {
+        'submit form': 'submitForm',
+        'click .show_more': 'showMore',
+        'change .have_pass input': 'showPass',
+        'change .have_key input': 'showKey',
+        'click .fa-key': 'channelKeyIconClick',
+        'click .show_server': 'showServer'
+    },
+
+    initialize: function () {
+        var that = this,
+            text = {
+                think_nick: _kiwi.global.i18n.translate('client_views_serverselect_form_title').fetch(),
+                nickname: _kiwi.global.i18n.translate('client_views_serverselect_nickname').fetch(),
+                have_password: _kiwi.global.i18n.translate('client_views_serverselect_enable_password').fetch(),
+                password: _kiwi.global.i18n.translate('client_views_serverselect_password').fetch(),
+                channel: _kiwi.global.i18n.translate('client_views_serverselect_channel').fetch(),
+                channel_key: _kiwi.global.i18n.translate('client_views_serverselect_channelkey').fetch(),
+                require_key: _kiwi.global.i18n.translate('client_views_serverselect_channelkey_required').fetch(),
+                key: _kiwi.global.i18n.translate('client_views_serverselect_key').fetch(),
+                start: _kiwi.global.i18n.translate('client_views_serverselect_connection_start').fetch(),
+                server_network: _kiwi.global.i18n.translate('client_views_serverselect_server_and_network').fetch(),
+                server: _kiwi.global.i18n.translate('client_views_serverselect_server').fetch(),
+                port: _kiwi.global.i18n.translate('client_views_serverselect_port').fetch(),
+                powered_by: _kiwi.global.i18n.translate('client_views_serverselect_poweredby').fetch()
+            };
+
+        this.$el = $(_.template($('#tmpl_server_select').html().trim(), text));
+
+        // Remove the 'more' link if the server has disabled server changing
+        if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) {
+            if (!_kiwi.app.server_settings.connection.allow_change) {
+                this.$el.find('.show_more').remove();
+                this.$el.addClass('single_server');
+            }
+        }
+
+        // Are currently showing all the controlls or just a nick_change box?
+        this.state = 'all';
+
+        this.more_shown = false;
+
+        this.model.bind('new_network', this.newNetwork, this);
+
+        this.gateway = _kiwi.global.components.Network();
+        this.gateway.on('connect', this.networkConnected, this);
+        this.gateway.on('connecting', this.networkConnecting, this);
+        this.gateway.on('disconnect', this.networkDisconnected, this);
+        this.gateway.on('irc_error', this.onIrcError, this);
+    },
+
+    dispose: function() {
+        this.model.off('new_network', this.newNetwork, this);
+        this.gateway.off();
+
+        this.remove();
+    },
+
+    submitForm: function (event) {
+        event.preventDefault();
+
+        // Make sure a nick is chosen
+        if (!$('input.nick', this.$el).val().trim()) {
+            this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_nickname_error_empty').fetch());
+            $('input.nick', this.$el).select();
+            return;
+        }
+
+        if (this.state === 'nick_change') {
+            this.submitNickChange(event);
+        } else {
+            this.submitLogin(event);
+        }
+
+        $('button', this.$el).attr('disabled', 1);
+        return;
+    },
+
+    submitLogin: function (event) {
+        // If submitting is disabled, don't do anything
+        if ($('button', this.$el).attr('disabled')) return;
+
+        var values = {
+            nick: $('input.nick', this.$el).val(),
+            server: $('input.server', this.$el).val(),
+            port: $('input.port', this.$el).val(),
+            ssl: $('input.ssl', this.$el).prop('checked'),
+            password: $('input.password', this.$el).val(),
+            channel: $('input.channel', this.$el).val(),
+            channel_key: $('input.channel_key', this.$el).val(),
+            options: this.server_options
+        };
+
+        this.trigger('server_connect', values);
+    },
+
+    submitNickChange: function (event) {
+        _kiwi.gateway.changeNick(null, $('input.nick', this.$el).val());
+        this.networkConnecting();
+    },
+
+    showPass: function (event) {
+        if (this.$el.find('tr.have_pass input').is(':checked')) {
+            this.$el.find('tr.pass').show().find('input').focus();
+        } else {
+            this.$el.find('tr.pass').hide().find('input').val('');
+        }
+    },
+
+    channelKeyIconClick: function (event) {
+        this.$el.find('tr.have_key input').click();
+    },
+
+    showKey: function (event) {
+        if (this.$el.find('tr.have_key input').is(':checked')) {
+            this.$el.find('tr.key').show().find('input').focus();
+        } else {
+            this.$el.find('tr.key').hide().find('input').val('');
+        }
+    },
+
+    showMore: function (event) {
+        if (!this.more_shown) {
+            $('.more', this.$el).slideDown('fast');
+            $('.show_more', this.$el)
+                .children('.fa-caret-down')
+                .removeClass('fa-caret-down')
+                .addClass('fa-caret-up');
+            $('input.server', this.$el).select();
+            this.more_shown = true;
+        } else {
+            $('.more', this.$el).slideUp('fast');
+            $('.show_more', this.$el)
+                .children('.fs-caret-up')
+                .removeClass('fa-caret-up')
+                .addClass('fa-caret-down');
+            $('input.nick', this.$el).select();
+            this.more_shown = false;
+        }
+    },
+
+    populateFields: function (defaults) {
+        var nick, server, port, channel, channel_key, ssl, password;
+
+        defaults = defaults || {};
+
+        nick = defaults.nick || '';
+        server = defaults.server || '';
+        port = defaults.port || 6667;
+        ssl = defaults.ssl || 0;
+        password = defaults.password || '';
+        channel = defaults.channel || '';
+        channel_key = defaults.channel_key || '';
+
+        $('input.nick', this.$el).val(nick);
+        $('input.server', this.$el).val(server);
+        $('input.port', this.$el).val(port);
+        $('input.ssl', this.$el).prop('checked', ssl);
+        $('input#server_select_show_pass', this.$el).prop('checked', !(!password));
+        $('input.password', this.$el).val(password);
+        if (!(!password)) {
+            $('tr.pass', this.$el).show();
+        }
+        $('input.channel', this.$el).val(channel);
+        $('input#server_select_show_channel_key', this.$el).prop('checked', !(!channel_key));
+        $('input.channel_key', this.$el).val(channel_key);
+        if (!(!channel_key)) {
+            $('tr.key', this.$el).show();
+        }
+
+        // Temporary values
+        this.server_options = {};
+
+        if (defaults.encoding)
+            this.server_options.encoding = defaults.encoding;
+    },
+
+    hide: function () {
+        this.$el.slideUp();
+    },
+
+    show: function (new_state) {
+        new_state = new_state || 'all';
+
+        this.$el.show();
+
+        if (new_state === 'all') {
+            $('.show_more', this.$el).show();
+
+        } else if (new_state === 'more') {
+            $('.more', this.$el).slideDown('fast');
+
+        } else if (new_state === 'nick_change') {
+            $('.more', this.$el).hide();
+            $('.show_more', this.$el).hide();
+            $('input.nick', this.$el).select();
+
+        } else if (new_state === 'enter_password') {
+            $('.more', this.$el).hide();
+            $('.show_more', this.$el).hide();
+            $('input.password', this.$el).select();
+        }
+
+        this.state = new_state;
+    },
+
+    infoBoxShow: function() {
+        var $side_panel = this.$el.find('.side_panel');
+
+        // Some theme may hide the info panel so check before we
+        // resize ourselves
+        if (!$side_panel.is(':visible'))
+            return;
+
+        this.$el.animate({
+            width: parseInt($side_panel.css('left'), 10) + $side_panel.find('.content:first').outerWidth()
+        });
+    },
+
+    infoBoxHide: function() {
+        var $side_panel = this.$el.find('.side_panel');
+        this.$el.animate({
+            width: parseInt($side_panel.css('left'), 10)
+        });
+    },
+
+    infoBoxSet: function($info_view) {
+        this.$el.find('.side_panel .content')
+            .empty()
+            .append($info_view);
+    },
+
+    setStatus: function (text, class_name) {
+        $('.status', this.$el)
+            .text(text)
+            .attr('class', 'status')
+            .addClass(class_name||'')
+            .show();
+    },
+    clearStatus: function () {
+        $('.status', this.$el).hide();
+    },
+
+    reset: function() {
+        this.populateFields();
+        this.clearStatus();
+
+        this.$('button').attr('disabled', null);
+    },
+
+    newNetwork: function(network) {
+        // Keep a reference to this network so we can interact with it
+        this.model.current_connecting_network = network;
+    },
+
+    networkConnected: function (event) {
+        this.model.trigger('connected', _kiwi.app.connections.getByConnectionId(event.server));
+        this.model.current_connecting_network = null;
+    },
+
+    networkDisconnected: function () {
+        this.model.current_connecting_network = null;
+        this.state = 'all';
+    },
+
+    networkConnecting: function (event) {
+        this.model.trigger('connecting');
+        this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_connection_trying').fetch(), 'ok');
+
+        this.$('.status').append('<a class="show_server"><i class="fa fa-info-circle"></i></a>');
+    },
+
+    showServer: function() {
+        // If we don't have a current connection in the making then we have nothing to show
+        if (!this.model.current_connecting_network)
+            return;
+
+        _kiwi.app.view.barsShow();
+        this.model.current_connecting_network.panels.server.view.show();
+    },
+
+    onIrcError: function (data) {
+        $('button', this.$el).attr('disabled', null);
+
+        switch(data.error) {
+        case 'nickname_in_use':
+            this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_nickname_error_alreadyinuse').fetch());
+            this.show('nick_change');
+            this.$el.find('.nick').select();
+            break;
+        case 'erroneus_nickname':
+            if (data.reason) {
+                this.setStatus(data.reason);
+            } else {
+                this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_nickname_invalid').fetch());
+            }
+            this.show('nick_change');
+            this.$el.find('.nick').select();
+            break;
+        case 'password_mismatch':
+            this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_password_incorrect').fetch());
+            this.show('enter_password');
+            this.$el.find('.password').select();
+            break;
+        default:
+            this.showError(data.reason || '');
+            break;
+        }
+    },
+
+    showError: function (error_reason) {
+        var err_text = _kiwi.global.i18n.translate('client_views_serverselect_connection_error').fetch();
+
+        if (error_reason) {
+            switch (error_reason) {
+            case 'ENOTFOUND':
+                err_text = _kiwi.global.i18n.translate('client_views_serverselect_server_notfound').fetch();
+                break;
+
+            case 'ECONNREFUSED':
+                err_text += ' (' + _kiwi.global.i18n.translate('client_views_serverselect_connection_refused').fetch() + ')';
+                break;
+
+            default:
+                err_text += ' (' + error_reason + ')';
+            }
+        }
+
+        this.setStatus(err_text, 'error');
+        $('button', this.$el).attr('disabled', null);
+        this.show();
+    }
+});
+
+
+_kiwi.view.StatusMessage = Backbone.View.extend({
+    initialize: function () {
+        this.$el.hide();
+
+        // Timer for hiding the message after X seconds
+        this.tmr = null;
+    },
+
+    text: function (text, opt) {
+        // Defaults
+        opt = opt || {};
+        opt.type = opt.type || '';
+        opt.timeout = opt.timeout || 5000;
+
+        this.$el.text(text).addClass(opt.type);
+        this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));
+
+        if (opt.timeout) this.doTimeout(opt.timeout);
+    },
+
+    html: function (html, opt) {
+        // Defaults
+        opt = opt || {};
+        opt.type = opt.type || '';
+        opt.timeout = opt.timeout || 5000;
+
+        this.$el.html(html).addClass(opt.type);
+        this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));
+
+        if (opt.timeout) this.doTimeout(opt.timeout);
+    },
+
+    hide: function () {
+        this.$el.slideUp($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));
+    },
+
+    doTimeout: function (length) {
+        if (this.tmr) clearTimeout(this.tmr);
+        var that = this;
+        this.tmr = setTimeout(function () { that.hide(); }, length);
+    }
+});
+
+
+// Model for this = _kiwi.model.PanelList
+_kiwi.view.Tabs = Backbone.View.extend({
+    tagName: 'ul',
+    className: 'panellist',
+
+    events: {
+        'click li': 'tabClick',
+        'click li .part': 'partClick'
+    },
+
+    initialize: function () {
+        this.model.on("add", this.panelAdded, this);
+        this.model.on("remove", this.panelRemoved, this);
+        this.model.on("reset", this.render, this);
+
+        this.model.on('active', this.panelActive, this);
+
+        // Network tabs start with a server, so determine what we are now
+        this.is_network = false;
+
+        if (this.model.network) {
+            this.is_network = true;
+
+            this.model.network.on('change:name', function (network, new_val) {
+                $('span', this.model.server.tab).text(new_val);
+            }, this);
+
+            this.model.network.on('change:connection_id', function (network, new_val) {
+                this.model.forEach(function(panel) {
+                    panel.tab.data('connection_id', new_val);
+                });
+            }, this);
+        }
+    },
+
+    render: function () {
+        var that = this;
+
+        this.$el.empty();
+
+        if (this.is_network) {
+            // Add the server tab first
+            this.model.server.tab
+                .data('panel', this.model.server)
+                .data('connection_id', this.model.network.get('connection_id'))
+                .appendTo(this.$el);
+        }
+
+        // Go through each panel adding its tab
+        this.model.forEach(function (panel) {
+            // If this is the server panel, ignore as it's already added
+            if (this.is_network && panel == that.model.server)
+                return;
+
+            panel.tab.data('panel', panel);
+
+            if (this.is_network)
+                panel.tab.data('connection_id', this.model.network.get('connection_id'));
+
+            panel.tab.appendTo(that.$el);
+        });
+
+        _kiwi.app.view.doLayout();
+    },
+
+    updateTabTitle: function (panel, new_title) {
+        $('span', panel.tab).text(new_title);
+    },
+
+    panelAdded: function (panel) {
+        // Add a tab to the panel
+        panel.tab = $('<li><span></span><div class="activity"></div></li>');
+        panel.tab.find('span').text(panel.get('title') || panel.get('name'));
+
+        if (panel.isServer()) {
+            panel.tab.addClass('server');
+            panel.tab.addClass('fa');
+            panel.tab.addClass('fa-nonexistant');
+        }
+
+        panel.tab.data('panel', panel);
+
+        if (this.is_network)
+            panel.tab.data('connection_id', this.model.network.get('connection_id'));
+
+        this.sortTabs();
+
+        panel.bind('change:title', this.updateTabTitle);
+        panel.bind('change:name', this.updateTabTitle);
+
+        _kiwi.app.view.doLayout();
+    },
+    panelRemoved: function (panel) {
+        var connection = _kiwi.app.connections.active_connection;
+
+        panel.tab.remove();
+        delete panel.tab;
+
+        _kiwi.app.panels.trigger('remove', panel);
+
+        _kiwi.app.view.doLayout();
+    },
+
+    panelActive: function (panel, previously_active_panel) {
+        // Remove any existing tabs or part images
+        _kiwi.app.view.$el.find('.panellist .part').remove();
+        _kiwi.app.view.$el.find('.panellist .active').removeClass('active');
+
+        panel.tab.addClass('active');
+
+        panel.tab.append('<span class="part fa fa-nonexistant"></span>');
+    },
+
+    tabClick: function (e) {
+        var tab = $(e.currentTarget);
+
+        var panel = tab.data('panel');
+        if (!panel) {
+            // A panel wasn't found for this tab... wadda fuck
+            return;
+        }
+
+        panel.view.show();
+    },
+
+    partClick: function (e) {
+        var tab = $(e.currentTarget).parent();
+        var panel = tab.data('panel');
+
+        if (!panel) return;
+
+        // If the nicklist is empty, we haven't joined the channel as yet
+        // If we part a server, then we need to disconnect from server, close channel tabs,
+        // close server tab, then bring client back to homepage
+        if (panel.isChannel() && panel.get('members').models.length > 0) {
+            this.model.network.gateway.part(panel.get('name'));
+
+        } else if(panel.isServer()) {
+            if (!this.model.network.get('connected') || confirm(translateText('disconnect_from_server'))) {
+                this.model.network.gateway.quit("Leaving");
+                _kiwi.app.connections.remove(this.model.network);
+                _kiwi.app.startup_applet.view.show();
+            }
+
+        } else {
+            panel.close();
+        }
+    },
+
+    sortTabs: function() {
+        var that = this,
+            panels = [];
+
+        this.model.forEach(function (panel) {
+            // Ignore the server tab, so all others get added after it
+            if (that.is_network && panel == that.model.server)
+                return;
+
+            panels.push([panel.get('title') || panel.get('name'), panel]);
+        });
+
+        // Sort by the panel name..
+        panels.sort(function(a, b) {
+            if (a[0].toLowerCase() > b[0].toLowerCase()) {
+                return 1;
+            } else if (a[0].toLowerCase() < b[0].toLowerCase()) {
+                return -1;
+            } else {
+                return 0;
+            }
+        });
+
+        // And add them all back in order.
+        _.each(panels, function(panel) {
+            panel[1].tab.appendTo(that.$el);
+        });
+    }
+});
+
+
+_kiwi.view.TopicBar = Backbone.View.extend({
+    events: {
+        'keydown div': 'process'
+    },
+
+    initialize: function () {
+        _kiwi.app.panels.bind('active', function (active_panel) {
+            // If it's a channel topic, update and make editable
+            if (active_panel.isChannel()) {
+                this.setCurrentTopicFromChannel(active_panel);
+                this.$el.find('div').attr('contentEditable', true);
+
+            } else {
+                // Not a channel topic.. clear and make uneditable
+                this.$el.find('div').attr('contentEditable', false)
+                    .text('');
+            }
+        }, this);
+    },
+
+    process: function (ev) {
+        var inp = $(ev.currentTarget),
+            inp_val = inp.text();
+
+        // Only allow topic editing if this is a channel panel
+        if (!_kiwi.app.panels().active.isChannel()) {
+            return false;
+        }
+
+        // If hit return key, update the current topic
+        if (ev.keyCode === 13) {
+            _kiwi.app.connections.active_connection.gateway.topic(_kiwi.app.panels().active.get('name'), inp_val);
+            return false;
+        }
+    },
+
+    setCurrentTopic: function (new_topic) {
+        new_topic = new_topic || '';
+
+        // We only want a plain text version
+        $('div', this.$el).html(formatIRCMsg(_.escape(new_topic)));
+    },
+
+    setCurrentTopicFromChannel: function(channel) {
+        var set_by = channel.get('topic_set_by'),
+            set_by_text = '';
+
+        this.setCurrentTopic(channel.get("topic"));
+
+        if (set_by) {
+            set_by_text += translateText('client_models_network_topic', [set_by.nick, _kiwi.utils.formatDate(set_by.when)]);
+            this.$el.attr('title', set_by_text);
+        } else {
+            this.$el.attr('title', '');
+        }
+    }
+});
+
+
+_kiwi.view.UserBox = Backbone.View.extend({
+    events: {
+        'click .query': 'queryClick',
+        'click .info': 'infoClick',
+        'change .ignore': 'ignoreChange',
+        'click .ignore': 'ignoreClick',
+        'click .op': 'opClick',
+        'click .deop': 'deopClick',
+        'click .voice': 'voiceClick',
+        'click .devoice': 'devoiceClick',
+        'click .kick': 'kickClick',
+        'click .ban': 'banClick'
+    },
+
+    initialize: function () {
+        var text = {
+            op: _kiwi.global.i18n.translate('client_views_userbox_op').fetch(),
+            de_op: _kiwi.global.i18n.translate('client_views_userbox_deop').fetch(),
+            voice: _kiwi.global.i18n.translate('client_views_userbox_voice').fetch(),
+            de_voice: _kiwi.global.i18n.translate('client_views_userbox_devoice').fetch(),
+            kick: _kiwi.global.i18n.translate('client_views_userbox_kick').fetch(),
+            ban: _kiwi.global.i18n.translate('client_views_userbox_ban').fetch(),
+            message: _kiwi.global.i18n.translate('client_views_userbox_query').fetch(),
+            info: _kiwi.global.i18n.translate('client_views_userbox_whois').fetch(),
+            ignore: _kiwi.global.i18n.translate('client_views_userbox_ignore').fetch()
+        };
+        this.$el = $(_.template($('#tmpl_userbox').html().trim(), text));
+    },
+
+    setTargets: function (user, channel) {
+        this.user = user;
+        this.channel = channel;
+
+        var is_ignored = _kiwi.app.connections.active_connection.isNickIgnored(this.user.get('nick'));
+        this.$('.ignore input').attr('checked', is_ignored ? 'checked' : false);
+    },
+
+    displayOpItems: function(display_items) {
+        if (display_items) {
+            this.$el.find('.if_op').css('display', 'block');
+        } else {
+            this.$el.find('.if_op').css('display', 'none');
+        }
+    },
+
+    queryClick: function (event) {
+        var nick = this.user.get('nick');
+        _kiwi.app.connections.active_connection.createQuery(nick);
+    },
+
+    infoClick: function (event) {
+        _kiwi.app.controlbox.processInput('/whois ' + this.user.get('nick'));
+    },
+
+    ignoreClick: function (event) {
+        // Stop the menubox from closing since it will not update the checkbox otherwise
+        event.stopPropagation();
+    },
+
+    ignoreChange: function (event) {
+        if ($(event.currentTarget).find('input').is(':checked')) {
+            _kiwi.app.controlbox.processInput('/ignore ' + this.user.get('nick'));
+        } else {
+            _kiwi.app.controlbox.processInput('/unignore ' + this.user.get('nick'));
+        }
+    },
+
+    opClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +o ' + this.user.get('nick'));
+    },
+
+    deopClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -o ' + this.user.get('nick'));
+    },
+
+    voiceClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +v ' + this.user.get('nick'));
+    },
+
+    devoiceClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -v ' + this.user.get('nick'));
+    },
+
+    kickClick: function (event) {
+        // TODO: Enable the use of a custom kick message
+        _kiwi.app.controlbox.processInput('/kick ' + this.user.get('nick') + ' Bye!');
+    },
+
+    banClick: function (event) {
+        // TODO: Set ban on host, not just on nick
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +b ' + this.user.get('nick') + '!*');
+    }
+});
+
+
+_kiwi.view.ChannelTools = Backbone.View.extend({
+    events: {
+        'click .channel_info': 'infoClick',
+        'click .channel_part': 'partClick'
+    },
+
+    initialize: function () {},
+
+    infoClick: function (event) {
+        new _kiwi.model.ChannelInfo({channel: _kiwi.app.panels().active});
+    },
+
+    partClick: function (event) {
+        _kiwi.app.connections.active_connection.gateway.part(_kiwi.app.panels().active.get('name'));
+    }
+});
+
+
+// var f = new _kiwi.model.ChannelInfo({channel: _kiwi.app.panels().active});
+
+_kiwi.view.ChannelInfo = Backbone.View.extend({
+    events: {
+        'click .toggle_banlist': 'toggleBanList',
+        'change .channel-mode': 'onModeChange',
+        'click .remove-ban': 'onRemoveBanClick'
+    },
+
+
+    initialize: function () {
+        var that = this,
+            network,
+            channel = this.model.get('channel'),
+            text;
+
+        text = {
+            moderated_chat: translateText('client_views_channelinfo_moderated'),
+            invite_only: translateText('client_views_channelinfo_inviteonly'),
+            ops_change_topic: translateText('client_views_channelinfo_opschangechannel'),
+            external_messages: translateText('client_views_channelinfo_externalmessages'),
+            toggle_banlist: translateText('client_views_channelinfo_togglebanlist'),
+            channel_name: channel.get('name')
+        };
+
+        this.$el = $(_.template($('#tmpl_channel_info').html().trim(), text));
+
+        // Create the menu box this view will sit inside
+        this.menu = new _kiwi.view.MenuBox(channel.get('name'));
+        this.menu.addItem('channel_info', this.$el);
+        this.menu.$el.appendTo(channel.view.$container);
+        this.menu.show();
+
+        this.menu.$el.offset({top: _kiwi.app.view.$el.find('.panels').offset().top});
+
+        // Menu box will call this destroy on closing
+        this.$el.dispose = _.bind(this.dispose, this);
+
+        // Display the info we have, then listen for further changes
+        this.updateInfo(channel);
+        channel.on('change:info_modes change:info_url change:banlist', this.updateInfo, this);
+
+        // Request the latest info for ths channel from the network
+        channel.get('network').gateway.channelInfo(channel.get('name'));
+    },
+
+
+    render: function () {
+    },
+
+
+    onModeChange: function(event) {
+        var $this = $(event.currentTarget),
+            channel = this.model.get('channel'),
+            mode = $this.data('mode'),
+            mode_string = '';
+
+        if ($this.attr('type') == 'checkbox') {
+            mode_string = $this.is(':checked') ? '+' : '-';
+            mode_string += mode;
+            channel.setMode(mode_string);
+
+            return;
+        }
+
+        if ($this.attr('type') == 'text') {
+            mode_string = $this.val() ?
+                '+' + mode + ' ' + $this.val() :
+                '-' + mode;
+
+            channel.setMode(mode_string);
+
+            return;
+        }
+    },
+
+
+    onRemoveBanClick: function (event) {
+        event.preventDefault();
+        event.stopPropagation();
+
+        var $this = $(event.currentTarget),
+            $tr = $this.parents('tr:first'),
+            ban = $tr.data('ban');
+
+        if (!ban)
+            return;
+
+        var channel = this.model.get('channel');
+        channel.setMode('-b ' + ban.banned);
+
+        $tr.remove();
+    },
+
+
+    updateInfo: function (channel, new_val) {
+        var that = this,
+            title, modes, url, banlist;
+
+        modes = channel.get('info_modes');
+        if (modes) {
+            _.each(modes, function(mode, idx) {
+                mode.mode = mode.mode.toLowerCase();
+
+                if (mode.mode == '+k') {
+                    that.$el.find('[name="channel_key"]').val(mode.param);
+                } else if (mode.mode == '+m') {
+                    that.$el.find('[name="channel_mute"]').attr('checked', 'checked');
+                } else if (mode.mode == '+i') {
+                    that.$el.find('[name="channel_invite"]').attr('checked', 'checked');
+                } else if (mode.mode == '+n') {
+                    that.$el.find('[name="channel_external_messages"]').attr('checked', 'checked');
+                } else if (mode.mode == '+t') {
+                    that.$el.find('[name="channel_topic"]').attr('checked', 'checked');
+                }
+            });
+        }
+
+        url = channel.get('info_url');
+        if (url) {
+            this.$el.find('.channel_url')
+                .text(url)
+                .attr('href', url);
+
+            this.$el.find('.channel_url').slideDown();
+        }
+
+        banlist = channel.get('banlist');
+        if (banlist && banlist.length) {
+            var $table = this.$el.find('.channel-banlist table tbody');
+
+            this.$el.find('.banlist-status').text('');
+
+            $table.empty();
+            _.each(banlist, function(ban) {
+                var $tr = $('<tr></tr>').data('ban', ban);
+
+                $('<td></td>').text(ban.banned).appendTo($tr);
+                $('<td></td>').text(ban.banned_by.split(/[!@]/)[0]).appendTo($tr);
+                $('<td></td>').text(_kiwi.utils.formatDate(new Date(parseInt(ban.banned_at, 10) * 1000))).appendTo($tr);
+                $('<td><i class="fa fa-rtimes remove-ban"></i></td>').appendTo($tr);
+
+                $table.append($tr);
+            });
+
+            this.$el.find('.channel-banlist table').slideDown();
+        } else {
+            this.$el.find('.banlist-status').text('Banlist empty');
+            this.$el.find('.channel-banlist table').hide();
+        }
+    },
+
+    toggleBanList: function (event) {
+        event.preventDefault();
+        this.$el.find('.channel-banlist table').toggle();
+
+        if(!this.$el.find('.channel-banlist table').is(':visible'))
+            return;
+
+        var channel = this.model.get('channel'),
+            network = channel.get('network');
+
+        network.gateway.raw('MODE ' + channel.get('name') + ' +b');
+    },
+
+    dispose: function () {
+        this.model.get('channel').off('change:info_modes change:info_url change:banlist', this.updateInfo, this);
+
+        this.$el.remove();
+    }
+});
+
+
+
+_kiwi.view.RightBar = Backbone.View.extend({
+    events: {
+        'click .right-bar-toggle': 'onClickToggle',
+        'click .right-bar-toggle-inner': 'onClickToggle'
+    },
+
+    initialize: function() {
+        this.keep_hidden = false;
+        this.hidden = this.$el.hasClass('disabled');
+
+        this.updateIcon();
+    },
+
+
+    hide: function() {
+        this.hidden = true;
+        this.$el.addClass('disabled');
+
+        this.updateIcon();
+    },
+
+
+    show: function() {
+        this.hidden = false;
+
+        if (!this.keep_hidden)
+            this.$el.removeClass('disabled');
+
+        this.updateIcon();
+    },
+
+
+    // Toggle if the rightbar should be shown or not
+    toggle: function(keep_hidden) {
+        // Hacky, but we need to ignore the toggle() call from doLayout() as we are overriding it
+        if (this.ignore_layout)
+            return true;
+
+        if (typeof keep_hidden === 'undefined') {
+            this.keep_hidden = !this.keep_hidden;
+        } else {
+            this.keep_hidden = keep_hidden;
+        }
+
+        if (this.keep_hidden || this.hidden) {
+            this.$el.addClass('disabled');
+        } else {
+            this.$el.removeClass('disabled');
+        }
+
+        this.updateIcon();
+    },
+
+
+    updateIcon: function() {
+        var $toggle = this.$('.right-bar-toggle'),
+            $icon = $toggle.find('i');
+
+        if (!this.hidden && this.keep_hidden) {
+            $toggle.show();
+        } else {
+            $toggle.hide();
+        }
+
+        if (this.keep_hidden) {
+            $icon.removeClass('fa fa-angle-double-right').addClass('fa fa-users');
+        } else {
+            $icon.removeClass('fa fa-users').addClass('fa fa-angle-double-right');
+        }
+    },
+
+
+    onClickToggle: function(event) {
+        this.toggle();
+
+        // Hacky, but we need to ignore the toggle() call from doLayout() as we are overriding it
+        this.ignore_layout = true;
+        _kiwi.app.view.doLayout();
+
+        // No longer ignoring the toggle() call from doLayout()
+        delete this.ignore_layout;
+    }
+});
+
+
+_kiwi.view.Notification = Backbone.View.extend({
+    className: 'notification',
+
+    events: {
+        'click .close': 'close'
+    },
+
+    initialize: function(title, content) {
+        this.title = title;
+        this.content = content;
+    },
+
+    render: function() {
+        this.$el.html($('#tmpl_notifications').html());
+        this.$('h6').text(this.title);
+
+        // HTML string or jquery object
+        if (typeof this.content === 'string') {
+                this.$('.content').html(this.content);
+            } else if (typeof this.content === 'object') {
+                this.$('.content').empty().append(this.content);
+            }
+
+        return this;
+    },
+
+    show: function() {
+        var that = this;
+
+        this.render().$el.appendTo(_kiwi.app.view.$el);
+
+        // The element won't have any CSS transitions applied
+        // until after a tick + paint.
+        _.defer(function() {
+            that.$el.addClass('show');
+        });
+    },
+
+    close: function() {
+        this.remove();
+    }
+});
+
+
+(function() {
+
+    function ClientUiCommands(app, controlbox) {
+        this.app = app;
+        this.controlbox = controlbox;
+
+        this.addDefaultAliases();
+        this.bindCommand(fn_to_bind);
+    }
+
+    _kiwi.misc.ClientUiCommands = ClientUiCommands;
+
+
+    // Add the default user command aliases
+    ClientUiCommands.prototype.addDefaultAliases = function() {
+        $.extend(this.controlbox.preprocessor.aliases, {
+            // General aliases
+            '/p':        '/part $1+',
+            '/me':       '/action $1+',
+            '/j':        '/join $1+',
+            '/q':        '/query $1+',
+            '/w':        '/whois $1+',
+            '/raw':      '/quote $1+',
+            '/connect':  '/server $1+',
+
+            // Op related aliases
+            '/op':       '/quote mode $channel +o $1+',
+            '/deop':     '/quote mode $channel -o $1+',
+            '/hop':      '/quote mode $channel +h $1+',
+            '/dehop':    '/quote mode $channel -h $1+',
+            '/voice':    '/quote mode $channel +v $1+',
+            '/devoice':  '/quote mode $channel -v $1+',
+            '/k':        '/kick $channel $1+',
+            '/ban':      '/quote mode $channel +b $1+',
+            '/unban':    '/quote mode $channel -b $1+',
+
+            // Misc aliases
+            '/slap':     '/me slaps $1 around a bit with a large trout',
+            '/tick':     '/msg $channel âœ”'
+        });
+    };
+
+
+    /**
+     * Add a new command action
+     * @var command Object {'command:the_command': fn}
+     */
+    ClientUiCommands.prototype.bindCommand = function(command) {
+        var that = this;
+
+        _.each(command, function(fn, event_name) {
+            that.controlbox.on(event_name, _.bind(fn, that));
+        });
+    };
+
+
+
+
+    /**
+     * Default functions to bind to controlbox events
+     **/
+
+    var fn_to_bind = {
+        'unknown_command':     unknownCommand,
+        'command':             allCommands,
+        'command:msg':         msgCommand,
+        'command:action':      actionCommand,
+        'command:join':        joinCommand,
+        'command:part':        partCommand,
+        'command:cycle':        cycleCommand,
+        'command:nick':        nickCommand,
+        'command:query':       queryCommand,
+        'command:invite':      inviteCommand,
+        'command:topic':       topicCommand,
+        'command:notice':      noticeCommand,
+        'command:quote':       quoteCommand,
+        'command:kick':        kickCommand,
+        'command:clear':       clearCommand,
+        'command:ctcp':        ctcpCommand,
+        'command:quit':        quitCommand,
+        'command:server':      serverCommand,
+        'command:whois':       whoisCommand,
+        'command:whowas':      whowasCommand,
+        'command:away':        awayCommand,
+        'command:encoding':    encodingCommand,
+        'command:channel':     channelCommand,
+        'command:applet':      appletCommand,
+        'command:settings':    settingsCommand,
+        'command:script':      scriptCommand
+    };
+
+
+    fn_to_bind['command:css'] = function (ev) {
+        var queryString = '?reload=' + new Date().getTime();
+        $('link[rel="stylesheet"]').each(function () {
+            this.href = this.href.replace(/\?.*|$/, queryString);
+        });
+    };
+
+
+    fn_to_bind['command:js'] = function (ev) {
+        if (!ev.params[0]) return;
+        $script(ev.params[0] + '?' + (new Date().getTime()));
+    };
+
+
+    fn_to_bind['command:set'] = function (ev) {
+        if (!ev.params[0]) return;
+
+        var setting = ev.params[0],
+            value;
+
+        // Do we have a second param to set a value?
+        if (ev.params[1]) {
+            ev.params.shift();
+
+            value = ev.params.join(' ');
+
+            // If we're setting a true boolean value..
+            if (value === 'true')
+                value = true;
+
+            // If we're setting a false boolean value..
+            if (value === 'false')
+                value = false;
+
+            // If we're setting a number..
+            if (parseInt(value, 10).toString() === value)
+                value = parseInt(value, 10);
+
+            _kiwi.global.settings.set(setting, value);
+        }
+
+        // Read the value to the user
+        this.app.panels().active.addMsg('', styleText('set_setting', {text: setting + ' = ' + _kiwi.global.settings.get(setting).toString()}));
+    };
+
+
+    fn_to_bind['command:save'] = function (ev) {
+        _kiwi.global.settings.save();
+        this.app.panels().active.addMsg('', styleText('settings_saved', {text: translateText('client_models_application_settings_saved')}));
+    };
+
+
+    fn_to_bind['command:alias'] = function (ev) {
+        var that = this,
+            name, rule;
+
+        // No parameters passed so list them
+        if (!ev.params[1]) {
+            $.each(this.controlbox.preprocessor.aliases, function (name, rule) {
+                that.app.panels().server.addMsg(' ', styleText('list_aliases', {text: name + '   =>   ' + rule}));
+            });
+            return;
+        }
+
+        // Deleting an alias?
+        if (ev.params[0] === 'del' || ev.params[0] === 'delete') {
+            name = ev.params[1];
+            if (name[0] !== '/') name = '/' + name;
+            delete this.controlbox.preprocessor.aliases[name];
+            return;
+        }
+
+        // Add the alias
+        name = ev.params[0];
+        ev.params.shift();
+        rule = ev.params.join(' ');
+
+        // Make sure the name starts with a slash
+        if (name[0] !== '/') name = '/' + name;
+
+        // Now actually add the alias
+        this.controlbox.preprocessor.aliases[name] = rule;
+    };
+
+
+    fn_to_bind['command:ignore'] = function (ev) {
+        var that = this,
+            list = this.app.connections.active_connection.get('ignore_list');
+
+        // No parameters passed so list them
+        if (!ev.params[0]) {
+            if (list.length > 0) {
+                this.app.panels().active.addMsg(' ', styleText('ignore_title', {text: translateText('client_models_application_ignore_title')}));
+                $.each(list, function (idx, ignored_pattern) {
+                    that.app.panels().active.addMsg(' ', styleText('ignored_pattern', {text: ignored_pattern}));
+                });
+            } else {
+                this.app.panels().active.addMsg(' ', styleText('ignore_none', {text: translateText('client_models_application_ignore_none')}));
+            }
+            return;
+        }
+
+        // We have a parameter, so add it
+        list.push(ev.params[0]);
+        this.app.connections.active_connection.set('ignore_list', list);
+        this.app.panels().active.addMsg(' ', styleText('ignore_nick', {text: translateText('client_models_application_ignore_nick', [ev.params[0]])}));
+    };
+
+
+    fn_to_bind['command:unignore'] = function (ev) {
+        var list = this.app.connections.active_connection.get('ignore_list');
+
+        if (!ev.params[0]) {
+            this.app.panels().active.addMsg(' ', styleText('ignore_stop_notice', {text: translateText('client_models_application_ignore_stop_notice')}));
+            return;
+        }
+
+        list = _.reject(list, function(pattern) {
+            return pattern === ev.params[0];
+        });
+
+        this.app.connections.active_connection.set('ignore_list', list);
+
+        this.app.panels().active.addMsg(' ', styleText('ignore_stopped', {text: translateText('client_models_application_ignore_stopped', [ev.params[0]])}));
+    };
+
+
+
+
+    // A fallback action. Send a raw command to the server
+    function unknownCommand (ev) {
+        var raw_cmd = ev.command + ' ' + ev.params.join(' ');
+        this.app.connections.active_connection.gateway.raw(raw_cmd);
+    }
+
+
+    function allCommands (ev) {}
+
+
+    function joinCommand (ev) {
+        var panels, channel_names;
+
+        channel_names = ev.params.join(' ').split(',');
+        panels = this.app.connections.active_connection.createAndJoinChannels(channel_names);
+
+        // Show the last channel if we have one
+        if (panels.length)
+            panels[panels.length - 1].view.show();
+    }
+
+
+    function queryCommand (ev) {
+        var destination, message, panel;
+
+        destination = ev.params[0];
+        ev.params.shift();
+
+        message = ev.params.join(' ');
+
+        // Check if we have the panel already. If not, create it
+        panel = this.app.connections.active_connection.panels.getByName(destination);
+        if (!panel) {
+            panel = new _kiwi.model.Query({name: destination});
+            this.app.connections.active_connection.panels.add(panel);
+        }
+
+        if (panel) panel.view.show();
+
+        if (message) {
+            this.app.connections.active_connection.gateway.msg(panel.get('name'), message);
+            panel.addMsg(this.app.connections.active_connection.get('nick'), styleText('privmsg', {text: message}), 'privmsg');
+        }
+
+    }
+
+
+    function msgCommand (ev) {
+        var message,
+            destination = ev.params[0],
+            panel = this.app.connections.active_connection.panels.getByName(destination) || this.app.panels().server;
+
+        ev.params.shift();
+        message = ev.params.join(' ');
+
+        panel.addMsg(this.app.connections.active_connection.get('nick'), styleText('privmsg', {text: message}), 'privmsg');
+        this.app.connections.active_connection.gateway.msg(destination, message);
+    }
+
+
+    function actionCommand (ev) {
+        if (this.app.panels().active.isServer()) {
+            return;
+        }
+
+        var panel = this.app.panels().active;
+        panel.addMsg('', styleText('action', {nick: this.app.connections.active_connection.get('nick'), text: ev.params.join(' ')}), 'action');
+        this.app.connections.active_connection.gateway.action(panel.get('name'), ev.params.join(' '));
+    }
+
+
+    function partCommand (ev) {
+        var that = this,
+            chans,
+            msg;
+        if (ev.params.length === 0) {
+            this.app.connections.active_connection.gateway.part(this.app.panels().active.get('name'));
+        } else {
+            chans = ev.params[0].split(',');
+            msg = ev.params[1];
+            _.each(chans, function (channel) {
+                that.connections.active_connection.gateway.part(channel, msg);
+            });
+        }
+    }
+
+
+    function cycleCommand (ev) {
+        var that = this,
+            chan_name;
+
+        if (ev.params.length === 0) {
+            chan_name = this.app.panels().active.get('name');
+        } else {
+            chan_name = ev.params[0];
+        }
+
+        this.app.connections.active_connection.gateway.part(chan_name);
+
+        // Wait for a second to give the network time to register the part command
+        setTimeout(function() {
+            // Use createAndJoinChannels() here as it auto-creates panels instead of waiting for the network
+            that.app.connections.active_connection.createAndJoinChannels(chan_name);
+            that.app.connections.active_connection.panels.getByName(chan_name).show();
+        }, 1000);
+    }
+
+
+    function nickCommand (ev) {
+        this.app.connections.active_connection.gateway.changeNick(ev.params[0]);
+    }
+
+
+    function topicCommand (ev) {
+        var channel_name;
+
+        if (ev.params.length === 0) return;
+
+        if (this.app.connections.active_connection.isChannelName(ev.params[0])) {
+            channel_name = ev.params[0];
+            ev.params.shift();
+        } else {
+            channel_name = this.app.panels().active.get('name');
+        }
+
+        this.app.connections.active_connection.gateway.topic(channel_name, ev.params.join(' '));
+    }
+
+
+    function noticeCommand (ev) {
+        var destination;
+
+        // Make sure we have a destination and some sort of message
+        if (ev.params.length <= 1) return;
+
+        destination = ev.params[0];
+        ev.params.shift();
+
+        this.app.connections.active_connection.gateway.notice(destination, ev.params.join(' '));
+    }
+
+
+    function quoteCommand (ev) {
+        var raw = ev.params.join(' ');
+        this.app.connections.active_connection.gateway.raw(raw);
+    }
+
+
+    function kickCommand (ev) {
+        var nick, panel = this.app.panels().active;
+
+        if (!panel.isChannel()) return;
+
+        // Make sure we have a nick
+        if (ev.params.length === 0) return;
+
+        nick = ev.params[0];
+        ev.params.shift();
+
+        this.app.connections.active_connection.gateway.kick(panel.get('name'), nick, ev.params.join(' '));
+    }
+
+
+    function clearCommand (ev) {
+        // Can't clear a server or applet panel
+        if (this.app.panels().active.isServer() || this.app.panels().active.isApplet()) {
+            return;
+        }
+
+        if (this.app.panels().active.clearMessages) {
+            this.app.panels().active.clearMessages();
+        }
+    }
+
+
+    function ctcpCommand(ev) {
+        var target, type;
+
+        // Make sure we have a target and a ctcp type (eg. version, time)
+        if (ev.params.length < 2) return;
+
+        target = ev.params[0];
+        ev.params.shift();
+
+        type = ev.params[0];
+        ev.params.shift();
+
+        this.app.connections.active_connection.gateway.ctcpRequest(type, target, ev.params.join(' '));
+    }
+
+
+    function settingsCommand (ev) {
+        var settings = _kiwi.model.Applet.loadOnce('kiwi_settings');
+        settings.view.show();
+    }
+
+
+    function scriptCommand (ev) {
+        var editor = _kiwi.model.Applet.loadOnce('kiwi_script_editor');
+        editor.view.show();
+    }
+
+
+    function appletCommand (ev) {
+        if (!ev.params[0]) return;
+
+        var panel = new _kiwi.model.Applet();
+
+        if (ev.params[1]) {
+            // Url and name given
+            panel.load(ev.params[0], ev.params[1]);
+        } else {
+            // Load a pre-loaded applet
+            if (this.applets[ev.params[0]]) {
+                panel.load(new this.applets[ev.params[0]]());
+            } else {
+                this.app.panels().server.addMsg('', styleText('applet_notfound', {text: translateText('client_models_application_applet_notfound', [ev.params[0]])}));
+                return;
+            }
+        }
+
+        this.app.connections.active_connection.panels.add(panel);
+        panel.view.show();
+    }
+
+
+    function inviteCommand (ev) {
+        var nick, channel;
+
+        // A nick must be specified
+        if (!ev.params[0])
+            return;
+
+        // Can only invite into channels
+        if (!this.app.panels().active.isChannel())
+            return;
+
+        nick = ev.params[0];
+        channel = this.app.panels().active.get('name');
+
+        this.app.connections.active_connection.gateway.raw('INVITE ' + nick + ' ' + channel);
+
+        this.app.panels().active.addMsg('', styleText('channel_has_been_invited', {nick: nick, text: translateText('client_models_application_has_been_invited', [channel])}), 'action');
+    }
+
+
+    function whoisCommand (ev) {
+        var nick;
+
+        if (ev.params[0]) {
+            nick = ev.params[0];
+        } else if (this.app.panels().active.isQuery()) {
+            nick = this.app.panels().active.get('name');
+        }
+
+        if (nick)
+            this.app.connections.active_connection.gateway.raw('WHOIS ' + nick + ' ' + nick);
+    }
+
+
+    function whowasCommand (ev) {
+        var nick;
+
+        if (ev.params[0]) {
+            nick = ev.params[0];
+        } else if (this.app.panels().active.isQuery()) {
+            nick = this.app.panels().active.get('name');
+        }
+
+        if (nick)
+            this.app.connections.active_connection.gateway.raw('WHOWAS ' + nick);
+    }
+
+
+    function awayCommand (ev) {
+        this.app.connections.active_connection.gateway.raw('AWAY :' + ev.params.join(' '));
+    }
+
+
+    function encodingCommand (ev) {
+        var that = this;
+
+        if (ev.params[0]) {
+            _kiwi.gateway.setEncoding(null, ev.params[0], function (success) {
+                if (success) {
+                    that.app.panels().active.addMsg('', styleText('encoding_changed', {text: translateText('client_models_application_encoding_changed', [ev.params[0]])}));
+                } else {
+                    that.app.panels().active.addMsg('', styleText('encoding_invalid', {text: translateText('client_models_application_encoding_invalid', [ev.params[0]])}));
+                }
+            });
+        } else {
+            this.app.panels().active.addMsg('', styleText('client_models_application_encoding_notspecified', {text: translateText('client_models_application_encoding_notspecified')}));
+            this.app.panels().active.addMsg('', styleText('client_models_application_encoding_usage', {text: translateText('client_models_application_encoding_usage')}));
+        }
+    }
+
+
+    function channelCommand (ev) {
+        var active_panel = this.app.panels().active;
+
+        if (!active_panel.isChannel())
+            return;
+
+        new _kiwi.model.ChannelInfo({channel: this.app.panels().active});
+    }
+
+
+    function quitCommand (ev) {
+        var network = this.app.connections.active_connection;
+
+        if (!network)
+            return;
+
+        network.gateway.quit(ev.params.join(' '));
+    }
+
+
+    function serverCommand (ev) {
+        var that = this,
+            server, port, ssl, password, nick,
+            tmp;
+
+        // If no server address given, show the new connection dialog
+        if (!ev.params[0]) {
+            tmp = new _kiwi.view.MenuBox(_kiwi.global.i18n.translate('client_models_application_connection_create').fetch());
+            tmp.addItem('new_connection', new _kiwi.model.NewConnection().view.$el);
+            tmp.show();
+
+            // Center screen the dialog
+            tmp.$el.offset({
+                top: (this.app.view.$el.height() / 2) - (tmp.$el.height() / 2),
+                left: (this.app.view.$el.width() / 2) - (tmp.$el.width() / 2)
+            });
+
+            return;
+        }
+
+        // Port given in 'host:port' format and no specific port given after a space
+        if (ev.params[0].indexOf(':') > 0) {
+            tmp = ev.params[0].split(':');
+            server = tmp[0];
+            port = tmp[1];
+
+            password = ev.params[1] || undefined;
+
+        } else {
+            // Server + port given as 'host port'
+            server = ev.params[0];
+            port = ev.params[1] || 6667;
+
+            password = ev.params[2] || undefined;
+        }
+
+        // + in the port means SSL
+        if (port.toString()[0] === '+') {
+            ssl = true;
+            port = parseInt(port.substring(1), 10);
+        } else {
+            ssl = false;
+        }
+
+        // Default port if one wasn't found
+        port = port || 6667;
+
+        // Use the same nick as we currently have
+        nick = this.app.connections.active_connection.get('nick');
+
+        this.app.panels().active.addMsg('', styleText('server_connecting', {text: translateText('client_models_application_connection_connecting', [server, port.toString()])}));
+
+        _kiwi.gateway.newConnection({
+            nick: nick,
+            host: server,
+            port: port,
+            ssl: ssl,
+            password: password
+        }, function(err, new_connection) {
+            var translated_err;
+
+            if (err) {
+                translated_err = translateText('client_models_application_connection_error', [server, port.toString(), err.toString()]);
+                that.app.panels().active.addMsg('', styleText('server_connecting_error', {text: translated_err}));
+            }
+        });
+    }
+
+})();
+
+
+(function () {\r
+    var View = Backbone.View.extend({\r
+        events: {\r
+            'change [data-setting]': 'saveSettings',\r
+            'click [data-setting="theme"]': 'selectTheme',\r
+            'click .register_protocol': 'registerProtocol',\r
+            'click .enable_notifications': 'enableNotifications'\r
+        },\r
+\r
+        initialize: function (options) {\r
+            var text = {\r
+                tabs                  : translateText('client_applets_settings_channelview_tabs'),\r
+                list                  : translateText('client_applets_settings_channelview_list'),\r
+                large_amounts_of_chans: translateText('client_applets_settings_channelview_list_notice'),\r
+                join_part             : translateText('client_applets_settings_notification_joinpart'),\r
+                count_all_activity    : translateText('client_applets_settings_notification_count_all_activity'),\r
+                timestamps            : translateText('client_applets_settings_timestamp'),\r
+                timestamp_24          : translateText('client_applets_settings_timestamp_24_hour'),\r
+                mute                  : translateText('client_applets_settings_notification_sound'),\r
+                emoticons             : translateText('client_applets_settings_emoticons'),\r
+                scroll_history        : translateText('client_applets_settings_history_length'),\r
+                languages             : _kiwi.app.translations,\r
+                default_client        : translateText('client_applets_settings_default_client'),\r
+                make_default          : translateText('client_applets_settings_default_client_enable'),\r
+                locale_restart_needed : translateText('client_applets_settings_locale_restart_needed'),\r
+                default_note          : translateText('client_applets_settings_default_client_notice', '<a href="chrome://settings/handlers">chrome://settings/handlers</a>'),\r
+                html5_notifications   : translateText('client_applets_settings_html5_notifications'),\r
+                enable_notifications  : translateText('client_applets_settings_enable_notifications'),\r
+                theme_thumbnails: _.map(_kiwi.app.themes, function (theme) {\r
+                    return _.template($('#tmpl_theme_thumbnail').html().trim(), theme);\r
+                })\r
+            };\r
+            this.$el = $(_.template($('#tmpl_applet_settings').html().trim(), text));\r
+\r
+            if (!navigator.registerProtocolHandler) {\r
+                this.$('.protocol_handler').remove();\r
+            }\r
+\r
+            if (_kiwi.utils.notifications.allowed() !== null) {\r
+                this.$('.notification_enabler').remove();\r
+            }\r
+\r
+            // Incase any settings change while we have this open, update them\r
+            _kiwi.global.settings.on('change', this.loadSettings, this);\r
+\r
+            // Now actually show the current settings\r
+            this.loadSettings();\r
+\r
+        },\r
+\r
+        loadSettings: function () {\r
+\r
+            _.each(_kiwi.global.settings.attributes, function(value, key) {\r
+\r
+                var $el = this.$('[data-setting="' + key + '"]');\r
+\r
+                // Only deal with settings we have a UI element for\r
+                if (!$el.length)\r
+                    return;\r
+\r
+                switch ($el.prop('type')) {\r
+                    case 'checkbox':\r
+                        $el.prop('checked', value);\r
+                        break;\r
+                    case 'radio':\r
+                        this.$('[data-setting="' + key + '"][value="' + value + '"]').prop('checked', true);\r
+                        break;\r
+                    case 'text':\r
+                        $el.val(value);\r
+                        break;\r
+                    case 'select-one':\r
+                        this.$('[value="' + value + '"]').prop('selected', true);\r
+                        break;\r
+                    default:\r
+                        this.$('[data-setting="' + key + '"][data-value="' + value + '"]').addClass('active');\r
+                        break;\r
+                }\r
+            }, this);\r
+        },\r
+\r
+        saveSettings: function (event) {\r
+            var value,\r
+                settings = _kiwi.global.settings,\r
+                $setting = $(event.currentTarget);\r
+\r
+            switch (event.currentTarget.type) {\r
+                case 'checkbox':\r
+                    value = $setting.is(':checked');\r
+                    break;\r
+                case 'radio':\r
+                case 'text':\r
+                    value = $setting.val();\r
+                    break;\r
+                case 'select-one':\r
+                    value = $(event.currentTarget[$setting.prop('selectedIndex')]).val();\r
+                    break;\r
+                default:\r
+                    value = $setting.data('value');\r
+                    break;\r
+            }\r
+\r
+            // Stop settings being updated while we're saving one by one\r
+            _kiwi.global.settings.off('change', this.loadSettings, this);\r
+            settings.set($setting.data('setting'), value);\r
+            settings.save();\r
+\r
+            // Continue listening for setting changes\r
+            _kiwi.global.settings.on('change', this.loadSettings, this);\r
+        },\r
+\r
+        selectTheme: function(event) {\r
+            event.preventDefault();\r
+\r
+            this.$('[data-setting="theme"].active').removeClass('active');\r
+            $(event.currentTarget).addClass('active').trigger('change');\r
+        },\r
+\r
+        registerProtocol: function (event) {\r
+            event.preventDefault();\r
+\r
+            navigator.registerProtocolHandler('irc', document.location.origin + _kiwi.app.get('base_path') + '/%s', 'Kiwi IRC');\r
+            navigator.registerProtocolHandler('ircs', document.location.origin + _kiwi.app.get('base_path') + '/%s', 'Kiwi IRC');\r
+        },\r
+\r
+        enableNotifications: function(event){\r
+            event.preventDefault();\r
+            var notifications = _kiwi.utils.notifications;\r
+\r
+            notifications.requestPermission().always(_.bind(function () {\r
+                if (notifications.allowed() !== null) {\r
+                    this.$('.notification_enabler').remove();\r
+                }\r
+            }, this));\r
+        }\r
+\r
+    });\r
+\r
+\r
+    var Applet = Backbone.Model.extend({\r
+        initialize: function () {\r
+            this.set('title', translateText('client_applets_settings_title'));\r
+            this.view = new View();\r
+        }\r
+    });\r
+\r
+\r
+    _kiwi.model.Applet.register('kiwi_settings', Applet);\r
+})();\r
+
+
+
+(function () {\r
+\r
+    var View = Backbone.View.extend({\r
+        events: {\r
+            "click .chan": "chanClick",\r
+            "click .channel_name_title": "sortChannelsByNameClick",\r
+            "click .users_title": "sortChannelsByUsersClick"\r
+        },\r
+\r
+\r
+\r
+        initialize: function (options) {\r
+            var text = {\r
+                channel_name: _kiwi.global.i18n.translate('client_applets_chanlist_channelname').fetch(),\r
+                users: _kiwi.global.i18n.translate('client_applets_chanlist_users').fetch(),\r
+                topic: _kiwi.global.i18n.translate('client_applets_chanlist_topic').fetch()\r
+            };\r
+            this.$el = $(_.template($('#tmpl_channel_list').html().trim(), text));\r
+\r
+            this.channels = [];\r
+\r
+            // Sort the table\r
+            this.order = '';\r
+\r
+            // Waiting to add the table back into the DOM?\r
+            this.waiting = false;\r
+        },\r
+\r
+        render: function () {\r
+            var table = $('table', this.$el),\r
+                tbody = table.children('tbody:first').detach(),\r
+                that = this,\r
+                i;\r
+\r
+            // Create the sort icon container and clean previous any previous ones\r
+            if($('.applet_chanlist .users_title').find('span.chanlist_sort_users').length == 0) {\r
+                this.$('.users_title').append('<span class="chanlist_sort_users">&nbsp;&nbsp;</span>');\r
+            } else {\r
+                this.$('.users_title span.chanlist_sort_users').removeClass('fa fa-sort-desc');\r
+                this.$('.users_title span.chanlist_sort_users').removeClass('fa fa-sort-asc');\r
+            }\r
+            if ($('.applet_chanlist .channel_name_title').find('span.chanlist_sort_names').length == 0) {\r
+                this.$('.channel_name_title').append('<span class="chanlist_sort_names">&nbsp;&nbsp;</span>');\r
+            } else {\r
+                this.$('.channel_name_title span.chanlist_sort_names').removeClass('fa fa-sort-desc');\r
+                this.$('.channel_name_title span.chanlist_sort_names').removeClass('fa fa-sort-asc');\r
+            }\r
+\r
+            // Push the new sort icon\r
+            switch (this.order) {\r
+                case 'user_desc':\r
+                default:\r
+                    this.$('.users_title span.chanlist_sort_users').addClass('fa fa-sort-asc');\r
+                    break;\r
+                case 'user_asc':\r
+                    this.$('.users_title span.chanlist_sort_users').addClass('fa fa-sort-desc');\r
+                    break;\r
+                case 'name_asc':\r
+                    this.$('.channel_name_title span.chanlist_sort_names').addClass('fa fa-sort-desc');\r
+                    break;\r
+                case 'name_desc':\r
+                    this.$('.channel_name_title span.chanlist_sort_names').addClass('fa fa-sort-asc');\r
+                    break;\r
+            }\r
+\r
+            this.channels = this.sortChannels(this.channels, this.order);\r
+\r
+            // Make sure all the channel DOM nodes are inserted in order\r
+            for (i = 0; i < this.channels.length; i++) {\r
+                tbody[0].appendChild(this.channels[i].dom);\r
+            }\r
+\r
+            table[0].appendChild(tbody[0]);\r
+        },\r
+\r
+\r
+        chanClick: function (event) {\r
+            if (event.target) {\r
+                _kiwi.gateway.join(null, $(event.target).data('channel'));\r
+            } else {\r
+                // IE...\r
+                _kiwi.gateway.join(null, $(event.srcElement).data('channel'));\r
+            }\r
+        },\r
+\r
+        sortChannelsByNameClick: function (event) {\r
+            // Revert the sorting to switch between orders\r
+            this.order = (this.order == 'name_asc') ? 'name_desc' : 'name_asc';\r
+\r
+            this.sortChannelsClick();\r
+        },\r
+\r
+        sortChannelsByUsersClick: function (event) {\r
+            // Revert the sorting to switch between orders\r
+            this.order = (this.order == 'user_desc' || this.order == '') ? 'user_asc' : 'user_desc';\r
+\r
+            this.sortChannelsClick();\r
+        },\r
+\r
+        sortChannelsClick: function() {\r
+            this.render();\r
+        },\r
+\r
+        sortChannels: function (channels, order) {\r
+            var sort_channels = [],\r
+                new_channels = [];\r
+\r
+\r
+            // First we create a light copy of the channels object to do the sorting\r
+            _.each(channels, function (chan, chan_idx) {\r
+                sort_channels.push({'chan_idx': chan_idx, 'num_users': chan.num_users, 'channel': chan.channel});\r
+            });\r
+\r
+            // Second, we apply the sorting\r
+            sort_channels.sort(function (a, b) {\r
+                switch (order) {\r
+                    case 'user_asc':\r
+                        return a.num_users - b.num_users;\r
+                    case 'user_desc':\r
+                        return b.num_users - a.num_users;\r
+                    case 'name_asc':\r
+                        if (a.channel.toLowerCase() > b.channel.toLowerCase()) return 1;\r
+                        if (a.channel.toLowerCase() < b.channel.toLowerCase()) return -1;\r
+                    case 'name_desc':\r
+                        if (a.channel.toLowerCase() < b.channel.toLowerCase()) return 1;\r
+                        if (a.channel.toLowerCase() > b.channel.toLowerCase()) return -1;\r
+                    default:\r
+                        return b.num_users - a.num_users;\r
+                }\r
+                return 0;\r
+            });\r
+\r
+            // Third, we re-shuffle the chanlist according to the sort order\r
+            _.each(sort_channels, function (chan) {\r
+                new_channels.push(channels[chan.chan_idx]);\r
+            });\r
+\r
+            return new_channels;\r
+        }\r
+    });\r
+\r
+\r
+\r
+    var Applet = Backbone.Model.extend({\r
+        initialize: function () {\r
+            this.set('title', _kiwi.global.i18n.translate('client_applets_chanlist_channellist').fetch());\r
+            this.view = new View();\r
+\r
+            this.network = _kiwi.global.components.Network();\r
+            this.network.on('list_channel', this.onListChannel, this);\r
+            this.network.on('list_start', this.onListStart, this);\r
+        },\r
+\r
+\r
+        // New channels to add to our list\r
+        onListChannel: function (event) {\r
+            this.addChannel(event.chans);\r
+        },\r
+\r
+        // A new, fresh channel list starting\r
+        onListStart: function (event) {\r
+            // TODO: clear out our existing list\r
+        },\r
+\r
+        addChannel: function (channels) {\r
+            var that = this;\r
+\r
+            if (!_.isArray(channels)) {\r
+                channels = [channels];\r
+            }\r
+            _.each(channels, function (chan) {\r
+                var row;\r
+                row = document.createElement("tr");\r
+                row.innerHTML = '<td class="chanlist_name"><a class="chan" data-channel="' + chan.channel + '">' + _.escape(chan.channel) + '</a></td><td class="chanlist_num_users" style="text-align: center;">' + chan.num_users + '</td><td style="padding-left: 2em;" class="chanlist_topic">' + formatIRCMsg(_.escape(chan.topic)) + '</td>';\r
+                chan.dom = row;\r
+                that.view.channels.push(chan);\r
+            });\r
+\r
+            if (!that.view.waiting) {\r
+                that.view.waiting = true;\r
+                _.defer(function () {\r
+                    that.view.render();\r
+                    that.view.waiting = false;\r
+                });\r
+            }\r
+        },\r
+\r
+\r
+        dispose: function () {\r
+            this.view.channels = null;\r
+            this.view.unbind();\r
+            this.view.$el.html('');\r
+            this.view.remove();\r
+            this.view = null;\r
+\r
+            // Remove any network event bindings\r
+            this.network.off();\r
+        }\r
+    });\r
+\r
+\r
+\r
+    _kiwi.model.Applet.register('kiwi_chanlist', Applet);\r
+})();
+
+
+    (function () {
+        var view = Backbone.View.extend({
+            events: {
+                'click .btn_save': 'onSave'
+            },
+
+            initialize: function (options) {
+                var that = this,
+                    text = {
+                        save: _kiwi.global.i18n.translate('client_applets_scripteditor_save').fetch()
+                    };
+                this.$el = $(_.template($('#tmpl_script_editor').html().trim(), text));
+
+                this.model.on('applet_loaded', function () {
+                    that.$el.parent().css('height', '100%');
+                    $script(_kiwi.app.get('base_path') + '/assets/libs/ace/ace.js', function (){ that.createAce(); });
+                });
+            },
+
+
+            createAce: function () {
+                var editor_id = 'editor_' + Math.floor(Math.random()*10000000).toString();
+                this.editor_id = editor_id;
+
+                this.$el.find('.editor').attr('id', editor_id);
+
+                this.editor = ace.edit(editor_id);
+                this.editor.setTheme("ace/theme/monokai");
+                this.editor.getSession().setMode("ace/mode/javascript");
+
+                var script_content = _kiwi.global.settings.get('user_script') || '';
+                this.editor.setValue(script_content);
+            },
+
+
+            onSave: function (event) {
+                var script_content, user_fn;
+
+                // Build the user script up with some pre-defined components
+                script_content = 'var network = kiwi.components.Network();\n';
+                script_content += 'var input = kiwi.components.ControlInput();\n';
+                script_content += 'var events = kiwi.components.Events();\n';
+                script_content += this.editor.getValue() + '\n';
+
+                // Add a dispose method to the user script for cleaning up
+                script_content += 'this._dispose = function(){ network.off(); input.off(); events.dispose(); if(this.dispose) this.dispose(); }';
+
+                // Try to compile the user script
+                try {
+                    user_fn = new Function(script_content);
+
+                    // Dispose any existing user script
+                    if (_kiwi.user_script && _kiwi.user_script._dispose)
+                        _kiwi.user_script._dispose();
+
+                    // Create and run the new user script
+                    _kiwi.user_script = new user_fn();
+
+                } catch (err) {
+                    this.setStatus(_kiwi.global.i18n.translate('client_applets_scripteditor_error').fetch(err.toString()));
+                    return;
+                }
+
+                // If we're this far, no errors occured. Save the user script
+                _kiwi.global.settings.set('user_script', this.editor.getValue());
+                _kiwi.global.settings.save();
+
+                this.setStatus(_kiwi.global.i18n.translate('client_applets_scripteditor_saved').fetch() + ' :)');
+            },
+
+
+            setStatus: function (status_text) {
+                var $status = this.$el.find('.toolbar .status');
+
+                status_text = status_text || '';
+                $status.slideUp('fast', function() {
+                    $status.text(status_text);
+                    $status.slideDown();
+                });
+            }
+        });
+
+
+
+        var applet = Backbone.Model.extend({
+            initialize: function () {
+                var that = this;
+
+                this.set('title', _kiwi.global.i18n.translate('client_applets_scripteditor_title').fetch());
+                this.view = new view({model: this});
+
+            }
+        });
+
+
+        _kiwi.model.Applet.register('kiwi_script_editor', applet);
+        //_kiwi.model.Applet.loadOnce('kiwi_script_editor');
+    })();
+
+
+(function () {
+    var view = Backbone.View.extend({
+        events: {},
+
+
+        initialize: function (options) {
+            this.showConnectionDialog();
+        },
+
+
+        showConnectionDialog: function() {
+            var connection_dialog = this.connection_dialog = new _kiwi.model.NewConnection();
+            connection_dialog.populateDefaultServerSettings();
+
+            connection_dialog.view.$el.addClass('initial');
+            this.$el.append(connection_dialog.view.$el);
+
+            var $info = $($('#tmpl_new_connection_info').html().trim());
+
+            if ($info.html()) {
+                connection_dialog.view.infoBoxSet($info);
+            } else {
+                $info = null;
+            }
+
+            this.listenTo(connection_dialog, 'connected', this.newConnectionConnected);
+
+            _.defer(function(){
+                if ($info) {
+                    connection_dialog.view.infoBoxShow();
+                }
+
+                // Only set focus if we're not within an iframe. (firefox auto scrolls to the embedded client on page load - bad)
+                if (window == window.top) {
+                    connection_dialog.view.$el.find('.nick').select();
+                }
+            });
+        },
+
+
+        newConnectionConnected: function(network) {
+            // Once connected, reset the connection form to be used again in future
+            this.connection_dialog.view.reset();
+        }
+    });
+
+
+
+    var applet = Backbone.Model.extend({
+        initialize: function () {
+            this.view = new view({model: this});
+        }
+    });
+
+
+    _kiwi.model.Applet.register('kiwi_startup', applet);
+})();
+
+
+
+_kiwi.utils.notifications = (function () {
+    if (!window.Notification) {
+        return {
+            allowed: _.constant(false),
+            requestPermission: _.constant($.Deferred().reject())
+        };
+    }
+
+    var notifications = {
+        /**
+         * Check if desktop notifications have been allowed by the user.
+         *
+         * @returns {?Boolean} `true`  - they have been allowed.
+         *                     `false` - they have been blocked.
+         *                     `null`  - the user hasn't answered yet.
+         */
+        allowed: function () {
+            return Notification.permission === 'granted' ? true
+                 : Notification.permission === 'denied' ? false
+                 : null;
+        },
+
+        /**
+         * Ask the user their permission to display desktop notifications.
+         * This will return a promise which will be resolved if the user allows notifications, or rejected if they blocked
+         * notifictions or simply closed the dialog. If the user had previously given their preference, the promise will be
+         * immediately resolved or rejected with their previous answer.
+         *
+         * @example
+         *   notifications.requestPermission().then(function () { 'allowed' }, function () { 'not allowed' });
+         *
+         * @returns {Promise}
+         */
+        requestPermission: function () {
+            var deferred = $.Deferred();
+            Notification.requestPermission(function (permission) {
+                deferred[(permission === 'granted') ? 'resolve' : 'reject']();
+            });
+            return deferred.promise();
+        },
+
+        /**
+         * Create a new notification. If the user has not yet given permission to display notifications, they will be asked
+         * to confirm first. The notification will show afterwards if they allow it.
+         *
+         * Notifications implement Backbone.Events (so you can use `on` and `off`). They trigger four different events:
+         *   - 'click'
+         *   - 'close'
+         *   - 'error'
+         *   - 'show'
+         *
+         * @example
+         *   notifications
+         *     .create('Cool notification', { icon: 'logo.png' })
+         *     .on('click', function () {
+         *       window.focus();
+         *     })
+         *     .closeAfter(5000);
+         *
+         * @param   {String}  title
+         * @param   {Object}  options
+         * @param   {String=} options.body  A string representing an extra content to display within the notification
+         * @param   {String=} options.dir   The direction of the notification; it can be auto, ltr, or rtl
+         * @param   {String=} options.lang  Specify the lang used within the notification. This string must be a valid BCP
+         *                                  47 language tag.
+         * @param   {String=} options.tag   An ID for a given notification that allows to retrieve, replace or remove it if necessary
+         * @param   {String=} options.icon  The URL of an image to be used as an icon by the notification
+         * @returns {Notifier}
+         */
+        create: function (title, options) {
+            return new Notifier(title, options);
+        }
+    };
+
+    function Notifier(title, options) {
+        createNotification.call(this, title, options);
+    }
+    _.extend(Notifier.prototype, Backbone.Events, {
+        closed: false,
+        _closeTimeout: null,
+
+        /**
+         * Close the notification after a given number of milliseconds.
+         * @param   {Number} timeout
+         * @returns {this}
+         */
+        closeAfter: function (timeout) {
+            if (!this.closed) {
+                if (this.notification) {
+                    this._closeTimeout = this._closeTimeout || setTimeout(_.bind(this.close, this), timeout);
+                } else {
+                    this.once('show', _.bind(this.closeAfter, this, timeout));
+                }
+            }
+            return this;
+        },
+
+        /**
+         * Close the notification immediately.
+         * @returns {this}
+         */
+        close: function () {
+            if (this.notification && !this.closed) {
+                this.notification.close();
+                this.closed = true;
+            }
+            return this;
+        }
+    });
+
+    function createNotification(title, options) {
+        switch (notifications.allowed()) {
+            case true:
+                this.notification = new Notification(title, options);
+                _.each(['click', 'close', 'error', 'show'], function (eventName) {
+                    this.notification['on' + eventName] = _.bind(this.trigger, this, eventName);
+                }, this);
+                break;
+            case null:
+                notifications.requestPermission().done(_.bind(createNotification, this, title, options));
+                break;
+        }
+    }
+
+    return notifications;
+}());
+
+
+
+_kiwi.utils.formatDate = (function() {
+    /*
+    Modified version of date.format.js
+    https://github.com/jacwright/date.format
+    */
+    var locale_init = false, // Once the loales have been loaded, this is set to true
+        shortMonths, longMonths, shortDays, longDays;
+
+    // defining patterns
+    var replaceChars = {
+        // Day
+        d: function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); },
+        D: function() { return Date.shortDays[this.getDay()]; },
+        j: function() { return this.getDate(); },
+        l: function() { return Date.longDays[this.getDay()]; },
+        N: function() { return this.getDay() + 1; },
+        S: function() { return (this.getDate() % 10 == 1 && this.getDate() != 11 ? 'st' : (this.getDate() % 10 == 2 && this.getDate() != 12 ? 'nd' : (this.getDate() % 10 == 3 && this.getDate() != 13 ? 'rd' : 'th'))); },
+        w: function() { return this.getDay(); },
+        z: function() { var d = new Date(this.getFullYear(),0,1); return Math.ceil((this - d) / 86400000); }, // Fixed now
+        // Week
+        W: function() { var d = new Date(this.getFullYear(), 0, 1); return Math.ceil((((this - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now
+        // Month
+        F: function() { return Date.longMonths[this.getMonth()]; },
+        m: function() { return (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1); },
+        M: function() { return Date.shortMonths[this.getMonth()]; },
+        n: function() { return this.getMonth() + 1; },
+        t: function() { var d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 0).getDate(); }, // Fixed now, gets #days of date
+        // Year
+        L: function() { var year = this.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); },   // Fixed now
+        o: function() { var d  = new Date(this.valueOf());  d.setDate(d.getDate() - ((this.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now
+        Y: function() { return this.getFullYear(); },
+        y: function() { return ('' + this.getFullYear()).substr(2); },
+        // Time
+        a: function() { return this.getHours() < 12 ? 'am' : 'pm'; },
+        A: function() { return this.getHours() < 12 ? 'AM' : 'PM'; },
+        B: function() { return Math.floor((((this.getUTCHours() + 1) % 24) + this.getUTCMinutes() / 60 + this.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
+        g: function() { return this.getHours() % 12 || 12; },
+        G: function() { return this.getHours(); },
+        h: function() { return ((this.getHours() % 12 || 12) < 10 ? '0' : '') + (this.getHours() % 12 || 12); },
+        H: function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); },
+        i: function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); },
+        s: function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); },
+        u: function() { var m = this.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m; },
+        // Timezone
+        e: function() { return "Not Yet Supported"; },
+        I: function() {
+            var DST = null;
+                for (var i = 0; i < 12; ++i) {
+                        var d = new Date(this.getFullYear(), i, 1);
+                        var offset = d.getTimezoneOffset();
+
+                        if (DST === null) DST = offset;
+                        else if (offset < DST) { DST = offset; break; }
+                        else if (offset > DST) break;
+                }
+                return (this.getTimezoneOffset() == DST) | 0;
+            },
+        O: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + '00'; },
+        P: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now
+        T: function() { var m = this.getMonth(); this.setMonth(0); var result = this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); this.setMonth(m); return result;},
+        Z: function() { return -this.getTimezoneOffset() * 60; },
+        // Full Date/Time
+        c: function() { return this.format("Y-m-d\\TH:i:sP"); }, // Fixed now
+        r: function() { return this.toString(); },
+        U: function() { return this.getTime() / 1000; }
+    };
+
+
+    var initLocaleFormats = function() {
+        shortMonths = [
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.january').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.february').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.march').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.april').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.may').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.june').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.july').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.august').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.september').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.october').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.november').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.december').fetch()
+        ];
+        longMonths = [
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.january').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.february').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.march').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.april').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.may').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.june').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.july').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.august').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.september').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.october').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.november').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.december').fetch()
+        ];
+        shortDays = [
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.monday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.tuesday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.wednesday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.thursday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.friday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.saturday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.sunday').fetch()
+        ];
+        longDays = [
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.monday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.tuesday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.wednesday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.thursday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.friday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.saturday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.sunday').fetch()
+        ];
+
+        locale_init = true;
+    };
+    /* End of date.format */
+
+
+    // Finally.. the actuall formatDate function
+    return function(working_date, format) {
+        if (!locale_init)
+            initLocaleFormats();
+
+        working_date = working_date || new Date();
+        format = format || _kiwi.global.i18n.translate('client_date_format').fetch();
+
+        return format.replace(/(\\?)(.)/g, function(_, esc, chr) {
+            return (esc === '' && replaceChars[chr]) ? replaceChars[chr].call(working_date) : chr;
+        });
+    };
+})();
+
+
+/*
+ * The same functionality as EventEmitter but with the inclusion of callbacks
+ */
+
+
+
+function PluginInterface () {
+    // Holder for all the bound listeners by this module
+    this._listeners = {};
+
+    // Event proxies
+    this._parent = null;
+    this._children = [];
+}
+
+
+
+PluginInterface.prototype.on = function (event_name, fn, scope) {
+    this._listeners[event_name] = this._listeners[event_name] || [];
+    this._listeners[event_name].push(['on', fn, scope]);
+};
+
+
+
+PluginInterface.prototype.once = function (event_name, fn, scope) {
+    this._listeners[event_name] = this._listeners[event_name] || [];
+    this._listeners[event_name].push(['once', fn, scope]);
+};
+
+
+
+PluginInterface.prototype.off = function (event_name, fn, scope) {
+    var idx;
+
+    if (typeof event_name === 'undefined') {
+        // Remove all listeners
+        this._listeners = {};
+
+    } else if (typeof fn === 'undefined') {
+        // Remove all of 1 event type
+        delete this._listeners[event_name];
+
+    } else if (typeof scope === 'undefined') {
+        // Remove a single event type + callback
+        for (idx in (this._listeners[event_name] || [])) {
+            if (this._listeners[event_name][idx][1] === fn) {
+                delete this._listeners[event_name][idx];
+            }
+        }
+    } else {
+        // Remove a single event type + callback + scope
+        for (idx in (this._listeners[event_name] || [])) {
+            if (this._listeners[event_name][idx][1] === fn && this._listeners[event_name][idx][2] === scope) {
+                delete this._listeners[event_name][idx];
+            }
+        }
+    }
+};
+
+
+
+PluginInterface.prototype.getListeners = function(event_name) {
+    return this._listeners[event_name] || [];
+};
+
+
+
+PluginInterface.prototype.createProxy = function() {
+    var proxy = new PluginInterface();
+    proxy._parent = this._parent || this;
+    proxy._parent._children.push(proxy);
+
+    return proxy;
+};
+
+
+
+PluginInterface.prototype.dispose = function() {
+    this.off();
+
+    if (this._parent) {
+        var idx = this._parent._children.indexOf(this);
+        if (idx > -1) {
+            this._parent._children.splice(idx, 1);
+        }
+    }
+};
+
+
+
+// Call all the listeners for a certain event, passing them some event data that may be changed
+PluginInterface.prototype.emit = function (event_name, event_data) {
+    var emitter = new this.EmitCall(event_name, event_data),
+        listeners = [],
+        child_idx;
+
+    // Get each childs event listeners in order of last created
+    for(child_idx=this._children.length-1; child_idx>=0; child_idx--) {
+        listeners = listeners.concat(this._children[child_idx].getListeners(event_name));
+    }
+
+    // Now include any listeners directly on this instance
+    listeners = listeners.concat(this.getListeners(event_name));
+
+    // Once emitted, remove any 'once' bound listeners
+    emitter.then(function () {
+        var len = listeners.length,
+            idx;
+
+        for(idx = 0; idx < len; idx++) {
+            if (listeners[idx][0] === 'once') {
+                listeners[idx] = undefined;
+            }
+        }
+    });
+
+    // Emit the event to the listeners and return
+    emitter.callListeners(listeners);
+    return emitter;
+};
+
+
+
+// Promise style object to emit events to listeners
+PluginInterface.prototype.EmitCall = function EmitCall (event_name, event_data) {
+    var that = this,
+        completed = false,
+        completed_fn = [],
+
+        // Has event.preventDefault() been called
+        prevented = false,
+        prevented_fn = [];
+
+
+    // Emit this event to an array of listeners
+    function callListeners(listeners) {
+        var current_event_idx = -1;
+
+        // Make sure we have some data to pass to the listeners
+        event_data = event_data || undefined;
+
+        // If no bound listeners for this event, leave now
+        if (listeners.length === 0) {
+            emitComplete();
+            return;
+        }
+
+
+        // Call the next listener in our array
+        function nextListener() {
+            var listener, event_obj;
+
+            // We want the next listener
+            current_event_idx++;
+
+            // If we've ran out of listeners end this emit call
+            if (!listeners[current_event_idx]) {
+                emitComplete();
+                return;
+            }
+
+            // Object the listener ammends to tell us what it's going to do
+            event_obj = {
+                // If changed to true, expect this listener is going to callback
+                wait: false,
+
+                // If wait is true, this callback must be called to continue running listeners
+                callback: function () {
+                    // Invalidate this callback incase a listener decides to call it again
+                    event_obj.callback = undefined;
+
+                    nextListener.apply(that);
+                },
+
+                // Prevents the default 'done' functions from executing
+                preventDefault: function () {
+                    prevented = true;
+                }
+            };
+
+
+            listener = listeners[current_event_idx];
+            listener[1].call(listener[2] || that, event_obj, event_data);
+
+            // If the listener hasn't signalled it's going to wait, proceed to next listener
+            if (!event_obj.wait) {
+                // Invalidate the callback just incase a listener decides to call it anyway
+                event_obj.callback = undefined;
+
+                nextListener();
+            }
+        }
+
+        nextListener();
+    }
+
+
+
+    function emitComplete() {
+        completed = true;
+
+        var funcs = prevented ? prevented_fn : completed_fn;
+        funcs = funcs || [];
+
+        // Call the completed/prevented functions
+        for (var idx = 0; idx < funcs.length; idx++) {
+            if (typeof funcs[idx] === 'function') funcs[idx]();
+        }
+    }
+
+
+
+    function addCompletedFunc(fn) {
+        // Only accept functions
+        if (typeof fn !== 'function') return false;
+
+        completed_fn.push(fn);
+
+        // If we have already completed the emits, call this now
+        if (completed && !prevented) fn();
+
+        return this;
+    }
+
+
+
+    function addPreventedFunc(fn) {
+        // Only accept functions
+        if (typeof fn !== 'function') return false;
+
+        prevented_fn.push(fn);
+
+        // If we have already completed the emits, call this now
+        if (completed && prevented) fn();
+
+        return this;
+    }
+
+
+    return {
+        callListeners: callListeners,
+        then: addCompletedFunc,
+        catch: addPreventedFunc
+    };
+};
+
+
+
+// If running a node module, set the exports
+if (typeof module === 'object' && typeof module.exports !== 'undefined') {
+    module.exports = PluginInterface;
+}
+
+
+
+/*
+ * Example usage
+ */
+
+
+/*
+var modules = new PluginInterface();
+
+
+
+// A plugin
+modules.on('irc message', function (event, data) {
+    //event.wait = true;
+    setTimeout(event.callback, 2000);
+});
+
+
+
+
+// Core code that is being extended by plugins
+var data = {
+    nick: 'prawnsalald',
+    command: '/dothis'
+};
+
+modules.emit('irc message', data).done(function () {
+    console.log('Your command is: ' + data.command);
+});
+*/
+
+
+/*jslint devel: true, browser: true, continue: true, sloppy: true, forin: true, plusplus: true, maxerr: 50, indent: 4, nomen: true, regexp: true*/
+/*globals $, front, gateway, Utilityview */
+
+
+
+/**
+*   Generate a random string of given length
+*   @param      {Number}    string_length   The length of the random string
+*   @returns    {String}                    The random string
+*/
+function randomString(string_length) {
+    var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",
+        randomstring = '',
+        i,
+        rnum;
+    for (i = 0; i < string_length; i++) {
+        rnum = Math.floor(Math.random() * chars.length);
+        randomstring += chars.substring(rnum, rnum + 1);
+    }
+    return randomstring;
+}
+
+/**
+*   String.trim shim
+*/
+if (typeof String.prototype.trim === 'undefined') {
+    String.prototype.trim = function () {
+        return this.replace(/^\s+|\s+$/g, "");
+    };
+}
+
+/**
+*   String.lpad shim
+*   @param      {Number}    length      The length of padding
+*   @param      {String}    characher   The character to pad with
+*   @returns    {String}                The padded string
+*/
+if (typeof String.prototype.lpad === 'undefined') {
+    String.prototype.lpad = function (length, character) {
+        var padding = "",
+            i;
+        for (i = 0; i < length; i++) {
+            padding += character;
+        }
+        return (padding + this).slice(-length);
+    };
+}
+
+
+/**
+*   Convert seconds into hours:minutes:seconds
+*   @param      {Number}    secs    The number of seconds to converts
+*   @returns    {Object}            An object representing the hours/minutes/second conversion of secs
+*/
+function secondsToTime(secs) {
+    var hours, minutes, seconds, divisor_for_minutes, divisor_for_seconds, obj;
+    hours = Math.floor(secs / (60 * 60));
+
+    divisor_for_minutes = secs % (60 * 60);
+    minutes = Math.floor(divisor_for_minutes / 60);
+
+    divisor_for_seconds = divisor_for_minutes % 60;
+    seconds = Math.ceil(divisor_for_seconds);
+
+    obj = {
+        "h": hours,
+        "m": minutes,
+        "s": seconds
+    };
+    return obj;
+}
+
+
+/* Command input Alias + re-writing */
+function InputPreProcessor () {
+    this.recursive_depth = 3;
+
+    this.aliases = {};
+    this.vars = {version: 1};
+
+    // Current recursive depth
+    var depth = 0;
+
+
+    // Takes an array of words to process!
+    this.processInput = function (input) {
+        var words = input || [],
+            alias = this.aliases[words[0]],
+            alias_len,
+            current_alias_word = '',
+            compiled = [];
+
+        // If an alias wasn't found, return the original input
+        if (!alias) return input;
+
+        // Split the alias up into useable words
+        alias = alias.split(' ');
+        alias_len = alias.length;
+
+        // Iterate over each word and pop them into the final compiled array.
+        // Any $ words are processed with the result ending into the compiled array.
+        for (var i=0; i<alias_len; i++) {
+            current_alias_word = alias[i];
+
+            // Non $ word
+            if (current_alias_word[0] !== '$') {
+                compiled.push(current_alias_word);
+                continue;
+            }
+
+            // Refering to an input word ($N)
+            if (!isNaN(current_alias_word[1])) {
+                var num = current_alias_word.match(/\$(\d+)(\+)?(\d+)?/);
+
+                // Did we find anything or does the word it refers to non-existant?
+                if (!num || !words[num[1]]) continue;
+
+                if (num[2] === '+' && num[3]) {
+                    // Add X number of words
+                    compiled = compiled.concat(words.slice(parseInt(num[1], 10), parseInt(num[1], 10) + parseInt(num[3], 10)));
+                } else if (num[2] === '+') {
+                    // Add the remaining of the words
+                    compiled = compiled.concat(words.slice(parseInt(num[1], 10)));
+                } else {
+                    // Add a single word
+                    compiled.push(words[parseInt(num[1], 10)]);
+                }
+
+                continue;
+            }
+
+
+            // Refering to a variable
+            if (typeof this.vars[current_alias_word.substr(1)] !== 'undefined') {
+
+                // Get the variable
+                compiled.push(this.vars[current_alias_word.substr(1)]);
+
+                continue;
+            }
+
+        }
+
+        return compiled;
+    };
+
+
+    this.process = function (input) {
+        input = input || '';
+
+        var words = input.split(' ');
+
+        depth++;
+        if (depth >= this.recursive_depth) {
+            depth--;
+            return input;
+        }
+
+        if (this.aliases[words[0]]) {
+            words = this.processInput(words);
+
+            if (this.aliases[words[0]]) {
+                words = this.process(words.join(' ')).split(' ');
+            }
+
+        }
+
+        depth--;
+        return words.join(' ');
+    };
+}
+
+
+/**
+ * Convert HSL to RGB formatted colour
+ */
+function hsl2rgb(h, s, l) {
+    var m1, m2, hue;
+    var r, g, b
+    s /=100;
+    l /= 100;
+    if (s == 0)
+        r = g = b = (l * 255);
+    else {
+        function HueToRgb(m1, m2, hue) {
+            var v;
+            if (hue < 0)
+                hue += 1;
+            else if (hue > 1)
+                hue -= 1;
+
+            if (6 * hue < 1)
+                v = m1 + (m2 - m1) * hue * 6;
+            else if (2 * hue < 1)
+                v = m2;
+            else if (3 * hue < 2)
+                v = m1 + (m2 - m1) * (2/3 - hue) * 6;
+            else
+                v = m1;
+
+            return 255 * v;
+        }
+        if (l <= 0.5)
+            m2 = l * (s + 1);
+        else
+            m2 = l + s - l * s;
+        m1 = l * 2 - m2;
+        hue = h / 360;
+        r = HueToRgb(m1, m2, hue + 1/3);
+        g = HueToRgb(m1, m2, hue);
+        b = HueToRgb(m1, m2, hue - 1/3);
+    }
+    return [r,g,b];
+}
+
+
+/**
+ * Formats a kiwi message to IRC format
+ */
+function formatToIrcMsg(message) {
+    // Format any colour codes (eg. $c4)
+    message = message.replace(/%C(\d)/g, function(match, colour_number) {
+        return String.fromCharCode(3) + colour_number.toString();
+    });
+
+    var formatters = {
+        B: '\x02',    // Bold
+        I: '\x1D',    // Italics
+        U: '\x1F',    // Underline
+        O: '\x0F'     // Out / Clear formatting
+    };
+    message = message.replace(/%([BIUO])/g, function(match, format_code) {
+        if (typeof formatters[format_code.toUpperCase()] !== 'undefined')
+            return formatters[format_code.toUpperCase()];
+    });
+
+    return message;
+}
+
+
+/**
+*   Formats a message. Adds bold, underline and colouring
+*   @param      {String}    msg The message to format
+*   @returns    {String}        The HTML formatted message
+*/
+function formatIRCMsg (msg) {
+    "use strict";
+    var out = '',
+        currentTag = '',
+        openTags = {
+            bold: false,
+            italic: false,
+            underline: false,
+            colour: false
+        },
+        spanFromOpen = function () {
+            var style = '',
+                colours;
+            if (!(openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                return '';
+            } else {
+                style += (openTags.bold) ? 'font-weight: bold; ' : '';
+                style += (openTags.italic) ? 'font-style: italic; ' : '';
+                style += (openTags.underline) ? 'text-decoration: underline; ' : '';
+                if (openTags.colour) {
+                    colours = openTags.colour.split(',');
+                    style += 'color: ' + colours[0] + ((colours[1]) ? '; background-color: ' + colours[1] + ';' : '');
+                }
+                return '<span class="format_span" style="' + style + '">';
+            }
+        },
+        colourMatch = function (str) {
+            var re = /^\x03(([0-9][0-9]?)(,([0-9][0-9]?))?)/;
+            return re.exec(str);
+        },
+        hexFromNum = function (num) {
+            switch (parseInt(num, 10)) {
+            case 0:
+                return '#FFFFFF';
+            case 1:
+                return '#000000';
+            case 2:
+                return '#000080';
+            case 3:
+                return '#008000';
+            case 4:
+                return '#FF0000';
+            case 5:
+                return '#800040';
+            case 6:
+                return '#800080';
+            case 7:
+                return '#FF8040';
+            case 8:
+                return '#FFFF00';
+            case 9:
+                return '#80FF00';
+            case 10:
+                return '#008080';
+            case 11:
+                return '#00FFFF';
+            case 12:
+                return '#0000FF';
+            case 13:
+                return '#FF55FF';
+            case 14:
+                return '#808080';
+            case 15:
+                return '#C0C0C0';
+            default:
+                return null;
+            }
+        },
+        i = 0,
+        colours = [],
+        match;
+
+    for (i = 0; i < msg.length; i++) {
+        switch (msg[i]) {
+        case '\x02':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            openTags.bold = !openTags.bold;
+            currentTag = spanFromOpen();
+            break;
+        case '\x1D':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            openTags.italic = !openTags.italic;
+            currentTag = spanFromOpen();
+            break;
+        case '\x1F':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            openTags.underline = !openTags.underline;
+            currentTag = spanFromOpen();
+            break;
+        case '\x03':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            match = colourMatch(msg.substr(i, 6));
+            if (match) {
+                i += match[1].length;
+                // 2 & 4
+                colours[0] = hexFromNum(match[2]);
+                if (match[4]) {
+                    colours[1] = hexFromNum(match[4]);
+                }
+                openTags.colour = colours.join(',');
+            } else {
+                openTags.colour = false;
+            }
+            currentTag = spanFromOpen();
+            break;
+        case '\x0F':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            openTags.bold = openTags.italic = openTags.underline = openTags.colour = false;
+            break;
+        default:
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                currentTag += msg[i];
+            } else {
+                out += msg[i];
+            }
+            break;
+        }
+    }
+    if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+        out += currentTag + '</span>';
+    }
+    return out;
+}
+
+function escapeRegex (str) {
+    return str.replace(/[\[\\\^\$\.\|\?\*\+\(\)]/g, '\\$&');
+}
+
+function emoticonFromText(str) {
+    var words_in = str.split(' '),
+        words_out = [],
+        i,
+        pushEmoticon = function (alt, emote_name) {
+            words_out.push('<i class="emoticon ' + emote_name + '">' + alt + '</i>');
+        };
+
+    for (i = 0; i < words_in.length; i++) {
+        switch(words_in[i]) {
+        case ':)':
+            pushEmoticon(':)', 'smile');
+            break;
+        case ':(':
+            pushEmoticon(':(', 'sad');
+            break;
+        case ':3':
+            pushEmoticon(':3', 'lion');
+            break;
+        case ';3':
+            pushEmoticon(';3', 'winky_lion');
+            break;
+        case ':s':
+        case ':S':
+            pushEmoticon(':s', 'confused');
+            break;
+        case ';(':
+        case ';_;':
+            pushEmoticon(';(', 'cry');
+            break;
+        case ';)':
+            pushEmoticon(';)', 'wink');
+            break;
+        case ';D':
+            pushEmoticon(';D', 'wink_happy');
+            break;
+        case ':P':
+        case ':p':
+            pushEmoticon(':P', 'tongue');
+            break;
+        case 'xP':
+            pushEmoticon('xP', 'cringe_tongue');
+            break;
+        case ':o':
+        case ':O':
+        case ':0':
+            pushEmoticon(':o', 'shocked');
+            break;
+        case ':D':
+            pushEmoticon(':D', 'happy');
+            break;
+        case '^^':
+        case '^.^':
+            pushEmoticon('^^,', 'eyebrows');
+            break;
+        case '&lt;3':
+            pushEmoticon('<3', 'heart');
+            break;
+        case '&gt;_&lt;':
+        case '&gt;.&lt;':
+            pushEmoticon('>_<', 'doh');
+            break;
+        case 'XD':
+        case 'xD':
+            pushEmoticon('xD', 'big_grin');
+            break;
+        case 'o.0':
+        case 'o.O':
+            pushEmoticon('o.0', 'wide_eye_right');
+            break;
+        case '0.o':
+        case 'O.o':
+            pushEmoticon('0.o', 'wide_eye_left');
+            break;
+        case ':\\':
+        case '=\\':
+        case ':/':
+        case '=/':
+            pushEmoticon(':\\', 'unsure');
+            break;
+        default:
+            words_out.push(words_in[i]);
+        }
+    }
+
+    return words_out.join(' ');
+}
+
+// Code based on http://anentropic.wordpress.com/2009/06/25/javascript-iso8601-parser-and-pretty-dates/#comment-154
+function parseISO8601(str) {
+    if (Date.prototype.toISOString) {
+        return new Date(str);
+    } else {
+        var parts = str.split('T'),
+            dateParts = parts[0].split('-'),
+            timeParts = parts[1].split('Z'),
+            timeSubParts = timeParts[0].split(':'),
+            timeSecParts = timeSubParts[2].split('.'),
+            timeHours = Number(timeSubParts[0]),
+            _date = new Date();
+
+        _date.setUTCFullYear(Number(dateParts[0]));
+        _date.setUTCDate(1);
+        _date.setUTCMonth(Number(dateParts[1])-1);
+        _date.setUTCDate(Number(dateParts[2]));
+        _date.setUTCHours(Number(timeHours));
+        _date.setUTCMinutes(Number(timeSubParts[1]));
+        _date.setUTCSeconds(Number(timeSecParts[0]));
+        if (timeSecParts[1]) {
+            _date.setUTCMilliseconds(Number(timeSecParts[1]));
+        }
+
+        return _date;
+    }
+}
+
+// Simplyfy the translation syntax
+function translateText(string_id, params) {
+    params = params || '';
+
+    return _kiwi.global.i18n.translate(string_id).fetch(params);
+}
+
+/**
+ * Simplyfy the text styling syntax
+ *
+ * Syntax:
+ *   %nick:     nickname
+ *   %channel:  channel
+ *   %ident:    ident
+ *   %host:     host
+ *   %realname: realname
+ *   %text:     translated text
+ *   %C[digit]: color
+ *   %B:        bold
+ *   %I:        italic
+ *   %U:        underline
+ *   %O:        cancel styles
+ **/
+function styleText(string_id, params) {
+    var style, text;
+
+    //style = formatToIrcMsg(_kiwi.app.text_theme[string_id]);
+    style = _kiwi.app.text_theme[string_id];
+    style = formatToIrcMsg(style);
+
+    // Expand a member mask into its individual parts (nick, ident, hostname)
+    if (params.member) {
+        params.nick = params.member.nick || '';
+        params.ident = params.member.ident || '';
+        params.host = params.member.hostname || '';
+        params.prefix = params.member.prefix || '';
+    }
+
+    // Do the magic. Use the %shorthand syntax to produce output.
+    text = style.replace(/%([A-Z]{2,})/ig, function(match, key) {
+        if (typeof params[key] !== 'undefined')
+            return params[key];
+    });
+
+    return text;
+}
+
+
+
+
+})(window);
diff --git a/2016/assets/js/kiwi.min.js b/2016/assets/js/kiwi.min.js
new file mode 100644 (file)
index 0000000..a61e5b5
--- /dev/null
@@ -0,0 +1,5 @@
+!function(e,t){function n(){this._listeners={},this._parent=null,this._children=[]}function i(e){var t,n,i="0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",s="";for(t=0;e>t;t++)n=Math.floor(Math.random()*i.length),s+=i.substring(n,n+1);return s}function s(e){var t,n,i,s,a,o;return t=Math.floor(e/3600),s=e%3600,n=Math.floor(s/60),a=s%60,i=Math.ceil(a),o={h:t,m:n,s:i}}function a(){this.recursive_depth=3,this.aliases={},this.vars={version:1};var e=0;this.processInput=function(e){var t,n=e||[],i=this.aliases[n[0]],s="",a=[];if(!i)return e;i=i.split(" "),t=i.length;for(var o=0;t>o;o++)if(s=i[o],"$"===s[0])if(isNaN(s[1]))"undefined"==typeof this.vars[s.substr(1)]||a.push(this.vars[s.substr(1)]);else{var c=s.match(/\$(\d+)(\+)?(\d+)?/);if(!c||!n[c[1]])continue;"+"===c[2]&&c[3]?a=a.concat(n.slice(parseInt(c[1],10),parseInt(c[1],10)+parseInt(c[3],10))):"+"===c[2]?a=a.concat(n.slice(parseInt(c[1],10))):a.push(n[parseInt(c[1],10)])}else a.push(s);return a},this.process=function(t){t=t||"";var n=t.split(" ");return e++,e>=this.recursive_depth?(e--,t):(this.aliases[n[0]]&&(n=this.processInput(n),this.aliases[n[0]]&&(n=this.process(n.join(" ")).split(" "))),e--,n.join(" "))}}function o(e,t,n){function i(e,t,n){var i;return 0>n?n+=1:n>1&&(n-=1),i=1>6*n?e+(t-e)*n*6:1>2*n?t:2>3*n?e+(t-e)*(2/3-n)*6:e,255*i}var s,a,o,c,l,r;return t/=100,n/=100,0==t?c=l=r=255*n:(a=.5>=n?n*(t+1):n+t-n*t,s=2*n-a,o=e/360,c=i(s,a,o+1/3),l=i(s,a,o),r=i(s,a,o-1/3)),[c,l,r]}function c(e){e=e.replace(/%C(\d)/g,function(e,t){return String.fromCharCode(3)+t.toString()});var t={B:"\ 2",I:"\1d",U:"\1f",O:"\ f"};return e=e.replace(/%([BIUO])/g,function(e,n){return"undefined"!=typeof t[n.toUpperCase()]?t[n.toUpperCase()]:void 0})}function l(e){"use strict";var t,n="",i="",s={bold:!1,italic:!1,underline:!1,colour:!1},a=function(){var e,t="";return s.bold||s.italic||s.underline||s.colour?(t+=s.bold?"font-weight: bold; ":"",t+=s.italic?"font-style: italic; ":"",t+=s.underline?"text-decoration: underline; ":"",s.colour&&(e=s.colour.split(","),t+="color: "+e[0]+(e[1]?"; background-color: "+e[1]+";":"")),'<span class="format_span" style="'+t+'">'):""},o=function(e){var t=/^\x03(([0-9][0-9]?)(,([0-9][0-9]?))?)/;return t.exec(e)},c=function(e){switch(parseInt(e,10)){case 0:return"#FFFFFF";case 1:return"#000000";case 2:return"#000080";case 3:return"#008000";case 4:return"#FF0000";case 5:return"#800040";case 6:return"#800080";case 7:return"#FF8040";case 8:return"#FFFF00";case 9:return"#80FF00";case 10:return"#008080";case 11:return"#00FFFF";case 12:return"#0000FF";case 13:return"#FF55FF";case 14:return"#808080";case 15:return"#C0C0C0";default:return null}},l=0,r=[];for(l=0;l<e.length;l++)switch(e[l]){case"\ 2":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),s.bold=!s.bold,i=a();break;case"\1d":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),s.italic=!s.italic,i=a();break;case"\1f":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),s.underline=!s.underline,i=a();break;case"\ 3":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),t=o(e.substr(l,6)),t?(l+=t[1].length,r[0]=c(t[2]),t[4]&&(r[1]=c(t[4])),s.colour=r.join(",")):s.colour=!1,i=a();break;case"\ f":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),s.bold=s.italic=s.underline=s.colour=!1;break;default:s.bold||s.italic||s.underline||s.colour?i+=e[l]:n+=e[l]}return(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),n}function r(e){return e.replace(/[\[\\\^\$\.\|\?\*\+\(\)]/g,"\\$&")}function h(e){var t,n=e.split(" "),i=[],s=function(e,t){i.push('<i class="emoticon '+t+'">'+e+"</i>")};for(t=0;t<n.length;t++)switch(n[t]){case":)":s(":)","smile");break;case":(":s(":(","sad");break;case":3":s(":3","lion");break;case";3":s(";3","winky_lion");break;case":s":case":S":s(":s","confused");break;case";(":case";_;":s(";(","cry");break;case";)":s(";)","wink");break;case";D":s(";D","wink_happy");break;case":P":case":p":s(":P","tongue");break;case"xP":s("xP","cringe_tongue");break;case":o":case":O":case":0":s(":o","shocked");break;case":D":s(":D","happy");break;case"^^":case"^.^":s("^^,","eyebrows");break;case"&lt;3":s("<3","heart");break;case"&gt;_&lt;":case"&gt;.&lt;":s(">_<","doh");break;case"XD":case"xD":s("xD","big_grin");break;case"o.0":case"o.O":s("o.0","wide_eye_right");break;case"0.o":case"O.o":s("0.o","wide_eye_left");break;case":\\":case"=\\":case":/":case"=/":s(":\\","unsure");break;default:i.push(n[t])}return i.join(" ")}function p(e){if(Date.prototype.toISOString)return new Date(e);var t=e.split("T"),n=t[0].split("-"),i=t[1].split("Z"),s=i[0].split(":"),a=s[2].split("."),o=Number(s[0]),c=new Date;return c.setUTCFullYear(Number(n[0])),c.setUTCDate(1),c.setUTCMonth(Number(n[1])-1),c.setUTCDate(Number(n[2])),c.setUTCHours(Number(o)),c.setUTCMinutes(Number(s[1])),c.setUTCSeconds(Number(a[0])),a[1]&&c.setUTCMilliseconds(Number(a[1])),c}function d(e,t){return t=t||"",m.global.i18n.translate(e).fetch(t)}function u(e,t){var n,i;return n=m.app.text_theme[e],n=c(n),t.member&&(t.nick=t.member.nick||"",t.ident=t.member.ident||"",t.host=t.member.hostname||"",t.prefix=t.member.prefix||""),i=n.replace(/%([A-Z]{2,})/gi,function(e,n){return"undefined"!=typeof t[n]?t[n]:void 0})}var m={};if(m.misc={},m.model={},m.view={},m.applets={},m.utils={},m.global={build_version:"",settings:t,plugins:t,events:t,rpc:t,utils:{},initUtils:function(){this.utils.randomString=i,this.utils.secondsToTime=s,this.utils.parseISO8601=p,this.utils.escapeRegex=r,this.utils.formatIRCMsg=l,this.utils.styleText=u,this.utils.hsl2rgb=o,this.utils.notifications=m.utils.notifications,this.utils.formatDate=m.utils.formatDate},addMediaMessageType:function(e,t){m.view.MediaMessage.addType(e,t)},components:{EventComponent:function(e,t){function n(e,n){"all"==t||(n=e.event_data,e=e.event_name),this.trigger(e,n)}t=t||"all",_.extend(this,Backbone.Events),this._source=e,e.on(t,n,this),this.dispose=function(){e.off(t,n),this.off(),delete this.event_source}},Network:function(e){var n;n="undefined"!=typeof e?"connection:"+e.toString():"connection";var i=function(){var n="undefined"==typeof e?m.app.connections.active_connection:m.app.connections.getByConnectionId(e);return n?n:t},s=new this.EventComponent(m.gateway,n),a={kiwi:"kiwi",raw:"raw",kick:"kick",topic:"topic",part:"part",join:"join",action:"action",ctcp:"ctcp",ctcpRequest:"ctcpRequest",ctcpResponse:"ctcpResponse",notice:"notice",msg:"privmsg",say:"privmsg",changeNick:"changeNick",channelInfo:"channelInfo",mode:"mode",quit:"quit"};return _.each(a,function(t,n){s[n]=function(){var n=t,i=Array.prototype.slice.call(arguments,0);return i.unshift(e),m.gateway[n].apply(m.gateway,i)}}),s.createQuery=function(e){var t;return(t=i())?t.createQuery(e):void 0},s.get=function(e){var n,s;return(n=i())?(s=["password"],s.indexOf(e)>-1?t:n.get(e)):void 0},s.set=function(){var e=i();if(e)return e.set.apply(e,arguments)},s},ControlInput:function(){var e=new this.EventComponent(m.app.controlbox),t={run:"processInput",addPluginIcon:"addPluginIcon"};return _.each(t,function(t,n){e[n]=function(){var e=t;return m.app.controlbox[e].apply(m.app.controlbox,arguments)}}),e.input=m.app.controlbox.$(".inp"),e}},init:function(e,t){var i,s,a=this;e=e||{},this.initUtils(),m.global.settings=m.model.DataStore.instance("kiwi.settings"),m.global.settings.load(),window.document.title=e.server_settings.client.window_title||"Kiwi IRC",i=new Promise(function(t){var n=m.global.settings.get("locale")||"magic";$.getJSON(e.base_path+"/assets/locales/"+n+".json",function(e){a.i18n=e?new Jed(e):new Jed,t()})}),s=new Promise(function(t){var n=e.server_settings.client.settings.text_theme||"default";$.getJSON(e.base_path+"/assets/text_themes/"+n+".json",function(n){e.text_theme=n,t()})}),Promise.all([i,s]).then(function(){m.app=new m.model.Application(e),m.app.initializeInterfaces(),m.global.events=new n,m.global.plugins=new m.model.PluginManager,t()}).then(null,function(e){console.error(e.stack)})},start:function(){m.app.showStartup()},registerStartupApplet:function(e){m.app.startup_applet_name=e},newIrcConnection:function(e,t){m.gateway.newConnection(e,t)},defaultServerSettings:function(){var e,t,n={nick:"",server:"",port:6667,ssl:!1,channel:"",channel_key:""};return m.app.server_settings.client&&(m.app.server_settings.client.nick&&(n.nick=m.app.server_settings.client.nick),m.app.server_settings.client.server&&(n.server=m.app.server_settings.client.server),m.app.server_settings.client.port&&(n.port=m.app.server_settings.client.port),m.app.server_settings.client.ssl&&(n.ssl=m.app.server_settings.client.ssl),m.app.server_settings.client.channel&&(n.channel=m.app.server_settings.client.channel),m.app.server_settings.client.channel_key&&(n.channel_key=m.app.server_settings.client.channel_key)),getQueryVariable("nick")&&(n.nick=getQueryVariable("nick")),window.location.hash&&(n.channel=window.location.hash),e=window.location.pathname.toString().replace(m.app.get("base_path"),"").split("/"),e.length>0&&(e.shift(),e.length>0&&e[0]&&(t=e[0].substr(0,7).toLowerCase(),"ircs%3a"===t||"irc%3a"===t.substr(0,6)?(e[0]=decodeURIComponent(e[0]),t=/^irc(s)?:(?:\/\/?)?([^:\/]+)(?::([0-9]+))?(?:(?:\/)([^\?]*)(?:(?:\?)(.*))?)?$/.exec(e[0]),t&&("undefined"!=typeof t[1]&&(n.ssl=!0,6667===n.port&&(n.port=6697)),n.server=t[2],"undefined"!=typeof t[3]&&(n.port=t[3]),"undefined"!=typeof t[4]&&(n.channel="#"+t[4],"undefined"!=typeof t[5]&&(n.channel_key=t[5]))),e=[]):(e[0].search(/:/)>0?(n.port=e[0].substring(e[0].search(/:/)+1),n.server=e[0].substring(0,e[0].search(/:/)),"+"===n.port[0]?(n.port=parseInt(n.port.substring(1),10),n.ssl=!0):n.ssl=!1):n.server=e[0],e.shift())),e.length>0&&e[0]&&(n.channel="#"+e[0],e.shift())),m.app.server_settings&&m.app.server_settings.connection&&(m.app.server_settings.connection.server&&(n.server=m.app.server_settings.connection.server),m.app.server_settings.connection.port&&(n.port=m.app.server_settings.connection.port),m.app.server_settings.connection.ssl&&(n.ssl=m.app.server_settings.connection.ssl),m.app.server_settings.connection.channel&&(n.channel=m.app.server_settings.connection.channel),m.app.server_settings.connection.channel_key&&(n.channel_key=m.app.server_settings.connection.channel_key),m.app.server_settings.connection.nick&&(n.nick=m.app.server_settings.connection.nick)),n.nick=n.nick.replace("?",Math.floor(1e5*Math.random()).toString()),getQueryVariable("encoding")&&(n.encoding=getQueryVariable("encoding")),n}},"undefined"!=typeof e)e.kiwi=m.global;else var g=m.global;!function(){m.model.Application=Backbone.Model.extend({view:null,message:null,initialize:function(e){this.app_options=e,e.container&&this.set("container",e.container),this.set("base_path",e.base_path?e.base_path:""),this.set("settings_path",e.settings_path?e.settings_path:this.get("base_path")+"/assets/settings.json"),this.server_settings=e.server_settings||{},this.translations=e.translations||{},this.themes=e.themes||[],this.text_theme=e.text_theme||{},this.startup_applet_name=e.startup||"kiwi_startup",this.server_settings&&this.server_settings.client&&this.server_settings.client.settings&&this.applyDefaultClientSettings(this.server_settings.client.settings)},initializeInterfaces:function(){var e=this.app_options.kiwi_server||this.detectKiwiServer();m.gateway=new m.model.Gateway({kiwi_server:e}),this.bindGatewayCommands(m.gateway),this.initializeClient(),this.initializeGlobals(),this.view.barsHide(!0)},detectKiwiServer:function(){return"file:"===window.location.protocol?"http://localhost:7778":window.location.protocol+"//"+window.location.host},showStartup:function(){this.startup_applet=m.model.Applet.load(this.startup_applet_name,{no_tab:!0}),this.startup_applet.tab=this.view.$(".console"),this.startup_applet.view.show(),m.global.events.emit("loaded")},initializeClient:function(){this.view=new m.view.Application({model:this,el:this.get("container")}),this.connections=new m.model.NetworkPanelList,this.connections.on("remove",_.bind(function(){0===this.connections.length&&this.view.barsHide()},this)),this.applet_panels=new m.model.PanelList,this.applet_panels.view.$el.addClass("panellist applets"),this.view.$el.find(".tabs").append(this.applet_panels.view.$el),this.controlbox=new m.view.ControlBox({el:$("#kiwi .controlbox")[0]}).render(),this.client_ui_commands=new m.misc.ClientUiCommands(this,this.controlbox),this.rightbar=new m.view.RightBar({el:this.view.$(".right_bar")[0]}),this.topicbar=new m.view.TopicBar({el:this.view.$el.find(".topic")[0]}),new m.view.AppToolbar({el:m.app.view.$el.find(".toolbar .app_tools")[0]}),new m.view.ChannelTools({el:m.app.view.$el.find(".channel_tools")[0]}),this.message=new m.view.StatusMessage({el:this.view.$el.find(".status_message")[0]}),this.resize_handle=new m.view.ResizeHandler({el:this.view.$el.find(".memberlists_resize_handle")[0]}),this.view.doLayout()},initializeGlobals:function(){m.global.connections=this.connections,m.global.panels=this.panels,m.global.panels.applets=this.applet_panels,m.global.components.Applet=m.model.Applet,m.global.components.Panel=m.model.Panel,m.global.components.MenuBox=m.view.MenuBox,m.global.components.DataStore=m.model.DataStore,m.global.components.Notification=m.view.Notification,m.global.components.Events=function(){return g.events.createProxy()}},applyDefaultClientSettings:function(e){_.each(e,function(e,t){"undefined"==typeof m.global.settings.get(t)&&m.global.settings.set(t,e)})},panels:function(){var e,t=function(t){var n,i=m.app;switch(t=t||"connections"){case"connections":n=i.connections.panels();break;case"applets":n=i.applet_panels.models}return n.active=e,n.server=i.connections.active_connection?i.connections.active_connection.panels.server:null,n};return _.extend(t,Backbone.Events),t.bind("active",function(t){var n=e;e=t,m.global.events.emit("panel:active",{previous:n,active:e})}),t}(),bindGatewayCommands:function(e){var t=this;e.on("connection:connect",function(){t.view.barsShow()}),function(){var n=0;e.on("disconnect",function(){t.view.$el.removeClass("connected"),n=1}),e.on("reconnecting",function(e){var t=d("client_models_application_reconnect_in_x_seconds",[e.delay/1e3])+"...";m.app.connections.forEach(function(e){e.panels.server.addMsg("",u("quit",{text:t}),"action quit")})}),e.on("kiwi:connected",function(){var e;t.view.$el.addClass("connected"),m.global.rpc=m.gateway.rpc,m.global.events.emit("connected"),1===n&&(n=0,e=d("client_models_application_reconnect_successfully")+" :)",t.message.text(e,{timeout:5e3}),m.app.connections.forEach(function(t){t.reconnect(),t.panels.server.addMsg("",u("rejoin",{text:e}),"action join"),t.panels.forEach(function(t){t.isChannel()&&t.addMsg("",u("rejoin",{text:e}),"action join")})}))})}(),e.on("kiwi:reconfig",function(){$.getJSON(t.get("settings_path"),function(e){t.server_settings=e.server_settings||{},t.translations=e.translations||{}})}),e.on("kiwi:jumpserver",function(e){var n;if("undefined"!=typeof e.kiwi_server&&(n=e.kiwi_server,"/"===n[n.length-1]&&(n=n.substring(0,n.length-1)),e.force)){var i=60*Math.random()+300;i=1;var s=m.global.i18n.translate("client_models_application_jumpserver_prepare").fetch();t.message.text(s,{timeout:1e4}),setTimeout(function(){var e=m.global.i18n.translate("client_models_application_jumpserver_reconnect").fetch();t.message.text(e,{timeout:8e3}),setTimeout(function(){m.gateway.set("kiwi_server",n),m.gateway.reconnect(function(){t.connections.forEach(function(e){e.reconnect()})})},5e3)},1e3*i)}})}})}(),m.model.Gateway=Backbone.Model.extend({initialize:function(){this.socket=this.get("socket"),this.disconnect_requested=!1},reconnect:function(e){this.disconnect_requested=!0,this.socket.close(),this.socket=null,this.connect(e)},connect:function(e){var t=this;this.connect_callback=e,this.socket=new EngineioTools.ReconnectingSocket(this.get("kiwi_server"),{transports:m.app.server_settings.transports||["polling","websocket"],path:m.app.get("base_path")+"/transport",reconnect_max_attempts:5,reconnect_delay:2e3}),this.rpc&&rpc.dispose(),this.rpc=new EngineioTools.Rpc(this.socket),this.socket.on("connect_failed",function(e){this.socket.disconnect(),this.trigger("connect_fail",{reason:e})}),this.socket.on("error",function(e){console.log("_kiwi.gateway.socket.on('error')",{reason:e}),t.connect_callback&&(t.connect_callback(e),delete t.connect_callback),t.trigger("connect_fail",{reason:e})}),this.socket.on("connecting",function(){console.log("_kiwi.gateway.socket.on('connecting')"),t.trigger("connecting")}),this.socket.on("open",function(){t.disconnect_requested=!1;var e=function(){t.rpc&&(t.rpc("kiwi.heartbeat"),t._heartbeat_tmr=setTimeout(e,6e4))};e(),console.log("_kiwi.gateway.socket.on('open')")}),this.rpc.on("too_many_connections",function(){t.trigger("connect_fail",{reason:"too_many_connections"})}),this.rpc.on("irc",function(e,n){t.parse(n.command,n.data)}),this.rpc.on("kiwi",function(e,n){t.parseKiwi(n.command,n.data)}),this.socket.on("close",function(){t.trigger("disconnect",{}),console.log("_kiwi.gateway.socket.on('close')")}),this.socket.on("reconnecting",function(e){console.log("_kiwi.gateway.socket.on('reconnecting')"),t.trigger("reconnecting",{delay:e.delay,attempts:e.attempts})}),this.socket.on("reconnecting_failed",function(){console.log("_kiwi.gateway.socket.on('reconnect_failed')")})},newConnection:function(e,t){var n=this;return this.isConnected()?void this.makeIrcConnection(e,function(n,i){var s;if(n)console.log("_kiwi.gateway.socket.on('error')",{reason:n}),t&&t(n);else{if(!m.app.connections.getByConnectionId(i)){var a={connection_id:i,nick:e.nick,address:e.host,port:e.port,ssl:e.ssl,password:e.password};s=new m.model.Network(a),m.app.connections.add(s)}console.log("_kiwi.gateway.socket.on('connect')",s),t&&t(n,s)}}):void this.connect(function(i){return i?void t(i):void n.newConnection(e,t)})},makeIrcConnection:function(e,t){var n={nick:e.nick,hostname:e.host,port:e.port,ssl:e.ssl,password:e.password};e.options=e.options||{},e.options.encoding&&(n.encoding=e.options.encoding),this.rpc("kiwi.connect_irc",n,function(e,n){e?t&&t(e):t&&t(e,n)})},isConnected:function(){return this.socket},parseKiwi:function(e,t){var n;switch(e){case"connected":n={build_version:m.global.build_version},this.rpc("kiwi.client_info",n),this.connect_callback&&this.connect_callback(),delete this.connect_callback}this.trigger("kiwi:"+e,t),this.trigger("kiwi",t)},parse:function(e,t){var n="";"undefined"!=typeof t.connection_id&&(n="connection:"+t.connection_id.toString(),this.trigger(n,{event_name:e,event_data:t}),"message"==e&&t.type&&this.trigger("connection "+n,{event_name:"message:"+t.type,event_data:t}),"channel"==e&&t.type&&this.trigger("connection "+n,{event_name:"channel:"+t.type,event_data:t})),this.trigger("connection",{event_name:e,event_data:t}),this.trigger("connection:"+e,t)},rpcCall:function(){var e=Array.prototype.slice.call(arguments,0);return("undefined"==typeof e[1]||null===e[1])&&(e[1]=m.app.connections.active_connection.get("connection_id")),this.rpc.apply(this.rpc,e)},privmsg:function(e,t,n,i){var s={target:t,msg:n};this.rpcCall("irc.privmsg",e,s,i)},notice:function(e,t,n,i){var s={target:t,msg:n};this.rpcCall("irc.notice",e,s,i)},ctcp:function(e,t,n,i,s,a){var o={is_request:t,type:n,target:i,params:s};this.rpcCall("irc.ctcp",e,o,a)},ctcpRequest:function(e,t,n,i,s){this.ctcp(e,!0,t,n,i,s)},ctcpResponse:function(e,t,n,i,s){this.ctcp(e,!1,t,n,i,s)},action:function(e,t,n,i){this.ctcp(e,!0,"ACTION",t,n,i)},join:function(e,t,n,i){var s={channel:t,key:n};this.rpcCall("irc.join",e,s,i)},channelInfo:function(e,t,n){var i={channel:t};this.rpcCall("irc.channel_info",e,i,n)},part:function(e,n,i,s){"use strict";"function"==typeof arguments[2]&&(s=arguments[2],i=t);var a={channel:n,message:i};this.rpcCall("irc.part",e,a,s)},topic:function(e,t,n,i){var s={channel:t,topic:n};this.rpcCall("irc.topic",e,s,i)},kick:function(e,t,n,i,s){var a={channel:t,nick:n,reason:i};this.rpcCall("irc.kick",e,a,s)},quit:function(e,t,n){t=t||"";var i={message:t};this.rpcCall("irc.quit",e,i,n)},raw:function(e,t,n){var i={data:t};this.rpcCall("irc.raw",e,i,n)},changeNick:function(e,t,n){var i={nick:t};this.rpcCall("irc.nick",e,i,n)},mode:function(e,t,n,i){var s={data:"MODE "+t+" "+n};this.rpcCall("irc.raw",e,s,i)},setEncoding:function(e,t,n){var i={encoding:t};this.rpcCall("irc.encoding",e,i,n)}}),function(){function e(){this.set("connected",!1),$.each(this.panels.models,function(e,t){t.isApplet()||t.addMsg("",u("network_disconnected",{text:d("client_models_network_disconnected",[])}),"action quit")})}function n(e){var t;this.set("nick",e.nick),this.set("connected",!0),this.rejoinAllChannels(),this.auto_join&&this.auto_join.channel&&(t=this.createAndJoinChannels(this.auto_join.channel+" "+(this.auto_join.key||"")),t&&t[t.length-1].view.show(),delete this.auto_join)}function i(e){var t=this;$.each(e.options,function(e,n){switch(e){case"CHANTYPES":t.set("channel_prefix",n.join(""));break;case"NETWORK":t.set("name",n);break;case"PREFIX":t.set("user_prefixes",n)}}),this.set("cap",e.cap)}function a(e){this.panels.server.addMsg(this.get("name"),u("motd",{text:e.msg}),"motd")}function o(e){var t,n,i;t=this.panels.getByName(e.channel),t||(t=new m.model.Channel({name:e.channel,network:this}),this.panels.add(t)),n=t.get("members"),n&&(n.getByNick(e.nick)||(i=new m.model.Member({nick:e.nick,ident:e.ident,hostname:e.hostname,user_prefixes:this.get("user_prefixes")}),m.global.events.emit("channel:join",{channel:e.channel,user:i,network:this.gateway}).then(function(){n.add(i,{kiwi:e})})))}function c(e){var t,n,i,s={};if(s.type="part",s.message=e.message||"",s.time=e.time,t=this.panels.getByName(e.channel)){if(e.nick===this.get("nick"))return void t.close();n=t.get("members"),n&&(i=n.getByNick(e.nick),i&&m.global.events.emit("channel:leave",{channel:e.channel,user:i,type:"part",message:s.message,network:this.gateway}).then(function(){n.remove(i,{kiwi:s})}))}}function l(e){var t,n={};n.type="quit",n.message=e.message||"",n.time=e.time,$.each(this.panels.models,function(i,s){s.isQuery()&&s.get("name").toLowerCase()===e.nick.toLowerCase()&&s.addMsg(" ",u("channel_quit",{nick:e.nick,text:d("client_models_channel_quit",[n.message])}),"action quit",{time:n.time}),s.isChannel()&&(t=s.get("members").getByNick(e.nick),t&&m.global.events.emit("channel:leave",{channel:s.get("name"),user:t,type:"quit",message:n.message,network:this.gateway}).then(function(){s.get("members").remove(t,{kiwi:n})}))})}function r(e){var t,n,i,s={};s.type="kick",s.by=e.nick,s.message=e.message||"",s.current_user_kicked=e.kicked==this.get("nick"),s.current_user_initiated=e.nick==this.get("nick"),s.time=e.time,t=this.panels.getByName(e.channel),t&&(n=t.get("members"),n&&(i=n.getByNick(e.kicked),i&&m.global.events.emit("channel:leave",{channel:e.channel,user:i,type:"kick",message:s.message,network:this.gateway}).then(function(){n.remove(i,{kiwi:s}),s.current_user_kicked&&n.reset([])})))}function h(e){m.global.events.emit("message:new",{network:this.gateway,message:e}).then(_.bind(function(){var t,n=(e.target||"").toLowerCase()==this.get("nick").toLowerCase();if(!this.isNickIgnored(e.nick))switch("notice"==e.type?(e.from_server?t=this.panels.server:(t=this.panels.getByName(e.target)||this.panels.getByName(e.nick),e.nick&&"chanserv"==e.nick.toLowerCase()&&"["==e.msg.charAt(0)&&(channel_name=/\[([^ \]]+)\]/gi.exec(e.msg),channel_name&&channel_name[1]&&(channel_name=channel_name[1],t=this.panels.getByName(channel_name)))),t||(t=this.panels.server)):n?(t=this.panels.getByName(e.nick),t||(t=new m.model.Query({name:e.nick,network:this}),this.panels.add(t))):(t=this.panels.getByName(e.target),t||(t=this.panels.server)),e.type){case"message":t.addMsg(e.nick,u("privmsg",{text:e.msg}),"privmsg",{time:e.time});break;case"action":t.addMsg("",u("action",{nick:e.nick,text:e.msg}),"action",{time:e.time});break;case"notice":t.addMsg("["+(e.nick||"")+"]",u("notice",{text:e.msg}),"notice",{time:e.time}),active_panel=m.app.panels().active,e.from_server||t!==this.panels.server||active_panel===this.panels.server||active_panel.get("network")===this&&(active_panel.isChannel()||active_panel.isQuery())&&active_panel.addMsg("["+(e.nick||"")+"]",u("notice",{text:e.msg}),"notice",{time:e.time})}},this))}function p(e){var t;$.each(this.panels.models,function(n,i){i.get("name")==e.nick&&i.set("name",e.newnick),i.isChannel()&&(t=i.get("members").getByNick(e.nick),t&&(t.set("nick",e.newnick),i.addMsg("",u("nick_changed",{nick:e.nick,text:d("client_models_network_nickname_changed",[e.newnick]),channel:name}),"action nick",{time:e.time})))})}function g(e){this.isNickIgnored(e.nick)||("TIME"===e.msg.toUpperCase()?this.gateway.ctcpResponse(e.type,e.nick,(new Date).toString()):"PING"===e.type.toUpperCase()&&this.gateway.ctcpResponse(e.type,e.nick,e.msg.substr(5)))}function f(e){this.isNickIgnored(e.nick)||this.panels.server.addMsg("["+e.nick+"]",u("ctcp",{text:e.msg}),"ctcp",{time:e.time})}function v(e){var t;t=this.panels.getByName(e.channel),t&&(t.set("topic",e.topic),t.get("name")===this.panels.active.get("name")&&m.app.topicbar.setCurrentTopic(e.topic))}function w(e){var t,n;t=this.panels.getByName(e.channel),t&&(n=new Date(1e3*e.when),t.set("topic_set_by",{nick:e.nick,when:n}))}function k(e){var t=this.panels.getByName(e.channel);t&&(e.url?t.set("info_url",e.url):e.modes&&t.set("info_modes",e.modes))}function b(e){var t=this,n=this.panels.getByName(e.channel);n&&(n.temp_userlist=n.temp_userlist||[],_.each(e.users,function(e){var i=new m.model.Member({nick:e.nick,modes:e.modes,user_prefixes:t.get("user_prefixes")});n.temp_userlist.push(i)}))}function y(e){var t;t=this.panels.getByName(e.channel),t&&(t.get("members").reset(t.temp_userlist||[]),delete t.temp_userlist)}function C(e){var t=this.panels.getByName(e.channel);t&&t.set("banlist",e.bans||[])}function x(e){function t(t,n){var i,s={};return t||(t=e.modes,n=e.target),_.each(t,function(e){var t=e.param||n||"";s[t]||(s[t]={"+":"","-":""}),s[t][e.mode[0]]+=e.mode.substr(1)}),i=[],_.each(s,function(e,t){var n="";e["+"]&&(n+="+"+e["+"]),e["-"]&&(n+="-"+e["-"]),i.push(n+" "+t)}),i=i.join(", ")}var n,i,s,a,o,c,l=!1;if(n=this.panels.getByName(e.target)){for(s=this.get("user_prefixes"),c=function(t){return e.modes[i].mode[1]===t.mode},i=0;i<e.modes.length;i++){if(_.any(s,c)){if(a||(a=n.get("members")),o=a.getByNick(e.modes[i].param),!o)return void console.log("MODE command recieved for unknown member %s on channel %s",e.modes[i].param,e.target);"+"===e.modes[i].mode[0]?o.addMode(e.modes[i].mode[1]):"-"===e.modes[i].mode[0]&&o.removeMode(e.modes[i].mode[1]),a.sort()}"b"==e.modes[i].mode[1]&&(l=!0)}n.addMsg("",u("mode",{nick:e.nick,text:d("client_models_network_mode",[t()]),channel:e.target}),"action mode",{time:e.time}),l&&this.gateway.raw("MODE "+n.get("name")+" +b")}else e.target.toLowerCase()===this.get("nick").toLowerCase()?this.panels.server.addMsg("",u("selfmode",{nick:e.nick,text:d("client_models_network_mode",[t()]),channel:e.target}),"action mode"):console.log("MODE command recieved for unknown target %s: ",e.target,e)}function M(e){var t,n,i="";e.end||("undefined"!=typeof e.idle&&(i=s(parseInt(e.idle,10)),i=i.h.toString().lpad(2,"0")+":"+i.m.toString().lpad(2,"0")+":"+i.s.toString().lpad(2,"0")),n=m.app.panels().active,e.ident?n.addMsg(e.nick,u("whois_ident",{nick:e.nick,ident:e.ident,host:e.hostname,text:e.msg}),"whois"):e.chans?n.addMsg(e.nick,u("whois_channels",{nick:e.nick,text:d("client_models_network_channels",[e.chans])}),"whois"):e.irc_server?n.addMsg(e.nick,u("whois_server",{nick:e.nick,text:d("client_models_network_server",[e.irc_server,e.server_info])}),"whois"):e.msg?n.addMsg(e.nick,u("whois",{text:e.msg}),"whois"):e.logon?(t=new Date,t.setTime(1e3*e.logon),t=m.utils.formatDate(t),n.addMsg(e.nick,u("whois_idle_and_signon",{nick:e.nick,text:d("client_models_network_idle_and_signon",[i,t])}),"whois")):e.away_reason?n.addMsg(e.nick,u("whois_away",{nick:e.nick,text:d("client_models_network_away",[e.away_reason])}),"whois"):n.addMsg(e.nick,u("whois_idle",{nick:e.nick,text:d("client_models_network_idle",[i])}),"whois"))}function S(e){var t;e.end||(t=m.app.panels().active,e.hostname?t.addMsg(e.nick,u("who",{nick:e.nick,ident:e.ident,host:e.hostname,realname:e.real_name,text:e.msg}),"whois"):t.addMsg(e.nick,u("whois_notfound",{nick:e.nick,text:d("client_models_network_nickname_notfound",[])}),"whois"))}function T(e){$.each(this.panels.models,function(t,n){n.isChannel()&&(member=n.get("members").getByNick(e.nick),member&&member.set("away",!!e.reason))})}function N(){var e=m.model.Applet.loadOnce("kiwi_chanlist");e.view.show()}function B(e){var n,i;switch(e.channel===t||(n=this.panels.getByName(e.channel))||(n=this.panels.server),e.error){case"banned_from_channel":n.addMsg(" ",u("channel_banned",{nick:e.nick,text:d("client_models_network_banned",[e.channel,e.reason]),channel:e.channel}),"status"),m.app.message.text(m.global.i18n.translate("client_models_network_banned").fetch(e.channel,e.reason));break;case"bad_channel_key":n.addMsg(" ",u("channel_badkey",{nick:e.nick,text:d("client_models_network_channel_badkey",[e.channel]),channel:e.channel}),"status"),m.app.message.text(m.global.i18n.translate("client_models_network_channel_badkey").fetch(e.channel));break;case"invite_only_channel":n.addMsg(" ",u("channel_inviteonly",{nick:e.nick,text:d("client_models_network_channel_inviteonly",[e.nick,e.channel]),channel:e.channel}),"status"),m.app.message.text(e.channel+" "+m.global.i18n.translate("client_models_network_channel_inviteonly").fetch());break;case"user_on_channel":n.addMsg(" ",u("channel_alreadyin",{nick:e.nick,text:d("client_models_network_channel_alreadyin"),channel:e.channel}));break;case"channel_is_full":n.addMsg(" ",u("channel_limitreached",{nick:e.nick,text:d("client_models_network_channel_limitreached",[e.channel]),channel:e.channel}),"status"),m.app.message.text(e.channel+" "+m.global.i18n.translate("client_models_network_channel_limitreached").fetch(e.channel));break;case"chanop_privs_needed":n.addMsg(" ",u("chanop_privs_needed",{text:e.reason,channel:e.channel}),"status"),m.app.message.text(e.reason+" ("+e.channel+")");break;case"cannot_send_to_channel":n.addMsg(" ","== "+m.global.i18n.translate("Cannot send message to channel, you are not voiced").fetch(e.channel,e.reason),"status");break;case"no_such_nick":i=this.panels.getByName(e.nick),i?i.addMsg(" ",u("no_such_nick",{nick:e.nick,text:e.reason,channel:e.channel}),"status"):this.panels.server.addMsg(" ",u("no_such_nick",{nick:e.nick,text:e.reason,channel:e.channel}),"status");break;case"nickname_in_use":this.panels.server.addMsg(" ",u("nickname_alreadyinuse",{nick:e.nick,text:d("client_models_network_nickname_alreadyinuse",[e.nick]),channel:e.channel}),"status"),this.panels.server!==this.panels.active&&m.app.message.text(m.global.i18n.translate("client_models_network_nickname_alreadyinuse").fetch(e.nick)),"none"!==m.app.controlbox.$el.css("display")&&(new m.view.NickChangeBox).render();break;case"password_mismatch":this.panels.server.addMsg(" ",u("channel_badpassword",{nick:e.nick,text:d("client_models_network_badpassword",[]),channel:e.channel}),"status");break;case"error":e.reason&&this.panels.server.addMsg(" ",u("general_error",{text:e.reason}),"status")}}function D(e){var t=_.clone(e.params);t[0]&&t[0]==this.get("nick")&&t.shift(),this.panels.server.addMsg("",u("unknown_command",{text:"["+e.command+"] "+t.join(", ","")}))}function I(e){var t=m.app.panels().active;this.panels.server.addMsg("["+(e.nick||"")+"]",u("wallops",{text:e.msg}),"wallops",{time:e.time}),t!==this.panels.server&&(t.isChannel()||t.isQuery())&&t.get("network")===this&&t.addMsg("["+(e.nick||"")+"]",u("wallops",{text:e.msg}),"wallops",{time:e.time})}m.model.Network=Backbone.Model.extend({defaults:{connection_id:0,name:"Network",address:"",port:6667,ssl:!1,password:"",nick:"",channel_prefix:"#",user_prefixes:[{symbol:"~",mode:"q"},{symbol:"&",mode:"a"},{symbol:"@",mode:"o"},{symbol:"%",mode:"h"},{symbol:"+",mode:"v"}],ignore_list:[]},initialize:function(){"undefined"!=typeof this.get("connection_id")&&(this.gateway=m.global.components.Network(this.get("connection_id")),this.bindGatewayEvents()),this.panels=new m.model.PanelList([],this);
+var e=new m.model.Server({name:"Server",network:this});this.panels.add(e),this.panels.server=this.panels.active=e},reconnect:function(e){var t=this,n={nick:this.get("nick"),host:this.get("address"),port:this.get("port"),ssl:this.get("ssl"),password:this.get("password")};m.gateway.makeIrcConnection(n,function(n,i){n?(console.log("_kiwi.gateway.socket.on('error')",{reason:n}),e&&e(n)):(t.gateway.dispose(),t.set("connection_id",i),t.gateway=m.global.components.Network(t.get("connection_id")),t.bindGatewayEvents(),t.panels.forEach(function(e){e.set("connection_id",i)}),e&&e(n))})},bindGatewayEvents:function(){this.gateway.on("connect",n,this),this.gateway.on("disconnect",e,this),this.gateway.on("nick",function(e){e.nick===this.get("nick")&&this.set("nick",e.newnick)},this),this.gateway.on("options",i,this),this.gateway.on("motd",a,this),this.gateway.on("channel:join",o,this),this.gateway.on("channel:part",c,this),this.gateway.on("channel:kick",r,this),this.gateway.on("quit",l,this),this.gateway.on("message",h,this),this.gateway.on("nick",p,this),this.gateway.on("ctcp_request",g,this),this.gateway.on("ctcp_response",f,this),this.gateway.on("topic",v,this),this.gateway.on("topicsetby",w,this),this.gateway.on("userlist",b,this),this.gateway.on("userlist_end",y,this),this.gateway.on("banlist",C,this),this.gateway.on("mode",x,this),this.gateway.on("whois",M,this),this.gateway.on("whowas",S,this),this.gateway.on("away",T,this),this.gateway.on("list_start",N,this),this.gateway.on("irc_error",B,this),this.gateway.on("unknown_command",D,this),this.gateway.on("channel_info",k,this),this.gateway.on("wallops",I,this)},createAndJoinChannels:function(e){var t=this,n=[];return"string"==typeof e&&(e=e.split(",")),$.each(e,function(e,i){var s=i.trim().split(" "),a=s[0],o=s[1]||"";a=a.trim(),-1===t.get("channel_prefix").indexOf(a[0])&&(a="#"+a),channel=t.panels.getByName(a),channel||(channel=new m.model.Channel({name:a,network:t}),t.panels.add(channel)),n.push(channel),t.gateway.join(a,o)}),n},rejoinAllChannels:function(){var e=this;this.panels.forEach(function(t){t.isChannel()&&e.gateway.join(t.get("name"))})},isChannelName:function(e){var t=this.get("channel_prefix");return e&&e.length?t.indexOf(e[0])>-1:!1},isNickIgnored:function(e){var t,n,i,s=this.get("ignore_list");for(t=0;t<s.length;t++)if(n=s[t].replace(/([.+^$[\]\\(){}|-])/g,"\\$1").replace("*",".*").replace("?","."),i=new RegExp(n,"i"),i.test(e))return!0;return!1},createQuery:function(e){var t,n=this;return t=n.panels.getByName(e),t||(t=new m.model.Query({name:e}),n.panels.add(t)),t.view.show(),t}})}(),m.model.Member=Backbone.Model.extend({initialize:function(){var e,t;e=this.stripPrefix(this.get("nick")),t=this.get("modes"),t=t||[],this.sortModes(t),this.set({nick:e,modes:t,prefix:this.getPrefix(t)},{silent:!0}),this.updateOpStatus(),this.view=new m.view.Member({model:this})},sortModes:function(e){var t=this;return e.sort(function(e,n){var i,s,a,o=t.get("user_prefixes");for(a=0;a<o.length;a++)o[a].mode===e&&(i=a);for(a=0;a<o.length;a++)o[a].mode===n&&(s=a);return s>i?-1:i>s?1:0})},addMode:function(e){var t,n=e.split("");t=this.get("modes"),$.each(n,function(e,n){t.push(n)}),t=this.sortModes(t),this.set({prefix:this.getPrefix(t),modes:t}),this.updateOpStatus(),this.view.render()},removeMode:function(e){var t,n=e.split("");t=this.get("modes"),t=_.reject(t,function(e){return-1!==_.indexOf(n,e)}),this.set({prefix:this.getPrefix(t),modes:t}),this.updateOpStatus(),this.view.render()},getPrefix:function(e){var t="",n=this.get("user_prefixes");return"undefined"!=typeof e[0]&&(t=_.detect(n,function(t){return t.mode===e[0]}),t=t?t.symbol:""),t},stripPrefix:function(e){var t,n,i,s,a=e,o=this.get("user_prefixes");t=0;e:for(n=0;n<e.length;n++){for(s=e.charAt(n),i=0;i<o.length;i++)if(s===o[i].symbol){t++;continue e}break}return a.substr(t)},displayNick:function(e){var t=this.get("nick");return e&&this.get("ident")&&(t+=" ["+this.get("ident")+"@"+this.get("hostname")+"]"),t},getMaskParts:function(){return{nick:this.get("nick")||"",ident:this.get("ident")||"",hostname:this.get("hostname")||""}},updateOpStatus:function(){var e,t,n=this.get("user_prefixes"),i=this.get("modes");i.length>0?(e=_.indexOf(n,_.find(n,function(e){return"o"===e.mode})),t=_.indexOf(n,_.find(n,function(e){return e.mode===i[0]})),-1===t||t>e?this.set({is_op:!1},{silent:!0}):this.set({is_op:!0},{silent:!0})):this.set({is_op:!1},{silent:!0})}}),m.model.MemberList=Backbone.Collection.extend({model:m.model.Member,comparator:function(e,t){var n,i,s,a,o,c,l,r=this.channel.get("network").get("user_prefixes");if(i=e.get("modes"),s=t.get("modes"),i.length>0){if(0===s.length)return-1;for(a=o=-1,n=0;n<r.length;n++)r[n].mode===i[0]&&(a=n);for(n=0;n<r.length;n++)r[n].mode===s[0]&&(o=n);if(o>a)return-1;if(a>o)return 1}else if(s.length>0)return 1;return c=e.get("nick").toLocaleUpperCase(),l=t.get("nick").toLocaleUpperCase(),l>c?-1:c>l?1:0},initialize:function(){this.view=new m.view.MemberList({model:this}),this.initNickCache()},initNickCache:function(){var e=this;this.nick_cache=Object.create(null),this.on("reset",function(){this.nick_cache=Object.create(null),this.models.forEach(function(t){e.nick_cache[t.get("nick").toLowerCase()]=t})}),this.on("add",function(t){e.nick_cache[t.get("nick").toLowerCase()]=t}),this.on("remove",function(t){delete e.nick_cache[t.get("nick").toLowerCase()]}),this.on("change:nick",function(t){e.nick_cache[t.get("nick").toLowerCase()]=t,delete e.nick_cache[t.previous("nick").toLowerCase()]})},getByNick:function(e){return"string"==typeof e?this.nick_cache[e.toLowerCase()]:void 0}}),m.model.NewConnection=Backbone.Collection.extend({initialize:function(){this.view=new m.view.ServerSelect({model:this}),this.view.bind("server_connect",this.onMakeConnection,this)},populateDefaultServerSettings:function(){var e=m.global.defaultServerSettings();this.view.populateFields(e)},onMakeConnection:function(e){var t=this;this.connect_details=e,this.view.networkConnecting(),m.gateway.newConnection({nick:e.nick,host:e.server,port:e.port,ssl:e.ssl,password:e.password,options:e.options},function(e,n){t.onNewNetwork(e,n)})},onNewNetwork:function(e,t){e&&this.view.showError(e),t&&this.connect_details&&(t.auto_join={channel:this.connect_details.channel,key:this.connect_details.channel_key},this.trigger("new_network",t))}}),m.model.Panel=Backbone.Model.extend({initialize:function(){var e=this.get("name")||"";this.view=new m.view.Panel({model:this,name:e}),this.set({scrollback:[],name:e},{silent:!0}),m.global.events.emit("panel:created",{panel:this})},close:function(){m.app.panels.trigger("close",this),m.global.events.emit("panel:close",{panel:this}),this.view&&(this.view.unbind(),this.view.remove(),this.view=t,delete this.view);var e=this.get("members");e&&(e.reset([]),this.unset("members")),this.get("panel_list").remove(this),this.unbind(),this.destroy()},isChannel:function(){return!1},isQuery:function(){return!1},isApplet:function(){return!1},isServer:function(){return!1},isActive:function(){return m.app.panels().active===this}}),m.model.PanelList=Backbone.Collection.extend({model:m.model.Panel,comparator:function(e){return e.get("name")},initialize:function(e,t){t&&(this.network=t),this.view=new m.view.Tabs({model:this}),this.active=null,this.bind("active",function(e){this.active=e},this),this.bind("add",function(e){e.set("panel_list",this)})},getByCid:function(e){return"string"==typeof name?this.find(function(t){return e===t.cid}):void 0},getByName:function(e){return"string"==typeof e?this.find(function(t){return e.toLowerCase()===t.get("name").toLowerCase()}):void 0}}),m.model.NetworkPanelList=Backbone.Collection.extend({model:m.model.Network,initialize:function(){this.view=new m.view.NetworkTabs({model:this}),this.on("add",this.onNetworkAdd,this),this.on("remove",this.onNetworkRemove,this),this.active_connection=t,this.active_panel=t,this.active=t},getByConnectionId:function(e){return this.find(function(t){return t.get("connection_id")==e})},panels:function(){var e=[];return this.each(function(t){e=e.concat(t.panels.models)}),e},onNetworkAdd:function(e){e.panels.on("active",this.onPanelActive,this),1===this.models.length&&(this.active_connection=e,this.active_panel=e.panels.server,this.active=this.active_panel)},onNetworkRemove:function(e){e.panels.off("active",this.onPanelActive,this)},onPanelActive:function(e){var t=this.getByConnectionId(e.tab.data("connection_id"));this.trigger("active",e,t),this.active_connection=t,this.active_panel=e,this.active=e}}),m.model.Channel=m.model.Panel.extend({initialize:function(){var e,t=this.get("name")||"";this.set({members:new m.model.MemberList,name:t,scrollback:[],topic:""},{silent:!0}),this.view=new m.view.Channel({model:this,name:t}),e=this.get("members"),e.channel=this,e.bind("add",function(e,n,i){var s=m.global.settings.get("show_joins_parts");s!==!1&&this.addMsg(" ",u("channel_join",{member:e.getMaskParts(),text:d("client_models_channel_join"),channel:t}),"action join",{time:i.kiwi.time})},this),e.bind("remove",function(e,n,i){var s=m.global.settings.get("show_joins_parts"),a=i.kiwi.message?"("+i.kiwi.message+")":"";"quit"===i.kiwi.type&&s?this.addMsg(" ",u("channel_quit",{member:e.getMaskParts(),text:d("client_models_channel_quit",[a]),channel:t}),"action quit",{time:i.kiwi.time}):"kick"===i.kiwi.type?i.kiwi.current_user_kicked?this.addMsg(" ",u("channel_selfkick",{text:d("client_models_channel_selfkick",[i.kiwi.by,a]),channel:t}),"action kick",{time:i.kiwi.time}):(s||i.kiwi.current_user_initiated)&&this.addMsg(" ",u("channel_kicked",{member:e.getMaskParts(),text:d("client_models_channel_kicked",[i.kiwi.by,a]),channel:t}),"action kick",{time:i.kiwi.time}):s&&this.addMsg(" ",u("channel_part",{member:e.getMaskParts(),text:d("client_models_channel_part",[a]),channel:t}),"action part",{time:i.kiwi.time})},this),m.global.events.emit("panel:created",{panel:this})},addMsg:function(e,t,n,i){var s,a,o,c,l=parseInt(m.global.settings.get("scrollback"),10)||250;i=i||{},i.time="number"==typeof i.time?new Date(i.time):new Date,i&&"undefined"!=typeof i.style||(i.style=""),s={msg:t,date:i.date,time:i.time,nick:e,chan:this.get("name"),type:n,style:i.style},o=this.get("members"),o&&(c=o.getByNick(s.nick),c&&(s.nick_prefix=c.get("prefix"))),"string"!=typeof s.type&&(s.type=""),"string"!=typeof s.msg&&(s.msg=""),a=this.get("scrollback"),a&&(a.push(s),a.length>l&&(a=_.last(a,l)),this.set({scrollback:a},{silent:!0})),this.trigger("msg",s)},clearMessages:function(){this.set({scrollback:[]},{silent:!0}),this.addMsg("","Window cleared"),this.view.render()},setMode:function(e){this.get("network").gateway.mode(this.get("name"),e)},isChannel:function(){return!0}}),m.model.Query=m.model.Channel.extend({initialize:function(){var e=this.get("name")||"";this.view=new m.view.Channel({model:this,name:e}),this.set({name:e,scrollback:[]},{silent:!0}),m.global.events.emit("panel:created",{panel:this})},isChannel:function(){return!1},isQuery:function(){return!0}}),m.model.Server=m.model.Channel.extend({initialize:function(){var e="Server";this.view=new m.view.Channel({model:this,name:e}),this.set({scrollback:[],name:e},{silent:!0}),m.global.events.emit("panel:created",{panel:this})},isServer:function(){return!0},isChannel:function(){return!1}}),m.model.Applet=m.model.Panel.extend({initialize:function(){var e="applet_"+(new Date).getTime().toString()+Math.ceil(100*Math.random()).toString();this.view=new m.view.Applet({model:this,name:e}),this.set({name:e},{silent:!0}),this.loaded_applet=null},load:function(e,t){return"object"==typeof e?(e.get||e.extend)&&(this.set("title",e.get("title")||m.global.i18n.translate("client_models_applet_unknown").fetch()),e.bind("change:title",function(e,t){this.set("title",t)},this),this.view.$el.html(""),e.view&&this.view.$el.append(e.view.$el),this.loaded_applet=e,this.loaded_applet.trigger("applet_loaded")):"string"==typeof e&&this.loadFromUrl(e,t),this},loadFromUrl:function(e,t){var n=this;this.view.$el.html(m.global.i18n.translate("client_models_applet_loading").fetch()),$script(e,function(){return m.applets[t]?void n.load(new m.applets[t]):void n.view.$el.html(m.global.i18n.translate("client_models_applet_notfound").fetch())})},close:function(){this.view.$el.remove(),this.destroy(),this.view=t,this.loaded_applet&&this.loaded_applet.dispose&&this.loaded_applet.dispose(),this.constructor.__super__.close.apply(this,arguments)},isApplet:function(){return!0}},{loadOnce:function(e){var t=_.find(m.app.panels("applets"),function(t){return t.isApplet()&&t.loaded_applet?t.loaded_applet.get("_applet_name")===e?!0:void 0:void 0});return t?t:this.load(e)},load:function(e,t){var n,i;return t=t||{},(i=this.getApplet(e))?(n=new m.model.Applet,n.load(new i({_applet_name:e})),t.no_tab||m.app.applet_panels.add(n),n):void 0},getApplet:function(e){return m.applets[e]||null},register:function(e,t){m.applets[e]=t}}),m.model.PluginManager=Backbone.Model.extend({initialize:function(){this.$plugin_holder=$('<div id="kiwi_plugins" style="display:none;"></div>').appendTo(m.app.view.$el),this.loading_plugins=0,this.loaded_plugins={}},load:function(e){var t=this;this.loaded_plugins[e]&&this.unload(e),this.loading_plugins++,this.loaded_plugins[e]=$("<div></div>"),this.loaded_plugins[e].appendTo(this.$plugin_holder).load(e,_.bind(t.pluginLoaded,t))},unload:function(e){this.loaded_plugins[e]&&(this.loaded_plugins[e].remove(),delete this.loaded_plugins[e])},pluginLoaded:function(){this.loading_plugins--,0===this.loading_plugins&&this.trigger("loaded")}}),m.model.DataStore=Backbone.Model.extend({initialize:function(){this._namespace="",this.new_data={}},namespace:function(e){return e&&(this._namespace=e),this._namespace},save:function(){localStorage.setItem(this._namespace,JSON.stringify(this.attributes))},load:function(){if(localStorage){var e;try{e=JSON.parse(localStorage.getItem(this._namespace))||{}}catch(t){e={}}this.attributes=e}}},{instance:function(e,t){var n=new m.model.DataStore(t);return n.namespace(e),n}}),m.model.ChannelInfo=Backbone.Model.extend({initialize:function(){this.view=new m.view.ChannelInfo({model:this})}}),m.view.Panel=Backbone.View.extend({tagName:"div",className:"panel",events:{},initialize:function(e){this.initializePanel(e)},initializePanel:function(e){this.$el.css("display","none"),e=e||{},this.$container=$(e.container?e.container:"#kiwi .panels .container1"),this.$el.appendTo(this.$container),this.alert_level=0,this.model.set({view:this},{silent:!0}),this.listenTo(this.model,"change:activity_counter",function(e,t){var n=this.model.tab.find(".activity");n.text(t>999?"999+":t),0===t?n.addClass("zero"):n.removeClass("zero")})},render:function(){},show:function(){var e=this.$el;this.$container.children(".panel").css("display","none"),e.css("display","block");var t=this.model.get("members");t?(m.app.rightbar.show(),t.view.show()):m.app.rightbar.hide(),this.alert("none"),this.model.set("activity_counter",0),m.app.panels.trigger("active",this.model,m.app.panels().active),this.model.trigger("active",this.model),m.app.view.doLayout(),this.model.isApplet()||this.scrollToBottom(!0)},alert:function(e){if(this.model!=m.app.panels().active){var t,n;t=["none","action","activity","highlight"],e=e||"none",n=_.indexOf(t,e),n||(e="none",n=0),0!==n&&n<=this.alert_level||(this.model.tab.removeClass(function(e,t){return(t.match(/\balert_\S+/g)||[]).join(" ")}),"none"!==e&&this.model.tab.addClass("alert_"+e),this.alert_level=n)}},scrollToBottom:function(e){this.model===m.app.panels().active&&(e||this.$container.scrollTop()+this.$container.height()>this.$el.outerHeight()-150)&&(this.$container[0].scrollTop=this.$container[0].scrollHeight)}}),m.view.Channel=m.view.Panel.extend({events:function(){var e=this.constructor.__super__.events;return _.isFunction(e)&&(e=e()),_.extend({},e,{"click .msg .nick":"nickClick","click .msg .inline-nick":"nickClick","click .chan":"chanClick","click .media .open":"mediaClick","mouseenter .msg .nick":"msgEnter","mouseleave .msg .nick":"msgLeave"})},initialize:function(e){this.initializePanel(e),this.$messages=$('<div class="messages"></div>'),this.$el.append(this.$messages),this.model.bind("change:topic",this.topic,this),this.model.bind("change:topic_set_by",this.topicSetBy,this),this.model.get("members")&&(this.model.get("members").bind("add",function(e){e.get("nick")===this.model.collection.network.get("nick")&&this.$el.find(".initial_loader").slideUp(function(){$(this).remove()})},this),this.model.get("members").bind("reset",function(e){e.getByNick(this.model.collection.network.get("nick"))&&this.$el.find(".initial_loader").slideUp(function(){$(this).remove()})},this)),this.model.isChannel()&&this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;"> '+m.global.i18n.translate("client_views_channel_joining").fetch()+' <span class="loader"></span></div>'),this.model.bind("msg",this.newMsg,this),this.msg_count=0},render:function(){var e=this;this.$messages.empty(),_.each(this.model.get("scrollback"),function(t){e.newMsg(t)})},newMsg:function(e){e=this.generateMessageDisplayObj(e),m.global.events.emit("message:display",{panel:this.model,message:e}).then(_.bind(function(){var t,n=_.clone(e);n.nick=u("message_nick",{nick:e.nick,prefix:e.nick_prefix||""}),t='<div class="msg <%= type %> <%= css_classes %>"><div class="time"><%- time_string %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>',this.$messages.append($(_.template(t,n)).data("message",e)),e.type.match(/^action /)?this.alert("action"):e.is_highlight?(m.app.view.alertWindow("* "+m.global.i18n.translate("client_views_panel_activity").fetch()),m.app.view.favicon.newHighlight(),m.app.view.playSound("highlight"),m.app.view.showNotification(this.model.get("name"),e.unparsed_msg),this.alert("highlight")):(this.model.isActive()&&m.app.view.alertWindow("* "+m.global.i18n.translate("client_views_panel_activity").fetch()),this.alert("activity")),this.model.isQuery()&&!this.model.isActive()&&(m.app.view.alertWindow("* "+m.global.i18n.translate("client_views_panel_activity").fetch()),e.is_highlight||m.app.view.favicon.newHighlight(),m.app.view.showNotification(this.model.get("name"),e.unparsed_msg),m.app.view.playSound("highlight")),function(){if(!this.model.isActive()){var t,n,i=m.global.settings.get("count_all_activity");"undefined"==typeof i&&(i=!1),t=["action join","action quit","action part","action kick","action nick","action mode"],(i||-1===_.indexOf(t,e.type))&&(n=this.model.get("activity_counter")||0,n++,this.model.set("activity_counter",n))}}.apply(this),this.model.isActive()&&this.scrollToBottom(),this.msg_count++,this.msg_count>(parseInt(m.global.settings.get("scrollback"),10)||250)&&($(".msg:first",this.$messages).remove(),this.msg_count--)},this))},parseMessageNicks:function(e,t){var n,i,s="";return n=this.model.get("members"),n&&(i=n.getByNick(e))?(t!==!1&&(s=this.getNickStyles(i.get("nick")).asCssString()),_.template('<span class="inline-nick" style="<%- style %>;cursor:pointer;" data-nick="<%- nick %>"><%- nick %></span>',{nick:e,style:s})):void 0},parseMessageChannels:function(e){var t,n=!1,i=this.model.get("network");if(i)return t=new RegExp("(^|\\s)(["+r(i.get("channel_prefix"))+"][^ ,\\007]+)","g"),e.match(t)?n=e.replace(t,function(e,t){return t+'<a class="chan" data-channel="'+_.escape(e.trim())+'">'+_.escape(e.trim())+"</a>"}):n},parseMessageUrls:function(e){var t,n=!1;return t=e.replace(/^(([A-Za-z][A-Za-z0-9\-]*\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w!:.?$'()[\]*,;~+=&%@!\-\/]*)?(#.*)?$/gi,function(e){var t=e,i="";return e.match(/^javascript:/)?e:(n=!0,e.match(/^www\./)&&(e="http://"+e),t.length>100&&(t=t.substr(0,100)+"..."),i=m.view.MediaMessage.buildHtml(e),'<a class="link_ext" target="_blank" rel="nofollow" href="'+e.replace(/"/g,"%22")+'">'+_.escape(t)+"</a>"+i)}),n?t:!1},getNickStyles:function(e){var t,n,i,s,a=0;return _.map(e.split(""),function(e){a+=e.charCodeAt(0)}),s=(_.find(m.app.themes,function(e){return e.name.toLowerCase()===m.global.settings.get("theme").toLowerCase()})||{}).nick_lightness,s="number"!=typeof s?35:Math.max(0,Math.min(100,s)),i=o(a%255,70,s),i=i[2]|i[1]<<8|i[0]<<16,n="#"+i.toString(16),t={color:n},t.asCssString=function(){return _.reduce(this,function(e,t,n){return e+n+":"+t+";"},"")},t},generateMessageDisplayObj:function(e){var t,n,i,s,a,o,c=this.model.get("scrollback"),p=c[c.length-2];e=_.clone(e),e.css_classes="",e.nick_style="",e.is_highlight=!1,e.time_string="";var u=m.app.connections.active_connection.get("nick");return new RegExp("(^|\\W)("+r(u)+")(\\W|$)","i").test(e.msg)&&0!==e.nick.localeCompare(u)&&(e.is_highlight=!0,e.css_classes+=" highlight"),i=e.msg.split(" "),i=_.map(i,function(t){var n;return n=this.parseMessageUrls(t),"string"==typeof n?n:(n=this.parseMessageChannels(t),"string"==typeof n?n:(n=this.parseMessageNicks(t,"privmsg"===e.type),"string"==typeof n?n:(n=_.escape(t),m.global.settings.get("show_emoticons")&&(n=h(n)),n)))},this),e.unparsed_msg=e.msg,e.msg=i.join(" "),e.msg=l(e.msg),e.nick_style=this.getNickStyles(e.nick).asCssString(),t="",e.nick&&(_.map(e.nick.split(""),function(e){t+=e.charCodeAt(0).toString(16)}),e.css_classes+=" nick_"+t),p&&(n=(e.time.getTime()-p.time.getTime())/1e3/60,p.nick===e.nick&&1>n&&(e.css_classes+=" repeated_nick")),m.global.settings.get("use_24_hour_timestamps")?e.time_string=e.time.getHours().toString().lpad(2,"0")+":"+e.time.getMinutes().toString().lpad(2,"0")+":"+e.time.getSeconds().toString().lpad(2,"0"):(s=e.time.getHours(),a=s>11,s%=12,0===s&&(s=12),o=a?"client_views_panel_timestamp_pm":"client_views_panel_timestamp_am",e.time_string=d(o,s+":"+e.time.getMinutes().toString().lpad(2,"0")+":"+e.time.getSeconds().toString().lpad(2,"0"))),e},topic:function(e){"string"==typeof e&&e||(e=this.model.get("topic")),this.model.addMsg("",u("channel_topic",{text:e,channel:this.model.get("name")}),"topic"),m.app.panels().active===this.model&&m.app.topicbar.setCurrentTopicFromChannel(this.model)},topicSetBy:function(){m.app.panels().active===this.model&&m.app.topicbar.setCurrentTopicFromChannel(this.model)},nickClick:function(e){var t,n,i=$(e.currentTarget),s=this.model.get("members");e.stopPropagation(),t=i.data("nick"),t||(t=i.parent(".msg").data("message").nick),n=s?s.getByNick(t):null,n&&m.global.events.emit("nick:select",{target:i,member:n,source:"message"}).then(_.bind(this.openUserMenuForNick,this,i,n))},updateLastSeenMarker:function(){this.model.isActive()&&(this.$(".last_seen").removeClass("last_seen"),this.$messages.children().last().addClass("last_seen"))},openUserMenuForNick:function(e,t){var n,i,s=this.model.get("members"),a=!!s.getByNick(m.app.connections.active_connection.get("nick")).get("is_op");n=new m.view.UserBox,n.setTargets(t,this.model),n.displayOpItems(a),i=new m.view.MenuBox(t.get("nick")||"User"),i.addItem("userbox",n.$el),i.showFooter(!1),m.global.events.emit("usermenu:created",{menu:i,userbox:n,user:t}).then(_.bind(function(){i.show();var t=e.offset(),n=t.top,s=n+i.$el.outerHeight(),a=this.$el.parent().offset().top+this.$el.parent().outerHeight();s>a&&(n=a-i.$el.outerHeight()),i.$el.offset({left:t.left,top:n})},this)).catch(_.bind(function(){n=null,menu.dispose(),menu=null},this))},chanClick:function(e){var t=e.target?$(e.target).data("channel"):$(e.srcElement).data("channel");m.app.connections.active_connection.gateway.join(t)},mediaClick:function(e){var t,n=$(e.target).parents(".media");n.data("media")?t=n.data("media"):(t=new m.view.MediaMessage({el:n[0]}),n.data("media",t)),t.toggle()},msgEnter:function(e){var t;_.each($(e.currentTarget).parent(".msg").attr("class").split(" "),function(e){e.match(/^nick_[a-z0-9]+/i)&&(t=e)}),t&&$("."+t).addClass("global_nick_highlight")},msgLeave:function(e){var t;_.each($(e.currentTarget).parent(".msg").attr("class").split(" "),function(e){e.match(/^nick_[a-z0-9]+/i)&&(t=e)}),t&&$("."+t).removeClass("global_nick_highlight")}}),m.view.Applet=m.view.Panel.extend({className:"panel applet",initialize:function(e){this.initializePanel(e)}}),m.view.Application=Backbone.View.extend({initialize:function(){var e=this;this.$el=$($("#tmpl_application").html().trim()),this.el=this.$el[0],$(this.model.get("container")||"body").append(this.$el),this.elements={panels:this.$el.find(".panels"),right_bar:this.$el.find(".right_bar"),toolbar:this.$el.find(".toolbar"),controlbox:this.$el.find(".controlbox"),resize_handle:this.$el.find(".memberlists_resize_handle")},$(window).resize(function(){e.doLayout.apply(e)}),this.elements.toolbar.resize(function(){e.doLayout.apply(e)}),this.elements.controlbox.resize(function(){e.doLayout.apply(e)}),m.global.settings.on("change:theme",this.updateTheme,this),this.updateTheme(getQueryVariable("theme")),m.global.settings.on("change:channel_list_style",this.setTabLayout,this),this.setTabLayout(m.global.settings.get("channel_list_style")),m.global.settings.on("change:show_timestamps",this.displayTimestamps,this),this.displayTimestamps(m.global.settings.get("show_timestamps")),this.$el.appendTo($("body")),this.doLayout(),$(document).keydown(this.setKeyFocus),window.onbeforeunload=function(){return m.gateway.isConnected()?m.global.i18n.translate("client_views_application_close_notice").fetch():void 0},this.has_focus=!0,$(window).on("focus",function(){e.has_focus=!0}),$(window).on("blur",function(){var t=e.model.panels().active;t&&t.view.updateLastSeenMarker&&t.view.updateLastSeenMarker(),e.has_focus=!1}),$(window).on("touchstart",function t(){e.$el.addClass("touch"),$(window).off("touchstart",t)}),this.favicon=new m.view.Favicon,this.initSound(),this.monitorPanelFallback()},updateTheme:function(e){e===m.global.settings&&(e=arguments[1]),e||(e=m.global.settings.get("theme")||"relaxed"),e=e.toLowerCase(),$("[data-theme]:not([disabled])").each(function(e,t){var n=$(t);n.attr("rel","alternate "+n.attr("rel")).attr("disabled",!0)[0].disabled=!0});var t=$("[data-theme][title="+e+"]");t.length>0&&(t.attr("rel","stylesheet").attr("disabled",!1)[0].disabled=!1),this.doLayout()},setTabLayout:function(e){e===m.global.settings&&(e=arguments[1]),"list"==e?this.$el.addClass("chanlist_treeview"):this.$el.removeClass("chanlist_treeview"),this.doLayout()},displayTimestamps:function(e){e===m.global.settings&&(e=arguments[1]),e?this.$el.addClass("timestamps"):this.$el.removeClass("timestamps")},setKeyFocus:function(e){e.ctrlKey||e.altKey||e.metaKey||"input"===e.target.tagName.toLowerCase()||"textarea"===e.target.tagName.toLowerCase()||$(e.target).attr("contenteditable")||$("#kiwi .controlbox .inp").focus()},doLayout:function(){var e=this.$el,t=this.elements.panels,n=this.elements.right_bar,i=this.elements.toolbar,s=this.elements.controlbox,a=this.elements.resize_handle;if(e.is(":visible")){var o={top:i.outerHeight(!0),bottom:s.outerHeight(!0)};i.is(":visible")||(o.top=0),s.is(":visible")||(o.bottom=0),t.css(o),n.css(o),a.css(o),e.hasClass("chanlist_treeview")&&this.$el.find(".tabs",e).css(o),e.outerWidth()<420?(e.addClass("narrow"),this.model.rightbar&&this.model.rightbar.keep_hidden!==!0&&this.model.rightbar.toggle(!0)):(e.removeClass("narrow"),this.model.rightbar&&this.model.rightbar.keep_hidden!==!1&&this.model.rightbar.toggle(!1)),n.hasClass("disabled")?(t.css("right",0),a.css("left",t.outerWidth(!0))):(t.css("right",n.outerWidth(!0)),a.css("left",n.position().left-a.outerWidth(!0)/2));var c=parseInt(s.find(".input_tools").outerWidth(),10);s.find(".input_wrap").css("right",c+7)}},alertWindow:function(e){this.alertWindowTimer||(this.alertWindowTimer=new function(){var e,t=this,n=!0,i=0,s=m.app.server_settings.client.window_title||"Kiwi IRC",a="Kiwi IRC";this.setTitle=function(e){return e=e||s,window.document.title=e,e},this.start=function(t){n||(a=t,e||(e=setInterval(this.update,1e3)))},this.stop=function(){e&&clearInterval(e),e=null,this.setTitle(),setTimeout(this.reset,2e3)},this.reset=function(){e||t.setTitle()},this.update=function(){0===i?(t.setTitle(a),i=1):(t.setTitle(),i=0)},$(window).focus(function(){n=!0,t.stop(),setTimeout(t.reset,2e3)}),$(window).blur(function(){n=!1})}),this.alertWindowTimer.start(e)},barsHide:function(e){e?(this.$el.find(".toolbar").slideUp(0),$("#kiwi .controlbox").slideUp(0),this.doLayout()):(this.$el.find(".toolbar").slideUp({queue:!1,duration:400,step:$.proxy(this.doLayout,this)}),$("#kiwi .controlbox").slideUp({queue:!1,duration:400,step:$.proxy(this.doLayout,this)}))},barsShow:function(e){e?(this.$el.find(".toolbar").slideDown(0),$("#kiwi .controlbox").slideDown(0),this.doLayout()):(this.$el.find(".toolbar").slideDown({queue:!1,duration:400,step:$.proxy(this.doLayout,this)}),$("#kiwi .controlbox").slideDown({queue:!1,duration:400,step:$.proxy(this.doLayout,this)}))},initSound:function(){var e=this,t=this.model.get("base_path");$script(t+"/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js",function(){"undefined"!=typeof soundManager&&soundManager.setup({url:t+"/assets/libs/soundmanager2/",flashVersion:9,preferFlash:!0,onready:function(){e.sound_object=soundManager.createSound({id:"highlight",url:t+"/assets/sound/highlight.mp3"})}})})},playSound:function(e){this.sound_object&&(m.global.settings.get("mute_sounds")||soundManager.play(e))},showNotification:function(e,t){var n=this.model.get("base_path")+"/assets/img/ico.png",i=m.utils.notifications;!this.has_focus&&i.allowed()&&i.create(e,{icon:n,body:t}).closeAfter(5e3).on("click",_.bind(window.focus,window))},monitorPanelFallback:function(){var e=[];this.model.panels.on("active",function(){var t,n=m.app.panels().active;t=_.indexOf(e,n.cid),t>-1&&e.splice(t,1),e.unshift(n.cid)}),this.model.panels.on("remove",function(t){if(e[0]===t.cid){e.shift();var n=_.find(m.app.panels("applets").concat(m.app.panels("connections")),{cid:e[0]});n&&n.view.show()}})}}),m.view.AppToolbar=Backbone.View.extend({events:{"click .settings":"clickSettings","click .startup":"clickStartup"},initialize:function(){m.app.server_settings.connection&&!m.app.server_settings.connection.allow_change&&this.$(".startup").css("display","none")},clickSettings:function(e){e.preventDefault(),m.app.controlbox.processInput("/settings")},clickStartup:function(e){e.preventDefault(),m.app.startup_applet.view.show()}}),m.view.ControlBox=Backbone.View.extend({events:{"keydown .inp":"process","click .nick":"showNickChange"},initialize:function(){var e=this;this.buffer=[],this.buffer_pos=0,this.preprocessor=new a,this.preprocessor.recursive_depth=5,this.tabcomplete={active:!1,data:[],prefix:""},m.app.connections.on("change:nick",function(t){t===m.app.connections.active_connection&&$(".nick",e.$el).text(t.get("nick"))}),m.app.connections.on("active",function(t,n){$(".nick",e.$el).text(n.get("nick"))}),m.app.panels.bind("active",function(t){(t.isChannel()||t.isServer()||t.isQuery())&&e.$(".inp").focus()})},render:function(){var e=d("client_views_controlbox_message");return this.$(".inp").attr("placeholder",e),this},showNickChange:function(){this.nick_change||(this.nick_change=new m.view.NickChangeBox,this.nick_change.render(),this.listenTo(this.nick_change,"close",function(){delete this.nick_change}))},process:function(e){var t,n=this,i=$(e.currentTarget),s=i.val();switch(t=-1!==navigator.appVersion.indexOf("Mac")?e.metaKey:e.altKey,this.tabcomplete.active&&9!==e.keyCode&&(this.tabcomplete.active=!1,this.tabcomplete.data=[],this.tabcomplete.prefix=""),!0){case 13===e.keyCode:return s=s.trim(),s&&($.each(s.split("\n"),function(e,t){n.processInput(t)}),this.buffer.push(s),this.buffer_pos=this.buffer.length),i.val(""),!1;case 38===e.keyCode:return this.buffer_pos>0&&(this.buffer_pos--,i.val(this.buffer[this.buffer_pos])),!1;case 40===e.keyCode:this.buffer_pos<this.buffer.length&&(this.buffer_pos++,i.val(this.buffer[this.buffer_pos]));break;case 219===e.keyCode&&t:var a=$("#kiwi .tabs").find("li[class!=connection]"),o=function(){for(var e=0;e<a.length;e++)if($(a[e]).hasClass("active"))return e}();return $prev_tab=$(0===o?a[a.length-1]:a[o-1]),$prev_tab.click(),!1;case 221===e.keyCode&&t:var a=$("#kiwi .tabs").find("li[class!=connection]"),o=function(){for(var e=0;e<a.length;e++)if($(a[e]).hasClass("active"))return e
+}();return $next_tab=$(o===a.length-1?a[0]:a[o+1]),$next_tab.click(),!1;case!(9!==e.keyCode||e.shiftKey||e.altKey||e.metaKey||e.ctrlKey):if(this.tabcomplete.active=!0,_.isEqual(this.tabcomplete.data,[])){var c=[],l=m.app.panels().active.get("members");l=l?l.models:[],$.each(l,function(e,t){t&&c.push(t.get("nick"))}),c.push(m.app.panels().active.get("name")),c=_.sortBy(c,function(e){return e.toLowerCase()}),this.tabcomplete.data=c}return" "===s[i[0].selectionStart-1]?!1:(function(){var e,t,a,o,c,l,r=": ";e=s.substring(0,i[0].selectionStart).split(" "),":"==e[e.length-1]&&e.pop(),e.length>1&&(r=""),l=e[e.length-1],""===this.tabcomplete.prefix&&(this.tabcomplete.prefix=l),this.tabcomplete.data=_.select(this.tabcomplete.data,function(e){return 0===e.toLowerCase().indexOf(n.tabcomplete.prefix.toLowerCase())}),this.tabcomplete.data.length>0&&(a=i[0].selectionStart-l.length,t=s.substr(0,a),o=this.tabcomplete.data.shift(),this.tabcomplete.data.push(o),t+=o,s.substr(i[0].selectionStart,2)!==r&&(t+=r),t+=s.substr(i[0].selectionStart),i.val(t),i[0].setSelectionRange?i[0].setSelectionRange(a+o.length+r.length,a+o.length+r.length):i[0].createTextRange&&(c=i[0].createTextRange(),c.collapse(!0),c.moveEnd("character",a+o.length+r.length),c.moveStart("character",a+o.length+r.length),c.select()))}.apply(this),!1)}},processInput:function(e){var t,n,i,s=this;"/"===e[0]||m.app.panels().active.isChannel()||m.app.panels().active.isQuery()||(e="/"+e),("/"!==e[0]||"//"===e.substr(0,2))&&(e=e.replace(/^\/\//,"/"),e="/msg "+m.app.panels().active.get("name")+" "+e),this.preprocessor.vars.server=m.app.connections.active_connection.get("name"),this.preprocessor.vars.channel=m.app.panels().active.get("name"),this.preprocessor.vars.destination=this.preprocessor.vars.channel,e=this.preprocessor.process(e),n=e.split(/\s/),"/"===n[0][0]?(t=n[0].substr(1).toLowerCase(),n=n.splice(1,n.length-1)):(t="msg",n.unshift(m.app.panels().active.get("name"))),i={command:t,params:n},m.global.events.emit("command",i).then(function(){s.trigger("command",{command:i.command,params:i.params}),s.trigger("command:"+i.command,{command:i.command,params:i.params}),s._events["command:"+i.command]||s.trigger("unknown_command",{command:i.command,params:i.params})})},addPluginIcon:function(e){var t=$('<div class="tool"></div>').append(e);this.$el.find(".input_tools").append(t),m.app.view.doLayout()}}),m.view.Favicon=Backbone.View.extend({initialize:function(){var e=this,t=$(window);this.has_focus=!0,this.highlight_count=0,this.has_canvas_support=!!window.CanvasRenderingContext2D,this.original_favicon=$('link[rel~="icon"]')[0].href,this._createCanvas(),t.on("focus",function(){e.has_focus=!0,e._resetHighlights()}),t.on("blur",function(){e.has_focus=!1})},newHighlight:function(){var e=this;this.has_focus||(this.highlight_count++,this.has_canvas_support&&this._drawFavicon(function(){e._drawBubble(e.highlight_count.toString()),e._refreshFavicon(e.canvas.toDataURL())}))},_resetHighlights:function(){this.highlight_count=0,this._refreshFavicon(this.original_favicon)},_drawFavicon:function(e){var t=this.canvas,n=t.getContext("2d"),i=new Image;i.crossOrigin="anonymous",i.src=this.original_favicon,i.onload=function(){n.clearRect(0,0,t.width,t.height),n.drawImage(i,0,0,t.width,t.height),e()}},_drawBubble:function(e){var t,n=0,i=0,s=this.canvas,a=test_context=s.getContext("2d"),o=s.width,c=s.height;t=-1!==navigator.appVersion.indexOf("Mac")?-1.5:-1,test_context.font=a.font="bold 10px Arial",test_context.textAlign="right",this._renderText(test_context,e,0,0,t),n=test_context.measureText(e).width+t*(e.length-1)+2,i=9,bubbleX=o-n,bubbleY=c-i,a.fillStyle="red",a.fillRect(bubbleX,bubbleY,n,i),a.fillStyle="white",this._renderText(a,e,o-1,c-1,t)},_refreshFavicon:function(e){$('link[rel~="icon"]').remove(),$('<link rel="shortcut icon" href="'+e+'">').appendTo($("head"))},_createCanvas:function(){var e=document.createElement("canvas");e.width=16,e.height=16,this.canvas=e},_renderText:function(e,t,n,i,s){for(var a,o=t.split("").reverse(),c=0,l=n;c<t.length;)a=o[c++],e.fillText(a,l,i),l+=-1*(e.measureText(a).width+s);return e}}),m.view.MediaMessage=Backbone.View.extend({events:{"click .media_close":"close"},initialize:function(){this.url=this.$el.data("url")},toggle:function(){this.$content&&this.$content.is(":visible")?this.close():this.open()},close:function(){var e=this;this.$content.slideUp("fast",function(){e.$content.remove()})},open:function(){this.$content||(this.$content=$('<div class="media_content"><a class="media_close"><i class="fa fa-chevron-up"></i> '+m.global.i18n.translate("client_views_mediamessage_close").fetch()+'</a><br /><div class="content"></div></div>'),this.$content.find(".content").append(this.mediaTypes[this.$el.data("type")].apply(this,[])||m.global.i18n.translate("client_views_mediamessage_notfound").fetch()+" :(")),this.$content.is(":visible")||(this.$content.hide(),this.$el.append(this.$content),this.$content.slideDown())},mediaTypes:{twitter:function(){var e=this.$el.data("tweetid"),t=this;return $.getJSON("https://api.twitter.com/1/statuses/oembed.json?id="+e+"&callback=?",function(e){t.$content.find(".content").html(e.html)}),$("<div>"+m.global.i18n.translate("client_views_mediamessage_load_tweet").fetch()+"...</div>")},image:function(){return $('<a href="'+this.url+'" target="_blank"><img height="100" src="'+this.url+'" /></a>')},imgur:function(){var e=this;return $.getJSON("http://api.imgur.com/oembed?url="+this.url,function(t){var n='<a href="'+t.url+'" target="_blank"><img height="100" src="'+t.url+'" /></a>';e.$content.find(".content").html(n)}),$("<div>"+m.global.i18n.translate("client_views_mediamessage_load_image").fetch()+"...</div>")},reddit:function(){var e=this,t=/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi.exec(this.url);return $.getJSON("http://www."+t[0]+".json?jsonp=?",function(t){console.log("Loaded reddit data",t);var n=t[0].data.children[0].data,i="";n.thumbnail&&(n.over_18?(i="<span class=\"thumbnail_nsfw\" onclick=\"$(this).find('p').remove(); $(this).find('img').css('visibility', 'visible');\">",i+='<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>',i+='<img src="'+n.thumbnail+'" class="thumbnail" style="visibility:hidden;" />',i+="</span>"):i='<img src="'+n.thumbnail+'" class="thumbnail" />');var s="<div>"+i+"<b><%- title %></b><br />Posted by <%- author %>. &nbsp;&nbsp; ";s+='<i class="fa fa-arrow-up"></i> <%- ups %> &nbsp;&nbsp; <i class="fa fa-arrow-down"></i> <%- downs %><br />',s+='<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>',e.$content.find(".content").html(_.template(s,n))}),$("<div>"+m.global.i18n.translate("client_views_mediamessage_load_reddit").fetch()+"...</div>")},youtube:function(){var e=this.$el.data("ytid"),t=this,n='<iframe width="480" height="270" src="https://www.youtube.com/embed/'+e+'?feature=oembed" frameborder="0" allowfullscreen=""></iframe>';return t.$content.find(".content").html(n),$("")},gist:function(){var e=this,t=/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i.exec(this.url);return $.getJSON("https://gist.github.com/"+t[1]+".json?callback=?"+(t[2]||""),function(t){$("body").append('<link rel="stylesheet" href="'+t.stylesheet+'" type="text/css" />'),e.$content.find(".content").html(t.div)}),$("<div>"+m.global.i18n.translate("client_views_mediamessage_load_gist").fetch()+"...</div>")},spotify:function(){var e,t,n=this.$el.data("uri"),i=this.$el.data("method");switch(i){case"track":case"album":e={url:"https://embed.spotify.com/?uri="+n,width:300,height:80};break;case"artist":e={url:"https://embed.spotify.com/follow/1/?uri="+n+"&size=detail&theme=dark",width:300,height:56}}return t='<iframe src="'+e.url+'" width="'+e.width+'" height="'+e.height+'" frameborder="0" allowtransparency="true"></iframe>',$(t)},soundcloud:function(){var e=this.$el.data("url"),t=$("<div></div>").text(m.global.i18n.translate("client_models_applet_loading").fetch());return $.getJSON("https://soundcloud.com/oembed",{url:e}).then(function(e){t.empty().append($(e.html).attr("height",e.height-100))},function(){t.text(m.global.i18n.translate("client_views_mediamessage_notfound").fetch())}),t},custom:function(){var e=this.constructor.types[this.$el.data("index")];if(e)return $(e.buildHtml(this.$el.data("url")))}}},{addType:function(e,t){"function"==typeof e&&"function"==typeof t&&(this.types=this.types||[],this.types.push({match:e,buildHtml:t}))},buildHtml:function(e){var t,n="";if(_.each(this.types||[],function(t,i){t.match(e)&&(n+='<span class="media" title="Open" data-type="custom" data-index="'+i+'" data-url="'+_.escape(e)+'"><a class="open"><i class="fa fa-chevron-right"></i></a></span>')}),e.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)&&(n+='<span class="media image" data-type="image" data-url="'+e+'" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/imgur\.com\/[^\/]*(?!=\.[^!.]+($|\?))/gi.exec(e),t&&!e.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)&&(n+='<span class="media imgur" data-type="imgur" data-url="'+e+'" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/gi.exec(e),t&&(n+='<span class="media twitter" data-type="twitter" data-url="'+e+'" data-tweetid="'+t[2]+'" title="Show tweet information"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi.exec(e),t&&(n+='<span class="media reddit" data-type="reddit" data-url="'+e+'" title="Reddit thread"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/gi.exec(e),t&&(n+='<span class="media youtube" data-type="youtube" data-url="'+e+'" data-ytid="'+t[1]+'" title="YouTube Video"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i.exec(e),t&&(n+='<span class="media gist" data-type="gist" data-url="'+e+'" data-gist_id="'+t[1]+'" title="GitHub Gist"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/http:\/\/(?:play|open\.)?spotify.com\/(album|track|artist)\/([a-zA-Z0-9]+)\/?/i.exec(e)){var i=t[1],s="spotify:"+t[1]+":"+t[2];n+='<span class="media spotify" data-type="spotify" data-uri="'+s+'" data-method="'+i+'" title="Spotify '+i+'"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'}return t=/(?:m\.)?(soundcloud\.com(?:\/.+))/i.exec(e),t&&(n+='<span class="media soundcloud" data-type="soundcloud" data-url="http://'+t[1]+'" title="SoundCloud player"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),n}}),m.view.Member=Backbone.View.extend({tagName:"li",initialize:function(){this.model.bind("change",this.render,this),this.render()},render:function(){var e=this.$el,t=(this.model.get("modes")||[]).join(" ");return e.attr("class","mode "+t),e.html('<a class="nick"><span class="prefix">'+this.model.get("prefix")+"</span>"+this.model.get("nick")+"</a>"),this}}),m.view.MemberList=Backbone.View.extend({tagName:"div",events:{"click .nick":"nickClick","click .channel_info":"channelInfoClick"},initialize:function(){this.model.bind("all",this.render,this),this.$el.appendTo("#kiwi .memberlists"),this.$meta=$('<div class="meta"></div>').appendTo(this.$el),this.$list=$("<ul></ul>").appendTo(this.$el)},render:function(){var e=this;return this.$list.empty(),this.model.forEach(function(t){t.view.$el.data("member",t),e.$list.append(t.view.$el)}),this.model.channel.isActive()&&this.renderMeta(),this},renderMeta:function(){var e=this.model.length+" "+d("client_applets_chanlist_users");this.$meta.text(e)},nickClick:function(e){var t=$(e.currentTarget).parent("li"),n=t.data("member");m.global.events.emit("nick:select",{target:t,member:n,source:"nicklist"}).then(_.bind(this.openUserMenuForItem,this,t))},openUserMenuForItem:function(e){var t,n=e.data("member"),i=!!this.model.getByNick(m.app.connections.active_connection.get("nick")).get("is_op");t=new m.view.UserBox,t.setTargets(n,this.model.channel),t.displayOpItems(i);var s=new m.view.MenuBox(n.get("nick")||"User");s.addItem("userbox",t.$el),s.showFooter(!1),m.global.events.emit("usermenu:created",{menu:s,userbox:t,user:n}).then(_.bind(function(){s.show();var t=e.offset(),n=t.top,i=n+s.$el.outerHeight(),a=this.$el.parent().offset().top+this.$el.parent().outerHeight(),o=t.left,c=o+s.$el.outerWidth(),l=this.$el.parent().offset().left+this.$el.parent().outerWidth();i>a&&(n=a-s.$el.outerHeight()),0>n&&(n=0),c>l&&(o=l-s.$el.outerWidth()),s.$el.offset({left:o,top:n})},this)).catch(_.bind(function(){t=null,s.dispose(),s=null},this))},channelInfoClick:function(){new m.model.ChannelInfo({channel:this.model.channel})},show:function(){$("#kiwi .memberlists").children().removeClass("active"),$(this.el).addClass("active"),this.renderMeta()}}),m.view.MenuBox=Backbone.View.extend({events:{"click .ui_menu_foot .close, a.close_menu":"dispose"},initialize:function(e){this.$el=$('<div class="ui_menu"><div class="items"></div></div>'),this._title=e||"",this._items={},this._display_footer=!0,this._close_on_blur=!0},render:function(){var e,t=this,n=t.$el.find(".items");n.find("*").remove(),this._title&&(e=$('<div class="ui_menu_title"></div>').text(this._title),this.$el.prepend(e)),_.each(this._items,function(e){var t=$('<div class="ui_menu_content hover"></div>').append(e);n.append(t)}),this._display_footer&&this.$el.append('<div class="ui_menu_foot"><a class="close" onclick="">Close <i class="fa fa-times"></i></a></div>')},setTitle:function(e){this._title=e,this._title&&this.$el.find(".ui_menu_title").text(this._title)},onDocumentClick:function(e){var t=$(e.target);this._close_on_blur&&t[0]!=this.$el[0]&&0===this.$el.has(t).length&&this.dispose()},dispose:function(){_.each(this._items,function(e){e.dispose&&e.dispose(),e.remove&&e.remove()}),this._items=null,this.remove(),this._close_proxy&&$(document).off("click",this._close_proxy)},addItem:function(e,t){t.is("a")&&t.addClass("fa fa-chevron-right"),this._items[e]=t},removeItem:function(e){delete this._items[e]},showFooter:function(e){this._display_footer=e},closeOnBlur:function(e){this._close_on_blur=e},show:function(){var e,t,n=this;this.render(),this.$el.appendTo(m.app.view.$el),e=m.app.view.$el.find(".controlbox"),$items=this.$el.find(".items"),t=this.$el.outerHeight()-$items.outerHeight(),$items.css({"overflow-y":"auto","max-height":e.offset().top-this.$el.offset().top-t}),setTimeout(function(){n._close_proxy=function(e){n.onDocumentClick(e)},$(document).on("click",n._close_proxy)},0)}}),m.view.NetworkTabs=Backbone.View.extend({tagName:"ul",className:"connections",initialize:function(){this.model.on("add",this.networkAdded,this),this.model.on("remove",this.networkRemoved,this),this.$el.appendTo(m.app.view.$el.find(".tabs"))},networkAdded:function(e){$('<li class="connection"></li>').append(e.panels.view.$el).appendTo(this.$el)},networkRemoved:function(e){e.panels.view.$el.parent().remove(),e.panels.view.remove(),m.app.view.doLayout()}}),m.view.NickChangeBox=Backbone.View.extend({events:{submit:"changeNick","click .cancel":"close"},initialize:function(){var e={new_nick:m.global.i18n.translate("client_views_nickchangebox_new").fetch(),change:m.global.i18n.translate("client_views_nickchangebox_change").fetch(),cancel:m.global.i18n.translate("client_views_nickchangebox_cancel").fetch()};this.$el=$(_.template($("#tmpl_nickchange").html().trim(),e))},render:function(){m.app.controlbox.$el.prepend(this.$el),this.$el.find("input").focus(),this.$el.css("bottom",m.app.controlbox.$el.outerHeight(!0))},close:function(){this.$el.remove(),this.trigger("close")},changeNick:function(e){e.preventDefault();var t=m.app.connections.active_connection;this.listenTo(t,"change:nick",function(){this.close()}),t.gateway.changeNick(this.$("input").val())}}),m.view.ResizeHandler=Backbone.View.extend({events:{mousedown:"startDrag",mouseup:"stopDrag"},initialize:function(){this.dragging=!1,this.starting_width={},$(window).on("mousemove",$.proxy(this.onDrag,this))},startDrag:function(){this.dragging=!0},stopDrag:function(){this.dragging=!1},onDrag:function(e){if(this.dragging){var t=$("#kiwi").offset().left;this.$el.css("left",e.clientX-this.$el.outerWidth(!0)/2-t),$("#kiwi .right_bar").css("width",this.$el.parent().width()-(this.$el.position().left+this.$el.outerWidth())),m.app.view.doLayout()}}}),m.view.ServerSelect=Backbone.View.extend({events:{"submit form":"submitForm","click .show_more":"showMore","change .have_pass input":"showPass","change .have_key input":"showKey","click .fa-key":"channelKeyIconClick","click .show_server":"showServer"},initialize:function(){var e={think_nick:m.global.i18n.translate("client_views_serverselect_form_title").fetch(),nickname:m.global.i18n.translate("client_views_serverselect_nickname").fetch(),have_password:m.global.i18n.translate("client_views_serverselect_enable_password").fetch(),password:m.global.i18n.translate("client_views_serverselect_password").fetch(),channel:m.global.i18n.translate("client_views_serverselect_channel").fetch(),channel_key:m.global.i18n.translate("client_views_serverselect_channelkey").fetch(),require_key:m.global.i18n.translate("client_views_serverselect_channelkey_required").fetch(),key:m.global.i18n.translate("client_views_serverselect_key").fetch(),start:m.global.i18n.translate("client_views_serverselect_connection_start").fetch(),server_network:m.global.i18n.translate("client_views_serverselect_server_and_network").fetch(),server:m.global.i18n.translate("client_views_serverselect_server").fetch(),port:m.global.i18n.translate("client_views_serverselect_port").fetch(),powered_by:m.global.i18n.translate("client_views_serverselect_poweredby").fetch()};this.$el=$(_.template($("#tmpl_server_select").html().trim(),e)),m.app.server_settings&&m.app.server_settings.connection&&(m.app.server_settings.connection.allow_change||(this.$el.find(".show_more").remove(),this.$el.addClass("single_server"))),this.state="all",this.more_shown=!1,this.model.bind("new_network",this.newNetwork,this),this.gateway=m.global.components.Network(),this.gateway.on("connect",this.networkConnected,this),this.gateway.on("connecting",this.networkConnecting,this),this.gateway.on("disconnect",this.networkDisconnected,this),this.gateway.on("irc_error",this.onIrcError,this)},dispose:function(){this.model.off("new_network",this.newNetwork,this),this.gateway.off(),this.remove()},submitForm:function(e){return e.preventDefault(),$("input.nick",this.$el).val().trim()?("nick_change"===this.state?this.submitNickChange(e):this.submitLogin(e),void $("button",this.$el).attr("disabled",1)):(this.setStatus(m.global.i18n.translate("client_views_serverselect_nickname_error_empty").fetch()),void $("input.nick",this.$el).select())},submitLogin:function(){if(!$("button",this.$el).attr("disabled")){var e={nick:$("input.nick",this.$el).val(),server:$("input.server",this.$el).val(),port:$("input.port",this.$el).val(),ssl:$("input.ssl",this.$el).prop("checked"),password:$("input.password",this.$el).val(),channel:$("input.channel",this.$el).val(),channel_key:$("input.channel_key",this.$el).val(),options:this.server_options};this.trigger("server_connect",e)}},submitNickChange:function(){m.gateway.changeNick(null,$("input.nick",this.$el).val()),this.networkConnecting()},showPass:function(){this.$el.find("tr.have_pass input").is(":checked")?this.$el.find("tr.pass").show().find("input").focus():this.$el.find("tr.pass").hide().find("input").val("")},channelKeyIconClick:function(){this.$el.find("tr.have_key input").click()},showKey:function(){this.$el.find("tr.have_key input").is(":checked")?this.$el.find("tr.key").show().find("input").focus():this.$el.find("tr.key").hide().find("input").val("")},showMore:function(){this.more_shown?($(".more",this.$el).slideUp("fast"),$(".show_more",this.$el).children(".fs-caret-up").removeClass("fa-caret-up").addClass("fa-caret-down"),$("input.nick",this.$el).select(),this.more_shown=!1):($(".more",this.$el).slideDown("fast"),$(".show_more",this.$el).children(".fa-caret-down").removeClass("fa-caret-down").addClass("fa-caret-up"),$("input.server",this.$el).select(),this.more_shown=!0)},populateFields:function(e){var t,n,i,s,a,o,c;e=e||{},t=e.nick||"",n=e.server||"",i=e.port||6667,o=e.ssl||0,c=e.password||"",s=e.channel||"",a=e.channel_key||"",$("input.nick",this.$el).val(t),$("input.server",this.$el).val(n),$("input.port",this.$el).val(i),$("input.ssl",this.$el).prop("checked",o),$("input#server_select_show_pass",this.$el).prop("checked",!!c),$("input.password",this.$el).val(c),c&&$("tr.pass",this.$el).show(),$("input.channel",this.$el).val(s),$("input#server_select_show_channel_key",this.$el).prop("checked",!!a),$("input.channel_key",this.$el).val(a),a&&$("tr.key",this.$el).show(),this.server_options={},e.encoding&&(this.server_options.encoding=e.encoding)},hide:function(){this.$el.slideUp()},show:function(e){e=e||"all",this.$el.show(),"all"===e?$(".show_more",this.$el).show():"more"===e?$(".more",this.$el).slideDown("fast"):"nick_change"===e?($(".more",this.$el).hide(),$(".show_more",this.$el).hide(),$("input.nick",this.$el).select()):"enter_password"===e&&($(".more",this.$el).hide(),$(".show_more",this.$el).hide(),$("input.password",this.$el).select()),this.state=e},infoBoxShow:function(){var e=this.$el.find(".side_panel");e.is(":visible")&&this.$el.animate({width:parseInt(e.css("left"),10)+e.find(".content:first").outerWidth()})},infoBoxHide:function(){var e=this.$el.find(".side_panel");this.$el.animate({width:parseInt(e.css("left"),10)})},infoBoxSet:function(e){this.$el.find(".side_panel .content").empty().append(e)},setStatus:function(e,t){$(".status",this.$el).text(e).attr("class","status").addClass(t||"").show()},clearStatus:function(){$(".status",this.$el).hide()},reset:function(){this.populateFields(),this.clearStatus(),this.$("button").attr("disabled",null)},newNetwork:function(e){this.model.current_connecting_network=e},networkConnected:function(e){this.model.trigger("connected",m.app.connections.getByConnectionId(e.server)),this.model.current_connecting_network=null},networkDisconnected:function(){this.model.current_connecting_network=null,this.state="all"},networkConnecting:function(){this.model.trigger("connecting"),this.setStatus(m.global.i18n.translate("client_views_serverselect_connection_trying").fetch(),"ok"),this.$(".status").append('<a class="show_server"><i class="fa fa-info-circle"></i></a>')},showServer:function(){this.model.current_connecting_network&&(m.app.view.barsShow(),this.model.current_connecting_network.panels.server.view.show())},onIrcError:function(e){switch($("button",this.$el).attr("disabled",null),e.error){case"nickname_in_use":this.setStatus(m.global.i18n.translate("client_views_serverselect_nickname_error_alreadyinuse").fetch()),this.show("nick_change"),this.$el.find(".nick").select();break;case"erroneus_nickname":this.setStatus(e.reason?e.reason:m.global.i18n.translate("client_views_serverselect_nickname_invalid").fetch()),this.show("nick_change"),this.$el.find(".nick").select();break;case"password_mismatch":this.setStatus(m.global.i18n.translate("client_views_serverselect_password_incorrect").fetch()),this.show("enter_password"),this.$el.find(".password").select();break;default:this.showError(e.reason||"")}},showError:function(e){var t=m.global.i18n.translate("client_views_serverselect_connection_error").fetch();if(e)switch(e){case"ENOTFOUND":t=m.global.i18n.translate("client_views_serverselect_server_notfound").fetch();break;case"ECONNREFUSED":t+=" ("+m.global.i18n.translate("client_views_serverselect_connection_refused").fetch()+")";break;default:t+=" ("+e+")"}this.setStatus(t,"error"),$("button",this.$el).attr("disabled",null),this.show()}}),m.view.StatusMessage=Backbone.View.extend({initialize:function(){this.$el.hide(),this.tmr=null},text:function(e,t){t=t||{},t.type=t.type||"",t.timeout=t.timeout||5e3,this.$el.text(e).addClass(t.type),this.$el.slideDown($.proxy(m.app.view.doLayout,m.app.view)),t.timeout&&this.doTimeout(t.timeout)},html:function(e,t){t=t||{},t.type=t.type||"",t.timeout=t.timeout||5e3,this.$el.html(e).addClass(t.type),this.$el.slideDown($.proxy(m.app.view.doLayout,m.app.view)),t.timeout&&this.doTimeout(t.timeout)},hide:function(){this.$el.slideUp($.proxy(m.app.view.doLayout,m.app.view))},doTimeout:function(e){this.tmr&&clearTimeout(this.tmr);var t=this;this.tmr=setTimeout(function(){t.hide()},e)}}),m.view.Tabs=Backbone.View.extend({tagName:"ul",className:"panellist",events:{"click li":"tabClick","click li .part":"partClick"},initialize:function(){this.model.on("add",this.panelAdded,this),this.model.on("remove",this.panelRemoved,this),this.model.on("reset",this.render,this),this.model.on("active",this.panelActive,this),this.is_network=!1,this.model.network&&(this.is_network=!0,this.model.network.on("change:name",function(e,t){$("span",this.model.server.tab).text(t)},this),this.model.network.on("change:connection_id",function(e,t){this.model.forEach(function(e){e.tab.data("connection_id",t)})},this))},render:function(){var e=this;this.$el.empty(),this.is_network&&this.model.server.tab.data("panel",this.model.server).data("connection_id",this.model.network.get("connection_id")).appendTo(this.$el),this.model.forEach(function(t){this.is_network&&t==e.model.server||(t.tab.data("panel",t),this.is_network&&t.tab.data("connection_id",this.model.network.get("connection_id")),t.tab.appendTo(e.$el))}),m.app.view.doLayout()},updateTabTitle:function(e,t){$("span",e.tab).text(t)},panelAdded:function(e){e.tab=$('<li><span></span><div class="activity"></div></li>'),e.tab.find("span").text(e.get("title")||e.get("name")),e.isServer()&&(e.tab.addClass("server"),e.tab.addClass("fa"),e.tab.addClass("fa-nonexistant")),e.tab.data("panel",e),this.is_network&&e.tab.data("connection_id",this.model.network.get("connection_id")),this.sortTabs(),e.bind("change:title",this.updateTabTitle),e.bind("change:name",this.updateTabTitle),m.app.view.doLayout()},panelRemoved:function(e){m.app.connections.active_connection;e.tab.remove(),delete e.tab,m.app.panels.trigger("remove",e),m.app.view.doLayout()},panelActive:function(e){m.app.view.$el.find(".panellist .part").remove(),m.app.view.$el.find(".panellist .active").removeClass("active"),e.tab.addClass("active"),e.tab.append('<span class="part fa fa-nonexistant"></span>')},tabClick:function(e){var t=$(e.currentTarget),n=t.data("panel");n&&n.view.show()},partClick:function(e){var t=$(e.currentTarget).parent(),n=t.data("panel");n&&(n.isChannel()&&n.get("members").models.length>0?this.model.network.gateway.part(n.get("name")):n.isServer()?(!this.model.network.get("connected")||confirm(d("disconnect_from_server")))&&(this.model.network.gateway.quit("Leaving"),m.app.connections.remove(this.model.network),m.app.startup_applet.view.show()):n.close())},sortTabs:function(){var e=this,t=[];this.model.forEach(function(n){e.is_network&&n==e.model.server||t.push([n.get("title")||n.get("name"),n])}),t.sort(function(e,t){return e[0].toLowerCase()>t[0].toLowerCase()?1:e[0].toLowerCase()<t[0].toLowerCase()?-1:0}),_.each(t,function(t){t[1].tab.appendTo(e.$el)})}}),m.view.TopicBar=Backbone.View.extend({events:{"keydown div":"process"},initialize:function(){m.app.panels.bind("active",function(e){e.isChannel()?(this.setCurrentTopicFromChannel(e),this.$el.find("div").attr("contentEditable",!0)):this.$el.find("div").attr("contentEditable",!1).text("")},this)},process:function(e){var t=$(e.currentTarget),n=t.text();return m.app.panels().active.isChannel()?13===e.keyCode?(m.app.connections.active_connection.gateway.topic(m.app.panels().active.get("name"),n),!1):void 0:!1},setCurrentTopic:function(e){e=e||"",$("div",this.$el).html(l(_.escape(e)))},setCurrentTopicFromChannel:function(e){var t=e.get("topic_set_by"),n="";this.setCurrentTopic(e.get("topic")),t?(n+=d("client_models_network_topic",[t.nick,m.utils.formatDate(t.when)]),this.$el.attr("title",n)):this.$el.attr("title","")}}),m.view.UserBox=Backbone.View.extend({events:{"click .query":"queryClick","click .info":"infoClick","change .ignore":"ignoreChange","click .ignore":"ignoreClick","click .op":"opClick","click .deop":"deopClick","click .voice":"voiceClick","click .devoice":"devoiceClick","click .kick":"kickClick","click .ban":"banClick"},initialize:function(){var e={op:m.global.i18n.translate("client_views_userbox_op").fetch(),de_op:m.global.i18n.translate("client_views_userbox_deop").fetch(),voice:m.global.i18n.translate("client_views_userbox_voice").fetch(),de_voice:m.global.i18n.translate("client_views_userbox_devoice").fetch(),kick:m.global.i18n.translate("client_views_userbox_kick").fetch(),ban:m.global.i18n.translate("client_views_userbox_ban").fetch(),message:m.global.i18n.translate("client_views_userbox_query").fetch(),info:m.global.i18n.translate("client_views_userbox_whois").fetch(),ignore:m.global.i18n.translate("client_views_userbox_ignore").fetch()};this.$el=$(_.template($("#tmpl_userbox").html().trim(),e))},setTargets:function(e,t){this.user=e,this.channel=t;var n=m.app.connections.active_connection.isNickIgnored(this.user.get("nick"));this.$(".ignore input").attr("checked",n?"checked":!1)},displayOpItems:function(e){e?this.$el.find(".if_op").css("display","block"):this.$el.find(".if_op").css("display","none")},queryClick:function(){var e=this.user.get("nick");m.app.connections.active_connection.createQuery(e)},infoClick:function(){m.app.controlbox.processInput("/whois "+this.user.get("nick"))},ignoreClick:function(e){e.stopPropagation()},ignoreChange:function(e){m.app.controlbox.processInput($(e.currentTarget).find("input").is(":checked")?"/ignore "+this.user.get("nick"):"/unignore "+this.user.get("nick"))},opClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" +o "+this.user.get("nick"))},deopClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" -o "+this.user.get("nick"))},voiceClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" +v "+this.user.get("nick"))},devoiceClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" -v "+this.user.get("nick"))},kickClick:function(){m.app.controlbox.processInput("/kick "+this.user.get("nick")+" Bye!")},banClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" +b "+this.user.get("nick")+"!*")}}),m.view.ChannelTools=Backbone.View.extend({events:{"click .channel_info":"infoClick","click .channel_part":"partClick"},initialize:function(){},infoClick:function(){new m.model.ChannelInfo({channel:m.app.panels().active})},partClick:function(){m.app.connections.active_connection.gateway.part(m.app.panels().active.get("name"))}}),m.view.ChannelInfo=Backbone.View.extend({events:{"click .toggle_banlist":"toggleBanList","change .channel-mode":"onModeChange","click .remove-ban":"onRemoveBanClick"},initialize:function(){var e,t=this.model.get("channel");e={moderated_chat:d("client_views_channelinfo_moderated"),invite_only:d("client_views_channelinfo_inviteonly"),ops_change_topic:d("client_views_channelinfo_opschangechannel"),external_messages:d("client_views_channelinfo_externalmessages"),toggle_banlist:d("client_views_channelinfo_togglebanlist"),channel_name:t.get("name")},this.$el=$(_.template($("#tmpl_channel_info").html().trim(),e)),this.menu=new m.view.MenuBox(t.get("name")),this.menu.addItem("channel_info",this.$el),this.menu.$el.appendTo(t.view.$container),this.menu.show(),this.menu.$el.offset({top:m.app.view.$el.find(".panels").offset().top}),this.$el.dispose=_.bind(this.dispose,this),this.updateInfo(t),t.on("change:info_modes change:info_url change:banlist",this.updateInfo,this),t.get("network").gateway.channelInfo(t.get("name"))},render:function(){},onModeChange:function(e){var t=$(e.currentTarget),n=this.model.get("channel"),i=t.data("mode"),s="";
+return"checkbox"==t.attr("type")?(s=t.is(":checked")?"+":"-",s+=i,void n.setMode(s)):"text"==t.attr("type")?(s=t.val()?"+"+i+" "+t.val():"-"+i,void n.setMode(s)):void 0},onRemoveBanClick:function(e){e.preventDefault(),e.stopPropagation();var t=$(e.currentTarget),n=t.parents("tr:first"),i=n.data("ban");if(i){var s=this.model.get("channel");s.setMode("-b "+i.banned),n.remove()}},updateInfo:function(e){var t,n,i,s=this;if(t=e.get("info_modes"),t&&_.each(t,function(e){e.mode=e.mode.toLowerCase(),"+k"==e.mode?s.$el.find('[name="channel_key"]').val(e.param):"+m"==e.mode?s.$el.find('[name="channel_mute"]').attr("checked","checked"):"+i"==e.mode?s.$el.find('[name="channel_invite"]').attr("checked","checked"):"+n"==e.mode?s.$el.find('[name="channel_external_messages"]').attr("checked","checked"):"+t"==e.mode&&s.$el.find('[name="channel_topic"]').attr("checked","checked")}),n=e.get("info_url"),n&&(this.$el.find(".channel_url").text(n).attr("href",n),this.$el.find(".channel_url").slideDown()),i=e.get("banlist"),i&&i.length){var a=this.$el.find(".channel-banlist table tbody");this.$el.find(".banlist-status").text(""),a.empty(),_.each(i,function(e){var t=$("<tr></tr>").data("ban",e);$("<td></td>").text(e.banned).appendTo(t),$("<td></td>").text(e.banned_by.split(/[!@]/)[0]).appendTo(t),$("<td></td>").text(m.utils.formatDate(new Date(1e3*parseInt(e.banned_at,10)))).appendTo(t),$('<td><i class="fa fa-rtimes remove-ban"></i></td>').appendTo(t),a.append(t)}),this.$el.find(".channel-banlist table").slideDown()}else this.$el.find(".banlist-status").text("Banlist empty"),this.$el.find(".channel-banlist table").hide()},toggleBanList:function(e){if(e.preventDefault(),this.$el.find(".channel-banlist table").toggle(),this.$el.find(".channel-banlist table").is(":visible")){var t=this.model.get("channel"),n=t.get("network");n.gateway.raw("MODE "+t.get("name")+" +b")}},dispose:function(){this.model.get("channel").off("change:info_modes change:info_url change:banlist",this.updateInfo,this),this.$el.remove()}}),m.view.RightBar=Backbone.View.extend({events:{"click .right-bar-toggle":"onClickToggle","click .right-bar-toggle-inner":"onClickToggle"},initialize:function(){this.keep_hidden=!1,this.hidden=this.$el.hasClass("disabled"),this.updateIcon()},hide:function(){this.hidden=!0,this.$el.addClass("disabled"),this.updateIcon()},show:function(){this.hidden=!1,this.keep_hidden||this.$el.removeClass("disabled"),this.updateIcon()},toggle:function(e){return this.ignore_layout?!0:(this.keep_hidden="undefined"==typeof e?!this.keep_hidden:e,this.keep_hidden||this.hidden?this.$el.addClass("disabled"):this.$el.removeClass("disabled"),void this.updateIcon())},updateIcon:function(){var e=this.$(".right-bar-toggle"),t=e.find("i");!this.hidden&&this.keep_hidden?e.show():e.hide(),this.keep_hidden?t.removeClass("fa fa-angle-double-right").addClass("fa fa-users"):t.removeClass("fa fa-users").addClass("fa fa-angle-double-right")},onClickToggle:function(){this.toggle(),this.ignore_layout=!0,m.app.view.doLayout(),delete this.ignore_layout}}),m.view.Notification=Backbone.View.extend({className:"notification",events:{"click .close":"close"},initialize:function(e,t){this.title=e,this.content=t},render:function(){return this.$el.html($("#tmpl_notifications").html()),this.$("h6").text(this.title),"string"==typeof this.content?this.$(".content").html(this.content):"object"==typeof this.content&&this.$(".content").empty().append(this.content),this},show:function(){var e=this;this.render().$el.appendTo(m.app.view.$el),_.defer(function(){e.$el.addClass("show")})},close:function(){this.remove()}}),function(){function e(e,t){this.app=e,this.controlbox=t,this.addDefaultAliases(),this.bindCommand(L)}function n(e){var t=e.command+" "+e.params.join(" ");this.app.connections.active_connection.gateway.raw(t)}function i(){}function s(e){var t,n;n=e.params.join(" ").split(","),t=this.app.connections.active_connection.createAndJoinChannels(n),t.length&&t[t.length-1].view.show()}function a(e){var t,n,i;t=e.params[0],e.params.shift(),n=e.params.join(" "),i=this.app.connections.active_connection.panels.getByName(t),i||(i=new m.model.Query({name:t}),this.app.connections.active_connection.panels.add(i)),i&&i.view.show(),n&&(this.app.connections.active_connection.gateway.msg(i.get("name"),n),i.addMsg(this.app.connections.active_connection.get("nick"),u("privmsg",{text:n}),"privmsg"))}function o(e){var t,n=e.params[0],i=this.app.connections.active_connection.panels.getByName(n)||this.app.panels().server;e.params.shift(),t=e.params.join(" "),i.addMsg(this.app.connections.active_connection.get("nick"),u("privmsg",{text:t}),"privmsg"),this.app.connections.active_connection.gateway.msg(n,t)}function c(e){if(!this.app.panels().active.isServer()){var t=this.app.panels().active;t.addMsg("",u("action",{nick:this.app.connections.active_connection.get("nick"),text:e.params.join(" ")}),"action"),this.app.connections.active_connection.gateway.action(t.get("name"),e.params.join(" "))}}function l(e){var t,n,i=this;0===e.params.length?this.app.connections.active_connection.gateway.part(this.app.panels().active.get("name")):(t=e.params[0].split(","),n=e.params[1],_.each(t,function(e){i.connections.active_connection.gateway.part(e,n)}))}function r(e){var t,n=this;t=0===e.params.length?this.app.panels().active.get("name"):e.params[0],this.app.connections.active_connection.gateway.part(t),setTimeout(function(){n.app.connections.active_connection.createAndJoinChannels(t),n.app.connections.active_connection.panels.getByName(t).show()},1e3)}function h(e){this.app.connections.active_connection.gateway.changeNick(e.params[0])}function p(e){var t;0!==e.params.length&&(this.app.connections.active_connection.isChannelName(e.params[0])?(t=e.params[0],e.params.shift()):t=this.app.panels().active.get("name"),this.app.connections.active_connection.gateway.topic(t,e.params.join(" ")))}function g(e){var t;e.params.length<=1||(t=e.params[0],e.params.shift(),this.app.connections.active_connection.gateway.notice(t,e.params.join(" ")))}function f(e){var t=e.params.join(" ");this.app.connections.active_connection.gateway.raw(t)}function v(e){var t,n=this.app.panels().active;n.isChannel()&&0!==e.params.length&&(t=e.params[0],e.params.shift(),this.app.connections.active_connection.gateway.kick(n.get("name"),t,e.params.join(" ")))}function w(){this.app.panels().active.isServer()||this.app.panels().active.isApplet()||this.app.panels().active.clearMessages&&this.app.panels().active.clearMessages()}function k(e){var t,n;e.params.length<2||(t=e.params[0],e.params.shift(),n=e.params[0],e.params.shift(),this.app.connections.active_connection.gateway.ctcpRequest(n,t,e.params.join(" ")))}function b(){var e=m.model.Applet.loadOnce("kiwi_settings");e.view.show()}function y(){var e=m.model.Applet.loadOnce("kiwi_script_editor");e.view.show()}function C(e){if(e.params[0]){var t=new m.model.Applet;if(e.params[1])t.load(e.params[0],e.params[1]);else{if(!this.applets[e.params[0]])return void this.app.panels().server.addMsg("",u("applet_notfound",{text:d("client_models_application_applet_notfound",[e.params[0]])}));t.load(new this.applets[e.params[0]])}this.app.connections.active_connection.panels.add(t),t.view.show()}}function x(e){var t,n;e.params[0]&&this.app.panels().active.isChannel()&&(t=e.params[0],n=this.app.panels().active.get("name"),this.app.connections.active_connection.gateway.raw("INVITE "+t+" "+n),this.app.panels().active.addMsg("",u("channel_has_been_invited",{nick:t,text:d("client_models_application_has_been_invited",[n])}),"action"))}function M(e){var t;e.params[0]?t=e.params[0]:this.app.panels().active.isQuery()&&(t=this.app.panels().active.get("name")),t&&this.app.connections.active_connection.gateway.raw("WHOIS "+t+" "+t)}function S(e){var t;e.params[0]?t=e.params[0]:this.app.panels().active.isQuery()&&(t=this.app.panels().active.get("name")),t&&this.app.connections.active_connection.gateway.raw("WHOWAS "+t)}function T(e){this.app.connections.active_connection.gateway.raw("AWAY :"+e.params.join(" "))}function N(e){var t=this;e.params[0]?m.gateway.setEncoding(null,e.params[0],function(n){n?t.app.panels().active.addMsg("",u("encoding_changed",{text:d("client_models_application_encoding_changed",[e.params[0]])})):t.app.panels().active.addMsg("",u("encoding_invalid",{text:d("client_models_application_encoding_invalid",[e.params[0]])}))}):(this.app.panels().active.addMsg("",u("client_models_application_encoding_notspecified",{text:d("client_models_application_encoding_notspecified")})),this.app.panels().active.addMsg("",u("client_models_application_encoding_usage",{text:d("client_models_application_encoding_usage")})))}function B(){var e=this.app.panels().active;e.isChannel()&&new m.model.ChannelInfo({channel:this.app.panels().active})}function D(e){var t=this.app.connections.active_connection;t&&t.gateway.quit(e.params.join(" "))}function I(e){var n,i,s,a,o,c,l=this;return e.params[0]?(e.params[0].indexOf(":")>0?(c=e.params[0].split(":"),n=c[0],i=c[1],a=e.params[1]||t):(n=e.params[0],i=e.params[1]||6667,a=e.params[2]||t),"+"===i.toString()[0]?(s=!0,i=parseInt(i.substring(1),10)):s=!1,i=i||6667,o=this.app.connections.active_connection.get("nick"),this.app.panels().active.addMsg("",u("server_connecting",{text:d("client_models_application_connection_connecting",[n,i.toString()])})),void m.gateway.newConnection({nick:o,host:n,port:i,ssl:s,password:a},function(e){var t;e&&(t=d("client_models_application_connection_error",[n,i.toString(),e.toString()]),l.app.panels().active.addMsg("",u("server_connecting_error",{text:t})))})):(c=new m.view.MenuBox(m.global.i18n.translate("client_models_application_connection_create").fetch()),c.addItem("new_connection",(new m.model.NewConnection).view.$el),c.show(),void c.$el.offset({top:this.app.view.$el.height()/2-c.$el.height()/2,left:this.app.view.$el.width()/2-c.$el.width()/2}))}m.misc.ClientUiCommands=e,e.prototype.addDefaultAliases=function(){$.extend(this.controlbox.preprocessor.aliases,{"/p":"/part $1+","/me":"/action $1+","/j":"/join $1+","/q":"/query $1+","/w":"/whois $1+","/raw":"/quote $1+","/connect":"/server $1+","/op":"/quote mode $channel +o $1+","/deop":"/quote mode $channel -o $1+","/hop":"/quote mode $channel +h $1+","/dehop":"/quote mode $channel -h $1+","/voice":"/quote mode $channel +v $1+","/devoice":"/quote mode $channel -v $1+","/k":"/kick $channel $1+","/ban":"/quote mode $channel +b $1+","/unban":"/quote mode $channel -b $1+","/slap":"/me slaps $1 around a bit with a large trout","/tick":"/msg $channel âœ”"})},e.prototype.bindCommand=function(e){var t=this;_.each(e,function(e,n){t.controlbox.on(n,_.bind(e,t))})};var L={unknown_command:n,command:i,"command:msg":o,"command:action":c,"command:join":s,"command:part":l,"command:cycle":r,"command:nick":h,"command:query":a,"command:invite":x,"command:topic":p,"command:notice":g,"command:quote":f,"command:kick":v,"command:clear":w,"command:ctcp":k,"command:quit":D,"command:server":I,"command:whois":M,"command:whowas":S,"command:away":T,"command:encoding":N,"command:channel":B,"command:applet":C,"command:settings":b,"command:script":y};L["command:css"]=function(){var e="?reload="+(new Date).getTime();$('link[rel="stylesheet"]').each(function(){this.href=this.href.replace(/\?.*|$/,e)})},L["command:js"]=function(e){e.params[0]&&$script(e.params[0]+"?"+(new Date).getTime())},L["command:set"]=function(e){if(e.params[0]){var t,n=e.params[0];e.params[1]&&(e.params.shift(),t=e.params.join(" "),"true"===t&&(t=!0),"false"===t&&(t=!1),parseInt(t,10).toString()===t&&(t=parseInt(t,10)),m.global.settings.set(n,t)),this.app.panels().active.addMsg("",u("set_setting",{text:n+" = "+m.global.settings.get(n).toString()}))}},L["command:save"]=function(){m.global.settings.save(),this.app.panels().active.addMsg("",u("settings_saved",{text:d("client_models_application_settings_saved")}))},L["command:alias"]=function(e){var t,n,i=this;return e.params[1]?"del"===e.params[0]||"delete"===e.params[0]?(t=e.params[1],"/"!==t[0]&&(t="/"+t),void delete this.controlbox.preprocessor.aliases[t]):(t=e.params[0],e.params.shift(),n=e.params.join(" "),"/"!==t[0]&&(t="/"+t),void(this.controlbox.preprocessor.aliases[t]=n)):void $.each(this.controlbox.preprocessor.aliases,function(e,t){i.app.panels().server.addMsg(" ",u("list_aliases",{text:e+"   =>   "+t}))})},L["command:ignore"]=function(e){var t=this,n=this.app.connections.active_connection.get("ignore_list");return e.params[0]?(n.push(e.params[0]),this.app.connections.active_connection.set("ignore_list",n),void this.app.panels().active.addMsg(" ",u("ignore_nick",{text:d("client_models_application_ignore_nick",[e.params[0]])}))):void(n.length>0?(this.app.panels().active.addMsg(" ",u("ignore_title",{text:d("client_models_application_ignore_title")})),$.each(n,function(e,n){t.app.panels().active.addMsg(" ",u("ignored_pattern",{text:n}))})):this.app.panels().active.addMsg(" ",u("ignore_none",{text:d("client_models_application_ignore_none")})))},L["command:unignore"]=function(e){var t=this.app.connections.active_connection.get("ignore_list");return e.params[0]?(t=_.reject(t,function(t){return t===e.params[0]}),this.app.connections.active_connection.set("ignore_list",t),void this.app.panels().active.addMsg(" ",u("ignore_stopped",{text:d("client_models_application_ignore_stopped",[e.params[0]])}))):void this.app.panels().active.addMsg(" ",u("ignore_stop_notice",{text:d("client_models_application_ignore_stop_notice")}))}}(),function(){var e=Backbone.View.extend({events:{"change [data-setting]":"saveSettings",'click [data-setting="theme"]':"selectTheme","click .register_protocol":"registerProtocol","click .enable_notifications":"enableNotifications"},initialize:function(){var e={tabs:d("client_applets_settings_channelview_tabs"),list:d("client_applets_settings_channelview_list"),large_amounts_of_chans:d("client_applets_settings_channelview_list_notice"),join_part:d("client_applets_settings_notification_joinpart"),count_all_activity:d("client_applets_settings_notification_count_all_activity"),timestamps:d("client_applets_settings_timestamp"),timestamp_24:d("client_applets_settings_timestamp_24_hour"),mute:d("client_applets_settings_notification_sound"),emoticons:d("client_applets_settings_emoticons"),scroll_history:d("client_applets_settings_history_length"),languages:m.app.translations,default_client:d("client_applets_settings_default_client"),make_default:d("client_applets_settings_default_client_enable"),locale_restart_needed:d("client_applets_settings_locale_restart_needed"),default_note:d("client_applets_settings_default_client_notice",'<a href="chrome://settings/handlers">chrome://settings/handlers</a>'),html5_notifications:d("client_applets_settings_html5_notifications"),enable_notifications:d("client_applets_settings_enable_notifications"),theme_thumbnails:_.map(m.app.themes,function(e){return _.template($("#tmpl_theme_thumbnail").html().trim(),e)})};this.$el=$(_.template($("#tmpl_applet_settings").html().trim(),e)),navigator.registerProtocolHandler||this.$(".protocol_handler").remove(),null!==m.utils.notifications.allowed()&&this.$(".notification_enabler").remove(),m.global.settings.on("change",this.loadSettings,this),this.loadSettings()},loadSettings:function(){_.each(m.global.settings.attributes,function(e,t){var n=this.$('[data-setting="'+t+'"]');if(n.length)switch(n.prop("type")){case"checkbox":n.prop("checked",e);break;case"radio":this.$('[data-setting="'+t+'"][value="'+e+'"]').prop("checked",!0);break;case"text":n.val(e);break;case"select-one":this.$('[value="'+e+'"]').prop("selected",!0);break;default:this.$('[data-setting="'+t+'"][data-value="'+e+'"]').addClass("active")}},this)},saveSettings:function(e){var t,n=m.global.settings,i=$(e.currentTarget);switch(e.currentTarget.type){case"checkbox":t=i.is(":checked");break;case"radio":case"text":t=i.val();break;case"select-one":t=$(e.currentTarget[i.prop("selectedIndex")]).val();break;default:t=i.data("value")}m.global.settings.off("change",this.loadSettings,this),n.set(i.data("setting"),t),n.save(),m.global.settings.on("change",this.loadSettings,this)},selectTheme:function(e){e.preventDefault(),this.$('[data-setting="theme"].active').removeClass("active"),$(e.currentTarget).addClass("active").trigger("change")},registerProtocol:function(e){e.preventDefault(),navigator.registerProtocolHandler("irc",document.location.origin+m.app.get("base_path")+"/%s","Kiwi IRC"),navigator.registerProtocolHandler("ircs",document.location.origin+m.app.get("base_path")+"/%s","Kiwi IRC")},enableNotifications:function(e){e.preventDefault();var t=m.utils.notifications;t.requestPermission().always(_.bind(function(){null!==t.allowed()&&this.$(".notification_enabler").remove()},this))}}),t=Backbone.Model.extend({initialize:function(){this.set("title",d("client_applets_settings_title")),this.view=new e}});m.model.Applet.register("kiwi_settings",t)}(),function(){var e=Backbone.View.extend({events:{"click .chan":"chanClick","click .channel_name_title":"sortChannelsByNameClick","click .users_title":"sortChannelsByUsersClick"},initialize:function(){var e={channel_name:m.global.i18n.translate("client_applets_chanlist_channelname").fetch(),users:m.global.i18n.translate("client_applets_chanlist_users").fetch(),topic:m.global.i18n.translate("client_applets_chanlist_topic").fetch()};this.$el=$(_.template($("#tmpl_channel_list").html().trim(),e)),this.channels=[],this.order="",this.waiting=!1},render:function(){var e,t=$("table",this.$el),n=t.children("tbody:first").detach();switch(0==$(".applet_chanlist .users_title").find("span.chanlist_sort_users").length?this.$(".users_title").append('<span class="chanlist_sort_users">&nbsp;&nbsp;</span>'):(this.$(".users_title span.chanlist_sort_users").removeClass("fa fa-sort-desc"),this.$(".users_title span.chanlist_sort_users").removeClass("fa fa-sort-asc")),0==$(".applet_chanlist .channel_name_title").find("span.chanlist_sort_names").length?this.$(".channel_name_title").append('<span class="chanlist_sort_names">&nbsp;&nbsp;</span>'):(this.$(".channel_name_title span.chanlist_sort_names").removeClass("fa fa-sort-desc"),this.$(".channel_name_title span.chanlist_sort_names").removeClass("fa fa-sort-asc")),this.order){case"user_desc":default:this.$(".users_title span.chanlist_sort_users").addClass("fa fa-sort-asc");break;case"user_asc":this.$(".users_title span.chanlist_sort_users").addClass("fa fa-sort-desc");break;case"name_asc":this.$(".channel_name_title span.chanlist_sort_names").addClass("fa fa-sort-desc");break;case"name_desc":this.$(".channel_name_title span.chanlist_sort_names").addClass("fa fa-sort-asc")}for(this.channels=this.sortChannels(this.channels,this.order),e=0;e<this.channels.length;e++)n[0].appendChild(this.channels[e].dom);t[0].appendChild(n[0])},chanClick:function(e){e.target?m.gateway.join(null,$(e.target).data("channel")):m.gateway.join(null,$(e.srcElement).data("channel"))},sortChannelsByNameClick:function(){this.order="name_asc"==this.order?"name_desc":"name_asc",this.sortChannelsClick()},sortChannelsByUsersClick:function(){this.order="user_desc"==this.order||""==this.order?"user_asc":"user_desc",this.sortChannelsClick()},sortChannelsClick:function(){this.render()},sortChannels:function(e,t){var n=[],i=[];return _.each(e,function(e,t){n.push({chan_idx:t,num_users:e.num_users,channel:e.channel})}),n.sort(function(e,n){switch(t){case"user_asc":return e.num_users-n.num_users;case"user_desc":return n.num_users-e.num_users;case"name_asc":if(e.channel.toLowerCase()>n.channel.toLowerCase())return 1;if(e.channel.toLowerCase()<n.channel.toLowerCase())return-1;case"name_desc":if(e.channel.toLowerCase()<n.channel.toLowerCase())return 1;if(e.channel.toLowerCase()>n.channel.toLowerCase())return-1;default:return n.num_users-e.num_users}return 0}),_.each(n,function(t){i.push(e[t.chan_idx])}),i}}),t=Backbone.Model.extend({initialize:function(){this.set("title",m.global.i18n.translate("client_applets_chanlist_channellist").fetch()),this.view=new e,this.network=m.global.components.Network(),this.network.on("list_channel",this.onListChannel,this),this.network.on("list_start",this.onListStart,this)},onListChannel:function(e){this.addChannel(e.chans)},onListStart:function(){},addChannel:function(e){var t=this;_.isArray(e)||(e=[e]),_.each(e,function(e){var n;n=document.createElement("tr"),n.innerHTML='<td class="chanlist_name"><a class="chan" data-channel="'+e.channel+'">'+_.escape(e.channel)+'</a></td><td class="chanlist_num_users" style="text-align: center;">'+e.num_users+'</td><td style="padding-left: 2em;" class="chanlist_topic">'+l(_.escape(e.topic))+"</td>",e.dom=n,t.view.channels.push(e)}),t.view.waiting||(t.view.waiting=!0,_.defer(function(){t.view.render(),t.view.waiting=!1}))},dispose:function(){this.view.channels=null,this.view.unbind(),this.view.$el.html(""),this.view.remove(),this.view=null,this.network.off()}});m.model.Applet.register("kiwi_chanlist",t)}(),function(){var e=Backbone.View.extend({events:{"click .btn_save":"onSave"},initialize:function(){var e=this,t={save:m.global.i18n.translate("client_applets_scripteditor_save").fetch()};this.$el=$(_.template($("#tmpl_script_editor").html().trim(),t)),this.model.on("applet_loaded",function(){e.$el.parent().css("height","100%"),$script(m.app.get("base_path")+"/assets/libs/ace/ace.js",function(){e.createAce()})})},createAce:function(){var e="editor_"+Math.floor(1e7*Math.random()).toString();this.editor_id=e,this.$el.find(".editor").attr("id",e),this.editor=ace.edit(e),this.editor.setTheme("ace/theme/monokai"),this.editor.getSession().setMode("ace/mode/javascript");var t=m.global.settings.get("user_script")||"";this.editor.setValue(t)},onSave:function(){var e,t;e="var network = kiwi.components.Network();\n",e+="var input = kiwi.components.ControlInput();\n",e+="var events = kiwi.components.Events();\n",e+=this.editor.getValue()+"\n",e+="this._dispose = function(){ network.off(); input.off(); events.dispose(); if(this.dispose) this.dispose(); }";try{t=new Function(e),m.user_script&&m.user_script._dispose&&m.user_script._dispose(),m.user_script=new t}catch(n){return void this.setStatus(m.global.i18n.translate("client_applets_scripteditor_error").fetch(n.toString()))}m.global.settings.set("user_script",this.editor.getValue()),m.global.settings.save(),this.setStatus(m.global.i18n.translate("client_applets_scripteditor_saved").fetch()+" :)")},setStatus:function(e){var t=this.$el.find(".toolbar .status");e=e||"",t.slideUp("fast",function(){t.text(e),t.slideDown()})}}),t=Backbone.Model.extend({initialize:function(){this.set("title",m.global.i18n.translate("client_applets_scripteditor_title").fetch()),this.view=new e({model:this})}});m.model.Applet.register("kiwi_script_editor",t)}(),function(){var e=Backbone.View.extend({events:{},initialize:function(){this.showConnectionDialog()},showConnectionDialog:function(){var e=this.connection_dialog=new m.model.NewConnection;e.populateDefaultServerSettings(),e.view.$el.addClass("initial"),this.$el.append(e.view.$el);var t=$($("#tmpl_new_connection_info").html().trim());t.html()?e.view.infoBoxSet(t):t=null,this.listenTo(e,"connected",this.newConnectionConnected),_.defer(function(){t&&e.view.infoBoxShow(),window==window.top&&e.view.$el.find(".nick").select()})},newConnectionConnected:function(){this.connection_dialog.view.reset()}}),t=Backbone.Model.extend({initialize:function(){this.view=new e({model:this})}});m.model.Applet.register("kiwi_startup",t)}(),m.utils.notifications=function(){function e(e,n){t.call(this,e,n)}function t(e,i){switch(n.allowed()){case!0:this.notification=new Notification(e,i),_.each(["click","close","error","show"],function(e){this.notification["on"+e]=_.bind(this.trigger,this,e)},this);break;case null:n.requestPermission().done(_.bind(t,this,e,i))}}if(!window.Notification)return{allowed:_.constant(!1),requestPermission:_.constant($.Deferred().reject())};var n={allowed:function(){return"granted"===Notification.permission?!0:"denied"===Notification.permission?!1:null},requestPermission:function(){var e=$.Deferred();return Notification.requestPermission(function(t){e["granted"===t?"resolve":"reject"]()}),e.promise()},create:function(t,n){return new e(t,n)}};return _.extend(e.prototype,Backbone.Events,{closed:!1,_closeTimeout:null,closeAfter:function(e){return this.closed||(this.notification?this._closeTimeout=this._closeTimeout||setTimeout(_.bind(this.close,this),e):this.once("show",_.bind(this.closeAfter,this,e))),this},close:function(){return this.notification&&!this.closed&&(this.notification.close(),this.closed=!0),this}}),n}(),m.utils.formatDate=function(){var e,t,n,i,s=!1,a={d:function(){return(this.getDate()<10?"0":"")+this.getDate()},D:function(){return Date.shortDays[this.getDay()]},j:function(){return this.getDate()},l:function(){return Date.longDays[this.getDay()]},N:function(){return this.getDay()+1},S:function(){return this.getDate()%10==1&&11!=this.getDate()?"st":this.getDate()%10==2&&12!=this.getDate()?"nd":this.getDate()%10==3&&13!=this.getDate()?"rd":"th"},w:function(){return this.getDay()},z:function(){var e=new Date(this.getFullYear(),0,1);return Math.ceil((this-e)/864e5)},W:function(){var e=new Date(this.getFullYear(),0,1);return Math.ceil(((this-e)/864e5+e.getDay()+1)/7)},F:function(){return Date.longMonths[this.getMonth()]},m:function(){return(this.getMonth()<9?"0":"")+(this.getMonth()+1)},M:function(){return Date.shortMonths[this.getMonth()]},n:function(){return this.getMonth()+1},t:function(){var e=new Date;return new Date(e.getFullYear(),e.getMonth(),0).getDate()},L:function(){var e=this.getFullYear();return e%400==0||e%100!=0&&e%4==0},o:function(){var e=new Date(this.valueOf());return e.setDate(e.getDate()-(this.getDay()+6)%7+3),e.getFullYear()},Y:function(){return this.getFullYear()},y:function(){return(""+this.getFullYear()).substr(2)},a:function(){return this.getHours()<12?"am":"pm"},A:function(){return this.getHours()<12?"AM":"PM"},B:function(){return Math.floor(1e3*((this.getUTCHours()+1)%24+this.getUTCMinutes()/60+this.getUTCSeconds()/3600)/24)},g:function(){return this.getHours()%12||12},G:function(){return this.getHours()},h:function(){return((this.getHours()%12||12)<10?"0":"")+(this.getHours()%12||12)},H:function(){return(this.getHours()<10?"0":"")+this.getHours()},i:function(){return(this.getMinutes()<10?"0":"")+this.getMinutes()},s:function(){return(this.getSeconds()<10?"0":"")+this.getSeconds()},u:function(){var e=this.getMilliseconds();return(10>e?"00":100>e?"0":"")+e},e:function(){return"Not Yet Supported"},I:function(){for(var e=null,t=0;12>t;++t){var n=new Date(this.getFullYear(),t,1),i=n.getTimezoneOffset();if(null===e)e=i;else{if(e>i){e=i;break}if(i>e)break}}return this.getTimezoneOffset()==e|0},O:function(){return(-this.getTimezoneOffset()<0?"-":"+")+(Math.abs(this.getTimezoneOffset()/60)<10?"0":"")+Math.abs(this.getTimezoneOffset()/60)+"00"},P:function(){return(-this.getTimezoneOffset()<0?"-":"+")+(Math.abs(this.getTimezoneOffset()/60)<10?"0":"")+Math.abs(this.getTimezoneOffset()/60)+":00"},T:function(){var e=this.getMonth();this.setMonth(0);var t=this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/,"$1");return this.setMonth(e),t},Z:function(){return 60*-this.getTimezoneOffset()},c:function(){return this.format("Y-m-d\\TH:i:sP")},r:function(){return this.toString()},U:function(){return this.getTime()/1e3}},o=function(){e=[m.global.i18n.translate("client.libs.date_format.short_months.january").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.february").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.march").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.april").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.may").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.june").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.july").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.august").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.september").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.october").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.november").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.december").fetch()],t=[m.global.i18n.translate("client.libs.date_format.long_months.january").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.february").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.march").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.april").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.may").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.june").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.july").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.august").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.september").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.october").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.november").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.december").fetch()],n=[m.global.i18n.translate("client.libs.date_format.short_days.monday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.tuesday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.wednesday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.thursday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.friday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.saturday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.sunday").fetch()],i=[m.global.i18n.translate("client.libs.date_format.long_days.monday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.tuesday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.wednesday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.thursday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.friday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.saturday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.sunday").fetch()],s=!0};return function(e,t){return s||o(),e=e||new Date,t=t||m.global.i18n.translate("client_date_format").fetch(),t.replace(/(\\?)(.)/g,function(t,n,i){return""===n&&a[i]?a[i].call(e):i})}}(),n.prototype.on=function(e,t,n){this._listeners[e]=this._listeners[e]||[],this._listeners[e].push(["on",t,n])},n.prototype.once=function(e,t,n){this._listeners[e]=this._listeners[e]||[],this._listeners[e].push(["once",t,n])},n.prototype.off=function(e,t,n){var i;if("undefined"==typeof e)this._listeners={};else if("undefined"==typeof t)delete this._listeners[e];else if("undefined"==typeof n)for(i in this._listeners[e]||[])this._listeners[e][i][1]===t&&delete this._listeners[e][i];else for(i in this._listeners[e]||[])this._listeners[e][i][1]===t&&this._listeners[e][i][2]===n&&delete this._listeners[e][i]},n.prototype.getListeners=function(e){return this._listeners[e]||[]},n.prototype.createProxy=function(){var e=new n;return e._parent=this._parent||this,e._parent._children.push(e),e},n.prototype.dispose=function(){if(this.off(),this._parent){var e=this._parent._children.indexOf(this);e>-1&&this._parent._children.splice(e,1)}},n.prototype.emit=function(e,n){var i,s=new this.EmitCall(e,n),a=[];for(i=this._children.length-1;i>=0;i--)a=a.concat(this._children[i].getListeners(e));return a=a.concat(this.getListeners(e)),s.then(function(){var e,n=a.length;for(e=0;n>e;e++)"once"===a[e][0]&&(a[e]=t)}),s.callListeners(a),s},n.prototype.EmitCall=function(e,n){function i(e){function i(){var o,l;return a++,e[a]?(l={wait:!1,callback:function(){l.callback=t,i.apply(c)},preventDefault:function(){h=!0}},o=e[a],o[1].call(o[2]||c,l,n),void(l.wait||(l.callback=t,i()))):void s()
+}var a=-1;return n=n||t,0===e.length?void s():void i()}function s(){l=!0;var e=h?p:r;e=e||[];for(var t=0;t<e.length;t++)"function"==typeof e[t]&&e[t]()}function a(e){return"function"!=typeof e?!1:(r.push(e),l&&!h&&e(),this)}function o(e){return"function"!=typeof e?!1:(p.push(e),l&&h&&e(),this)}var c=this,l=!1,r=[],h=!1,p=[];return{callListeners:i,then:a,"catch":o}},"object"==typeof module&&"undefined"!=typeof module.exports&&(module.exports=n),"undefined"==typeof String.prototype.trim&&(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),"undefined"==typeof String.prototype.lpad&&(String.prototype.lpad=function(e,t){var n,i="";for(n=0;e>n;n++)i+=t;return(i+this).slice(-e)})}(window);
\ No newline at end of file
diff --git a/2016/assets/js/lodash.js b/2016/assets/js/lodash.js
new file mode 100644 (file)
index 0000000..cab4e79
--- /dev/null
@@ -0,0 +1,15073 @@
+/**
+ * @license
+ * lodash 4.6.1 (Custom Build) <https://lodash.com/>
+ * Build: `lodash -o ./dist/lodash.js`
+ * Copyright 2012-2016 The Dojo Foundation <http://dojofoundation.org/>
+ * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
+ * Copyright 2009-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ * Available under MIT license <https://lodash.com/license>
+ */
+;(function() {
+
+  /** Used as a safe reference for `undefined` in pre-ES5 environments. */
+  var undefined;
+
+  /** Used as the semantic version number. */
+  var VERSION = '4.6.1';
+
+  /** Used as the size to enable large array optimizations. */
+  var LARGE_ARRAY_SIZE = 200;
+
+  /** Used as the `TypeError` message for "Functions" methods. */
+  var FUNC_ERROR_TEXT = 'Expected a function';
+
+  /** Used to stand-in for `undefined` hash values. */
+  var HASH_UNDEFINED = '__lodash_hash_undefined__';
+
+  /** Used as the internal argument placeholder. */
+  var PLACEHOLDER = '__lodash_placeholder__';
+
+  /** Used to compose bitmasks for wrapper metadata. */
+  var BIND_FLAG = 1,
+      BIND_KEY_FLAG = 2,
+      CURRY_BOUND_FLAG = 4,
+      CURRY_FLAG = 8,
+      CURRY_RIGHT_FLAG = 16,
+      PARTIAL_FLAG = 32,
+      PARTIAL_RIGHT_FLAG = 64,
+      ARY_FLAG = 128,
+      REARG_FLAG = 256,
+      FLIP_FLAG = 512;
+
+  /** Used to compose bitmasks for comparison styles. */
+  var UNORDERED_COMPARE_FLAG = 1,
+      PARTIAL_COMPARE_FLAG = 2;
+
+  /** Used as default options for `_.truncate`. */
+  var DEFAULT_TRUNC_LENGTH = 30,
+      DEFAULT_TRUNC_OMISSION = '...';
+
+  /** Used to detect hot functions by number of calls within a span of milliseconds. */
+  var HOT_COUNT = 150,
+      HOT_SPAN = 16;
+
+  /** Used to indicate the type of lazy iteratees. */
+  var LAZY_FILTER_FLAG = 1,
+      LAZY_MAP_FLAG = 2,
+      LAZY_WHILE_FLAG = 3;
+
+  /** Used as references for various `Number` constants. */
+  var INFINITY = 1 / 0,
+      MAX_SAFE_INTEGER = 9007199254740991,
+      MAX_INTEGER = 1.7976931348623157e+308,
+      NAN = 0 / 0;
+
+  /** Used as references for the maximum length and index of an array. */
+  var MAX_ARRAY_LENGTH = 4294967295,
+      MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1,
+      HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1;
+
+  /** `Object#toString` result references. */
+  var argsTag = '[object Arguments]',
+      arrayTag = '[object Array]',
+      boolTag = '[object Boolean]',
+      dateTag = '[object Date]',
+      errorTag = '[object Error]',
+      funcTag = '[object Function]',
+      genTag = '[object GeneratorFunction]',
+      mapTag = '[object Map]',
+      numberTag = '[object Number]',
+      objectTag = '[object Object]',
+      regexpTag = '[object RegExp]',
+      setTag = '[object Set]',
+      stringTag = '[object String]',
+      symbolTag = '[object Symbol]',
+      weakMapTag = '[object WeakMap]',
+      weakSetTag = '[object WeakSet]';
+
+  var arrayBufferTag = '[object ArrayBuffer]',
+      float32Tag = '[object Float32Array]',
+      float64Tag = '[object Float64Array]',
+      int8Tag = '[object Int8Array]',
+      int16Tag = '[object Int16Array]',
+      int32Tag = '[object Int32Array]',
+      uint8Tag = '[object Uint8Array]',
+      uint8ClampedTag = '[object Uint8ClampedArray]',
+      uint16Tag = '[object Uint16Array]',
+      uint32Tag = '[object Uint32Array]';
+
+  /** Used to match empty string literals in compiled template source. */
+  var reEmptyStringLeading = /\b__p \+= '';/g,
+      reEmptyStringMiddle = /\b(__p \+=) '' \+/g,
+      reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g;
+
+  /** Used to match HTML entities and HTML characters. */
+  var reEscapedHtml = /&(?:amp|lt|gt|quot|#39|#96);/g,
+      reUnescapedHtml = /[&<>"'`]/g,
+      reHasEscapedHtml = RegExp(reEscapedHtml.source),
+      reHasUnescapedHtml = RegExp(reUnescapedHtml.source);
+
+  /** Used to match template delimiters. */
+  var reEscape = /<%-([\s\S]+?)%>/g,
+      reEvaluate = /<%([\s\S]+?)%>/g,
+      reInterpolate = /<%=([\s\S]+?)%>/g;
+
+  /** Used to match property names within property paths. */
+  var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,
+      reIsPlainProp = /^\w*$/,
+      rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]/g;
+
+  /** Used to match `RegExp` [syntax characters](http://ecma-international.org/ecma-262/6.0/#sec-patterns). */
+  var reRegExpChar = /[\\^$.*+?()[\]{}|]/g,
+      reHasRegExpChar = RegExp(reRegExpChar.source);
+
+  /** Used to match leading and trailing whitespace. */
+  var reTrim = /^\s+|\s+$/g,
+      reTrimStart = /^\s+/,
+      reTrimEnd = /\s+$/;
+
+  /** Used to match backslashes in property paths. */
+  var reEscapeChar = /\\(\\)?/g;
+
+  /** Used to match [ES template delimiters](http://ecma-international.org/ecma-262/6.0/#sec-template-literal-lexical-components). */
+  var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g;
+
+  /** Used to match `RegExp` flags from their coerced string values. */
+  var reFlags = /\w*$/;
+
+  /** Used to detect hexadecimal string values. */
+  var reHasHexPrefix = /^0x/i;
+
+  /** Used to detect bad signed hexadecimal string values. */
+  var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
+
+  /** Used to detect binary string values. */
+  var reIsBinary = /^0b[01]+$/i;
+
+  /** Used to detect host constructors (Safari > 5). */
+  var reIsHostCtor = /^\[object .+?Constructor\]$/;
+
+  /** Used to detect octal string values. */
+  var reIsOctal = /^0o[0-7]+$/i;
+
+  /** Used to detect unsigned integer values. */
+  var reIsUint = /^(?:0|[1-9]\d*)$/;
+
+  /** Used to match latin-1 supplementary letters (excluding mathematical operators). */
+  var reLatin1 = /[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g;
+
+  /** Used to ensure capturing order of template delimiters. */
+  var reNoMatch = /($^)/;
+
+  /** Used to match unescaped characters in compiled string literals. */
+  var reUnescapedString = /['\n\r\u2028\u2029\\]/g;
+
+  /** Used to compose unicode character classes. */
+  var rsAstralRange = '\\ud800-\\udfff',
+      rsComboMarksRange = '\\u0300-\\u036f\\ufe20-\\ufe23',
+      rsComboSymbolsRange = '\\u20d0-\\u20f0',
+      rsDingbatRange = '\\u2700-\\u27bf',
+      rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff',
+      rsMathOpRange = '\\xac\\xb1\\xd7\\xf7',
+      rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf',
+      rsQuoteRange = '\\u2018\\u2019\\u201c\\u201d',
+      rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000',
+      rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde',
+      rsVarRange = '\\ufe0e\\ufe0f',
+      rsBreakRange = rsMathOpRange + rsNonCharRange + rsQuoteRange + rsSpaceRange;
+
+  /** Used to compose unicode capture groups. */
+  var rsAstral = '[' + rsAstralRange + ']',
+      rsBreak = '[' + rsBreakRange + ']',
+      rsCombo = '[' + rsComboMarksRange + rsComboSymbolsRange + ']',
+      rsDigits = '\\d+',
+      rsDingbat = '[' + rsDingbatRange + ']',
+      rsLower = '[' + rsLowerRange + ']',
+      rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']',
+      rsFitz = '\\ud83c[\\udffb-\\udfff]',
+      rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')',
+      rsNonAstral = '[^' + rsAstralRange + ']',
+      rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}',
+      rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]',
+      rsUpper = '[' + rsUpperRange + ']',
+      rsZWJ = '\\u200d';
+
+  /** Used to compose unicode regexes. */
+  var rsLowerMisc = '(?:' + rsLower + '|' + rsMisc + ')',
+      rsUpperMisc = '(?:' + rsUpper + '|' + rsMisc + ')',
+      reOptMod = rsModifier + '?',
+      rsOptVar = '[' + rsVarRange + ']?',
+      rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*',
+      rsSeq = rsOptVar + reOptMod + rsOptJoin,
+      rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq,
+      rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')';
+
+  /**
+   * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and
+   * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols).
+   */
+  var reComboMark = RegExp(rsCombo, 'g');
+
+  /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */
+  var reComplexSymbol = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g');
+
+  /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */
+  var reHasComplexSymbol = RegExp('[' + rsZWJ + rsAstralRange  + rsComboMarksRange + rsComboSymbolsRange + rsVarRange + ']');
+
+  /** Used to match non-compound words composed of alphanumeric characters. */
+  var reBasicWord = /[a-zA-Z0-9]+/g;
+
+  /** Used to match complex or compound words. */
+  var reComplexWord = RegExp([
+    rsUpper + '?' + rsLower + '+(?=' + [rsBreak, rsUpper, '$'].join('|') + ')',
+    rsUpperMisc + '+(?=' + [rsBreak, rsUpper + rsLowerMisc, '$'].join('|') + ')',
+    rsUpper + '?' + rsLowerMisc + '+',
+    rsUpper + '+',
+    rsDigits,
+    rsEmoji
+  ].join('|'), 'g');
+
+  /** Used to detect strings that need a more robust regexp to match words. */
+  var reHasComplexWord = /[a-z][A-Z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;
+
+  /** Used to assign default `context` object properties. */
+  var contextProps = [
+    'Array', 'Buffer', 'Date', 'Error', 'Float32Array', 'Float64Array',
+    'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object',
+    'Reflect', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array',
+    'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', '_',
+    'clearTimeout', 'isFinite', 'parseInt', 'setTimeout'
+  ];
+
+  /** Used to make template sourceURLs easier to identify. */
+  var templateCounter = -1;
+
+  /** Used to identify `toStringTag` values of typed arrays. */
+  var typedArrayTags = {};
+  typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =
+  typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =
+  typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =
+  typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =
+  typedArrayTags[uint32Tag] = true;
+  typedArrayTags[argsTag] = typedArrayTags[arrayTag] =
+  typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =
+  typedArrayTags[dateTag] = typedArrayTags[errorTag] =
+  typedArrayTags[funcTag] = typedArrayTags[mapTag] =
+  typedArrayTags[numberTag] = typedArrayTags[objectTag] =
+  typedArrayTags[regexpTag] = typedArrayTags[setTag] =
+  typedArrayTags[stringTag] = typedArrayTags[weakMapTag] = false;
+
+  /** Used to identify `toStringTag` values supported by `_.clone`. */
+  var cloneableTags = {};
+  cloneableTags[argsTag] = cloneableTags[arrayTag] =
+  cloneableTags[arrayBufferTag] = cloneableTags[boolTag] =
+  cloneableTags[dateTag] = cloneableTags[float32Tag] =
+  cloneableTags[float64Tag] = cloneableTags[int8Tag] =
+  cloneableTags[int16Tag] = cloneableTags[int32Tag] =
+  cloneableTags[mapTag] = cloneableTags[numberTag] =
+  cloneableTags[objectTag] = cloneableTags[regexpTag] =
+  cloneableTags[setTag] = cloneableTags[stringTag] =
+  cloneableTags[symbolTag] = cloneableTags[uint8Tag] =
+  cloneableTags[uint8ClampedTag] = cloneableTags[uint16Tag] =
+  cloneableTags[uint32Tag] = true;
+  cloneableTags[errorTag] = cloneableTags[funcTag] =
+  cloneableTags[weakMapTag] = false;
+
+  /** Used to map latin-1 supplementary letters to basic latin letters. */
+  var deburredLetters = {
+    '\xc0': 'A',  '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A',
+    '\xe0': 'a',  '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a',
+    '\xc7': 'C',  '\xe7': 'c',
+    '\xd0': 'D',  '\xf0': 'd',
+    '\xc8': 'E',  '\xc9': 'E', '\xca': 'E', '\xcb': 'E',
+    '\xe8': 'e',  '\xe9': 'e', '\xea': 'e', '\xeb': 'e',
+    '\xcC': 'I',  '\xcd': 'I', '\xce': 'I', '\xcf': 'I',
+    '\xeC': 'i',  '\xed': 'i', '\xee': 'i', '\xef': 'i',
+    '\xd1': 'N',  '\xf1': 'n',
+    '\xd2': 'O',  '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O',
+    '\xf2': 'o',  '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o',
+    '\xd9': 'U',  '\xda': 'U', '\xdb': 'U', '\xdc': 'U',
+    '\xf9': 'u',  '\xfa': 'u', '\xfb': 'u', '\xfc': 'u',
+    '\xdd': 'Y',  '\xfd': 'y', '\xff': 'y',
+    '\xc6': 'Ae', '\xe6': 'ae',
+    '\xde': 'Th', '\xfe': 'th',
+    '\xdf': 'ss'
+  };
+
+  /** Used to map characters to HTML entities. */
+  var htmlEscapes = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#39;',
+    '`': '&#96;'
+  };
+
+  /** Used to map HTML entities to characters. */
+  var htmlUnescapes = {
+    '&amp;': '&',
+    '&lt;': '<',
+    '&gt;': '>',
+    '&quot;': '"',
+    '&#39;': "'",
+    '&#96;': '`'
+  };
+
+  /** Used to determine if values are of the language type `Object`. */
+  var objectTypes = {
+    'function': true,
+    'object': true
+  };
+
+  /** Used to escape characters for inclusion in compiled string literals. */
+  var stringEscapes = {
+    '\\': '\\',
+    "'": "'",
+    '\n': 'n',
+    '\r': 'r',
+    '\u2028': 'u2028',
+    '\u2029': 'u2029'
+  };
+
+  /** Built-in method references without a dependency on `root`. */
+  var freeParseFloat = parseFloat,
+      freeParseInt = parseInt;
+
+  /** Detect free variable `exports`. */
+  var freeExports = (objectTypes[typeof exports] && exports && !exports.nodeType)
+    ? exports
+    : undefined;
+
+  /** Detect free variable `module`. */
+  var freeModule = (objectTypes[typeof module] && module && !module.nodeType)
+    ? module
+    : undefined;
+
+  /** Detect the popular CommonJS extension `module.exports`. */
+  var moduleExports = (freeModule && freeModule.exports === freeExports)
+    ? freeExports
+    : undefined;
+
+  /** Detect free variable `global` from Node.js. */
+  var freeGlobal = checkGlobal(freeExports && freeModule && typeof global == 'object' && global);
+
+  /** Detect free variable `self`. */
+  var freeSelf = checkGlobal(objectTypes[typeof self] && self);
+
+  /** Detect free variable `window`. */
+  var freeWindow = checkGlobal(objectTypes[typeof window] && window);
+
+  /** Detect `this` as the global object. */
+  var thisGlobal = checkGlobal(objectTypes[typeof this] && this);
+
+  /**
+   * Used as a reference to the global object.
+   *
+   * The `this` value is used if it's the global object to avoid Greasemonkey's
+   * restricted `window` object, otherwise the `window` object is used.
+   */
+  var root = freeGlobal ||
+    ((freeWindow !== (thisGlobal && thisGlobal.window)) && freeWindow) ||
+      freeSelf || thisGlobal || Function('return this')();
+
+  /*--------------------------------------------------------------------------*/
+
+  /**
+   * Adds the key-value `pair` to `map`.
+   *
+   * @private
+   * @param {Object} map The map to modify.
+   * @param {Array} pair The key-value pair to add.
+   * @returns {Object} Returns `map`.
+   */
+  function addMapEntry(map, pair) {
+    // Don't return `Map#set` because it doesn't return the map instance in IE 11.
+    map.set(pair[0], pair[1]);
+    return map;
+  }
+
+  /**
+   * Adds `value` to `set`.
+   *
+   * @private
+   * @param {Object} set The set to modify.
+   * @param {*} value The value to add.
+   * @returns {Object} Returns `set`.
+   */
+  function addSetEntry(set, value) {
+    set.add(value);
+    return set;
+  }
+
+  /**
+   * A faster alternative to `Function#apply`, this function invokes `func`
+   * with the `this` binding of `thisArg` and the arguments of `args`.
+   *
+   * @private
+   * @param {Function} func The function to invoke.
+   * @param {*} thisArg The `this` binding of `func`.
+   * @param {...*} args The arguments to invoke `func` with.
+   * @returns {*} Returns the result of `func`.
+   */
+  function apply(func, thisArg, args) {
+    var length = args.length;
+    switch (length) {
+      case 0: return func.call(thisArg);
+      case 1: return func.call(thisArg, args[0]);
+      case 2: return func.call(thisArg, args[0], args[1]);
+      case 3: return func.call(thisArg, args[0], args[1], args[2]);
+    }
+    return func.apply(thisArg, args);
+  }
+
+  /**
+   * A specialized version of `baseAggregator` for arrays.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} setter The function to set `accumulator` values.
+   * @param {Function} iteratee The iteratee to transform keys.
+   * @param {Object} accumulator The initial aggregated object.
+   * @returns {Function} Returns `accumulator`.
+   */
+  function arrayAggregator(array, setter, iteratee, accumulator) {
+    var index = -1,
+        length = array.length;
+
+    while (++index < length) {
+      var value = array[index];
+      setter(accumulator, value, iteratee(value), array);
+    }
+    return accumulator;
+  }
+
+  /**
+   * Creates a new array concatenating `array` with `other`.
+   *
+   * @private
+   * @param {Array} array The first array to concatenate.
+   * @param {Array} other The second array to concatenate.
+   * @returns {Array} Returns the new concatenated array.
+   */
+  function arrayConcat(array, other) {
+    var index = -1,
+        length = array.length,
+        othIndex = -1,
+        othLength = other.length,
+        result = Array(length + othLength);
+
+    while (++index < length) {
+      result[index] = array[index];
+    }
+    while (++othIndex < othLength) {
+      result[index++] = other[othIndex];
+    }
+    return result;
+  }
+
+  /**
+   * A specialized version of `_.forEach` for arrays without support for
+   * iteratee shorthands.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} iteratee The function invoked per iteration.
+   * @returns {Array} Returns `array`.
+   */
+  function arrayEach(array, iteratee) {
+    var index = -1,
+        length = array.length;
+
+    while (++index < length) {
+      if (iteratee(array[index], index, array) === false) {
+        break;
+      }
+    }
+    return array;
+  }
+
+  /**
+   * A specialized version of `_.forEachRight` for arrays without support for
+   * iteratee shorthands.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} iteratee The function invoked per iteration.
+   * @returns {Array} Returns `array`.
+   */
+  function arrayEachRight(array, iteratee) {
+    var length = array.length;
+
+    while (length--) {
+      if (iteratee(array[length], length, array) === false) {
+        break;
+      }
+    }
+    return array;
+  }
+
+  /**
+   * A specialized version of `_.every` for arrays without support for
+   * iteratee shorthands.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} predicate The function invoked per iteration.
+   * @returns {boolean} Returns `true` if all elements pass the predicate check, else `false`.
+   */
+  function arrayEvery(array, predicate) {
+    var index = -1,
+        length = array.length;
+
+    while (++index < length) {
+      if (!predicate(array[index], index, array)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * A specialized version of `_.filter` for arrays without support for
+   * iteratee shorthands.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} predicate The function invoked per iteration.
+   * @returns {Array} Returns the new filtered array.
+   */
+  function arrayFilter(array, predicate) {
+    var index = -1,
+        length = array.length,
+        resIndex = 0,
+        result = [];
+
+    while (++index < length) {
+      var value = array[index];
+      if (predicate(value, index, array)) {
+        result[resIndex++] = value;
+      }
+    }
+    return result;
+  }
+
+  /**
+   * A specialized version of `_.includes` for arrays without support for
+   * specifying an index to search from.
+   *
+   * @private
+   * @param {Array} array The array to search.
+   * @param {*} target The value to search for.
+   * @returns {boolean} Returns `true` if `target` is found, else `false`.
+   */
+  function arrayIncludes(array, value) {
+    return !!array.length && baseIndexOf(array, value, 0) > -1;
+  }
+
+  /**
+   * This function is like `arrayIncludes` except that it accepts a comparator.
+   *
+   * @private
+   * @param {Array} array The array to search.
+   * @param {*} target The value to search for.
+   * @param {Function} comparator The comparator invoked per element.
+   * @returns {boolean} Returns `true` if `target` is found, else `false`.
+   */
+  function arrayIncludesWith(array, value, comparator) {
+    var index = -1,
+        length = array.length;
+
+    while (++index < length) {
+      if (comparator(value, array[index])) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * A specialized version of `_.map` for arrays without support for iteratee
+   * shorthands.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} iteratee The function invoked per iteration.
+   * @returns {Array} Returns the new mapped array.
+   */
+  function arrayMap(array, iteratee) {
+    var index = -1,
+        length = array.length,
+        result = Array(length);
+
+    while (++index < length) {
+      result[index] = iteratee(array[index], index, array);
+    }
+    return result;
+  }
+
+  /**
+   * Appends the elements of `values` to `array`.
+   *
+   * @private
+   * @param {Array} array The array to modify.
+   * @param {Array} values The values to append.
+   * @returns {Array} Returns `array`.
+   */
+  function arrayPush(array, values) {
+    var index = -1,
+        length = values.length,
+        offset = array.length;
+
+    while (++index < length) {
+      array[offset + index] = values[index];
+    }
+    return array;
+  }
+
+  /**
+   * A specialized version of `_.reduce` for arrays without support for
+   * iteratee shorthands.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} iteratee The function invoked per iteration.
+   * @param {*} [accumulator] The initial value.
+   * @param {boolean} [initAccum] Specify using the first element of `array` as the initial value.
+   * @returns {*} Returns the accumulated value.
+   */
+  function arrayReduce(array, iteratee, accumulator, initAccum) {
+    var index = -1,
+        length = array.length;
+
+    if (initAccum && length) {
+      accumulator = array[++index];
+    }
+    while (++index < length) {
+      accumulator = iteratee(accumulator, array[index], index, array);
+    }
+    return accumulator;
+  }
+
+  /**
+   * A specialized version of `_.reduceRight` for arrays without support for
+   * iteratee shorthands.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} iteratee The function invoked per iteration.
+   * @param {*} [accumulator] The initial value.
+   * @param {boolean} [initAccum] Specify using the last element of `array` as the initial value.
+   * @returns {*} Returns the accumulated value.
+   */
+  function arrayReduceRight(array, iteratee, accumulator, initAccum) {
+    var length = array.length;
+    if (initAccum && length) {
+      accumulator = array[--length];
+    }
+    while (length--) {
+      accumulator = iteratee(accumulator, array[length], length, array);
+    }
+    return accumulator;
+  }
+
+  /**
+   * A specialized version of `_.some` for arrays without support for iteratee
+   * shorthands.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} predicate The function invoked per iteration.
+   * @returns {boolean} Returns `true` if any element passes the predicate check, else `false`.
+   */
+  function arraySome(array, predicate) {
+    var index = -1,
+        length = array.length;
+
+    while (++index < length) {
+      if (predicate(array[index], index, array)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * The base implementation of methods like `_.max` and `_.min` which accepts a
+   * `comparator` to determine the extremum value.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} iteratee The iteratee invoked per iteration.
+   * @param {Function} comparator The comparator used to compare values.
+   * @returns {*} Returns the extremum value.
+   */
+  function baseExtremum(array, iteratee, comparator) {
+    var index = -1,
+        length = array.length;
+
+    while (++index < length) {
+      var value = array[index],
+          current = iteratee(value);
+
+      if (current != null && (computed === undefined
+            ? current === current
+            : comparator(current, computed)
+          )) {
+        var computed = current,
+            result = value;
+      }
+    }
+    return result;
+  }
+
+  /**
+   * The base implementation of methods like `_.find` and `_.findKey`, without
+   * support for iteratee shorthands, which iterates over `collection` using
+   * `eachFunc`.
+   *
+   * @private
+   * @param {Array|Object} collection The collection to search.
+   * @param {Function} predicate The function invoked per iteration.
+   * @param {Function} eachFunc The function to iterate over `collection`.
+   * @param {boolean} [retKey] Specify returning the key of the found element instead of the element itself.
+   * @returns {*} Returns the found element or its key, else `undefined`.
+   */
+  function baseFind(collection, predicate, eachFunc, retKey) {
+    var result;
+    eachFunc(collection, function(value, key, collection) {
+      if (predicate(value, key, collection)) {
+        result = retKey ? key : value;
+        return false;
+      }
+    });
+    return result;
+  }
+
+  /**
+   * The base implementation of `_.findIndex` and `_.findLastIndex` without
+   * support for iteratee shorthands.
+   *
+   * @private
+   * @param {Array} array The array to search.
+   * @param {Function} predicate The function invoked per iteration.
+   * @param {boolean} [fromRight] Specify iterating from right to left.
+   * @returns {number} Returns the index of the matched value, else `-1`.
+   */
+  function baseFindIndex(array, predicate, fromRight) {
+    var length = array.length,
+        index = fromRight ? length : -1;
+
+    while ((fromRight ? index-- : ++index < length)) {
+      if (predicate(array[index], index, array)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * The base implementation of `_.indexOf` without `fromIndex` bounds checks.
+   *
+   * @private
+   * @param {Array} array The array to search.
+   * @param {*} value The value to search for.
+   * @param {number} fromIndex The index to search from.
+   * @returns {number} Returns the index of the matched value, else `-1`.
+   */
+  function baseIndexOf(array, value, fromIndex) {
+    if (value !== value) {
+      return indexOfNaN(array, fromIndex);
+    }
+    var index = fromIndex - 1,
+        length = array.length;
+
+    while (++index < length) {
+      if (array[index] === value) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * This function is like `baseIndexOf` except that it accepts a comparator.
+   *
+   * @private
+   * @param {Array} array The array to search.
+   * @param {*} value The value to search for.
+   * @param {number} fromIndex The index to search from.
+   * @param {Function} comparator The comparator invoked per element.
+   * @returns {number} Returns the index of the matched value, else `-1`.
+   */
+  function baseIndexOfWith(array, value, fromIndex, comparator) {
+    var index = fromIndex - 1,
+        length = array.length;
+
+    while (++index < length) {
+      if (comparator(array[index], value)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * The base implementation of `_.reduce` and `_.reduceRight`, without support
+   * for iteratee shorthands, which iterates over `collection` using `eachFunc`.
+   *
+   * @private
+   * @param {Array|Object} collection The collection to iterate over.
+   * @param {Function} iteratee The function invoked per iteration.
+   * @param {*} accumulator The initial value.
+   * @param {boolean} initAccum Specify using the first or last element of `collection` as the initial value.
+   * @param {Function} eachFunc The function to iterate over `collection`.
+   * @returns {*} Returns the accumulated value.
+   */
+  function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) {
+    eachFunc(collection, function(value, index, collection) {
+      accumulator = initAccum
+        ? (initAccum = false, value)
+        : iteratee(accumulator, value, index, collection);
+    });
+    return accumulator;
+  }
+
+  /**
+   * The base implementation of `_.sortBy` which uses `comparer` to define the
+   * sort order of `array` and replaces criteria objects with their corresponding
+   * values.
+   *
+   * @private
+   * @param {Array} array The array to sort.
+   * @param {Function} comparer The function to define sort order.
+   * @returns {Array} Returns `array`.
+   */
+  function baseSortBy(array, comparer) {
+    var length = array.length;
+
+    array.sort(comparer);
+    while (length--) {
+      array[length] = array[length].value;
+    }
+    return array;
+  }
+
+  /**
+   * The base implementation of `_.sum` without support for iteratee shorthands.
+   *
+   * @private
+   * @param {Array} array The array to iterate over.
+   * @param {Function} iteratee The function invoked per iteration.
+   * @returns {number} Returns the sum.
+   */
+  function baseSum(array, iteratee) {
+    var result,
+        index = -1,
+        length = array.length;
+
+    while (++index < length) {
+      var current = iteratee(array[index]);
+      if (current !== undefined) {
+        result = result === undefined ? current : (result + current);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * The base implementation of `_.times` without support for iteratee shorthands
+   * or max array length checks.
+   *
+   * @private
+   * @param {number} n The number of times to invoke `iteratee`.
+   * @param {Function} iteratee The function invoked per iteration.
+   * @returns {Array} Returns the array of results.
+   */
+  function baseTimes(n, iteratee) {
+    var index = -1,
+        result = Array(n);
+
+    while (++index < n) {
+      result[index] = iteratee(index);
+    }
+    return result;
+  }
+
+  /**
+   * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array
+   * of key-value pairs for `object` corresponding to the property names of `props`.
+   *
+   * @private
+   * @param {Object} object The object to query.
+   * @param {Array} props The property names to get values for.
+   * @returns {Object} Returns the new array of key-value pairs.
+   */
+  function baseToPairs(object, props) {
+    return arrayMap(props, function(key) {
+      return [key, object[key]];
+    });
+  }
+
+  /**
+   * The base implementation of `_.unary` without support for storing wrapper metadata.
+   *
+   * @private
+   * @param {Function} func The function to cap arguments for.
+   * @returns {Function} Returns the new function.
+   */
+  function baseUnary(func) {
+    return function(value) {
+      return func(value);
+    };
+  }
+
+  /**
+   * The base implementation of `_.values` and `_.valuesIn` which creates an
+   * array of `object` property values corresponding to the property names
+   * of `props`.
+   *
+   * @private
+   * @param {Object} object The object to query.
+   * @param {Array} props The property names to get values for.
+   * @returns {Object} Returns the array of property values.
+   */
+  function baseValues(object, props) {
+    return arrayMap(props, function(key) {
+      return object[key];
+    });
+  }
+
+  /**
+   * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol
+   * that is not found in the character symbols.
+   *
+   * @private
+   * @param {Array} strSymbols The string symbols to inspect.
+   * @param {Array} chrSymbols The character symbols to find.
+   * @returns {number} Returns the index of the first unmatched string symbol.
+   */
+  function charsStartIndex(strSymbols, chrSymbols) {
+    var index = -1,
+        length = strSymbols.length;
+
+    while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}
+    return index;
+  }
+
+  /**
+   * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol
+   * that is not found in the character symbols.
+   *
+   * @private
+   * @param {Array} strSymbols The string symbols to inspect.
+   * @param {Array} chrSymbols The character symbols to find.
+   * @returns {number} Returns the index of the last unmatched string symbol.
+   */
+  function charsEndIndex(strSymbols, chrSymbols) {
+    var index = strSymbols.length;
+
+    while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}
+    return index;
+  }
+
+  /**
+   * Checks if `value` is a global object.
+   *
+   * @private
+   * @param {*} value The value to check.
+   * @returns {null|Object} Returns `value` if it's a global object, else `null`.
+   */
+  function checkGlobal(value) {
+    return (value && value.Object === Object) ? value : null;
+  }
+
+  /**
+   * Compares values to sort them in ascending order.
+   *
+   * @private
+   * @param {*} value The value to compare.
+   * @param {*} other The other value to compare.
+   * @returns {number} Returns the sort order indicator for `value`.
+   */
+  function compareAscending(value, other) {
+    if (value !== other) {
+      var valIsNull = value === null,
+          valIsUndef = value === undefined,
+          valIsReflexive = value === value;
+
+      var othIsNull = other === null,
+          othIsUndef = other === undefined,
+          othIsReflexive = other === other;
+
+      if ((value > other && !othIsNull) || !valIsReflexive ||
+          (valIsNull && !othIsUndef && othIsReflexive) ||
+          (valIsUndef && othIsReflexive)) {
+        return 1;
+      }
+      if ((value < other && !valIsNull) || !othIsReflexive ||
+          (othIsNull && !valIsUndef && valIsReflexive) ||
+          (othIsUndef && valIsReflexive)) {
+        return -1;
+      }
+    }
+    return 0;
+  }
+
+  /**
+   * Used by `_.orderBy` to compare multiple properties of a value to another
+   * and stable sort them.
+   *
+   * If `orders` is unspecified, all values are sorted in ascending order. Otherwise,
+   * specify an order of "desc" for descending or "asc" for ascending sort order
+   * of corresponding values.
+   *
+   * @private
+   * @param {Object} object The object to compare.
+   * @param {Object} other The other object to compare.
+   * @param {boolean[]|string[]} orders The order to sort by for each property.
+   * @returns {number} Returns the sort order indicator for `object`.
+   */
+  function compareMultiple(object, other, orders) {
+    var index = -1,
+        objCriteria = object.criteria,
+        othCriteria = other.criteria,
+        length = objCriteria.length,
+        ordersLength = orders.length;
+
+    while (++index < length) {
+      var result = compareAscending(objCriteria[index], othCriteria[index]);
+      if (result) {
+        if (index >= ordersLength) {
+          return result;
+        }
+        var order = orders[index];
+        return result * (order == 'desc' ? -1 : 1);
+      }
+    }
+    // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications
+    // that causes it, under certain circumstances, to provide the same value for
+    // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247
+    // for more details.
+    //
+    // This also ensures a stable sort in V8 and other engines.
+    // See https://code.google.com/p/v8/issues/detail?id=90 for more details.
+    return object.index - other.index;
+  }
+
+  /**
+   * Gets the number of `placeholder` occurrences in `array`.
+   *
+   * @private
+   * @param {Array} array The array to inspect.
+   * @param {*} placeholder The placeholder to search for.
+   * @returns {number} Returns the placeholder count.
+   */
+  function countHolders(array, placeholder) {
+    var length = array.length,
+        result = 0;
+
+    while (length--) {
+      if (array[length] === placeholder) {
+        result++;
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Used by `_.deburr` to convert latin-1 supplementary letters to basic latin letters.
+   *
+   * @private
+   * @param {string} letter The matched letter to deburr.
+   * @returns {string} Returns the deburred letter.
+   */
+  function deburrLetter(letter) {
+    return deburredLetters[letter];
+  }
+
+  /**
+   * Used by `_.escape` to convert characters to HTML entities.
+   *
+   * @private
+   * @param {string} chr The matched character to escape.
+   * @returns {string} Returns the escaped character.
+   */
+  function escapeHtmlChar(chr) {
+    return htmlEscapes[chr];
+  }
+
+  /**
+   * Used by `_.template` to escape characters for inclusion in compiled string literals.
+   *
+   * @private
+   * @param {string} chr The matched character to escape.
+   * @returns {string} Returns the escaped character.
+   */
+  function escapeStringChar(chr) {
+    return '\\' + stringEscapes[chr];
+  }
+
+  /**
+   * Gets the index at which the first occurrence of `NaN` is found in `array`.
+   *
+   * @private
+   * @param {Array} array The array to search.
+   * @param {number} fromIndex The index to search from.
+   * @param {boolean} [fromRight] Specify iterating from right to left.
+   * @returns {number} Returns the index of the matched `NaN`, else `-1`.
+   */
+  function indexOfNaN(array, fromIndex, fromRight) {
+    var length = array.length,
+        index = fromIndex + (fromRight ? 0 : -1);
+
+    while ((fromRight ? index-- : ++index < length)) {
+      var other = array[index];
+      if (other !== other) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Checks if `value` is a host object in IE < 9.
+   *
+   * @private
+   * @param {*} value The value to check.
+   * @returns {boolean} Returns `true` if `value` is a host object, else `false`.
+   */
+  function isHostObject(value) {
+    // Many host objects are `Object` objects that can coerce to strings
+    // despite having improperly defined `toString` methods.
+    var result = false;
+    if (value != null && typeof value.toString != 'function') {
+      try {
+        result = !!(value + '');
+      } catch (e) {}
+    }
+    return result;
+  }
+
+  /**
+   * Checks if `value` is a valid array-like index.
+   *
+   * @private
+   * @param {*} value The value to check.
+   * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
+   * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
+   */
+  function isIndex(value, length) {
+    value = (typeof value == 'number' || reIsUint.test(value)) ? +value : -1;
+    length = length == null ? MAX_SAFE_INTEGER : length;
+    return value > -1 && value % 1 == 0 && value < length;
+  }
+
+  /**
+   * Converts `iterator` to an array.
+   *
+   * @private
+   * @param {Object} iterator The iterator to convert.
+   * @returns {Array} Returns the converted array.
+   */
+  function iteratorToArray(iterator) {
+    var data,
+        result = [];
+
+    while (!(data = iterator.next()).done) {
+      result.push(data.value);
+    }
+    return result;
+  }
+
+  /**
+   * Converts `map` to an array.
+   *
+   * @private
+   * @param {Object} map The map to convert.
+   * @returns {Array} Returns the converted array.
+   */
+  function mapToArray(map) {
+    var index = -1,
+        result = Array(map.size);
+
+    map.forEach(function(value, key) {
+      result[++index] = [key, value];
+    });
+    return result;
+  }
+
+  /**
+   * Replaces all `placeholder` elements in `array` with an internal placeholder
+   * and returns an array of their indexes.
+   *
+   * @private
+   * @param {Array} array The array to modify.
+   * @param {*} placeholder The placeholder to replace.
+   * @returns {Array} Returns the new array of placeholder indexes.
+   */
+  function replaceHolders(array, placeholder) {
+    var index = -1,
+        length = array.length,
+        resIndex = 0,
+        result = [];
+
+    while (++index < length) {
+      var value = array[index];
+      if (value === placeholder || value === PLACEHOLDER) {
+        array[index] = PLACEHOLDER;
+        result[resIndex++] = index;
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Converts `set` to an array.
+   *
+   * @private
+   * @param {Object} set The set to convert.
+   * @returns {Array} Returns the converted array.
+   */
+  function setToArray(set) {
+    var index = -1,
+        result = Array(set.size);
+
+    set.forEach(function(value) {
+      result[++index] = value;
+    });
+    return result;
+  }
+
+  /**
+   * Gets the number of symbols in `string`.
+   *
+   * @private
+   * @param {string} string The string to inspect.
+   * @returns {number} Returns the string size.
+   */
+  function stringSize(string) {
+    if (!(string && reHasComplexSymbol.test(string))) {
+      return string.length;
+    }
+    var result = reComplexSymbol.lastIndex = 0;
+    while (reComplexSymbol.test(string)) {
+      result++;
+    }
+    return result;
+  }
+
+  /**
+   * Converts `string` to an array.
+   *
+   * @private
+   * @param {string} string The string to convert.
+   * @returns {Array} Returns the converted array.
+   */
+  function stringToArray(string) {
+    return string.match(reComplexSymbol);
+  }
+
+  /**
+   * Used by `_.unescape` to convert HTML entities to characters.
+   *
+   * @private
+   * @param {string} chr The matched character to unescape.
+   * @returns {string} Returns the unescaped character.
+   */
+  function unescapeHtmlChar(chr) {
+    return htmlUnescapes[chr];
+  }
+
+  /*--------------------------------------------------------------------------*/
+
+  /**
+   * Create a new pristine `lodash` function using the `context` object.
+   *
+   * @static
+   * @memberOf _
+   * @category Util
+   * @param {Object} [context=root] The context object.
+   * @returns {Function} Returns a new `lodash` function.
+   * @example
+   *
+   * _.mixin({ 'foo': _.constant('foo') });
+   *
+   * var lodash = _.runInContext();
+   * lodash.mixin({ 'bar': lodash.constant('bar') });
+   *
+   * _.isFunction(_.foo);
+   * // => true
+   * _.isFunction(_.bar);
+   * // => false
+   *
+   * lodash.isFunction(lodash.foo);
+   * // => false
+   * lodash.isFunction(lodash.bar);
+   * // => true
+   *
+   * // Use `context` to mock `Date#getTime` use in `_.now`.
+   * var mock = _.runInContext({
+   *   'Date': function() {
+   *     return { 'getTime': getTimeMock };
+   *   }
+   * });
+   *
+   * // Create a suped-up `defer` in Node.js.
+   * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer;
+   */
+  function runInContext(context) {
+    context = context ? _.defaults({}, context, _.pick(root, contextProps)) : root;
+
+    /** Built-in constructor references. */
+    var Date = context.Date,
+        Error = context.Error,
+        Math = context.Math,
+        RegExp = context.RegExp,
+        TypeError = context.TypeError;
+
+    /** Used for built-in method references. */
+    var arrayProto = context.Array.prototype,
+        objectProto = context.Object.prototype;
+
+    /** Used to resolve the decompiled source of functions. */
+    var funcToString = context.Function.prototype.toString;
+
+    /** Used to check objects for own properties. */
+    var hasOwnProperty = objectProto.hasOwnProperty;
+
+    /** Used to generate unique IDs. */
+    var idCounter = 0;
+
+    /** Used to infer the `Object` constructor. */
+    var objectCtorString = funcToString.call(Object);
+
+    /**
+     * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
+     * of values.
+     */
+    var objectToString = objectProto.toString;
+
+    /** Used to restore the original `_` reference in `_.noConflict`. */
+    var oldDash = root._;
+
+    /** Used to detect if a method is native. */
+    var reIsNative = RegExp('^' +
+      funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&')
+      .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
+    );
+
+    /** Built-in value references. */
+    var Buffer = moduleExports ? context.Buffer : undefined,
+        Reflect = context.Reflect,
+        Symbol = context.Symbol,
+        Uint8Array = context.Uint8Array,
+        clearTimeout = context.clearTimeout,
+        enumerate = Reflect ? Reflect.enumerate : undefined,
+        getPrototypeOf = Object.getPrototypeOf,
+        getOwnPropertySymbols = Object.getOwnPropertySymbols,
+        iteratorSymbol = typeof (iteratorSymbol = Symbol && Symbol.iterator) == 'symbol' ? iteratorSymbol : undefined,
+        objectCreate = Object.create,
+        propertyIsEnumerable = objectProto.propertyIsEnumerable,
+        setTimeout = context.setTimeout,
+        splice = arrayProto.splice;
+
+    /* Built-in method references for those with the same name as other `lodash` methods. */
+    var nativeCeil = Math.ceil,
+        nativeFloor = Math.floor,
+        nativeIsFinite = context.isFinite,
+        nativeJoin = arrayProto.join,
+        nativeKeys = Object.keys,
+        nativeMax = Math.max,
+        nativeMin = Math.min,
+        nativeParseInt = context.parseInt,
+        nativeRandom = Math.random,
+        nativeReverse = arrayProto.reverse;
+
+    /* Built-in method references that are verified to be native. */
+    var Map = getNative(context, 'Map'),
+        Set = getNative(context, 'Set'),
+        WeakMap = getNative(context, 'WeakMap'),
+        nativeCreate = getNative(Object, 'create');
+
+    /** Used to store function metadata. */
+    var metaMap = WeakMap && new WeakMap;
+
+    /** Detect if properties shadowing those on `Object.prototype` are non-enumerable. */
+    var nonEnumShadows = !propertyIsEnumerable.call({ 'valueOf': 1 }, 'valueOf');
+
+    /** Used to lookup unminified function names. */
+    var realNames = {};
+
+    /** Used to detect maps, sets, and weakmaps. */
+    var mapCtorString = Map ? funcToString.call(Map) : '',
+        setCtorString = Set ? funcToString.call(Set) : '',
+        weakMapCtorString = WeakMap ? funcToString.call(WeakMap) : '';
+
+    /** Used to convert symbols to primitives and strings. */
+    var symbolProto = Symbol ? Symbol.prototype : undefined,
+        symbolValueOf = symbolProto ? symbolProto.valueOf : undefined,
+        symbolToString = symbolProto ? symbolProto.toString : undefined;
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Creates a `lodash` object which wraps `value` to enable implicit method
+     * chaining. Methods that operate on and return arrays, collections, and
+     * functions can be chained together. Methods that retrieve a single value or
+     * may return a primitive value will automatically end the chain sequence and
+     * return the unwrapped value. Otherwise, the value must be unwrapped with
+     * `_#value`.
+     *
+     * Explicit chaining, which must be unwrapped with `_#value` in all cases,
+     * may be enabled using `_.chain`.
+     *
+     * The execution of chained methods is lazy, that is, it's deferred until
+     * `_#value` is implicitly or explicitly called.
+     *
+     * Lazy evaluation allows several methods to support shortcut fusion. Shortcut
+     * fusion is an optimization to merge iteratee calls; this avoids the creation
+     * of intermediate arrays and can greatly reduce the number of iteratee executions.
+     * Sections of a chain sequence qualify for shortcut fusion if the section is
+     * applied to an array of at least two hundred elements and any iteratees
+     * accept only one argument. The heuristic for whether a section qualifies
+     * for shortcut fusion is subject to change.
+     *
+     * Chaining is supported in custom builds as long as the `_#value` method is
+     * directly or indirectly included in the build.
+     *
+     * In addition to lodash methods, wrappers have `Array` and `String` methods.
+     *
+     * The wrapper `Array` methods are:
+     * `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift`
+     *
+     * The wrapper `String` methods are:
+     * `replace` and `split`
+     *
+     * The wrapper methods that support shortcut fusion are:
+     * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`,
+     * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`,
+     * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray`
+     *
+     * The chainable wrapper methods are:
+     * `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`,
+     * `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`,
+     * `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`,
+     * `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`,
+     * `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`,
+     * `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`,
+     * `flatten`, `flattenDeep`, `flattenDepth`, `flip`, `flow`, `flowRight`,
+     * `fromPairs`, `functions`, `functionsIn`, `groupBy`, `initial`, `intersection`,
+     * `intersectionBy`, `intersectionWith`, `invert`, `invertBy`, `invokeMap`,
+     * `iteratee`, `keyBy`, `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`,
+     * `matches`, `matchesProperty`, `memoize`, `merge`, `mergeWith`, `method`,
+     * `methodOf`, `mixin`, `negate`, `nthArg`, `omit`, `omitBy`, `once`, `orderBy`,
+     * `over`, `overArgs`, `overEvery`, `overSome`, `partial`, `partialRight`,
+     * `partition`, `pick`, `pickBy`, `plant`, `property`, `propertyOf`, `pull`,
+     * `pullAll`, `pullAllBy`, `pullAllWith`, `pullAt`, `push`, `range`,
+     * `rangeRight`, `rearg`, `reject`, `remove`, `rest`, `reverse`, `sampleSize`,
+     * `set`, `setWith`, `shuffle`, `slice`, `sort`, `sortBy`, `splice`, `spread`,
+     * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, `tap`, `throttle`,
+     * `thru`, `toArray`, `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`,
+     * `transform`, `unary`, `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`,
+     * `uniqWith`, `unset`, `unshift`, `unzip`, `unzipWith`, `update`, `values`,
+     * `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`, `zipObject`,
+     * `zipObjectDeep`, and `zipWith`
+     *
+     * The wrapper methods that are **not** chainable by default are:
+     * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`,
+     * `cloneDeep`, `cloneDeepWith`, `cloneWith`, `deburr`, `each`, `eachRight`,
+     * `endsWith`, `eq`, `escape`, `escapeRegExp`, `every`, `find`, `findIndex`,
+     * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `first`, `floor`,
+     * `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`,
+     * `get`, `gt`, `gte`, `has`, `hasIn`, `head`, `identity`, `includes`,
+     * `indexOf`, `inRange`, `invoke`, `isArguments`, `isArray`, `isArrayBuffer`,
+     * `isArrayLike`, `isArrayLikeObject`, `isBoolean`, `isBuffer`, `isDate`,
+     * `isElement`, `isEmpty`, `isEqual`, `isEqualWith`, `isError`, `isFinite`,
+     * `isFunction`, `isInteger`, `isLength`, `isMap`, `isMatch`, `isMatchWith`,
+     * `isNaN`, `isNative`, `isNil`, `isNull`, `isNumber`, `isObject`, `isObjectLike`,
+     * `isPlainObject`, `isRegExp`, `isSafeInteger`, `isSet`, `isString`,
+     * `isUndefined`, `isTypedArray`, `isWeakMap`, `isWeakSet`, `join`, `kebabCase`,
+     * `last`, `lastIndexOf`, `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`,
+     * `maxBy`, `mean`, `min`, `minBy`, `noConflict`, `noop`, `now`, `pad`,
+     * `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`,
+     * `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`,
+     * `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`,
+     * `sortedLastIndexBy`, `startCase`, `startsWith`, `subtract`, `sum`, `sumBy`,
+     * `template`, `times`, `toInteger`, `toJSON`, `toLength`, `toLower`,
+     * `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`, `trimEnd`,
+     * `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`, `upperFirst`,
+     * `value`, and `words`
+     *
+     * @name _
+     * @constructor
+     * @category Seq
+     * @param {*} value The value to wrap in a `lodash` instance.
+     * @returns {Object} Returns the new `lodash` wrapper instance.
+     * @example
+     *
+     * function square(n) {
+     *   return n * n;
+     * }
+     *
+     * var wrapped = _([1, 2, 3]);
+     *
+     * // Returns an unwrapped value.
+     * wrapped.reduce(_.add);
+     * // => 6
+     *
+     * // Returns a wrapped value.
+     * var squares = wrapped.map(square);
+     *
+     * _.isArray(squares);
+     * // => false
+     *
+     * _.isArray(squares.value());
+     * // => true
+     */
+    function lodash(value) {
+      if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {
+        if (value instanceof LodashWrapper) {
+          return value;
+        }
+        if (hasOwnProperty.call(value, '__wrapped__')) {
+          return wrapperClone(value);
+        }
+      }
+      return new LodashWrapper(value);
+    }
+
+    /**
+     * The function whose prototype all chaining wrappers inherit from.
+     *
+     * @private
+     */
+    function baseLodash() {
+      // No operation performed.
+    }
+
+    /**
+     * The base constructor for creating `lodash` wrapper objects.
+     *
+     * @private
+     * @param {*} value The value to wrap.
+     * @param {boolean} [chainAll] Enable chaining for all wrapper methods.
+     */
+    function LodashWrapper(value, chainAll) {
+      this.__wrapped__ = value;
+      this.__actions__ = [];
+      this.__chain__ = !!chainAll;
+      this.__index__ = 0;
+      this.__values__ = undefined;
+    }
+
+    /**
+     * By default, the template delimiters used by lodash are like those in
+     * embedded Ruby (ERB). Change the following template settings to use
+     * alternative delimiters.
+     *
+     * @static
+     * @memberOf _
+     * @type {Object}
+     */
+    lodash.templateSettings = {
+
+      /**
+       * Used to detect `data` property values to be HTML-escaped.
+       *
+       * @memberOf _.templateSettings
+       * @type {RegExp}
+       */
+      'escape': reEscape,
+
+      /**
+       * Used to detect code to be evaluated.
+       *
+       * @memberOf _.templateSettings
+       * @type {RegExp}
+       */
+      'evaluate': reEvaluate,
+
+      /**
+       * Used to detect `data` property values to inject.
+       *
+       * @memberOf _.templateSettings
+       * @type {RegExp}
+       */
+      'interpolate': reInterpolate,
+
+      /**
+       * Used to reference the data object in the template text.
+       *
+       * @memberOf _.templateSettings
+       * @type {string}
+       */
+      'variable': '',
+
+      /**
+       * Used to import variables into the compiled template.
+       *
+       * @memberOf _.templateSettings
+       * @type {Object}
+       */
+      'imports': {
+
+        /**
+         * A reference to the `lodash` function.
+         *
+         * @memberOf _.templateSettings.imports
+         * @type {Function}
+         */
+        '_': lodash
+      }
+    };
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation.
+     *
+     * @private
+     * @constructor
+     * @param {*} value The value to wrap.
+     */
+    function LazyWrapper(value) {
+      this.__wrapped__ = value;
+      this.__actions__ = [];
+      this.__dir__ = 1;
+      this.__filtered__ = false;
+      this.__iteratees__ = [];
+      this.__takeCount__ = MAX_ARRAY_LENGTH;
+      this.__views__ = [];
+    }
+
+    /**
+     * Creates a clone of the lazy wrapper object.
+     *
+     * @private
+     * @name clone
+     * @memberOf LazyWrapper
+     * @returns {Object} Returns the cloned `LazyWrapper` object.
+     */
+    function lazyClone() {
+      var result = new LazyWrapper(this.__wrapped__);
+      result.__actions__ = copyArray(this.__actions__);
+      result.__dir__ = this.__dir__;
+      result.__filtered__ = this.__filtered__;
+      result.__iteratees__ = copyArray(this.__iteratees__);
+      result.__takeCount__ = this.__takeCount__;
+      result.__views__ = copyArray(this.__views__);
+      return result;
+    }
+
+    /**
+     * Reverses the direction of lazy iteration.
+     *
+     * @private
+     * @name reverse
+     * @memberOf LazyWrapper
+     * @returns {Object} Returns the new reversed `LazyWrapper` object.
+     */
+    function lazyReverse() {
+      if (this.__filtered__) {
+        var result = new LazyWrapper(this);
+        result.__dir__ = -1;
+        result.__filtered__ = true;
+      } else {
+        result = this.clone();
+        result.__dir__ *= -1;
+      }
+      return result;
+    }
+
+    /**
+     * Extracts the unwrapped value from its lazy wrapper.
+     *
+     * @private
+     * @name value
+     * @memberOf LazyWrapper
+     * @returns {*} Returns the unwrapped value.
+     */
+    function lazyValue() {
+      var array = this.__wrapped__.value(),
+          dir = this.__dir__,
+          isArr = isArray(array),
+          isRight = dir < 0,
+          arrLength = isArr ? array.length : 0,
+          view = getView(0, arrLength, this.__views__),
+          start = view.start,
+          end = view.end,
+          length = end - start,
+          index = isRight ? end : (start - 1),
+          iteratees = this.__iteratees__,
+          iterLength = iteratees.length,
+          resIndex = 0,
+          takeCount = nativeMin(length, this.__takeCount__);
+
+      if (!isArr || arrLength < LARGE_ARRAY_SIZE ||
+          (arrLength == length && takeCount == length)) {
+        return baseWrapperValue(array, this.__actions__);
+      }
+      var result = [];
+
+      outer:
+      while (length-- && resIndex < takeCount) {
+        index += dir;
+
+        var iterIndex = -1,
+            value = array[index];
+
+        while (++iterIndex < iterLength) {
+          var data = iteratees[iterIndex],
+              iteratee = data.iteratee,
+              type = data.type,
+              computed = iteratee(value);
+
+          if (type == LAZY_MAP_FLAG) {
+            value = computed;
+          } else if (!computed) {
+            if (type == LAZY_FILTER_FLAG) {
+              continue outer;
+            } else {
+              break outer;
+            }
+          }
+        }
+        result[resIndex++] = value;
+      }
+      return result;
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Creates an hash object.
+     *
+     * @private
+     * @constructor
+     * @returns {Object} Returns the new hash object.
+     */
+    function Hash() {}
+
+    /**
+     * Removes `key` and its value from the hash.
+     *
+     * @private
+     * @param {Object} hash The hash to modify.
+     * @param {string} key The key of the value to remove.
+     * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+     */
+    function hashDelete(hash, key) {
+      return hashHas(hash, key) && delete hash[key];
+    }
+
+    /**
+     * Gets the hash value for `key`.
+     *
+     * @private
+     * @param {Object} hash The hash to query.
+     * @param {string} key The key of the value to get.
+     * @returns {*} Returns the entry value.
+     */
+    function hashGet(hash, key) {
+      if (nativeCreate) {
+        var result = hash[key];
+        return result === HASH_UNDEFINED ? undefined : result;
+      }
+      return hasOwnProperty.call(hash, key) ? hash[key] : undefined;
+    }
+
+    /**
+     * Checks if a hash value for `key` exists.
+     *
+     * @private
+     * @param {Object} hash The hash to query.
+     * @param {string} key The key of the entry to check.
+     * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+     */
+    function hashHas(hash, key) {
+      return nativeCreate ? hash[key] !== undefined : hasOwnProperty.call(hash, key);
+    }
+
+    /**
+     * Sets the hash `key` to `value`.
+     *
+     * @private
+     * @param {Object} hash The hash to modify.
+     * @param {string} key The key of the value to set.
+     * @param {*} value The value to set.
+     */
+    function hashSet(hash, key, value) {
+      hash[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Creates a map cache object to store key-value pairs.
+     *
+     * @private
+     * @constructor
+     * @param {Array} [values] The values to cache.
+     */
+    function MapCache(values) {
+      var index = -1,
+          length = values ? values.length : 0;
+
+      this.clear();
+      while (++index < length) {
+        var entry = values[index];
+        this.set(entry[0], entry[1]);
+      }
+    }
+
+    /**
+     * Removes all key-value entries from the map.
+     *
+     * @private
+     * @name clear
+     * @memberOf MapCache
+     */
+    function mapClear() {
+      this.__data__ = {
+        'hash': new Hash,
+        'map': Map ? new Map : [],
+        'string': new Hash
+      };
+    }
+
+    /**
+     * Removes `key` and its value from the map.
+     *
+     * @private
+     * @name delete
+     * @memberOf MapCache
+     * @param {string} key The key of the value to remove.
+     * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+     */
+    function mapDelete(key) {
+      var data = this.__data__;
+      if (isKeyable(key)) {
+        return hashDelete(typeof key == 'string' ? data.string : data.hash, key);
+      }
+      return Map ? data.map['delete'](key) : assocDelete(data.map, key);
+    }
+
+    /**
+     * Gets the map value for `key`.
+     *
+     * @private
+     * @name get
+     * @memberOf MapCache
+     * @param {string} key The key of the value to get.
+     * @returns {*} Returns the entry value.
+     */
+    function mapGet(key) {
+      var data = this.__data__;
+      if (isKeyable(key)) {
+        return hashGet(typeof key == 'string' ? data.string : data.hash, key);
+      }
+      return Map ? data.map.get(key) : assocGet(data.map, key);
+    }
+
+    /**
+     * Checks if a map value for `key` exists.
+     *
+     * @private
+     * @name has
+     * @memberOf MapCache
+     * @param {string} key The key of the entry to check.
+     * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+     */
+    function mapHas(key) {
+      var data = this.__data__;
+      if (isKeyable(key)) {
+        return hashHas(typeof key == 'string' ? data.string : data.hash, key);
+      }
+      return Map ? data.map.has(key) : assocHas(data.map, key);
+    }
+
+    /**
+     * Sets the map `key` to `value`.
+     *
+     * @private
+     * @name set
+     * @memberOf MapCache
+     * @param {string} key The key of the value to set.
+     * @param {*} value The value to set.
+     * @returns {Object} Returns the map cache object.
+     */
+    function mapSet(key, value) {
+      var data = this.__data__;
+      if (isKeyable(key)) {
+        hashSet(typeof key == 'string' ? data.string : data.hash, key, value);
+      } else if (Map) {
+        data.map.set(key, value);
+      } else {
+        assocSet(data.map, key, value);
+      }
+      return this;
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     *
+     * Creates a set cache object to store unique values.
+     *
+     * @private
+     * @constructor
+     * @param {Array} [values] The values to cache.
+     */
+    function SetCache(values) {
+      var index = -1,
+          length = values ? values.length : 0;
+
+      this.__data__ = new MapCache;
+      while (++index < length) {
+        this.push(values[index]);
+      }
+    }
+
+    /**
+     * Checks if `value` is in `cache`.
+     *
+     * @private
+     * @param {Object} cache The set cache to search.
+     * @param {*} value The value to search for.
+     * @returns {number} Returns `true` if `value` is found, else `false`.
+     */
+    function cacheHas(cache, value) {
+      var map = cache.__data__;
+      if (isKeyable(value)) {
+        var data = map.__data__,
+            hash = typeof value == 'string' ? data.string : data.hash;
+
+        return hash[value] === HASH_UNDEFINED;
+      }
+      return map.has(value);
+    }
+
+    /**
+     * Adds `value` to the set cache.
+     *
+     * @private
+     * @name push
+     * @memberOf SetCache
+     * @param {*} value The value to cache.
+     */
+    function cachePush(value) {
+      var map = this.__data__;
+      if (isKeyable(value)) {
+        var data = map.__data__,
+            hash = typeof value == 'string' ? data.string : data.hash;
+
+        hash[value] = HASH_UNDEFINED;
+      }
+      else {
+        map.set(value, HASH_UNDEFINED);
+      }
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Creates a stack cache object to store key-value pairs.
+     *
+     * @private
+     * @constructor
+     * @param {Array} [values] The values to cache.
+     */
+    function Stack(values) {
+      var index = -1,
+          length = values ? values.length : 0;
+
+      this.clear();
+      while (++index < length) {
+        var entry = values[index];
+        this.set(entry[0], entry[1]);
+      }
+    }
+
+    /**
+     * Removes all key-value entries from the stack.
+     *
+     * @private
+     * @name clear
+     * @memberOf Stack
+     */
+    function stackClear() {
+      this.__data__ = { 'array': [], 'map': null };
+    }
+
+    /**
+     * Removes `key` and its value from the stack.
+     *
+     * @private
+     * @name delete
+     * @memberOf Stack
+     * @param {string} key The key of the value to remove.
+     * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+     */
+    function stackDelete(key) {
+      var data = this.__data__,
+          array = data.array;
+
+      return array ? assocDelete(array, key) : data.map['delete'](key);
+    }
+
+    /**
+     * Gets the stack value for `key`.
+     *
+     * @private
+     * @name get
+     * @memberOf Stack
+     * @param {string} key The key of the value to get.
+     * @returns {*} Returns the entry value.
+     */
+    function stackGet(key) {
+      var data = this.__data__,
+          array = data.array;
+
+      return array ? assocGet(array, key) : data.map.get(key);
+    }
+
+    /**
+     * Checks if a stack value for `key` exists.
+     *
+     * @private
+     * @name has
+     * @memberOf Stack
+     * @param {string} key The key of the entry to check.
+     * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+     */
+    function stackHas(key) {
+      var data = this.__data__,
+          array = data.array;
+
+      return array ? assocHas(array, key) : data.map.has(key);
+    }
+
+    /**
+     * Sets the stack `key` to `value`.
+     *
+     * @private
+     * @name set
+     * @memberOf Stack
+     * @param {string} key The key of the value to set.
+     * @param {*} value The value to set.
+     * @returns {Object} Returns the stack cache object.
+     */
+    function stackSet(key, value) {
+      var data = this.__data__,
+          array = data.array;
+
+      if (array) {
+        if (array.length < (LARGE_ARRAY_SIZE - 1)) {
+          assocSet(array, key, value);
+        } else {
+          data.array = null;
+          data.map = new MapCache(array);
+        }
+      }
+      var map = data.map;
+      if (map) {
+        map.set(key, value);
+      }
+      return this;
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Removes `key` and its value from the associative array.
+     *
+     * @private
+     * @param {Array} array The array to query.
+     * @param {string} key The key of the value to remove.
+     * @returns {boolean} Returns `true` if the entry was removed, else `false`.
+     */
+    function assocDelete(array, key) {
+      var index = assocIndexOf(array, key);
+      if (index < 0) {
+        return false;
+      }
+      var lastIndex = array.length - 1;
+      if (index == lastIndex) {
+        array.pop();
+      } else {
+        splice.call(array, index, 1);
+      }
+      return true;
+    }
+
+    /**
+     * Gets the associative array value for `key`.
+     *
+     * @private
+     * @param {Array} array The array to query.
+     * @param {string} key The key of the value to get.
+     * @returns {*} Returns the entry value.
+     */
+    function assocGet(array, key) {
+      var index = assocIndexOf(array, key);
+      return index < 0 ? undefined : array[index][1];
+    }
+
+    /**
+     * Checks if an associative array value for `key` exists.
+     *
+     * @private
+     * @param {Array} array The array to query.
+     * @param {string} key The key of the entry to check.
+     * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
+     */
+    function assocHas(array, key) {
+      return assocIndexOf(array, key) > -1;
+    }
+
+    /**
+     * Gets the index at which the first occurrence of `key` is found in `array`
+     * of key-value pairs.
+     *
+     * @private
+     * @param {Array} array The array to search.
+     * @param {*} key The key to search for.
+     * @returns {number} Returns the index of the matched value, else `-1`.
+     */
+    function assocIndexOf(array, key) {
+      var length = array.length;
+      while (length--) {
+        if (eq(array[length][0], key)) {
+          return length;
+        }
+      }
+      return -1;
+    }
+
+    /**
+     * Sets the associative array `key` to `value`.
+     *
+     * @private
+     * @param {Array} array The array to modify.
+     * @param {string} key The key of the value to set.
+     * @param {*} value The value to set.
+     */
+    function assocSet(array, key, value) {
+      var index = assocIndexOf(array, key);
+      if (index < 0) {
+        array.push([key, value]);
+      } else {
+        array[index][1] = value;
+      }
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Used by `_.defaults` to customize its `_.assignIn` use.
+     *
+     * @private
+     * @param {*} objValue The destination value.
+     * @param {*} srcValue The source value.
+     * @param {string} key The key of the property to assign.
+     * @param {Object} object The parent object of `objValue`.
+     * @returns {*} Returns the value to assign.
+     */
+    function assignInDefaults(objValue, srcValue, key, object) {
+      if (objValue === undefined ||
+          (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) {
+        return srcValue;
+      }
+      return objValue;
+    }
+
+    /**
+     * This function is like `assignValue` except that it doesn't assign
+     * `undefined` values.
+     *
+     * @private
+     * @param {Object} object The object to modify.
+     * @param {string} key The key of the property to assign.
+     * @param {*} value The value to assign.
+     */
+    function assignMergeValue(object, key, value) {
+      if ((value !== undefined && !eq(object[key], value)) ||
+          (typeof key == 'number' && value === undefined && !(key in object))) {
+        object[key] = value;
+      }
+    }
+
+    /**
+     * Assigns `value` to `key` of `object` if the existing value is not equivalent
+     * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * for equality comparisons.
+     *
+     * @private
+     * @param {Object} object The object to modify.
+     * @param {string} key The key of the property to assign.
+     * @param {*} value The value to assign.
+     */
+    function assignValue(object, key, value) {
+      var objValue = object[key];
+      if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||
+          (value === undefined && !(key in object))) {
+        object[key] = value;
+      }
+    }
+
+    /**
+     * Aggregates elements of `collection` on `accumulator` with keys transformed
+     * by `iteratee` and values set by `setter`.
+     *
+     * @private
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} setter The function to set `accumulator` values.
+     * @param {Function} iteratee The iteratee to transform keys.
+     * @param {Object} accumulator The initial aggregated object.
+     * @returns {Function} Returns `accumulator`.
+     */
+    function baseAggregator(collection, setter, iteratee, accumulator) {
+      baseEach(collection, function(value, key, collection) {
+        setter(accumulator, value, iteratee(value), collection);
+      });
+      return accumulator;
+    }
+
+    /**
+     * The base implementation of `_.assign` without support for multiple sources
+     * or `customizer` functions.
+     *
+     * @private
+     * @param {Object} object The destination object.
+     * @param {Object} source The source object.
+     * @returns {Object} Returns `object`.
+     */
+    function baseAssign(object, source) {
+      return object && copyObject(source, keys(source), object);
+    }
+
+    /**
+     * The base implementation of `_.at` without support for individual paths.
+     *
+     * @private
+     * @param {Object} object The object to iterate over.
+     * @param {string[]} paths The property paths of elements to pick.
+     * @returns {Array} Returns the new array of picked elements.
+     */
+    function baseAt(object, paths) {
+      var index = -1,
+          isNil = object == null,
+          length = paths.length,
+          result = Array(length);
+
+      while (++index < length) {
+        result[index] = isNil ? undefined : get(object, paths[index]);
+      }
+      return result;
+    }
+
+    /**
+     * Casts `value` to an empty array if it's not an array like object.
+     *
+     * @private
+     * @param {*} value The value to inspect.
+     * @returns {Array} Returns the array-like object.
+     */
+    function baseCastArrayLikeObject(value) {
+      return isArrayLikeObject(value) ? value : [];
+    }
+
+    /**
+     * Casts `value` to `identity` if it's not a function.
+     *
+     * @private
+     * @param {*} value The value to inspect.
+     * @returns {Array} Returns the array-like object.
+     */
+    function baseCastFunction(value) {
+      return typeof value == 'function' ? value : identity;
+    }
+
+    /**
+     * Casts `value` to a path array if it's not one.
+     *
+     * @private
+     * @param {*} value The value to inspect.
+     * @returns {Array} Returns the cast property path array.
+     */
+    function baseCastPath(value) {
+      return isArray(value) ? value : stringToPath(value);
+    }
+
+    /**
+     * The base implementation of `_.clamp` which doesn't coerce arguments to numbers.
+     *
+     * @private
+     * @param {number} number The number to clamp.
+     * @param {number} [lower] The lower bound.
+     * @param {number} upper The upper bound.
+     * @returns {number} Returns the clamped number.
+     */
+    function baseClamp(number, lower, upper) {
+      if (number === number) {
+        if (upper !== undefined) {
+          number = number <= upper ? number : upper;
+        }
+        if (lower !== undefined) {
+          number = number >= lower ? number : lower;
+        }
+      }
+      return number;
+    }
+
+    /**
+     * The base implementation of `_.clone` and `_.cloneDeep` which tracks
+     * traversed objects.
+     *
+     * @private
+     * @param {*} value The value to clone.
+     * @param {boolean} [isDeep] Specify a deep clone.
+     * @param {boolean} [isFull] Specify a clone including symbols.
+     * @param {Function} [customizer] The function to customize cloning.
+     * @param {string} [key] The key of `value`.
+     * @param {Object} [object] The parent object of `value`.
+     * @param {Object} [stack] Tracks traversed objects and their clone counterparts.
+     * @returns {*} Returns the cloned value.
+     */
+    function baseClone(value, isDeep, isFull, customizer, key, object, stack) {
+      var result;
+      if (customizer) {
+        result = object ? customizer(value, key, object, stack) : customizer(value);
+      }
+      if (result !== undefined) {
+        return result;
+      }
+      if (!isObject(value)) {
+        return value;
+      }
+      var isArr = isArray(value);
+      if (isArr) {
+        result = initCloneArray(value);
+        if (!isDeep) {
+          return copyArray(value, result);
+        }
+      } else {
+        var tag = getTag(value),
+            isFunc = tag == funcTag || tag == genTag;
+
+        if (isBuffer(value)) {
+          return cloneBuffer(value, isDeep);
+        }
+        if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
+          if (isHostObject(value)) {
+            return object ? value : {};
+          }
+          result = initCloneObject(isFunc ? {} : value);
+          if (!isDeep) {
+            result = baseAssign(result, value);
+            return isFull ? copySymbols(value, result) : result;
+          }
+        } else {
+          if (!cloneableTags[tag]) {
+            return object ? value : {};
+          }
+          result = initCloneByTag(value, tag, isDeep);
+        }
+      }
+      // Check for circular references and return its corresponding clone.
+      stack || (stack = new Stack);
+      var stacked = stack.get(value);
+      if (stacked) {
+        return stacked;
+      }
+      stack.set(value, result);
+
+      // Recursively populate clone (susceptible to call stack limits).
+      (isArr ? arrayEach : baseForOwn)(value, function(subValue, key) {
+        assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack));
+      });
+      return (isFull && !isArr) ? copySymbols(value, result) : result;
+    }
+
+    /**
+     * The base implementation of `_.conforms` which doesn't clone `source`.
+     *
+     * @private
+     * @param {Object} source The object of property predicates to conform to.
+     * @returns {Function} Returns the new function.
+     */
+    function baseConforms(source) {
+      var props = keys(source),
+          length = props.length;
+
+      return function(object) {
+        if (object == null) {
+          return !length;
+        }
+        var index = length;
+        while (index--) {
+          var key = props[index],
+              predicate = source[key],
+              value = object[key];
+
+          if ((value === undefined && !(key in Object(object))) || !predicate(value)) {
+            return false;
+          }
+        }
+        return true;
+      };
+    }
+
+    /**
+     * The base implementation of `_.create` without support for assigning
+     * properties to the created object.
+     *
+     * @private
+     * @param {Object} prototype The object to inherit from.
+     * @returns {Object} Returns the new object.
+     */
+    function baseCreate(proto) {
+      return isObject(proto) ? objectCreate(proto) : {};
+    }
+
+    /**
+     * The base implementation of `_.delay` and `_.defer` which accepts an array
+     * of `func` arguments.
+     *
+     * @private
+     * @param {Function} func The function to delay.
+     * @param {number} wait The number of milliseconds to delay invocation.
+     * @param {Object} args The arguments to provide to `func`.
+     * @returns {number} Returns the timer id.
+     */
+    function baseDelay(func, wait, args) {
+      if (typeof func != 'function') {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      return setTimeout(function() { func.apply(undefined, args); }, wait);
+    }
+
+    /**
+     * The base implementation of methods like `_.difference` without support for
+     * excluding multiple arrays or iteratee shorthands.
+     *
+     * @private
+     * @param {Array} array The array to inspect.
+     * @param {Array} values The values to exclude.
+     * @param {Function} [iteratee] The iteratee invoked per element.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns the new array of filtered values.
+     */
+    function baseDifference(array, values, iteratee, comparator) {
+      var index = -1,
+          includes = arrayIncludes,
+          isCommon = true,
+          length = array.length,
+          result = [],
+          valuesLength = values.length;
+
+      if (!length) {
+        return result;
+      }
+      if (iteratee) {
+        values = arrayMap(values, baseUnary(iteratee));
+      }
+      if (comparator) {
+        includes = arrayIncludesWith;
+        isCommon = false;
+      }
+      else if (values.length >= LARGE_ARRAY_SIZE) {
+        includes = cacheHas;
+        isCommon = false;
+        values = new SetCache(values);
+      }
+      outer:
+      while (++index < length) {
+        var value = array[index],
+            computed = iteratee ? iteratee(value) : value;
+
+        if (isCommon && computed === computed) {
+          var valuesIndex = valuesLength;
+          while (valuesIndex--) {
+            if (values[valuesIndex] === computed) {
+              continue outer;
+            }
+          }
+          result.push(value);
+        }
+        else if (!includes(values, computed, comparator)) {
+          result.push(value);
+        }
+      }
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.forEach` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} iteratee The function invoked per iteration.
+     * @returns {Array|Object} Returns `collection`.
+     */
+    var baseEach = createBaseEach(baseForOwn);
+
+    /**
+     * The base implementation of `_.forEachRight` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} iteratee The function invoked per iteration.
+     * @returns {Array|Object} Returns `collection`.
+     */
+    var baseEachRight = createBaseEach(baseForOwnRight, true);
+
+    /**
+     * The base implementation of `_.every` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} predicate The function invoked per iteration.
+     * @returns {boolean} Returns `true` if all elements pass the predicate check, else `false`
+     */
+    function baseEvery(collection, predicate) {
+      var result = true;
+      baseEach(collection, function(value, index, collection) {
+        result = !!predicate(value, index, collection);
+        return result;
+      });
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.fill` without an iteratee call guard.
+     *
+     * @private
+     * @param {Array} array The array to fill.
+     * @param {*} value The value to fill `array` with.
+     * @param {number} [start=0] The start position.
+     * @param {number} [end=array.length] The end position.
+     * @returns {Array} Returns `array`.
+     */
+    function baseFill(array, value, start, end) {
+      var length = array.length;
+
+      start = toInteger(start);
+      if (start < 0) {
+        start = -start > length ? 0 : (length + start);
+      }
+      end = (end === undefined || end > length) ? length : toInteger(end);
+      if (end < 0) {
+        end += length;
+      }
+      end = start > end ? 0 : toLength(end);
+      while (start < end) {
+        array[start++] = value;
+      }
+      return array;
+    }
+
+    /**
+     * The base implementation of `_.filter` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} predicate The function invoked per iteration.
+     * @returns {Array} Returns the new filtered array.
+     */
+    function baseFilter(collection, predicate) {
+      var result = [];
+      baseEach(collection, function(value, index, collection) {
+        if (predicate(value, index, collection)) {
+          result.push(value);
+        }
+      });
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.flatten` with support for restricting flattening.
+     *
+     * @private
+     * @param {Array} array The array to flatten.
+     * @param {number} depth The maximum recursion depth.
+     * @param {boolean} [isStrict] Restrict flattening to arrays-like objects.
+     * @param {Array} [result=[]] The initial result value.
+     * @returns {Array} Returns the new flattened array.
+     */
+    function baseFlatten(array, depth, isStrict, result) {
+      result || (result = []);
+
+      var index = -1,
+          length = array.length;
+
+      while (++index < length) {
+        var value = array[index];
+        if (depth > 0 && isArrayLikeObject(value) &&
+            (isStrict || isArray(value) || isArguments(value))) {
+          if (depth > 1) {
+            // Recursively flatten arrays (susceptible to call stack limits).
+            baseFlatten(value, depth - 1, isStrict, result);
+          } else {
+            arrayPush(result, value);
+          }
+        } else if (!isStrict) {
+          result[result.length] = value;
+        }
+      }
+      return result;
+    }
+
+    /**
+     * The base implementation of `baseForIn` and `baseForOwn` which iterates
+     * over `object` properties returned by `keysFunc` invoking `iteratee` for
+     * each property. Iteratee functions may exit iteration early by explicitly
+     * returning `false`.
+     *
+     * @private
+     * @param {Object} object The object to iterate over.
+     * @param {Function} iteratee The function invoked per iteration.
+     * @param {Function} keysFunc The function to get the keys of `object`.
+     * @returns {Object} Returns `object`.
+     */
+    var baseFor = createBaseFor();
+
+    /**
+     * This function is like `baseFor` except that it iterates over properties
+     * in the opposite order.
+     *
+     * @private
+     * @param {Object} object The object to iterate over.
+     * @param {Function} iteratee The function invoked per iteration.
+     * @param {Function} keysFunc The function to get the keys of `object`.
+     * @returns {Object} Returns `object`.
+     */
+    var baseForRight = createBaseFor(true);
+
+    /**
+     * The base implementation of `_.forIn` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Object} object The object to iterate over.
+     * @param {Function} iteratee The function invoked per iteration.
+     * @returns {Object} Returns `object`.
+     */
+    function baseForIn(object, iteratee) {
+      return object == null ? object : baseFor(object, iteratee, keysIn);
+    }
+
+    /**
+     * The base implementation of `_.forOwn` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Object} object The object to iterate over.
+     * @param {Function} iteratee The function invoked per iteration.
+     * @returns {Object} Returns `object`.
+     */
+    function baseForOwn(object, iteratee) {
+      return object && baseFor(object, iteratee, keys);
+    }
+
+    /**
+     * The base implementation of `_.forOwnRight` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Object} object The object to iterate over.
+     * @param {Function} iteratee The function invoked per iteration.
+     * @returns {Object} Returns `object`.
+     */
+    function baseForOwnRight(object, iteratee) {
+      return object && baseForRight(object, iteratee, keys);
+    }
+
+    /**
+     * The base implementation of `_.functions` which creates an array of
+     * `object` function property names filtered from `props`.
+     *
+     * @private
+     * @param {Object} object The object to inspect.
+     * @param {Array} props The property names to filter.
+     * @returns {Array} Returns the new array of filtered property names.
+     */
+    function baseFunctions(object, props) {
+      return arrayFilter(props, function(key) {
+        return isFunction(object[key]);
+      });
+    }
+
+    /**
+     * The base implementation of `_.get` without support for default values.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path of the property to get.
+     * @returns {*} Returns the resolved value.
+     */
+    function baseGet(object, path) {
+      path = isKey(path, object) ? [path + ''] : baseCastPath(path);
+
+      var index = 0,
+          length = path.length;
+
+      while (object != null && index < length) {
+        object = object[path[index++]];
+      }
+      return (index && index == length) ? object : undefined;
+    }
+
+    /**
+     * The base implementation of `_.has` without support for deep paths.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @param {Array|string} key The key to check.
+     * @returns {boolean} Returns `true` if `key` exists, else `false`.
+     */
+    function baseHas(object, key) {
+      // Avoid a bug in IE 10-11 where objects with a [[Prototype]] of `null`,
+      // that are composed entirely of index properties, return `false` for
+      // `hasOwnProperty` checks of them.
+      return hasOwnProperty.call(object, key) ||
+        (typeof object == 'object' && key in object && getPrototypeOf(object) === null);
+    }
+
+    /**
+     * The base implementation of `_.hasIn` without support for deep paths.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @param {Array|string} key The key to check.
+     * @returns {boolean} Returns `true` if `key` exists, else `false`.
+     */
+    function baseHasIn(object, key) {
+      return key in Object(object);
+    }
+
+    /**
+     * The base implementation of `_.inRange` which doesn't coerce arguments to numbers.
+     *
+     * @private
+     * @param {number} number The number to check.
+     * @param {number} start The start of the range.
+     * @param {number} end The end of the range.
+     * @returns {boolean} Returns `true` if `number` is in the range, else `false`.
+     */
+    function baseInRange(number, start, end) {
+      return number >= nativeMin(start, end) && number < nativeMax(start, end);
+    }
+
+    /**
+     * The base implementation of methods like `_.intersection`, without support
+     * for iteratee shorthands, that accepts an array of arrays to inspect.
+     *
+     * @private
+     * @param {Array} arrays The arrays to inspect.
+     * @param {Function} [iteratee] The iteratee invoked per element.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns the new array of shared values.
+     */
+    function baseIntersection(arrays, iteratee, comparator) {
+      var includes = comparator ? arrayIncludesWith : arrayIncludes,
+          length = arrays[0].length,
+          othLength = arrays.length,
+          othIndex = othLength,
+          caches = Array(othLength),
+          maxLength = Infinity,
+          result = [];
+
+      while (othIndex--) {
+        var array = arrays[othIndex];
+        if (othIndex && iteratee) {
+          array = arrayMap(array, baseUnary(iteratee));
+        }
+        maxLength = nativeMin(array.length, maxLength);
+        caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120))
+          ? new SetCache(othIndex && array)
+          : undefined;
+      }
+      array = arrays[0];
+
+      var index = -1,
+          seen = caches[0];
+
+      outer:
+      while (++index < length && result.length < maxLength) {
+        var value = array[index],
+            computed = iteratee ? iteratee(value) : value;
+
+        if (!(seen
+              ? cacheHas(seen, computed)
+              : includes(result, computed, comparator)
+            )) {
+          othIndex = othLength;
+          while (--othIndex) {
+            var cache = caches[othIndex];
+            if (!(cache
+                  ? cacheHas(cache, computed)
+                  : includes(arrays[othIndex], computed, comparator))
+                ) {
+              continue outer;
+            }
+          }
+          if (seen) {
+            seen.push(computed);
+          }
+          result.push(value);
+        }
+      }
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.invert` and `_.invertBy` which inverts
+     * `object` with values transformed by `iteratee` and set by `setter`.
+     *
+     * @private
+     * @param {Object} object The object to iterate over.
+     * @param {Function} setter The function to set `accumulator` values.
+     * @param {Function} iteratee The iteratee to transform values.
+     * @param {Object} accumulator The initial inverted object.
+     * @returns {Function} Returns `accumulator`.
+     */
+    function baseInverter(object, setter, iteratee, accumulator) {
+      baseForOwn(object, function(value, key, object) {
+        setter(accumulator, iteratee(value), key, object);
+      });
+      return accumulator;
+    }
+
+    /**
+     * The base implementation of `_.invoke` without support for individual
+     * method arguments.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path of the method to invoke.
+     * @param {Array} args The arguments to invoke the method with.
+     * @returns {*} Returns the result of the invoked method.
+     */
+    function baseInvoke(object, path, args) {
+      if (!isKey(path, object)) {
+        path = baseCastPath(path);
+        object = parent(object, path);
+        path = last(path);
+      }
+      var func = object == null ? object : object[path];
+      return func == null ? undefined : apply(func, object, args);
+    }
+
+    /**
+     * The base implementation of `_.isEqual` which supports partial comparisons
+     * and tracks traversed objects.
+     *
+     * @private
+     * @param {*} value The value to compare.
+     * @param {*} other The other value to compare.
+     * @param {Function} [customizer] The function to customize comparisons.
+     * @param {boolean} [bitmask] The bitmask of comparison flags.
+     *  The bitmask may be composed of the following flags:
+     *     1 - Unordered comparison
+     *     2 - Partial comparison
+     * @param {Object} [stack] Tracks traversed `value` and `other` objects.
+     * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
+     */
+    function baseIsEqual(value, other, customizer, bitmask, stack) {
+      if (value === other) {
+        return true;
+      }
+      if (value == null || other == null || (!isObject(value) && !isObjectLike(other))) {
+        return value !== value && other !== other;
+      }
+      return baseIsEqualDeep(value, other, baseIsEqual, customizer, bitmask, stack);
+    }
+
+    /**
+     * A specialized version of `baseIsEqual` for arrays and objects which performs
+     * deep comparisons and tracks traversed objects enabling objects with circular
+     * references to be compared.
+     *
+     * @private
+     * @param {Object} object The object to compare.
+     * @param {Object} other The other object to compare.
+     * @param {Function} equalFunc The function to determine equivalents of values.
+     * @param {Function} [customizer] The function to customize comparisons.
+     * @param {number} [bitmask] The bitmask of comparison flags. See `baseIsEqual` for more details.
+     * @param {Object} [stack] Tracks traversed `object` and `other` objects.
+     * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
+     */
+    function baseIsEqualDeep(object, other, equalFunc, customizer, bitmask, stack) {
+      var objIsArr = isArray(object),
+          othIsArr = isArray(other),
+          objTag = arrayTag,
+          othTag = arrayTag;
+
+      if (!objIsArr) {
+        objTag = getTag(object);
+        objTag = objTag == argsTag ? objectTag : objTag;
+      }
+      if (!othIsArr) {
+        othTag = getTag(other);
+        othTag = othTag == argsTag ? objectTag : othTag;
+      }
+      var objIsObj = objTag == objectTag && !isHostObject(object),
+          othIsObj = othTag == objectTag && !isHostObject(other),
+          isSameTag = objTag == othTag;
+
+      if (isSameTag && !objIsObj) {
+        stack || (stack = new Stack);
+        return (objIsArr || isTypedArray(object))
+          ? equalArrays(object, other, equalFunc, customizer, bitmask, stack)
+          : equalByTag(object, other, objTag, equalFunc, customizer, bitmask, stack);
+      }
+      if (!(bitmask & PARTIAL_COMPARE_FLAG)) {
+        var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),
+            othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__');
+
+        if (objIsWrapped || othIsWrapped) {
+          stack || (stack = new Stack);
+          return equalFunc(objIsWrapped ? object.value() : object, othIsWrapped ? other.value() : other, customizer, bitmask, stack);
+        }
+      }
+      if (!isSameTag) {
+        return false;
+      }
+      stack || (stack = new Stack);
+      return equalObjects(object, other, equalFunc, customizer, bitmask, stack);
+    }
+
+    /**
+     * The base implementation of `_.isMatch` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Object} object The object to inspect.
+     * @param {Object} source The object of property values to match.
+     * @param {Array} matchData The property names, values, and compare flags to match.
+     * @param {Function} [customizer] The function to customize comparisons.
+     * @returns {boolean} Returns `true` if `object` is a match, else `false`.
+     */
+    function baseIsMatch(object, source, matchData, customizer) {
+      var index = matchData.length,
+          length = index,
+          noCustomizer = !customizer;
+
+      if (object == null) {
+        return !length;
+      }
+      object = Object(object);
+      while (index--) {
+        var data = matchData[index];
+        if ((noCustomizer && data[2])
+              ? data[1] !== object[data[0]]
+              : !(data[0] in object)
+            ) {
+          return false;
+        }
+      }
+      while (++index < length) {
+        data = matchData[index];
+        var key = data[0],
+            objValue = object[key],
+            srcValue = data[1];
+
+        if (noCustomizer && data[2]) {
+          if (objValue === undefined && !(key in object)) {
+            return false;
+          }
+        } else {
+          var stack = new Stack,
+              result = customizer ? customizer(objValue, srcValue, key, object, source, stack) : undefined;
+
+          if (!(result === undefined
+                ? baseIsEqual(srcValue, objValue, customizer, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG, stack)
+                : result
+              )) {
+            return false;
+          }
+        }
+      }
+      return true;
+    }
+
+    /**
+     * The base implementation of `_.iteratee`.
+     *
+     * @private
+     * @param {*} [value=_.identity] The value to convert to an iteratee.
+     * @returns {Function} Returns the iteratee.
+     */
+    function baseIteratee(value) {
+      var type = typeof value;
+      if (type == 'function') {
+        return value;
+      }
+      if (value == null) {
+        return identity;
+      }
+      if (type == 'object') {
+        return isArray(value)
+          ? baseMatchesProperty(value[0], value[1])
+          : baseMatches(value);
+      }
+      return property(value);
+    }
+
+    /**
+     * The base implementation of `_.keys` which doesn't skip the constructor
+     * property of prototypes or treat sparse arrays as dense.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the array of property names.
+     */
+    function baseKeys(object) {
+      return nativeKeys(Object(object));
+    }
+
+    /**
+     * The base implementation of `_.keysIn` which doesn't skip the constructor
+     * property of prototypes or treat sparse arrays as dense.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the array of property names.
+     */
+    function baseKeysIn(object) {
+      object = object == null ? object : Object(object);
+
+      var result = [];
+      for (var key in object) {
+        result.push(key);
+      }
+      return result;
+    }
+
+    // Fallback for IE < 9 with es6-shim.
+    if (enumerate && !propertyIsEnumerable.call({ 'valueOf': 1 }, 'valueOf')) {
+      baseKeysIn = function(object) {
+        return iteratorToArray(enumerate(object));
+      };
+    }
+
+    /**
+     * The base implementation of `_.map` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} iteratee The function invoked per iteration.
+     * @returns {Array} Returns the new mapped array.
+     */
+    function baseMap(collection, iteratee) {
+      var index = -1,
+          result = isArrayLike(collection) ? Array(collection.length) : [];
+
+      baseEach(collection, function(value, key, collection) {
+        result[++index] = iteratee(value, key, collection);
+      });
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.matches` which doesn't clone `source`.
+     *
+     * @private
+     * @param {Object} source The object of property values to match.
+     * @returns {Function} Returns the new function.
+     */
+    function baseMatches(source) {
+      var matchData = getMatchData(source);
+      if (matchData.length == 1 && matchData[0][2]) {
+        var key = matchData[0][0],
+            value = matchData[0][1];
+
+        return function(object) {
+          if (object == null) {
+            return false;
+          }
+          return object[key] === value &&
+            (value !== undefined || (key in Object(object)));
+        };
+      }
+      return function(object) {
+        return object === source || baseIsMatch(object, source, matchData);
+      };
+    }
+
+    /**
+     * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`.
+     *
+     * @private
+     * @param {string} path The path of the property to get.
+     * @param {*} srcValue The value to match.
+     * @returns {Function} Returns the new function.
+     */
+    function baseMatchesProperty(path, srcValue) {
+      return function(object) {
+        var objValue = get(object, path);
+        return (objValue === undefined && objValue === srcValue)
+          ? hasIn(object, path)
+          : baseIsEqual(srcValue, objValue, undefined, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG);
+      };
+    }
+
+    /**
+     * The base implementation of `_.merge` without support for multiple sources.
+     *
+     * @private
+     * @param {Object} object The destination object.
+     * @param {Object} source The source object.
+     * @param {number} srcIndex The index of `source`.
+     * @param {Function} [customizer] The function to customize merged values.
+     * @param {Object} [stack] Tracks traversed source values and their merged counterparts.
+     */
+    function baseMerge(object, source, srcIndex, customizer, stack) {
+      if (object === source) {
+        return;
+      }
+      var props = (isArray(source) || isTypedArray(source))
+        ? undefined
+        : keysIn(source);
+
+      arrayEach(props || source, function(srcValue, key) {
+        if (props) {
+          key = srcValue;
+          srcValue = source[key];
+        }
+        if (isObject(srcValue)) {
+          stack || (stack = new Stack);
+          baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
+        }
+        else {
+          var newValue = customizer
+            ? customizer(object[key], srcValue, (key + ''), object, source, stack)
+            : undefined;
+
+          if (newValue === undefined) {
+            newValue = srcValue;
+          }
+          assignMergeValue(object, key, newValue);
+        }
+      });
+    }
+
+    /**
+     * A specialized version of `baseMerge` for arrays and objects which performs
+     * deep merges and tracks traversed objects enabling objects with circular
+     * references to be merged.
+     *
+     * @private
+     * @param {Object} object The destination object.
+     * @param {Object} source The source object.
+     * @param {string} key The key of the value to merge.
+     * @param {number} srcIndex The index of `source`.
+     * @param {Function} mergeFunc The function to merge values.
+     * @param {Function} [customizer] The function to customize assigned values.
+     * @param {Object} [stack] Tracks traversed source values and their merged counterparts.
+     */
+    function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {
+      var objValue = object[key],
+          srcValue = source[key],
+          stacked = stack.get(srcValue);
+
+      if (stacked) {
+        assignMergeValue(object, key, stacked);
+        return;
+      }
+      var newValue = customizer
+        ? customizer(objValue, srcValue, (key + ''), object, source, stack)
+        : undefined;
+
+      var isCommon = newValue === undefined;
+
+      if (isCommon) {
+        newValue = srcValue;
+        if (isArray(srcValue) || isTypedArray(srcValue)) {
+          if (isArray(objValue)) {
+            newValue = objValue;
+          }
+          else if (isArrayLikeObject(objValue)) {
+            newValue = copyArray(objValue);
+          }
+          else {
+            isCommon = false;
+            newValue = baseClone(srcValue, !customizer);
+          }
+        }
+        else if (isPlainObject(srcValue) || isArguments(srcValue)) {
+          if (isArguments(objValue)) {
+            newValue = toPlainObject(objValue);
+          }
+          else if (!isObject(objValue) || (srcIndex && isFunction(objValue))) {
+            isCommon = false;
+            newValue = baseClone(srcValue, !customizer);
+          }
+          else {
+            newValue = objValue;
+          }
+        }
+        else {
+          isCommon = false;
+        }
+      }
+      stack.set(srcValue, newValue);
+
+      if (isCommon) {
+        // Recursively merge objects and arrays (susceptible to call stack limits).
+        mergeFunc(newValue, srcValue, srcIndex, customizer, stack);
+      }
+      stack['delete'](srcValue);
+      assignMergeValue(object, key, newValue);
+    }
+
+    /**
+     * The base implementation of `_.orderBy` without param guards.
+     *
+     * @private
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by.
+     * @param {string[]} orders The sort orders of `iteratees`.
+     * @returns {Array} Returns the new sorted array.
+     */
+    function baseOrderBy(collection, iteratees, orders) {
+      var index = -1;
+      iteratees = arrayMap(iteratees.length ? iteratees : Array(1), getIteratee());
+
+      var result = baseMap(collection, function(value, key, collection) {
+        var criteria = arrayMap(iteratees, function(iteratee) {
+          return iteratee(value);
+        });
+        return { 'criteria': criteria, 'index': ++index, 'value': value };
+      });
+
+      return baseSortBy(result, function(object, other) {
+        return compareMultiple(object, other, orders);
+      });
+    }
+
+    /**
+     * The base implementation of `_.pick` without support for individual
+     * property names.
+     *
+     * @private
+     * @param {Object} object The source object.
+     * @param {string[]} props The property names to pick.
+     * @returns {Object} Returns the new object.
+     */
+    function basePick(object, props) {
+      object = Object(object);
+      return arrayReduce(props, function(result, key) {
+        if (key in object) {
+          result[key] = object[key];
+        }
+        return result;
+      }, {});
+    }
+
+    /**
+     * The base implementation of  `_.pickBy` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Object} object The source object.
+     * @param {Function} predicate The function invoked per property.
+     * @returns {Object} Returns the new object.
+     */
+    function basePickBy(object, predicate) {
+      var result = {};
+      baseForIn(object, function(value, key) {
+        if (predicate(value, key)) {
+          result[key] = value;
+        }
+      });
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.property` without support for deep paths.
+     *
+     * @private
+     * @param {string} key The key of the property to get.
+     * @returns {Function} Returns the new function.
+     */
+    function baseProperty(key) {
+      return function(object) {
+        return object == null ? undefined : object[key];
+      };
+    }
+
+    /**
+     * A specialized version of `baseProperty` which supports deep paths.
+     *
+     * @private
+     * @param {Array|string} path The path of the property to get.
+     * @returns {Function} Returns the new function.
+     */
+    function basePropertyDeep(path) {
+      return function(object) {
+        return baseGet(object, path);
+      };
+    }
+
+    /**
+     * The base implementation of `_.pullAllBy` without support for iteratee
+     * shorthands.
+     *
+     * @private
+     * @param {Array} array The array to modify.
+     * @param {Array} values The values to remove.
+     * @param {Function} [iteratee] The iteratee invoked per element.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns `array`.
+     */
+    function basePullAll(array, values, iteratee, comparator) {
+      var indexOf = comparator ? baseIndexOfWith : baseIndexOf,
+          index = -1,
+          length = values.length,
+          seen = array;
+
+      if (iteratee) {
+        seen = arrayMap(array, baseUnary(iteratee));
+      }
+      while (++index < length) {
+        var fromIndex = 0,
+            value = values[index],
+            computed = iteratee ? iteratee(value) : value;
+
+        while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) {
+          if (seen !== array) {
+            splice.call(seen, fromIndex, 1);
+          }
+          splice.call(array, fromIndex, 1);
+        }
+      }
+      return array;
+    }
+
+    /**
+     * The base implementation of `_.pullAt` without support for individual
+     * indexes or capturing the removed elements.
+     *
+     * @private
+     * @param {Array} array The array to modify.
+     * @param {number[]} indexes The indexes of elements to remove.
+     * @returns {Array} Returns `array`.
+     */
+    function basePullAt(array, indexes) {
+      var length = array ? indexes.length : 0,
+          lastIndex = length - 1;
+
+      while (length--) {
+        var index = indexes[length];
+        if (lastIndex == length || index != previous) {
+          var previous = index;
+          if (isIndex(index)) {
+            splice.call(array, index, 1);
+          }
+          else if (!isKey(index, array)) {
+            var path = baseCastPath(index),
+                object = parent(array, path);
+
+            if (object != null) {
+              delete object[last(path)];
+            }
+          }
+          else {
+            delete array[index];
+          }
+        }
+      }
+      return array;
+    }
+
+    /**
+     * The base implementation of `_.random` without support for returning
+     * floating-point numbers.
+     *
+     * @private
+     * @param {number} lower The lower bound.
+     * @param {number} upper The upper bound.
+     * @returns {number} Returns the random number.
+     */
+    function baseRandom(lower, upper) {
+      return lower + nativeFloor(nativeRandom() * (upper - lower + 1));
+    }
+
+    /**
+     * The base implementation of `_.range` and `_.rangeRight` which doesn't
+     * coerce arguments to numbers.
+     *
+     * @private
+     * @param {number} start The start of the range.
+     * @param {number} end The end of the range.
+     * @param {number} step The value to increment or decrement by.
+     * @param {boolean} [fromRight] Specify iterating from right to left.
+     * @returns {Array} Returns the new array of numbers.
+     */
+    function baseRange(start, end, step, fromRight) {
+      var index = -1,
+          length = nativeMax(nativeCeil((end - start) / (step || 1)), 0),
+          result = Array(length);
+
+      while (length--) {
+        result[fromRight ? length : ++index] = start;
+        start += step;
+      }
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.set`.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path of the property to set.
+     * @param {*} value The value to set.
+     * @param {Function} [customizer] The function to customize path creation.
+     * @returns {Object} Returns `object`.
+     */
+    function baseSet(object, path, value, customizer) {
+      path = isKey(path, object) ? [path + ''] : baseCastPath(path);
+
+      var index = -1,
+          length = path.length,
+          lastIndex = length - 1,
+          nested = object;
+
+      while (nested != null && ++index < length) {
+        var key = path[index];
+        if (isObject(nested)) {
+          var newValue = value;
+          if (index != lastIndex) {
+            var objValue = nested[key];
+            newValue = customizer ? customizer(objValue, key, nested) : undefined;
+            if (newValue === undefined) {
+              newValue = objValue == null
+                ? (isIndex(path[index + 1]) ? [] : {})
+                : objValue;
+            }
+          }
+          assignValue(nested, key, newValue);
+        }
+        nested = nested[key];
+      }
+      return object;
+    }
+
+    /**
+     * The base implementation of `setData` without support for hot loop detection.
+     *
+     * @private
+     * @param {Function} func The function to associate metadata with.
+     * @param {*} data The metadata.
+     * @returns {Function} Returns `func`.
+     */
+    var baseSetData = !metaMap ? identity : function(func, data) {
+      metaMap.set(func, data);
+      return func;
+    };
+
+    /**
+     * The base implementation of `_.slice` without an iteratee call guard.
+     *
+     * @private
+     * @param {Array} array The array to slice.
+     * @param {number} [start=0] The start position.
+     * @param {number} [end=array.length] The end position.
+     * @returns {Array} Returns the slice of `array`.
+     */
+    function baseSlice(array, start, end) {
+      var index = -1,
+          length = array.length;
+
+      if (start < 0) {
+        start = -start > length ? 0 : (length + start);
+      }
+      end = end > length ? length : end;
+      if (end < 0) {
+        end += length;
+      }
+      length = start > end ? 0 : ((end - start) >>> 0);
+      start >>>= 0;
+
+      var result = Array(length);
+      while (++index < length) {
+        result[index] = array[index + start];
+      }
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.some` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} predicate The function invoked per iteration.
+     * @returns {boolean} Returns `true` if any element passes the predicate check, else `false`.
+     */
+    function baseSome(collection, predicate) {
+      var result;
+
+      baseEach(collection, function(value, index, collection) {
+        result = predicate(value, index, collection);
+        return !result;
+      });
+      return !!result;
+    }
+
+    /**
+     * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which
+     * performs a binary search of `array` to determine the index at which `value`
+     * should be inserted into `array` in order to maintain its sort order.
+     *
+     * @private
+     * @param {Array} array The sorted array to inspect.
+     * @param {*} value The value to evaluate.
+     * @param {boolean} [retHighest] Specify returning the highest qualified index.
+     * @returns {number} Returns the index at which `value` should be inserted
+     *  into `array`.
+     */
+    function baseSortedIndex(array, value, retHighest) {
+      var low = 0,
+          high = array ? array.length : low;
+
+      if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) {
+        while (low < high) {
+          var mid = (low + high) >>> 1,
+              computed = array[mid];
+
+          if ((retHighest ? (computed <= value) : (computed < value)) && computed !== null) {
+            low = mid + 1;
+          } else {
+            high = mid;
+          }
+        }
+        return high;
+      }
+      return baseSortedIndexBy(array, value, identity, retHighest);
+    }
+
+    /**
+     * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy`
+     * which invokes `iteratee` for `value` and each element of `array` to compute
+     * their sort ranking. The iteratee is invoked with one argument; (value).
+     *
+     * @private
+     * @param {Array} array The sorted array to inspect.
+     * @param {*} value The value to evaluate.
+     * @param {Function} iteratee The iteratee invoked per element.
+     * @param {boolean} [retHighest] Specify returning the highest qualified index.
+     * @returns {number} Returns the index at which `value` should be inserted into `array`.
+     */
+    function baseSortedIndexBy(array, value, iteratee, retHighest) {
+      value = iteratee(value);
+
+      var low = 0,
+          high = array ? array.length : 0,
+          valIsNaN = value !== value,
+          valIsNull = value === null,
+          valIsUndef = value === undefined;
+
+      while (low < high) {
+        var mid = nativeFloor((low + high) / 2),
+            computed = iteratee(array[mid]),
+            isDef = computed !== undefined,
+            isReflexive = computed === computed;
+
+        if (valIsNaN) {
+          var setLow = isReflexive || retHighest;
+        } else if (valIsNull) {
+          setLow = isReflexive && isDef && (retHighest || computed != null);
+        } else if (valIsUndef) {
+          setLow = isReflexive && (retHighest || isDef);
+        } else if (computed == null) {
+          setLow = false;
+        } else {
+          setLow = retHighest ? (computed <= value) : (computed < value);
+        }
+        if (setLow) {
+          low = mid + 1;
+        } else {
+          high = mid;
+        }
+      }
+      return nativeMin(high, MAX_ARRAY_INDEX);
+    }
+
+    /**
+     * The base implementation of `_.sortedUniq`.
+     *
+     * @private
+     * @param {Array} array The array to inspect.
+     * @returns {Array} Returns the new duplicate free array.
+     */
+    function baseSortedUniq(array) {
+      return baseSortedUniqBy(array);
+    }
+
+    /**
+     * The base implementation of `_.sortedUniqBy` without support for iteratee
+     * shorthands.
+     *
+     * @private
+     * @param {Array} array The array to inspect.
+     * @param {Function} [iteratee] The iteratee invoked per element.
+     * @returns {Array} Returns the new duplicate free array.
+     */
+    function baseSortedUniqBy(array, iteratee) {
+      var index = 0,
+          length = array.length,
+          value = array[0],
+          computed = iteratee ? iteratee(value) : value,
+          seen = computed,
+          resIndex = 1,
+          result = [value];
+
+      while (++index < length) {
+        value = array[index],
+        computed = iteratee ? iteratee(value) : value;
+
+        if (!eq(computed, seen)) {
+          seen = computed;
+          result[resIndex++] = value;
+        }
+      }
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.uniqBy` without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Array} array The array to inspect.
+     * @param {Function} [iteratee] The iteratee invoked per element.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns the new duplicate free array.
+     */
+    function baseUniq(array, iteratee, comparator) {
+      var index = -1,
+          includes = arrayIncludes,
+          length = array.length,
+          isCommon = true,
+          result = [],
+          seen = result;
+
+      if (comparator) {
+        isCommon = false;
+        includes = arrayIncludesWith;
+      }
+      else if (length >= LARGE_ARRAY_SIZE) {
+        var set = iteratee ? null : createSet(array);
+        if (set) {
+          return setToArray(set);
+        }
+        isCommon = false;
+        includes = cacheHas;
+        seen = new SetCache;
+      }
+      else {
+        seen = iteratee ? [] : result;
+      }
+      outer:
+      while (++index < length) {
+        var value = array[index],
+            computed = iteratee ? iteratee(value) : value;
+
+        if (isCommon && computed === computed) {
+          var seenIndex = seen.length;
+          while (seenIndex--) {
+            if (seen[seenIndex] === computed) {
+              continue outer;
+            }
+          }
+          if (iteratee) {
+            seen.push(computed);
+          }
+          result.push(value);
+        }
+        else if (!includes(seen, computed, comparator)) {
+          if (seen !== result) {
+            seen.push(computed);
+          }
+          result.push(value);
+        }
+      }
+      return result;
+    }
+
+    /**
+     * The base implementation of `_.unset`.
+     *
+     * @private
+     * @param {Object} object The object to modify.
+     * @param {Array|string} path The path of the property to unset.
+     * @returns {boolean} Returns `true` if the property is deleted, else `false`.
+     */
+    function baseUnset(object, path) {
+      path = isKey(path, object) ? [path + ''] : baseCastPath(path);
+      object = parent(object, path);
+      var key = last(path);
+      return (object != null && has(object, key)) ? delete object[key] : true;
+    }
+
+    /**
+     * The base implementation of `_.update`.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path of the property to update.
+     * @param {Function} updater The function to produce the updated value.
+     * @param {Function} [customizer] The function to customize path creation.
+     * @returns {Object} Returns `object`.
+     */
+    function baseUpdate(object, path, updater, customizer) {
+      return baseSet(object, path, updater(baseGet(object, path)), customizer);
+    }
+
+    /**
+     * The base implementation of methods like `_.dropWhile` and `_.takeWhile`
+     * without support for iteratee shorthands.
+     *
+     * @private
+     * @param {Array} array The array to query.
+     * @param {Function} predicate The function invoked per iteration.
+     * @param {boolean} [isDrop] Specify dropping elements instead of taking them.
+     * @param {boolean} [fromRight] Specify iterating from right to left.
+     * @returns {Array} Returns the slice of `array`.
+     */
+    function baseWhile(array, predicate, isDrop, fromRight) {
+      var length = array.length,
+          index = fromRight ? length : -1;
+
+      while ((fromRight ? index-- : ++index < length) &&
+        predicate(array[index], index, array)) {}
+
+      return isDrop
+        ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length))
+        : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index));
+    }
+
+    /**
+     * The base implementation of `wrapperValue` which returns the result of
+     * performing a sequence of actions on the unwrapped `value`, where each
+     * successive action is supplied the return value of the previous.
+     *
+     * @private
+     * @param {*} value The unwrapped value.
+     * @param {Array} actions Actions to perform to resolve the unwrapped value.
+     * @returns {*} Returns the resolved value.
+     */
+    function baseWrapperValue(value, actions) {
+      var result = value;
+      if (result instanceof LazyWrapper) {
+        result = result.value();
+      }
+      return arrayReduce(actions, function(result, action) {
+        return action.func.apply(action.thisArg, arrayPush([result], action.args));
+      }, result);
+    }
+
+    /**
+     * The base implementation of methods like `_.xor`, without support for
+     * iteratee shorthands, that accepts an array of arrays to inspect.
+     *
+     * @private
+     * @param {Array} arrays The arrays to inspect.
+     * @param {Function} [iteratee] The iteratee invoked per element.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns the new array of values.
+     */
+    function baseXor(arrays, iteratee, comparator) {
+      var index = -1,
+          length = arrays.length;
+
+      while (++index < length) {
+        var result = result
+          ? arrayPush(
+              baseDifference(result, arrays[index], iteratee, comparator),
+              baseDifference(arrays[index], result, iteratee, comparator)
+            )
+          : arrays[index];
+      }
+      return (result && result.length) ? baseUniq(result, iteratee, comparator) : [];
+    }
+
+    /**
+     * This base implementation of `_.zipObject` which assigns values using `assignFunc`.
+     *
+     * @private
+     * @param {Array} props The property names.
+     * @param {Array} values The property values.
+     * @param {Function} assignFunc The function to assign values.
+     * @returns {Object} Returns the new object.
+     */
+    function baseZipObject(props, values, assignFunc) {
+      var index = -1,
+          length = props.length,
+          valsLength = values.length,
+          result = {};
+
+      while (++index < length) {
+        assignFunc(result, props[index], index < valsLength ? values[index] : undefined);
+      }
+      return result;
+    }
+
+    /**
+     * Creates a clone of  `buffer`.
+     *
+     * @private
+     * @param {Buffer} buffer The buffer to clone.
+     * @param {boolean} [isDeep] Specify a deep clone.
+     * @returns {Buffer} Returns the cloned buffer.
+     */
+    function cloneBuffer(buffer, isDeep) {
+      if (isDeep) {
+        return buffer.slice();
+      }
+      var result = new buffer.constructor(buffer.length);
+      buffer.copy(result);
+      return result;
+    }
+
+    /**
+     * Creates a clone of `arrayBuffer`.
+     *
+     * @private
+     * @param {ArrayBuffer} arrayBuffer The array buffer to clone.
+     * @returns {ArrayBuffer} Returns the cloned array buffer.
+     */
+    function cloneArrayBuffer(arrayBuffer) {
+      var result = new arrayBuffer.constructor(arrayBuffer.byteLength);
+      new Uint8Array(result).set(new Uint8Array(arrayBuffer));
+      return result;
+    }
+
+    /**
+     * Creates a clone of `map`.
+     *
+     * @private
+     * @param {Object} map The map to clone.
+     * @returns {Object} Returns the cloned map.
+     */
+    function cloneMap(map) {
+      return arrayReduce(mapToArray(map), addMapEntry, new map.constructor);
+    }
+
+    /**
+     * Creates a clone of `regexp`.
+     *
+     * @private
+     * @param {Object} regexp The regexp to clone.
+     * @returns {Object} Returns the cloned regexp.
+     */
+    function cloneRegExp(regexp) {
+      var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
+      result.lastIndex = regexp.lastIndex;
+      return result;
+    }
+
+    /**
+     * Creates a clone of `set`.
+     *
+     * @private
+     * @param {Object} set The set to clone.
+     * @returns {Object} Returns the cloned set.
+     */
+    function cloneSet(set) {
+      return arrayReduce(setToArray(set), addSetEntry, new set.constructor);
+    }
+
+    /**
+     * Creates a clone of the `symbol` object.
+     *
+     * @private
+     * @param {Object} symbol The symbol object to clone.
+     * @returns {Object} Returns the cloned symbol object.
+     */
+    function cloneSymbol(symbol) {
+      return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
+    }
+
+    /**
+     * Creates a clone of `typedArray`.
+     *
+     * @private
+     * @param {Object} typedArray The typed array to clone.
+     * @param {boolean} [isDeep] Specify a deep clone.
+     * @returns {Object} Returns the cloned typed array.
+     */
+    function cloneTypedArray(typedArray, isDeep) {
+      var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;
+      return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
+    }
+
+    /**
+     * Creates an array that is the composition of partially applied arguments,
+     * placeholders, and provided arguments into a single array of arguments.
+     *
+     * @private
+     * @param {Array|Object} args The provided arguments.
+     * @param {Array} partials The arguments to prepend to those provided.
+     * @param {Array} holders The `partials` placeholder indexes.
+     * @params {boolean} [isCurried] Specify composing for a curried function.
+     * @returns {Array} Returns the new array of composed arguments.
+     */
+    function composeArgs(args, partials, holders, isCurried) {
+      var argsIndex = -1,
+          argsLength = args.length,
+          holdersLength = holders.length,
+          leftIndex = -1,
+          leftLength = partials.length,
+          rangeLength = nativeMax(argsLength - holdersLength, 0),
+          result = Array(leftLength + rangeLength),
+          isUncurried = !isCurried;
+
+      while (++leftIndex < leftLength) {
+        result[leftIndex] = partials[leftIndex];
+      }
+      while (++argsIndex < holdersLength) {
+        if (isUncurried || argsIndex < argsLength) {
+          result[holders[argsIndex]] = args[argsIndex];
+        }
+      }
+      while (rangeLength--) {
+        result[leftIndex++] = args[argsIndex++];
+      }
+      return result;
+    }
+
+    /**
+     * This function is like `composeArgs` except that the arguments composition
+     * is tailored for `_.partialRight`.
+     *
+     * @private
+     * @param {Array|Object} args The provided arguments.
+     * @param {Array} partials The arguments to append to those provided.
+     * @param {Array} holders The `partials` placeholder indexes.
+     * @params {boolean} [isCurried] Specify composing for a curried function.
+     * @returns {Array} Returns the new array of composed arguments.
+     */
+    function composeArgsRight(args, partials, holders, isCurried) {
+      var argsIndex = -1,
+          argsLength = args.length,
+          holdersIndex = -1,
+          holdersLength = holders.length,
+          rightIndex = -1,
+          rightLength = partials.length,
+          rangeLength = nativeMax(argsLength - holdersLength, 0),
+          result = Array(rangeLength + rightLength),
+          isUncurried = !isCurried;
+
+      while (++argsIndex < rangeLength) {
+        result[argsIndex] = args[argsIndex];
+      }
+      var offset = argsIndex;
+      while (++rightIndex < rightLength) {
+        result[offset + rightIndex] = partials[rightIndex];
+      }
+      while (++holdersIndex < holdersLength) {
+        if (isUncurried || argsIndex < argsLength) {
+          result[offset + holders[holdersIndex]] = args[argsIndex++];
+        }
+      }
+      return result;
+    }
+
+    /**
+     * Copies the values of `source` to `array`.
+     *
+     * @private
+     * @param {Array} source The array to copy values from.
+     * @param {Array} [array=[]] The array to copy values to.
+     * @returns {Array} Returns `array`.
+     */
+    function copyArray(source, array) {
+      var index = -1,
+          length = source.length;
+
+      array || (array = Array(length));
+      while (++index < length) {
+        array[index] = source[index];
+      }
+      return array;
+    }
+
+    /**
+     * Copies properties of `source` to `object`.
+     *
+     * @private
+     * @param {Object} source The object to copy properties from.
+     * @param {Array} props The property names to copy.
+     * @param {Object} [object={}] The object to copy properties to.
+     * @returns {Object} Returns `object`.
+     */
+    function copyObject(source, props, object) {
+      return copyObjectWith(source, props, object);
+    }
+
+    /**
+     * This function is like `copyObject` except that it accepts a function to
+     * customize copied values.
+     *
+     * @private
+     * @param {Object} source The object to copy properties from.
+     * @param {Array} props The property names to copy.
+     * @param {Object} [object={}] The object to copy properties to.
+     * @param {Function} [customizer] The function to customize copied values.
+     * @returns {Object} Returns `object`.
+     */
+    function copyObjectWith(source, props, object, customizer) {
+      object || (object = {});
+
+      var index = -1,
+          length = props.length;
+
+      while (++index < length) {
+        var key = props[index];
+
+        var newValue = customizer
+          ? customizer(object[key], source[key], key, object, source)
+          : source[key];
+
+        assignValue(object, key, newValue);
+      }
+      return object;
+    }
+
+    /**
+     * Copies own symbol properties of `source` to `object`.
+     *
+     * @private
+     * @param {Object} source The object to copy symbols from.
+     * @param {Object} [object={}] The object to copy symbols to.
+     * @returns {Object} Returns `object`.
+     */
+    function copySymbols(source, object) {
+      return copyObject(source, getSymbols(source), object);
+    }
+
+    /**
+     * Creates a function like `_.groupBy`.
+     *
+     * @private
+     * @param {Function} setter The function to set accumulator values.
+     * @param {Function} [initializer] The accumulator object initializer.
+     * @returns {Function} Returns the new aggregator function.
+     */
+    function createAggregator(setter, initializer) {
+      return function(collection, iteratee) {
+        var func = isArray(collection) ? arrayAggregator : baseAggregator,
+            accumulator = initializer ? initializer() : {};
+
+        return func(collection, setter, getIteratee(iteratee), accumulator);
+      };
+    }
+
+    /**
+     * Creates a function like `_.assign`.
+     *
+     * @private
+     * @param {Function} assigner The function to assign values.
+     * @returns {Function} Returns the new assigner function.
+     */
+    function createAssigner(assigner) {
+      return rest(function(object, sources) {
+        var index = -1,
+            length = sources.length,
+            customizer = length > 1 ? sources[length - 1] : undefined,
+            guard = length > 2 ? sources[2] : undefined;
+
+        customizer = typeof customizer == 'function'
+          ? (length--, customizer)
+          : undefined;
+
+        if (guard && isIterateeCall(sources[0], sources[1], guard)) {
+          customizer = length < 3 ? undefined : customizer;
+          length = 1;
+        }
+        object = Object(object);
+        while (++index < length) {
+          var source = sources[index];
+          if (source) {
+            assigner(object, source, index, customizer);
+          }
+        }
+        return object;
+      });
+    }
+
+    /**
+     * Creates a `baseEach` or `baseEachRight` function.
+     *
+     * @private
+     * @param {Function} eachFunc The function to iterate over a collection.
+     * @param {boolean} [fromRight] Specify iterating from right to left.
+     * @returns {Function} Returns the new base function.
+     */
+    function createBaseEach(eachFunc, fromRight) {
+      return function(collection, iteratee) {
+        if (collection == null) {
+          return collection;
+        }
+        if (!isArrayLike(collection)) {
+          return eachFunc(collection, iteratee);
+        }
+        var length = collection.length,
+            index = fromRight ? length : -1,
+            iterable = Object(collection);
+
+        while ((fromRight ? index-- : ++index < length)) {
+          if (iteratee(iterable[index], index, iterable) === false) {
+            break;
+          }
+        }
+        return collection;
+      };
+    }
+
+    /**
+     * Creates a base function for methods like `_.forIn`.
+     *
+     * @private
+     * @param {boolean} [fromRight] Specify iterating from right to left.
+     * @returns {Function} Returns the new base function.
+     */
+    function createBaseFor(fromRight) {
+      return function(object, iteratee, keysFunc) {
+        var index = -1,
+            iterable = Object(object),
+            props = keysFunc(object),
+            length = props.length;
+
+        while (length--) {
+          var key = props[fromRight ? length : ++index];
+          if (iteratee(iterable[key], key, iterable) === false) {
+            break;
+          }
+        }
+        return object;
+      };
+    }
+
+    /**
+     * Creates a function that wraps `func` to invoke it with the optional `this`
+     * binding of `thisArg`.
+     *
+     * @private
+     * @param {Function} func The function to wrap.
+     * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details.
+     * @param {*} [thisArg] The `this` binding of `func`.
+     * @returns {Function} Returns the new wrapped function.
+     */
+    function createBaseWrapper(func, bitmask, thisArg) {
+      var isBind = bitmask & BIND_FLAG,
+          Ctor = createCtorWrapper(func);
+
+      function wrapper() {
+        var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
+        return fn.apply(isBind ? thisArg : this, arguments);
+      }
+      return wrapper;
+    }
+
+    /**
+     * Creates a function like `_.lowerFirst`.
+     *
+     * @private
+     * @param {string} methodName The name of the `String` case method to use.
+     * @returns {Function} Returns the new function.
+     */
+    function createCaseFirst(methodName) {
+      return function(string) {
+        string = toString(string);
+
+        var strSymbols = reHasComplexSymbol.test(string)
+          ? stringToArray(string)
+          : undefined;
+
+        var chr = strSymbols ? strSymbols[0] : string.charAt(0),
+            trailing = strSymbols ? strSymbols.slice(1).join('') : string.slice(1);
+
+        return chr[methodName]() + trailing;
+      };
+    }
+
+    /**
+     * Creates a function like `_.camelCase`.
+     *
+     * @private
+     * @param {Function} callback The function to combine each word.
+     * @returns {Function} Returns the new compounder function.
+     */
+    function createCompounder(callback) {
+      return function(string) {
+        return arrayReduce(words(deburr(string)), callback, '');
+      };
+    }
+
+    /**
+     * Creates a function that produces an instance of `Ctor` regardless of
+     * whether it was invoked as part of a `new` expression or by `call` or `apply`.
+     *
+     * @private
+     * @param {Function} Ctor The constructor to wrap.
+     * @returns {Function} Returns the new wrapped function.
+     */
+    function createCtorWrapper(Ctor) {
+      return function() {
+        // Use a `switch` statement to work with class constructors.
+        // See http://ecma-international.org/ecma-262/6.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist
+        // for more details.
+        var args = arguments;
+        switch (args.length) {
+          case 0: return new Ctor;
+          case 1: return new Ctor(args[0]);
+          case 2: return new Ctor(args[0], args[1]);
+          case 3: return new Ctor(args[0], args[1], args[2]);
+          case 4: return new Ctor(args[0], args[1], args[2], args[3]);
+          case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]);
+          case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]);
+          case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
+        }
+        var thisBinding = baseCreate(Ctor.prototype),
+            result = Ctor.apply(thisBinding, args);
+
+        // Mimic the constructor's `return` behavior.
+        // See https://es5.github.io/#x13.2.2 for more details.
+        return isObject(result) ? result : thisBinding;
+      };
+    }
+
+    /**
+     * Creates a function that wraps `func` to enable currying.
+     *
+     * @private
+     * @param {Function} func The function to wrap.
+     * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details.
+     * @param {number} arity The arity of `func`.
+     * @returns {Function} Returns the new wrapped function.
+     */
+    function createCurryWrapper(func, bitmask, arity) {
+      var Ctor = createCtorWrapper(func);
+
+      function wrapper() {
+        var length = arguments.length,
+            args = Array(length),
+            index = length,
+            placeholder = getPlaceholder(wrapper);
+
+        while (index--) {
+          args[index] = arguments[index];
+        }
+        var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder)
+          ? []
+          : replaceHolders(args, placeholder);
+
+        length -= holders.length;
+        if (length < arity) {
+          return createRecurryWrapper(
+            func, bitmask, createHybridWrapper, wrapper.placeholder, undefined,
+            args, holders, undefined, undefined, arity - length);
+        }
+        var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
+        return apply(fn, this, args);
+      }
+      return wrapper;
+    }
+
+    /**
+     * Creates a `_.flow` or `_.flowRight` function.
+     *
+     * @private
+     * @param {boolean} [fromRight] Specify iterating from right to left.
+     * @returns {Function} Returns the new flow function.
+     */
+    function createFlow(fromRight) {
+      return rest(function(funcs) {
+        funcs = baseFlatten(funcs, 1);
+
+        var length = funcs.length,
+            index = length,
+            prereq = LodashWrapper.prototype.thru;
+
+        if (fromRight) {
+          funcs.reverse();
+        }
+        while (index--) {
+          var func = funcs[index];
+          if (typeof func != 'function') {
+            throw new TypeError(FUNC_ERROR_TEXT);
+          }
+          if (prereq && !wrapper && getFuncName(func) == 'wrapper') {
+            var wrapper = new LodashWrapper([], true);
+          }
+        }
+        index = wrapper ? index : length;
+        while (++index < length) {
+          func = funcs[index];
+
+          var funcName = getFuncName(func),
+              data = funcName == 'wrapper' ? getData(func) : undefined;
+
+          if (data && isLaziable(data[0]) &&
+                data[1] == (ARY_FLAG | CURRY_FLAG | PARTIAL_FLAG | REARG_FLAG) &&
+                !data[4].length && data[9] == 1
+              ) {
+            wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]);
+          } else {
+            wrapper = (func.length == 1 && isLaziable(func)) ? wrapper[funcName]() : wrapper.thru(func);
+          }
+        }
+        return function() {
+          var args = arguments,
+              value = args[0];
+
+          if (wrapper && args.length == 1 &&
+              isArray(value) && value.length >= LARGE_ARRAY_SIZE) {
+            return wrapper.plant(value).value();
+          }
+          var index = 0,
+              result = length ? funcs[index].apply(this, args) : value;
+
+          while (++index < length) {
+            result = funcs[index].call(this, result);
+          }
+          return result;
+        };
+      });
+    }
+
+    /**
+     * Creates a function that wraps `func` to invoke it with optional `this`
+     * binding of `thisArg`, partial application, and currying.
+     *
+     * @private
+     * @param {Function|string} func The function or method name to wrap.
+     * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details.
+     * @param {*} [thisArg] The `this` binding of `func`.
+     * @param {Array} [partials] The arguments to prepend to those provided to the new function.
+     * @param {Array} [holders] The `partials` placeholder indexes.
+     * @param {Array} [partialsRight] The arguments to append to those provided to the new function.
+     * @param {Array} [holdersRight] The `partialsRight` placeholder indexes.
+     * @param {Array} [argPos] The argument positions of the new function.
+     * @param {number} [ary] The arity cap of `func`.
+     * @param {number} [arity] The arity of `func`.
+     * @returns {Function} Returns the new wrapped function.
+     */
+    function createHybridWrapper(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) {
+      var isAry = bitmask & ARY_FLAG,
+          isBind = bitmask & BIND_FLAG,
+          isBindKey = bitmask & BIND_KEY_FLAG,
+          isCurried = bitmask & (CURRY_FLAG | CURRY_RIGHT_FLAG),
+          isFlip = bitmask & FLIP_FLAG,
+          Ctor = isBindKey ? undefined : createCtorWrapper(func);
+
+      function wrapper() {
+        var length = arguments.length,
+            index = length,
+            args = Array(length);
+
+        while (index--) {
+          args[index] = arguments[index];
+        }
+        if (isCurried) {
+          var placeholder = getPlaceholder(wrapper),
+              holdersCount = countHolders(args, placeholder);
+        }
+        if (partials) {
+          args = composeArgs(args, partials, holders, isCurried);
+        }
+        if (partialsRight) {
+          args = composeArgsRight(args, partialsRight, holdersRight, isCurried);
+        }
+        length -= holdersCount;
+        if (isCurried && length < arity) {
+          var newHolders = replaceHolders(args, placeholder);
+          return createRecurryWrapper(
+            func, bitmask, createHybridWrapper, wrapper.placeholder, thisArg,
+            args, newHolders, argPos, ary, arity - length
+          );
+        }
+        var thisBinding = isBind ? thisArg : this,
+            fn = isBindKey ? thisBinding[func] : func;
+
+        length = args.length;
+        if (argPos) {
+          args = reorder(args, argPos);
+        } else if (isFlip && length > 1) {
+          args.reverse();
+        }
+        if (isAry && ary < length) {
+          args.length = ary;
+        }
+        if (this && this !== root && this instanceof wrapper) {
+          fn = Ctor || createCtorWrapper(fn);
+        }
+        return fn.apply(thisBinding, args);
+      }
+      return wrapper;
+    }
+
+    /**
+     * Creates a function like `_.invertBy`.
+     *
+     * @private
+     * @param {Function} setter The function to set accumulator values.
+     * @param {Function} toIteratee The function to resolve iteratees.
+     * @returns {Function} Returns the new inverter function.
+     */
+    function createInverter(setter, toIteratee) {
+      return function(object, iteratee) {
+        return baseInverter(object, setter, toIteratee(iteratee), {});
+      };
+    }
+
+    /**
+     * Creates a function like `_.over`.
+     *
+     * @private
+     * @param {Function} arrayFunc The function to iterate over iteratees.
+     * @returns {Function} Returns the new invoker function.
+     */
+    function createOver(arrayFunc) {
+      return rest(function(iteratees) {
+        iteratees = arrayMap(baseFlatten(iteratees, 1), getIteratee());
+        return rest(function(args) {
+          var thisArg = this;
+          return arrayFunc(iteratees, function(iteratee) {
+            return apply(iteratee, thisArg, args);
+          });
+        });
+      });
+    }
+
+    /**
+     * Creates the padding for `string` based on `length`. The `chars` string
+     * is truncated if the number of characters exceeds `length`.
+     *
+     * @private
+     * @param {string} string The string to create padding for.
+     * @param {number} [length=0] The padding length.
+     * @param {string} [chars=' '] The string used as padding.
+     * @returns {string} Returns the padding for `string`.
+     */
+    function createPadding(string, length, chars) {
+      length = toInteger(length);
+
+      var strLength = stringSize(string);
+      if (!length || strLength >= length) {
+        return '';
+      }
+      var padLength = length - strLength;
+      chars = chars === undefined ? ' ' : (chars + '');
+
+      var result = repeat(chars, nativeCeil(padLength / stringSize(chars)));
+      return reHasComplexSymbol.test(chars)
+        ? stringToArray(result).slice(0, padLength).join('')
+        : result.slice(0, padLength);
+    }
+
+    /**
+     * Creates a function that wraps `func` to invoke it with the optional `this`
+     * binding of `thisArg` and the `partials` prepended to those provided to
+     * the wrapper.
+     *
+     * @private
+     * @param {Function} func The function to wrap.
+     * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details.
+     * @param {*} thisArg The `this` binding of `func`.
+     * @param {Array} partials The arguments to prepend to those provided to the new function.
+     * @returns {Function} Returns the new wrapped function.
+     */
+    function createPartialWrapper(func, bitmask, thisArg, partials) {
+      var isBind = bitmask & BIND_FLAG,
+          Ctor = createCtorWrapper(func);
+
+      function wrapper() {
+        var argsIndex = -1,
+            argsLength = arguments.length,
+            leftIndex = -1,
+            leftLength = partials.length,
+            args = Array(leftLength + argsLength),
+            fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
+
+        while (++leftIndex < leftLength) {
+          args[leftIndex] = partials[leftIndex];
+        }
+        while (argsLength--) {
+          args[leftIndex++] = arguments[++argsIndex];
+        }
+        return apply(fn, isBind ? thisArg : this, args);
+      }
+      return wrapper;
+    }
+
+    /**
+     * Creates a `_.range` or `_.rangeRight` function.
+     *
+     * @private
+     * @param {boolean} [fromRight] Specify iterating from right to left.
+     * @returns {Function} Returns the new range function.
+     */
+    function createRange(fromRight) {
+      return function(start, end, step) {
+        if (step && typeof step != 'number' && isIterateeCall(start, end, step)) {
+          end = step = undefined;
+        }
+        // Ensure the sign of `-0` is preserved.
+        start = toNumber(start);
+        start = start === start ? start : 0;
+        if (end === undefined) {
+          end = start;
+          start = 0;
+        } else {
+          end = toNumber(end) || 0;
+        }
+        step = step === undefined ? (start < end ? 1 : -1) : (toNumber(step) || 0);
+        return baseRange(start, end, step, fromRight);
+      };
+    }
+
+    /**
+     * Creates a function that wraps `func` to continue currying.
+     *
+     * @private
+     * @param {Function} func The function to wrap.
+     * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details.
+     * @param {Function} wrapFunc The function to create the `func` wrapper.
+     * @param {*} placeholder The placeholder value.
+     * @param {*} [thisArg] The `this` binding of `func`.
+     * @param {Array} [partials] The arguments to prepend to those provided to the new function.
+     * @param {Array} [holders] The `partials` placeholder indexes.
+     * @param {Array} [argPos] The argument positions of the new function.
+     * @param {number} [ary] The arity cap of `func`.
+     * @param {number} [arity] The arity of `func`.
+     * @returns {Function} Returns the new wrapped function.
+     */
+    function createRecurryWrapper(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) {
+      var isCurry = bitmask & CURRY_FLAG,
+          newArgPos = argPos ? copyArray(argPos) : undefined,
+          newHolders = isCurry ? holders : undefined,
+          newHoldersRight = isCurry ? undefined : holders,
+          newPartials = isCurry ? partials : undefined,
+          newPartialsRight = isCurry ? undefined : partials;
+
+      bitmask |= (isCurry ? PARTIAL_FLAG : PARTIAL_RIGHT_FLAG);
+      bitmask &= ~(isCurry ? PARTIAL_RIGHT_FLAG : PARTIAL_FLAG);
+
+      if (!(bitmask & CURRY_BOUND_FLAG)) {
+        bitmask &= ~(BIND_FLAG | BIND_KEY_FLAG);
+      }
+      var newData = [
+        func, bitmask, thisArg, newPartials, newHolders, newPartialsRight,
+        newHoldersRight, newArgPos, ary, arity
+      ];
+
+      var result = wrapFunc.apply(undefined, newData);
+      if (isLaziable(func)) {
+        setData(result, newData);
+      }
+      result.placeholder = placeholder;
+      return result;
+    }
+
+    /**
+     * Creates a function like `_.round`.
+     *
+     * @private
+     * @param {string} methodName The name of the `Math` method to use when rounding.
+     * @returns {Function} Returns the new round function.
+     */
+    function createRound(methodName) {
+      var func = Math[methodName];
+      return function(number, precision) {
+        number = toNumber(number);
+        precision = toInteger(precision);
+        if (precision) {
+          // Shift with exponential notation to avoid floating-point issues.
+          // See [MDN](https://mdn.io/round#Examples) for more details.
+          var pair = (toString(number) + 'e').split('e'),
+              value = func(pair[0] + 'e' + (+pair[1] + precision));
+
+          pair = (toString(value) + 'e').split('e');
+          return +(pair[0] + 'e' + (+pair[1] - precision));
+        }
+        return func(number);
+      };
+    }
+
+    /**
+     * Creates a set of `values`.
+     *
+     * @private
+     * @param {Array} values The values to add to the set.
+     * @returns {Object} Returns the new set.
+     */
+    var createSet = !(Set && new Set([1, 2]).size === 2) ? noop : function(values) {
+      return new Set(values);
+    };
+
+    /**
+     * Creates a function that either curries or invokes `func` with optional
+     * `this` binding and partially applied arguments.
+     *
+     * @private
+     * @param {Function|string} func The function or method name to wrap.
+     * @param {number} bitmask The bitmask of wrapper flags.
+     *  The bitmask may be composed of the following flags:
+     *     1 - `_.bind`
+     *     2 - `_.bindKey`
+     *     4 - `_.curry` or `_.curryRight` of a bound function
+     *     8 - `_.curry`
+     *    16 - `_.curryRight`
+     *    32 - `_.partial`
+     *    64 - `_.partialRight`
+     *   128 - `_.rearg`
+     *   256 - `_.ary`
+     * @param {*} [thisArg] The `this` binding of `func`.
+     * @param {Array} [partials] The arguments to be partially applied.
+     * @param {Array} [holders] The `partials` placeholder indexes.
+     * @param {Array} [argPos] The argument positions of the new function.
+     * @param {number} [ary] The arity cap of `func`.
+     * @param {number} [arity] The arity of `func`.
+     * @returns {Function} Returns the new wrapped function.
+     */
+    function createWrapper(func, bitmask, thisArg, partials, holders, argPos, ary, arity) {
+      var isBindKey = bitmask & BIND_KEY_FLAG;
+      if (!isBindKey && typeof func != 'function') {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      var length = partials ? partials.length : 0;
+      if (!length) {
+        bitmask &= ~(PARTIAL_FLAG | PARTIAL_RIGHT_FLAG);
+        partials = holders = undefined;
+      }
+      ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0);
+      arity = arity === undefined ? arity : toInteger(arity);
+      length -= holders ? holders.length : 0;
+
+      if (bitmask & PARTIAL_RIGHT_FLAG) {
+        var partialsRight = partials,
+            holdersRight = holders;
+
+        partials = holders = undefined;
+      }
+      var data = isBindKey ? undefined : getData(func);
+
+      var newData = [
+        func, bitmask, thisArg, partials, holders, partialsRight, holdersRight,
+        argPos, ary, arity
+      ];
+
+      if (data) {
+        mergeData(newData, data);
+      }
+      func = newData[0];
+      bitmask = newData[1];
+      thisArg = newData[2];
+      partials = newData[3];
+      holders = newData[4];
+      arity = newData[9] = newData[9] == null
+        ? (isBindKey ? 0 : func.length)
+        : nativeMax(newData[9] - length, 0);
+
+      if (!arity && bitmask & (CURRY_FLAG | CURRY_RIGHT_FLAG)) {
+        bitmask &= ~(CURRY_FLAG | CURRY_RIGHT_FLAG);
+      }
+      if (!bitmask || bitmask == BIND_FLAG) {
+        var result = createBaseWrapper(func, bitmask, thisArg);
+      } else if (bitmask == CURRY_FLAG || bitmask == CURRY_RIGHT_FLAG) {
+        result = createCurryWrapper(func, bitmask, arity);
+      } else if ((bitmask == PARTIAL_FLAG || bitmask == (BIND_FLAG | PARTIAL_FLAG)) && !holders.length) {
+        result = createPartialWrapper(func, bitmask, thisArg, partials);
+      } else {
+        result = createHybridWrapper.apply(undefined, newData);
+      }
+      var setter = data ? baseSetData : setData;
+      return setter(result, newData);
+    }
+
+    /**
+     * A specialized version of `baseIsEqualDeep` for arrays with support for
+     * partial deep comparisons.
+     *
+     * @private
+     * @param {Array} array The array to compare.
+     * @param {Array} other The other array to compare.
+     * @param {Function} equalFunc The function to determine equivalents of values.
+     * @param {Function} customizer The function to customize comparisons.
+     * @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual` for more details.
+     * @param {Object} stack Tracks traversed `array` and `other` objects.
+     * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
+     */
+    function equalArrays(array, other, equalFunc, customizer, bitmask, stack) {
+      var index = -1,
+          isPartial = bitmask & PARTIAL_COMPARE_FLAG,
+          isUnordered = bitmask & UNORDERED_COMPARE_FLAG,
+          arrLength = array.length,
+          othLength = other.length;
+
+      if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
+        return false;
+      }
+      // Assume cyclic values are equal.
+      var stacked = stack.get(array);
+      if (stacked) {
+        return stacked == other;
+      }
+      var result = true;
+      stack.set(array, other);
+
+      // Ignore non-index properties.
+      while (++index < arrLength) {
+        var arrValue = array[index],
+            othValue = other[index];
+
+        if (customizer) {
+          var compared = isPartial
+            ? customizer(othValue, arrValue, index, other, array, stack)
+            : customizer(arrValue, othValue, index, array, other, stack);
+        }
+        if (compared !== undefined) {
+          if (compared) {
+            continue;
+          }
+          result = false;
+          break;
+        }
+        // Recursively compare arrays (susceptible to call stack limits).
+        if (isUnordered) {
+          if (!arraySome(other, function(othValue) {
+                return arrValue === othValue || equalFunc(arrValue, othValue, customizer, bitmask, stack);
+              })) {
+            result = false;
+            break;
+          }
+        } else if (!(arrValue === othValue || equalFunc(arrValue, othValue, customizer, bitmask, stack))) {
+          result = false;
+          break;
+        }
+      }
+      stack['delete'](array);
+      return result;
+    }
+
+    /**
+     * A specialized version of `baseIsEqualDeep` for comparing objects of
+     * the same `toStringTag`.
+     *
+     * **Note:** This function only supports comparing values with tags of
+     * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
+     *
+     * @private
+     * @param {Object} object The object to compare.
+     * @param {Object} other The other object to compare.
+     * @param {string} tag The `toStringTag` of the objects to compare.
+     * @param {Function} equalFunc The function to determine equivalents of values.
+     * @param {Function} customizer The function to customize comparisons.
+     * @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual` for more details.
+     * @param {Object} stack Tracks traversed `object` and `other` objects.
+     * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
+     */
+    function equalByTag(object, other, tag, equalFunc, customizer, bitmask, stack) {
+      switch (tag) {
+        case arrayBufferTag:
+          if ((object.byteLength != other.byteLength) ||
+              !equalFunc(new Uint8Array(object), new Uint8Array(other))) {
+            return false;
+          }
+          return true;
+
+        case boolTag:
+        case dateTag:
+          // Coerce dates and booleans to numbers, dates to milliseconds and booleans
+          // to `1` or `0` treating invalid dates coerced to `NaN` as not equal.
+          return +object == +other;
+
+        case errorTag:
+          return object.name == other.name && object.message == other.message;
+
+        case numberTag:
+          // Treat `NaN` vs. `NaN` as equal.
+          return (object != +object) ? other != +other : object == +other;
+
+        case regexpTag:
+        case stringTag:
+          // Coerce regexes to strings and treat strings primitives and string
+          // objects as equal. See https://es5.github.io/#x15.10.6.4 for more details.
+          return object == (other + '');
+
+        case mapTag:
+          var convert = mapToArray;
+
+        case setTag:
+          var isPartial = bitmask & PARTIAL_COMPARE_FLAG;
+          convert || (convert = setToArray);
+
+          if (object.size != other.size && !isPartial) {
+            return false;
+          }
+          // Assume cyclic values are equal.
+          var stacked = stack.get(object);
+          if (stacked) {
+            return stacked == other;
+          }
+          // Recursively compare objects (susceptible to call stack limits).
+          return equalArrays(convert(object), convert(other), equalFunc, customizer, bitmask | UNORDERED_COMPARE_FLAG, stack.set(object, other));
+
+        case symbolTag:
+          if (symbolValueOf) {
+            return symbolValueOf.call(object) == symbolValueOf.call(other);
+          }
+      }
+      return false;
+    }
+
+    /**
+     * A specialized version of `baseIsEqualDeep` for objects with support for
+     * partial deep comparisons.
+     *
+     * @private
+     * @param {Object} object The object to compare.
+     * @param {Object} other The other object to compare.
+     * @param {Function} equalFunc The function to determine equivalents of values.
+     * @param {Function} customizer The function to customize comparisons.
+     * @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual` for more details.
+     * @param {Object} stack Tracks traversed `object` and `other` objects.
+     * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
+     */
+    function equalObjects(object, other, equalFunc, customizer, bitmask, stack) {
+      var isPartial = bitmask & PARTIAL_COMPARE_FLAG,
+          objProps = keys(object),
+          objLength = objProps.length,
+          othProps = keys(other),
+          othLength = othProps.length;
+
+      if (objLength != othLength && !isPartial) {
+        return false;
+      }
+      var index = objLength;
+      while (index--) {
+        var key = objProps[index];
+        if (!(isPartial ? key in other : baseHas(other, key))) {
+          return false;
+        }
+      }
+      // Assume cyclic values are equal.
+      var stacked = stack.get(object);
+      if (stacked) {
+        return stacked == other;
+      }
+      var result = true;
+      stack.set(object, other);
+
+      var skipCtor = isPartial;
+      while (++index < objLength) {
+        key = objProps[index];
+        var objValue = object[key],
+            othValue = other[key];
+
+        if (customizer) {
+          var compared = isPartial
+            ? customizer(othValue, objValue, key, other, object, stack)
+            : customizer(objValue, othValue, key, object, other, stack);
+        }
+        // Recursively compare objects (susceptible to call stack limits).
+        if (!(compared === undefined
+              ? (objValue === othValue || equalFunc(objValue, othValue, customizer, bitmask, stack))
+              : compared
+            )) {
+          result = false;
+          break;
+        }
+        skipCtor || (skipCtor = key == 'constructor');
+      }
+      if (result && !skipCtor) {
+        var objCtor = object.constructor,
+            othCtor = other.constructor;
+
+        // Non `Object` object instances with different constructors are not equal.
+        if (objCtor != othCtor &&
+            ('constructor' in object && 'constructor' in other) &&
+            !(typeof objCtor == 'function' && objCtor instanceof objCtor &&
+              typeof othCtor == 'function' && othCtor instanceof othCtor)) {
+          result = false;
+        }
+      }
+      stack['delete'](object);
+      return result;
+    }
+
+    /**
+     * Gets metadata for `func`.
+     *
+     * @private
+     * @param {Function} func The function to query.
+     * @returns {*} Returns the metadata for `func`.
+     */
+    var getData = !metaMap ? noop : function(func) {
+      return metaMap.get(func);
+    };
+
+    /**
+     * Gets the name of `func`.
+     *
+     * @private
+     * @param {Function} func The function to query.
+     * @returns {string} Returns the function name.
+     */
+    function getFuncName(func) {
+      var result = (func.name + ''),
+          array = realNames[result],
+          length = hasOwnProperty.call(realNames, result) ? array.length : 0;
+
+      while (length--) {
+        var data = array[length],
+            otherFunc = data.func;
+        if (otherFunc == null || otherFunc == func) {
+          return data.name;
+        }
+      }
+      return result;
+    }
+
+    /**
+     * Gets the appropriate "iteratee" function. If the `_.iteratee` method is
+     * customized this function returns the custom method, otherwise it returns
+     * `baseIteratee`. If arguments are provided the chosen function is invoked
+     * with them and its result is returned.
+     *
+     * @private
+     * @param {*} [value] The value to convert to an iteratee.
+     * @param {number} [arity] The arity of the created iteratee.
+     * @returns {Function} Returns the chosen function or its result.
+     */
+    function getIteratee() {
+      var result = lodash.iteratee || iteratee;
+      result = result === iteratee ? baseIteratee : result;
+      return arguments.length ? result(arguments[0], arguments[1]) : result;
+    }
+
+    /**
+     * Gets the "length" property value of `object`.
+     *
+     * **Note:** This function is used to avoid a [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792)
+     * that affects Safari on at least iOS 8.1-8.3 ARM64.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @returns {*} Returns the "length" value.
+     */
+    var getLength = baseProperty('length');
+
+    /**
+     * Gets the property names, values, and compare flags of `object`.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the match data of `object`.
+     */
+    function getMatchData(object) {
+      var result = toPairs(object),
+          length = result.length;
+
+      while (length--) {
+        result[length][2] = isStrictComparable(result[length][1]);
+      }
+      return result;
+    }
+
+    /**
+     * Gets the native function at `key` of `object`.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @param {string} key The key of the method to get.
+     * @returns {*} Returns the function if it's native, else `undefined`.
+     */
+    function getNative(object, key) {
+      var value = object[key];
+      return isNative(value) ? value : undefined;
+    }
+
+    /**
+     * Gets the argument placeholder value for `func`.
+     *
+     * @private
+     * @param {Function} func The function to inspect.
+     * @returns {*} Returns the placeholder value.
+     */
+    function getPlaceholder(func) {
+      var object = hasOwnProperty.call(lodash, 'placeholder') ? lodash : func;
+      return object.placeholder;
+    }
+
+    /**
+     * Creates an array of the own symbol properties of `object`.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the array of symbols.
+     */
+    var getSymbols = getOwnPropertySymbols || function() {
+      return [];
+    };
+
+    /**
+     * Gets the `toStringTag` of `value`.
+     *
+     * @private
+     * @param {*} value The value to query.
+     * @returns {string} Returns the `toStringTag`.
+     */
+    function getTag(value) {
+      return objectToString.call(value);
+    }
+
+    // Fallback for IE 11 providing `toStringTag` values for maps, sets, and weakmaps.
+    if ((Map && getTag(new Map) != mapTag) ||
+        (Set && getTag(new Set) != setTag) ||
+        (WeakMap && getTag(new WeakMap) != weakMapTag)) {
+      getTag = function(value) {
+        var result = objectToString.call(value),
+            Ctor = result == objectTag ? value.constructor : null,
+            ctorString = typeof Ctor == 'function' ? funcToString.call(Ctor) : '';
+
+        if (ctorString) {
+          switch (ctorString) {
+            case mapCtorString: return mapTag;
+            case setCtorString: return setTag;
+            case weakMapCtorString: return weakMapTag;
+          }
+        }
+        return result;
+      };
+    }
+
+    /**
+     * Gets the view, applying any `transforms` to the `start` and `end` positions.
+     *
+     * @private
+     * @param {number} start The start of the view.
+     * @param {number} end The end of the view.
+     * @param {Array} transforms The transformations to apply to the view.
+     * @returns {Object} Returns an object containing the `start` and `end`
+     *  positions of the view.
+     */
+    function getView(start, end, transforms) {
+      var index = -1,
+          length = transforms.length;
+
+      while (++index < length) {
+        var data = transforms[index],
+            size = data.size;
+
+        switch (data.type) {
+          case 'drop':      start += size; break;
+          case 'dropRight': end -= size; break;
+          case 'take':      end = nativeMin(end, start + size); break;
+          case 'takeRight': start = nativeMax(start, end - size); break;
+        }
+      }
+      return { 'start': start, 'end': end };
+    }
+
+    /**
+     * Checks if `path` exists on `object`.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path to check.
+     * @param {Function} hasFunc The function to check properties.
+     * @returns {boolean} Returns `true` if `path` exists, else `false`.
+     */
+    function hasPath(object, path, hasFunc) {
+      if (object == null) {
+        return false;
+      }
+      var result = hasFunc(object, path);
+      if (!result && !isKey(path)) {
+        path = baseCastPath(path);
+        object = parent(object, path);
+        if (object != null) {
+          path = last(path);
+          result = hasFunc(object, path);
+        }
+      }
+      var length = object ? object.length : undefined;
+      return result || (
+        !!length && isLength(length) && isIndex(path, length) &&
+        (isArray(object) || isString(object) || isArguments(object))
+      );
+    }
+
+    /**
+     * Initializes an array clone.
+     *
+     * @private
+     * @param {Array} array The array to clone.
+     * @returns {Array} Returns the initialized clone.
+     */
+    function initCloneArray(array) {
+      var length = array.length,
+          result = array.constructor(length);
+
+      // Add properties assigned by `RegExp#exec`.
+      if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
+        result.index = array.index;
+        result.input = array.input;
+      }
+      return result;
+    }
+
+    /**
+     * Initializes an object clone.
+     *
+     * @private
+     * @param {Object} object The object to clone.
+     * @returns {Object} Returns the initialized clone.
+     */
+    function initCloneObject(object) {
+      return (typeof object.constructor == 'function' && !isPrototype(object))
+        ? baseCreate(getPrototypeOf(object))
+        : {};
+    }
+
+    /**
+     * Initializes an object clone based on its `toStringTag`.
+     *
+     * **Note:** This function only supports cloning values with tags of
+     * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
+     *
+     * @private
+     * @param {Object} object The object to clone.
+     * @param {string} tag The `toStringTag` of the object to clone.
+     * @param {boolean} [isDeep] Specify a deep clone.
+     * @returns {Object} Returns the initialized clone.
+     */
+    function initCloneByTag(object, tag, isDeep) {
+      var Ctor = object.constructor;
+      switch (tag) {
+        case arrayBufferTag:
+          return cloneArrayBuffer(object);
+
+        case boolTag:
+        case dateTag:
+          return new Ctor(+object);
+
+        case float32Tag: case float64Tag:
+        case int8Tag: case int16Tag: case int32Tag:
+        case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
+          return cloneTypedArray(object, isDeep);
+
+        case mapTag:
+          return cloneMap(object);
+
+        case numberTag:
+        case stringTag:
+          return new Ctor(object);
+
+        case regexpTag:
+          return cloneRegExp(object);
+
+        case setTag:
+          return cloneSet(object);
+
+        case symbolTag:
+          return cloneSymbol(object);
+      }
+    }
+
+    /**
+     * Creates an array of index keys for `object` values of arrays,
+     * `arguments` objects, and strings, otherwise `null` is returned.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @returns {Array|null} Returns index keys, else `null`.
+     */
+    function indexKeys(object) {
+      var length = object ? object.length : undefined;
+      if (isLength(length) &&
+          (isArray(object) || isString(object) || isArguments(object))) {
+        return baseTimes(length, String);
+      }
+      return null;
+    }
+
+    /**
+     * Checks if the given arguments are from an iteratee call.
+     *
+     * @private
+     * @param {*} value The potential iteratee value argument.
+     * @param {*} index The potential iteratee index or key argument.
+     * @param {*} object The potential iteratee object argument.
+     * @returns {boolean} Returns `true` if the arguments are from an iteratee call, else `false`.
+     */
+    function isIterateeCall(value, index, object) {
+      if (!isObject(object)) {
+        return false;
+      }
+      var type = typeof index;
+      if (type == 'number'
+          ? (isArrayLike(object) && isIndex(index, object.length))
+          : (type == 'string' && index in object)) {
+        return eq(object[index], value);
+      }
+      return false;
+    }
+
+    /**
+     * Checks if `value` is a property name and not a property path.
+     *
+     * @private
+     * @param {*} value The value to check.
+     * @param {Object} [object] The object to query keys on.
+     * @returns {boolean} Returns `true` if `value` is a property name, else `false`.
+     */
+    function isKey(value, object) {
+      if (typeof value == 'number') {
+        return true;
+      }
+      return !isArray(value) &&
+        (reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
+          (object != null && value in Object(object)));
+    }
+
+    /**
+     * Checks if `value` is suitable for use as unique object key.
+     *
+     * @private
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is suitable, else `false`.
+     */
+    function isKeyable(value) {
+      var type = typeof value;
+      return type == 'number' || type == 'boolean' ||
+        (type == 'string' && value != '__proto__') || value == null;
+    }
+
+    /**
+     * Checks if `func` has a lazy counterpart.
+     *
+     * @private
+     * @param {Function} func The function to check.
+     * @returns {boolean} Returns `true` if `func` has a lazy counterpart, else `false`.
+     */
+    function isLaziable(func) {
+      var funcName = getFuncName(func),
+          other = lodash[funcName];
+
+      if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) {
+        return false;
+      }
+      if (func === other) {
+        return true;
+      }
+      var data = getData(other);
+      return !!data && func === data[0];
+    }
+
+    /**
+     * Checks if `value` is likely a prototype object.
+     *
+     * @private
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
+     */
+    function isPrototype(value) {
+      var Ctor = value && value.constructor,
+          proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;
+
+      return value === proto;
+    }
+
+    /**
+     * Checks if `value` is suitable for strict equality comparisons, i.e. `===`.
+     *
+     * @private
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` if suitable for strict
+     *  equality comparisons, else `false`.
+     */
+    function isStrictComparable(value) {
+      return value === value && !isObject(value);
+    }
+
+    /**
+     * Merges the function metadata of `source` into `data`.
+     *
+     * Merging metadata reduces the number of wrappers used to invoke a function.
+     * This is possible because methods like `_.bind`, `_.curry`, and `_.partial`
+     * may be applied regardless of execution order. Methods like `_.ary` and `_.rearg`
+     * modify function arguments, making the order in which they are executed important,
+     * preventing the merging of metadata. However, we make an exception for a safe
+     * combined case where curried functions have `_.ary` and or `_.rearg` applied.
+     *
+     * @private
+     * @param {Array} data The destination metadata.
+     * @param {Array} source The source metadata.
+     * @returns {Array} Returns `data`.
+     */
+    function mergeData(data, source) {
+      var bitmask = data[1],
+          srcBitmask = source[1],
+          newBitmask = bitmask | srcBitmask,
+          isCommon = newBitmask < (BIND_FLAG | BIND_KEY_FLAG | ARY_FLAG);
+
+      var isCombo =
+        ((srcBitmask == ARY_FLAG) && (bitmask == CURRY_FLAG)) ||
+        ((srcBitmask == ARY_FLAG) && (bitmask == REARG_FLAG) && (data[7].length <= source[8])) ||
+        ((srcBitmask == (ARY_FLAG | REARG_FLAG)) && (source[7].length <= source[8]) && (bitmask == CURRY_FLAG));
+
+      // Exit early if metadata can't be merged.
+      if (!(isCommon || isCombo)) {
+        return data;
+      }
+      // Use source `thisArg` if available.
+      if (srcBitmask & BIND_FLAG) {
+        data[2] = source[2];
+        // Set when currying a bound function.
+        newBitmask |= bitmask & BIND_FLAG ? 0 : CURRY_BOUND_FLAG;
+      }
+      // Compose partial arguments.
+      var value = source[3];
+      if (value) {
+        var partials = data[3];
+        data[3] = partials ? composeArgs(partials, value, source[4]) : copyArray(value);
+        data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : copyArray(source[4]);
+      }
+      // Compose partial right arguments.
+      value = source[5];
+      if (value) {
+        partials = data[5];
+        data[5] = partials ? composeArgsRight(partials, value, source[6]) : copyArray(value);
+        data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : copyArray(source[6]);
+      }
+      // Use source `argPos` if available.
+      value = source[7];
+      if (value) {
+        data[7] = copyArray(value);
+      }
+      // Use source `ary` if it's smaller.
+      if (srcBitmask & ARY_FLAG) {
+        data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]);
+      }
+      // Use source `arity` if one is not provided.
+      if (data[9] == null) {
+        data[9] = source[9];
+      }
+      // Use source `func` and merge bitmasks.
+      data[0] = source[0];
+      data[1] = newBitmask;
+
+      return data;
+    }
+
+    /**
+     * Used by `_.defaultsDeep` to customize its `_.merge` use.
+     *
+     * @private
+     * @param {*} objValue The destination value.
+     * @param {*} srcValue The source value.
+     * @param {string} key The key of the property to merge.
+     * @param {Object} object The parent object of `objValue`.
+     * @param {Object} source The parent object of `srcValue`.
+     * @param {Object} [stack] Tracks traversed source values and their merged counterparts.
+     * @returns {*} Returns the value to assign.
+     */
+    function mergeDefaults(objValue, srcValue, key, object, source, stack) {
+      if (isObject(objValue) && isObject(srcValue)) {
+        baseMerge(objValue, srcValue, undefined, mergeDefaults, stack.set(srcValue, objValue));
+      }
+      return objValue;
+    }
+
+    /**
+     * Gets the parent value at `path` of `object`.
+     *
+     * @private
+     * @param {Object} object The object to query.
+     * @param {Array} path The path to get the parent value of.
+     * @returns {*} Returns the parent value.
+     */
+    function parent(object, path) {
+      return path.length == 1 ? object : get(object, baseSlice(path, 0, -1));
+    }
+
+    /**
+     * Reorder `array` according to the specified indexes where the element at
+     * the first index is assigned as the first element, the element at
+     * the second index is assigned as the second element, and so on.
+     *
+     * @private
+     * @param {Array} array The array to reorder.
+     * @param {Array} indexes The arranged array indexes.
+     * @returns {Array} Returns `array`.
+     */
+    function reorder(array, indexes) {
+      var arrLength = array.length,
+          length = nativeMin(indexes.length, arrLength),
+          oldArray = copyArray(array);
+
+      while (length--) {
+        var index = indexes[length];
+        array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined;
+      }
+      return array;
+    }
+
+    /**
+     * Sets metadata for `func`.
+     *
+     * **Note:** If this function becomes hot, i.e. is invoked a lot in a short
+     * period of time, it will trip its breaker and transition to an identity function
+     * to avoid garbage collection pauses in V8. See [V8 issue 2070](https://code.google.com/p/v8/issues/detail?id=2070)
+     * for more details.
+     *
+     * @private
+     * @param {Function} func The function to associate metadata with.
+     * @param {*} data The metadata.
+     * @returns {Function} Returns `func`.
+     */
+    var setData = (function() {
+      var count = 0,
+          lastCalled = 0;
+
+      return function(key, value) {
+        var stamp = now(),
+            remaining = HOT_SPAN - (stamp - lastCalled);
+
+        lastCalled = stamp;
+        if (remaining > 0) {
+          if (++count >= HOT_COUNT) {
+            return key;
+          }
+        } else {
+          count = 0;
+        }
+        return baseSetData(key, value);
+      };
+    }());
+
+    /**
+     * Converts `string` to a property path array.
+     *
+     * @private
+     * @param {string} string The string to convert.
+     * @returns {Array} Returns the property path array.
+     */
+    function stringToPath(string) {
+      var result = [];
+      toString(string).replace(rePropName, function(match, number, quote, string) {
+        result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match));
+      });
+      return result;
+    }
+
+    /**
+     * Creates a clone of `wrapper`.
+     *
+     * @private
+     * @param {Object} wrapper The wrapper to clone.
+     * @returns {Object} Returns the cloned wrapper.
+     */
+    function wrapperClone(wrapper) {
+      if (wrapper instanceof LazyWrapper) {
+        return wrapper.clone();
+      }
+      var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__);
+      result.__actions__ = copyArray(wrapper.__actions__);
+      result.__index__  = wrapper.__index__;
+      result.__values__ = wrapper.__values__;
+      return result;
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Creates an array of elements split into groups the length of `size`.
+     * If `array` can't be split evenly, the final chunk will be the remaining
+     * elements.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to process.
+     * @param {number} [size=0] The length of each chunk.
+     * @returns {Array} Returns the new array containing chunks.
+     * @example
+     *
+     * _.chunk(['a', 'b', 'c', 'd'], 2);
+     * // => [['a', 'b'], ['c', 'd']]
+     *
+     * _.chunk(['a', 'b', 'c', 'd'], 3);
+     * // => [['a', 'b', 'c'], ['d']]
+     */
+    function chunk(array, size) {
+      size = nativeMax(toInteger(size), 0);
+
+      var length = array ? array.length : 0;
+      if (!length || size < 1) {
+        return [];
+      }
+      var index = 0,
+          resIndex = 0,
+          result = Array(nativeCeil(length / size));
+
+      while (index < length) {
+        result[resIndex++] = baseSlice(array, index, (index += size));
+      }
+      return result;
+    }
+
+    /**
+     * Creates an array with all falsey values removed. The values `false`, `null`,
+     * `0`, `""`, `undefined`, and `NaN` are falsey.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to compact.
+     * @returns {Array} Returns the new array of filtered values.
+     * @example
+     *
+     * _.compact([0, 1, false, 2, '', 3]);
+     * // => [1, 2, 3]
+     */
+    function compact(array) {
+      var index = -1,
+          length = array ? array.length : 0,
+          resIndex = 0,
+          result = [];
+
+      while (++index < length) {
+        var value = array[index];
+        if (value) {
+          result[resIndex++] = value;
+        }
+      }
+      return result;
+    }
+
+    /**
+     * Creates a new array concatenating `array` with any additional arrays
+     * and/or values.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to concatenate.
+     * @param {...*} [values] The values to concatenate.
+     * @returns {Array} Returns the new concatenated array.
+     * @example
+     *
+     * var array = [1];
+     * var other = _.concat(array, 2, [3], [[4]]);
+     *
+     * console.log(other);
+     * // => [1, 2, 3, [4]]
+     *
+     * console.log(array);
+     * // => [1]
+     */
+    var concat = rest(function(array, values) {
+      if (!isArray(array)) {
+        array = array == null ? [] : [Object(array)];
+      }
+      values = baseFlatten(values, 1);
+      return arrayConcat(array, values);
+    });
+
+    /**
+     * Creates an array of unique `array` values not included in the other
+     * given arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * for equality comparisons. The order of result values is determined by the
+     * order they occur in the first array.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to inspect.
+     * @param {...Array} [values] The values to exclude.
+     * @returns {Array} Returns the new array of filtered values.
+     * @example
+     *
+     * _.difference([3, 2, 1], [4, 2]);
+     * // => [3, 1]
+     */
+    var difference = rest(function(array, values) {
+      return isArrayLikeObject(array)
+        ? baseDifference(array, baseFlatten(values, 1, true))
+        : [];
+    });
+
+    /**
+     * This method is like `_.difference` except that it accepts `iteratee` which
+     * is invoked for each element of `array` and `values` to generate the criterion
+     * by which they're compared. Result values are chosen from the first array.
+     * The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to inspect.
+     * @param {...Array} [values] The values to exclude.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {Array} Returns the new array of filtered values.
+     * @example
+     *
+     * _.differenceBy([3.1, 2.2, 1.3], [4.4, 2.5], Math.floor);
+     * // => [3.1, 1.3]
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');
+     * // => [{ 'x': 2 }]
+     */
+    var differenceBy = rest(function(array, values) {
+      var iteratee = last(values);
+      if (isArrayLikeObject(iteratee)) {
+        iteratee = undefined;
+      }
+      return isArrayLikeObject(array)
+        ? baseDifference(array, baseFlatten(values, 1, true), getIteratee(iteratee))
+        : [];
+    });
+
+    /**
+     * This method is like `_.difference` except that it accepts `comparator`
+     * which is invoked to compare elements of `array` to `values`. Result values
+     * are chosen from the first array. The comparator is invoked with two arguments:
+     * (arrVal, othVal).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to inspect.
+     * @param {...Array} [values] The values to exclude.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns the new array of filtered values.
+     * @example
+     *
+     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
+     *
+     * _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual);
+     * // => [{ 'x': 2, 'y': 1 }]
+     */
+    var differenceWith = rest(function(array, values) {
+      var comparator = last(values);
+      if (isArrayLikeObject(comparator)) {
+        comparator = undefined;
+      }
+      return isArrayLikeObject(array)
+        ? baseDifference(array, baseFlatten(values, 1, true), undefined, comparator)
+        : [];
+    });
+
+    /**
+     * Creates a slice of `array` with `n` elements dropped from the beginning.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @param {number} [n=1] The number of elements to drop.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * _.drop([1, 2, 3]);
+     * // => [2, 3]
+     *
+     * _.drop([1, 2, 3], 2);
+     * // => [3]
+     *
+     * _.drop([1, 2, 3], 5);
+     * // => []
+     *
+     * _.drop([1, 2, 3], 0);
+     * // => [1, 2, 3]
+     */
+    function drop(array, n, guard) {
+      var length = array ? array.length : 0;
+      if (!length) {
+        return [];
+      }
+      n = (guard || n === undefined) ? 1 : toInteger(n);
+      return baseSlice(array, n < 0 ? 0 : n, length);
+    }
+
+    /**
+     * Creates a slice of `array` with `n` elements dropped from the end.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @param {number} [n=1] The number of elements to drop.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * _.dropRight([1, 2, 3]);
+     * // => [1, 2]
+     *
+     * _.dropRight([1, 2, 3], 2);
+     * // => [1]
+     *
+     * _.dropRight([1, 2, 3], 5);
+     * // => []
+     *
+     * _.dropRight([1, 2, 3], 0);
+     * // => [1, 2, 3]
+     */
+    function dropRight(array, n, guard) {
+      var length = array ? array.length : 0;
+      if (!length) {
+        return [];
+      }
+      n = (guard || n === undefined) ? 1 : toInteger(n);
+      n = length - n;
+      return baseSlice(array, 0, n < 0 ? 0 : n);
+    }
+
+    /**
+     * Creates a slice of `array` excluding elements dropped from the end.
+     * Elements are dropped until `predicate` returns falsey. The predicate is
+     * invoked with three arguments: (value, index, array).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney',  'active': true },
+     *   { 'user': 'fred',    'active': false },
+     *   { 'user': 'pebbles', 'active': false }
+     * ];
+     *
+     * _.dropRightWhile(users, function(o) { return !o.active; });
+     * // => objects for ['barney']
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.dropRightWhile(users, { 'user': 'pebbles', 'active': false });
+     * // => objects for ['barney', 'fred']
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.dropRightWhile(users, ['active', false]);
+     * // => objects for ['barney']
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.dropRightWhile(users, 'active');
+     * // => objects for ['barney', 'fred', 'pebbles']
+     */
+    function dropRightWhile(array, predicate) {
+      return (array && array.length)
+        ? baseWhile(array, getIteratee(predicate, 3), true, true)
+        : [];
+    }
+
+    /**
+     * Creates a slice of `array` excluding elements dropped from the beginning.
+     * Elements are dropped until `predicate` returns falsey. The predicate is
+     * invoked with three arguments: (value, index, array).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney',  'active': false },
+     *   { 'user': 'fred',    'active': false },
+     *   { 'user': 'pebbles', 'active': true }
+     * ];
+     *
+     * _.dropWhile(users, function(o) { return !o.active; });
+     * // => objects for ['pebbles']
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.dropWhile(users, { 'user': 'barney', 'active': false });
+     * // => objects for ['fred', 'pebbles']
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.dropWhile(users, ['active', false]);
+     * // => objects for ['pebbles']
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.dropWhile(users, 'active');
+     * // => objects for ['barney', 'fred', 'pebbles']
+     */
+    function dropWhile(array, predicate) {
+      return (array && array.length)
+        ? baseWhile(array, getIteratee(predicate, 3), true)
+        : [];
+    }
+
+    /**
+     * Fills elements of `array` with `value` from `start` up to, but not
+     * including, `end`.
+     *
+     * **Note:** This method mutates `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to fill.
+     * @param {*} value The value to fill `array` with.
+     * @param {number} [start=0] The start position.
+     * @param {number} [end=array.length] The end position.
+     * @returns {Array} Returns `array`.
+     * @example
+     *
+     * var array = [1, 2, 3];
+     *
+     * _.fill(array, 'a');
+     * console.log(array);
+     * // => ['a', 'a', 'a']
+     *
+     * _.fill(Array(3), 2);
+     * // => [2, 2, 2]
+     *
+     * _.fill([4, 6, 8, 10], '*', 1, 3);
+     * // => [4, '*', '*', 10]
+     */
+    function fill(array, value, start, end) {
+      var length = array ? array.length : 0;
+      if (!length) {
+        return [];
+      }
+      if (start && typeof start != 'number' && isIterateeCall(array, value, start)) {
+        start = 0;
+        end = length;
+      }
+      return baseFill(array, value, start, end);
+    }
+
+    /**
+     * This method is like `_.find` except that it returns the index of the first
+     * element `predicate` returns truthy for instead of the element itself.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to search.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {number} Returns the index of the found element, else `-1`.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney',  'active': false },
+     *   { 'user': 'fred',    'active': false },
+     *   { 'user': 'pebbles', 'active': true }
+     * ];
+     *
+     * _.findIndex(users, function(o) { return o.user == 'barney'; });
+     * // => 0
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.findIndex(users, { 'user': 'fred', 'active': false });
+     * // => 1
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.findIndex(users, ['active', false]);
+     * // => 0
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.findIndex(users, 'active');
+     * // => 2
+     */
+    function findIndex(array, predicate) {
+      return (array && array.length)
+        ? baseFindIndex(array, getIteratee(predicate, 3))
+        : -1;
+    }
+
+    /**
+     * This method is like `_.findIndex` except that it iterates over elements
+     * of `collection` from right to left.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to search.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {number} Returns the index of the found element, else `-1`.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney',  'active': true },
+     *   { 'user': 'fred',    'active': false },
+     *   { 'user': 'pebbles', 'active': false }
+     * ];
+     *
+     * _.findLastIndex(users, function(o) { return o.user == 'pebbles'; });
+     * // => 2
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.findLastIndex(users, { 'user': 'barney', 'active': true });
+     * // => 0
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.findLastIndex(users, ['active', false]);
+     * // => 2
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.findLastIndex(users, 'active');
+     * // => 0
+     */
+    function findLastIndex(array, predicate) {
+      return (array && array.length)
+        ? baseFindIndex(array, getIteratee(predicate, 3), true)
+        : -1;
+    }
+
+    /**
+     * Flattens `array` a single level deep.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to flatten.
+     * @returns {Array} Returns the new flattened array.
+     * @example
+     *
+     * _.flatten([1, [2, [3, [4]], 5]]);
+     * // => [1, 2, [3, [4]], 5]
+     */
+    function flatten(array) {
+      var length = array ? array.length : 0;
+      return length ? baseFlatten(array, 1) : [];
+    }
+
+    /**
+     * Recursively flattens `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to flatten.
+     * @returns {Array} Returns the new flattened array.
+     * @example
+     *
+     * _.flattenDeep([1, [2, [3, [4]], 5]]);
+     * // => [1, 2, 3, 4, 5]
+     */
+    function flattenDeep(array) {
+      var length = array ? array.length : 0;
+      return length ? baseFlatten(array, INFINITY) : [];
+    }
+
+    /**
+     * Recursively flatten `array` up to `depth` times.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to flatten.
+     * @param {number} [depth=1] The maximum recursion depth.
+     * @returns {Array} Returns the new flattened array.
+     * @example
+     *
+     * var array = [1, [2, [3, [4]], 5]];
+     *
+     * _.flattenDepth(array, 1);
+     * // => [1, 2, [3, [4]], 5]
+     *
+     * _.flattenDepth(array, 2);
+     * // => [1, 2, 3, [4], 5]
+     */
+    function flattenDepth(array, depth) {
+      var length = array ? array.length : 0;
+      if (!length) {
+        return [];
+      }
+      depth = depth === undefined ? 1 : toInteger(depth);
+      return baseFlatten(array, depth);
+    }
+
+    /**
+     * The inverse of `_.toPairs`; this method returns an object composed
+     * from key-value `pairs`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} pairs The key-value pairs.
+     * @returns {Object} Returns the new object.
+     * @example
+     *
+     * _.fromPairs([['fred', 30], ['barney', 40]]);
+     * // => { 'fred': 30, 'barney': 40 }
+     */
+    function fromPairs(pairs) {
+      var index = -1,
+          length = pairs ? pairs.length : 0,
+          result = {};
+
+      while (++index < length) {
+        var pair = pairs[index];
+        result[pair[0]] = pair[1];
+      }
+      return result;
+    }
+
+    /**
+     * Gets the first element of `array`.
+     *
+     * @static
+     * @memberOf _
+     * @alias first
+     * @category Array
+     * @param {Array} array The array to query.
+     * @returns {*} Returns the first element of `array`.
+     * @example
+     *
+     * _.head([1, 2, 3]);
+     * // => 1
+     *
+     * _.head([]);
+     * // => undefined
+     */
+    function head(array) {
+      return array ? array[0] : undefined;
+    }
+
+    /**
+     * Gets the index at which the first occurrence of `value` is found in `array`
+     * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * for equality comparisons. If `fromIndex` is negative, it's used as the offset
+     * from the end of `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to search.
+     * @param {*} value The value to search for.
+     * @param {number} [fromIndex=0] The index to search from.
+     * @returns {number} Returns the index of the matched value, else `-1`.
+     * @example
+     *
+     * _.indexOf([1, 2, 1, 2], 2);
+     * // => 1
+     *
+     * // Search from the `fromIndex`.
+     * _.indexOf([1, 2, 1, 2], 2, 2);
+     * // => 3
+     */
+    function indexOf(array, value, fromIndex) {
+      var length = array ? array.length : 0;
+      if (!length) {
+        return -1;
+      }
+      fromIndex = toInteger(fromIndex);
+      if (fromIndex < 0) {
+        fromIndex = nativeMax(length + fromIndex, 0);
+      }
+      return baseIndexOf(array, value, fromIndex);
+    }
+
+    /**
+     * Gets all but the last element of `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * _.initial([1, 2, 3]);
+     * // => [1, 2]
+     */
+    function initial(array) {
+      return dropRight(array, 1);
+    }
+
+    /**
+     * Creates an array of unique values that are included in all given arrays
+     * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * for equality comparisons. The order of result values is determined by the
+     * order they occur in the first array.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to inspect.
+     * @returns {Array} Returns the new array of intersecting values.
+     * @example
+     *
+     * _.intersection([2, 1], [4, 2], [1, 2]);
+     * // => [2]
+     */
+    var intersection = rest(function(arrays) {
+      var mapped = arrayMap(arrays, baseCastArrayLikeObject);
+      return (mapped.length && mapped[0] === arrays[0])
+        ? baseIntersection(mapped)
+        : [];
+    });
+
+    /**
+     * This method is like `_.intersection` except that it accepts `iteratee`
+     * which is invoked for each element of each `arrays` to generate the criterion
+     * by which they're compared. Result values are chosen from the first array.
+     * The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to inspect.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {Array} Returns the new array of intersecting values.
+     * @example
+     *
+     * _.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor);
+     * // => [2.1]
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
+     * // => [{ 'x': 1 }]
+     */
+    var intersectionBy = rest(function(arrays) {
+      var iteratee = last(arrays),
+          mapped = arrayMap(arrays, baseCastArrayLikeObject);
+
+      if (iteratee === last(mapped)) {
+        iteratee = undefined;
+      } else {
+        mapped.pop();
+      }
+      return (mapped.length && mapped[0] === arrays[0])
+        ? baseIntersection(mapped, getIteratee(iteratee))
+        : [];
+    });
+
+    /**
+     * This method is like `_.intersection` except that it accepts `comparator`
+     * which is invoked to compare elements of `arrays`. Result values are chosen
+     * from the first array. The comparator is invoked with two arguments:
+     * (arrVal, othVal).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to inspect.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns the new array of intersecting values.
+     * @example
+     *
+     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
+     * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
+     *
+     * _.intersectionWith(objects, others, _.isEqual);
+     * // => [{ 'x': 1, 'y': 2 }]
+     */
+    var intersectionWith = rest(function(arrays) {
+      var comparator = last(arrays),
+          mapped = arrayMap(arrays, baseCastArrayLikeObject);
+
+      if (comparator === last(mapped)) {
+        comparator = undefined;
+      } else {
+        mapped.pop();
+      }
+      return (mapped.length && mapped[0] === arrays[0])
+        ? baseIntersection(mapped, undefined, comparator)
+        : [];
+    });
+
+    /**
+     * Converts all elements in `array` into a string separated by `separator`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to convert.
+     * @param {string} [separator=','] The element separator.
+     * @returns {string} Returns the joined string.
+     * @example
+     *
+     * _.join(['a', 'b', 'c'], '~');
+     * // => 'a~b~c'
+     */
+    function join(array, separator) {
+      return array ? nativeJoin.call(array, separator) : '';
+    }
+
+    /**
+     * Gets the last element of `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @returns {*} Returns the last element of `array`.
+     * @example
+     *
+     * _.last([1, 2, 3]);
+     * // => 3
+     */
+    function last(array) {
+      var length = array ? array.length : 0;
+      return length ? array[length - 1] : undefined;
+    }
+
+    /**
+     * This method is like `_.indexOf` except that it iterates over elements of
+     * `array` from right to left.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to search.
+     * @param {*} value The value to search for.
+     * @param {number} [fromIndex=array.length-1] The index to search from.
+     * @returns {number} Returns the index of the matched value, else `-1`.
+     * @example
+     *
+     * _.lastIndexOf([1, 2, 1, 2], 2);
+     * // => 3
+     *
+     * // Search from the `fromIndex`.
+     * _.lastIndexOf([1, 2, 1, 2], 2, 2);
+     * // => 1
+     */
+    function lastIndexOf(array, value, fromIndex) {
+      var length = array ? array.length : 0;
+      if (!length) {
+        return -1;
+      }
+      var index = length;
+      if (fromIndex !== undefined) {
+        index = toInteger(fromIndex);
+        index = (index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1)) + 1;
+      }
+      if (value !== value) {
+        return indexOfNaN(array, index, true);
+      }
+      while (index--) {
+        if (array[index] === value) {
+          return index;
+        }
+      }
+      return -1;
+    }
+
+    /**
+     * Removes all given values from `array` using
+     * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * for equality comparisons.
+     *
+     * **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove`
+     * to remove elements from an array by predicate.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to modify.
+     * @param {...*} [values] The values to remove.
+     * @returns {Array} Returns `array`.
+     * @example
+     *
+     * var array = [1, 2, 3, 1, 2, 3];
+     *
+     * _.pull(array, 2, 3);
+     * console.log(array);
+     * // => [1, 1]
+     */
+    var pull = rest(pullAll);
+
+    /**
+     * This method is like `_.pull` except that it accepts an array of values to remove.
+     *
+     * **Note:** Unlike `_.difference`, this method mutates `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to modify.
+     * @param {Array} values The values to remove.
+     * @returns {Array} Returns `array`.
+     * @example
+     *
+     * var array = [1, 2, 3, 1, 2, 3];
+     *
+     * _.pullAll(array, [2, 3]);
+     * console.log(array);
+     * // => [1, 1]
+     */
+    function pullAll(array, values) {
+      return (array && array.length && values && values.length)
+        ? basePullAll(array, values)
+        : array;
+    }
+
+    /**
+     * This method is like `_.pullAll` except that it accepts `iteratee` which is
+     * invoked for each element of `array` and `values` to generate the criterion
+     * by which they're compared. The iteratee is invoked with one argument: (value).
+     *
+     * **Note:** Unlike `_.differenceBy`, this method mutates `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to modify.
+     * @param {Array} values The values to remove.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {Array} Returns `array`.
+     * @example
+     *
+     * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }];
+     *
+     * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x');
+     * console.log(array);
+     * // => [{ 'x': 2 }]
+     */
+    function pullAllBy(array, values, iteratee) {
+      return (array && array.length && values && values.length)
+        ? basePullAll(array, values, getIteratee(iteratee))
+        : array;
+    }
+
+    /**
+     * This method is like `_.pullAll` except that it accepts `comparator` which
+     * is invoked to compare elements of `array` to `values`. The comparator is
+     * invoked with two arguments: (arrVal, othVal).
+     *
+     * **Note:** Unlike `_.differenceWith`, this method mutates `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to modify.
+     * @param {Array} values The values to remove.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns `array`.
+     * @example
+     *
+     * var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }];
+     *
+     * _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual);
+     * console.log(array);
+     * // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }]
+     */
+    function pullAllWith(array, values, comparator) {
+      return (array && array.length && values && values.length)
+        ? basePullAll(array, values, undefined, comparator)
+        : array;
+    }
+
+    /**
+     * Removes elements from `array` corresponding to `indexes` and returns an
+     * array of removed elements.
+     *
+     * **Note:** Unlike `_.at`, this method mutates `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to modify.
+     * @param {...(number|number[])} [indexes] The indexes of elements to remove,
+     *  specified individually or in arrays.
+     * @returns {Array} Returns the new array of removed elements.
+     * @example
+     *
+     * var array = [5, 10, 15, 20];
+     * var evens = _.pullAt(array, 1, 3);
+     *
+     * console.log(array);
+     * // => [5, 15]
+     *
+     * console.log(evens);
+     * // => [10, 20]
+     */
+    var pullAt = rest(function(array, indexes) {
+      indexes = arrayMap(baseFlatten(indexes, 1), String);
+
+      var result = baseAt(array, indexes);
+      basePullAt(array, indexes.sort(compareAscending));
+      return result;
+    });
+
+    /**
+     * Removes all elements from `array` that `predicate` returns truthy for
+     * and returns an array of the removed elements. The predicate is invoked
+     * with three arguments: (value, index, array).
+     *
+     * **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull`
+     * to pull elements from an array by value.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to modify.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the new array of removed elements.
+     * @example
+     *
+     * var array = [1, 2, 3, 4];
+     * var evens = _.remove(array, function(n) {
+     *   return n % 2 == 0;
+     * });
+     *
+     * console.log(array);
+     * // => [1, 3]
+     *
+     * console.log(evens);
+     * // => [2, 4]
+     */
+    function remove(array, predicate) {
+      var result = [];
+      if (!(array && array.length)) {
+        return result;
+      }
+      var index = -1,
+          indexes = [],
+          length = array.length;
+
+      predicate = getIteratee(predicate, 3);
+      while (++index < length) {
+        var value = array[index];
+        if (predicate(value, index, array)) {
+          result.push(value);
+          indexes.push(index);
+        }
+      }
+      basePullAt(array, indexes);
+      return result;
+    }
+
+    /**
+     * Reverses `array` so that the first element becomes the last, the second
+     * element becomes the second to last, and so on.
+     *
+     * **Note:** This method mutates `array` and is based on
+     * [`Array#reverse`](https://mdn.io/Array/reverse).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @returns {Array} Returns `array`.
+     * @example
+     *
+     * var array = [1, 2, 3];
+     *
+     * _.reverse(array);
+     * // => [3, 2, 1]
+     *
+     * console.log(array);
+     * // => [3, 2, 1]
+     */
+    function reverse(array) {
+      return array ? nativeReverse.call(array) : array;
+    }
+
+    /**
+     * Creates a slice of `array` from `start` up to, but not including, `end`.
+     *
+     * **Note:** This method is used instead of [`Array#slice`](https://mdn.io/Array/slice)
+     * to ensure dense arrays are returned.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to slice.
+     * @param {number} [start=0] The start position.
+     * @param {number} [end=array.length] The end position.
+     * @returns {Array} Returns the slice of `array`.
+     */
+    function slice(array, start, end) {
+      var length = array ? array.length : 0;
+      if (!length) {
+        return [];
+      }
+      if (end && typeof end != 'number' && isIterateeCall(array, start, end)) {
+        start = 0;
+        end = length;
+      }
+      else {
+        start = start == null ? 0 : toInteger(start);
+        end = end === undefined ? length : toInteger(end);
+      }
+      return baseSlice(array, start, end);
+    }
+
+    /**
+     * Uses a binary search to determine the lowest index at which `value` should
+     * be inserted into `array` in order to maintain its sort order.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The sorted array to inspect.
+     * @param {*} value The value to evaluate.
+     * @returns {number} Returns the index at which `value` should be inserted into `array`.
+     * @example
+     *
+     * _.sortedIndex([30, 50], 40);
+     * // => 1
+     *
+     * _.sortedIndex([4, 5], 4);
+     * // => 0
+     */
+    function sortedIndex(array, value) {
+      return baseSortedIndex(array, value);
+    }
+
+    /**
+     * This method is like `_.sortedIndex` except that it accepts `iteratee`
+     * which is invoked for `value` and each element of `array` to compute their
+     * sort ranking. The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The sorted array to inspect.
+     * @param {*} value The value to evaluate.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {number} Returns the index at which `value` should be inserted into `array`.
+     * @example
+     *
+     * var dict = { 'thirty': 30, 'forty': 40, 'fifty': 50 };
+     *
+     * _.sortedIndexBy(['thirty', 'fifty'], 'forty', _.propertyOf(dict));
+     * // => 1
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.sortedIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');
+     * // => 0
+     */
+    function sortedIndexBy(array, value, iteratee) {
+      return baseSortedIndexBy(array, value, getIteratee(iteratee));
+    }
+
+    /**
+     * This method is like `_.indexOf` except that it performs a binary
+     * search on a sorted `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to search.
+     * @param {*} value The value to search for.
+     * @returns {number} Returns the index of the matched value, else `-1`.
+     * @example
+     *
+     * _.sortedIndexOf([1, 1, 2, 2], 2);
+     * // => 2
+     */
+    function sortedIndexOf(array, value) {
+      var length = array ? array.length : 0;
+      if (length) {
+        var index = baseSortedIndex(array, value);
+        if (index < length && eq(array[index], value)) {
+          return index;
+        }
+      }
+      return -1;
+    }
+
+    /**
+     * This method is like `_.sortedIndex` except that it returns the highest
+     * index at which `value` should be inserted into `array` in order to
+     * maintain its sort order.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The sorted array to inspect.
+     * @param {*} value The value to evaluate.
+     * @returns {number} Returns the index at which `value` should be inserted into `array`.
+     * @example
+     *
+     * _.sortedLastIndex([4, 5], 4);
+     * // => 1
+     */
+    function sortedLastIndex(array, value) {
+      return baseSortedIndex(array, value, true);
+    }
+
+    /**
+     * This method is like `_.sortedLastIndex` except that it accepts `iteratee`
+     * which is invoked for `value` and each element of `array` to compute their
+     * sort ranking. The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The sorted array to inspect.
+     * @param {*} value The value to evaluate.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {number} Returns the index at which `value` should be inserted into `array`.
+     * @example
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.sortedLastIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');
+     * // => 1
+     */
+    function sortedLastIndexBy(array, value, iteratee) {
+      return baseSortedIndexBy(array, value, getIteratee(iteratee), true);
+    }
+
+    /**
+     * This method is like `_.lastIndexOf` except that it performs a binary
+     * search on a sorted `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to search.
+     * @param {*} value The value to search for.
+     * @returns {number} Returns the index of the matched value, else `-1`.
+     * @example
+     *
+     * _.sortedLastIndexOf([1, 1, 2, 2], 2);
+     * // => 3
+     */
+    function sortedLastIndexOf(array, value) {
+      var length = array ? array.length : 0;
+      if (length) {
+        var index = baseSortedIndex(array, value, true) - 1;
+        if (eq(array[index], value)) {
+          return index;
+        }
+      }
+      return -1;
+    }
+
+    /**
+     * This method is like `_.uniq` except that it's designed and optimized
+     * for sorted arrays.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to inspect.
+     * @returns {Array} Returns the new duplicate free array.
+     * @example
+     *
+     * _.sortedUniq([1, 1, 2]);
+     * // => [1, 2]
+     */
+    function sortedUniq(array) {
+      return (array && array.length)
+        ? baseSortedUniq(array)
+        : [];
+    }
+
+    /**
+     * This method is like `_.uniqBy` except that it's designed and optimized
+     * for sorted arrays.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to inspect.
+     * @param {Function} [iteratee] The iteratee invoked per element.
+     * @returns {Array} Returns the new duplicate free array.
+     * @example
+     *
+     * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor);
+     * // => [1.1, 2.3]
+     */
+    function sortedUniqBy(array, iteratee) {
+      return (array && array.length)
+        ? baseSortedUniqBy(array, getIteratee(iteratee))
+        : [];
+    }
+
+    /**
+     * Gets all but the first element of `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * _.tail([1, 2, 3]);
+     * // => [2, 3]
+     */
+    function tail(array) {
+      return drop(array, 1);
+    }
+
+    /**
+     * Creates a slice of `array` with `n` elements taken from the beginning.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @param {number} [n=1] The number of elements to take.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * _.take([1, 2, 3]);
+     * // => [1]
+     *
+     * _.take([1, 2, 3], 2);
+     * // => [1, 2]
+     *
+     * _.take([1, 2, 3], 5);
+     * // => [1, 2, 3]
+     *
+     * _.take([1, 2, 3], 0);
+     * // => []
+     */
+    function take(array, n, guard) {
+      if (!(array && array.length)) {
+        return [];
+      }
+      n = (guard || n === undefined) ? 1 : toInteger(n);
+      return baseSlice(array, 0, n < 0 ? 0 : n);
+    }
+
+    /**
+     * Creates a slice of `array` with `n` elements taken from the end.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @param {number} [n=1] The number of elements to take.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * _.takeRight([1, 2, 3]);
+     * // => [3]
+     *
+     * _.takeRight([1, 2, 3], 2);
+     * // => [2, 3]
+     *
+     * _.takeRight([1, 2, 3], 5);
+     * // => [1, 2, 3]
+     *
+     * _.takeRight([1, 2, 3], 0);
+     * // => []
+     */
+    function takeRight(array, n, guard) {
+      var length = array ? array.length : 0;
+      if (!length) {
+        return [];
+      }
+      n = (guard || n === undefined) ? 1 : toInteger(n);
+      n = length - n;
+      return baseSlice(array, n < 0 ? 0 : n, length);
+    }
+
+    /**
+     * Creates a slice of `array` with elements taken from the end. Elements are
+     * taken until `predicate` returns falsey. The predicate is invoked with three
+     * arguments: (value, index, array).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney',  'active': true },
+     *   { 'user': 'fred',    'active': false },
+     *   { 'user': 'pebbles', 'active': false }
+     * ];
+     *
+     * _.takeRightWhile(users, function(o) { return !o.active; });
+     * // => objects for ['fred', 'pebbles']
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.takeRightWhile(users, { 'user': 'pebbles', 'active': false });
+     * // => objects for ['pebbles']
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.takeRightWhile(users, ['active', false]);
+     * // => objects for ['fred', 'pebbles']
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.takeRightWhile(users, 'active');
+     * // => []
+     */
+    function takeRightWhile(array, predicate) {
+      return (array && array.length)
+        ? baseWhile(array, getIteratee(predicate, 3), false, true)
+        : [];
+    }
+
+    /**
+     * Creates a slice of `array` with elements taken from the beginning. Elements
+     * are taken until `predicate` returns falsey. The predicate is invoked with
+     * three arguments: (value, index, array).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to query.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the slice of `array`.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney',  'active': false },
+     *   { 'user': 'fred',    'active': false},
+     *   { 'user': 'pebbles', 'active': true }
+     * ];
+     *
+     * _.takeWhile(users, function(o) { return !o.active; });
+     * // => objects for ['barney', 'fred']
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.takeWhile(users, { 'user': 'barney', 'active': false });
+     * // => objects for ['barney']
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.takeWhile(users, ['active', false]);
+     * // => objects for ['barney', 'fred']
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.takeWhile(users, 'active');
+     * // => []
+     */
+    function takeWhile(array, predicate) {
+      return (array && array.length)
+        ? baseWhile(array, getIteratee(predicate, 3))
+        : [];
+    }
+
+    /**
+     * Creates an array of unique values, in order, from all given arrays using
+     * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * for equality comparisons.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to inspect.
+     * @returns {Array} Returns the new array of combined values.
+     * @example
+     *
+     * _.union([2, 1], [4, 2], [1, 2]);
+     * // => [2, 1, 4]
+     */
+    var union = rest(function(arrays) {
+      return baseUniq(baseFlatten(arrays, 1, true));
+    });
+
+    /**
+     * This method is like `_.union` except that it accepts `iteratee` which is
+     * invoked for each element of each `arrays` to generate the criterion by which
+     * uniqueness is computed. The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to inspect.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {Array} Returns the new array of combined values.
+     * @example
+     *
+     * _.unionBy([2.1, 1.2], [4.3, 2.4], Math.floor);
+     * // => [2.1, 1.2, 4.3]
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
+     * // => [{ 'x': 1 }, { 'x': 2 }]
+     */
+    var unionBy = rest(function(arrays) {
+      var iteratee = last(arrays);
+      if (isArrayLikeObject(iteratee)) {
+        iteratee = undefined;
+      }
+      return baseUniq(baseFlatten(arrays, 1, true), getIteratee(iteratee));
+    });
+
+    /**
+     * This method is like `_.union` except that it accepts `comparator` which
+     * is invoked to compare elements of `arrays`. The comparator is invoked
+     * with two arguments: (arrVal, othVal).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to inspect.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns the new array of combined values.
+     * @example
+     *
+     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
+     * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
+     *
+     * _.unionWith(objects, others, _.isEqual);
+     * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]
+     */
+    var unionWith = rest(function(arrays) {
+      var comparator = last(arrays);
+      if (isArrayLikeObject(comparator)) {
+        comparator = undefined;
+      }
+      return baseUniq(baseFlatten(arrays, 1, true), undefined, comparator);
+    });
+
+    /**
+     * Creates a duplicate-free version of an array, using
+     * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * for equality comparisons, in which only the first occurrence of each element
+     * is kept.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to inspect.
+     * @returns {Array} Returns the new duplicate free array.
+     * @example
+     *
+     * _.uniq([2, 1, 2]);
+     * // => [2, 1]
+     */
+    function uniq(array) {
+      return (array && array.length)
+        ? baseUniq(array)
+        : [];
+    }
+
+    /**
+     * This method is like `_.uniq` except that it accepts `iteratee` which is
+     * invoked for each element in `array` to generate the criterion by which
+     * uniqueness is computed. The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to inspect.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {Array} Returns the new duplicate free array.
+     * @example
+     *
+     * _.uniqBy([2.1, 1.2, 2.3], Math.floor);
+     * // => [2.1, 1.2]
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');
+     * // => [{ 'x': 1 }, { 'x': 2 }]
+     */
+    function uniqBy(array, iteratee) {
+      return (array && array.length)
+        ? baseUniq(array, getIteratee(iteratee))
+        : [];
+    }
+
+    /**
+     * This method is like `_.uniq` except that it accepts `comparator` which
+     * is invoked to compare elements of `array`. The comparator is invoked with
+     * two arguments: (arrVal, othVal).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to inspect.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns the new duplicate free array.
+     * @example
+     *
+     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 },  { 'x': 1, 'y': 2 }];
+     *
+     * _.uniqWith(objects, _.isEqual);
+     * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]
+     */
+    function uniqWith(array, comparator) {
+      return (array && array.length)
+        ? baseUniq(array, undefined, comparator)
+        : [];
+    }
+
+    /**
+     * This method is like `_.zip` except that it accepts an array of grouped
+     * elements and creates an array regrouping the elements to their pre-zip
+     * configuration.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array of grouped elements to process.
+     * @returns {Array} Returns the new array of regrouped elements.
+     * @example
+     *
+     * var zipped = _.zip(['fred', 'barney'], [30, 40], [true, false]);
+     * // => [['fred', 30, true], ['barney', 40, false]]
+     *
+     * _.unzip(zipped);
+     * // => [['fred', 'barney'], [30, 40], [true, false]]
+     */
+    function unzip(array) {
+      if (!(array && array.length)) {
+        return [];
+      }
+      var length = 0;
+      array = arrayFilter(array, function(group) {
+        if (isArrayLikeObject(group)) {
+          length = nativeMax(group.length, length);
+          return true;
+        }
+      });
+      return baseTimes(length, function(index) {
+        return arrayMap(array, baseProperty(index));
+      });
+    }
+
+    /**
+     * This method is like `_.unzip` except that it accepts `iteratee` to specify
+     * how regrouped values should be combined. The iteratee is invoked with the
+     * elements of each group: (...group).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array of grouped elements to process.
+     * @param {Function} [iteratee=_.identity] The function to combine regrouped values.
+     * @returns {Array} Returns the new array of regrouped elements.
+     * @example
+     *
+     * var zipped = _.zip([1, 2], [10, 20], [100, 200]);
+     * // => [[1, 10, 100], [2, 20, 200]]
+     *
+     * _.unzipWith(zipped, _.add);
+     * // => [3, 30, 300]
+     */
+    function unzipWith(array, iteratee) {
+      if (!(array && array.length)) {
+        return [];
+      }
+      var result = unzip(array);
+      if (iteratee == null) {
+        return result;
+      }
+      return arrayMap(result, function(group) {
+        return apply(iteratee, undefined, group);
+      });
+    }
+
+    /**
+     * Creates an array excluding all given values using
+     * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * for equality comparisons.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} array The array to filter.
+     * @param {...*} [values] The values to exclude.
+     * @returns {Array} Returns the new array of filtered values.
+     * @example
+     *
+     * _.without([1, 2, 1, 3], 1, 2);
+     * // => [3]
+     */
+    var without = rest(function(array, values) {
+      return isArrayLikeObject(array)
+        ? baseDifference(array, values)
+        : [];
+    });
+
+    /**
+     * Creates an array of unique values that is the [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference)
+     * of the given arrays. The order of result values is determined by the order
+     * they occur in the arrays.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to inspect.
+     * @returns {Array} Returns the new array of values.
+     * @example
+     *
+     * _.xor([2, 1], [4, 2]);
+     * // => [1, 4]
+     */
+    var xor = rest(function(arrays) {
+      return baseXor(arrayFilter(arrays, isArrayLikeObject));
+    });
+
+    /**
+     * This method is like `_.xor` except that it accepts `iteratee` which is
+     * invoked for each element of each `arrays` to generate the criterion by which
+     * by which they're compared. The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to inspect.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {Array} Returns the new array of values.
+     * @example
+     *
+     * _.xorBy([2.1, 1.2], [4.3, 2.4], Math.floor);
+     * // => [1.2, 4.3]
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
+     * // => [{ 'x': 2 }]
+     */
+    var xorBy = rest(function(arrays) {
+      var iteratee = last(arrays);
+      if (isArrayLikeObject(iteratee)) {
+        iteratee = undefined;
+      }
+      return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee));
+    });
+
+    /**
+     * This method is like `_.xor` except that it accepts `comparator` which is
+     * invoked to compare elements of `arrays`. The comparator is invoked with
+     * two arguments: (arrVal, othVal).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to inspect.
+     * @param {Function} [comparator] The comparator invoked per element.
+     * @returns {Array} Returns the new array of values.
+     * @example
+     *
+     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
+     * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];
+     *
+     * _.xorWith(objects, others, _.isEqual);
+     * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]
+     */
+    var xorWith = rest(function(arrays) {
+      var comparator = last(arrays);
+      if (isArrayLikeObject(comparator)) {
+        comparator = undefined;
+      }
+      return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined, comparator);
+    });
+
+    /**
+     * Creates an array of grouped elements, the first of which contains the first
+     * elements of the given arrays, the second of which contains the second elements
+     * of the given arrays, and so on.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to process.
+     * @returns {Array} Returns the new array of grouped elements.
+     * @example
+     *
+     * _.zip(['fred', 'barney'], [30, 40], [true, false]);
+     * // => [['fred', 30, true], ['barney', 40, false]]
+     */
+    var zip = rest(unzip);
+
+    /**
+     * This method is like `_.fromPairs` except that it accepts two arrays,
+     * one of property names and one of corresponding values.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} [props=[]] The property names.
+     * @param {Array} [values=[]] The property values.
+     * @returns {Object} Returns the new object.
+     * @example
+     *
+     * _.zipObject(['a', 'b'], [1, 2]);
+     * // => { 'a': 1, 'b': 2 }
+     */
+    function zipObject(props, values) {
+      return baseZipObject(props || [], values || [], assignValue);
+    }
+
+    /**
+     * This method is like `_.zipObject` except that it supports property paths.
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {Array} [props=[]] The property names.
+     * @param {Array} [values=[]] The property values.
+     * @returns {Object} Returns the new object.
+     * @example
+     *
+     * _.zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2]);
+     * // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } }
+     */
+    function zipObjectDeep(props, values) {
+      return baseZipObject(props || [], values || [], baseSet);
+    }
+
+    /**
+     * This method is like `_.zip` except that it accepts `iteratee` to specify
+     * how grouped values should be combined. The iteratee is invoked with the
+     * elements of each group: (...group).
+     *
+     * @static
+     * @memberOf _
+     * @category Array
+     * @param {...Array} [arrays] The arrays to process.
+     * @param {Function} [iteratee=_.identity] The function to combine grouped values.
+     * @returns {Array} Returns the new array of grouped elements.
+     * @example
+     *
+     * _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) {
+     *   return a + b + c;
+     * });
+     * // => [111, 222]
+     */
+    var zipWith = rest(function(arrays) {
+      var length = arrays.length,
+          iteratee = length > 1 ? arrays[length - 1] : undefined;
+
+      iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined;
+      return unzipWith(arrays, iteratee);
+    });
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Creates a `lodash` object that wraps `value` with explicit method chaining enabled.
+     * The result of such method chaining must be unwrapped with `_#value`.
+     *
+     * @static
+     * @memberOf _
+     * @category Seq
+     * @param {*} value The value to wrap.
+     * @returns {Object} Returns the new `lodash` wrapper instance.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney',  'age': 36 },
+     *   { 'user': 'fred',    'age': 40 },
+     *   { 'user': 'pebbles', 'age': 1 }
+     * ];
+     *
+     * var youngest = _
+     *   .chain(users)
+     *   .sortBy('age')
+     *   .map(function(o) {
+     *     return o.user + ' is ' + o.age;
+     *   })
+     *   .head()
+     *   .value();
+     * // => 'pebbles is 1'
+     */
+    function chain(value) {
+      var result = lodash(value);
+      result.__chain__ = true;
+      return result;
+    }
+
+    /**
+     * This method invokes `interceptor` and returns `value`. The interceptor
+     * is invoked with one argument; (value). The purpose of this method is to
+     * "tap into" a method chain in order to modify intermediate results.
+     *
+     * @static
+     * @memberOf _
+     * @category Seq
+     * @param {*} value The value to provide to `interceptor`.
+     * @param {Function} interceptor The function to invoke.
+     * @returns {*} Returns `value`.
+     * @example
+     *
+     * _([1, 2, 3])
+     *  .tap(function(array) {
+     *    // Mutate input array.
+     *    array.pop();
+     *  })
+     *  .reverse()
+     *  .value();
+     * // => [2, 1]
+     */
+    function tap(value, interceptor) {
+      interceptor(value);
+      return value;
+    }
+
+    /**
+     * This method is like `_.tap` except that it returns the result of `interceptor`.
+     * The purpose of this method is to "pass thru" values replacing intermediate
+     * results in a method chain.
+     *
+     * @static
+     * @memberOf _
+     * @category Seq
+     * @param {*} value The value to provide to `interceptor`.
+     * @param {Function} interceptor The function to invoke.
+     * @returns {*} Returns the result of `interceptor`.
+     * @example
+     *
+     * _('  abc  ')
+     *  .chain()
+     *  .trim()
+     *  .thru(function(value) {
+     *    return [value];
+     *  })
+     *  .value();
+     * // => ['abc']
+     */
+    function thru(value, interceptor) {
+      return interceptor(value);
+    }
+
+    /**
+     * This method is the wrapper version of `_.at`.
+     *
+     * @name at
+     * @memberOf _
+     * @category Seq
+     * @param {...(string|string[])} [paths] The property paths of elements to pick,
+     *  specified individually or in arrays.
+     * @returns {Object} Returns the new `lodash` wrapper instance.
+     * @example
+     *
+     * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };
+     *
+     * _(object).at(['a[0].b.c', 'a[1]']).value();
+     * // => [3, 4]
+     *
+     * _(['a', 'b', 'c']).at(0, 2).value();
+     * // => ['a', 'c']
+     */
+    var wrapperAt = rest(function(paths) {
+      paths = baseFlatten(paths, 1);
+      var length = paths.length,
+          start = length ? paths[0] : 0,
+          value = this.__wrapped__,
+          interceptor = function(object) { return baseAt(object, paths); };
+
+      if (length > 1 || this.__actions__.length ||
+          !(value instanceof LazyWrapper) || !isIndex(start)) {
+        return this.thru(interceptor);
+      }
+      value = value.slice(start, +start + (length ? 1 : 0));
+      value.__actions__.push({
+        'func': thru,
+        'args': [interceptor],
+        'thisArg': undefined
+      });
+      return new LodashWrapper(value, this.__chain__).thru(function(array) {
+        if (length && !array.length) {
+          array.push(undefined);
+        }
+        return array;
+      });
+    });
+
+    /**
+     * Enables explicit method chaining on the wrapper object.
+     *
+     * @name chain
+     * @memberOf _
+     * @category Seq
+     * @returns {Object} Returns the new `lodash` wrapper instance.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney', 'age': 36 },
+     *   { 'user': 'fred',   'age': 40 }
+     * ];
+     *
+     * // A sequence without explicit chaining.
+     * _(users).head();
+     * // => { 'user': 'barney', 'age': 36 }
+     *
+     * // A sequence with explicit chaining.
+     * _(users)
+     *   .chain()
+     *   .head()
+     *   .pick('user')
+     *   .value();
+     * // => { 'user': 'barney' }
+     */
+    function wrapperChain() {
+      return chain(this);
+    }
+
+    /**
+     * Executes the chained sequence and returns the wrapped result.
+     *
+     * @name commit
+     * @memberOf _
+     * @category Seq
+     * @returns {Object} Returns the new `lodash` wrapper instance.
+     * @example
+     *
+     * var array = [1, 2];
+     * var wrapped = _(array).push(3);
+     *
+     * console.log(array);
+     * // => [1, 2]
+     *
+     * wrapped = wrapped.commit();
+     * console.log(array);
+     * // => [1, 2, 3]
+     *
+     * wrapped.last();
+     * // => 3
+     *
+     * console.log(array);
+     * // => [1, 2, 3]
+     */
+    function wrapperCommit() {
+      return new LodashWrapper(this.value(), this.__chain__);
+    }
+
+    /**
+     * This method is the wrapper version of `_.flatMap`.
+     *
+     * @name flatMap
+     * @memberOf _
+     * @category Seq
+     * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Object} Returns the new `lodash` wrapper instance.
+     * @example
+     *
+     * function duplicate(n) {
+     *   return [n, n];
+     * }
+     *
+     * _([1, 2]).flatMap(duplicate).value();
+     * // => [1, 1, 2, 2]
+     */
+    function wrapperFlatMap(iteratee) {
+      return this.map(iteratee).flatten();
+    }
+
+    /**
+     * Gets the next value on a wrapped object following the
+     * [iterator protocol](https://mdn.io/iteration_protocols#iterator).
+     *
+     * @name next
+     * @memberOf _
+     * @category Seq
+     * @returns {Object} Returns the next iterator value.
+     * @example
+     *
+     * var wrapped = _([1, 2]);
+     *
+     * wrapped.next();
+     * // => { 'done': false, 'value': 1 }
+     *
+     * wrapped.next();
+     * // => { 'done': false, 'value': 2 }
+     *
+     * wrapped.next();
+     * // => { 'done': true, 'value': undefined }
+     */
+    function wrapperNext() {
+      if (this.__values__ === undefined) {
+        this.__values__ = toArray(this.value());
+      }
+      var done = this.__index__ >= this.__values__.length,
+          value = done ? undefined : this.__values__[this.__index__++];
+
+      return { 'done': done, 'value': value };
+    }
+
+    /**
+     * Enables the wrapper to be iterable.
+     *
+     * @name Symbol.iterator
+     * @memberOf _
+     * @category Seq
+     * @returns {Object} Returns the wrapper object.
+     * @example
+     *
+     * var wrapped = _([1, 2]);
+     *
+     * wrapped[Symbol.iterator]() === wrapped;
+     * // => true
+     *
+     * Array.from(wrapped);
+     * // => [1, 2]
+     */
+    function wrapperToIterator() {
+      return this;
+    }
+
+    /**
+     * Creates a clone of the chained sequence planting `value` as the wrapped value.
+     *
+     * @name plant
+     * @memberOf _
+     * @category Seq
+     * @param {*} value The value to plant.
+     * @returns {Object} Returns the new `lodash` wrapper instance.
+     * @example
+     *
+     * function square(n) {
+     *   return n * n;
+     * }
+     *
+     * var wrapped = _([1, 2]).map(square);
+     * var other = wrapped.plant([3, 4]);
+     *
+     * other.value();
+     * // => [9, 16]
+     *
+     * wrapped.value();
+     * // => [1, 4]
+     */
+    function wrapperPlant(value) {
+      var result,
+          parent = this;
+
+      while (parent instanceof baseLodash) {
+        var clone = wrapperClone(parent);
+        clone.__index__ = 0;
+        clone.__values__ = undefined;
+        if (result) {
+          previous.__wrapped__ = clone;
+        } else {
+          result = clone;
+        }
+        var previous = clone;
+        parent = parent.__wrapped__;
+      }
+      previous.__wrapped__ = value;
+      return result;
+    }
+
+    /**
+     * This method is the wrapper version of `_.reverse`.
+     *
+     * **Note:** This method mutates the wrapped array.
+     *
+     * @name reverse
+     * @memberOf _
+     * @category Seq
+     * @returns {Object} Returns the new `lodash` wrapper instance.
+     * @example
+     *
+     * var array = [1, 2, 3];
+     *
+     * _(array).reverse().value()
+     * // => [3, 2, 1]
+     *
+     * console.log(array);
+     * // => [3, 2, 1]
+     */
+    function wrapperReverse() {
+      var value = this.__wrapped__;
+      if (value instanceof LazyWrapper) {
+        var wrapped = value;
+        if (this.__actions__.length) {
+          wrapped = new LazyWrapper(this);
+        }
+        wrapped = wrapped.reverse();
+        wrapped.__actions__.push({
+          'func': thru,
+          'args': [reverse],
+          'thisArg': undefined
+        });
+        return new LodashWrapper(wrapped, this.__chain__);
+      }
+      return this.thru(reverse);
+    }
+
+    /**
+     * Executes the chained sequence to extract the unwrapped value.
+     *
+     * @name value
+     * @memberOf _
+     * @alias toJSON, valueOf
+     * @category Seq
+     * @returns {*} Returns the resolved unwrapped value.
+     * @example
+     *
+     * _([1, 2, 3]).value();
+     * // => [1, 2, 3]
+     */
+    function wrapperValue() {
+      return baseWrapperValue(this.__wrapped__, this.__actions__);
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Creates an object composed of keys generated from the results of running
+     * each element of `collection` through `iteratee`. The corresponding value
+     * of each key is the number of times the key was returned by `iteratee`.
+     * The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee to transform keys.
+     * @returns {Object} Returns the composed aggregate object.
+     * @example
+     *
+     * _.countBy([6.1, 4.2, 6.3], Math.floor);
+     * // => { '4': 1, '6': 2 }
+     *
+     * _.countBy(['one', 'two', 'three'], 'length');
+     * // => { '3': 2, '5': 1 }
+     */
+    var countBy = createAggregator(function(result, value, key) {
+      hasOwnProperty.call(result, key) ? ++result[key] : (result[key] = 1);
+    });
+
+    /**
+     * Checks if `predicate` returns truthy for **all** elements of `collection`.
+     * Iteration is stopped once `predicate` returns falsey. The predicate is
+     * invoked with three arguments: (value, index|key, collection).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {boolean} Returns `true` if all elements pass the predicate check, else `false`.
+     * @example
+     *
+     * _.every([true, 1, null, 'yes'], Boolean);
+     * // => false
+     *
+     * var users = [
+     *   { 'user': 'barney', 'active': false },
+     *   { 'user': 'fred',   'active': false }
+     * ];
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.every(users, { 'user': 'barney', 'active': false });
+     * // => false
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.every(users, ['active', false]);
+     * // => true
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.every(users, 'active');
+     * // => false
+     */
+    function every(collection, predicate, guard) {
+      var func = isArray(collection) ? arrayEvery : baseEvery;
+      if (guard && isIterateeCall(collection, predicate, guard)) {
+        predicate = undefined;
+      }
+      return func(collection, getIteratee(predicate, 3));
+    }
+
+    /**
+     * Iterates over elements of `collection`, returning an array of all elements
+     * `predicate` returns truthy for. The predicate is invoked with three arguments:
+     * (value, index|key, collection).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the new filtered array.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney', 'age': 36, 'active': true },
+     *   { 'user': 'fred',   'age': 40, 'active': false }
+     * ];
+     *
+     * _.filter(users, function(o) { return !o.active; });
+     * // => objects for ['fred']
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.filter(users, { 'age': 36, 'active': true });
+     * // => objects for ['barney']
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.filter(users, ['active', false]);
+     * // => objects for ['fred']
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.filter(users, 'active');
+     * // => objects for ['barney']
+     */
+    function filter(collection, predicate) {
+      var func = isArray(collection) ? arrayFilter : baseFilter;
+      return func(collection, getIteratee(predicate, 3));
+    }
+
+    /**
+     * Iterates over elements of `collection`, returning the first element
+     * `predicate` returns truthy for. The predicate is invoked with three arguments:
+     * (value, index|key, collection).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to search.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {*} Returns the matched element, else `undefined`.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney',  'age': 36, 'active': true },
+     *   { 'user': 'fred',    'age': 40, 'active': false },
+     *   { 'user': 'pebbles', 'age': 1,  'active': true }
+     * ];
+     *
+     * _.find(users, function(o) { return o.age < 40; });
+     * // => object for 'barney'
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.find(users, { 'age': 1, 'active': true });
+     * // => object for 'pebbles'
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.find(users, ['active', false]);
+     * // => object for 'fred'
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.find(users, 'active');
+     * // => object for 'barney'
+     */
+    function find(collection, predicate) {
+      predicate = getIteratee(predicate, 3);
+      if (isArray(collection)) {
+        var index = baseFindIndex(collection, predicate);
+        return index > -1 ? collection[index] : undefined;
+      }
+      return baseFind(collection, predicate, baseEach);
+    }
+
+    /**
+     * This method is like `_.find` except that it iterates over elements of
+     * `collection` from right to left.
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to search.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {*} Returns the matched element, else `undefined`.
+     * @example
+     *
+     * _.findLast([1, 2, 3, 4], function(n) {
+     *   return n % 2 == 1;
+     * });
+     * // => 3
+     */
+    function findLast(collection, predicate) {
+      predicate = getIteratee(predicate, 3);
+      if (isArray(collection)) {
+        var index = baseFindIndex(collection, predicate, true);
+        return index > -1 ? collection[index] : undefined;
+      }
+      return baseFind(collection, predicate, baseEachRight);
+    }
+
+    /**
+     * Creates an array of flattened values by running each element in `collection`
+     * through `iteratee` and concating its result to the other mapped values.
+     * The iteratee is invoked with three arguments: (value, index|key, collection).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the new flattened array.
+     * @example
+     *
+     * function duplicate(n) {
+     *   return [n, n];
+     * }
+     *
+     * _.flatMap([1, 2], duplicate);
+     * // => [1, 1, 2, 2]
+     */
+    function flatMap(collection, iteratee) {
+      return baseFlatten(map(collection, iteratee), 1);
+    }
+
+    /**
+     * Iterates over elements of `collection` invoking `iteratee` for each element.
+     * The iteratee is invoked with three arguments: (value, index|key, collection).
+     * Iteratee functions may exit iteration early by explicitly returning `false`.
+     *
+     * **Note:** As with other "Collections" methods, objects with a "length" property
+     * are iterated like arrays. To avoid this behavior use `_.forIn` or `_.forOwn`
+     * for object iteration.
+     *
+     * @static
+     * @memberOf _
+     * @alias each
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Array|Object} Returns `collection`.
+     * @example
+     *
+     * _([1, 2]).forEach(function(value) {
+     *   console.log(value);
+     * });
+     * // => logs `1` then `2`
+     *
+     * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) {
+     *   console.log(key);
+     * });
+     * // => logs 'a' then 'b' (iteration order is not guaranteed)
+     */
+    function forEach(collection, iteratee) {
+      return (typeof iteratee == 'function' && isArray(collection))
+        ? arrayEach(collection, iteratee)
+        : baseEach(collection, baseCastFunction(iteratee));
+    }
+
+    /**
+     * This method is like `_.forEach` except that it iterates over elements of
+     * `collection` from right to left.
+     *
+     * @static
+     * @memberOf _
+     * @alias eachRight
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Array|Object} Returns `collection`.
+     * @example
+     *
+     * _.forEachRight([1, 2], function(value) {
+     *   console.log(value);
+     * });
+     * // => logs `2` then `1`
+     */
+    function forEachRight(collection, iteratee) {
+      return (typeof iteratee == 'function' && isArray(collection))
+        ? arrayEachRight(collection, iteratee)
+        : baseEachRight(collection, baseCastFunction(iteratee));
+    }
+
+    /**
+     * Creates an object composed of keys generated from the results of running
+     * each element of `collection` through `iteratee`. The corresponding value
+     * of each key is an array of elements responsible for generating the key.
+     * The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee to transform keys.
+     * @returns {Object} Returns the composed aggregate object.
+     * @example
+     *
+     * _.groupBy([6.1, 4.2, 6.3], Math.floor);
+     * // => { '4': [4.2], '6': [6.1, 6.3] }
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.groupBy(['one', 'two', 'three'], 'length');
+     * // => { '3': ['one', 'two'], '5': ['three'] }
+     */
+    var groupBy = createAggregator(function(result, value, key) {
+      if (hasOwnProperty.call(result, key)) {
+        result[key].push(value);
+      } else {
+        result[key] = [value];
+      }
+    });
+
+    /**
+     * Checks if `value` is in `collection`. If `collection` is a string it's checked
+     * for a substring of `value`, otherwise [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * is used for equality comparisons. If `fromIndex` is negative, it's used as
+     * the offset from the end of `collection`.
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object|string} collection The collection to search.
+     * @param {*} value The value to search for.
+     * @param {number} [fromIndex=0] The index to search from.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.reduce`.
+     * @returns {boolean} Returns `true` if `value` is found, else `false`.
+     * @example
+     *
+     * _.includes([1, 2, 3], 1);
+     * // => true
+     *
+     * _.includes([1, 2, 3], 1, 2);
+     * // => false
+     *
+     * _.includes({ 'user': 'fred', 'age': 40 }, 'fred');
+     * // => true
+     *
+     * _.includes('pebbles', 'eb');
+     * // => true
+     */
+    function includes(collection, value, fromIndex, guard) {
+      collection = isArrayLike(collection) ? collection : values(collection);
+      fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0;
+
+      var length = collection.length;
+      if (fromIndex < 0) {
+        fromIndex = nativeMax(length + fromIndex, 0);
+      }
+      return isString(collection)
+        ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1)
+        : (!!length && baseIndexOf(collection, value, fromIndex) > -1);
+    }
+
+    /**
+     * Invokes the method at `path` of each element in `collection`, returning
+     * an array of the results of each invoked method. Any additional arguments
+     * are provided to each invoked method. If `methodName` is a function it's
+     * invoked for, and `this` bound to, each element in `collection`.
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Array|Function|string} path The path of the method to invoke or
+     *  the function invoked per iteration.
+     * @param {...*} [args] The arguments to invoke each method with.
+     * @returns {Array} Returns the array of results.
+     * @example
+     *
+     * _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort');
+     * // => [[1, 5, 7], [1, 2, 3]]
+     *
+     * _.invokeMap([123, 456], String.prototype.split, '');
+     * // => [['1', '2', '3'], ['4', '5', '6']]
+     */
+    var invokeMap = rest(function(collection, path, args) {
+      var index = -1,
+          isFunc = typeof path == 'function',
+          isProp = isKey(path),
+          result = isArrayLike(collection) ? Array(collection.length) : [];
+
+      baseEach(collection, function(value) {
+        var func = isFunc ? path : ((isProp && value != null) ? value[path] : undefined);
+        result[++index] = func ? apply(func, value, args) : baseInvoke(value, path, args);
+      });
+      return result;
+    });
+
+    /**
+     * Creates an object composed of keys generated from the results of running
+     * each element of `collection` through `iteratee`. The corresponding value
+     * of each key is the last element responsible for generating the key. The
+     * iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee to transform keys.
+     * @returns {Object} Returns the composed aggregate object.
+     * @example
+     *
+     * var array = [
+     *   { 'dir': 'left', 'code': 97 },
+     *   { 'dir': 'right', 'code': 100 }
+     * ];
+     *
+     * _.keyBy(array, function(o) {
+     *   return String.fromCharCode(o.code);
+     * });
+     * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } }
+     *
+     * _.keyBy(array, 'dir');
+     * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } }
+     */
+    var keyBy = createAggregator(function(result, value, key) {
+      result[key] = value;
+    });
+
+    /**
+     * Creates an array of values by running each element in `collection` through
+     * `iteratee`. The iteratee is invoked with three arguments:
+     * (value, index|key, collection).
+     *
+     * Many lodash methods are guarded to work as iteratees for methods like
+     * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`.
+     *
+     * The guarded methods are:
+     * `ary`, `curry`, `curryRight`, `drop`, `dropRight`, `every`, `fill`,
+     * `invert`, `parseInt`, `random`, `range`, `rangeRight`, `slice`, `some`,
+     * `sortBy`, `take`, `takeRight`, `template`, `trim`, `trimEnd`, `trimStart`,
+     * and `words`
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the new mapped array.
+     * @example
+     *
+     * function square(n) {
+     *   return n * n;
+     * }
+     *
+     * _.map([4, 8], square);
+     * // => [16, 64]
+     *
+     * _.map({ 'a': 4, 'b': 8 }, square);
+     * // => [16, 64] (iteration order is not guaranteed)
+     *
+     * var users = [
+     *   { 'user': 'barney' },
+     *   { 'user': 'fred' }
+     * ];
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.map(users, 'user');
+     * // => ['barney', 'fred']
+     */
+    function map(collection, iteratee) {
+      var func = isArray(collection) ? arrayMap : baseMap;
+      return func(collection, getIteratee(iteratee, 3));
+    }
+
+    /**
+     * This method is like `_.sortBy` except that it allows specifying the sort
+     * orders of the iteratees to sort by. If `orders` is unspecified, all values
+     * are sorted in ascending order. Otherwise, specify an order of "desc" for
+     * descending or "asc" for ascending sort order of corresponding values.
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function[]|Object[]|string[]} [iteratees=[_.identity]] The iteratees to sort by.
+     * @param {string[]} [orders] The sort orders of `iteratees`.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.reduce`.
+     * @returns {Array} Returns the new sorted array.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'fred',   'age': 48 },
+     *   { 'user': 'barney', 'age': 34 },
+     *   { 'user': 'fred',   'age': 42 },
+     *   { 'user': 'barney', 'age': 36 }
+     * ];
+     *
+     * // Sort by `user` in ascending order and by `age` in descending order.
+     * _.orderBy(users, ['user', 'age'], ['asc', 'desc']);
+     * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]]
+     */
+    function orderBy(collection, iteratees, orders, guard) {
+      if (collection == null) {
+        return [];
+      }
+      if (!isArray(iteratees)) {
+        iteratees = iteratees == null ? [] : [iteratees];
+      }
+      orders = guard ? undefined : orders;
+      if (!isArray(orders)) {
+        orders = orders == null ? [] : [orders];
+      }
+      return baseOrderBy(collection, iteratees, orders);
+    }
+
+    /**
+     * Creates an array of elements split into two groups, the first of which
+     * contains elements `predicate` returns truthy for, the second of which
+     * contains elements `predicate` returns falsey for. The predicate is
+     * invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the array of grouped elements.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney',  'age': 36, 'active': false },
+     *   { 'user': 'fred',    'age': 40, 'active': true },
+     *   { 'user': 'pebbles', 'age': 1,  'active': false }
+     * ];
+     *
+     * _.partition(users, function(o) { return o.active; });
+     * // => objects for [['fred'], ['barney', 'pebbles']]
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.partition(users, { 'age': 1, 'active': false });
+     * // => objects for [['pebbles'], ['barney', 'fred']]
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.partition(users, ['active', false]);
+     * // => objects for [['barney', 'pebbles'], ['fred']]
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.partition(users, 'active');
+     * // => objects for [['fred'], ['barney', 'pebbles']]
+     */
+    var partition = createAggregator(function(result, value, key) {
+      result[key ? 0 : 1].push(value);
+    }, function() { return [[], []]; });
+
+    /**
+     * Reduces `collection` to a value which is the accumulated result of running
+     * each element in `collection` through `iteratee`, where each successive
+     * invocation is supplied the return value of the previous. If `accumulator`
+     * is not given the first element of `collection` is used as the initial
+     * value. The iteratee is invoked with four arguments:
+     * (accumulator, value, index|key, collection).
+     *
+     * Many lodash methods are guarded to work as iteratees for methods like
+     * `_.reduce`, `_.reduceRight`, and `_.transform`.
+     *
+     * The guarded methods are:
+     * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`,
+     * and `sortBy`
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @param {*} [accumulator] The initial value.
+     * @returns {*} Returns the accumulated value.
+     * @example
+     *
+     * _.reduce([1, 2], function(sum, n) {
+     *   return sum + n;
+     * }, 0);
+     * // => 3
+     *
+     * _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {
+     *   (result[value] || (result[value] = [])).push(key);
+     *   return result;
+     * }, {});
+     * // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed)
+     */
+    function reduce(collection, iteratee, accumulator) {
+      var func = isArray(collection) ? arrayReduce : baseReduce,
+          initAccum = arguments.length < 3;
+
+      return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach);
+    }
+
+    /**
+     * This method is like `_.reduce` except that it iterates over elements of
+     * `collection` from right to left.
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @param {*} [accumulator] The initial value.
+     * @returns {*} Returns the accumulated value.
+     * @example
+     *
+     * var array = [[0, 1], [2, 3], [4, 5]];
+     *
+     * _.reduceRight(array, function(flattened, other) {
+     *   return flattened.concat(other);
+     * }, []);
+     * // => [4, 5, 2, 3, 0, 1]
+     */
+    function reduceRight(collection, iteratee, accumulator) {
+      var func = isArray(collection) ? arrayReduceRight : baseReduce,
+          initAccum = arguments.length < 3;
+
+      return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEachRight);
+    }
+
+    /**
+     * The opposite of `_.filter`; this method returns the elements of `collection`
+     * that `predicate` does **not** return truthy for.
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the new filtered array.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney', 'age': 36, 'active': false },
+     *   { 'user': 'fred',   'age': 40, 'active': true }
+     * ];
+     *
+     * _.reject(users, function(o) { return !o.active; });
+     * // => objects for ['fred']
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.reject(users, { 'age': 40, 'active': true });
+     * // => objects for ['barney']
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.reject(users, ['active', false]);
+     * // => objects for ['fred']
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.reject(users, 'active');
+     * // => objects for ['barney']
+     */
+    function reject(collection, predicate) {
+      var func = isArray(collection) ? arrayFilter : baseFilter;
+      predicate = getIteratee(predicate, 3);
+      return func(collection, function(value, index, collection) {
+        return !predicate(value, index, collection);
+      });
+    }
+
+    /**
+     * Gets a random element from `collection`.
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to sample.
+     * @returns {*} Returns the random element.
+     * @example
+     *
+     * _.sample([1, 2, 3, 4]);
+     * // => 2
+     */
+    function sample(collection) {
+      var array = isArrayLike(collection) ? collection : values(collection),
+          length = array.length;
+
+      return length > 0 ? array[baseRandom(0, length - 1)] : undefined;
+    }
+
+    /**
+     * Gets `n` random elements at unique keys from `collection` up to the
+     * size of `collection`.
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to sample.
+     * @param {number} [n=0] The number of elements to sample.
+     * @returns {Array} Returns the random elements.
+     * @example
+     *
+     * _.sampleSize([1, 2, 3], 2);
+     * // => [3, 1]
+     *
+     * _.sampleSize([1, 2, 3], 4);
+     * // => [2, 3, 1]
+     */
+    function sampleSize(collection, n) {
+      var index = -1,
+          result = toArray(collection),
+          length = result.length,
+          lastIndex = length - 1;
+
+      n = baseClamp(toInteger(n), 0, length);
+      while (++index < n) {
+        var rand = baseRandom(index, lastIndex),
+            value = result[rand];
+
+        result[rand] = result[index];
+        result[index] = value;
+      }
+      result.length = n;
+      return result;
+    }
+
+    /**
+     * Creates an array of shuffled values, using a version of the
+     * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to shuffle.
+     * @returns {Array} Returns the new shuffled array.
+     * @example
+     *
+     * _.shuffle([1, 2, 3, 4]);
+     * // => [4, 1, 3, 2]
+     */
+    function shuffle(collection) {
+      return sampleSize(collection, MAX_ARRAY_LENGTH);
+    }
+
+    /**
+     * Gets the size of `collection` by returning its length for array-like
+     * values or the number of own enumerable properties for objects.
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to inspect.
+     * @returns {number} Returns the collection size.
+     * @example
+     *
+     * _.size([1, 2, 3]);
+     * // => 3
+     *
+     * _.size({ 'a': 1, 'b': 2 });
+     * // => 2
+     *
+     * _.size('pebbles');
+     * // => 7
+     */
+    function size(collection) {
+      if (collection == null) {
+        return 0;
+      }
+      if (isArrayLike(collection)) {
+        var result = collection.length;
+        return (result && isString(collection)) ? stringSize(collection) : result;
+      }
+      return keys(collection).length;
+    }
+
+    /**
+     * Checks if `predicate` returns truthy for **any** element of `collection`.
+     * Iteration is stopped once `predicate` returns truthy. The predicate is
+     * invoked with three arguments: (value, index|key, collection).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {boolean} Returns `true` if any element passes the predicate check, else `false`.
+     * @example
+     *
+     * _.some([null, 0, 'yes', false], Boolean);
+     * // => true
+     *
+     * var users = [
+     *   { 'user': 'barney', 'active': true },
+     *   { 'user': 'fred',   'active': false }
+     * ];
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.some(users, { 'user': 'barney', 'active': false });
+     * // => false
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.some(users, ['active', false]);
+     * // => true
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.some(users, 'active');
+     * // => true
+     */
+    function some(collection, predicate, guard) {
+      var func = isArray(collection) ? arraySome : baseSome;
+      if (guard && isIterateeCall(collection, predicate, guard)) {
+        predicate = undefined;
+      }
+      return func(collection, getIteratee(predicate, 3));
+    }
+
+    /**
+     * Creates an array of elements, sorted in ascending order by the results of
+     * running each element in a collection through each iteratee. This method
+     * performs a stable sort, that is, it preserves the original sort order of
+     * equal elements. The iteratees are invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Collection
+     * @param {Array|Object} collection The collection to iterate over.
+     * @param {...(Function|Function[]|Object|Object[]|string|string[])} [iteratees=[_.identity]]
+     *  The iteratees to sort by, specified individually or in arrays.
+     * @returns {Array} Returns the new sorted array.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'fred',   'age': 48 },
+     *   { 'user': 'barney', 'age': 36 },
+     *   { 'user': 'fred',   'age': 42 },
+     *   { 'user': 'barney', 'age': 34 }
+     * ];
+     *
+     * _.sortBy(users, function(o) { return o.user; });
+     * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]]
+     *
+     * _.sortBy(users, ['user', 'age']);
+     * // => objects for [['barney', 34], ['barney', 36], ['fred', 42], ['fred', 48]]
+     *
+     * _.sortBy(users, 'user', function(o) {
+     *   return Math.floor(o.age / 10);
+     * });
+     * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]]
+     */
+    var sortBy = rest(function(collection, iteratees) {
+      if (collection == null) {
+        return [];
+      }
+      var length = iteratees.length;
+      if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) {
+        iteratees = [];
+      } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) {
+        iteratees.length = 1;
+      }
+      return baseOrderBy(collection, baseFlatten(iteratees, 1), []);
+    });
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Gets the timestamp of the number of milliseconds that have elapsed since
+     * the Unix epoch (1 January 1970 00:00:00 UTC).
+     *
+     * @static
+     * @memberOf _
+     * @type {Function}
+     * @category Date
+     * @returns {number} Returns the timestamp.
+     * @example
+     *
+     * _.defer(function(stamp) {
+     *   console.log(_.now() - stamp);
+     * }, _.now());
+     * // => logs the number of milliseconds it took for the deferred function to be invoked
+     */
+    var now = Date.now;
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * The opposite of `_.before`; this method creates a function that invokes
+     * `func` once it's called `n` or more times.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {number} n The number of calls before `func` is invoked.
+     * @param {Function} func The function to restrict.
+     * @returns {Function} Returns the new restricted function.
+     * @example
+     *
+     * var saves = ['profile', 'settings'];
+     *
+     * var done = _.after(saves.length, function() {
+     *   console.log('done saving!');
+     * });
+     *
+     * _.forEach(saves, function(type) {
+     *   asyncSave({ 'type': type, 'complete': done });
+     * });
+     * // => logs 'done saving!' after the two async saves have completed
+     */
+    function after(n, func) {
+      if (typeof func != 'function') {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      n = toInteger(n);
+      return function() {
+        if (--n < 1) {
+          return func.apply(this, arguments);
+        }
+      };
+    }
+
+    /**
+     * Creates a function that accepts up to `n` arguments, ignoring any
+     * additional arguments.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to cap arguments for.
+     * @param {number} [n=func.length] The arity cap.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * _.map(['6', '8', '10'], _.ary(parseInt, 1));
+     * // => [6, 8, 10]
+     */
+    function ary(func, n, guard) {
+      n = guard ? undefined : n;
+      n = (func && n == null) ? func.length : n;
+      return createWrapper(func, ARY_FLAG, undefined, undefined, undefined, undefined, n);
+    }
+
+    /**
+     * Creates a function that invokes `func`, with the `this` binding and arguments
+     * of the created function, while it's called less than `n` times. Subsequent
+     * calls to the created function return the result of the last `func` invocation.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {number} n The number of calls at which `func` is no longer invoked.
+     * @param {Function} func The function to restrict.
+     * @returns {Function} Returns the new restricted function.
+     * @example
+     *
+     * jQuery(element).on('click', _.before(5, addContactToList));
+     * // => allows adding up to 4 contacts to the list
+     */
+    function before(n, func) {
+      var result;
+      if (typeof func != 'function') {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      n = toInteger(n);
+      return function() {
+        if (--n > 0) {
+          result = func.apply(this, arguments);
+        }
+        if (n <= 1) {
+          func = undefined;
+        }
+        return result;
+      };
+    }
+
+    /**
+     * Creates a function that invokes `func` with the `this` binding of `thisArg`
+     * and prepends any additional `_.bind` arguments to those provided to the
+     * bound function.
+     *
+     * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds,
+     * may be used as a placeholder for partially applied arguments.
+     *
+     * **Note:** Unlike native `Function#bind` this method doesn't set the "length"
+     * property of bound functions.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to bind.
+     * @param {*} thisArg The `this` binding of `func`.
+     * @param {...*} [partials] The arguments to be partially applied.
+     * @returns {Function} Returns the new bound function.
+     * @example
+     *
+     * var greet = function(greeting, punctuation) {
+     *   return greeting + ' ' + this.user + punctuation;
+     * };
+     *
+     * var object = { 'user': 'fred' };
+     *
+     * var bound = _.bind(greet, object, 'hi');
+     * bound('!');
+     * // => 'hi fred!'
+     *
+     * // Bound with placeholders.
+     * var bound = _.bind(greet, object, _, '!');
+     * bound('hi');
+     * // => 'hi fred!'
+     */
+    var bind = rest(function(func, thisArg, partials) {
+      var bitmask = BIND_FLAG;
+      if (partials.length) {
+        var holders = replaceHolders(partials, getPlaceholder(bind));
+        bitmask |= PARTIAL_FLAG;
+      }
+      return createWrapper(func, bitmask, thisArg, partials, holders);
+    });
+
+    /**
+     * Creates a function that invokes the method at `object[key]` and prepends
+     * any additional `_.bindKey` arguments to those provided to the bound function.
+     *
+     * This method differs from `_.bind` by allowing bound functions to reference
+     * methods that may be redefined or don't yet exist.
+     * See [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern)
+     * for more details.
+     *
+     * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic
+     * builds, may be used as a placeholder for partially applied arguments.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Object} object The object to invoke the method on.
+     * @param {string} key The key of the method.
+     * @param {...*} [partials] The arguments to be partially applied.
+     * @returns {Function} Returns the new bound function.
+     * @example
+     *
+     * var object = {
+     *   'user': 'fred',
+     *   'greet': function(greeting, punctuation) {
+     *     return greeting + ' ' + this.user + punctuation;
+     *   }
+     * };
+     *
+     * var bound = _.bindKey(object, 'greet', 'hi');
+     * bound('!');
+     * // => 'hi fred!'
+     *
+     * object.greet = function(greeting, punctuation) {
+     *   return greeting + 'ya ' + this.user + punctuation;
+     * };
+     *
+     * bound('!');
+     * // => 'hiya fred!'
+     *
+     * // Bound with placeholders.
+     * var bound = _.bindKey(object, 'greet', _, '!');
+     * bound('hi');
+     * // => 'hiya fred!'
+     */
+    var bindKey = rest(function(object, key, partials) {
+      var bitmask = BIND_FLAG | BIND_KEY_FLAG;
+      if (partials.length) {
+        var holders = replaceHolders(partials, getPlaceholder(bindKey));
+        bitmask |= PARTIAL_FLAG;
+      }
+      return createWrapper(key, bitmask, object, partials, holders);
+    });
+
+    /**
+     * Creates a function that accepts arguments of `func` and either invokes
+     * `func` returning its result, if at least `arity` number of arguments have
+     * been provided, or returns a function that accepts the remaining `func`
+     * arguments, and so on. The arity of `func` may be specified if `func.length`
+     * is not sufficient.
+     *
+     * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds,
+     * may be used as a placeholder for provided arguments.
+     *
+     * **Note:** This method doesn't set the "length" property of curried functions.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to curry.
+     * @param {number} [arity=func.length] The arity of `func`.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {Function} Returns the new curried function.
+     * @example
+     *
+     * var abc = function(a, b, c) {
+     *   return [a, b, c];
+     * };
+     *
+     * var curried = _.curry(abc);
+     *
+     * curried(1)(2)(3);
+     * // => [1, 2, 3]
+     *
+     * curried(1, 2)(3);
+     * // => [1, 2, 3]
+     *
+     * curried(1, 2, 3);
+     * // => [1, 2, 3]
+     *
+     * // Curried with placeholders.
+     * curried(1)(_, 3)(2);
+     * // => [1, 2, 3]
+     */
+    function curry(func, arity, guard) {
+      arity = guard ? undefined : arity;
+      var result = createWrapper(func, CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity);
+      result.placeholder = curry.placeholder;
+      return result;
+    }
+
+    /**
+     * This method is like `_.curry` except that arguments are applied to `func`
+     * in the manner of `_.partialRight` instead of `_.partial`.
+     *
+     * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic
+     * builds, may be used as a placeholder for provided arguments.
+     *
+     * **Note:** This method doesn't set the "length" property of curried functions.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to curry.
+     * @param {number} [arity=func.length] The arity of `func`.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {Function} Returns the new curried function.
+     * @example
+     *
+     * var abc = function(a, b, c) {
+     *   return [a, b, c];
+     * };
+     *
+     * var curried = _.curryRight(abc);
+     *
+     * curried(3)(2)(1);
+     * // => [1, 2, 3]
+     *
+     * curried(2, 3)(1);
+     * // => [1, 2, 3]
+     *
+     * curried(1, 2, 3);
+     * // => [1, 2, 3]
+     *
+     * // Curried with placeholders.
+     * curried(3)(1, _)(2);
+     * // => [1, 2, 3]
+     */
+    function curryRight(func, arity, guard) {
+      arity = guard ? undefined : arity;
+      var result = createWrapper(func, CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity);
+      result.placeholder = curryRight.placeholder;
+      return result;
+    }
+
+    /**
+     * Creates a debounced function that delays invoking `func` until after `wait`
+     * milliseconds have elapsed since the last time the debounced function was
+     * invoked. The debounced function comes with a `cancel` method to cancel
+     * delayed `func` invocations and a `flush` method to immediately invoke them.
+     * Provide an options object to indicate whether `func` should be invoked on
+     * the leading and/or trailing edge of the `wait` timeout. The `func` is invoked
+     * with the last arguments provided to the debounced function. Subsequent calls
+     * to the debounced function return the result of the last `func` invocation.
+     *
+     * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked
+     * on the trailing edge of the timeout only if the debounced function is
+     * invoked more than once during the `wait` timeout.
+     *
+     * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation)
+     * for details over the differences between `_.debounce` and `_.throttle`.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to debounce.
+     * @param {number} [wait=0] The number of milliseconds to delay.
+     * @param {Object} [options] The options object.
+     * @param {boolean} [options.leading=false] Specify invoking on the leading
+     *  edge of the timeout.
+     * @param {number} [options.maxWait] The maximum time `func` is allowed to be
+     *  delayed before it's invoked.
+     * @param {boolean} [options.trailing=true] Specify invoking on the trailing
+     *  edge of the timeout.
+     * @returns {Function} Returns the new debounced function.
+     * @example
+     *
+     * // Avoid costly calculations while the window size is in flux.
+     * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
+     *
+     * // Invoke `sendMail` when clicked, debouncing subsequent calls.
+     * jQuery(element).on('click', _.debounce(sendMail, 300, {
+     *   'leading': true,
+     *   'trailing': false
+     * }));
+     *
+     * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
+     * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
+     * var source = new EventSource('/stream');
+     * jQuery(source).on('message', debounced);
+     *
+     * // Cancel the trailing debounced invocation.
+     * jQuery(window).on('popstate', debounced.cancel);
+     */
+    function debounce(func, wait, options) {
+      var args,
+          maxTimeoutId,
+          result,
+          stamp,
+          thisArg,
+          timeoutId,
+          trailingCall,
+          lastCalled = 0,
+          leading = false,
+          maxWait = false,
+          trailing = true;
+
+      if (typeof func != 'function') {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      wait = toNumber(wait) || 0;
+      if (isObject(options)) {
+        leading = !!options.leading;
+        maxWait = 'maxWait' in options && nativeMax(toNumber(options.maxWait) || 0, wait);
+        trailing = 'trailing' in options ? !!options.trailing : trailing;
+      }
+
+      function cancel() {
+        if (timeoutId) {
+          clearTimeout(timeoutId);
+        }
+        if (maxTimeoutId) {
+          clearTimeout(maxTimeoutId);
+        }
+        lastCalled = 0;
+        args = maxTimeoutId = thisArg = timeoutId = trailingCall = undefined;
+      }
+
+      function complete(isCalled, id) {
+        if (id) {
+          clearTimeout(id);
+        }
+        maxTimeoutId = timeoutId = trailingCall = undefined;
+        if (isCalled) {
+          lastCalled = now();
+          result = func.apply(thisArg, args);
+          if (!timeoutId && !maxTimeoutId) {
+            args = thisArg = undefined;
+          }
+        }
+      }
+
+      function delayed() {
+        var remaining = wait - (now() - stamp);
+        if (remaining <= 0 || remaining > wait) {
+          complete(trailingCall, maxTimeoutId);
+        } else {
+          timeoutId = setTimeout(delayed, remaining);
+        }
+      }
+
+      function flush() {
+        if ((timeoutId && trailingCall) || (maxTimeoutId && trailing)) {
+          result = func.apply(thisArg, args);
+        }
+        cancel();
+        return result;
+      }
+
+      function maxDelayed() {
+        complete(trailing, timeoutId);
+      }
+
+      function debounced() {
+        args = arguments;
+        stamp = now();
+        thisArg = this;
+        trailingCall = trailing && (timeoutId || !leading);
+
+        if (maxWait === false) {
+          var leadingCall = leading && !timeoutId;
+        } else {
+          if (!lastCalled && !maxTimeoutId && !leading) {
+            lastCalled = stamp;
+          }
+          var remaining = maxWait - (stamp - lastCalled);
+
+          var isCalled = (remaining <= 0 || remaining > maxWait) &&
+            (leading || maxTimeoutId);
+
+          if (isCalled) {
+            if (maxTimeoutId) {
+              maxTimeoutId = clearTimeout(maxTimeoutId);
+            }
+            lastCalled = stamp;
+            result = func.apply(thisArg, args);
+          }
+          else if (!maxTimeoutId) {
+            maxTimeoutId = setTimeout(maxDelayed, remaining);
+          }
+        }
+        if (isCalled && timeoutId) {
+          timeoutId = clearTimeout(timeoutId);
+        }
+        else if (!timeoutId && wait !== maxWait) {
+          timeoutId = setTimeout(delayed, wait);
+        }
+        if (leadingCall) {
+          isCalled = true;
+          result = func.apply(thisArg, args);
+        }
+        if (isCalled && !timeoutId && !maxTimeoutId) {
+          args = thisArg = undefined;
+        }
+        return result;
+      }
+      debounced.cancel = cancel;
+      debounced.flush = flush;
+      return debounced;
+    }
+
+    /**
+     * Defers invoking the `func` until the current call stack has cleared. Any
+     * additional arguments are provided to `func` when it's invoked.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to defer.
+     * @param {...*} [args] The arguments to invoke `func` with.
+     * @returns {number} Returns the timer id.
+     * @example
+     *
+     * _.defer(function(text) {
+     *   console.log(text);
+     * }, 'deferred');
+     * // => logs 'deferred' after one or more milliseconds
+     */
+    var defer = rest(function(func, args) {
+      return baseDelay(func, 1, args);
+    });
+
+    /**
+     * Invokes `func` after `wait` milliseconds. Any additional arguments are
+     * provided to `func` when it's invoked.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to delay.
+     * @param {number} wait The number of milliseconds to delay invocation.
+     * @param {...*} [args] The arguments to invoke `func` with.
+     * @returns {number} Returns the timer id.
+     * @example
+     *
+     * _.delay(function(text) {
+     *   console.log(text);
+     * }, 1000, 'later');
+     * // => logs 'later' after one second
+     */
+    var delay = rest(function(func, wait, args) {
+      return baseDelay(func, toNumber(wait) || 0, args);
+    });
+
+    /**
+     * Creates a function that invokes `func` with arguments reversed.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to flip arguments for.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var flipped = _.flip(function() {
+     *   return _.toArray(arguments);
+     * });
+     *
+     * flipped('a', 'b', 'c', 'd');
+     * // => ['d', 'c', 'b', 'a']
+     */
+    function flip(func) {
+      return createWrapper(func, FLIP_FLAG);
+    }
+
+    /**
+     * Creates a function that memoizes the result of `func`. If `resolver` is
+     * provided it determines the cache key for storing the result based on the
+     * arguments provided to the memoized function. By default, the first argument
+     * provided to the memoized function is used as the map cache key. The `func`
+     * is invoked with the `this` binding of the memoized function.
+     *
+     * **Note:** The cache is exposed as the `cache` property on the memoized
+     * function. Its creation may be customized by replacing the `_.memoize.Cache`
+     * constructor with one whose instances implement the [`Map`](http://ecma-international.org/ecma-262/6.0/#sec-properties-of-the-map-prototype-object)
+     * method interface of `delete`, `get`, `has`, and `set`.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to have its output memoized.
+     * @param {Function} [resolver] The function to resolve the cache key.
+     * @returns {Function} Returns the new memoizing function.
+     * @example
+     *
+     * var object = { 'a': 1, 'b': 2 };
+     * var other = { 'c': 3, 'd': 4 };
+     *
+     * var values = _.memoize(_.values);
+     * values(object);
+     * // => [1, 2]
+     *
+     * values(other);
+     * // => [3, 4]
+     *
+     * object.a = 2;
+     * values(object);
+     * // => [1, 2]
+     *
+     * // Modify the result cache.
+     * values.cache.set(object, ['a', 'b']);
+     * values(object);
+     * // => ['a', 'b']
+     *
+     * // Replace `_.memoize.Cache`.
+     * _.memoize.Cache = WeakMap;
+     */
+    function memoize(func, resolver) {
+      if (typeof func != 'function' || (resolver && typeof resolver != 'function')) {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      var memoized = function() {
+        var args = arguments,
+            key = resolver ? resolver.apply(this, args) : args[0],
+            cache = memoized.cache;
+
+        if (cache.has(key)) {
+          return cache.get(key);
+        }
+        var result = func.apply(this, args);
+        memoized.cache = cache.set(key, result);
+        return result;
+      };
+      memoized.cache = new memoize.Cache;
+      return memoized;
+    }
+
+    /**
+     * Creates a function that negates the result of the predicate `func`. The
+     * `func` predicate is invoked with the `this` binding and arguments of the
+     * created function.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} predicate The predicate to negate.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * function isEven(n) {
+     *   return n % 2 == 0;
+     * }
+     *
+     * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven));
+     * // => [1, 3, 5]
+     */
+    function negate(predicate) {
+      if (typeof predicate != 'function') {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      return function() {
+        return !predicate.apply(this, arguments);
+      };
+    }
+
+    /**
+     * Creates a function that is restricted to invoking `func` once. Repeat calls
+     * to the function return the value of the first invocation. The `func` is
+     * invoked with the `this` binding and arguments of the created function.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to restrict.
+     * @returns {Function} Returns the new restricted function.
+     * @example
+     *
+     * var initialize = _.once(createApplication);
+     * initialize();
+     * initialize();
+     * // `initialize` invokes `createApplication` once
+     */
+    function once(func) {
+      return before(2, func);
+    }
+
+    /**
+     * Creates a function that invokes `func` with arguments transformed by
+     * corresponding `transforms`.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to wrap.
+     * @param {...(Function|Function[])} [transforms] The functions to transform
+     * arguments, specified individually or in arrays.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * function doubled(n) {
+     *   return n * 2;
+     * }
+     *
+     * function square(n) {
+     *   return n * n;
+     * }
+     *
+     * var func = _.overArgs(function(x, y) {
+     *   return [x, y];
+     * }, square, doubled);
+     *
+     * func(9, 3);
+     * // => [81, 6]
+     *
+     * func(10, 5);
+     * // => [100, 10]
+     */
+    var overArgs = rest(function(func, transforms) {
+      transforms = arrayMap(baseFlatten(transforms, 1), getIteratee());
+
+      var funcsLength = transforms.length;
+      return rest(function(args) {
+        var index = -1,
+            length = nativeMin(args.length, funcsLength);
+
+        while (++index < length) {
+          args[index] = transforms[index].call(this, args[index]);
+        }
+        return apply(func, this, args);
+      });
+    });
+
+    /**
+     * Creates a function that invokes `func` with `partial` arguments prepended
+     * to those provided to the new function. This method is like `_.bind` except
+     * it does **not** alter the `this` binding.
+     *
+     * The `_.partial.placeholder` value, which defaults to `_` in monolithic
+     * builds, may be used as a placeholder for partially applied arguments.
+     *
+     * **Note:** This method doesn't set the "length" property of partially
+     * applied functions.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to partially apply arguments to.
+     * @param {...*} [partials] The arguments to be partially applied.
+     * @returns {Function} Returns the new partially applied function.
+     * @example
+     *
+     * var greet = function(greeting, name) {
+     *   return greeting + ' ' + name;
+     * };
+     *
+     * var sayHelloTo = _.partial(greet, 'hello');
+     * sayHelloTo('fred');
+     * // => 'hello fred'
+     *
+     * // Partially applied with placeholders.
+     * var greetFred = _.partial(greet, _, 'fred');
+     * greetFred('hi');
+     * // => 'hi fred'
+     */
+    var partial = rest(function(func, partials) {
+      var holders = replaceHolders(partials, getPlaceholder(partial));
+      return createWrapper(func, PARTIAL_FLAG, undefined, partials, holders);
+    });
+
+    /**
+     * This method is like `_.partial` except that partially applied arguments
+     * are appended to those provided to the new function.
+     *
+     * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic
+     * builds, may be used as a placeholder for partially applied arguments.
+     *
+     * **Note:** This method doesn't set the "length" property of partially
+     * applied functions.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to partially apply arguments to.
+     * @param {...*} [partials] The arguments to be partially applied.
+     * @returns {Function} Returns the new partially applied function.
+     * @example
+     *
+     * var greet = function(greeting, name) {
+     *   return greeting + ' ' + name;
+     * };
+     *
+     * var greetFred = _.partialRight(greet, 'fred');
+     * greetFred('hi');
+     * // => 'hi fred'
+     *
+     * // Partially applied with placeholders.
+     * var sayHelloTo = _.partialRight(greet, 'hello', _);
+     * sayHelloTo('fred');
+     * // => 'hello fred'
+     */
+    var partialRight = rest(function(func, partials) {
+      var holders = replaceHolders(partials, getPlaceholder(partialRight));
+      return createWrapper(func, PARTIAL_RIGHT_FLAG, undefined, partials, holders);
+    });
+
+    /**
+     * Creates a function that invokes `func` with arguments arranged according
+     * to the specified indexes where the argument value at the first index is
+     * provided as the first argument, the argument value at the second index is
+     * provided as the second argument, and so on.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to rearrange arguments for.
+     * @param {...(number|number[])} indexes The arranged argument indexes,
+     *  specified individually or in arrays.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var rearged = _.rearg(function(a, b, c) {
+     *   return [a, b, c];
+     * }, 2, 0, 1);
+     *
+     * rearged('b', 'c', 'a')
+     * // => ['a', 'b', 'c']
+     */
+    var rearg = rest(function(func, indexes) {
+      return createWrapper(func, REARG_FLAG, undefined, undefined, undefined, baseFlatten(indexes, 1));
+    });
+
+    /**
+     * Creates a function that invokes `func` with the `this` binding of the
+     * created function and arguments from `start` and beyond provided as an array.
+     *
+     * **Note:** This method is based on the [rest parameter](https://mdn.io/rest_parameters).
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to apply a rest parameter to.
+     * @param {number} [start=func.length-1] The start position of the rest parameter.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var say = _.rest(function(what, names) {
+     *   return what + ' ' + _.initial(names).join(', ') +
+     *     (_.size(names) > 1 ? ', & ' : '') + _.last(names);
+     * });
+     *
+     * say('hello', 'fred', 'barney', 'pebbles');
+     * // => 'hello fred, barney, & pebbles'
+     */
+    function rest(func, start) {
+      if (typeof func != 'function') {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      start = nativeMax(start === undefined ? (func.length - 1) : toInteger(start), 0);
+      return function() {
+        var args = arguments,
+            index = -1,
+            length = nativeMax(args.length - start, 0),
+            array = Array(length);
+
+        while (++index < length) {
+          array[index] = args[start + index];
+        }
+        switch (start) {
+          case 0: return func.call(this, array);
+          case 1: return func.call(this, args[0], array);
+          case 2: return func.call(this, args[0], args[1], array);
+        }
+        var otherArgs = Array(start + 1);
+        index = -1;
+        while (++index < start) {
+          otherArgs[index] = args[index];
+        }
+        otherArgs[start] = array;
+        return apply(func, this, otherArgs);
+      };
+    }
+
+    /**
+     * Creates a function that invokes `func` with the `this` binding of the created
+     * function and an array of arguments much like [`Function#apply`](https://es5.github.io/#x15.3.4.3).
+     *
+     * **Note:** This method is based on the [spread operator](https://mdn.io/spread_operator).
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to spread arguments over.
+     * @param {number} [start=0] The start position of the spread.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var say = _.spread(function(who, what) {
+     *   return who + ' says ' + what;
+     * });
+     *
+     * say(['fred', 'hello']);
+     * // => 'fred says hello'
+     *
+     * var numbers = Promise.all([
+     *   Promise.resolve(40),
+     *   Promise.resolve(36)
+     * ]);
+     *
+     * numbers.then(_.spread(function(x, y) {
+     *   return x + y;
+     * }));
+     * // => a Promise of 76
+     */
+    function spread(func, start) {
+      if (typeof func != 'function') {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      start = start === undefined ? 0 : nativeMax(toInteger(start), 0);
+      return rest(function(args) {
+        var array = args[start],
+            otherArgs = args.slice(0, start);
+
+        if (array) {
+          arrayPush(otherArgs, array);
+        }
+        return apply(func, this, otherArgs);
+      });
+    }
+
+    /**
+     * Creates a throttled function that only invokes `func` at most once per
+     * every `wait` milliseconds. The throttled function comes with a `cancel`
+     * method to cancel delayed `func` invocations and a `flush` method to
+     * immediately invoke them. Provide an options object to indicate whether
+     * `func` should be invoked on the leading and/or trailing edge of the `wait`
+     * timeout. The `func` is invoked with the last arguments provided to the
+     * throttled function. Subsequent calls to the throttled function return the
+     * result of the last `func` invocation.
+     *
+     * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked
+     * on the trailing edge of the timeout only if the throttled function is
+     * invoked more than once during the `wait` timeout.
+     *
+     * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation)
+     * for details over the differences between `_.throttle` and `_.debounce`.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to throttle.
+     * @param {number} [wait=0] The number of milliseconds to throttle invocations to.
+     * @param {Object} [options] The options object.
+     * @param {boolean} [options.leading=true] Specify invoking on the leading
+     *  edge of the timeout.
+     * @param {boolean} [options.trailing=true] Specify invoking on the trailing
+     *  edge of the timeout.
+     * @returns {Function} Returns the new throttled function.
+     * @example
+     *
+     * // Avoid excessively updating the position while scrolling.
+     * jQuery(window).on('scroll', _.throttle(updatePosition, 100));
+     *
+     * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
+     * var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
+     * jQuery(element).on('click', throttled);
+     *
+     * // Cancel the trailing throttled invocation.
+     * jQuery(window).on('popstate', throttled.cancel);
+     */
+    function throttle(func, wait, options) {
+      var leading = true,
+          trailing = true;
+
+      if (typeof func != 'function') {
+        throw new TypeError(FUNC_ERROR_TEXT);
+      }
+      if (isObject(options)) {
+        leading = 'leading' in options ? !!options.leading : leading;
+        trailing = 'trailing' in options ? !!options.trailing : trailing;
+      }
+      return debounce(func, wait, {
+        'leading': leading,
+        'maxWait': wait,
+        'trailing': trailing
+      });
+    }
+
+    /**
+     * Creates a function that accepts up to one argument, ignoring any
+     * additional arguments.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {Function} func The function to cap arguments for.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * _.map(['6', '8', '10'], _.unary(parseInt));
+     * // => [6, 8, 10]
+     */
+    function unary(func) {
+      return ary(func, 1);
+    }
+
+    /**
+     * Creates a function that provides `value` to the wrapper function as its
+     * first argument. Any additional arguments provided to the function are
+     * appended to those provided to the wrapper function. The wrapper is invoked
+     * with the `this` binding of the created function.
+     *
+     * @static
+     * @memberOf _
+     * @category Function
+     * @param {*} value The value to wrap.
+     * @param {Function} [wrapper=identity] The wrapper function.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var p = _.wrap(_.escape, function(func, text) {
+     *   return '<p>' + func(text) + '</p>';
+     * });
+     *
+     * p('fred, barney, & pebbles');
+     * // => '<p>fred, barney, &amp; pebbles</p>'
+     */
+    function wrap(value, wrapper) {
+      wrapper = wrapper == null ? identity : wrapper;
+      return partial(wrapper, value);
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Casts `value` as an array if it's not one.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to inspect.
+     * @returns {Array} Returns the cast array.
+     * @example
+     *
+     * _.castArray(1);
+     * // => [1]
+     *
+     * _.castArray({ 'a': 1 });
+     * // => [{ 'a': 1 }]
+     *
+     * _.castArray('abc');
+     * // => ['abc']
+     *
+     * _.castArray(null);
+     * // => [null]
+     *
+     * _.castArray(undefined);
+     * // => [undefined]
+     *
+     * _.castArray();
+     * // => []
+     *
+     * var array = [1, 2, 3];
+     * console.log(_.castArray(array) === array);
+     * // => true
+     */
+    function castArray() {
+      if (!arguments.length) {
+        return [];
+      }
+      var value = arguments[0];
+      return isArray(value) ? value : [value];
+    }
+
+    /**
+     * Creates a shallow clone of `value`.
+     *
+     * **Note:** This method is loosely based on the
+     * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm)
+     * and supports cloning arrays, array buffers, booleans, date objects, maps,
+     * numbers, `Object` objects, regexes, sets, strings, symbols, and typed
+     * arrays. The own enumerable properties of `arguments` objects are cloned
+     * as plain objects. An empty object is returned for uncloneable values such
+     * as error objects, functions, DOM nodes, and WeakMaps.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to clone.
+     * @returns {*} Returns the cloned value.
+     * @example
+     *
+     * var objects = [{ 'a': 1 }, { 'b': 2 }];
+     *
+     * var shallow = _.clone(objects);
+     * console.log(shallow[0] === objects[0]);
+     * // => true
+     */
+    function clone(value) {
+      return baseClone(value, false, true);
+    }
+
+    /**
+     * This method is like `_.clone` except that it accepts `customizer` which
+     * is invoked to produce the cloned value. If `customizer` returns `undefined`
+     * cloning is handled by the method instead. The `customizer` is invoked with
+     * up to four arguments; (value [, index|key, object, stack]).
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to clone.
+     * @param {Function} [customizer] The function to customize cloning.
+     * @returns {*} Returns the cloned value.
+     * @example
+     *
+     * function customizer(value) {
+     *   if (_.isElement(value)) {
+     *     return value.cloneNode(false);
+     *   }
+     * }
+     *
+     * var el = _.cloneWith(document.body, customizer);
+     *
+     * console.log(el === document.body);
+     * // => false
+     * console.log(el.nodeName);
+     * // => 'BODY'
+     * console.log(el.childNodes.length);
+     * // => 0
+     */
+    function cloneWith(value, customizer) {
+      return baseClone(value, false, true, customizer);
+    }
+
+    /**
+     * This method is like `_.clone` except that it recursively clones `value`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to recursively clone.
+     * @returns {*} Returns the deep cloned value.
+     * @example
+     *
+     * var objects = [{ 'a': 1 }, { 'b': 2 }];
+     *
+     * var deep = _.cloneDeep(objects);
+     * console.log(deep[0] === objects[0]);
+     * // => false
+     */
+    function cloneDeep(value) {
+      return baseClone(value, true, true);
+    }
+
+    /**
+     * This method is like `_.cloneWith` except that it recursively clones `value`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to recursively clone.
+     * @param {Function} [customizer] The function to customize cloning.
+     * @returns {*} Returns the deep cloned value.
+     * @example
+     *
+     * function customizer(value) {
+     *   if (_.isElement(value)) {
+     *     return value.cloneNode(true);
+     *   }
+     * }
+     *
+     * var el = _.cloneDeepWith(document.body, customizer);
+     *
+     * console.log(el === document.body);
+     * // => false
+     * console.log(el.nodeName);
+     * // => 'BODY'
+     * console.log(el.childNodes.length);
+     * // => 20
+     */
+    function cloneDeepWith(value, customizer) {
+      return baseClone(value, true, true, customizer);
+    }
+
+    /**
+     * Performs a [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
+     * comparison between two values to determine if they are equivalent.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to compare.
+     * @param {*} other The other value to compare.
+     * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
+     * @example
+     *
+     * var object = { 'user': 'fred' };
+     * var other = { 'user': 'fred' };
+     *
+     * _.eq(object, object);
+     * // => true
+     *
+     * _.eq(object, other);
+     * // => false
+     *
+     * _.eq('a', 'a');
+     * // => true
+     *
+     * _.eq('a', Object('a'));
+     * // => false
+     *
+     * _.eq(NaN, NaN);
+     * // => true
+     */
+    function eq(value, other) {
+      return value === other || (value !== value && other !== other);
+    }
+
+    /**
+     * Checks if `value` is greater than `other`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to compare.
+     * @param {*} other The other value to compare.
+     * @returns {boolean} Returns `true` if `value` is greater than `other`, else `false`.
+     * @example
+     *
+     * _.gt(3, 1);
+     * // => true
+     *
+     * _.gt(3, 3);
+     * // => false
+     *
+     * _.gt(1, 3);
+     * // => false
+     */
+    function gt(value, other) {
+      return value > other;
+    }
+
+    /**
+     * Checks if `value` is greater than or equal to `other`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to compare.
+     * @param {*} other The other value to compare.
+     * @returns {boolean} Returns `true` if `value` is greater than or equal to `other`, else `false`.
+     * @example
+     *
+     * _.gte(3, 1);
+     * // => true
+     *
+     * _.gte(3, 3);
+     * // => true
+     *
+     * _.gte(1, 3);
+     * // => false
+     */
+    function gte(value, other) {
+      return value >= other;
+    }
+
+    /**
+     * Checks if `value` is likely an `arguments` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isArguments(function() { return arguments; }());
+     * // => true
+     *
+     * _.isArguments([1, 2, 3]);
+     * // => false
+     */
+    function isArguments(value) {
+      // Safari 8.1 incorrectly makes `arguments.callee` enumerable in strict mode.
+      return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') &&
+        (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag);
+    }
+
+    /**
+     * Checks if `value` is classified as an `Array` object.
+     *
+     * @static
+     * @memberOf _
+     * @type {Function}
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isArray([1, 2, 3]);
+     * // => true
+     *
+     * _.isArray(document.body.children);
+     * // => false
+     *
+     * _.isArray('abc');
+     * // => false
+     *
+     * _.isArray(_.noop);
+     * // => false
+     */
+    var isArray = Array.isArray;
+
+    /**
+     * Checks if `value` is classified as an `ArrayBuffer` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isArrayBuffer(new ArrayBuffer(2));
+     * // => true
+     *
+     * _.isArrayBuffer(new Array(2));
+     * // => false
+     */
+    function isArrayBuffer(value) {
+      return isObjectLike(value) && objectToString.call(value) == arrayBufferTag;
+    }
+
+    /**
+     * Checks if `value` is array-like. A value is considered array-like if it's
+     * not a function and has a `value.length` that's an integer greater than or
+     * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
+     * @example
+     *
+     * _.isArrayLike([1, 2, 3]);
+     * // => true
+     *
+     * _.isArrayLike(document.body.children);
+     * // => true
+     *
+     * _.isArrayLike('abc');
+     * // => true
+     *
+     * _.isArrayLike(_.noop);
+     * // => false
+     */
+    function isArrayLike(value) {
+      return value != null && isLength(getLength(value)) && !isFunction(value);
+    }
+
+    /**
+     * This method is like `_.isArrayLike` except that it also checks if `value`
+     * is an object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is an array-like object, else `false`.
+     * @example
+     *
+     * _.isArrayLikeObject([1, 2, 3]);
+     * // => true
+     *
+     * _.isArrayLikeObject(document.body.children);
+     * // => true
+     *
+     * _.isArrayLikeObject('abc');
+     * // => false
+     *
+     * _.isArrayLikeObject(_.noop);
+     * // => false
+     */
+    function isArrayLikeObject(value) {
+      return isObjectLike(value) && isArrayLike(value);
+    }
+
+    /**
+     * Checks if `value` is classified as a boolean primitive or object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isBoolean(false);
+     * // => true
+     *
+     * _.isBoolean(null);
+     * // => false
+     */
+    function isBoolean(value) {
+      return value === true || value === false ||
+        (isObjectLike(value) && objectToString.call(value) == boolTag);
+    }
+
+    /**
+     * Checks if `value` is a buffer.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
+     * @example
+     *
+     * _.isBuffer(new Buffer(2));
+     * // => true
+     *
+     * _.isBuffer(new Uint8Array(2));
+     * // => false
+     */
+    var isBuffer = !Buffer ? constant(false) : function(value) {
+      return value instanceof Buffer;
+    };
+
+    /**
+     * Checks if `value` is classified as a `Date` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isDate(new Date);
+     * // => true
+     *
+     * _.isDate('Mon April 23 2012');
+     * // => false
+     */
+    function isDate(value) {
+      return isObjectLike(value) && objectToString.call(value) == dateTag;
+    }
+
+    /**
+     * Checks if `value` is likely a DOM element.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`.
+     * @example
+     *
+     * _.isElement(document.body);
+     * // => true
+     *
+     * _.isElement('<body>');
+     * // => false
+     */
+    function isElement(value) {
+      return !!value && value.nodeType === 1 && isObjectLike(value) && !isPlainObject(value);
+    }
+
+    /**
+     * Checks if `value` is an empty collection or object. A value is considered
+     * empty if it's an `arguments` object, array, string, or jQuery-like collection
+     * with a length of `0` or has no own enumerable properties.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is empty, else `false`.
+     * @example
+     *
+     * _.isEmpty(null);
+     * // => true
+     *
+     * _.isEmpty(true);
+     * // => true
+     *
+     * _.isEmpty(1);
+     * // => true
+     *
+     * _.isEmpty([1, 2, 3]);
+     * // => false
+     *
+     * _.isEmpty({ 'a': 1 });
+     * // => false
+     */
+    function isEmpty(value) {
+      if (isArrayLike(value) &&
+          (isArray(value) || isString(value) ||
+            isFunction(value.splice) || isArguments(value))) {
+        return !value.length;
+      }
+      for (var key in value) {
+        if (hasOwnProperty.call(value, key)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    /**
+     * Performs a deep comparison between two values to determine if they are
+     * equivalent.
+     *
+     * **Note:** This method supports comparing arrays, array buffers, booleans,
+     * date objects, error objects, maps, numbers, `Object` objects, regexes,
+     * sets, strings, symbols, and typed arrays. `Object` objects are compared
+     * by their own, not inherited, enumerable properties. Functions and DOM
+     * nodes are **not** supported.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to compare.
+     * @param {*} other The other value to compare.
+     * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
+     * @example
+     *
+     * var object = { 'user': 'fred' };
+     * var other = { 'user': 'fred' };
+     *
+     * _.isEqual(object, other);
+     * // => true
+     *
+     * object === other;
+     * // => false
+     */
+    function isEqual(value, other) {
+      return baseIsEqual(value, other);
+    }
+
+    /**
+     * This method is like `_.isEqual` except that it accepts `customizer` which
+     * is invoked to compare values. If `customizer` returns `undefined` comparisons
+     * are handled by the method instead. The `customizer` is invoked with up to
+     * six arguments: (objValue, othValue [, index|key, object, other, stack]).
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to compare.
+     * @param {*} other The other value to compare.
+     * @param {Function} [customizer] The function to customize comparisons.
+     * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
+     * @example
+     *
+     * function isGreeting(value) {
+     *   return /^h(?:i|ello)$/.test(value);
+     * }
+     *
+     * function customizer(objValue, othValue) {
+     *   if (isGreeting(objValue) && isGreeting(othValue)) {
+     *     return true;
+     *   }
+     * }
+     *
+     * var array = ['hello', 'goodbye'];
+     * var other = ['hi', 'goodbye'];
+     *
+     * _.isEqualWith(array, other, customizer);
+     * // => true
+     */
+    function isEqualWith(value, other, customizer) {
+      customizer = typeof customizer == 'function' ? customizer : undefined;
+      var result = customizer ? customizer(value, other) : undefined;
+      return result === undefined ? baseIsEqual(value, other, customizer) : !!result;
+    }
+
+    /**
+     * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`,
+     * `SyntaxError`, `TypeError`, or `URIError` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is an error object, else `false`.
+     * @example
+     *
+     * _.isError(new Error);
+     * // => true
+     *
+     * _.isError(Error);
+     * // => false
+     */
+    function isError(value) {
+      if (!isObjectLike(value)) {
+        return false;
+      }
+      return (objectToString.call(value) == errorTag) ||
+        (typeof value.message == 'string' && typeof value.name == 'string');
+    }
+
+    /**
+     * Checks if `value` is a finite primitive number.
+     *
+     * **Note:** This method is based on [`Number.isFinite`](https://mdn.io/Number/isFinite).
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is a finite number, else `false`.
+     * @example
+     *
+     * _.isFinite(3);
+     * // => true
+     *
+     * _.isFinite(Number.MAX_VALUE);
+     * // => true
+     *
+     * _.isFinite(3.14);
+     * // => true
+     *
+     * _.isFinite(Infinity);
+     * // => false
+     */
+    function isFinite(value) {
+      return typeof value == 'number' && nativeIsFinite(value);
+    }
+
+    /**
+     * Checks if `value` is classified as a `Function` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isFunction(_);
+     * // => true
+     *
+     * _.isFunction(/abc/);
+     * // => false
+     */
+    function isFunction(value) {
+      // The use of `Object#toString` avoids issues with the `typeof` operator
+      // in Safari 8 which returns 'object' for typed array and weak map constructors,
+      // and PhantomJS 1.9 which returns 'function' for `NodeList` instances.
+      var tag = isObject(value) ? objectToString.call(value) : '';
+      return tag == funcTag || tag == genTag;
+    }
+
+    /**
+     * Checks if `value` is an integer.
+     *
+     * **Note:** This method is based on [`Number.isInteger`](https://mdn.io/Number/isInteger).
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is an integer, else `false`.
+     * @example
+     *
+     * _.isInteger(3);
+     * // => true
+     *
+     * _.isInteger(Number.MIN_VALUE);
+     * // => false
+     *
+     * _.isInteger(Infinity);
+     * // => false
+     *
+     * _.isInteger('3');
+     * // => false
+     */
+    function isInteger(value) {
+      return typeof value == 'number' && value == toInteger(value);
+    }
+
+    /**
+     * Checks if `value` is a valid array-like length.
+     *
+     * **Note:** This function is loosely based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
+     * @example
+     *
+     * _.isLength(3);
+     * // => true
+     *
+     * _.isLength(Number.MIN_VALUE);
+     * // => false
+     *
+     * _.isLength(Infinity);
+     * // => false
+     *
+     * _.isLength('3');
+     * // => false
+     */
+    function isLength(value) {
+      return typeof value == 'number' &&
+        value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
+    }
+
+    /**
+     * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`.
+     * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is an object, else `false`.
+     * @example
+     *
+     * _.isObject({});
+     * // => true
+     *
+     * _.isObject([1, 2, 3]);
+     * // => true
+     *
+     * _.isObject(_.noop);
+     * // => true
+     *
+     * _.isObject(null);
+     * // => false
+     */
+    function isObject(value) {
+      var type = typeof value;
+      return !!value && (type == 'object' || type == 'function');
+    }
+
+    /**
+     * Checks if `value` is object-like. A value is object-like if it's not `null`
+     * and has a `typeof` result of "object".
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+     * @example
+     *
+     * _.isObjectLike({});
+     * // => true
+     *
+     * _.isObjectLike([1, 2, 3]);
+     * // => true
+     *
+     * _.isObjectLike(_.noop);
+     * // => false
+     *
+     * _.isObjectLike(null);
+     * // => false
+     */
+    function isObjectLike(value) {
+      return !!value && typeof value == 'object';
+    }
+
+    /**
+     * Checks if `value` is classified as a `Map` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isMap(new Map);
+     * // => true
+     *
+     * _.isMap(new WeakMap);
+     * // => false
+     */
+    function isMap(value) {
+      return isObjectLike(value) && getTag(value) == mapTag;
+    }
+
+    /**
+     * Performs a partial deep comparison between `object` and `source` to
+     * determine if `object` contains equivalent property values. This method is
+     * equivalent to a `_.matches` function when `source` is partially applied.
+     *
+     * **Note:** This method supports comparing the same values as `_.isEqual`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {Object} object The object to inspect.
+     * @param {Object} source The object of property values to match.
+     * @returns {boolean} Returns `true` if `object` is a match, else `false`.
+     * @example
+     *
+     * var object = { 'user': 'fred', 'age': 40 };
+     *
+     * _.isMatch(object, { 'age': 40 });
+     * // => true
+     *
+     * _.isMatch(object, { 'age': 36 });
+     * // => false
+     */
+    function isMatch(object, source) {
+      return object === source || baseIsMatch(object, source, getMatchData(source));
+    }
+
+    /**
+     * This method is like `_.isMatch` except that it accepts `customizer` which
+     * is invoked to compare values. If `customizer` returns `undefined` comparisons
+     * are handled by the method instead. The `customizer` is invoked with five
+     * arguments: (objValue, srcValue, index|key, object, source).
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {Object} object The object to inspect.
+     * @param {Object} source The object of property values to match.
+     * @param {Function} [customizer] The function to customize comparisons.
+     * @returns {boolean} Returns `true` if `object` is a match, else `false`.
+     * @example
+     *
+     * function isGreeting(value) {
+     *   return /^h(?:i|ello)$/.test(value);
+     * }
+     *
+     * function customizer(objValue, srcValue) {
+     *   if (isGreeting(objValue) && isGreeting(srcValue)) {
+     *     return true;
+     *   }
+     * }
+     *
+     * var object = { 'greeting': 'hello' };
+     * var source = { 'greeting': 'hi' };
+     *
+     * _.isMatchWith(object, source, customizer);
+     * // => true
+     */
+    function isMatchWith(object, source, customizer) {
+      customizer = typeof customizer == 'function' ? customizer : undefined;
+      return baseIsMatch(object, source, getMatchData(source), customizer);
+    }
+
+    /**
+     * Checks if `value` is `NaN`.
+     *
+     * **Note:** This method is not the same as [`isNaN`](https://es5.github.io/#x15.1.2.4)
+     * which returns `true` for `undefined` and other non-numeric values.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.
+     * @example
+     *
+     * _.isNaN(NaN);
+     * // => true
+     *
+     * _.isNaN(new Number(NaN));
+     * // => true
+     *
+     * isNaN(undefined);
+     * // => true
+     *
+     * _.isNaN(undefined);
+     * // => false
+     */
+    function isNaN(value) {
+      // An `NaN` primitive is the only value that is not equal to itself.
+      // Perform the `toStringTag` check first to avoid errors with some ActiveX objects in IE.
+      return isNumber(value) && value != +value;
+    }
+
+    /**
+     * Checks if `value` is a native function.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is a native function, else `false`.
+     * @example
+     *
+     * _.isNative(Array.prototype.push);
+     * // => true
+     *
+     * _.isNative(_);
+     * // => false
+     */
+    function isNative(value) {
+      if (value == null) {
+        return false;
+      }
+      if (isFunction(value)) {
+        return reIsNative.test(funcToString.call(value));
+      }
+      return isObjectLike(value) &&
+        (isHostObject(value) ? reIsNative : reIsHostCtor).test(value);
+    }
+
+    /**
+     * Checks if `value` is `null`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is `null`, else `false`.
+     * @example
+     *
+     * _.isNull(null);
+     * // => true
+     *
+     * _.isNull(void 0);
+     * // => false
+     */
+    function isNull(value) {
+      return value === null;
+    }
+
+    /**
+     * Checks if `value` is `null` or `undefined`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is nullish, else `false`.
+     * @example
+     *
+     * _.isNil(null);
+     * // => true
+     *
+     * _.isNil(void 0);
+     * // => true
+     *
+     * _.isNil(NaN);
+     * // => false
+     */
+    function isNil(value) {
+      return value == null;
+    }
+
+    /**
+     * Checks if `value` is classified as a `Number` primitive or object.
+     *
+     * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are classified
+     * as numbers, use the `_.isFinite` method.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isNumber(3);
+     * // => true
+     *
+     * _.isNumber(Number.MIN_VALUE);
+     * // => true
+     *
+     * _.isNumber(Infinity);
+     * // => true
+     *
+     * _.isNumber('3');
+     * // => false
+     */
+    function isNumber(value) {
+      return typeof value == 'number' ||
+        (isObjectLike(value) && objectToString.call(value) == numberTag);
+    }
+
+    /**
+     * Checks if `value` is a plain object, that is, an object created by the
+     * `Object` constructor or one with a `[[Prototype]]` of `null`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     * }
+     *
+     * _.isPlainObject(new Foo);
+     * // => false
+     *
+     * _.isPlainObject([1, 2, 3]);
+     * // => false
+     *
+     * _.isPlainObject({ 'x': 0, 'y': 0 });
+     * // => true
+     *
+     * _.isPlainObject(Object.create(null));
+     * // => true
+     */
+    function isPlainObject(value) {
+      if (!isObjectLike(value) ||
+          objectToString.call(value) != objectTag || isHostObject(value)) {
+        return false;
+      }
+      var proto = getPrototypeOf(value);
+      if (proto === null) {
+        return true;
+      }
+      var Ctor = proto.constructor;
+      return (typeof Ctor == 'function' &&
+        Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
+    }
+
+    /**
+     * Checks if `value` is classified as a `RegExp` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isRegExp(/abc/);
+     * // => true
+     *
+     * _.isRegExp('/abc/');
+     * // => false
+     */
+    function isRegExp(value) {
+      return isObject(value) && objectToString.call(value) == regexpTag;
+    }
+
+    /**
+     * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754
+     * double precision number which isn't the result of a rounded unsafe integer.
+     *
+     * **Note:** This method is based on [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger).
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`.
+     * @example
+     *
+     * _.isSafeInteger(3);
+     * // => true
+     *
+     * _.isSafeInteger(Number.MIN_VALUE);
+     * // => false
+     *
+     * _.isSafeInteger(Infinity);
+     * // => false
+     *
+     * _.isSafeInteger('3');
+     * // => false
+     */
+    function isSafeInteger(value) {
+      return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER;
+    }
+
+    /**
+     * Checks if `value` is classified as a `Set` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isSet(new Set);
+     * // => true
+     *
+     * _.isSet(new WeakSet);
+     * // => false
+     */
+    function isSet(value) {
+      return isObjectLike(value) && getTag(value) == setTag;
+    }
+
+    /**
+     * Checks if `value` is classified as a `String` primitive or object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isString('abc');
+     * // => true
+     *
+     * _.isString(1);
+     * // => false
+     */
+    function isString(value) {
+      return typeof value == 'string' ||
+        (!isArray(value) && isObjectLike(value) && objectToString.call(value) == stringTag);
+    }
+
+    /**
+     * Checks if `value` is classified as a `Symbol` primitive or object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isSymbol(Symbol.iterator);
+     * // => true
+     *
+     * _.isSymbol('abc');
+     * // => false
+     */
+    function isSymbol(value) {
+      return typeof value == 'symbol' ||
+        (isObjectLike(value) && objectToString.call(value) == symbolTag);
+    }
+
+    /**
+     * Checks if `value` is classified as a typed array.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isTypedArray(new Uint8Array);
+     * // => true
+     *
+     * _.isTypedArray([]);
+     * // => false
+     */
+    function isTypedArray(value) {
+      return isObjectLike(value) &&
+        isLength(value.length) && !!typedArrayTags[objectToString.call(value)];
+    }
+
+    /**
+     * Checks if `value` is `undefined`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`.
+     * @example
+     *
+     * _.isUndefined(void 0);
+     * // => true
+     *
+     * _.isUndefined(null);
+     * // => false
+     */
+    function isUndefined(value) {
+      return value === undefined;
+    }
+
+    /**
+     * Checks if `value` is classified as a `WeakMap` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isWeakMap(new WeakMap);
+     * // => true
+     *
+     * _.isWeakMap(new Map);
+     * // => false
+     */
+    function isWeakMap(value) {
+      return isObjectLike(value) && getTag(value) == weakMapTag;
+    }
+
+    /**
+     * Checks if `value` is classified as a `WeakSet` object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to check.
+     * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+     * @example
+     *
+     * _.isWeakSet(new WeakSet);
+     * // => true
+     *
+     * _.isWeakSet(new Set);
+     * // => false
+     */
+    function isWeakSet(value) {
+      return isObjectLike(value) && objectToString.call(value) == weakSetTag;
+    }
+
+    /**
+     * Checks if `value` is less than `other`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to compare.
+     * @param {*} other The other value to compare.
+     * @returns {boolean} Returns `true` if `value` is less than `other`, else `false`.
+     * @example
+     *
+     * _.lt(1, 3);
+     * // => true
+     *
+     * _.lt(3, 3);
+     * // => false
+     *
+     * _.lt(3, 1);
+     * // => false
+     */
+    function lt(value, other) {
+      return value < other;
+    }
+
+    /**
+     * Checks if `value` is less than or equal to `other`.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to compare.
+     * @param {*} other The other value to compare.
+     * @returns {boolean} Returns `true` if `value` is less than or equal to `other`, else `false`.
+     * @example
+     *
+     * _.lte(1, 3);
+     * // => true
+     *
+     * _.lte(3, 3);
+     * // => true
+     *
+     * _.lte(3, 1);
+     * // => false
+     */
+    function lte(value, other) {
+      return value <= other;
+    }
+
+    /**
+     * Converts `value` to an array.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to convert.
+     * @returns {Array} Returns the converted array.
+     * @example
+     *
+     * _.toArray({ 'a': 1, 'b': 2 });
+     * // => [1, 2]
+     *
+     * _.toArray('abc');
+     * // => ['a', 'b', 'c']
+     *
+     * _.toArray(1);
+     * // => []
+     *
+     * _.toArray(null);
+     * // => []
+     */
+    function toArray(value) {
+      if (!value) {
+        return [];
+      }
+      if (isArrayLike(value)) {
+        return isString(value) ? stringToArray(value) : copyArray(value);
+      }
+      if (iteratorSymbol && value[iteratorSymbol]) {
+        return iteratorToArray(value[iteratorSymbol]());
+      }
+      var tag = getTag(value),
+          func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values);
+
+      return func(value);
+    }
+
+    /**
+     * Converts `value` to an integer.
+     *
+     * **Note:** This function is loosely based on [`ToInteger`](http://www.ecma-international.org/ecma-262/6.0/#sec-tointeger).
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to convert.
+     * @returns {number} Returns the converted integer.
+     * @example
+     *
+     * _.toInteger(3);
+     * // => 3
+     *
+     * _.toInteger(Number.MIN_VALUE);
+     * // => 0
+     *
+     * _.toInteger(Infinity);
+     * // => 1.7976931348623157e+308
+     *
+     * _.toInteger('3');
+     * // => 3
+     */
+    function toInteger(value) {
+      if (!value) {
+        return value === 0 ? value : 0;
+      }
+      value = toNumber(value);
+      if (value === INFINITY || value === -INFINITY) {
+        var sign = (value < 0 ? -1 : 1);
+        return sign * MAX_INTEGER;
+      }
+      var remainder = value % 1;
+      return value === value ? (remainder ? value - remainder : value) : 0;
+    }
+
+    /**
+     * Converts `value` to an integer suitable for use as the length of an
+     * array-like object.
+     *
+     * **Note:** This method is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to convert.
+     * @returns {number} Returns the converted integer.
+     * @example
+     *
+     * _.toLength(3);
+     * // => 3
+     *
+     * _.toLength(Number.MIN_VALUE);
+     * // => 0
+     *
+     * _.toLength(Infinity);
+     * // => 4294967295
+     *
+     * _.toLength('3');
+     * // => 3
+     */
+    function toLength(value) {
+      return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0;
+    }
+
+    /**
+     * Converts `value` to a number.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to process.
+     * @returns {number} Returns the number.
+     * @example
+     *
+     * _.toNumber(3);
+     * // => 3
+     *
+     * _.toNumber(Number.MIN_VALUE);
+     * // => 5e-324
+     *
+     * _.toNumber(Infinity);
+     * // => Infinity
+     *
+     * _.toNumber('3');
+     * // => 3
+     */
+    function toNumber(value) {
+      if (isObject(value)) {
+        var other = isFunction(value.valueOf) ? value.valueOf() : value;
+        value = isObject(other) ? (other + '') : other;
+      }
+      if (typeof value != 'string') {
+        return value === 0 ? value : +value;
+      }
+      value = value.replace(reTrim, '');
+      var isBinary = reIsBinary.test(value);
+      return (isBinary || reIsOctal.test(value))
+        ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
+        : (reIsBadHex.test(value) ? NAN : +value);
+    }
+
+    /**
+     * Converts `value` to a plain object flattening inherited enumerable
+     * properties of `value` to own properties of the plain object.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to convert.
+     * @returns {Object} Returns the converted plain object.
+     * @example
+     *
+     * function Foo() {
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.assign({ 'a': 1 }, new Foo);
+     * // => { 'a': 1, 'b': 2 }
+     *
+     * _.assign({ 'a': 1 }, _.toPlainObject(new Foo));
+     * // => { 'a': 1, 'b': 2, 'c': 3 }
+     */
+    function toPlainObject(value) {
+      return copyObject(value, keysIn(value));
+    }
+
+    /**
+     * Converts `value` to a safe integer. A safe integer can be compared and
+     * represented correctly.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to convert.
+     * @returns {number} Returns the converted integer.
+     * @example
+     *
+     * _.toSafeInteger(3);
+     * // => 3
+     *
+     * _.toSafeInteger(Number.MIN_VALUE);
+     * // => 0
+     *
+     * _.toSafeInteger(Infinity);
+     * // => 9007199254740991
+     *
+     * _.toSafeInteger('3');
+     * // => 3
+     */
+    function toSafeInteger(value) {
+      return baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
+    }
+
+    /**
+     * Converts `value` to a string if it's not one. An empty string is returned
+     * for `null` and `undefined` values. The sign of `-0` is preserved.
+     *
+     * @static
+     * @memberOf _
+     * @category Lang
+     * @param {*} value The value to process.
+     * @returns {string} Returns the string.
+     * @example
+     *
+     * _.toString(null);
+     * // => ''
+     *
+     * _.toString(-0);
+     * // => '-0'
+     *
+     * _.toString([1, 2, 3]);
+     * // => '1,2,3'
+     */
+    function toString(value) {
+      // Exit early for strings to avoid a performance hit in some environments.
+      if (typeof value == 'string') {
+        return value;
+      }
+      if (value == null) {
+        return '';
+      }
+      if (isSymbol(value)) {
+        return symbolToString ? symbolToString.call(value) : '';
+      }
+      var result = (value + '');
+      return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Assigns own enumerable properties of source objects to the destination
+     * object. Source objects are applied from left to right. Subsequent sources
+     * overwrite property assignments of previous sources.
+     *
+     * **Note:** This method mutates `object` and is loosely based on
+     * [`Object.assign`](https://mdn.io/Object/assign).
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The destination object.
+     * @param {...Object} [sources] The source objects.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * function Foo() {
+     *   this.c = 3;
+     * }
+     *
+     * function Bar() {
+     *   this.e = 5;
+     * }
+     *
+     * Foo.prototype.d = 4;
+     * Bar.prototype.f = 6;
+     *
+     * _.assign({ 'a': 1 }, new Foo, new Bar);
+     * // => { 'a': 1, 'c': 3, 'e': 5 }
+     */
+    var assign = createAssigner(function(object, source) {
+      if (nonEnumShadows || isPrototype(source) || isArrayLike(source)) {
+        copyObject(source, keys(source), object);
+        return;
+      }
+      for (var key in source) {
+        if (hasOwnProperty.call(source, key)) {
+          assignValue(object, key, source[key]);
+        }
+      }
+    });
+
+    /**
+     * This method is like `_.assign` except that it iterates over own and
+     * inherited source properties.
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @alias extend
+     * @category Object
+     * @param {Object} object The destination object.
+     * @param {...Object} [sources] The source objects.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * function Foo() {
+     *   this.b = 2;
+     * }
+     *
+     * function Bar() {
+     *   this.d = 4;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     * Bar.prototype.e = 5;
+     *
+     * _.assignIn({ 'a': 1 }, new Foo, new Bar);
+     * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5 }
+     */
+    var assignIn = createAssigner(function(object, source) {
+      if (nonEnumShadows || isPrototype(source) || isArrayLike(source)) {
+        copyObject(source, keysIn(source), object);
+        return;
+      }
+      for (var key in source) {
+        assignValue(object, key, source[key]);
+      }
+    });
+
+    /**
+     * This method is like `_.assignIn` except that it accepts `customizer` which
+     * is invoked to produce the assigned values. If `customizer` returns `undefined`
+     * assignment is handled by the method instead. The `customizer` is invoked
+     * with five arguments: (objValue, srcValue, key, object, source).
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @alias extendWith
+     * @category Object
+     * @param {Object} object The destination object.
+     * @param {...Object} sources The source objects.
+     * @param {Function} [customizer] The function to customize assigned values.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * function customizer(objValue, srcValue) {
+     *   return _.isUndefined(objValue) ? srcValue : objValue;
+     * }
+     *
+     * var defaults = _.partialRight(_.assignInWith, customizer);
+     *
+     * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });
+     * // => { 'a': 1, 'b': 2 }
+     */
+    var assignInWith = createAssigner(function(object, source, srcIndex, customizer) {
+      copyObjectWith(source, keysIn(source), object, customizer);
+    });
+
+    /**
+     * This method is like `_.assign` except that it accepts `customizer` which
+     * is invoked to produce the assigned values. If `customizer` returns `undefined`
+     * assignment is handled by the method instead. The `customizer` is invoked
+     * with five arguments: (objValue, srcValue, key, object, source).
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The destination object.
+     * @param {...Object} sources The source objects.
+     * @param {Function} [customizer] The function to customize assigned values.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * function customizer(objValue, srcValue) {
+     *   return _.isUndefined(objValue) ? srcValue : objValue;
+     * }
+     *
+     * var defaults = _.partialRight(_.assignWith, customizer);
+     *
+     * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });
+     * // => { 'a': 1, 'b': 2 }
+     */
+    var assignWith = createAssigner(function(object, source, srcIndex, customizer) {
+      copyObjectWith(source, keys(source), object, customizer);
+    });
+
+    /**
+     * Creates an array of values corresponding to `paths` of `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to iterate over.
+     * @param {...(string|string[])} [paths] The property paths of elements to pick,
+     *  specified individually or in arrays.
+     * @returns {Array} Returns the new array of picked elements.
+     * @example
+     *
+     * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };
+     *
+     * _.at(object, ['a[0].b.c', 'a[1]']);
+     * // => [3, 4]
+     *
+     * _.at(['a', 'b', 'c'], 0, 2);
+     * // => ['a', 'c']
+     */
+    var at = rest(function(object, paths) {
+      return baseAt(object, baseFlatten(paths, 1));
+    });
+
+    /**
+     * Creates an object that inherits from the `prototype` object. If a `properties`
+     * object is given its own enumerable properties are assigned to the created object.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} prototype The object to inherit from.
+     * @param {Object} [properties] The properties to assign to the object.
+     * @returns {Object} Returns the new object.
+     * @example
+     *
+     * function Shape() {
+     *   this.x = 0;
+     *   this.y = 0;
+     * }
+     *
+     * function Circle() {
+     *   Shape.call(this);
+     * }
+     *
+     * Circle.prototype = _.create(Shape.prototype, {
+     *   'constructor': Circle
+     * });
+     *
+     * var circle = new Circle;
+     * circle instanceof Circle;
+     * // => true
+     *
+     * circle instanceof Shape;
+     * // => true
+     */
+    function create(prototype, properties) {
+      var result = baseCreate(prototype);
+      return properties ? baseAssign(result, properties) : result;
+    }
+
+    /**
+     * Assigns own and inherited enumerable properties of source objects to the
+     * destination object for all destination properties that resolve to `undefined`.
+     * Source objects are applied from left to right. Once a property is set,
+     * additional values of the same property are ignored.
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The destination object.
+     * @param {...Object} [sources] The source objects.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * _.defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' });
+     * // => { 'user': 'barney', 'age': 36 }
+     */
+    var defaults = rest(function(args) {
+      args.push(undefined, assignInDefaults);
+      return apply(assignInWith, undefined, args);
+    });
+
+    /**
+     * This method is like `_.defaults` except that it recursively assigns
+     * default properties.
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The destination object.
+     * @param {...Object} [sources] The source objects.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * _.defaultsDeep({ 'user': { 'name': 'barney' } }, { 'user': { 'name': 'fred', 'age': 36 } });
+     * // => { 'user': { 'name': 'barney', 'age': 36 } }
+     *
+     */
+    var defaultsDeep = rest(function(args) {
+      args.push(undefined, mergeDefaults);
+      return apply(mergeWith, undefined, args);
+    });
+
+    /**
+     * This method is like `_.find` except that it returns the key of the first
+     * element `predicate` returns truthy for instead of the element itself.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to search.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {string|undefined} Returns the key of the matched element, else `undefined`.
+     * @example
+     *
+     * var users = {
+     *   'barney':  { 'age': 36, 'active': true },
+     *   'fred':    { 'age': 40, 'active': false },
+     *   'pebbles': { 'age': 1,  'active': true }
+     * };
+     *
+     * _.findKey(users, function(o) { return o.age < 40; });
+     * // => 'barney' (iteration order is not guaranteed)
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.findKey(users, { 'age': 1, 'active': true });
+     * // => 'pebbles'
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.findKey(users, ['active', false]);
+     * // => 'fred'
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.findKey(users, 'active');
+     * // => 'barney'
+     */
+    function findKey(object, predicate) {
+      return baseFind(object, getIteratee(predicate, 3), baseForOwn, true);
+    }
+
+    /**
+     * This method is like `_.findKey` except that it iterates over elements of
+     * a collection in the opposite order.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to search.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration.
+     * @returns {string|undefined} Returns the key of the matched element, else `undefined`.
+     * @example
+     *
+     * var users = {
+     *   'barney':  { 'age': 36, 'active': true },
+     *   'fred':    { 'age': 40, 'active': false },
+     *   'pebbles': { 'age': 1,  'active': true }
+     * };
+     *
+     * _.findLastKey(users, function(o) { return o.age < 40; });
+     * // => returns 'pebbles' assuming `_.findKey` returns 'barney'
+     *
+     * // The `_.matches` iteratee shorthand.
+     * _.findLastKey(users, { 'age': 36, 'active': true });
+     * // => 'barney'
+     *
+     * // The `_.matchesProperty` iteratee shorthand.
+     * _.findLastKey(users, ['active', false]);
+     * // => 'fred'
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.findLastKey(users, 'active');
+     * // => 'pebbles'
+     */
+    function findLastKey(object, predicate) {
+      return baseFind(object, getIteratee(predicate, 3), baseForOwnRight, true);
+    }
+
+    /**
+     * Iterates over own and inherited enumerable properties of an object invoking
+     * `iteratee` for each property. The iteratee is invoked with three arguments:
+     * (value, key, object). Iteratee functions may exit iteration early by explicitly
+     * returning `false`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to iterate over.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.forIn(new Foo, function(value, key) {
+     *   console.log(key);
+     * });
+     * // => logs 'a', 'b', then 'c' (iteration order is not guaranteed)
+     */
+    function forIn(object, iteratee) {
+      return object == null
+        ? object
+        : baseFor(object, baseCastFunction(iteratee), keysIn);
+    }
+
+    /**
+     * This method is like `_.forIn` except that it iterates over properties of
+     * `object` in the opposite order.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to iterate over.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.forInRight(new Foo, function(value, key) {
+     *   console.log(key);
+     * });
+     * // => logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c'
+     */
+    function forInRight(object, iteratee) {
+      return object == null
+        ? object
+        : baseForRight(object, baseCastFunction(iteratee), keysIn);
+    }
+
+    /**
+     * Iterates over own enumerable properties of an object invoking `iteratee`
+     * for each property. The iteratee is invoked with three arguments:
+     * (value, key, object). Iteratee functions may exit iteration early by
+     * explicitly returning `false`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to iterate over.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.forOwn(new Foo, function(value, key) {
+     *   console.log(key);
+     * });
+     * // => logs 'a' then 'b' (iteration order is not guaranteed)
+     */
+    function forOwn(object, iteratee) {
+      return object && baseForOwn(object, baseCastFunction(iteratee));
+    }
+
+    /**
+     * This method is like `_.forOwn` except that it iterates over properties of
+     * `object` in the opposite order.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to iterate over.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.forOwnRight(new Foo, function(value, key) {
+     *   console.log(key);
+     * });
+     * // => logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b'
+     */
+    function forOwnRight(object, iteratee) {
+      return object && baseForOwnRight(object, baseCastFunction(iteratee));
+    }
+
+    /**
+     * Creates an array of function property names from own enumerable properties
+     * of `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to inspect.
+     * @returns {Array} Returns the new array of property names.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = _.constant('a');
+     *   this.b = _.constant('b');
+     * }
+     *
+     * Foo.prototype.c = _.constant('c');
+     *
+     * _.functions(new Foo);
+     * // => ['a', 'b']
+     */
+    function functions(object) {
+      return object == null ? [] : baseFunctions(object, keys(object));
+    }
+
+    /**
+     * Creates an array of function property names from own and inherited
+     * enumerable properties of `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to inspect.
+     * @returns {Array} Returns the new array of property names.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = _.constant('a');
+     *   this.b = _.constant('b');
+     * }
+     *
+     * Foo.prototype.c = _.constant('c');
+     *
+     * _.functionsIn(new Foo);
+     * // => ['a', 'b', 'c']
+     */
+    function functionsIn(object) {
+      return object == null ? [] : baseFunctions(object, keysIn(object));
+    }
+
+    /**
+     * Gets the value at `path` of `object`. If the resolved value is
+     * `undefined` the `defaultValue` is used in its place.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path of the property to get.
+     * @param {*} [defaultValue] The value returned if the resolved value is `undefined`.
+     * @returns {*} Returns the resolved value.
+     * @example
+     *
+     * var object = { 'a': [{ 'b': { 'c': 3 } }] };
+     *
+     * _.get(object, 'a[0].b.c');
+     * // => 3
+     *
+     * _.get(object, ['a', '0', 'b', 'c']);
+     * // => 3
+     *
+     * _.get(object, 'a.b.c', 'default');
+     * // => 'default'
+     */
+    function get(object, path, defaultValue) {
+      var result = object == null ? undefined : baseGet(object, path);
+      return result === undefined ? defaultValue : result;
+    }
+
+    /**
+     * Checks if `path` is a direct property of `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path to check.
+     * @returns {boolean} Returns `true` if `path` exists, else `false`.
+     * @example
+     *
+     * var object = { 'a': { 'b': { 'c': 3 } } };
+     * var other = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) });
+     *
+     * _.has(object, 'a');
+     * // => true
+     *
+     * _.has(object, 'a.b.c');
+     * // => true
+     *
+     * _.has(object, ['a', 'b', 'c']);
+     * // => true
+     *
+     * _.has(other, 'a');
+     * // => false
+     */
+    function has(object, path) {
+      return hasPath(object, path, baseHas);
+    }
+
+    /**
+     * Checks if `path` is a direct or inherited property of `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path to check.
+     * @returns {boolean} Returns `true` if `path` exists, else `false`.
+     * @example
+     *
+     * var object = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) });
+     *
+     * _.hasIn(object, 'a');
+     * // => true
+     *
+     * _.hasIn(object, 'a.b.c');
+     * // => true
+     *
+     * _.hasIn(object, ['a', 'b', 'c']);
+     * // => true
+     *
+     * _.hasIn(object, 'b');
+     * // => false
+     */
+    function hasIn(object, path) {
+      return hasPath(object, path, baseHasIn);
+    }
+
+    /**
+     * Creates an object composed of the inverted keys and values of `object`.
+     * If `object` contains duplicate values, subsequent values overwrite property
+     * assignments of previous values.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to invert.
+     * @returns {Object} Returns the new inverted object.
+     * @example
+     *
+     * var object = { 'a': 1, 'b': 2, 'c': 1 };
+     *
+     * _.invert(object);
+     * // => { '1': 'c', '2': 'b' }
+     */
+    var invert = createInverter(function(result, value, key) {
+      result[value] = key;
+    }, constant(identity));
+
+    /**
+     * This method is like `_.invert` except that the inverted object is generated
+     * from the results of running each element of `object` through `iteratee`.
+     * The corresponding inverted value of each inverted key is an array of keys
+     * responsible for generating the inverted value. The iteratee is invoked
+     * with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to invert.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {Object} Returns the new inverted object.
+     * @example
+     *
+     * var object = { 'a': 1, 'b': 2, 'c': 1 };
+     *
+     * _.invertBy(object);
+     * // => { '1': ['a', 'c'], '2': ['b'] }
+     *
+     * _.invertBy(object, function(value) {
+     *   return 'group' + value;
+     * });
+     * // => { 'group1': ['a', 'c'], 'group2': ['b'] }
+     */
+    var invertBy = createInverter(function(result, value, key) {
+      if (hasOwnProperty.call(result, value)) {
+        result[value].push(key);
+      } else {
+        result[value] = [key];
+      }
+    }, getIteratee);
+
+    /**
+     * Invokes the method at `path` of `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path of the method to invoke.
+     * @param {...*} [args] The arguments to invoke the method with.
+     * @returns {*} Returns the result of the invoked method.
+     * @example
+     *
+     * var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] };
+     *
+     * _.invoke(object, 'a[0].b.c.slice', 1, 3);
+     * // => [2, 3]
+     */
+    var invoke = rest(baseInvoke);
+
+    /**
+     * Creates an array of the own enumerable property names of `object`.
+     *
+     * **Note:** Non-object values are coerced to objects. See the
+     * [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys)
+     * for more details.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the array of property names.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.keys(new Foo);
+     * // => ['a', 'b'] (iteration order is not guaranteed)
+     *
+     * _.keys('hi');
+     * // => ['0', '1']
+     */
+    function keys(object) {
+      var isProto = isPrototype(object);
+      if (!(isProto || isArrayLike(object))) {
+        return baseKeys(object);
+      }
+      var indexes = indexKeys(object),
+          skipIndexes = !!indexes,
+          result = indexes || [],
+          length = result.length;
+
+      for (var key in object) {
+        if (baseHas(object, key) &&
+            !(skipIndexes && (key == 'length' || isIndex(key, length))) &&
+            !(isProto && key == 'constructor')) {
+          result.push(key);
+        }
+      }
+      return result;
+    }
+
+    /**
+     * Creates an array of the own and inherited enumerable property names of `object`.
+     *
+     * **Note:** Non-object values are coerced to objects.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the array of property names.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.keysIn(new Foo);
+     * // => ['a', 'b', 'c'] (iteration order is not guaranteed)
+     */
+    function keysIn(object) {
+      var index = -1,
+          isProto = isPrototype(object),
+          props = baseKeysIn(object),
+          propsLength = props.length,
+          indexes = indexKeys(object),
+          skipIndexes = !!indexes,
+          result = indexes || [],
+          length = result.length;
+
+      while (++index < propsLength) {
+        var key = props[index];
+        if (!(skipIndexes && (key == 'length' || isIndex(key, length))) &&
+            !(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {
+          result.push(key);
+        }
+      }
+      return result;
+    }
+
+    /**
+     * The opposite of `_.mapValues`; this method creates an object with the
+     * same values as `object` and keys generated by running each own enumerable
+     * property of `object` through `iteratee`. The iteratee is invoked with
+     * three arguments: (value, key, object).
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Object} Returns the new mapped object.
+     * @example
+     *
+     * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) {
+     *   return key + value;
+     * });
+     * // => { 'a1': 1, 'b2': 2 }
+     */
+    function mapKeys(object, iteratee) {
+      var result = {};
+      iteratee = getIteratee(iteratee, 3);
+
+      baseForOwn(object, function(value, key, object) {
+        result[iteratee(value, key, object)] = value;
+      });
+      return result;
+    }
+
+    /**
+     * Creates an object with the same keys as `object` and values generated by
+     * running each own enumerable property of `object` through `iteratee`. The
+     * iteratee is invoked with three arguments: (value, key, object).
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Object} Returns the new mapped object.
+     * @example
+     *
+     * var users = {
+     *   'fred':    { 'user': 'fred',    'age': 40 },
+     *   'pebbles': { 'user': 'pebbles', 'age': 1 }
+     * };
+     *
+     * _.mapValues(users, function(o) { return o.age; });
+     * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.mapValues(users, 'age');
+     * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)
+     */
+    function mapValues(object, iteratee) {
+      var result = {};
+      iteratee = getIteratee(iteratee, 3);
+
+      baseForOwn(object, function(value, key, object) {
+        result[key] = iteratee(value, key, object);
+      });
+      return result;
+    }
+
+    /**
+     * This method is like `_.assign` except that it recursively merges own and
+     * inherited enumerable properties of source objects into the destination
+     * object. Source properties that resolve to `undefined` are skipped if a
+     * destination value exists. Array and plain object properties are merged
+     * recursively.Other objects and value types are overridden by assignment.
+     * Source objects are applied from left to right. Subsequent sources
+     * overwrite property assignments of previous sources.
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The destination object.
+     * @param {...Object} [sources] The source objects.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * var users = {
+     *   'data': [{ 'user': 'barney' }, { 'user': 'fred' }]
+     * };
+     *
+     * var ages = {
+     *   'data': [{ 'age': 36 }, { 'age': 40 }]
+     * };
+     *
+     * _.merge(users, ages);
+     * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] }
+     */
+    var merge = createAssigner(function(object, source, srcIndex) {
+      baseMerge(object, source, srcIndex);
+    });
+
+    /**
+     * This method is like `_.merge` except that it accepts `customizer` which
+     * is invoked to produce the merged values of the destination and source
+     * properties. If `customizer` returns `undefined` merging is handled by the
+     * method instead. The `customizer` is invoked with seven arguments:
+     * (objValue, srcValue, key, object, source, stack).
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The destination object.
+     * @param {...Object} sources The source objects.
+     * @param {Function} customizer The function to customize assigned values.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * function customizer(objValue, srcValue) {
+     *   if (_.isArray(objValue)) {
+     *     return objValue.concat(srcValue);
+     *   }
+     * }
+     *
+     * var object = {
+     *   'fruits': ['apple'],
+     *   'vegetables': ['beet']
+     * };
+     *
+     * var other = {
+     *   'fruits': ['banana'],
+     *   'vegetables': ['carrot']
+     * };
+     *
+     * _.mergeWith(object, other, customizer);
+     * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] }
+     */
+    var mergeWith = createAssigner(function(object, source, srcIndex, customizer) {
+      baseMerge(object, source, srcIndex, customizer);
+    });
+
+    /**
+     * The opposite of `_.pick`; this method creates an object composed of the
+     * own and inherited enumerable properties of `object` that are not omitted.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The source object.
+     * @param {...(string|string[])} [props] The property names to omit, specified
+     *  individually or in arrays.
+     * @returns {Object} Returns the new object.
+     * @example
+     *
+     * var object = { 'a': 1, 'b': '2', 'c': 3 };
+     *
+     * _.omit(object, ['a', 'c']);
+     * // => { 'b': '2' }
+     */
+    var omit = rest(function(object, props) {
+      if (object == null) {
+        return {};
+      }
+      props = arrayMap(baseFlatten(props, 1), String);
+      return basePick(object, baseDifference(keysIn(object), props));
+    });
+
+    /**
+     * The opposite of `_.pickBy`; this method creates an object composed of
+     * the own and inherited enumerable properties of `object` that `predicate`
+     * doesn't return truthy for. The predicate is invoked with two arguments:
+     * (value, key).
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The source object.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per property.
+     * @returns {Object} Returns the new object.
+     * @example
+     *
+     * var object = { 'a': 1, 'b': '2', 'c': 3 };
+     *
+     * _.omitBy(object, _.isNumber);
+     * // => { 'b': '2' }
+     */
+    function omitBy(object, predicate) {
+      predicate = getIteratee(predicate);
+      return basePickBy(object, function(value, key) {
+        return !predicate(value, key);
+      });
+    }
+
+    /**
+     * Creates an object composed of the picked `object` properties.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The source object.
+     * @param {...(string|string[])} [props] The property names to pick, specified
+     *  individually or in arrays.
+     * @returns {Object} Returns the new object.
+     * @example
+     *
+     * var object = { 'a': 1, 'b': '2', 'c': 3 };
+     *
+     * _.pick(object, ['a', 'c']);
+     * // => { 'a': 1, 'c': 3 }
+     */
+    var pick = rest(function(object, props) {
+      return object == null ? {} : basePick(object, baseFlatten(props, 1));
+    });
+
+    /**
+     * Creates an object composed of the `object` properties `predicate` returns
+     * truthy for. The predicate is invoked with two arguments: (value, key).
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The source object.
+     * @param {Function|Object|string} [predicate=_.identity] The function invoked per property.
+     * @returns {Object} Returns the new object.
+     * @example
+     *
+     * var object = { 'a': 1, 'b': '2', 'c': 3 };
+     *
+     * _.pickBy(object, _.isNumber);
+     * // => { 'a': 1, 'c': 3 }
+     */
+    function pickBy(object, predicate) {
+      return object == null ? {} : basePickBy(object, getIteratee(predicate));
+    }
+
+    /**
+     * This method is like `_.get` except that if the resolved value is a function
+     * it's invoked with the `this` binding of its parent object and its result
+     * is returned.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @param {Array|string} path The path of the property to resolve.
+     * @param {*} [defaultValue] The value returned if the resolved value is `undefined`.
+     * @returns {*} Returns the resolved value.
+     * @example
+     *
+     * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] };
+     *
+     * _.result(object, 'a[0].b.c1');
+     * // => 3
+     *
+     * _.result(object, 'a[0].b.c2');
+     * // => 4
+     *
+     * _.result(object, 'a[0].b.c3', 'default');
+     * // => 'default'
+     *
+     * _.result(object, 'a[0].b.c3', _.constant('default'));
+     * // => 'default'
+     */
+    function result(object, path, defaultValue) {
+      if (!isKey(path, object)) {
+        path = baseCastPath(path);
+        var result = get(object, path);
+        object = parent(object, path);
+      } else {
+        result = object == null ? undefined : object[path];
+      }
+      if (result === undefined) {
+        result = defaultValue;
+      }
+      return isFunction(result) ? result.call(object) : result;
+    }
+
+    /**
+     * Sets the value at `path` of `object`. If a portion of `path` doesn't exist
+     * it's created. Arrays are created for missing index properties while objects
+     * are created for all other missing properties. Use `_.setWith` to customize
+     * `path` creation.
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to modify.
+     * @param {Array|string} path The path of the property to set.
+     * @param {*} value The value to set.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * var object = { 'a': [{ 'b': { 'c': 3 } }] };
+     *
+     * _.set(object, 'a[0].b.c', 4);
+     * console.log(object.a[0].b.c);
+     * // => 4
+     *
+     * _.set(object, 'x[0].y.z', 5);
+     * console.log(object.x[0].y.z);
+     * // => 5
+     */
+    function set(object, path, value) {
+      return object == null ? object : baseSet(object, path, value);
+    }
+
+    /**
+     * This method is like `_.set` except that it accepts `customizer` which is
+     * invoked to produce the objects of `path`.  If `customizer` returns `undefined`
+     * path creation is handled by the method instead. The `customizer` is invoked
+     * with three arguments: (nsValue, key, nsObject).
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to modify.
+     * @param {Array|string} path The path of the property to set.
+     * @param {*} value The value to set.
+     * @param {Function} [customizer] The function to customize assigned values.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * var object = {};
+     *
+     * _.setWith(object, '[0][1]', 'a', Object);
+     * // => { '0': { '1': 'a' } }
+     */
+    function setWith(object, path, value, customizer) {
+      customizer = typeof customizer == 'function' ? customizer : undefined;
+      return object == null ? object : baseSet(object, path, value, customizer);
+    }
+
+    /**
+     * Creates an array of own enumerable key-value pairs for `object` which
+     * can be consumed by `_.fromPairs`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the new array of key-value pairs.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.toPairs(new Foo);
+     * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed)
+     */
+    function toPairs(object) {
+      return baseToPairs(object, keys(object));
+    }
+
+    /**
+     * Creates an array of own and inherited enumerable key-value pairs for
+     * `object` which can be consumed by `_.fromPairs`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the new array of key-value pairs.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.toPairsIn(new Foo);
+     * // => [['a', 1], ['b', 2], ['c', 1]] (iteration order is not guaranteed)
+     */
+    function toPairsIn(object) {
+      return baseToPairs(object, keysIn(object));
+    }
+
+    /**
+     * An alternative to `_.reduce`; this method transforms `object` to a new
+     * `accumulator` object which is the result of running each of its own enumerable
+     * properties through `iteratee`, with each invocation potentially mutating
+     * the `accumulator` object. The iteratee is invoked with four arguments:
+     * (accumulator, value, key, object). Iteratee functions may exit iteration
+     * early by explicitly returning `false`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Array|Object} object The object to iterate over.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @param {*} [accumulator] The custom accumulator value.
+     * @returns {*} Returns the accumulated value.
+     * @example
+     *
+     * _.transform([2, 3, 4], function(result, n) {
+     *   result.push(n *= n);
+     *   return n % 2 == 0;
+     * }, []);
+     * // => [4, 9]
+     *
+     * _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {
+     *   (result[value] || (result[value] = [])).push(key);
+     * }, {});
+     * // => { '1': ['a', 'c'], '2': ['b'] }
+     */
+    function transform(object, iteratee, accumulator) {
+      var isArr = isArray(object) || isTypedArray(object);
+      iteratee = getIteratee(iteratee, 4);
+
+      if (accumulator == null) {
+        if (isArr || isObject(object)) {
+          var Ctor = object.constructor;
+          if (isArr) {
+            accumulator = isArray(object) ? new Ctor : [];
+          } else {
+            accumulator = isFunction(Ctor) ? baseCreate(getPrototypeOf(object)) : {};
+          }
+        } else {
+          accumulator = {};
+        }
+      }
+      (isArr ? arrayEach : baseForOwn)(object, function(value, index, object) {
+        return iteratee(accumulator, value, index, object);
+      });
+      return accumulator;
+    }
+
+    /**
+     * Removes the property at `path` of `object`.
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to modify.
+     * @param {Array|string} path The path of the property to unset.
+     * @returns {boolean} Returns `true` if the property is deleted, else `false`.
+     * @example
+     *
+     * var object = { 'a': [{ 'b': { 'c': 7 } }] };
+     * _.unset(object, 'a[0].b.c');
+     * // => true
+     *
+     * console.log(object);
+     * // => { 'a': [{ 'b': {} }] };
+     *
+     * _.unset(object, 'a[0].b.c');
+     * // => true
+     *
+     * console.log(object);
+     * // => { 'a': [{ 'b': {} }] };
+     */
+    function unset(object, path) {
+      return object == null ? true : baseUnset(object, path);
+    }
+
+    /**
+     * This method is like `_.set` except that accepts `updater` to produce the
+     * value to set. Use `_.updateWith` to customize `path` creation. The `updater`
+     * is invoked with one argument: (value).
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to modify.
+     * @param {Array|string} path The path of the property to set.
+     * @param {Function} updater The function to produce the updated value.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * var object = { 'a': [{ 'b': { 'c': 3 } }] };
+     *
+     * _.update(object, 'a[0].b.c', function(n) { return n * n; });
+     * console.log(object.a[0].b.c);
+     * // => 9
+     *
+     * _.update(object, 'x[0].y.z', function(n) { return n ? n + 1 : 0; });
+     * console.log(object.x[0].y.z);
+     * // => 0
+     */
+    function update(object, path, updater) {
+      return object == null ? object : baseUpdate(object, path, baseCastFunction(updater));
+    }
+
+    /**
+     * This method is like `_.update` except that it accepts `customizer` which is
+     * invoked to produce the objects of `path`.  If `customizer` returns `undefined`
+     * path creation is handled by the method instead. The `customizer` is invoked
+     * with three arguments: (nsValue, key, nsObject).
+     *
+     * **Note:** This method mutates `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to modify.
+     * @param {Array|string} path The path of the property to set.
+     * @param {Function} updater The function to produce the updated value.
+     * @param {Function} [customizer] The function to customize assigned values.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * var object = {};
+     *
+     * _.updateWith(object, '[0][1]', _.constant('a'), Object);
+     * // => { '0': { '1': 'a' } }
+     */
+    function updateWith(object, path, updater, customizer) {
+      customizer = typeof customizer == 'function' ? customizer : undefined;
+      return object == null ? object : baseUpdate(object, path, baseCastFunction(updater), customizer);
+    }
+
+    /**
+     * Creates an array of the own enumerable property values of `object`.
+     *
+     * **Note:** Non-object values are coerced to objects.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the array of property values.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.values(new Foo);
+     * // => [1, 2] (iteration order is not guaranteed)
+     *
+     * _.values('hi');
+     * // => ['h', 'i']
+     */
+    function values(object) {
+      return object ? baseValues(object, keys(object)) : [];
+    }
+
+    /**
+     * Creates an array of the own and inherited enumerable property values of `object`.
+     *
+     * **Note:** Non-object values are coerced to objects.
+     *
+     * @static
+     * @memberOf _
+     * @category Object
+     * @param {Object} object The object to query.
+     * @returns {Array} Returns the array of property values.
+     * @example
+     *
+     * function Foo() {
+     *   this.a = 1;
+     *   this.b = 2;
+     * }
+     *
+     * Foo.prototype.c = 3;
+     *
+     * _.valuesIn(new Foo);
+     * // => [1, 2, 3] (iteration order is not guaranteed)
+     */
+    function valuesIn(object) {
+      return object == null ? [] : baseValues(object, keysIn(object));
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Clamps `number` within the inclusive `lower` and `upper` bounds.
+     *
+     * @static
+     * @memberOf _
+     * @category Number
+     * @param {number} number The number to clamp.
+     * @param {number} [lower] The lower bound.
+     * @param {number} upper The upper bound.
+     * @returns {number} Returns the clamped number.
+     * @example
+     *
+     * _.clamp(-10, -5, 5);
+     * // => -5
+     *
+     * _.clamp(10, -5, 5);
+     * // => 5
+     */
+    function clamp(number, lower, upper) {
+      if (upper === undefined) {
+        upper = lower;
+        lower = undefined;
+      }
+      if (upper !== undefined) {
+        upper = toNumber(upper);
+        upper = upper === upper ? upper : 0;
+      }
+      if (lower !== undefined) {
+        lower = toNumber(lower);
+        lower = lower === lower ? lower : 0;
+      }
+      return baseClamp(toNumber(number), lower, upper);
+    }
+
+    /**
+     * Checks if `n` is between `start` and up to but not including, `end`. If
+     * `end` is not specified it's set to `start` with `start` then set to `0`.
+     * If `start` is greater than `end` the params are swapped to support
+     * negative ranges.
+     *
+     * @static
+     * @memberOf _
+     * @category Number
+     * @param {number} number The number to check.
+     * @param {number} [start=0] The start of the range.
+     * @param {number} end The end of the range.
+     * @returns {boolean} Returns `true` if `number` is in the range, else `false`.
+     * @example
+     *
+     * _.inRange(3, 2, 4);
+     * // => true
+     *
+     * _.inRange(4, 8);
+     * // => true
+     *
+     * _.inRange(4, 2);
+     * // => false
+     *
+     * _.inRange(2, 2);
+     * // => false
+     *
+     * _.inRange(1.2, 2);
+     * // => true
+     *
+     * _.inRange(5.2, 4);
+     * // => false
+     *
+     * _.inRange(-3, -2, -6);
+     * // => true
+     */
+    function inRange(number, start, end) {
+      start = toNumber(start) || 0;
+      if (end === undefined) {
+        end = start;
+        start = 0;
+      } else {
+        end = toNumber(end) || 0;
+      }
+      number = toNumber(number);
+      return baseInRange(number, start, end);
+    }
+
+    /**
+     * Produces a random number between the inclusive `lower` and `upper` bounds.
+     * If only one argument is provided a number between `0` and the given number
+     * is returned. If `floating` is `true`, or either `lower` or `upper` are floats,
+     * a floating-point number is returned instead of an integer.
+     *
+     * **Note:** JavaScript follows the IEEE-754 standard for resolving
+     * floating-point values which can produce unexpected results.
+     *
+     * @static
+     * @memberOf _
+     * @category Number
+     * @param {number} [lower=0] The lower bound.
+     * @param {number} [upper=1] The upper bound.
+     * @param {boolean} [floating] Specify returning a floating-point number.
+     * @returns {number} Returns the random number.
+     * @example
+     *
+     * _.random(0, 5);
+     * // => an integer between 0 and 5
+     *
+     * _.random(5);
+     * // => also an integer between 0 and 5
+     *
+     * _.random(5, true);
+     * // => a floating-point number between 0 and 5
+     *
+     * _.random(1.2, 5.2);
+     * // => a floating-point number between 1.2 and 5.2
+     */
+    function random(lower, upper, floating) {
+      if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) {
+        upper = floating = undefined;
+      }
+      if (floating === undefined) {
+        if (typeof upper == 'boolean') {
+          floating = upper;
+          upper = undefined;
+        }
+        else if (typeof lower == 'boolean') {
+          floating = lower;
+          lower = undefined;
+        }
+      }
+      if (lower === undefined && upper === undefined) {
+        lower = 0;
+        upper = 1;
+      }
+      else {
+        lower = toNumber(lower) || 0;
+        if (upper === undefined) {
+          upper = lower;
+          lower = 0;
+        } else {
+          upper = toNumber(upper) || 0;
+        }
+      }
+      if (lower > upper) {
+        var temp = lower;
+        lower = upper;
+        upper = temp;
+      }
+      if (floating || lower % 1 || upper % 1) {
+        var rand = nativeRandom();
+        return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper);
+      }
+      return baseRandom(lower, upper);
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the camel cased string.
+     * @example
+     *
+     * _.camelCase('Foo Bar');
+     * // => 'fooBar'
+     *
+     * _.camelCase('--foo-bar');
+     * // => 'fooBar'
+     *
+     * _.camelCase('__foo_bar__');
+     * // => 'fooBar'
+     */
+    var camelCase = createCompounder(function(result, word, index) {
+      word = word.toLowerCase();
+      return result + (index ? capitalize(word) : word);
+    });
+
+    /**
+     * Converts the first character of `string` to upper case and the remaining
+     * to lower case.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to capitalize.
+     * @returns {string} Returns the capitalized string.
+     * @example
+     *
+     * _.capitalize('FRED');
+     * // => 'Fred'
+     */
+    function capitalize(string) {
+      return upperFirst(toString(string).toLowerCase());
+    }
+
+    /**
+     * Deburrs `string` by converting [latin-1 supplementary letters](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table)
+     * to basic latin letters and removing [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to deburr.
+     * @returns {string} Returns the deburred string.
+     * @example
+     *
+     * _.deburr('déjà vu');
+     * // => 'deja vu'
+     */
+    function deburr(string) {
+      string = toString(string);
+      return string && string.replace(reLatin1, deburrLetter).replace(reComboMark, '');
+    }
+
+    /**
+     * Checks if `string` ends with the given target string.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to search.
+     * @param {string} [target] The string to search for.
+     * @param {number} [position=string.length] The position to search from.
+     * @returns {boolean} Returns `true` if `string` ends with `target`, else `false`.
+     * @example
+     *
+     * _.endsWith('abc', 'c');
+     * // => true
+     *
+     * _.endsWith('abc', 'b');
+     * // => false
+     *
+     * _.endsWith('abc', 'b', 2);
+     * // => true
+     */
+    function endsWith(string, target, position) {
+      string = toString(string);
+      target = typeof target == 'string' ? target : (target + '');
+
+      var length = string.length;
+      position = position === undefined
+        ? length
+        : baseClamp(toInteger(position), 0, length);
+
+      position -= target.length;
+      return position >= 0 && string.indexOf(target, position) == position;
+    }
+
+    /**
+     * Converts the characters "&", "<", ">", '"', "'", and "\`" in `string` to
+     * their corresponding HTML entities.
+     *
+     * **Note:** No other characters are escaped. To escape additional
+     * characters use a third-party library like [_he_](https://mths.be/he).
+     *
+     * Though the ">" character is escaped for symmetry, characters like
+     * ">" and "/" don't need escaping in HTML and have no special meaning
+     * unless they're part of a tag or unquoted attribute value.
+     * See [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands)
+     * (under "semi-related fun fact") for more details.
+     *
+     * Backticks are escaped because in IE < 9, they can break out of
+     * attribute values or HTML comments. See [#59](https://html5sec.org/#59),
+     * [#102](https://html5sec.org/#102), [#108](https://html5sec.org/#108), and
+     * [#133](https://html5sec.org/#133) of the [HTML5 Security Cheatsheet](https://html5sec.org/)
+     * for more details.
+     *
+     * When working with HTML you should always [quote attribute values](http://wonko.com/post/html-escaping)
+     * to reduce XSS vectors.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to escape.
+     * @returns {string} Returns the escaped string.
+     * @example
+     *
+     * _.escape('fred, barney, & pebbles');
+     * // => 'fred, barney, &amp; pebbles'
+     */
+    function escape(string) {
+      string = toString(string);
+      return (string && reHasUnescapedHtml.test(string))
+        ? string.replace(reUnescapedHtml, escapeHtmlChar)
+        : string;
+    }
+
+    /**
+     * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+",
+     * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to escape.
+     * @returns {string} Returns the escaped string.
+     * @example
+     *
+     * _.escapeRegExp('[lodash](https://lodash.com/)');
+     * // => '\[lodash\]\(https://lodash\.com/\)'
+     */
+    function escapeRegExp(string) {
+      string = toString(string);
+      return (string && reHasRegExpChar.test(string))
+        ? string.replace(reRegExpChar, '\\$&')
+        : string;
+    }
+
+    /**
+     * Converts `string` to [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the kebab cased string.
+     * @example
+     *
+     * _.kebabCase('Foo Bar');
+     * // => 'foo-bar'
+     *
+     * _.kebabCase('fooBar');
+     * // => 'foo-bar'
+     *
+     * _.kebabCase('__foo_bar__');
+     * // => 'foo-bar'
+     */
+    var kebabCase = createCompounder(function(result, word, index) {
+      return result + (index ? '-' : '') + word.toLowerCase();
+    });
+
+    /**
+     * Converts `string`, as space separated words, to lower case.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the lower cased string.
+     * @example
+     *
+     * _.lowerCase('--Foo-Bar');
+     * // => 'foo bar'
+     *
+     * _.lowerCase('fooBar');
+     * // => 'foo bar'
+     *
+     * _.lowerCase('__FOO_BAR__');
+     * // => 'foo bar'
+     */
+    var lowerCase = createCompounder(function(result, word, index) {
+      return result + (index ? ' ' : '') + word.toLowerCase();
+    });
+
+    /**
+     * Converts the first character of `string` to lower case.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the converted string.
+     * @example
+     *
+     * _.lowerFirst('Fred');
+     * // => 'fred'
+     *
+     * _.lowerFirst('FRED');
+     * // => 'fRED'
+     */
+    var lowerFirst = createCaseFirst('toLowerCase');
+
+    /**
+     * Converts the first character of `string` to upper case.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the converted string.
+     * @example
+     *
+     * _.upperFirst('fred');
+     * // => 'Fred'
+     *
+     * _.upperFirst('FRED');
+     * // => 'FRED'
+     */
+    var upperFirst = createCaseFirst('toUpperCase');
+
+    /**
+     * Pads `string` on the left and right sides if it's shorter than `length`.
+     * Padding characters are truncated if they can't be evenly divided by `length`.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to pad.
+     * @param {number} [length=0] The padding length.
+     * @param {string} [chars=' '] The string used as padding.
+     * @returns {string} Returns the padded string.
+     * @example
+     *
+     * _.pad('abc', 8);
+     * // => '  abc   '
+     *
+     * _.pad('abc', 8, '_-');
+     * // => '_-abc_-_'
+     *
+     * _.pad('abc', 3);
+     * // => 'abc'
+     */
+    function pad(string, length, chars) {
+      string = toString(string);
+      length = toInteger(length);
+
+      var strLength = stringSize(string);
+      if (!length || strLength >= length) {
+        return string;
+      }
+      var mid = (length - strLength) / 2,
+          leftLength = nativeFloor(mid),
+          rightLength = nativeCeil(mid);
+
+      return createPadding('', leftLength, chars) + string + createPadding('', rightLength, chars);
+    }
+
+    /**
+     * Pads `string` on the right side if it's shorter than `length`. Padding
+     * characters are truncated if they exceed `length`.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to pad.
+     * @param {number} [length=0] The padding length.
+     * @param {string} [chars=' '] The string used as padding.
+     * @returns {string} Returns the padded string.
+     * @example
+     *
+     * _.padEnd('abc', 6);
+     * // => 'abc   '
+     *
+     * _.padEnd('abc', 6, '_-');
+     * // => 'abc_-_'
+     *
+     * _.padEnd('abc', 3);
+     * // => 'abc'
+     */
+    function padEnd(string, length, chars) {
+      string = toString(string);
+      return string + createPadding(string, length, chars);
+    }
+
+    /**
+     * Pads `string` on the left side if it's shorter than `length`. Padding
+     * characters are truncated if they exceed `length`.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to pad.
+     * @param {number} [length=0] The padding length.
+     * @param {string} [chars=' '] The string used as padding.
+     * @returns {string} Returns the padded string.
+     * @example
+     *
+     * _.padStart('abc', 6);
+     * // => '   abc'
+     *
+     * _.padStart('abc', 6, '_-');
+     * // => '_-_abc'
+     *
+     * _.padStart('abc', 3);
+     * // => 'abc'
+     */
+    function padStart(string, length, chars) {
+      string = toString(string);
+      return createPadding(string, length, chars) + string;
+    }
+
+    /**
+     * Converts `string` to an integer of the specified radix. If `radix` is
+     * `undefined` or `0`, a `radix` of `10` is used unless `value` is a hexadecimal,
+     * in which case a `radix` of `16` is used.
+     *
+     * **Note:** This method aligns with the [ES5 implementation](https://es5.github.io/#x15.1.2.2)
+     * of `parseInt`.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} string The string to convert.
+     * @param {number} [radix=10] The radix to interpret `value` by.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {number} Returns the converted integer.
+     * @example
+     *
+     * _.parseInt('08');
+     * // => 8
+     *
+     * _.map(['6', '08', '10'], _.parseInt);
+     * // => [6, 8, 10]
+     */
+    function parseInt(string, radix, guard) {
+      // Chrome fails to trim leading <BOM> whitespace characters.
+      // See https://code.google.com/p/v8/issues/detail?id=3109 for more details.
+      if (guard || radix == null) {
+        radix = 0;
+      } else if (radix) {
+        radix = +radix;
+      }
+      string = toString(string).replace(reTrim, '');
+      return nativeParseInt(string, radix || (reHasHexPrefix.test(string) ? 16 : 10));
+    }
+
+    /**
+     * Repeats the given string `n` times.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to repeat.
+     * @param {number} [n=0] The number of times to repeat the string.
+     * @returns {string} Returns the repeated string.
+     * @example
+     *
+     * _.repeat('*', 3);
+     * // => '***'
+     *
+     * _.repeat('abc', 2);
+     * // => 'abcabc'
+     *
+     * _.repeat('abc', 0);
+     * // => ''
+     */
+    function repeat(string, n) {
+      string = toString(string);
+      n = toInteger(n);
+
+      var result = '';
+      if (!string || n < 1 || n > MAX_SAFE_INTEGER) {
+        return result;
+      }
+      // Leverage the exponentiation by squaring algorithm for a faster repeat.
+      // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details.
+      do {
+        if (n % 2) {
+          result += string;
+        }
+        n = nativeFloor(n / 2);
+        string += string;
+      } while (n);
+
+      return result;
+    }
+
+    /**
+     * Replaces matches for `pattern` in `string` with `replacement`.
+     *
+     * **Note:** This method is based on [`String#replace`](https://mdn.io/String/replace).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to modify.
+     * @param {RegExp|string} pattern The pattern to replace.
+     * @param {Function|string} replacement The match replacement.
+     * @returns {string} Returns the modified string.
+     * @example
+     *
+     * _.replace('Hi Fred', 'Fred', 'Barney');
+     * // => 'Hi Barney'
+     */
+    function replace() {
+      var args = arguments,
+          string = toString(args[0]);
+
+      return args.length < 3 ? string : string.replace(args[1], args[2]);
+    }
+
+    /**
+     * Converts `string` to [snake case](https://en.wikipedia.org/wiki/Snake_case).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the snake cased string.
+     * @example
+     *
+     * _.snakeCase('Foo Bar');
+     * // => 'foo_bar'
+     *
+     * _.snakeCase('fooBar');
+     * // => 'foo_bar'
+     *
+     * _.snakeCase('--foo-bar');
+     * // => 'foo_bar'
+     */
+    var snakeCase = createCompounder(function(result, word, index) {
+      return result + (index ? '_' : '') + word.toLowerCase();
+    });
+
+    /**
+     * Splits `string` by `separator`.
+     *
+     * **Note:** This method is based on [`String#split`](https://mdn.io/String/split).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to split.
+     * @param {RegExp|string} separator The separator pattern to split by.
+     * @param {number} [limit] The length to truncate results to.
+     * @returns {Array} Returns the new array of string segments.
+     * @example
+     *
+     * _.split('a-b-c', '-', 2);
+     * // => ['a', 'b']
+     */
+    function split(string, separator, limit) {
+      return toString(string).split(separator, limit);
+    }
+
+    /**
+     * Converts `string` to [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the start cased string.
+     * @example
+     *
+     * _.startCase('--foo-bar');
+     * // => 'Foo Bar'
+     *
+     * _.startCase('fooBar');
+     * // => 'Foo Bar'
+     *
+     * _.startCase('__foo_bar__');
+     * // => 'Foo Bar'
+     */
+    var startCase = createCompounder(function(result, word, index) {
+      return result + (index ? ' ' : '') + capitalize(word);
+    });
+
+    /**
+     * Checks if `string` starts with the given target string.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to search.
+     * @param {string} [target] The string to search for.
+     * @param {number} [position=0] The position to search from.
+     * @returns {boolean} Returns `true` if `string` starts with `target`, else `false`.
+     * @example
+     *
+     * _.startsWith('abc', 'a');
+     * // => true
+     *
+     * _.startsWith('abc', 'b');
+     * // => false
+     *
+     * _.startsWith('abc', 'b', 1);
+     * // => true
+     */
+    function startsWith(string, target, position) {
+      string = toString(string);
+      position = baseClamp(toInteger(position), 0, string.length);
+      return string.lastIndexOf(target, position) == position;
+    }
+
+    /**
+     * Creates a compiled template function that can interpolate data properties
+     * in "interpolate" delimiters, HTML-escape interpolated data properties in
+     * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data
+     * properties may be accessed as free variables in the template. If a setting
+     * object is given it takes precedence over `_.templateSettings` values.
+     *
+     * **Note:** In the development build `_.template` utilizes
+     * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl)
+     * for easier debugging.
+     *
+     * For more information on precompiling templates see
+     * [lodash's custom builds documentation](https://lodash.com/custom-builds).
+     *
+     * For more information on Chrome extension sandboxes see
+     * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The template string.
+     * @param {Object} [options] The options object.
+     * @param {RegExp} [options.escape] The HTML "escape" delimiter.
+     * @param {RegExp} [options.evaluate] The "evaluate" delimiter.
+     * @param {Object} [options.imports] An object to import into the template as free variables.
+     * @param {RegExp} [options.interpolate] The "interpolate" delimiter.
+     * @param {string} [options.sourceURL] The sourceURL of the template's compiled source.
+     * @param {string} [options.variable] The data object variable name.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {Function} Returns the compiled template function.
+     * @example
+     *
+     * // Use the "interpolate" delimiter to create a compiled template.
+     * var compiled = _.template('hello <%= user %>!');
+     * compiled({ 'user': 'fred' });
+     * // => 'hello fred!'
+     *
+     * // Use the HTML "escape" delimiter to escape data property values.
+     * var compiled = _.template('<b><%- value %></b>');
+     * compiled({ 'value': '<script>' });
+     * // => '<b>&lt;script&gt;</b>'
+     *
+     * // Use the "evaluate" delimiter to execute JavaScript and generate HTML.
+     * var compiled = _.template('<% _.forEach(users, function(user) { %><li><%- user %></li><% }); %>');
+     * compiled({ 'users': ['fred', 'barney'] });
+     * // => '<li>fred</li><li>barney</li>'
+     *
+     * // Use the internal `print` function in "evaluate" delimiters.
+     * var compiled = _.template('<% print("hello " + user); %>!');
+     * compiled({ 'user': 'barney' });
+     * // => 'hello barney!'
+     *
+     * // Use the ES delimiter as an alternative to the default "interpolate" delimiter.
+     * var compiled = _.template('hello ${ user }!');
+     * compiled({ 'user': 'pebbles' });
+     * // => 'hello pebbles!'
+     *
+     * // Use custom template delimiters.
+     * _.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
+     * var compiled = _.template('hello {{ user }}!');
+     * compiled({ 'user': 'mustache' });
+     * // => 'hello mustache!'
+     *
+     * // Use backslashes to treat delimiters as plain text.
+     * var compiled = _.template('<%= "\\<%- value %\\>" %>');
+     * compiled({ 'value': 'ignored' });
+     * // => '<%- value %>'
+     *
+     * // Use the `imports` option to import `jQuery` as `jq`.
+     * var text = '<% jq.each(users, function(user) { %><li><%- user %></li><% }); %>';
+     * var compiled = _.template(text, { 'imports': { 'jq': jQuery } });
+     * compiled({ 'users': ['fred', 'barney'] });
+     * // => '<li>fred</li><li>barney</li>'
+     *
+     * // Use the `sourceURL` option to specify a custom sourceURL for the template.
+     * var compiled = _.template('hello <%= user %>!', { 'sourceURL': '/basic/greeting.jst' });
+     * compiled(data);
+     * // => find the source of "greeting.jst" under the Sources tab or Resources panel of the web inspector
+     *
+     * // Use the `variable` option to ensure a with-statement isn't used in the compiled template.
+     * var compiled = _.template('hi <%= data.user %>!', { 'variable': 'data' });
+     * compiled.source;
+     * // => function(data) {
+     * //   var __t, __p = '';
+     * //   __p += 'hi ' + ((__t = ( data.user )) == null ? '' : __t) + '!';
+     * //   return __p;
+     * // }
+     *
+     * // Use the `source` property to inline compiled templates for meaningful
+     * // line numbers in error messages and stack traces.
+     * fs.writeFileSync(path.join(cwd, 'jst.js'), '\
+     *   var JST = {\
+     *     "main": ' + _.template(mainText).source + '\
+     *   };\
+     * ');
+     */
+    function template(string, options, guard) {
+      // Based on John Resig's `tmpl` implementation (http://ejohn.org/blog/javascript-micro-templating/)
+      // and Laura Doktorova's doT.js (https://github.com/olado/doT).
+      var settings = lodash.templateSettings;
+
+      if (guard && isIterateeCall(string, options, guard)) {
+        options = undefined;
+      }
+      string = toString(string);
+      options = assignInWith({}, options, settings, assignInDefaults);
+
+      var imports = assignInWith({}, options.imports, settings.imports, assignInDefaults),
+          importsKeys = keys(imports),
+          importsValues = baseValues(imports, importsKeys);
+
+      var isEscaping,
+          isEvaluating,
+          index = 0,
+          interpolate = options.interpolate || reNoMatch,
+          source = "__p += '";
+
+      // Compile the regexp to match each delimiter.
+      var reDelimiters = RegExp(
+        (options.escape || reNoMatch).source + '|' +
+        interpolate.source + '|' +
+        (interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + '|' +
+        (options.evaluate || reNoMatch).source + '|$'
+      , 'g');
+
+      // Use a sourceURL for easier debugging.
+      var sourceURL = '//# sourceURL=' +
+        ('sourceURL' in options
+          ? options.sourceURL
+          : ('lodash.templateSources[' + (++templateCounter) + ']')
+        ) + '\n';
+
+      string.replace(reDelimiters, function(match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) {
+        interpolateValue || (interpolateValue = esTemplateValue);
+
+        // Escape characters that can't be included in string literals.
+        source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar);
+
+        // Replace delimiters with snippets.
+        if (escapeValue) {
+          isEscaping = true;
+          source += "' +\n__e(" + escapeValue + ") +\n'";
+        }
+        if (evaluateValue) {
+          isEvaluating = true;
+          source += "';\n" + evaluateValue + ";\n__p += '";
+        }
+        if (interpolateValue) {
+          source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'";
+        }
+        index = offset + match.length;
+
+        // The JS engine embedded in Adobe products needs `match` returned in
+        // order to produce the correct `offset` value.
+        return match;
+      });
+
+      source += "';\n";
+
+      // If `variable` is not specified wrap a with-statement around the generated
+      // code to add the data object to the top of the scope chain.
+      var variable = options.variable;
+      if (!variable) {
+        source = 'with (obj) {\n' + source + '\n}\n';
+      }
+      // Cleanup code by stripping empty strings.
+      source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source)
+        .replace(reEmptyStringMiddle, '$1')
+        .replace(reEmptyStringTrailing, '$1;');
+
+      // Frame code as the function body.
+      source = 'function(' + (variable || 'obj') + ') {\n' +
+        (variable
+          ? ''
+          : 'obj || (obj = {});\n'
+        ) +
+        "var __t, __p = ''" +
+        (isEscaping
+           ? ', __e = _.escape'
+           : ''
+        ) +
+        (isEvaluating
+          ? ', __j = Array.prototype.join;\n' +
+            "function print() { __p += __j.call(arguments, '') }\n"
+          : ';\n'
+        ) +
+        source +
+        'return __p\n}';
+
+      var result = attempt(function() {
+        return Function(importsKeys, sourceURL + 'return ' + source)
+          .apply(undefined, importsValues);
+      });
+
+      // Provide the compiled function's source by its `toString` method or
+      // the `source` property as a convenience for inlining compiled templates.
+      result.source = source;
+      if (isError(result)) {
+        throw result;
+      }
+      return result;
+    }
+
+    /**
+     * Converts `string`, as a whole, to lower case just like
+     * [String#toLowerCase](https://mdn.io/toLowerCase).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the lower cased string.
+     * @example
+     *
+     * _.toLower('--Foo-Bar');
+     * // => '--foo-bar'
+     *
+     * _.toLower('fooBar');
+     * // => 'foobar'
+     *
+     * _.toLower('__FOO_BAR__');
+     * // => '__foo_bar__'
+     */
+    function toLower(value) {
+      return toString(value).toLowerCase();
+    }
+
+    /**
+     * Converts `string`, as a whole, to upper case just like
+     * [String#toUpperCase](https://mdn.io/toUpperCase).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the upper cased string.
+     * @example
+     *
+     * _.toUpper('--foo-bar');
+     * // => '--FOO-BAR'
+     *
+     * _.toUpper('fooBar');
+     * // => 'FOOBAR'
+     *
+     * _.toUpper('__foo_bar__');
+     * // => '__FOO_BAR__'
+     */
+    function toUpper(value) {
+      return toString(value).toUpperCase();
+    }
+
+    /**
+     * Removes leading and trailing whitespace or specified characters from `string`.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to trim.
+     * @param {string} [chars=whitespace] The characters to trim.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {string} Returns the trimmed string.
+     * @example
+     *
+     * _.trim('  abc  ');
+     * // => 'abc'
+     *
+     * _.trim('-_-abc-_-', '_-');
+     * // => 'abc'
+     *
+     * _.map(['  foo  ', '  bar  '], _.trim);
+     * // => ['foo', 'bar']
+     */
+    function trim(string, chars, guard) {
+      string = toString(string);
+      if (!string) {
+        return string;
+      }
+      if (guard || chars === undefined) {
+        return string.replace(reTrim, '');
+      }
+      chars = (chars + '');
+      if (!chars) {
+        return string;
+      }
+      var strSymbols = stringToArray(string),
+          chrSymbols = stringToArray(chars);
+
+      return strSymbols
+        .slice(charsStartIndex(strSymbols, chrSymbols), charsEndIndex(strSymbols, chrSymbols) + 1)
+        .join('');
+    }
+
+    /**
+     * Removes trailing whitespace or specified characters from `string`.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to trim.
+     * @param {string} [chars=whitespace] The characters to trim.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {string} Returns the trimmed string.
+     * @example
+     *
+     * _.trimEnd('  abc  ');
+     * // => '  abc'
+     *
+     * _.trimEnd('-_-abc-_-', '_-');
+     * // => '-_-abc'
+     */
+    function trimEnd(string, chars, guard) {
+      string = toString(string);
+      if (!string) {
+        return string;
+      }
+      if (guard || chars === undefined) {
+        return string.replace(reTrimEnd, '');
+      }
+      chars = (chars + '');
+      if (!chars) {
+        return string;
+      }
+      var strSymbols = stringToArray(string);
+      return strSymbols
+        .slice(0, charsEndIndex(strSymbols, stringToArray(chars)) + 1)
+        .join('');
+    }
+
+    /**
+     * Removes leading whitespace or specified characters from `string`.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to trim.
+     * @param {string} [chars=whitespace] The characters to trim.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {string} Returns the trimmed string.
+     * @example
+     *
+     * _.trimStart('  abc  ');
+     * // => 'abc  '
+     *
+     * _.trimStart('-_-abc-_-', '_-');
+     * // => 'abc-_-'
+     */
+    function trimStart(string, chars, guard) {
+      string = toString(string);
+      if (!string) {
+        return string;
+      }
+      if (guard || chars === undefined) {
+        return string.replace(reTrimStart, '');
+      }
+      chars = (chars + '');
+      if (!chars) {
+        return string;
+      }
+      var strSymbols = stringToArray(string);
+      return strSymbols
+        .slice(charsStartIndex(strSymbols, stringToArray(chars)))
+        .join('');
+    }
+
+    /**
+     * Truncates `string` if it's longer than the given maximum string length.
+     * The last characters of the truncated string are replaced with the omission
+     * string which defaults to "...".
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to truncate.
+     * @param {Object} [options=({})] The options object.
+     * @param {number} [options.length=30] The maximum string length.
+     * @param {string} [options.omission='...'] The string to indicate text is omitted.
+     * @param {RegExp|string} [options.separator] The separator pattern to truncate to.
+     * @returns {string} Returns the truncated string.
+     * @example
+     *
+     * _.truncate('hi-diddly-ho there, neighborino');
+     * // => 'hi-diddly-ho there, neighbo...'
+     *
+     * _.truncate('hi-diddly-ho there, neighborino', {
+     *   'length': 24,
+     *   'separator': ' '
+     * });
+     * // => 'hi-diddly-ho there,...'
+     *
+     * _.truncate('hi-diddly-ho there, neighborino', {
+     *   'length': 24,
+     *   'separator': /,? +/
+     * });
+     * // => 'hi-diddly-ho there...'
+     *
+     * _.truncate('hi-diddly-ho there, neighborino', {
+     *   'omission': ' [...]'
+     * });
+     * // => 'hi-diddly-ho there, neig [...]'
+     */
+    function truncate(string, options) {
+      var length = DEFAULT_TRUNC_LENGTH,
+          omission = DEFAULT_TRUNC_OMISSION;
+
+      if (isObject(options)) {
+        var separator = 'separator' in options ? options.separator : separator;
+        length = 'length' in options ? toInteger(options.length) : length;
+        omission = 'omission' in options ? toString(options.omission) : omission;
+      }
+      string = toString(string);
+
+      var strLength = string.length;
+      if (reHasComplexSymbol.test(string)) {
+        var strSymbols = stringToArray(string);
+        strLength = strSymbols.length;
+      }
+      if (length >= strLength) {
+        return string;
+      }
+      var end = length - stringSize(omission);
+      if (end < 1) {
+        return omission;
+      }
+      var result = strSymbols
+        ? strSymbols.slice(0, end).join('')
+        : string.slice(0, end);
+
+      if (separator === undefined) {
+        return result + omission;
+      }
+      if (strSymbols) {
+        end += (result.length - end);
+      }
+      if (isRegExp(separator)) {
+        if (string.slice(end).search(separator)) {
+          var match,
+              substring = result;
+
+          if (!separator.global) {
+            separator = RegExp(separator.source, toString(reFlags.exec(separator)) + 'g');
+          }
+          separator.lastIndex = 0;
+          while ((match = separator.exec(substring))) {
+            var newEnd = match.index;
+          }
+          result = result.slice(0, newEnd === undefined ? end : newEnd);
+        }
+      } else if (string.indexOf(separator, end) != end) {
+        var index = result.lastIndexOf(separator);
+        if (index > -1) {
+          result = result.slice(0, index);
+        }
+      }
+      return result + omission;
+    }
+
+    /**
+     * The inverse of `_.escape`; this method converts the HTML entities
+     * `&amp;`, `&lt;`, `&gt;`, `&quot;`, `&#39;`, and `&#96;` in `string` to their
+     * corresponding characters.
+     *
+     * **Note:** No other HTML entities are unescaped. To unescape additional HTML
+     * entities use a third-party library like [_he_](https://mths.be/he).
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to unescape.
+     * @returns {string} Returns the unescaped string.
+     * @example
+     *
+     * _.unescape('fred, barney, &amp; pebbles');
+     * // => 'fred, barney, & pebbles'
+     */
+    function unescape(string) {
+      string = toString(string);
+      return (string && reHasEscapedHtml.test(string))
+        ? string.replace(reEscapedHtml, unescapeHtmlChar)
+        : string;
+    }
+
+    /**
+     * Converts `string`, as space separated words, to upper case.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to convert.
+     * @returns {string} Returns the upper cased string.
+     * @example
+     *
+     * _.upperCase('--foo-bar');
+     * // => 'FOO BAR'
+     *
+     * _.upperCase('fooBar');
+     * // => 'FOO BAR'
+     *
+     * _.upperCase('__foo_bar__');
+     * // => 'FOO BAR'
+     */
+    var upperCase = createCompounder(function(result, word, index) {
+      return result + (index ? ' ' : '') + word.toUpperCase();
+    });
+
+    /**
+     * Splits `string` into an array of its words.
+     *
+     * @static
+     * @memberOf _
+     * @category String
+     * @param {string} [string=''] The string to inspect.
+     * @param {RegExp|string} [pattern] The pattern to match words.
+     * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`.
+     * @returns {Array} Returns the words of `string`.
+     * @example
+     *
+     * _.words('fred, barney, & pebbles');
+     * // => ['fred', 'barney', 'pebbles']
+     *
+     * _.words('fred, barney, & pebbles', /[^, ]+/g);
+     * // => ['fred', 'barney', '&', 'pebbles']
+     */
+    function words(string, pattern, guard) {
+      string = toString(string);
+      pattern = guard ? undefined : pattern;
+
+      if (pattern === undefined) {
+        pattern = reHasComplexWord.test(string) ? reComplexWord : reBasicWord;
+      }
+      return string.match(pattern) || [];
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Attempts to invoke `func`, returning either the result or the caught error
+     * object. Any additional arguments are provided to `func` when it's invoked.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Function} func The function to attempt.
+     * @returns {*} Returns the `func` result or error object.
+     * @example
+     *
+     * // Avoid throwing errors for invalid selectors.
+     * var elements = _.attempt(function(selector) {
+     *   return document.querySelectorAll(selector);
+     * }, '>_>');
+     *
+     * if (_.isError(elements)) {
+     *   elements = [];
+     * }
+     */
+    var attempt = rest(function(func, args) {
+      try {
+        return apply(func, undefined, args);
+      } catch (e) {
+        return isError(e) ? e : new Error(e);
+      }
+    });
+
+    /**
+     * Binds methods of an object to the object itself, overwriting the existing
+     * method.
+     *
+     * **Note:** This method doesn't set the "length" property of bound functions.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Object} object The object to bind and assign the bound methods to.
+     * @param {...(string|string[])} methodNames The object method names to bind,
+     *  specified individually or in arrays.
+     * @returns {Object} Returns `object`.
+     * @example
+     *
+     * var view = {
+     *   'label': 'docs',
+     *   'onClick': function() {
+     *     console.log('clicked ' + this.label);
+     *   }
+     * };
+     *
+     * _.bindAll(view, 'onClick');
+     * jQuery(element).on('click', view.onClick);
+     * // => logs 'clicked docs' when clicked
+     */
+    var bindAll = rest(function(object, methodNames) {
+      arrayEach(baseFlatten(methodNames, 1), function(key) {
+        object[key] = bind(object[key], object);
+      });
+      return object;
+    });
+
+    /**
+     * Creates a function that iterates over `pairs` invoking the corresponding
+     * function of the first predicate to return truthy. The predicate-function
+     * pairs are invoked with the `this` binding and arguments of the created
+     * function.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Array} pairs The predicate-function pairs.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var func = _.cond([
+     *   [_.matches({ 'a': 1 }),           _.constant('matches A')],
+     *   [_.conforms({ 'b': _.isNumber }), _.constant('matches B')],
+     *   [_.constant(true),                _.constant('no match')]
+     * ]);
+     *
+     * func({ 'a': 1, 'b': 2 });
+     * // => 'matches A'
+     *
+     * func({ 'a': 0, 'b': 1 });
+     * // => 'matches B'
+     *
+     * func({ 'a': '1', 'b': '2' });
+     * // => 'no match'
+     */
+    function cond(pairs) {
+      var length = pairs ? pairs.length : 0,
+          toIteratee = getIteratee();
+
+      pairs = !length ? [] : arrayMap(pairs, function(pair) {
+        if (typeof pair[1] != 'function') {
+          throw new TypeError(FUNC_ERROR_TEXT);
+        }
+        return [toIteratee(pair[0]), pair[1]];
+      });
+
+      return rest(function(args) {
+        var index = -1;
+        while (++index < length) {
+          var pair = pairs[index];
+          if (apply(pair[0], this, args)) {
+            return apply(pair[1], this, args);
+          }
+        }
+      });
+    }
+
+    /**
+     * Creates a function that invokes the predicate properties of `source` with
+     * the corresponding property values of a given object, returning `true` if
+     * all predicates return truthy, else `false`.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Object} source The object of property predicates to conform to.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney', 'age': 36 },
+     *   { 'user': 'fred',   'age': 40 }
+     * ];
+     *
+     * _.filter(users, _.conforms({ 'age': _.partial(_.gt, _, 38) }));
+     * // => [{ 'user': 'fred', 'age': 40 }]
+     */
+    function conforms(source) {
+      return baseConforms(baseClone(source, true));
+    }
+
+    /**
+     * Creates a function that returns `value`.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {*} value The value to return from the new function.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var object = { 'user': 'fred' };
+     * var getter = _.constant(object);
+     *
+     * getter() === object;
+     * // => true
+     */
+    function constant(value) {
+      return function() {
+        return value;
+      };
+    }
+
+    /**
+     * Creates a function that returns the result of invoking the given functions
+     * with the `this` binding of the created function, where each successive
+     * invocation is supplied the return value of the previous.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {...(Function|Function[])} [funcs] Functions to invoke.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * function square(n) {
+     *   return n * n;
+     * }
+     *
+     * var addSquare = _.flow(_.add, square);
+     * addSquare(1, 2);
+     * // => 9
+     */
+    var flow = createFlow();
+
+    /**
+     * This method is like `_.flow` except that it creates a function that
+     * invokes the given functions from right to left.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {...(Function|Function[])} [funcs] Functions to invoke.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * function square(n) {
+     *   return n * n;
+     * }
+     *
+     * var addSquare = _.flowRight(square, _.add);
+     * addSquare(1, 2);
+     * // => 9
+     */
+    var flowRight = createFlow(true);
+
+    /**
+     * This method returns the first argument given to it.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {*} value Any value.
+     * @returns {*} Returns `value`.
+     * @example
+     *
+     * var object = { 'user': 'fred' };
+     *
+     * _.identity(object) === object;
+     * // => true
+     */
+    function identity(value) {
+      return value;
+    }
+
+    /**
+     * Creates a function that invokes `func` with the arguments of the created
+     * function. If `func` is a property name the created callback returns the
+     * property value for a given element. If `func` is an object the created
+     * callback returns `true` for elements that contain the equivalent object
+     * properties, otherwise it returns `false`.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {*} [func=_.identity] The value to convert to a callback.
+     * @returns {Function} Returns the callback.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney', 'age': 36 },
+     *   { 'user': 'fred',   'age': 40 }
+     * ];
+     *
+     * // Create custom iteratee shorthands.
+     * _.iteratee = _.wrap(_.iteratee, function(callback, func) {
+     *   var p = /^(\S+)\s*([<>])\s*(\S+)$/.exec(func);
+     *   return !p ? callback(func) : function(object) {
+     *     return (p[2] == '>' ? object[p[1]] > p[3] : object[p[1]] < p[3]);
+     *   };
+     * });
+     *
+     * _.filter(users, 'age > 36');
+     * // => [{ 'user': 'fred', 'age': 40 }]
+     */
+    function iteratee(func) {
+      return baseIteratee(typeof func == 'function' ? func : baseClone(func, true));
+    }
+
+    /**
+     * Creates a function that performs a partial deep comparison between a given
+     * object and `source`, returning `true` if the given object has equivalent
+     * property values, else `false`. The created function is equivalent to
+     * `_.isMatch` with a `source` partially applied.
+     *
+     * **Note:** This method supports comparing the same values as `_.isEqual`.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Object} source The object of property values to match.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney', 'age': 36, 'active': true },
+     *   { 'user': 'fred',   'age': 40, 'active': false }
+     * ];
+     *
+     * _.filter(users, _.matches({ 'age': 40, 'active': false }));
+     * // => [{ 'user': 'fred', 'age': 40, 'active': false }]
+     */
+    function matches(source) {
+      return baseMatches(baseClone(source, true));
+    }
+
+    /**
+     * Creates a function that performs a partial deep comparison between the
+     * value at `path` of a given object to `srcValue`, returning `true` if the
+     * object value is equivalent, else `false`.
+     *
+     * **Note:** This method supports comparing the same values as `_.isEqual`.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Array|string} path The path of the property to get.
+     * @param {*} srcValue The value to match.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var users = [
+     *   { 'user': 'barney' },
+     *   { 'user': 'fred' }
+     * ];
+     *
+     * _.find(users, _.matchesProperty('user', 'fred'));
+     * // => { 'user': 'fred' }
+     */
+    function matchesProperty(path, srcValue) {
+      return baseMatchesProperty(path, baseClone(srcValue, true));
+    }
+
+    /**
+     * Creates a function that invokes the method at `path` of a given object.
+     * Any additional arguments are provided to the invoked method.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Array|string} path The path of the method to invoke.
+     * @param {...*} [args] The arguments to invoke the method with.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var objects = [
+     *   { 'a': { 'b': { 'c': _.constant(2) } } },
+     *   { 'a': { 'b': { 'c': _.constant(1) } } }
+     * ];
+     *
+     * _.map(objects, _.method('a.b.c'));
+     * // => [2, 1]
+     *
+     * _.invokeMap(_.sortBy(objects, _.method(['a', 'b', 'c'])), 'a.b.c');
+     * // => [1, 2]
+     */
+    var method = rest(function(path, args) {
+      return function(object) {
+        return baseInvoke(object, path, args);
+      };
+    });
+
+    /**
+     * The opposite of `_.method`; this method creates a function that invokes
+     * the method at a given path of `object`. Any additional arguments are
+     * provided to the invoked method.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Object} object The object to query.
+     * @param {...*} [args] The arguments to invoke the method with.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var array = _.times(3, _.constant),
+     *     object = { 'a': array, 'b': array, 'c': array };
+     *
+     * _.map(['a[2]', 'c[0]'], _.methodOf(object));
+     * // => [2, 0]
+     *
+     * _.map([['a', '2'], ['c', '0']], _.methodOf(object));
+     * // => [2, 0]
+     */
+    var methodOf = rest(function(object, args) {
+      return function(path) {
+        return baseInvoke(object, path, args);
+      };
+    });
+
+    /**
+     * Adds all own enumerable function properties of a source object to the
+     * destination object. If `object` is a function then methods are added to
+     * its prototype as well.
+     *
+     * **Note:** Use `_.runInContext` to create a pristine `lodash` function to
+     * avoid conflicts caused by modifying the original.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Function|Object} [object=lodash] The destination object.
+     * @param {Object} source The object of functions to add.
+     * @param {Object} [options] The options object.
+     * @param {boolean} [options.chain=true] Specify whether the functions added
+     *  are chainable.
+     * @returns {Function|Object} Returns `object`.
+     * @example
+     *
+     * function vowels(string) {
+     *   return _.filter(string, function(v) {
+     *     return /[aeiou]/i.test(v);
+     *   });
+     * }
+     *
+     * _.mixin({ 'vowels': vowels });
+     * _.vowels('fred');
+     * // => ['e']
+     *
+     * _('fred').vowels().value();
+     * // => ['e']
+     *
+     * _.mixin({ 'vowels': vowels }, { 'chain': false });
+     * _('fred').vowels();
+     * // => ['e']
+     */
+    function mixin(object, source, options) {
+      var props = keys(source),
+          methodNames = baseFunctions(source, props);
+
+      if (options == null &&
+          !(isObject(source) && (methodNames.length || !props.length))) {
+        options = source;
+        source = object;
+        object = this;
+        methodNames = baseFunctions(source, keys(source));
+      }
+      var chain = (isObject(options) && 'chain' in options) ? options.chain : true,
+          isFunc = isFunction(object);
+
+      arrayEach(methodNames, function(methodName) {
+        var func = source[methodName];
+        object[methodName] = func;
+        if (isFunc) {
+          object.prototype[methodName] = function() {
+            var chainAll = this.__chain__;
+            if (chain || chainAll) {
+              var result = object(this.__wrapped__),
+                  actions = result.__actions__ = copyArray(this.__actions__);
+
+              actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
+              result.__chain__ = chainAll;
+              return result;
+            }
+            return func.apply(object, arrayPush([this.value()], arguments));
+          };
+        }
+      });
+
+      return object;
+    }
+
+    /**
+     * Reverts the `_` variable to its previous value and returns a reference to
+     * the `lodash` function.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @returns {Function} Returns the `lodash` function.
+     * @example
+     *
+     * var lodash = _.noConflict();
+     */
+    function noConflict() {
+      if (root._ === this) {
+        root._ = oldDash;
+      }
+      return this;
+    }
+
+    /**
+     * A no-operation function that returns `undefined` regardless of the
+     * arguments it receives.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @example
+     *
+     * var object = { 'user': 'fred' };
+     *
+     * _.noop(object) === undefined;
+     * // => true
+     */
+    function noop() {
+      // No operation performed.
+    }
+
+    /**
+     * Creates a function that returns its nth argument.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {number} [n=0] The index of the argument to return.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var func = _.nthArg(1);
+     *
+     * func('a', 'b', 'c');
+     * // => 'b'
+     */
+    function nthArg(n) {
+      n = toInteger(n);
+      return function() {
+        return arguments[n];
+      };
+    }
+
+    /**
+     * Creates a function that invokes `iteratees` with the arguments provided
+     * to the created function and returns their results.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {...(Function|Function[])} iteratees The iteratees to invoke.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var func = _.over(Math.max, Math.min);
+     *
+     * func(1, 2, 3, 4);
+     * // => [4, 1]
+     */
+    var over = createOver(arrayMap);
+
+    /**
+     * Creates a function that checks if **all** of the `predicates` return
+     * truthy when invoked with the arguments provided to the created function.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {...(Function|Function[])} predicates The predicates to check.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var func = _.overEvery(Boolean, isFinite);
+     *
+     * func('1');
+     * // => true
+     *
+     * func(null);
+     * // => false
+     *
+     * func(NaN);
+     * // => false
+     */
+    var overEvery = createOver(arrayEvery);
+
+    /**
+     * Creates a function that checks if **any** of the `predicates` return
+     * truthy when invoked with the arguments provided to the created function.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {...(Function|Function[])} predicates The predicates to check.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var func = _.overSome(Boolean, isFinite);
+     *
+     * func('1');
+     * // => true
+     *
+     * func(null);
+     * // => true
+     *
+     * func(NaN);
+     * // => false
+     */
+    var overSome = createOver(arraySome);
+
+    /**
+     * Creates a function that returns the value at `path` of a given object.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Array|string} path The path of the property to get.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var objects = [
+     *   { 'a': { 'b': { 'c': 2 } } },
+     *   { 'a': { 'b': { 'c': 1 } } }
+     * ];
+     *
+     * _.map(objects, _.property('a.b.c'));
+     * // => [2, 1]
+     *
+     * _.map(_.sortBy(objects, _.property(['a', 'b', 'c'])), 'a.b.c');
+     * // => [1, 2]
+     */
+    function property(path) {
+      return isKey(path) ? baseProperty(path) : basePropertyDeep(path);
+    }
+
+    /**
+     * The opposite of `_.property`; this method creates a function that returns
+     * the value at a given path of `object`.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {Object} object The object to query.
+     * @returns {Function} Returns the new function.
+     * @example
+     *
+     * var array = [0, 1, 2],
+     *     object = { 'a': array, 'b': array, 'c': array };
+     *
+     * _.map(['a[2]', 'c[0]'], _.propertyOf(object));
+     * // => [2, 0]
+     *
+     * _.map([['a', '2'], ['c', '0']], _.propertyOf(object));
+     * // => [2, 0]
+     */
+    function propertyOf(object) {
+      return function(path) {
+        return object == null ? undefined : baseGet(object, path);
+      };
+    }
+
+    /**
+     * Creates an array of numbers (positive and/or negative) progressing from
+     * `start` up to, but not including, `end`. A step of `-1` is used if a negative
+     * `start` is specified without an `end` or `step`. If `end` is not specified
+     * it's set to `start` with `start` then set to `0`.
+     *
+     * **Note:** JavaScript follows the IEEE-754 standard for resolving
+     * floating-point values which can produce unexpected results.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {number} [start=0] The start of the range.
+     * @param {number} end The end of the range.
+     * @param {number} [step=1] The value to increment or decrement by.
+     * @returns {Array} Returns the new array of numbers.
+     * @example
+     *
+     * _.range(4);
+     * // => [0, 1, 2, 3]
+     *
+     * _.range(-4);
+     * // => [0, -1, -2, -3]
+     *
+     * _.range(1, 5);
+     * // => [1, 2, 3, 4]
+     *
+     * _.range(0, 20, 5);
+     * // => [0, 5, 10, 15]
+     *
+     * _.range(0, -4, -1);
+     * // => [0, -1, -2, -3]
+     *
+     * _.range(1, 4, 0);
+     * // => [1, 1, 1]
+     *
+     * _.range(0);
+     * // => []
+     */
+    var range = createRange();
+
+    /**
+     * This method is like `_.range` except that it populates values in
+     * descending order.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {number} [start=0] The start of the range.
+     * @param {number} end The end of the range.
+     * @param {number} [step=1] The value to increment or decrement by.
+     * @returns {Array} Returns the new array of numbers.
+     * @example
+     *
+     * _.rangeRight(4);
+     * // => [3, 2, 1, 0]
+     *
+     * _.rangeRight(-4);
+     * // => [-3, -2, -1, 0]
+     *
+     * _.rangeRight(1, 5);
+     * // => [4, 3, 2, 1]
+     *
+     * _.rangeRight(0, 20, 5);
+     * // => [15, 10, 5, 0]
+     *
+     * _.rangeRight(0, -4, -1);
+     * // => [-3, -2, -1, 0]
+     *
+     * _.rangeRight(1, 4, 0);
+     * // => [1, 1, 1]
+     *
+     * _.rangeRight(0);
+     * // => []
+     */
+    var rangeRight = createRange(true);
+
+    /**
+     * Invokes the iteratee `n` times, returning an array of the results of
+     * each invocation. The iteratee is invoked with one argument; (index).
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {number} n The number of times to invoke `iteratee`.
+     * @param {Function} [iteratee=_.identity] The function invoked per iteration.
+     * @returns {Array} Returns the array of results.
+     * @example
+     *
+     * _.times(3, String);
+     * // => ['0', '1', '2']
+     *
+     *  _.times(4, _.constant(true));
+     * // => [true, true, true, true]
+     */
+    function times(n, iteratee) {
+      n = toInteger(n);
+      if (n < 1 || n > MAX_SAFE_INTEGER) {
+        return [];
+      }
+      var index = MAX_ARRAY_LENGTH,
+          length = nativeMin(n, MAX_ARRAY_LENGTH);
+
+      iteratee = baseCastFunction(iteratee);
+      n -= MAX_ARRAY_LENGTH;
+
+      var result = baseTimes(length, iteratee);
+      while (++index < n) {
+        iteratee(index);
+      }
+      return result;
+    }
+
+    /**
+     * Converts `value` to a property path array.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {*} value The value to convert.
+     * @returns {Array} Returns the new property path array.
+     * @example
+     *
+     * _.toPath('a.b.c');
+     * // => ['a', 'b', 'c']
+     *
+     * _.toPath('a[0].b.c');
+     * // => ['a', '0', 'b', 'c']
+     *
+     * var path = ['a', 'b', 'c'],
+     *     newPath = _.toPath(path);
+     *
+     * console.log(newPath);
+     * // => ['a', 'b', 'c']
+     *
+     * console.log(path === newPath);
+     * // => false
+     */
+    function toPath(value) {
+      return isArray(value) ? arrayMap(value, String) : stringToPath(value);
+    }
+
+    /**
+     * Generates a unique ID. If `prefix` is given the ID is appended to it.
+     *
+     * @static
+     * @memberOf _
+     * @category Util
+     * @param {string} [prefix=''] The value to prefix the ID with.
+     * @returns {string} Returns the unique ID.
+     * @example
+     *
+     * _.uniqueId('contact_');
+     * // => 'contact_104'
+     *
+     * _.uniqueId();
+     * // => '105'
+     */
+    function uniqueId(prefix) {
+      var id = ++idCounter;
+      return toString(prefix) + id;
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * Adds two numbers.
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {number} augend The first number in an addition.
+     * @param {number} addend The second number in an addition.
+     * @returns {number} Returns the total.
+     * @example
+     *
+     * _.add(6, 4);
+     * // => 10
+     */
+    function add(augend, addend) {
+      var result;
+      if (augend === undefined && addend === undefined) {
+        return 0;
+      }
+      if (augend !== undefined) {
+        result = augend;
+      }
+      if (addend !== undefined) {
+        result = result === undefined ? addend : (result + addend);
+      }
+      return result;
+    }
+
+    /**
+     * Computes `number` rounded up to `precision`.
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {number} number The number to round up.
+     * @param {number} [precision=0] The precision to round up to.
+     * @returns {number} Returns the rounded up number.
+     * @example
+     *
+     * _.ceil(4.006);
+     * // => 5
+     *
+     * _.ceil(6.004, 2);
+     * // => 6.01
+     *
+     * _.ceil(6040, -2);
+     * // => 6100
+     */
+    var ceil = createRound('ceil');
+
+    /**
+     * Computes `number` rounded down to `precision`.
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {number} number The number to round down.
+     * @param {number} [precision=0] The precision to round down to.
+     * @returns {number} Returns the rounded down number.
+     * @example
+     *
+     * _.floor(4.006);
+     * // => 4
+     *
+     * _.floor(0.046, 2);
+     * // => 0.04
+     *
+     * _.floor(4060, -2);
+     * // => 4000
+     */
+    var floor = createRound('floor');
+
+    /**
+     * Computes the maximum value of `array`. If `array` is empty or falsey
+     * `undefined` is returned.
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {Array} array The array to iterate over.
+     * @returns {*} Returns the maximum value.
+     * @example
+     *
+     * _.max([4, 2, 8, 6]);
+     * // => 8
+     *
+     * _.max([]);
+     * // => undefined
+     */
+    function max(array) {
+      return (array && array.length)
+        ? baseExtremum(array, identity, gt)
+        : undefined;
+    }
+
+    /**
+     * This method is like `_.max` except that it accepts `iteratee` which is
+     * invoked for each element in `array` to generate the criterion by which
+     * the value is ranked. The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {Array} array The array to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {*} Returns the maximum value.
+     * @example
+     *
+     * var objects = [{ 'n': 1 }, { 'n': 2 }];
+     *
+     * _.maxBy(objects, function(o) { return o.n; });
+     * // => { 'n': 2 }
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.maxBy(objects, 'n');
+     * // => { 'n': 2 }
+     */
+    function maxBy(array, iteratee) {
+      return (array && array.length)
+        ? baseExtremum(array, getIteratee(iteratee), gt)
+        : undefined;
+    }
+
+    /**
+     * Computes the mean of the values in `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {Array} array The array to iterate over.
+     * @returns {number} Returns the mean.
+     * @example
+     *
+     * _.mean([4, 2, 8, 6]);
+     * // => 5
+     */
+    function mean(array) {
+      return sum(array) / (array ? array.length : 0);
+    }
+
+    /**
+     * Computes the minimum value of `array`. If `array` is empty or falsey
+     * `undefined` is returned.
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {Array} array The array to iterate over.
+     * @returns {*} Returns the minimum value.
+     * @example
+     *
+     * _.min([4, 2, 8, 6]);
+     * // => 2
+     *
+     * _.min([]);
+     * // => undefined
+     */
+    function min(array) {
+      return (array && array.length)
+        ? baseExtremum(array, identity, lt)
+        : undefined;
+    }
+
+    /**
+     * This method is like `_.min` except that it accepts `iteratee` which is
+     * invoked for each element in `array` to generate the criterion by which
+     * the value is ranked. The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {Array} array The array to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {*} Returns the minimum value.
+     * @example
+     *
+     * var objects = [{ 'n': 1 }, { 'n': 2 }];
+     *
+     * _.minBy(objects, function(o) { return o.n; });
+     * // => { 'n': 1 }
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.minBy(objects, 'n');
+     * // => { 'n': 1 }
+     */
+    function minBy(array, iteratee) {
+      return (array && array.length)
+        ? baseExtremum(array, getIteratee(iteratee), lt)
+        : undefined;
+    }
+
+    /**
+     * Computes `number` rounded to `precision`.
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {number} number The number to round.
+     * @param {number} [precision=0] The precision to round to.
+     * @returns {number} Returns the rounded number.
+     * @example
+     *
+     * _.round(4.006);
+     * // => 4
+     *
+     * _.round(4.006, 2);
+     * // => 4.01
+     *
+     * _.round(4060, -2);
+     * // => 4100
+     */
+    var round = createRound('round');
+
+    /**
+     * Subtract two numbers.
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {number} minuend The first number in a subtraction.
+     * @param {number} subtrahend The second number in a subtraction.
+     * @returns {number} Returns the difference.
+     * @example
+     *
+     * _.subtract(6, 4);
+     * // => 2
+     */
+    function subtract(minuend, subtrahend) {
+      var result;
+      if (minuend === undefined && subtrahend === undefined) {
+        return 0;
+      }
+      if (minuend !== undefined) {
+        result = minuend;
+      }
+      if (subtrahend !== undefined) {
+        result = result === undefined ? subtrahend : (result - subtrahend);
+      }
+      return result;
+    }
+
+    /**
+     * Computes the sum of the values in `array`.
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {Array} array The array to iterate over.
+     * @returns {number} Returns the sum.
+     * @example
+     *
+     * _.sum([4, 2, 8, 6]);
+     * // => 20
+     */
+    function sum(array) {
+      return (array && array.length)
+        ? baseSum(array, identity)
+        : 0;
+    }
+
+    /**
+     * This method is like `_.sum` except that it accepts `iteratee` which is
+     * invoked for each element in `array` to generate the value to be summed.
+     * The iteratee is invoked with one argument: (value).
+     *
+     * @static
+     * @memberOf _
+     * @category Math
+     * @param {Array} array The array to iterate over.
+     * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.
+     * @returns {number} Returns the sum.
+     * @example
+     *
+     * var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];
+     *
+     * _.sumBy(objects, function(o) { return o.n; });
+     * // => 20
+     *
+     * // The `_.property` iteratee shorthand.
+     * _.sumBy(objects, 'n');
+     * // => 20
+     */
+    function sumBy(array, iteratee) {
+      return (array && array.length)
+        ? baseSum(array, getIteratee(iteratee))
+        : 0;
+    }
+
+    /*------------------------------------------------------------------------*/
+
+    // Ensure wrappers are instances of `baseLodash`.
+    lodash.prototype = baseLodash.prototype;
+    lodash.prototype.constructor = lodash;
+
+    LodashWrapper.prototype = baseCreate(baseLodash.prototype);
+    LodashWrapper.prototype.constructor = LodashWrapper;
+
+    LazyWrapper.prototype = baseCreate(baseLodash.prototype);
+    LazyWrapper.prototype.constructor = LazyWrapper;
+
+    // Avoid inheriting from `Object.prototype` when possible.
+    Hash.prototype = nativeCreate ? nativeCreate(null) : objectProto;
+
+    // Add functions to the `MapCache`.
+    MapCache.prototype.clear = mapClear;
+    MapCache.prototype['delete'] = mapDelete;
+    MapCache.prototype.get = mapGet;
+    MapCache.prototype.has = mapHas;
+    MapCache.prototype.set = mapSet;
+
+    // Add functions to the `SetCache`.
+    SetCache.prototype.push = cachePush;
+
+    // Add functions to the `Stack` cache.
+    Stack.prototype.clear = stackClear;
+    Stack.prototype['delete'] = stackDelete;
+    Stack.prototype.get = stackGet;
+    Stack.prototype.has = stackHas;
+    Stack.prototype.set = stackSet;
+
+    // Assign cache to `_.memoize`.
+    memoize.Cache = MapCache;
+
+    // Add functions that return wrapped values when chaining.
+    lodash.after = after;
+    lodash.ary = ary;
+    lodash.assign = assign;
+    lodash.assignIn = assignIn;
+    lodash.assignInWith = assignInWith;
+    lodash.assignWith = assignWith;
+    lodash.at = at;
+    lodash.before = before;
+    lodash.bind = bind;
+    lodash.bindAll = bindAll;
+    lodash.bindKey = bindKey;
+    lodash.castArray = castArray;
+    lodash.chain = chain;
+    lodash.chunk = chunk;
+    lodash.compact = compact;
+    lodash.concat = concat;
+    lodash.cond = cond;
+    lodash.conforms = conforms;
+    lodash.constant = constant;
+    lodash.countBy = countBy;
+    lodash.create = create;
+    lodash.curry = curry;
+    lodash.curryRight = curryRight;
+    lodash.debounce = debounce;
+    lodash.defaults = defaults;
+    lodash.defaultsDeep = defaultsDeep;
+    lodash.defer = defer;
+    lodash.delay = delay;
+    lodash.difference = difference;
+    lodash.differenceBy = differenceBy;
+    lodash.differenceWith = differenceWith;
+    lodash.drop = drop;
+    lodash.dropRight = dropRight;
+    lodash.dropRightWhile = dropRightWhile;
+    lodash.dropWhile = dropWhile;
+    lodash.fill = fill;
+    lodash.filter = filter;
+    lodash.flatMap = flatMap;
+    lodash.flatten = flatten;
+    lodash.flattenDeep = flattenDeep;
+    lodash.flattenDepth = flattenDepth;
+    lodash.flip = flip;
+    lodash.flow = flow;
+    lodash.flowRight = flowRight;
+    lodash.fromPairs = fromPairs;
+    lodash.functions = functions;
+    lodash.functionsIn = functionsIn;
+    lodash.groupBy = groupBy;
+    lodash.initial = initial;
+    lodash.intersection = intersection;
+    lodash.intersectionBy = intersectionBy;
+    lodash.intersectionWith = intersectionWith;
+    lodash.invert = invert;
+    lodash.invertBy = invertBy;
+    lodash.invokeMap = invokeMap;
+    lodash.iteratee = iteratee;
+    lodash.keyBy = keyBy;
+    lodash.keys = keys;
+    lodash.keysIn = keysIn;
+    lodash.map = map;
+    lodash.mapKeys = mapKeys;
+    lodash.mapValues = mapValues;
+    lodash.matches = matches;
+    lodash.matchesProperty = matchesProperty;
+    lodash.memoize = memoize;
+    lodash.merge = merge;
+    lodash.mergeWith = mergeWith;
+    lodash.method = method;
+    lodash.methodOf = methodOf;
+    lodash.mixin = mixin;
+    lodash.negate = negate;
+    lodash.nthArg = nthArg;
+    lodash.omit = omit;
+    lodash.omitBy = omitBy;
+    lodash.once = once;
+    lodash.orderBy = orderBy;
+    lodash.over = over;
+    lodash.overArgs = overArgs;
+    lodash.overEvery = overEvery;
+    lodash.overSome = overSome;
+    lodash.partial = partial;
+    lodash.partialRight = partialRight;
+    lodash.partition = partition;
+    lodash.pick = pick;
+    lodash.pickBy = pickBy;
+    lodash.property = property;
+    lodash.propertyOf = propertyOf;
+    lodash.pull = pull;
+    lodash.pullAll = pullAll;
+    lodash.pullAllBy = pullAllBy;
+    lodash.pullAllWith = pullAllWith;
+    lodash.pullAt = pullAt;
+    lodash.range = range;
+    lodash.rangeRight = rangeRight;
+    lodash.rearg = rearg;
+    lodash.reject = reject;
+    lodash.remove = remove;
+    lodash.rest = rest;
+    lodash.reverse = reverse;
+    lodash.sampleSize = sampleSize;
+    lodash.set = set;
+    lodash.setWith = setWith;
+    lodash.shuffle = shuffle;
+    lodash.slice = slice;
+    lodash.sortBy = sortBy;
+    lodash.sortedUniq = sortedUniq;
+    lodash.sortedUniqBy = sortedUniqBy;
+    lodash.split = split;
+    lodash.spread = spread;
+    lodash.tail = tail;
+    lodash.take = take;
+    lodash.takeRight = takeRight;
+    lodash.takeRightWhile = takeRightWhile;
+    lodash.takeWhile = takeWhile;
+    lodash.tap = tap;
+    lodash.throttle = throttle;
+    lodash.thru = thru;
+    lodash.toArray = toArray;
+    lodash.toPairs = toPairs;
+    lodash.toPairsIn = toPairsIn;
+    lodash.toPath = toPath;
+    lodash.toPlainObject = toPlainObject;
+    lodash.transform = transform;
+    lodash.unary = unary;
+    lodash.union = union;
+    lodash.unionBy = unionBy;
+    lodash.unionWith = unionWith;
+    lodash.uniq = uniq;
+    lodash.uniqBy = uniqBy;
+    lodash.uniqWith = uniqWith;
+    lodash.unset = unset;
+    lodash.unzip = unzip;
+    lodash.unzipWith = unzipWith;
+    lodash.update = update;
+    lodash.updateWith = updateWith;
+    lodash.values = values;
+    lodash.valuesIn = valuesIn;
+    lodash.without = without;
+    lodash.words = words;
+    lodash.wrap = wrap;
+    lodash.xor = xor;
+    lodash.xorBy = xorBy;
+    lodash.xorWith = xorWith;
+    lodash.zip = zip;
+    lodash.zipObject = zipObject;
+    lodash.zipObjectDeep = zipObjectDeep;
+    lodash.zipWith = zipWith;
+
+    // Add aliases.
+    lodash.extend = assignIn;
+    lodash.extendWith = assignInWith;
+
+    // Add functions to `lodash.prototype`.
+    mixin(lodash, lodash);
+
+    /*------------------------------------------------------------------------*/
+
+    // Add functions that return unwrapped values when chaining.
+    lodash.add = add;
+    lodash.attempt = attempt;
+    lodash.camelCase = camelCase;
+    lodash.capitalize = capitalize;
+    lodash.ceil = ceil;
+    lodash.clamp = clamp;
+    lodash.clone = clone;
+    lodash.cloneDeep = cloneDeep;
+    lodash.cloneDeepWith = cloneDeepWith;
+    lodash.cloneWith = cloneWith;
+    lodash.deburr = deburr;
+    lodash.endsWith = endsWith;
+    lodash.eq = eq;
+    lodash.escape = escape;
+    lodash.escapeRegExp = escapeRegExp;
+    lodash.every = every;
+    lodash.find = find;
+    lodash.findIndex = findIndex;
+    lodash.findKey = findKey;
+    lodash.findLast = findLast;
+    lodash.findLastIndex = findLastIndex;
+    lodash.findLastKey = findLastKey;
+    lodash.floor = floor;
+    lodash.forEach = forEach;
+    lodash.forEachRight = forEachRight;
+    lodash.forIn = forIn;
+    lodash.forInRight = forInRight;
+    lodash.forOwn = forOwn;
+    lodash.forOwnRight = forOwnRight;
+    lodash.get = get;
+    lodash.gt = gt;
+    lodash.gte = gte;
+    lodash.has = has;
+    lodash.hasIn = hasIn;
+    lodash.head = head;
+    lodash.identity = identity;
+    lodash.includes = includes;
+    lodash.indexOf = indexOf;
+    lodash.inRange = inRange;
+    lodash.invoke = invoke;
+    lodash.isArguments = isArguments;
+    lodash.isArray = isArray;
+    lodash.isArrayBuffer = isArrayBuffer;
+    lodash.isArrayLike = isArrayLike;
+    lodash.isArrayLikeObject = isArrayLikeObject;
+    lodash.isBoolean = isBoolean;
+    lodash.isBuffer = isBuffer;
+    lodash.isDate = isDate;
+    lodash.isElement = isElement;
+    lodash.isEmpty = isEmpty;
+    lodash.isEqual = isEqual;
+    lodash.isEqualWith = isEqualWith;
+    lodash.isError = isError;
+    lodash.isFinite = isFinite;
+    lodash.isFunction = isFunction;
+    lodash.isInteger = isInteger;
+    lodash.isLength = isLength;
+    lodash.isMap = isMap;
+    lodash.isMatch = isMatch;
+    lodash.isMatchWith = isMatchWith;
+    lodash.isNaN = isNaN;
+    lodash.isNative = isNative;
+    lodash.isNil = isNil;
+    lodash.isNull = isNull;
+    lodash.isNumber = isNumber;
+    lodash.isObject = isObject;
+    lodash.isObjectLike = isObjectLike;
+    lodash.isPlainObject = isPlainObject;
+    lodash.isRegExp = isRegExp;
+    lodash.isSafeInteger = isSafeInteger;
+    lodash.isSet = isSet;
+    lodash.isString = isString;
+    lodash.isSymbol = isSymbol;
+    lodash.isTypedArray = isTypedArray;
+    lodash.isUndefined = isUndefined;
+    lodash.isWeakMap = isWeakMap;
+    lodash.isWeakSet = isWeakSet;
+    lodash.join = join;
+    lodash.kebabCase = kebabCase;
+    lodash.last = last;
+    lodash.lastIndexOf = lastIndexOf;
+    lodash.lowerCase = lowerCase;
+    lodash.lowerFirst = lowerFirst;
+    lodash.lt = lt;
+    lodash.lte = lte;
+    lodash.max = max;
+    lodash.maxBy = maxBy;
+    lodash.mean = mean;
+    lodash.min = min;
+    lodash.minBy = minBy;
+    lodash.noConflict = noConflict;
+    lodash.noop = noop;
+    lodash.now = now;
+    lodash.pad = pad;
+    lodash.padEnd = padEnd;
+    lodash.padStart = padStart;
+    lodash.parseInt = parseInt;
+    lodash.random = random;
+    lodash.reduce = reduce;
+    lodash.reduceRight = reduceRight;
+    lodash.repeat = repeat;
+    lodash.replace = replace;
+    lodash.result = result;
+    lodash.round = round;
+    lodash.runInContext = runInContext;
+    lodash.sample = sample;
+    lodash.size = size;
+    lodash.snakeCase = snakeCase;
+    lodash.some = some;
+    lodash.sortedIndex = sortedIndex;
+    lodash.sortedIndexBy = sortedIndexBy;
+    lodash.sortedIndexOf = sortedIndexOf;
+    lodash.sortedLastIndex = sortedLastIndex;
+    lodash.sortedLastIndexBy = sortedLastIndexBy;
+    lodash.sortedLastIndexOf = sortedLastIndexOf;
+    lodash.startCase = startCase;
+    lodash.startsWith = startsWith;
+    lodash.subtract = subtract;
+    lodash.sum = sum;
+    lodash.sumBy = sumBy;
+    lodash.template = template;
+    lodash.times = times;
+    lodash.toInteger = toInteger;
+    lodash.toLength = toLength;
+    lodash.toLower = toLower;
+    lodash.toNumber = toNumber;
+    lodash.toSafeInteger = toSafeInteger;
+    lodash.toString = toString;
+    lodash.toUpper = toUpper;
+    lodash.trim = trim;
+    lodash.trimEnd = trimEnd;
+    lodash.trimStart = trimStart;
+    lodash.truncate = truncate;
+    lodash.unescape = unescape;
+    lodash.uniqueId = uniqueId;
+    lodash.upperCase = upperCase;
+    lodash.upperFirst = upperFirst;
+
+    // Add aliases.
+    lodash.each = forEach;
+    lodash.eachRight = forEachRight;
+    lodash.first = head;
+
+    mixin(lodash, (function() {
+      var source = {};
+      baseForOwn(lodash, function(func, methodName) {
+        if (!hasOwnProperty.call(lodash.prototype, methodName)) {
+          source[methodName] = func;
+        }
+      });
+      return source;
+    }()), { 'chain': false });
+
+    /*------------------------------------------------------------------------*/
+
+    /**
+     * The semantic version number.
+     *
+     * @static
+     * @memberOf _
+     * @type {string}
+     */
+    lodash.VERSION = VERSION;
+
+    // Assign default placeholders.
+    arrayEach(['bind', 'bindKey', 'curry', 'curryRight', 'partial', 'partialRight'], function(methodName) {
+      lodash[methodName].placeholder = lodash;
+    });
+
+    // Add `LazyWrapper` methods for `_.drop` and `_.take` variants.
+    arrayEach(['drop', 'take'], function(methodName, index) {
+      LazyWrapper.prototype[methodName] = function(n) {
+        var filtered = this.__filtered__;
+        if (filtered && !index) {
+          return new LazyWrapper(this);
+        }
+        n = n === undefined ? 1 : nativeMax(toInteger(n), 0);
+
+        var result = this.clone();
+        if (filtered) {
+          result.__takeCount__ = nativeMin(n, result.__takeCount__);
+        } else {
+          result.__views__.push({
+            'size': nativeMin(n, MAX_ARRAY_LENGTH),
+            'type': methodName + (result.__dir__ < 0 ? 'Right' : '')
+          });
+        }
+        return result;
+      };
+
+      LazyWrapper.prototype[methodName + 'Right'] = function(n) {
+        return this.reverse()[methodName](n).reverse();
+      };
+    });
+
+    // Add `LazyWrapper` methods that accept an `iteratee` value.
+    arrayEach(['filter', 'map', 'takeWhile'], function(methodName, index) {
+      var type = index + 1,
+          isFilter = type == LAZY_FILTER_FLAG || type == LAZY_WHILE_FLAG;
+
+      LazyWrapper.prototype[methodName] = function(iteratee) {
+        var result = this.clone();
+        result.__iteratees__.push({
+          'iteratee': getIteratee(iteratee, 3),
+          'type': type
+        });
+        result.__filtered__ = result.__filtered__ || isFilter;
+        return result;
+      };
+    });
+
+    // Add `LazyWrapper` methods for `_.head` and `_.last`.
+    arrayEach(['head', 'last'], function(methodName, index) {
+      var takeName = 'take' + (index ? 'Right' : '');
+
+      LazyWrapper.prototype[methodName] = function() {
+        return this[takeName](1).value()[0];
+      };
+    });
+
+    // Add `LazyWrapper` methods for `_.initial` and `_.tail`.
+    arrayEach(['initial', 'tail'], function(methodName, index) {
+      var dropName = 'drop' + (index ? '' : 'Right');
+
+      LazyWrapper.prototype[methodName] = function() {
+        return this.__filtered__ ? new LazyWrapper(this) : this[dropName](1);
+      };
+    });
+
+    LazyWrapper.prototype.compact = function() {
+      return this.filter(identity);
+    };
+
+    LazyWrapper.prototype.find = function(predicate) {
+      return this.filter(predicate).head();
+    };
+
+    LazyWrapper.prototype.findLast = function(predicate) {
+      return this.reverse().find(predicate);
+    };
+
+    LazyWrapper.prototype.invokeMap = rest(function(path, args) {
+      if (typeof path == 'function') {
+        return new LazyWrapper(this);
+      }
+      return this.map(function(value) {
+        return baseInvoke(value, path, args);
+      });
+    });
+
+    LazyWrapper.prototype.reject = function(predicate) {
+      predicate = getIteratee(predicate, 3);
+      return this.filter(function(value) {
+        return !predicate(value);
+      });
+    };
+
+    LazyWrapper.prototype.slice = function(start, end) {
+      start = toInteger(start);
+
+      var result = this;
+      if (result.__filtered__ && (start > 0 || end < 0)) {
+        return new LazyWrapper(result);
+      }
+      if (start < 0) {
+        result = result.takeRight(-start);
+      } else if (start) {
+        result = result.drop(start);
+      }
+      if (end !== undefined) {
+        end = toInteger(end);
+        result = end < 0 ? result.dropRight(-end) : result.take(end - start);
+      }
+      return result;
+    };
+
+    LazyWrapper.prototype.takeRightWhile = function(predicate) {
+      return this.reverse().takeWhile(predicate).reverse();
+    };
+
+    LazyWrapper.prototype.toArray = function() {
+      return this.take(MAX_ARRAY_LENGTH);
+    };
+
+    // Add `LazyWrapper` methods to `lodash.prototype`.
+    baseForOwn(LazyWrapper.prototype, function(func, methodName) {
+      var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),
+          isTaker = /^(?:head|last)$/.test(methodName),
+          lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],
+          retUnwrapped = isTaker || /^find/.test(methodName);
+
+      if (!lodashFunc) {
+        return;
+      }
+      lodash.prototype[methodName] = function() {
+        var value = this.__wrapped__,
+            args = isTaker ? [1] : arguments,
+            isLazy = value instanceof LazyWrapper,
+            iteratee = args[0],
+            useLazy = isLazy || isArray(value);
+
+        var interceptor = function(value) {
+          var result = lodashFunc.apply(lodash, arrayPush([value], args));
+          return (isTaker && chainAll) ? result[0] : result;
+        };
+
+        if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {
+          // Avoid lazy use if the iteratee has a "length" value other than `1`.
+          isLazy = useLazy = false;
+        }
+        var chainAll = this.__chain__,
+            isHybrid = !!this.__actions__.length,
+            isUnwrapped = retUnwrapped && !chainAll,
+            onlyLazy = isLazy && !isHybrid;
+
+        if (!retUnwrapped && useLazy) {
+          value = onlyLazy ? value : new LazyWrapper(this);
+          var result = func.apply(value, args);
+          result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined });
+          return new LodashWrapper(result, chainAll);
+        }
+        if (isUnwrapped && onlyLazy) {
+          return func.apply(this, args);
+        }
+        result = this.thru(interceptor);
+        return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;
+      };
+    });
+
+    // Add `Array` and `String` methods to `lodash.prototype`.
+    arrayEach(['pop', 'push', 'shift', 'sort', 'splice', 'unshift'], function(methodName) {
+      var func = arrayProto[methodName],
+          chainName = /^(?:push|sort|unshift)$/.test(methodName) ? 'tap' : 'thru',
+          retUnwrapped = /^(?:pop|shift)$/.test(methodName);
+
+      lodash.prototype[methodName] = function() {
+        var args = arguments;
+        if (retUnwrapped && !this.__chain__) {
+          return func.apply(this.value(), args);
+        }
+        return this[chainName](function(value) {
+          return func.apply(value, args);
+        });
+      };
+    });
+
+    // Map minified function names to their real names.
+    baseForOwn(LazyWrapper.prototype, function(func, methodName) {
+      var lodashFunc = lodash[methodName];
+      if (lodashFunc) {
+        var key = (lodashFunc.name + ''),
+            names = realNames[key] || (realNames[key] = []);
+
+        names.push({ 'name': methodName, 'func': lodashFunc });
+      }
+    });
+
+    realNames[createHybridWrapper(undefined, BIND_KEY_FLAG).name] = [{
+      'name': 'wrapper',
+      'func': undefined
+    }];
+
+    // Add functions to the lazy wrapper.
+    LazyWrapper.prototype.clone = lazyClone;
+    LazyWrapper.prototype.reverse = lazyReverse;
+    LazyWrapper.prototype.value = lazyValue;
+
+    // Add chaining functions to the `lodash` wrapper.
+    lodash.prototype.at = wrapperAt;
+    lodash.prototype.chain = wrapperChain;
+    lodash.prototype.commit = wrapperCommit;
+    lodash.prototype.flatMap = wrapperFlatMap;
+    lodash.prototype.next = wrapperNext;
+    lodash.prototype.plant = wrapperPlant;
+    lodash.prototype.reverse = wrapperReverse;
+    lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;
+
+    if (iteratorSymbol) {
+      lodash.prototype[iteratorSymbol] = wrapperToIterator;
+    }
+    return lodash;
+  }
+
+  /*--------------------------------------------------------------------------*/
+
+  // Export lodash.
+  var _ = runInContext();
+
+  // Expose lodash on the free variable `window` or `self` when available. This
+  // prevents errors in cases where lodash is loaded by a script tag in the presence
+  // of an AMD loader. See http://requirejs.org/docs/errors.html#mismatch for more details.
+  (freeWindow || freeSelf || {})._ = _;
+
+  // Some AMD build optimizers like r.js check for condition patterns like the following:
+  if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
+    // Define as an anonymous module so, through path mapping, it can be
+    // referenced as the "underscore" module.
+    define(function() {
+      return _;
+    });
+  }
+  // Check for `exports` after `define` in case a build optimizer adds an `exports` object.
+  else if (freeExports && freeModule) {
+    // Export for Node.js.
+    if (moduleExports) {
+      (freeModule.exports = _)._ = _;
+    }
+    // Export for CommonJS support.
+    freeExports._ = _;
+  }
+  else {
+    // Export to the global object.
+    root._ = _;
+  }
+}.call(this));
diff --git a/2016/assets/js/lodash.min.js b/2016/assets/js/lodash.min.js
new file mode 100644 (file)
index 0000000..3d4d741
--- /dev/null
@@ -0,0 +1,85 @@
+/*@licstart  The following is the entire license notice for the
+JavaScript code in this page.
+
+Copyright 2012-2015 The Dojo Foundation
+Based on Underscore.js 1.7.0,
+Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWAR
+
+@licend  The above is the entire license notice
+for the JavaScript code in this page.
+*/
+;(function(){function n(n,t,e){e=(e||0)-1;for(var r=n?n.length:0;++e<r;)if(n[e]===t)return e;return-1}function t(t,e){var r=typeof e;if(t=t.l,"boolean"==r||null==e)return t[e]?0:-1;"number"!=r&&"string"!=r&&(r="object");var u="number"==r?e:b+e;return t=(t=t[r])&&t[u],"object"==r?t&&-1<n(t,e)?0:-1:t?0:-1}function e(n){var t=this.l,e=typeof n;if("boolean"==e||null==n)t[n]=true;else{"number"!=e&&"string"!=e&&(e="object");var r="number"==e?n:b+n,t=t[e]||(t[e]={});"object"==e?(t[r]||(t[r]=[])).push(n):t[r]=true
+}}function r(n){return n.charCodeAt(0)}function u(n,t){for(var e=n.m,r=t.m,u=-1,o=e.length;++u<o;){var a=e[u],i=r[u];if(a!==i){if(a>i||typeof a=="undefined")return 1;if(a<i||typeof i=="undefined")return-1}}return n.n-t.n}function o(n){var t=-1,r=n.length,u=n[0],o=n[r/2|0],a=n[r-1];if(u&&typeof u=="object"&&o&&typeof o=="object"&&a&&typeof a=="object")return false;for(u=l(),u["false"]=u["null"]=u["true"]=u.undefined=false,o=l(),o.k=n,o.l=u,o.push=e;++t<r;)o.push(n[t]);return o}function a(n){return"\\"+Y[n]
+}function i(){return v.pop()||[]}function l(){return y.pop()||{k:null,l:null,m:null,"false":false,n:0,"null":false,number:null,object:null,push:null,string:null,"true":false,undefined:false,o:null}}function f(n){return typeof n.toString!="function"&&typeof(n+"")=="string"}function c(n){n.length=0,v.length<w&&v.push(n)}function p(n){var t=n.l;t&&p(t),n.k=n.l=n.m=n.object=n.number=n.string=n.o=null,y.length<w&&y.push(n)}function s(n,t,e){t||(t=0),typeof e=="undefined"&&(e=n?n.length:0);var r=-1;e=e-t||0;for(var u=Array(0>e?0:e);++r<e;)u[r]=n[t+r];
+return u}function g(e){function v(n){return n&&typeof n=="object"&&!qe(n)&&we.call(n,"__wrapped__")?n:new y(n)}function y(n,t){this.__chain__=!!t,this.__wrapped__=n}function w(n){function t(){if(r){var n=s(r);je.apply(n,arguments)}if(this instanceof t){var o=nt(e.prototype),n=e.apply(o,n||arguments);return xt(n)?n:o}return e.apply(u,n||arguments)}var e=n[0],r=n[2],u=n[4];return ze(t,n),t}function Y(n,t,e,r,u){if(e){var o=e(n);if(typeof o!="undefined")return o}if(!xt(n))return n;var a=he.call(n);if(!V[a]||!Le.nodeClass&&f(n))return n;
+var l=Te[a];switch(a){case L:case z:return new l(+n);case W:case M:return new l(n);case J:return o=l(n.source,S.exec(n)),o.lastIndex=n.lastIndex,o}if(a=qe(n),t){var p=!r;r||(r=i()),u||(u=i());for(var g=r.length;g--;)if(r[g]==n)return u[g];o=a?l(n.length):{}}else o=a?s(n):Ye({},n);return a&&(we.call(n,"index")&&(o.index=n.index),we.call(n,"input")&&(o.input=n.input)),t?(r.push(n),u.push(o),(a?Xe:tr)(n,function(n,a){o[a]=Y(n,t,e,r,u)}),p&&(c(r),c(u)),o):o}function nt(n){return xt(n)?Se(n):{}}function tt(n,t,e){if(typeof n!="function")return Ht;
+if(typeof t=="undefined"||!("prototype"in n))return n;var r=n.__bindData__;if(typeof r=="undefined"&&(Le.funcNames&&(r=!n.name),r=r||!Le.funcDecomp,!r)){var u=be.call(n);Le.funcNames||(r=!A.test(u)),r||(r=B.test(u),ze(n,r))}if(false===r||true!==r&&1&r[1])return n;switch(e){case 1:return function(e){return n.call(t,e)};case 2:return function(e,r){return n.call(t,e,r)};case 3:return function(e,r,u){return n.call(t,e,r,u)};case 4:return function(e,r,u,o){return n.call(t,e,r,u,o)}}return Mt(n,t)}function et(n){function t(){var n=l?a:this;
+if(u){var h=s(u);je.apply(h,arguments)}return(o||c)&&(h||(h=s(arguments)),o&&je.apply(h,o),c&&h.length<i)?(r|=16,et([e,p?r:-4&r,h,null,a,i])):(h||(h=arguments),f&&(e=n[g]),this instanceof t?(n=nt(e.prototype),h=e.apply(n,h),xt(h)?h:n):e.apply(n,h))}var e=n[0],r=n[1],u=n[2],o=n[3],a=n[4],i=n[5],l=1&r,f=2&r,c=4&r,p=8&r,g=e;return ze(t,n),t}function rt(e,r){var u=-1,a=ht(),i=e?e.length:0,l=i>=_&&a===n,f=[];if(l){var c=o(r);c?(a=t,r=c):l=false}for(;++u<i;)c=e[u],0>a(r,c)&&f.push(c);return l&&p(r),f}function ot(n,t,e,r){r=(r||0)-1;
+for(var u=n?n.length:0,o=[];++r<u;){var a=n[r];if(a&&typeof a=="object"&&typeof a.length=="number"&&(qe(a)||dt(a))){t||(a=ot(a,t,e));var i=-1,l=a.length,f=o.length;for(o.length+=l;++i<l;)o[f++]=a[i]}else e||o.push(a)}return o}function at(n,t,e,r,u,o){if(e){var a=e(n,t);if(typeof a!="undefined")return!!a}if(n===t)return 0!==n||1/n==1/t;if(n===n&&!(n&&X[typeof n]||t&&X[typeof t]))return false;if(null==n||null==t)return n===t;var l=he.call(n),p=he.call(t);if(l==T&&(l=G),p==T&&(p=G),l!=p)return false;switch(l){case L:case z:return+n==+t;
+case W:return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case J:case M:return n==ie(t)}if(p=l==$,!p){var s=we.call(n,"__wrapped__"),g=we.call(t,"__wrapped__");if(s||g)return at(s?n.__wrapped__:n,g?t.__wrapped__:t,e,r,u,o);if(l!=G||!Le.nodeClass&&(f(n)||f(t)))return false;if(l=!Le.argsObject&&dt(n)?oe:n.constructor,s=!Le.argsObject&&dt(t)?oe:t.constructor,l!=s&&!(jt(l)&&l instanceof l&&jt(s)&&s instanceof s)&&"constructor"in n&&"constructor"in t)return false}for(l=!u,u||(u=i()),o||(o=i()),s=u.length;s--;)if(u[s]==n)return o[s]==t;
+var h=0,a=true;if(u.push(n),o.push(t),p){if(s=n.length,h=t.length,(a=h==s)||r)for(;h--;)if(p=s,g=t[h],r)for(;p--&&!(a=at(n[p],g,e,r,u,o)););else if(!(a=at(n[h],g,e,r,u,o)))break}else nr(t,function(t,i,l){return we.call(l,i)?(h++,a=we.call(n,i)&&at(n[i],t,e,r,u,o)):void 0}),a&&!r&&nr(n,function(n,t,e){return we.call(e,t)?a=-1<--h:void 0});return u.pop(),o.pop(),l&&(c(u),c(o)),a}function it(n,t,e,r,u){(qe(t)?Dt:tr)(t,function(t,o){var a,i,l=t,f=n[o];if(t&&((i=qe(t))||er(t))){for(l=r.length;l--;)if(a=r[l]==t){f=u[l];
+break}if(!a){var c;e&&(l=e(f,t),c=typeof l!="undefined")&&(f=l),c||(f=i?qe(f)?f:[]:er(f)?f:{}),r.push(t),u.push(f),c||it(f,t,e,r,u)}}else e&&(l=e(f,t),typeof l=="undefined"&&(l=t)),typeof l!="undefined"&&(f=l);n[o]=f})}function lt(n,t){return n+de(Fe()*(t-n+1))}function ft(e,r,u){var a=-1,l=ht(),f=e?e.length:0,s=[],g=!r&&f>=_&&l===n,h=u||g?i():s;for(g&&(h=o(h),l=t);++a<f;){var v=e[a],y=u?u(v,a,e):v;(r?!a||h[h.length-1]!==y:0>l(h,y))&&((u||g)&&h.push(y),s.push(v))}return g?(c(h.k),p(h)):u&&c(h),s}function ct(n){return function(t,e,r){var u={};
+if(e=v.createCallback(e,r,3),qe(t)){r=-1;for(var o=t.length;++r<o;){var a=t[r];n(u,a,e(a,r,t),t)}}else Xe(t,function(t,r,o){n(u,t,e(t,r,o),o)});return u}}function pt(n,t,e,r,u,o){var a=1&t,i=4&t,l=16&t,f=32&t;if(!(2&t||jt(n)))throw new le;l&&!e.length&&(t&=-17,l=e=false),f&&!r.length&&(t&=-33,f=r=false);var c=n&&n.__bindData__;return c&&true!==c?(c=s(c),c[2]&&(c[2]=s(c[2])),c[3]&&(c[3]=s(c[3])),!a||1&c[1]||(c[4]=u),!a&&1&c[1]&&(t|=8),!i||4&c[1]||(c[5]=o),l&&je.apply(c[2]||(c[2]=[]),e),f&&Ee.apply(c[3]||(c[3]=[]),r),c[1]|=t,pt.apply(null,c)):(1==t||17===t?w:et)([n,t,e,r,u,o])
+}function st(){Q.h=F,Q.b=Q.c=Q.g=Q.i="",Q.e="t",Q.j=true;for(var n,t=0;n=arguments[t];t++)for(var e in n)Q[e]=n[e];t=Q.a,Q.d=/^[^,]+/.exec(t)[0],n=ee,t="return function("+t+"){",e=Q;var r="var n,t="+e.d+",E="+e.e+";if(!t)return E;"+e.i+";";e.b?(r+="var u=t.length;n=-1;if("+e.b+"){",Le.unindexedChars&&(r+="if(s(t)){t=t.split('')}"),r+="while(++n<u){"+e.g+";}}else{"):Le.nonEnumArgs&&(r+="var u=t.length;n=-1;if(u&&p(t)){while(++n<u){n+='';"+e.g+";}}else{"),Le.enumPrototypes&&(r+="var G=typeof t=='function';"),Le.enumErrorProps&&(r+="var F=t===k||t instanceof Error;");
+var u=[];if(Le.enumPrototypes&&u.push('!(G&&n=="prototype")'),Le.enumErrorProps&&u.push('!(F&&(n=="message"||n=="name"))'),e.j&&e.f)r+="var C=-1,D=B[typeof t]&&v(t),u=D?D.length:0;while(++C<u){n=D[C];",u.length&&(r+="if("+u.join("&&")+"){"),r+=e.g+";",u.length&&(r+="}"),r+="}";else if(r+="for(n in t){",e.j&&u.push("m.call(t, n)"),u.length&&(r+="if("+u.join("&&")+"){"),r+=e.g+";",u.length&&(r+="}"),r+="}",Le.nonEnumShadows){for(r+="if(t!==A){var i=t.constructor,r=t===(i&&i.prototype),f=t===J?I:t===k?j:L.call(t),x=y[f];",k=0;7>k;k++)r+="n='"+e.h[k]+"';if((!(r&&x[n])&&m.call(t,n))",e.j||(r+="||(!x[n]&&t[n]!==A[n])"),r+="){"+e.g+"}";
+r+="}"}return(e.b||Le.nonEnumArgs)&&(r+="}"),r+=e.c+";return E",n("d,j,k,m,o,p,q,s,v,A,B,y,I,J,L",t+r+"}")(tt,q,ce,we,d,dt,qe,kt,Q.f,pe,X,$e,M,se,he)}function gt(n){return Ve[n]}function ht(){var t=(t=v.indexOf)===zt?n:t;return t}function vt(n){return typeof n=="function"&&ve.test(n)}function yt(n){var t,e;return!n||he.call(n)!=G||(t=n.constructor,jt(t)&&!(t instanceof t))||!Le.argsClass&&dt(n)||!Le.nodeClass&&f(n)?false:Le.ownLast?(nr(n,function(n,t,r){return e=we.call(r,t),false}),false!==e):(nr(n,function(n,t){e=t
+}),typeof e=="undefined"||we.call(n,e))}function mt(n){return He[n]}function dt(n){return n&&typeof n=="object"&&typeof n.length=="number"&&he.call(n)==T||false}function bt(n,t,e){var r=We(n),u=r.length;for(t=tt(t,e,3);u--&&(e=r[u],false!==t(n[e],e,n)););return n}function _t(n){var t=[];return nr(n,function(n,e){jt(n)&&t.push(e)}),t.sort()}function wt(n){for(var t=-1,e=We(n),r=e.length,u={};++t<r;){var o=e[t];u[n[o]]=o}return u}function jt(n){return typeof n=="function"}function xt(n){return!(!n||!X[typeof n])
+}function Ct(n){return typeof n=="number"||n&&typeof n=="object"&&he.call(n)==W||false}function kt(n){return typeof n=="string"||n&&typeof n=="object"&&he.call(n)==M||false}function Et(n){for(var t=-1,e=We(n),r=e.length,u=Zt(r);++t<r;)u[t]=n[e[t]];return u}function Ot(n,t,e){var r=-1,u=ht(),o=n?n.length:0,a=false;return e=(0>e?Be(0,o+e):e)||0,qe(n)?a=-1<u(n,t,e):typeof o=="number"?a=-1<(kt(n)?n.indexOf(t,e):u(n,t,e)):Xe(n,function(n){return++r<e?void 0:!(a=n===t)}),a}function St(n,t,e){var r=true;if(t=v.createCallback(t,e,3),qe(n)){e=-1;
+for(var u=n.length;++e<u&&(r=!!t(n[e],e,n)););}else Xe(n,function(n,e,u){return r=!!t(n,e,u)});return r}function At(n,t,e){var r=[];if(t=v.createCallback(t,e,3),qe(n)){e=-1;for(var u=n.length;++e<u;){var o=n[e];t(o,e,n)&&r.push(o)}}else Xe(n,function(n,e,u){t(n,e,u)&&r.push(n)});return r}function It(n,t,e){if(t=v.createCallback(t,e,3),!qe(n)){var r;return Xe(n,function(n,e,u){return t(n,e,u)?(r=n,false):void 0}),r}e=-1;for(var u=n.length;++e<u;){var o=n[e];if(t(o,e,n))return o}}function Dt(n,t,e){if(t&&typeof e=="undefined"&&qe(n)){e=-1;
+for(var r=n.length;++e<r&&false!==t(n[e],e,n););}else Xe(n,t,e);return n}function Nt(n,t,e){var r=n,u=n?n.length:0;if(t=t&&typeof e=="undefined"?t:tt(t,e,3),qe(n))for(;u--&&false!==t(n[u],u,n););else{if(typeof u!="number")var o=We(n),u=o.length;else Le.unindexedChars&&kt(n)&&(r=n.split(""));Xe(n,function(n,e,a){return e=o?o[--u]:--u,t(r[e],e,a)})}return n}function Bt(n,t,e){var r=-1,u=n?n.length:0,o=Zt(typeof u=="number"?u:0);if(t=v.createCallback(t,e,3),qe(n))for(;++r<u;)o[r]=t(n[r],r,n);else Xe(n,function(n,e,u){o[++r]=t(n,e,u)
+});return o}function Pt(n,t,e){var u=-1/0,o=u;if(typeof t!="function"&&e&&e[t]===n&&(t=null),null==t&&qe(n)){e=-1;for(var a=n.length;++e<a;){var i=n[e];i>o&&(o=i)}}else t=null==t&&kt(n)?r:v.createCallback(t,e,3),Xe(n,function(n,e,r){e=t(n,e,r),e>u&&(u=e,o=n)});return o}function Rt(n,t,e,r){var u=3>arguments.length;if(t=v.createCallback(t,r,4),qe(n)){var o=-1,a=n.length;for(u&&(e=n[++o]);++o<a;)e=t(e,n[o],o,n)}else Xe(n,function(n,r,o){e=u?(u=false,n):t(e,n,r,o)});return e}function Ft(n,t,e,r){var u=3>arguments.length;
+return t=v.createCallback(t,r,4),Nt(n,function(n,r,o){e=u?(u=false,n):t(e,n,r,o)}),e}function Tt(n){var t=-1,e=n?n.length:0,r=Zt(typeof e=="number"?e:0);return Dt(n,function(n){var e=lt(0,++t);r[t]=r[e],r[e]=n}),r}function $t(n,t,e){var r;if(t=v.createCallback(t,e,3),qe(n)){e=-1;for(var u=n.length;++e<u&&!(r=t(n[e],e,n)););}else Xe(n,function(n,e,u){return!(r=t(n,e,u))});return!!r}function Lt(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=-1;for(t=v.createCallback(t,e,3);++o<u&&t(n[o],o,n);)r++
+}else if(r=t,null==r||e)return n?n[0]:h;return s(n,0,Pe(Be(0,r),u))}function zt(t,e,r){if(typeof r=="number"){var u=t?t.length:0;r=0>r?Be(0,u+r):r||0}else if(r)return r=Kt(t,e),t[r]===e?r:-1;return n(t,e,r)}function qt(n,t,e){if(typeof t!="number"&&null!=t){var r=0,u=-1,o=n?n.length:0;for(t=v.createCallback(t,e,3);++u<o&&t(n[u],u,n);)r++}else r=null==t||e?1:Be(0,t);return s(n,r)}function Kt(n,t,e,r){var u=0,o=n?n.length:u;for(e=e?v.createCallback(e,r,1):Ht,t=e(t);u<o;)r=u+o>>>1,e(n[r])<t?u=r+1:o=r;
+return u}function Wt(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=typeof t!="function"&&r&&r[t]===n?null:t,t=false),null!=e&&(e=v.createCallback(e,r,3)),ft(n,t,e)}function Gt(){for(var n=1<arguments.length?arguments:arguments[0],t=-1,e=n?Pt(ar(n,"length")):0,r=Zt(0>e?0:e);++t<e;)r[t]=ar(n,t);return r}function Jt(n,t){var e=-1,r=n?n.length:0,u={};for(t||!r||qe(n[0])||(t=[]);++e<r;){var o=n[e];t?u[o]=t[e]:o&&(u[o[0]]=o[1])}return u}function Mt(n,t){return 2<arguments.length?pt(n,17,s(arguments,2),null,t):pt(n,1,null,null,t)
+}function Vt(n,t,e){var r,u,o,a,i,l,f,c=0,p=false,s=true;if(!jt(n))throw new le;if(t=Be(0,t)||0,true===e)var g=true,s=false;else xt(e)&&(g=e.leading,p="maxWait"in e&&(Be(t,e.maxWait)||0),s="trailing"in e?e.trailing:s);var v=function(){var e=t-(ir()-a);0<e?l=Ce(v,e):(u&&me(u),e=f,u=l=f=h,e&&(c=ir(),o=n.apply(i,r),l||u||(r=i=null)))},y=function(){l&&me(l),u=l=f=h,(s||p!==t)&&(c=ir(),o=n.apply(i,r),l||u||(r=i=null))};return function(){if(r=arguments,a=ir(),i=this,f=s&&(l||!g),false===p)var e=g&&!l;else{u||g||(c=a);
+var h=p-(a-c),m=0>=h;m?(u&&(u=me(u)),c=a,o=n.apply(i,r)):u||(u=Ce(y,h))}return m&&l?l=me(l):l||t===p||(l=Ce(v,t)),e&&(m=true,o=n.apply(i,r)),!m||l||u||(r=i=null),o}}function Ht(n){return n}function Ut(n,t,e){var r=true,u=t&&_t(t);t&&(e||u.length)||(null==e&&(e=t),o=y,t=n,n=v,u=_t(t)),false===e?r=false:xt(e)&&"chain"in e&&(r=e.chain);var o=n,a=jt(o);Dt(u,function(e){var u=n[e]=t[e];a&&(o.prototype[e]=function(){var t=this.__chain__,e=this.__wrapped__,a=[e];if(je.apply(a,arguments),a=u.apply(n,a),r||t){if(e===a&&xt(a))return this;
+a=new o(a),a.__chain__=t}return a})})}function Qt(){}function Xt(n){return function(t){return t[n]}}function Yt(){return this.__wrapped__}e=e?ut.defaults(Z.Object(),e,ut.pick(Z,R)):Z;var Zt=e.Array,ne=e.Boolean,te=e.Date,ee=e.Function,re=e.Math,ue=e.Number,oe=e.Object,ae=e.RegExp,ie=e.String,le=e.TypeError,fe=[],ce=e.Error.prototype,pe=oe.prototype,se=ie.prototype,ge=e._,he=pe.toString,ve=ae("^"+ie(he).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/toString| for [^\]]+/g,".*?")+"$"),ye=re.ceil,me=e.clearTimeout,de=re.floor,be=ee.prototype.toString,_e=vt(_e=oe.getPrototypeOf)&&_e,we=pe.hasOwnProperty,je=fe.push,xe=pe.propertyIsEnumerable,Ce=e.setTimeout,ke=fe.splice,Ee=fe.unshift,Oe=function(){try{var n={},t=vt(t=oe.defineProperty)&&t,e=t(n,n,n)&&t
+}catch(r){}return e}(),Se=vt(Se=oe.create)&&Se,Ae=vt(Ae=Zt.isArray)&&Ae,Ie=e.isFinite,De=e.isNaN,Ne=vt(Ne=oe.keys)&&Ne,Be=re.max,Pe=re.min,Re=e.parseInt,Fe=re.random,Te={};Te[$]=Zt,Te[L]=ne,Te[z]=te,Te[K]=ee,Te[G]=oe,Te[W]=ue,Te[J]=ae,Te[M]=ie;var $e={};$e[$]=$e[z]=$e[W]={constructor:true,toLocaleString:true,toString:true,valueOf:true},$e[L]=$e[M]={constructor:true,toString:true,valueOf:true},$e[q]=$e[K]=$e[J]={constructor:true,toString:true},$e[G]={constructor:true},function(){for(var n=F.length;n--;){var t,e=F[n];
+for(t in $e)we.call($e,t)&&!we.call($e[t],e)&&($e[t][e]=false)}}(),y.prototype=v.prototype;var Le=v.support={};!function(){var n=function(){this.x=1},t={0:1,length:1},r=[];n.prototype={valueOf:1,y:1};for(var u in new n)r.push(u);for(u in arguments);Le.argsClass=he.call(arguments)==T,Le.argsObject=arguments.constructor==oe&&!(arguments instanceof Zt),Le.enumErrorProps=xe.call(ce,"message")||xe.call(ce,"name"),Le.enumPrototypes=xe.call(n,"prototype"),Le.funcDecomp=!vt(e.WinRTError)&&B.test(g),Le.funcNames=typeof ee.name=="string",Le.nonEnumArgs=0!=u,Le.nonEnumShadows=!/valueOf/.test(r),Le.ownLast="x"!=r[0],Le.spliceObjects=(fe.splice.call(t,0,1),!t[0]),Le.unindexedChars="xx"!="x"[0]+oe("x")[0];
+try{Le.nodeClass=!(he.call(document)==G&&!({toString:0}+""))}catch(o){Le.nodeClass=true}}(1),v.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:I,variable:"",imports:{_:v}},Se||(nt=function(){function n(){}return function(t){if(xt(t)){n.prototype=t;var r=new n;n.prototype=null}return r||e.Object()}}());var ze=Oe?function(n,t){U.value=t,Oe(n,"__bindData__",U)}:Qt;Le.argsClass||(dt=function(n){return n&&typeof n=="object"&&typeof n.length=="number"&&we.call(n,"callee")&&!xe.call(n,"callee")||false
+});var qe=Ae||function(n){return n&&typeof n=="object"&&typeof n.length=="number"&&he.call(n)==$||false},Ke=st({a:"z",e:"[]",i:"if(!(B[typeof z]))return E",g:"E.push(n)"}),We=Ne?function(n){return xt(n)?Le.enumPrototypes&&typeof n=="function"||Le.nonEnumArgs&&n.length&&dt(n)?Ke(n):Ne(n):[]}:Ke,Ge={a:"g,e,K",i:"e=e&&typeof K=='undefined'?e:d(e,K,3)",b:"typeof u=='number'",v:We,g:"if(e(t[n],n,g)===false)return E"},Je={a:"z,H,l",i:"var a=arguments,b=0,c=typeof l=='number'?2:a.length;while(++b<c){t=a[b];if(t&&B[typeof t]){",v:We,g:"if(typeof E[n]=='undefined')E[n]=t[n]",c:"}}"},Me={i:"if(!B[typeof t])return E;"+Ge.i,b:false},Ve={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},He=wt(Ve),Ue=ae("("+We(He).join("|")+")","g"),Qe=ae("["+We(Ve).join("")+"]","g"),Xe=st(Ge),Ye=st(Je,{i:Je.i.replace(";",";if(c>3&&typeof a[c-2]=='function'){var e=d(a[--c-1],a[c--],2)}else if(c>2&&typeof a[c-1]=='function'){e=a[--c]}"),g:"E[n]=e?e(E[n],t[n]):t[n]"}),Ze=st(Je),nr=st(Ge,Me,{j:false}),tr=st(Ge,Me);
+jt(/x/)&&(jt=function(n){return typeof n=="function"&&he.call(n)==K});var er=_e?function(n){if(!n||he.call(n)!=G||!Le.argsClass&&dt(n))return false;var t=n.valueOf,e=vt(t)&&(e=_e(t))&&_e(e);return e?n==e||_e(n)==e:yt(n)}:yt,rr=ct(function(n,t,e){we.call(n,e)?n[e]++:n[e]=1}),ur=ct(function(n,t,e){(we.call(n,e)?n[e]:n[e]=[]).push(t)}),or=ct(function(n,t,e){n[e]=t}),ar=Bt,ir=vt(ir=te.now)&&ir||function(){return(new te).getTime()},lr=8==Re(j+"08")?Re:function(n,t){return Re(kt(n)?n.replace(D,""):n,t||0)};
+return v.after=function(n,t){if(!jt(t))throw new le;return function(){return 1>--n?t.apply(this,arguments):void 0}},v.assign=Ye,v.at=function(n){var t=arguments,e=-1,r=ot(t,true,false,1),t=t[2]&&t[2][t[1]]===n?1:r.length,u=Zt(t);for(Le.unindexedChars&&kt(n)&&(n=n.split(""));++e<t;)u[e]=n[r[e]];return u},v.bind=Mt,v.bindAll=function(n){for(var t=1<arguments.length?ot(arguments,true,false,1):_t(n),e=-1,r=t.length;++e<r;){var u=t[e];n[u]=pt(n[u],1,null,null,n)}return n},v.bindKey=function(n,t){return 2<arguments.length?pt(t,19,s(arguments,2),null,n):pt(t,3,null,null,n)
+},v.chain=function(n){return n=new y(n),n.__chain__=true,n},v.compact=function(n){for(var t=-1,e=n?n.length:0,r=[];++t<e;){var u=n[t];u&&r.push(u)}return r},v.compose=function(){for(var n=arguments,t=n.length;t--;)if(!jt(n[t]))throw new le;return function(){for(var t=arguments,e=n.length;e--;)t=[n[e].apply(this,t)];return t[0]}},v.constant=function(n){return function(){return n}},v.countBy=rr,v.create=function(n,t){var e=nt(n);return t?Ye(e,t):e},v.createCallback=function(n,t,e){var r=typeof n;if(null==n||"function"==r)return tt(n,t,e);
+if("object"!=r)return Xt(n);var u=We(n),o=u[0],a=n[o];return 1!=u.length||a!==a||xt(a)?function(t){for(var e=u.length,r=false;e--&&(r=at(t[u[e]],n[u[e]],null,true)););return r}:function(n){return n=n[o],a===n&&(0!==a||1/a==1/n)}},v.curry=function(n,t){return t=typeof t=="number"?t:+t||n.length,pt(n,4,null,null,null,t)},v.debounce=Vt,v.defaults=Ze,v.defer=function(n){if(!jt(n))throw new le;var t=s(arguments,1);return Ce(function(){n.apply(h,t)},1)},v.delay=function(n,t){if(!jt(n))throw new le;var e=s(arguments,2);
+return Ce(function(){n.apply(h,e)},t)},v.difference=function(n){return rt(n,ot(arguments,true,true,1))},v.filter=At,v.flatten=function(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=typeof t!="function"&&r&&r[t]===n?null:t,t=false),null!=e&&(n=Bt(n,e,r)),ot(n,t)},v.forEach=Dt,v.forEachRight=Nt,v.forIn=nr,v.forInRight=function(n,t,e){var r=[];nr(n,function(n,t){r.push(t,n)});var u=r.length;for(t=tt(t,e,3);u--&&false!==t(r[u--],r[u],n););return n},v.forOwn=tr,v.forOwnRight=bt,v.functions=_t,v.groupBy=ur,v.indexBy=or,v.initial=function(n,t,e){var r=0,u=n?n.length:0;
+if(typeof t!="number"&&null!=t){var o=u;for(t=v.createCallback(t,e,3);o--&&t(n[o],o,n);)r++}else r=null==t||e?1:t||r;return s(n,0,Pe(Be(0,u-r),u))},v.intersection=function(){for(var e=[],r=-1,u=arguments.length,a=i(),l=ht(),f=l===n,s=i();++r<u;){var g=arguments[r];(qe(g)||dt(g))&&(e.push(g),a.push(f&&g.length>=_&&o(r?e[r]:s)))}var f=e[0],h=-1,v=f?f.length:0,y=[];n:for(;++h<v;){var m=a[0],g=f[h];if(0>(m?t(m,g):l(s,g))){for(r=u,(m||s).push(g);--r;)if(m=a[r],0>(m?t(m,g):l(e[r],g)))continue n;y.push(g)
+}}for(;u--;)(m=a[u])&&p(m);return c(a),c(s),y},v.invert=wt,v.invoke=function(n,t){var e=s(arguments,2),r=-1,u=typeof t=="function",o=n?n.length:0,a=Zt(typeof o=="number"?o:0);return Dt(n,function(n){a[++r]=(u?t:n[t]).apply(n,e)}),a},v.keys=We,v.map=Bt,v.mapValues=function(n,t,e){var r={};return t=v.createCallback(t,e,3),tr(n,function(n,e,u){r[e]=t(n,e,u)}),r},v.max=Pt,v.memoize=function(n,t){if(!jt(n))throw new le;var e=function(){var r=e.cache,u=t?t.apply(this,arguments):b+arguments[0];return we.call(r,u)?r[u]:r[u]=n.apply(this,arguments)
+};return e.cache={},e},v.merge=function(n){var t=arguments,e=2;if(!xt(n))return n;if("number"!=typeof t[2]&&(e=t.length),3<e&&"function"==typeof t[e-2])var r=tt(t[--e-1],t[e--],2);else 2<e&&"function"==typeof t[e-1]&&(r=t[--e]);for(var t=s(arguments,1,e),u=-1,o=i(),a=i();++u<e;)it(n,t[u],r,o,a);return c(o),c(a),n},v.min=function(n,t,e){var u=1/0,o=u;if(typeof t!="function"&&e&&e[t]===n&&(t=null),null==t&&qe(n)){e=-1;for(var a=n.length;++e<a;){var i=n[e];i<o&&(o=i)}}else t=null==t&&kt(n)?r:v.createCallback(t,e,3),Xe(n,function(n,e,r){e=t(n,e,r),e<u&&(u=e,o=n)
+});return o},v.omit=function(n,t,e){var r={};if(typeof t!="function"){var u=[];nr(n,function(n,t){u.push(t)});for(var u=rt(u,ot(arguments,true,false,1)),o=-1,a=u.length;++o<a;){var i=u[o];r[i]=n[i]}}else t=v.createCallback(t,e,3),nr(n,function(n,e,u){t(n,e,u)||(r[e]=n)});return r},v.once=function(n){var t,e;if(!jt(n))throw new le;return function(){return t?e:(t=true,e=n.apply(this,arguments),n=null,e)}},v.pairs=function(n){for(var t=-1,e=We(n),r=e.length,u=Zt(r);++t<r;){var o=e[t];u[t]=[o,n[o]]}return u
+},v.partial=function(n){return pt(n,16,s(arguments,1))},v.partialRight=function(n){return pt(n,32,null,s(arguments,1))},v.pick=function(n,t,e){var r={};if(typeof t!="function")for(var u=-1,o=ot(arguments,true,false,1),a=xt(n)?o.length:0;++u<a;){var i=o[u];i in n&&(r[i]=n[i])}else t=v.createCallback(t,e,3),nr(n,function(n,e,u){t(n,e,u)&&(r[e]=n)});return r},v.pluck=ar,v.property=Xt,v.pull=function(n){for(var t=arguments,e=0,r=t.length,u=n?n.length:0;++e<r;)for(var o=-1,a=t[e];++o<u;)n[o]===a&&(ke.call(n,o--,1),u--);
+return n},v.range=function(n,t,e){n=+n||0,e=typeof e=="number"?e:+e||1,null==t&&(t=n,n=0);var r=-1;t=Be(0,ye((t-n)/(e||1)));for(var u=Zt(t);++r<t;)u[r]=n,n+=e;return u},v.reject=function(n,t,e){return t=v.createCallback(t,e,3),At(n,function(n,e,r){return!t(n,e,r)})},v.remove=function(n,t,e){var r=-1,u=n?n.length:0,o=[];for(t=v.createCallback(t,e,3);++r<u;)e=n[r],t(e,r,n)&&(o.push(e),ke.call(n,r--,1),u--);return o},v.rest=qt,v.shuffle=Tt,v.sortBy=function(n,t,e){var r=-1,o=qe(t),a=n?n.length:0,f=Zt(typeof a=="number"?a:0);
+for(o||(t=v.createCallback(t,e,3)),Dt(n,function(n,e,u){var a=f[++r]=l();o?a.m=Bt(t,function(t){return n[t]}):(a.m=i())[0]=t(n,e,u),a.n=r,a.o=n}),a=f.length,f.sort(u);a--;)n=f[a],f[a]=n.o,o||c(n.m),p(n);return f},v.tap=function(n,t){return t(n),n},v.throttle=function(n,t,e){var r=true,u=true;if(!jt(n))throw new le;return false===e?r=false:xt(e)&&(r="leading"in e?e.leading:r,u="trailing"in e?e.trailing:u),H.leading=r,H.maxWait=t,H.trailing=u,Vt(n,t,H)},v.times=function(n,t,e){n=-1<(n=+n)?n:0;var r=-1,u=Zt(n);
+for(t=tt(t,e,1);++r<n;)u[r]=t(r);return u},v.toArray=function(n){return n&&typeof n.length=="number"?Le.unindexedChars&&kt(n)?n.split(""):s(n):Et(n)},v.transform=function(n,t,e,r){var u=qe(n);if(null==e)if(u)e=[];else{var o=n&&n.constructor;e=nt(o&&o.prototype)}return t&&(t=v.createCallback(t,r,4),(u?Xe:tr)(n,function(n,r,u){return t(e,n,r,u)})),e},v.union=function(){return ft(ot(arguments,true,true))},v.uniq=Wt,v.values=Et,v.where=At,v.without=function(n){return rt(n,s(arguments,1))},v.wrap=function(n,t){return pt(t,16,[n])
+},v.xor=function(){for(var n=-1,t=arguments.length;++n<t;){var e=arguments[n];if(qe(e)||dt(e))var r=r?ft(rt(r,e).concat(rt(e,r))):e}return r||[]},v.zip=Gt,v.zipObject=Jt,v.collect=Bt,v.drop=qt,v.each=Dt,v.eachRight=Nt,v.extend=Ye,v.methods=_t,v.object=Jt,v.select=At,v.tail=qt,v.unique=Wt,v.unzip=Gt,Ut(v),v.clone=function(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=t,t=false),Y(n,t,typeof e=="function"&&tt(e,r,1))},v.cloneDeep=function(n,t,e){return Y(n,true,typeof t=="function"&&tt(t,e,1))},v.contains=Ot,v.escape=function(n){return null==n?"":ie(n).replace(Qe,gt)
+},v.every=St,v.find=It,v.findIndex=function(n,t,e){var r=-1,u=n?n.length:0;for(t=v.createCallback(t,e,3);++r<u;)if(t(n[r],r,n))return r;return-1},v.findKey=function(n,t,e){var r;return t=v.createCallback(t,e,3),tr(n,function(n,e,u){return t(n,e,u)?(r=e,false):void 0}),r},v.findLast=function(n,t,e){var r;return t=v.createCallback(t,e,3),Nt(n,function(n,e,u){return t(n,e,u)?(r=n,false):void 0}),r},v.findLastIndex=function(n,t,e){var r=n?n.length:0;for(t=v.createCallback(t,e,3);r--;)if(t(n[r],r,n))return r;
+return-1},v.findLastKey=function(n,t,e){var r;return t=v.createCallback(t,e,3),bt(n,function(n,e,u){return t(n,e,u)?(r=e,false):void 0}),r},v.has=function(n,t){return n?we.call(n,t):false},v.identity=Ht,v.indexOf=zt,v.isArguments=dt,v.isArray=qe,v.isBoolean=function(n){return true===n||false===n||n&&typeof n=="object"&&he.call(n)==L||false},v.isDate=function(n){return n&&typeof n=="object"&&he.call(n)==z||false},v.isElement=function(n){return n&&1===n.nodeType||false},v.isEmpty=function(n){var t=true;if(!n)return t;var e=he.call(n),r=n.length;
+return e==$||e==M||(Le.argsClass?e==T:dt(n))||e==G&&typeof r=="number"&&jt(n.splice)?!r:(tr(n,function(){return t=false}),t)},v.isEqual=function(n,t,e,r){return at(n,t,typeof e=="function"&&tt(e,r,2))},v.isFinite=function(n){return Ie(n)&&!De(parseFloat(n))},v.isFunction=jt,v.isNaN=function(n){return Ct(n)&&n!=+n},v.isNull=function(n){return null===n},v.isNumber=Ct,v.isObject=xt,v.isPlainObject=er,v.isRegExp=function(n){return n&&X[typeof n]&&he.call(n)==J||false},v.isString=kt,v.isUndefined=function(n){return typeof n=="undefined"
+},v.lastIndexOf=function(n,t,e){var r=n?n.length:0;for(typeof e=="number"&&(r=(0>e?Be(0,r+e):Pe(e,r-1))+1);r--;)if(n[r]===t)return r;return-1},v.mixin=Ut,v.noConflict=function(){return e._=ge,this},v.noop=Qt,v.now=ir,v.parseInt=lr,v.random=function(n,t,e){var r=null==n,u=null==t;return null==e&&(typeof n=="boolean"&&u?(e=n,n=1):u||typeof t!="boolean"||(e=t,u=true)),r&&u&&(t=1),n=+n||0,u?(t=n,n=0):t=+t||0,e||n%1||t%1?(e=Fe(),Pe(n+e*(t-n+parseFloat("1e-"+((e+"").length-1))),t)):lt(n,t)},v.reduce=Rt,v.reduceRight=Ft,v.result=function(n,t){if(n){var e=n[t];
+return jt(e)?n[t]():e}},v.runInContext=g,v.size=function(n){var t=n?n.length:0;return typeof t=="number"?t:We(n).length},v.some=$t,v.sortedIndex=Kt,v.template=function(n,t,e){var r=v.templateSettings;n=ie(n||""),e=Ze({},e,r);var u,o=Ze({},e.imports,r.imports),r=We(o),o=Et(o),i=0,l=e.interpolate||N,f="__p+='",l=ae((e.escape||N).source+"|"+l.source+"|"+(l===I?O:N).source+"|"+(e.evaluate||N).source+"|$","g");n.replace(l,function(t,e,r,o,l,c){return r||(r=o),f+=n.slice(i,c).replace(P,a),e&&(f+="'+__e("+e+")+'"),l&&(u=true,f+="';"+l+";\n__p+='"),r&&(f+="'+((__t=("+r+"))==null?'':__t)+'"),i=c+t.length,t
+}),f+="';",l=e=e.variable,l||(e="obj",f="with("+e+"){"+f+"}"),f=(u?f.replace(x,""):f).replace(C,"$1").replace(E,"$1;"),f="function("+e+"){"+(l?"":e+"||("+e+"={});")+"var __t,__p='',__e=_.escape"+(u?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+f+"return __p}";try{var c=ee(r,"return "+f).apply(h,o)}catch(p){throw p.source=f,p}return t?c(t):(c.source=f,c)},v.unescape=function(n){return null==n?"":ie(n).replace(Ue,mt)},v.uniqueId=function(n){var t=++m;return ie(null==n?"":n)+t
+},v.all=St,v.any=$t,v.detect=It,v.findWhere=It,v.foldl=Rt,v.foldr=Ft,v.include=Ot,v.inject=Rt,Ut(function(){var n={};return tr(v,function(t,e){v.prototype[e]||(n[e]=t)}),n}(),false),v.first=Lt,v.last=function(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=u;for(t=v.createCallback(t,e,3);o--&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n?n[u-1]:h;return s(n,Be(0,u-r))},v.sample=function(n,t,e){return n&&typeof n.length!="number"?n=Et(n):Le.unindexedChars&&kt(n)&&(n=n.split("")),null==t||e?n?n[lt(0,n.length-1)]:h:(n=Tt(n),n.length=Pe(Be(0,t),n.length),n)
+},v.take=Lt,v.head=Lt,tr(v,function(n,t){var e="sample"!==t;v.prototype[t]||(v.prototype[t]=function(t,r){var u=this.__chain__,o=n(this.__wrapped__,t,r);return u||null!=t&&(!r||e&&typeof t=="function")?new y(o,u):o})}),v.VERSION="2.4.1",v.prototype.chain=function(){return this.__chain__=true,this},v.prototype.toString=function(){return ie(this.__wrapped__)},v.prototype.value=Yt,v.prototype.valueOf=Yt,Xe(["join","pop","shift"],function(n){var t=fe[n];v.prototype[n]=function(){var n=this.__chain__,e=t.apply(this.__wrapped__,arguments);
+return n?new y(e,n):e}}),Xe(["push","reverse","sort","unshift"],function(n){var t=fe[n];v.prototype[n]=function(){return t.apply(this.__wrapped__,arguments),this}}),Xe(["concat","slice","splice"],function(n){var t=fe[n];v.prototype[n]=function(){return new y(t.apply(this.__wrapped__,arguments),this.__chain__)}}),Le.spliceObjects||Xe(["pop","shift","splice"],function(n){var t=fe[n],e="splice"==n;v.prototype[n]=function(){var n=this.__chain__,r=this.__wrapped__,u=t.apply(r,arguments);return 0===r.length&&delete r[0],n||e?new y(u,n):u
+}}),v}var h,v=[],y=[],m=0,d={},b=+new Date+"",_=75,w=40,j=" \t\x0B\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000",x=/\b__p\+='';/g,C=/\b(__p\+=)''\+/g,E=/(__e\(.*?\)|\b__t\))\+'';/g,O=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,S=/\w*$/,A=/^\s*function[ \n\r\t]+\w/,I=/<%=([\s\S]+?)%>/g,D=RegExp("^["+j+"]*0+(?=.$)"),N=/($^)/,B=/\bthis\b/,P=/['\n\r\t\u2028\u2029\\]/g,R="Array Boolean Date Error Function Math Number Object RegExp String _ attachEvent clearTimeout isFinite isNaN parseInt setTimeout".split(" "),F="constructor hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf".split(" "),T="[object Arguments]",$="[object Array]",L="[object Boolean]",z="[object Date]",q="[object Error]",K="[object Function]",W="[object Number]",G="[object Object]",J="[object RegExp]",M="[object String]",V={};
+V[K]=false,V[T]=V[$]=V[L]=V[z]=V[W]=V[G]=V[J]=V[M]=true;var H={leading:false,maxWait:0,trailing:false},U={configurable:false,enumerable:false,value:null,writable:false},Q={a:"",b:null,c:"",d:"",e:"",v:null,g:"",h:null,support:null,i:"",j:false},X={"boolean":false,"function":true,object:true,number:false,string:false,undefined:false},Y={"\\":"\\","'":"'","\n":"n","\r":"r","\t":"t","\u2028":"u2028","\u2029":"u2029"},Z=X[typeof window]&&window||this,nt=X[typeof exports]&&exports&&!exports.nodeType&&exports,tt=X[typeof module]&&module&&!module.nodeType&&module,et=tt&&tt.exports===nt&&nt,rt=X[typeof global]&&global;
+!rt||rt.global!==rt&&rt.window!==rt||(Z=rt);var ut=g();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Z._=ut, define(function(){return ut})):nt&&tt?et?(tt.exports=ut)._=ut:nt._=ut:Z._=ut}).call(this);