/* */ 
(function(Buffer) {
  if (global.GENTLY)
    require = GENTLY.hijack(require);
  var fs = require('fs');
  var util = require('util'),
      path = require('path'),
      File = require('./file'),
      MultipartParser = require('./multipart_parser').MultipartParser,
      QuerystringParser = require('./querystring_parser').QuerystringParser,
      OctetParser = require('./octet_parser').OctetParser,
      JSONParser = require('./json_parser').JSONParser,
      StringDecoder = require('string_decoder').StringDecoder,
      EventEmitter = require('events').EventEmitter,
      Stream = require('stream').Stream,
      os = require('os');
  function IncomingForm(opts) {
    if (!(this instanceof IncomingForm))
      return new IncomingForm(opts);
    EventEmitter.call(this);
    opts = opts || {};
    this.error = null;
    this.ended = false;
    this.maxFields = opts.maxFields || 1000;
    this.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024;
    this.keepExtensions = opts.keepExtensions || false;
    this.uploadDir = opts.uploadDir || os.tmpDir();
    this.encoding = opts.encoding || 'utf-8';
    this.headers = null;
    this.type = null;
    this.hash = false;
    this.bytesReceived = null;
    this.bytesExpected = null;
    this._parser = null;
    this._flushing = 0;
    this._fieldsSize = 0;
    this.openedFiles = [];
    return this;
  }
  ;
  util.inherits(IncomingForm, EventEmitter);
  exports.IncomingForm = IncomingForm;
  IncomingForm.prototype.parse = function(req, cb) {
    this.pause = function() {
      try {
        req.pause();
      } catch (err) {
        if (!this.ended) {
          this._error(err);
        }
        return false;
      }
      return true;
    };
    this.resume = function() {
      try {
        req.resume();
      } catch (err) {
        if (!this.ended) {
          this._error(err);
        }
        return false;
      }
      return true;
    };
    if (cb) {
      var fields = {},
          files = {};
      this.on('field', function(name, value) {
        fields[name] = value;
      }).on('file', function(name, file) {
        files[name] = file;
      }).on('error', function(err) {
        cb(err, fields, files);
      }).on('end', function() {
        cb(null, fields, files);
      });
    }
    this.writeHeaders(req.headers);
    var self = this;
    req.on('error', function(err) {
      self._error(err);
    }).on('aborted', function() {
      self.emit('aborted');
      self._error(new Error('Request aborted'));
    }).on('data', function(buffer) {
      self.write(buffer);
    }).on('end', function() {
      if (self.error) {
        return;
      }
      var err = self._parser.end();
      if (err) {
        self._error(err);
      }
    });
    return this;
  };
  IncomingForm.prototype.writeHeaders = function(headers) {
    this.headers = headers;
    this._parseContentLength();
    this._parseContentType();
  };
  IncomingForm.prototype.write = function(buffer) {
    if (!this._parser) {
      this._error(new Error('unintialized parser'));
      return;
    }
    this.bytesReceived += buffer.length;
    this.emit('progress', this.bytesReceived, this.bytesExpected);
    var bytesParsed = this._parser.write(buffer);
    if (bytesParsed !== buffer.length) {
      this._error(new Error('parser error, ' + bytesParsed + ' of ' + buffer.length + ' bytes parsed'));
    }
    return bytesParsed;
  };
  IncomingForm.prototype.pause = function() {
    return false;
  };
  IncomingForm.prototype.resume = function() {
    return false;
  };
  IncomingForm.prototype.onPart = function(part) {
    this.handlePart(part);
  };
  IncomingForm.prototype.handlePart = function(part) {
    var self = this;
    if (part.filename === undefined) {
      var value = '',
          decoder = new StringDecoder(this.encoding);
      part.on('data', function(buffer) {
        self._fieldsSize += buffer.length;
        if (self._fieldsSize > self.maxFieldsSize) {
          self._error(new Error('maxFieldsSize exceeded, received ' + self._fieldsSize + ' bytes of field data'));
          return;
        }
        value += decoder.write(buffer);
      });
      part.on('end', function() {
        self.emit('field', part.name, value);
      });
      return;
    }
    this._flushing++;
    var file = new File({
      path: this._uploadPath(part.filename),
      name: part.filename,
      type: part.mime,
      hash: self.hash
    });
    this.emit('fileBegin', part.name, file);
    file.open();
    this.openedFiles.push(file);
    part.on('data', function(buffer) {
      self.pause();
      file.write(buffer, function() {
        self.resume();
      });
    });
    part.on('end', function() {
      file.end(function() {
        self._flushing--;
        self.emit('file', part.name, file);
        self._maybeEnd();
      });
    });
  };
  function dummyParser(self) {
    return {end: function() {
        self.ended = true;
        self._maybeEnd();
        return null;
      }};
  }
  IncomingForm.prototype._parseContentType = function() {
    if (this.bytesExpected === 0) {
      this._parser = dummyParser(this);
      return;
    }
    if (!this.headers['content-type']) {
      this._error(new Error('bad content-type header, no content-type'));
      return;
    }
    if (this.headers['content-type'].match(/octet-stream/i)) {
      this._initOctetStream();
      return;
    }
    if (this.headers['content-type'].match(/urlencoded/i)) {
      this._initUrlencoded();
      return;
    }
    if (this.headers['content-type'].match(/multipart/i)) {
      var m;
      if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) {
        this._initMultipart(m[1] || m[2]);
      } else {
        this._error(new Error('bad content-type header, no multipart boundary'));
      }
      return;
    }
    if (this.headers['content-type'].match(/json/i)) {
      this._initJSONencoded();
      return;
    }
    this._error(new Error('bad content-type header, unknown content-type: ' + this.headers['content-type']));
  };
  IncomingForm.prototype._error = function(err) {
    if (this.error || this.ended) {
      return;
    }
    this.error = err;
    this.pause();
    this.emit('error', err);
    if (Array.isArray(this.openedFiles)) {
      this.openedFiles.forEach(function(file) {
        file._writeStream.destroy();
        setTimeout(fs.unlink, 0, file.path);
      });
    }
  };
  IncomingForm.prototype._parseContentLength = function() {
    this.bytesReceived = 0;
    if (this.headers['content-length']) {
      this.bytesExpected = parseInt(this.headers['content-length'], 10);
    } else if (this.headers['transfer-encoding'] === undefined) {
      this.bytesExpected = 0;
    }
    if (this.bytesExpected !== null) {
      this.emit('progress', this.bytesReceived, this.bytesExpected);
    }
  };
  IncomingForm.prototype._newParser = function() {
    return new MultipartParser();
  };
  IncomingForm.prototype._initMultipart = function(boundary) {
    this.type = 'multipart';
    var parser = new MultipartParser(),
        self = this,
        headerField,
        headerValue,
        part;
    parser.initWithBoundary(boundary);
    parser.onPartBegin = function() {
      part = new Stream();
      part.readable = true;
      part.headers = {};
      part.name = null;
      part.filename = null;
      part.mime = null;
      part.transferEncoding = 'binary';
      part.transferBuffer = '';
      headerField = '';
      headerValue = '';
    };
    parser.onHeaderField = function(b, start, end) {
      headerField += b.toString(self.encoding, start, end);
    };
    parser.onHeaderValue = function(b, start, end) {
      headerValue += b.toString(self.encoding, start, end);
    };
    parser.onHeaderEnd = function() {
      headerField = headerField.toLowerCase();
      part.headers[headerField] = headerValue;
      var m;
      if (headerField == 'content-disposition') {
        if (m = headerValue.match(/\bname="([^"]+)"/i)) {
          part.name = m[1];
        }
        part.filename = self._fileName(headerValue);
      } else if (headerField == 'content-type') {
        part.mime = headerValue;
      } else if (headerField == 'content-transfer-encoding') {
        part.transferEncoding = headerValue.toLowerCase();
      }
      headerField = '';
      headerValue = '';
    };
    parser.onHeadersEnd = function() {
      switch (part.transferEncoding) {
        case 'binary':
        case '7bit':
        case '8bit':
          parser.onPartData = function(b, start, end) {
            part.emit('data', b.slice(start, end));
          };
          parser.onPartEnd = function() {
            part.emit('end');
          };
          break;
        case 'base64':
          parser.onPartData = function(b, start, end) {
            part.transferBuffer += b.slice(start, end).toString('ascii');
            var offset = parseInt(part.transferBuffer.length / 4) * 4;
            part.emit('data', new Buffer(part.transferBuffer.substring(0, offset), 'base64'));
            part.transferBuffer = part.transferBuffer.substring(offset);
          };
          parser.onPartEnd = function() {
            part.emit('data', new Buffer(part.transferBuffer, 'base64'));
            part.emit('end');
          };
          break;
        default:
          return self._error(new Error('unknown transfer-encoding'));
      }
      self.onPart(part);
    };
    parser.onEnd = function() {
      self.ended = true;
      self._maybeEnd();
    };
    this._parser = parser;
  };
  IncomingForm.prototype._fileName = function(headerValue) {
    var m = headerValue.match(/\bfilename="(.*?)"($|; )/i);
    if (!m)
      return;
    var filename = m[1].substr(m[1].lastIndexOf('\\') + 1);
    filename = filename.replace(/%22/g, '"');
    filename = filename.replace(/&#([\d]{4});/g, function(m, code) {
      return String.fromCharCode(code);
    });
    return filename;
  };
  IncomingForm.prototype._initUrlencoded = function() {
    this.type = 'urlencoded';
    var parser = new QuerystringParser(this.maxFields),
        self = this;
    parser.onField = function(key, val) {
      self.emit('field', key, val);
    };
    parser.onEnd = function() {
      self.ended = true;
      self._maybeEnd();
    };
    this._parser = parser;
  };
  IncomingForm.prototype._initOctetStream = function() {
    this.type = 'octet-stream';
    var filename = this.headers['x-file-name'];
    var mime = this.headers['content-type'];
    var file = new File({
      path: this._uploadPath(filename),
      name: filename,
      type: mime
    });
    file.open();
    this.emit('fileBegin', filename, file);
    this._flushing++;
    var self = this;
    self._parser = new OctetParser();
    var outstandingWrites = 0;
    self._parser.on('data', function(buffer) {
      self.pause();
      outstandingWrites++;
      file.write(buffer, function() {
        outstandingWrites--;
        self.resume();
        if (self.ended) {
          self._parser.emit('doneWritingFile');
        }
      });
    });
    self._parser.on('end', function() {
      self._flushing--;
      self.ended = true;
      var done = function() {
        self.emit('file', 'file', file);
        self._maybeEnd();
      };
      if (outstandingWrites === 0) {
        done();
      } else {
        self._parser.once('doneWritingFile', done);
      }
    });
  };
  IncomingForm.prototype._initJSONencoded = function() {
    this.type = 'json';
    var parser = new JSONParser(),
        self = this;
    if (this.bytesExpected) {
      parser.initWithLength(this.bytesExpected);
    }
    parser.onField = function(key, val) {
      self.emit('field', key, val);
    };
    parser.onEnd = function() {
      self.ended = true;
      self._maybeEnd();
    };
    this._parser = parser;
  };
  IncomingForm.prototype._uploadPath = function(filename) {
    var name = '';
    for (var i = 0; i < 32; i++) {
      name += Math.floor(Math.random() * 16).toString(16);
    }
    if (this.keepExtensions) {
      var ext = path.extname(filename);
      ext = ext.replace(/(\.[a-z0-9]+).*/, '$1');
      name += ext;
    }
    return path.join(this.uploadDir, name);
  };
  IncomingForm.prototype._maybeEnd = function() {
    if (!this.ended || this._flushing || this.error) {
      return;
    }
    this.emit('end');
  };
})(require('buffer').Buffer);
