// 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.

/// @docImport 'ios/mac.dart';
library;

import 'base/error_handling_io.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/template.dart';
import 'base/utils.dart';
import 'base/version.dart';
import 'build_info.dart';
import 'build_system/build_system.dart';
import 'bundle.dart' as bundle;
import 'convert.dart';
import 'features.dart';
import 'flutter_plugins.dart';
import 'globals.dart' as globals;
import 'ios/code_signing.dart';
import 'ios/plist_parser.dart';
import 'ios/xcode_build_settings.dart' as xcode;
import 'ios/xcodeproj.dart';
import 'macos/swift_package_manager.dart';
import 'macos/xcode.dart';
import 'platform_plugins.dart';
import 'project.dart';
import 'template.dart';

/// Represents an Xcode-based sub-project.
///
/// This defines interfaces common to iOS and macOS projects.
abstract class XcodeBasedProject extends FlutterProjectPlatform {
  static const _defaultHostAppName = 'Runner';

  /// The Xcode workspace (.xcworkspace directory) of the host app.
  Directory? get xcodeWorkspace {
    if (!hostAppRoot.existsSync()) {
      return null;
    }
    return _xcodeDirectoryWithExtension('.xcworkspace');
  }

  /// The project name (.xcodeproj basename) of the host app.
  late final String hostAppProjectName = () {
    if (!hostAppRoot.existsSync()) {
      return _defaultHostAppName;
    }
    final Directory? xcodeProjectDirectory = _xcodeDirectoryWithExtension('.xcodeproj');
    return xcodeProjectDirectory != null
        ? xcodeProjectDirectory.fileSystem.path.basenameWithoutExtension(xcodeProjectDirectory.path)
        : _defaultHostAppName;
  }();

  Directory? _xcodeDirectoryWithExtension(String extension) {
    final List<FileSystemEntity> contents = hostAppRoot.listSync();
    for (final entity in contents) {
      if (globals.fs.path.extension(entity.path) == extension &&
          !globals.fs.path.basename(entity.path).startsWith('.')) {
        return hostAppRoot.childDirectory(entity.basename);
      }
    }
    return null;
  }

  /// The parent of this project.
  FlutterProject get parent;

  Directory get hostAppRoot;

  /// The default 'Info.plist' file of the host app. The developer can change this location in Xcode.
  File get defaultHostInfoPlist =>
      hostAppRoot.childDirectory(_defaultHostAppName).childFile('Info.plist');

  /// The Xcode project (.xcodeproj directory) of the host app.
  Directory get xcodeProject => hostAppRoot.childDirectory('$hostAppProjectName.xcodeproj');

  /// The 'project.pbxproj' file of [xcodeProject].
  File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');

  /// The 'Runner.xcscheme' file of [xcodeProject].
  File xcodeProjectSchemeFile({String? scheme}) {
    final String schemeName = scheme ?? 'Runner';
    return xcodeProject
        .childDirectory('xcshareddata')
        .childDirectory('xcschemes')
        .childFile('$schemeName.xcscheme');
  }

  File get xcodeProjectWorkspaceData =>
      xcodeProject.childDirectory('project.xcworkspace').childFile('contents.xcworkspacedata');

  /// Xcode workspace shared data directory for the host app.
  Directory? get xcodeWorkspaceSharedData => xcodeWorkspace?.childDirectory('xcshareddata');

  /// Xcode workspace shared workspace settings file for the host app.
  File? get xcodeWorkspaceSharedSettings =>
      xcodeWorkspaceSharedData?.childFile('WorkspaceSettings.xcsettings');

  /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
  /// the Xcode build.
  File get generatedXcodePropertiesFile;

  /// The Flutter-managed Xcode config file for [mode].
  File xcodeConfigFor(String mode);

  /// The script that exports environment variables needed for Flutter tools.
  /// Can be run first in a Xcode Script build phase to make FLUTTER_ROOT,
  /// LOCAL_ENGINE, and other Flutter variables available to any flutter
  /// tooling (`flutter build`, etc) to convert into flags.
  File get generatedEnvironmentVariableExportScript;

  /// The CocoaPods 'Podfile'.
  File get podfile => hostAppRoot.childFile('Podfile');

  /// The CocoaPods 'Podfile.lock'.
  File get podfileLock => hostAppRoot.childFile('Podfile.lock');

  /// The CocoaPods 'Manifest.lock'.
  File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock');

