// "maintainers": [
//   {
//     "name": "Garen J. Torikian",
//     "email": "gjtorikian@gmail.com"
//   }
// ],
// "repository": {
//   "type": "git",
//   "url": "git+https://github.com/gjtorikian/isBinaryFile.git"
// },

/**
 * Module dependencies
 */
var fs        = require('fs');
var path      = require('path');
var util      = require('util');
var mkdirp    = require('mkdirp');
var cronJob   = require('cron').CronJob;
var isBinary  = require('./tools/isbinaryfile.js');
var Utility   = require('./Utility.js');
var async     = require('async');
var cst       = require('../constants.js');
var extItps   = require('./interpreter.json');
var shelljs   = require('shelljs');
var p         = path;
var Satan     = require('./Satan.js');

var InteractorDaemonizer = require('./Interactor/InteractorDaemonizer.js');

/**
 * Common methods (used by CLI and God)
 */

var Common = module.exports;

/**
 * Resolve app paths and replace missing values with defaults.
 * @method prepareAppConf
 * @param app {Object}
 * @param {} cwd
 * @param {} outputter
 * @return app
 */
Common.prepareAppConf = function(app, outputter) {
  /**
   * Minimum validation
   */
  if (!app.script)
    return new Error('No script path - aborting');

  // Forbidden application name
  if (app.name.indexOf(['push']) > -1)
    return new Error('Push application name is not allowed');

  if (app.automation == false)
    app.pmx = false;

  if (!app.node_args)
    app.node_args = [];

  if (app.port && app.env)
    app.env.PORT = app.port;

  // CRON
  var ret;
  if ((ret = Common.sink.determineCron(app)) instanceof Error)
    return ret;

  var cwd = null;

  if (app.cwd) {
    cwd = p.resolve(app.cwd);
    process.env.PWD = app.cwd;
  }

  // CWD option resolving
  cwd && (cwd[0] != '/') && (cwd = p.resolve(process.cwd(), cwd));
  cwd = cwd || process.cwd();

  // Full path script resolution
  app.pm_exec_path = p.resolve(cwd, app.script);

  // If script does not exists after resolution
  if (!fs.existsSync(app.pm_exec_path)) {
    var ckd;
    // Try resolve command available in $PATH
    if ((ckd = shelljs.which(app.script)))
      app.pm_exec_path = ckd;
    else
      // Throw critical error
      return new Error('script not found : ' + app.pm_exec_path);
  }

  /**
   * Auto detect .map file and enable source map support automatically
   */
  if (app.disable_source_map_support != true) {
    try {
      if (fs.accessSync) {
        fs.accessSync(app.pm_exec_path + '.map', fs.R_OK);
        app.source_map_support = true;
      }
      else {
        // Support for Node 0.10.x
        if (fs.existsSync(app.pm_exec_path + '.map')) {
          app.source_map_support = true;
        }
      }
    } catch(e) {}
    delete app.disable_source_map_support;
  }

  delete app.script;

  // Set current env by first adding the process environment and then extending/replacing it
  // with env specified on command-line or JSON file.
  app.env = [{}, process.env, app.env || {}, {pm_cwd: cwd}].reduce(function(e1, e2){
    return util._extend(e1, e2);
  });

  app.pm_cwd = cwd;

  // Interpreter
  Common.sink.resolveInterpreter(app);

  // Exec mode and cluster stuff
  Common.sink.determineExecMode(app);

  /**
   * Scary
   */
  var formated_app_name = app.name.replace(/[^a-zA-Z0-9\\.\\-]/g, '-');

  ['log', 'out', 'error', 'pid'].forEach(function(f){

    var af = app[f + '_file'], ps, ext = (f == 'pid' ? 'pid':'log'), isStd = !~['log', 'pid'].indexOf(f);
    if ((f == 'log' && typeof af == 'boolean' && af) || (f != 'log' && !af)) {
      ps = [cst['DEFAULT_' + ext.toUpperCase() + '_PATH'], formated_app_name + (isStd ? '-' + f : '') + '.' + ext];
    } else if (f != 'log' || (f == 'log' && af)) {
      ps = [cwd, af];

      if (!fs.existsSync(path.dirname(af))) {
        Common.printError(cst.PREFIX_MSG_ERR + 'Folder does not exists: ' + path.dirname(af));
        Common.printOut(cst.PREFIX_MSG + 'Creating folder: ' + path.dirname(af));
        mkdirp(path.dirname(af), function(err) {
          if (!err) return;
          Common.printError(cst.PREFIX_MSG_ERR + 'Could not create folder: ' + path.dirname(af));
          throw new Error('Could not create folder');
        });
      }

    }
    // PM2 paths
    ps && (app['pm_' + (isStd ? f.substr(0, 3) + '_' : '') + ext + '_path'] = p.resolve.apply(null, ps));
    delete app[f + '_file'];
  });

  return app;
};

