/*
 * grunt
 * https://github.com/cowboy/grunt
 *
 * Copyright (c) 2012 "Cowboy" Ben Alman
 * Licensed under the MIT license.
 * http://benalman.com/about/license/
 */

module.exports = function(grunt) {

  // Nodejs libs.
  var fs = require('fs');
  var path = require('path');

  // External libs.
  var semver = require('semver');

  var prompt = require('prompt');
  prompt.message = '[' + '?'.green + ']';
  prompt.delimiter = ' ';

  // ==========================================================================
  // TASKS
  // ==========================================================================

  // An array of all available license files.
  function availableLicenses() {
    return grunt.task.expandFiles('init/licenses/*').map(function(obj) {
      return path.basename(String(obj)).replace(/^LICENSE-/, '');
    });
  }

  grunt.registerInitTask('init', 'Generate project scaffolding from a predefined template.', function() {
    // Extra arguments will be applied to the template file.
    var args = grunt.utils.toArray(arguments);
    // Template name.
    var name = args.shift();
    // Default to last-specified grunt.npmTasks plugin name if template name
    // was omitted. Note that specifying just a : after init like "grunt init:"
    // will allow all available templates to be listed.
    if (name == null) {
      name = grunt._npmTasks[grunt._npmTasks.length - 1];
    }
    // Valid init templates (.js files).
    var templates = {};
    grunt.task.expandFiles('init/*.js').forEach(function(fileobj) {
      // Add template (plus its path) to the templates object.
      templates[path.basename(fileobj.abs, '.js')] = require(fileobj.abs);
    });
    var initTemplate = templates[name];

    // Give the user a little help.
    grunt.log.writelns(
      'This task will create one or more files in the current directory, ' +
      'based on the environment and the answers to a few questions. ' +
      'Note that answering "?" to any question will show question-specific ' +
      'help and answering "none" to most questions will leave its value blank.'
    );

    // Abort if a valid template was not specified.
    if (!initTemplate) {
      grunt.log.writeln().write('Loading' + (name ? ' "' + name + '"' : '') + ' init template...').error();
      grunt.log.errorlns('A valid template name must be specified, eg. "grunt ' +
        'init:commonjs". The currently-available init templates are: ');
      Object.keys(templates).forEach(function(name) {
        var description = templates[name].description || '(no description)';
        grunt.log.errorlns(name.cyan + ' - ' + description);
      });
      return false;
    }

    // Abort if matching files or directories were found (to avoid accidentally
    // nuking them).
    if (initTemplate.warnOn && grunt.file.expand(initTemplate.warnOn).length > 0) {
      grunt.log.writeln();
      grunt.warn('Existing files may be overwritten!');
    }

    // This task is asynchronous.
    var taskDone = this.async();

    var pathPrefix = 'init/' + name + '/root/';

    // Useful init sub-task-specific utilities.
    var init = {
      // Expose any user-specified default init values.
      defaults: grunt.task.readDefaults('init/defaults.json'),
      // Expose rename rules for this template.
      renames: grunt.task.readDefaults('init', name, 'rename.json'),
      // Return an object containing files to copy with their absolute source path
      // and relative destination path, renamed (or omitted) according to rules in
      // rename.json (if it exists).
      filesToCopy: function(props) {
        var files = {};
        // Iterate over all source files.
        grunt.task.expandFiles({dot: true}, pathPrefix + '**').forEach(function(obj) {
          // Get the path relative to the template root.
          var relpath = obj.rel.slice(pathPrefix.length);
          var rule = init.renames[relpath];
          // Omit files that have an empty / false rule value.
          if (!rule && relpath in init.renames) { return; }
          // Create a property for this file.
          files[rule ? grunt.template.process(rule, props, 'init') : relpath] = obj.rel;
        });
        return files;
      },
      // Search init template paths for filename.
      srcpath: function(arg1) {
        if (arg1 == null) { return null; }
        var args = ['init', name, 'root'].concat(grunt.utils.toArray(arguments));
        return grunt.task.getFile.apply(grunt.file, args);
      },
      // Determine absolute destination file path.
      destpath: path.join.bind(path, process.cwd()),
      // Given some number of licenses, add properly-named license files to the
      // files object.
      addLicenseFiles: function(files, licenses) {
        var available = availableLicenses();
        licenses.forEach(function(license) {
          var fileobj = grunt.task.expandFiles('init/licenses/LICENSE-' + license)[0];
          files['LICENSE-' + license] = fileobj ? fileobj.rel : null;
        });
      },
      // Given an absolute or relative source path, and an optional relative
      // destination path, copy a file, optionally processing it through the
      // passed callback.
      copy: function(srcpath, destpath, options) {
        // Destpath is optional.
        if (typeof destpath !== 'string') {
          options = destpath;
          destpath = srcpath;
        }
        // Ensure srcpath is absolute.
        if (!grunt.file.isPathAbsolute(srcpath)) {
          srcpath = init.srcpath(srcpath);
        }
        // Use placeholder file if no src exists.
        if (!srcpath) {
          srcpath = grunt.task.getFile('init/misc/placeholder');
        }
        grunt.verbose.or.write('Writing ' + destpath + '...');
        try {
          grunt.file.copy(srcpath, init.destpath(destpath), options);
          grunt.verbose.or.ok();
        } catch(e) {
          grunt.verbose.or.error().error(e);
          throw e;
        }
      },
      // Iterate over all files in the passed object, copying the source file to
      // the destination, processing the contents.
      copyAndProcess: function(files, props, options) {
        options = grunt.utils._.defaults(options || {}, {
          process: function(contents) {
            return grunt.template.process(contents, props, 'init');
          }
        });
        Object.keys(files).forEach(function(destpath) {
          var o = Object.create(options);
          var srcpath = files[destpath];
          // If srcpath is relative, match it against options.noProcess if
          // necessary, then make srcpath absolute.
          var relpath;
          if (srcpath && !grunt.file.isPathAbsolute(srcpath)) {
            if (o.noProcess) {
              relpath = srcpath.slice(pathPrefix.length);
              o.noProcess = grunt.file.isMatch(o.noProcess, relpath);
            }
            srcpath = grunt.task.getFile(srcpath);
          }
          // Copy!
          init.copy(srcpath, destpath, o);
        });
      },
      // Save a package.json file in the destination directory. The callback
      // can be used to post-process properties to add/remove/whatever.
      writePackageJSON: function(filename, props, callback) {
        var pkg = {};
        // Basic values.
        ['name', 'title', 'description', 'version', 'homepage'].forEach(function(prop) {
          if (prop in props) { pkg[prop] = props[prop]; }
        });
        // Author.
        var hasAuthor = Object.keys(props).some(function(prop) {
          return (/^author_/).test(prop);
        });
        if (hasAuthor) {
          pkg.author = {};
          ['name', 'email', 'url'].forEach(function(prop) {
            if (props['author_' + prop]) {
              pkg.author[prop] = props['author_' + prop];
            }
          });
        }
        // Other stuff.
        if ('repository' in props) { pkg.repository = {type: 'git', url: props.repository}; }
        if ('bugs' in props) { pkg.bugs = {url: props.bugs}; }
        if (props.licenses) {
          pkg.licenses = props.licenses.map(function(license) {
            return {type: license, url: props.homepage + '/blob/master/LICENSE-' + license};
          });
        }

        // Node/npm-specific (?)
        if (props.main) { pkg.main = props.main; }
        if (props.bin) { pkg.bin = props.bin; }
        if (props.node_version) { pkg.engines = {node: props.node_version}; }
        if (props.npm_test) {
          pkg.scripts = {test: props.npm_test};
          if (props.npm_test.split(' ')[0] === 'grunt') {
            if (!props.devDependencies) { props.devDependencies = {}; }
            props.devDependencies.grunt = '~' + grunt.version;
          }
        }

        if (props.dependencies) { pkg.dependencies = props.dependencies; }
        if (props.devDependencies) { pkg.devDependencies = props.devDependencies; }
        if (props.keywords) { pkg.keywords = props.keywords; }

        // Allow final tweaks to the pkg object.
        if (callback) { pkg = callback(pkg, props); }

        // Write file.
        grunt.file.write(init.destpath(filename), JSON.stringify(pkg, null, 2));
      }
    };

    // Make args available as flags.
    init.flags = {};
    args.forEach(function(flag) { init.flags[flag] = true; });

    // Show any template-specific notes.
    if (initTemplate.notes) {
      grunt.log.subhead('"' + name + '" template notes:').writelns(initTemplate.notes);
    }

    // Execute template code, passing in the init object, done function, and any
    // other arguments specified after the init:name:???.
    initTemplate.template.apply(this, [grunt, init, function() {
      // Fail task if errors were logged.
      if (grunt.task.current.errorCount) { taskDone(false); }
      // Otherwise, print a success message.
      grunt.log.writeln().writeln('Initialized from template "' + name + '".');
      // All done!
      taskDone();
    }].concat(args));
  });

  // ==========================================================================
  // HELPERS
  // ==========================================================================

  // Prompt user to override default values passed in obj.
  grunt.registerHelper('prompt', function(defaults, options, done) {
    // If defaults are omitted, shuffle arguments a bit.
    if (grunt.utils.kindOf(defaults) === 'array') {
      done = options;
      options = defaults;
      defaults = {};
    }

    // Keep track of any "sanitize" functions for later use.
    var sanitize = {};
    options.forEach(function(option) {
      if (option.sanitize) {
        sanitize[option.name] = option.sanitize;
      }
    });

    // Add one final "are you sure?" prompt.
    if (options.length > 0) {
      options.push({
        message: 'Do you need to make any changes to the above before continuing?'.green,
        name: 'ANSWERS_VALID',
        default: 'y/N'
      });
    }

    // Ask user for input. This is in an IIFE because it has to execute at least
    // once, and might be repeated.
    (function ask() {
      grunt.log.subhead('Please answer the following:');
      var result = grunt.utils._.clone(defaults);
      // Loop over each prompt option.
      grunt.utils.async.forEachSeries(options, function(option, done) {
        var defaultValue;
        grunt.utils.async.forEachSeries(['default', 'altDefault'], function(prop, next) {
          if (typeof option[prop] === 'function') {
            // If the value is a function, execute that function, using the
            // value passed into the return callback as the new default value.
            option[prop](defaultValue, result, function(err, value) {
              defaultValue = String(value);
              next();
            });
          } else {
            // Otherwise, if the value actually exists, use it.
            if (prop in option) {
              defaultValue = option[prop];
            }
            next();
          }
        }, function() {
          // Handle errors (there should never be errors).
          option.default = defaultValue;
          delete option.altDefault;
          // Wrap validator so that answering '?' always fails.
          var validator = option.validator;
          option.validator = function(line, next) {
            if (line === '?') {
              return next(false);
            } else if (validator) {
              if (validator.test) {
                return next(validator.test(line));
              } else if (typeof validator === 'function') {
                return validator.length < 2 ? next(validator(line)) : validator(line, next);
              }
            }
            next(true);
          };
          // Actually get user input.
          prompt.start();
          prompt.getInput(option, function(err, line) {
            if (err) { return done(err); }
            option.validator = validator;
            result[option.name] = line;
            done();
          });
        });
      }, function(err) {
        // After all prompt questions have been answered...
        if (/n/i.test(result.ANSWERS_VALID)) {
          // User accepted all answers. Suspend prompt.
          prompt.pause();
          // Clean up.
          delete result.ANSWERS_VALID;
          // Iterate over all results.
          grunt.utils.async.forEachSeries(Object.keys(result), function(name, next) {
            // If this value needs to be sanitized, process it now.
            if (sanitize[name]) {
              sanitize[name](result[name], result, function(err, value) {
                if (err) {
                  result[name] = err;
                } else if (arguments.length === 2) {
                  result[name] = value === 'none' ? '' : value;
                }
                next();
              });
            } else {
              if (result[name] === 'none') { result[name] = ''; }
              next();
            }
          }, function(err) {
            // Done!
            grunt.log.writeln();
            done(err, result);
          });
        } else {
          // Otherwise update the default value for each user prompt option...
          options.slice(0, -1).forEach(function(option) {
            option.default = result[option.name];
          });
          // ...and start over again.
          ask();
        }
      });
    }());
  });

  // Built-in prompt options for the prompt_for helper.
  // These generally follow the node "prompt" module convention, except:
  // * The "default" value can be a function which is executed at run-time.
  // * An optional "sanitize" function has been added to post-process data.
  var prompts = {
    name: {
      message: 'Project name',
      default: function(value, data, done) {
        var types = ['javascript', 'js'];
        if (data.type) { types.push(data.type); }
        var type = '(?:' + types.join('|') + ')';
        // This regexp matches:
        //   leading type- type. type_
        //   trailing -type .type _type and/or -js .js _js
        var re = new RegExp('^' + type + '[\\-\\._]?|(?:[\\-\\._]?' + type + ')?(?:[\\-\\._]?js)?$', 'ig');
        // Strip the above stuff from the current dirname.
        var name = path.basename(process.cwd()).replace(re, '');
        // Remove anything not a letter, number, dash, dot or underscore.
        name = name.replace(/[^\w\-\.]/g, '');
        done(null, name);
      },
      validator: /^[\w\-\.]+$/,
      warning: 'Must be only letters, numbers, dashes, dots or underscores.',
      sanitize: function(value, data, done) {
        // An additional value, safe to use as a JavaScript identifier.
        data.js_safe_name = value.replace(/[\W_]+/g, '_').replace(/^(\d)/, '_$1');
        // If no value is passed to `done`, the original property isn't modified.
        done();
      }
    },
    title: {
      message: 'Project title',
      default: function(value, data, done) {
        var title = data.name || '';
        title = title.replace(/[\W_]+/g, ' ');
        title = title.replace(/\w+/g, function(word) {
          return word[0].toUpperCase() + word.slice(1).toLowerCase();
        });
        done(null, title);
      },
      warning: 'May consist of any characters.'
    },
    description: {
      message: 'Description',
      default: 'The best project ever.',
      warning: 'May consist of any characters.'
    },
    version: {
      message: 'Version',
      default: function(value, data, done) {
        // Get a valid semver tag from `git describe --tags` if possible.
        grunt.utils.spawn({
          cmd: 'git',
          args: ['describe', '--tags'],
          fallback: ''
        }, function(err, result, code) {
          result = result.split('-')[0];
          done(null, semver.valid(result) || '0.1.0');
        });
      },
      validator: semver.valid,
      warning: 'Must be a valid semantic version (semver.org).'
    },
    repository: {
      message: 'Project git repository',
      default: function(value, data, done) {
        // Change any git@...:... uri to git://.../... format.
        grunt.helper('git_origin', function(err, result) {
          if (err) {
            // Attempt to guess at the repo name. Maybe we'll get lucky!
            result = 'git://github.com/' + (process.env.USER || process.env.USERNAME || '???') + '/' +
              data.name + '.git';
          } else {
            result = result.replace(/^git@([^:]+):/, 'git://$1/');
          }
          done(null, result);
        });
      },
      sanitize: function(value, data, done) {
        // An additional computed "git_user" property.
        var repo = grunt.helper('github_web_url', data.repository);
        var parts;
        if (repo != null) {
          parts = repo.split('/');
          data.git_user = parts[parts.length - 2];
          done();
        } else {
          // Attempt to pull the data from the user's git config.
          grunt.utils.spawn({
            cmd: 'git',
            args: ['config', '--get', 'github.user'],
            fallback: ''
          }, function(err, result, code) {
            data.git_user = result || process.env.USER || process.env.USERNAME || '???';
            done();
          });
        }
      },
      warning: 'Should be a public git:// URI.'
    },
    homepage: {
      message: 'Project homepage',
      // If GitHub is the origin, the (potential) homepage is easy to figure out.
      default: function(value, data, done) {
        done(null, grunt.helper('github_web_url', data.repository) || 'none');
      },
      warning: 'Should be a public URL.'
    },
    bugs: {
      message: 'Project issues tracker',
      // If GitHub is the origin, the issues tracker is easy to figure out.
      default: function(value, data, done) {
        done(null, grunt.helper('github_web_url', data.repository, 'issues') || 'none');
      },
      warning: 'Should be a public URL.'
    },
    licenses: {
      message: 'Licenses',
      default: 'MIT',
      validator: /^[\w\-]+(?:\s+[\w\-]+)*$/,
      warning: 'Must be zero or more space-separated licenses. Built-in ' +
        'licenses are: ' + availableLicenses().join(' ') + ', but you may ' +
        'specify any number of custom licenses.',
      // Split the string on spaces.
      sanitize: function(value, data, done) { done(value.split(/\s+/)); }
    },
    author_name: {
      message: 'Author name',
      default: function(value, data, done) {
        // Attempt to pull the data from the user's git config.
        grunt.utils.spawn({
          cmd: 'git',
          args: ['config', '--get', 'user.name'],
          fallback: 'none'
        }, done);
      },
      warning: 'May consist of any characters.'
    },
    author_email: {
      message: 'Author email',
      default: function(value, data, done) {
        // Attempt to pull the data from the user's git config.
        grunt.utils.spawn({
          cmd: 'git',
          args: ['config', '--get', 'user.email'],
          fallback: 'none'
        }, done);
      },
      warning: 'Should be a valid email address.'
    },
    author_url: {
      message: 'Author url',
      default: 'none',
      warning: 'Should be a public URL.'
    },
    jquery_version: {
      message: 'Required jQuery version',
      default: '>= 1.6',
      warning: 'Must be a valid semantic version range descriptor.'
    },
    node_version: {
      message: 'What versions of node does it run on?',
      // TODO: pull from grunt's package.json
      default: '>= 0.6.0',
      warning: 'Must be a valid semantic version range descriptor.'
    },
    main: {
      message: 'Main module/entry point',
      default: function(value, data, done) {
        done(null, 'lib/' + data.name);
      },
      warning: 'Must be a path relative to the project root.'
    },
    bin: {
      message: 'CLI script',
      default: function(value, data, done) {
        done(null, 'bin/' + data.name);
      },
      warning: 'Must be a path relative to the project root.'
    },
    npm_test: {
      message: 'Npm test command',
      default: 'grunt test',
      warning: 'Must be an executable command.'
    },
    grunt_version: {
      message: 'What versions of grunt does it require?',
      default: '~' + grunt.version,
      warning: 'Must be a valid semantic version range descriptor.'
    }
  };

  // Expose prompts object so that prompt_for prompts can be added or modified.
  grunt.registerHelper('prompt_for_obj', function() {
    return prompts;
  });

  // Commonly-used prompt options with meaningful default values.
  grunt.registerHelper('prompt_for', function(name, altDefault) {
    // Clone the option so the original options object doesn't get modified.
    var option = grunt.utils._.clone(prompts[name]);
    option.name = name;

    var defaults = grunt.task.readDefaults('init/defaults.json');
    if (name in defaults) {
      // A user default was specified for this option, so use its value.
      option.default = defaults[name];
    } else if (arguments.length === 2) {
      // An alternate default was specified, so use it.
      option.altDefault = altDefault;
    }
    return option;
  });

  // Get the git origin url from the current repo (if possible).
  grunt.registerHelper('git_origin', function(done) {
    grunt.utils.spawn({
      cmd: 'git',
      args: ['remote', '-v']
    }, function(err, result, code) {
      var re = /^origin\s/;
      var lines;
      if (!err) {
        lines = result.split('\n').filter(re.test, re);
        if (lines.length > 0) {
          done(null, lines[0].split(/\s/)[1]);
          return;
        }
      }
      done(true, 'none');
    });
  });

  // Generate a GitHub web URL from a GitHub repo URI.
  var githubWebUrlRe = /^.+(?:@|:\/\/)(github.com)[:\/](.+?)(?:\.git|\/)?$/;
  grunt.registerHelper('github_web_url', function(uri, suffix) {
    var matches = githubWebUrlRe.exec(uri);
    if (!matches) { return null; }
    var url = 'https://' + matches[1] + '/' + matches[2];
    if (suffix) {
      url += '/' + suffix.replace(/^\//, '');
    }
    return url;
  });

};