  /// The CocoaPods generated 'Pods-Runner-frameworks.sh'.
  File get podRunnerFrameworksScript =>
      podRunnerTargetSupportFiles.childFile('Pods-Runner-frameworks.sh');

  /// The CocoaPods generated directory 'Pods-Runner'.
  Directory get podRunnerTargetSupportFiles => hostAppRoot
      .childDirectory('Pods')
      .childDirectory('Target Support Files')
      .childDirectory('Pods-Runner');

  /// The directory in the project that is managed by Flutter. As much as
  /// possible, files that are edited by Flutter tooling after initial project
  /// creation should live here.
  Directory get managedDirectory => hostAppRoot.childDirectory('Flutter');

  /// The subdirectory of [managedDirectory] that contains files that are
  /// generated on the fly. All generated files that are not intended to be
  /// checked in should live here.
  Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');

  /// The Flutter generated directory for generated Swift packages.
  Directory get flutterSwiftPackagesDirectory => ephemeralDirectory.childDirectory('Packages');

  /// Flutter plugins that support SwiftPM will be symlinked in this directory to keep all
  /// Swift packages relative to each other.
  Directory get relativeSwiftPackagesDirectory =>
      flutterSwiftPackagesDirectory.childDirectory('.packages');

  /// The Flutter generated directory for the Swift package handling plugin
  /// dependencies.
  Directory get flutterPluginSwiftPackageDirectory =>
      flutterSwiftPackagesDirectory.childDirectory(kFlutterGeneratedPluginSwiftPackageName);

  /// The Flutter generated Swift package manifest (Package.swift) for plugin
  /// dependencies.
  File get flutterPluginSwiftPackageManifest =>
      flutterPluginSwiftPackageDirectory.childFile('Package.swift');

  /// Checks if FlutterGeneratedPluginSwiftPackage has been added to the
  /// project's build settings by checking the contents of the pbxproj.
  bool get flutterPluginSwiftPackageInProjectSettings {
    return xcodeProjectInfoFile.existsSync() &&
        xcodeProjectInfoFile.readAsStringSync().contains(kFlutterGeneratedPluginSwiftPackageName);
  }

  /// True if this project doesn't have Swift Package Manager disabled in the
  /// pubspec, has either an iOS or macOS platform implementation, is not a
  /// module project, Xcode is 15 or greater, and the Swift Package Manager
  /// feature is enabled.
  bool get usesSwiftPackageManager {
    if (!featureFlags.isSwiftPackageManagerEnabled) {
      return false;
    }

    // TODO(loic-sharma): Support Swift Package Manager in add-to-app modules.
    // https://github.com/flutter/flutter/issues/146957
    if (parent.isModule) {
      return false;
    }

    if (!existsSync()) {
      return false;
    }

    // Swift Package Manager requires Xcode 15 or greater.
    final Xcode? xcode = globals.xcode;
    final Version? xcodeVersion = xcode?.currentVersion;
    if (xcodeVersion == null || xcodeVersion.major < 15) {
      return false;
    }

    return true;
  }

  Future<XcodeProjectInfo?> projectInfo() async {
    final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
    if (!xcodeProject.existsSync() ||
        xcodeProjectInterpreter == null ||
        !xcodeProjectInterpreter.isInstalled) {
      return null;
    }
    return _projectInfo ??= await xcodeProjectInterpreter.getInfo(hostAppRoot.path);
  }

  XcodeProjectInfo? _projectInfo;

  /// Get the scheme using the Xcode's project [XcodeProjectInfo.schemes] and
  /// the [BuildInfo.flavor].
  Future<String?> schemeForBuildInfo(BuildInfo buildInfo, {Logger? logger}) async {
    final XcodeProjectInfo? info = await projectInfo();
    if (info == null) {
      logger?.printError('Xcode project info not found.');
      return null;
    }

    final String? scheme = info.schemeFor(buildInfo);
    if (scheme == null) {
      info.reportFlavorNotFoundAndExit();
    }
    return scheme;
  }

