/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var spdy = require('../spdy'),
    util = require('util'),
    https = require('https'),
    stream = require('stream'),
    Buffer = require('buffer').Buffer;

//
// ### function instantiate (HTTPSServer)
// #### @HTTPSServer {https.Server|Function} Base server class
// Will return constructor for SPDY Server, based on the HTTPSServer class
//
function instantiate(HTTPSServer) {
  //
  // ### function Server (options, requestListener)
  // #### @options {Object} tls server options
  // #### @requestListener {Function} (optional) request callback
  // SPDY Server @constructor
  //
  function Server(options, requestListener) {
    if (!options) options = {};
    if (!options.maxStreams) options.maxStreams = 100;
    options.NPNProtocols = ['spdy/2', 'http/1.1', 'http/1.0'];

    HTTPSServer.call(this, options, requestListener);

    // Use https if NPN is not supported
    if (!process.features.tls_npn && !options.debug) return;

    // Wrap connection handler
    var self = this,
        connectionHandler = this.listeners('secureConnection')[0];

    var pool = spdy.zlibpool.create();

    this.removeAllListeners('secureConnection');
    this.on('secureConnection', function secureConnection(socket) {
      // Fallback to HTTPS if needed
      if (socket.npnProtocol !== 'spdy/2' && !options.debug) {
        return connectionHandler.call(this, socket);
      }

      // Wrap incoming socket into abstract class
      var connection = new Connection(socket, pool, options);

      // Emulate each stream like connection
      connection.on('stream', connectionHandler);

      connection.on('request', function(req, res) {
        res._renderHeaders = spdy.response._renderHeaders;
        res.writeHead = spdy.response.writeHead;
        res.push = spdy.response.push;
        res.streamID = req.streamID = req.socket.id;
        res.isSpdy = req.isSpdy = true;

        res.on('finish', function() {
          req.connection.end();
        });
        self.emit('request', req, res);
      });

      connection.on('error', function(e) {
        socket.destroy(e.errno === 'EPIPE' ? undefined : e);
      });
    });
  }
  util.inherits(Server, HTTPSServer);

  return Server;
}
exports.instantiate = instantiate;

// Export Server instantiated from https.Server
var Server = instantiate(https.Server);
exports.Server = Server;

//
// ### function create (base, options, requestListener)
// #### @base {Function} (optional) base server class (https.Server)
// #### @options {Object} tls server options
// #### @requestListener {Function} (optional) request callback
// @constructor wrapper
//
exports.create = function create(base, options, requestListener) {
  var server;
  if (typeof base === 'function') {
    server = instantiate(base);
  } else {
    server = Server;

    requestListener = options;
    options = base;
    base = null;
  }

  return new server(options, requestListener);
};

