package io.anemos.metastore.core.proto.validate;

import com.google.protobuf.ByteString;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.Descriptors;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.Message;
import com.google.protobuf.UnknownFieldSet;
import io.anemos.metastore.putils.ProtoDomain;
import io.anemos.metastore.v1alpha1.ChangeInfo;
import io.anemos.metastore.v1alpha1.ChangeType;
import io.anemos.metastore.v1alpha1.EnumValueChangeInfo;
import io.anemos.metastore.v1alpha1.FieldChangeInfo;
import io.anemos.metastore.v1alpha1.ImportChangeInfo;
import io.anemos.metastore.v1alpha1.MethodChangeInfo;
import io.anemos.metastore.v1alpha1.OptionChangeInfo;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

/**
 * File in package -> can be removed/added/keep the same Message -> can move from
 * file/added/removed/keep the same Message content (fields) -> can keep the same/added/removed
 */
public class ProtoDiff {

  private ProtoDomain proto_ref;
  private ProtoDomain proto_new;
  private ValidationResults results;

  public ProtoDiff(ProtoDomain fd_ref, ProtoDomain fd_new, ValidationResults results) {
    this.proto_ref = fd_ref;
    this.proto_new = fd_new;
    this.results = results;
  }

  private static Map<String, Descriptors.FileDescriptor> toMap4FileDescriptor(
      Collection<Descriptors.FileDescriptor> in) {
    Map<String, Descriptors.FileDescriptor> out = new HashMap<>();
    in.forEach(
        descriptor -> {
          out.put(descriptor.getName(), descriptor);
        });
    return out;
  }

  private static Map<String, Descriptors.FieldDescriptor> toMap4FieldDescriptor(
      Collection<Descriptors.FieldDescriptor> in) {
    Map<String, Descriptors.FieldDescriptor> out = new HashMap<>();
    in.forEach(
        descriptor -> {
          out.put(String.valueOf(descriptor.getNumber()), descriptor);
        });
    return out;
  }

  private static Map<String, Descriptors.EnumValueDescriptor> toMap4EnumValueDescriptor(
      Collection<Descriptors.EnumValueDescriptor> in) {
    Map<String, Descriptors.EnumValueDescriptor> out = new HashMap<>();
    in.forEach(
        descriptor -> {
          out.put(String.valueOf(descriptor.getNumber()), descriptor);
        });
    return out;
  }

  private static Map<String, Descriptors.MethodDescriptor> toMap4MethodDescriptor(
      Collection<Descriptors.MethodDescriptor> in) {
    Map<String, Descriptors.MethodDescriptor> out = new HashMap<>();
    in.forEach(
        descriptor -> {
          out.put(String.valueOf(descriptor.getName()), descriptor);
        });
    return out;
  }

  public void diffOnFileName(String fileName) {
    Descriptors.FileDescriptor fdRef = proto_ref.getFileDescriptorByFileName(fileName);
    Descriptors.FileDescriptor fdNew = proto_new.getFileDescriptorByFileName(fileName);

    if (fdRef != null && fdNew != null) {
      diffFileDescriptor(fdRef, fdNew);
      diffOptionsFromFile(fdRef, fdNew);
    }
  }

  private void diffFileDescriptor(
      @Nullable Descriptors.FileDescriptor fdRef, @Nullable Descriptors.FileDescriptor fdNew) {
    List<Descriptors.Descriptor> refDescriptors;
    List<Descriptors.EnumDescriptor> refEnumDescriptors;
    List<Descriptors.ServiceDescriptor> refServiceDescriptors;
    List<String> refDependencies;
    List<Descriptors.Descriptor> newDescriptors;
    List<Descriptors.EnumDescriptor> newEnumDescriptors;
    List<Descriptors.ServiceDescriptor> newServiceDescriptors;
    List<String> newDependencies;
    String fileName = null;
    if (fdRef != null) {
      fileName = fdRef.getFullName();
      refDescriptors = fdRef.getMessageTypes();
      refEnumDescriptors = fdRef.getEnumTypes();
      refServiceDescriptors = fdRef.getServices();
      refDependencies =
          fdRef.getDependencies().stream()
              .map(Descriptors.FileDescriptor::getFullName)
              .collect(Collectors.toList());
    } else {
      results.setPatch(fdNew, ChangeInfo.newBuilder().setChangeType(ChangeType.ADDITION).build());
      refDescriptors = new ArrayList<>(0);
      refEnumDescriptors = new ArrayList<>(0);
      refServiceDescriptors = new ArrayList<>(0);
      refDependencies = new ArrayList<>(0);
    }

    if (fdNew != null) {
      fileName = fdNew.getFullName();
      newDescriptors = fdNew.getMessageTypes();
      newEnumDescriptors = fdNew.getEnumTypes();
      newServiceDescriptors = fdNew.getServices();
      newDependencies =
          fdNew.getDependencies().stream()
              .map(Descriptors.FileDescriptor::getFullName)
              .collect(Collectors.toList());
    } else {
      results.setPatch(fdRef, ChangeInfo.newBuilder().setChangeType(ChangeType.REMOVAL).build());
      newDescriptors = new ArrayList<>(0);
      newEnumDescriptors = new ArrayList<>(0);
      newServiceDescriptors = new ArrayList<>(0);
      newDependencies = new ArrayList<>(0);
    }

    diffMessageTypes(refDescriptors, newDescriptors);
    diffEnumTypes(refEnumDescriptors, newEnumDescriptors);
    diffServices(refServiceDescriptors, newServiceDescriptors);
    diffImports(fileName, refDependencies, newDependencies);
  }

