/*
 * Copyright (C) 2018-2021 Lightbend Inc. <https://www.lightbend.com>
 */

// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
// http://code.google.com/p/protobuf/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package akka.protobuf;

import akka.protobuf.Descriptors.Descriptor;
import akka.protobuf.Descriptors.FieldDescriptor;
import akka.protobuf.GeneratedMessage.ExtendableBuilder;
import akka.protobuf.Internal.EnumLite;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * A partial implementation of the {@link Message} interface which implements
 * as many methods of that interface as possible in terms of other methods.
 *
 * @author kenton@google.com Kenton Varda
 */
public abstract class AbstractMessage extends AbstractMessageLite
                                      implements Message {
  @SuppressWarnings("unchecked")
  public boolean isInitialized() {
    // Check that all required fields are present.
    for (final FieldDescriptor field : getDescriptorForType().getFields()) {
      if (field.isRequired()) {
        if (!hasField(field)) {
          return false;
        }
      }
    }

    // Check that embedded messages are initialized.
    for (final Map.Entry<FieldDescriptor, Object> entry :
        getAllFields().entrySet()) {
      final FieldDescriptor field = entry.getKey();
      if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
        if (field.isRepeated()) {
          for (final Message element : (List<Message>) entry.getValue()) {
            if (!element.isInitialized()) {
              return false;
            }
          }
        } else {
          if (!((Message) entry.getValue()).isInitialized()) {
            return false;
          }
        }
      }
    }

    return true;
  }

  public List<String> findInitializationErrors() {
    return Builder.findMissingFields(this);
  }

  public String getInitializationErrorString() {
    return delimitWithCommas(findInitializationErrors());
  }

  private static String delimitWithCommas(List<String> parts) {
    StringBuilder result = new StringBuilder();
    for (String part : parts) {
      if (result.length() > 0) {
        result.append(", ");
      }
      result.append(part);
    }
    return result.toString();
  }

  @Override
  public final String toString() {
    return TextFormat.printToString(this);
  }

  public void writeTo(final CodedOutputStream output) throws IOException {
    final boolean isMessageSet =
        getDescriptorForType().getOptions().getMessageSetWireFormat();

    for (final Map.Entry<FieldDescriptor, Object> entry :
        getAllFields().entrySet()) {
      final FieldDescriptor field = entry.getKey();
      final Object value = entry.getValue();
      if (isMessageSet && field.isExtension() &&
          field.getType() == FieldDescriptor.Type.MESSAGE &&
          !field.isRepeated()) {
        output.writeMessageSetExtension(field.getNumber(), (Message) value);
      } else {
        FieldSet.writeField(field, value, output);
      }
    }

    final UnknownFieldSet unknownFields = getUnknownFields();
    if (isMessageSet) {
      unknownFields.writeAsMessageSetTo(output);
    } else {
      unknownFields.writeTo(output);
    }
  }

  private int memoizedSize = -1;

  public int getSerializedSize() {
    int size = memoizedSize;
    if (size != -1) {
      return size;
    }

    size = 0;
    final boolean isMessageSet =
        getDescriptorForType().getOptions().getMessageSetWireFormat();

    for (final Map.Entry<FieldDescriptor, Object> entry :
        getAllFields().entrySet()) {
      final FieldDescriptor field = entry.getKey();
      final Object value = entry.getValue();
      if (isMessageSet && field.isExtension() &&
          field.getType() == FieldDescriptor.Type.MESSAGE &&
          !field.isRepeated()) {
        size += CodedOutputStream.computeMessageSetExtensionSize(
            field.getNumber(), (Message) value);
      } else {
        size += FieldSet.computeFieldSize(field, value);
      }
    }

    final UnknownFieldSet unknownFields = getUnknownFields();
    if (isMessageSet) {
      size += unknownFields.getSerializedSizeAsMessageSet();
    } else {
      size += unknownFields.getSerializedSize();
    }

    memoizedSize = size;
    return size;
  }

  @Override
  public boolean equals(final Object other) {
    if (other == this) {
      return true;
    }
    if (!(other instanceof Message)) {
      return false;
    }
    final Message otherMessage = (Message) other;
    if (getDescriptorForType() != otherMessage.getDescriptorForType()) {
      return false;
    }
    return getAllFields().equals(otherMessage.getAllFields()) &&
        getUnknownFields().equals(otherMessage.getUnknownFields());
  }

  @Override
  public int hashCode() {
    int hash = 41;
    hash = (19 * hash) + getDescriptorForType().hashCode();
    hash = hashFields(hash, getAllFields());
    hash = (29 * hash) + getUnknownFields().hashCode();
    return hash;
  }

  /** Get a hash code for given fields and values, using the given seed. */
  @SuppressWarnings("unchecked")
  protected int hashFields(int hash, Map<FieldDescriptor, Object> map) {
    for (Map.Entry<FieldDescriptor, Object> entry : map.entrySet()) {
      FieldDescriptor field = entry.getKey();
      Object value = entry.getValue();
      hash = (37 * hash) + field.getNumber();
      if (field.getType() != FieldDescriptor.Type.ENUM){
        hash = (53 * hash) + value.hashCode();
      } else if (field.isRepeated()) {
        List<? extends EnumLite> list = (List<? extends EnumLite>) value;
        hash = (53 * hash) + hashEnumList(list);
      } else {
        hash = (53 * hash) + hashEnum((EnumLite) value);
      }
    }
    return hash;
  }

  /**
   * Helper method for implementing {@link Message#hashCode()}.
   * @see Boolean#hashCode()
   */
  protected static int hashLong(long n) {
    return (int) (n ^ (n >>> 32));
  }

  /**
   * Helper method for implementing {@link Message#hashCode()}.
   * @see Boolean#hashCode()
   */
  protected static int hashBoolean(boolean b) {
    return b ? 1231 : 1237;
  }

  /**
   * Package private helper method for AbstractParser to create
   * UninitializedMessageException with missing field information.
   */
  @Override
  UninitializedMessageException newUninitializedMessageException() {
    return Builder.newUninitializedMessageException(this);
  }

  /**
   * Helper method for implementing {@link Message#hashCode()}.
   * <p>
   * This is needed because {@link java.lang.Enum#hashCode()} is final, but we
   * need to use the field number as the hash code to ensure compatibility
   * between statically and dynamically generated enum objects.
   */
  protected static int hashEnum(EnumLite e) {
    return e.getNumber();
  }

  /** Helper method for implementing {@link Message#hashCode()}. */
  protected static int hashEnumList(List<? extends EnumLite> list) {
    int hash = 1;
    for (EnumLite e : list) {
      hash = 31 * hash + hashEnum(e);
    }
    return hash;
  }

  // =================================================================

  /**
   * A partial implementation of the {@link Message.Builder} interface which
   * implements as many methods of that interface as possible in terms of
   * other methods.
   */
  @SuppressWarnings("unchecked")
  public static abstract class Builder<BuilderType extends Builder>
      extends AbstractMessageLite.Builder<BuilderType>
      implements Message.Builder {
    // The compiler produces an error if this is not declared explicitly.
    @Override
    public abstract BuilderType clone();

    public BuilderType clear() {
      for (final Map.Entry<FieldDescriptor, Object> entry :
           getAllFields().entrySet()) {
        clearField(entry.getKey());
      }
      return (BuilderType) this;
    }

    public List<String> findInitializationErrors() {
      return findMissingFields(this);
    }

    public String getInitializationErrorString() {
      return delimitWithCommas(findInitializationErrors());
    }

    public BuilderType mergeFrom(final Message other) {
      if (other.getDescriptorForType() != getDescriptorForType()) {
        throw new IllegalArgumentException(
          "mergeFrom(Message) can only merge messages of the same type.");
      }

      // Note:  We don't attempt to verify that other's fields have valid
      //   types.  Doing so would be a losing battle.  We'd have to verify
      //   all sub-messages as well, and we'd have to make copies of all of
      //   them to insure that they don't change after verification (since
      //   the Message interface itself cannot enforce immutability of
      //   implementations).
      // TODO(kenton):  Provide a function somewhere called makeDeepCopy()
      //   which allows people to make secure deep copies of messages.

      for (final Map.Entry<FieldDescriptor, Object> entry :
           other.getAllFields().entrySet()) {
        final FieldDescriptor field = entry.getKey();
        if (field.isRepeated()) {
          for (final Object element : (List)entry.getValue()) {
            addRepeatedField(field, element);
          }
        } else if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
          final Message existingValue = (Message)getField(field);
          if (existingValue == existingValue.getDefaultInstanceForType()) {
            setField(field, entry.getValue());
          } else {
            setField(field,
              existingValue.newBuilderForType()
                .mergeFrom(existingValue)
                .mergeFrom((Message)entry.getValue())
                .build());
          }
        } else {
          setField(field, entry.getValue());
        }
      }

      mergeUnknownFields(other.getUnknownFields());

      return (BuilderType) this;
    }

    @Override
    public BuilderType mergeFrom(final CodedInputStream input)
                                 throws IOException {
      return mergeFrom(input, ExtensionRegistry.getEmptyRegistry());
    }

    @Override
    public BuilderType mergeFrom(
        final CodedInputStream input,
        final ExtensionRegistryLite extensionRegistry)
        throws IOException {
      final UnknownFieldSet.Builder unknownFields =
        UnknownFieldSet.newBuilder(getUnknownFields());
      while (true) {
        final int tag = input.readTag();
        if (tag == 0) {
          break;
        }

        if (!mergeFieldFrom(input, unknownFields, extensionRegistry,
                            getDescriptorForType(), this, null, tag)) {
          // end group tag
          break;
        }
      }
      setUnknownFields(unknownFields.build());
      return (BuilderType) this;
    }

    /** helper method to handle {@code builder} and {@code extensions}. */
    private static void addRepeatedField(
        Message.Builder builder,
        FieldSet<FieldDescriptor> extensions,
        FieldDescriptor field,
        Object value) {
      if (builder != null) {
        builder.addRepeatedField(field, value);
      } else {
        extensions.addRepeatedField(field, value);
      }
    }

    /** helper method to handle {@code builder} and {@code extensions}. */
    private static void setField(
        Message.Builder builder,
        FieldSet<FieldDescriptor> extensions,
        FieldDescriptor field,
        Object value) {
      if (builder != null) {
        builder.setField(field, value);
      } else {
        extensions.setField(field, value);
      }
    }

    /** helper method to handle {@code builder} and {@code extensions}. */
    private static boolean hasOriginalMessage(
        Message.Builder builder,
        FieldSet<FieldDescriptor> extensions,
        FieldDescriptor field) {
      if (builder != null) {
        return builder.hasField(field);
      } else {
        return extensions.hasField(field);
      }
    }

    /** helper method to handle {@code builder} and {@code extensions}. */
    private static Message getOriginalMessage(
        Message.Builder builder,
        FieldSet<FieldDescriptor> extensions,
        FieldDescriptor field) {
      if (builder != null) {
        return (Message) builder.getField(field);
      } else {
        return (Message) extensions.getField(field);
      }
    }

    /** helper method to handle {@code builder} and {@code extensions}. */
    private static void mergeOriginalMessage(
        Message.Builder builder,
        FieldSet<FieldDescriptor> extensions,
        FieldDescriptor field,
        Message.Builder subBuilder) {
      Message originalMessage = getOriginalMessage(builder, extensions, field);
      if (originalMessage != null) {
        subBuilder.mergeFrom(originalMessage);
      }
    }

    /**
     * Like {@link #mergeFrom(CodedInputStream, ExtensionRegistryLite)}, but
     * parses a single field.
     *
     * When {@code builder} is not null, the method will parse and merge the
     * field into {@code builder}. Otherwise, it will try to parse the field
     * into {@code extensions}, when it's called by the parsing constructor in
     * generated classes.
     *
     * Package-private because it is used by GeneratedMessage.ExtendableMessage.
     * @param tag The tag, which should have already been read.
     * @return {@code true} unless the tag is an end-group tag.
     */
    static boolean mergeFieldFrom(
        CodedInputStream input,
        UnknownFieldSet.Builder unknownFields,
        ExtensionRegistryLite extensionRegistry,
        Descriptor type,
        Message.Builder builder,
        FieldSet<FieldDescriptor> extensions,
        int tag) throws IOException {
      if (type.getOptions().getMessageSetWireFormat() &&
          tag == WireFormat.MESSAGE_SET_ITEM_TAG) {
        mergeMessageSetExtensionFromCodedStream(
            input, unknownFields, extensionRegistry, type, builder, extensions);
        return true;
      }

      final int wireType = WireFormat.getTagWireType(tag);
      final int fieldNumber = WireFormat.getTagFieldNumber(tag);

      final FieldDescriptor field;
      Message defaultInstance = null;

      if (type.isExtensionNumber(fieldNumber)) {
        // extensionRegistry may be either ExtensionRegistry or
        // ExtensionRegistryLite.  Since the type we are parsing is a full
        // message, only a full ExtensionRegistry could possibly contain
        // extensions of it.  Otherwise we will treat the registry as if it
        // were empty.
        if (extensionRegistry instanceof ExtensionRegistry) {
          final ExtensionRegistry.ExtensionInfo extension =
            ((ExtensionRegistry) extensionRegistry)
              .findExtensionByNumber(type, fieldNumber);
          if (extension == null) {
            field = null;
          } else {
            field = extension.descriptor;
            defaultInstance = extension.defaultInstance;
            if (defaultInstance == null &&
                field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
              throw new IllegalStateException(
                  "Message-typed extension lacked default instance: " +
                  field.getFullName());
            }
          }
        } else {
          field = null;
        }
      } else if (builder != null) {
        field = type.findFieldByNumber(fieldNumber);
      } else {
        field = null;
      }

      boolean unknown = false;
      boolean packed = false;
      if (field == null) {
        unknown = true;  // Unknown field.
      } else if (wireType == FieldSet.getWireFormatForFieldType(
                   field.getLiteType(),
                   false  /* isPacked */)) {
        packed = false;
      } else if (field.isPackable() &&
                 wireType == FieldSet.getWireFormatForFieldType(
                   field.getLiteType(),
                   true  /* isPacked */)) {
        packed = true;
      } else {
        unknown = true;  // Unknown wire type.
      }

      if (unknown) {  // Unknown field or wrong wire type.  Skip.
        return unknownFields.mergeFieldFrom(tag, input);
      }

      if (packed) {
        final int length = input.readRawVarint32();
        final int limit = input.pushLimit(length);
        if (field.getLiteType() == WireFormat.FieldType.ENUM) {
          while (input.getBytesUntilLimit() > 0) {
            final int rawValue = input.readEnum();
            final Object value = field.getEnumType().findValueByNumber(rawValue);
            if (value == null) {
              // If the number isn't recognized as a valid value for this
              // enum, drop it (don't even add it to unknownFields).
              return true;
            }
            addRepeatedField(builder, extensions, field, value);
          }
        } else {
          while (input.getBytesUntilLimit() > 0) {
            final Object value =
              FieldSet.readPrimitiveField(input, field.getLiteType());
            addRepeatedField(builder, extensions, field, value);
          }
        }
        input.popLimit(limit);
      } else {
        final Object value;
        switch (field.getType()) {
          case GROUP: {
            final Message.Builder subBuilder;
            if (defaultInstance != null) {
              subBuilder = defaultInstance.newBuilderForType();
            } else {
              subBuilder = builder.newBuilderForField(field);
            }
            if (!field.isRepeated()) {
              mergeOriginalMessage(builder, extensions, field, subBuilder);
            }
            input.readGroup(field.getNumber(), subBuilder, extensionRegistry);
            value = subBuilder.buildPartial();
            break;
          }
          case MESSAGE: {
            final Message.Builder subBuilder;
            if (defaultInstance != null) {
              subBuilder = defaultInstance.newBuilderForType();
            } else {
              subBuilder = builder.newBuilderForField(field);
            }
            if (!field.isRepeated()) {
              mergeOriginalMessage(builder, extensions, field, subBuilder);
            }
            input.readMessage(subBuilder, extensionRegistry);
            value = subBuilder.buildPartial();
            break;
          }
          case ENUM:
            final int rawValue = input.readEnum();
            value = field.getEnumType().findValueByNumber(rawValue);
            // If the number isn't recognized as a valid value for this enum,
            // drop it.
            if (value == null) {
              unknownFields.mergeVarintField(fieldNumber, rawValue);
              return true;
            }
            break;
          default:
            value = FieldSet.readPrimitiveField(input, field.getLiteType());
            break;
        }

        if (field.isRepeated()) {
          addRepeatedField(builder, extensions, field, value);
        } else {
          setField(builder, extensions, field, value);
        }
      }

      return true;
    }

    /**
     * Called by {@code #mergeFieldFrom()} to parse a MessageSet extension.
     * If {@code builder} is not null, this method will merge MessageSet into
     * the builder.  Otherwise, it will merge the MessageSet into {@code
     * extensions}.
     */
    private static void mergeMessageSetExtensionFromCodedStream(
        CodedInputStream input,
        UnknownFieldSet.Builder unknownFields,
        ExtensionRegistryLite extensionRegistry,
        Descriptor type,
        Message.Builder builder,
        FieldSet<FieldDescriptor> extensions) throws IOException {

      // The wire format for MessageSet is:
      //   message MessageSet {
      //     repeated group Item = 1 {
      //       required int32 typeId = 2;
      //       required bytes message = 3;
      //     }
      //   }
      // "typeId" is the extension's field number.  The extension can only be
      // a message type, where "message" contains the encoded bytes of that
      // message.
      //
      // In practice, we will probably never see a MessageSet item in which
      // the message appears before the type ID, or where either field does not
      // appear exactly once.  However, in theory such cases are valid, so we
      // should be prepared to accept them.

      int typeId = 0;
      ByteString rawBytes = null; // If we encounter "message" before "typeId"
      ExtensionRegistry.ExtensionInfo extension = null;

      // Read bytes from input, if we get it's type first then parse it eagerly,
      // otherwise we store the raw bytes in a local variable.
      while (true) {
        final int tag = input.readTag();
        if (tag == 0) {
          break;
        }

        if (tag == WireFormat.MESSAGE_SET_TYPE_ID_TAG) {
          typeId = input.readUInt32();
          if (typeId != 0) {
            // extensionRegistry may be either ExtensionRegistry or
            // ExtensionRegistryLite. Since the type we are parsing is a full
            // message, only a full ExtensionRegistry could possibly contain
            // extensions of it. Otherwise we will treat the registry as if it
            // were empty.
            if (extensionRegistry instanceof ExtensionRegistry) {
              extension = ((ExtensionRegistry) extensionRegistry)
                  .findExtensionByNumber(type, typeId);
            }
          }

        } else if (tag == WireFormat.MESSAGE_SET_MESSAGE_TAG) {
          if (typeId != 0) {
            if (extension != null && ExtensionRegistryLite.isEagerlyParseMessageSets()) {
              // We already know the type, so we can parse directly from the
              // input with no copying.  Hooray!
              eagerlyMergeMessageSetExtension(
                  input, extension, extensionRegistry, builder, extensions);
              rawBytes = null;
              continue;
            }
          }
          // We haven't seen a type ID yet or we want parse message lazily.
          rawBytes = input.readBytes();

        } else { // Unknown tag. Skip it.
          if (!input.skipField(tag)) {
            break; // End of group
          }
        }
      }
      input.checkLastTagWas(WireFormat.MESSAGE_SET_ITEM_END_TAG);

      // Process the raw bytes.
      if (rawBytes != null && typeId != 0) { // Zero is not a valid type ID.
        if (extension != null) { // We known the type
          mergeMessageSetExtensionFromBytes(
              rawBytes, extension, extensionRegistry, builder, extensions);
        } else { // We don't know how to parse this. Ignore it.
          if (rawBytes != null) {
            unknownFields.mergeField(typeId, UnknownFieldSet.Field.newBuilder()
                .addLengthDelimited(rawBytes).build());
          }
        }
      }
    }

    private static void eagerlyMergeMessageSetExtension(
        CodedInputStream input,
        ExtensionRegistry.ExtensionInfo extension,
        ExtensionRegistryLite extensionRegistry,
        Message.Builder builder,
        FieldSet<FieldDescriptor> extensions) throws IOException {

      FieldDescriptor field = extension.descriptor;
      Message value = null;
      if (hasOriginalMessage(builder, extensions, field)) {
        Message originalMessage =
            getOriginalMessage(builder, extensions, field);
        Message.Builder subBuilder = originalMessage.toBuilder();
        input.readMessage(subBuilder, extensionRegistry);
        value = subBuilder.buildPartial();
      } else {
        value = input.readMessage(extension.defaultInstance.getParserForType(),
          extensionRegistry);
      }

      if (builder != null) {
        builder.setField(field, value);
      } else {
        extensions.setField(field, value);
      }
    }

    private static void mergeMessageSetExtensionFromBytes(
        ByteString rawBytes,
        ExtensionRegistry.ExtensionInfo extension,
        ExtensionRegistryLite extensionRegistry,
        Message.Builder builder,
        FieldSet<FieldDescriptor> extensions) throws IOException {

      FieldDescriptor field = extension.descriptor;
      boolean hasOriginalValue = hasOriginalMessage(builder, extensions, field);

      if (hasOriginalValue || ExtensionRegistryLite.isEagerlyParseMessageSets()) {
        // If the field already exists, we just parse the field.
        Message value = null;
        if (hasOriginalValue) {
          Message originalMessage =
              getOriginalMessage(builder, extensions, field);
          Message.Builder subBuilder= originalMessage.toBuilder();
          subBuilder.mergeFrom(rawBytes, extensionRegistry);
          value = subBuilder.buildPartial();
        } else {
          value = extension.defaultInstance.getParserForType()
              .parsePartialFrom(rawBytes, extensionRegistry);
        }
        setField(builder, extensions, field, value);
      } else {
        // Use LazyField to load MessageSet lazily.
        LazyField lazyField = new LazyField(
            extension.defaultInstance, extensionRegistry, rawBytes);
        if (builder != null) {
          // TODO(xiangl): it looks like this method can only be invoked by
          // ExtendableBuilder, but I'm not sure. So I double check the type of
          // builder here. It may be useless and need more investigation.
          if (builder instanceof ExtendableBuilder) {
            builder.setField(field, lazyField);
          } else {
            builder.setField(field, lazyField.getValue());
          }
        } else {
          extensions.setField(field, lazyField);
        }
      }
    }

    public BuilderType mergeUnknownFields(final UnknownFieldSet unknownFields) {
      setUnknownFields(
        UnknownFieldSet.newBuilder(getUnknownFields())
                       .mergeFrom(unknownFields)
                       .build());
      return (BuilderType) this;
    }

    public Message.Builder getFieldBuilder(final FieldDescriptor field) {
      throw new UnsupportedOperationException(
          "getFieldBuilder() called on an unsupported message type.");
    }

    /**
     * Construct an UninitializedMessageException reporting missing fields in
     * the given message.
     */
    protected static UninitializedMessageException
        newUninitializedMessageException(Message message) {
      return new UninitializedMessageException(findMissingFields(message));
    }

    /**
     * Populates {@code this.missingFields} with the full "path" of each
     * missing required field in the given message.
     */
    private static List<String> findMissingFields(
        final MessageOrBuilder message) {
      final List<String> results = new ArrayList<String>();
      findMissingFields(message, "", results);
      return results;
    }

    /** Recursive helper implementing {@link #findMissingFields(Message)}. */
    private static void findMissingFields(final MessageOrBuilder message,
                                          final String prefix,
                                          final List<String> results) {
      for (final FieldDescriptor field :
          message.getDescriptorForType().getFields()) {
        if (field.isRequired() && !message.hasField(field)) {
          results.add(prefix + field.getName());
        }
      }

      for (final Map.Entry<FieldDescriptor, Object> entry :
           message.getAllFields().entrySet()) {
        final FieldDescriptor field = entry.getKey();
        final Object value = entry.getValue();

        if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
          if (field.isRepeated()) {
            int i = 0;
            for (final Object element : (List) value) {
              findMissingFields((MessageOrBuilder) element,
                                subMessagePrefix(prefix, field, i++),
                                results);
            }
          } else {
            if (message.hasField(field)) {
              findMissingFields((MessageOrBuilder) value,
                                subMessagePrefix(prefix, field, -1),
                                results);
            }
          }
        }
      }
    }

    private static String subMessagePrefix(final String prefix,
                                           final FieldDescriptor field,
                                           final int index) {
      final StringBuilder result = new StringBuilder(prefix);
      if (field.isExtension()) {
        result.append('(')
              .append(field.getFullName())
              .append(')');
      } else {
        result.append(field.getName());
      }
      if (index != -1) {
        result.append('[')
              .append(index)
              .append(']');
      }
      result.append('.');
      return result.toString();
    }

    // ===============================================================
    // The following definitions seem to be required in order to make javac
    // not produce weird errors like:
    //
    // java/com/google/protobuf/DynamicMessage.java:203: types
    //   akka.protobuf.AbstractMessage.Builder<
    //     akka.protobuf.DynamicMessage.Builder> and
    //   akka.protobuf.AbstractMessage.Builder<
    //     akka.protobuf.DynamicMessage.Builder> are incompatible; both
    //   define mergeFrom(akka.protobuf.ByteString), but with unrelated
    //   return types.
    //
    // Strangely, these lines are only needed if javac is invoked separately
    // on AbstractMessage.java and AbstractMessageLite.java.  If javac is
    // invoked on both simultaneously, it works.  (Or maybe the important
    // point is whether or not DynamicMessage.java is compiled together with
    // AbstractMessageLite.java -- not sure.)  I suspect this is a compiler
    // bug.

    @Override
    public BuilderType mergeFrom(final ByteString data)
        throws InvalidProtocolBufferException {
      return super.mergeFrom(data);
    }

    @Override
    public BuilderType mergeFrom(
        final ByteString data,
        final ExtensionRegistryLite extensionRegistry)
        throws InvalidProtocolBufferException {
      return super.mergeFrom(data, extensionRegistry);
    }

    @Override
    public BuilderType mergeFrom(final byte[] data)
        throws InvalidProtocolBufferException {
      return super.mergeFrom(data);
    }

    @Override
    public BuilderType mergeFrom(
        final byte[] data, final int off, final int len)
        throws InvalidProtocolBufferException {
      return super.mergeFrom(data, off, len);
    }

    @Override
    public BuilderType mergeFrom(
        final byte[] data,
        final ExtensionRegistryLite extensionRegistry)
        throws InvalidProtocolBufferException {
      return super.mergeFrom(data, extensionRegistry);
    }

    @Override
    public BuilderType mergeFrom(
        final byte[] data, final int off, final int len,
        final ExtensionRegistryLite extensionRegistry)
        throws InvalidProtocolBufferException {
      return super.mergeFrom(data, off, len, extensionRegistry);
    }

    @Override
    public BuilderType mergeFrom(final InputStream input)
        throws IOException {
      return super.mergeFrom(input);
    }

    @Override
    public BuilderType mergeFrom(
        final InputStream input,
        final ExtensionRegistryLite extensionRegistry)
        throws IOException {
      return super.mergeFrom(input, extensionRegistry);
    }

    @Override
    public boolean mergeDelimitedFrom(final InputStream input)
        throws IOException {
      return super.mergeDelimitedFrom(input);
    }

    @Override
    public boolean mergeDelimitedFrom(
        final InputStream input,
        final ExtensionRegistryLite extensionRegistry)
        throws IOException {
      return super.mergeDelimitedFrom(input, extensionRegistry);
    }

  }
}
