var fs = require('fs'),
    tls = require('tls'),
    zlib = require('zlib'),
    Socket = require('net').Socket,
    EventEmitter = require('events').EventEmitter,
    inherits = require('util').inherits,
    inspect = require('util').inspect;

var Parser = require('./parser');
var XRegExp = require('xregexp').XRegExp;

var REX_TIMEVAL = XRegExp.cache('^(?<year>\\d{4})(?<month>\\d{2})(?<date>\\d{2})(?<hour>\\d{2})(?<minute>\\d{2})(?<second>\\d+)(?:.\\d+)?$'),
    RE_PASV = /([\d]+),([\d]+),([\d]+),([\d]+),([-\d]+),([-\d]+)/,
    RE_EOL = /\r?\n/g,
    RE_WD = /"(.+)"(?: |$)/,
    RE_SYST = /^([^ ]+)(?: |$)/;

var /*TYPE = {
      SYNTAX: 0,
      INFO: 1,
      SOCKETS: 2,
      AUTH: 3,
      UNSPEC: 4,
      FILESYS: 5
    },*/
    RETVAL = {
      PRELIM: 1,
      OK: 2,
      WAITING: 3,
      ERR_TEMP: 4,
      ERR_PERM: 5
    },
    /*ERRORS = {
      421: 'Service not available, closing control connection',
      425: 'Can\'t open data connection',
      426: 'Connection closed; transfer aborted',
      450: 'Requested file action not taken / File unavailable (e.g., file busy)',
      451: 'Requested action aborted: local error in processing',
      452: 'Requested action not taken / Insufficient storage space in system',
      500: 'Syntax error / Command unrecognized',
      501: 'Syntax error in parameters or arguments',
      502: 'Command not implemented',
      503: 'Bad sequence of commands',
      504: 'Command not implemented for that parameter',
      530: 'Not logged in',
      532: 'Need account for storing files',
      550: 'Requested action not taken / File unavailable (e.g., file not found, no access)',
      551: 'Requested action aborted: page type unknown',
      552: 'Requested file action aborted / Exceeded storage allocation (for current directory or dataset)',
      553: 'Requested action not taken / File name not allowed'
    },*/
    bytesNOOP = new Buffer('NOOP\r\n');

var FTP = module.exports = function() {
  if (!(this instanceof FTP))
    return new FTP();

  this._socket = undefined;
  this._pasvSock = undefined;
  this._feat = undefined;
  this._curReq = undefined;
  this._queue = [];
  this._secstate = undefined;
  this._debug = undefined;
  this._keepalive = undefined;
  this._ending = false;
  this._parser = undefined;
  this.options = {
    host: undefined,
    port: undefined,
    user: undefined,
    password: undefined,
    secure: false,
    secureOptions: undefined,
    connTimeout: undefined,
    pasvTimeout: undefined,
    aliveTimeout: undefined
  };
  this.connected = false;
};
inherits(FTP, EventEmitter);

