// 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.

library generate_vm_service_java;

import 'package:markdown/markdown.dart';
import 'package:pub_semver/pub_semver.dart';

import '../common/generate_common.dart';
import '../common/parser.dart';
import '../common/src_gen_common.dart';
import 'src_gen_java.dart';

export 'src_gen_java.dart' show JavaGenerator;

const String servicePackage = 'org.dartlang.vm.service';

const List<String> simpleTypes = [
  'BigDecimal',
  'boolean',
  'int',
  'String',
  'double'
];

const vmServiceJavadoc = '''
{@link VmService} allows control of and access to information in a running
Dart VM instance.
<br/>
Launch the Dart VM with the arguments:
<pre>
--pause_isolates_on_start
--observe
--enable-vm-service=some-port
</pre>
where <strong>some-port</strong> is a port number of your choice
which this client will use to communicate with the Dart VM.
See https://dart.dev/tools/dart-vm for more details.
Once the VM is running, instantiate a new {@link VmService}
to connect to that VM via {@link VmService#connect(String)}
or {@link VmService#localConnect(int)}.
<br/>
{@link VmService} is not thread safe and should only be accessed from
a single thread. In addition, a given VM should only be accessed from
a single instance of {@link VmService}.
<br/>
Calls to {@link VmService} should not be nested.
More specifically, you should not make any calls to {@link VmService}
from within any {@link Consumer} method.
''';

late Api api;

/// Convert documentation references
/// from spec style of `className` to javadoc style {@link className}
String? convertDocLinks(String? doc) {
  if (doc == null) return null;
  var sb = StringBuffer();
  int start = 0;
  int end = doc.indexOf('[');
  while (end != -1) {
    sb.write(doc.substring(start, end));
    start = end;
    end = doc.indexOf(']', start);
    if (end == -1) break;
    if (end == start + 1) {
      sb.write('[]');
    } else {
      sb.write('{@link ');
      sb.write(doc.substring(start + 1, end));
      sb.write('}');
    }
    start = end + 1;
    end = doc.indexOf('[', start);
  }
  sb.write(doc.substring(start));
  return sb.toString();
}

String? _coerceRefType(String? typeName) {
  if (typeName == 'Class') typeName = 'ClassObj';
  if (typeName == 'Error') typeName = 'ErrorObj';
  if (typeName == 'Object') typeName = 'Obj';
  if (typeName == '@Object') typeName = 'ObjRef';
  if (typeName == 'Function') typeName = 'Func';
  if (typeName == '@Function') typeName = 'FuncRef';
  if (typeName!.startsWith('@')) typeName = '${typeName.substring(1)}Ref';
  if (typeName == 'string') typeName = 'String';
  if (typeName == 'bool') typeName = 'boolean';
  if (typeName == 'num') typeName = 'BigDecimal';
  if (typeName == 'map') typeName = 'Map';
  return typeName;
}

class Api extends Member with ApiParseUtil {
  int? serviceMajor;
  int? serviceMinor;
  String? serviceVersion;
  List<Method> methods = [];
  List<Enum?> enums = [];
  List<Type?> types = [];
  Map<String, List<String>> streamIdMap = {};
  final String scriptLocation;

  @override
  String? get docs => null;

  @override
  String get name => 'api';

  Api(this.scriptLocation);

  void addProperty(String typeName, String propertyName, {String? javadoc}) {
    var t = types.firstWhere((t) => t!.name == typeName)!;
    for (var f in t.fields) {
      if (f.name == propertyName) {
        print('$typeName already has $propertyName field');
        return;
      }
    }
    var f = TypeField(t, javadoc);
    f.name = propertyName;
    f.type = MemberType();
    f.type.types = [TypeRef('String')];
    t.fields.add(f);
    print('added $propertyName field to $typeName');
  }

