// 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 'dart:async';

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

import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/user_messages.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
import '../ios/xcodeproj.dart';

Version get xcodeRequiredVersion => Version(14, null, null);

/// Diverging this number from the minimum required version will provide a doctor
/// warning, not error, that users should upgrade Xcode.
Version get xcodeRecommendedVersion => xcodeRequiredVersion;

/// SDK name passed to `xcrun --sdk`. Corresponds to undocumented Xcode
/// SUPPORTED_PLATFORMS values.
///
/// Usage: xcrun [options] <tool name> ... arguments ...
/// ...
/// --sdk <sdk name>            find the tool for the given SDK name.
String getSDKNameForIOSEnvironmentType(EnvironmentType environmentType) {
  return (environmentType == EnvironmentType.simulator)
      ? 'iphonesimulator'
      : 'iphoneos';
}

/// A utility class for interacting with Xcode command line tools.
class Xcode {
  Xcode({
    required Platform platform,
    required ProcessManager processManager,
    required Logger logger,
    required FileSystem fileSystem,
    required XcodeProjectInterpreter xcodeProjectInterpreter,
    required UserMessages userMessages,
    String? flutterRoot,
  })  : _platform = platform,
        _fileSystem = fileSystem,
        _xcodeProjectInterpreter = xcodeProjectInterpreter,
        _userMessage = userMessages,
        _flutterRoot = flutterRoot,
        _processUtils =
            ProcessUtils(logger: logger, processManager: processManager),
        _logger = logger;

  /// Create an [Xcode] for testing.
  ///
  /// Defaults to a memory file system, fake platform,
  /// buffer logger, and test [XcodeProjectInterpreter].
  @visibleForTesting
  factory Xcode.test({
    required ProcessManager processManager,
    XcodeProjectInterpreter? xcodeProjectInterpreter,
    Platform? platform,
    FileSystem? fileSystem,
    String? flutterRoot,
    Logger? logger,
  }) {
    platform ??= FakePlatform(
      operatingSystem: 'macos',
      environment: <String, String>{},
    );
    logger ??= BufferLogger.test();
    return Xcode(
      platform: platform,
      processManager: processManager,
      fileSystem: fileSystem ?? MemoryFileSystem.test(),
      userMessages: UserMessages(),
      flutterRoot: flutterRoot,
      logger: logger,
      xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
    );
  }

  final Platform _platform;
  final ProcessUtils _processUtils;
  final FileSystem _fileSystem;
  final XcodeProjectInterpreter _xcodeProjectInterpreter;
  final UserMessages _userMessage;
  final String? _flutterRoot;
  final Logger _logger;

  bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;

  String? _xcodeSelectPath;
  String? get xcodeSelectPath {
    if (_xcodeSelectPath == null) {
      try {
        _xcodeSelectPath = _processUtils.runSync(
          <String>['/usr/bin/xcode-select', '--print-path'],
        ).stdout.trim();
      } on ProcessException {
        // Ignored, return null below.
      } on ArgumentError {
        // Ignored, return null below.
      }
    }
    return _xcodeSelectPath;
  }

  String get xcodeAppPath {
    // If the Xcode Select Path is /Applications/Xcode.app/Contents/Developer,
    // the path to Xcode App is /Applications/Xcode.app

    final String? pathToXcode = xcodeSelectPath;
    if (pathToXcode == null || pathToXcode.isEmpty) {
      throwToolExit(_userMessage.xcodeMissing);
    }
    final int index = pathToXcode.indexOf('.app');
    if (index == -1) {
      throwToolExit(_userMessage.xcodeMissing);
    }
    return pathToXcode.substring(0, index + 4);
  }

  /// Path to script to automate debugging through Xcode. Used in xcode_debug.dart.
  /// Located in this file to make it easily overrideable in google3.
  String get xcodeAutomationScriptPath {
    final String flutterRoot = _flutterRoot ?? Cache.flutterRoot!;
    final String flutterToolsAbsolutePath = _fileSystem.path.join(
      flutterRoot,
      'packages',
      'flutter_tools',
    );

    final String filePath = '$flutterToolsAbsolutePath/bin/xcode_debug.js';
    if (!_fileSystem.file(filePath).existsSync()) {
      throwToolExit('Unable to find Xcode automation script at $filePath');
    }
    return filePath;
  }

  bool get isInstalled => _xcodeProjectInterpreter.isInstalled;

  Version? get currentVersion => _xcodeProjectInterpreter.version;

  String? get buildVersion => _xcodeProjectInterpreter.build;

  String? get versionText => _xcodeProjectInterpreter.versionText;

