// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library analyzer.src.command_line.arguments;

import 'dart:collection';

import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/context/builder.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:args/args.dart';
import 'package:path/path.dart';

const String analysisOptionsFileOption = 'options';
const String bazelAnalysisOptionsPath =
    'package:dart.analysis_options/default.yaml';
const String declarationCastsFlag = 'declaration-casts';
const String defineVariableOption = 'D';
const String enableInitializingFormalAccessFlag = 'initializing-formal-access';
const String enableSuperMixinFlag = 'supermixin';
const String flutterAnalysisOptionsPath =
    'package:flutter/analysis_options_user.yaml';
const String ignoreUnrecognizedFlagsFlag = 'ignore-unrecognized-flags';
const String implicitCastsFlag = 'implicit-casts';
const String lintsFlag = 'lints';
const String noImplicitDynamicFlag = 'no-implicit-dynamic';
const String packageRootOption = 'package-root';
const String packagesOption = 'packages';
const String sdkPathOption = 'dart-sdk';

const String sdkSummaryPathOption = 'dart-sdk-summary';

/**
 * Update [options] with the value of each analysis option command line flag.
 */
void applyAnalysisOptionFlags(AnalysisOptionsImpl options, ArgResults args,
    {void verbosePrint(String text)}) {
  void verbose(String text) {
    if (verbosePrint != null) {
      verbosePrint('Analysis options: $text');
    }
  }

  if (args.wasParsed(enableSuperMixinFlag)) {
    options.enableSuperMixins = args[enableSuperMixinFlag];
    verbose('$enableSuperMixinFlag = ${options.enableSuperMixins}');
  }
  if (args.wasParsed(implicitCastsFlag)) {
    options.implicitCasts = args[implicitCastsFlag];
    verbose('$implicitCastsFlag = ${options.implicitCasts}');
  }
  if (args.wasParsed(declarationCastsFlag)) {
    options.declarationCasts = args[declarationCastsFlag];
    verbose('$declarationCastsFlag = ${options.declarationCasts}');
  } else if (args.wasParsed(implicitCastsFlag)) {
    options.declarationCasts = args[implicitCastsFlag];
    verbose('$declarationCastsFlag = ${options.declarationCasts}');
  }
  if (args.wasParsed(noImplicitDynamicFlag)) {
    options.implicitDynamic = !args[noImplicitDynamicFlag];
    verbose('$noImplicitDynamicFlag = ${options.implicitDynamic}');
  }
  try {
    if (args.wasParsed(lintsFlag)) {
      options.lint = args[lintsFlag];
      verbose('$lintsFlag = ${options.lint}');
    }
  } on ArgumentError {
    // lints were not defined - ignore and fall through
  }
}

/**
 * Use the given [resourceProvider], [contentCache] and command-line [args] to
 * create a context builder.
 */
ContextBuilderOptions createContextBuilderOptions(ArgResults args,
    {bool trackCacheDependencies}) {
  ContextBuilderOptions builderOptions = new ContextBuilderOptions();
  builderOptions.argResults = args;
  //
  // File locations.
  //
  builderOptions.dartSdkSummaryPath = args[sdkSummaryPathOption];
  builderOptions.defaultAnalysisOptionsFilePath =
      args[analysisOptionsFileOption];
  builderOptions.defaultPackageFilePath = args[packagesOption];
  builderOptions.defaultPackagesDirectoryPath = args[packageRootOption];
  //
  // Analysis options.
  //
  AnalysisOptionsImpl defaultOptions = new AnalysisOptionsImpl();
  applyAnalysisOptionFlags(defaultOptions, args);
  if (trackCacheDependencies != null) {
    defaultOptions.trackCacheDependencies = trackCacheDependencies;
  }
  builderOptions.defaultOptions = defaultOptions;
  //
  // Declared variables.
  //
  Map<String, String> declaredVariables = <String, String>{};
  List<String> variables = (args[defineVariableOption] as List).cast<String>();
  for (String variable in variables) {
    int index = variable.indexOf('=');
    if (index < 0) {
      // TODO (brianwilkerson) Decide the semantics we want in this case.
      // The VM prints "No value given to -D option", then tries to load '-Dfoo'
      // as a file and dies. Unless there was nothing after the '-D', in which
      // case it prints the warning and ignores the option.
    } else {
      String name = variable.substring(0, index);
      if (name.isNotEmpty) {
        // TODO (brianwilkerson) Decide the semantics we want in the case where
        // there is no name. If there is no name, the VM tries to load a file
        // named '-D' and dies.
        declaredVariables[name] = variable.substring(index + 1);
      }
    }
  }
  builderOptions.declaredVariables = declaredVariables;

  return builderOptions;
}

