/* 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 parser = exports;

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

//
// ### function Parser (connection, deflate, inflate)
// #### @connection {spdy.Connection} connection
// #### @deflate {zlib.Deflate} Deflate stream
// #### @inflate {zlib.Inflate} Inflate stream
// SPDY protocol frames parser's @constructor
//
function Parser(connection, deflate, inflate) {
  stream.Stream.call(this);

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

  this.state = { type: 'frame-head' };
  this.deflate = deflate;
  this.inflate = inflate;
  this.framer = null;

  this.connection = connection;

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

//
// ### function create (connection, deflate, inflate)
// #### @connection {spdy.Connection} connection
// #### @deflate {zlib.Deflate} Deflate stream
// #### @inflate {zlib.Inflate} Inflate stream
// @constructor wrapper
//
parser.create = function create(connection, deflate, inflate) {
  return new Parser(connection, deflate, inflate);
};

//
// ### function write (data)
// #### @data {Buffer} chunk of data
// Writes or buffers data to parser
//
Parser.prototype.write = function write(data) {
  if (data !== undefined) {
    // Buffer data
    this.buffer.push(data);
    this.buffered += data.length;
  }

  // Notify caller about state (for piping)
  if (this.paused) return false;

  // We shall not do anything until we get all expected data
  if (this.buffered < this.waiting) return;

  // Mark parser as not drained
  if (data !== undefined) this.drained = false;

  var self = this,
      buffer = new Buffer(this.waiting),
      sliced = 0,
      offset = 0;

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

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

      this.buffer[--sliced] = chunk.slice(this.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);

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

    // Propagate errors
    if (err) return self.emit('error', err);

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

    if (self.waiting <= self.buffered) {
      self.write();
    } else {
      process.nextTick(function() {
        if (self.drained) return;

        // Mark parser as drained
        self.drained = true;
        self.emit('drain');
      });
    }
  });
};

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

//
// ### 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 = spdy.protocol.generic.parseHeader(data);

    // Lazily create framer
    if (!this.framer && header.control) {
      if (spdy.protocol[header.version]) {
        this.framer = new spdy.protocol[header.version].Framer(
          spdy.utils.zwrap(this.deflate),
          spdy.utils.zwrap(this.inflate)
        );

        // Propagate framer to connection
        this.connection.framer = this.framer;
        this.emit('_framer', this.framer);
      }
    }

    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.framer.execute(state.header, data, onFrame);
    }

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

      self.emit('frame', frame);

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