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

import '../artifacts.dart';
import '../build_info.dart';
import '../macos/xcode.dart';

import 'file_system.dart';
import 'logger.dart';
import 'process.dart';

/// A snapshot build configuration.
class SnapshotType {
  SnapshotType(this.platform, this.mode)
    : assert(mode != null);

  final TargetPlatform? platform;
  final BuildMode mode;

  @override
  String toString() => '$platform $mode';
}

/// Interface to the gen_snapshot command-line tool.
class GenSnapshot {
  GenSnapshot({
    required Artifacts artifacts,
    required ProcessManager processManager,
    required Logger logger,
  }) : _artifacts = artifacts,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager);

  final Artifacts _artifacts;
  final ProcessUtils _processUtils;

  String getSnapshotterPath(SnapshotType snapshotType) {
    return _artifacts.getArtifactPath(
        Artifact.genSnapshot, platform: snapshotType.platform, mode: snapshotType.mode);
  }

  /// Ignored warning messages from gen_snapshot.
  static const Set<String> kIgnoredWarnings = <String>{
    // --strip on elf snapshot.
    'Warning: Generating ELF library without DWARF debugging information.',
    // --strip on ios-assembly snapshot.
    'Warning: Generating assembly code without DWARF debugging information.',
    // A fun two-part message with spaces for obfuscation.
    'Warning: This VM has been configured to obfuscate symbol information which violates the Dart standard.',
    '         See dartbug.com/30524 for more information.',
  };

  Future<int> run({
    required SnapshotType snapshotType,
    DarwinArch? darwinArch,
    Iterable<String> additionalArgs = const <String>[],
  }) {
    assert(darwinArch != DarwinArch.armv7);
    assert(snapshotType.platform != TargetPlatform.ios || darwinArch != null);
    final List<String> args = <String>[
      ...additionalArgs,
    ];

    String snapshotterPath = getSnapshotterPath(snapshotType);

    // iOS and macOS have separate gen_snapshot binaries for each target
    // architecture (iOS: armv7, arm64; macOS: x86_64, arm64). Select the right
    // one for the target architecture in question.
    if (snapshotType.platform == TargetPlatform.ios ||
        snapshotType.platform == TargetPlatform.darwin) {
      snapshotterPath += '_${getDartNameForDarwinArch(darwinArch!)}';
    }

    return _processUtils.stream(
      <String>[snapshotterPath, ...args],
      mapFunction: (String line) =>  kIgnoredWarnings.contains(line) ? null : line,
    );
  }
}

class AOTSnapshotter {
  AOTSnapshotter({
    this.reportTimings = false,
    required Logger logger,
    required FileSystem fileSystem,
    required Xcode xcode,
    required ProcessManager processManager,
    required Artifacts artifacts,
  }) : _logger = logger,
      _fileSystem = fileSystem,
      _xcode = xcode,
      _genSnapshot = GenSnapshot(
        artifacts: artifacts,
        processManager: processManager,
        logger: logger,
      );

  final Logger _logger;
  final FileSystem _fileSystem;
  final Xcode _xcode;
  final GenSnapshot _genSnapshot;

  /// If true then AOTSnapshotter would report timings for individual building
  /// steps (Dart front-end parsing and snapshot generation) in a stable
  /// machine readable form. See [AOTSnapshotter._timedStep].
  final bool reportTimings;

