// Copyright (c) 2025, 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 'package:analysis_server_client/protocol.dart';
import 'package:analysis_server_client/server.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/utilities/extensions/object.dart';
import 'package:analyzer_testing/package_root.dart' as pkg_root;
import 'package:analyzer_utilities/tools.dart';
import 'package:path/path.dart';

void main() async {
  await GeneratedContent.generateAll(pkg_root.packageRoot, await allTargets);
}

Future<List<GeneratedContent>> get allTargets async {
  var astLibrary = await _getAstLibrary();
  return <GeneratedContent>[
    GeneratedFile('analyzer/lib/dart/ast/visitor.g.dart', (_) async {
      var generator = _ConcreteAstVisitorGenerator(astLibrary);
      return await generator.generate();
    }),
    GeneratedFile('analyzer/lib/src/dart/ast/ast.g.dart', (_) async {
      var generator = _AstVisitorGenerator(astLibrary);
      return await generator.generate();
    }),
    GeneratedFile('analyzer/lib/src/lint/linter_visitor.g.dart', (_) async {
      var generator = _LinterVisitorGenerator(astLibrary);
      return await generator.generate();
    }),
    GeneratedFile('analyzer/lib/analysis_rule/rule_visitor_registry.g.dart', (
      _,
    ) async {
      var generator = _RuleVisitorGenerator(astLibrary);
      return await generator.generate();
    }),
  ];
}

String get _analyzerPath => normalize(join(pkg_root.packageRoot, 'analyzer'));

Future<String> _formatSortCode(String path, String code) async {
  var server = Server();
  await server.start();
  server.listenToOutput();

  await server.send('analysis.setAnalysisRoots', {
    'included': [path],
    'excluded': [],
  });

  Future<void> updateContent() async {
    await server.send('analysis.updateContent', {
      'files': {
        path: {'type': 'add', 'content': code},
      },
    });
  }

  await updateContent();
  var formatResponse = await server.send('edit.format', {
    'file': path,
    'selectionOffset': 0,
    'selectionLength': code.length,
  });
  var formatResult = EditFormatResult.fromJson(
    ResponseDecoder(null),
    'result',
    formatResponse,
  );
  code = SourceEdit.applySequence(code, formatResult.edits);

  await updateContent();
  var sortResponse = await server.send('edit.sortMembers', {'file': path});
  var sortResult = EditSortMembersResult.fromJson(
    ResponseDecoder(null),
    'result',
    sortResponse,
  );
  code = SourceEdit.applySequence(code, sortResult.edit.edits);

  await server.kill();
  return code;
}

Future<LibraryElement> _getAstLibrary() async {
  var collection = AnalysisContextCollection(includedPaths: [_analyzerPath]);
  var analysisContext = collection.contextFor(_analyzerPath);
  var analysisSession = analysisContext.currentSession;

  var libraryResult = await analysisSession.getLibraryByUri(
    'package:analyzer/src/dart/ast/ast.dart',
  );
  libraryResult as LibraryElementResult;
  return libraryResult.element;
}

class _AstVisitorGenerator {
  final LibraryElement astLibrary;

  final StringBuffer out = StringBuffer('''
// Copyright (c) 2025, 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.
//
// Run 'dart pkg/analyzer/tool/ast/generate.dart' to update.

part of 'ast.dart';
''');

  _AstVisitorGenerator(this.astLibrary);

  Future<String> generate() async {
    _writeAstVisitor();

    var resultPath = normalize(
      join(_analyzerPath, 'lib', 'src', 'dart', 'ast', 'ast.g.dart'),
    );
    return _formatSortCode(resultPath, out.toString());
  }

