/**
 * Parted (https://github.com/chjj/parted)
 * A streaming multipart state parser.
 * Copyright (c) 2011, Christopher Jeffrey. (MIT Licensed)
 */

var fs = require('fs')
  , path = require('path')
  , EventEmitter = require('events').EventEmitter
  , StringDecoder = require('string_decoder').StringDecoder
  , set = require('qs').set
  , each = Array.prototype.forEach;

/**
 * Character Constants
 */

var DASH = '-'.charCodeAt(0)
  , CR = '\r'.charCodeAt(0)
  , LF = '\n'.charCodeAt(0)
  , COLON = ':'.charCodeAt(0)
  , SPACE = ' '.charCodeAt(0);

/**
 * Parser
 */

var Parser = function(type, options) {
  if (!(this instanceof Parser)) {
    return new Parser(type, options);
  }

  EventEmitter.call(this);

  this.writable = true;
  this.readable = true;

  this.options = options || {};

  var key = grab(type, 'boundary');
  if (!key) {
    return this._error('No boundary key found.');
  }

  this.key = Buffer.allocUnsafe('\r\n--' + key);

  this._key = {};
  each.call(this.key, function(ch) {
    this._key[ch] = true;
  }, this);

  this.state = 'start';
  this.pending = 0;
  this.written = 0;
  this.writtenDisk = 0;
  this.buff = Buffer.allocUnsafe(200);

  this.preamble = true;
  this.epilogue = false;

  this._reset();
};

Parser.prototype.__proto__ = EventEmitter.prototype;

/**
 * Parsing
 */

Parser.prototype.write = function(data) {
  if (!this.writable
      || this.epilogue) return;

  try {
    this._parse(data);
  } catch (e) {
    this._error(e);
  }

  return true;
};

Parser.prototype.end = function(data) {
  if (!this.writable) return;

  if (data) this.write(data);

  if (!this.epilogue) {
    return this._error('Message underflow.');
  }

  return true;
};

Parser.prototype._parse = function(data) {
  var i = 0
    , len = data.length
    , buff = this.buff
    , key = this.key
    , ch
    , val
    , j;

  for (; i < len; i++) {
    if (this.pos >= 200) {
      return this._error('Potential buffer overflow.');
    }

    ch = data[i];

    switch (this.state) {
      case 'start':
        switch (ch) {
          case DASH:
            this.pos = 3;
            this.state = 'key';
            break;
          default:
            break;
        }
        break;
      case 'key':
        if (this.pos === key.length) {
          this.state = 'key_end';
          i--;
        } else if (ch !== key[this.pos]) {
          if (this.preamble) {
            this.state = 'start';
            i--;
          } else {
            this.state = 'body';
            val = this.pos - i;
            if (val > 0) {
              this._write(key.slice(0, val));
            }
            i--;
          }
        } else {
          this.pos++;
        }
        break;
      case 'key_end':
        switch (ch) {
          case CR:
            this.state = 'key_line_end';
            break;
          case DASH:
            this.state = 'key_dash_end';
            break;
          default:
            return this._error('Expected CR or DASH.');
        }
        break;
      case 'key_line_end':
        switch (ch) {
          case LF:
            if (this.preamble) {
              this.preamble = false;
            } else {
              this._finish();
            }
            this.state = 'header_name';
            this.pos = 0;
            break;
          default:
            return this._error('Expected CR.');
        }
        break;
      case 'key_dash_end':
        switch (ch) {
          case DASH:
            this.epilogue = true;
            this._finish();
            return;
          default:
            return this._error('Expected DASH.');
        }
        break;
      case 'header_name':
        switch (ch) {
          case COLON:
            this.header = buff.toString('ascii', 0, this.pos);
            this.pos = 0;
            this.state = 'header_val';
            break;
          default:
            buff[this.pos++] = ch | 32;
            break;
        }
        break;
      case 'header_val':
        switch (ch) {
          case CR:
            this.state = 'header_val_end';
            break;
          case SPACE:
            if (this.pos === 0) {
              break;
            }
            ; // FALL-THROUGH
          default:
            buff[this.pos++] = ch;
            break;
        }
        break;
      case 'header_val_end':
        switch (ch) {
          case LF:
            val = buff.toString('ascii', 0, this.pos);
            this._header(this.header, val);
            this.pos = 0;
            this.state = 'header_end';
            break;
          default:
            return this._error('Expected LF.');
        }
        break;
      case 'header_end':
        switch (ch) {
          case CR:
            this.state = 'head_end';
            break;
          default:
            this.state = 'header_name';
            i--;
            break;
        }
        break;
      case 'head_end':
        switch (ch) {
          case LF:
            this.state = 'body';
            i++;
            if (i >= len) return;
            data = data.slice(i);
            i = -1;
            len = data.length;
            break;
          default:
            return this._error('Expected LF.');
        }
        break;
      case 'body':
        switch (ch) {
          case CR:
            if (i > 0) {
              this._write(data.slice(0, i));
            }
            this.pos = 1;
            this.state = 'key';
            data = data.slice(i);
            i = 0;
            len = data.length;
            break;
          default:
            // boyer-moore-like algorithm
            // at felixge's suggestion
            while ((j = i + key.length - 1) < len) {
              if (this._key[data[j]]) break;
              i = j;
            }
            break;
        }
        break;
    }
  }

  if (this.state === 'body') {
    this._write(data);
  }
};

