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

part of '../protoc.dart';

final RegExp _dartIdentifier = RegExp(r'^\w+$');

const String _asyncImportUrl = 'dart:async';

const String _convertImportPrefix = r'$convert';
const String _convertImportUrl = 'dart:convert';

const String _coreImportUrl = 'dart:core';
const String _grpcImportUrl = 'package:grpc/service_api.dart';
const String _protobufImportUrl = 'package:protobuf/protobuf.dart';

const String _typedDataImportPrefix = r'$typed_data';
const String _typedDataImportUrl = 'dart:typed_data';

/// Generates the Dart output files for one .proto input file.
///
/// Outputs include .pb.dart, pbenum.dart, and .pbjson.dart.
class FileGenerator extends ProtobufContainer {
  final FileDescriptorProto descriptor;
  final GenerationOptions options;

  // The relative path used to import the .proto file, as a URI.
  final Uri protoFileUri;

  final enumGenerators = <EnumGenerator>[];
  final messageGenerators = <MessageGenerator>[];
  final extensionGenerators = <ExtensionGenerator>[];
  final clientApiGenerators = <ClientApiGenerator>[];
  final serviceGenerators = <ServiceGenerator>[];
  final grpcGenerators = <GrpcServiceGenerator>[];

  /// Used to avoid collisions after names have been mangled to match the Dart
  /// style.
  final Set<String> usedTopLevelNames = <String>{...forbiddenTopLevelNames};

  /// Used to avoid collisions in the service file after names have been mangled
  /// to match the dart style.
  final Set<String> usedTopLevelServiceNames = <String>{
    ...forbiddenTopLevelNames,
  };

  final Set<String> usedExtensionNames = <String>{...forbiddenExtensionNames};

  /// Whether cross-references have been resolved.
  bool _linked = false;

  final Edition edition;

  @override
  final FeatureSet features;

  /// Maps imports in the current file to their import prefixes.
  /// E.g. in `import 'x/y/z.pb.dart' as $1` this maps `x/y/z.pb.dart` to `$1`.
  final Map<String, String> _importPrefixes = {};

  /// Get the import prefix of `container` in the current file generator.
  ///
  /// Note that just calling this does not import the `container` in the current
  /// file. This just assigns an prefix to the container in the current file
  /// generator.
  String importPrefix(ProtobufContainer container) {
    final protoFilePath = container.fileGen!.protoFileUri.toString();
    return _importPrefixes.putIfAbsent(
      protoFilePath,
      () => '\$${_importPrefixes.length}',
    );
  }

  FileGenerator(
    FeatureSetDefaults editionDefaults,
    this.descriptor,
    this.options,
  ) : protoFileUri = Uri.file(descriptor.name),
      edition = _getEdition(descriptor),
      features = resolveFeatures(
        _getEditionDefaults(editionDefaults, _getEdition(descriptor)),
        descriptor.options.features,
      ) {
    if (protoFileUri.isAbsolute) {
      // protoc should never generate an import with an absolute path.
      throw 'FAILURE: Import with absolute path is not supported';
    }

    final declaredMixins = _getDeclaredMixins(descriptor);
    final defaultMixinName =
        descriptor.options.getExtension(Dart_options.defaultMixin) as String? ??
        '';
    final defaultMixin =
        declaredMixins[defaultMixinName] ?? findMixin(defaultMixinName);
    if (defaultMixin == null && defaultMixinName.isNotEmpty) {
      throw 'Option default_mixin on file ${descriptor.name}: Unknown mixin '
          '$defaultMixinName';
    }

    // Load and register all enum and message types.
    for (var i = 0; i < descriptor.enumType.length; i++) {
      enumGenerators.add(
        EnumGenerator.topLevel(
          descriptor.enumType[i],
          this,
          usedTopLevelNames,
          i,
        ),
      );
    }
    for (var i = 0; i < descriptor.messageType.length; i++) {
      messageGenerators.add(
        MessageGenerator.topLevel(
          descriptor.messageType[i],
          this,
          declaredMixins,
          defaultMixin,
          usedTopLevelNames,
          i,
        ),
      );
    }
    for (var i = 0; i < descriptor.extension.length; i++) {
      extensionGenerators.add(
        ExtensionGenerator.topLevel(
          descriptor.extension[i],
          this,
          usedExtensionNames,
          i,
        ),
      );
    }
    for (var i = 0; i < descriptor.service.length; i++) {
      final service = descriptor.service[i];
      if (options.useGrpc) {
        grpcGenerators.add(GrpcServiceGenerator(service, this, i));
      } else {
        final serviceGen = ServiceGenerator(
          service,
          this,
          usedTopLevelServiceNames,
        );
        serviceGenerators.add(serviceGen);
        clientApiGenerators.add(
          ClientApiGenerator(serviceGen, usedTopLevelNames, i),
        );
      }
    }
  }

