/*
 * Copyright 2021 Red Hat
 * Portions Copyright 2020 Amazon.com, Inc. or its affiliates.
 * All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */

package com.amazonaws.services.schemaregistry.utils.apicurio;

import additionalTypes.Decimals;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.*;
import com.google.protobuf.Descriptors.DescriptorValidationException;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.FileDescriptor;
import com.google.type.*;
import com.squareup.wire.Syntax;
import com.squareup.wire.schema.Field;
import com.squareup.wire.schema.Schema;
import com.squareup.wire.schema.Service;
import com.squareup.wire.schema.Type;
import com.squareup.wire.schema.*;
import com.squareup.wire.schema.internal.parser.*;
import kotlin.ranges.IntRange;
import metadata.ProtobufSchemaMetadata;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.google.common.base.CaseFormat.LOWER_UNDERSCORE;
import static com.google.common.base.CaseFormat.UPPER_CAMEL;
import static com.google.protobuf.DescriptorProtos.*;

/**
 * @author Fabian Martinez, Ravindranath Kakarla, Carles Arnal
 */
public class FileDescriptorUtils {

    public static final Location DEFAULT_LOCATION = Location.get("");

    private static final String PROTO2 = "proto2";
    private static final String PROTO3 = "proto3";
    private static final String ALLOW_ALIAS_OPTION = "allow_alias";
    private static final String MAP_ENTRY_OPTION = "map_entry";
    private static final String KEY_FIELD = "key";
    private static final String VALUE_FIELD = "value";
    private static final String MAP_ENTRY_SUFFIX = "Entry";
    private static final String DEPRECATED_OPTION = "deprecated";
    private static final String OPTIONAL = "optional";

    // field options
    private static final String PACKED_OPTION = "packed";
    private static final String JSON_NAME_OPTION = "json_name";
    private static final String CTYPE_OPTION = "ctype";
    private static final String JSTYPE_OPTION = "jstype";
    // file options
    private static final String CC_GENERIC_SERVICES_OPTION = "cc_generic_services";
    private static final String CC_ENABLE_ARENAS_OPTION = "cc_enable_arenas";
    private static final String CSHARP_NAMESPACE_OPTION = "csharp_namespace";
    private static final String GO_PACKAGE_OPTION = "go_package";
    private static final String JAVA_GENERIC_SERVICES_OPTION = "java_generic_services";
    private static final String JAVA_MULTIPLE_FILES_OPTION = "java_multiple_files";
    private static final String JAVA_OUTER_CLASSNAME_OPTION = "java_outer_classname";
    private static final String JAVA_PACKAGE_OPTION = "java_package";
    private static final String JAVA_STRING_CHECK_UTF8_OPTION = "java_string_check_utf8";
    private static final String OBJC_CLASS_PREFIX_OPTION = "objc_class_prefix";
    private static final String OPTIMIZE_FOR_OPTION = "optimize_for";
    private static final String PHP_CLASS_PREFIX_OPTION = "php_class_prefix";
    private static final String PHP_GENERIC_SERVICES_OPTION = "php_generic_services";
    private static final String PHP_METADATA_NAMESPACE_OPTION = "php_metadata_namespace";
    private static final String PHP_NAMESPACE_OPTION = "php_namespace";
    private static final String PY_GENERIC_SERVICES_OPTION = "py_generic_services";
    private static final String RUBY_PACKAGE_OPTION = "ruby_package";
    private static final String SWIFT_PREFIX_OPTION = "swift_prefix";
    // message options
    private static final String NO_STANDARD_DESCRIPTOR_OPTION = "no_standard_descriptor_accessor";
    // rpc options
    private static final String IDEMPOTENCY_LEVEL_OPTION = "idempotency_level";

    private static final OptionElement.Kind booleanKind =  OptionElement.Kind.BOOLEAN;
    private static final OptionElement.Kind stringKind =  OptionElement.Kind.STRING;
    private static final OptionElement.Kind enumKind =  OptionElement.Kind.ENUM;