  bool? _eulaSigned;
  /// Has the EULA been signed?
  bool get eulaSigned {
    if (_eulaSigned == null) {
      try {
        final RunResult result = _processUtils.runSync(
          <String>[...xcrunCommand(), 'clang'],
        );
        if (result.stdout.contains('license')) {
          _eulaSigned = false;
        } else if (result.stderr.contains('license')) {
          _eulaSigned = false;
        } else {
          _eulaSigned = true;
        }
      } on ProcessException {
        _eulaSigned = false;
      }
    }
    return _eulaSigned ?? false;
  }

  bool? _isSimctlInstalled;

  /// Verifies that simctl is installed by trying to run it.
  bool get isSimctlInstalled {
    if (_isSimctlInstalled == null) {
      try {
        // This command will error if additional components need to be installed in
        // xcode 9.2 and above.
        final RunResult result = _processUtils.runSync(
          <String>[...xcrunCommand(), 'simctl', 'list', 'devices', 'booted'],
        );
        _isSimctlInstalled = result.exitCode == 0;
      } on ProcessException {
        _isSimctlInstalled = false;
      }
    }
    return _isSimctlInstalled ?? false;
  }

  bool? _isDevicectlInstalled;

  /// Verifies that `devicectl` is installed by checking Xcode version and trying
  /// to run it. `devicectl` is made available in Xcode 15.
  bool get isDevicectlInstalled {
    if (_isDevicectlInstalled == null) {
      try {
        if (currentVersion == null || currentVersion!.major < 15) {
          _isDevicectlInstalled = false;
          return _isDevicectlInstalled!;
        }
        final RunResult result = _processUtils.runSync(
          <String>[...xcrunCommand(), 'devicectl', '--version'],
        );
        _isDevicectlInstalled = result.exitCode == 0;
      } on ProcessException {
        _isDevicectlInstalled = false;
      }
    }
    return _isDevicectlInstalled ?? false;
  }

  bool get isRequiredVersionSatisfactory {
    final Version? version = currentVersion;
    if (version == null) {
      return false;
    }
    return version >= xcodeRequiredVersion;
  }

  bool get isRecommendedVersionSatisfactory {
    final Version? version = currentVersion;
    if (version == null) {
      return false;
    }
    return version >= xcodeRecommendedVersion;
  }

  /// See [XcodeProjectInterpreter.xcrunCommand].
  List<String> xcrunCommand() => _xcodeProjectInterpreter.xcrunCommand();

  Future<RunResult> cc(List<String> args) => _run('cc', args);

  Future<RunResult> clang(List<String> args) => _run('clang', args);

  Future<RunResult> dsymutil(List<String> args) => _run('dsymutil', args);

  Future<RunResult> strip(List<String> args) => _run('strip', args);

  Future<RunResult> _run(String command, List<String> args) {
    return _processUtils.run(
      <String>[...xcrunCommand(), command, ...args],
      throwOnError: true,
    );
  }

  Future<String> sdkLocation(EnvironmentType environmentType) async {
    final RunResult runResult = await _processUtils.run(
      <String>[...xcrunCommand(), '--sdk', getSDKNameForIOSEnvironmentType(environmentType), '--show-sdk-path'],
    );
    if (runResult.exitCode != 0) {
      throwToolExit('Could not find SDK location: ${runResult.stderr}');
    }
    return runResult.stdout.trim();
  }

  String? getSimulatorPath() {
    final String? selectPath = xcodeSelectPath;
    if (selectPath == null) {
      return null;
    }
    final String appPath = _fileSystem.path.join(selectPath, 'Applications', 'Simulator.app');
    return _fileSystem.directory(appPath).existsSync() ? appPath : null;
  }

  /// Gets the version number of the platform for the selected SDK.
  Future<Version?> sdkPlatformVersion(EnvironmentType environmentType) async {
    final RunResult runResult = await _processUtils.run(
      <String>[...xcrunCommand(), '--sdk', getSDKNameForIOSEnvironmentType(environmentType), '--show-sdk-platform-version'],
    );
    if (runResult.exitCode != 0) {
      _logger.printError('Could not find SDK Platform Version: ${runResult.stderr}');
      return null;
    }
    final String versionString = runResult.stdout.trim();
    return Version.parse(versionString);
  }
}

EnvironmentType? environmentTypeFromSdkroot(String sdkroot, FileSystem fileSystem) {
  // iPhoneSimulator.sdk or iPhoneOS.sdk
  final String sdkName = fileSystem.path.basename(sdkroot).toLowerCase();
  if (sdkName.contains('iphone')) {
    return sdkName.contains('simulator') ? EnvironmentType.simulator : EnvironmentType.physical;
  }
  assert(false);
  return null;
}
