
// show the installed versions of packages
//
// --parseable creates output like this:
// <fullpath>:<name@ver>:<realpath>:<flags>
// Flags are a :-separated list of zero or more indicators

module.exports = exports = ls

var npm = require("./npm.js")
  , readInstalled = require("read-installed")
  , log = require("npmlog")
  , path = require("path")
  , archy = require("archy")
  , semver = require("semver")
  , url = require("url")
  , color = require("ansicolors")
  , npa = require("npm-package-arg")

ls.usage = "npm ls"

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

function ls (args, silent, cb) {
  if (typeof cb !== "function") cb = silent, silent = false

  var dir = path.resolve(npm.dir, "..")

  // npm ls 'foo@~1.3' bar 'baz@<2'
  if (!args) args = []
  else args = args.map(function (a) {
    var p = npa(a)
      , name = p.name
      , ver = semver.validRange(p.rawSpec) || ""

    return [ name, ver ]
  })

  var depth = npm.config.get("depth")
  var opt = { depth: depth, log: log.warn, dev: true }
  readInstalled(dir, opt, function (er, data) {
    pruneNestedExtraneous(data)
    filterByEnv(data)
    var bfs = bfsify(data, args)
      , lite = getLite(bfs)

    if (er || silent) return cb(er, data, lite)

    var long = npm.config.get("long")
      , json = npm.config.get("json")
      , out
    if (json) {
      var seen = []
      var d = long ? bfs : lite
      // the raw data can be circular
      out = JSON.stringify(d, function (k, o) {
        if (typeof o === "object") {
          if (-1 !== seen.indexOf(o)) return "[Circular]"
          seen.push(o)
        }
        return o
      }, 2)
    } else if (npm.config.get("parseable")) {
      out = makeParseable(bfs, long, dir)
    } else if (data) {
      out = makeArchy(bfs, long, dir)
    }
    console.log(out)

    if (args.length && !data._found) process.exitCode = 1

    // if any errors were found, then complain and exit status 1
    if (lite.problems && lite.problems.length) {
      er = lite.problems.join("\n")
    }
    cb(er, data, lite)
  })
}

function pruneNestedExtraneous (data, visited) {
  visited = visited || []
  visited.push(data)
  for (var i in data.dependencies) {
    if (data.dependencies[i].extraneous) {
      data.dependencies[i].dependencies = {}
    } else if (visited.indexOf(data.dependencies[i]) === -1) {
      pruneNestedExtraneous(data.dependencies[i], visited)
    }
  }
}

function filterByEnv (data) {
  var dev = npm.config.get("dev")
  var production = npm.config.get("production")
  if (dev === production) return
  var dependencies = {}
  var devDependencies = data.devDependencies || []
  Object.keys(data.dependencies).forEach(function (name) {
    var keys = Object.keys(devDependencies)
    if (production && keys.indexOf(name) !== -1) return
    if (dev && keys.indexOf(name) === -1) return
    dependencies[name] = data.dependencies[name]
  })
  data.dependencies = dependencies
}

function alphasort (a, b) {
  a = a.toLowerCase()
  b = b.toLowerCase()
  return a > b ? 1
       : a < b ? -1 : 0
}

function isCruft (data) {
  return data.extraneous && data.error && data.error.code === 'ENOTDIR'
}

