var sys = require("sys");
var events = require('events');

exports.parse = function(options) {
  var promise = new events.Promise();

  try {
    var stream = new exports.Stream(options);
  } catch (e) {
    process.nextTick(function() {
      promise.emitError(e);
    });
    return promise;
  }

  var parts = {};
  stream.addListener('part', function(part) {
    var name = part.name;
    var buffer = '';

    part.addListener('body', function(chunk) {
      buffer = buffer + chunk;
    });

    part.addListener('complete', function() {
      parts[name] = buffer;
    });
  });

  stream.addListener('complete', function() {
    promise.emitSuccess(parts);
  });

  return promise;
};

exports.Stream = function(options) {
  events.EventEmitter.call(this);

  this.init(options);
};
sys.inherits(exports.Stream, events.EventEmitter);


var proto = exports.Stream.prototype;

proto.init = function(options) {
  this.buffer = '';
  this.bytesReceived = 0;
  this.bytesTotal = 0;
  this.part = null;

  if ('headers' in options) {
    var req = options, contentType = req.headers['content-type'];
    if (!contentType) {
      throw new Error('Content-Type header not set');
    }

    if (!contentType.match(/^multipart\/form-data/i)) {
      throw new Error('Content-Type is not multipart: "'+contentType+'"');
    }

    var boundary = contentType.match(/boundary=([^;]+)/i)
    if (!boundary) {
      throw new Error('No boundary in Content-Type header: "'+contentType+'"');
    }

    this.boundary = '--'+boundary[1];
    this.bytesTotal = req.headers['content-length'];

    var self = this;
    req
      .addListener('body', function(chunk) {
        self.write(chunk);
      })
      .addListener('complete', function() {
        self.emit('complete');
      });
  } else {
    if (!options.boundary) {
      throw new Error('No boundary option given');
    }

    this.boundary = options.boundary;
    this.write(options.data || '');
  }
};

proto.write = function(chunk) {
  this.bytesReceived = this.bytesReceived + chunk.length;
  this.buffer = this.buffer + chunk;

  while (this.buffer.length) {
    var offset = this.buffer.indexOf(this.boundary);

    if (offset === 0) {
      this.buffer = this.buffer.substr(offset + this.boundary.length + 2);
    } else if (offset == -1) {
      if (this.buffer === "\r\n") {
        this.buffer = '';
      } else {
        this.part = (this.part || new Part(this));
        this.part.write(this.buffer);
        this.buffer = [];
      }
    } else if (offset > 0) {
      this.part = (this.part || new Part(this));
      this.part.write(this.buffer.substr(0, offset - 2));

      this.part.emit('complete');

      this.part = new Part(this);
      this.buffer = this.buffer.substr(offset + this.boundary.length + 2);
    }
  }
};

function Part(stream) {
  events.EventEmitter.call(this);

  this.headers = {};
  this.name = null;
  this.filename = null;
  this.buffer = '';
  this.bytesReceived = 0;

  // Avoids turning Part into a circular JSON object
  this.getStream = function() {
    return stream;
  };

  this._headersComplete = false;
}
sys.inherits(Part, events.EventEmitter);

Part.prototype.parsedHeaders = function() {
  for (var header in this.headers) {
    var parts = this.headers[header].split(/; ?/), parsedHeader = {};
    for (var i = 0; i < parts.length; i++) {
      var pair = parts[i].split('=');
      if (pair.length < 2) {
        continue;
      }

      var key = pair[0].toLowerCase(), val = pair[1] || '';
      val = stripslashes(val).substr(1);
      val = val.substr(0, val.length - 1);

      parsedHeader[key] = val;
    }

    if (header == 'content-disposition') {
      this.name = parsedHeader.name || null;
      this.filename = parsedHeader.filename || null;
    }

    this.headers[header] = parsedHeader;
  }
};

Part.prototype.write = function(chunk) {
  if (this._headersComplete) {
    this.bytesReceived = this.bytesReceived + chunk.length;
    this.emit('body', chunk);
    return;
  }

  this.buffer = this.buffer + chunk;
  while (this.buffer.length) {
    var offset = this.buffer.indexOf("\r\n");

    if (offset === 0) {
      this._headersComplete = true;
      this.parsedHeaders();
      this.getStream().emit('part', this);

      this.buffer = this.buffer.substr(2);
      this.bytesReceived = this.bytesReceived + this.buffer.length;
      this.emit('body', this.buffer);
      this.buffer = '';
      return;
    } else if (offset > 0) {
      var header = this.buffer.substr(0, offset).split(/: ?/);
      this.headers[header[0].toLowerCase()] = header[1];
      this.buffer = this.buffer.substr(offset+2);
    } else if (offset === -1) {
      return;
    }
  }
};

function stripslashes(str) {
  // +   original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // +   improved by: Ates Goral (http://magnetiq.com)
  // +      fixed by: Mick@el
  // +   improved by: marrtins
  // +   bugfixed by: Onno Marsman
  // +   improved by: rezna
  // +   input by: Rick Waldron
  // +   reimplemented by: Brett Zamir (http://brett-zamir.me)
  // *     example 1: stripslashes('Kevin\'s code');
  // *     returns 1: "Kevin's code"
  // *     example 2: stripslashes('Kevin\\\'s code');
  // *     returns 2: "Kevin\'s code"
  return (str+'').replace(/\\(.?)/g, function (s, n1) {
    switch(n1) {
      case '\\':
        return '\\';
      case '0':
        return '\0';
      case '':
        return '';
      default:
        return n1;
    }
  });
}