/**
 * Use the given [resourceProvider] and command-line [args] to create a Dart SDK
 * manager. The manager will use summary information if [useSummaries] is `true`
 * and if the summary information exists.
 */
DartSdkManager createDartSdkManager(
    ResourceProvider resourceProvider, bool useSummaries, ArgResults args) {
  String sdkPath = args[sdkPathOption];

  bool canUseSummaries = useSummaries &&
      args.rest.every((String sourcePath) {
        sourcePath = context.absolute(sourcePath);
        sourcePath = context.normalize(sourcePath);
        return !context.isWithin(sdkPath, sourcePath);
      });
  return new DartSdkManager(
      sdkPath ?? FolderBasedDartSdk.defaultSdkDirectory(resourceProvider),
      canUseSummaries);
}

/**
 * Add the standard flags and options to the given [parser]. The standard flags
 * are those that are typically used to control the way in which the code is
 * analyzed.
 *
 * TODO(danrubel) Update DDC to support all the options defined in this method
 * then remove the [ddc] named argument from this method.
 */
void defineAnalysisArguments(ArgParser parser, {bool hide: true, ddc: false}) {
  parser.addOption(sdkPathOption,
      help: 'The path to the Dart SDK.', hide: ddc && hide);
  parser.addOption(analysisOptionsFileOption,
      help: 'Path to an analysis options file.', hide: ddc && hide);
  parser.addOption(packageRootOption,
      help: 'The path to a package root directory (deprecated). '
          'This option cannot be used with --packages.',
      hide: ddc && hide);
  parser.addFlag('strong',
      help: 'Enable strong mode (deprecated); this option is now ignored.',
      defaultsTo: true,
      hide: true,
      negatable: true);
  parser.addFlag(declarationCastsFlag,
      negatable: true,
      help: 'Disable declaration casts in strong mode (https://goo.gl/cTLz40)\n'
          'This option is deprecated and will be removed in a future release.',
      hide: ddc && hide);
  parser.addFlag(implicitCastsFlag,
      negatable: true,
      help: 'Disable implicit casts in strong mode (https://goo.gl/cTLz40).',
      hide: ddc && hide);
  parser.addFlag(noImplicitDynamicFlag,
      negatable: false,
      help: 'Disable implicit dynamic (https://goo.gl/m0UgXD).',
      hide: ddc && hide);

  //
  // Hidden flags and options.
  //
  parser.addMultiOption(defineVariableOption,
      abbr: 'D',
      help: 'Define environment variables. For example, "-Dfoo=bar" defines an '
          'environment variable named "foo" whose value is "bar".',
      hide: hide);
  parser.addOption(packagesOption,
      help: 'The path to the package resolution configuration file, which '
          'supplies a mapping of package names\nto paths. This option cannot be '
          'used with --package-root.',
      hide: ddc);
  parser.addOption(sdkSummaryPathOption,
      help: 'The path to the Dart SDK summary file.', hide: hide);
  parser.addFlag(enableInitializingFormalAccessFlag,
      help:
          'Enable support for allowing access to field formal parameters in a '
          'constructor\'s initializer list (deprecated).',
      defaultsTo: false,
      negatable: false,
      hide: hide || ddc);
  parser.addFlag(enableSuperMixinFlag,
      help: 'Relax restrictions on mixins (DEP 34).',
      defaultsTo: false,
      negatable: false,
      hide: hide);
  if (!ddc) {
    parser.addFlag(lintsFlag,
        help: 'Show lint results.', defaultsTo: false, negatable: true);
  }
}