FTP.prototype.connect = function(options) {
  var self = this;
  if (typeof options !== 'object')
    options = {};
  this.connected = false;
  this.options.host = options.host || 'localhost';
  this.options.port = options.port || 21;
  this.options.user = options.user || 'anonymous';
  this.options.password = options.password || 'anonymous@';
  this.options.secure = options.secure || false;
  this.options.secureOptions = options.secureOptions;
  this.options.connTimeout = options.connTimeout || 10000;
  this.options.pasvTimeout = options.pasvTimeout || 10000;
  this.options.aliveTimeout = options.keepalive || 10000;

  if (typeof options.debug === 'function')
    this._debug = options.debug;

  var secureOptions,
      debug = this._debug,
      socket = new Socket();

  socket.setTimeout(0);
  socket.setKeepAlive(true);

  this._parser = new Parser({ debug: debug });
  this._parser.on('response', function(code, text) {
    var retval = code / 100 >> 0;
    if (retval === RETVAL.ERR_TEMP || retval === RETVAL.ERR_PERM) {
      if (self._curReq)
        self._curReq.cb(makeError(code, text), undefined, code);
      else
        self.emit('error', makeError(code, text));
    } else if (self._curReq)
      self._curReq.cb(undefined, text, code);

    // a hack to signal we're waiting for a PASV data connection to complete
    // first before executing any more queued requests ...
    //
    // also: don't forget our current request if we're expecting another
    // terminating response ....
    if (self._curReq && retval !== RETVAL.PRELIM) {
      self._curReq = undefined;
      self._send();
    }

    noopreq.cb();
  });

  if (this.options.secure) {
    secureOptions = {};
    secureOptions.host = this.options.host;
    for (var k in this.options.secureOptions)
      secureOptions[k] = this.options.secureOptions[k];
    secureOptions.socket = socket;
    this.options.secureOptions = secureOptions;
  }

  if (this.options.secure === 'implicit')
    this._socket = tls.connect(secureOptions, onconnect);
  else {
    socket.once('connect', onconnect);
    this._socket = socket;
  }

  var noopreq = {
        cmd: 'NOOP',
        cb: function() {
          clearTimeout(self._keepalive);
          self._keepalive = setTimeout(donoop, self.options.aliveTimeout);
        }
      };

  function donoop() {
    if (!self._socket || !self._socket.writable)
      clearTimeout(self._keepalive);
    else if (!self._curReq && self._queue.length === 0) {
      self._curReq = noopreq;
      debug&&debug('[connection] > NOOP');
      self._socket.write(bytesNOOP);
    } else
      noopreq.cb();
  }

  function onconnect() {
    clearTimeout(timer);
    clearTimeout(self._keepalive);
    self.connected = true;
    self._socket = socket; // re-assign for implicit secure connections

    var cmd;

    if (self._secstate) {
      if (self._secstate === 'upgraded-tls' && self.options.secure === true) {
        cmd = 'PBSZ';
        self._send('PBSZ 0', reentry, true);
      } else {
        cmd = 'USER';
        self._send('USER ' + self.options.user, reentry, true);
      }
    } else {
      self._curReq = {
        cmd: '',
        cb: reentry
      };
    }

    function reentry(err, text, code) {
      if (err && (!cmd || cmd === 'USER' || cmd === 'PASS' || cmd === 'TYPE')) {
        self.emit('error', err);
        return self._socket && self._socket.end();
      }
      if ((cmd === 'AUTH TLS' && code !== 234 && self.options.secure !== true)
          || (cmd === 'AUTH SSL' && code !== 334)
          || (cmd === 'PBSZ' && code !== 200)
          || (cmd === 'PROT' && code !== 200)) {
        self.emit('error', makeError(code, 'Unable to secure connection(s)'));
        return self._socket && self._socket.end();
      }

      if (!cmd) {
        // sometimes the initial greeting can contain useful information
        // about authorized use, other limits, etc.
        self.emit('greeting', text);

        if (self.options.secure && self.options.secure !== 'implicit') {
          cmd = 'AUTH TLS';
          self._send(cmd, reentry, true);
        } else {
          cmd = 'USER';
          self._send('USER ' + self.options.user, reentry, true);
        }
      } else if (cmd === 'USER') {
        if (code !== 230) {
          // password required
          if (!self.options.password) {
            self.emit('error', makeError(code, 'Password required'));
            return self._socket && self._socket.end();
          }
          cmd = 'PASS';
          self._send('PASS ' + self.options.password, reentry, true);
        } else {
          // no password required
          cmd = 'PASS';
          reentry(undefined, text, code);
        }
      } else if (cmd === 'PASS') {
        cmd = 'FEAT';
        self._send(cmd, reentry, true);
      } else if (cmd === 'FEAT') {
        if (!err)
          self._feat = Parser.parseFeat(text);
        cmd = 'TYPE';
        self._send('TYPE I', reentry, true);
      } else if (cmd === 'TYPE')
        self.emit('ready');
      else if (cmd === 'PBSZ') {
        cmd = 'PROT';
        self._send('PROT P', reentry, true);
      } else if (cmd === 'PROT') {
        cmd = 'USER';
        self._send('USER ' + self.options.user, reentry, true);
      } else if (cmd.substr(0, 4) === 'AUTH') {
        if (cmd === 'AUTH TLS' && code !== 234) {
          cmd = 'AUTH SSL';
          return self._send(cmd, reentry, true);
        } else if (cmd === 'AUTH TLS')
          self._secstate = 'upgraded-tls';
        else if (cmd === 'AUTH SSL')
          self._secstate = 'upgraded-ssl';
        socket.removeAllListeners('data');
        socket.removeAllListeners('error');
        socket._decoder = null;
        self._curReq = null; // prevent queue from being processed during
                             // TLS/SSL negotiation
        secureOptions.socket = self._socket;
        secureOptions.session = undefined;
        socket = tls.connect(secureOptions, onconnect);
        socket.setEncoding('binary');
        socket.on('data', ondata);
        socket.once('end', onend);
        socket.on('error', onerror);
      }
    }
  }

  socket.on('data', ondata);
  function ondata(chunk) {
    debug&&debug('[connection] < ' + inspect(chunk.toString('binary')));
    if (self._parser)
      self._parser.write(chunk);
  }

  socket.on('error', onerror);
  function onerror(err) {
    clearTimeout(timer);
    clearTimeout(self._keepalive);
    self.emit('error', err);
  }

  socket.once('end', onend);
  function onend() {
    ondone();
    self.emit('end');
  }

  socket.once('close', function(had_err) {
    ondone();
    self.emit('close', had_err);
  });

  var hasReset = false;
  function ondone() {
    if (!hasReset) {
      hasReset = true;
      clearTimeout(timer);
      self._reset();
    }
  }

  var timer = setTimeout(function() {
    self.emit('error', new Error('Timeout while connecting to server'));
    self._socket && self._socket.destroy();
    self._reset();
  }, this.options.connTimeout);

  this._socket.connect(this.options.port, this.options.host);
};

