/*
 * monitor.js: Core functionality for the Monitor object.
 *
 * (C) 2010 Charlie Robbins & the Contributors
 * MIT LICENCE
 *
 */

const fs = require('fs');
const path = require('path');
const child_process = require('child_process');
const events = require('eventemitter2');
const spawn = child_process.spawn;
const common = require('./common');
const cluster = require('cluster');
const plugins = require('./plugins');
const utils = require('./utils');
const util = require('util');


//
// ### function Monitor (script, options)
// #### @script {string} Location of the target script to run.
// #### @options {Object} Configuration for this instance.
// Creates a new instance of forever with specified `options`.
//
const Monitor = (exports.Monitor = function(script, options) {
  //
  // Simple bootstrapper for attaching logger
  // and watch plugins by default. Other plugins
  // can be attached through `monitor.use(plugin, options)`.
  //
  function bootstrap(monitor) {
    plugins.logger.attach.call(monitor, options);
    if (options.watch) {
      plugins.watch.attach.call(monitor, options);
    }
  }

  let execPath = process.execPath;
  const self = this;

  //
  // Setup basic configuration options
  //
  options = options || {};
  this.silent = options.silent || false;
  this.killTree = options.killTree !== false;
  this.uid = options.uid || utils.randomString(4);
  this.id = options.id || false;
  this.pidFile = options.pidFile;
  this.max = options.max;
  this.killTTL = options.killTTL;
  this.killSignal = options.killSignal || 'SIGKILL';
  this.childExists = false;
  this.checkFile = options.checkFile !== false;
  this.times = 0;
  this.warn = console.error;

  this.logFile = options.logFile;
  this.outFile = options.outFile;
  this.errFile = options.errFile;
  this.append = options.append;
  this.usePolling = options.usePolling;
  this.pollingInterval = options.pollingInterval;

  //
  // Define some safety checks for commands with spaces
  //
  this.parser = options.parser || Monitor.parseCommand;

  //
  // Setup restart timing. These options control how quickly forever restarts
  // a child process as well as when to kill a "spinning" process
  //
  this.minUptime =
    typeof options.minUptime !== 'number' ? 0 : options.minUptime;
  this.spinSleepTime = options.spinSleepTime || null;

  //
  // Special case Windows separately to decouple any
  // future changes
  //
  if (process.platform === 'win32') {
    execPath = '"' + execPath + '"';
  }

  if (options.options) {
    console.warn('options.options is deprecated. Use options.args instead.');
  }

  //
  // Setup the command to spawn and the options to pass
  // to that command.
  //
  this.command = options.command || execPath;
  this.args = options.args || options.options || [];
  this.spawnWith = options.spawnWith || {};
  this.sourceDir = options.sourceDir;
  this.fork = options.fork || false;
  this.cwd = options.cwd || process.cwd();
  this.hideEnv = options.hideEnv || [];
  this._env = options.env || {};
  this._hideEnv = {};

  //
  // Allow for custom stdio configuration of forked processes
  //
  this.stdio = options.stdio || null;

  //
  // Setup watch configuration options
  //
  this.watchIgnoreDotFiles = options.watchIgnoreDotFiles !== false;
  this.watchIgnorePatterns = options.watchIgnorePatterns || [];
  this.watchDirectory = options.watchDirectory || this.sourceDir;

  //
  // Create a simple mapping of `this.hideEnv` to an easily indexable
  // object
  //
  this.hideEnv.forEach(function(key) {
    self._hideEnv[key] = true;
  });

  if (Array.isArray(script)) {
    this.command = script[0];
    this.args = script.slice(1);
  } else {
    this.args.unshift(script);
  }

  if (this.sourceDir) {
    this.args[0] = path.join(this.sourceDir, this.args[0]);
  }

  //
  // Bootstrap this instance now that options
  // have been set
  //
  bootstrap(this);
});

// Inherit from events.EventEmitter
util.inherits(Monitor, events.EventEmitter2);