  public void diffOnMessage(String messageName) {
    Descriptors.Descriptor refDescriptor = proto_ref.getDescriptorByName(messageName);
    Descriptors.Descriptor newDescriptor = proto_new.getDescriptorByName(messageName);

    diffMessageType(refDescriptor, newDescriptor);
  }

  public void diffOnPackagePrefix(String packagePrefix) {
    List<Descriptors.FileDescriptor> fdRef =
        proto_ref.getFileDescriptorsByPackagePrefix(packagePrefix);
    List<Descriptors.FileDescriptor> fdNew =
        proto_new.getFileDescriptorsByPackagePrefix(packagePrefix);
    diffFiles(fdRef, fdNew);
  }

  public void diffOnPackage(String packageName) {
    List<Descriptors.FileDescriptor> fdRef = proto_ref.getFileDescriptorsByPackageName(packageName);
    List<Descriptors.FileDescriptor> fdNew = proto_new.getFileDescriptorsByPackageName(packageName);
    diffFiles(fdRef, fdNew);
  }

  private <T extends Descriptors.GenericDescriptor> Map<String, T> toMap4Descriptor(List<T> in) {
    Map<String, T> out = new HashMap<>();
    in.forEach(
        descriptor -> {
          out.put(descriptor.getName(), descriptor);
        });
    return out;
  }

  private <T> Set<T> onlyInLeft(Map<T, ?> left, Map<T, ?> right) {
    HashSet<T> integers = new HashSet<T>(left.keySet());
    integers.removeAll(right.keySet());
    return integers;
  }

  private <T> Set<T> onlyInCommon(Map<T, ?> left, Map<T, ?> right) {
    Set<T> intersect = new HashSet<>(left.keySet());
    intersect.addAll(right.keySet());
    intersect.removeAll(onlyInLeft(left, right));
    intersect.removeAll(onlyInLeft(right, left));
    return intersect;
  }

  private void diffImports(String fullFileName, List<String> c_ref, List<String> c_new) {
    Map<String, String> m_ref =
        c_ref.stream().collect(Collectors.toMap(String::toString, String::toString));
    Map<String, String> m_new =
        c_new.stream().collect(Collectors.toMap(String::toString, String::toString));

    onlyInLeft(m_ref, m_new)
        .forEach(
            v -> {
              results.addImportChange(
                  fullFileName,
                  ImportChangeInfo.newBuilder()
                      .setChangeType(ChangeType.REMOVAL)
                      .setName(v)
                      .build());
            });

    onlyInLeft(m_new, m_ref)
        .forEach(
            v ->
                results.addImportChange(
                    fullFileName,
                    ImportChangeInfo.newBuilder()
                        .setChangeType(ChangeType.ADDITION)
                        .setName(v)
                        .build()));
  }

  private <T extends Descriptors.GenericDescriptor> void diffGenericDescriptor(
      List<T> mt_ref,
      List<T> mt_new,
      Consumer<T> removal,
      Consumer<T> addition,
      BiConsumer<T, T> diff) {
    Map<String, T> m_ref = toMap4Descriptor(mt_ref);
    Map<String, T> m_new = toMap4Descriptor(mt_new);

    Set<String> onlyRef = onlyInLeft(m_ref, m_new);
    onlyRef.forEach(k -> removal.accept(m_ref.get(k)));

    Set<String> onlyNew = onlyInLeft(m_new, m_ref);
    onlyNew.forEach(k -> addition.accept(m_new.get(k)));

    Set<String> common = onlyInCommon(m_new, m_ref);
    common.forEach(k -> diff.accept(m_ref.get(k), m_new.get(k)));
  }