FTP.prototype.end = function() {
  if (this._queue.length)
    this._ending = true;
  else
    this._reset();
};

FTP.prototype.destroy = function() {
  this._reset();
};

// "Standard" (RFC 959) commands
FTP.prototype.ascii = function(cb) {
  return this._send('TYPE A', cb);
};

FTP.prototype.binary = function(cb) {
  return this._send('TYPE I', cb);
};

FTP.prototype.abort = function(immediate, cb) {
  if (typeof immediate === 'function') {
    cb = immediate;
    immediate = true;
  }
  if (immediate)
    this._send('ABOR', cb, true);
  else
    this._send('ABOR', cb);
};

FTP.prototype.cwd = function(path, cb, promote) {
  this._send('CWD ' + path, function(err, text, code) {
    if (err)
      return cb(err);
    var m = RE_WD.exec(text);
    cb(undefined, m ? m[1] : undefined);
  }, promote);
};

FTP.prototype.delete = function(path, cb) {
  this._send('DELE ' + path, cb);
};

FTP.prototype.site = function(cmd, cb) {
  this._send('SITE ' + cmd, cb);
};

FTP.prototype.status = function(cb) {
  this._send('STAT', cb);
};

FTP.prototype.rename = function(from, to, cb) {
  var self = this;
  this._send('RNFR ' + from, function(err) {
    if (err)
      return cb(err);

    self._send('RNTO ' + to, cb, true);
  });
};

FTP.prototype.logout = function(cb) {
  this._send('QUIT', cb);
};

FTP.prototype.listSafe = function(path, zcomp, cb) {
  if (typeof path === 'string') {
    var self = this;
    // store current path
    this.pwd(function(err, origpath) {
      if (err) return cb(err);
      // change to destination path
      self.cwd(path, function(err) {
        if (err) return cb(err);
        // get dir listing
        self.list(zcomp || false, function(err, list) {
          // change back to original path
          if (err) return self.cwd(origpath, cb);
          self.cwd(origpath, function(err) {
            if (err) return cb(err);
            cb(err, list);
          });
        });
      });
    });
  } else
    this.list(path, zcomp, cb);
};