  /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script.
  Future<int> build({
    required TargetPlatform platform,
    required BuildMode buildMode,
    required String mainPath,
    required String outputPath,
    DarwinArch? darwinArch,
    String? sdkRoot,
    List<String> extraGenSnapshotOptions = const <String>[],
    String? splitDebugInfo,
    required bool dartObfuscation,
    bool quiet = false,
  }) async {
    assert(platform != TargetPlatform.ios || darwinArch != null);

    if (!_isValidAotPlatform(platform, buildMode)) {
      _logger.printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
      return 1;
    }

    final Directory outputDir = _fileSystem.directory(outputPath);
    outputDir.createSync(recursive: true);

    final List<String> genSnapshotArgs = <String>[
      '--deterministic',
    ];

    final bool targetingApplePlatform =
        platform == TargetPlatform.ios || platform == TargetPlatform.darwin;
    _logger.printTrace('targetingApplePlatform = $targetingApplePlatform');

    final bool extractAppleDebugSymbols =
        buildMode == BuildMode.profile || buildMode == BuildMode.release;
    _logger.printTrace('extractAppleDebugSymbols = $extractAppleDebugSymbols');

    // We strip snapshot by default, but allow to suppress this behavior
    // by supplying --no-strip in extraGenSnapshotOptions.
    bool shouldStrip = true;
    if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) {
      _logger.printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
      for (final String option in extraGenSnapshotOptions) {
        if (option == '--no-strip') {
          shouldStrip = false;
          continue;
        }
        genSnapshotArgs.add(option);
      }
    }

    final String assembly = _fileSystem.path.join(outputDir.path, 'snapshot_assembly.S');
    if (targetingApplePlatform) {
      genSnapshotArgs.addAll(<String>[
        '--snapshot_kind=app-aot-assembly',
        '--assembly=$assembly',
      ]);
    } else {
      final String aotSharedLibrary = _fileSystem.path.join(outputDir.path, 'app.so');
      genSnapshotArgs.addAll(<String>[
        '--snapshot_kind=app-aot-elf',
        '--elf=$aotSharedLibrary',
      ]);
    }

    // When buiding for iOS and splitting out debug info, we want to strip
    // manually after the dSYM export, instead of in the `gen_snapshot`.
    final bool stripAfterBuild;
    if (targetingApplePlatform) {
      stripAfterBuild = shouldStrip;
      if (stripAfterBuild) {
        _logger.printTrace('Will strip AOT snapshot manually after build and dSYM generation.');
      }
    } else {
      stripAfterBuild = false;
      if (shouldStrip) {
        genSnapshotArgs.add('--strip');
        _logger.printTrace('Will strip AOT snapshot during build.');
      }
    }

    if (platform == TargetPlatform.android_arm) {
      // Use softfp for Android armv7 devices.
      // TODO(cbracken): eliminate this when we fix https://github.com/flutter/flutter/issues/17489
      genSnapshotArgs.add('--no-sim-use-hardfp');

      // Not supported by the Pixel in 32-bit mode.
      genSnapshotArgs.add('--no-use-integer-division');
    }

    // The name of the debug file must contain additional information about
    // the architecture, since a single build command may produce
    // multiple debug files.
    final String archName = getNameForTargetPlatform(platform, darwinArch: darwinArch);
    final String debugFilename = 'app.$archName.symbols';
    final bool shouldSplitDebugInfo = splitDebugInfo?.isNotEmpty ?? false;
    if (shouldSplitDebugInfo) {
      _fileSystem.directory(splitDebugInfo)
        .createSync(recursive: true);
    }

    // Optimization arguments.
    genSnapshotArgs.addAll(<String>[
      // Faster async/await
      if (shouldSplitDebugInfo) ...<String>[
        '--dwarf-stack-traces',
        '--save-debugging-info=${_fileSystem.path.join(splitDebugInfo!, debugFilename)}',
      ],
      if (dartObfuscation)
        '--obfuscate',
    ]);

    genSnapshotArgs.add(mainPath);

    final SnapshotType snapshotType = SnapshotType(platform, buildMode);
    final int genSnapshotExitCode = await _genSnapshot.run(
      snapshotType: snapshotType,
      additionalArgs: genSnapshotArgs,
      darwinArch: darwinArch,
    );
    if (genSnapshotExitCode != 0) {
      _logger.printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
      return genSnapshotExitCode;
    }