function getLite (data, noname, depth) {
  var lite = {}
    , maxDepth = npm.config.get("depth")

  if (typeof depth === 'undefined') depth = 0
  if (!noname && data.name) lite.name = data.name
  if (data.version) lite.version = data.version
  if (data.extraneous) {
    lite.extraneous = true
    lite.problems = lite.problems || []
    lite.problems.push( "extraneous: "
                      + data.name + "@" + data.version
                      + " " + (data.path || "") )
  }

  if (data._from)
    lite.from = data._from

  if (data._resolved)
    lite.resolved = data._resolved

  if (data.invalid) {
    lite.invalid = true
    lite.problems = lite.problems || []
    lite.problems.push( "invalid: "
                      + data.name + "@" + data.version
                      + " " + (data.path || "") )
  }

  if (data.peerInvalid) {
    lite.peerInvalid = true
    lite.problems = lite.problems || []
    lite.problems.push( "peer invalid: "
                      + data.name + "@" + data.version
                      + " " + (data.path || "") )
  }

  if (data.dependencies) {
    var deps = Object.keys(data.dependencies)
    if (deps.length) lite.dependencies = deps.map(function (d) {
      var dep = data.dependencies[d]
      if (typeof dep === "string") {
        lite.problems = lite.problems || []
        var p
        if (data.depth > maxDepth) {
          p = "max depth reached: "
        } else {
          p = "missing: "
        }
        p += d + "@" + dep
          + ", required by "
          + data.name + "@" + data.version
        lite.problems.push(p)
        return [d, { required: dep.requiredBy, missing: true }]
      } else if (dep.peerMissing) {
        lite.problems = lite.problems || []
        dep.peerMissing.forEach(function (missing) {
          var pdm = 'peer dep missing: ' +
              missing.requires +
              ', required by ' +
              missing.requiredBy
          lite.problems.push(pdm)
        })
        return [d, { required: dep, peerMissing: true }]
      } else if (npm.config.get('json')) {
        if (depth === maxDepth) delete dep.dependencies
        return [d, getLite(dep, true, depth + 1)]
      }
      return [d, getLite(dep, true)]
    }).reduce(function (deps, d) {
      if (d[1].problems) {
        lite.problems = lite.problems || []
        lite.problems.push.apply(lite.problems, d[1].problems)
      }
      deps[d[0]] = d[1]
      return deps
    }, {})
  }
  return lite
}

function bfsify (root, args, current, queue, seen) {
  // walk over the data, and turn it from this:
  // +-- a
  // |   `-- b
  // |       `-- a (truncated)
  // `--b (truncated)
  // into this:
  // +-- a
  // `-- b
  // which looks nicer
  args = args || []
  current = current || root
  queue = queue || []
  seen = seen || [root]
  var deps = current.dependencies = current.dependencies || {}
  Object.keys(deps).forEach(function (d) {
    var dep = deps[d]
    if (typeof dep !== "object") return
    if (seen.indexOf(dep) !== -1) {
      if (npm.config.get("parseable") || !npm.config.get("long")) {
        delete deps[d]
        return
      } else {
        dep = deps[d] = Object.create(dep)
        dep.dependencies = {}
      }
    }
    queue.push(dep)
    seen.push(dep)
  })

  if (!queue.length) {
    // if there were args, then only show the paths to found nodes.
    return filterFound(root, args)
  }
  return bfsify(root, args, queue.shift(), queue, seen)
}

function filterFound (root, args) {
  if (!args.length) return root
  var deps = root.dependencies
  if (deps) Object.keys(deps).forEach(function (d) {
    var dep = filterFound(deps[d], args)

    // see if this one itself matches
    var found = false
    for (var i = 0; !found && i < args.length; i ++) {
      if (d === args[i][0]) {
        found = semver.satisfies(dep.version, args[i][1], true)
      }
    }
    // included explicitly
    if (found) dep._found = true
    // included because a child was included
    if (dep._found && !root._found) root._found = 1
    // not included
    if (!dep._found) delete deps[d]
  })
  if (!root._found) root._found = false
  return root
}

function makeArchy (data, long, dir) {
  var out = makeArchy_(data, long, dir, 0)
  return archy(out, "", { unicode: npm.config.get("unicode") })
}

