text: 'use strict';
  
  const { parseDateTime } = require('./util');
  
  module.exports = Parse.create = Parse;
  
  const Transform = require('readable-stream/transform');
  const inherits = require('util').inherits;
  const zlib = require('zlib');
  const binary = require('binary');
  const PullStream = require('pullstream');
  const MatchStream = require('match-stream');
  const Entry = require('./entry');
  
  inherits(Parse, Transform);
  
  function Parse(opts) {
    var self = this;
    if (!(this instanceof Parse)) {
      return new Parse(opts);
    }
  
    Transform.call(this, { lowWaterMark: 0 });
    this._opts = opts || { verbose: false };
    this._hasEntryListener = false;
  
    this._pullStream = new PullStream();
    this._pullStream.on("error", function (e) {
      self.emit('error', e);
    });
    this._pullStream.once("end", function () {
      self._streamEnd = true;
    });
    this._pullStream.once("finish", function () {
      self.emit('cd.entries', self.entries);
      self._streamFinish = true;
    });
  
    this._readRecord();
    this.entries = [];
  }
  
  Parse.prototype._readRecord = function () {
    var self = this;
    this._pullStream.pull(4, function (err, data) {
      if (err) {
        return self.emit('error', err);
      }
  
      if (data.length === 0) {
        return;
      }
  
      var signature = data.readUInt32LE(0);
      if (signature === 0x04034b50) {
        self._readFile();
      } else if (signature === 0x02014b50) {
        self._readCentralDirectoryFileHeader();
      } else if (signature === 0x06054b50) {
        self._readEndOfCentralDirectoryRecord();
      } else {
        err = new Error('invalid signature: 0x' + signature.toString(16));
        self.emit('error', err);
      }
    });
  };
  
  Parse.prototype._readFile = function () {
    var self = this;
    this._pullStream.pull(26, function (err, data) {
      if (err) {
        return self.emit('error', err);
      }
  
      var vars = binary.parse(data)
        .word16lu('versionsNeededToExtract')
        .word16lu('flags')
        .word16lu('compressionMethod')
        .word16lu('lastModifiedTime')
        .word16lu('lastModifiedDate')
        .word32lu('crc32')
        .word32lu('compressedSize')
        .word32lu('uncompressedSize')
        .word16lu('fileNameLength')
        .word16lu('extraFieldLength')
        .vars;
  
      return self._pullStream.pull(vars.fileNameLength, function (err, fileName) {
        if (err) {
          return self.emit('error', err);
        }
        fileName = fileName.toString('utf8');
        var entry = new Entry();
        entry.path = fileName;
        entry.props.path = fileName;
        entry.type = (vars.compressedSize === 0 && /[\/\\]$/.test(fileName)) ? 'Directory' : 'File';
  
        if (self._opts.verbose) {
          if (entry.type === 'Directory') {
            console.log('   creating:', fileName);
          } else if (entry.type === 'File') {
            if (vars.compressionMethod === 0) {
              console.log(' extracting:', fileName);
            } else {
              console.log('  inflating:', fileName);
            }
          }
        }
  
        var hasEntryListener = self._hasEntryListener;
        if (hasEntryListener) {
          self.emit('entry', entry);
        }
  
        self._pullStream.pull(vars.extraFieldLength, function (err, extraField) {
          if (err) {
            return self.emit('error', err);
          }
          if (vars.compressionMethod === 0) {
            self._pullStream.pull(vars.compressedSize, function (err, compressedData) {
              if (err) {
                return self.emit('error', err);
              }
  
              if (hasEntryListener) {
                entry.write(compressedData);
                entry.end();
              }
  
              return self._readRecord();
            });
          } else {
            var fileSizeKnown = !(vars.flags & 0x08);
  
            var inflater = zlib.createInflateRaw();
            inflater.on('error', function (err) {
              self.emit('error', err);
            });
  
            if (fileSizeKnown) {
              entry.size = vars.uncompressedSize;
              if (hasEntryListener) {
                entry.on('finish', self._readRecord.bind(self));
                self._pullStream.pipe(vars.compressedSize, inflater).pipe(entry);
              } else {
                self._pullStream.drain(vars.compressedSize, function (err) {
                  if (err) {
                    return self.emit('error', err);
                  }
                  self._readRecord();
                });
              }
            } else {
              var descriptorSig = new Buffer(4);
              descriptorSig.writeUInt32LE(0x08074b50, 0);
  
              var matchStream = new MatchStream({ pattern: descriptorSig }, function (buf, matched, extra) {
                if (hasEntryListener) {
                  if (!matched) {
                    return this.push(buf);
                  }
                  this.push(buf);
                }
                setImmediate(function() {
                  self._pullStream.unpipe();
                  self._pullStream.prepend(extra);
                  self._processDataDescriptor(entry);
                });
                return this.push(null);
              });
  
              self._pullStream.pipe(matchStream);
              if (hasEntryListener) {
                matchStream.pipe(inflater).pipe(entry);
              }
            }
          }
        });
      });
    });
  };
  
  Parse.prototype._processDataDescriptor = function (entry) {
    var self = this;
    this._pullStream.pull(16, function (err, data) {
      if (err) {
        return self.emit('error', err);
      }
  
      var vars = binary.parse(data)
        .word32lu('dataDescriptorSignature')
        .word32lu('crc32')
        .word32lu('compressedSize')
        .word32lu('uncompressedSize')
        .vars;
  
      entry.size = vars.uncompressedSize;
      self._readRecord();
    });
  };
  
  Parse.prototype._readCentralDirectoryFileHeader = function () {
    var self = this;
    this._pullStream.pull(42, function (err, data) {
      if (err) {
        return self.emit('error', err);
      }
  
      var vars = binary.parse(data)
        .word16lu('versionMadeBy')
        .word16lu('versionsNeededToExtract')
        .word16lu('flags')
        .word16lu('compressionMethod')
        .word16bu('lastModifiedTime')
        .word16bu('lastModifiedDate')
        .word32lu('crc32')
        .word32lu('compressedSize')
        .word32lu('uncompressedSize')
        .word16lu('fileNameLength')
        .word16lu('extraFieldLength')
        .word16lu('fileCommentLength')
        .word16lu('diskNumber')
        .word16lu('internalFileAttributes')
        .word32lu('externalFileAttributes')
        .word32lu('offsetToLocalFileHeader')
        .vars;
  
      return self._pullStream.pull(vars.fileNameLength, function (err, fileName) {
        if (err) {
          return self.emit('error', err);
        }
        self._pullStream.pull(vars.extraFieldLength, function (err, extraField) {
          if (err) {
            return self.emit('error', err);
          }
          self._pullStream.pull(vars.fileCommentLength, function (err, fileComment) {
            if (err) {
              return self.emit('error', err);
            }
            fileName = fileName.toString('utf8');
            const lastModifiedData = data.slice(8, 12);
            const lastModified = parseDateTime(lastModifiedData);
            const info = {
              path: fileName,
              compressedSize: vars.compressedSize,
              uncompressedSize: vars.uncompressedSize,
              lastModified,
              type: (vars.compressedSize === 0 && /[\/\\]$/.test(fileName)) ? 'Directory' : 'File',
            };
            self.entries.push(info);
            self.emit('cd.entry', info);
            return self._readRecord();
          });
        });
      });
    });
  };
  
  Parse.prototype._readEndOfCentralDirectoryRecord = function () {
    var self = this;
    this._pullStream.pull(18, function (err, data) {
      if (err) {
        return self.emit('error', err);
      }
  
      var vars = binary.parse(data)
        .word16lu('diskNumber')
        .word16lu('diskStart')
        .word16lu('numberOfRecordsOnDisk')
        .word16lu('numberOfRecords')
        .word32lu('sizeOfCentralDirectory')
        .word32lu('offsetToStartOfCentralDirectory')
        .word16lu('commentLength')
        .vars;
  
      if (vars.commentLength) {
        setImmediate(function() {
          self._pullStream.pull(vars.commentLength, function (err, comment) {
            if (err) {
              return self.emit('error', err);
            }
            comment = comment.toString('utf8');
            return self._pullStream.end();
          });
        });
  
      } else {
        self._pullStream.end();
      }
    });
  };
  
  Parse.prototype._transform = function (chunk, encoding, callback) {
    if (this._pullStream.write(chunk)) {
      return callback();
    }
  
    this._pullStream.once('drain', callback);
  };
  
  Parse.prototype.pipe = function (dest, opts) {
    var self = this;
    if (typeof dest.add === "function") {
      self.on("entry", function (entry) {
        dest.add(entry);
      })
    }
    return Transform.prototype.pipe.apply(this, arguments);
  };
  
  Parse.prototype._flush = function (callback) {
    if (!this._streamEnd || !this._streamFinish) {
      return setImmediate(this._flush.bind(this, callback));
    }
  
    this.emit('close');
    return callback();
  };
  
  Parse.prototype.addListener = function(type, listener) {
    if ('entry' === type) {
      this._hasEntryListener = true;
    }
    return Transform.prototype.addListener.call(this, type, listener);
  };
  
  Parse.prototype.on = Parse.prototype.addListener;
  