  private void diffMessageTypes(
      List<Descriptors.Descriptor> mt_ref, List<Descriptors.Descriptor> mt_new) {
    diffGenericDescriptor(
        mt_ref,
        mt_new,
        d ->
            results.setPatch(
                d,
                ChangeInfo.newBuilder()
                    .setChangeType(ChangeType.REMOVAL)
                    .setFromName(d.getFullName())
                    .build()),
        d ->
            results.setPatch(
                d,
                ChangeInfo.newBuilder()
                    .setChangeType(ChangeType.ADDITION)
                    .setToName(d.getFullName())
                    .build()),
        this::diffMessageType);
  }

  private void diffFiles(
      List<Descriptors.FileDescriptor> f_ref, List<Descriptors.FileDescriptor> f_new) {
    Map<String, Descriptors.FileDescriptor> m_ref = toMap4FileDescriptor(f_ref);
    Map<String, Descriptors.FileDescriptor> m_new = toMap4FileDescriptor(f_new);

    Set<String> onlyRef = onlyInLeft(m_ref, m_new);
    onlyRef.forEach(
        k -> {
          Descriptors.FileDescriptor fd = m_ref.get(k);
          results.setPatch(
              fd,
              ChangeInfo.newBuilder()
                  .setChangeType(ChangeType.REMOVAL)
                  .setFromName(fd.getName())
                  .build());
          diffFileDescriptor(fd, null);
        });

    Set<String> onlyNew = onlyInLeft(m_new, m_ref);
    onlyNew.forEach(
        k -> {
          Descriptors.FileDescriptor fd = m_new.get(k);
          results.setPatch(
              fd,
              ChangeInfo.newBuilder()
                  .setChangeType(ChangeType.ADDITION)
                  .setToName(fd.getName())
                  .build());
          diffFileDescriptor(null, fd);
        });

    Set<String> common = onlyInCommon(m_new, m_ref);
    common.forEach(k -> diffFileDescriptor(m_ref.get(k), m_new.get(k)));
  }

  private void diffMessageType(
      Descriptors.Descriptor descriptorRef, Descriptors.Descriptor descriptorNew) {
    DescriptorProtos.MessageOptions optionsRef = descriptorRef.getOptions();
    DescriptorProtos.MessageOptions optionsNew = descriptorNew.getOptions();
    diffExtensionOptions(
        OptionChangeInfo.OptionType.MESSAGE_OPTION,
        descriptorRef,
        optionsRef.getAllFields(),
        descriptorNew,
        optionsNew.getAllFields());
    diffUnknownOptions(
        OptionChangeInfo.OptionType.MESSAGE_OPTION,
        descriptorRef,
        optionsRef.getUnknownFields(),
        descriptorNew,
        optionsNew.getUnknownFields());
    diffFields(descriptorRef, descriptorNew);
  }

  private void diffFields(Descriptors.Descriptor d_ref, Descriptors.Descriptor d_new) {
    Map<String, Descriptors.FieldDescriptor> m_ref = toMap4FieldDescriptor(d_ref.getFields());
    Map<String, Descriptors.FieldDescriptor> m_new = toMap4FieldDescriptor(d_new.getFields());

    Set<String> onlyRef = onlyInLeft(m_ref, m_new);
    onlyRef.forEach(
        k -> {
          Descriptors.FieldDescriptor fd = m_ref.get(k);
          FieldChangeInfo.Builder builder =
              FieldChangeInfo.newBuilder()
                  .setChangeType(ChangeType.REMOVAL)
                  .setFromName(fd.getName())
                  .setFromTypeValue(fd.getType().toProto().getNumber())
                  .setFromDeprecated(isDeprecated(fd));
          if (d_new.isReservedNumber(fd.getNumber())) {
            builder.setChangeType(ChangeType.RESERVED);
            if (d_new.isReservedName(fd.getName())) {
              builder.setToName(fd.getName());
            }
          }
          results.setPatch(fd, builder.build());
        });

    Set<String> onlyNew = onlyInLeft(m_new, m_ref);
    onlyNew.forEach(
        k -> {
          Descriptors.FieldDescriptor fd = m_new.get(k);
          FieldChangeInfo.Builder builder =
              FieldChangeInfo.newBuilder()
                  .setChangeType(ChangeType.ADDITION)
                  .setToName(fd.getName())
                  .setToTypeValue(fd.getType().toProto().getNumber())
                  .setToDeprecated(isDeprecated(fd));
          if (d_ref.isReservedNumber(fd.getNumber())) {
            builder.setChangeType(ChangeType.UNRESERVED);
            if (d_ref.isReservedName(fd.getName())) {
              builder.setFromName(fd.getName());
            }
          }
          results.setPatch(fd, builder.build());
        });

    Set<String> common = onlyInCommon(m_new, m_ref);
    common.forEach(
        k -> {
          FieldChangeInfo fieldDiff = diffField(m_ref.get(k), m_new.get(k));
          if (fieldDiff != null) {
            results.setPatch(m_new.get(k), fieldDiff);
          }
        });
  }

