// commands for packing and unpacking tarballs
// this file is used by lib/cache.js

var npm = require("../npm.js")
  , fs = require("graceful-fs")
  , path = require("path")
  , log = require("npmlog")
  , uidNumber = require("uid-number")
  , rm = require("rimraf")
  , readJson = require("read-package-json")
  , cache = require("../cache.js")
  , myUid = process.getuid && process.getuid()
  , myGid = process.getgid && process.getgid()
  , tar = require("tar")
  , zlib = require("zlib")
  , fstream = require("fstream")
  , Packer = require("fstream-npm")
  , lifecycle = require("./lifecycle.js")

function lock(path, cb) {
  return cache.lock('tar://' + path, cb)
}

function unlock(path, cb) {
  return cache.unlock('tar://' + path, cb)
}

if (process.env.SUDO_UID && myUid === 0) {
  if (!isNaN(process.env.SUDO_UID)) myUid = +process.env.SUDO_UID
  if (!isNaN(process.env.SUDO_GID)) myGid = +process.env.SUDO_GID
}

exports.pack = pack
exports.unpack = unpack

function pack (tarball, folder, pkg, dfc, cb) {
  log.verbose("tar pack", [tarball, folder])
  if (typeof cb !== "function") cb = dfc, dfc = false

  log.verbose("tarball", tarball)
  log.verbose("folder", folder)

  if (dfc) {
    // do fancy crap
    return lifecycle(pkg, "prepublish", folder, function (er) {
      if (er) return cb(er)
      pack_(tarball, folder, pkg, cb)
    })
  } else {
    pack_(tarball, folder, pkg, cb)
  }
}

function pack_ (tarball, folder, pkg, cb_) {
  var tarballLock = false
    , folderLock = false

  function cb (er) {
    if (folderLock)
      unlock(folder, function() {
        folderLock = false
        cb(er)
      })
    else if (tarballLock)
      unlock(tarball, function() {
        tarballLock = false
        cb(er)
      })
    else
      cb_(er)
  }

  lock(folder, function(er) {
    if (er) return cb(er)
    folderLock = true
    next()
  })

  lock(tarball, function (er) {
    if (er) return cb(er)
    tarballLock = true
    next()
  })

  function next () {
    if (!tarballLock || !folderLock) return

    new Packer({ path: folder, type: "Directory", isDirectory: true })
      .on("error", function (er) {
        if (er) log.error("tar pack", "Error reading " + folder)
        return cb(er)
      })

      // By default, npm includes some proprietary attributes in the
      // package tarball.  This is sane, and allowed by the spec.
      // However, npm *itself* excludes these from its own package,
      // so that it can be more easily bootstrapped using old and
      // non-compliant tar implementations.
      .pipe(tar.Pack({ noProprietary: !npm.config.get("proprietary-attribs") }))
      .on("error", function (er) {
        if (er) log.error("tar.pack", "tar creation error", tarball)
        cb(er)
      })
      .pipe(zlib.Gzip())
      .on("error", function (er) {
        if (er) log.error("tar.pack", "gzip error "+tarball)
        cb(er)
      })
      .pipe(fstream.Writer({ type: "File", path: tarball }))
      .on("error", function (er) {
        if (er) log.error("tar.pack", "Could not write "+tarball)
        cb(er)
      })
      .on("close", cb)
  }
}


function unpack (tarball, unpackTarget, dMode, fMode, uid, gid, cb) {
  log.verbose("tar unpack", tarball)
  if (typeof cb !== "function") cb = gid, gid = null
  if (typeof cb !== "function") cb = uid, uid = null
  if (typeof cb !== "function") cb = fMode, fMode = npm.modes.file
  if (typeof cb !== "function") cb = dMode, dMode = npm.modes.exec

  uidNumber(uid, gid, function (er, uid, gid) {
    if (er) return cb(er)
    unpack_(tarball, unpackTarget, dMode, fMode, uid, gid, cb)
  })
}

function unpack_ ( tarball, unpackTarget, dMode, fMode, uid, gid, cb_ ) {
  var parent = path.dirname(unpackTarget)
    , base = path.basename(unpackTarget)
    , folderLock
    , tarballLock

  function cb (er) {
    if (folderLock)
      unlock(unpackTarget, function() {
        folderLock = false
        cb(er)
      })
    else if (tarballLock)
      unlock(tarball, function() {
        tarballLock = false
        cb(er)
      })
    else
      cb_(er)
  }

  lock(unpackTarget, function (er) {
    if (er) return cb(er)
    folderLock = true
    next()
  })

  lock(tarball, function (er) {
    if (er) return cb(er)
    tarballLock = true
    next()
  })

  function next() {
    if (!tarballLock || !folderLock) return
    rmGunz()
  }

  function rmGunz () {
    rm(unpackTarget, function (er) {
      if (er) return cb(er)
      gtp()
    })
  }

  function gtp () {
    // gzip {tarball} --decompress --stdout \
    //   | tar -mvxpf - --strip-components=1 -C {unpackTarget}
    gunzTarPerm( tarball, unpackTarget
               , dMode, fMode
               , uid, gid
               , function (er, folder) {
      if (er) return cb(er)
      readJson(path.resolve(folder, "package.json"), cb)
    })
  }
}


