// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Description of this file.
 */

'use strict';

const assert = require('assert');
const fs = require('fs');
const path = require('path');

const program = require('commander');

const corpus = require('./corpus.js');
const differentialScriptMutator = require('./differential_script_mutator.js');
const random = require('./random.js');
const scriptMutator = require('./script_mutator.js');
const sourceHelpers = require('./source_helpers.js');

// Maximum number of test inputs to use for one fuzz test.
const MAX_TEST_INPUTS_PER_TEST = 10;

// Base implementations for default or differential fuzzing.
const SCRIPT_MUTATORS = {
  default: scriptMutator.ScriptMutator,
  foozzie: differentialScriptMutator.DifferentialScriptMutator,
};

function getRandomInputs(primaryCorpus, secondaryCorpora, count) {
  count = random.randInt(2, count);

  // Choose 40%-80% of inputs from primary corpus.
  const primaryCount = Math.floor(random.uniform(0.4, 0.8) * count);
  count -= primaryCount;

  let inputs = primaryCorpus.getRandomTestcases(primaryCount);

  // Split remainder equally between the secondary corpora.
  const secondaryCount = Math.floor(count / secondaryCorpora.length);

  for (let i = 0; i < secondaryCorpora.length; i++) {
    let currentCount = secondaryCount;
    if (i == secondaryCorpora.length - 1) {
      // Last one takes the remainder.
      currentCount = count;
    }

    count -= currentCount;
    if (currentCount) {
      inputs = inputs.concat(
          secondaryCorpora[i].getRandomTestcases(currentCount));
    }
  }

  return random.shuffle(inputs);
}

function collect(value, total) {
  total.push(value);
  return total;
}

function overrideSettings(settings, settingOverrides) {
  for (const setting of settingOverrides) {
    const parts = setting.split('=');
    settings[parts[0]] = parseFloat(parts[1]);
  }
}

function* randomInputGen(engine) {
  const inputDir = path.resolve(program.input_dir);

  const v8Corpus = new corpus.Corpus(inputDir, 'v8');
  const chakraCorpus = new corpus.Corpus(inputDir, 'chakra');
  const spiderMonkeyCorpus = new corpus.Corpus(inputDir, 'spidermonkey');
  const jscCorpus = new corpus.Corpus(inputDir, 'WebKit/JSTests');
  const crashTestsCorpus = new corpus.Corpus(inputDir, 'CrashTests');

  for (let i = 0; i < program.no_of_files; i++) {
    let inputs;
    if (engine === 'V8') {
      inputs = getRandomInputs(
          v8Corpus,
          random.shuffle([chakraCorpus, spiderMonkeyCorpus, jscCorpus,
                          crashTestsCorpus, v8Corpus]),
          MAX_TEST_INPUTS_PER_TEST);
    } else if (engine == 'chakra') {
      inputs = getRandomInputs(
          chakraCorpus,
          random.shuffle([v8Corpus, spiderMonkeyCorpus, jscCorpus,
                          crashTestsCorpus]),
          MAX_TEST_INPUTS_PER_TEST);
    } else if (engine == 'spidermonkey') {
      inputs = getRandomInputs(
          spiderMonkeyCorpus,
          random.shuffle([v8Corpus, chakraCorpus, jscCorpus,
                          crashTestsCorpus]),
          MAX_TEST_INPUTS_PER_TEST);
    } else {
      inputs = getRandomInputs(
          jscCorpus,
          random.shuffle([chakraCorpus, spiderMonkeyCorpus, v8Corpus,
                          crashTestsCorpus]),
          MAX_TEST_INPUTS_PER_TEST);
    }

    if (inputs.length > 0) {
      yield inputs;
    }
  }
}

function* corpusInputGen() {
  const inputCorpus = new corpus.Corpus(
      path.resolve(program.input_dir),
      program.mutate_corpus,
      program.extra_strict);
  for (const input of inputCorpus.getAllTestcases()) {
    yield [input];
  }
}

function* enumerate(iterable) {
  let i = 0;
  for (const value of iterable) {
    yield [i, value];
    i++;
  }
}

function main() {
  Error.stackTraceLimit = Infinity;

  program
    .version('0.0.1')
    .option('-i, --input_dir <path>', 'Input directory.')
    .option('-o, --output_dir <path>', 'Output directory.')
    .option('-n, --no_of_files <n>', 'Output directory.', parseInt)
    .option('-c, --mutate_corpus <name>', 'Mutate single files in a corpus.')
    .option('-e, --extra_strict', 'Additionally parse files in strict mode.')
    .option('-m, --mutate <path>', 'Mutate a file and output results.')
    .option('-s, --setting [setting]', 'Settings overrides.', collect, [])
    .option('-v, --verbose', 'More verbose printing.')
    .option('-z, --zero_settings', 'Zero all settings.')
    .parse(process.argv);

  const settings = scriptMutator.defaultSettings();
  if (program.zero_settings) {
    for (const key of Object.keys(settings)) {
      settings[key] = 0.0;
    }
  }

  if (program.setting.length > 0) {
    overrideSettings(settings, program.setting);
  }

  let app_name = process.env.APP_NAME;
  if (app_name && app_name.endsWith('.exe')) {
    app_name = app_name.substr(0, app_name.length - 4);
  }

  if (app_name === 'd8' || app_name === 'v8_foozzie.py') {
    // V8 supports running the raw d8 executable or the differential fuzzing
    // harness 'foozzie'.
    settings.engine = 'V8';
  } else if (app_name === 'ch') {
    settings.engine = 'chakra';
  } else if (app_name === 'js') {
    settings.engine = 'spidermonkey';
  } else if (app_name === 'jsc') {
    settings.engine = 'jsc';
  } else {
    console.log('ERROR: Invalid APP_NAME');
    process.exit(1);
  }

  const mode = process.env.FUZZ_MODE || 'default';
  assert(mode in SCRIPT_MUTATORS, `Unknown mode ${mode}`);
  const mutator = new SCRIPT_MUTATORS[mode](settings);

  if (program.mutate) {
    const absPath = path.resolve(program.mutate);
    const baseDir = path.dirname(absPath);
    const fileName = path.basename(absPath);
    const input = sourceHelpers.loadSource(
        baseDir, fileName, program.extra_strict);
    const mutated = mutator.mutateMultiple([input]);
    console.log(mutated.code);
    return;
  }

  let inputGen;

  if (program.mutate_corpus) {
    inputGen = corpusInputGen();
  } else {
    inputGen = randomInputGen(settings.engine);
  }

  for (const [i, inputs] of enumerate(inputGen)) {
    const outputPath = path.join(program.output_dir, 'fuzz-' + i + '.js');

    const start = Date.now();
    const paths = inputs.map(input => input.relPath);

    try {
      const mutated = mutator.mutateMultiple(inputs);
      fs.writeFileSync(outputPath, mutated.code);

      if (settings.engine === 'V8' && mutated.flags && mutated.flags.length > 0) {
        const flagsPath = path.join(program.output_dir, 'flags-' + i + '.js');
        fs.writeFileSync(flagsPath, mutated.flags.join(' '));
      }
    } catch (e) {
      if (e.message.startsWith('ENOSPC')) {
        console.log('ERROR: No space left. Bailing out...');
        console.log(e);
        return;
      }
      console.log(`ERROR: Exception during mutate: ${paths}`);
      console.log(e);
      continue;
    } finally {
      if (program.verbose) {
        const duration = Date.now() - start;
        console.log(`Mutating ${paths} took ${duration} ms.`);
      }
    }
    if ((i + 1)  % 10 == 0) {
      console.log('Up to ', i + 1);
    }
  }
}

main();