  private FieldChangeInfo diffField(
      Descriptors.FieldDescriptor f_ref, Descriptors.FieldDescriptor f_new) {
    diffOptionsFromField(f_ref, f_new);
    FieldChangeInfo.Builder builder = FieldChangeInfo.newBuilder();

    if (!f_ref.getName().equals(f_new.getName())) {
      builder.setChangeType(ChangeType.CHANGED);
      builder.setFromName(f_ref.getName());
      builder.setToName(f_new.getName());
    }

    if (!f_ref.getType().equals(f_new.getType())) {
      builder.setChangeType(ChangeType.CHANGED);
      builder.setFromTypeValue(f_ref.getType().toProto().getNumber());
      builder.setToTypeValue(f_new.getType().toProto().getNumber());
      if (f_ref.getType().equals(Descriptors.FieldDescriptor.Type.MESSAGE)) {
        builder.setFromTypeName(f_ref.getMessageType().getFullName());
      }
      if (f_new.getType().equals(Descriptors.FieldDescriptor.Type.MESSAGE)) {
        builder.setFromTypeName(f_new.getMessageType().getFullName());
      }
    } else if (f_ref.getType().equals(Descriptors.FieldDescriptor.Type.MESSAGE)
        && !f_ref.getMessageType().getFullName().equals(f_new.getMessageType().getFullName())) {
      builder.setChangeType(ChangeType.CHANGED);
      builder.setFromTypeName(f_ref.getMessageType().getFullName());
      builder.setToTypeName(f_new.getMessageType().getFullName());
    }

    if (isDeprecated(f_ref) != isDeprecated(f_new)) {
      builder.setChangeType(ChangeType.CHANGED);
      builder.setFromDeprecated(isDeprecated(f_ref));
      builder.setToDeprecated(isDeprecated(f_new));
    }
    if (builder.getChangeType().equals(ChangeType.CHANGED)) {
      return builder.build();
    }
    return null;
  }

  private void diffServices(
      List<Descriptors.ServiceDescriptor> s_ref, List<Descriptors.ServiceDescriptor> s_new) {
    diffGenericDescriptor(
        s_ref,
        s_new,
        d ->
            results.setPatch(
                d,
                ChangeInfo.newBuilder()
                    .setChangeType(ChangeType.REMOVAL)
                    .setFromName(d.getFullName())
                    .build()),
        d ->
            results.setPatch(
                d,
                ChangeInfo.newBuilder()
                    .setChangeType(ChangeType.ADDITION)
                    .setToName(d.getFullName())
                    .build()),
        this::diffServiceDescriptor);
  }

  private void diffServiceDescriptor(
      Descriptors.ServiceDescriptor descriptorRef, Descriptors.ServiceDescriptor descriptorNew) {
    DescriptorProtos.ServiceOptions optionsRef = descriptorRef.getOptions();
    DescriptorProtos.ServiceOptions optionsNew = descriptorNew.getOptions();
    diffExtensionOptions(
        OptionChangeInfo.OptionType.SERVICE_OPTION,
        descriptorRef,
        optionsRef.getAllFields(),
        descriptorNew,
        optionsNew.getAllFields());
    diffUnknownOptions(
        OptionChangeInfo.OptionType.SERVICE_OPTION,
        descriptorRef,
        optionsRef.getUnknownFields(),
        descriptorNew,
        optionsNew.getUnknownFields());
    diffMethods(descriptorRef, descriptorNew);
  }