FTP.prototype.list = function(path, zcomp, cb) {
  var self = this, cmd;

  if (typeof path === 'function') {
    // list(function() {})
    cb = path;
    path = undefined;
    cmd = 'LIST';
    zcomp = false;
  } else if (typeof path === 'boolean') {
    // list(true, function() {})
    cb = zcomp;
    zcomp = path;
    path = undefined;
    cmd = 'LIST';
  } else if (typeof zcomp === 'function') {
    // list('/foo', function() {})
    cb = zcomp;
    cmd = 'LIST ' + path;
    zcomp = false;
  } else
    cmd = 'LIST ' + path;

  this._pasv(function(err, sock) {
    if (err)
      return cb(err);

    if (self._queue[0] && self._queue[0].cmd === 'ABOR') {
      sock.destroy();
      return cb();
    }

    var sockerr, done = false, replies = 0, entries, buffer = '', source = sock;

    if (zcomp) {
      source = zlib.createInflate();
      sock.pipe(source);
    }

    source.on('data', function(chunk) { buffer += chunk.toString('binary'); });
    source.once('error', function(err) {
      if (!sock.aborting)
        sockerr = err;
    });
    source.once('end', ondone);
    source.once('close', ondone);

    function ondone() {
      done = true;
      final();
    }
    function final() {
      if (done && replies === 2) {
        replies = 3;
        if (sockerr)
          return cb(new Error('Unexpected data connection error: ' + sockerr));
        if (sock.aborting)
          return cb();

        // process received data
        entries = buffer.split(RE_EOL);
        entries.pop(); // ending EOL
        var parsed = [];
        for (var i = 0, len = entries.length; i < len; ++i) {
          var parsedVal = Parser.parseListEntry(entries[i]);
          if (parsedVal !== null)
            parsed.push(parsedVal);
        }

        if (zcomp) {
          self._send('MODE S', function() {
            cb(undefined, parsed);
          }, true);
        } else
          cb(undefined, parsed);
      }
    }

    if (zcomp) {
      self._send('MODE Z', function(err, text, code) {
        if (err) {
          sock.destroy();
          return cb(makeError(code, 'Compression not supported'));
        }
        sendList();
      }, true);
    } else
      sendList();

    function sendList() {
      // this callback will be executed multiple times, the first is when server
      // replies with 150 and then a final reply to indicate whether the
      // transfer was actually a success or not
      self._send(cmd, function(err, text, code) {
        if (err) {
          sock.destroy();
          if (zcomp) {
            self._send('MODE S', function() {
              cb(err);
            }, true);
          } else
            cb(err);
          return;
        }

        // some servers may not open a data connection for empty directories
        if (++replies === 1 && code === 226) {
          replies = 2;
          sock.destroy();
          final();
        } else if (replies === 2)
          final();
      }, true);
    }
  });
};

FTP.prototype.get = function(path, zcomp, cb) {
  var self = this;
  if (typeof zcomp === 'function') {
    cb = zcomp;
    zcomp = false;
  }

  this._pasv(function(err, sock) {
    if (err)
      return cb(err);

    if (self._queue[0] && self._queue[0].cmd === 'ABOR') {
      sock.destroy();
      return cb();
    }

    // modify behavior of socket events so that we can emit 'error' once for
    // either a TCP-level error OR an FTP-level error response that we get when
    // the socket is closed (e.g. the server ran out of space).
    var sockerr, started = false, lastreply = false, done = false,
        source = sock;

    if (zcomp) {
      source = zlib.createInflate();
      sock.pipe(source);
      sock._emit = sock.emit;
      sock.emit = function(ev, arg1) {
        if (ev === 'error') {
          if (!sockerr)
            sockerr = arg1;
          return;
        }
        sock._emit.apply(sock, Array.prototype.slice.call(arguments));
      };
    }

    source._emit = source.emit;
    source.emit = function(ev, arg1) {
      if (ev === 'error') {
        if (!sockerr)
          sockerr = arg1;
        return;
      } else if (ev === 'end' || ev === 'close') {
        if (!done) {
          done = true;
          ondone();
        }
        return;
      }
      source._emit.apply(source, Array.prototype.slice.call(arguments));
    };

    function ondone() {
      if (done && lastreply) {
        self._send('MODE S', function() {
          source._emit('end');
          source._emit('close');
        }, true);
      }
    }

    sock.pause();

    if (zcomp) {
      self._send('MODE Z', function(err, text, code) {
        if (err) {
          sock.destroy();
          return cb(makeError(code, 'Compression not supported'));
        }
        sendRetr();
      }, true);
    } else
      sendRetr();

    function sendRetr() {
      // this callback will be executed multiple times, the first is when server
      // replies with 150, then a final reply after the data connection closes
      // to indicate whether the transfer was actually a success or not
      self._send('RETR ' + path, function(err, text, code) {
        if (sockerr || err) {
          sock.destroy();
          if (!started) {
            if (zcomp) {
              self._send('MODE S', function() {
                cb(sockerr || err);
              }, true);
            } else
              cb(sockerr || err);
          } else {
            source._emit('error', sockerr || err);
            source._emit('close', true);
          }
          return;
        }
        // server returns 125 when data connection is already open; we treat it
        // just like a 150
        if (code === 150 || code === 125) {
          started = true;
          cb(undefined, source);
          sock.resume();
        } else {
          lastreply = true;
          ondone();
        }
      }, true);
    }
  });
};

