text: var logger = process.logging || require('../util/log');
  var log = logger('generators:action');
  
  var fs = require('fs');
  var path = require('path');
  var events = require('events');
  var mkdirp = require('mkdirp');
  var isBinaryFile = require('isbinaryfile');
  var rimraf = require('rimraf');
  var async = require('async');
  var iconv = require('iconv-lite');
  var chalk = require('chalk');
  
  var actions = module.exports;
  
  actions.log = log;
  
  /**
   * Stores and return the source root for this class. The source root is used to
   * prefix filepath with `.read()` or `.template()`.
   *
   * @param {String} root
   */
  
  actions.sourceRoot = function sourceRoot(root) {
    if (root) {
      this._sourceRoot = path.resolve(root);
    }
  
    return this._sourceRoot;
  };
  
  /**
   * Sets the destination root for this class, the working directory. Relative
   * path are added to the directory where the script was invoked and
   * expanded.
   *
   * This automatically creates the working directory if it doensn't exists and
   * `cd` into it.
   *
   * @param {String} root
   */
  
  actions.destinationRoot = function destinationRoot(root) {
    if (root) {
      this._destinationRoot = path.resolve(root);
  
      if (!fs.existsSync(root)) {
        this.mkdir(root);
      }
  
      process.chdir(root);
    }
  
    return this._destinationRoot || './';
  };
  
  /**
   * Stores and return the cache root for this class. The cache root is used to
   * `git clone` repositories from github by `.remote()` for example.
   */
  
  actions.cacheRoot = function cacheRoot() {
    // we follow XDG specs if possible:
    // http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
    if (process.env.XDG_CACHE_HOME) {
      return path.join(process.env.XDG_CACHE_HOME, 'yeoman');
    }
  
    // otherwise, we fallback to a temp dir in the home
    var home = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'];
    return path.join(home, '.cache/yeoman');
  };
  
  /**
   * Make some of the file API aware of our source/destination root paths.
   * `copy`, `template` (only when could be applied/required by legacy code),
   * `write` and alike consider.
   *
   * @param {String} source
   * @param {String} destination
   * @param {Function} process
   */
  
  actions.copy = function copy(source, destination, process) {
    var body;
    destination = destination || source;
  
    if (typeof destination === 'function') {
      process = destination;
      destination = source;
    }
  
    source = this.isPathAbsolute(source) ? source : path.join(this.sourceRoot(), source);
  
    var encoding = null;
    var binary = isBinaryFile(source);
    if (!binary) {
      encoding = 'utf8';
    }
  
    body = fs.readFileSync(source, encoding);
  
    if (typeof process === 'function' && !binary) {
      body = process(body, source, destination, {
        encoding: encoding
      });
    }
  
    try {
      body = this.engine(body, this);
    } catch (err) {
      // this happens in some cases when trying to copy a JS file like lodash/underscore
      // (conflicting the templating engine)
    }
  
    this.checkForCollision(destination, body, function (err, config) {
      var stats;
  
      if (err) {
        config.callback(err);
        return this.emit('error', err);
      }
  
      // create or force means file write, identical or skip prevent the
      // actual write.
      if (!(/force|create/.test(config.status))) {
        return config.callback();
      }
  
      mkdirp.sync(path.dirname(destination));
      fs.writeFileSync(destination, body);
  
      // synchronize stats and modification times from the original file.
      stats = fs.statSync(source);
      try {
        fs.chmodSync(destination, stats.mode);
        fs.utimesSync(destination, stats.atime, stats.mtime);
      } catch (err) {
        this.log.error('Error setting permissions of "' + chalk.bold(destination) + '" file: ' + err);
      }
  
      config.callback();
    }.bind(this));
  
    return this;
  };
  
  /**
   * A simple method to read the content of the a file borrowed from Grunt:
   * https://github.com/gruntjs/grunt/blob/master/lib/grunt/file.js
   *
   * Discussion and future plans:
   * https://github.com/yeoman/generator/pull/220
   *
   * The encoding is `utf8` by default, to read binary files, pass the proper
   * encoding or null. Non absolute path are prefixed by the source root.
   *
   * @param {String} filepath
   * @param {String} encoding
   */
  
  actions.read = function read(filepath, encoding) {
    var contents;
  
    if (!this.isPathAbsolute(filepath)) {
      filepath = path.join(this.sourceRoot(), filepath);
    }
  
    try {
      contents = fs.readFileSync(String(filepath));
  
      // if encoding is not explicitly null, convert from encoded buffer to a
      // string. if no encoding was specified, use the default.
      if (encoding !== null) {
        contents = iconv.decode(contents, encoding || 'utf8');
  
        // strip any BOM that might exist.
        if (contents.charCodeAt(0) === 0xFEFF) {
          contents = contents.substring(1);
        }
      }
  
      return contents;
    } catch (e) {
      throw new Error('Unable to read "' + filepath + '" file (Error code: ' + e.code + ').');
    }
  };
  
  /**
   * Writes a chunk of data to a given `filepath`, checking for collision prior
   * to the file write.
   *
   * @param {String} filepath
   * @param {String} content
   */
  
  actions.write = function write(filepath, content) {
    this.checkForCollision(filepath, content, function (err, config) {
      if (err) {
        config.callback(err);
        return this.emit('error', err);
      }
  
      // create or force means file write, identical or skip prevent the
      // actual write
      if (/force|create/.test(config.status)) {
        mkdirp.sync(path.dirname(filepath));
        fs.writeFileSync(filepath, content);
      }
  
      config.callback();
    });
    return this;
  };
  
  /**
   * File collision checked. Takes a `filepath` (the file about to be written)
   * and the actual content. A basic check is done to see if the file exists, if
   * it does:
   *
   *   1. Read its content from  `fs`
   *   2. Compare it with the provided content
   *   3. If identical, mark it as is and skip the check
   *   4. If diverged, prepare and show up the file collision menu
   *
   * The menu has the following options:
   *
   *   - `Y` Yes, overwrite
   *   - `n` No, do not overwrite
   *   - `a` All, overwrite this and all others
   *   - `q` Quit, abort
   *   - `d` Diff, show the differences between the old and the new
   *   - `h` Help, show this help
   *
   * @param {String} filepath
   * @param {String} content
   * @param {Function} cb
   */
  
  actions.checkForCollision = function checkForCollision(filepath, content, cb) {
    this.conflicter.add({
      file: filepath,
      content: content
    });
  
    this.conflicter.once('resolved:' + filepath, cb.bind(this, null));
  };
  
  /**
   * Gets a template at the relative source, executes it and makes a copy
   * at the relative destination. If the destination is not given it's assumed
   * to be equal to the source relative to destination.
   *
   * Use configured engine to render the provided `source` template at the given
   * `destination`. `data` is an optional hash to pass to the template, if
   * undefined, executes the template in the generator instance context.
   *
   * @param {String} source
   * @param {String} destination
   * @param {Object} data
   */
  
  actions.template = function template(source, destination, data) {
    data = data || this;
    destination = destination || source;
  
    var body = this.read(source, 'utf8');
    body = this.engine(body, data);
  
    this.write(destination, body);
    return this;
  };
  
  /**
   * The engine method is the function used whenever a template needs to be rendered.
   *
   * It uses the configured engine (default: underscore) to render the `body`
   * template with the provided `data`.
   *
   * @param {String} body
   * @param {Object} data
   */
  
  actions.engine = function engine(body, data) {
    if (!this._engine) {
      throw new Error('Trying to render template without valid engine.');
    }
  
    return this._engine.detect && this._engine.detect(body) ?
      this._engine(body, data) :
      body;
  };
  
  /**
   * Copies recursively the files from source directory to root directory.
   *
   * @param {String} source
   * @param {String} destination
   * @param {Function} process
   */
  
  actions.directory = function directory(source, destination, process) {
    var root = path.join(this.sourceRoot(), source);
    var files = this.expandFiles('**', { dot: true, cwd: root });
    var self = this;
  
    destination = destination || source;
  
    if (typeof destination === 'function') {
      process = destination;
      destination = source;
    }
  
    // get the path relative to the template root, and copy to the relative destination
    var resolveFiles = function (filepath) {
      return function (next) {
        if (!filepath) {
          self.emit('directory:end');
          return next();
        }
  
        var dest = path.join(destination, filepath);
        self.copy(path.join(root, filepath), dest, process);
  
        return next();
      };
    };
  
    async.parallel(files.map(resolveFiles));
  
    return this;
  };
  
  /**
   * Remotely fetch a package on github, store this into a _cache folder, and
   * provide a "remote" object as a facade API to ourself (part of genrator API,
   * copy, template, directory). It's possible to remove local cache, and force
   * a new remote fetch of the package on Github.
   *
   * ### Examples:
   *
   *     this.remote('user', 'repo', function(err, remote) {
   *       remote.copy('.', 'vendors/user-repo');
   *     });
   *
   * @param {String} username
   * @param {String} repo
   * @param {String} branch
   * @param {Function} cb
   * @param {Boolean} refresh
   */
  
  actions.remote = function (username, repo, branch, refresh, cb) {
  
    if (!cb) {
      cb = branch;
      branch = 'master';
    }
  
    var self = this;
    var cache = path.join(this.cacheRoot(), username, repo, branch);
    var url = 'http://github.com/' + [username, repo, 'archive', branch].join('/') + '.tar.gz';
  
    fs.stat(cache, function (err) {
      // already cached
      if (!err) {
        // no refresh, so we can use this cache
        if (!refresh) {
          return done();
        }
  
        // otherwise, we need to remove it, to fetch it again
        rimraf(cache, function (err) {
          if (err) {
            return cb(err);
          }
          self.tarball(url, cache, done);
        });
  
      } else {
        self.tarball(url, cache, done);
      }
    });
  
    function done(err) {
      if (err) {
        return cb(err);
      }
  
      var files = self.expandFiles('**', { cwd: cache, dot: true });
  
      var remote = {};
      remote.cachePath = cache;
  
      // simple proxy to `.copy(source, destination)`
      remote.copy = function copy(source, destination) {
        source = path.join(cache, source);
        self.copy(source, destination);
        return this;
      };
  
      // same as `.template(source, destination, data)`
      remote.template = function template(source, destination, data) {
        data = data || self;
        destination = destination || source;
        source = path.join(cache, source);
  
        var body = self.engine(self.read(source), data);
        self.write(destination, body);
      };
  
      // same as `.template(source, destination)`
      remote.directory = function directory(source, destination) {
        var root = self.sourceRoot();
        self.sourceRoot(cache);
        self.directory(source, destination);
        self.sourceRoot(root);
      };
  
      cb(err, remote, files);
    }
  
    return this;
  };
  