  /// Creates the fields in each message.
  /// Resolves field types and extension targets using the supplied context.
  void resolve(GenerationContext ctx) {
    if (_linked) throw StateError('cross references already resolved');

    for (final m in messageGenerators) {
      m.resolve(ctx);
    }
    for (final x in extensionGenerators) {
      x.resolve(ctx);
    }

    _linked = true;
  }

  @override
  String get package => descriptor.package;

  @override
  String get classname => '';

  @override
  String get fullName => descriptor.package;

  @override
  FileGenerator get fileGen => this;

  @override
  ProtobufContainer? get parent => null;

  @override
  List<int> get fieldPath => [];

  /// Generates all the Dart files for this .proto file.
  List<CodeGeneratorResponse_File> generateFiles(OutputConfiguration config) {
    if (!_linked) throw StateError('not linked');

    CodeGeneratorResponse_File makeFile(String extension, String content) {
      final protoUrl = Uri.file(descriptor.name);
      final dartUrl = config.outputPathFor(protoUrl, extension);
      return CodeGeneratorResponse_File()
        ..name = dartUrl.path
        ..content = content;
    }

    final mainWriter = generateMainFile(config);
    final enumWriter = generateEnumFile(config);

    final generateMetadata = options.generateMetadata;

    final files = [
      makeFile('.pb.dart', mainWriter.emitSource(format: !generateMetadata)),
      makeFile(
        '.pbenum.dart',
        enumWriter.emitSource(format: !generateMetadata),
      ),
      // TODO(devoncarew): Consider not emitting empty json files.
      makeFile('.pbjson.dart', generateJsonFile(config)),
    ];

    if (generateMetadata) {
      files.addAll([
        makeFile(
          '.pb.dart.meta',
          mainWriter.sourceLocationInfo.writeToJson().toString(),
        ),
        makeFile(
          '.pbenum.dart.meta',
          enumWriter.sourceLocationInfo.writeToJson().toString(),
        ),
      ]);
    }
    if (options.useGrpc) {
      if (grpcGenerators.isNotEmpty) {
        files.add(makeFile('.pbgrpc.dart', generateGrpcFile(config)));
      }
    } else {
      if (serviceGenerators.isNotEmpty) {
        files.add(makeFile('.pbserver.dart', generateServerFile(config)));
      }
    }

    return files;
  }

  /// Creates an IndentingWriter with metadata generation enabled or disabled.
  IndentingWriter makeWriter() {
    return IndentingWriter(
      fileName: descriptor.name,
      generateMetadata: options.generateMetadata,
    );
  }

