// Copyright (c) 2015, 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 serialization/deserialization logic for
/// summaries based on an "IDL" description of the summary format (written in
/// stylized Dart).
///
/// For each class in the "IDL" input, two corresponding classes are generated:
/// - A class with the same name which represents deserialized summary data in
///   memory.  This class has read-only semantics.
/// - A "builder" class which can be used to generate serialized summary data.
///   This class has write-only semantics.
///
/// Each of the "builder" classes has a single `finish` method which writes
/// the entity being built into the given FlatBuffer and returns the `Offset`
/// reference to it.
///
/// @docImport 'package:analyzer/src/summary/api_signature.dart';
library;

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

import 'package:_fe_analyzer_shared/src/scanner/scanner.dart';
import 'package:analyzer_utilities/tools.dart';

import 'idl_model.dart' as idl_model;
import 'mini_ast.dart';

void main(List<String> args) async {
  if (args.length != 1) {
    print('Error: IDL path is required');
    print('usage: dart generate.dart path/to/idl.dart');
    return;
  }
  String idlPath = args[0];
  await GeneratedContent.generateAll(
      File(idlPath).parent.path, getAllTargets(idlPath));
}

List<GeneratedContent> getAllTargets(String idlPath) {
  GeneratedFile formatTarget = GeneratedFile('format.dart', (_) async {
    _CodeGenerator codeGenerator = _CodeGenerator(idlPath);
    codeGenerator.generateFormatCode();
    return codeGenerator._outBuffer.toString();
  });

  GeneratedFile schemaTarget = GeneratedFile('format.fbs', (_) async {
    _CodeGenerator codeGenerator = _CodeGenerator(idlPath);
    codeGenerator.generateFlatBufferSchema();
    return codeGenerator._outBuffer.toString();
  });

  return <GeneratedContent>[formatTarget, schemaTarget];
}

typedef _StringToString = String Function(String s);

class _BaseGenerator {
  static const String _throwDeprecated =
      "throw new UnimplementedError('attempt to access deprecated field')";

  /// Semantic model of the "IDL" input file.
  final idl_model.Idl _idl;

  /// Buffer in which generated code is accumulated.
  final StringBuffer _outBuffer;

  /// Current indentation level.
  String _indentation = '';

  _BaseGenerator(this._idl, this._outBuffer);

  /// Generate a Dart expression representing the default value for a field
  /// having the given [type], or `null` if there is no default value.
  ///
  /// If [builder] is `true`, the returned type should be appropriate for use in
  /// a builder class.
  String? defaultValue(idl_model.FieldType type, bool builder) {
    if (type.isList) {
      if (builder) {
        idl_model.FieldType elementType =
            idl_model.FieldType(type.typeName, false);
        return '<${encodedType(elementType)}>[]';
      } else {
        return 'const <${idlPrefix(type.typeName)}>[]';
      }
    }

    var enum_ = _idl.enums[type.typeName];
    if (enum_ != null) {
      return '${idlPrefix(type.typeName)}.${enum_.values[0].name}';
    } else if (type.typeName == 'double') {
      return '0.0';
    } else if (type.typeName == 'int') {
      return '0';
    } else if (type.typeName == 'String') {
      return "''";
    } else if (type.typeName == 'bool') {
      return 'false';
    } else {
      return null;
    }
  }

  /// Generate a string representing the Dart type which should be used to
  /// represent [type] while building a serialized data structure.
  String encodedType(idl_model.FieldType type) {
    String typeStr;
    if (_idl.classes.containsKey(type.typeName)) {
      typeStr = '${type.typeName}Builder';
    } else {
      typeStr = idlPrefix(type.typeName);
    }
    if (type.isList) {
      return 'List<$typeStr>';
    } else {
      return typeStr;
    }
  }

  /// Return the nullable [encodedType] of the [type].
  String encodedType2(idl_model.FieldType type) {
    return '${encodedType(type)}?';
  }

  /// Add the prefix `idl.` to a type name, unless that type name is the name of
  /// a built-in type.
  String idlPrefix(String s) {
    switch (s) {
      case 'bool':
      case 'double':
      case 'int':
      case 'String':
        return s;
      default:
        return 'idl.$s';
    }
  }

  /// Execute [callback] with two spaces added to [_indentation].
  void indent(void Function() callback) {
    String oldIndentation = _indentation;
    try {
      _indentation += '  ';
      callback();
    } finally {
      _indentation = oldIndentation;
    }
  }

  /// Add the string [s] to the output as a single line, indenting as
  /// appropriate.
  void out([String s = '']) {
    if (s == '') {
      _outBuffer.writeln();
    } else {
      _outBuffer.writeln('$_indentation$s');
    }
  }