  private void diffMethods(
      Descriptors.ServiceDescriptor d_ref, Descriptors.ServiceDescriptor d_new) {
    Map<String, Descriptors.MethodDescriptor> m_ref = toMap4MethodDescriptor(d_ref.getMethods());
    Map<String, Descriptors.MethodDescriptor> m_new = toMap4MethodDescriptor(d_new.getMethods());

    Set<String> onlyRef = onlyInLeft(m_ref, m_new);
    onlyRef.forEach(
        k -> {
          Descriptors.MethodDescriptor fd = m_ref.get(k);

          MethodChangeInfo.Builder builder =
              MethodChangeInfo.newBuilder()
                  .setChangeType(ChangeType.REMOVAL)
                  .setFromName(fd.getName())
                  .setFromDeprecated(isDeprecated(fd));
          //              if (d_new.isReservedNumber(fd.getNumber())) {
          //
          // builder.setChangeType(EnumValueChangeInfo.ValueChangeType.VALUE_RESERVED);
          //                if (d_new.isReservedName(fd.getName())) {
          //                  builder.setToName(fd.getName());
          //                }
          //              }
          results.setPatch(fd, builder.build());
        });

    Set<String> onlyNew = onlyInLeft(m_new, m_ref);
    onlyNew.forEach(
        k -> {
          Descriptors.MethodDescriptor fd = m_new.get(k);
          MethodChangeInfo.Builder builder =
              MethodChangeInfo.newBuilder()
                  .setChangeType(ChangeType.ADDITION)
                  .setToName(fd.getName())
                  .setToDeprecated(isDeprecated(fd));
          //              if (d_ref.isReservedNumber(fd.getNumber())) {
          //
          // builder.setChangeType(EnumValueChangeInfo.ValueChangeType.VALUE_UNRESERVED);
          //                if (d_ref.isReservedName(fd.getName())) {
          //                  builder.setFromName(fd.getName());
          //                }
          //              }
          results.setPatch(fd, builder.build());
        });

    Set<String> common = onlyInCommon(m_new, m_ref);
    common.forEach(
        k -> {
          MethodChangeInfo fieldDiff = diffMethod(m_ref.get(k), m_new.get(k));
          if (fieldDiff != null) {
            results.setPatch(m_new.get(k), fieldDiff);
          }
        });
  }

  private MethodChangeInfo diffMethod(
      Descriptors.MethodDescriptor f_ref, Descriptors.MethodDescriptor f_new) {
    diffOptionsFromMethod(f_ref, f_new);
    MethodChangeInfo.Builder builder = MethodChangeInfo.newBuilder();

    if (!f_ref.getName().equals(f_new.getName())) {
      builder.setChangeType(ChangeType.CHANGED);
      builder.setFromName(f_ref.getName());
      builder.setToName(f_new.getName());
    }
    if (isDeprecated(f_ref) != isDeprecated(f_new)) {
      builder.setChangeType(ChangeType.CHANGED);
      builder.setFromDeprecated(isDeprecated(f_ref));
      builder.setToDeprecated(isDeprecated(f_new));
    }

    if (builder.getChangeType().equals(ChangeType.CHANGED)) {
      return builder.build();
    }
    return null;
  }

  private void diffEnumTypes(
      List<Descriptors.EnumDescriptor> e_ref, List<Descriptors.EnumDescriptor> e_new) {
    diffGenericDescriptor(
        e_ref,
        e_new,
        d ->
            results.setPatch(
                d,
                ChangeInfo.newBuilder()
                    .setChangeType(ChangeType.REMOVAL)
                    .setFromName(d.getFullName())
                    .build()),
        d ->
            results.setPatch(
                d,
                ChangeInfo.newBuilder()
                    .setChangeType(ChangeType.ADDITION)
                    .setToName(d.getFullName())
                    .build()),
        this::diffEnumDescriptor);
  }

  private void diffEnumDescriptor(
      Descriptors.EnumDescriptor descriptorRef, Descriptors.EnumDescriptor descriptorNew) {
    DescriptorProtos.EnumOptions optionsRef = descriptorRef.getOptions();
    DescriptorProtos.EnumOptions optionsNew = descriptorNew.getOptions();
    diffExtensionOptions(
        OptionChangeInfo.OptionType.ENUM_OPTION,
        descriptorRef,
        optionsRef.getAllFields(),
        descriptorNew,
        optionsNew.getAllFields());
    diffUnknownOptions(
        OptionChangeInfo.OptionType.ENUM_OPTION,
        descriptorRef,
        optionsRef.getUnknownFields(),
        descriptorNew,
        optionsNew.getUnknownFields());
    diffEnumValues(descriptorRef, descriptorNew);
  }