  void generate(JavaGenerator gen) {
    _setFileHeader();

    // Set default value for unspecified property
    setDefaultValue('Instance', 'valueAsStringIsTruncated');
    setDefaultValue('InstanceRef', 'valueAsStringIsTruncated');

    // Hack to populate method argument docs
    for (var m in methods) {
      for (var a in m.args) {
        if (a.hasDocs) continue;
        var t = types.firstWhere((Type? t) => t!.name == a.type.name,
            orElse: () => null);
        if (t != null) {
          a.docs = t.docs;
          continue;
        }
        var e = enums.firstWhere((Enum? e) => e!.name == a.type.name,
            orElse: () => null);
        if (e != null) {
          a.docs = e.docs;
          continue;
        }
      }
    }

    gen.writeType('$servicePackage.VmService', scriptLocation,
        (TypeWriter writer) {
      writer.addImport('com.google.gson.JsonArray');
      writer.addImport('com.google.gson.JsonObject');
      writer.addImport('com.google.gson.JsonPrimitive');
      writer.addImport('java.util.List');

      writer.addImport('$servicePackage.consumer.*');
      writer.addImport('$servicePackage.element.*');
      writer.javadoc = vmServiceJavadoc;
      writer.superclassName = '$servicePackage.VmServiceBase';

      for (String streamId in streamIdMap.keys.toList()..sort()) {
        String alias = streamId.toUpperCase();
        while (alias.startsWith('_')) {
          alias = alias.substring(1);
        }
        writer.addField('${alias}_STREAM_ID', 'String',
            modifiers: 'public static final', value: '"$streamId"');
      }

      writer.addField('versionMajor', 'int',
          modifiers: 'public static final',
          value: '$serviceMajor',
          javadoc:
              'The major version number of the protocol supported by this client.');
      writer.addField('versionMinor', 'int',
          modifiers: 'public static final',
          value: '$serviceMinor',
          javadoc:
              'The minor version number of the protocol supported by this client.');
      for (var m in methods) {
        m.generateVmServiceMethod(writer);
        if (m.hasOptionalArgs) {
          m.generateVmServiceMethod(writer, includeOptional: true);
        }
      }

      writer.addMethod('forwardResponse', [
        JavaMethodArg('consumer', 'Consumer'),
        JavaMethodArg('responseType', 'String'),
        JavaMethodArg('json', 'JsonObject')
      ], (StatementWriter writer) {
        var generatedForwards = <String>{};

        var sorted = methods.toList()
          ..sort((m1, m2) {
            return m1.consumerTypeName.compareTo(m2.consumerTypeName);
          });
        for (var m in sorted) {
          if (generatedForwards.add(m.consumerTypeName)) {
            m.generateVmServiceForward(writer);
          }
        }
        writer.addLine('if (consumer instanceof ServiceExtensionConsumer) {');
        writer
            .addLine('  ((ServiceExtensionConsumer) consumer).received(json);');
        writer.addLine('  return;');
        writer.addLine('}');
        writer.addLine('logUnknownResponse(consumer, json);');
      }, modifiers: null, isOverride: true);

      writer.addMethod('convertMapToJsonObject', [
        JavaMethodArg('map', 'Map<String, String>')
      ], (StatementWriter writer) {
        writer.addLine('JsonObject obj = new JsonObject();');
        writer.addLine('for (String key : map.keySet()) {');
        writer.addLine('  obj.addProperty(key, map.get(key));');
        writer.addLine('}');
        writer.addLine('return obj;');
      }, modifiers: 'private', returnType: 'JsonObject');

      writer.addMethod(
          'convertIterableToJsonArray', [JavaMethodArg('list', 'Iterable')],
          (StatementWriter writer) {
        writer.addLine('JsonArray arr = new JsonArray();');
        writer.addLine('for (Object element : list) {');
        writer.addLine('  arr.add(new JsonPrimitive(element.toString()));');
        writer.addLine('}');
        writer.addLine('return arr;');
      }, modifiers: 'private', returnType: 'JsonArray');
    });

    for (var m in methods) {
      m.generateConsumerInterface(gen);
    }
    for (var t in types) {
      t!.generateElement(gen);
    }
    for (var e in enums) {
      e!.generateEnum(gen);
    }
  }

  Type? getType(String? name) =>
      types.firstWhere((t) => t!.name == name, orElse: () => null);

  bool isEnumName(String? typeName) =>
      enums.any((Enum? e) => e!.name == typeName);

  void parse(List<Node> nodes) {
    Version version = ApiParseUtil.parseVersionSemVer(nodes);
    serviceMajor = version.major;
    serviceMinor = version.minor;
    serviceVersion = '$serviceMajor.$serviceMinor';

    // Look for h3 nodes
    // the pre following it is the definition
    // the optional p following that is the documentation

    String? h3Name;

    // TODO(helin24): This code does not capture documentation with more than
    //  one paragraph or lists. It may help to check generate_dart to fix this.
    for (int i = 0; i < nodes.length; i++) {
      Node node = nodes[i];

      if (isPre(node) && h3Name != null) {
        String definition = textForCode(node)
            .replaceAll('(string|Null)', 'string'); // this is terrible.
        String? docs;

        if (i + 1 < nodes.length && isPara(nodes[i + 1])) {
          Element p = nodes[++i] as Element;
          docs = collapseWhitespace(TextOutputVisitor.printText(p));
        }

        _parse(h3Name, definition, docs);
      } else if (isH3(node)) {
        h3Name = textForElement(node);
      } else if (isHeader(node)) {
        h3Name = null;
      } else if (isPara(node)) {
        var children = (node as Element).children!;
        if (children.isNotEmpty && children.first is Text) {
          var text = children.expand<String>((child) {
            if (child is Text) return [child.text];
            return [];
          }).join();
          if (text.startsWith('streamId |')) {
            _parseStreamIds(text);
          }
        }
      }
    }
    for (Type? type in types) {
      type!.calculateFieldOverrides();
    }
  }