  /// The build settings for the host app of this project, as a detached map.
  ///
  /// Returns null, if Xcode tooling is unavailable.
  Future<Map<String, String>?> buildSettingsForBuildInfo(
    BuildInfo? buildInfo, {
    String? scheme,
    String? configuration,
    String? target,
    EnvironmentType environmentType = EnvironmentType.physical,
    String? deviceId,
    bool isWatch = false,
  }) async {
    if (!existsSync()) {
      return null;
    }
    final XcodeProjectInfo? info = await projectInfo();
    if (info == null) {
      return null;
    }

    scheme ??= info.schemeFor(buildInfo);
    if (scheme == null) {
      info.reportFlavorNotFoundAndExit();
    }

    configuration ??= (await projectInfo())?.buildConfigurationFor(buildInfo, scheme);

    final XcodeSdk sdk = switch ((environmentType, this)) {
      (EnvironmentType.physical, _) when isWatch => XcodeSdk.WatchOS,
      (EnvironmentType.simulator, _) when isWatch => XcodeSdk.WatchSimulator,
      (EnvironmentType.physical, IosProject _) => XcodeSdk.IPhoneOS,
      (EnvironmentType.simulator, IosProject _) => XcodeSdk.IPhoneSimulator,
      (EnvironmentType.physical, MacOSProject _) => XcodeSdk.MacOSX,
      (_, _) => throw ArgumentError('Unsupported SDK'),
    };

    return _buildSettingsForXcodeProjectBuildContext(
      XcodeProjectBuildContext(
        scheme: scheme,
        configuration: configuration,
        sdk: sdk,
        target: target,
        deviceId: deviceId,
      ),
    );
  }

  Future<Map<String, String>?> _buildSettingsForXcodeProjectBuildContext(
    XcodeProjectBuildContext buildContext,
  ) async {
    if (!existsSync()) {
      return null;
    }
    final Map<String, String>? currentBuildSettings = _buildSettingsByBuildContext[buildContext];
    if (currentBuildSettings == null) {
      final Map<String, String>? calculatedBuildSettings = await _xcodeProjectBuildSettings(
        buildContext,
      );
      if (calculatedBuildSettings != null) {
        _buildSettingsByBuildContext[buildContext] = calculatedBuildSettings;
      }
    }
    return _buildSettingsByBuildContext[buildContext];
  }

  final _buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};

  Future<Map<String, String>?> _xcodeProjectBuildSettings(
    XcodeProjectBuildContext buildContext,
  ) async {
    final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
    if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) {
      return null;
    }

    final Map<String, String> buildSettings = await xcodeProjectInterpreter.getBuildSettings(
      xcodeProject.path,
      buildContext: buildContext,
    );
    if (buildSettings.isNotEmpty) {
      // No timeouts, flakes, or errors.
      return buildSettings;
    }
    return null;
  }

  /// When flutter assemble runs within an Xcode run script, it does not know
  /// the scheme and therefore doesn't know what flavor is being used. This
  /// makes a best effort to parse the scheme name from the [kXcodeConfiguration].
  /// Most flavor's [kXcodeConfiguration] should follow the naming convention
  /// of '$baseConfiguration-$scheme'. This is only semi-enforced by
  /// [buildXcodeProject], so it may not work. Also check if separated by a
  /// space instead of a `-`. Once parsed, match it with a scheme/flavor name.
  /// If the flavor cannot be parsed or matched, use the [kFlavor] environment
  /// variable, which may or may not be set/correct, as a fallback.
  Future<String?> parseFlavorFromConfiguration(Environment environment) async {
    final String? configuration = environment.defines[kXcodeConfiguration];
    final String? flavor = environment.defines[kFlavor];
    if (configuration == null) {
      return flavor;
    }
    List<String> splitConfiguration = configuration.split('-');
    if (splitConfiguration.length == 1) {
      splitConfiguration = configuration.split(' ');
    }
    if (splitConfiguration.length == 1) {
      return flavor;
    }
    final String parsedScheme = splitConfiguration[1];

    final XcodeProjectInfo? info = await projectInfo();
    if (info == null) {
      return flavor;
    }
    for (final String schemeName in info.schemes) {
      if (schemeName.toLowerCase() == parsedScheme.toLowerCase()) {
        return schemeName;
      }
    }
    return flavor;
  }
}

/// Represents the iOS sub-project of a Flutter project.
///
/// Instances will reflect the contents of the `ios/` sub-folder of
/// Flutter applications and the `.ios/` sub-folder of Flutter module projects.
class IosProject extends XcodeBasedProject {
  IosProject.fromFlutter(this.parent);

  @override
  final FlutterProject parent;

  @override
  String get pluginConfigKey => IOSPlugin.kConfigKey;

  // build setting keys
  static const kProductBundleIdKey = 'PRODUCT_BUNDLE_IDENTIFIER';
  static const kTeamIdKey = 'DEVELOPMENT_TEAM';
  static const kEntitlementFilePathKey = 'CODE_SIGN_ENTITLEMENTS';
  static const kProductNameKey = 'PRODUCT_NAME';