  private void diffEnumValues(Descriptors.EnumDescriptor d_ref, Descriptors.EnumDescriptor d_new) {
    Map<String, Descriptors.EnumValueDescriptor> m_ref =
        toMap4EnumValueDescriptor(d_ref.getValues());
    Map<String, Descriptors.EnumValueDescriptor> m_new =
        toMap4EnumValueDescriptor(d_new.getValues());

    Set<String> onlyRef = onlyInLeft(m_ref, m_new);
    onlyRef.forEach(
        k -> {
          Descriptors.EnumValueDescriptor fd = m_ref.get(k);

          EnumValueChangeInfo.Builder builder =
              EnumValueChangeInfo.newBuilder()
                  .setChangeType(ChangeType.REMOVAL)
                  .setFromName(fd.getName())
                  .setFromDeprecated(isDeprecated(fd));
          //              if (d_new.isReservedNumber(fd.getNumber())) {
          //
          // builder.setChangeType(EnumValueChangeInfo.ValueChangeType.VALUE_RESERVED);
          //                if (d_new.isReservedName(fd.getName())) {
          //                  builder.setToName(fd.getName());
          //                }
          //              }
          results.setPatch(fd, builder.build());
        });

    Set<String> onlyNew = onlyInLeft(m_new, m_ref);
    onlyNew.forEach(
        k -> {
          Descriptors.EnumValueDescriptor fd = m_new.get(k);
          EnumValueChangeInfo.Builder builder =
              EnumValueChangeInfo.newBuilder()
                  .setChangeType(ChangeType.ADDITION)
                  .setToName(fd.getName())
                  .setToDeprecated(isDeprecated(fd));
          //              if (d_ref.isReservedNumber(fd.getNumber())) {
          //
          // builder.setChangeType(EnumValueChangeInfo.ValueChangeType.VALUE_UNRESERVED);
          //                if (d_ref.isReservedName(fd.getName())) {
          //                  builder.setFromName(fd.getName());
          //                }
          //              }
          results.setPatch(fd, builder.build());
        });

    Set<String> common = onlyInCommon(m_new, m_ref);
    common.forEach(
        k -> {
          EnumValueChangeInfo fieldDiff = diffEnumValue(m_ref.get(k), m_new.get(k));
          if (fieldDiff != null) {
            results.setPatch(m_new.get(k), fieldDiff);
          }
        });
  }

  private EnumValueChangeInfo diffEnumValue(
      Descriptors.EnumValueDescriptor f_ref, Descriptors.EnumValueDescriptor f_new) {
    diffOptionsFromEnumValue(f_ref, f_new);
    EnumValueChangeInfo.Builder builder = EnumValueChangeInfo.newBuilder();

    if (!f_ref.getName().equals(f_new.getName())) {
      builder.setChangeType(ChangeType.CHANGED);
      builder.setFromName(f_ref.getName());
      builder.setToName(f_new.getName());
    }
    if (isDeprecated(f_ref) != isDeprecated(f_new)) {
      builder.setChangeType(ChangeType.CHANGED);
      builder.setFromDeprecated(isDeprecated(f_ref));
      builder.setToDeprecated(isDeprecated(f_new));
    }

    if (builder.getChangeType().equals(ChangeType.CHANGED)) {
      return builder.build();
    }
    return null;
  }

  private boolean isDeprecated(Descriptors.FieldDescriptor fieldDescriptor) {
    Map<Descriptors.FieldDescriptor, Object> allFields =
        fieldDescriptor.getOptions().getAllFields();
    if (allFields.size() > 0) {
      for (Map.Entry<Descriptors.FieldDescriptor, Object> entry : allFields.entrySet()) {
        Descriptors.FieldDescriptor f = entry.getKey();
        switch (f.getFullName()) {
          case "google.protobuf.FieldOptions.deprecated":
            return true;
        }
      }
    }
    return false;
  }

  private boolean isDeprecated(Descriptors.EnumValueDescriptor fieldDescriptor) {
    Map<Descriptors.FieldDescriptor, Object> allFields =
        fieldDescriptor.getOptions().getAllFields();
    if (allFields.size() > 0) {
      for (Map.Entry<Descriptors.FieldDescriptor, Object> entry : allFields.entrySet()) {
        Descriptors.FieldDescriptor f = entry.getKey();
        switch (f.getFullName()) {
          case "google.protobuf.EnumValueOptions.deprecated":
            return true;
        }
      }
    }
    return false;
  }

  private boolean isDeprecated(Descriptors.MethodDescriptor fieldDescriptor) {
    Map<Descriptors.FieldDescriptor, Object> allFields =
        fieldDescriptor.getOptions().getAllFields();
    if (allFields.size() > 0) {
      for (Map.Entry<Descriptors.FieldDescriptor, Object> entry : allFields.entrySet()) {
        Descriptors.FieldDescriptor f = entry.getKey();
        switch (f.getFullName()) {
          case "google.protobuf.MethodOptions.deprecated":
            return true;
        }
      }
    }
    return false;
  }