  void setDefaultValue(String typeName, String propertyName) {
    var type = types.firstWhere((t) => t!.name == typeName)!;
    var field = type.fields.firstWhere((f) => f.name == propertyName);
    field.defaultValue = 'false';
  }

  void _parse(String name, String definition, [String? docs]) {
    name = replaceHTMLEntities(name.trim());
    definition = replaceHTMLEntities(definition.trim());
    if (docs != null) docs = replaceHTMLEntities(docs.trim());

    if (definition.startsWith('class ')) {
      types.add(Type(this, scriptLocation, name, definition, docs));
    } else if (name.substring(0, 1).toLowerCase() == name.substring(0, 1)) {
      methods.add(Method(name, scriptLocation, definition, docs));
    } else if (definition.startsWith('enum ')) {
      enums.add(Enum(name, scriptLocation, definition, docs));
    } else {
      throw 'unexpected entity: $name, $definition';
    }
  }

  void _parseStreamIds(String text) {
    for (String line in text.split('\n')) {
      if (line.startsWith('streamId |')) continue;
      if (line.startsWith('---')) continue;
      var index = line.indexOf('|');
      var streamId = line.substring(0, index).trim();
      List<String> eventTypes =
          List.from(line.substring(index + 1).split(',').map((t) => t.trim()));
      eventTypes.sort();
      streamIdMap[streamId] = eventTypes;
    }
  }

  void _setFileHeader() {
    fileHeader = r'''/*
 * Copyright (c) 2015, the Dart project authors.
 *
 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
''';
  }

  static String printNode(Node n) {
    if (n is Text) {
      return n.text;
    } else if (n is Element) {
      if (n.tag != 'h3') return n.tag;
      return '${n.tag}:[${n.children!.map((c) => printNode(c)).join(', ')}]';
    } else {
      return '$n';
    }
  }
}

class Enum extends Member {
  @override
  final String name;
  final String scriptLocation;
  @override
  final String? docs;

  List<EnumValue> enums = [];

  Enum(this.name, this.scriptLocation, String definition, [this.docs]) {
    _parse(Tokenizer(definition).tokenize());
  }

  String get elementTypeName => '$servicePackage.element.$name';

  void generateEnum(JavaGenerator gen) {
    gen.writeType(elementTypeName, scriptLocation, (TypeWriter writer) {
      writer.javadoc = convertDocLinks(docs);
      writer.isEnum = true;
      enums.sort((v1, v2) => v1.name!.compareTo(v2.name!));
      for (var value in enums) {
        writer.addEnumValue(value.name, javadoc: value.docs);
      }
      writer.addEnumValue('Unknown',
          javadoc: 'Represents a value returned by the VM'
              ' but unknown to this client.',
          isLast: true);
    });
  }

  void _parse(Token? token) {
    EnumParser(token).parseInto(this);
  }
}

class EnumParser extends Parser {
  EnumParser(super.startToken);

  void parseInto(Enum e) {
    // enum ErrorKind { UnhandledException, Foo, Bar }
    // enum name { (comment* name ,)+ }
    expect('enum');

    Token t = expectName();
    validate(t.text == e.name, 'enum name ${e.name} equals ${t.text}');
    expect('{');

    while (!t.eof) {
      if (consume('}')) break;
      String? docs = collectComments();
      t = expectName();
      consume(',');

      e.enums.add(EnumValue(e, t.text, docs));
    }
  }
}

class EnumValue extends Member {
  final Enum parent;
  @override
  final String? name;
  @override
  final String? docs;

  EnumValue(this.parent, this.name, [this.docs]);

  bool get isLast => parent.enums.last == this;
}

abstract class Member {
  String? get docs => null;

  bool get hasDocs => docs != null;

  String? get name;

  @override
  String toString() => name!;
}

class MemberType extends Member {
  List<TypeRef> types = [];

  MemberType();

  bool get hasSentinel => types.any((t) => t.name == 'Sentinel');

  bool get isEnum => types.length == 1 && api.isEnumName(types.first.name);