  /// Returns the contents of the .pb.dart file for this .proto file.
  IndentingWriter generateMainFile([
    OutputConfiguration config = const DefaultOutputConfiguration(),
  ]) {
    if (!_linked) throw StateError('not linked');

    final out = makeWriter();
    writeMainHeader(out, config);

    // Generate code.
    for (final m in messageGenerators) {
      m.generate(out);
    }

    // Generate code for extensions defined at top-level using a class name
    // derived from the file name.
    if (extensionGenerators.isNotEmpty) {
      // TODO(antonm): do not generate a class.
      final className = extensionClassName(descriptor, usedTopLevelNames);
      out.addBlock('class $className {', '}\n', () {
        for (final x in extensionGenerators) {
          x.generate(out);
        }
        out.println(
          'static void registerAllExtensions('
          '$protobufImportPrefix.ExtensionRegistry registry) {',
        );
        for (final x in extensionGenerators) {
          out.println('  registry.add(${x.name});');
        }
        out.println('}');
      });
    }

    for (final c in clientApiGenerators) {
      c.generate(out);
    }
    return out;
  }

  /// Writes the header and imports for the .pb.dart file.
  void writeMainHeader(
    IndentingWriter out, [
    OutputConfiguration config = const DefaultOutputConfiguration(),
  ]) {
    final importWriter = ImportWriter();

    // We only add the dart:async import if there are generic client API
    // generators for services in the FileDescriptorProto.
    if (clientApiGenerators.isNotEmpty) {
      importWriter.addImport(_asyncImportUrl, prefix: asyncImportPrefix);
    }

    importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);

    if (_needsFixnumImport) {
      importWriter.addImport(
        'package:fixnum/fixnum.dart',
        prefix: fixnumImportPrefix,
      );
    }

    if (_needsProtobufImport) {
      importWriter.addImport(_protobufImportUrl, prefix: protobufImportPrefix);
    }

    for (final libraryUri in findMixinImports()) {
      importWriter.addImport(libraryUri, prefix: mixinImportPrefix);
    }

    // Import the .pb.dart files we depend on.
    final imports = Set<FileGenerator>.identity();
    final enumImports = Set<FileGenerator>.identity();
    _findProtosToImport(imports, enumImports);

    for (final target in imports) {
      _addImport(importWriter, config, target, '.pb.dart');
    }

    for (final target in enumImports) {
      // If we're already adding the main file (.pb.dart) as an import, we don't
      // need to add the enums file, as that's exported from the main file.
      if (!imports.contains(target)) {
        _addImport(importWriter, config, target, '.pbenum.dart');
      }
    }

    importWriter.addExport(
      _protobufImportUrl,
      members: ['GeneratedMessageGenericExtensions'],
    );

    for (final publicDependency in descriptor.publicDependency) {
      _addExport(
        importWriter,
        config,
        Uri.file(descriptor.dependency[publicDependency]),
        '.pb.dart',
      );
    }

    // Export enums in main file for backward compatibility.
    if (hasEnums) {
      final url = config.resolveImport(
        protoFileUri,
        protoFileUri,
        '.pbenum.dart',
      );
      importWriter.addExport(url.toString());
    }

    // The well-known-types mixins create src/ refs into package:protobuf; we
    // should likely refactor this so they're regular (non-src/) references.
    //
    // For now, we surpress the analysis warning.
    _writeHeading(
      out,
      extraIgnores: {if (importWriter.hasSrcImport) 'implementation_imports'},
    );