  private void diffUnknownOptions(
      OptionChangeInfo.OptionType changeType,
      Descriptors.GenericDescriptor descriptorRef,
      UnknownFieldSet unknownFieldSetRef,
      Descriptors.GenericDescriptor descriptorNew,
      UnknownFieldSet unknownFieldSetNew) {

    Map<Integer, UnknownFieldSet.Field> fieldsRef = unknownFieldSetRef.asMap();
    Map<Integer, UnknownFieldSet.Field> fieldsNew = unknownFieldSetNew.asMap();

    Set<Integer> onlyInLeft = onlyInLeft(fieldsRef, fieldsNew);
    onlyInLeft.forEach(
        optionNumber -> {
          UnknownFieldSet.Field field = fieldsRef.get(optionNumber);
          ByteString payload = serializeUnknownField(optionNumber, field);
          OptionChangeInfo.Builder builder =
              OptionChangeInfo.newBuilder()
                  .setChangeType(ChangeType.REMOVAL)
                  .setType(changeType)
                  .setOptionNumber(optionNumber)
                  .setPayloadNew(payload);
          results.addOptionChange(descriptorRef, builder.build());
        });

    Set<Integer> onlyNew = onlyInLeft(fieldsNew, fieldsRef);
    onlyNew.forEach(
        optionNumber -> {
          UnknownFieldSet.Field field = fieldsNew.get(optionNumber);
          ByteString payload = serializeUnknownField(optionNumber, field);
          OptionChangeInfo.Builder builder =
              OptionChangeInfo.newBuilder()
                  .setChangeType(ChangeType.ADDITION)
                  .setType(changeType)
                  .setOptionNumber(optionNumber)
                  .setPayloadNew(payload);
          results.addOptionChange(descriptorNew, builder.build());
        });

    Set<Integer> common = onlyInCommon(fieldsRef, fieldsNew);
    common.forEach(
        optionNumber -> {
          UnknownFieldSet.Field fieldOld = fieldsRef.get(optionNumber);
          UnknownFieldSet.Field fieldNew = fieldsNew.get(optionNumber);

          ByteString payloadOld = serializeUnknownField(optionNumber, fieldOld);
          ByteString payloadNew = serializeUnknownField(optionNumber, fieldNew);
          if (!payloadOld.equals(payloadNew)) {
            OptionChangeInfo.Builder builder =
                OptionChangeInfo.newBuilder()
                    .setChangeType(ChangeType.PAYLOAD_CHANGED)
                    .setType(changeType)
                    .setPayloadOld(payloadOld)
                    .setPayloadNew(payloadNew)
                    .setOptionNumber(optionNumber);
            results.addOptionChange(descriptorNew, builder.build());
          }
        });
  }

  private void diffExtensionOptions(
      OptionChangeInfo.OptionType changeType,
      Descriptors.GenericDescriptor descriptorRef,
      Map<Descriptors.FieldDescriptor, Object> optionFieldMapRef,
      Descriptors.GenericDescriptor descriptorNew,
      Map<Descriptors.FieldDescriptor, Object> optionFieldMapNew) {

    Map<Integer, Message> fieldsRef =
        optionFieldMapRef.entrySet().stream()
            .collect(
                Collectors.toMap(
                    k -> k.getKey().getNumber(),
                    v ->
                        DynamicMessage.newBuilder(v.getKey().getContainingType())
                            .setField(v.getKey(), v.getValue())
                            .build()));
    Map<Integer, Message> fieldsNew =
        optionFieldMapNew.entrySet().stream()
            .collect(
                Collectors.toMap(
                    k -> k.getKey().getNumber(),
                    v ->
                        DynamicMessage.newBuilder(v.getKey().getContainingType())
                            .setField(v.getKey(), v.getValue())
                            .build()));

    Set<Integer> onlyInLeft = onlyInLeft(fieldsRef, fieldsNew);
    onlyInLeft.forEach(
        optionNumber -> {
          Message field = fieldsRef.get(optionNumber);
          ByteString payload = serializePayload(field);
          OptionChangeInfo.Builder builder =
              OptionChangeInfo.newBuilder()
                  .setChangeType(ChangeType.REMOVAL)
                  .setType(changeType)
                  .setOptionNumber(optionNumber)
                  .setPayloadNew(payload);
          results.addOptionChange(descriptorRef, builder.build());
        });

    Set<Integer> onlyNew = onlyInLeft(fieldsNew, fieldsRef);
    onlyNew.forEach(
        optionNumber -> {
          Message field = fieldsNew.get(optionNumber);
          ByteString payload = serializePayload(field);
          OptionChangeInfo.Builder builder =
              OptionChangeInfo.newBuilder()
                  .setChangeType(ChangeType.ADDITION)
                  .setType(changeType)
                  .setOptionNumber(optionNumber)
                  .setPayloadNew(payload);
          results.addOptionChange(descriptorNew, builder.build());
        });

    Set<Integer> common = onlyInCommon(fieldsRef, fieldsNew);
    common.forEach(
        optionNumber -> {
          Message fieldRef = fieldsRef.get(optionNumber);
          Message fieldNew = fieldsNew.get(optionNumber);
          ByteString payloadRef = serializePayload(fieldRef);
          ByteString payloadNew = serializePayload(fieldNew);
          if (!payloadRef.equals(payloadNew)) {
            OptionChangeInfo.Builder builder =
                OptionChangeInfo.newBuilder()
                    .setChangeType(ChangeType.PAYLOAD_CHANGED)
                    .setType(changeType)
                    .setOptionNumber(optionNumber)
                    .setPayloadOld(payloadRef)
                    .setPayloadNew(payloadNew);
            results.addOptionChange(descriptorNew, builder.build());
          }
        });
  }

