#!/usr/bin/env node
var fs = require('fs'),
    util = require('util'),
    childProcess = require('child_process'),
    dirs = [],
    path = require('path'),
    exists = fs.exists || path.exists, // yay, exists moved from path to fs in 0.7.x ... :-\
    existsSync = fs.existsSync || path.existsSync,
    spawn = childProcess.spawn,
    meta = JSON.parse(fs.readFileSync(__dirname + '/package.json')),
    exec = childProcess.exec,
    flag = './.monitor',
    child = null,
    monitor = null,
    ignoreFilePath = './.nodemonignore',
    oldIgnoreFilePath = './nodemon-ignore',
    ignoreFiles = [],
    reIgnoreFiles = null,
    timeout = 1000, // check every 1 second
    restartDelay = 0, // controlled through arg --delay 10 (for 10 seconds)
    restartTimer = null,
    lastStarted = +new Date,
    statOffset = 0, // stupid fix for https://github.com/joyent/node/issues/2705
    platform = process.platform,
    isWindows = platform === 'win32',
    noWatch = (platform !== 'win32') || !fs.watch, //  && platform !== 'linux' - removed linux fs.watch usage #72
    watchFile = platform === 'darwin' ? fs.watchFile : fs.watch, // lame :(
    watchWorks = true, // whether or not fs.watch actually works on this platform, tested and set later before starting
    // create once, reuse as needed
    reEscComments = /\\#/g,
    reUnescapeComments = /\^\^/g, // note that '^^' is used in place of escaped comments
    reComments = /#.*$/,
    reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g,
    reEscapeChars = /[.|\-[\]()\\]/g,
    reAsterisk = /\*/g,
    // Flag to distinguish an app crash from intentional killing (used on Windows only for now)
    killedAfterChange = false,
    // Make this the last call so it can use the variables defined above (specifically isWindows)
    program = getNodemonArgs(),
    watched = [];



// test to see if the version of find being run supports searching by seconds (-mtime -1s -print)
var testAndStart = function() {
  if (noWatch) {
    exec('find -L /dev/null -type f -mtime -1s -print', function(error, stdout, stderr) {
      if (error) {
        if (!fs.watch) {
          util.error('\x1B[1;31mThe version of node you are using combined with the version of find being used does not support watching files. Upgrade to a newer version of node, or install a version of find that supports search by seconds.\x1B[0m');
          process.exit(1);
        } else {
          noWatch = false;
          watchFileChecker.check(function(success) {
            watchWorks = success;
            startNode();
          });
        }
      } else {
        // Find is compatible with -1s
        startNode();
      }
    });
  } else {
    watchFileChecker.check(function(success) {
      watchWorks = success;
      startNode();
    });
  }
}

// This is a fallback function if fs.watch does not work
function changedSince(time, dir, callback) {
  callback || (callback = dir);
  var changed = [],
      i = 0,
      j = 0,
      dir = dir && typeof dir !== 'function' ? [dir] : dirs,
      dlen = dir.length,
      todo = 0,
      flen = 0,
      done = function () {
        todo--;
        if (todo === 0) callback(changed);
      };

  dir.forEach(function (dir) {
    todo++;
    fs.readdir(dir, function (err, files) {
      if (err) return;

      files.forEach(function (file) {
        if (program.includeHidden == true || !program.includeHidden && file.indexOf('.') !== 0) {
          todo++;
          file = path.resolve(dir + '/' + file);
          var stat = fs.stat(file, function (err, stat) {
            if (stat) {
              if (stat.isDirectory()) {
                todo++;
                changedSince(time, file, function (subChanged) {
                  if (subChanged.length) changed = changed.concat(subChanged);
                  done();
                });
              } else if (stat.mtime > time) {
                changed.push(file);
              }
            }
            done();
          });
        }
      });
      done();
    });    
  });
}

