#!/usr/bin/env node
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

var fs = require('fs');
var path = require('path');

var flowRemoveTypes = require('./index');

var usage = 'Usage: flow-remove-types [options] [sources] \n' +

'\nOptions:\n' +
'  -h, --help        Show this message\n' +
'  -v, --version     Prints the current version of flow-remove-types\n' +
'  -i, --ignore      Paths to ignore, Regular Expression\n' +
'  -x, --extensions  File extensions to transform\n' +
'  -o, --out-file    The file path to write transformed file to\n' +
'  -d, --out-dir     The directory path to write transformed files within\n' +
'  -a, --all         Transform all files, not just those with a @flow comment\n' +
'  -p, --pretty      Remove flow types without replacing with spaces, \n' +
'                    producing prettier output but may require using source maps\n' +
'      --ignore-uninitialized-fields\n' +
'                    Removes uninitialized class fields (`foo;`, `foo: string;`)\n' +
'                    completely rather than only removing the type. THIS IS NOT\n' +
'                    SPEC COMPLIANT! Instead, use `declare foo: string;` for\n' +
'                    type-only fields.\n' +
'  -m, --sourcemaps  Also output source map files. Optionally pass "inline"\n' +
'  -q, --quiet       Does not produce any output concerning successful progress.\n' +

'\nExamples:\n' +

'\nTransform one file:\n' +
'  flow-remove-types --out-file output.js input.js\n' +

'\nTransform many files:\n' +
'  flow-remove-types --out-dir out/ input1.js input2.js\n' +

'\nTransform files in directory:\n' +
'  flow-remove-types --out-dir out/ indir/\n' +

'\nTransform files with source maps:\n' +
'  flow-remove-types --out-dir out/ indir/ --sourcemaps\n' +

'\nTransform files with inline source maps:\n' +
'  flow-remove-types --out-dir out/ indir/ --sourcemaps inline\n' +

'\nTransform stdin:\n' +
'  cat input.js | flow-remove-types > output.js\n';

var _memo = {};

function mkdirp(dirpath) {
  if (_memo[dirpath]) {
    return;
  }
  _memo[dirpath] = true;
  try {
    fs.mkdirSync(dirpath);
  } catch (err) {
    if (err.code === 'ENOENT') {
      mkdirp(path.dirname(dirpath));
      fs.mkdirSync(dirpath);
    } else {
      try {
        stat = fs.statSync(dirpath);
      } catch (ignored) {
        throw err;
      }
      if (!stat.isDirectory()) {
        throw err;
      }
    }
  }
}

// Collect arguments
var ignore = /node_modules/;
var extensions = [ '.js', '.mjs', '.cjs', '.jsx', '.flow', '.es6' ];
var outDir;
var outFile;
var all;
var pretty;
var ignoreUninitializedFields;
var sourceMaps;
var inlineSourceMaps;
var quiet;
var sources = [];
var i = 2;
while (i < process.argv.length) {
  var arg = process.argv[i++];
  if (arg === '-h' || arg === '--help') {
    process.stdout.write(usage);
    process.exit(0);
  } else if (arg === '-v' || arg === '--version') {
    process.stdout.write('v' + require('./package').version);
    process.exit(0);
  } else if (arg === '-i' || arg === '--ignore') {
    ignore = new RegExp(process.argv[i++]);
  } else if (arg === '-x' || arg === '--extensions') {
    extensions = process.argv[i++].split(',');
  } else if (arg === '-o' || arg === '--out-file') {
    outFile = process.argv[i++];
  } else if (arg === '-d' || arg === '--out-dir') {
    outDir = process.argv[i++];
  } else if (arg === '-a' || arg === '--all') {
    all = true;
  } else if (arg === '-p' || arg === '--pretty') {
    pretty = true;
  } else if (arg === '--ignore-uninitialized-fields') {
    ignoreUninitializedFields = true;
  } else if (arg === '-m' || arg === '--sourcemaps') {
    sourceMaps = true;
    if (process.argv[i] === 'inline') {
      inlineSourceMaps = true;
      i++;
    }
  } else if (arg === '-q' || arg === '--quiet') {
    quiet = true;
  } else {
    sources.push(arg);
  }
}

function info(msg) {
  if (!quiet) {
    process.stderr.write(msg);
  }
}

function error(msg) {
  process.stderr.write('\n\033[31m  ' + msg + '\033[0m\n\n');
  process.exit(1);
}

