// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:meta/meta.dart';
import 'package:process/process.dart';

import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/terminal.dart';
import '../project.dart';
import '../project_validator.dart';
import '../runner/flutter_command.dart';
import 'analyze_base.dart';
import 'analyze_continuously.dart';
import 'analyze_once.dart';
import 'android_analyze.dart';
import 'ios_analyze.dart';
import 'validate_project.dart';

class AnalyzeCommand extends FlutterCommand {
  AnalyzeCommand({
    bool verboseHelp = false,
    this.workingDirectory,
    required FileSystem fileSystem,
    required Platform platform,
    required Terminal terminal,
    required Logger logger,
    required ProcessManager processManager,
    required Artifacts artifacts,
    required List<ProjectValidator> allProjectValidators,
    required bool suppressAnalytics,
  }) : _artifacts = artifacts,
       _fileSystem = fileSystem,
       _processManager = processManager,
       _logger = logger,
       _terminal = terminal,
       _allProjectValidators = allProjectValidators,
       _platform = platform,
       _suppressAnalytics = suppressAnalytics {
    argParser.addFlag('flutter-repo',
        negatable: false,
        help: 'Include all the examples and tests from the Flutter repository.',
        hide: !verboseHelp);
    argParser.addFlag('current-package',
        help: 'Analyze the current project, if applicable.', defaultsTo: true);
    argParser.addFlag('dartdocs',
        negatable: false,
        help: '(deprecated) List every public member that is lacking documentation. '
              'This command will be removed in a future version of Flutter.',
        hide: !verboseHelp);
    argParser.addFlag('watch',
        help: 'Run analysis continuously, watching the filesystem for changes.',
        negatable: false);
    argParser.addOption('write',
        valueHelp: 'file',
        help: 'Also output the results to a file. This is useful with "--watch" '
              'if you want a file to always contain the latest results.');
    argParser.addOption('dart-sdk',
        valueHelp: 'path-to-sdk',
        help: 'The path to the Dart SDK.',
        hide: !verboseHelp);
    argParser.addOption('protocol-traffic-log',
        valueHelp: 'path-to-protocol-traffic-log',
        help: 'The path to write the request and response protocol. This is '
              'only intended to be used for debugging the tooling.',
        hide: !verboseHelp);
    argParser.addFlag('suggestions',
        help: 'Show suggestions about the current flutter project.'
    );
    argParser.addFlag('machine',
        negatable: false,
        help: 'Dumps a JSON with a subset of relevant data about the tool, project, '
              'and environment.',
        hide: !verboseHelp,
    );

    // Hidden option to enable a benchmarking mode.
    argParser.addFlag('benchmark',
        negatable: false,
        hide: !verboseHelp,
        help: 'Also output the analysis time.');

    usesPubOption();

    // Not used by analyze --watch
    argParser.addFlag('congratulate',
        help: 'Show output even when there are no errors, warnings, hints, or lints. '
              'Ignored if "--watch" is specified.',
        defaultsTo: true);
    argParser.addFlag('preamble',
        defaultsTo: true,
        help: 'When analyzing the flutter repository, display the number of '
              'files that will be analyzed.\n'
              'Ignored if "--watch" is specified.');
    argParser.addFlag('fatal-infos',
        help: 'Treat info level issues as fatal.',
        defaultsTo: true);
    argParser.addFlag('fatal-warnings',
        help: 'Treat warning level issues as fatal.',
        defaultsTo: true);

    argParser.addFlag('android',
      negatable: false,
      help: 'Analyze Android sub-project. Used by internal tools only.',
      hide: !verboseHelp,
    );

    argParser.addFlag('ios',
      negatable: false,
      help: 'Analyze iOS Xcode sub-project. Used by internal tools only.',
      hide: !verboseHelp,
    );

    if (verboseHelp) {
      argParser.addSeparator('Usage: flutter analyze --android [arguments]');
    }

    argParser.addFlag('list-build-variants',
      negatable: false,
      help: 'Print out a list of available build variants for the '
          'Android sub-project.',
      hide: !verboseHelp,
    );

    argParser.addFlag('output-app-link-settings',
      negatable: false,
      help: 'Output a JSON with Android app link settings into a file. '
          'The "--build-variant" must also be set.',
      hide: !verboseHelp,
    );

    argParser.addOption('build-variant',
      help: 'Sets the Android build variant to be analyzed.',
      valueHelp: 'build variant',
      hide: !verboseHelp,
    );

    if (verboseHelp) {
      argParser.addSeparator('Usage: flutter analyze --ios [arguments]');
    }

    argParser.addFlag('list-build-options',
      help: 'Print out a list of available build options for the '
          'iOS Xcode sub-project.',
      hide: !verboseHelp,
    );

    argParser.addFlag('output-universal-link-settings',
      negatable: false,
      help: 'Output a JSON with iOS Xcode universal link settings into a file. '
          'The "--configuration" and "--target" must be set.',
      hide: !verboseHelp,
    );

    argParser.addOption('configuration',
      help: 'Sets the iOS build configuration to be analyzed.',
      valueHelp: 'configuration',
      hide: !verboseHelp,
    );

    argParser.addOption('target',
      help: 'Sets the iOS build target to be analyzed.',
      valueHelp: 'target',
      hide: !verboseHelp,
    );
  }

  /// The working directory for testing analysis using dartanalyzer.
  final Directory? workingDirectory;

  final Artifacts _artifacts;
  final FileSystem _fileSystem;
  final Logger _logger;
  final Terminal _terminal;
  final ProcessManager _processManager;
  final Platform _platform;
  final List<ProjectValidator> _allProjectValidators;
  final bool _suppressAnalytics;