  private void diffOptionsFromFile(
      Descriptors.FileDescriptor descriptorRef, Descriptors.FileDescriptor descriptorNew) {
    DescriptorProtos.FileOptions optionsRef = descriptorRef.getOptions();
    DescriptorProtos.FileOptions optionsNew = descriptorNew.getOptions();
    diffExtensionOptions(
        OptionChangeInfo.OptionType.FILE_OPTION,
        descriptorRef,
        optionsRef.getAllFields(),
        descriptorNew,
        optionsNew.getAllFields());
    diffUnknownOptions(
        OptionChangeInfo.OptionType.FILE_OPTION,
        descriptorRef,
        optionsRef.getUnknownFields(),
        descriptorNew,
        optionsNew.getUnknownFields());
  }

  private void diffOptionsFromField(
      Descriptors.FieldDescriptor descriptorRef, Descriptors.FieldDescriptor descriptorNew) {
    DescriptorProtos.FieldOptions optionsRef = descriptorRef.getOptions();
    DescriptorProtos.FieldOptions optionsNew = descriptorNew.getOptions();
    diffExtensionOptions(
        OptionChangeInfo.OptionType.FIELD_OPTION,
        descriptorRef,
        optionsRef.getAllFields(),
        descriptorNew,
        optionsNew.getAllFields());
    diffUnknownOptions(
        OptionChangeInfo.OptionType.FIELD_OPTION,
        descriptorRef,
        optionsRef.getUnknownFields(),
        descriptorNew,
        optionsNew.getUnknownFields());
  }

  private void diffOptionsFromEnumValue(
      Descriptors.EnumValueDescriptor descriptorRef,
      Descriptors.EnumValueDescriptor descriptorNew) {
    DescriptorProtos.EnumValueOptions optionsRef = descriptorRef.getOptions();
    DescriptorProtos.EnumValueOptions optionsNew = descriptorNew.getOptions();
    diffExtensionOptions(
        OptionChangeInfo.OptionType.ENUM_VALUE_OPTION,
        descriptorRef,
        optionsRef.getAllFields(),
        descriptorNew,
        optionsNew.getAllFields());
    diffUnknownOptions(
        OptionChangeInfo.OptionType.ENUM_VALUE_OPTION,
        descriptorRef,
        optionsRef.getUnknownFields(),
        descriptorNew,
        optionsNew.getUnknownFields());
  }

  private void diffOptionsFromMethod(
      Descriptors.MethodDescriptor descriptorRef, Descriptors.MethodDescriptor descriptorNew) {
    DescriptorProtos.MethodOptions optionsRef = descriptorRef.getOptions();
    DescriptorProtos.MethodOptions optionsNew = descriptorNew.getOptions();
    diffExtensionOptions(
        OptionChangeInfo.OptionType.METHOD_OPTION,
        descriptorRef,
        optionsRef.getAllFields(),
        descriptorNew,
        optionsNew.getAllFields());
    diffUnknownOptions(
        OptionChangeInfo.OptionType.METHOD_OPTION,
        descriptorRef,
        optionsRef.getUnknownFields(),
        descriptorNew,
        optionsNew.getUnknownFields());
  }

  private ByteString serializeUnknownField(int optionNumber, UnknownFieldSet.Field field) {
    ByteBuffer byteBuffer = ByteBuffer.allocate(field.getSerializedSize(optionNumber));
    CodedOutputStream stream = CodedOutputStream.newInstance(byteBuffer);
    try {
      field.writeTo(optionNumber, stream);
    } catch (IOException e) {
      throw new RuntimeException(
          "failed to serialize unknown field with number " + optionNumber, e);
    }
    return ByteString.copyFrom(byteBuffer);
  }

  private ByteString serializePayload(Message field) {
    ByteBuffer byteBuffer = ByteBuffer.allocate(field.getSerializedSize());
    try {
      CodedOutputStream stream = CodedOutputStream.newInstance(byteBuffer);
      field.writeTo(stream);
    } catch (IOException e) {
      throw new RuntimeException("failed to serialize unknown field with number ", e);
    }
    return ByteString.copyFrom(byteBuffer);
  }
}