  void outDoc(String? documentation) {
    if (documentation != null) {
      documentation.split('\n').forEach(out);
    }
  }

  /// Execute [out] of [s] in [indent].
  void outWithIndent(String s) {
    indent(() {
      out(s);
    });
  }

  /// Enclose [s] in quotes, escaping as necessary.
  String quoted(String s) {
    return json.encode(s);
  }

  bool _isNullable(idl_model.FieldType type) {
    return !type.isList && _idl.classes.containsKey(type.typeName);
  }
}

class _BuilderGenerator extends _BaseGenerator {
  final idl_model.ClassDeclaration cls;
  List<String> constructorParams = <String>[];

  _BuilderGenerator(super.idl, super.outBuffer, this.cls);

  String get builderName => '${name}Builder';

  String get name => cls.name;

  void generate() {
    String mixinName = '_${name}Mixin';
    var implementsClause =
        cls.isDeprecated ? '' : ' implements ${idlPrefix(name)}';
    out('class $builderName extends Object with $mixinName$implementsClause {');
    indent(() {
      _generateFields();
      _generateGettersSetters();
      _generateConstructors();
      _generateFlushInformative();
      _generateCollectApiSignature();
      _generateToBuffer();
      _generateFinish();
    });
    out('}');
  }

  void _generateCollectApiSignature() {
    out();
    out('/// Accumulate non-informative data into [signatureSink].');
    out('void collectApiSignature(api_sig.ApiSignature signatureSink) {');

    void writeField(idl_model.FieldDeclaration field) {
      if (field.isInformative) {
        return;
      }
      var type = field.type;
      var name = field.name;
      if (type.isList) {
        var localName = name;
        out('var $name = this._$name;');
        out('if ($localName == null) {');
        indent(() {
          out('signatureSink.addInt(0);');
        });
        out('} else {');
        indent(() {
          out('signatureSink.addInt($localName.length);');
          out('for (var x in $localName) {');
          indent(() {
            _generateSignatureCall(type.typeName, 'x', false);
          });
          out('}');
        });
        out('}');
      } else {
        _generateSignatureCall(type.typeName, 'this._$name', true);
      }
    }

    indent(() {
      List<idl_model.FieldDeclaration> sortedFields = cls.fields.toList()
        ..sort((idl_model.FieldDeclaration a, idl_model.FieldDeclaration b) =>
            a.id.compareTo(b.id));
      for (idl_model.FieldDeclaration field in sortedFields) {
        writeField(field);
      }
    });
    out('}');
  }

  void _generateConstructors() {
    out();
    out('$builderName({${constructorParams.join(', ')}})');
    List<idl_model.FieldDeclaration> fields = cls.fields.toList();
    for (int i = 0; i < fields.length; i++) {
      idl_model.FieldDeclaration field = fields[i];
      String prefix = i == 0 ? '  : ' : '    ';
      String suffix = i == fields.length - 1 ? ';' : ',';
      out('${prefix}_${field.name} = ${field.name}$suffix');
    }
  }

  void _generateFields() {
    for (idl_model.FieldDeclaration field in cls.fields) {
      String fieldName = field.name;
      idl_model.FieldType type = field.type;
      String typeStr = encodedType2(type);
      out('$typeStr _$fieldName;');
    }
  }

