// 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 '../asset.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../bundle_builder.dart';
import '../convert.dart';
import '../devfs.dart';
import '../globals.dart' as globals;
import '../project.dart';

import 'fuchsia_pm.dart';

Future<void> _timedBuildStep(String name, Future<void> Function() action) async {
  final Stopwatch sw = Stopwatch()..start();
  await action();
  globals.printTrace('$name: ${sw.elapsedMilliseconds} ms.');
  globals.flutterUsage.sendTiming('build', name, Duration(milliseconds: sw.elapsedMilliseconds));
}

Future<void> _validateCmxFile(FuchsiaProject fuchsiaProject) async {
  final String appName = fuchsiaProject.project.manifest.appName;
  final String cmxPath = globals.fs.path.join(fuchsiaProject.meta.path, '$appName.cmx');
  final File cmxFile = globals.fs.file(cmxPath);
  if (!await cmxFile.exists()) {
    throwToolExit('The Fuchsia build requires a .cmx file at $cmxPath for the app: $appName.');
  }
}

// Building a Fuchsia package has a few steps:
// 1. Do the custom kernel compile using the kernel compiler from the Fuchsia
//    SDK. This produces .dilp files (among others) and a manifest file.
// 2. Create a manifest file for assets.
// 3. Using these manifests, use the Fuchsia SDK 'pm' tool to create the
//    Fuchsia package.
Future<void> buildFuchsia({
  required FuchsiaProject fuchsiaProject,
  required TargetPlatform targetPlatform,
  String? target, // E.g., lib/main.dart
  BuildInfo buildInfo = BuildInfo.debug,
  String runnerPackageSource = FuchsiaPackageServer.toolHost,
}) async {
  final String targetPath = target ??= 'lib/main.dart';
  await _validateCmxFile(fuchsiaProject);
  final Directory outDir = globals.fs.directory(getFuchsiaBuildDirectory());
  if (!outDir.existsSync()) {
    outDir.createSync(recursive: true);
  }

  await _timedBuildStep('fuchsia-kernel-compile',
    () => globals.fuchsiaSdk!.fuchsiaKernelCompiler.build(
      fuchsiaProject: fuchsiaProject, target: targetPath, buildInfo: buildInfo));

  if (buildInfo.usesAot) {
    await _timedBuildStep('fuchsia-gen-snapshot',
      () => _genSnapshot(fuchsiaProject, targetPath, buildInfo, targetPlatform));
  }

  await _timedBuildStep('fuchsia-build-assets',
    () => _buildAssets(fuchsiaProject, targetPath, buildInfo));
  await _timedBuildStep('fuchsia-build-package',
    () => _buildPackage(fuchsiaProject, targetPath, buildInfo, runnerPackageSource));
}

Future<void> _genSnapshot(
  FuchsiaProject fuchsiaProject,
  String target, // lib/main.dart
  BuildInfo buildInfo,
  TargetPlatform targetPlatform,
) async {
  final String outDir = getFuchsiaBuildDirectory();
  final String appName = fuchsiaProject.project.manifest.appName;
  final String dilPath = globals.fs.path.join(outDir, '$appName.dil');

  final String elf = globals.fs.path.join(outDir, 'elf.aotsnapshot');

  final String genSnapshot = globals.artifacts!.getArtifactPath(
    Artifact.genSnapshot,
    platform: targetPlatform,
    mode: buildInfo.mode,
  );

  final List<String> command = <String>[
    genSnapshot,
    '--deterministic',
    '--snapshot_kind=app-aot-elf',
    '--elf=$elf',
    if (buildInfo.isDebug) '--enable-asserts',
    dilPath,
  ];
  int result;
  final Status status = globals.logger.startProgress(
    'Compiling Fuchsia application to native code...',
  );
  try {
    result = await globals.processUtils.stream(command, trace: true);
  } finally {
    status.cancel();
  }
  if (result != 0) {
    throwToolExit('Build process failed');
  }
}