  static final _productBundleIdPattern = RegExp(
    '^\\s*$kProductBundleIdKey\\s*=\\s*(["\']?)(.*?)\\1;\\s*\$',
  );
  static const _kProductBundleIdVariable = '\$($kProductBundleIdKey)';

  // The string starts with `applinks:` and ignores the query param which starts with `?`.
  static final _associatedDomainPattern = RegExp(r'^applinks:([^?]+)');

  static const _lldbPythonHelperTemplateName = 'flutter_lldb_helper.py';

  static const _lldbInitTemplate =
      '''
#
# Generated file, do not edit.
#

command script import --relative-to-command-file $_lldbPythonHelperTemplateName
''';

  static const _lldbPythonHelperTemplate = r'''
#
# Generated file, do not edit.
#

import lldb

def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
    """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
    base = frame.register["x0"].GetValueAsAddress()
    page_len = frame.register["x1"].GetValueAsUnsigned()

    # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
    # first page to see if handled it correctly. This makes diagnosing
    # misconfiguration (e.g. missing breakpoint) easier.
    data = bytearray(page_len)
    data[0:8] = b'IHELPED!'

    error = lldb.SBError()
    frame.GetThread().GetProcess().WriteMemory(base, data, error)
    if not error.Success():
        print(f'Failed to write into {base}[+{page_len}]', error)
        return

def __lldb_init_module(debugger: lldb.SBDebugger, _):
    target = debugger.GetDummyTarget()
    # Caveat: must use BreakpointCreateByRegEx here and not
    # BreakpointCreateByName. For some reasons callback function does not
    # get carried over from dummy target for the later.
    bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
    bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
    bp.SetAutoContinue(True)
    print("-- LLDB integration loaded --")
''';

  Directory get ephemeralModuleDirectory => parent.directory.childDirectory('.ios');
  Directory get _editableDirectory => parent.directory.childDirectory('ios');

  /// This parent folder of `Runner.xcodeproj`.
  @override
  Directory get hostAppRoot {
    if (!isModule || _editableDirectory.existsSync()) {
      return _editableDirectory;
    }
    return ephemeralModuleDirectory;
  }

  /// The root directory of the iOS wrapping of Flutter and plugins. This is the
  /// parent of the `Flutter/` folder into which Flutter artifacts are written
  /// during build.
  ///
  /// This is the same as [hostAppRoot] except when the project is
  /// a Flutter module with an editable host app.
  Directory get _flutterLibRoot => isModule ? ephemeralModuleDirectory : _editableDirectory;

  /// True, if the parent Flutter project is a module project.
  bool get isModule => parent.isModule;

  /// Whether the Flutter application has an iOS project.
  bool get exists => hostAppRoot.existsSync();

  @override
  Directory get managedDirectory => _flutterLibRoot.childDirectory('Flutter');

  @override
  File xcodeConfigFor(String mode) => managedDirectory.childFile('$mode.xcconfig');

  @override
  File get generatedEnvironmentVariableExportScript =>
      managedDirectory.childFile('flutter_export_environment.sh');

  File get appFrameworkInfoPlist => managedDirectory.childFile('AppFrameworkInfo.plist');

  /// The 'AppDelegate.swift' file of the host app. This file might not exist if the app project uses Objective-C.
  File get appDelegateSwift =>
      _editableDirectory.childDirectory('Runner').childFile('AppDelegate.swift');

  /// The 'AppDelegate.m' file of the host app. This file might not exist if the app project uses Swift.
  File get appDelegateObjc =>
      _editableDirectory.childDirectory('Runner').childFile('AppDelegate.m');

  File get infoPlist => _editableDirectory.childDirectory('Runner').childFile('Info.plist');

  Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks');

  /// True if the app project uses Swift.
  bool get isSwift => appDelegateSwift.existsSync();

  /// Do all plugins support arm64 simulators to run natively on an ARM Mac?
  Future<bool> pluginsSupportArmSimulator() async {
    final Directory podXcodeProject = hostAppRoot
        .childDirectory('Pods')
        .childDirectory('Pods.xcodeproj');
    if (!podXcodeProject.existsSync()) {
      // No plugins.
      return true;
    }

    final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
    if (xcodeProjectInterpreter == null) {
      // Xcode isn't installed, don't try to check.
      return false;
    }
    final String? buildSettings = await xcodeProjectInterpreter.pluginsBuildSettingsOutput(
      podXcodeProject,
    );

    // See if any plugins or their dependencies exclude arm64 simulators
    // as a valid architecture, usually because a binary is missing that slice.
    // Example: EXCLUDED_ARCHS = arm64 i386
    // NOT: EXCLUDED_ARCHS = i386
    return buildSettings != null && !buildSettings.contains(RegExp('EXCLUDED_ARCHS.*arm64'));
  }

