text: var glob = require('glob');
  var path = require('path');
  var fs = require('graceful-fs');
  var Q = require('q');
  var mout = require('mout');
  var rimraf = require('rimraf');
  var endpointParser = require('bower-endpoint-parser');
  var Logger = require('bower-logger');
  var Manager = require('./Manager');
  var defaultConfig = require('../config');
  var semver = require('../util/semver');
  var md5 = require('../util/md5');
  var createError = require('../util/createError');
  var readJson = require('../util/readJson');
  var validLink = require('../util/validLink');
  var scripts = require('./scripts');
  var sortobject = require('deep-sort-object');
  
  function Project(config, logger) {
      // This is the only architecture component that ensures defaults
      // on config and logger
      // The reason behind it is that users can likely use this component
      // directly if commands do not fulfil their needs
      this._config = defaultConfig(config);
      this._logger = logger || new Logger();
      this._manager = new Manager(this._config, this._logger);
  
      this._options = {};
  }
  
  // -----------------
  
  Project.prototype.install = function (decEndpoints, options, config) {
      var that = this;
      var targets = [];
      var resolved = {};
      var incompatibles = [];
  
      // If already working, error out
      if (this._working) {
          return Q.reject(createError('Already working', 'EWORKING'));
      }
  
      this._options = options || {};
      this._config = config || {};
      this._working = true;
  
      // Analyse the project
      return this._analyse()
      .spread(function (json, tree) {
          // It shows an error when issuing `bower install`
          // and no bower.json is present in current directory
          if(!that._jsonFile && decEndpoints.length === 0 ) {
              throw createError('No bower.json present', 'ENOENT');
          }
  
          // Recover tree
          that.walkTree(tree, function (node, name) {
              if (node.incompatible) {
                  incompatibles.push(node);
              } else if (node.missing || node.different || that._config.force) {
                  targets.push(node);
              } else {
                  resolved[name] = node;
              }
          }, true);
  
          // Add decomposed endpoints as targets
          decEndpoints = decEndpoints || [];
          decEndpoints.forEach(function (decEndpoint) {
              // Mark as new so that a conflict for this target
              // always require a choice
              // Also allows for the target to be converted in case
              // of being *
              decEndpoint.newly = true;
              targets.push(decEndpoint);
          });
  
          // Bootstrap the process
          return that._bootstrap(targets, resolved, incompatibles);
      })
      .then(function () {
          return that._manager.preinstall(that._json);
      })
      .then(function () {
          return that._manager.install(that._json);
      })
      .then(function (installed) {
          // Handle save and saveDev options
          if (that._options.save || that._options.saveDev) {
              // Cycle through the specified endpoints
              decEndpoints.forEach(function (decEndpoint) {
                  var jsonEndpoint;
  
                  jsonEndpoint = endpointParser.decomposed2json(decEndpoint);
  
                  if (that._options.saveExact) {
                      jsonEndpoint[decEndpoint.name] = decEndpoint.pkgMeta.version;
                  }
  
                  if (that._options.save) {
                      that._json.dependencies = sortobject(mout.object.mixIn(that._json.dependencies || {}, jsonEndpoint));
                  }
  
                  if (that._options.saveDev) {
                      that._json.devDependencies = sortobject(mout.object.mixIn(that._json.devDependencies || {}, jsonEndpoint));
                  }
              });
          }
  
          // Save JSON, might contain changes to dependencies and resolutions
          return that.saveJson()
          .then(function () {
              return that._manager.postinstall(that._json).then(function () {
                  return installed;
              });
          });
      })
      .fin(function () {
          that._installed = null;
          that._working = false;
      });
  };
  
  Project.prototype.update = function (names, options) {
      var that = this;
      var targets = [];
      var resolved = {};
      var incompatibles = [];
  
      // If already working, error out
      if (this._working) {
          return Q.reject(createError('Already working', 'EWORKING'));
      }
  
      this._options = options || {};
      this._working = true;
  
      // Analyse the project
      return this._analyse()
      .spread(function (json, tree, flattened) {
          // If no names were specified, update every package
          if (!names) {
              // Mark each root dependency as targets
              that.walkTree(tree, function (node) {
                  // We don't know the real source of linked packages
                  // Instead we read its dependencies
                  if (node.linked) {
                      targets.push.apply(targets, mout.object.values(node.dependencies));
                  } else {
                      targets.push(node);
                  }
  
                  return false;
              }, true);
          // Otherwise, selectively update the specified ones
          } else {
              // Error out if some of the specified names
              // are not installed
              names.forEach(function (name) {
                  if (!flattened[name]) {
                      throw createError('Package ' + name + ' is not installed', 'ENOTINS', {
                          name: name
                      });
                  }
              });
  
              // Add packages whose names are specified to be updated
              that.walkTree(tree, function (node, name) {
                  if (names.indexOf(name) !== -1) {
                      // We don't know the real source of linked packages
                      // Instead we read its dependencies
                      if (node.linked) {
                          targets.push.apply(targets, mout.object.values(node.dependencies));
                      } else {
                          targets.push(node);
                      }
  
                      return false;
                  }
              }, true);
  
              // Recover tree
              that.walkTree(tree, function (node, name) {
                  if (node.missing || node.different) {
                      targets.push(node);
                  } else if (node.incompatible) {
                      incompatibles.push(node);
                  } else {
                      resolved[name] = node;
                  }
              }, true);
          }
  
          // Bootstrap the process
          return that._bootstrap(targets, resolved, incompatibles)
          .then(function () {
              return that._manager.preinstall(that._json);
          })
          .then(function () {
              return that._manager.install(that._json);
          })
          .then(function (installed) {
              // Save JSON, might contain changes to resolutions
              return that.saveJson()
              .then(function () {
                  return that._manager.postinstall(that._json).then(function () {
                      return installed;
                  });
              });
          });
      })
      .fin(function () {
          that._installed = null;
          that._working = false;
      });
  };
  
  Project.prototype.uninstall = function (names, options) {
      var that = this;
      var packages = {};
  
      // If already working, error out
      if (this._working) {
          return Q.reject(createError('Already working', 'EWORKING'));
      }
  
      this._options = options || {};
      this._working = true;
  
      // Analyse the project
      return this._analyse()
      // Fill in the packages to be uninstalled
      .spread(function (json, tree, flattened) {
          var promise = Q.resolve();
  
          names.forEach(function (name) {
              var decEndpoint = flattened[name];
  
              // Check if it is not installed
              if (!decEndpoint || decEndpoint.missing) {
                  packages[name] = null;
                  return;
              }
  
              promise = promise
              .then(function () {
                  var message;
                  var data;
                  var dependantsNames;
                  var dependants = [];
  
                  // Walk the down the tree, gathering dependants of the package
                  that.walkTree(tree, function (node, nodeName) {
                      if (name === nodeName) {
                          dependants.push.apply(dependants, mout.object.values(node.dependants));
                      }
                  }, true);
  
                  // Remove duplicates
                  dependants = mout.array.unique(dependants);
  
                  // Note that the root is filtered from the dependants
                  // as well as other dependants marked to be uninstalled
                  dependants = dependants.filter(function (dependant) {
                      return !dependant.root && names.indexOf(dependant.name) === -1;
                  });
  
                  // If the package has no dependants or the force config is enabled,
                  // mark it to be removed
                  if (!dependants.length || that._config.force) {
                      packages[name] = decEndpoint.canonicalDir;
                      return;
                  }
  
                  // Otherwise we need to figure it out if the user really wants to remove it,
                  // even with dependants
                  // As such we need to prompt the user with a meaningful message
                  dependantsNames = dependants.map(function (dep) { return dep.name; });
                  dependantsNames.sort(function (name1, name2) { return name1.localeCompare(name2); });
                  dependantsNames = mout.array.unique(dependantsNames);
                  dependants = dependants.map(function (dependant) { return that._manager.toData(dependant); });
                  message = dependantsNames.join(', ') + ' depends on ' + decEndpoint.name;
                  data = {
                      name: decEndpoint.name,
                      dependants: dependants
                  };
  
                  // If interactive is disabled, error out
                  if (!that._config.interactive) {
                      throw createError(message, 'ECONFLICT', {
                          data: data
                      });
                  }
  
                  that._logger.conflict('mutual', message, data);
  
                  // Prompt the user
                  return Q.nfcall(that._logger.prompt.bind(that._logger), {
                      type: 'confirm',
                      message: 'Continue anyway?',
                      default: true
                  })
                  .then(function (confirmed) {
                      // If the user decided to skip it, remove from the array so that it won't
                      // influence subsequent dependants
                      if (!confirmed) {
                          mout.array.remove(names, name);
                      } else {
                          packages[name] = decEndpoint.canonicalDir;
                      }
                  });
              });
          });
  
          return promise;
      })
      // Remove packages
      .then(function () {
          return that._removePackages(packages);
      })
      .fin(function () {
          that._installed = null;
          that._working = false;
      });
  };
  
  Project.prototype.getTree = function (options) {
      this._options = options || {};
  
      return this._analyse()
      .spread(function (json, tree, flattened) {
          var extraneous = [];
          var additionalKeys = ['missing', 'extraneous', 'different', 'linked'];
  
          // Convert tree
          tree = this._manager.toData(tree, additionalKeys);
  
          // Mark incompatibles
          this.walkTree(tree, function (node) {
              var version;
              var target = node.endpoint.target;
  
              if (node.pkgMeta && semver.validRange(target)) {
                  version = node.pkgMeta.version;
  
                  // Ignore if target is '*' and resolved to a non-semver release
                  if (!version && target === '*') {
                      return;
                  }
  
                  if (!version || !semver.satisfies(version, target)) {
                      node.incompatible = true;
                  }
              }
          }, true);
  
          // Convert extraneous
          mout.object.forOwn(flattened, function (pkg) {
              if (pkg.extraneous) {
                  extraneous.push(this._manager.toData(pkg, additionalKeys));
              }
          }, this);
  
          // Convert flattened
          flattened = mout.object.map(flattened, function (node) {
              return this._manager.toData(node, additionalKeys);
          }, this);
  
          return [tree, flattened, extraneous];
      }.bind(this));
  };
  
  Project.prototype.walkTree = function (node, fn, onlyOnce) {
      var result;
      var dependencies;
      var queue = mout.object.values(node.dependencies);
  
      if (onlyOnce === true) {
          onlyOnce = [];
      }
  
      while (queue.length) {
          node = queue.shift();
          result = fn(node, node.endpoint ? node.endpoint.name : node.name);
  
          // Abort traversal if result is false
          if (result === false) {
              continue;
          }
  
          // Add dependencies to the queue
          dependencies = mout.object.values(node.dependencies);
  
          // If onlyOnce was true, do not add if already traversed
          if (onlyOnce) {
              dependencies = dependencies.filter(function (dependency) {
                  return !mout.array.find(onlyOnce, function (stacked) {
                      if (dependency.endpoint) {
                          return mout.object.equals(dependency.endpoint, stacked.endpoint);
                      }
  
                      return dependency.name === stacked.name &&
                             dependency.source === stacked.source &&
                             dependency.target === stacked.target;
                  });
              });
  
              onlyOnce.push.apply(onlyOnce, dependencies);
          }
  
          queue.unshift.apply(queue, dependencies);
      }
  };
  
  Project.prototype.saveJson = function (forceCreate) {
      var file;
      var jsonStr = JSON.stringify(this._json, null, '  ') + '\n';
      var jsonHash = md5(jsonStr);
  
      // Save only if there's something different
      if (jsonHash === this._jsonHash) {
          return Q.resolve();
      }
  
      // Error out if the json file does not exist, unless force create
      // is true
      if (!this._jsonFile && !forceCreate) {
          this._logger.warn('no-json', 'No bower.json file to save to, use bower init to create one');
          return Q.resolve();
      }
  
      file = this._jsonFile || path.join(this._config.cwd, 'bower.json');
      return Q.nfcall(fs.writeFile, file, jsonStr)
      .then(function () {
          this._jsonHash = jsonHash;
          this._jsonFile = file;
          return this._json;
      }.bind(this));
  };
  
  Project.prototype.hasJson = function () {
      return this._readJson()
      .then(function (json) {
          return json ? this._jsonFile : false;
      }.bind(this));
  };
  
  Project.prototype.getJson = function () {
      return this._readJson();
  };
  
  Project.prototype.getManager = function () {
      return this._manager;
  };
  
  Project.prototype.getPackageRepository = function () {
      return this._manager.getPackageRepository();
  };
  
  // -----------------
  
  Project.prototype._analyse = function () {
      return Q.all([
          this._readJson(),
          this._readInstalled(),
          this._readLinks()
      ])
      .spread(function (json, installed, links) {
          var root;
          var jsonCopy = mout.lang.deepClone(json);
  
          root = {
              name: json.name,
              source: this._config.cwd,
              target: json.version || '*',
              pkgMeta: jsonCopy,
              canonicalDir: this._config.cwd,
              root: true
          };
  
          mout.object.mixIn(installed, links);
  
          // Mix direct extraneous as dependencies
          // (dependencies installed without --save/--save-dev)
          jsonCopy.dependencies = jsonCopy.dependencies || {};
          jsonCopy.devDependencies = jsonCopy.devDependencies || {};
          mout.object.forOwn(installed, function (decEndpoint, key) {
              var pkgMeta = decEndpoint.pkgMeta;
              var isSaved = jsonCopy.dependencies[key] || jsonCopy.devDependencies[key];
  
              // The _direct propery is saved by the manager when .newly is specified
              // It may happen pkgMeta is undefined if package is uninstalled
              if (!isSaved && pkgMeta && pkgMeta._direct) {
                  decEndpoint.extraneous = true;
  
                  if (decEndpoint.linked) {
                      jsonCopy.dependencies[key] = pkgMeta.version || '*';
                  } else {
                      jsonCopy.dependencies[key] = (pkgMeta._originalSource || pkgMeta._source) + '#' + pkgMeta._target;
                  }
              }
          });
  
          // Restore the original dependencies cross-references,
          // that is, the parent-child relationships
          this._restoreNode(root, installed, 'dependencies');
          // Do the same for the dev dependencies
          if (!this._options.production) {
              this._restoreNode(root, installed, 'devDependencies');
          }
  
          // Restore the rest of the extraneous (not installed directly)
          mout.object.forOwn(installed, function (decEndpoint, name) {
              if (!decEndpoint.dependants) {
                  decEndpoint.extraneous = true;
                  this._restoreNode(decEndpoint, installed, 'dependencies');
                  // Note that it has no dependants, just dependencies!
                  root.dependencies[name] = decEndpoint;
              }
          }, this);
  
          // Remove root from the flattened tree
          delete installed[json.name];
  
          return [json, root, installed];
      }.bind(this));
  };
  
  Project.prototype._bootstrap = function (targets, resolved, incompatibles) {
      var installed = mout.object.map(this._installed, function (decEndpoint) {
          return decEndpoint.pkgMeta;
      });
  
      this._json.resolutions = this._json.resolutions || {};
  
      // Configure the manager and kick in the resolve process
      return this._manager
      .configure({
          targets: targets,
          resolved: resolved,
          incompatibles: incompatibles,
          resolutions: this._json.resolutions,
          installed: installed,
          forceLatest: this._options.forceLatest
      })
      .resolve()
      .then(function () {
          // If the resolutions is empty, delete key
          if (!mout.object.size(this._json.resolutions)) {
              delete this._json.resolutions;
          }
      }.bind(this));
  };
  
  Project.prototype._readJson = function () {
      var that = this;
  
      if (this._json) {
          return Q.resolve(this._json);
      }
  
      // Read local json
      return this._json = readJson(this._config.cwd, {
          assume: { name: path.basename(this._config.cwd) || 'root' }
      })
      .spread(function (json, deprecated, assumed) {
          var jsonStr;
  
          if (deprecated) {
              that._logger.warn('deprecated', 'You are using the deprecated ' + deprecated + ' file');
          }
  
          if (!assumed) {
              that._jsonFile = path.join(that._config.cwd, deprecated ? deprecated : 'bower.json');
          }
  
          jsonStr = JSON.stringify(json, null, '  ') + '\n';
          that._jsonHash = md5(jsonStr);
          return that._json = json;
      });
  };
  
  Project.prototype._readInstalled = function () {
      var componentsDir;
      var that = this;
  
      if (this._installed) {
          return Q.resolve(this._installed);
      }
  
      // Gather all folders that are actual packages by
      // looking for the package metadata file
      componentsDir = path.join(this._config.cwd, this._config.directory);
      return this._installed = Q.nfcall(glob, '*/.bower.json', {
          cwd: componentsDir,
          dot: true
      })
      .then(function (filenames) {
          var promises;
          var decEndpoints = {};
  
          // Foreach bower.json found
          promises = filenames.map(function (filename) {
              var name = path.dirname(filename);
              var metaFile = path.join(componentsDir, filename);
  
              // Read package metadata
              return readJson(metaFile)
              .spread(function (pkgMeta) {
                  decEndpoints[name] = {
                      name: name,
                      source: pkgMeta._originalSource || pkgMeta._source,
                      target: pkgMeta._target,
                      canonicalDir: path.dirname(metaFile),
                      pkgMeta: pkgMeta
                  };
              });
          });
  
          // Wait until all files have been read
          // and resolve with the decomposed endpoints
          return Q.all(promises)
          .then(function () {
              return that._installed = decEndpoints;
          });
      });
  };
  
  Project.prototype._readLinks = function () {
      var componentsDir;
      var that = this;
  
      // Read directory, looking for links
      componentsDir = path.join(this._config.cwd, this._config.directory);
      return Q.nfcall(fs.readdir, componentsDir)
      .then(function (filenames) {
          var promises;
          var decEndpoints = {};
  
          promises = filenames.map(function (filename) {
              var dir = path.join(componentsDir, filename);
  
              // Filter only those that are valid links
              return validLink(dir)
              .spread(function (valid, err) {
                  var name;
  
                  if (!valid) {
                      if (err) {
                          that._logger.debug('read-link', 'Link ' + dir + ' is invalid', {
                              filename: dir,
                              error: err
                          });
                      }
                      return;
                  }
  
                  // Skip links to files (see #783)
                  if (!valid.isDirectory()) {
                      return;
                  }
  
                  name = path.basename(dir);
                  return readJson(dir, {
                      assume: { name: name }
                  })
                  .spread(function (json, deprecated) {
                      if (deprecated) {
                          that._logger.warn('deprecated', 'Package ' + name + ' is using the deprecated ' + deprecated);
                      }
  
                      json._direct = true;  // Mark as a direct dep of root
                      decEndpoints[name] = {
                          name: name,
                          source: dir,
                          target: '*',
                          canonicalDir: dir,
                          pkgMeta: json,
                          linked: true
                      };
                  });
              });
          });
  
          // Wait until all links have been read
          // and resolve with the decomposed endpoints
          return Q.all(promises)
          .then(function () {
              return decEndpoints;
          });
      // Ignore if folder does not exist
      }, function (err) {
          if (err.code !== 'ENOENT') {
              throw err;
          }
  
          return {};
      });
  };
  
  Project.prototype._removePackages = function (packages) {
      var that = this;
      var promises = [];
  
      return scripts.preuninstall(that._config, that._logger, packages, that._installed, that._json)
      .then(function () {
  
          mout.object.forOwn(packages, function (dir, name) {
              var promise;
  
              // Delete directory
              if (!dir) {
                  promise = Q.resolve();
                  that._logger.warn('not-installed', '\'' + name + '\'' + ' cannot be uninstalled as it is not currently installed', {
                      name: name
                  });
              } else {
                  promise = Q.nfcall(rimraf, dir);
                  that._logger.action('uninstall', name, {
                      name: name,
                      dir: dir
                  });
              }
  
              // Remove from json only if successfully deleted
              if (that._options.save && that._json.dependencies) {
                  promise = promise
                  .then(function () {
                      delete that._json.dependencies[name];
                  });
              }
  
              if (that._options.saveDev && that._json.devDependencies) {
                  promise = promise
                  .then(function () {
                      delete that._json.devDependencies[name];
                  });
              }
  
              promises.push(promise);
          });
  
          return Q.all(promises);
  
      })
      .then(function () {
          return that.saveJson();
      })
      // Resolve with removed packages
      .then(function () {
          return mout.object.filter(packages, function (dir) {
              return !!dir;
          });
      });
  };
  
  Project.prototype._restoreNode = function (node, flattened, jsonKey, processed) {
      var deps;
  
      // Do not restore if the node is missing
      if (node.missing) {
          return;
      }
  
      node.dependencies = node.dependencies || {};
      node.dependants = node.dependants || {};
      processed = processed || {};
  
      // Only process deps that are not yet processed
      deps = mout.object.filter(node.pkgMeta[jsonKey], function (value, key) {
          return !processed[node.name + ':' + key];
      });
  
      mout.object.forOwn(deps, function (value, key) {
          var local = flattened[key];
          var json = endpointParser.json2decomposed(key, value);
          var restored;
          var compatible;
          var originalSource;
  
          // Check if the dependency is not installed
          if (!local) {
              flattened[key] = restored = json;
              restored.missing = true;
          // Even if it is installed, check if it's compatible
          // Note that linked packages are interpreted as compatible
          // This might change in the future: #673
          } else {
              compatible = local.linked || (!local.missing && json.target === local.pkgMeta._target);
  
              if (!compatible) {
                  restored = json;
  
                  if (!local.missing) {
                      restored.pkgMeta = local.pkgMeta;
                      restored.canonicalDir = local.canonicalDir;
                      restored.incompatible = true;
                  } else {
                      restored.missing = true;
                  }
              } else {
                  restored = local;
                  mout.object.mixIn(local, json);
              }
  
              // Check if source changed, marking as different if it did
              // We only do this for direct root dependencies that are compatible
              if (node.root && compatible) {
                  originalSource = mout.object.get(local, 'pkgMeta._originalSource');
                  if (originalSource && originalSource !== json.source) {
                      restored.different = true;
                  }
              }
          }
  
          // Cross reference
          node.dependencies[key] = restored;
          processed[node.name + ':' + key] = true;
  
          restored.dependants = restored.dependants || {};
          restored.dependants[node.name] = mout.object.mixIn({}, node);  // We need to clone due to shared objects in the manager!
  
          // Call restore for this dependency
          this._restoreNode(restored, flattened, 'dependencies', processed);
  
          // Do the same for the incompatible local package
          if (local && restored !== local) {
              this._restoreNode(local, flattened, 'dependencies', processed);
          }
      }, this);
  };
  
  module.exports = Project;
  