  void _writeAstVisitor() {
    out.write('''
/// An object that can be used to visit an AST structure.
///
/// Clients may not extend, implement or mix-in this class. There are classes
/// that implement this interface that provide useful default behaviors in
/// `package:analyzer/dart/ast/visitor.dart`. A couple of the most useful
/// include
/// - SimpleAstVisitor which implements every visit method by doing nothing,
/// - RecursiveAstVisitor which causes every node in a structure to be visited,
///   and
/// - ThrowingAstVisitor which implements every visit method by throwing an
///   exception.
@AnalyzerPublicApi(message: 'exported by lib/dart/ast/ast.dart')
abstract class AstVisitor<R> {
''');
    for (var node in astLibrary.nodes) {
      if (node.isConcrete) {
        var name = node.apiElementName;
        if (node.isExperimental) {
          out.writeln('  @experimental');
        }
        out.writeln('''
  R? visit$name($name node);
''');
      }
    }
    out.writeln('}');
  }
}

class _ConcreteAstVisitorGenerator {
  final LibraryElement astLibrary;

  final StringBuffer out = StringBuffer('''
// Copyright (c) 2025, 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.
//
// Run 'dart pkg/analyzer/tool/ast/generate.dart' to update.

part of 'visitor.dart';
''');

  _ConcreteAstVisitorGenerator(this.astLibrary);

  Future<String> generate() async {
    _writeGeneralizing();
    _writeRecursive();
    _writeSimple();
    _writeTimed();
    _writeThrowing();
    _writeUnifying();

    var resultPath = normalize(
      join(_analyzerPath, 'lib', 'dart', 'ast', 'visitor.g.dart'),
    );
    return _formatSortCode(resultPath, out.toString());
  }

  void _writeGeneralizing() {
    out.write('''
/// An AST visitor that will recursively visit all of the nodes in an AST
/// structure (like instances of the class [RecursiveAstVisitor]). In addition,
/// when a node of a specific type is visited not only will the visit method for
/// that specific type of node be invoked, but additional methods for the
/// superclasses of that node will also be invoked. For example, using an
/// instance of this class to visit a [Block] will cause the method [visitBlock]
/// to be invoked but will also cause the methods [visitStatement] and
/// [visitNode] to be subsequently invoked. This allows visitors to be written
/// that visit all statements without needing to override the visit method for
/// each of the specific subclasses of [Statement].
///
/// Subclasses that override a visit method must either invoke the overridden
/// visit method or explicitly invoke the more general visit method. Failure to
/// do so will cause the visit methods for superclasses of the node to not be
/// invoked and will cause the children of the visited node to not be visited.
///
/// Clients may extend this class.
class GeneralizingAstVisitor<R> implements AstVisitor<R> {
  /// Initialize a newly created visitor.
  const GeneralizingAstVisitor();

  R? visitNode(AstNode node) {
    node.visitChildren(this);
    return null;
  }
''');
    for (var node in astLibrary.nodes) {
      var name = node.apiElementName;
      var superNode = node.superNode;
      if (superNode == null) {
        continue;
      }
      if (node.isExperimental) {
        out.writeln('@experimental');
      }
      if (node.isConcrete) {
        out.writeln('@override');
      }

      // TODO(fshcheglov): Remove special case after AST hierarchy is fixed.
      // https://github.com/dart-lang/sdk/issues/61224
      if (node.apiElementName == 'FunctionDeclaration') {
        out.writeln(r'''
R? visitFunctionDeclaration(FunctionDeclaration node) {
  if (node.parent is FunctionDeclarationStatement) {
    return visitNode(node);
  }
  return visitNamedCompilationUnitMember(node);
}''');
        continue;
      }

      if (superNode.implElement.isAstNodeImplExactly) {
        out.writeln('''
R? visit$name($name node) => visitNode(node);
''');
      } else {
        out.writeln('''
R? visit$name($name node) => visit${superNode.apiElementName}(node);
''');
      }
    }
    out.writeln('}');
  }

  void _writeRecursive() {
    out.writeln(r'''
/// An AST visitor that will recursively visit all of the nodes in an AST
/// structure. For example, using an instance of this class to visit a [Block]
/// will also cause all of the statements in the block to be visited.
///
/// Subclasses that override a visit method must either invoke the overridden
/// visit method or must explicitly ask the visited node to visit its children.
/// Failure to do so will cause the children of the visited node to not be
/// visited.
///
/// Clients may extend this class.
class RecursiveAstVisitor<R> implements AstVisitor<R> {
  /// Initialize a newly created visitor.
  const RecursiveAstVisitor();
''');
    for (var node in astLibrary.nodes) {
      if (node.isConcrete) {
        var name = node.apiElementName;
        if (node.isExperimental) {
          out.writeln('  @experimental');
        }
        out.writeln('''
  @override
  R? visit$name($name node) {
    node.visitChildren(this);
    return null;
  }
''');
      }
    }
    out.writeln('}');
  }