  void _generateFinish() {
    out();
    out('fb.Offset finish(fb.Builder fbBuilder) {');
    indent(() {
      // Write objects and remember Offset(s).
      for (idl_model.FieldDeclaration field in cls.fields) {
        idl_model.FieldType fieldType = field.type;
        String offsetName = 'offset_${field.name}';
        if (fieldType.isList ||
            fieldType.typeName == 'String' ||
            _idl.classes.containsKey(fieldType.typeName)) {
          out('fb.Offset? $offsetName;');
        }
      }

      for (idl_model.FieldDeclaration field in cls.fields) {
        idl_model.FieldType fieldType = field.type;
        String valueName = field.name;
        String offsetName = 'offset_${field.name}';
        String? condition;
        String? writeCode;
        if (fieldType.isList) {
          condition = ' || $valueName.isEmpty';
          if (_idl.classes.containsKey(fieldType.typeName)) {
            String itemCode = 'b.finish(fbBuilder)';
            String listCode = '$valueName.map((b) => $itemCode).toList()';
            writeCode = '$offsetName = fbBuilder.writeList($listCode);';
          } else if (_idl.enums.containsKey(fieldType.typeName)) {
            String itemCode = 'b.index';
            String listCode = '$valueName.map((b) => $itemCode).toList()';
            writeCode = '$offsetName = fbBuilder.writeListUint8($listCode);';
          } else if (fieldType.typeName == 'bool') {
            writeCode = '$offsetName = fbBuilder.writeListBool($valueName);';
          } else if (fieldType.typeName == 'int') {
            writeCode = '$offsetName = fbBuilder.writeListUint32($valueName);';
          } else if (fieldType.typeName == 'double') {
            writeCode = '$offsetName = fbBuilder.writeListFloat64($valueName);';
          } else {
            assert(fieldType.typeName == 'String');
            String itemCode = 'fbBuilder.writeString(b)';
            String listCode = '$valueName.map((b) => $itemCode).toList()';
            writeCode = '$offsetName = fbBuilder.writeList($listCode);';
          }
        } else if (fieldType.typeName == 'String') {
          writeCode = '$offsetName = fbBuilder.writeString($valueName);';
        } else if (_idl.classes.containsKey(fieldType.typeName)) {
          writeCode = '$offsetName = $valueName.finish(fbBuilder);';
        }
        if (writeCode != null) {
          out('var $valueName = _${field.name};');
          if (condition == null) {
            out('if ($valueName != null) {');
          } else {
            out('if (!($valueName == null$condition)) {');
          }
          outWithIndent(writeCode);
          out('}');
        }
      }

      // Write the table.
      out('fbBuilder.startTable();');
      for (idl_model.FieldDeclaration field in cls.fields) {
        int index = field.id;
        idl_model.FieldType fieldType = field.type;
        String valueName = '_${field.name}';
        if (fieldType.isList ||
            fieldType.typeName == 'String' ||
            _idl.classes.containsKey(fieldType.typeName)) {
          String offsetName = 'offset_${field.name}';
          out('if ($offsetName != null) {');
          outWithIndent('fbBuilder.addOffset($index, $offsetName);');
          out('}');
        } else if (fieldType.typeName == 'bool') {
          out('fbBuilder.addBool($index, $valueName == true);');
        } else if (fieldType.typeName == 'double') {
          var defValue = defaultValue(fieldType, true);
          out('fbBuilder.addFloat64($index, $valueName, $defValue);');
        } else if (fieldType.typeName == 'int') {
          var defValue = defaultValue(fieldType, true);
          out('fbBuilder.addUint32($index, $valueName, $defValue);');
        } else if (_idl.enums.containsKey(fieldType.typeName)) {
          var defValue = '${defaultValue(fieldType, true)}.index';
          out('fbBuilder.addUint8($index, $valueName?.index, $defValue);');
        } else {
          throw UnimplementedError('Writing type ${fieldType.typeName}');
        }
      }
      out('return fbBuilder.endTable();');
    });
    out('}');
  }

  void _generateFlushInformative() {
    out();
    out('/// Flush informative data recursively.');
    out('void flushInformative() {');

    void writeField(String name, idl_model.FieldType type, bool isInformative) {
      if (isInformative) {
        out('$name = null;');
      } else if (_idl.classes.containsKey(type.typeName)) {
        if (type.isList) {
          out('$name?.forEach((b) => b.flushInformative());');
        } else {
          out('$name?.flushInformative();');
        }
      }
    }

    indent(() {
      for (idl_model.FieldDeclaration field in cls.fields) {
        writeField('_${field.name}', field.type, field.isInformative);
      }
    });
    out('}');
  }

  void _generateGettersSetters() {
    for (idl_model.FieldDeclaration field in cls.allFields) {
      String fieldName = field.name;
      idl_model.FieldType fieldType = field.type;
      String typeStr = encodedType(fieldType);
      String? def = defaultValue(fieldType, true);
      String defSuffix = def == null ? '' : ' ??= $def';
      out();
      if (field.isDeprecated) {
        out('@override');
        out('Null get $fieldName => ${_BaseGenerator._throwDeprecated};');
      } else {
        var nn = _isNullable(field.type) ? '?' : '';

        out('@override');
        out('$typeStr$nn get $fieldName => _$fieldName$defSuffix;');
        out();

        constructorParams.add('$typeStr? $fieldName');

        outDoc(field.documentation);

        out('set $fieldName($typeStr$nn value) {');
        indent(() {
          _generateNonNegativeInt(fieldType);
          out('this._$fieldName = value;');
        });
        out('}');
      }
    }
  }

  void _generateNonNegativeInt(idl_model.FieldType fieldType) {
    if (fieldType.typeName == 'int') {
      if (!fieldType.isList) {
        out('assert(value >= 0);');
      } else {
        out('assert(value.every((e) => e >= 0));');
      }
    }
  }