FTP.prototype.put = function(input, path, zcomp, cb) {
  this._store('STOR ' + path, input, zcomp, cb);
};

FTP.prototype.append = function(input, path, zcomp, cb) {
  this._store('APPE ' + path, input, zcomp, cb);
};

FTP.prototype.pwd = function(cb) { // PWD is optional
  var self = this;
  this._send('PWD', function(err, text, code) {
    if (code === 502) {
      return self.cwd('.', function(cwderr, cwd) {
        if (cwderr)
          return cb(cwderr);
        if (cwd === undefined)
          cb(err);
        else
          cb(undefined, cwd);
      }, true);
    } else if (err)
      return cb(err);
    cb(undefined, RE_WD.exec(text)[1]);
  });
};

FTP.prototype.cdup = function(cb) { // CDUP is optional
  var self = this;
  this._send('CDUP', function(err, text, code) {
    if (code === 502)
      self.cwd('..', cb, true);
    else
      cb(err);
  });
};

FTP.prototype.mkdir = function(path, recursive, cb) { // MKD is optional
  if (typeof recursive === 'function') {
    cb = recursive;
    recursive = false;
  }
  if (!recursive)
    this._send('MKD ' + path, cb);
  else {
    var self = this, owd, abs, dirs, dirslen, i = -1, searching = true;

    abs = (path[0] === '/');

    var nextDir = function() {
      if (++i === dirslen) {
        // return to original working directory
        return self._send('CWD ' + owd, cb, true);
      }
      if (searching) {
        self._send('CWD ' + dirs[i], function(err, text, code) {
          if (code === 550) {
            searching = false;
            --i;
          } else if (err) {
            // return to original working directory
            return self._send('CWD ' + owd, function() {
              cb(err);
            }, true);
          }
          nextDir();
        }, true);
      } else {
        self._send('MKD ' + dirs[i], function(err, text, code) {
          if (err) {
            // return to original working directory
            return self._send('CWD ' + owd, function() {
              cb(err);
            }, true);
          }
          self._send('CWD ' + dirs[i], nextDir, true);
        }, true);
      }
    };
    this.pwd(function(err, cwd) {
      if (err)
        return cb(err);
      owd = cwd;
      if (abs)
        path = path.substr(1);
      if (path[path.length - 1] === '/')
        path = path.substring(0, path.length - 1);
      dirs = path.split('/');
      dirslen = dirs.length;
      if (abs)
        self._send('CWD /', function(err) {
          if (err)
            return cb(err);
          nextDir();
        }, true);
      else
        nextDir();
    });
  }
};

FTP.prototype.rmdir = function(path, recursive, cb) { // RMD is optional
  if (typeof recursive === 'function') {
    cb = recursive;
    recursive = false;
  }
  if (!recursive) {
    return this._send('RMD ' + path, cb);
  }
  
  var self = this;
  this.list(path, function(err, list) {
    if (err) return cb(err);
    var idx = 0;
    
    // this function will be called once per listing entry
    var deleteNextEntry;
    deleteNextEntry = function(err) {
      if (err) return cb(err);
      if (idx >= list.length) {
        if (list[0] && list[0].name === path) {
          return cb(null);
        } else {
          return self.rmdir(path, cb);
        }
      }
      
      var entry = list[idx++];
      
      // get the path to the file
      var subpath = null;
      if (entry.name[0] === '/') {
        // this will be the case when you call deleteRecursively() and pass
        // the path to a plain file
        subpath = entry.name;
      } else {
        if (path[path.length - 1] == '/') {
          subpath = path + entry.name;
        } else {
          subpath = path + '/' + entry.name
        }
      }
      
      // delete the entry (recursively) according to its type
      if (entry.type === 'd') {
        if (entry.name === "." || entry.name === "..") {
          return deleteNextEntry();
        }
        self.rmdir(subpath, true, deleteNextEntry);
      } else {
        self.delete(subpath, deleteNextEntry);
      }
    }
    deleteNextEntry();
  });
};