  bool get isMultipleReturns => types.length > 1;

  List<TypeRef> get subsetOfTypesThatAreSimple =>
      types.where((TypeRef typeRef) => typeRef.isSimple).toList();

  List<TypeRef> get subsetOfTypesThatAreNotSimple =>
      types.where((TypeRef typeRef) => !typeRef.isSimple).toList();

  bool get isSimple => types.length == 1 && types.first.isSimple;

  bool get isValueAndSentinel => types.length == 2 && hasSentinel;

  @override
  String? get name {
    if (types.isEmpty) return '';
    if (types.length == 1) return types.first.ref;
    return 'dynamic';
  }

  TypeRef? get valueType {
    if (types.length == 1) return types.first;
    if (isValueAndSentinel) {
      return types.firstWhere((t) => t.name != 'Sentinel');
    }
    return null;
  }

  void parse(Parser parser) {
    // foo|bar[]|baz
    // (@Instance|Sentinel)[]
    bool loop = true;
    bool isMulti = false;

    while (loop) {
      parser.consume('(');
      Token t = parser.expectName();
      if (parser.consume(')')) isMulti = true;
      TypeRef ref = TypeRef(_coerceRefType(t.text));
      types.add(ref);

      while (parser.consume('[')) {
        parser.expect(']');
        if (isMulti) {
          for (var t in types) {
            t.arrayDepth++;
          }
        } else {
          ref.arrayDepth++;
        }
      }

      loop = parser.consume('|');
    }
  }
}

class Method extends Member {
  @override
  final String name;
  final String scriptLocation;
  @override
  final String? docs;

  MemberType returnType = MemberType();
  bool deprecated = false;
  List<MethodArg> args = [];

  Method(this.name, this.scriptLocation, String definition, [this.docs]) {
    _parse(Tokenizer(definition).tokenize());
  }

  String get consumerTypeName {
    String? prefix;
    if (returnType.isMultipleReturns) {
      prefix = titleCase(name);
    } else {
      prefix = returnType.types.first.javaBoxedName;
    }
    return '$servicePackage.consumer.${prefix}Consumer';
  }

  bool get hasArgs => args.isNotEmpty;

  bool get hasOptionalArgs => args.any((MethodArg arg) => arg.optional);

  void generateConsumerInterface(JavaGenerator gen) {
    gen.writeType(consumerTypeName, scriptLocation, (TypeWriter writer) {
      writer.javadoc = convertDocLinks(returnType.docs);
      writer.interfaceNames.add('$servicePackage.consumer.Consumer');
      writer.isInterface = true;
      for (var t in returnType.types) {
        writer.addImport(t.elementTypeName);
        writer.addMethod(
            'received', [JavaMethodArg('response', t.elementTypeName)], null);
      }
    });
  }

  void generateVmServiceForward(StatementWriter writer) {
    var consumerName = classNameFor(consumerTypeName);
    writer.addLine('if (consumer instanceof $consumerName) {');
    List<Type?> types = List.from(returnType.types.map((ref) => ref.type));
    for (int index = 0; index < types.length; ++index) {
      types.addAll(types[index]!.subtypes);
    }
    types.sort((t1, t2) => t1!.name!.compareTo(t2!.name!));
    for (var t in types) {
      var responseName = classNameFor(t!.elementTypeName!);
      writer.addLine('  if (responseType.equals("${t.rawName}")) {');
      writer.addLine(
          '    (($consumerName) consumer).received(new $responseName(json));');
      writer.addLine('    return;');
      writer.addLine('  }');
    }
    writer.addLine('}');
  }