  /// Generates a call to the appropriate method of [ApiSignature] for the type
  /// [typeName], using the data named by [ref].  If [couldBeNull] is `true`,
  /// generate code to handle the possibility that [ref] is `null` (substituting
  /// in the appropriate default value).
  void _generateSignatureCall(String typeName, String ref, bool couldBeNull) {
    if (_idl.enums.containsKey(typeName)) {
      if (couldBeNull) {
        out('signatureSink.addInt($ref?.index ?? 0);');
      } else {
        out('signatureSink.addInt($ref.index);');
      }
    } else if (_idl.classes.containsKey(typeName)) {
      if (couldBeNull) {
        out('signatureSink.addBool($ref != null);');
        out('$ref?.collectApiSignature(signatureSink);');
      } else {
        out('$ref.collectApiSignature(signatureSink);');
      }
    } else {
      switch (typeName) {
        case 'String':
          if (couldBeNull) {
            ref += " ?? ''";
          }
          out("signatureSink.addString($ref);");
        case 'int':
          if (couldBeNull) {
            ref += ' ?? 0';
          }
          out('signatureSink.addInt($ref);');
        case 'bool':
          if (couldBeNull) {
            ref += ' == true';
          }
          out('signatureSink.addBool($ref);');
        case 'double':
          if (couldBeNull) {
            ref += ' ?? 0.0';
          }
          out('signatureSink.addDouble($ref);');
        default:
          throw "Don't know how to generate signature call for $typeName";
      }
    }
  }

  void _generateToBuffer() {
    if (cls.isTopLevel) {
      out();
      out('typed_data.Uint8List toBuffer() {');
      indent(() {
        out('fb.Builder fbBuilder = fb.Builder();');
        var idOrNull = cls.fileIdentifier;
        var fileId = idOrNull == null ? '' : ', ${quoted(idOrNull)}';
        out('return fbBuilder.finish(finish(fbBuilder)$fileId);');
      });
      out('}');
    }
  }
}

class _CodeGenerator {
  /// Buffer in which generated code is accumulated.
  final StringBuffer _outBuffer = StringBuffer();

  /// Semantic model of the "IDL" input file.
  final idl_model.Idl _idl = idl_model.Idl();

  _CodeGenerator(String idlPath) {
    // Parse the input "IDL" file.
    File idlFile = File(idlPath);
    String idlText =
        idlFile.readAsStringSync().replaceAll(RegExp('\r\n?'), '\n');
    // Extract a description of the IDL and make sure it is valid.
    var startingToken = scanString(idlText, includeComments: true).tokens;
    var listener = MiniAstBuilder();
    var parser = MiniAstParser(listener);
    parser.parseUnit(startingToken);
    extractIdl(listener.compilationUnit);
    checkIdl();
  }

  /// Perform basic sanity checking of the IDL (over and above that done by
  /// [extractIdl]).
  void checkIdl() {
    _idl.classes.forEach((String name, idl_model.ClassDeclaration cls) {
      var fileIdentifier = cls.fileIdentifier;
      if (fileIdentifier != null) {
        if (fileIdentifier.length != 4) {
          throw Exception('$name: file identifier must be 4 characters');
        }
        for (int i = 0; i < fileIdentifier.length; i++) {
          if (fileIdentifier.codeUnitAt(i) >= 256) {
            throw Exception(
                '$name: file identifier must be encodable as Latin-1');
          }
        }
      }
      Map<int, String> idsUsed = <int, String>{};
      for (idl_model.FieldDeclaration field in cls.allFields) {
        String fieldName = field.name;
        idl_model.FieldType type = field.type;
        if (type.isList) {
          if (_idl.classes.containsKey(type.typeName)) {
            // List of classes is ok
          } else if (_idl.enums.containsKey(type.typeName)) {
            // List of enums is ok
          } else if (type.typeName == 'bool') {
            // List of booleans is ok
          } else if (type.typeName == 'int') {
            // List of ints is ok
          } else if (type.typeName == 'double') {
            // List of doubles is ok
          } else if (type.typeName == 'String') {
            // List of strings is ok
          } else {
            throw Exception(
                '$name.$fieldName: illegal type (list of ${type.typeName})');
          }
        }
        if (idsUsed.containsKey(field.id)) {
          throw Exception('$name.$fieldName: id ${field.id} already used by'
              ' ${idsUsed[field.id]}');
        }
        idsUsed[field.id] = fieldName;
      }
      for (int i = 0; i < idsUsed.length; i++) {
        if (!idsUsed.containsKey(i)) {
          throw Exception('$name: no field uses id $i');
        }
      }
    });
  }

