/**
 * Copyright (c) 2013 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 **/

var http = function() {

if (!chrome.sockets || !chrome.sockets.tcpServer)
    return {};

// Wrap chrome.sockets.tcp socketId with a Promise API.
var PSocket = (function() {
  // chrome.sockets.tcp uses a global listener for incoming data so
  // use a map to dispatch to the proper instance.
  var socketMap = {};
  chrome.sockets.tcp.onReceive.addListener(function(info) {
    var pSocket = socketMap[info.socketId];
    if (pSocket) {
      if (pSocket.handlers) {
        // Fulfil the pending read.
        pSocket.handlers.resolve(info.data);
        delete pSocket.handlers;
      }
      else {
        // No pending read so put data on the queue.
        pSocket.readQueue.push(info);
      }
    }
  });

  // Read errors also use a global listener.
  chrome.sockets.tcp.onReceiveError.addListener(function(info) {
    var pSocket = socketMap[info.socketId];
    if (pSocket) {
      if (pSocket.handlers) {
        // Reject the pending read.
        pSocket.handlers.reject(new Error('chrome.sockets.tcp error ' + info.resultCode));
        delete pSocket.handlers;
      }
      else {
        // No pending read so put data on the queue.
        pSocket.readQueue.push(info);
      }
    }
  });

  // PSocket constructor.
  return function(socketId) {
    this.socketId = socketId;
    this.readQueue = [];

    // Register this instance for incoming data processing.
    socketMap[socketId] = this;
    chrome.sockets.tcp.setPaused(socketId, false);
  };
})();

// Returns a Promise<ArrayBuffer> with read data.
PSocket.prototype.read = function() {
  var that = this;
  if (this.readQueue.length) {
    // Return data from the queue.
    var info = this.readQueue.shift();
    if (!info.resultCode)
      return Promise.resolve(info.data);
    else
      return Promise.reject(new Error('chrome.sockets.tcp error ' + info.resultCode));
  }
  else {
    // The queue is empty so install handlers.
    return new Promise(function(resolve, reject) {
      that.handlers = { resolve: resolve, reject: reject };
    });
  }
};

// Returns a Promise<integer> with the number of bytes written.
PSocket.prototype.write = function(data) {
  var that = this;
  return new Promise(function(resolve, reject) {
    chrome.sockets.tcp.send(that.socketId, data, function(info) {
      if (info && info.resultCode >= 0)
        resolve(info.bytesSent);
      else
        reject(new Error('chrome sockets.tcp error ' + (info && info.resultCode)));
    });
  });
};

// Returns a Promise.
PSocket.prototype.close = function() {
  var that = this;
  return new Promise(function(resolve, reject) {
    chrome.sockets.tcp.disconnect(that.socketId, function() {
      chrome.sockets.tcp.close(that.socketId, resolve);
    });
  });
};

// Http response code strings.
var responseMap = {
  200: 'OK',
  301: 'Moved Permanently',
  304: 'Not Modified',
  400: 'Bad Request',
  401: 'Unauthorized',
  403: 'Forbidden',
  404: 'Not Found',
  413: 'Request Entity Too Large',
  414: 'Request-URI Too Long',
  500: 'Internal Server Error'};

/**
 * Convert from an ArrayBuffer to a string.
 * @param {ArrayBuffer} buffer The array buffer to convert.
 * @return {string} The textual representation of the array.
 */
var arrayBufferToString = function(buffer) {
  var array = new Uint8Array(buffer);
  var str = '';
  for (var i = 0; i < array.length; ++i) {
    str += String.fromCharCode(array[i]);
  }
  return str;
};

/**
 * Convert from an UTF-8 array to UTF-8 string.
 * @param {array} UTF-8 array
 * @return {string} UTF-8 string
 */
var ary2utf8 = (function() {

  var patterns = [
    {pattern: '0xxxxxxx', bytes: 1},
    {pattern: '110xxxxx', bytes: 2},
    {pattern: '1110xxxx', bytes: 3},
    {pattern: '11110xxx', bytes: 4},
    {pattern: '111110xx', bytes: 5},
    {pattern: '1111110x', bytes: 6}
  ];
  patterns.forEach(function(item) {
    item.header = item.pattern.replace(/[^10]/g, '');
    item.pattern01 = item.pattern.replace(/[^10]/g, '0');
    item.pattern01 = parseInt(item.pattern01, 2);
    item.mask_length = item.header.length;
    item.data_length = 8 - item.header.length;
    var mask = '';
    for (var i = 0, len = item.mask_length; i < len; i++) {
      mask += '1';
    }
    for (var i = 0, len = item.data_length; i < len; i++) {
      mask += '0';
    }
    item.mask = mask;
    item.mask = parseInt(item.mask, 2);
  });

  return function(ary) {
    var codes = [];
    var cur = 0;
    while(cur < ary.length) {
      var first = ary[cur];
      var pattern = null;
      for (var i = 0, len = patterns.length; i < len; i++) {
        if ((first & patterns[i].mask) == patterns[i].pattern01) {
          pattern = patterns[i];
          break;
        }
      }
      if (pattern == null) {
        throw 'utf-8 decode error';
      }
      var rest = ary.slice(cur + 1, cur + pattern.bytes);
      cur += pattern.bytes;
      var code = '';
      code += ('00000000' + (first & (255 ^ pattern.mask)).toString(2)).slice(-pattern.data_length);
      for (var i = 0, len = rest.length; i < len; i++) {
        code += ('00000000' + (rest[i] & parseInt('111111', 2)).toString(2)).slice(-6);
      }
      codes.push(parseInt(code, 2));
    }
    return String.fromCharCode.apply(null, codes);
  };

})();

/**
 * Convert from an UTF-8 string to UTF-8 array.
 * @param {string} UTF-8 string
 * @return {array} UTF-8 array
 */
var utf82ary = (function() {

  var patterns = [
    {pattern: '0xxxxxxx', bytes: 1},
    {pattern: '110xxxxx', bytes: 2},
    {pattern: '1110xxxx', bytes: 3},
    {pattern: '11110xxx', bytes: 4},
    {pattern: '111110xx', bytes: 5},
    {pattern: '1111110x', bytes: 6}
  ];
  patterns.forEach(function(item) {
    item.header = item.pattern.replace(/[^10]/g, '');
    item.mask_length = item.header.length;
    item.data_length = 8 - item.header.length;
    item.max_bit_length = (item.bytes - 1) * 6 + item.data_length;
  });

  var code2utf8array = function(code) {
    var pattern = null;
    var code01 = code.toString(2);
    for (var i = 0, len = patterns.length; i < len; i++) {
      if (code01.length <= patterns[i].max_bit_length) {
        pattern = patterns[i];
        break;
      }
    }
    if (pattern == null) {
      throw 'utf-8 encode error';
    }
    var ary = [];
    for (var i = 0, len = pattern.bytes - 1; i < len; i++) {
      ary.unshift(parseInt('10' + ('000000' + code01.slice(-6)).slice(-6), 2));
      code01 = code01.slice(0, -6);
    }
    ary.unshift(parseInt(pattern.header + ('00000000' + code01).slice(-pattern.data_length), 2));
    return ary;
  };

  return function(str) {
    var codes = [];
    for (var i = 0, len = str.length; i < len; i++) {
      var code = str.charCodeAt(i);
      Array.prototype.push.apply(codes, code2utf8array(code));
    }
    return codes;
  };

})();

/**
 * Convert a string to an ArrayBuffer.
 * @param {string} string The string to convert.
 * @return {ArrayBuffer} An array buffer whose bytes correspond to the string.
 */
var stringToArrayBuffer = function(string) {
  var buffer = new ArrayBuffer(string.length);
  var bufferView = new Uint8Array(buffer);
  for (var i = 0; i < string.length; i++) {
    bufferView[i] = string.charCodeAt(i);
  }
  return buffer;
};

/**
 * An event source can dispatch events. These are dispatched to all of the
 * functions listening for that event type with arguments.
 * @constructor
 */
function EventSource() {
  this.listeners_ = {};
};

EventSource.prototype = {
  /**
   * Add |callback| as a listener for |type| events.
   * @param {string} type The type of the event.
   * @param {function(Object|undefined): boolean} callback The function to call
   *     when this event type is dispatched. Arguments depend on the event
   *     source and type. The function returns whether the event was "handled"
   *     which will prevent delivery to the rest of the listeners.
   */
  addEventListener: function(type, callback) {
    if (!this.listeners_[type])
      this.listeners_[type] = [];
    this.listeners_[type].push(callback);
  },

  /**
   * Remove |callback| as a listener for |type| events.
   * @param {string} type The type of the event.
   * @param {function(Object|undefined): boolean} callback The callback
   *     function to remove from the event listeners for events having type
   *     |type|.
   */
  removeEventListener: function(type, callback) {
    if (!this.listeners_[type])
      return;
    for (var i = this.listeners_[type].length - 1; i >= 0; i--) {
      if (this.listeners_[type][i] == callback) {
        this.listeners_[type].splice(i, 1);
      }
    }
  },

  /**
   * Dispatch an event to all listeners for events of type |type|.
   * @param {type} type The type of the event being dispatched.
   * @param {...Object} var_args The arguments to pass when calling the
   *     callback function.
   * @return {boolean} Returns true if the event was handled.
   */
  dispatchEvent: function(type, var_args) {
    if (!this.listeners_[type])
      return false;
    for (var i = 0; i < this.listeners_[type].length; i++) {
      if (this.listeners_[type][i].apply(
              /* this */ null,
              /* var_args */ Array.prototype.slice.call(arguments, 1))) {
        return true;
      }
    }
  }
};

/**
 * HttpServer provides a lightweight Http web server. Currently it only
 * supports GET requests and upgrading to other protocols (i.e. WebSockets).
 * @constructor
 */
function HttpServer() {
  EventSource.apply(this);
  this.readyState_ = 0;
}

HttpServer.prototype = {
  __proto__: EventSource.prototype,

  /**
   * Listen for connections on |port| using the interface |host|.
   * @param {number} port The port to listen for incoming connections on.
   * @param {string=} opt_host The host interface to listen for connections on.
   *     This will default to 0.0.0.0 if not specified which will listen on
   *     all interfaces.
   */
  listen: function(port, opt_host) {
    var t = this;
    chrome.sockets.tcpServer.create(function(socketInfo) {
      chrome.sockets.tcpServer.onAccept.addListener(function(acceptInfo) {
        if (acceptInfo.socketId === socketInfo.socketId)
          t.readRequestFromSocket_(new PSocket(acceptInfo.clientSocketId));
      });
      this.socketId = socketInfo.socketId;
      chrome.runtime.sendMessage({socketId: this.socketId});
      chrome.sockets.tcpServer.listen(
        socketInfo.socketId,
        opt_host || '0.0.0.0',
        port,
        50,
        function(result) {
          if (!result) {
            t.readyState_ = 1;
          }
          else {
            console.log(
              'listen error ' +
              chrome.runtime.lastError.message +
                ' (normal if another instance is already serving requests)');
          }
        });
    });
  },

  close: function() {
    if (this.socketId) {
      chrome.sockets.tcpServer.close(this.socketId);
      this.socketId = 0;
    }
  },
  readRequestFromSocket_: function(pSocket) {
    var t = this;
    var requestData = '';
    var endIndex = 0;
    var onDataRead = function(data) {
      requestData += arrayBufferToString(data).replace(/\r\n/g, '\n');
      // Check for end of request.
      endIndex = requestData.indexOf('\n\n', endIndex);
      if (endIndex == -1) {
        endIndex = requestData.length - 1;
        return pSocket.read().then(onDataRead);
      }

      var headers = requestData.substring(0, endIndex).split('\n');
      var headerMap = {};
      // headers[0] should be the Request-Line
      var requestLine = headers[0].split(' ');
      headerMap['method'] = requestLine[0];
      headerMap['url'] = requestLine[1];
      headerMap['Http-Version'] = requestLine[2];
      for (var i = 1; i < headers.length; i++) {
        requestLine = headers[i].split(':', 2);
        if (requestLine.length == 2)
          headerMap[requestLine[0]] = requestLine[1].trim();
      }
      var request = new HttpRequest(headerMap, pSocket);
      t.onRequest_(request);
    };

    pSocket.read().then(onDataRead).catch(function(e) {
      pSocket.close();
    });
  },

  onRequest_: function(request) {
    var type = request.headers['Upgrade'] ? 'upgrade' : 'request';
    var keepAlive = request.headers['Connection'] == 'keep-alive';
    if (!this.dispatchEvent(type, request))
      request.close();
    else if (keepAlive)
      this.readRequestFromSocket_(request.pSocket_);
  },
};

// MIME types for common extensions.
var extensionTypes = {
  'css': 'text/css',
  'html': 'text/html',
  'htm': 'text/html',
  'jpg': 'image/jpeg',
  'jpeg': 'image/jpeg',
  'js': 'text/javascript',
  'png': 'image/png',
  'svg': 'image/svg+xml',
  'txt': 'text/plain'};

/**
 * Constructs an HttpRequest object which tracks all of the request headers and
 * socket for an active Http request.
 * @param {Object} headers The HTTP request headers.
 * @param {Object} pSocket The socket to use for the response.
 * @constructor
 */
function HttpRequest(headers, pSocket) {
  this.version = 'HTTP/1.1';
  this.headers = headers;
  this.responseHeaders_ = {};
  this.headersSent = false;
  this.pSocket_ = pSocket;
  this.writes_ = 0;
  this.bytesRemaining = 0;
  this.finished_ = false;
  this.readyState = 1;
}

HttpRequest.prototype = {
  __proto__: EventSource.prototype,

  /**
   * Closes the Http request.
   */
  close: function() {
    // The socket for keep alive connections will be re-used by the server.
    // Just stop referencing or using the socket in this HttpRequest.
    if (this.headers['Connection'] != 'keep-alive')
      pSocket.close();

    this.pSocket_ = null;
    this.readyState = 3;
  },

  /**
   * Write the provided headers as a response to the request.
   * @param {int} responseCode The HTTP status code to respond with.
   * @param {Object} responseHeaders The response headers describing the
   *     response.
   */
  writeHead: function(responseCode, responseHeaders) {
    var headerString = this.version + ' ' + responseCode + ' ' +
        (responseMap[responseCode] || 'Unknown');
    this.responseHeaders_ = responseHeaders;
    if (this.headers['Connection'] == 'keep-alive')
      responseHeaders['Connection'] = 'keep-alive';
    if (!responseHeaders['Content-Length'] && responseHeaders['Connection'] == 'keep-alive')
      responseHeaders['Transfer-Encoding'] = 'chunked';
    for (var i in responseHeaders) {
      headerString += '\r\n' + i + ': ' + responseHeaders[i];
    }
    headerString += '\r\n\r\n';
    this.write_(stringToArrayBuffer(headerString));
  },

  /**
   * Writes data to the response stream.
   * @param {string|ArrayBuffer} data The data to write to the stream.
   */
  write: function(data) {
    if (this.responseHeaders_['Transfer-Encoding'] == 'chunked') {
      var newline = '\r\n';
      var byteLength = (data instanceof ArrayBuffer) ? data.byteLength : data.length;
      var chunkLength = byteLength.toString(16).toUpperCase() + newline;
      var buffer = new ArrayBuffer(chunkLength.length + byteLength + newline.length);
      var bufferView = new Uint8Array(buffer);
      for (var i = 0; i < chunkLength.length; i++)
        bufferView[i] = chunkLength.charCodeAt(i);
      if (data instanceof ArrayBuffer) {
        bufferView.set(new Uint8Array(data), chunkLength.length);
      } else {
        for (var i = 0; i < data.length; i++)
          bufferView[chunkLength.length + i] = data.charCodeAt(i);
      }
      for (var i = 0; i < newline.length; i++)
        bufferView[chunkLength.length + byteLength + i] = newline.charCodeAt(i);
      data = buffer;
    } else if (!(data instanceof ArrayBuffer)) {
      data = stringToArrayBuffer(data);
    }
    this.write_(data);
  },

  /**
   * Finishes the HTTP response writing |data| before closing.
   * @param {string|ArrayBuffer=} opt_data Optional data to write to the stream
   *     before closing it.
   */
  end: function(opt_data) {
    if (opt_data)
      this.write(opt_data);
    if (this.responseHeaders_['Transfer-Encoding'] == 'chunked')
      this.write('');
    this.finished_ = true;
    this.checkFinished_();
  },

  /**
   * Automatically serve the given |url| request.
   * @param {string} url The URL to fetch the file to be served from. This is
   *     retrieved via an XmlHttpRequest and served as the response to the
   *     request.
   */
  serveUrl: function(url) {
    var t = this;
    var xhr = new XMLHttpRequest();
    xhr.onloadend = function() {
      var type = 'text/plain';
      if (this.getResponseHeader('Content-Type')) {
        type = this.getResponseHeader('Content-Type');
      } else if (url.indexOf('.') != -1) {
        var extension = url.substr(url.indexOf('.') + 1);
        type = extensionTypes[extension] || type;
      }
      console.log('Served ' + url);
      var contentLength = this.getResponseHeader('Content-Length');
      if (xhr.status == 200)
        contentLength = (this.response && this.response.byteLength) || 0;
      t.writeHead(this.status, {
        'Content-Type': type,
        'Content-Length': contentLength});
      t.end(this.response);
    };
    xhr.open('GET', url, true);
    xhr.responseType = 'arraybuffer';
    xhr.send();
  },

  write_: function(array) {
    var t = this;
    this.bytesRemaining += array.byteLength;
    this.pSocket_.write(array).then(function(bytesWritten) {
      t.bytesRemaining -= bytesWritten;
      t.checkFinished_();
    }).catch(function(e) {
      console.error(e.message);
      return;
    });
  },

  checkFinished_: function() {
    if (!this.finished_ || this.bytesRemaining > 0)
      return;
    this.close();
  }
};

/**
 * Constructs a server which is capable of accepting WebSocket connections.
 * @param {HttpServer} httpServer The Http Server to listen and handle
 *     WebSocket upgrade requests on.
 * @constructor
 */
function WebSocketServer(httpServer) {
  EventSource.apply(this);
  httpServer.addEventListener('upgrade', this.upgradeToWebSocket_.bind(this));
}

WebSocketServer.prototype = {
  __proto__: EventSource.prototype,

  upgradeToWebSocket_: function(request) {
    if (request.headers['Upgrade'] != 'websocket' ||
        !request.headers['Sec-WebSocket-Key']) {
      return false;
    }

    if (this.dispatchEvent('request', new WebSocketRequest(request))) {
      if (request.pSocket_)
        request.reject();
      return true;
    }

    return false;
  }
};

/**
 * Constructs a WebSocket request object from an Http request. This invalidates
 * the Http request's socket and offers accept and reject methods for accepting
 * and rejecting the WebSocket upgrade request.
 * @param {HttpRequest} httpRequest The HTTP request to upgrade.
 */
function WebSocketRequest(httpRequest) {
  // We'll assume control of the socket for this request.
  HttpRequest.apply(this, [httpRequest.headers, httpRequest.pSocket_]);
  httpRequest.pSocket_ = null;
}

WebSocketRequest.prototype = {
  __proto__: HttpRequest.prototype,

  /**
   * Accepts the WebSocket request.
   * @return {WebSocketServerSocket} The websocket for the accepted request.
   */
  accept: function() {
    // Construct WebSocket response key.
    var clientKey = this.headers['Sec-WebSocket-Key'];
    var toArray = function(str) {
      var a = [];
      for (var i = 0; i < str.length; i++) {
        a.push(str.charCodeAt(i));
      }
      return a;
    }
    var toString = function(a) {
      var str = '';
      for (var i = 0; i < a.length; i++) {
        str += String.fromCharCode(a[i]);
      }
      return str;
    }

    // Magic string used for websocket connection key hashing:
    // http://en.wikipedia.org/wiki/WebSocket
    var magicStr = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

    // clientKey is base64 encoded key.
    clientKey += magicStr;
    var sha1 = new Sha1();
    sha1.reset();
    sha1.update(toArray(clientKey));
    var responseKey = btoa(toString(sha1.digest()));
    var responseHeader = {
      'Upgrade': 'websocket',
      'Connection': 'Upgrade',
      'Sec-WebSocket-Accept': responseKey};
    if (this.headers['Sec-WebSocket-Protocol'])
      responseHeader['Sec-WebSocket-Protocol'] = this.headers['Sec-WebSocket-Protocol'];
    this.writeHead(101, responseHeader);
    var socket = new WebSocketServerSocket(this.pSocket_);
    // Detach the socket so that we don't use it anymore.
    this.pSocket_ = 0;
    return socket;
  },

  /**
   * Rejects the WebSocket request, closing the connection.
   */
  reject: function() {
    this.close();
  }
}

/**
 * Constructs a WebSocketServerSocket using the given socketId. This should be
 * a socket which has already been upgraded from an Http request.
 * @param {number} socketId The socket id with an active websocket connection.
 */
function WebSocketServerSocket(pSocket) {
  this.pSocket_ = pSocket;
  this.readyState = 1;
  EventSource.apply(this);
  this.readFromSocket_();
}

WebSocketServerSocket.prototype = {
  __proto__: EventSource.prototype,

  /**
   * Send |data| on the WebSocket.
   * @param {string|Array.<number>|ArrayBuffer} data The data to send over the WebSocket.
   */
  send: function(data) {
    // WebSocket must specify opcode when send frame.
    // The opcode for data frame is 1(text) or 2(binary).
    if (typeof data == 'string' || data instanceof String) {
      this.sendFrame_(1, data);
    } else {
      this.sendFrame_(2, data);
    }
  },

  /**
   * Begin closing the WebSocket. Note that the WebSocket protocol uses a
   * handshake to close the connection, so this call will begin the closing
   * process.
   */
  close: function() {
    if (this.readyState === 1) {
      this.sendFrame_(8);
      this.readyState = 2;
    }
  },

  readFromSocket_: function() {
    var t = this;
    var data = [];
    var message = '';
    var fragmentedOp = 0;
    var fragmentedMessages = [];

    var onDataRead = function(dataBuffer) {
      var a = new Uint8Array(dataBuffer);
      for (var i = 0; i < a.length; i++)
        data.push(a[i]);

      while (data.length) {
        var length_code = -1;
        var data_start = 6;
        var mask;
        var fin = (data[0] & 128) >> 7;
        var op = data[0] & 15;

        if (data.length > 1)
          length_code = data[1] & 127;
        if (length_code > 125) {
          if ((length_code == 126 && data.length > 7) ||
              (length_code == 127 && data.length > 14)) {
            if (length_code == 126) {
              length_code = data[2] * 256 + data[3];
              mask = data.slice(4, 8);
              data_start = 8;
            } else if (length_code == 127) {
              length_code = 0;
              for (var i = 0; i < 8; i++) {
                length_code = length_code * 256 + data[2 + i];
              }
              mask = data.slice(10, 14);
              data_start = 14;
            }
          } else {
            length_code = -1; // Insufficient data to compute length
          }
        } else {
          if (data.length > 5)
            mask = data.slice(2, 6);
        }

        if (length_code > -1 && data.length >= data_start + length_code) {
          var decoded = data.slice(data_start, data_start + length_code).map(function(byte, index) {
            return byte ^ mask[index % 4];
          });
          if (op == 1) {
            decoded = ary2utf8(decoded);
          }
          data = data.slice(data_start + length_code);
          if (fin && op > 0) {
            // Unfragmented message.
            if (!t.onFrame_(op, decoded))
              return;
          } else {
            // Fragmented message.
            fragmentedOp = fragmentedOp || op;
            fragmentedMessages.push(decoded);
            if (fin) {
              var joinMessage = null;
              if (op == 1) {
                joinMessage = fragmentedMessagess.join('');
              } else {
                joinMessage = fragmentedMessages.reduce(function(pre, cur) {
                  return Array.prototype.push.apply(pre, cur);
                }, []);
              }
              if (!t.onFrame_(fragmentedOp, joinMessage))
                return;
              fragmentedOp = 0;
              fragmentedMessages = [];
            }
          }
        } else {
          break; // Insufficient data, wait for more.
        }
      }

      return t.pSocket_.read().then(onDataRead);
    };

    this.pSocket_.read().then(function(data) {
      return onDataRead(data);
    }).catch(function(e) {
      t.close_();
    });
  },

  onFrame_: function(op, data) {
    if (op == 1 || op == 2) {
      if (typeof data == 'string' || data instanceof String) {
        // Don't do anything.
      } else if (Array.isArray(data)) {
        data = new Uint8Array(data).buffer;
      } else if (data instanceof ArrayBuffer) {
        // Don't do anything.
      } else {
        data = data.buffer;
      }
      this.dispatchEvent('message', {'data': data});
    } else if (op == 8) {
      // A close message must be confirmed before the websocket is closed.
      if (this.readyState === 1) {
        this.sendFrame_(8);
        this.readyState = 2;
      } else {
        this.close_();
        return false;
      }
    }
    return true;
  },

  sendFrame_: function(op, data) {
    var t = this;
    var WebsocketFrameData = function(op, data) {
      var ary = data;
      if (typeof data == 'string' || data instanceof String) {
        ary = utf82ary(data);
      }
      if (Array.isArray(ary)) {
        ary = new Uint8Array(ary);
      }
      if (ary instanceof ArrayBuffer) {
        ary = new Uint8Array(ary);
      }
      ary = new Uint8Array(ary.buffer);
      var length = ary.length;
      if (ary.length > 65535)
        length += 10;
      else if (ary.length > 125)
        length += 4;
      else
        length += 2;
      var lengthBytes = 0;
      var buffer = new ArrayBuffer(length);
      var bv = new Uint8Array(buffer);
      bv[0] = 128 | (op & 15); // Fin and type text.
      bv[1] = ary.length > 65535 ? 127 :
              (ary.length > 125 ? 126 : ary.length);
      if (ary.length > 65535)
        lengthBytes = 8;
      else if (ary.length > 125)
        lengthBytes = 2;
      var len = ary.length;
      for (var i = lengthBytes - 1; i >= 0; i--) {
        bv[2 + i] = len & 255;
        len = len >> 8;
      }
      var dataStart = lengthBytes + 2;
      for (var i = 0; i < ary.length; i++) {
        bv[dataStart + i] = ary[i];
      }
      return buffer;
    }
    var array = WebsocketFrameData(op, data || '');
    this.pSocket_.write(array).then(function(bytesWritten) {
      if (bytesWritten !== array.byteLength)
        throw new Error('insufficient write');
    }).catch(function(e) {
      t.close_();
    });
  },

  close_: function() {
    if (this.readyState !== 3) {
      this.pSocket_.close();
      this.readyState = 3;
      this.dispatchEvent('close');
    }
  }
};

return {
  'Server': HttpServer,
  'WebSocketServer': WebSocketServer,
};
}();
