var parser = exports;

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

var legacy = !stream.Duplex;

if (legacy) {
  var DuplexStream = stream;
} else {
  var DuplexStream = stream.Duplex;
}

//
// ### function Parser (connection)
// #### @connection {spdy.Connection} connection
// SPDY protocol frames parser's @constructor
//
function Parser(connection) {
  DuplexStream.call(this);

  this.paused = false;
  this.buffer = [];
  this.buffered = 0;
  this.waiting = 8;

  this.state = { type: 'frame-head' };
  this.socket = connection.socket;
  this.connection = connection;

  this.version = null;
  this.deflate = null;
  this.inflate = null;

  this.connection = connection;

  if (legacy) {
    this.readable = this.writable = true;
  }
}
util.inherits(Parser, DuplexStream);

//
// ### function create (connection)
// #### @connection {spdy.Connection} connection
// @constructor wrapper
//
parser.create = function create(connection) {
  return new Parser(connection);
};

//
// ### function destroy ()
// Just a stub.
//
Parser.prototype.destroy = function destroy() {
};

Parser.prototype._slice = function _slice(waiting) {
  var buffer = new Buffer(waiting);
  var sliced = 0;
  var offset = 0;

  while (waiting > offset && sliced < this.buffer.length) {
    var chunk = this.buffer[sliced++],
        overmatched = false;

    // Copy chunk into `buffer`
    if (chunk.length > waiting - offset) {
      chunk.copy(buffer, offset, 0, waiting - offset);

      this.buffer[--sliced] = chunk.slice(waiting - offset);
      this.buffered += this.buffer[sliced].length;

      overmatched = true;
    } else {
      chunk.copy(buffer, offset);
    }

    // Move offset and decrease amount of buffered data
    offset += chunk.length;
    this.buffered -= chunk.length;

    if (overmatched) break;
  }

  // Remove used buffers
  this.buffer = this.buffer.slice(sliced);

  return buffer;
};

//
// ### function _write (data, encoding, cb)
// #### @data {Buffer} chunk of data
// #### @encoding {Null} encoding
// #### @cb {Function} callback
// Writes or buffers data to parser
//
Parser.prototype._write = function write(data, encoding, cb) {
  // Legacy compatibility
  if (!cb) cb = function() {};

  if (data !== undefined) {
    // Buffer data
    this.buffer.push(data);
    this.buffered += data.length;
  }

  // Notify caller about state (for piping)
  if (this.paused) {
    this.needDrain = true;
    cb();
    return false;
  }

  if (this.needDrain) {
    // Mark parser as drained
    this.needDrain = false;
    this.emit('drain');
  }

  // We shall not do anything until we get all expected data
  if (this.buffered < this.waiting) {
    // Emit DATA by chunks as they come
    if (this.buffered !== 0 &&
        this.state.header &&
        !this.state.header.control) {
      var buffer = this._slice(this.buffered);
      this.waiting -= buffer.length;

      this.emit('frame', {
        type: 'DATA',
        id: this.state.header.id,
        fin: false,
        compressed: (this.state.header.flags & 0x02) === 0x02,
        data: buffer
      });
    }

    cb();
    return;
  }

  var self = this;
  var buffer = this._slice(this.waiting);

  // Executed parser for buffered data
  this.paused = true;
  var sync = true;
  this.execute(this.state, buffer, function (err, waiting) {
    // Propagate errors
    if (err) {
      // And unpause once execution finished
      self.paused = false;

      cb();
      return self.emit('error', err);
    }

    // Set new `waiting`
    self.waiting = waiting;

    if (sync) {
      utils.nextTick(function() {
        // Unpause right before entering new `_write()` call
        self.paused = false;
        self._write(undefined, null, cb);
      });
    } else {
      // Unpause right before entering new `_write()` call
      self.paused = false;
      self._write(undefined, null, cb);
    }
  });
  sync = false;
};

if (legacy) {
  //
  // ### function write (data, encoding, cb)
  // #### @data {Buffer} chunk of data
  // #### @encoding {Null} encoding
  // #### @cb {Function} callback
  // Legacy method
  //
  Parser.prototype.write = Parser.prototype._write;

  //
  // ### function end ()
  // Stream's end() implementation
  //
  Parser.prototype.end = function end() {
    this.emit('end');
  };
}

//
// ### function setVersion (version)
// #### @version {Number} Protocol version
// Set protocol version to use
//
Parser.prototype.setVersion = function setVersion(version) {
  this.version = version;
  this.emit('version', version);
  this.deflate = spdy.utils.zwrap(this.connection._spdyState.deflate);
  this.inflate = spdy.utils.zwrap(this.connection._spdyState.inflate);
};