  /// Process the AST in [idlParsed] and store the resulting semantic model in
  /// [_idl].  Also perform some error checking.
  void extractIdl(CompilationUnit idlParsed) {
    for (CompilationUnitMember decl in idlParsed.declarations) {
      if (decl is ClassDeclaration) {
        bool isTopLevel = false;
        bool isDeprecated = false;
        String? fileIdentifier;
        String clsName = decl.name;
        for (Annotation annotation in decl.metadata) {
          var arguments = annotation.arguments;
          if (arguments != null &&
              annotation.name == 'TopLevel' &&
              annotation.constructorName == null) {
            isTopLevel = true;
            if (arguments.length == 1) {
              Expression arg = arguments[0];
              if (arg is StringLiteral) {
                fileIdentifier = arg.stringValue;
              } else {
                throw Exception(
                    'Class `$clsName`: TopLevel argument must be a string'
                    ' literal');
              }
            } else if (arguments.isNotEmpty) {
              throw Exception(
                  'Class `$clsName`: TopLevel requires 0 or 1 arguments');
            }
          } else if (arguments == null &&
              annotation.name == 'deprecated' &&
              annotation.constructorName == null) {
            isDeprecated = true;
          }
        }
        idl_model.ClassDeclaration cls = idl_model.ClassDeclaration(
          documentation: _getNodeDoc(decl),
          name: clsName,
          isTopLevel: isTopLevel,
          fileIdentifier: fileIdentifier,
          isDeprecated: isDeprecated,
        );
        _idl.classes[clsName] = cls;
        String expectedBase = 'base.SummaryClass';
        var superclass = decl.superclass;
        if (superclass == null || superclass.name != expectedBase) {
          throw Exception('Class `$clsName` needs to extend `$expectedBase`');
        }
        for (ClassMember classMember in decl.members) {
          if (classMember is MethodDeclaration && classMember.isGetter) {
            _addFieldForGetter(cls, classMember);
          } else if (classMember is ConstructorDeclaration &&
              classMember.name.endsWith('fromBuffer')) {
            // Ignore `fromBuffer` declarations; they simply forward to the
            // read functions generated by [_generateReadFunction].
          } else {
            throw Exception('Unexpected class member `$classMember`');
          }
        }
      } else if (decl is EnumDeclaration) {
        var doc = _getNodeDoc(decl);
        idl_model.EnumDeclaration enm =
            idl_model.EnumDeclaration(doc, decl.name);
        _idl.enums[enm.name] = enm;
        for (EnumConstantDeclaration constDecl in decl.constants) {
          var doc = _getNodeDoc(constDecl);
          enm.values.add(idl_model.EnumValueDeclaration(doc, constDecl.name));
        }
      } else {
        throw Exception('Unexpected declaration `$decl`');
      }
    }
  }

  /// Entry point to the code generator when generating the "format.fbs" file.
  void generateFlatBufferSchema() {
    outputHeader();
    _FlatBufferSchemaGenerator(_idl, _outBuffer).generate();
  }

  /// Entry point to the code generator when generating the "format.dart" file.
  void generateFormatCode() {
    outputHeader();
    out("// The generator sometimes generates unnecessary 'this' references.");
    out('// ignore_for_file: unnecessary_this');
    out();
    out("import 'dart:convert' as convert;");
    out("import 'dart:typed_data' as typed_data;");
    out();
    out("import 'package:analyzer/src/summary/api_signature.dart' as api_sig;");
    out("import 'package:analyzer/src/summary/flat_buffers.dart' as fb;");
    out("import 'package:analyzer/src/summary/idl.dart' as idl;");
    out();
    for (idl_model.EnumDeclaration enum_ in _idl.enums.values) {
      _EnumReaderGenerator(_idl, _outBuffer, enum_).generate();
      out();
    }
    for (idl_model.ClassDeclaration cls in _idl.classes.values) {
      if (!cls.isDeprecated) {
        _BuilderGenerator(_idl, _outBuffer, cls).generate();
        out();
      }
      if (cls.isTopLevel) {
        _ReaderGenerator(_idl, _outBuffer, cls).generateReaderFunction();
        out();
      }
      if (!cls.isDeprecated) {
        _ReaderGenerator(_idl, _outBuffer, cls).generateReader();
        out();
        _ImplGenerator(_idl, _outBuffer, cls).generate();
        out();
        _MixinGenerator(_idl, _outBuffer, cls).generate();
        out();
      }
    }
  }

  /// Add the string [s] to the output as a single line.
  void out([String s = '']) {
    _outBuffer.writeln(s);
  }

