// Copyright (c) 2021, 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 file contains code to generate scanner and parser message
/// based on the information in pkg/front_end/messages.yaml.
///
/// For each message in messages.yaml that contains an 'index:' field,
/// this tool generates an error with the name specified by the 'analyzerCode:'
/// field and an entry in the fastaAnalyzerErrorList for that generated error.
/// The text in the 'analyzerCode:' field must contain the name of the class
/// containing the error and the name of the error separated by a `.`
/// (e.g. ParserErrorCode.EQUALITY_CANNOT_BE_EQUALITY_OPERAND).
///
/// It is expected that 'dart pkg/front_end/tool/generate_messages.dart'
/// has already been successfully run.
library;

import 'dart:convert';
import 'dart:io';

import 'package:analyzer_testing/package_root.dart' as pkg_root;
import 'package:analyzer_utilities/extensions/string.dart';
import 'package:analyzer_utilities/messages.dart';
import 'package:analyzer_utilities/tools.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart';

import 'error_code_info.dart';

Future<void> main() async {
  await GeneratedContent.generateAll(pkg_root.packageRoot, allTargets);

  _SyntacticErrorGenerator()
    ..checkForManualChanges()
    ..printSummary();
}

/// A list of all targets generated by this code generator.
final List<GeneratedContent> allTargets = _analyzerGeneratedFiles();

/// Generates a list of [GeneratedContent] objects describing all the analyzer
/// files that need to be generated.
List<GeneratedContent> _analyzerGeneratedFiles() {
  var classesByFile = <GeneratedErrorCodeFile, List<ErrorClassInfo>>{};
  for (var errorClassInfo in errorClasses) {
    (classesByFile[errorClassInfo.file] ??= []).add(errorClassInfo);
  }
  var generatedCodes = <(String, String)>[];
  return [
    for (var entry in classesByFile.entries)
      GeneratedFile(entry.key.path, (pkgRoot) async {
        var codeGenerator = _AnalyzerErrorGenerator(
          entry.key,
          entry.value,
          generatedCodes,
        );
        codeGenerator.generate();
        return codeGenerator.out.toString();
      }),
    GeneratedFile('analyzer/lib/src/diagnostic/diagnostic_code_values.g.dart', (
      pkgRoot,
    ) async {
      var codeGenerator = _DiagnosticCodeValuesGenerator(generatedCodes);
      codeGenerator.generate();
      return codeGenerator.out.toString();
    }),
  ];
}

/// Code generator for analyzer error classes.
class _AnalyzerErrorGenerator {
  final GeneratedErrorCodeFile file;

  final List<ErrorClassInfo> errorClasses;

  final List<(String, String)> generatedCodes;

  final StringBuffer out = StringBuffer('''
// Copyright (c) 2021, 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 FILE IS GENERATED. DO NOT EDIT.
//
// Instead modify 'pkg/analyzer/messages.yaml' and run
// 'dart run pkg/analyzer/tool/messages/generate.dart' to update.

// While transitioning `HintCodes` to `WarningCodes`, we refer to deprecated
// codes here.
''');

  _AnalyzerErrorGenerator(this.file, this.errorClasses, this.generatedCodes);