  void generateVmServiceMethod(TypeWriter writer, {includeOptional = false}) {
    // Update method docs
    var javadoc = StringBuffer(docs == null ? '' : docs!);
    bool firstParamDoc = true;
    for (var a in args) {
      if (!includeOptional && a.optional) continue;
      var paramDoc = StringBuffer(a.docs ?? '');
      if (paramDoc.isEmpty) {}
      if (a.optional) {
        if (paramDoc.isNotEmpty) paramDoc.write(' ');
        paramDoc.write('This parameter is optional and may be null.');
      }
      if (paramDoc.isNotEmpty) {
        if (firstParamDoc) {
          javadoc.writeln();
          firstParamDoc = false;
        }
        javadoc.writeln('@param ${a.name} $paramDoc');
      }
    }

    if (args.any((MethodArg arg) => (arg.type.name == 'Map'))) {
      writer.addImport('java.util.Map');
    }

    List<MethodArg> mthArgs = args;
    if (!includeOptional) {
      mthArgs = mthArgs.toList()..removeWhere((a) => a.optional);
    }

    List<JavaMethodArg> javaMethodArgs =
        List.from(mthArgs.map((a) => a.asJavaMethodArg));
    javaMethodArgs
        .add(JavaMethodArg('consumer', classNameFor(consumerTypeName)));
    writer.addMethod(name, javaMethodArgs, (StatementWriter writer) {
      writer.addLine('final JsonObject params = new JsonObject();');
      for (MethodArg arg in args) {
        if (!includeOptional && arg.optional) continue;
        var argName = arg.name;
        String op = arg.optional ? 'if ($argName != null) ' : '';
        if (arg.isEnumType) {
          writer
              .addLine('${op}params.addProperty("$argName", $argName.name());');
        } else if (arg.type.name == 'Map') {
          writer.addLine(
              '${op}params.add("$argName", convertMapToJsonObject($argName));');
        } else if (arg.type.arrayDepth > 0) {
          writer.addLine(
              '${op}params.add("$argName", convertIterableToJsonArray($argName));');
        } else if (name.startsWith('evaluate') && argName == 'expression') {
          // Special case the eval expression parameters.
          writer.addLine(
              '${op}params.addProperty("$argName", removeNewLines($argName));');
        } else {
          writer.addLine('${op}params.addProperty("$argName", $argName);');
        }
      }
      writer.addLine('request("$name", params, consumer);');
    }, javadoc: javadoc.toString(), isDeprecated: deprecated);
  }

  void _parse(Token? token) {
    MethodParser(token).parseInto(this);
  }
}

class MethodArg extends Member {
  final Method parent;
  final TypeRef type;
  @override
  String? name;
  @override
  String? docs;
  bool optional = false;

  MethodArg(this.parent, this.type, this.name);

  JavaMethodArg get asJavaMethodArg {
    if (optional && type.ref == 'int') {
      return JavaMethodArg(name, 'Integer');
    }
    if (optional && type.ref == 'double') {
      return JavaMethodArg(name, 'Double');
    }
    if (optional && type.ref == 'boolean') {
      return JavaMethodArg(name, 'Boolean');
    }
    return JavaMethodArg(name, type.ref);
  }

  /// TODO: Hacked enum arg type determination
  bool get isEnumType =>
      name == 'step' || name == 'mode' || name == 'exceptionPauseMode';
}

class MethodParser extends Parser {
  MethodParser(super.startToken);

  void parseInto(Method method) {
    // method is return type, name, (, args )
    // args is type name, [optional], comma

    if (peek()?.text?.startsWith('@deprecated') ?? false) {
      advance();
      expect('(');
      consumeString();
      expect(')');
      method.deprecated = true;
    }
    method.returnType.parse(this);

    Token t = expectName();
    validate(
        t.text == method.name, 'method name ${method.name} equals ${t.text}');

    expect('(');

    while (peek()!.text != ')') {
      Token type = expectName();
      TypeRef ref = TypeRef(_coerceRefType(type.text));
      if (peek()!.text == '[') {
        while (consume('[')) {
          expect(']');
          ref.arrayDepth++;
        }
      } else if (peek()!.text == '<') {
        // handle generics
        expect('<');
        ref.genericTypes = [];
        while (peek()!.text != '>') {
          Token genericTypeName = expectName();
          ref.genericTypes!.add(TypeRef(_coerceRefType(genericTypeName.text)));
          consume(',');
        }
        expect('>');
      }
      Token name = expectName();
      MethodArg arg = MethodArg(method, ref, name.text);
      if (consume('[')) {
        expect('optional');
        expect(']');
        arg.optional = true;
      }
      method.args.add(arg);
      consume(',');
    }

    expect(')');
  }
}

class TextOutputVisitor implements NodeVisitor {
  StringBuffer buf = StringBuffer();

  bool _inRef = false;

  TextOutputVisitor();

  @override
  String toString() => buf.toString().trim();

  @override
  bool visitElementBefore(Element element) {
    if (element.tag == 'em') {
      buf.write('[');
      _inRef = true;
    } else if (element.tag == 'p') {
      // Nothing to do.
    } else if (element.tag == 'a') {
      // Nothing to do - we're not writing out <a> refs (they won't resolve).
    } else if (element.tag == 'code') {
      buf.write(renderToHtml([element]));
    } else {
      print('unknown tag: ${element.tag}');
      buf.write(renderToHtml([element]));
    }

    return true;
  }

  @override
  void visitText(Text text) {
    String? t = text.text;
    if (_inRef) t = _coerceRefType(t);
    buf.write(t);
  }