//
// ### function execute (state, data, callback)
// #### @state {Object} Parser's state
// #### @data {Buffer} Incoming data
// #### @callback {Function} continuation callback
// Parse buffered data
//
Parser.prototype.execute = function execute(state, data, callback) {
  if (state.type === 'frame-head') {
    var header = state.header = this.parseHeader(data);

    if (this.version === null && header.control) {
      if (header.version !== 2 && header.version !== 3) {
        return callback(new Error('Unsupported spdy version: ' +
                                  header.version));
      }
      this.setVersion(header.version);
    }

    state.type = 'frame-body';
    callback(null, header.length);
  } else if (state.type === 'frame-body') {
    var self = this;

    // Data frame
    if (!state.header.control) {
      return onFrame(null, {
        type: 'DATA',
        id: state.header.id,
        fin: (state.header.flags & 0x01) === 0x01,
        compressed: (state.header.flags & 0x02) === 0x02,
        data: data
      });
    } else {
      // Control frame
      this.parseBody(state.header, data, onFrame);
    }

    function onFrame(err, frame) {
      if (err) return callback(err);

      self.emit('frame', frame);

      state.type = 'frame-head';
      state.header = null;
      callback(null, 8);
    };
  }
};


//
// ### function parseHeader (data)
// ### @data {Buffer} incoming data
// Returns parsed SPDY frame header
//
Parser.prototype.parseHeader = function parseHeader(data) {
  var header = {
    control: (data.readUInt8(0) & 0x80) === 0x80 ? true : false,
    version: null,
    type: null,
    id: null,
    flags: data.readUInt8(4),
    length: data.readUInt32BE(4) & 0x00ffffff
  };

  if (header.control) {
    header.version = data.readUInt16BE(0) & 0x7fff;
    header.type = data.readUInt16BE(2);
  } else {
    header.id = data.readUInt32BE(0) & 0x7fffffff;
  }

  return header;
};


//
// ### function execute (header, body, callback)
// #### @header {Object} Frame headers
// #### @body {Buffer} Frame's body
// #### @callback {Function} Continuation callback
// Parse frame (decompress data and create streams)
//
Parser.prototype.parseBody = function parseBody(header, body, callback) {
  // SYN_STREAM or SYN_REPLY
  if (header.type === 0x01 || header.type === 0x02)
    this.parseSynHead(header.type, header.flags, body, callback);
  // RST_STREAM
  else if (header.type === 0x03)
    this.parseRst(body, callback);
  // SETTINGS
  else if (header.type === 0x04)
    this.parseSettings(body, callback);
  else if (header.type === 0x05)
    callback(null, { type: 'NOOP' });
  // PING
  else if (header.type === 0x06)
    this.parsePing(body, callback);
  // GOAWAY
  else if (header.type === 0x07)
    this.parseGoaway(body, callback);
  // HEADERS
  else if (header.type === 0x08)
    this.parseHeaders(body, callback);
  // WINDOW_UPDATE
  else if (header.type === 0x09)
    this.parseWindowUpdate(body, callback);
  // X-FORWARDED
  else if (header.type === 0xf000)
    this.parseXForwarded(body, callback);
  else
    callback(null, { type: 'unknown: ' + header.type, body: body });
};


//
// ### function parseSynHead (type, flags, data)
// #### @type {Number} Frame type
// #### @flags {Number} Frame flags
// #### @data {Buffer} input data
// Returns parsed syn_* frame's head
//
Parser.prototype.parseSynHead = function parseSynHead(type,
                                                      flags,
                                                      data,
                                                      callback) {
  var stream = type === 0x01;
  var offset = stream ? 10 : this.version === 2 ? 6 : 4;

  if (data.length < offset)
    return callback(new Error('SynHead OOB'));

  var kvs = data.slice(offset);
  this.parseKVs(kvs, function(err, headers) {
    if (err)
      return callback(err);

    if (stream === 'SYN_STREAM' &&
        (!headers.method || !(headers.path || headers.url))) {
      return callback(new Error('Missing `:method` and/or `:path` header'));
    }

    callback(null, {
      type: stream ? 'SYN_STREAM' : 'SYN_REPLY',
      id: data.readUInt32BE(0, true) & 0x7fffffff,
      associated: stream ? data.readUInt32BE(4, true) & 0x7fffffff : 0,
      priority: stream ? data[8] >> 5 : 0,
      fin: (flags & 0x01) === 0x01,
      unidir: (flags & 0x02) === 0x02,
      headers: headers,
      url: headers.path || headers.url || ''
    });
  });
};


//
// ### function parseHeaders (data, callback)
// #### @data {Buffer} input data
// #### @callback {Function} continuation
// Parse HEADERS
//
Parser.prototype.parseHeaders = function parseHeaders(data, callback) {
  var offset = this.version === 2 ? 6 : 4;
  if (data.length < offset)
    return callback(new Error('HEADERS OOB'));

  var streamId = data.readUInt32BE(0, true) & 0x7fffffff;

  this.parseKVs(data.slice(offset), function(err, headers) {
    if (err)
      return callback(err);

    callback(null, {
      type: 'HEADERS',
      id: streamId,
      headers: headers
    });
  });
};