FTP.prototype.system = function(cb) { // SYST is optional
  this._send('SYST', function(err, text) {
    if (err)
      return cb(err);
    cb(undefined, RE_SYST.exec(text)[1]);
  });
};

// "Extended" (RFC 3659) commands
FTP.prototype.size = function(path, cb) {
  var self = this;
  this._send('SIZE ' + path, function(err, text, code) {
    if (code === 502) {
      // Note: this may cause a problem as list() is _appended_ to the queue
      return self.list(path, function(err, list) {
        if (err)
          return cb(err);
        if (list.length === 1)
          cb(undefined, list[0].size);
        else {
          // path could have been a directory and we got a listing of its
          // contents, but here we echo the behavior of the real SIZE and
          // return 'File not found' for directories
          cb(new Error('File not found'));
        }
      }, true);
    } else if (err)
      return cb(err);
    cb(undefined, parseInt(text, 10));
  });
};

FTP.prototype.lastMod = function(path, cb) {
  var self = this;
  this._send('MDTM ' + path, function(err, text, code) {
    if (code === 502) {
      return self.list(path, function(err, list) {
        if (err)
          return cb(err);
        if (list.length === 1)
          cb(undefined, list[0].date);
        else
          cb(new Error('File not found'));
      }, true);
    } else if (err)
      return cb(err);
    var val = XRegExp.exec(text, REX_TIMEVAL), ret;
    if (!val)
      return cb(new Error('Invalid date/time format from server'));
    ret = new Date(val.year + '-' + val.month + '-' + val.date + 'T' + val.hour
                   + ':' + val.minute + ':' + val.second);
    cb(undefined, ret);
  });
};

FTP.prototype.restart = function(offset, cb) {
  this._send('REST ' + offset, cb);
};



// Private/Internal methods
FTP.prototype._pasv = function(cb) {
  var self = this, first = true, ip, port;
  this._send('PASV', function reentry(err, text) {
    if (err)
      return cb(err);

    self._curReq = undefined;

    if (first) {
      var m = RE_PASV.exec(text);
      if (!m)
        return cb(new Error('Unable to parse PASV server response'));
      ip = m[1];
      ip += '.';
      ip += m[2];
      ip += '.';
      ip += m[3];
      ip += '.';
      ip += m[4];
      port = (parseInt(m[5], 10) * 256) + parseInt(m[6], 10);

      first = false;
    }
    self._pasvConnect(ip, port, function(err, sock) {
      if (err) {
        // try the IP of the control connection if the server was somehow
        // misconfigured and gave for example a LAN IP instead of WAN IP over
        // the Internet
        if (self._socket && ip !== self._socket.remoteAddress) {
          ip = self._socket.remoteAddress;
          return reentry();
        }

        // automatically abort PASV mode
        self._send('ABOR', function() {
          cb(err);
          self._send();
        }, true);

        return;
      }
      cb(undefined, sock);
      self._send();
    });
  });
};

FTP.prototype._pasvConnect = function(ip, port, cb) {
  var self = this,
      socket = new Socket(),
      sockerr,
      timedOut = false,
      timer = setTimeout(function() {
        timedOut = true;
        socket.destroy();
        cb(new Error('Timed out while making data connection'));
      }, this.options.pasvTimeout);

  socket.setTimeout(0);

  socket.once('connect', function() {
    self._debug&&self._debug('[connection] PASV socket connected');
    if (self.options.secure === true) {
      self.options.secureOptions.socket = socket;
      self.options.secureOptions.session = self._socket.getSession();
      //socket.removeAllListeners('error');
      socket = tls.connect(self.options.secureOptions);
      //socket.once('error', onerror);
      socket.setTimeout(0);
    }
    clearTimeout(timer);
    self._pasvSocket = socket;
    cb(undefined, socket);
  });
  socket.once('error', onerror);
  function onerror(err) {
    sockerr = err;
  }
  socket.once('end', function() {
    clearTimeout(timer);
  });
  socket.once('close', function(had_err) {
    clearTimeout(timer);
    if (!self._pasvSocket && !timedOut) {
      var errmsg = 'Unable to make data connection';
      if (sockerr) {
        errmsg += '( ' + sockerr + ')';
        sockerr = undefined;
      }
      cb(new Error(errmsg));
    }
    self._pasvSocket = undefined;
  });

  socket.connect(port, ip);
};