//
// ### function Connection (socket, pool, options)
// #### @socket {net.Socket} server's connection
// #### @pool {spdy.ZlibPool} zlib pool
// #### @options {Object} server's options
// Abstract connection @constructor
//
function Connection(socket, pool, options) {
  process.EventEmitter.call(this);

  var self = this;

  this._closed = false;

  this.pool = pool;
  var pair = pool.get();

  this.deflate = pair.deflate;
  this.inflate = pair.inflate;

  // Init streams list
  this.streams = {};
  this.streamsCount = 0;
  this.pushId = 0;
  this.goaway = false;

  this.framer = null;

  // Initialize parser
  this.parser = spdy.parser.create(this, this.deflate, this.inflate);
  this.parser.on('frame', function (frame) {
    if (this._closed) return;

    var stream;

    // Create new stream
    if (frame.type === 'SYN_STREAM') {
      self.streamsCount++;

      stream = self.streams[frame.id] = new Stream(self, frame);

      // If we reached stream limit
      if (self.streamsCount > options.maxStreams) {
        stream.once('error', function() {});
        // REFUSED_STREAM
        stream.rstCode = 3;
        stream.destroy(true);
      } else {
        self.emit('stream', stream);

        stream.init();
      }
    } else {
      if (frame.id) {
        // Load created one
        stream = self.streams[frame.id];

        // Fail if not found
        if (stream === undefined) {
          if (frame.type === 'RST_STREAM') return;
          return self.emit('error', 'Stream ' + frame.id + ' not found');
        }
      }

      // Emit 'data' event
      if (frame.type === 'DATA') {
        if (frame.data.length > 0){
          if (stream.closedBy.client) {
            stream.rstCode = 2;
            stream.emit('error', 'Writing to half-closed stream');
          } else {
            stream._read(frame.data);
          }
        }
      // Destroy stream if we was asked to do this
      } else if (frame.type === 'RST_STREAM') {
        stream.rstCode = 0;
        if (frame.status === 5) {
          // If client "cancels" connection - close stream and
          // all associated push streams without error
          stream.pushes.forEach(function(stream) {
            stream.close();
          });
          stream.close();
        } else {
          // Emit error on destroy
          stream.destroy(new Error('Received rst: ' + frame.status));
        }
      // Respond with same PING
      } else if (frame.type === 'PING') {
        self.write(self.framer.pingFrame(frame.pingId));
      // Ignore SETTINGS for now
      } else if (frame.type === 'SETTINGS' || frame.type === 'NOOP') {
        // TODO: Handle something?
      } else if (frame.type === 'GOAWAY') {
        self.goaway = frame.lastId;
      } else {
        console.error('Unknown type: ', frame.type);
      }
    }

    // Handle half-closed
    if (frame.fin) {
      // Don't allow to close stream twice
      if (stream.closedBy.client) {
        stream.rstCode = 2;
        stream.emit('error', 'Already half-closed');
      } else {
        stream.closedBy.client = true;
        stream.handleClose();
      }
    }
  });

  this.parser.on('_framer', function(framer) {
    // Generate custom settings frame and send
    self.write(
      framer.maxStreamsFrame(options.maxStreams)
    );
  });

  // Initialize scheduler
  this.scheduler = spdy.scheduler.create(this);

  // Store socket and pipe it to parser
  this.socket = socket;

  socket.pipe(this.parser);

  // 2 minutes socket timeout
  socket.setTimeout(2 * 60 * 1000);
  socket.once('timeout', function() {
    socket.destroy();
  });

  // Allow high-level api to catch socket errors
  socket.on('error', function(e) {
    self.emit('error', e);
  });

  socket.on('close', function() {
    self._closed = true;
    pool.put(pair);
  });

  socket.on('drain', function () {
    self.emit('drain');
  });
}
util.inherits(Connection, process.EventEmitter);
exports.Connection = Connection;

//
// ### function write (data, encoding)
// #### @data {String|Buffer} data
// #### @encoding {String} (optional) encoding
// Writes data to socket
//
Connection.prototype.write = function write(data, encoding) {
  if (this.socket.writable) {
    return this.socket.write(data, encoding);
  }
};

//
// ### function Stream (connection, frame)
// #### @connection {Connection} SPDY Connection
// #### @frame {Object} SYN_STREAM data
// Abstract stream @constructor
//
function Stream(connection, frame) {
  var self = this;
  stream.Stream.call(this);

  this.connection = connection;
  this.framer = connection.framer;

  this.ondata = this.onend = null;

  // RST_STREAM code if any
  this.rstCode = 1;
  this._destroyed = false;

  this.closedBy = {
    client: false,
    server: false
  };

  // Lock data
  this.locked = false;
  this.lockBuffer = [];

  // Store id
  this.id = frame.id;

  // Store priority
  this.priority = frame.priority;

  // Array of push streams associated to that one
  this.pushes = [];

  this._paused = false;
  this._buffer = [];

  // Create compression streams
  this.deflate = connection.deflate;
  this.inflate = connection.inflate;

  // Store headers
  this.headers = frame.headers;

  this.frame = frame;

  this.readable = this.writable = true;
}
util.inherits(Stream, stream.Stream);
exports.Stream = Stream;

//
// ### function isGoaway ()
// Returns true if any writes to that stream should be ignored
//
Stream.prototype.isGoaway = function isGoaway() {
  return this.connection.goaway && this.id > this.connection.goaway;
};

//
// ### function init ()
// Initialize stream, internal
//
Stream.prototype.init = function init() {
  var headers = this.headers,
      req = [headers.method + ' ' + headers.url + ' ' + headers.version];

  Object.keys(headers).forEach(function (key) {
    if (key !== 'method' && key !== 'url' && key !== 'version' &&
        key !== 'scheme') {
      req.push(key + ': ' + headers[key]);
    }
  });

  // Add '\r\n\r\n'
  req.push('', '');

  req = new Buffer(req.join('\r\n'));

  this._read(req);
};