  @override
  void visitElementAfter(Element element) {
    if (element.tag == 'em') {
      buf.write(']');
      _inRef = false;
    } else if (element.tag == 'p') {
      buf.write('\n\n');
    }
  }

  static String printText(Node node) {
    TextOutputVisitor visitor = TextOutputVisitor();
    node.accept(visitor);
    return visitor.toString();
  }
}

class Type extends Member {
  final Api parent;
  final String scriptLocation;
  String? rawName;
  @override
  String? name;
  String? superName;
  @override
  final String? docs;
  List<TypeField> fields = [];

  Type(this.parent, this.scriptLocation, String categoryName, String definition,
      [this.docs]) {
    _parse(Tokenizer(definition).tokenize());
  }

  String? get elementTypeName {
    if (isSimple) return null;
    return '$servicePackage.element.$name';
  }

  bool get isRef => name!.endsWith('Ref');

  bool get isResponse {
    if (superName == null) return false;
    if (name == 'Response' || superName == 'Response') return true;
    return parent.getType(superName)!.isResponse;
  }

  bool get isSimple => simpleTypes.contains(name);

  String? get jsonTypeName {
    if (name == 'ClassObj') return 'Class';
    if (name == 'ErrorObj') return 'Error';
    return name;
  }

  Iterable<Type?> get subtypes =>
      api.types.toList()..retainWhere((t) => t!.superName == name);

  void generateElement(JavaGenerator gen) {
    gen.writeType('$servicePackage.element.$name', scriptLocation,
        (TypeWriter writer) {
      if (fields.any((f) => f.type.types.any((t) => t.isArray))) {
        writer.addImport('com.google.gson.JsonObject');
      }
      writer.addImport('com.google.gson.JsonObject');
      writer.javadoc = convertDocLinks(docs);
      writer.superclassName = superName ?? 'Element';
      writer.addConstructor(
          <JavaMethodArg>[JavaMethodArg('json', 'com.google.gson.JsonObject')],
          (StatementWriter writer) {
        writer.addLine('super(json);');
      });

      if (name == 'InstanceRef' || name == 'Instance') {
        writer.addMethod(
          'isNull',
          [],
          (StatementWriter writer) {
            writer.addLine('return getKind() == InstanceKind.Null;');
          },
          returnType: 'boolean',
          javadoc: 'Returns whether this instance represents null.',
        );
      }

      for (var field in fields) {
        field.generateAccessor(writer);
      }
    });
  }

  List<TypeField> getAllFields() {
    if (superName == null) return fields;

    List<TypeField> all = [];
    all.insertAll(0, fields);

    Type? s = getSuper();
    while (s != null) {
      all.insertAll(0, s.fields);
      s = s.getSuper();
    }

    return all;
  }

  Type? getSuper() => superName == null ? null : api.getType(superName);

  bool hasField(String? name) {
    if (fields.any((field) => field.name == name)) return true;
    return getSuper()?.hasField(name) ?? false;
  }

  void _parse(Token? token) {
    TypeParser(token).parseInto(this);
  }

  void calculateFieldOverrides() {
    for (TypeField field in fields.toList()) {
      if (superName == null) continue;

      if (getSuper()!.hasField(field.name)) {
        field.setOverrides();
      }
    }
  }
}

// @Instance|@Error|Sentinel evaluate(
//     string isolateId,
//     string targetId [optional],
//     string expression)
class TypeField extends Member {
  static final Map<String, String> _nameRemap = {
    'const': 'isConst',
    'final': 'isFinal',
    'static': 'isStatic',
    'abstract': 'isAbstract',
    'super': 'superClass',
    'class': 'classRef'
  };

  final Type parent;
  final String? _docs;
  MemberType type = MemberType();
  @override
  String? name;
  bool optional = false;
  String? defaultValue;
  bool overrides = false;

  TypeField(this.parent, this._docs);

  void setOverrides() {
    overrides = true;
  }

  String get accessorName {
    var remappedName = _nameRemap[name];
    if (remappedName != null) {
      if (remappedName.startsWith('is')) return remappedName;
    } else {
      remappedName = name;
    }
    return 'get${titleCase(remappedName!)}';
  }

  @override
  String? get docs {
    String str = _docs ?? '';
    if (type.isMultipleReturns) {
      str += '\n\n@return one of '
          '${joinLast(type.types.map((t) => '<code>$t</code>'), ', ', ' or ')}';
      str = str.trim();
    }
    if (optional) {
      str += '\n\nCan return <code>null</code>.';
      str = str.trim();
    }
    return str;
  }