Common.sink = {};

Common.sink.determineCron = function(app) {
  if (app.cron_restart) {
    try {
      Common.printOut(cst.PREFIX_MSG + 'cron restart at ' + app.cron_restart);
      new cronJob(app.cron_restart, function() {
        Common.printOut(cst.PREFIX_MSG + 'cron pattern for auto restart detected and valid');
      });
    } catch(ex) {
      return new Error('Cron pattern is not valid, trace: ' + ex.stack);
    }
  }
};

Common.sink.determineExecMode = function(app) {
  if (typeof app.instances == 'undefined')
    app.instances = 1;
  if (app.exec_mode)
    app.exec_mode = app.exec_mode.replace(/^(fork|cluster)$/, '$1_mode');

  /**
   * Here we put the default exec mode
   */
  if (!app.exec_mode && app.instances > 1) {
    app.exec_mode = 'cluster_mode';
  } else if (!app.exec_mode) {
    app.exec_mode = 'fork_mode';
  }
};

/**
 * Resolve interpreter
 */
Common.sink.resolveInterpreter = function(app) {
  var noInterpreter     = (!app.exec_interpreter || 'none' == app.exec_interpreter),
      extName           = p.extname(app.pm_exec_path),
      betterInterpreter = extItps[extName];

  var thereIsNVMInstalled = false;

  // No interpreter defined and correspondance in schema hashmap
  if (noInterpreter && betterInterpreter)
    app.exec_interpreter = betterInterpreter;
  // Else if no Interpreter detect if process is binary
  else if (noInterpreter)
    app.exec_interpreter = isBinary(app.pm_exec_path) ? 'none' : 'node';
  else if (app.exec_interpreter.indexOf('node@') > -1 ||
           app.node_version && thereIsNVMInstalled)
    console.log('Special interpreter defined');
  return app;
};

Common.deepCopy = Common.serialize = Common.clone = function(obj) {
  if (obj === null || obj === undefined) return {};

  return Utility.clone(obj);
};

/**
 * Return a safe copy of pm2_env
 */
Common.formatCLU = function(csp) {
  if (!csp.pm2_env) {
    return csp;
  }

  var obj = Common.clone(csp.pm2_env);
  delete obj.env;

  return obj;
};

/**
 * Description
 * @method exitCli
 * @param {} code
 * @return CallExpression
 */
Common.exitCli = function(code) {
  InteractorDaemonizer.disconnectRPC(function() {
    Satan.disconnectRPC(function() {
      code = code || 0;
      // Safe exits process after all streams are drained.
      // file descriptor flag.
      var fds = 0;
      // exits process when stdout (1) and sdterr(2) are both drained.
      function tryToExit() {
        if ((fds & 1) && (fds & 2)) {
          process.exit(code);
        }
      }

      [process.stdout, process.stderr].forEach(function(std) {
        var fd = std.fd;
        if (!std.bufferSize) {
          // bufferSize equals 0 means current stream is drained.
          fds = fds | fd;
        } else {
          // Appends nothing to the std queue, but will trigger `tryToExit` event on `drain`.
          std.write && std.write('', function() {
            fds = fds | fd;
            tryToExit();
          });
        }
        // Does not write anything more.
        delete std.write;
      });
      tryToExit();
    });
  });
};