Parser.prototype._header = function(name, val) {
  /*if (name === 'content-disposition') {
    this.field = grab(val, 'name');
    this.file = grab(val, 'filename');

    if (this.file) {
      this.data = stream(this.file, this.options.path);
    } else {
      this.decode = new StringDecoder('utf8');
      this.data = '';
    }
  }*/

  return this.emit('header', name, val);
};

Parser.prototype._write = function(data) {
  /*if (this.data == null) {
    return this._error('No disposition.');
  }

  if (this.file) {
    this.data.write(data);
    this.writtenDisk += data.length;
  } else {
    this.data += this.decode.write(data);
    this.written += data.length;
  }*/

  this.emit('data', data);
};

Parser.prototype._reset = function() {
  this.pos = 0;
  this.decode = null;
  this.field = null;
  this.data = null;
  this.file = null;
  this.header = null;
};

Parser.prototype._error = function(err) {
  this.destroy();
  this.emit('error', typeof err === 'string'
    ? new Error(err)
    : err);
};

Parser.prototype.destroy = function(err) {
  this.writable = false;
  this.readable = false;
  this._reset();
};

Parser.prototype._finish = function() {
  var self = this
    , field = this.field
    , data = this.data
    , file = this.file
    , part;

  this.pending++;

  this._reset();

  if (data && data.path) {
    part = data.path;
    data.end(next);
  } else {
    part = data;
    next();
  }

  function next() {
    if (!self.readable) return;

    self.pending--;

    self.emit('part', field, part);

    if (data && data.path) {
      self.emit('file', field, part, file);
    }

    if (self.epilogue && !self.pending) {
      self.emit('end');
      self.destroy();
    }
  }
};

/**
 * Uploads
 */

Parser.root = process.platform === 'win32'
  ? 'C:/Temp'
  : '/tmp';

/**
 * Middleware
 */

Parser.middleware = function(options) {
  options = options || {};
  return function(req, res, next) {
    if (options.ensureBody) {
      req.body = {};
    }

    if (req.method === 'GET'
        || req.method === 'HEAD'
        || req._multipart) return next();

    req._multipart = true;

    var type = req.headers['content-type'];

    if (type) type = type.split(';')[0].trim().toLowerCase();

    if (type === 'multipart/form-data') {
      Parser.handle(req, res, next, options);
    } else {
      next();
    }
  };
};

/**
 * Handler
 */

Parser.handle = function(req, res, next, options) {
  var parser = new Parser(req.headers['content-type'], options)
    , diskLimit = options.diskLimit
    , limit = options.limit
    , parts = {}
    , files = {};

  parser.on('error', function(err) {
    req.destroy();
    next(err);
  });

  parser.on('part', function(field, part) {
    set(parts, field, part);
  });

  parser.on('file', function(field, path, name) {
    set(files, field, {
      path: path,
      name: name,
      toString: function() {
        return path;
      }
    });
  });

  parser.on('data', function() {
    if (this.writtenDisk > diskLimit || this.written > limit) {
      this.emit('error', new Error('Overflow.'));
      this.destroy();
    }
  });

  parser.on('end', next);

  req.body = parts;
  req.files = files;
  req.pipe(parser);
};

/**
 * Helpers
 */

var isWindows = process.platform === 'win32';

var stream = function(name, dir) {
  var ext = path.extname(name) || ''
    , name = path.basename(name, ext) || ''
    , dir = dir || Parser.root
    , tag;

  tag = Math.random().toString(36).substring(2);

  name = name.substring(0, 200) + '.' + tag;
  name = path.join(dir, name) + ext.substring(0, 6);
  name = name.replace(/\0/g, '');

  if (isWindows) {
    name = name.replace(/[:*<>|"?]/g, '');
  }

  return fs.createWriteStream(name);
};

var grab = function(str, name) {
  if (!str) return;

  var rx = new RegExp('\\b' + name + '\\s*=\\s*("[^"]+"|\'[^\']+\'|[^;,]+)', 'i')
    , cap = rx.exec(str);

  if (cap) {
    return cap[1].trim().replace(/^['"]|['"]$/g, '');
  }
};

/**
 * Expose
 */

module.exports = Parser;