    public static FileDescriptor[] baseDependencies() {
        //Support all the Protobuf WellKnownTypes
        //and the protos from Google API, https://github.com/googleapis/googleapis
        return new FileDescriptor[] {
            ApiProto.getDescriptor().getFile(),
            FieldMaskProto.getDescriptor().getFile(),
            SourceContextProto.getDescriptor().getFile(),
            StructProto.getDescriptor().getFile(),
            TypeProto.getDescriptor().getFile(),
            TimestampProto.getDescriptor().getFile(),
            WrappersProto.getDescriptor().getFile(),
            AnyProto.getDescriptor().getFile(),
            EmptyProto.getDescriptor().getFile(),
            DurationProto.getDescriptor().getFile(),
            TimeOfDayProto.getDescriptor().getFile(),
            DateProto.getDescriptor().getFile(),
            CalendarPeriodProto.getDescriptor().getFile(),
            ColorProto.getDescriptor().getFile(),
            DayOfWeek.getDescriptor().getFile(),
            LatLng.getDescriptor().getFile(),
            FractionProto.getDescriptor().getFile(),
            MoneyProto.getDescriptor().getFile(),
            MonthProto.getDescriptor().getFile(),
            PhoneNumberProto.getDescriptor().getFile(),
            PostalAddressProto.getDescriptor().getFile(),
            CalendarPeriodProto.getDescriptor().getFile(),
            LocalizedTextProto.getDescriptor().getFile(),
            IntervalProto.getDescriptor().getFile(),
            ExprProto.getDescriptor().getFile(),
            QuaternionProto.getDescriptor().getFile(),
            PostalAddressProto.getDescriptor().getFile(),
            ProtobufSchemaMetadata.getDescriptor().getFile(),
            Decimals.getDescriptor().getFile()
        };
    }

    public static FileDescriptor protoFileToFileDescriptor(ProtoFileElement element)
        throws DescriptorValidationException {
        return protoFileToFileDescriptor(element, "default.proto");
    }

    public static FileDescriptor protoFileToFileDescriptor(ProtoFileElement element, String protoFileName) throws DescriptorValidationException {
        Objects.requireNonNull(element);
        Objects.requireNonNull(protoFileName);

        return protoFileToFileDescriptor(element.toSchema(), protoFileName,
            Optional.ofNullable(element.getPackageName()));
    }

    public static FileDescriptor protoFileToFileDescriptor(String schemaDefinition, String protoFileName, Optional<String> optionalPackageName)
        throws DescriptorValidationException {
        Objects.requireNonNull(schemaDefinition);
        Objects.requireNonNull(protoFileName);

        return FileDescriptor.buildFrom(toFileDescriptorProto(schemaDefinition, protoFileName, optionalPackageName), baseDependencies());
    }

