var util = require('util');
var assert = require('assert');
var spdy = require('../spdy');
var constants = spdy.protocol.constants;

var Stream = spdy.Stream;

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

  var state = {};
  this._spdyState = state;

  // NOTE: There's a big trick here. Connection is used as a `this` argument
  // to the wrapped `connection` event listener.
  // socket end doesn't necessarly mean connection drop
  this.httpAllowHalfOpen = true;

  // Socket timeout
  this.timeout = server && server.timeout || 0;

  // Defaults
  state.maxStreams = options.maxStreams || Infinity;
  state.initialSinkSize = constants.DEFAULT_WINDOW;
  state.initialWindowSize = options.windowSize || 1 << 20;
  state.autoSpdy31 = options.autoSpdy31;

  // Connection-level flow control
  state.sinkSize = state.initialSinkSize;
  state.windowSize = constants.DEFAULT_WINDOW;

  // Interleaving configuration
  state.maxChunk = options.maxChunk === undefined ? 8 * 1024 : options.maxChunk;

  // Various state info
  state.closed = false;
  state.pool = spdy.zlibpool.create(options.headerCompression);
  state.counters = {
    pushCount: 0,
    streamCount: 0
  };
  state.socketBuffering = false;

  state.version = null;
  state.deflate = null;
  state.inflate = null;

  // Init streams list
  state.isServer = options.isServer;
  state.streams = {};
  state.streamCount = 0;
  state.lastId = 0;
  state.pushId = 0;
  state.pingId = state.isServer ? 0 : 1;
  state.pings = {};
  state.goaway = false;

  // X-Forwarded feature
  state.xForward = null;

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

  // Create parser and hole for framer
  state.parser = spdy.protocol.parser.create(this);
  state.framer = spdy.protocol.framer.create();

  // Lock data
  state.locked = false;
  state.lockQueue = [];

  this.socket = socket;
  this.encrypted = socket.encrypted;
  this.readable = true;
  this.writable = true;

  this._init();
}
util.inherits(Connection, process.EventEmitter);
exports.Connection = Connection;

Connection.prototype._init = function init() {
  var self = this;
  var state = this._spdyState;
  var pool = state.pool;
  var pair = null;
  var sentSettings = false;

  // Initialize parser
  this._spdyState.parser.on('frame', this._handleFrame.bind(this));

  this._spdyState.parser.on('version', function onversion(version) {
    var prev = state.version;
    var framer = state.framer;

    state.version = version;

    // Ignore transition to 3.1
    if (!prev) {
      pair = pool.get('spdy/' + version);
      state.deflate = pair.deflate;
      state.inflate = pair.inflate;
      framer.setCompression(pair.deflate, pair.inflate);
    }
    framer.setVersion(version);

    // Send settings frame (once)
    if (!sentSettings) {
      sentSettings = true;
      framer.settingsFrame({
        maxStreams: state.maxStreams,
        windowSize: state.initialWindowSize
      }, function(err, frame) {
        if (err)
          return self.emit('error', err);
        self.write(frame);
      });

      // Send WINDOW_UPDATE for 3.1
      self._sendWindowUpdate(true);
    }
  });

  // Propagate parser errors
  state.parser.on('error', function onParserError(err) {
    self.emit('error', err);
  });

  this.socket.pipe(state.parser);

  // 2 minutes socket timeout
  this.socket.setTimeout(this.timeout);
  this.socket.once('timeout', function ontimeout() {
    self.socket.destroy();
  });

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

  this.socket.once('close', function onclose() {
    var err = new Error('socket hang up');
    err.code = 'ECONNRESET';
    self._destroyStreams(err);
    self.emit('close');

    state.closed = true;
    if (pair)
      pool.put(pair);
  });

  // Do not allow half-open connections
  this.socket.allowHalfOpen = false;

  if (spdy.utils.isLegacy) {
    this.socket.on('drain', function ondrain() {
      self.emit('drain');
    });
  }

  // for both legacy and non-legacy, when our socket is ready for writes again,
  //   drain the sinks in case they were queuing due to socketBuffering.
  this.socket.on('drain', function () {
    state.socketBuffering = false;
    Object.keys(state.streams).forEach(function(id) {
      state.streams[id]._drainSink(0);
    });
  });
};