// Attempts to see if fs.watch will work. On some platforms, it doesn't.
// See: http://nodejs.org/api/fs.html#fs_caveats  
// Sends the callback true if fs.watch will work, false if it won't
//
// Caveats:
// If there is no writable tmp directory, it will also return true, although
// a warning message will be displayed.
//
var watchFileChecker = {};
watchFileChecker.check = function(cb) {
  var tmpdir,
      seperator = '/';

  this.cb = cb;
  this.changeDetected = false;
  if (isWindows) {
    seperator = '\\';
    tmpdir = process.env.TEMP;
  } else if (process.env.TMPDIR) {
    tmpdir = process.env.TMPDIR
  } else {
    tmpdir = '/tmp';
  }
  var watchFileName = tmpdir + seperator + 'nodemonCheckFsWatch'
  var watchFile = fs.openSync(watchFileName, 'w');
  if (!watchFile) {
    util.log('\x1B[32m[nodemon] Unable to write to temp directory. If you experience problems with file reloading, ensure ' + tmpdir + ' is writable.\x1B[0m');
    cb(true);
    return;
  }  
  fs.watch(watchFileName, function(event, filename) {
    if (watchFileChecker.changeDetected) { return; }
    watchFileChecker.changeDetected = true;
    cb(true);
  });
  // This should trigger fs.watch, if it works
  fs.writeSync(watchFile, '1');
  fs.unlinkSync(watchFileName);

  setTimeout(function() { watchFileChecker.verify() }, 250);
};

// Verifies that fs.watch was not triggered and sends false to the callback
watchFileChecker.verify = function() {
  if (!this.changeDetected) {
    this.cb(false);
  }
};



function startNode() {
  util.log('\x1B[32m[nodemon] starting `' + program.options.exec + ' ' + program.args.join(' ') + '`\x1B[0m');

  child = spawn(program.options.exec, program.args);

  lastStarted = +new Date;

  child.stdout.on('data', function (data) {
    util.print(data);
  });

  child.stderr.on('data', function (data) {
    process.stderr.write(data);
  });

  child.on('exit', function (code, signal) {
    // In case we killed the app ourselves, set the signal thusly
    if (killedAfterChange) {
      killedAfterChange = false;
      signal = 'SIGUSR2';
    }
    // this is nasty, but it gives it windows support
    if (isWindows && signal == 'SIGTERM') signal = 'SIGUSR2';
    // exit the monitor, but do it gracefully
    if (signal == 'SIGUSR2') {
      // restart
      startNode();
    } else if (code === 0) { // clean exit - wait until file change to restart
      util.log('\x1B[32m[nodemon] clean exit - waiting for changes before restart\x1B[0m');
      child = null;
    } else if (program.options.exitcrash) {
      util.log('\x1B[1;31m[nodemon] app crashed\x1B[0m');
      process.exit(0);
    } else {
      util.log('\x1B[1;31m[nodemon] app crashed - waiting for file changes before starting...\x1B[0m');
      child = null;
    }
  });

  // pinched from https://github.com/DTrejo/run.js - pipes stdin to the child process - cheers DTrejo ;-)
  if (program.options.stdin) {
    process.stdin.resume();
    process.stdin.setEncoding('utf8');
    process.stdin.pipe(child.stdin);
  }

  setTimeout(startMonitor, timeout);
}