    out.println(importWriter.emit());
  }

  bool get _needsFixnumImport {
    for (final m in messageGenerators) {
      if (m.needsFixnumImport) return true;
    }
    for (final x in extensionGenerators) {
      if (x.needsFixnumImport) return true;
    }
    return false;
  }

  bool get _needsProtobufImport =>
      messageGenerators.isNotEmpty ||
      extensionGenerators.isNotEmpty ||
      clientApiGenerators.isNotEmpty;

  /// Returns the generator for each .pb.dart file we need to import.
  void _findProtosToImport(
    Set<FileGenerator> imports,
    Set<FileGenerator> enumImports,
  ) {
    for (final m in messageGenerators) {
      m.addImportsTo(imports, enumImports);
    }
    for (final x in extensionGenerators) {
      x.addImportsTo(imports, enumImports);
    }
    // Add imports needed for client-side services.
    for (final x in serviceGenerators) {
      x.addImportsTo(imports);
    }
    // Don't need to import self. (But we may need to import the enums.)
    imports.remove(this);
  }

  /// Returns a sorted list of imports needed to support all mixins.
  List<String> findMixinImports() {
    final mixins = <PbMixin>{};
    for (final m in messageGenerators) {
      m.addMixinsTo(mixins);
    }

    return mixins
      .map((mixin) => mixin.importFrom)
      .toSet()
      .toList(growable: false)..sort();
  }

  /// Returns the contents of the .pbenum.dart file for this .proto file.
  IndentingWriter generateEnumFile([
    OutputConfiguration config = const DefaultOutputConfiguration(),
  ]) {
    if (!_linked) throw StateError('not linked');

    final out = makeWriter();
    _writeHeading(out);

    final importWriter = ImportWriter();

    if (hasEnums) {
      // Make sure any other symbols in dart:core don't cause name conflicts
      // with enums that have the same name.
      importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);
      importWriter.addImport(_protobufImportUrl, prefix: protobufImportPrefix);
    }

    for (final publicDependency in descriptor.publicDependency) {
      _addExport(
        importWriter,
        config,
        Uri.file(descriptor.dependency[publicDependency]),
        '.pbenum.dart',
      );
    }

    if (importWriter.hasImports) {
      out.println(importWriter.emit());
    }

    for (final e in enumGenerators) {
      e.generate(out);
    }

    for (final m in messageGenerators) {
      m.generateEnums(out);
    }

    return out;
  }

  /// Returns the number of enum types generated in the .pbenum.dart file.
  int get enumCount {
    var count = enumGenerators.length;
    for (final m in messageGenerators) {
      count += m.enumCount;
    }
    return count;
  }

  /// Returns whether this proto file defines any enums (either top level or
  /// nested within messages).
  bool get hasEnums => enumCount > 0;

  /// Returns the contents of the .pbserver.dart file for this .proto file.
  String generateServerFile([
    OutputConfiguration config = const DefaultOutputConfiguration(),
  ]) {
    if (!_linked) throw StateError('not linked');

    final out = makeWriter();
    _writeHeading(
      out,
      extraIgnores: {'deprecated_member_use_from_same_package'},
    );

    final importWriter = ImportWriter();

    if (serviceGenerators.isNotEmpty) {
      importWriter.addImport(_asyncImportUrl, prefix: asyncImportPrefix);
      importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);
      importWriter.addImport(_protobufImportUrl, prefix: protobufImportPrefix);
    }

    // Import .pb.dart files needed for requests and responses.
    final imports = <FileGenerator>{};
    for (final x in serviceGenerators) {
      x.addImportsTo(imports);
    }
    for (final target in imports) {
      _addImport(importWriter, config, target, '.pb.dart');
    }

    // Import .pbjson.dart file needed for $json and $messageJson.
    if (serviceGenerators.isNotEmpty) {
      _addImport(importWriter, config, this, '.pbjson.dart');
    }

    final url = config.resolveImport(protoFileUri, protoFileUri, '.pb.dart');
    importWriter.addExport(url.toString());

    if (importWriter.hasImports) {
      out.println(importWriter.emit());
    }

    for (final s in serviceGenerators) {
      s.generate(out);
    }

    return out.emitSource(format: true);
  }

  /// Returns the contents of the .pbgrpc.dart file for this .proto file.
  String generateGrpcFile([
    OutputConfiguration config = const DefaultOutputConfiguration(),
  ]) {
    if (!_linked) throw StateError('not linked');

    final out = makeWriter();
    _writeHeading(out);

    final importWriter = ImportWriter();

    importWriter.addImport(_asyncImportUrl, prefix: asyncImportPrefix);
    importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);
    importWriter.addImport(_grpcImportUrl, prefix: grpcImportPrefix);
    importWriter.addImport(_protobufImportUrl, prefix: protobufImportPrefix);

    // Import .pb.dart files needed for requests and responses.
    final imports = <FileGenerator>{};
    for (final generator in grpcGenerators) {
      generator.addImportsTo(imports);
    }
    for (final target in imports) {
      _addImport(importWriter, config, target, '.pb.dart');
    }

    final url = config.resolveImport(protoFileUri, protoFileUri, '.pb.dart');
    importWriter.addExport(url.toString());

    out.println(importWriter.emit());

    for (final generator in grpcGenerators) {
      generator.generate(out);
    }

    return out.emitSource(format: true);
  }

  void writeBinaryDescriptor(
    IndentingWriter out,
    String identifierName,
    String name,
    GeneratedMessage descriptor,
  ) {
    final base64 = base64Encode(descriptor.writeToBuffer());
    out.println(
      '/// Descriptor for `$name`. Decode as a '
      '`${descriptor.info_.qualifiedMessageName}`.',
    );

    const indent = '    ';

    final base64Lines = _splitString(
      base64,
      74,
    ).map((s) => "'$s'").join('\n$indent');
    out.println(
      'final $_typedDataImportPrefix.Uint8List '
      '$identifierName = '
      '$_convertImportPrefix.base64Decode(\n$indent$base64Lines);',
    );
  }

  /// Return the given [str], split into separate segments, where no segment is
  /// longer than [segmentLength].
  static List<String> _splitString(String str, int segmentLength) {
    final result = <String>[];
    while (str.length >= segmentLength) {
      result.add(str.substring(0, segmentLength));
      str = str.substring(segmentLength);
    }
    if (str.isNotEmpty) result.add(str);
    return result;
  }

  /// Returns the contents of the .pbjson.dart file for this .proto file.
  String generateJsonFile([
    OutputConfiguration config = const DefaultOutputConfiguration(),
  ]) {
    if (!_linked) throw StateError('not linked');

    final out = makeWriter();
    _writeHeading(out, extraIgnores: {'unused_import'});

    final importWriter = ImportWriter();
    importWriter.addImport(_convertImportUrl, prefix: _convertImportPrefix);
    importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);
    importWriter.addImport(_typedDataImportUrl, prefix: _typedDataImportPrefix);

    // Import the .pbjson.dart files we depend on.
    final imports = _findJsonProtosToImport();
    for (final target in imports) {
      _addImport(importWriter, config, target, '.pbjson.dart');
    }

    out.println(importWriter.emit());

    for (final e in enumGenerators) {
      e.generateConstants(out);
      writeBinaryDescriptor(
        out,
        e.binaryDescriptorName,
        e._descriptor.name,
        e._descriptor,
      );
      out.println('');
    }
    for (final m in messageGenerators) {
      m.generateConstants(out);
      writeBinaryDescriptor(
        out,
        m.binaryDescriptorName,
        m._descriptor.name,
        m._descriptor,
      );
      out.println('');
    }
    for (final s in serviceGenerators) {
      s.generateConstants(out);
      writeBinaryDescriptor(
        out,
        s.binaryDescriptorName,
        s._descriptor.name,
        s._descriptor,
      );
      out.println('');
    }

    return out.emitSource(format: true);
  }

  /// Returns the generator for each .pbjson.dart file the generated
  /// .pbjson.dart needs to import.
  Set<FileGenerator> _findJsonProtosToImport() {
    final imports = Set<FileGenerator>.identity();
    for (final m in messageGenerators) {
      m.addConstantImportsTo(imports);
    }
    for (final x in extensionGenerators) {
      x.addConstantImportsTo(imports);
    }
    for (final x in serviceGenerators) {
      x.addConstantImportsTo(imports);
    }
    imports.remove(this); // Don't need to import self.
    return imports;
  }

  /// Writes the header at the top of the dart file.
  void _writeHeading(
    IndentingWriter out, {
    Set<String> extraIgnores = const <String>{},
  }) {
    final ignores = ({..._fileIgnores, ...extraIgnores}).toList()..sort();

    // Group the ignores into lines not longer than 80 chars.
    final ignorelines = <String>[];

    if (ignores.isNotEmpty) {
      ignorelines.add('// ignore_for_file: ${ignores.first}');

      for (final ignore in ignores.skip(1)) {
        if (ignorelines.last.length + ignore.length + ', '.length > 80) {
          ignorelines.add('// ignore_for_file: $ignore');
        } else {
          ignorelines.add('${ignorelines.removeLast()}, $ignore');
        }
      }
    }

    out.println('''
// This is a generated file - do not edit.
//
// Generated from ${descriptor.name}.

// @dart = 3.3
''');
    ignorelines.forEach(out.println);
    out.println('');
  }

  /// Writes an import of a .dart file corresponding to a .proto file.
  /// (Possibly the same .proto file.)
  void _addImport(
    ImportWriter importWriter,
    OutputConfiguration config,
    FileGenerator target,
    String ext,
  ) {
    final url = config.resolveImport(target.protoFileUri, protoFileUri, ext);
    final import = url.toString();

    // .pb.dart files should always be prefixed -- the protoFileUri check will
    // evaluate to true not just for the main .pb.dart file based off the proto
    // file, but also for the .pbserver.dart, .pbgrpc.dart files.
    if (ext == '.pb.dart' || protoFileUri != target.protoFileUri) {
      importWriter.addImport(import, prefix: fileGen.importPrefix(target));
    } else {
      importWriter.addImport(import);
    }
  }

  /// Writes an export of a pb.dart file corresponding to a .proto file.
  /// (Possibly the same .proto file.)
  void _addExport(
    ImportWriter importWriter,
    OutputConfiguration config,
    Uri target,
    String ext,
  ) {
    final url = config.resolveImport(target, protoFileUri, ext);
    importWriter.addExport(url.toString());
  }
}