function gunzTarPerm (tarball, target, dMode, fMode, uid, gid, cb_) {
  if (!dMode) dMode = npm.modes.exec
  if (!fMode) fMode = npm.modes.file
  log.silly("gunzTarPerm", "modes", [dMode.toString(8), fMode.toString(8)])

  var cbCalled = false
  function cb (er) {
    if (cbCalled) return
    cbCalled = true
    cb_(er, target)
  }

  var fst = fs.createReadStream(tarball)

  // figure out who we're supposed to be, if we're not pretending
  // to be a specific user.
  if (npm.config.get("unsafe-perm") && process.platform !== "win32") {
    uid = myUid
    gid = myGid
  }

  function extractEntry (entry) {
    log.silly("gunzTarPerm", "extractEntry", entry.path)
    // never create things that are user-unreadable,
    // or dirs that are user-un-listable. Only leads to headaches.
    var originalMode = entry.mode = entry.mode || entry.props.mode
    entry.mode = entry.mode | (entry.type === "Directory" ? dMode : fMode)
    entry.mode = entry.mode & (~npm.modes.umask)
    entry.props.mode = entry.mode
    if (originalMode !== entry.mode) {
      log.silly( "gunzTarPerm", "modified mode"
               , [entry.path, originalMode, entry.mode])
    }

    // if there's a specific owner uid/gid that we want, then set that
    if (process.platform !== "win32" &&
        typeof uid === "number" &&
        typeof gid === "number") {
      entry.props.uid = entry.uid = uid
      entry.props.gid = entry.gid = gid
    }
  }

  var extractOpts = { type: "Directory", path: target, strip: 1 }

  if (process.platform !== "win32" &&
      typeof uid === "number" &&
      typeof gid === "number") {
    extractOpts.uid = uid
    extractOpts.gid = gid
  }

  extractOpts.filter = function () {
    // symbolic links are not allowed in packages.
    if (this.type.match(/^.*Link$/)) {
      log.warn( "excluding symbolic link"
              , this.path.substr(target.length + 1)
              + " -> " + this.linkpath )
      return false
    }
    return true
  }


  fst.on("error", function (er) {
    if (er) log.error("tar.unpack", "error reading "+tarball)
    cb(er)
  })
  fst.on("data", function OD (c) {
    // detect what it is.
    // Then, depending on that, we'll figure out whether it's
    // a single-file module, gzipped tarball, or naked tarball.
    // gzipped files all start with 1f8b08
    if (c[0] === 0x1F &&
        c[1] === 0x8B &&
        c[2] === 0x08) {
      fst
        .pipe(zlib.Unzip())
        .on("error", function (er) {
          if (er) log.error("tar.unpack", "unzip error "+tarball)
          cb(er)
        })
        .pipe(tar.Extract(extractOpts))
        .on("entry", extractEntry)
        .on("error", function (er) {
          if (er) log.error("tar.unpack", "untar error "+tarball)
          cb(er)
        })
        .on("close", cb)
    } else if (c.toString().match(/^package\//)) {
      // naked tar
      fst
        .pipe(tar.Extract(extractOpts))
        .on("entry", extractEntry)
        .on("error", function (er) {
          if (er) log.error("tar.unpack", "untar error "+tarball)
          cb(er)
        })
        .on("close", cb)
    } else {
      // naked js file
      var jsOpts = { path: path.resolve(target, "index.js") }

      if (process.platform !== "win32" &&
          typeof uid === "number" &&
          typeof gid === "number") {
        jsOpts.uid = uid
        jsOpts.gid = gid
      }

      fst
        .pipe(fstream.Writer(jsOpts))
        .on("error", function (er) {
          if (er) log.error("tar.unpack", "copy error "+tarball)
          cb(er)
        })
        .on("close", function () {
          var j = path.resolve(target, "package.json")
          readJson(j, function (er, d) {
            if (er) {
              log.error("not a package", tarball)
              return cb(er)
            }
            fs.writeFile(j, JSON.stringify(d) + "\n", cb)
          })
        })
    }

    // now un-hook, and re-emit the chunk
    fst.removeListener("data", OD)
    fst.emit("data", c)
  })
}