//
// ### function handleFrame (frame)
// #### @frame {Object} SPDY frame
//
Connection.prototype._handleFrame = function handleFrame(frame) {
  var state = this._spdyState;

  if (state.closed)
    return;

  var stream;

  // Create new stream
  if (frame.type === 'SYN_STREAM') {
    stream = this._handleSynStream(frame);
  } else {
    if (frame.id !== undefined) {
      // Load created one
      stream = state.streams[frame.id];

      // Fail if not found
      if (stream === undefined &&
          !(frame.type === 'WINDOW_UPDATE' && frame.id === 0)) {
        if (frame.type === 'RST_STREAM')
          return;
        this._rst(frame.id, constants.rst.INVALID_STREAM);
        return;
      }
    }

    // Emit 'data' event
    if (frame.type === 'DATA') {
      this._handleData(stream, frame);
    // Reply for client stream
    } else if (frame.type === 'SYN_REPLY') {
      // If stream not client - send RST
      if (!stream._spdyState.isClient) {
        this._rst(frame.id, constants.rst.PROTOCOL_ERROR);
        return;
      }

      stream._handleResponse(frame);

    // Destroy stream if we were asked to do this
    } else if (frame.type === 'RST_STREAM') {
      stream._spdyState.rstCode = 0;
      stream._spdyState.closedBy.us = true;
      stream._spdyState.closedBy.them = true;

      // Emit error on destroy
      var err = new Error('Received rst: ' + frame.status);
      err.code = 'RST_STREAM';
      err.status = frame.status;
      stream.destroy(err);
    // Respond with same PING
    } else if (frame.type === 'PING') {
      this._handlePing(frame.pingId);
    } else if (frame.type === 'SETTINGS') {
      this._setDefaultWindow(frame.settings);
    } else if (frame.type === 'GOAWAY') {
      state.goaway = frame.lastId;
      this.writable = false;
    } else if (frame.type === 'WINDOW_UPDATE') {
      if (stream)
        stream._drainSink(frame.delta);
      else
        this._drainSink(frame.delta);
    } else if (frame.type === 'HEADERS') {
      stream.emit('headers', frame.headers);
    } else if (frame.type === 'X_FORWARDED') {
      state.xForward = frame.host;
    } else {
      console.error('Unknown type: ', frame.type);
    }
  }

  // Handle half-closed
  if (frame.fin) {
    // Don't allow to close stream twice
    if (stream._spdyState.closedBy.them) {
      stream._spdyState.rstCode = constants.rst.PROTOCOL_ERROR;
      stream.emit('error', 'Already half-closed');
    } else {
      stream._spdyState.closedBy.them = true;

      // Receive FIN
      stream._recvEnd();

      stream._handleClose();
    }
  }
};

//
// ### function handleSynStream (frame)
// #### @frame {Object} SPDY frame
//
Connection.prototype._handleSynStream = function handleSynStream(frame) {
  var state = this._spdyState;
  var associated;

  if (state.goaway && state.goaway < frame.id)
    return this._rst(frame.id, constants.rst.REFUSED_STREAM);

  // PUSH stream
  if (!state.isServer) {
    // Incorrect frame id
    if (frame.id % 2 === 1 || frame.associated % 2 === 0)
      return this._rst(frame.id, constants.rst.PROTOCOL_ERROR);

    associated = state.streams[frame.associated];

    // Fail if not found
    if (associated === undefined) {
      if (frame.type === 'RST_STREAM')
        return;
      this._rst(frame.id, constants.rst.INVALID_STREAM);
      return;
    }
  }

  var stream = new Stream(this, frame);
  this._addStream(stream);

  // Associate streams
  if (associated) {
    stream.associated = associated;
  }

  // TODO(indutny) handle stream limit
  this.emit('stream', stream);
  stream._start(frame.url, frame.headers);

  return stream;
};