//
// ### function start ([restart])
// #### @restart {boolean} Value indicating whether this is a restart.
// Start the process that this instance is configured for
//
Monitor.prototype.start = function(restart) {
  const self = this;

  if (this.running && !restart) {
    process.nextTick(function() {
      self.emit(
        'error',
        new Error('Cannot start process that is already running.')
      );
    });
    return this;
  }

  const child = this.trySpawn();
  if (!child) {
    process.nextTick(function() {
      self.emit(
        'error',
        new Error('Target script does not exist: ' + self.args[0])
      );
    });
    return this;
  }

  this.ctime = Date.now();
  this.child = child;
  this.running = true;
  this.isMaster = cluster.isMaster;

  process.nextTick(function() {
    self.emit(restart ? 'restart' : 'start', self, self.data);
  });

  function onMessage(msg) {
    self.emit('message', msg);
  }

  // Re-emit messages from the child process
  this.child.on('message', onMessage);

  child.on('exit', function(code, signal) {
    const spinning = Date.now() - self.ctime < self.minUptime;
    child.removeListener('message', onMessage);
    self.emit('exit:code', code, signal);

    function letChildDie() {
      self.running = false;
      self.forceStop = false;
      self.forceRestart = false;
      self.emit('exit', self, spinning);
    }

    function restartChild() {
      self.forceStop = false;
      self.forceRestart = false;
      process.nextTick(function() {
        self.start(true);
      });
    }

    self.times++;

    if (
      self.forceStop ||
      (self.times >= self.max && !self.forceRestart) ||
      (spinning && typeof self.spinSleepTime !== 'number' && !self.forceRestart)
    ) {
      letChildDie();
    } else if (spinning) {
      setTimeout(restartChild, self.spinSleepTime);
    } else {
      restartChild();
    }
  });

  return this;
};

//
// ### function trySpawn()
// Tries to spawn the target Forever child process. Depending on
// configuration, it checks the first argument of the options
// to see if the file exists. This is useful is you are
// trying to execute a script with an env: e.g. node myfile.js
//
Monitor.prototype.trySpawn = function() {
  const run = this.parser(this.command, this.args.slice());
  let stats;

  if (/[^\w]node$/.test(this.command) && this.checkFile && !this.childExists) {
    try {
      stats = fs.statSync(this.args[0]);
      this.childExists = true;
    } catch (ex) {
      return false;
    }
  }

  this.spawnWith.cwd = this.spawnWith.cwd || this.cwd;
  this.spawnWith.env = this._getEnv();

  if (process.platform === 'win32') {
    this.spawnWith.detached = true;
  }

  if (this.stdio) {
    this.spawnWith.stdio = this.stdio;
  }

  if (this.fork) {
    if (!this.stdio) {
      this.spawnWith.stdio = ['pipe', 'pipe', 'pipe', 'ipc'];
    }
    return spawn(run.command, run.args, this.spawnWith);
  }

  return spawn(run.command, run.args, this.spawnWith);
};

//
// ### @data {Object}
// Responds with the appropriate information about
// this `Monitor` instance and it's associated child process.
//
Monitor.prototype.__defineGetter__('data', function() {
  const self = this;
  const childData = {
    ctime: this.ctime,
    command: this.command,
    file: this.args[0],
    foreverPid: process.pid,
    logFile: this.logFile,
    args: this.args.slice(1),
    pid: this.child ? this.child.pid : undefined,
    silent: this.silent,
    uid: this.uid,
    id: this.id,
    spawnWith: this.spawnWith,
    running: this.running,
    restarts: this.times,
    isMaster: this.isMaster,
  };

  ['pidFile', 'outFile', 'errFile', 'env', 'cwd'].forEach(function(key) {
    if (self[key]) {
      childData[key] = self[key];
    }
  });

  if (this.sourceDir) {
    childData.sourceDir = this.sourceDir;
    childData.file = childData.file.replace(this.sourceDir + '/', '');
  }

  this.childData = childData;
  return this.childData;

  //
  // Setup the forever process to listen to
  // SIGINT and SIGTERM events so that we can
  // clean up the *.pid file
  //
  // Remark: This should work, but the fd gets screwed up
  //         with the daemon process.
  //
  // process.on('SIGINT', function () {
  //   process.exit(0);
  // });
  //
  // process.on('SIGTERM', function () {
  //   process.exit(0);
  // });
  // process.on('exit', function () {
  //   fs.unlinkSync(childPath);
  // });
});