  void _writeSimple() {
    out.write('''
/// An AST visitor that will do nothing when visiting an AST node. It is
/// intended to be a superclass for classes that use the visitor pattern
/// primarily as a dispatch mechanism (and hence don't need to recursively visit
/// a whole structure) and that only need to visit a small number of node types.
///
/// Clients may extend this class.
class SimpleAstVisitor<R> implements AstVisitor<R> {
  /// Initialize a newly created visitor.
  const SimpleAstVisitor();
''');
    for (var node in astLibrary.nodes) {
      if (node.isConcrete) {
        var name = node.apiElementName;
        if (node.isExperimental) {
          out.writeln('  @experimental');
        }
        out.writeln('''
  @override
  R? visit$name($name node) => null;
''');
      }
    }
    out.writeln('}');
  }

  void _writeThrowing() {
    out.writeln(r'''
/// An AST visitor that will throw an exception if any of the visit methods that
/// are invoked have not been overridden. It is intended to be a superclass for
/// classes that implement the visitor pattern and need to (a) override all of
/// the visit methods or (b) need to override a subset of the visit method and
/// want to catch when any other visit methods have been invoked.
///
/// Clients may extend this class.
class ThrowingAstVisitor<R> implements AstVisitor<R> {
  /// Initialize a newly created visitor.
  const ThrowingAstVisitor();

  Never _throw(AstNode node) {
    var typeName = node.runtimeType.toString();
    if (typeName.endsWith('Impl')) {
      typeName = typeName.substring(0, typeName.length - 4);
    }
    throw Exception('Missing implementation of visit$typeName');
  }
''');
    for (var node in astLibrary.nodes) {
      if (node.isConcrete) {
        var name = node.apiElementName;
        if (node.isExperimental) {
          out.writeln('  @experimental');
        }
        out.writeln('''
  @override
  R? visit$name($name node) => _throw(node);
''');
      }
    }
    out.writeln('}');
  }

  void _writeTimed() {
    out.writeln(r'''
/// An AST visitor that captures visit call timings.
///
/// Clients may not extend, implement or mix-in this class.
class TimedAstVisitor<T> implements AstVisitor<T> {
  /// The base visitor whose visit methods will be timed.
  final AstVisitor<T> _baseVisitor;

  /// Collects elapsed time for visit calls.
  final Stopwatch stopwatch;

  /// Initialize a newly created visitor to time calls to the given base
  /// visitor's visits.
  TimedAstVisitor(this._baseVisitor, [Stopwatch? watch])
    : stopwatch = watch ?? Stopwatch();
''');
    for (var node in astLibrary.nodes) {
      if (node.isConcrete) {
        var name = node.apiElementName;
        if (node.isExperimental) {
          out.writeln('  @experimental');
        }
        out.writeln('''
  @override
  T? visit$name($name node) {
    stopwatch.start();
    T? result = _baseVisitor.visit$name(node);
    stopwatch.stop();
    return result;
  }
''');
      }
    }
    out.writeln('}');
  }

  void _writeUnifying() {
    out.writeln(r'''
/// An AST visitor that will recursively visit all of the nodes in an AST
/// structure (like instances of the class [RecursiveAstVisitor]). In addition,
/// every node will also be visited by using a single unified [visitNode]
/// method.
///
/// Subclasses that override a visit method must either invoke the overridden
/// visit method or explicitly invoke the more general [visitNode] method.
/// Failure to do so will cause the children of the visited node to not be
/// visited.
///
/// Clients may extend this class.
class UnifyingAstVisitor<R> implements AstVisitor<R> {
  /// Initialize a newly created visitor.
  const UnifyingAstVisitor();

  R? visitNode(AstNode node) {
    node.visitChildren(this);
    return null;
  }
''');
    for (var node in astLibrary.nodes) {
      if (node.isConcrete) {
        var name = node.apiElementName;
        if (node.isExperimental) {
          out.writeln('  @experimental');
        }
        out.writeln('''
  @override
  R? visit$name($name node) => visitNode(node);
''');
      }
    }
    out.writeln('}');
  }
}

