// TODO:
//  * support 1 nested multipart level
//    (see second multipart example here:
//     http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data)
//  * support limits.fieldNameSize
//     -- this will require modifications to utils.parseParams

var ReadableStream = require('stream').Readable,
    inherits = require('util').inherits;

var Dicer = require('dicer');

var parseParams = require('../utils').parseParams,
    decodeText = require('../utils').decodeText,
    basename = require('../utils').basename,
    getLimit = require('../utils').getLimit;

var RE_BOUNDARY = /^boundary$/i,
    RE_FIELD = /^form-data$/i,
    RE_CHARSET = /^charset$/i,
    RE_FILENAME = /^filename$/i,
    RE_NAME = /^name$/i;

Multipart.detect = /^multipart\/form-data/i;
function Multipart(boy, cfg) {
  if (!(this instanceof Multipart))
    return new Multipart(boy, cfg);
  var i,
      len,
      self = this,
      boundary,
      limits = cfg.limits,
      parsedConType = cfg.parsedConType || [],
      defCharset = cfg.defCharset || 'utf8',
      preservePath = cfg.preservePath,
      fileopts = (typeof cfg.fileHwm === 'number'
                  ? { highWaterMark: cfg.fileHwm }
                  : {});

  for (i = 0, len = parsedConType.length; i < len; ++i) {
    if (Array.isArray(parsedConType[i])
        && RE_BOUNDARY.test(parsedConType[i][0])) {
      boundary = parsedConType[i][1];
      break;
    }
  }

  function checkFinished() {
    if (nends === 0 && finished && !boy._done) {
      finished = false;
      process.nextTick(function() {
        boy._done = true;
        boy.emit('finish');
      });
    }
  }

  if (typeof boundary !== 'string')
    throw new Error('Multipart: Boundary not found');

  var fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024),
      fileSizeLimit = getLimit(limits, 'fileSize', Infinity),
      filesLimit = getLimit(limits, 'files', Infinity),
      fieldsLimit = getLimit(limits, 'fields', Infinity),
      partsLimit = getLimit(limits, 'parts', Infinity);

  var nfiles = 0,
      nfields = 0,
      nends = 0,
      curFile,
      curField,
      finished = false;

  this._needDrain = false;
  this._pause = false;
  this._cb = undefined;
  this._nparts = 0;
  this._boy = boy;

  var parserCfg = {
    boundary: boundary,
    maxHeaderPairs: (limits && limits.headerPairs)
  };
  if (fileopts.highWaterMark)
    parserCfg.partHwm = fileopts.highWaterMark;
  if (cfg.highWaterMark)
    parserCfg.highWaterMark = cfg.highWaterMark;

  this.parser = new Dicer(parserCfg);
  this.parser.on('drain', function() {
    self._needDrain = false;
    if (self._cb && !self._pause) {
      var cb = self._cb;
      self._cb = undefined;
      cb();
    }
  }).on('part', function onPart(part) {
    if (++self._nparts > partsLimit) {
      self.parser.removeListener('part', onPart);
      self.parser.on('part', skipPart);
      boy.hitPartsLimit = true;
      boy.emit('partsLimit');
      return skipPart(part);
    }

    // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let
    // us emit 'end' early since we know the part has ended if we are already
    // seeing the next part
    if (curField) {
      var field = curField;
      field.emit('end');
      field.removeAllListeners('end');
    }

    part.on('header', function(header) {
      var contype,
          fieldname,
          parsed,
          charset,
          encoding,
          filename,
          nsize = 0;

      if (header['content-type']) {
        parsed = parseParams(header['content-type'][0]);
        if (parsed[0]) {
          contype = parsed[0].toLowerCase();
          for (i = 0, len = parsed.length; i < len; ++i) {
            if (RE_CHARSET.test(parsed[i][0])) {
              charset = parsed[i][1].toLowerCase();
              break;
            }
          }
        }
      }

      if (contype === undefined)
        contype = 'text/plain';
      if (charset === undefined)
        charset = defCharset;

      if (header['content-disposition']) {
        parsed = parseParams(header['content-disposition'][0]);
        if (!RE_FIELD.test(parsed[0]))
          return skipPart(part);
        for (i = 0, len = parsed.length; i < len; ++i) {
          if (RE_NAME.test(parsed[i][0])) {
            fieldname = decodeText(parsed[i][1], 'binary', 'utf8');
          } else if (RE_FILENAME.test(parsed[i][0])) {
            filename = decodeText(parsed[i][1], 'binary', 'utf8');
            if (!preservePath)
              filename = basename(filename);
          }
        }
      } else
        return skipPart(part);

      if (header['content-transfer-encoding'])
        encoding = header['content-transfer-encoding'][0].toLowerCase();
      else
        encoding = '7bit';

      var onData,
          onEnd;
      if (contype === 'application/octet-stream' || filename !== undefined) {
        // file/binary field
        if (nfiles === filesLimit) {
          if (!boy.hitFilesLimit) {
            boy.hitFilesLimit = true;
            boy.emit('filesLimit');
          }
          return skipPart(part);
        }

        ++nfiles;

        if (!boy._events.file) {
          self.parser._ignore();
          return;
        }

        ++nends;
        var file = new FileStream(fileopts);
        curFile = file;
        file.on('end', function() {
          --nends;
          self._pause = false;
          checkFinished();
          if (self._cb && !self._needDrain) {
            var cb = self._cb;
            self._cb = undefined;
            cb();
          }
        });
        file._read = function(n) {
          if (!self._pause)
            return;
          self._pause = false;
          if (self._cb && !self._needDrain) {
            var cb = self._cb;
            self._cb = undefined;
            cb();
          }
        };
        boy.emit('file', fieldname, file, filename, encoding, contype);

        onData = function(data) {
          if ((nsize += data.length) > fileSizeLimit) {
            var extralen = (fileSizeLimit - (nsize - data.length));
            if (extralen > 0)
              file.push(data.slice(0, extralen));
            file.emit('limit');
            file.truncated = true;
            part.removeAllListeners('data');
          } else if (!file.push(data))
            self._pause = true;
        };

        onEnd = function() {
          curFile = undefined;
          file.push(null);
        };
      } else {
        // non-file field
        if (nfields === fieldsLimit) {
          if (!boy.hitFieldsLimit) {
            boy.hitFieldsLimit = true;
            boy.emit('fieldsLimit');
          }
          return skipPart(part);
        }

        ++nfields;
        ++nends;
        var buffer = '',
            truncated = false;
        curField = part;

        onData = function(data) {
          if ((nsize += data.length) > fieldSizeLimit) {
            var extralen = (fieldSizeLimit - (nsize - data.length));
            buffer += data.toString('binary', 0, extralen);
            truncated = true;
            part.removeAllListeners('data');
          } else
            buffer += data.toString('binary');
        };

        onEnd = function() {
          curField = undefined;
          if (buffer.length)
            buffer = decodeText(buffer, 'binary', charset);
          boy.emit('field', fieldname, buffer, false, truncated, encoding, contype);
          --nends;
          checkFinished();
        };
      }

      /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become
         broken. Streams2/streams3 is a huge black box of confusion, but
         somehow overriding the sync state seems to fix things again (and still
         seems to work for previous node versions).
      */
      part._readableState.sync = false;

      part.on('data', onData);
      part.on('end', onEnd);
    }).on('error', function(err) {
      if (curFile)
        curFile.emit('error', err);
    });
  }).on('error', function(err) {
    boy.emit('error', err);
  }).on('finish', function() {
    finished = true;
    checkFinished();
  });
}

Multipart.prototype.write = function(chunk, cb) {
  var r;
  if ((r = this.parser.write(chunk)) && !this._pause)
    cb();
  else {
    this._needDrain = !r;
    this._cb = cb;
  }
};

Multipart.prototype.end = function() {
  var self = this;
  if (this._nparts === 0 && !self._boy._done) {
    process.nextTick(function() {
      self._boy._done = true;
      self._boy.emit('finish');
    });
  } else if (this.parser.writable)
    this.parser.end();
};

function skipPart(part) {
  part.resume();
}

function FileStream(opts) {
  if (!(this instanceof FileStream))
    return new FileStream(opts);
  ReadableStream.call(this, opts);

  this.truncated = false;
}
inherits(FileStream, ReadableStream);

FileStream.prototype._read = function(n) {};

module.exports = Multipart;