function makeArchy_ (data, long, dir, depth, parent, d) {
  if (typeof data === "string") {
    if (depth -1 <= npm.config.get("depth")) {
      // just missing
      var unmet = "UNMET DEPENDENCY"
      if (npm.color) {
        unmet = color.bgBlack(color.red(unmet))
      }
      data = unmet + " " + d + "@" + data
    } else {
      data = d+"@"+ data
    }
    return data
  }

  var out = {}
  // the top level is a bit special.
  out.label = data._id || ""
  if (data._found === true && data._id) {
    if (npm.color) {
      out.label = color.bgBlack(color.yellow(out.label.trim())) + " "
    } else {
      out.label = out.label.trim() + " "
    }
  }
  if (data.link) out.label += " -> " + data.link

  if (data.invalid) {
    if (data.realName !== data.name) out.label += " ("+data.realName+")"
    var invalid = "invalid"
    if (npm.color) invalid = color.bgBlack(color.red(invalid))
    out.label += " " + invalid
  }

  if (data.peerInvalid) {
    var peerInvalid = "peer invalid"
    if (npm.color) peerInvalid = color.bgBlack(color.red(peerInvalid))
    out.label += " " + peerInvalid
  }

  if (data.extraneous && data.path !== dir) {
    var extraneous = "extraneous"
    if (npm.color) extraneous = color.bgBlack(color.green(extraneous))
    out.label += " " + extraneous
  }

  // add giturl to name@version
  if (data._resolved) {
    var type = npa(data._resolved).type
    var isGit = type === 'git' || type === 'hosted'
    if (isGit) {
      out.label += ' (' + data._resolved + ')'
    }
  }

  if (long) {
    if (dir === data.path) out.label += "\n" + dir
    out.label += "\n" + getExtras(data, dir)
  } else if (dir === data.path) {
    if (out.label) out.label += " "
    out.label += dir
  }

  // now all the children.
  out.nodes = []
  if (depth <= npm.config.get("depth")) {
    out.nodes = Object.keys(data.dependencies || {})
      .sort(alphasort).map(function (d) {
        return makeArchy_(data.dependencies[d], long, dir, depth + 1, data, d)
      })
  }

  if (out.nodes.length === 0 && data.path === dir) {
    out.nodes = ["(empty)"]
  }

  return out
}

function getExtras (data) {
  var extras = []

  if (data.description) extras.push(data.description)
  if (data.repository) extras.push(data.repository.url)
  if (data.homepage) extras.push(data.homepage)
  if (data._from) {
    var from = data._from
    if (from.indexOf(data.name + "@") === 0) {
      from = from.substr(data.name.length + 1)
    }
    var u = url.parse(from)
    if (u.protocol) extras.push(from)
  }
  return extras.join("\n")
}


function makeParseable (data, long, dir, depth, parent, d) {
  depth = depth || 0

  return [ makeParseable_(data, long, dir, depth, parent, d) ]
  .concat(Object.keys(data.dependencies || {})
    .sort(alphasort).map(function (d) {
      return makeParseable(data.dependencies[d], long, dir, depth + 1, data, d)
    }))
  .filter(function (x) { return x })
  .join("\n")
}

function makeParseable_ (data, long, dir, depth, parent, d) {
  if (data.hasOwnProperty("_found") && data._found !== true) return ""

  if (typeof data === "string") {
    if (data.depth < npm.config.get("depth")) {
      data = npm.config.get("long")
           ? path.resolve(parent.path, "node_modules", d)
           + ":"+d+"@"+JSON.stringify(data)+":INVALID:MISSING"
           : ""
    } else {
      data = path.resolve(data.path || "", "node_modules", d || "")
           + (npm.config.get("long")
             ? ":" + d + "@" + JSON.stringify(data)
             + ":" // no realpath resolved
             + ":MAXDEPTH"
             : "")
    }

    return data
  }

  if (!npm.config.get("long")) return data.path

  return data.path
       + ":" + (data._id || "")
       + ":" + (data.realPath !== data.path ? data.realPath : "")
       + (data.extraneous ? ":EXTRANEOUS" : "")
       + (data.invalid ? ":INVALID" : "")
       + (data.peerInvalid ? ":PEERINVALID" : "")
}
