// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
// for details. 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:convert';
import 'dart:io';

import '../args_parser.dart';
import '../config.dart';
import '../validation.dart';

/// Builds assets in a `hook/build.dart`.
///
/// If a build hook is defined (`hook/build.dart`) then `build` must be called
/// by that hook, to write the [BuildInput.outputFile], even if the [builder]
/// function has no work to do.
///
/// Can build native assets which are not already available, or expose existing
/// files. Each individual asset is assigned a unique asset ID.
///
/// Example using `package:native_toolchain_c`:
///
/// <!-- file://./../../../example/api/build_snippet_1.dart -->
/// ```dart
/// import 'package:hooks/hooks.dart';
/// import 'package:native_toolchain_c/native_toolchain_c.dart';
///
/// void main(List<String> args) async {
///   await build(args, (input, output) async {
///     final packageName = input.packageName;
///     final cbuilder = CBuilder.library(
///       name: packageName,
///       assetName: '$packageName.dart',
///       sources: ['src/$packageName.c'],
///     );
///     await cbuilder.run(input: input, output: output);
///   });
/// }
/// ```
///
/// Example outputting assets manually:
///
/// <!-- file://./../../../example/api/build_snippet_2.dart -->
/// ```dart
/// import 'dart:io';
///
/// import 'package:code_assets/code_assets.dart';
/// import 'package:hooks/hooks.dart';
///
/// const assetName = 'asset.txt';
/// final packageAssetPath = Uri.file('data/$assetName');
///
/// void main(List<String> args) async {
///   await build(args, (input, output) async {
///     if (input.config.code.linkModePreference == .static) {
///       // Simulate that this hook only supports dynamic libraries.
///       throw UnsupportedError('LinkModePreference.static is not supported.');
///     }
///
///     final packageName = input.packageName;
///     final assetPath = input.outputDirectory.resolve(assetName);
///     final assetSourcePath = input.packageRoot.resolveUri(packageAssetPath);
///     // Insert code that downloads or builds the asset to `assetPath`.
///     await File.fromUri(assetSourcePath).copy(assetPath.toFilePath());
///
///     output.dependencies.add(assetSourcePath);
///
///     output.assets.code.add(
///       // TODO: Change to DataAsset once the Dart/Flutter SDK can consume it.
///       CodeAsset(
///         package: packageName,
///         name: 'asset.txt',
///         file: assetPath,
///         linkMode: DynamicLoadingBundled(),
///       ),
///     );
///   });
/// }
/// ```
///
/// ## Environment
///
/// Build hooks are executed in a semi-hermetic environment. This means that
/// `Platform.environment` does not expose all environment variables from the
/// parent process. This ensures that hook invocations are reproducible and
/// cacheable, and do not depend on accidental environment variables.
///
/// However, some environment variables are necessary for locating tools (like
/// compilers) or configuring network access. The following environment
/// variables are passed through to the hook process:
///
/// *   **Path and system roots:**
///     *   `PATH`: Invoke native tools.
///     *   `HOME`, `USERPROFILE`: Find tools in default install locations.
///     *   `SYSTEMDRIVE`, `SYSTEMROOT`, `WINDIR`: Process invocations and CMake
///         on Windows.
///     *   `PROGRAMDATA`: For `vswhere.exe` on Windows.
/// *   **Temporary directories:**
///     *   `TEMP`, `TMP`, `TMPDIR`: Temporary directories.
/// *   **HTTP proxies:**
///     *   `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`: Network access behind
///         proxies.
/// *   **Clang/LLVM:**
///     *   `LIBCLANG_PATH`: Rust's `bindgen` + `clang-sys`.
/// *   **Android NDK:**
///     *   `ANDROID_HOME`: Standard location for the Android SDK/NDK.
///     *   `ANDROID_NDK`, `ANDROID_NDK_HOME`, `ANDROID_NDK_LATEST_HOME`,
///         `ANDROID_NDK_ROOT`: Alternative locations for the NDK.
/// *   **Ccache:**
///     *   Any variable starting with `CCACHE_`.
/// *   **Nix:**
///     *   Any variable starting with `NIX_`.
///
/// Any changes to these environment variables will cause cache invalidation for
/// hooks.
///
/// All other environment variables are stripped.
///
/// ## Debugging
///
/// When a build hook doesn't work as expected, you can investigate the
/// intermediate files generated by the Dart and Flutter SDK build process.
///
/// The most important files for debugging are located in a subdirectory
/// specific to your hook's execution. The path is of the form
/// `.dart_tool/hooks_runner/<package_name>/<some_hash>/`, where
/// `<package_name>` is the name of the package containing the hook. Inside, you
/// will find:
///
/// * `input.json`: The configuration and data passed into your build hook.
/// * `output.json`: The JSON data that your build hook produced.
/// * `stdout.txt`: Any standard output from your build hook.
/// * `stderr.txt`: Any error messages or exceptions.
///
/// When you run a build, hooks for all dependencies are executed, so you might
/// see multiple package directories.
///
/// The `<some_hash>` is a checksum of the [BuildConfig] in the `input.json`. If
/// you are unsure which hash directory to inspect within your package's hook
/// directory, you can delete the `.dart_tool/hooks_runner/<package_name>/`
/// directory and re-run the command that failed. The newly created directory
/// will be for the latest invocation.
///
/// You can step through your code with a debugger by running the build hook
/// from its source file and providing the `input.json` via the `--config` flag:
///
/// ```sh
/// dart run hook/build.dart --config .dart_tool/hooks_runner/<package_name>/<some_hash>/input.json
/// ```
///
/// To debug in VS Code, you can create a `launch.json` file in a `.vscode`
/// directory in your project root. This allows you to run your hook with a
/// debugger attached.
///
/// Here is an example configuration:
///
/// ```json
/// {
///   "version": "0.2.0",
///   "configurations": [
///     {
///       "name": "Debug Build Hook",
///       "type": "dart",
///       "request": "launch",
///       "program": "hook/build.dart",
///       "args": [
///         "--config",
///         ".dart_tool/hooks_runner/your_package_name/some_hash/input.json"
///       ]
///     }
///   ]
/// }
/// ```
///
/// Again, make sure to replace `your_package_name`, and `some_hash` with the
/// actual paths from your project. After setting this up, you can run the
/// "Debug Build Hook" configuration from the "Run and Debug" view in VS Code.
Future<void> build(
  List<String> arguments,
  Future<void> Function(BuildInput input, BuildOutputBuilder output) builder,
) async {
  final inputPath = getInputArgument(arguments);
  final bytes = File(inputPath).readAsBytesSync();
  final jsonInput =
      const Utf8Decoder().fuse(const JsonDecoder()).convert(bytes)
          as Map<String, Object?>;
  final input = BuildInput(jsonInput);
  final outputFile = input.outputFile;
  final output = BuildOutputBuilder();
  try {
    await builder(input, output);
    // ignore: avoid_catching_errors
  } on HookError catch (e, st) {
    output.setFailure(e.failureType);
    await _writeOutput(output, outputFile);
    _exitViaHookException(e, st);
  }
  final errors = await ProtocolBase.validateBuildOutput(
    input,
    BuildOutput(output.json),
  );
  if (errors.isNotEmpty) {
    final message = [
      'The output contained unsupported output:',
      for (final error in errors) '- $error',
    ].join('\n');
    stderr.writeln(message);
    output.setFailure(.build);
    await _writeOutput(output, outputFile);
    exit(BuildError(message: message).exitCode);
  }

  await _writeOutput(output, outputFile);
}