  void generateAccessor(TypeWriter writer) {
    if (type.isMultipleReturns && !type.isValueAndSentinel) {
      writer.addMethod(accessorName, [], (StatementWriter w) {
        w.addImport('com.google.gson.JsonElement');
        w.addLine('final JsonElement elem = json.get("$name");');
        w.addLine('if (elem == null) return null;\n');
        final subsetOfTypesThatAreSimple = type.subsetOfTypesThatAreSimple;
        final subsetOfTypesThatAreNotSimple =
            type.subsetOfTypesThatAreNotSimple;
        if (subsetOfTypesThatAreSimple.isNotEmpty) {
          w.addImport('com.google.gson.JsonPrimitive');
          w.addLine('if (elem.isJsonPrimitive()) {');
          w.addLine('final JsonPrimitive p = (JsonPrimitive) elem;');
          for (TypeRef t in subsetOfTypesThatAreSimple) {
            if (t.name == 'boolean') {
              w.addLine('if (p.isBoolean()) return p.getAsBoolean();');
            } else if (t.name == 'int') {
              w.addLine('if (p.isNumber()) return p.getAsInt();');
            } else if (t.name == 'String') {
              w.addLine('if (p.isString()) return p.getAsString();');
            }
          }
          w.addLine('}');
        }
        if (subsetOfTypesThatAreNotSimple.isNotEmpty) {
          w.addImport('com.google.gson.JsonObject');
          w.addLine('if (elem.isJsonObject()) {');
          w.addLine('final JsonObject o = (JsonObject) elem;');
          for (TypeRef t in subsetOfTypesThatAreNotSimple) {
            String refName = t.name!;
            if (refName.endsWith('Ref')) {
              refName = '@${refName.substring(0, refName.length - 3)}';
            }
            w.addLine('if (o.get("type").getAsString().equals("$refName")) '
                'return new ${t.name}(o);');
          }
          w.addLine('}');
        }
        w.addLine('return null;');
      }, javadoc: docs, returnType: 'Object');
    } else {
      String? returnType = type.valueType!.ref;
      if (name == 'timestamp') {
        returnType = 'long';
      }

      writer.addMethod(
        accessorName,
        [],
        (StatementWriter writer) {
          type.valueType!.generateAccessStatements(
            writer,
            name,
            canBeSentinel: type.isValueAndSentinel,
            defaultValue: defaultValue,
            optional: optional,
          );
        },
        javadoc: docs,
        returnType: returnType,
        isOverride: overrides,
      );
    }
  }
}

class TypeParser extends Parser {
  TypeParser(super.startToken);

  void parseInto(Type type) {
    // class ClassList extends Response {
    //   // Docs here.
    //   @Class[] classes [optional];
    // }
    expect('class');

    Token t = expectName();
    type.rawName = t.text;
    type.name = _coerceRefType(type.rawName);
    if (consume('extends')) {
      t = expectName();
      type.superName = _coerceRefType(t.text);
    }

    expect('{');

    while (peek()!.text != '}') {
      TypeField field = TypeField(type, collectComments());
      field.type.parse(this);
      field.name = expectName().text;
      if (consume('[')) {
        expect('optional');
        expect(']');
        field.optional = true;
      }
      type.fields.add(field);
      expect(';');
    }

    expect('}');
  }
}

class TypeRef {
  String? name;
  int arrayDepth = 0;
  List<TypeRef>? genericTypes;

  TypeRef(this.name);

  String? get elementTypeName {
    if (isSimple) return null;
    return '$servicePackage.element.$name';
  }

  bool get isArray => arrayDepth > 0;

  /// Hacked enum determination
  bool get isEnum => name!.endsWith('Kind') || name!.endsWith('Mode');

  bool get isSimple => simpleTypes.contains(name);

  String? get javaBoxedName {
    if (name == 'boolean') return 'Boolean';
    if (name == 'int') return 'Integer';
    if (name == 'double') return 'Double';
    return name;
  }

  String? get ref {
    if (genericTypes != null) {
      return '$name<${genericTypes!.join(', ')}>';
    } else if (isSimple) {
      if (arrayDepth == 2) return 'List<List<$javaBoxedName>>';
      if (arrayDepth == 1) return 'List<$javaBoxedName>';
    } else {
      if (arrayDepth == 2) return 'ElementList<ElementList<$javaBoxedName>>';
      if (arrayDepth == 1) return 'ElementList<$javaBoxedName>';
    }
    return name;
  }

  Type? get type => api.types.firstWhere((t) => t!.name == name);