class _LinterVisitorGenerator {
  final LibraryElement astLibrary;

  final StringBuffer out = StringBuffer('''
// Copyright (c) 2025, 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.
//
// Run 'dart pkg/analyzer/tool/ast/generate.dart' to update.

part of 'linter_visitor.dart';
''');

  _LinterVisitorGenerator(this.astLibrary);

  Future<String> generate() async {
    _writeLinterVisitor();
    _writeRuleVisitorRegistryImpl();

    var resultPath = normalize(
      join(_analyzerPath, 'lib', 'src', 'lint', 'linter_visitor.g.dart'),
    );
    return _formatSortCode(resultPath, out.toString());
  }

  void _writeLinterVisitor() {
    out.write('''
/// The AST visitor that runs handlers for nodes from the [_registry].
class AnalysisRuleVisitor implements AstVisitor<void> {
  final RuleVisitorRegistryImpl _registry;

  /// Whether exceptions should be propagated (by rethrowing them).
  final bool _shouldPropagateExceptions;

  AnalysisRuleVisitor(this._registry, {bool shouldPropagateExceptions = false})
      : _shouldPropagateExceptions = shouldPropagateExceptions;

  void afterLibrary() {
    _runAfterLibrarySubscriptions(_registry._afterLibrary);
  }

  void _runAfterLibrarySubscriptions(
    List<_AfterLibrarySubscription> subscriptions,
  ) {
    for (var subscription in subscriptions) {
      var timer = subscription.timer;
      timer?.start();
      subscription.callback();
      timer?.stop();
    }
  }

  void _runSubscriptions<T extends AstNode>(
    T node,
    List<_Subscription<T>> subscriptions,
  ) {
    for (var subscription in subscriptions) {
      var timer = subscription.timer;
      timer?.start();
      try {
        node.accept(subscription.visitor);
      } catch (exception, stackTrace) {
        _logException(node, subscription.rule, exception, stackTrace);
        if (_shouldPropagateExceptions) {
          rethrow;
        }
      }
      timer?.stop();
    }
  }

  /// Handles exceptions that occur during the execution of an [AnalysisRule].
  void _logException(
    AstNode node,
    AbstractAnalysisRule visitor,
    Object exception,
    StackTrace stackTrace,
  ) {
    var buffer = StringBuffer();
    buffer.write('Exception while using a \${visitor.runtimeType} to visit a ');
    AstNode? currentNode = node;
    var first = true;
    while (currentNode != null) {
      if (first) {
        first = false;
      } else {
        buffer.write(' in ');
      }
      buffer.write(currentNode.runtimeType);
      currentNode = currentNode.parent;
    }
    // TODO(39284): should this exception be silent?
    AnalysisEngine.instance.instrumentationService.logException(
      SilentException(buffer.toString(), exception, stackTrace),
    );
  }
''');
    for (var node in astLibrary.nodes) {
      if (node.isConcrete) {
        var name = node.apiElementName;
        if (node.isExperimental) {
          out.writeln('  @experimental');
        }
        out.writeln('''
  @override
  void visit$name($name node) {
    _runSubscriptions(node, _registry._for$name);
    node.visitChildren(this);
  }
''');
      }
    }
    out.writeln('}');
  }

