// Copyright (c) 2023, 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.

/// This script generates a test package with a specified number of libraries,
/// classes, methods, and doc comment references, in order to test analyzer's
/// performance, scalability, and stability characteristics.
///
/// Call with `--help` to see all of the args.
library;

import 'dart:io';
import 'dart:math';

import 'package:args/args.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

void main(List<String> args) async {
  // TODO(srawlins): Support multiple packages which depend on each other, in a
  // DAG similar to the import graph.
  var argParser = ArgParser()
    ..addOption(
      'library-count',
      defaultsTo: '1',
      help: 'the number of libraries',
    )
    ..addOption(
      'class-count',
      defaultsTo: '1',
      help: 'the number of classes per library',
    )
    ..addOption(
      'method-count',
      defaultsTo: '1',
      help: 'the number of methods per class',
    )
    ..addOption(
      'parameter-count',
      defaultsTo: '1',
      help: 'the number of parameters per method',
    )
    ..addFlag('use-barrel-file', help: 'Whether to add a barrel import')
    ..addFlag(
      'use-json-serializable',
      help: 'Whether to declare @JsonSerializable classes',
    )
    ..addFlag('help', help: 'Print help text');

  var argResults = argParser.parse(args);
  if (argResults.flag('help')) {
    print(argParser.usage);
  }

  var libraryCount = int.parse(argResults.option('library-count')!);
  var classCount = int.parse(argResults.option('class-count')!);
  var methodCount = int.parse(argResults.option('method-count')!);
  var parameterCount = int.parse(argResults.option('parameter-count')!);
  var useBarrelFile = argResults.flag('use-barrel-file');
  var useJsonSerializable = argResults.flag('use-json-serializable');
  var testDataDir = Directory('test_data')..createSync();
  var libFiles = <d.Descriptor>[];
  var classCounter = 1;
  var methodCounter = 1;
  var middleImportIndex = libraryCount ~/ 2;
  // We need a global index for the names of top-level variables, to avoid
  // ambiguous elements from imports.
  var topLevelVariableIndex = 1;
  for (var lIndex = 1; lIndex <= libraryCount; lIndex++) {
    var libraryName = 'lib$lIndex'.padLeft(3, '0');
    // Each library has an index, starting at 1. The libraries depend on each
    // other in tiers. The "tier" of a library is the base-2 logarithm of its
    // index. Libraries in higher tiers depend on libraries in lower tiers.
    // Libraries in higher tiers depend on more libraries than those in lower
    // tiers.
    //
    // In a package with 10 libraries, we have the following tiers and
    // dependencies:
    // * T0: lib1 - no imports
    // * T1: lib2 - imports lib1
    // * T2: lib3, lib4 - each imports two libraries from T0, T1
    // * T3: lib5, lib6, lib7, lib8 - each imports three libraries from T0, T1,
    //       and T2
    // * T4: lib9, lib10 - each imports four libraries from T0, T1, T2, T3
    //
    // In a package with 1000 libraries, there are 11 tiers, and libraries in
    // the last have 10 imports each.
    // TODO(srawlins): Make the "connectedness" of the import graph
    // configurable.
    var importGraphTier = (log(lIndex) / ln2).ceil();
    var importIndexStep = importGraphTier == 0
        ? -1
        : (lIndex - 1) ~/ importGraphTier;
    var content = StringBuffer();
    if (useJsonSerializable) {
      content.writeln("import 'package:json_annotation/json_annotation.dart';");
    }
    // Add imports in a library above tier 0.
    if (importGraphTier > 0) {
      if (useBarrelFile && lIndex > middleImportIndex) {
        content.writeln(import(testPackageLibUri('barrel.dart')));
      }
      for (var tierIndex = 1; tierIndex <= importGraphTier; tierIndex++) {
        var importIndex = tierIndex * importIndexStep;
        if (useBarrelFile) {
          if (lIndex <= middleImportIndex || importIndex > middleImportIndex) {
            content.writeln(import(testPackageLibUri('lib$importIndex.dart')));
          }
        } else {
          content.writeln(import(testPackageLibUri('lib$importIndex.dart')));
        }
      }
      content.writeln();
    }

    if (useJsonSerializable) {
      content.writeln("part '$libraryName.g.dart';");
      content.writeln();
    }

    // Add top-level variables above tier 0.
    if (importGraphTier > 0) {
      for (var tierIndex = 1; tierIndex <= importGraphTier; tierIndex++) {
        var importIndex = tierIndex * importIndexStep;
        // We instantiate a class which is guaranteed to be found in
        // `lib$importIndex.dart`.
        var classReferenceIndex = classCount * importIndex;
        content.writeln(
          'var x$topLevelVariableIndex = C$classReferenceIndex();',
        );
        topLevelVariableIndex++;
      }
      content.writeln();
    }

    for (var cIndex = 1; cIndex <= classCount; cIndex++) {
      content.writeln('/// Doc comment.');
      if (useJsonSerializable) {
        content.writeln('@JsonSerializable()');
      }
      content.writeln('class C$classCounter {');
      if (useJsonSerializable) {
        content.writeln('  C$classCounter();');
        content.writeln(
          '  factory C$classCounter.fromJson(Map<String, dynamic> json) => '
          '_\$C${classCounter}FromJson(json);',
        );
        content.writeln(
          '  Map<String, dynamic> toJson() => '
          '_\$C${classCounter}ToJson(this);',
        );
      }
      for (var mIndex = 1; mIndex <= methodCount; mIndex++) {
        content.write('  void m$methodCounter(');
        content.write(
          List.generate(parameterCount, (pIndex) => 'int p$pIndex').join(', '),
        );
        content.writeln(') {}');
        methodCounter++;
      }
      content.writeln('}');
      classCounter++;
    }
    libFiles.add(d.file('$libraryName.dart', content.toString()));
  }

  if (useBarrelFile) {
    // Write the barrel file, which exports the first half of the libraries.
    var content = StringBuffer();
    for (var j = 1; j <= middleImportIndex; j++) {
      content.writeln(export(testPackageLibUri('lib$j.dart')));
    }
    libFiles.add(d.file('barrel.dart', content.toString()));
  }

  var testPackageDir = d.dir('test_package', [
    d.file('pubspec.yaml', pubspec(useJsonSerializable: useJsonSerializable)),
    d.dir('lib', libFiles),
  ]);
  await testPackageDir.create(testDataDir.path);
}

String export(String uri) => "export '$uri';";

String import(String uri) => "import '$uri';";

/// Returns the text of a 'pubspec.yaml' file.
///
/// With [useJsonSerializable], several packages are added to the dependencies
/// in order to test using build_runner.
String pubspec({required bool useJsonSerializable}) {
  var dependencies = useJsonSerializable
      ? '''
dependencies:
  json_annotation: any
  json_serializable: any
'''
      : '';
  var devDependencies = useJsonSerializable
      ? '''
dev_dependencies:
  build_runner: any
'''
      : '';
  return '''
name: test_package
version: 0.0.1
environment:
  sdk: ^3.9.0
$dependencies
$devDependencies
''';
}

String testPackageLibUri(String path) => 'package:test_package/$path';