function startMonitor() {
  var changeFunction;

  if (noWatch) {
    // if native fs.watch doesn't work the way we want, we keep polling find command (mac only oddly)
    changeFunction = function (lastStarted, callback) {
      var cmds = [],
          changed = [];

      dirs.forEach(function(dir) {
        cmds.push('find -L "' + dir + '" -type f -mtime -' + ((+new Date - lastStarted)/1000|0) + 's -print');
      });

      exec(cmds.join(';'), function (error, stdout, stderr) {
        var files = stdout.split(/\n/);
        files.pop(); // remove blank line ending and split
        callback(files);
      });
    }
  } else if (watchWorks) {
    changeFunction = function (lastStarted, callback) {
      // recursive watch - watch each directory and it's subdirectories, etc, etc
      var watch = function (err, dir) {
        try {
          fs.watch(dir, { persistent: false }, function (event, filename) {
            var filepath = path.join(dir, filename);
            callback([filepath]);
          });

          fs.readdir(dir, function (err, files) {
            if (!err) {
              files = files.
                map(function (file ) { return path.join(dir, file); }).
                filter(ignoredFilter);
              files.forEach(function (file) {
                if (-1 === watched.indexOf(file)) {
                  watched.push(file);
                  fs.stat(file, function (err, stat) {
                    if (!err && stat) {
                      if (stat.isDirectory()) {
                        fs.realpath(file, watch);
                      }
                    }
                  });
                }
              });
            }
          });
        } catch (e) {
          if ('EMFILE' === e.code) {
            console.error('EMFILE: Watching too many files.');
          }
          // ignoring this directory, likely it's "My Music"
          // or some such windows fangled stuff
        }
      }
      dirs.forEach(function (dir) {
        fs.realpath(dir, watch);
      });
    }  
  } else {
    // changedSince, the fallback for when both the find method and fs.watch don't work, 
    // is not compatible with the way changeFunction works. If we have reached this point,
    // changeFunction should not be called from herein out. 
    changeFunction = function() { util.error("Nodemon error: changeFunction called when it shouldn't be.") }
  }

  // filter ignored files
  var ignoredFilter = function (file) {
    // If we are in a Windows machine
    if (isWindows) {
      // Break up the file by slashes
      var fileParts = file.split(/\\/g);

      // Remove the first piece (C:)
      fileParts.shift();

      // Join the parts together with Unix slashes
      file = '/' + fileParts.join('/');
    }
    return !reIgnoreFiles.test(file);
  };

  var isWindows = process.platform === 'win32';
  if ((noWatch || watchWorks) && !program.options.forceLegacyWatch) {
    changeFunction(lastStarted, function (files) {
      if (files.length) {
        files = files.filter(ignoredFilter);
        if (files.length) {
          if (restartTimer !== null) clearTimeout(restartTimer);
          restartTimer = setTimeout(function () {
            if (program.options.verbose) util.log('[nodemon] restarting due to changes...');
            files.forEach(function (file) {
              if (program.options.verbose) util.log('[nodemon] ' + file);
            });
            if (program.options.verbose) util.print('\n\n');

            killNode();
            
          }, restartDelay);
          return;
        }
      }

      if (noWatch) setTimeout(startMonitor, timeout);
    });
  } else {
    // Fallback for when both find and fs.watch don't work
    changedSince(lastStarted, function (files) {
      if (files.length) {
        // filter ignored files
        if (ignoreFiles.length) {
          files = files.filter(function(file) {
            return !reIgnoreFiles.test(file);
          });
        }

        if (files.length) {
          if (restartTimer !== null) clearTimeout(restartTimer);
          restartTimer = setTimeout(function () {
            if (program.options.verbose) util.log('[nodemon] restarting due to changes...');
            files.forEach(function (file) {
              if (program.options.verbose) util.log('[nodemon] ' + file);
            });
            if (program.options.verbose) util.print('\n\n');

            killNode();
            
          }, restartDelay);
          return;
        }
      }

      setTimeout(startMonitor, timeout);
    });
  }
}

function killNode() {
  if (child !== null) {
    // When using CoffeeScript under Windows, child's process is not node.exe
    // Instead coffee.cmd is launched, which launches cmd.exe, which starts node.exe as a child process
    // child.kill() would only kill cmd.exe, not node.exe
    // Therefore we use the Windows taskkill utility to kill the process and all its children (/T for tree)
    if (isWindows) {
      // For the on('exit', ...) handler above the following looks like a crash, so we set the killedAfterChange flag
      killedAfterChange = true;
      // Force kill (/F) the whole child tree (/T) by PID (/PID 123)
      exec('taskkill /pid '+child.pid+' /T /F');
    } else {
      child.kill('SIGUSR2');
    }
  } else {
    startNode();
  }
}

function addIgnoreRule(line, noEscape) {
  // remove comments and trim lines
  // this mess of replace methods is escaping "\#" to allow for emacs temp files
  if (!noEscape) {
    if (line = line.replace(reEscComments, '^^').replace(reComments, '').replace(reUnescapeComments, '#').replace(reTrim, '')) {
       ignoreFiles.push(line.replace(reEscapeChars, '\\$&').replace(reAsterisk, '.*'));
    }
  } else if (line = line.replace(reTrim, '')) {
    ignoreFiles.push(line);
  }
  reIgnoreFiles = new RegExp(ignoreFiles.join('|'));
}

function readIgnoreFile(curr, prev) {
  // unless the ignore file was actually modified, do no re-read it
  if(curr && prev && curr.mtime.valueOf() === prev.mtime.valueOf()) return;

  if (platform === 'darwin') fs.unwatchFile(ignoreFilePath);

  // Check if ignore file still exists. Vim tends to delete it before replacing with changed file
  exists(ignoreFilePath, function(exists) {
    if (program.options.verbose) util.log('[nodemon] reading ignore list');

    // ignoreFiles = ignoreFiles.concat([flag, ignoreFilePath]);
    // addIgnoreRule(flag);
    addIgnoreRule(ignoreFilePath.substring(2)); // ignore the ./ part of the filename
    fs.readFileSync(ignoreFilePath).toString().split(/\n/).forEach(function (rule, i) {
      addIgnoreRule(rule);
    });

    watchFile(ignoreFilePath, { persistent: false }, readIgnoreFile);
  });
}

// attempt to shutdown the wrapped node instance and remove
// the monitor file as nodemon exists
function cleanup() {
  child && child.kill();
  // fs.unlink(flag);
}