    // On iOS and macOS, we use Xcode to compile the snapshot into a dynamic library that the
    // end-developer can link into their app.
    if (targetingApplePlatform) {
      return _buildFramework(
        appleArch: darwinArch!,
        isIOS: platform == TargetPlatform.ios,
        sdkRoot: sdkRoot,
        assemblyPath: assembly,
        outputPath: outputDir.path,
        quiet: quiet,
        stripAfterBuild: stripAfterBuild,
        extractAppleDebugSymbols: extractAppleDebugSymbols
      );
    } else {
      return 0;
    }
  }

  /// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly
  /// source at [assemblyPath].
  Future<int> _buildFramework({
    required DarwinArch appleArch,
    required bool isIOS,
    String? sdkRoot,
    required String assemblyPath,
    required String outputPath,
    required bool quiet,
    required bool stripAfterBuild,
    required bool extractAppleDebugSymbols
  }) async {
    final String targetArch = getNameForDarwinArch(appleArch);
    if (!quiet) {
      _logger.printStatus('Building App.framework for $targetArch...');
    }

    final List<String> commonBuildOptions = <String>[
      '-arch',
      targetArch,
      if (isIOS)
        // When the minimum version is updated, remember to update
        // template MinimumOSVersion.
        // https://github.com/flutter/flutter/pull/62902
        '-miphoneos-version-min=11.0',
      if (sdkRoot != null) ...<String>[
        '-isysroot',
        sdkRoot,
      ],
    ];

    final String assemblyO = _fileSystem.path.join(outputPath, 'snapshot_assembly.o');

    final RunResult compileResult = await _xcode.cc(<String>[
      ...commonBuildOptions,
      '-c',
      assemblyPath,
      '-o',
      assemblyO,
    ]);
    if (compileResult.exitCode != 0) {
      _logger.printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
      return compileResult.exitCode;
    }

    final String frameworkDir = _fileSystem.path.join(outputPath, 'App.framework');
    _fileSystem.directory(frameworkDir).createSync(recursive: true);
    final String appLib = _fileSystem.path.join(frameworkDir, 'App');
    final List<String> linkArgs = <String>[
      ...commonBuildOptions,
      '-dynamiclib',
      '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
      '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
      '-install_name', '@rpath/App.framework/App',
      '-o', appLib,
      assemblyO,
    ];

    final RunResult linkResult = await _xcode.clang(linkArgs);
    if (linkResult.exitCode != 0) {
      _logger.printError('Failed to link AOT snapshot. Linker terminated with exit code ${linkResult.exitCode}');
      return linkResult.exitCode;
    }

    if (extractAppleDebugSymbols) {
      final RunResult dsymResult = await _xcode.dsymutil(<String>['-o', '$frameworkDir.dSYM', appLib]);
      if (dsymResult.exitCode != 0) {
        _logger.printError('Failed to generate dSYM - dsymutil terminated with exit code ${dsymResult.exitCode}');
        return dsymResult.exitCode;
      }

      if (stripAfterBuild) {
        // See https://www.unix.com/man-page/osx/1/strip/ for arguments
        final RunResult stripResult = await _xcode.strip(<String>['-x', appLib, '-o', appLib]);
        if (stripResult.exitCode != 0) {
          _logger.printError('Failed to strip debugging symbols from the generated AOT snapshot - strip terminated with exit code ${stripResult.exitCode}');
          return stripResult.exitCode;
        }
      }
    } else {
      assert(stripAfterBuild == false);
    }

    return 0;
  }

  bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) {
    if (buildMode == BuildMode.debug) {
      return false;
    }
    return const <TargetPlatform>[
      TargetPlatform.android_arm,
      TargetPlatform.android_arm64,
      TargetPlatform.android_x64,
      TargetPlatform.ios,
      TargetPlatform.darwin,
      TargetPlatform.linux_x64,
      TargetPlatform.linux_arm64,
      TargetPlatform.windows_x64,
    ].contains(platform);
  }
}
