text: /**
   * Module dependencies.
   */
  
  var keys = require('./keys');
  var hasBinary = require('has-binary');
  var sliceBuffer = require('arraybuffer.slice');
  var after = require('after');
  var utf8 = require('wtf-8');
  
  var base64encoder;
  if (global && global.ArrayBuffer) {
    base64encoder = require('base64-arraybuffer');
  }
  
  /**
   * 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 = typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent);
  
  /**
   * Check if we are running in PhantomJS.
   * Uploading a Blob with PhantomJS does not work correctly, as reported here:
   * https://github.com/ariya/phantomjs/issues/11395
   * @type boolean
   */
  var isPhantomJS = typeof navigator !== 'undefined' && /PhantomJS/i.test(navigator.userAgent);
  
  /**
   * When true, avoids using Blobs to encode payloads.
   * @type boolean
   */
  var dontSendBlobs = isAndroid || isPhantomJS;
  
  /**
   * 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 = require('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);
    }
  
    // might be an object with { base64: true, data: dataAsBase64String }
    if (data && data.base64) {
      return encodeBase64Object(packet, 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);
  
  };
  
  function encodeBase64Object(packet, callback) {
    // packet data is an object { base64: true, data: dataAsBase64String }
    var message = 'b' + exports.packets[packet.type] + packet.data.data;
    return callback(message);
  }
  
  /**
   * 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 (dontSendBlobs) {
      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 global.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) {
    if (data === undefined) {
      return err;
    }
    // String data
    if (typeof data == 'string') {
      if (data.charAt(0) == 'b') {
        return exports.decodeBase64Packet(data.substr(1), binaryType);
      }
  
      if (utf8decode) {
        data = tryDecode(data);
        if (data === false) {
          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 };
  };
  
  function tryDecode(data) {
    try {
      data = utf8.decode(data);
    } catch (e) {
      return false;
    }
    return data;
  }
  
  /**
   * 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 (!base64encoder) {
      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;
    }
  
    var isBinary = hasBinary(packets);
  
    if (supportsBinary && isBinary) {
      if (Blob && !dontSendBlobs) {
        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, !isBinary ? false : 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);
    });
  };
  