//
// ### function _handleData (stream, frame)
// #### @stream {Stream} SPDY Stream
// #### @frame {Object} SPDY frame
//
Connection.prototype._handleData = function handleData(stream, frame) {
  var state = this._spdyState;

  if (frame.data.length > 0) {
    if (stream._spdyState.closedBy.them) {
      stream._spdyState.rstCode = constants.rst.PROTOCOL_ERROR;
      stream.emit('error', 'Writing to half-closed stream');
    } else {
      // Connection-level flow control
      state.windowSize -= frame.data.length;
      this._sendWindowUpdate();
      stream._recv(frame.data);
    }
  }
};

//
// ### function sendWindowUpdate (force)
// #### @force {Boolean} send even if windowSize is positive
// Send WINDOW_UPDATE if needed
//
Connection.prototype._sendWindowUpdate = function sendWindowUpdate(force) {
  var state = this._spdyState;

  if (state.version < 3.1 && (!state.isServer || !state.autoSpdy31) ||
      state.windowSize > state.initialWindowSize / 2 && !force) {
    return;
  }

  var self = this;
  var delta = state.initialWindowSize - state.windowSize;
  if (delta === 0)
    return;

  state.windowSize += delta;
  state.framer.windowUpdateFrame(0, delta, function(err, frame) {
    if (err)
      return self.emit('error', err);
    self.write(frame);
  });
};

//
// ### function _drainSink (delta)
// #### @delta {Number} WINDOW_UPDATE delta value
//
Connection.prototype._drainSink = function drainSink(delta) {
  var state = this._spdyState;

  // Switch to 3.1
  if (state.version !== 3.1) {
    this._setVersion(3.1);
    this._sendWindowUpdate();
  }

  state.sinkSize += delta;

  if (state.sinkSize <= 0)
    return;

  this.emit('drain');

  // Try to write all pending data to the socket
  Object.keys(state.streams).forEach(function(id) {
    state.streams[id]._drainSink(0);
  });
};

//
// ### function setVersion (version)
// #### @version {Number} Protocol version
// Set protocol version to use
//
Connection.prototype._setVersion = function setVersion(version) {
  this._spdyState.parser.setVersion(version);
};

//
// ### function addStream (stream)
// #### @stream {Stream}
//
Connection.prototype._addStream = function addStream(stream) {
  var state = this._spdyState;
  var id = stream._spdyState.id;
  if (state.streams[id])
    return;
  state.streams[id] = stream;

  // Update lastId
  state.lastId = Math.max(state.lastId, id);

  var isClient = id % 2 == 1;
  if (isClient && state.isServer || !isClient && !state.isServer)
    state.streamCount++;
};

//
// ### function removeStream (stream)
// #### @stream {Stream}
//
Connection.prototype._removeStream = function removeStream(stream) {
  var state = this._spdyState;
  var id = stream._spdyState.id;
  if (!state.streams[id])
    return;

  delete state.streams[id];

  var isClient = id % 2 == 1;
  if (isClient && state.isServer || !isClient && !state.isServer)
    state.streamCount--;

  if (!state.isServer &&
      state.goaway &&
      state.streamCount === 0 &&
      this.socket) {
    this.socket.destroySoon();
  }
};

//
// ### function destroyStreams (err)
// #### @err {Error} *optional*
// Destroys all active streams
//
Connection.prototype._destroyStreams = function destroyStreams(err) {
  var state = this._spdyState;
  var streams = state.streams;
  state.streams = {};
  state.streamCount = 0;
  Object.keys(streams).forEach(function(id) {
    streams[id].destroy();
  });
};

