var fs = require('fs');
var path = require('path');

var async = require('async');
var rimraf = require('rimraf');

var util = require('../lib/util');

var counter = 0;
var configCache = {};

function cacheConfig(config) {
  ++counter;
  configCache[counter] = config;
  return counter;
}

function pluckConfig(id) {
  if (!configCache.hasOwnProperty(id)) {
    throw new Error('Failed to find id in cache');
  }
  var config = configCache[id];
  delete configCache[id];
  return config;
}

function nullOverride(details, include) {
  include(false);
}

function createTask(grunt) {
  return function(taskName, targetName) {
    var tasks = [];
    var prefix = this.name;
    if (!targetName) {
      if (!grunt.config(taskName)) {
        grunt.fatal('The "' + prefix + '" prefix is not supported for aliases');
        return;
      }
      Object.keys(grunt.config(taskName)).forEach(function(targetName) {
        if (!/^_|^options$/.test(targetName)) {
          tasks.push(prefix + ':' + taskName + ':' + targetName);
        }
      });
      return grunt.task.run(tasks);
    }
    var args = Array.prototype.slice.call(arguments, 2).join(':');
    var options = this.options({
      cache: path.join(__dirname, '..', '.cache'),
      override: nullOverride,
      tolerance: 0 // allowed difference between src and dst in ms
    });

    // support deprecated timestamps option
    if (options.timestamps) {
      grunt.log.warn('DEPRECATED OPTION.  Use the "cache" option instead');
      options.cache = options.timestamps;
    }

    // Sanity check for the tolerance option
    if (typeof options.tolerance !== 'number') {
      grunt.log.warn('The tolerance value must be a number, ignoring current ' +
      'value');
      options.tolerance = 0;
    }
    if (options.tolerance < 0) {
      grunt.log.warn('A tolerance value of ' + options.tolerance +
      ' is invalid');
      options.tolerance = 0;
    }

    var done = this.async();

    var originalConfig = grunt.config.get([taskName, targetName]);
    var config = grunt.util._.clone(originalConfig);

    /**
     * Special handling for tasks that expect the `files` config to be a string
     * or array of string source paths.
     */
    var srcFiles = true;
    if (typeof config.files === 'string') {
      config.src = [config.files];
      delete config.files;
      srcFiles = false;
    } else if (Array.isArray(config.files) &&
        typeof config.files[0] === 'string') {
      config.src = config.files;
      delete config.files;
      srcFiles = false;
    }

    var stamp = util.getStampPath(options.cache, taskName, targetName);
    var previous;
    try {
      previous = fs.statSync(stamp).mtime;
    } catch (err) {
      // task has never succeeded before
      previous = new Date(0);
    }

    function override(filePath, time, include) {
      var details = {
        task: taskName,
        target: targetName,
        path: filePath,
        time: time
      };
      options.override(details, include);
    }

    var files = grunt.task.normalizeMultiTaskFiles(config, targetName);
    util.filterFilesByTime(
      files, previous, options.tolerance, override, function(e, newerFiles) {
      if (e) {
        return done(e);
      } else if (newerFiles.length === 0) {
        grunt.log.writeln('No newer files to process.');
        return done();
      }

      /**
       * If we started out with only src files in the files config,
       * transform the newerFiles array into an array of source files.
       */
      if (!srcFiles) {
        newerFiles = newerFiles.map(function(obj) {
          return obj.src;
        });
      }

      // configure target with only newer files
      config.files = newerFiles;
      delete config.src;
      delete config.dest;
      grunt.config.set([taskName, targetName], config);

      // because we modified the task config, cache the original
      var id = cacheConfig(originalConfig);

      // run the task, and attend to postrun tasks
      var qualified = taskName + ':' + targetName;
      var tasks = [
        qualified + (args ? ':' + args : ''),
        'newer-postrun:' + qualified + ':' + id + ':' + options.cache
      ];
      grunt.task.run(tasks);

      done();
    });

  };
}


/** @param {Object} grunt Grunt. */
module.exports = function(grunt) {

  grunt.registerTask(
      'newer', 'Run a task with only those source files that have been ' +
      'modified since the last successful run.', createTask(grunt));

  var deprecated = 'DEPRECATED TASK.  Use the "newer" task instead';
  grunt.registerTask(
      'any-newer', deprecated, function() {
        grunt.log.warn(deprecated);
        var args = Array.prototype.join.call(arguments, ':');
        grunt.task.run(['newer:' + args]);
      });

  var internal = 'Internal task.';
  grunt.registerTask(
      'newer-postrun', internal, function(taskName, targetName, id, dir) {

        // if dir includes a ':', grunt will split it among multiple args
        dir = Array.prototype.slice.call(arguments, 3).join(':');
        grunt.file.write(util.getStampPath(dir, taskName, targetName),
            String(Date.now()));

        // reconfigure task with original config
        grunt.config.set([taskName, targetName], pluckConfig(id));

      });

  var clean = 'Remove cached timestamps.';
  grunt.registerTask(
      'newer-clean', clean, function(taskName, targetName) {
        var done = this.async();

        /**
         * This intentionally only works with the default cache dir.  If a
         * custom cache dir is provided, it is up to the user to keep it clean.
         */
        var cacheDir = path.join(__dirname, '..', '.cache');
        if (taskName && targetName) {
          cacheDir = util.getStampPath(cacheDir, taskName, targetName);
        } else if (taskName) {
          cacheDir = path.join(cacheDir, taskName);
        }
        if (grunt.file.exists(cacheDir)) {
          grunt.log.writeln('Cleaning ' + cacheDir);
          rimraf(cacheDir, done);
        } else {
          done();
        }
      });

};