  void generateAccessStatements(
    StatementWriter writer,
    String? propertyName, {
    bool canBeSentinel = false,
    String? defaultValue,
    bool optional = false,
  }) {
    if (name == 'boolean') {
      if (isArray) {
        print('skipped accessor body for $propertyName');
      } else {
        if (defaultValue != null) {
          writer.addImport('com.google.gson.JsonElement');
          writer.addLine('final JsonElement elem = json.get("$propertyName");');
          writer.addLine(
              'return elem != null ? elem.getAsBoolean() : $defaultValue;');
        } else {
          writer.addLine('return getAsBoolean("$propertyName");');
        }
      }
    } else if (name == 'int') {
      if (arrayDepth > 1) {
        writer.addImport('java.util.List');
        writer.addLine('return getListListInt("$propertyName");');
      } else if (arrayDepth == 1) {
        writer.addImport('java.util.List');
        writer.addLine('return getListInt("$propertyName");');
      } else {
        if (propertyName == 'timestamp') {
          writer.addLine('return json.get("$propertyName") == null ? '
              '-1 : json.get("$propertyName").getAsLong();');
        } else {
          writer.addLine('return getAsInt("$propertyName");');
        }
      }
    } else if (name == 'double') {
      writer.addLine('return json.get("$propertyName") == null ? '
          '0.0 : json.get("$propertyName").getAsDouble();');
    } else if (name == 'BigDecimal') {
      if (isArray) {
        print('skipped accessor body for $propertyName');
      } else {
        writer.addImport('java.math.BigDecimal');
        writer.addLine('return json.get("$propertyName").getAsBigDecimal();');
      }
    } else if (name == 'String') {
      if (isArray) {
        writer.addImport('java.util.List');
        if (optional) {
          writer.addLine('return json.get("$propertyName") == null ? '
              'null : getListString("$propertyName");');
        } else {
          writer.addLine('return getListString("$propertyName");');
        }
      } else {
        writer.addLine('return getAsString("$propertyName");');
      }
    } else if (isEnum) {
      if (isArray) {
        print('skipped accessor body for $propertyName');
      } else {
        if (optional) {
          writer.addLine('if (json.get("$propertyName") == null) return null;');
          writer.addLine('');
        }
        writer.addImport('com.google.gson.JsonElement');
        writer.addLine('final JsonElement value = json.get("$propertyName");');
        writer.addLine('try {');
        writer.addLine('  return value == null ? $name.Unknown'
            ' : $name.valueOf(value.getAsString());');
        writer.addLine('} catch (IllegalArgumentException e) {');
        writer.addLine('  return $name.Unknown;');
        writer.addLine('}');
      }
    } else {
      if (arrayDepth > 1) {
        print('skipped accessor body for $propertyName');
      } else if (arrayDepth == 1) {
        writer.addImport('com.google.gson.JsonArray');
        if (optional) {
          writer.addLine('if (json.get("$propertyName") == null) return null;');
          writer.addLine('');
        }
        writer.addLine(
            'return new ElementList<$javaBoxedName>(json.get("$propertyName").getAsJsonArray()) {');
        writer.addLine('  @Override');
        writer.addLine(
            '  protected $javaBoxedName basicGet(JsonArray array, int index) {');
        writer.addLine(
            '    return new $javaBoxedName(array.get(index).getAsJsonObject());');
        writer.addLine('  }');
        writer.addLine('};');
      } else {
        if (canBeSentinel) {
          writer.addImport('com.google.gson.JsonElement');
          writer.addLine('final JsonElement elem = json.get("$propertyName");');
          writer.addLine('if (!elem.isJsonObject()) return null;');
          writer.addLine('final JsonObject child = elem.getAsJsonObject();');
          writer
              .addLine('final String type = child.get("type").getAsString();');
          writer.addLine('if ("Sentinel".equals(type)) return null;');
          writer.addLine('return new $name(child);');
        } else {
          if (optional) {
            writer.addLine(
                'JsonObject obj = (JsonObject) json.get("$propertyName");');
            writer.addLine('if (obj == null) return null;');
            if ((name != 'InstanceRef') && (name != 'Instance')) {
              writer.addLine(
                  'final String type = json.get("type").getAsString();');
              writer.addLine(
                  'if ("Instance".equals(type) || "@Instance".equals(type)) {');
              writer.addLine(
                  '  final String kind = json.get("kind").getAsString();');
              writer.addLine('  if ("Null".equals(kind)) return null;');
              writer.addLine('}');
            }
            writer.addLine('return new $name(obj);');
          } else {
            writer.addLine(
                'return new $name((JsonObject) json.get("$propertyName"));');
          }
        }
      }
    }
  }

  @override
  String toString() => ref!;
}