//
// ### function _rst (streamId, code, extra)
// #### @streamId {Number}
// #### @code {Number}
// #### @extra {String}
// Send RST frame
//
Connection.prototype._rst = function rst(streamId, code, extra) {
  var self = this;
  this._spdyState.framer.rstFrame(streamId, code, extra, function(err, frame) {
    if (err)
      return self.emit('error', err);
    self.write(frame);
  });
};

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

  var state = this._spdyState;
  if (state.locked) {
    state.lockQueue.push(callback);
  } else {
    state.locked = true;
    callback(null);
  }
};

//
// ### function unlock ()
// Release lock and call all buffered callbacks
//
Connection.prototype._unlock = function unlock() {
  var state = this._spdyState;
  if (state.locked) {
    if (state.lockQueue.length) {
      var cb = state.lockQueue.shift();
      cb(null);
    } else {
      state.locked = false;
    }
  }
};

//
// ### 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) {
    var wroteThrough = this.socket.write(data, encoding);
    // if write returns false, the socket layer is buffering the data, so let's
    //   set a flag that we can use to create some backpressure
    if (!wroteThrough)
      this._spdyState.socketBuffering = true;
    return wroteThrough;
  }
};

//
// ### function _setDefaultWindow (settings)
// #### @settings {Object}
// Update the default transfer window -- in the connection and in the
// active streams
//
Connection.prototype._setDefaultWindow = function _setDefaultWindow(settings) {
  if (!settings)
    return;
  if (!settings.initial_window_size ||
      settings.initial_window_size.persisted) {
    return;
  }
  var state = this._spdyState;
  state.initialSinkSize = settings.initial_window_size.value;

  Object.keys(state.streams).forEach(function(id) {
    state.streams[id]._updateSinkSize(settings.initial_window_size.value);
  });
};

//
// ### function handlePing (id)
// #### @id {Number} PING id
//
Connection.prototype._handlePing = function handlePing(id) {
  var self = this;
  var state = this._spdyState;

  var ours = state.isServer && (id % 2 === 0) ||
             !state.isServer && (id % 2 === 1);

  // Handle incoming PING
  if (!ours) {
    state.framer.pingFrame(id, function(err, frame) {
      if (err)
        return self.emit('error', err);
      self.write(frame);
    });
    return;
  }

  // Handle reply PING
  if (!state.pings[id])
    return;
  var ping = state.pings[id];
  delete state.pings[id];

  if (ping.cb)
    ping.cb(null);
};

//
// ### function ping (callback)
// #### @callback {Function}
// Send PING frame and invoke callback once received it back
//
Connection.prototype.ping = function ping(callback) {
  var self = this;
  var state = this._spdyState;
  var id = state.pingId;

  state.pingId += 2;

  state.framer.pingFrame(id, function(err, frame) {
    if (err)
      return self.emit('error', err);

    state.pings[id] = { cb: callback };
    self.write(frame);
  });
};

//
// ### function getCounter (name)
// #### @name {String} Counter name
// Get counter value
//
Connection.prototype.getCounter = function getCounter(name) {
  return this._spdyState.counters[name];
};

//
// ### function cork ()
// Accumulate data before writing out
//
Connection.prototype.cork = function cork() {
  if (this.socket && this.socket.cork)
    this.socket.cork();
};

//
// ### function uncork ()
// Write out accumulated data
//
Connection.prototype.uncork = function uncork() {
  if (this.socket && this.socket.uncork)
    this.socket.uncork();
};

Connection.prototype.end = function end() {
  var self = this;
  var state = this._spdyState;

  state.framer.goawayFrame(state.lastId,
                           constants.goaway.OK,
                           function(err, frame) {
    if (err)
      return self.emit('error', err);

    self.write(frame, function() {
      state.goaway = state.lastId;

      // Destroy socket if there are no streams
      if (!state.isServer &&
          state.goaway &&
          state.streamCount === 0 &&
          self.socket) {
        self.socket.destroySoon();
      }
    });
  });
};