class ConditionalConstDefinition {
  final String envName;
  final String constFieldName;

  ConditionalConstDefinition(this.envName)
    : constFieldName = _convertToCamelCase(envName);

  String get constDefinition {
    return 'const $coreImportPrefix.bool $constFieldName = '
        "$coreImportPrefix.bool.fromEnvironment(${quoted('protobuf.$envName')});";
  }

  String createTernary(String ifFalse) {
    return "$constFieldName ? '' : ${quoted(ifFalse)}";
  }

  // Convert foo_bar_baz to _fooBarBaz.
  static String _convertToCamelCase(String lowerUnderscoreCase) {
    final parts = lowerUnderscoreCase.split('_');
    final rest =
        parts.skip(1).map((item) {
          return item.substring(0, 1).toUpperCase() + item.substring(1);
        }).join();
    return '_${parts.first}$rest';
  }
}

Edition _getEdition(FileDescriptorProto file) {
  if (file.edition != Edition.EDITION_UNKNOWN) {
    return file.edition;
  }
  if (file.syntax == 'proto3') {
    return Edition.EDITION_PROTO3;
  }
  return Edition.EDITION_PROTO2;
}

FeatureSet resolveFeatures(FeatureSet parent, FeatureSet child) {
  final result = parent.deepCopy();
  result.mergeFromMessage(child);
  return result;
}