  void generate() {
    out.writeln('// ignore_for_file: deprecated_member_use_from_same_package');
    if (file.shouldIgnorePreferSingleQuotes) {
      out.writeln('// ignore_for_file: prefer_single_quotes');
    }
    out.write('''
// 
// Generated comments don't quite align with flutter style.
// ignore_for_file: flutter_style_todos
''');
    out.writeln();
    out.write('''
part of ${json.encode(file.parentLibrary)};
''');
    bool shouldGenerateFastaAnalyzerErrorCodes = false;
    for (var errorClass in errorClasses) {
      if (errorClass.includeCfeMessages) {
        shouldGenerateFastaAnalyzerErrorCodes = true;
      }
    }
    if (shouldGenerateFastaAnalyzerErrorCodes) {
      out.writeln();
      _generateFastaAnalyzerErrorCodeList();
    }
    for (var errorClass
        in errorClasses.toList()..sort((a, b) => a.name.compareTo(b.name))) {
      out.writeln();
      if (errorClass.comment.isNotEmpty) {
        errorClass.comment.trimRight().split('\n').forEach((line) {
          out.writeln('/// $line');
        });
      }
      out.write(
        'class ${errorClass.name} extends DiagnosticCodeWithExpectedTypes {',
      );
      var memberAccumulator = MemberAccumulator();
      var entries = [
        ...analyzerMessages[errorClass.name]!.entries,
        if (errorClass.includeCfeMessages)
          ...sharedToAnalyzerErrorCodeTables.analyzerCodeToInfo.entries,
      ].where((error) => !error.value.isRemoved);
      for (var entry in entries) {
        var errorName = entry.key;
        var errorCodeInfo = entry.value;

        try {
          if (errorCodeInfo is! AliasErrorCodeInfo &&
              errorClass.includeInDiagnosticCodeValues) {
            generatedCodes.add((errorClass.name, errorName));
          }
          errorCodeInfo.toAnalyzerCode(
            errorClass,
            errorName,
            memberAccumulator: memberAccumulator,
          );
        } catch (e, st) {
          Error.throwWithStackTrace(
            'While processing ${errorClass.name}.$errorName: $e',
            st,
          );
        }
      }

      var constructor = StringBuffer();
      constructor.writeln(
        '/// Initialize a newly created error code to have the given '
        '[name].',
      );
      constructor.writeln(
        'const ${errorClass.name}(String name, String problemMessage, {',
      );
      constructor.writeln('super.correctionMessage,');
      constructor.writeln('super.hasPublishedDocs = false,');
      constructor.writeln('super.isUnresolvedIdentifier = false,');
      constructor.writeln('String? uniqueName,');
      constructor.writeln('required super.expectedTypes,');
      constructor.writeln('}) : super(');
      constructor.writeln('name: name,');
      constructor.writeln('problemMessage: problemMessage,');
      constructor.writeln(
        "uniqueName: '${errorClass.name}.\${uniqueName ?? name}',",
      );
      constructor.writeln(');');
      memberAccumulator.constructors[''] = constructor.toString();

      memberAccumulator.accessors['severity'] =
          '''
@override
DiagnosticSeverity get severity => ${errorClass.severityCode};
''';
      memberAccumulator.accessors['type'] =
          '''
@override
DiagnosticType get type => ${errorClass.typeCode};
''';

      memberAccumulator.writeTo(out);
      out.writeln('}');

      out.writeln();
      _outputDerivedClass(errorClass, withArguments: true);
      out.writeln();
      _outputDerivedClass(errorClass, withArguments: false);
    }
  }

  void _generateFastaAnalyzerErrorCodeList() {
    out.writeln('final fastaAnalyzerErrorCodes = <DiagnosticCode?>[');
    for (var entry in sharedToAnalyzerErrorCodeTables.indexToInfo) {
      var name = sharedToAnalyzerErrorCodeTables.infoToAnalyzerCode[entry]
          ?.toCamelCase();
      out.writeln('${name == null ? 'null' : 'ParserErrorCode.$name'},');
    }
    out.writeln('];');
  }

  void _outputDerivedClass(
    ErrorClassInfo errorClass, {
    required bool withArguments,
  }) {
    var className = withArguments
        ? errorClass.templateName
        : errorClass.withoutArgumentsName;
    out.writeln('final class $className');
    if (withArguments) out.writeln('<T extends Function>');
    out.writeln('    extends ${errorClass.name}');
    if (!withArguments) out.writeln('    with DiagnosticWithoutArguments');
    out.writeln('{');
    if (withArguments) {
      out.writeln('final T withArguments;');
      out.writeln();
    }
    out.writeln(
      '/// Initialize a newly created error code to have the given '
      '[name].',
    );
    out.writeln('const $className(');
    out.writeln('super.name,');
    out.writeln('super.problemMessage, {');
    out.writeln('super.correctionMessage,');
    out.writeln('super.hasPublishedDocs = false,');
    out.writeln('super.isUnresolvedIdentifier = false,');
    out.writeln('super.uniqueName,');
    out.writeln('required super.expectedTypes,');
    if (withArguments) out.writeln('required this.withArguments,');
    out.writeln('});');
    out.writeln('}');
  }
}

class _DiagnosticCodeValuesGenerator {
  final List<(String, String)> generatedCodes;

  final StringBuffer out = StringBuffer('''
// Copyright (c) 2014, 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 FILE IS GENERATED. DO NOT EDIT.
//
// Instead modify 'pkg/analyzer/messages.yaml' and run
// 'dart run pkg/analyzer/tool/messages/generate.dart' to update.

// We allow some snake_case and SCREAMING_SNAKE_CASE identifiers in generated
// code, as they match names declared in the source configuration files.
// ignore_for_file: constant_identifier_names

// While transitioning `HintCodes` to `WarningCodes`, we refer to deprecated
// codes here.
// ignore_for_file: deprecated_member_use_from_same_package
''');

  _DiagnosticCodeValuesGenerator(this.generatedCodes);