  void _writeRuleVisitorRegistryImpl() {
    out.write('''
class RuleVisitorRegistryImpl implements RuleVisitorRegistry {
  final bool _enableTiming;
  final List<_AfterLibrarySubscription> _afterLibrary = [];

  RuleVisitorRegistryImpl({required bool enableTiming})
  : _enableTiming = enableTiming;

  /// Get the timer associated with the given [rule].
  Stopwatch? _getTimer(AbstractAnalysisRule rule) {
    if (_enableTiming) {
      return analysisRuleTimers.getTimer(rule);
    } else {
      return null;
    }
  }

  @override
  void afterLibrary(AbstractAnalysisRule rule, void Function() callback) {
    _afterLibrary.add(
      _AfterLibrarySubscription(rule, callback, _getTimer(rule)),
    );
  }
''');
    for (var node in astLibrary.nodes) {
      if (node.isConcrete) {
        var name = node.apiElementName;
        out.writeln('''
final List<_Subscription<$name>> _for$name = [];

@override
void add$name(AbstractAnalysisRule rule, AstVisitor visitor) {
  _for$name.add(_Subscription(rule, visitor, _getTimer(rule)));
}
''');
      }
    }
    out.writeln('}');
  }
}

class _Node {
  final ClassElement implElement;
  final ClassElement apiElement;
  final String apiElementName;

  _Node({
    required this.implElement,
    required this.apiElement,
    required this.apiElementName,
  });

  bool get isConcrete => !implElement.isAbstract;

  bool get isExperimental {
    return apiElement.metadata.hasExperimental;
  }

  _Node? get superNode {
    var superElement = implElement.supertype?.element;
    return superElement.ifTypeOrNull<ClassElement>()?.asNode;
  }
}

class _RuleVisitorGenerator {
  final LibraryElement astLibrary;

  final StringBuffer out = StringBuffer('''
// Copyright (c) 2025, 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.
//
// Run 'dart pkg/analyzer/tool/ast/generate.dart' to update.

part of 'rule_visitor_registry.dart';
''');

  _RuleVisitorGenerator(this.astLibrary);

  Future<String> generate() async {
    _writeRuleVisitor();

    var resultPath = normalize(
      join(
        _analyzerPath,
        'lib',
        'analysis_rule',
        'rule_visitor_registry.g.dart',
      ),
    );
    return _formatSortCode(resultPath, out.toString());
  }

  void _writeRuleVisitor() {
    out.write('''
/// The container to register visitors for separate AST node types.
///
/// Source files are visited by the Dart analysis server recursively. Only the
/// visitors that are registered to visit a given node type will visit such
/// nodes. Each analysis rule overrides
/// [AbstractAnalysisRule.registerNodeProcessors] and calls `add*` for each of
/// the node types it needs to visit with an [AstVisitor], which registers that
/// visitor.
abstract class RuleVisitorRegistry {
  void afterLibrary(AbstractAnalysisRule rule, void Function() callback);
''');
    for (var node in astLibrary.nodes) {
      if (node.isConcrete) {
        var name = node.apiElementName;
        if (node.isExperimental) {
          out.writeln('@experimental');
        }
        out.writeln('''
void add$name(AbstractAnalysisRule rule, AstVisitor visitor);
''');
      }
    }
    out.writeln('}');
  }
}

extension on InterfaceElement {
  /// Whether the class is [AstNodeImpl].
  bool get isAstNodeImplExactly => name == 'AstNodeImpl';
}

extension on ClassElement {
  _Node? get asNode {
    if (isAstNodeImplSubtype || isAstNodeImplExactly) {
      var apiElement = interfaces.lastOrNull?.element;
      if (apiElement is ClassElement) {
        var apiElementName = apiElement.name!;
        if ('${apiElementName}Impl' == name) {
          return _Node(
            implElement: this,
            apiElement: apiElement,
            apiElementName: apiElementName,
          );
        }
      }
    }
    return null;
  }

  /// Whether the class is a subtype of [AstNodeImpl].
  bool get isAstNodeImplSubtype {
    // Not a real AST node.
    if (name == 'ConstantContextForExpressionImpl') {
      return false;
    }
    return allSupertypes.any((type) => type.element.isAstNodeImplExactly);
  }
}

extension on LibraryElement {
  List<_Node> get nodes {
    return classes.map((element) => element.asNode).nonNulls.toList();
  }
}