Future<void> _buildAssets(
  FuchsiaProject fuchsiaProject,
  String target, // lib/main.dart
  BuildInfo buildInfo,
) async {
  final String assetDir = getAssetBuildDirectory();
  final AssetBundle? assets = await buildAssets(
    manifestPath: fuchsiaProject.project.pubspecFile.path,
    packagesPath: fuchsiaProject.project.packagesFile.path,
    assetDirPath: assetDir,
  );

  if (assets == null) {
    throwToolExit('Unable to find assets.', exitCode: 1);
  }

  final Map<String, DevFSContent> assetEntries =
      Map<String, DevFSContent>.of(assets.entries);
  await writeBundle(globals.fs.directory(assetDir), assetEntries);

  final String appName = fuchsiaProject.project.manifest.appName;
  final String outDir = getFuchsiaBuildDirectory();
  final String assetManifest = globals.fs.path.join(outDir, '${appName}_pkgassets');

  final File destFile = globals.fs.file(assetManifest);
  await destFile.create(recursive: true);
  final IOSink outFile = destFile.openWrite();

  for (final String path in assets.entries.keys) {
    outFile.write('data/$appName/$path=$assetDir/$path\n');
  }
  await outFile.flush();
  await outFile.close();
}

void _rewriteCmx(BuildMode mode, String runnerPackageSource, File src, File dst) {
  final Map<String, Object?> cmx = castStringKeyedMap(json.decode(src.readAsStringSync())) ?? <String, Object?>{};
  // If the app author has already specified the runner in the cmx file, then
  // do not override it with something else.
  if (cmx.containsKey('runner')) {
    dst.writeAsStringSync(json.encode(cmx));
    return;
  }
  String runner;
  switch (mode) {
    case BuildMode.debug:
      runner = 'flutter_jit_runner';
      break;
    case BuildMode.profile:
      runner = 'flutter_aot_runner';
      break;
    case BuildMode.jitRelease:
      runner = 'flutter_jit_product_runner';
      break;
    case BuildMode.release:
      runner = 'flutter_aot_product_runner';
      break;
    default:
      throwToolExit('Fuchsia does not support build mode "$mode"');
  }
  cmx['runner'] = 'fuchsia-pkg://$runnerPackageSource/$runner#meta/$runner.cmx';
  dst.writeAsStringSync(json.encode(cmx));
}

// TODO(zanderso): Allow supplying a signing key.
Future<void> _buildPackage(
  FuchsiaProject fuchsiaProject,
  String target, // lib/main.dart
  BuildInfo buildInfo,
  String runnerPackageSource,
) async {
  final String outDir = getFuchsiaBuildDirectory();
  final String pkgDir = globals.fs.path.join(outDir, 'pkg');
  final String appName = fuchsiaProject.project.manifest.appName;
  final String pkgassets = globals.fs.path.join(outDir, '${appName}_pkgassets');
  final String packageManifest = globals.fs.path.join(pkgDir, 'package_manifest');

  final Directory pkg = globals.fs.directory(pkgDir);
  if (!pkg.existsSync()) {
    pkg.createSync(recursive: true);
  }

  final File srcCmx =
      globals.fs.file(globals.fs.path.join(fuchsiaProject.meta.path, '$appName.cmx'));
  final File dstCmx = globals.fs.file(globals.fs.path.join(outDir, '$appName.cmx'));
  _rewriteCmx(buildInfo.mode, runnerPackageSource, srcCmx, dstCmx);

  final File manifestFile = globals.fs.file(packageManifest);

  if (buildInfo.usesAot) {
    final String elf = globals.fs.path.join(outDir, 'elf.aotsnapshot');
    manifestFile.writeAsStringSync(
      'data/$appName/app_aot_snapshot.so=$elf\n');
  } else {
    final String dilpmanifest = globals.fs.path.join(outDir, '$appName.dilpmanifest');
    manifestFile.writeAsStringSync(globals.fs.file(dilpmanifest).readAsStringSync());
  }

  manifestFile.writeAsStringSync(globals.fs.file(pkgassets).readAsStringSync(),
      mode: FileMode.append);
  manifestFile.writeAsStringSync('meta/$appName.cmx=${dstCmx.path}\n',
      mode: FileMode.append);
  manifestFile.writeAsStringSync('meta/package=$pkgDir/meta/package\n',
      mode: FileMode.append);

  final FuchsiaPM? fuchsiaPM = globals.fuchsiaSdk?.fuchsiaPM;
  if (fuchsiaPM == null) {
    return;
  }
  if (!await fuchsiaPM.init(pkgDir, appName)) {
    return;
  }
  if (!await fuchsiaPM.build(pkgDir, packageManifest)) {
    return;
  }
  if (!await fuchsiaPM.archive(pkgDir, packageManifest)) {
    return;
  }
}
