// 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 '../../artifacts.dart';
import '../../base/build.dart';
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../base/process.dart';
import '../../build_info.dart';
import '../../globals.dart' as globals;
import '../../macos/xcode.dart';
import '../build_system.dart';
import '../exceptions.dart';
import 'dart.dart';

/// Supports compiling a dart kernel file to an assembly file.
///
/// If more than one iOS arch is provided, then this rule will
/// produce a universal binary.
abstract class AotAssemblyBase extends Target {
  const AotAssemblyBase();

  @override
  Future<void> build(Environment environment) async {
    final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
    final String buildOutputPath = environment.buildDir.path;
    if (environment.defines[kBuildMode] == null) {
      throw MissingDefineException(kBuildMode, 'aot_assembly');
    }
    if (environment.defines[kTargetPlatform] == null) {
      throw MissingDefineException(kTargetPlatform, 'aot_assembly');
    }
    final bool bitcode = environment.defines[kBitcodeFlag] == 'true';
    final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
    final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
    final List<DarwinArch> iosArchs = environment.defines[kIosArchs]?.split(',')?.map(getIOSArchForName)?.toList()
        ?? <DarwinArch>[DarwinArch.arm64];
    if (targetPlatform != TargetPlatform.ios) {
      throw Exception('aot_assembly is only supported for iOS applications');
    }

    // If we're building for a single architecture (common), then skip the lipo.
    if (iosArchs.length == 1) {
      final int snapshotExitCode = await snapshotter.build(
        platform: targetPlatform,
        buildMode: buildMode,
        mainPath: environment.buildDir.childFile('app.dill').path,
        packagesPath: environment.projectDir.childFile('.packages').path,
        outputPath: environment.outputDir.path,
        darwinArch: iosArchs.single,
        bitcode: bitcode,
      );
      if (snapshotExitCode != 0) {
        throw Exception('AOT snapshotter exited with code $snapshotExitCode');
      }
    } else {
      // If we're building multiple iOS archs the binaries need to be lipo'd
      // together.
      final List<Future<int>> pending = <Future<int>>[];
      for (final DarwinArch iosArch in iosArchs) {
        pending.add(snapshotter.build(
          platform: targetPlatform,
          buildMode: buildMode,
          mainPath: environment.buildDir.childFile('app.dill').path,
          packagesPath: environment.projectDir.childFile('.packages').path,
          outputPath: globals.fs.path.join(buildOutputPath, getNameForDarwinArch(iosArch)),
          darwinArch: iosArch,
          bitcode: bitcode,
        ));
      }
      final List<int> results = await Future.wait(pending);
      if (results.any((int result) => result != 0)) {
        throw Exception('AOT snapshotter exited with code ${results.join()}');
      }
      final ProcessResult result = await globals.processManager.run(<String>[
        'lipo',
        ...iosArchs.map((DarwinArch iosArch) =>
            globals.fs.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')),
        '-create',
        '-output',
        globals.fs.path.join(environment.outputDir.path, 'App.framework', 'App'),
      ]);
      if (result.exitCode != 0) {
        throw Exception('lipo exited with code ${result.exitCode}');
      }
    }
  }
}

/// Generate an assembly target from a dart kernel file in release mode.
class AotAssemblyRelease extends AotAssemblyBase {
  const AotAssemblyRelease();

  @override
  String get name => 'aot_assembly_release';

  @override
  List<Source> get inputs => const <Source>[
    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
    Source.pattern('{BUILD_DIR}/app.dill'),
    Source.pattern('{PROJECT_DIR}/.packages'),
    Source.artifact(Artifact.engineDartBinary),
    Source.artifact(Artifact.skyEnginePath),
    Source.artifact(Artifact.genSnapshot,
      platform: TargetPlatform.ios,
      mode: BuildMode.release,
    ),
  ];

  @override
  List<Source> get outputs => const <Source>[
    Source.pattern('{OUTPUT_DIR}/App.framework/App'),
  ];

  @override
  List<Target> get dependencies => const <Target>[
    KernelSnapshot(),
  ];
}


/// Generate an assembly target from a dart kernel file in profile mode.
class AotAssemblyProfile extends AotAssemblyBase {
  const AotAssemblyProfile();

  @override
  String get name => 'aot_assembly_profile';

  @override
  List<Source> get inputs => const <Source>[
    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
    Source.pattern('{BUILD_DIR}/app.dill'),
    Source.pattern('{PROJECT_DIR}/.packages'),
    Source.artifact(Artifact.engineDartBinary),
    Source.artifact(Artifact.skyEnginePath),
    Source.artifact(Artifact.genSnapshot,
      platform: TargetPlatform.ios,
      mode: BuildMode.profile,
    ),
  ];

  @override
  List<Source> get outputs => const <Source>[
    Source.pattern('{OUTPUT_DIR}/App.framework/App'),
  ];

  @override
  List<Target> get dependencies => const <Target>[
    KernelSnapshot(),
  ];
}

/// Create an App.framework for debug iOS targets.
///
/// This framework needs to exist for the Xcode project to link/bundle,
/// but it isn't actually executed. To generate something valid, we compile a trivial
/// constant.
Future<RunResult> createStubAppFramework(File outputFile, SdkType sdk) async {
  try {
    outputFile.createSync(recursive: true);
  } catch (e) {
    throwToolExit('Failed to create App.framework stub at ${outputFile.path}');
  }

  final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_stub_source.');
  try {
    final File stubSource = tempDir.childFile('debug_app.cc')
      ..writeAsStringSync(r'''
  static const int Moo = 88;
  ''');

    List<String> archFlags;
    if (sdk == SdkType.iPhone) {
      archFlags = <String>[
        '-arch',
        getNameForDarwinArch(DarwinArch.armv7),
        '-arch',
        getNameForDarwinArch(DarwinArch.arm64),
      ];
    } else {
      archFlags = <String>[
        '-arch',
        getNameForDarwinArch(DarwinArch.x86_64),
      ];
    }

    return await xcode.clang(<String>[
      '-x',
      'c',
      ...archFlags,
      stubSource.path,
      '-dynamiclib',
      '-fembed-bitcode-marker',
      '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
      '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
      '-install_name', '@rpath/App.framework/App',
      '-isysroot', await xcode.sdkLocation(sdk),
      '-o', outputFile.path,
    ]);
  } finally {
    try {
      tempDir.deleteSync(recursive: true);
    } on FileSystemException catch (_) {
      // Best effort. Sometimes we can't delete things from system temp.
    } catch (e) {
      throwToolExit('Failed to create App.framework stub at ${outputFile.path}');
    }
  }
}