  @override
  bool existsSync() {
    return parent.isModule || _editableDirectory.existsSync();
  }

  /// Outputs universal link related project settings of the iOS sub-project into
  /// a json file.
  ///
  /// The return future will resolve to string path to the output file.
  Future<String> outputsUniversalLinkSettings({
    required String configuration,
    required String target,
  }) async {
    final context = XcodeProjectBuildContext(configuration: configuration, target: target);
    final File file = await parent.buildDirectory
        .childDirectory('deeplink_data')
        .childFile('universal-link-settings-$configuration-$target.json')
        .create(recursive: true);

    await file.writeAsString(
      jsonEncode(<String, Object?>{
        'bundleIdentifier': await _productBundleIdentifierWithBuildContext(context),
        'teamIdentifier': await _getTeamIdentifier(context),
        'associatedDomains': await _getAssociatedDomains(context),
      }),
    );
    return file.absolute.path;
  }

  /// The product bundle identifier of the host app, or null if not set or if
  /// iOS tooling needed to read it is not installed.
  Future<String?> productBundleIdentifier(BuildInfo? buildInfo) async {
    if (!existsSync()) {
      return null;
    }

    XcodeProjectBuildContext? buildContext;
    final XcodeProjectInfo? info = await projectInfo();
    if (info != null) {
      final String? scheme = info.schemeFor(buildInfo);
      if (scheme == null) {
        info.reportFlavorNotFoundAndExit();
      }
      final String? configuration = info.buildConfigurationFor(buildInfo, scheme);
      buildContext = XcodeProjectBuildContext(configuration: configuration, scheme: scheme);
    }
    return _productBundleIdentifierWithBuildContext(buildContext);
  }

  Future<String?> _productBundleIdentifierWithBuildContext(
    XcodeProjectBuildContext? buildContext,
  ) async {
    if (!existsSync()) {
      return null;
    }
    if (_productBundleIdentifiers.containsKey(buildContext)) {
      return _productBundleIdentifiers[buildContext];
    }
    return _productBundleIdentifiers[buildContext] = await _parseProductBundleIdentifier(
      buildContext,
    );
  }

  final _productBundleIdentifiers = <XcodeProjectBuildContext?, String?>{};

  Future<String?> _parseProductBundleIdentifier(XcodeProjectBuildContext? buildContext) async {
    String? fromPlist;
    final File defaultInfoPlist = defaultHostInfoPlist;
    // Users can change the location of the Info.plist.
    // Try parsing the default, first.
    if (defaultInfoPlist.existsSync()) {
      try {
        fromPlist = globals.plistParser.getValueFromFile<String>(
          defaultHostInfoPlist.path,
          PlistParser.kCFBundleIdentifierKey,
        );
      } on FileNotFoundException {
        // iOS tooling not found; likely not running OSX; let [fromPlist] be null
      }
      if (fromPlist != null && !fromPlist.contains(r'$')) {
        // Info.plist has no build variables in product bundle ID.
        return fromPlist;
      }
    }
    if (buildContext == null) {
      // Getting build settings to evaluate info.Plist requires a context.
      return null;
    }

    final Map<String, String>? allBuildSettings = await _buildSettingsForXcodeProjectBuildContext(
      buildContext,
    );
    if (allBuildSettings != null) {
      if (fromPlist != null) {
        // Perform variable substitution using build settings.
        return substituteXcodeVariables(fromPlist, allBuildSettings);
      }
      return allBuildSettings[kProductBundleIdKey];
    }

    // On non-macOS platforms, parse the first PRODUCT_BUNDLE_IDENTIFIER from
    // the project file. This can return the wrong bundle identifier if additional
    // bundles have been added to the project and are found first, like frameworks
    // or companion watchOS projects. However, on non-macOS platforms this is
    // only used for display purposes and to regenerate organization names, so
    // best-effort is probably fine.
    final String? fromPbxproj = firstMatchInFile(
      xcodeProjectInfoFile,
      _productBundleIdPattern,
    )?.group(2);
    if (fromPbxproj != null && (fromPlist == null || fromPlist == _kProductBundleIdVariable)) {
      return fromPbxproj;
    }
    return null;
  }