/**
 * Find arguments of the form -Dkey=value
 * or argument pairs of the form -Dkey value
 * and place those key/value pairs into [definedVariables].
 * Return a list of arguments with the key/value arguments removed.
 */
List<String> extractDefinedVariables(
    List<String> args, Map<String, String> definedVariables) {
  //TODO(danrubel) extracting defined variables is already handled by the
  // createContextBuilderOptions method.
  // Long term we should switch to using that instead.
  int count = args.length;
  List<String> remainingArgs = <String>[];
  for (int i = 0; i < count; i++) {
    String arg = args[i];
    if (arg == '--') {
      while (i < count) {
        remainingArgs.add(args[i++]);
      }
    } else if (arg.startsWith("-D")) {
      int end = arg.indexOf('=');
      if (end > 2) {
        definedVariables[arg.substring(2, end)] = arg.substring(end + 1);
      } else if (i + 1 < count) {
        definedVariables[arg.substring(2)] = args[++i];
      } else {
        remainingArgs.add(arg);
      }
    } else {
      remainingArgs.add(arg);
    }
  }
  return remainingArgs;
}

/**
 * Return a list of command-line arguments containing all of the given [args]
 * that are defined by the given [parser]. An argument is considered to be
 * defined by the parser if
 * - it starts with '--' and the rest of the argument (minus any value
 *   introduced by '=') is the name of a known option,
 * - it starts with '-' and the rest of the argument (minus any value
 *   introduced by '=') is the name of a known abbreviation, or
 * - it starts with something other than '--' or '-'.
 *
 * This function allows command-line tools to implement the
 * '--ignore-unrecognized-flags' option.
 */
List<String> filterUnknownArguments(List<String> args, ArgParser parser) {
  Set<String> knownOptions = new HashSet<String>();
  Set<String> knownAbbreviations = new HashSet<String>();
  parser.options.forEach((String name, Option option) {
    knownOptions.add(name);
    String abbreviation = option.abbr;
    if (abbreviation != null) {
      knownAbbreviations.add(abbreviation);
    }
    if (option.negatable) {
      knownOptions.add('no-$name');
    }
  });
  String optionName(int prefixLength, String argument) {
    int equalsOffset = argument.lastIndexOf('=');
    if (equalsOffset < 0) {
      return argument.substring(prefixLength);
    }
    return argument.substring(prefixLength, equalsOffset);
  }

  List<String> filtered = <String>[];
  for (int i = 0; i < args.length; i++) {
    String argument = args[i];
    if (argument.startsWith('--') && argument.length > 2) {
      if (knownOptions.contains(optionName(2, argument))) {
        filtered.add(argument);
      }
    } else if (argument.startsWith('-') && argument.length > 1) {
      if (knownAbbreviations.contains(optionName(1, argument))) {
        filtered.add(argument);
      }
    } else {
      filtered.add(argument);
    }
  }
  return filtered;
}

/**
 * Use the given [parser] to parse the given command-line [args], and return the
 * result.
 */
ArgResults parse(
    ResourceProvider provider, ArgParser parser, List<String> args) {
  args = preprocessArgs(provider, args);
  if (args.contains('--$ignoreUnrecognizedFlagsFlag')) {
    args = filterUnknownArguments(args, parser);
  }
  return parser.parse(args);
}

/**
 * Preprocess the given list of command line [args].
 * If the final arg is `@file_path` (Bazel worker mode),
 * then read in all the lines of that file and add those as args.
 * Always returns a new modifiable list.
 */
List<String> preprocessArgs(ResourceProvider provider, List<String> args) {
  args = new List.from(args);
  if (args.isEmpty) {
    return args;
  }
  String lastArg = args.last;
  if (lastArg.startsWith('@')) {
    File argsFile = provider.getFile(lastArg.substring(1));
    try {
      args.removeLast();
      args.addAll(argsFile
          .readAsStringSync()
          .replaceAll('\r\n', '\n')
          .replaceAll('\r', '\n')
          .split('\n')
          .where((String line) => line.isNotEmpty));
    } on FileSystemException catch (e) {
      throw new Exception('Failed to read file specified by $lastArg : $e');
    }
  }
  return args;
}
