/*

npm outdated [pkg]

Does the following:

1. check for a new version of pkg

If no packages are specified, then run for all installed
packages.

*/

module.exports = outdated

outdated.usage = "npm outdated [<pkg> [<pkg> ...]]"

outdated.completion = require("./utils/completion/installed-deep.js")


var path = require("path")
  , fs = require("graceful-fs")
  , readJson = require("read-package-json")
  , cache = require("./cache.js")
  , asyncMap = require("slide").asyncMap
  , npm = require("./npm.js")
  , url = require("url")

function outdated (args, silent, cb) {
  if (typeof cb !== "function") cb = silent, silent = false
  var dir = path.resolve(npm.dir, "..")
  outdated_(args, dir, {}, function (er, list) {
    if (er || silent) return cb(er, list)
    var outList = list.map(makePretty)
    console.log(outList.join("\n"))
    cb(null, list)
  })
}

// [[ dir, dep, has, want ]]
function makePretty (p) {
  var parseable = npm.config.get("parseable")
    , long = npm.config.get("long")
    , dep = p[1]
    , dir = path.resolve(p[0], "node_modules", dep)
    , has = p[2]
    , want = p[3]

  // XXX add --json support
  // Should match (more or less) the output of ls --json

  if (parseable) {
    var str = dir
    if (npm.config.get("long")) {
      str += ":" + dep + "@" + want
           + ":" + (has ? (dep + "@" + has) : "MISSING")
    }
    return str
  }

  if (!npm.config.get("global")) {
    dir = path.relative(process.cwd(), dir)
  }
  return dep + "@" + want + " " + dir
       + " current=" + (has || "MISSING")
}

function outdated_ (args, dir, parentHas, cb) {
  // get the deps from package.json, or {<dir/node_modules/*>:"*"}
  // asyncMap over deps:
  //   shouldHave = cache.add(dep, req).version
  //   if has === shouldHave then
  //     return outdated(args, dir/node_modules/dep, parentHas + has)
  //   else if dep in args or args is empty
  //     return [dir, dep, has, shouldHave]

  var deps = null
  readJson(path.resolve(dir, "package.json"), function (er, d) {
    if (er && er.code !== "ENOENT" && er.code !== "ENOTDIR") return cb(er)
    deps = (er) ? true : (d.dependencies || {})
    return next()
  })

  var has = null
  fs.readdir(path.resolve(dir, "node_modules"), function (er, pkgs) {
    if (er) {
      has = Object.create(parentHas)
      return next()
    }
    pkgs = pkgs.filter(function (p) {
      return !p.match(/^[\._-]/)
    })
    asyncMap(pkgs, function (pkg, cb) {
      var jsonFile = path.resolve(dir, "node_modules", pkg, "package.json")
      readJson(jsonFile, function (er, d) {
        if (er && er.code !== "ENOENT" && er.code !== "ENOTDIR") return cb(er)
        cb(null, er ? [] : [[d.name, d.version, d._from]])
      })
    }, function (er, pvs) {
      if (er) return cb(er)
      has = Object.create(parentHas)
      pvs.forEach(function (pv) {
        has[pv[0]] = {
          version: pv[1],
          from: pv[2]
        }
      })

      next()
    })
  })

  function next () {
    if (!has || !deps) return
    if (deps === true) {
      deps = Object.keys(has).reduce(function (l, r) {
        l[r] = "*"
        return l
      }, {})
    }

    // now get what we should have, based on the dep.
    // if has[dep] !== shouldHave[dep], then cb with the data
    // otherwise dive into the folder
    asyncMap(Object.keys(deps), function (dep, cb) {
      shouldUpdate(args, dir, dep, has, deps[dep], cb)
    }, cb)
  }
}

function shouldUpdate (args, dir, dep, has, req, cb) {
  // look up the most recent version.
  // if that's what we already have, or if it's not on the args list,
  // then dive into it.  Otherwise, cb() with the data.

  // { version: , from: }
  var curr = has[dep]

  function skip () {
    outdated_( args
             , path.resolve(dir, "node_modules", dep)
             , has
             , cb )
  }

  function doIt (shouldHave) {
    cb(null, [[ dir, dep, curr && curr.version, shouldHave, req ]])
  }

  if (args.length && args.indexOf(dep) === -1) {
    return skip()
  }

  // so, we can conceivably update this.  find out if we need to.
  cache.add(dep, req, function (er, d) {
    // if this fails, then it means we can't update this thing.
    // it's probably a thing that isn't published.
    if (er) return skip()

    // check that the url origin hasn't changed (#1727) and that
    // there is no newer version available
    var dFromUrl = d._from && url.parse(d._from).protocol
    var cFromUrl = curr && curr.from && url.parse(curr.from).protocol

    if (!curr || dFromUrl && cFromUrl && d._from !== curr.from
        || d.version !== curr.version)
      doIt(d.version)
    else
      skip()
  })
}