  Future<String?> _getTeamIdentifier(XcodeProjectBuildContext buildContext) async {
    final Map<String, String>? buildSettings = await _buildSettingsForXcodeProjectBuildContext(
      buildContext,
    );
    return buildSettings?[kTeamIdKey];
  }

  Future<List<String>> _getAssociatedDomains(XcodeProjectBuildContext buildContext) async {
    final Map<String, String>? buildSettings = await _buildSettingsForXcodeProjectBuildContext(
      buildContext,
    );
    if (buildSettings != null) {
      final String? entitlementPath = buildSettings[kEntitlementFilePathKey];
      if (entitlementPath != null) {
        final File entitlement = hostAppRoot.childFile(entitlementPath);
        if (entitlement.existsSync()) {
          final List<String>? domains = globals.plistParser
              .getValueFromFile<List<Object>>(entitlement.path, PlistParser.kAssociatedDomainsKey)
              ?.cast<String>();

          if (domains != null) {
            return <String>[
              for (final String domain in domains)
                if (_associatedDomainPattern.firstMatch(domain) case final RegExpMatch match)
                  match.group(1)!,
            ];
          }
        }
      }
    }
    return const <String>[];
  }

  /// The product name of the app, `My App`.
  Future<String?> productName(BuildInfo? buildInfo) async {
    if (!existsSync()) {
      return null;
    }
    return _productName ??= await _parseProductName(buildInfo);
  }

  String? _productName;

  Future<String> _parseProductName(BuildInfo? buildInfo) async {
    // The product name and bundle name are derived from the display name, which the user
    // is instructed to change in Xcode as part of deploying to the App Store.
    // https://flutter.dev/to/xcode-name-config
    // The only source of truth for the name is Xcode's interpretation of the build settings.
    String? productName;
    if (globals.xcodeProjectInterpreter?.isInstalled ?? false) {
      final Map<String, String>? xcodeBuildSettings = await buildSettingsForBuildInfo(buildInfo);
      if (xcodeBuildSettings != null) {
        productName = xcodeBuildSettings[kProductNameKey];
      }
    }
    if (productName == null) {
      globals.printTrace('$kProductNameKey not present, defaulting to $hostAppProjectName');
    }
    return productName ?? XcodeBasedProject._defaultHostAppName;
  }

  Future<void> ensureReadyForPlatformSpecificTooling() async {
    await _regenerateModuleFromTemplateIfNeeded();
    await _updateLLDBIfNeeded();
    if (!_flutterLibRoot.existsSync()) {
      return;
    }
    await _updateGeneratedXcodeConfigIfNeeded();
  }