/**
 * Description
 * @method printError
 * @param {} msg
 * @return CallExpression
 */
Common.printError = function(msg) {
  if (process.env.PM2_SILENT || process.env.PM2_PROGRAMMATIC === 'true') return false;
  if (msg instanceof Error)
    return console.error(msg.message);
  return console.error.apply(console, arguments);
};

/**
 * Description
 * @method printOut
 * @return
 */
Common.printOut = function() {
  if (process.env.PM2_SILENT || process.env.PM2_PROGRAMMATIC === 'true') return false;
  return console.log.apply(console, arguments);
};

Common.getAllModulesId = function(cb) {
  var found_proc = [];

  Satan.executeRemote('getMonitorData', {}, function(err, list) {
    if (err) {
      Common.printError('Error retrieving process list: ' + err);
      return cb(err);
    }

    list.forEach(function(proc) {
      if (proc.pm2_env.pmx_module)
        found_proc.push(proc.pm_id);
    });

    return cb(null, found_proc);
  });
};

Common.getAllProcess = function(cb) {
  var found_proc = [];

  Satan.executeRemote('getMonitorData', {}, function(err, list) {
    if (err) {
      Common.printError('Error retrieving process list: ' + err);
      return cb(err);
    }

    list.forEach(function(proc) {
      found_proc.push(proc);
    });

    return cb(null, found_proc);
  });
};


Common.getAllProcessId = function(cb) {
  var found_proc = [];

  Satan.executeRemote('getMonitorData', {}, function(err, list) {
    if (err) {
      Common.printError('Error retrieving process list: ' + err);
      return cb(err);
    }

    list.forEach(function(proc) {
      if (!proc.pm2_env.pmx_module)
        found_proc.push(proc.pm_id);
    });

    return cb(null, found_proc);
  });
};

Common.getProcessIdByName = function(name, force_all, cb) {
  var found_proc   = [];
  var full_details = {};

  if (typeof(cb) === 'undefined') {
    cb = force_all;
    force_all = false;
  }

  if (typeof(name) == 'number')
    name = name.toString();

  Satan.executeRemote('getMonitorData', {}, function(err, list) {
    if (err) {
      Common.printError('Error retrieving process list: ' + err);
      return cb(err);
    }

    list.forEach(function(proc) {
      if ((proc.pm2_env.name == name || proc.pm2_env.pm_exec_path == p.resolve(name)) &&
          !(proc.pm2_env.pmx_module && !force_all)) {
        found_proc.push(proc.pm_id);
        full_details[proc.pm_id] = proc;
      }
    });

    return cb(null, found_proc, full_details);
  });
};

Common.getProcessByName = function(name, cb) {
  var found_proc = [];

  Satan.executeRemote('getMonitorData', {}, function(err, list) {
    if (err) {
      Common.printError('Error retrieving process list: ' + err);
      return cb(err);
    }

    list.forEach(function(proc) {
      if (proc.pm2_env.name == name ||
          proc.pm2_env.pm_exec_path == p.resolve(name)) {
        found_proc.push(proc);
      }
    });

    return cb(null, found_proc);
  });
};

Common.extend = function(origin, add){
  // Don't do anything if add isn't an object
  if (!add || typeof add != 'object') return origin;

  //Ignore PM2's set environment variables from the nested env
  var keysToIgnore = ['name', 'exec_mode', 'env', 'args', 'pm_cwd', 'exec_interpreter', 'pm_exec_path', 'node_args', 'pm_out_log_path', 'pm_err_log_path', 'pm_pid_path', 'pm_id', 'status', 'pm_uptime', 'created_at', 'started_inside', 'unstable_restarts', 'restart_time', 'pm_id', 'axm_actions', 'pmx_module', 'command', 'watch', 'versioning', 'vizion_runing', 'MODULE_DEBUG'];

  var keys = Object.keys(add);
  var i = keys.length;
  while (i--) {
  	//Only copy stuff into the env that we don't have already.
  	if(keysToIgnore.indexOf(keys[i]) == -1 && add[keys[i]] != '[object Object]')
      origin[keys[i]] = add[keys[i]];
  }
  return origin;
};