  void outputHeader() {
    out('// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file');
    out('// for details. All rights reserved. Use of this source code is governed by a');
    out('// BSD-style license that can be found in the LICENSE file.');
    out();
    out('// This file has been automatically generated.  Please do not edit it manually.');
    out('// To regenerate the file, use the SDK script');
    out('// "pkg/analyzer/tool/summary/generate.dart \$IDL_FILE_PATH",');
    out('// or "pkg/analyzer/tool/generate_files" for the analyzer package IDL/sources.');
    out();
  }

  void _addFieldForGetter(
    idl_model.ClassDeclaration cls,
    MethodDeclaration getter,
  ) {
    var desc = '${cls.name}.${getter.name}';

    var type = getter.returnType;
    if (type == null) {
      throw Exception('Getter needs a type: $desc');
    }

    var isList = false;
    if (type.name == 'List') {
      var typeArguments = type.typeArguments;
      if (typeArguments != null && typeArguments.length == 1) {
        isList = true;
        type = typeArguments[0];
      }
    }
    if (type.typeArguments != null) {
      throw Exception('Cannot handle type arguments in `$type`');
    }

    int? id;
    bool isDeprecated = false;
    bool isInformative = false;

    for (Annotation annotation in getter.metadata) {
      var arguments = annotation.arguments;
      if (annotation.name == 'Id') {
        if (id != null) {
          throw Exception('Duplicate @id annotation ($getter)');
        }
        if (arguments == null) {
          throw Exception('@Id must be passed an argument ($desc)');
        }
        if (arguments.length != 1) {
          throw Exception('@Id must be passed exactly one argument ($desc)');
        }

        var idExpression = arguments[0];
        if (idExpression is IntegerLiteral) {
          id = idExpression.value;
        } else {
          throw Exception(
            '@Id argument must be an integer literal ($desc)',
          );
        }
      } else if (annotation.name == 'deprecated') {
        if (arguments != null) {
          throw Exception('@deprecated does not take args ($desc)');
        }
        isDeprecated = true;
      } else if (annotation.name == 'informative') {
        isInformative = true;
      }
    }
    if (id == null) {
      throw Exception('Missing @id annotation ($desc)');
    }

    var fieldType = idl_model.FieldType(type.name, isList);

    cls.allFields.add(
      idl_model.FieldDeclaration(
        documentation: _getNodeDoc(getter),
        name: getter.name,
        type: fieldType,
        id: id,
        isDeprecated: isDeprecated,
        isInformative: isInformative,
      ),
    );
  }

  /// Return the documentation text of the given [node], or `null` if the [node]
  /// does not have a comment.  Each line is `\n` separated.
  String? _getNodeDoc(AnnotatedNode node) {
    var comment = node.documentationComment;
    if (comment != null && comment.isDocumentation) {
      if (comment.tokens.length == 1 &&
          comment.tokens.first.lexeme.startsWith('/*')) {
        Token token = comment.tokens.first;
        return token.lexeme.split('\n').map((String line) {
          line = line.trimLeft();
          if (line.startsWith('*')) line = ' $line';
          return line;
        }).join('\n');
      } else if (comment.tokens
          .every((token) => token.lexeme.startsWith('///'))) {
        return comment.tokens
            .map((token) => token.lexeme.trimLeft())
            .join('\n');
      }
    }
    return null;
  }
}

class _EnumReaderGenerator extends _BaseGenerator {
  final idl_model.EnumDeclaration enum_;

  _EnumReaderGenerator(super.idl, super.outBuffer, this.enum_);

  void generate() {
    String name = enum_.name;
    String readerName = '_${name}Reader';
    String count = '${idlPrefix(name)}.values.length';
    String def = '${idlPrefix(name)}.${enum_.values[0].name}';
    out('class $readerName extends fb.Reader<${idlPrefix(name)}> {');
    indent(() {
      out('const $readerName() : super();');
      out();
      out('@override');
      out('int get size => 1;');
      out();
      out('@override');
      out('${idlPrefix(name)} read(fb.BufferContext bc, int offset) {');
      indent(() {
        out('int index = const fb.Uint8Reader().read(bc, offset);');
        out('return index < $count ? ${idlPrefix(name)}.values[index] : $def;');
      });
      out('}');
    });
    out('}');
  }
}

class _FlatBufferSchemaGenerator extends _BaseGenerator {
  _FlatBufferSchemaGenerator(super.idl, super.outBuffer);

