var path = require('path');
var util = require('util');
var _ = require('lodash');
var File = require('./file');

module.exports = Env;

function Env(opt) {
  File.apply(this, arguments);

  this._base = opt.base || '';
  this._destBase = opt.dest || '';
  this._writeFilters = {};
  this._validationFilters = {};

  var methodsToPrefix = [ 'mkdir', 'recurse', 'read', 'readJSON', 'write', 'delete' ];

  // Prefix path arguments with this environment root dir
  methodsToPrefix.forEach(function(methodName) {
    this[methodName] = function() {
      var args = _.toArray(arguments);
      args[0] = this.fromBase(args[0]);
      return File.prototype[methodName].apply(this, args);
    };
  }.bind(this));

  this.copy = function() {
    var args = _.toArray(arguments);
    args[0] = this.fromBase(args[0]);
    args[1] = this.fromDestBase(args[1]);
    return File.prototype.copy.apply(this, args);
  };

  this._actualWrite = this.write;
  this.write = function(filepath, contents, options) {
    return this.applyWriteFilters({
      path: filepath,
      contents: contents
    }, this.applyValidationFilters, options);
  };
}

util.inherits(Env, File);

Env.prototype.setBase = function(filepath) {
  this._base = filepath;
};

// Return a path prefixed by the base (if not absolute)
Env.prototype.fromBase = function(filepath) {
  if (this.isPathAbsolute(filepath)) {
    return filepath;
  }
  return path.join(this._base, filepath);
};

Env.prototype.setDestBase = function(filepath) {
  this._destBase = filepath;
};

// Return a path prefixed by the destination base (if not absolute)
Env.prototype.fromDestBase = function(filepath) {
  if (this.isPathAbsolute(filepath)) {
    return filepath;
  }
  return path.join(this._destBase, filepath);
};

Env.prototype.registerWriteFilter = function(name, filter) {
  this._writeFilters[name] = filter;
};

Env.prototype.removeWriteFilter = function(name) {
  delete this._writeFilters[name];
};

Env.prototype.registerValidationFilter = function(name, filter) {
  this._validationFilters[name] = filter;
};

Env.prototype.removeValidationFilter = function(name) {
  delete this._validationFilters[name];
};


Env.prototype.applyWriteFilters = function(file, validate, options) {
  var writeFilters = _.reduce(this._writeFilters, function(m, v) { m.push(v); return m; }, []);
  if (!writeFilters.length) {
    return validate.call(this, file, options);
  }

  var i = 0;
  var output;
  var recurse = function(file) {
    i++;
    if (writeFilters[i]) {
      runAsync(writeFilters[i], recurse, file);
    } else {
      output = validate.call(this, file, options);
    }
  }.bind(this);

  runAsync(writeFilters[i], recurse, file);

  return output;
};

Env.prototype.applyValidationFilters = function( file, options ) {
  var validationFilters = _.reduce(this._validationFilters, function(m, v) { m.push(v); return m; }, []);
  if (!validationFilters.length) {
    return this._actualWrite.call(this, file.path, file.contents, options);
  }

  var i = 0;
  var output;
  var recurse = function(validated) {
    if ( validated !== true ) {
      return this.log.write(validated || 'Not actually writing to '+ file.path +' haven\'t pass validation' );
    }
    i++;
    if (validationFilters[i]) {
      runAsync(validationFilters[i], recurse, file);
    } else {
      output = this._actualWrite.call(this, file.path, file.contents, options);
    }
  }.bind(this);

  runAsync(validationFilters[i], recurse, file);

  return output;
};


/**
 * Allow a function to be run async by using `this.async()`. If not, then the function is
 * runned synchronously.
 * @param  {Function} func  The function to run
 * @param  {Function} cb    Callback to be provided even if the function is run sync
 * @param  {...rest}        Arguments passed to `func`
 * @return {null}
 */
function runAsync( func, cb ) {
  var rest = [];
  var len = 1;

  while ( len++ < arguments.length ) {
    rest.push( arguments[len] );
  }

  var async = false;
  var returnValue = func.apply({
    async: function() {
      async = true;
      return _.once(cb);
    }
  }, rest );

  // Note: Call the callback synchronously to keep the sync flow by default
  if ( !async ) {
    cb(returnValue);
  }
}