//
// ### function restart ()
// Restarts the target script associated with this instance.
//
Monitor.prototype.restart = function() {
  this.times = this.times || 0;
  this.forceRestart = true;

  return !this.running ? this.start(true) : this.kill(false);
};

//
// ### function stop ()
// Stops the target script associated with this instance. Prevents it from auto-respawning
//
Monitor.prototype.stop = function() {
  return this.kill(true);
};

//
// ### function kill (forceStop)
// #### @forceStop {boolean} Value indicating whether short circuit forever auto-restart.
// Kills the ChildProcess object associated with this instance.
//
Monitor.prototype.kill = function(forceStop) {
  const child = this.child,
    self = this;
  let timer;

  if (!child || (!this.running && !this.forceRestart)) {
    process.nextTick(function() {
      self.emit('error', new Error('Cannot stop process that is not running.'));
    });
  } else {
    //
    // Set an instance variable here to indicate this
    // stoppage is forced so that when `child.on('exit', ..)`
    // fires in `Monitor.prototype.start` we can short circuit
    // and prevent auto-restart
    //
    if (forceStop) {
      this.forceStop = true;
      //
      // If we have a time before we truly kill forcefully, set up a timer
      //
      if (this.killTTL) {
        timer = setTimeout(function() {
          common.kill(
            self.child.pid,
            self.killTree,
            self.killSignal || 'SIGKILL'
          );
        }, this.killTTL);

        child.once('exit', function() {
          clearTimeout(timer);
        });
      }
    }

    child.once('exit', function() {
      self.emit('stop', self.childData);
      if (self.forceRestart && !self.running) {
        self.start(true);
      }
    });

    common.kill(this.child.pid, this.killTree, this.killSignal);
  }

  return this;
};

//
// ### function send ()
// Sends a message to a forked ChildProcess object associated with this instance.
// see http://nodejs.org/api/child_process.html#child_process_child_send_message_sendhandle
//
Monitor.prototype.send = function(msg) {
  const child = this.child,
    self = this;

  if (!child || !this.running) {
    process.nextTick(function() {
      self.emit(
        'error',
        new Error('Cannot send to process that is not running.')
      );
    });
  }

  if (child.send) {
    child.send(msg);
  }
};

//
// ### function toString ()
// Override default toString behavior and just respond
// with JSON for this instance.
//
Monitor.prototype.toString = function() {
  return JSON.stringify(this);
};

//
// ### function inspect ()
// Set this to null so that `util.inspect` does not
// return `undefined`.'
//
Monitor.prototype.inspect = null;

//
// ### @private function _getEnv ()
// Returns the environment variables that should be passed along
// to the target process spawned by this instance.
//
Monitor.prototype._getEnv = function() {
  const self = this,
    merged = {};

  function addKey(key, source) {
    merged[key] = source[key];
  }

  //
  // Mixin the key:value pairs from `process.env` and the custom
  // environment variables in `this._env`.
  //
  Object.keys(process.env).forEach(function(key) {
    if (!self._hideEnv[key]) {
      addKey(key, process.env);
    }
  });

  Object.keys(this._env).forEach(function(key) {
    addKey(key, self._env);
  });

  return merged;
};

//
// ### function parseCommand (command, args)
// #### @command {String} Command string to parse
// #### @args    {Array}  Additional default arguments
//
// Returns the `command` and the `args` parsed
// from the command depending on the Platform.
//
Monitor.parseCommand = function(command, args) {
  const match = command.match(
    process.platform === 'win32' ? safetyChecks.windows : safetyChecks.linux
  );

  //
  // No match means it's a bad command. This is configurable
  // by passing a custom `parser` function into the `Monitor`
  // constructor function.
  //
  if (!match) {
    return false;
  }

  if (process.platform == 'win32') {
    command = match[1] || match[2];
    if (match[3]) {
      args = match[3].split(' ').concat(args);
    }
  } else {
    command = match[1];
    if (match[2]) {
      args = match[2].split(' ').concat(this.args);
    }
  }

  return {
    command: command,
    args: args,
  };
};

//
// ### @private {Object} safetyChecks
// Define default safety checks for commands
// with spaces in Windows & Linux
//
const safetyChecks = {
  windows: /(?:"(.*[^\/])"|(\w+))(?:\s(.*))?/,
  linux: /(.*?[^\\])(?: (.*)|$)/,
};