  void generate() {
    for (idl_model.EnumDeclaration enm in _idl.enums.values) {
      out();
      outDoc(enm.documentation);
      out('enum ${enm.name} : byte {');
      indent(() {
        for (int i = 0; i < enm.values.length; i++) {
          idl_model.EnumValueDeclaration value = enm.values[i];
          if (i != 0) {
            out();
          }
          String suffix = i < enm.values.length - 1 ? ',' : '';
          outDoc(value.documentation);
          out('${value.name}$suffix');
        }
      });
      out('}');
    }
    for (idl_model.ClassDeclaration cls in _idl.classes.values) {
      out();
      outDoc(cls.documentation);
      out('table ${cls.name} {');
      indent(() {
        for (int i = 0; i < cls.allFields.length; i++) {
          idl_model.FieldDeclaration field = cls.allFields[i];
          if (i != 0) {
            out();
          }
          outDoc(field.documentation);
          List<String> attributes = <String>['id: ${field.id}'];
          if (field.isDeprecated) {
            attributes.add('deprecated');
          }
          String attrText = attributes.join(', ');
          out('${field.name}:${_fbsType(field.type)} ($attrText);');
        }
      });
      out('}');
    }
    out();
    // Standard flatbuffers only support one root type.  We support multiple
    // root types.  For now work around this by forcing PackageBundle to be the
    // root type.
    // TODO(paulberry): come up with a better solution.
    var rootType = _idl.classes['AnalysisDriverResolvedUnit']!;
    out('root_type ${rootType.name};');
    var rootFileIdentifier = rootType.fileIdentifier;
    if (rootFileIdentifier != null) {
      out();
      out('file_identifier ${quoted(rootFileIdentifier)};');
    }
  }

  /// Generate a string representing the FlatBuffer schema type which should be
  /// used to represent [type].
  String _fbsType(idl_model.FieldType type) {
    String typeStr;
    switch (type.typeName) {
      case 'bool':
        typeStr = 'bool';
      case 'double':
        typeStr = 'double';
      case 'int':
        typeStr = 'uint';
      case 'String':
        typeStr = 'string';
      default:
        typeStr = type.typeName;
    }
    if (type.isList) {
      // FlatBuffers don't natively support a packed list of booleans, so we
      // treat it as a list of unsigned bytes, which is a compatible data
      // structure.
      if (typeStr == 'bool') {
        typeStr = 'ubyte';
      }
      return '[$typeStr]';
    } else {
      return typeStr;
    }
  }
}

class _ImplGenerator extends _BaseGenerator {
  final idl_model.ClassDeclaration cls;

  _ImplGenerator(super.idl, super.outBuffer, this.cls);

  void generate() {
    String name = cls.name;
    String implName = '_${name}Impl';
    String mixinName = '_${name}Mixin';
    out('class $implName extends Object with $mixinName'
        ' implements ${idlPrefix(name)} {');
    indent(() {
      out('final fb.BufferContext _bc;');
      out('final int _bcOffset;');
      out();
      out('$implName(this._bc, this._bcOffset);');
      out();
      // Write cache fields.
      for (idl_model.FieldDeclaration field in cls.fields) {
        String returnType = _dartType(field.type);
        String fieldName = field.name;
        out('$returnType? _$fieldName;');
      }
      // Write getters.
      for (idl_model.FieldDeclaration field in cls.allFields) {
        int index = field.id;
        String fieldName = field.name;
        idl_model.FieldType type = field.type;
        String typeName = type.typeName;
        // Prepare "readCode" + "def"
        String readCode;
        String? def = defaultValue(type, false);
        if (type.isList) {
          if (typeName == 'bool') {
            readCode = 'const fb.BoolListReader()';
          } else if (typeName == 'int') {
            readCode = 'const fb.Uint32ListReader()';
          } else if (typeName == 'double') {
            readCode = 'const fb.Float64ListReader()';
          } else if (typeName == 'String') {
            String itemCode = 'fb.StringReader()';
            readCode = 'const fb.ListReader<String>($itemCode)';
          } else if (_idl.classes.containsKey(typeName)) {
            String itemCode = '_${typeName}Reader()';
            readCode = 'const fb.ListReader<${idlPrefix(typeName)}>($itemCode)';
          } else if (_idl.enums.containsKey(typeName)) {
            String itemCode = '_${typeName}Reader()';
            readCode = 'const fb.ListReader<${idlPrefix(typeName)}>($itemCode)';
          } else {
            throw Exception('$name.$fieldName: illegal type ($type)');
          }
        } else if (typeName == 'bool') {
          readCode = 'const fb.BoolReader()';
        } else if (typeName == 'double') {
          readCode = 'const fb.Float64Reader()';
        } else if (typeName == 'int') {
          readCode = 'const fb.Uint32Reader()';
        } else if (typeName == 'String') {
          readCode = 'const fb.StringReader()';
        } else if (_idl.classes.containsKey(typeName)) {
          readCode = 'const _${typeName}Reader()';
        } else if (_idl.enums.containsKey(typeName)) {
          readCode = 'const _${typeName}Reader()';
        } else {
          throw Exception('$name.$fieldName: illegal type ($type)');
        }
        // Write the getter implementation.
        out();
        String returnType = _dartType(type);
        if (field.isDeprecated) {
          out('@override');
          out('Null get $fieldName => ${_BaseGenerator._throwDeprecated};');
        } else {
          out('@override');
          if (_isNullable(type)) {
            out('$returnType? get $fieldName {');
          } else {
            out('$returnType get $fieldName {');
          }
          indent(() {
            String readExpr;
            if (_isNullable(type)) {
              readExpr = '$readCode.vTableGetOrNull(_bc, _bcOffset, $index)';
            } else {
              readExpr = '$readCode.vTableGet(_bc, _bcOffset, $index, $def)';
            }
            out('return _$fieldName ??= $readExpr;');
          });
          out('}');
        }
      }
    });
    out('}');
  }