  /// Check if one the [XcodeProjectInfo.targets] of the project is
  /// a watchOS companion app target.
  Future<bool> containsWatchCompanion({
    required XcodeProjectInfo projectInfo,
    required BuildInfo buildInfo,
    String? deviceId,
  }) async {
    final String? bundleIdentifier = await productBundleIdentifier(buildInfo);
    // A bundle identifier is required for a companion app.
    if (bundleIdentifier == null) {
      return false;
    }
    for (final String target in projectInfo.targets) {
      // Create Info.plist file of the target.
      final File infoFile = hostAppRoot.childDirectory(target).childFile('Info.plist');
      // In older versions of Xcode, if the target was a watchOS companion app,
      // the Info.plist file of the target contained the key WKCompanionAppBundleIdentifier.
      if (infoFile.existsSync()) {
        final String? fromPlist = globals.plistParser.getValueFromFile<String>(
          infoFile.path,
          'WKCompanionAppBundleIdentifier',
        );
        if (bundleIdentifier == fromPlist) {
          return true;
        }

        // The key WKCompanionAppBundleIdentifier might contain an xcode variable
        // that needs to be substituted before comparing it with bundle id
        if (fromPlist != null && fromPlist.contains(r'$')) {
          final Map<String, String>? allBuildSettings = await buildSettingsForBuildInfo(
            buildInfo,
            deviceId: deviceId,
          );
          if (allBuildSettings != null) {
            final String substitutedVariable = substituteXcodeVariables(
              fromPlist,
              allBuildSettings,
            );
            if (substitutedVariable == bundleIdentifier) {
              return true;
            }
          }
        }
      }
    }

    // If key not found in Info.plist above, do more expensive check of build settings.
    // In newer versions of Xcode, the build settings of the watchOS companion
    // app's scheme should contain the key INFOPLIST_KEY_WKCompanionAppBundleIdentifier.
    final bool watchIdentifierFound = xcodeProjectInfoFile.readAsStringSync().contains(
      'WKCompanionAppBundleIdentifier',
    );
    if (!watchIdentifierFound) {
      return false;
    }

    final String? defaultScheme = projectInfo.schemeFor(buildInfo);
    if (defaultScheme == null) {
      projectInfo.reportFlavorNotFoundAndExit();
    }
    for (final String scheme in projectInfo.schemes) {
      // the default scheme should not be a watch scheme, so skip it
      if (scheme == defaultScheme) {
        continue;
      }
      final Map<String, String>? allBuildSettings = await buildSettingsForBuildInfo(
        buildInfo,
        deviceId: deviceId,
        scheme: scheme,
        isWatch: true,
      );
      if (allBuildSettings != null) {
        final String? fromBuild = allBuildSettings['INFOPLIST_KEY_WKCompanionAppBundleIdentifier'];
        if (bundleIdentifier == fromBuild) {
          return true;
        }
        if (fromBuild != null && fromBuild.contains(r'$')) {
          final String substitutedVariable = substituteXcodeVariables(fromBuild, allBuildSettings);
          if (substitutedVariable == bundleIdentifier) {
            return true;
          }
        }
      }
    }
    return false;
  }

  Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
    if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
      await xcode.updateGeneratedXcodeProperties(
        project: parent,
        buildInfo: BuildInfo.dummy,
        targetOverride: bundle.defaultMainPath,
      );
    }
  }

  Future<void> _updateLLDBIfNeeded() async {
    if (globals.cache.isOlderThanToolsStamp(lldbInitFile) ||
        globals.cache.isOlderThanToolsStamp(lldbHelperPythonFile)) {
      await _renderTemplateToFile(_lldbInitTemplate, null, lldbInitFile, globals.templateRenderer);
      await _renderTemplateToFile(
        _lldbPythonHelperTemplate,
        null,
        lldbHelperPythonFile,
        globals.templateRenderer,
      );
    }
  }

  Future<void> _renderTemplateToFile(
    String template,
    Object? context,
    File file,
    TemplateRenderer templateRenderer,
  ) async {
    final String renderedTemplate = templateRenderer.renderString(template, context);
    await file.create(recursive: true);
    await file.writeAsString(renderedTemplate);
  }

  Future<void> _regenerateModuleFromTemplateIfNeeded() async {
    if (!isModule) {
      return;
    }
    final bool pubspecChanged = globals.fsUtils.isOlderThanReference(
      entity: ephemeralModuleDirectory,
      referenceFile: parent.pubspecFile,
    );
    final bool toolingChanged = globals.cache.isOlderThanToolsStamp(ephemeralModuleDirectory);
    if (!pubspecChanged && !toolingChanged) {
      return;
    }

    ErrorHandlingFileSystem.deleteIfExists(ephemeralModuleDirectory, recursive: true);
    await _overwriteFromTemplate(
      globals.fs.path.join('module', 'ios', 'library'),
      ephemeralModuleDirectory,
    );
    // Add ephemeral host app, if a editable host app does not already exist.
    if (!_editableDirectory.existsSync()) {
      await _overwriteFromTemplate(
        globals.fs.path.join('module', 'ios', 'host_app_ephemeral'),
        ephemeralModuleDirectory,
      );
      if (hasPlugins(parent)) {
        await _overwriteFromTemplate(
          globals.fs.path.join('module', 'ios', 'host_app_ephemeral_cocoapods'),
          ephemeralModuleDirectory,
        );
      }
    }
  }

  @override
  File get generatedXcodePropertiesFile =>
      _flutterLibRoot.childDirectory('Flutter').childFile('Generated.xcconfig');

  /// No longer compiled to this location.
  ///
  /// Used only for "flutter clean" to remove old references.
  Directory get deprecatedCompiledDartFramework =>
      _flutterLibRoot.childDirectory('Flutter').childDirectory('App.framework');

  /// No longer copied to this location.
  ///
  /// Used only for "flutter clean" to remove old references.
  Directory get deprecatedProjectFlutterFramework =>
      _flutterLibRoot.childDirectory('Flutter').childDirectory('Flutter.framework');

  /// Used only for "flutter clean" to remove old references.
  File get flutterPodspec => _flutterLibRoot.childDirectory('Flutter').childFile('Flutter.podspec');

  Directory get pluginRegistrantHost {
    return isModule
        ? _flutterLibRoot.childDirectory('Flutter').childDirectory('FlutterPluginRegistrant')
        : hostAppRoot.childDirectory(XcodeBasedProject._defaultHostAppName);
  }

  File get pluginRegistrantHeader {
    final Directory registryDirectory = isModule
        ? pluginRegistrantHost.childDirectory('Classes')
        : pluginRegistrantHost;
    return registryDirectory.childFile('GeneratedPluginRegistrant.h');
  }

  File get pluginRegistrantImplementation {
    final Directory registryDirectory = isModule
        ? pluginRegistrantHost.childDirectory('Classes')
        : pluginRegistrantHost;
    return registryDirectory.childFile('GeneratedPluginRegistrant.m');
  }

  File get lldbInitFile {
    return ephemeralDirectory.childFile('flutter_lldbinit');
  }

  File get lldbHelperPythonFile {
    return ephemeralDirectory.childFile(_lldbPythonHelperTemplateName);
  }

  Future<void> _overwriteFromTemplate(String path, Directory target) async {
    final Template template = await Template.fromName(
      path,
      fileSystem: globals.fs,
      templateManifest: null,
      logger: globals.logger,
      templateRenderer: globals.templateRenderer,
    );
    final String iosBundleIdentifier =
        parent.manifest.iosBundleIdentifier ?? 'com.example.${parent.manifest.appName}';

    final String? iosDevelopmentTeam = await getCodeSigningIdentityDevelopmentTeam(
      processManager: globals.processManager,
      platform: globals.platform,
      logger: globals.logger,
      config: globals.config,
      terminal: globals.terminal,
      fileSystem: globals.fs,
      fileSystemUtils: globals.fsUtils,
      plistParser: globals.plistParser,
    );

    final String projectName = parent.manifest.appName;

    // The dart project_name is in snake_case, this variable is the Title Case of the Project Name.
    final String titleCaseProjectName = snakeCaseToTitleCase(projectName);

    template.render(target, <String, Object>{
      'ios': true,
      'projectName': projectName,
      'titleCaseProjectName': titleCaseProjectName,
      'iosIdentifier': iosBundleIdentifier,
      'hasIosDevelopmentTeam': iosDevelopmentTeam != null && iosDevelopmentTeam.isNotEmpty,
      'iosDevelopmentTeam': iosDevelopmentTeam ?? '',
    }, printStatusWhenWriting: false);
  }
}