//
// ### function parseKVs (pairs, callback)
// #### @pairs {Buffer} header pairs
// #### @callback {Function} continuation
// Returns hashmap of parsed headers
//
Parser.prototype.parseKVs = function parseKVs(pairs, callback) {
  var self = this;
  this.inflate(pairs, function(err, chunks, length) {
    if (err)
      return callback(err);

    var pairs = Buffer.concat(chunks, length);

    var size = self.version === 2 ? 2 : 4;
    if (pairs.length < size)
      return callback(new Error('KV OOB'));

    var count = size === 2 ? pairs.readUInt16BE(0, true) :
                             pairs.readUInt32BE(0, true),
        headers = {};

    pairs = pairs.slice(size);

    function readString() {
      if (pairs.length < size)
        return null;
      var len = size === 2 ? pairs.readUInt16BE(0, true) :
                             pairs.readUInt32BE(0, true);

      if (pairs.length < size + len) {
        return null;
      }
      var value = pairs.slice(size, size + len);

      pairs = pairs.slice(size + len);

      return value.toString();
    }

    while(count > 0) {
      var key = readString(),
          value = readString();

      if (key === null || value === null)
        return callback(new Error('Headers OOB'));

      if (self.version >= 3)
        headers[key.replace(/^:/, '')] = value;
      else
        headers[key] = value;
      count--;
    }

    callback(null, headers);
  });
};


//
// ### function parseRst (data, callback)
// #### @data {Buffer} input data
// #### @callback {Function} continuation
// Parse RST
//
Parser.prototype.parseRst = function parseRst(data, callback) {
  if (data.length < 8)
    return callback(new Error('RST OOB'));

  callback(null, {
    type: 'RST_STREAM',
    id: data.readUInt32BE(0, true) & 0x7fffffff,
    status: data.readUInt32BE(4, true),
    extra: data.length > 8 ? data.slice(8) : null
  });
};


//
// ### function parseSettings (data, callback)
// #### @data {Buffer} input data
// #### @callback {Function} continuation
// Parse SETTINGS
//
Parser.prototype.parseSettings = function parseSettings(data, callback) {
  if (data.length < 4)
    return callback(new Error('SETTINGS OOB'));

  var settings = {},
      number = data.readUInt32BE(0, true),
      idMap = {
        1: 'upload_bandwidth',
        2: 'download_bandwidth',
        3: 'round_trip_time',
        4: 'max_concurrent_streams',
        5: 'current_cwnd',
        6: 'download_retrans_rate',
        7: 'initial_window_size',
        8: 'client_certificate_vector_size'
      };

  if (data.length < 4 + number * 8)
    return callback(new Error('SETTINGS OOB#2'));

  for (var i = 0; i < number; i++) {
    var id = (this.version === 2 ? data.readUInt32LE(4 + i * 8, true) :
                                   data.readUInt32BE(4 + i * 8, true)),
        flags = (id >> 24) & 0xff;
    id = id & 0xffffff;

    var name = idMap[id];

    settings[id] = settings[name] = {
      persist: !!(flags & 0x1),
      persisted: !!(flags & 0x2),
      value: data.readUInt32BE(8 + (i*8), true)
    };
  }

  callback(null, {
    type: 'SETTINGS',
    settings: settings
  });
};


//
// ### function parseGoaway (data, callback)
// #### @data {Buffer} input data
// #### @callback {Function} continuation
// Parse PING
//
Parser.prototype.parsePing = function parsePing(body, callback) {
  if (body.length < 4)
    return callback(new Error('PING OOB'));
  callback(null, { type: 'PING', pingId: body.readUInt32BE(0, true) });
};


//
// ### function parseGoaway (data, callback)
// #### @data {Buffer} input data
// #### @callback {Function} continuation
// Parse GOAWAY
//
Parser.prototype.parseGoaway = function parseGoaway(data, callback) {
  if (data.length < 4)
    return callback(new Error('GOAWAY OOB'));

  callback(null, {
    type: 'GOAWAY',
    lastId: data.readUInt32BE(0, true) & 0x7fffffff
  });
};


//
// ### function parseWindowUpdate (data, callback)
// #### @data {Buffer} input data
// #### @callback {Function} continuation
// Parse WINDOW_UPDATE
//
Parser.prototype.parseWindowUpdate = function parseWindowUpdate(data, callback) {
  if (data.length < 8)
    return callback(new Error('WINDOW_UPDATE OOB'));

  callback(null, {
    type: 'WINDOW_UPDATE',
    id: data.readUInt32BE(0, true) & 0x7fffffff,
    delta: data.readUInt32BE(4, true) & 0x7fffffff
  });
};


//
// ### function parseXForwarded (data, callback)
// #### @data {Buffer} input data
// #### @callback {Function} continuation
// Parse X_FORWARDED
//
Parser.prototype.parseXForwarded = function parseXForwarded(data, callback) {
  if (data.length < 4)
    return callback(new Error('X_FORWARDED OOB'));

  var len = data.readUInt32BE(0, true);
  if (len + 4 > data.length)
    return callback(new Error('X_FORWARDED host length OOB'));

  callback(null, {
    type: 'X_FORWARDED',
    host: data.slice(4, 4 + len).toString()
  });
};