//
// ### function lock (callback)
// #### @callback {Function} continuation callback
// Acquire lock
//
Stream.prototype.lock = function lock(callback) {
  if (!callback) return;

  if (this.locked) {
    this.lockBuffer.push(callback);
  } else {
    this.locked = true;
    callback.call(this, null);
  }
};

//
// ### function unlock ()
// Release lock and call all buffered callbacks
//
Stream.prototype.unlock = function unlock() {
  if (this.locked) {
    this.locked = false;
    this.lock(this.lockBuffer.shift());
  }
};

//
// ### function setTimeout ()
// TODO: use timers.enroll, timers.active, timers.unenroll
//
Stream.prototype.setTimeout = function setTimeout(time) {};

//
// ### function handleClose ()
// Close stream if it was closed by both server and client
//
Stream.prototype.handleClose = function handleClose() {
  if (this.closedBy.client && this.closedBy.server) {
    this.close();
  }
};

//
// ### function close ()
// Destroys stream
//
Stream.prototype.close = function close() {
  this.destroy();
};

//
// ### function destroy (error)
// #### @error {Error} (optional) error
// Destroys stream
//
Stream.prototype.destroy = function destroy(error) {
  if (this._destroyed) return;
  this._destroyed = true;

  delete this.connection.streams[this.id];
  this.connection.streamsCount--;

  if (error) {
    if (this.rstCode) {
      this.connection.write(this.framer.rstFrame(this.id, this.rstCode));
    }
  }

  var self = this;
  process.nextTick(function() {
    if (error) self.emit('error', error);
    self.emit('close');
  });
};

//
// ### function _writeData (fin, buffer)
// #### @fin {Boolean}
// #### @buffer {Buffer}
// Internal function
//
Stream.prototype._writeData = function _writeData(fin, buffer) {
  this.lock(function() {
    var stream = this,
        frame = this.framer.dataFrame(this.id, fin, buffer);

    stream.connection.scheduler.schedule(stream, frame);
    stream.connection.scheduler.tick();

    if (fin) this.close();

    this.unlock();
  });
};

//
// ### function write (data, encoding)
// #### @data {Buffer|String} data
// #### @encoding {String} data encoding
// Writes data to connection
//
Stream.prototype.write = function write(data, encoding) {
  // Do not send data to new connections after GOAWAY
  if (this.isGoaway()) return;

  var buffer;

  // Send DATA
  if (typeof data === 'string') {
    buffer = new Buffer(data, encoding);
  } else {
    buffer = data;
  }

  // TCP Frame size is equal to:
  // data frame size + data frame headers (8 bytes) +
  // tls frame headers and data (encrypted) ~ 140 bytes
  // So setting MTU equal to 1300 is most optimal
  // as end packet will have size ~ 1442 bytes
  // (My MTU is 1492 bytes)
  var MTU = 1300;

  // Try to fit MTU
  if (buffer.length < MTU) {
    this._writeData(false, buffer);
  } else {
    var len = buffer.length - MTU;
    for (var offset = 0; offset < len; offset += MTU) {
      this._writeData(false, buffer.slice(offset, offset + MTU));
    }
    this._writeData(false, buffer.slice(offset));
  }
};

//
// ### function end (data)
// #### @data {Buffer|String} (optional) data to write before ending stream
// #### @encoding {String} (optional) string encoding
// Send FIN data frame
//
Stream.prototype.end = function end(data, encoding) {
  // Do not send data to new connections after GOAWAY
  if (this.isGoaway()) return;

  if (data) this.write(data, encoding);

  this._writeData(true, []);
  this.closedBy.server = true;
  this.handleClose();
};

//
// ### function pause ()
// Start buffering all data
//
Stream.prototype.pause = function pause() {
  if (this._paused) return;
  this._paused = true;
};

//
// ### function resume ()
// Start buffering all data
//
Stream.prototype.resume = function resume() {
  if (!this._paused) return;
  this._paused = false;

  var self = this,
      buffer = this._buffer;

  this._buffer = [];

  process.nextTick(function() {
    buffer.forEach(function(chunk) {
      self._read(chunk);
    });
  });
};

//
// ### function _read (data)
// #### @data {Buffer} buffer to read
// (internal)
//
Stream.prototype._read = function _read(data) {
  if (this._paused) {
    this._buffer.push(data);
    return;
  }

  var self = this;
  process.nextTick(function() {
    if (self.ondata) self.ondata(data, 0, data.length);
    self.emit('data', data);
  });
};
