// Github: http://github.com/ncr/node.ws.js
// Compatible with node v0.1.91
// Author: Jacek Becela
// Contributors:
//   Michael Stillwell  http://github.com/ithinkihaveacat
//   Nick Chapman       http://github.com/nchapman
//   Dmitriy Shalashov  http://github.com/skaurus
//   Johan Dahlberg
//   Andreas Kompanez
//   Samuel Cyprian		http://github.com/samcyp
// License: MIT
// Based on: http://github.com/Guille/node.websocket.js

function nano(template, data) {
  return template.replace(/\{([\w\.]*)}/g, function (str, key) {
    var keys = key.split("."), value = data[keys.shift()];
    keys.forEach(function (key) { value = value[key];});
    return value;
  });
}

function pack(num) {
  var result = '';
  result += String.fromCharCode(num >> 24 & 0xFF);
  result += String.fromCharCode(num >> 16 & 0xFF);
  result += String.fromCharCode(num >> 8 & 0xFF);
  result += String.fromCharCode(num & 0xFF);
  return result;
}

var sys  = require("sys"),
  net    = require("net"),
  crypto = require("crypto"),
  requiredHeaders = {
    'get': /^GET (\/[^\s]*)/,
    'upgrade': /^WebSocket$/,
    'connection': /^Upgrade$/,
    'host': /^(.+)$/,
    'origin': /^(.+)$/
  },
  handshakeTemplate75 = [
    'HTTP/1.1 101 Web Socket Protocol Handshake', 
    'Upgrade: WebSocket', 
    'Connection: Upgrade',
    'WebSocket-Origin: {origin}',
    'WebSocket-Location: {protocol}://{host}{resource}',
    '',
    ''
  ].join("\r\n"),
  handshakeTemplate76 = [
    'HTTP/1.1 101 WebSocket Protocol Handshake', // note a diff here
    'Upgrade: WebSocket',
    'Connection: Upgrade',
    'Sec-WebSocket-Origin: {origin}',
    'Sec-WebSocket-Location: {protocol}://{host}{resource}',
    '',
    '{data}'
  ].join("\r\n"),
  flashPolicy = '<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>';



exports.createSecureServer = function (websocketListener, credentials, options) {
	if (!options) options = {};
	options.secure = credentials;
	return this.createServer(websocketListener, options);
};

exports.createServer = function (websocketListener, options) {
  if (!options) options = {};
  if (!options.flashPolicy) options.flashPolicy = flashPolicy;
  // The value should be a crypto credentials
  if (!options.secure) options.secure = null;

  return net.createServer(function (socket) {
	//Secure WebSockets
	var wsProtocol = 'ws';
	if(options.secure) {
	  wsProtocol = 'wss';
	  socket.setSecure(options.secure);
	}
    socket.setTimeout(0);
    socket.setNoDelay(true);
    socket.setKeepAlive(true, 0);

    var emitter = new process.EventEmitter(),
      handshaked = false,
      buffer = "";
      
    function handle(data) {
      buffer += data;
      
      var chunks = buffer.split("\ufffd"),
        count = chunks.length - 1; // last is "" or a partial packet
        
      for(var i = 0; i < count; i++) {
        var chunk = chunks[i];
        if(chunk[0] == "\u0000") {
          emitter.emit("data", chunk.slice(1));
        } else {
          socket.end();
          return;
        }
      }
      
      buffer = chunks[count];
    }

    function handshake(data) {
      var _headers = data.split("\r\n");

      if ( /<policy-file-request.*>/.exec(_headers[0]) ) {
        socket.write( options.flashPolicy );
        socket.end();
        return;
      }

      // go to more convenient hash form
      var headers = {}, upgradeHead, len = _headers.length;
      if ( _headers[0].match(/^GET /) ) {
        headers["get"] = _headers[0];
      } else {
        socket.end();
        return;
      }
      if ( _headers[ _headers.length - 1 ] ) {
        upgradeHead = _headers[ _headers.length - 1 ];
        len--;
      }
      while (--len) { // _headers[0] will be skipped
        var header = _headers[len];
        if (!header) continue;

        var split = header.split(": ", 2); // second parameter actually seems to not work in node
        headers[ split[0].toLowerCase() ] = split[1];
      }

      // check if we have all needed headers and fetch data from them
      var data = {}, match;
      for (var header in requiredHeaders) {
        //           regexp                          actual header value
        if ( match = requiredHeaders[ header ].exec( headers[header] ) ) {
          data[header] = match;
        } else {
          socket.end();
          return;
        }
      }

      // draft auto-sensing
      if ( headers["sec-websocket-key1"] && headers["sec-websocket-key2"] && upgradeHead ) { // 76
        var strkey1 = headers["sec-websocket-key1"]
          , strkey2 = headers["sec-websocket-key2"]

          , numkey1 = parseInt(strkey1.replace(/[^\d]/g, ""), 10)
          , numkey2 = parseInt(strkey2.replace(/[^\d]/g, ""), 10)

          , spaces1 = strkey1.replace(/[^\ ]/g, "").length
          , spaces2 = strkey2.replace(/[^\ ]/g, "").length;

        if (spaces1 == 0 || spaces2 == 0 || numkey1 % spaces1 != 0 || numkey2 % spaces2 != 0) {
          socket.end();
          return;
        }

        var hash = crypto.createHash("md5")
        , key1 = pack(parseInt(numkey1/spaces1))
        , key2 = pack(parseInt(numkey2/spaces2));
        
        hash.update(key1);
        hash.update(key2);
        hash.update(upgradeHead);

        socket.write(nano(handshakeTemplate76, {
          protocol: wsProtocol,
          resource: data.get[1],
          host:     data.host[1],
          origin:   data.origin[1],
          data:     hash.digest("binary")
        }), "binary");

      } else { // 75
        socket.write(nano(handshakeTemplate75, {
          protocol: wsProtocol,
          resource: data.get[1],
          host:     data.host[1],
          origin:   data.origin[1]
        }));

      }

      handshaked = true;
      emitter.emit("connect", data.get[1]);
    }

    socket.addListener("data", function (data) {
      if(handshaked) {
        handle(data.toString("utf8"));
      } else {
        handshake(data.toString("binary")); // because of draft76 handshakes
      }
    }).addListener("end", function () {
      socket.end();
    }).addListener("close", function () {
      if (handshaked) { // don't emit close from policy-requests
        emitter.emit("close");
      }
    }).addListener("error", function (exception) {
      if (emitter.listeners("error").length > 0) {
        emitter.emit("error", exception);
      } else {
        throw exception;
      }
    });

    emitter.remoteAddress = socket.remoteAddress;
    
    emitter.write = function (data) {
      try {
        socket.write('\u0000', 'binary');
        socket.write(data, 'utf8');
        socket.write('\uffff', 'binary');
      } catch(e) { 
        // Socket not open for writing, 
        // should get "close" event just before.
        socket.end();
      }
    };
    
    emitter.end = function () {
      socket.end();
    };
    
    websocketListener(emitter); // emits: "connect", "data", "close", provides: write(data), end()
  });
};