'use strict';

const rulesDirs = ['tools/eslint-rules'];
// This is the maximum number of files to be linted per worker at any given time
const maxWorkload = 40;

const cluster = require('cluster');
const path = require('path');
const fs = require('fs');
const totalCPUs = require('os').cpus().length;

const CLIEngine = require('./eslint').CLIEngine;
const glob = require('./eslint/node_modules/glob');

const cwd = process.cwd();
const cli = new CLIEngine({
  rulePaths: rulesDirs
});

if (cluster.isMaster) {
  var numCPUs = 1;
  const paths = [];
  var files = null;
  var totalPaths = 0;
  var failures = 0;
  var successes = 0;
  var lastLineLen = 0;
  var curPath = 'Starting ...';
  var showProgress = true;
  const globOptions = {
    nodir: true
  };
  const workerConfig = {};
  var startTime;
  var formatter;
  var outFn;
  var fd;
  var i;

  // Check if spreading work among all cores/cpus
  if (process.argv.indexOf('-J') !== -1)
    numCPUs = totalCPUs;

  // Check if spreading work among an explicit number of cores/cpus
  i = process.argv.indexOf('-j');
  if (i !== -1) {
    if (!process.argv[i + 1])
      throw new Error('Missing parallel job count');
    numCPUs = parseInt(process.argv[i + 1], 10);
    if (!isFinite(numCPUs) || numCPUs <= 0)
      throw new Error('Bad parallel job count');
  }

  // Check for custom JSLint report formatter
  i = process.argv.indexOf('-f');
  if (i !== -1) {
    if (!process.argv[i + 1])
      throw new Error('Missing format name');
    const format = process.argv[i + 1];
    formatter = cli.getFormatter(format);
    if (!formatter)
      throw new Error('Invalid format name');
    // Automatically disable progress display
    showProgress = false;
    // Tell worker to send all results, not just linter errors
    workerConfig.sendAll = true;
  } else {
    // Use default formatter
    formatter = cli.getFormatter();
  }

  // Check if outputting JSLint report to a file instead of stdout
  i = process.argv.indexOf('-o');
  if (i !== -1) {
    if (!process.argv[i + 1])
      throw new Error('Missing output filename');
    var outPath = process.argv[i + 1];
    if (!path.isAbsolute(outPath))
      outPath = path.join(cwd, outPath);
    fd = fs.openSync(outPath, 'w');
    outFn = function(str) {
      fs.writeSync(fd, str, 'utf8');
    };
    process.on('exit', function() {
      fs.closeSync(fd);
    });
  } else {
    outFn = function(str) {
      process.stdout.write(str);
    };
  }

  // Process the rest of the arguments as paths to lint, ignoring any unknown
  // flags
  for (i = 2; i < process.argv.length; ++i) {
    if (process.argv[i][0] === '-') {
      switch (process.argv[i]) {
        case '-f': // Skip format name
        case '-o': // Skip filename
        case '-j': // Skip parallel job count number
          ++i;
          break;
      }
      continue;
    }
    paths.push(process.argv[i]);
  }

  if (paths.length === 0)
    return;
  totalPaths = paths.length;

  if (showProgress) {
    // Start the progress display update timer when the first worker is ready
    cluster.once('online', function(worker) {
      startTime = process.hrtime();
      setInterval(printProgress, 1000).unref();
      printProgress();
    });
  }

  cluster.on('online', function(worker) {
    // Configure worker and give it some initial work to do
    worker.send(workerConfig);
    sendWork(worker);
  });

  process.on('exit', function() {
    if (showProgress) {
      curPath = 'Done';
      printProgress();
      outFn('\r\n');
    }
    process.exit(failures ? 1 : 0);
  });

  for (i = 0; i < numCPUs; ++i)
    cluster.fork().on('message', onWorkerMessage);

  function onWorkerMessage(results) {
    if (typeof results !== 'number') {
      // The worker sent us results that are not all successes
      if (!workerConfig.sendAll)
        failures += results.length;
      outFn(formatter(results) + '\r\n');
      printProgress();
    } else {
      successes += results;
    }
    // Try to give the worker more work to do
    sendWork(this);
  }

  function sendWork(worker) {
    if (!files || !files.length) {
      // We either just started or we have no more files to lint for the current
      // path. Find the next path that has some files to be linted.
      while (paths.length) {
        var dir = paths.shift();
        curPath = dir;
        if (dir.indexOf('/') > 0)
          dir = path.join(cwd, dir);
        const patterns = cli.resolveFileGlobPatterns([dir]);
        dir = path.resolve(patterns[0]);
        files = glob.sync(dir, globOptions);
        if (files.length)
          break;
      }
      if ((!files || !files.length) && !paths.length) {
        // We exhausted all input paths and thus have nothing left to do, so end
        // the worker
        return worker.disconnect();
      }
    }
    // Give the worker an equal portion of the work left for the current path,
    // but not exceeding a maximum file count in order to help keep *all*
    // workers busy most of the time instead of only a minority doing most of
    // the work.
    const sliceLen = Math.min(maxWorkload, Math.ceil(files.length / numCPUs));
    var slice;
    if (sliceLen === files.length) {
      // Micro-ptimization to avoid splicing to an empty array
      slice = files;
      files = null;
    } else {
      slice = files.splice(0, sliceLen);
    }
    worker.send(slice);
  }

  function printProgress() {
    if (!showProgress)
      return;

    // Clear line
    outFn('\r' + ' '.repeat(lastLineLen) + '\r');

    // Calculate and format the data for displaying
    const elapsed = process.hrtime(startTime)[0];
    const mins = padString(Math.floor(elapsed / 60), 2, '0');
    const secs = padString(elapsed % 60, 2, '0');
    const passed = padString(successes, 6, ' ');
    const failed = padString(failures, 6, ' ');
    var pct = Math.ceil(((totalPaths - paths.length) / totalPaths) * 100);
    pct = padString(pct, 3, ' ');

    var line = `[${mins}:${secs}|%${pct}|+${passed}|-${failed}]: ${curPath}`;

    // Truncate line like cpplint does in case it gets too long
    if (line.length > 75)
      line = line.slice(0, 75) + '...';

    // Store the line length so we know how much to erase the next time around
    lastLineLen = line.length;

    outFn(line);
  }

  function padString(str, len, chr) {
    str = '' + str;
    if (str.length >= len)
      return str;
    return chr.repeat(len - str.length) + str;
  }
} else {
  // Worker

  var config = {};
  process.on('message', function(files) {
    if (files instanceof Array) {
      // Lint some files
      const report = cli.executeOnFiles(files);
      if (config.sendAll) {
        // Return both success and error results

        const results = report.results;
        // Silence warnings for files with no errors while keeping the "ok"
        // status
        if (report.warningCount > 0) {
          for (var i = 0; i < results.length; ++i) {
            const result = results[i];
            if (result.errorCount === 0 && result.warningCount > 0) {
              result.warningCount = 0;
              result.messages = [];
            }
          }
        }
        process.send(results);
      } else if (report.errorCount === 0) {
        // No errors, return number of successful lint operations
        process.send(files.length);
      } else {
        // One or more errors, return the error results only
        process.send(CLIEngine.getErrorResults(report.results));
      }
    } else if (typeof files === 'object') {
      // The master process is actually sending us our configuration and not a
      // list of files to lint
      config = files;
    }
  });
}