FeatureSet _getEditionDefaults(
  FeatureSetDefaults editionDefaults,
  Edition edition,
) {
  if (edition.value < editionDefaults.minimumEdition.value) {
    throw ArgumentError(
      'Edition $edition is earlier than the minimum supported edition ${editionDefaults.minimumEdition}!',
    );
  }
  if (edition.value > editionDefaults.maximumEdition.value) {
    throw ArgumentError(
      'Edition $edition is later than the maximum supported edition ${editionDefaults.maximumEdition}!',
    );
  }
  FeatureSetDefaults_FeatureSetEditionDefault? found;
  for (final d in editionDefaults.defaults) {
    if (d.edition.value > edition.value) {
      break;
    }
    found = d;
  }
  if (found == null) {
    throw ArgumentError('No default found for edition $edition!');
  }
  final defaults = found.fixedFeatures.deepCopy();
  defaults.mergeFromMessage(found.overridableFeatures);
  return defaults;
}

/// Reads and the declared mixins in the file, keyed by name.
///
/// Performs some basic validation on declared mixins, e.g. whether names
/// are valid dart identifiers and whether there are cycles in the `parent`
/// hierarchy.
/// Does not check for existence of import files or classes.
Map<String, PbMixin> _getDeclaredMixins(FileDescriptorProto desc) {
  String mixinError(String error) => 'Option "mixins" in ${desc.name}: $error';

  if (!desc.hasOptions() || !desc.options.hasExtension(Dart_options.imports)) {
    return <String, PbMixin>{};
  }
  final dartMixins = <String, DartMixin>{};
  final importedMixins =
      desc.options.getExtension(Dart_options.imports) as Imports;
  for (final mixin in importedMixins.mixins) {
    if (dartMixins.containsKey(mixin.name)) {
      throw mixinError('Duplicate mixin name: "${mixin.name}"');
    }
    if (!mixin.name.startsWith(_dartIdentifier)) {
      throw mixinError('"${mixin.name}" is not a valid dart class identifier');
    }
    if (mixin.hasParent() && !mixin.parent.startsWith(_dartIdentifier)) {
      throw mixinError(
        'Mixin parent "${mixin.parent}" of "${mixin.name}" is '
        'not a valid dart class identifier',
      );
    }
    dartMixins[mixin.name] = mixin;
  }

  // Detect cycles and unknown parents.
  for (final mixin in dartMixins.values) {
    if (!mixin.hasParent()) continue;
    var currentMixin = mixin;
    final parentChain = <String>[];
    while (currentMixin.hasParent()) {
      final parentName = currentMixin.parent;

      final declaredMixin = dartMixins.containsKey(parentName);
      final internalMixin = !declaredMixin && findMixin(parentName) != null;

      if (internalMixin) break; // No further validation of parent chain.

      if (!declaredMixin) {
        throw mixinError(
          'Unknown mixin parent "${mixin.parent}" of '
          '"${currentMixin.name}"',
        );
      }

      if (parentChain.contains(parentName)) {
        final cycle = '${parentChain.join('->')}->$parentName';
        throw mixinError('Cycle in parent chain: $cycle');
      }
      parentChain.add(parentName);
      currentMixin = dartMixins[parentName]!;
    }
  }

  // Turn DartMixins into PbMixins.
  final pbMixins = <String, PbMixin>{};
  PbMixin? resolveMixin(String name) {
    if (pbMixins.containsKey(name)) return pbMixins[name];
    if (dartMixins.containsKey(name)) {
      final dartMixin = dartMixins[name]!;
      final pbMixin = PbMixin(
        dartMixin.name,
        importFrom: dartMixin.importFrom,
        parent: resolveMixin(dartMixin.parent),
      );
      pbMixins[name] = pbMixin;
      return pbMixin;
    }
    return findMixin(name);
  }

  for (final mixin in dartMixins.values) {
    resolveMixin(mixin.name);
  }
  return pbMixins;
}

const _fileIgnores = {
  'annotate_overrides',
  'camel_case_types',
  'comment_references',
  'constant_identifier_names',
  'curly_braces_in_flow_control_structures',
  'deprecated_member_use_from_same_package',
  'library_prefixes',
  'non_constant_identifier_names',
  'prefer_relative_imports',
};
