var util = require('util');
var path = require('path');
var EE = require('events').EventEmitter;

var extend = require('extend');
var resolve = require('resolve');
var flaggedRespawn = require('flagged-respawn');
var isPlainObject = require('is-plain-object');
var mapValues = require('object.map');
var fined = require('fined');

var findCwd = require('./lib/find_cwd');
var findConfig = require('./lib/find_config');
var fileSearch = require('./lib/file_search');
var parseOptions = require('./lib/parse_options');
var silentRequire = require('./lib/silent_require');
var buildConfigName = require('./lib/build_config_name');
var registerLoader = require('./lib/register_loader');
var getNodeFlags = require('./lib/get_node_flags');

function Liftoff(opts) {
  EE.call(this);
  extend(this, parseOptions(opts));
}
util.inherits(Liftoff, EE);

Liftoff.prototype.requireLocal = function(module, basedir) {
  try {
    this.emit('preload:before', module);
    var result = require(resolve.sync(module, { basedir: basedir }));
    this.emit('preload:success', module, result);
    return result;
  } catch (e) {
    this.emit('preload:failure', module, e);
  }
};

Liftoff.prototype.buildEnvironment = function(opts) {
  opts = opts || {};

  // get modules we want to preload
  var preload = opts.preload || [];

  // ensure items to preload is an array
  if (!Array.isArray(preload)) {
    preload = [preload];
  }

  // make a copy of search paths that can be mutated for this run
  var searchPaths = this.searchPaths.slice();

  // calculate current cwd
  var cwd = findCwd(opts);

  // if cwd was provided explicitly, only use it for searching config
  if (opts.cwd) {
    searchPaths = [cwd];
  } else {
    // otherwise just search in cwd first
    searchPaths.unshift(cwd);
  }

  // calculate the regex to use for finding the config file
  var configNameSearch = buildConfigName({
    configName: this.configName,
    extensions: Object.keys(this.extensions),
  });

  // calculate configPath
  var configPath = findConfig({
    configNameSearch: configNameSearch,
    searchPaths: searchPaths,
    configPath: opts.configPath,
  });

  // if we have a config path, save the directory it resides in.
  var configBase;
  if (configPath) {
    configBase = path.dirname(configPath);
    // if cwd wasn't provided explicitly, it should match configBase
    if (!opts.cwd) {
      cwd = configBase;
    }
  }

  // TODO: break this out into lib/
  // locate local module and package next to config or explicitly provided cwd
  /* eslint one-var: 0 */
  var modulePath, modulePackage;
  try {
    var delim = path.delimiter;
    var paths = (process.env.NODE_PATH ? process.env.NODE_PATH.split(delim) : []);
    modulePath = resolve.sync(this.moduleName, { basedir: configBase || cwd, paths: paths });
    modulePackage = silentRequire(fileSearch('package.json', [modulePath]));
  } catch (e) {}

  // if we have a configuration but we failed to find a local module, maybe
  // we are developing against ourselves?
  if (!modulePath && configPath) {
    // check the package.json sibling to our config to see if its `name`
    // matches the module we're looking for
    var modulePackagePath = fileSearch('package.json', [configBase]);
    modulePackage = silentRequire(modulePackagePath);
    if (modulePackage && modulePackage.name === this.moduleName) {
      // if it does, our module path is `main` inside package.json
      modulePath = path.join(path.dirname(modulePackagePath), modulePackage.main || 'index.js');
      cwd = configBase;
    } else {
      // clear if we just required a package for some other project
      modulePackage = {};
    }
  }

  var exts = this.extensions;
  var eventEmitter = this;

  var configFiles = {};
  if (isPlainObject(this.configFiles)) {
    var notfound = { path: null };
    configFiles = mapValues(this.configFiles, function(prop, name) {
      var defaultObj = { name: name, cwd: cwd, extensions: exts };
      return mapValues(prop, function(pathObj) {
        var found = fined(pathObj, defaultObj) || notfound;
        if (isPlainObject(found.extension)) {
          registerLoader(eventEmitter, found.extension, found.path, cwd);
        }
        return found.path;
      });
    });
  }

  return {
    cwd: cwd,
    preload: preload,
    configNameSearch: configNameSearch,
    configPath: configPath,
    configBase: configBase,
    modulePath: modulePath,
    modulePackage: modulePackage || {},
    configFiles: configFiles,
  };
};

Liftoff.prototype.handleFlags = function(cb) {
  if (typeof this.v8flags === 'function') {
    this.v8flags(function(err, flags) {
      if (err) {
        cb(err);
      } else {
        cb(null, flags);
      }
    });
  } else {
    process.nextTick(function() {
      cb(null, this.v8flags);
    }.bind(this));
  }
};

Liftoff.prototype.prepare = function(opts, fn) {
  if (typeof fn !== 'function') {
    throw new Error('You must provide a callback function.');
  }

  process.title = this.processTitle;

  var completion = opts.completion;
  if (completion && this.completions) {
    return this.completions(completion);
  }

  var env = this.buildEnvironment(opts);

  fn.call(this, env);
};

Liftoff.prototype.execute = function(env, forcedFlags, fn) {
  if (typeof forcedFlags === 'function') {
    fn = forcedFlags;
    forcedFlags = undefined;
  }
  if (typeof fn !== 'function') {
    throw new Error('You must provide a callback function.');
  }

  this.handleFlags(function(err, flags) {
    if (err) {
      throw err;
    }
    flags = flags || [];

    flaggedRespawn(flags, process.argv, forcedFlags, execute.bind(this));

    function execute(ready, child, argv) {
      if (child !== process) {
        var execArgv = getNodeFlags.fromReorderedArgv(argv);
        this.emit('respawn', execArgv, child);
      }
      if (ready) {
        preloadModules(this, env);
        registerLoader(this, this.extensions, env.configPath, env.cwd);
        fn.call(this, env, argv);
      }
    }
  }.bind(this));
};

function preloadModules(inst, env) {
  var basedir = env.cwd;
  env.preload.filter(toUnique).forEach(function(module) {
    inst.requireLocal(module, basedir);
  });
}

function toUnique(elem, index, array) {
  return array.indexOf(elem) === index;
}

module.exports = Liftoff;