  /// Generate a string representing the Dart type which should be used to
  /// represent [type] when deserialized.
  String _dartType(idl_model.FieldType type) {
    String baseType = idlPrefix(type.typeName);
    if (type.isList) {
      return 'List<$baseType>';
    } else {
      return baseType;
    }
  }
}

class _MixinGenerator extends _BaseGenerator {
  final idl_model.ClassDeclaration cls;

  _MixinGenerator(super.idl, super.outBuffer, this.cls);

  void generate() {
    String name = cls.name;
    String mixinName = '_${name}Mixin';
    out('mixin $mixinName implements ${idlPrefix(name)} {');
    indent(() {
      String jsonCondition(idl_model.FieldType type, String name) {
        if (type.isList) {
          return '$name.isNotEmpty';
        } else {
          return '$name != ${defaultValue(type, false)}';
        }
      }

      String jsonStore(
          idl_model.FieldType type, String name, String localName) {
        _StringToString? convertItem;
        if (_idl.classes.containsKey(type.typeName)) {
          convertItem = (String name) => '$name.toJson()';
        } else if (_idl.enums.containsKey(type.typeName)) {
          // TODO(paulberry): it would be better to generate a const list of
          // strings so that we don't have to do this kludge.
          convertItem = (String name) => "$name.toString().split('.')[1]";
        } else if (type.typeName == 'double') {
          convertItem =
              (String name) => '$name.isFinite ? $name : $name.toString()';
        }
        String convertField;
        if (convertItem == null) {
          convertField = localName;
        } else if (type.isList) {
          convertField = '$localName.map((value) =>'
              ' ${convertItem('value')}).toList()';
        } else {
          convertField = convertItem(localName);
        }
        return 'result[${quoted(name)}] = $convertField';
      }

      void writeConditionalStatement(String condition, String statement) {
        out('if ($condition) {');
        out('  $statement;');
        out('}');
      }

      // Write toJson().
      out('@override');
      out('Map<String, Object> toJson() {');
      indent(() {
        out('Map<String, Object> result = <String, Object>{};');

        indent(() {
          for (idl_model.FieldDeclaration field in cls.fields) {
            var localName = 'local_${field.name}';
            out('var $localName = ${field.name};');
            String condition = jsonCondition(field.type, localName);
            String storeField = jsonStore(field.type, field.name, localName);
            writeConditionalStatement(condition, storeField);
          }
        });

        out('return result;');
      });
      out('}');
      out();

      // Write toMap().
      out('@override');
      out('Map<String, Object?> toMap() => {');
      indent(() {
        for (idl_model.FieldDeclaration field in cls.fields) {
          String fieldName = field.name;
          out('${quoted(fieldName)}: $fieldName,');
        }
      });
      out('};');
      out();
      // Write toString().
      out('@override');
      out('String toString() => convert.json.encode(toJson());');
    });
    out('}');
  }
}

class _ReaderGenerator extends _BaseGenerator {
  final idl_model.ClassDeclaration cls;

  _ReaderGenerator(super.idl, super.outBuffer, this.cls);

  void generateReader() {
    String name = cls.name;
    String readerName = '_${name}Reader';
    String implName = '_${name}Impl';
    out('class $readerName extends fb.TableReader<$implName> {');
    indent(() {
      out('const $readerName();');
      out();
      out('@override');
      out('$implName createObject(fb.BufferContext bc, int offset) => $implName(bc, offset);');
    });
    out('}');
  }

  void generateReaderFunction() {
    String name = cls.name;
    out('${idlPrefix(name)} read$name(List<int> buffer) {');
    indent(() {
      out('fb.BufferContext rootRef = fb.BufferContext.fromBytes(buffer);');
      out('return const _${name}Reader().read(rootRef, 0);');
    });
    out('}');
  }
}