FTP.prototype._store = function(cmd, input, zcomp, cb) {
  var isBuffer = Buffer.isBuffer(input);

  if (!isBuffer && input.pause !== undefined)
    input.pause();

  if (typeof zcomp === 'function') {
    cb = zcomp;
    zcomp = false;
  }

  var self = this;
  this._pasv(function(err, sock) {
    if (err)
      return cb(err);

    if (self._queue[0] && self._queue[0].cmd === 'ABOR') {
      sock.destroy();
      return cb();
    }

    var sockerr, dest = sock;
    sock.once('error', function(err) {
      sockerr = err;
    });

    if (zcomp) {
      self._send('MODE Z', function(err, text, code) {
        if (err) {
          sock.destroy();
          return cb(makeError(code, 'Compression not supported'));
        }
        // draft-preston-ftpext-deflate-04 says min of 8 should be supported
        dest = zlib.createDeflate({ level: 8 });
        dest.pipe(sock);
        sendStore();
      }, true);
    } else
      sendStore();

    function sendStore() {
      // this callback will be executed multiple times, the first is when server
      // replies with 150, then a final reply after the data connection closes
      // to indicate whether the transfer was actually a success or not
      self._send(cmd, function(err, text, code) {
        if (sockerr || err) {
          if (zcomp) {
            self._send('MODE S', function() {
              cb(sockerr || err);
            }, true);
          } else
            cb(sockerr || err);
          return;
        }

        if (code === 150 || code === 125) {
          if (isBuffer)
            dest.end(input);
          else if (typeof input === 'string') {
            // check if input is a file path or just string data to store
            fs.stat(input, function(err, stats) {
              if (err)
                dest.end(input);
              else
                fs.createReadStream(input).pipe(dest);
            });
          } else {
            input.pipe(dest);
            input.resume();
          }
        } else {
          if (zcomp)
            self._send('MODE S', cb, true);
          else
            cb();
        }
      }, true);
    }
  });
};

FTP.prototype._send = function(cmd, cb, promote) {
  clearTimeout(this._keepalive);
  if (cmd !== undefined) {
    if (promote)
      this._queue.unshift({ cmd: cmd, cb: cb });
    else
      this._queue.push({ cmd: cmd, cb: cb });
  }
  var queueLen = this._queue.length;
  if (!this._curReq && queueLen && this._socket && this._socket.readable) {
    this._curReq = this._queue.shift();
    if (this._curReq.cmd === 'ABOR' && this._pasvSocket)
      this._pasvSocket.aborting = true;
    this._debug&&this._debug('[connection] > ' + inspect(this._curReq.cmd));
    this._socket.write(this._curReq.cmd + '\r\n');
  } else if (!this._curReq && !queueLen && this._ending)
    this._reset();
};

FTP.prototype._reset = function() {
  if (this._pasvSock && this._pasvSock.writable)
    this._pasvSock.end();
  if (this._socket && this._socket.writable)
    this._socket.end();
  this._socket = undefined;
  this._pasvSock = undefined;
  this._feat = undefined;
  this._curReq = undefined;
  this._secstate = undefined;
  clearTimeout(this._keepalive);
  this._keepalive = undefined;
  this._queue = [];
  this._ending = false;
  this._parser = undefined;
  this.options.host = this.options.port = this.options.user
                    = this.options.password = this.options.secure
                    = this.options.connTimeout = this.options.pasvTimeout
                    = this.options.keepalive = this._debug = undefined;
  this.connected = false;
};

// Utility functions
function makeError(code, text) {
  var err = new Error(text);
  err.code = code;
  return err;
}