function getNodemonArgs() {
  var args = process.argv,
      len = args.length,
      i = 2,
      dir = process.cwd(),
      indexOfApp = -1,
      app = null;

  for (; i < len; i++) {
    if (existsSync(path.resolve(dir, args[i]))) {
      // double check we didn't use the --watch or -w opt before this arg
      if (args[i-1] && (args[i-1] == '-w' || args[i-1] == '--watch')) {
        // ignore
      } else {
        indexOfApp = i;
        break;
      }
    }
  }

  if (indexOfApp !== -1) {
    // not found, so assume we're reading the package.json and thus swallow up all the args
    // indexOfApp = len;
    app = process.argv[i];
    indexOfApp++;
  } else {
    indexOfApp = len;
  }

  var appargs = [], //process.argv.slice(indexOfApp),
      // app = appargs[0],
      nodemonargs = process.argv.slice(2, indexOfApp - (app ? 1 : 0)),
      arg,
      options = {
        delay: 1,
        watch: [],
        exec: 'node',
        verbose: true,
        js: false, // becomes the default anyway...
        includeHidden: false,
        exitcrash: false,
        forceLegacyWatch: false, // forces nodemon to use the slowest but most compatible method for watching for file changes
        stdin: true
        // args: []
      };

  // process nodemon args
  args.splice(0, 2);
  while (arg = args.shift()) {
    if (arg === '--help' || arg === '-h' || arg === '-?') {
      return help(); // exits program
    } else if (arg === '--version' || arg == '-v') {
      return version(); // also exits
    } else if (arg == '--js') {
      options.js = true;
    } else if (arg == '--quiet' || arg == '-q') {
      options.verbose = false;
    } else if (arg == '--hidden') {
      options.includeHidden = true;
    } else if (arg === '--watch' || arg === '-w') {
      options.watch.push(args.shift());
    } else if (arg === '--exitcrash') {
      options.exitcrash = true;
    } else if (arg === '--delay' || arg === '-d') {
      options.delay = parseInt(args.shift());
    } else if (arg === '--exec' || arg === '-x') {
      options.exec = args.shift();
    } else if (arg == '--legacy-watch' || arg == '-L') {
      options.forceLegacyWatch = true;
    } else if (arg === '--no-stdin' || arg === '-I') {
      options.stdin = false;
    } else { //if (arg === "--") {
      // Remaining args are node arguments
      appargs.push(arg);
    }
  }

  var program = { options: options, args: appargs, app: app };

  getAppScript(program);

  return program;
}

function getAppScript(program) {
  var hokeycokey = false;
  if (!program.args.length || program.app === null) {
    // try to get the app from the package.json
    // doing a try/catch because we can't use the path.exist callback pattern
    // or we could, but the code would get messy, so this will do exactly
    // what we're after - if the file doesn't exist, it'll throw.
    try {
      // note: this isn't nodemon's package, it's the user's cwd package
      program.app = JSON.parse(fs.readFileSync('./package.json').toString()).main;
      if (program.app === undefined) {
        // no app found to run - so give them a tip and get the feck out
        help();
      }
      program.args.unshift(program.app);
      hokeycokey = true;
    } catch (e) {}
  }

  if (!program.app) {
    program.app = program.args[0];
  }

  program.app = path.basename(program.app);
  program.ext = path.extname(program.app);

  if (program.options.exec.indexOf(' ') !== -1) {
    var execOptions = program.options.exec.split(' ');
    program.options.exec = execOptions.splice(0, 1)[0];
    program.args = execOptions.concat(program.args);
  }

  if (program.options.exec === 'node' && program.ext == '.coffee') {
    program.options.exec = 'coffee';
  }

  if (program.options.exec === 'coffee') {
    if (hokeycokey) {
      program.args.push(program.args.shift());
    }
    //coffeescript requires --nodejs --debug
    var debugIndex = program.args.indexOf('--debug');
    if (debugIndex === -1) debugIndex = program.args.indexOf('--debug-brk');
    if (debugIndex !== -1 && program.args.indexOf('--nodejs') === -1) {
      program.args.splice(debugIndex, 0, '--nodejs');
    }
    // monitor both types - TODO possibly make this an option?
    program.ext = '.coffee|.js';
    if (!program.options.exec || program.options.exec == 'node') program.options.exec = 'coffee';

    // because windows can't find 'coffee', it needs the real file 'coffee.cmd'
    if (isWindows) program.options.exec += '.cmd';
  }
}

function findStatOffset() {
  var filename = './.stat-test';
  fs.writeFile(filename, function (err) {
    if (err) return;
    fs.stat(filename, function (err, stat) {
      if (err) return;

      statOffset = stat.mtime.getTime() - new Date().getTime();
      fs.unlink(filename);
    });
  });
}