/// Links assets in a `hook/link.dart`.
///
/// If a link hook is defined (`hook/link.dart`) then `link` must be called by
/// that hook, to write the [BuildInput.outputFile], even if the [linker]
/// function has no work to do.
///
/// Can link native assets which are not already available, or expose existing
/// files. Each individual asset is assigned a unique asset ID.
///
/// The linking script may receive assets from build scripts, which are accessed
/// through [LinkInputAssets.encodedAssets]. They will only be bundled with the
/// final application if included in the [LinkOutput].
///
///
/// <!-- file://./../../../example/api/link_snippet.dart -->
/// ```dart
/// import 'package:data_assets/data_assets.dart';
/// import 'package:hooks/hooks.dart';
///
/// void main(List<String> args) async {
///   await link(args, (input, output) async {
///     final dataEncodedAssets = input.assets.encodedAssets.where(
///       (e) => e.isDataAsset,
///     );
///     output.assets.addEncodedAssets(dataEncodedAssets);
///   });
/// }
/// ```
/// If the [linker] fails, it must `throw` a [HookError]. Link hooks are
/// guaranteed to be invoked with a process invocation and should return a
/// non-zero exit code on failure. Throwing will lead to an uncaught exception,
/// causing a non-zero exit code.
///
/// ## Environment
///
/// Link hooks are executed in a semi-hermetic environment. This means that
/// `Platform.environment` does not expose all environment variables from the
/// parent process. This ensures that hook invocations are reproducible and
/// cacheable, and do not depend on accidental environment variables.
///
/// However, some environment variables are necessary for locating tools (like
/// compilers) or configuring network access. The following environment
/// variables are passed through to the hook process:
///
/// *   **Path and system roots:**
///     *   `PATH`: Invoke native tools.
///     *   `HOME`, `USERPROFILE`: Find tools in default install locations.
///     *   `SYSTEMDRIVE`, `SYSTEMROOT`, `WINDIR`: Process invocations and CMake
///         on Windows.
///     *   `PROGRAMDATA`: For `vswhere.exe` on Windows.
/// *   **Temporary directories:**
///     *   `TEMP`, `TMP`, `TMPDIR`: Temporary directories.
/// *   **HTTP proxies:**
///     *   `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`: Network access behind
///         proxies.
/// *   **Clang/LLVM:**
///     *   `LIBCLANG_PATH`: Rust's `bindgen` + `clang-sys`.
/// *   **Android NDK:**
///     *   `ANDROID_HOME`: Standard location for the Android SDK/NDK.
///     *   `ANDROID_NDK`, `ANDROID_NDK_HOME`, `ANDROID_NDK_LATEST_HOME`,
///         `ANDROID_NDK_ROOT`: Alternative locations for the NDK.
/// *   **Ccache:**
///     *   Any variable starting with `CCACHE_`.
/// *   **Nix:**
///     *   Any variable starting with `NIX_`.
///
/// Any changes to these environment variables will cause cache invalidation for
/// hooks.
///
/// All other environment variables are stripped.
///
/// ## Debugging
///
/// When a link hook doesn't work as expected, you can investigate the
/// intermediate files generated by the Dart and Flutter SDK build process.
///
/// The most important files for debugging are located in a subdirectory
/// specific to your hook's execution. The path is of the form
/// `.dart_tool/hooks_runner/<package_name>/<some_hash>/`, where
/// `<package_name>` is the name of the package containing the hook. Inside, you
/// will find:
///
/// * `input.json`: The configuration and data passed into your link hook.
/// * `output.json`: The JSON data that your link hook produced.
/// * `stdout.txt`: Any standard output from your link hook.
/// * `stderr.txt`: Any error messages or exceptions.
///
/// When you run a build, hooks for all dependencies are executed, so you might
/// see multiple package directories.
///
/// The `<some_hash>` is a checksum of the [LinkConfig] in the `input.json`. If
/// you are unsure which hash directory to inspect within your package's hook
/// directory, you can delete the `.dart_tool/hooks_runner/<package_name>/`
/// directory and re-run the command that failed. The newly created directory
/// will be for the latest invocation.
///
/// You can step through your code with a debugger by running the link hook from
/// its source file and providing the `input.json` via the `--config` flag:
///
/// ```sh
/// dart run hook/link.dart --config .dart_tool/hooks_runner/<package_name>/<some_hash>/input.json
/// ```
///
/// To debug in VS Code, you can create a `launch.json` file in a `.vscode`
/// directory in your project root. This allows you to run your hook with a
/// debugger attached.
///
/// Here is an example configuration:
///
/// ```json
/// {
///   "version": "0.2.0",
///   "configurations": [
///     {
///       "name": "Debug Link Hook",
///       "type": "dart",
///       "request": "launch",
///       "program": "hook/link.dart",
///       "args": [
///         "--config",
///         ".dart_tool/hooks_runner/your_package_name/some_hash/input.json"
///       ]
///     }
///   ]
/// }
/// ```
///
/// Again, make sure to replace `your_package_name`, and `some_hash` with the
/// actual paths from your project. After setting this up, you can run the
/// "Debug Link Hook" configuration from the "Run and Debug" view in VS Code.
Future<void> link(
  List<String> arguments,
  Future<void> Function(LinkInput input, LinkOutputBuilder output) linker,
) async {
  final inputPath = getInputArgument(arguments);
  final bytes = File(inputPath).readAsBytesSync();
  final jsonInput =
      const Utf8Decoder().fuse(const JsonDecoder()).convert(bytes)
          as Map<String, Object?>;
  final input = LinkInput(jsonInput);
  final outputFile = input.outputFile;
  final output = LinkOutputBuilder();
  try {
    await linker(input, output);
    // ignore: avoid_catching_errors
  } on HookError catch (e, st) {
    output.setFailure(e.failureType);
    await _writeOutput(output, outputFile);
    _exitViaHookException(e, st);
  }
  final errors = await ProtocolBase.validateLinkOutput(
    input,
    LinkOutput(output.json),
  );
  if (errors.isNotEmpty) {
    final message = [
      'The output contained unsupported output:',
      for (final error in errors) '- $error',
    ].join('\n');
    stderr.writeln(message);
    output.setFailure(.build);
    await _writeOutput(output, outputFile);
    exit(BuildError(message: message).exitCode);
  }

  await _writeOutput(output, outputFile);
}

Future<void> _writeOutput(HookOutputBuilder output, Uri outputFile) async {
  final jsonOutput = const JsonEncoder.withIndent(
    '  ',
  ).fuse(const Utf8Encoder()).convert(output.json);
  await File.fromUri(outputFile).writeAsBytes(jsonOutput);
}

Never _exitViaHookException(HookError exception, StackTrace stackTrace) {
  stderr.writeln(exception.message);
  stderr.writeln(stackTrace);
  if (exception.wrappedException != null) {
    stderr.writeln('Wrapped exception:');
    stderr.writeln(exception.wrappedException);
    stderr.writeln(exception.wrappedTrace);
  }
  exit(exception.exitCode);
}