  @override
  String get name => 'analyze';

  @override
  String get description => "Analyze the project's Dart code.";

  @override
  String get category => FlutterCommandCategory.project;

  @visibleForTesting
  List<ProjectValidator> allProjectValidators() => _allProjectValidators;

  @override
  bool get shouldRunPub {
    // If they're not analyzing the current project.
    if (!boolArg('current-package')) {
      return false;
    }

    // Or we're not in a project directory.
    if (!_fileSystem.file('pubspec.yaml').existsSync()) {
      return false;
    }

    // Don't run pub if asking for machine output.
    if (boolArg('machine')) {
      return false;
    }

    // Don't run pub if asking for android analysis.
    if (boolArg('android')) {
      return false;
    }

    return super.shouldRunPub;
  }

  @override
  Future<FlutterCommandResult> runCommand() async {
    if (boolArg('android')) {
      final AndroidAnalyzeOption option;
      final String? buildVariant;
      if (argResults!['list-build-variants'] as bool && argResults!['output-app-link-settings'] as bool) {
        throwToolExit('Only one of "--list-build-variants" or "--output-app-link-settings" can be provided');
      }
      if (argResults!['list-build-variants'] as bool) {
        option = AndroidAnalyzeOption.listBuildVariant;
        buildVariant = null;
      } else if (argResults!['output-app-link-settings'] as bool) {
        option = AndroidAnalyzeOption.outputAppLinkSettings;
        buildVariant = argResults!['build-variant'] as String?;
        if (buildVariant == null) {
          throwToolExit('"--build-variant" must be provided');
        }
      } else {
        throwToolExit('No argument is provided to analyze. Use -h to see available commands.');
      }
      final Set<String> items = findDirectories(argResults!, _fileSystem);
      final String directoryPath;
      if (items.isEmpty) { // user did not specify any path
        directoryPath = _fileSystem.currentDirectory.path;
      } else if (items.length > 1) { // if the user sends more than one path
        throwToolExit('The Android analyze can process only one directory path');
      } else {
        directoryPath = items.first;
      }
      await AndroidAnalyze(
        fileSystem: _fileSystem,
        option: option,
        userPath: directoryPath,
        buildVariant: buildVariant,
        logger: _logger,
      ).analyze();
    } else if (boolArg('ios')) {
      final IOSAnalyzeOption option;
      final String? configuration;
      final String? target;
      if (argResults!['list-build-options'] as bool && argResults!['output-universal-link-settings'] as bool) {
        throwToolExit('Only one of "--list-build-options" or "--output-universal-link-settings" can be provided');
      }
      if (argResults!['list-build-options'] as bool) {
        option = IOSAnalyzeOption.listBuildOptions;
        configuration = null;
        target = null;
      } else if (argResults!['output-universal-link-settings'] as bool) {
        option = IOSAnalyzeOption.outputUniversalLinkSettings;
        configuration = argResults!['configuration'] as String?;
        if (configuration == null) {
          throwToolExit('"--configuration" must be provided');
        }
        target = argResults!['target'] as String?;
        if (target == null) {
          throwToolExit('"--target" must be provided');
        }
      } else {
        throwToolExit('No argument is provided to analyze. Use -h to see available commands.');
      }
      final Set<String> items = findDirectories(argResults!, _fileSystem);
      final String directoryPath;
      if (items.isEmpty) { // user did not specify any path
        directoryPath = _fileSystem.currentDirectory.path;
      } else if (items.length > 1) { // if the user sends more than one path
        throwToolExit('The iOS analyze can process only one directory path');
      } else {
        directoryPath = items.first;
      }
      await IOSAnalyze(
        project: FlutterProject.fromDirectory(_fileSystem.directory(directoryPath)),
        option: option,
        configuration: configuration,
        target: target,
        logger: _logger,
      ).analyze();
    } else if (boolArg('suggestions')) {
      final String directoryPath;
      if (boolArg('watch')) {
        throwToolExit('flag --watch is not compatible with --suggestions');
      }
      if (workingDirectory == null) {
        final Set<String> items = findDirectories(argResults!, _fileSystem);
        if (items.isEmpty) { // user did not specify any path
          directoryPath = _fileSystem.currentDirectory.path;
          _logger.printTrace('Showing suggestions for current directory: $directoryPath');
        } else if (items.length > 1) { // if the user sends more than one path
          throwToolExit('The suggestions flag can process only one directory path');
        } else {
          directoryPath = items.first;
        }
      } else {
        directoryPath = workingDirectory!.path;
      }
      return ValidateProject(
        fileSystem: _fileSystem,
        logger: _logger,
        allProjectValidators: _allProjectValidators,
        userPath: directoryPath,
        processManager: _processManager,
        machine: boolArg('machine'),
      ).run();
    } else if (boolArg('watch')) {
      await AnalyzeContinuously(
        argResults!,
        runner!.getRepoPackages(),
        fileSystem: _fileSystem,
        logger: _logger,
        platform: _platform,
        processManager: _processManager,
        terminal: _terminal,
        artifacts: _artifacts,
        suppressAnalytics: _suppressAnalytics,
      ).analyze();
    } else {
      await AnalyzeOnce(
        argResults!,
        runner!.getRepoPackages(),
        workingDirectory: workingDirectory,
        fileSystem: _fileSystem,
        logger: _logger,
        platform: _platform,
        processManager: _processManager,
        terminal: _terminal,
        artifacts: _artifacts,
        suppressAnalytics: _suppressAnalytics,
      ).analyze();
    }
    return FlutterCommandResult.success();
  }
}