// Taken from https://github.com/maxogden/extract-zip/blob/422a39ff47996b802946169a3a47f1496253c81e/index.js

const fs = require('fs'),
      path = require('path'),
      yauzl = require('yauzl'),
      mkdirp = require('mkdirp');

module.exports = function (zipPath, opts, cb) {
  if (path.isAbsolute(opts.dir) === false) {
    return cb(new Error('Target directory is expected to be absolute'));
  }

  mkdirp(opts.dir, function (err) {
    if (err) { return cb(err); }

    fs.realpath(opts.dir, function (err, canonicalDir) {
      if (err) { return cb(err); }

      opts.dir = canonicalDir;

      openZip(opts);
    });
  });

  /**
   * Unzips the file
   */
  function openZip () {
    yauzl.open(zipPath, { lazyEntries: true }, function (err, zipfile) {
      if (err) { return cb(err); }

      var cancelled = false;

      zipfile.readEntry();

      zipfile.on('close', function () {
        if (!cancelled) {
          return cb();
        }
      });

      zipfile.on('entry', function (entry) {
        if (cancelled) {
          return;
        }


        if (/^__MACOSX\//.test(entry.fileName)) {
          // dir name starts with __MACOSX/
          zipfile.readEntry();
          return;
        }

        var destDir = path.dirname(path.join(opts.dir, entry.fileName));

        mkdirp(destDir, function (err) {
          if (err) {
            cancelled = true;
            zipfile.close();
            return cb(err);
          }

          fs.realpath(destDir, function (err, canonicalDestDir) {
            if (err) {
              cancelled = true;
              zipfile.close();
              return cb(err);
            }

            var relativeDestDir = path.relative(opts.dir, canonicalDestDir);

            if (relativeDestDir.split(path.sep).indexOf('..') !== -1) {
              cancelled = true;
              zipfile.close();
              return cb(new Error('Out of bound path "' + canonicalDestDir + '" found while processing file ' +
                entry.fileName));
            }

            extractEntry(entry, function (err) {
              // if any extraction fails then abort everything
              if (err) {
                cancelled = true;
                zipfile.close();
                return cb(err);
              }
              zipfile.readEntry();
            });
          });
        });
      });

      /**
       * Unzips one entry
       *
       * @param {Object} entry
       * @param {Function} done
       */
      function extractEntry (entry, done) {
        if (cancelled) {
          return setImmediate(done);
        }

        if (opts.onEntry) {
          opts.onEntry(entry, zipfile);
        }

        var dest = path.join(opts.dir, entry.fileName),

            // convert external file attr int into a fs stat mode int
            mode = (entry.externalFileAttributes >> 16) & 0xFFFF,

            // check if it's a symlink or dir (using stat mode constants)
            IFMT = 61440,
            IFDIR = 16384,
            IFLNK = 40960,
            symlink = (mode & IFMT) === IFLNK,
            isDir = (mode & IFMT) === IFDIR,
            madeBy,
            umask,
            procMode,
            destDir;

        // Failsafe, borrowed from jsZip
        if (!isDir && entry.fileName.slice(-1) === '/') {
          isDir = true;
        }

        // check for windows weird way of specifying a directory
        // https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566
        madeBy = entry.versionMadeBy >> 8;
        if (!isDir) { isDir = (madeBy === 0 && entry.externalFileAttributes === 16); }

        // if no mode then default to default modes
        if (mode === 0) {
          if (isDir) {
            if (opts.defaultDirMode) { mode = parseInt(opts.defaultDirMode, 10); }
            if (!mode) { mode = 493; } // Default to 0755
          }
          else {
            if (opts.defaultFileMode) { mode = parseInt(opts.defaultFileMode, 10); }
            if (!mode) { mode = 420; } // Default to 0644
          }
        }


        // reverse umask first (~)
        umask = ~process.umask();

        // & with processes umask to override invalid perms
        procMode = mode & umask;

        // always ensure folders are created
        destDir = dest;
        if (!isDir) { destDir = path.dirname(dest); }

        mkdirp(destDir, function (err) {
          if (err) {
            cancelled = true;
            return done(err);
          }

          if (isDir || symlink) { return done(); }

          zipfile.openReadStream(entry, function (err, readStream) {
            if (err) {
              cancelled = true;
              return done(err);
            }

            readStream.on('error', function (err) {
              console.log('read err', err);
            });

            writeStream();

            /**
             * Writes the file to file-system
             */
            function writeStream () {
              var writeStream = fs.createWriteStream(dest, { mode: procMode });
              readStream.pipe(writeStream);

              writeStream.on('finish', function () {
                done();
              });

              writeStream.on('error', function (err) {
                cancelled = true;
                return done(err);
              });
            }
          });
        });
      }
    });
  }
};
