content: // Copyright 2013 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 'dart:async';
  
  import 'package:file/file.dart';
  import 'package:platform/platform.dart';
  import 'package:yaml/yaml.dart';
  
  import 'common/core.dart';
  import 'common/package_looping_command.dart';
  import 'common/plugin_utils.dart';
  import 'common/process_runner.dart';
  import 'common/repository_package.dart';
  
  /// Key for APK.
  const String _platformFlagApk = 'apk';
  
  const String _pluginToolsConfigFileName = '.pluginToolsConfig.yaml';
  const String _pluginToolsConfigBuildFlagsKey = 'buildFlags';
  const String _pluginToolsConfigGlobalKey = 'global';
  
  const String _pluginToolsConfigExample = '''
  $_pluginToolsConfigBuildFlagsKey:
    $_pluginToolsConfigGlobalKey:
      - "--no-tree-shake-icons"
      - "--dart-define=buildmode=testing"
  ''';
  
  const int _exitNoPlatformFlags = 3;
  const int _exitInvalidPluginToolsConfig = 4;
  
  // Flutter build types. These are the values passed to `flutter build <foo>`.
  const String _flutterBuildTypeAndroid = 'apk';
  const String _flutterBuildTypeIOS = 'ios';
  const String _flutterBuildTypeLinux = 'linux';
  const String _flutterBuildTypeMacOS = 'macos';
  const String _flutterBuildTypeWeb = 'web';
  const String _flutterBuildTypeWindows = 'windows';
  
  /// A command to build the example applications for packages.
  class BuildExamplesCommand extends PackageLoopingCommand {
    /// Creates an instance of the build command.
    BuildExamplesCommand(
      Directory packagesDir, {
      ProcessRunner processRunner = const ProcessRunner(),
      Platform platform = const LocalPlatform(),
    }) : super(packagesDir, processRunner: processRunner, platform: platform) {
      argParser.addFlag(platformLinux);
      argParser.addFlag(platformMacOS);
      argParser.addFlag(platformWeb);
      argParser.addFlag(platformWindows);
      argParser.addFlag(platformIOS);
      argParser.addFlag(_platformFlagApk);
      argParser.addOption(
        kEnableExperiment,
        defaultsTo: '',
        help: 'Enables the given Dart SDK experiments.',
      );
    }
  
    // Maps the switch this command uses to identify a platform to information
    // about it.
    static final Map<String, _PlatformDetails> _platforms =
        <String, _PlatformDetails>{
      _platformFlagApk: const _PlatformDetails(
        'Android',
        pluginPlatform: platformAndroid,
        flutterBuildType: _flutterBuildTypeAndroid,
      ),
      platformIOS: const _PlatformDetails(
        'iOS',
        pluginPlatform: platformIOS,
        flutterBuildType: _flutterBuildTypeIOS,
        extraBuildFlags: <String>['--no-codesign'],
      ),
      platformLinux: const _PlatformDetails(
        'Linux',
        pluginPlatform: platformLinux,
        flutterBuildType: _flutterBuildTypeLinux,
      ),
      platformMacOS: const _PlatformDetails(
        'macOS',
        pluginPlatform: platformMacOS,
        flutterBuildType: _flutterBuildTypeMacOS,
      ),
      platformWeb: const _PlatformDetails(
        'web',
        pluginPlatform: platformWeb,
        flutterBuildType: _flutterBuildTypeWeb,
      ),
      platformWindows: const _PlatformDetails(
        'Windows',
        pluginPlatform: platformWindows,
        flutterBuildType: _flutterBuildTypeWindows,
      ),
    };
  
    @override
    final String name = 'build-examples';
  
    @override
    final String description =
        'Builds all example apps (IPA for iOS and APK for Android).\n\n'
        'This command requires "flutter" to be in your path.\n\n'
        'A $_pluginToolsConfigFileName file can be placed in an example app '
        'directory to specify additional build arguments. It should be a YAML '
        'file with a top-level map containing a single key '
        '"$_pluginToolsConfigBuildFlagsKey" containing a map containing a '
        'single key "$_pluginToolsConfigGlobalKey" containing a list of build '
        'arguments.';
  
    @override
    Future<void> initializeRun() async {
      final List<String> platformFlags = _platforms.keys.toList();
      platformFlags.sort();
      if (!platformFlags.any(getBoolArg)) {
        printError(
            'None of ${platformFlags.map((String platform) => '--$platform').join(', ')} '
            'were specified. At least one platform must be provided.');
        throw ToolExit(_exitNoPlatformFlags);
      }
    }
  
    @override
    Future<PackageResult> runForPackage(RepositoryPackage package) async {
      final List<String> errors = <String>[];
  
      final bool isPlugin = isFlutterPlugin(package);
      final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries
          .where(
              (MapEntry<String, _PlatformDetails> entry) => getBoolArg(entry.key))
          .map((MapEntry<String, _PlatformDetails> entry) => entry.value);
  
      // Platform support is checked at the package level for plugins; there is
      // no package-level platform information for non-plugin packages.
      final Set<_PlatformDetails> buildPlatforms = isPlugin
          ? requestedPlatforms
              .where((_PlatformDetails platform) =>
                  pluginSupportsPlatform(platform.pluginPlatform, package))
              .toSet()
          : requestedPlatforms.toSet();
  
      String platformDisplayList(Iterable<_PlatformDetails> platforms) =>
          platforms.map((_PlatformDetails p) => p.label).join(', ');
  
      if (buildPlatforms.isEmpty) {
        final String unsupported = requestedPlatforms.length == 1
            ? '${requestedPlatforms.first.label} is not supported'
            : 'None of [${platformDisplayList(requestedPlatforms)}] are supported';
        return PackageResult.skip('$unsupported by this plugin');
      }
      print('Building for: ${platformDisplayList(buildPlatforms)}');
  
      final Set<_PlatformDetails> unsupportedPlatforms =
          requestedPlatforms.toSet().difference(buildPlatforms);
      if (unsupportedPlatforms.isNotEmpty) {
        final List<String> skippedPlatforms = unsupportedPlatforms
            .map((_PlatformDetails platform) => platform.label)
            .toList();
        skippedPlatforms.sort();
        print('Skipping unsupported platform(s): '
            '${skippedPlatforms.join(', ')}');
      }
      print('');
  
      bool builtSomething = false;
      for (final RepositoryPackage example in package.getExamples()) {
        final String packageName =
            getRelativePosixPath(example.directory, from: packagesDir);
  
        for (final _PlatformDetails platform in buildPlatforms) {
          // Repo policy is that a plugin must have examples configured for all
          // supported platforms. For packages, just log and skip any requested
          // platform that a package doesn't have set up.
          if (!isPlugin &&
              !example.directory
                  .childDirectory(platform.flutterPlatformDirectory)
                  .existsSync()) {
            print('Skipping ${platform.label} for $packageName; not supported.');
            continue;
          }
  
          builtSomething = true;
  
          String buildPlatform = platform.label;
          if (platform.label.toLowerCase() != platform.flutterBuildType) {
            buildPlatform += ' (${platform.flutterBuildType})';
          }
          print('\nBUILDING $packageName for $buildPlatform');
          if (!await _buildExample(example, platform.flutterBuildType,
              extraBuildFlags: platform.extraBuildFlags)) {
            errors.add('$packageName (${platform.label})');
          }
        }
      }
  
      if (!builtSomething) {
        if (isPlugin) {
          errors.add('No examples found');
        } else {
          return PackageResult.skip(
              'No examples found supporting requested platform(s).');
        }
      }
  
      return errors.isEmpty
          ? PackageResult.success()
          : PackageResult.fail(errors);
    }
  
    Iterable<String> _readExtraBuildFlagsConfiguration(
        Directory directory) sync* {
      final File pluginToolsConfig =
          directory.childFile(_pluginToolsConfigFileName);
      if (pluginToolsConfig.existsSync()) {
        final Object? configuration =
            loadYaml(pluginToolsConfig.readAsStringSync());
        if (configuration is! YamlMap) {
          printError('The $_pluginToolsConfigFileName file must be a YAML map.');
          printError(
              'Currently, the key "$_pluginToolsConfigBuildFlagsKey" is the only one that has an effect.');
          printError(
              'It must itself be a map. Currently, in that map only the key "$_pluginToolsConfigGlobalKey"');
          printError(
              'has any effect; it must contain a list of arguments to pass to the');
          printError('flutter tool.');
          printError(_pluginToolsConfigExample);
          throw ToolExit(_exitInvalidPluginToolsConfig);
        }
        if (configuration.containsKey(_pluginToolsConfigBuildFlagsKey)) {
          final Object? buildFlagsConfiguration =
              configuration[_pluginToolsConfigBuildFlagsKey];
          if (buildFlagsConfiguration is! YamlMap) {
            printError(
                'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map.');
            printError(
                'Currently, in that map only the key "$_pluginToolsConfigGlobalKey" has any effect; it must ');
            printError(
                'contain a list of arguments to pass to the flutter tool.');
            printError(_pluginToolsConfigExample);
            throw ToolExit(_exitInvalidPluginToolsConfig);
          }
          if (buildFlagsConfiguration.containsKey(_pluginToolsConfigGlobalKey)) {
            final Object? globalBuildFlagsConfiguration =
                buildFlagsConfiguration[_pluginToolsConfigGlobalKey];
            if (globalBuildFlagsConfiguration is! YamlList) {
              printError(
                  'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map');
              printError('whose "$_pluginToolsConfigGlobalKey" key is a list.');
              printError(
                  'That list must contain a list of arguments to pass to the flutter tool.');
              printError(
                  'For example, the $_pluginToolsConfigFileName file could look like:');
              printError(_pluginToolsConfigExample);
              throw ToolExit(_exitInvalidPluginToolsConfig);
            }
            yield* globalBuildFlagsConfiguration.cast<String>();
          }
        }
      }
    }
  
    Future<bool> _buildExample(
      RepositoryPackage example,
      String flutterBuildType, {
      List<String> extraBuildFlags = const <String>[],
    }) async {
      final String enableExperiment = getStringArg(kEnableExperiment);
  
      final int exitCode = await processRunner.runAndStream(
        flutterCommand,
        <String>[
          'build',
          flutterBuildType,
          ...extraBuildFlags,
          ..._readExtraBuildFlagsConfiguration(example.directory),
          if (enableExperiment.isNotEmpty)
            '--enable-experiment=$enableExperiment',
        ],
        workingDir: example.directory,
      );
      return exitCode == 0;
    }
  }
  
  /// A collection of information related to a specific platform.
  class _PlatformDetails {
    const _PlatformDetails(
      this.label, {
      required this.pluginPlatform,
      required this.flutterBuildType,
      this.extraBuildFlags = const <String>[],
    });
  
    /// The name to use in output.
    final String label;
  
    /// The key in a pubspec's platform: entry.
    final String pluginPlatform;
  
    /// The `flutter build` build type.
    final String flutterBuildType;
  
    /// The Flutter platform directory name.
    // In practice, this is the same as the plugin platform key for all platforms.
    // If that changes, this can be adjusted.
    String get flutterPlatformDirectory => pluginPlatform;
  
    /// Any extra flags to pass to `flutter build`.
    final List<String> extraBuildFlags;
  }
  