#!/usr/bin/env node

var fork = require('child_process').fork;
var fs = require('fs');
var path = require('path');
var util = require('util');
var open = require('opener');
var yargs = require('yargs');
var whichSync = require('which').sync;
var inspector = require('..');

var WIN_CMD_LINK_MATCHER = /node  "%~dp0\\(.*?)"/;

var argvOptions = {
  'debug-brk': {
    alias: 'b',
    default: true,
    description: 'Break on the first line (`node --debug-brk`)'
  },
  'web-port': {
    alias: ['p', 'port'],
    type: 'number',
    description: 'Node Inspector port (`node-inspector --web-port={port}`)'
  },
  'debug-port': {
    alias: 'd',
    type: 'number',
    description: 'Node/V8 debugger port (`node --debug={port}`)'
  },
  nodejs: {
    type: 'string',
    description: 'Pass NodeJS options to debugged process (`node --option={value}`)\n' +
                  'Usage example:  node-debug --nodejs --harmony --nodejs --random_seed=2 app'
  },
  cli: {
    alias: 'c',
    type: 'boolean',
    description: 'CLI mode, do not open browser.'
  },
  version: {
    alias: 'v',
    type: 'boolean',
    description: 'Print Node Inspector\'s version.'
  },
  help: {
    alias: 'h',
    type: 'boolean',
    description: 'Show this help.'
  }
};

var argvParser = createYargs();

module.exports = main;
module.exports.parseArgs = parseArgs;

if (require.main == module)
  main();

//-- Implementation --

var config;

/**
 * By default:
 *
 * 1. Runs node-inspector.
 * 2. Runs the supplied script in debug mode
 * 3. Opens the user's browser, pointing it at the inspector.
 *
 * NOTE: Finishes with a call to process.exit() when running without arguments
 * or when an error occured.
 */
function main() {
  config = parseArgs(process.argv);
  if (config.options.help) {
    argvParser.showHelp(console.log);
    console.log('The [script] argument is resolved relative to the current working\n' +
      'directory. If no such file exists, then env.PATH is searched.\n');
    console.log('The default mode is to break on the first line of the script, to run\n' +
      'immediately on start use `--no-debug-brk` or press the Resume button.\n');
    console.log('When there is no script specified, the module in the current working\n' +
      'directory is loaded in the REPL session as `m`. This allows you to call\n' +
      'and debug arbitrary functions exported by the current module.\n');
    process.exit();
  }

  if (config.options.version) {
    console.log('v' + require('../package.json').version);
    process.exit();
  }

  startInspector(function(err) {
    if (err) {
      console.error(formatNodeInspectorError(err));
      process.exit(1);
    }

    startDebuggedProcess(function(err) {
      if (err) {
        console.error(
          'Cannot start %s: %s',
          config.subproc.script,
          err.message || err
        );
        process.exit(2);
      }

      openBrowserAndPrintInfo();
    });
  });
}

function parseArgs(argv) {
  argv = argv.slice(2);

  //Preparse --nodejs options
  var nodejsArgs = [];
  var nodejsIndex = argv.indexOf('--nodejs');
  while (nodejsIndex !== -1) {
    var nodejsArg = argv.splice(nodejsIndex, 2)[1];
    if (nodejsArg !== undefined) {
      nodejsArgs.push(nodejsArg);
    }
    nodejsIndex = argv.indexOf('--nodejs');
  }

  var options = argvParser.parse(argv);
  var script = options._[0];
  var printScript = true;

  var subprocArgs;

  if (script) {
    // We want to pass along subarguments, but re-parse our arguments.
    subprocArgs = argv.splice(argv.indexOf(script) + 1);
  } else {
    script = require.resolve('./run-repl');
    subprocArgs = [];
    printScript = false;
    process.env.CMD = process.env.CMD || process.argv[1];
  }

  options = argvParser.parse(argv);

  var subprocPort = options['debug-port'] || 5858;
  var subprocExecArgs = ['--debug=' + subprocPort].concat(nodejsArgs);

  if (options['debug-brk']) {
    subprocExecArgs.push('--debug-brk');
  }

  var inspectorPort = options['web-port'] || 8080;
  var inspectorArgs = extractPassThroughArgs(options, argvOptions)
    .concat(['--web-port=' + inspectorPort]);

  return {
    printScript: printScript,
    options: options,
    subproc: {
      script: script,
      args: subprocArgs,
      execArgs:  subprocExecArgs,
      debugPort: subprocPort
    },
    inspector: {
      port: inspectorPort,
      args: inspectorArgs
    }
  };
}