// Validate arguments
if (outDir && outFile) {
  error('Only specify one of --out-dir or --out-file');
}

if (outDir && sources.length === 0) {
  error('Must specify files when providing --out-dir');
}

if (!outDir && !outFile && sourceMaps && !inlineSourceMaps) {
  error('Must specify either an output path or inline source maps');
}

// Ensure all sources exist
for (var i = 0; i < sources.length; i++) {
  try {
    var stat = fs.lstatSync(sources[i]);
    if (sources.length > 1 && !stat.isFile()) {
      error('Source "' + sources[i] + '" is not a file.');
    }
  } catch (err) {
    error('Source "' + sources[i] + '" does not exist.');
  }
}

// Process stdin if no sources were provided
if (sources.length === 0) {
  var content = '';
  process.stdin.setEncoding('utf-8');
  process.stdin.resume();
  process.stdin.on('data', function (str) { content += str; });
  process.stdin.on('end', function () {
    transformAndOutput(content, outFile);
  });
  return;
}

var isDirSource = sources.length === 1 && fs.statSync(sources[0]).isDirectory();

if ((sources.length > 1 || isDirSource) && !outDir) {
  error('Multiple files require providing --out-dir');
}

// Process multiple files
for (var i = 0; i < sources.length; i++) {
  var source = sources[i];
  var stat = fs.lstatSync(source);
  if (stat.isDirectory()) {
    var files = fs.readdirSync(source);
    for (var j = 0; j < files.length; j++) {
      var subSource = path.join(source, files[j]);
      if (!ignore || !ignore.test(subSource)) {
        sources.push(subSource);
      }
    }
  } else if (stat.isFile() && extensions.indexOf(path.extname(source)) !== -1) {
    if (outDir) {
      outFile = path.join(outDir, isDirSource ? path.relative(sources[0], source) : source);
      mkdirp(path.dirname(outFile));
    }
    var content = fs.readFileSync(source, 'utf8');
    transformAndOutput(content, outFile, source);
  }
}

function transformAndOutput(content, outFile, source) {
  var fileName = source || '<stdin>';
  var result = transformSource(content, fileName);
  var code = result.toString();

  if (sourceMaps) {
    var map = result.generateMap();
    delete map.file;
    map.sources[0] = fileName;

    if (source) {
      delete map.sourcesContent;
      if (outFile) {
        map.sources[0] = path.join(path.relative(path.dirname(outFile), path.dirname(source)), path.basename(source));
      }
    } else {
      map.sourcesContent = [content];
    }

    code += '\n//# sourceMappingURL=' + (inlineSourceMaps ?
      'data:application/json;charset=utf-8;base64,' + btoa(JSON.stringify(map)) :
      path.basename(outFile) + '.map'
    ) + '\n';
  }

  if (outFile) {
    fs.writeFileSync(outFile, code);
    info(fileName + '\n \u21B3 \033[32m' + outFile + '\033[0m\n');
    if (sourceMaps && !inlineSourceMaps) {
      var mapOutFile = outFile + '.map';
      fs.writeFileSync(mapOutFile, JSON.stringify(map) + '\n');
      info('\033[2m \u21B3 \033[32m' + mapOutFile + '\033[0m\n');
    }
  } else {
    process.stdout.write(code);
  }
}

function btoa(str) {
  // There are 5.x versions of Node that have `Buffer.from` but don't have the
  // `Buffer.from(string)` overload, so check for other new methods to be sure.
  return (Buffer.from && Buffer.alloc && Buffer.allocUnsafe
    ? Buffer.from(str)
    : new Buffer(str)
  ).toString('base64');
}

function transformSource(content, filepath) {
  try {
    return flowRemoveTypes(content, {
      all: all,
      pretty: pretty,
      ignoreUninitializedFields: ignoreUninitializedFields,
    });
  } catch (error) {
    if (error.loc) {
      var line = error.loc.line - 1;
      var col = error.loc.column;
      var text = content.split(/\r\n?|\n|\u2028|\u2029/)[line];
      process.stderr.write(
        filepath + '\n' +
        ' \u21B3 \033[31mSyntax Error: ' + error.message + '\033[0m\n' +
        '   \033[90m' + line + ':  \033[0m' +
        text.slice(0, col) + '\033[7;31m' + text[col] + '\033[0m' + text.slice(col + 1) + '\n'
      );
      process.exit(1);
    }
    throw error;
  }
}