  void generate() {
    generatedCodes.sortBy((x) => '${x.$1}.${x.$2}');

    out.writeln();
    out.writeln(r'''
import 'package:_fe_analyzer_shared/src/base/analyzer_public_api.dart';
import 'package:_fe_analyzer_shared/src/base/errors.dart';
import 'package:analyzer/src/dart/error/ffi_code.dart';
import 'package:analyzer/src/dart/error/syntactic_errors.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/manifest/manifest_warning_code.dart';
import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
''');
    out.writeln();
    out.writeln(
      "@AnalyzerPublicApi(message: 'exported by lib/error/error.dart')",
    );
    out.writeln('const List<DiagnosticCode> diagnosticCodeValues = [');
    for (var (className, errorName) in generatedCodes) {
      errorName = errorName.toCamelCase();
      out.writeln('  $className.$errorName,');
    }
    out.writeln('];');
    out.writeln();
    out.writeln(
      "@AnalyzerPublicApi(message: 'exported by lib/error/error.dart')",
    );
    out.writeln('@Deprecated("Use \'diagnosticCodeValues\' instead")');
    out.writeln(
      'List<DiagnosticCode> get errorCodeValues => diagnosticCodeValues;',
    );
  }
}

class _SyntacticErrorGenerator {
  final String errorConverterSource;
  final String parserSource;

  factory _SyntacticErrorGenerator() {
    String frontEndSharedPkgPath = normalize(
      join(pkg_root.packageRoot, '_fe_analyzer_shared'),
    );

    String errorConverterSource = File(
      join(
        analyzerPkgPath,
        joinAll(posix.split('lib/src/fasta/error_converter.dart')),
      ),
    ).readAsStringSync();
    String parserSource = File(
      join(
        frontEndSharedPkgPath,
        joinAll(posix.split('lib/src/parser/parser.dart')),
      ),
    ).readAsStringSync();

    return _SyntacticErrorGenerator._(errorConverterSource, parserSource);
  }

  _SyntacticErrorGenerator._(this.errorConverterSource, this.parserSource);

  void checkForManualChanges() {
    // Check for ParserErrorCodes that could be removed from
    // error_converter.dart now that those ParserErrorCodes are auto generated.
    int converterCount = 0;
    for (var errorCode
        in sharedToAnalyzerErrorCodeTables.infoToAnalyzerCode.values) {
      if (errorConverterSource.contains('"$errorCode"')) {
        if (converterCount == 0) {
          print('');
          print(
            'The following ParserErrorCodes could be removed'
            ' from error_converter.dart:',
          );
        }
        print('  $errorCode');
        ++converterCount;
      }
    }
    if (converterCount > 3) {
      print('  $converterCount total');
    }

    // Fail if there are manual changes to be made, but do so
    // in a fire and forget manner so that the files are still generated.
    if (converterCount > 0) {
      print('');
      throw 'Error: missing manual code changes';
    }
  }

  void printSummary() {
    // Build a map of error message to ParserErrorCode
    var messageToName = <String, String>{};
    for (var entry in analyzerMessages['ParserErrorCode']!.entries) {
      String message = entry.value.problemMessage.replaceAll(
        placeholderPattern,
        '',
      );
      messageToName[message] = entry.key;
    }

    String messageFromEntryTemplate(ErrorCodeInfo entry) {
      String problemMessage = entry.problemMessage;
      String message = problemMessage.replaceAll(RegExp(r'#\w+'), '');
      return message;
    }

    // Remove entries that have already been translated
    for (ErrorCodeInfo entry
        in sharedToAnalyzerErrorCodeTables.infoToAnalyzerCode.keys) {
      messageToName.remove(messageFromEntryTemplate(entry));
    }

    // Print the # of autogenerated ParserErrorCodes.
    print(
      '${messageToName.length} of '
      '${sharedToAnalyzerErrorCodeTables.infoToAnalyzerCode.length} ParserErrorCodes generated.',
    );

    // List the ParserErrorCodes that could easily be auto generated
    // but have not been already.
    var analyzerToFasta = <String, List<String>>{};
    frontEndMessages.forEach((fastaName, entry) {
      var analyzerName = messageToName[messageFromEntryTemplate(entry)];
      if (analyzerName != null) {
        analyzerToFasta
            .putIfAbsent(analyzerName, () => <String>[])
            .add(fastaName);
      }
    });
    if (analyzerToFasta.isNotEmpty) {
      print('');
      print('The following ParserErrorCodes could be auto generated:');
      for (String analyzerName in analyzerToFasta.keys.toList()..sort()) {
        List<String> fastaNames = analyzerToFasta[analyzerName]!;
        if (fastaNames.length == 1) {
          print('  $analyzerName = ${fastaNames.first}');
        } else {
          print('  $analyzerName = $fastaNames');
        }
      }
      if (analyzerToFasta.length > 3) {
        print('  ${analyzerToFasta.length} total');
      }
    }
  }
}