function version() {
  console.log(meta.version);
  process.exit(0);
}

function help() {
  util.print([
    '',
    ' Usage: nodemon [options] [script.js] [args]',
    '',
    ' Options:',
    '',
    '  -d, --delay n      throttle restart for "n" seconds',
    '  -w, --watch dir    watch directory "dir". use once for each',
    '                     directory to watch',
    '  -x, --exec app     execute script with "app", ie. -x "python -v"',
    '  -I, --no-stdin     don\'t try to read from stdin',
    '  -q, --quiet        minimise nodemon messages to start/stop only',
    '  --exitcrash        exit on crash, allows use of nodemon with',
    '                     daemon tools like forever.js',
    '  -L, --legacy-watch Forces node to use the most compatible',
    '                     version for watching file changes',
    '  -v, --version      current nodemon version',
    '  -h, --help         you\'re looking at it',
    '',
    ' Note: if the script is omitted, nodemon will try to ',
    ' read "main" from package.json and without a .nodemonignore,',
    ' nodemon will monitor .js and .coffee by default.',
    '',
    ' Examples:',
    '',
    '  $ nodemon server.js',
    '  $ nodemon -w ../foo server.js apparg1 apparg2',
    '  $ PORT=8000 nodemon --debug-brk server.js',
    '  $ nodemon --exec python app.py',
    '',
    ' For more details see http://github.com/remy/nodemon/',
    ''
  ].join('\n') + '\n');
  process.exit(0);
}

// this little bit of hoop jumping is because sometimes the file can't be
// touched properly, and it send nodemon in to a loop of restarting.
// this way, the .monitor file is removed entirely, and recreated with
// permissions that anyone can remove it later (i.e. if you run as root
// by accident and then try again later).
// if (path.existsSync(flag)) fs.unlinkSync(flag);
// fs.writeFileSync(flag, '.'); // requires some content https://github.com/remy/nodemon/issues/36
// fs.chmodSync(flag, '666');

// remove the flag file on exit
process.on('exit', function (code) {
  if (program.options.verbose) util.log('[nodemon] exiting');
  cleanup();
});

if (!isWindows) { // because windows borks when listening for the SIG* events
  // usual suspect: ctrl+c exit
  process.on('SIGINT', function () {
    child && child.kill('SIGINT');
    cleanup();
    process.exit(0);
  });

  process.on('SIGTERM', function () {
    cleanup();
    process.exit(0);
  });
}

// TODO on a clean exit, we could continue to monitor the directory and reboot the service

// on exception *inside* nodemon, shutdown wrapped node app
process.on('uncaughtException', function (err) {
  util.log('[nodemon] exception in nodemon killing node');
  util.error(err.stack);
  cleanup();
});


if (program.options.delay) {
  restartDelay = program.options.delay * 1000;
}

// this is the default - why am I making it a cmd line opt?
if (program.options.js) {
  addIgnoreRule('^((?!\.js|\.coffee$).)*$', true); // ignores everything except JS
}

if (program.options.watch && program.options.watch.length > 0) {
  program.options.watch.forEach(function (dir) {
    dirs.push(path.resolve(dir));
  });
} else {
  dirs.unshift(process.cwd());
}

if (!program.app) {
  help();
}

if (program.options.verbose) util.log('[nodemon] v' + meta.version);

// this was causing problems for a lot of people, so now not moving to the subdirectory
// process.chdir(path.dirname(app));
dirs.forEach(function(dir) {
  if (program.options.verbose) util.log('\x1B[32m[nodemon] watching: ' + dir + '\x1B[0m');
});

// findStatOffset();

exists(ignoreFilePath, function (exist) {
  // watch it: "exist" not to be confused with "exists"....
  if (!exist) {
    // try the old format
    exists(oldIgnoreFilePath, function (exist) {
      if (exist) {
        if (program.options.verbose) util.log('[nodemon] detected old style .nodemonignore');
        ignoreFilePath = oldIgnoreFilePath;
      } else {
        // don't create the ignorefile, just ignore the flag & JS
        // addIgnoreRule(flag);
        var ext = program.ext.replace(/\./g, '\\.');
        if (ext) {
          addIgnoreRule('^((?!' + ext + '$).)*$', true);
        } else {
          addIgnoreRule('^((?!\.js|\.coffee$).)*$', true); // ignores everything except JS
        }
      }
    });
  } else {
    readIgnoreFile();
  }
});

testAndStart();