/// The macOS sub project.
class MacOSProject extends XcodeBasedProject {
  MacOSProject.fromFlutter(this.parent);

  @override
  final FlutterProject parent;

  @override
  String get pluginConfigKey => MacOSPlugin.kConfigKey;

  @override
  bool existsSync() => hostAppRoot.existsSync();

  @override
  Directory get hostAppRoot => parent.directory.childDirectory('macos');

  /// The xcfilelist used to track the inputs for the Flutter script phase in
  /// the Xcode build.
  File get inputFileList => ephemeralDirectory.childFile('FlutterInputs.xcfilelist');

  /// The xcfilelist used to track the outputs for the Flutter script phase in
  /// the Xcode build.
  File get outputFileList => ephemeralDirectory.childFile('FlutterOutputs.xcfilelist');

  @override
  File get generatedXcodePropertiesFile =>
      ephemeralDirectory.childFile('Flutter-Generated.xcconfig');

  File get pluginRegistrantImplementation =>
      managedDirectory.childFile('GeneratedPluginRegistrant.swift');

  /// The 'AppDelegate.swift' file of the host app. This file might not exist if the app project uses Objective-C.
  File get appDelegateSwift => hostAppRoot.childDirectory('Runner').childFile('AppDelegate.swift');

  @override
  File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig');

  @override
  File get generatedEnvironmentVariableExportScript =>
      ephemeralDirectory.childFile('flutter_export_environment.sh');

  /// The file where the Xcode build will write the name of the built app.
  ///
  /// Ideally this will be replaced in the future with inspection of the Runner
  /// scheme's target.
  File get nameFile => ephemeralDirectory.childFile('.app_filename');

  Future<void> ensureReadyForPlatformSpecificTooling() async {
    // TODO(stuartmorgan): Add create-from-template logic here.
    await _updateGeneratedXcodeConfigIfNeeded();
  }

  Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
    if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
      await xcode.updateGeneratedXcodeProperties(
        project: parent,
        buildInfo: BuildInfo.dummy,
        useMacOSConfig: true,
      );
    }
  }
}