function createYargs() {
  var y = yargs
    .options(argvOptions)
    .usage('Usage:\n' +
      '    $0 [node-inspector-options] [options] script [script-arguments]');
  y.$0 = getCmd();
  return y;
}

function getCmd() {
  return process.env.CMD || path.basename(process.argv[1]);
}

function extractPassThroughArgs(options, argvOptions) {
  var result = [];
  var optionsToSkip = { _: true, $0: true };

  // Skip options handled by node-debug
  Object.keys(argvOptions).forEach(function(key) {
    optionsToSkip[key] = true;
    var alias = argvOptions[key].alias;
    if (Array.isArray(alias)) {
      alias.forEach(function(opt) { optionsToSkip[opt] = true; });
    } else if (alias) {
      optionsToSkip[alias] = true;
    }
  });

  // Filter options not handled by node-debug
  Object.keys(options).forEach(function(key) {
    //Filter options handled by node-debug
    if (optionsToSkip[key]) return;
    //Filter camelKey options created by yargs
    if (/[A-Z]/.test(key)) return;
    
    var value = options[key];
    if (value === undefined) return;
    if (value === true) {
      result.push('--' + key);
    } else if (value === false) {
      result.push('--no-' + key);
    } else {
      result.push('--' + key);
      result.push(value);
    }
  });

  return result;
}

function startInspector(callback) {
  var inspectorProcess = fork(
    require.resolve('./inspector'),
    config.inspector.args,
    { silent: true }
  );

  inspectorProcess.once('message', function(msg) {
    switch (msg.event) {
    case 'SERVER.LISTENING':
      return callback(null, msg.address);
    case 'SERVER.ERROR':
      return callback(msg.error);
    default:
      console.warn('Unknown Node Inspector event: %s', msg.event);
      return callback(
        null,
        {
          address: 'localhost',
          port: config.inspector.port
        }
      );
    }
  });

  process.on('exit', function() {
    inspectorProcess.kill();
  });
}

function formatNodeInspectorError(err) {
  var reason = err.message || err.code || err;
  if (err.code === 'EADDRINUSE') {
    reason += '\nThere is another process already listening at 0.0.0.0:' +
      config.inspector.port + '.\n' +
      'Run `' + getCmd() + ' -p {port}` to use a different port.';
  }

  return util.format('Cannot start Node Inspector:', reason);
}

function startDebuggedProcess(callback) {
  var script = path.resolve(process.cwd(), config.subproc.script);
  if (!fs.existsSync(script)) {
    try {
      script = whichSync(config.subproc.script);
      script = checkWinCmdFiles(script);
    } catch (err) {
      return  callback(err);
    }
  }

  var debuggedProcess = fork(
    script,
    config.subproc.args,
    {
      execArgv: config.subproc.execArgs
    }
  );
  debuggedProcess.on('exit', function() { process.exit(); });
  callback();
}

function checkWinCmdFiles(script) {
  if (process.platform == 'win32' && path.extname(script).toLowerCase() == '.cmd') {
    var cmdContent = '' + fs.readFileSync(script);
    var link = (WIN_CMD_LINK_MATCHER.exec(cmdContent) || [])[1];
    
    if (link) script = path.resolve(path.dirname(script), link);
  }
  return script;
}

function openBrowserAndPrintInfo() {
  var url = inspector.buildInspectorUrl(
    'localhost',
    config.inspector.port,
    config.subproc.debugPort
  );

  if (!config.options.cli) {
    open(url);
  }

  console.log('Node Inspector is now available from %s', url);
  if (config.printScript)
    console.log('Debugging `%s`\n', config.subproc.script);
}