    private static FileDescriptorProto toFileDescriptorProto(String schemaDefinition, String protoFileName, Optional<String> optionalPackageName) {
        final ProtobufSchemaLoader.ProtobufSchemaLoaderContext protobufSchemaLoaderContext;
        try {
            protobufSchemaLoaderContext =
                ProtobufSchemaLoader.loadSchema(optionalPackageName, protoFileName, schemaDefinition);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        FileDescriptorProto.Builder schema = FileDescriptorProto.newBuilder();

        ProtoFile element = protobufSchemaLoaderContext.getProtoFile();
        Schema schemaContext = protobufSchemaLoaderContext.getSchema();

        schema.setName(protoFileName);

        Syntax syntax = element.getSyntax();
        if (Syntax.PROTO_3.equals(syntax)) {
            schema.setSyntax(syntax.toString());
        }
        if (element.getPackageName() != null) {
            schema.setPackage(element.getPackageName());
        }

        for (ProtoType protoType : schemaContext.getTypes()) {
            if (!isParentLevelType(protoType, optionalPackageName)) {
                continue;
            }

            Type type = schemaContext.getType(protoType);
            if (type instanceof MessageType) {
                DescriptorProto
                    message = messageElementToDescriptorProto((MessageType) type, schemaContext, element);
                schema.addMessageType(message);
            } else if (type instanceof EnumType) {
                EnumDescriptorProto message = enumElementToProto((EnumType) type);
                schema.addEnumType(message);
            }
        }

        for (Service service : element.getServices()) {
            ServiceDescriptorProto serviceDescriptorProto = serviceElementToProto(service);
            schema.addService(serviceDescriptorProto);
        }

        //dependencies on protobuf default types are always added
        for (String ref : element.getImports()) {
            schema.addDependency(ref);
        }
        for (String ref : element.getPublicImports()) {
            boolean add = true;
            for (int i = 0; i < schema.getDependencyCount(); i++) {
                if (schema.getDependency(i).equals(ref)) {
                    schema.addPublicDependency(i);
                    add = false;
                }
            }
            if (add) {
                schema.addDependency(ref);
                schema.addPublicDependency(schema.getDependencyCount() - 1);
            }
        }

        String javaPackageName = findOptionString(JAVA_PACKAGE_OPTION, element.getOptions());
        if (javaPackageName != null) {
            FileOptions options = FileOptions.newBuilder().setJavaPackage(javaPackageName).build();
            schema.mergeOptions(options);
        }

        String javaOuterClassname = findOptionString(JAVA_OUTER_CLASSNAME_OPTION, element.getOptions());
        if (javaOuterClassname != null) {
            FileOptions options = FileOptions.newBuilder().setJavaOuterClassname(javaOuterClassname).build();
            schema.mergeOptions(options);
        }

        Boolean javaMultipleFiles = findOptionBoolean(JAVA_MULTIPLE_FILES_OPTION, element.getOptions());
        if (javaMultipleFiles != null) {
            FileOptions options = FileOptions.newBuilder().setJavaMultipleFiles(javaMultipleFiles).build();
            schema.mergeOptions(options);
        }

        Boolean javaStringCheckUtf8 = findOptionBoolean(JAVA_STRING_CHECK_UTF8_OPTION, element.getOptions());
        if (javaStringCheckUtf8 != null) {
            FileOptions options = FileOptions.newBuilder().setJavaStringCheckUtf8(javaStringCheckUtf8).build();
            schema.mergeOptions(options);
        }

        Boolean javaGenericServices= findOptionBoolean(JAVA_GENERIC_SERVICES_OPTION, element.getOptions());
        if (javaGenericServices != null) {
            FileOptions options = FileOptions.newBuilder().setJavaGenericServices(javaGenericServices).build();
            schema.mergeOptions(options);
        }

        Boolean ccGenericServices= findOptionBoolean(CC_GENERIC_SERVICES_OPTION, element.getOptions());
        if (ccGenericServices != null) {
            FileOptions options = FileOptions.newBuilder().setCcGenericServices(ccGenericServices).build();
            schema.mergeOptions(options);
        }

        Boolean ccEnableArenas= findOptionBoolean(CC_ENABLE_ARENAS_OPTION, element.getOptions());
        if (ccEnableArenas != null) {
            FileOptions options = FileOptions.newBuilder().setCcEnableArenas(ccEnableArenas).build();
            schema.mergeOptions(options);
        }

        String csharpNamespace = findOptionString(CSHARP_NAMESPACE_OPTION, element.getOptions());
        if (csharpNamespace != null) {
            FileOptions options = FileOptions.newBuilder().setCsharpNamespace(csharpNamespace).build();
            schema.mergeOptions(options);
        }

        String goPackageName = findOptionString(GO_PACKAGE_OPTION, element.getOptions());
        if (goPackageName != null) {
            FileOptions options = FileOptions.newBuilder().setGoPackage(goPackageName).build();
            schema.mergeOptions(options);
        }

        String objcClassPrefix = findOptionString(OBJC_CLASS_PREFIX_OPTION, element.getOptions());
        if (objcClassPrefix != null) {
            FileOptions options = FileOptions.newBuilder().setObjcClassPrefix(objcClassPrefix).build();
            schema.mergeOptions(options);
        }

        Boolean phpGenericServices= findOptionBoolean(PHP_GENERIC_SERVICES_OPTION, element.getOptions());
        if (phpGenericServices != null) {
            FileOptions options = FileOptions.newBuilder().setPhpGenericServices(phpGenericServices).build();
            schema.mergeOptions(options);
        }

        String phpClassPrefix = findOptionString(PHP_CLASS_PREFIX_OPTION, element.getOptions());
        if (phpClassPrefix != null) {
            FileOptions options = FileOptions.newBuilder().setPhpClassPrefix(phpClassPrefix).build();
            schema.mergeOptions(options);
        }

        String phpMetadataNamespace = findOptionString(PHP_METADATA_NAMESPACE_OPTION, element.getOptions());
        if (phpMetadataNamespace != null) {
            FileOptions options = FileOptions.newBuilder().setPhpMetadataNamespace(phpMetadataNamespace).build();
            schema.mergeOptions(options);
        }

        String phpNamespace = findOptionString(PHP_NAMESPACE_OPTION, element.getOptions());
        if (phpNamespace != null) {
            FileOptions options = FileOptions.newBuilder().setPhpNamespace(phpNamespace).build();
            schema.mergeOptions(options);
        }

        Boolean pyGenericServices= findOptionBoolean(PY_GENERIC_SERVICES_OPTION, element.getOptions());
        if (pyGenericServices != null) {
            FileOptions options = FileOptions.newBuilder().setPyGenericServices(pyGenericServices).build();
            schema.mergeOptions(options);
        }

        String rubyPackage = findOptionString(RUBY_PACKAGE_OPTION, element.getOptions());
        if (rubyPackage != null) {
            FileOptions options = FileOptions.newBuilder().setRubyPackage(rubyPackage).build();
            schema.mergeOptions(options);
        }

        String swiftPrefix = findOptionString(SWIFT_PREFIX_OPTION, element.getOptions());
        if (swiftPrefix != null) {
            FileOptions options = FileOptions.newBuilder().setSwiftPrefix(swiftPrefix).build();
            schema.mergeOptions(options);
        }

        FileOptions.OptimizeMode optimizeFor = findOption(OPTIMIZE_FOR_OPTION, element.getOptions())
                .map(o -> FileOptions.OptimizeMode.valueOf(o.getValue().toString())).orElse(null);
        if (optimizeFor != null) {
            FileOptions options = FileOptions.newBuilder().setOptimizeFor(optimizeFor).build();
            schema.mergeOptions(options);
        }

        return schema.build();
    }

    /**
     * When schema loader links the schema, it also includes google.protobuf types in it.
     * We want to ignore all the other types except for the ones that are present in the current file.
     *
     * @return true if a type is a parent type, false otherwise.
     */
    private static boolean isParentLevelType(ProtoType protoType, Optional<String> optionalPackageName) {
        String typeName = protoType.toString();
        if (optionalPackageName.isPresent()) {
            String packageName = optionalPackageName.get();

            //If the type doesn't start with the package name, ignore it.
            if (!typeName.startsWith(packageName)) {
                return false;
            }
            //We only want to consider the parent level types. The list can contain following,
            //[io.apicurio.foo.bar.Customer.Address, io.apicurio.foo.bar.Customer, google.protobuf.Timestamp]
            //We want to only get the type "io.apicurio.foo.bar.Customer" which is parent level type.
            String[] typeNames = typeName.split(packageName)[1].split("\\.");
            boolean isNotNested = typeNames.length <= 2;
            return isNotNested;
        }

        //In case the package is not defined, we select the types that are not google types or metadata types.
        return !typeName.startsWith("google.type") && !typeName.startsWith("google.protobuf")
            && !typeName.startsWith("metadata")
            && !typeName.startsWith("additionalTypes");
    }

    private static DescriptorProto messageElementToDescriptorProto(
        MessageType messageElem, Schema schema, ProtoFile element) {
        ProtobufMessage message = new ProtobufMessage();
        message.protoBuilder().setName(messageElem.getType().getSimpleName());

        Comparator<Location> locationComparator =
                Comparator.comparing(Location::getLine).thenComparing(Location::getColumn);
        Map<Location, DescriptorProto> allNestedTypes = new TreeMap<>(locationComparator);
        List<FieldDescriptorProto> allFields = new ArrayList<>();

        for (Type type : messageElem.getNestedTypes()) {
            if (type instanceof MessageType) {
                allNestedTypes.put(type.getLocation(),
                        messageElementToDescriptorProto((MessageType) type, schema, element));
            } else if (type instanceof EnumType) {
                message.protoBuilder().addEnumType(enumElementToProto((EnumType) type));
            }
        }

        final Predicate<Field> isProto3Optional =
                field ->
                        Field.Label.OPTIONAL.equals(field.getLabel()) && Syntax.PROTO_3.equals(element.getSyntax());

        final List<OneOf> oneOfs = messageElem.getOneOfs();
        final List<OneOf> proto3OptionalOneOfs =
                messageElem.getFieldsAndOneOfFields()
                        .stream()
                        .filter(isProto3Optional)
                        .map(FileDescriptorUtils::getProto3OptionalField)
                        .collect(Collectors.toList());

        //Proto3 Optionals are considered as "synthetic-oneofs" by Protobuf compiler.
        oneOfs.addAll(proto3OptionalOneOfs);

        final Function<String, Optional<OneOf>> findOneOfByFieldName = fieldName -> {
            for (OneOf oneOf : oneOfs) {
                if (oneOf.getFields().stream().map(Field::getName).anyMatch(f -> f.equals(fieldName))) {
                    return Optional.of(oneOf);
                }
            }
            return Optional.empty();
        };

        //Add all the declared fields first skipping oneOfs.
        for (final Field field : messageElem.getDeclaredFields()) {
            final Optional<OneOf> optionalOneOf = findOneOfByFieldName.apply(field.getName());
            if (!optionalOneOf.isPresent()) {
                Field.Label fieldLabel = field.getLabel();
                //Fields are optional by default in Proto3.
                String label = fieldLabel != null ? fieldLabel.toString().toLowerCase() : OPTIONAL;

                String fieldType = determineFieldType(field.getType(), schema);
                ProtoType protoType = field.getType();
                String fieldTypeName = String.valueOf(protoType);
                ProtoType keyType = protoType.getKeyType();
                ProtoType valueType = protoType.getValueType();
                // Map fields are only permitted in messages
                if (protoType.isMap() && keyType != null && valueType != null) {
                    label = "repeated";
                    fieldType = "message";
                    String fieldMapEntryName = toMapEntry(field.getName());
                    // Map entry field name is capitalized
                    fieldMapEntryName = fieldMapEntryName.substring(0, 1).toUpperCase() + fieldMapEntryName.substring(1);
                    // Map field type name is resolved with reference to the package
                    fieldTypeName = String.format("%s.%s", messageElem.getType(), fieldMapEntryName);
                    ProtobufMessage protobufMapMessage = new ProtobufMessage();
                    DescriptorProto.Builder mapMessage = protobufMapMessage
                            .protoBuilder()
                            .setName(fieldMapEntryName)
                            .mergeOptions(DescriptorProtos.MessageOptions.newBuilder()
                                    .setMapEntry(true)
                                    .build());

                    protobufMapMessage
                            .addField(OPTIONAL, determineFieldType(keyType, schema), String.valueOf(keyType), KEY_FIELD, 1, null, null, null, null, null, null, null, null, null, null);
                    protobufMapMessage
                            .addField(OPTIONAL, determineFieldType(valueType, schema), String.valueOf(valueType), VALUE_FIELD, 2, null, null, null, null, null, null, null, null, null, null);
                    allNestedTypes.put(field.getLocation(), mapMessage.build());
                }

                String jsonName = getDefaultJsonName(field.getName()).equals(field.getDeclaredJsonName())
                        ? null : field.getDeclaredJsonName();
                Boolean isDeprecated = findOptionBoolean(DEPRECATED_OPTION, field.getOptions());
                Boolean isPacked = findOptionBoolean(PACKED_OPTION, field.getOptions());
                DescriptorProtos.FieldOptions.CType cType = findOption(CTYPE_OPTION, field.getOptions())
                        .map(o -> DescriptorProtos.FieldOptions.CType.valueOf(o.getValue().toString())).orElse(null);
                DescriptorProtos.FieldOptions.JSType jsType = findOption(JSTYPE_OPTION, field.getOptions())
                        .map(o -> DescriptorProtos.FieldOptions.JSType.valueOf(o.getValue().toString())).orElse(null);

                String metadataKey = findOptionString(ProtobufSchemaMetadata.metadataKey.getDescriptor().getFullName(),
                        field.getOptions());
                String metadataValue = findOptionString(ProtobufSchemaMetadata.metadataValue.getDescriptor().getFullName(),
                        field.getOptions());

                allFields.add(ProtobufMessage.buildFieldDescriptorProto(
                        label, fieldType, fieldTypeName, field.getName(), field.getTag(), field.getDefault(),
                        jsonName, isDeprecated, isPacked, cType, jsType, metadataKey, metadataValue, null, null));
            }
        }

        final Set<OneOf> addedOneOfs = new LinkedHashSet<>();

        //Add the oneOfs next including Proto3 Optionals.
        for (final OneOf oneOf : oneOfs) {
            if (addedOneOfs.contains(oneOf)) {
                continue;
            }

            Boolean isProto3OptionalField = null;
            if (proto3OptionalOneOfs.contains(oneOf)) {
                isProto3OptionalField = true;
            }
            OneofDescriptorProto.Builder oneofBuilder = OneofDescriptorProto.newBuilder().setName(oneOf.getName());
            message.protoBuilder().addOneofDecl(oneofBuilder);

            for (Field oneOfField : oneOf.getFields()) {
                String oneOfJsonName = getDefaultJsonName(oneOfField.getName()).equals(oneOfField.getDeclaredJsonName())
                        ? null : oneOfField.getDeclaredJsonName();
                Boolean oneOfIsDeprecated = findOptionBoolean(DEPRECATED_OPTION, oneOfField.getOptions());
                Boolean oneOfIsPacked = findOptionBoolean(PACKED_OPTION, oneOfField.getOptions());
                DescriptorProtos.FieldOptions.CType oneOfCType = findOption(CTYPE_OPTION, oneOfField.getOptions())
                        .map(o -> DescriptorProtos.FieldOptions.CType.valueOf(o.getValue().toString())).orElse(null);
                DescriptorProtos.FieldOptions.JSType oneOfJsType = findOption(JSTYPE_OPTION, oneOfField.getOptions())
                        .map(o -> DescriptorProtos.FieldOptions.JSType.valueOf(o.getValue().toString())).orElse(null);
                String metadataKey = findOptionString(ProtobufSchemaMetadata.metadataKey.getDescriptor().getFullName(),
                        oneOfField.getOptions());
                String metadataValue = findOptionString(ProtobufSchemaMetadata.metadataValue.getDescriptor().getFullName(),
                        oneOfField.getOptions());

                allFields.add(ProtobufMessage.buildFieldDescriptorProto(
                        OPTIONAL,
                        determineFieldType(oneOfField.getType(), schema),
                        String.valueOf(oneOfField.getType()),
                        oneOfField.getName(),
                        oneOfField.getTag(),
                        oneOfField.getDefault(),
                        oneOfJsonName,
                        oneOfIsDeprecated,
                        oneOfIsPacked,
                        oneOfCType,
                        oneOfJsType,
                        metadataKey,
                        metadataValue,
                        message.protoBuilder().getOneofDeclCount() - 1,
                        isProto3OptionalField));

            }
            addedOneOfs.add(oneOf);
        }

        for (ReservedElement reserved : messageElem.toElement().getReserveds()) {
            for (Object elem : reserved.getValues()) {
                if (elem instanceof String) {
                    message.protoBuilder().addReservedName((String) elem);
                } else if (elem instanceof Integer) {
                    int tag = (Integer) elem;
                    DescriptorProto.ReservedRange.Builder rangeBuilder = DescriptorProto.ReservedRange.newBuilder()
                            .setStart(tag)
                            .setEnd(tag + 1);
                    message.protoBuilder().addReservedRange(rangeBuilder.build());
                } else if (elem instanceof IntRange) {
                    IntRange range = (IntRange) elem;
                    DescriptorProto.ReservedRange.Builder rangeBuilder = DescriptorProto.ReservedRange.newBuilder()
                            .setStart(range.getStart())
                            .setEnd(range.getEndInclusive() + 1);
                    message.protoBuilder().addReservedRange(rangeBuilder.build());
                } else {
                    throw new IllegalStateException(
                            "Unsupported reserved type: " + elem.getClass().getName());
                }
            }
        }
        for (ExtensionsElement extensions : messageElem.toElement().getExtensions()) {
            for (Object elem : extensions.getValues()) {
                if (elem instanceof Integer) {
                    int tag = (Integer) elem;
                    DescriptorProto.ExtensionRange.Builder extensionBuilder = DescriptorProto.ExtensionRange.newBuilder()
                            .setStart(tag)
                            .setEnd(tag + 1);
                    message.protoBuilder().addExtensionRange(extensionBuilder.build());
                } else if (elem instanceof IntRange) {
                    IntRange range = (IntRange) elem;
                    DescriptorProto.ExtensionRange.Builder extensionBuilder = DescriptorProto.ExtensionRange.newBuilder()
                            .setStart(range.getStart())
                            .setEnd(range.getEndInclusive() + 1);
                    message.protoBuilder().addExtensionRange(extensionBuilder.build());
                } else {
                    throw new IllegalStateException(
                            "Unsupported extension type: " + elem.getClass().getName());
                }
            }
        }

        Boolean isMapEntry = findOptionBoolean(MAP_ENTRY_OPTION, messageElem.getOptions());
        if (isMapEntry != null) {
            DescriptorProtos.MessageOptions.Builder optionsBuilder = DescriptorProtos.MessageOptions.newBuilder()
                    .setMapEntry(isMapEntry);
            message.protoBuilder().mergeOptions(optionsBuilder.build());
        }
        Boolean noStandardDescriptorAccessor = findOptionBoolean(NO_STANDARD_DESCRIPTOR_OPTION, messageElem.getOptions());
        if (noStandardDescriptorAccessor != null) {
            DescriptorProtos.MessageOptions.Builder optionsBuilder = DescriptorProtos.MessageOptions.newBuilder()
                    .setNoStandardDescriptorAccessor(noStandardDescriptorAccessor);
            message.protoBuilder().mergeOptions(optionsBuilder.build());
        }

        message.protoBuilder().addAllNestedType(allNestedTypes.values());
        message.protoBuilder().addAllField(allFields);
        return message.build();
    }

    private static String determineFieldType(ProtoType protoType, Schema schema) {
        Type typeReference = schema.getType(protoType);
        if (typeReference != null) {
            if (typeReference instanceof MessageType) {
                return "message";
            }
            if (typeReference instanceof EnumType) {
                return "enum";
            }
        }
        return null;
    }

    /**
     * Proto3 optional fields are "synthetic one-ofs" and are written as one-of fields over the wire.
     * This method generates the synthetic one-of from a Proto3 optional field.
     */
    private static OneOf getProto3OptionalField(Field field) {
        return new OneOf("_" + field.getName(), "", Collections.singletonList(field), DEFAULT_LOCATION, new Options(Options.FIELD_OPTIONS, Collections.emptyList()));
    }

    private static EnumDescriptorProto enumElementToProto(EnumType enumElem) {
        Boolean allowAlias = findOptionBoolean(ALLOW_ALIAS_OPTION, enumElem.getOptions());

        EnumDescriptorProto.Builder builder = EnumDescriptorProto.newBuilder()
                .setName(enumElem.getName());
        if (allowAlias != null) {
            DescriptorProtos.EnumOptions.Builder optionsBuilder = DescriptorProtos.EnumOptions.newBuilder()
                    .setAllowAlias(allowAlias);
            builder.mergeOptions(optionsBuilder.build());
        }
        for (EnumConstant constant : enumElem.getConstants()) {
            builder.addValue(EnumValueDescriptorProto.newBuilder()
                    .setName(constant.getName())
                    .setNumber(constant.getTag())
                    .build());
        }
        return builder.build();
    }

    private static DescriptorProtos.ServiceDescriptorProto serviceElementToProto(Service serviceElem) {
        ServiceDescriptorProto.Builder builder = ServiceDescriptorProto.newBuilder().setName(serviceElem.name());

        for (Rpc rpc : serviceElem.rpcs()) {
            MethodDescriptorProto.Builder methodBuilder = MethodDescriptorProto
                    .newBuilder()
                    .setName(rpc.getName())
                    .setInputType(getTypeName(rpc.getRequestType().toString()))
                    .setOutputType(getTypeName(rpc.getResponseType().toString()));
            if (rpc.getRequestStreaming()) {
                methodBuilder.setClientStreaming(rpc.getRequestStreaming());
            }
            if (rpc.getResponseStreaming()) {
                methodBuilder.setServerStreaming(rpc.getResponseStreaming());
            }
            Boolean deprecated = findOptionBoolean(DEPRECATED_OPTION, rpc.getOptions());
            if (deprecated != null) {
                MethodOptions.Builder optionsBuilder = MethodOptions.newBuilder()
                        .setDeprecated(deprecated);
                methodBuilder.mergeOptions(optionsBuilder.build());
            }
            MethodOptions.IdempotencyLevel idempotencyLevel = findOption(IDEMPOTENCY_LEVEL_OPTION, rpc.getOptions())
                    .map(o -> MethodOptions.IdempotencyLevel.valueOf(o.getValue().toString()))
                    .orElse(null);
            if (idempotencyLevel != null) {
                MethodOptions.Builder optionsBuilder = MethodOptions.newBuilder()
                        .setIdempotencyLevel(idempotencyLevel);
                methodBuilder.mergeOptions(optionsBuilder.build());
            }

            builder.addMethod(methodBuilder.build());
        }

        Boolean deprecated = findOptionBoolean(DEPRECATED_OPTION, serviceElem.options());
        if (deprecated != null) {
