// Copyright (c) 2017, 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:async';

import 'package:build_config/build_config.dart';
import 'package:dart_style/dart_style.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';

import '../build_plan/build_configs.dart';
import '../build_plan/build_packages.dart';
import '../build_plan/build_paths.dart';
import '../constants.dart';
import '../exceptions.dart';
import '../io/reader_writer.dart';
import '../logging/build_log.dart';

final _lastShortFormatDartVersion = Version(3, 6, 0);

Future<String> generateBuildScript({required BuildPaths buildPaths}) async {
  final builderFactories = await loadBuilderFactories(buildPaths: buildPaths);

  try {
    // The `build_runner` version number is included to force a rebuild if the
    // script was generated by a different version. It only needs updating if
    // the host<->isolate relationship changed in a breaking way, for example
    // if command line args or messages passed via sendports have changed
    // in a breaking way.
    return DartFormatter(languageVersion: _lastShortFormatDartVersion).format(
      '''
// @dart=${_lastShortFormatDartVersion.major}.${_lastShortFormatDartVersion.minor}
// ignore_for_file: type=lint
// build_runner >=2.4.16
import 'dart:io' as _io;
import 'package:build_runner/src/build_plan/builder_factories.dart' as _build_runner;
import 'package:build_runner/src/bootstrap/processes.dart' as _build_runner;
${builderFactories.renderImports}

final _builderFactories = _build_runner.BuilderFactories(
  {${builderFactories.renderBuilderFactories}},
  postProcessBuilderFactories: {${builderFactories.renderPostProcessBuilderFactories}},
);
void main(List<String> args) async {
  _io.exitCode = await _build_runner.ChildProcess.run(
    args,
    _builderFactories,
  )!;
}
''',
    );
  } on FormatterException {
    buildLog.error(
      'Generated build script could not be parsed. '
      'Check builder definitions.',
    );
    throw const CannotBuildException();
  }
}

/// Loads builder factories for the current package and its transitive
/// dependencies.
///
/// If `buildPaths.buildWorkspace`, builder factories are also loaded for other
/// packages in the current workspace, if any, and their transitive
/// dependencies.
Future<BuilderFactoriesExpressions> loadBuilderFactories({
  required BuildPaths buildPaths,
}) async {
  final buildPackages = await BuildPackages.forPaths(buildPaths);
  final readerWriter = ReaderWriter(buildPackages);
  final overrides = await findBuildConfigOverrides(
    buildPackages: buildPackages,
    readerWriter: readerWriter,
    configKey: null,
  );
  Future<BuildConfig> packageBuildConfig(String packageName) async {
    if (overrides.containsKey(packageName)) {
      return overrides[packageName]!;
    }
    final package = buildPackages[packageName]!;
    try {
      return await BuildConfig.fromBuildConfigDir(
        package.name,
        package.dependencies,
        package.path,
      );
    } on ArgumentError // ignore: avoid_catching_errors
    catch (_) {
      // During the build an error will be logged.
      return BuildConfig.useDefault(package.name, package.dependencies);
    }
  }

  final buildConfigs = await Future.wait(
    buildPackages.orderedPackages.map(packageBuildConfig),
  );
  final builderDefinitions =
      buildConfigs
          .expand((c) => c.builderDefinitions.values)
          .where(
            (c) =>
                c.import.startsWith('package:') ||
                c.package == buildPackages.outputRoot,
          )
          .toList()
        ..sort((a, b) => a.key.compareTo(b.key));
  final postProcessBuilderDefinitions =
      buildConfigs
          .expand((c) => c.postProcessBuilderDefinitions.values)
          .where(
            (c) =>
                c.import.startsWith('package:') ||
                c.package == buildPackages.outputRoot,
          )
          .toList()
        ..sort((a, b) => a.key.compareTo(b.key));

  return BuilderFactoriesExpressions(
    builderFactories: {
      for (final builder in builderDefinitions)
        builder.key: _builderFactories(builder),
    },
    postProcessBuilderFactories: {
      for (final postProcessBuilder in postProcessBuilderDefinitions)
        postProcessBuilder.key: _postProcessBuilderFactory(postProcessBuilder),
    },
  );
}

class BuilderFactoriesExpressions {
  final Map<String, List<FactoryExpression>> builderFactories;
  final Map<String, FactoryExpression> postProcessBuilderFactories;
  final Map<String, String> importPrefixes = {};

  BuilderFactoriesExpressions({
    required this.builderFactories,
    required this.postProcessBuilderFactories,
  }) {
    var prefixNumber = 0;
    for (final factories in builderFactories.values) {
      for (final factory in factories) {
        if (!importPrefixes.containsKey(factory.import)) {
          importPrefixes[factory.import] = '_i${++prefixNumber}';
        }
      }
    }
  }

  String get renderImports {
    final result = StringBuffer();
    for (final entry in importPrefixes.entries) {
      result.writeln("import '${entry.key}' as ${entry.value};");
    }
    return result.toString();
  }

  String get renderBuilderFactories {
    final result = StringBuffer();
    for (final entry in builderFactories.entries) {
      final factories = entry.value
          .map((f) => f.render(importPrefixes))
          .join(', ');
      result.writeln("'${entry.key}': [$factories],");
    }
    return result.toString();
  }

  String get renderPostProcessBuilderFactories {
    final result = StringBuffer();
    for (final entry in postProcessBuilderFactories.entries) {
      result.writeln("'${entry.key}': ${entry.value.render(importPrefixes)},");
    }
    return result.toString();
  }
}

/// The `List` of `_Factory` declared in [definition].
List<FactoryExpression> _builderFactories(BuilderDefinition definition) {
  final import = _buildScriptImport(definition.import);
  return [
    for (final f in definition.builderFactories)
      FactoryExpression(import: import, name: f),
  ];
}

/// The `_Factory` declared in [definition].
FactoryExpression _postProcessBuilderFactory(
  PostProcessBuilderDefinition definition,
) {
  final import = _buildScriptImport(definition.import);
  return FactoryExpression(import: import, name: definition.builderFactory);
}

/// Returns the actual import to put in the generated script based on an import
/// found in the build.yaml.
String _buildScriptImport(String import) {
  if (import.startsWith('package:') ||
      import.startsWith('../') ||
      import.startsWith('/')) {
    return import;
  } else {
    return p.url.relative(import, from: p.url.dirname(entrypointScriptPath));
  }
}

class FactoryExpression {
  final String import;
  final String name;

  FactoryExpression({required this.name, required this.import});

  String render(Map<String, String> importPrefixes) =>
      '${importPrefixes[import]}.$name';
}
