// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// 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 com.google.protobuf;

import com.google.protobuf.FieldSet.FieldDescriptorLite;
import com.google.protobuf.Internal.EnumLiteMap;
import com.google.protobuf.Internal.EnumVerifier;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.List;
import java.util.RandomAccess;

/** Helper methods used by schemas. */
@ExperimentalApi
final class SchemaUtil {
  private static final Class<?> GENERATED_MESSAGE_CLASS = getGeneratedMessageClass();
  private static final UnknownFieldSchema<?, ?> PROTO2_UNKNOWN_FIELD_SET_SCHEMA =
      getUnknownFieldSetSchema(false);
  private static final UnknownFieldSchema<?, ?> PROTO3_UNKNOWN_FIELD_SET_SCHEMA =
      getUnknownFieldSetSchema(true);
  private static final UnknownFieldSchema<?, ?> UNKNOWN_FIELD_SET_LITE_SCHEMA =
      new UnknownFieldSetLiteSchema();

  private static final int DEFAULT_LOOK_UP_START_NUMBER = 40;

  private SchemaUtil() {}

  /**
   * Requires that the given message extend {@link com.google.protobuf.GeneratedMessageV3} or {@link
   * GeneratedMessageLite}.
   */
  public static void requireGeneratedMessage(Class<?> messageType) {
    // TODO(b/248560713) decide if we're keeping support for Full in schema classes and handle this
    // better.
    if (!GeneratedMessageLite.class.isAssignableFrom(messageType)
        && GENERATED_MESSAGE_CLASS != null
        && !GENERATED_MESSAGE_CLASS.isAssignableFrom(messageType)) {
      throw new IllegalArgumentException(
          "Message classes must extend GeneratedMessageV3 or GeneratedMessageLite");
    }
  }

  public static void writeDouble(int fieldNumber, double value, Writer writer) throws IOException {
    if (Double.doubleToRawLongBits(value) != 0) {
      writer.writeDouble(fieldNumber, value);
    }
  }

  public static void writeFloat(int fieldNumber, float value, Writer writer) throws IOException {
    if (Float.floatToRawIntBits(value) != 0) {
      writer.writeFloat(fieldNumber, value);
    }
  }

  public static void writeInt64(int fieldNumber, long value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeInt64(fieldNumber, value);
    }
  }

  public static void writeUInt64(int fieldNumber, long value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeUInt64(fieldNumber, value);
    }
  }

  public static void writeSInt64(int fieldNumber, long value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeSInt64(fieldNumber, value);
    }
  }

  public static void writeFixed64(int fieldNumber, long value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeFixed64(fieldNumber, value);
    }
  }

  public static void writeSFixed64(int fieldNumber, long value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeSFixed64(fieldNumber, value);
    }
  }

  public static void writeInt32(int fieldNumber, int value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeInt32(fieldNumber, value);
    }
  }

  public static void writeUInt32(int fieldNumber, int value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeUInt32(fieldNumber, value);
    }
  }

  public static void writeSInt32(int fieldNumber, int value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeSInt32(fieldNumber, value);
    }
  }

  public static void writeFixed32(int fieldNumber, int value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeFixed32(fieldNumber, value);
    }
  }

  public static void writeSFixed32(int fieldNumber, int value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeSFixed32(fieldNumber, value);
    }
  }

  public static void writeEnum(int fieldNumber, int value, Writer writer) throws IOException {
    if (value != 0) {
      writer.writeEnum(fieldNumber, value);
    }
  }

  public static void writeBool(int fieldNumber, boolean value, Writer writer) throws IOException {
    if (value) {
      writer.writeBool(fieldNumber, true);
    }
  }

  public static void writeString(int fieldNumber, Object value, Writer writer) throws IOException {
    if (value instanceof String) {
      writeStringInternal(fieldNumber, (String) value, writer);
    } else {
      writeBytes(fieldNumber, (ByteString) value, writer);
    }
  }

  private static void writeStringInternal(int fieldNumber, String value, Writer writer)
      throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeString(fieldNumber, value);
    }
  }

  public static void writeBytes(int fieldNumber, ByteString value, Writer writer)
      throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeBytes(fieldNumber, value);
    }
  }

  public static void writeMessage(int fieldNumber, Object value, Writer writer) throws IOException {
    if (value != null) {
      writer.writeMessage(fieldNumber, value);
    }
  }

  public static void writeDoubleList(
      int fieldNumber, List<Double> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeDoubleList(fieldNumber, value, packed);
    }
  }

  public static void writeFloatList(
      int fieldNumber, List<Float> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeFloatList(fieldNumber, value, packed);
    }
  }

  public static void writeInt64List(
      int fieldNumber, List<Long> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeInt64List(fieldNumber, value, packed);
    }
  }

  public static void writeUInt64List(
      int fieldNumber, List<Long> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeUInt64List(fieldNumber, value, packed);
    }
  }

  public static void writeSInt64List(
      int fieldNumber, List<Long> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeSInt64List(fieldNumber, value, packed);
    }
  }

  public static void writeFixed64List(
      int fieldNumber, List<Long> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeFixed64List(fieldNumber, value, packed);
    }
  }

  public static void writeSFixed64List(
      int fieldNumber, List<Long> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeSFixed64List(fieldNumber, value, packed);
    }
  }

  public static void writeInt32List(
      int fieldNumber, List<Integer> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeInt32List(fieldNumber, value, packed);
    }
  }

  public static void writeUInt32List(
      int fieldNumber, List<Integer> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeUInt32List(fieldNumber, value, packed);
    }
  }

  public static void writeSInt32List(
      int fieldNumber, List<Integer> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeSInt32List(fieldNumber, value, packed);
    }
  }

  public static void writeFixed32List(
      int fieldNumber, List<Integer> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeFixed32List(fieldNumber, value, packed);
    }
  }

  public static void writeSFixed32List(
      int fieldNumber, List<Integer> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeSFixed32List(fieldNumber, value, packed);
    }
  }

  public static void writeEnumList(
      int fieldNumber, List<Integer> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeEnumList(fieldNumber, value, packed);
    }
  }

  public static void writeBoolList(
      int fieldNumber, List<Boolean> value, Writer writer, boolean packed) throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeBoolList(fieldNumber, value, packed);
    }
  }

  public static void writeStringList(int fieldNumber, List<String> value, Writer writer)
      throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeStringList(fieldNumber, value);
    }
  }

  public static void writeBytesList(int fieldNumber, List<ByteString> value, Writer writer)
      throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeBytesList(fieldNumber, value);
    }
  }

  public static void writeMessageList(int fieldNumber, List<?> value, Writer writer)
      throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeMessageList(fieldNumber, value);
    }
  }

  public static void writeMessageList(int fieldNumber, List<?> value, Writer writer, Schema schema)
      throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeMessageList(fieldNumber, value, schema);
    }
  }

  public static void writeLazyFieldList(int fieldNumber, List<?> value, Writer writer)
      throws IOException {
    if (value != null && !value.isEmpty()) {
      for (Object item : value) {
        ((LazyFieldLite) item).writeTo(writer, fieldNumber);
      }
    }
  }

  public static void writeGroupList(int fieldNumber, List<?> value, Writer writer)
      throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeGroupList(fieldNumber, value);
    }
  }

  public static void writeGroupList(int fieldNumber, List<?> value, Writer writer, Schema schema)
      throws IOException {
    if (value != null && !value.isEmpty()) {
      writer.writeGroupList(fieldNumber, value, schema);
    }
  }

  static int computeSizeInt64ListNoTag(List<Long> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }

    int size = 0;

    if (list instanceof LongArrayList) {
      final LongArrayList primitiveList = (LongArrayList) list;
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeInt64SizeNoTag(primitiveList.getLong(i));
      }
    } else {
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeInt64SizeNoTag(list.get(i));
      }
    }
    return size;
  }

  static int computeSizeInt64List(int fieldNumber, List<Long> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = computeSizeInt64ListNoTag(list);

    if (packed) {
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(size);
    } else {
      return size + (list.size() * CodedOutputStream.computeTagSize(fieldNumber));
    }
  }

  static int computeSizeUInt64ListNoTag(List<Long> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }

    int size = 0;

    if (list instanceof LongArrayList) {
      final LongArrayList primitiveList = (LongArrayList) list;
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeUInt64SizeNoTag(primitiveList.getLong(i));
      }
    } else {
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeUInt64SizeNoTag(list.get(i));
      }
    }
    return size;
  }

  static int computeSizeUInt64List(int fieldNumber, List<Long> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = computeSizeUInt64ListNoTag(list);

    if (packed) {
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(size);
    } else {
      return size + (length * CodedOutputStream.computeTagSize(fieldNumber));
    }
  }

  static int computeSizeSInt64ListNoTag(List<Long> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }

    int size = 0;

    if (list instanceof LongArrayList) {
      final LongArrayList primitiveList = (LongArrayList) list;
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeSInt64SizeNoTag(primitiveList.getLong(i));
      }
    } else {
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeSInt64SizeNoTag(list.get(i));
      }
    }
    return size;
  }

  static int computeSizeSInt64List(int fieldNumber, List<Long> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = computeSizeSInt64ListNoTag(list);

    if (packed) {
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(size);
    } else {
      return size + (length * CodedOutputStream.computeTagSize(fieldNumber));
    }
  }

  static int computeSizeEnumListNoTag(List<Integer> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }

    int size = 0;

    if (list instanceof IntArrayList) {
      final IntArrayList primitiveList = (IntArrayList) list;
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeEnumSizeNoTag(primitiveList.getInt(i));
      }
    } else {
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeEnumSizeNoTag(list.get(i));
      }
    }
    return size;
  }

  static int computeSizeEnumList(int fieldNumber, List<Integer> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = computeSizeEnumListNoTag(list);

    if (packed) {
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(size);
    } else {
      return size + (length * CodedOutputStream.computeTagSize(fieldNumber));
    }
  }

  static int computeSizeInt32ListNoTag(List<Integer> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }

    int size = 0;

    if (list instanceof IntArrayList) {
      final IntArrayList primitiveList = (IntArrayList) list;
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeInt32SizeNoTag(primitiveList.getInt(i));
      }
    } else {
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeInt32SizeNoTag(list.get(i));
      }
    }
    return size;
  }

  static int computeSizeInt32List(int fieldNumber, List<Integer> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = computeSizeInt32ListNoTag(list);

    if (packed) {
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(size);
    } else {
      return size + (length * CodedOutputStream.computeTagSize(fieldNumber));
    }
  }

  static int computeSizeUInt32ListNoTag(List<Integer> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }

    int size = 0;

    if (list instanceof IntArrayList) {
      final IntArrayList primitiveList = (IntArrayList) list;
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeUInt32SizeNoTag(primitiveList.getInt(i));
      }
    } else {
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeUInt32SizeNoTag(list.get(i));
      }
    }
    return size;
  }

  static int computeSizeUInt32List(int fieldNumber, List<Integer> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = computeSizeUInt32ListNoTag(list);

    if (packed) {
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(size);
    } else {
      return size + (length * CodedOutputStream.computeTagSize(fieldNumber));
    }
  }

  static int computeSizeSInt32ListNoTag(List<Integer> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }

    int size = 0;

    if (list instanceof IntArrayList) {
      final IntArrayList primitiveList = (IntArrayList) list;
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeSInt32SizeNoTag(primitiveList.getInt(i));
      }
    } else {
      for (int i = 0; i < length; i++) {
        size += CodedOutputStream.computeSInt32SizeNoTag(list.get(i));
      }
    }
    return size;
  }

  static int computeSizeSInt32List(int fieldNumber, List<Integer> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }

    int size = computeSizeSInt32ListNoTag(list);

    if (packed) {
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(size);
    } else {
      return size + (length * CodedOutputStream.computeTagSize(fieldNumber));
    }
  }

  static int computeSizeFixed32ListNoTag(List<?> list) {
    return list.size() * WireFormat.FIXED32_SIZE;
  }

  static int computeSizeFixed32List(int fieldNumber, List<?> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    if (packed) {
      int dataSize = length * WireFormat.FIXED32_SIZE;
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(dataSize);
    } else {
      return length * CodedOutputStream.computeFixed32Size(fieldNumber, 0);
    }
  }

  static int computeSizeFixed64ListNoTag(List<?> list) {
    return list.size() * WireFormat.FIXED64_SIZE;
  }

  static int computeSizeFixed64List(int fieldNumber, List<?> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    if (packed) {
      final int dataSize = length * WireFormat.FIXED64_SIZE;
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(dataSize);
    } else {
      return length * CodedOutputStream.computeFixed64Size(fieldNumber, 0);
    }
  }

  static int computeSizeBoolListNoTag(List<?> list) {
    // bools are 1 byte varints
    return list.size();
  }

  static int computeSizeBoolList(int fieldNumber, List<?> list, boolean packed) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    if (packed) {
      // bools are 1 byte varints
      return CodedOutputStream.computeTagSize(fieldNumber)
          + CodedOutputStream.computeLengthDelimitedFieldSize(length);
    } else {
      return length * CodedOutputStream.computeBoolSize(fieldNumber, true);
    }
  }

  static int computeSizeStringList(int fieldNumber, List<?> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = length * CodedOutputStream.computeTagSize(fieldNumber);
    if (list instanceof LazyStringList) {
      LazyStringList lazyList = ((LazyStringList) list);
      for (int i = 0; i < length; i++) {
        Object value = lazyList.getRaw(i);
        if (value instanceof ByteString) {
          size += CodedOutputStream.computeBytesSizeNoTag((ByteString) value);
        } else {
          size += CodedOutputStream.computeStringSizeNoTag((String) value);
        }
      }
    } else {
      for (int i = 0; i < length; i++) {
        Object value = list.get(i);
        if (value instanceof ByteString) {
          size += CodedOutputStream.computeBytesSizeNoTag((ByteString) value);
        } else {
          size += CodedOutputStream.computeStringSizeNoTag((String) value);
        }
      }
    }
    return size;
  }

  static int computeSizeMessage(int fieldNumber, Object value, Schema schema) {
    if (value instanceof LazyFieldLite) {
      return CodedOutputStream.computeLazyFieldSize(fieldNumber, (LazyFieldLite) value);
    } else {
      return CodedOutputStream.computeMessageSize(fieldNumber, (MessageLite) value, schema);
    }
  }

  static int computeSizeMessageList(int fieldNumber, List<?> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = length * CodedOutputStream.computeTagSize(fieldNumber);
    for (int i = 0; i < length; i++) {
      Object value = list.get(i);
      if (value instanceof LazyFieldLite) {
        size += CodedOutputStream.computeLazyFieldSizeNoTag((LazyFieldLite) value);
      } else {
        size += CodedOutputStream.computeMessageSizeNoTag((MessageLite) value);
      }
    }
    return size;
  }

  static int computeSizeMessageList(int fieldNumber, List<?> list, Schema schema) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = length * CodedOutputStream.computeTagSize(fieldNumber);
    for (int i = 0; i < length; i++) {
      Object value = list.get(i);
      if (value instanceof LazyFieldLite) {
        size += CodedOutputStream.computeLazyFieldSizeNoTag((LazyFieldLite) value);
      } else {
        size += CodedOutputStream.computeMessageSizeNoTag((MessageLite) value, schema);
      }
    }
    return size;
  }

  static int computeSizeByteStringList(int fieldNumber, List<ByteString> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = length * CodedOutputStream.computeTagSize(fieldNumber);
    for (int i = 0; i < list.size(); i++) {
      size += CodedOutputStream.computeBytesSizeNoTag(list.get(i));
    }
    return size;
  }

  static int computeSizeGroupList(int fieldNumber, List<MessageLite> list) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = 0;
    for (int i = 0; i < length; i++) {
      size += CodedOutputStream.computeGroupSize(fieldNumber, list.get(i));
    }
    return size;
  }

  static int computeSizeGroupList(int fieldNumber, List<MessageLite> list, Schema schema) {
    final int length = list.size();
    if (length == 0) {
      return 0;
    }
    int size = 0;
    for (int i = 0; i < length; i++) {
      size += CodedOutputStream.computeGroupSize(fieldNumber, list.get(i), schema);
    }
    return size;
  }

  /**
   * Determines whether to issue tableswitch or lookupswitch for the mergeFrom method.
   *
   * @see #shouldUseTableSwitch(int, int, int)
   */
  public static boolean shouldUseTableSwitch(FieldInfo[] fields) {
    // Determine whether to issue a tableswitch or a lookupswitch
    // instruction.
    if (fields.length == 0) {
      return false;
    }

    int lo = fields[0].getFieldNumber();
    int hi = fields[fields.length - 1].getFieldNumber();
    return shouldUseTableSwitch(lo, hi, fields.length);
  }

  /**
   * Determines whether to issue tableswitch or lookupswitch for the mergeFrom method. This is based
   * on the <a href=
   * "http://hg.openjdk.java.net/jdk8/jdk8/langtools/file/30db5e0aaf83/src/share/classes/com/sun/tools/javac/jvm/Gen.java#l1159">
   * logic in the JDK</a>.
   *
   * @param lo the lowest fieldNumber contained within the message.
   * @param hi the highest fieldNumber contained within the message.
   * @param numFields the total number of fields in the message.
   * @return {@code true} if tableswitch should be used, rather than lookupswitch.
   */
  public static boolean shouldUseTableSwitch(int lo, int hi, int numFields) {
    if (hi < DEFAULT_LOOK_UP_START_NUMBER) {
      return true;
    }
    long tableSpaceCost = ((long) hi - lo + 1); // words
    long tableTimeCost = 3; // comparisons
    long lookupSpaceCost = 3 + 2 * (long) numFields;
    long lookupTimeCost = 3 + (long) numFields;
    return tableSpaceCost + 3 * tableTimeCost <= lookupSpaceCost + 3 * lookupTimeCost;
  }

  public static UnknownFieldSchema<?, ?> proto2UnknownFieldSetSchema() {
    return PROTO2_UNKNOWN_FIELD_SET_SCHEMA;
  }

  public static UnknownFieldSchema<?, ?> proto3UnknownFieldSetSchema() {
    return PROTO3_UNKNOWN_FIELD_SET_SCHEMA;
  }

  public static UnknownFieldSchema<?, ?> unknownFieldSetLiteSchema() {
    return UNKNOWN_FIELD_SET_LITE_SCHEMA;
  }

  private static UnknownFieldSchema<?, ?> getUnknownFieldSetSchema(boolean proto3) {
    try {
      Class<?> clz = getUnknownFieldSetSchemaClass();
      if (clz == null) {
        return null;
      }
      return (UnknownFieldSchema) clz.getConstructor(boolean.class).newInstance(proto3);
    } catch (Throwable t) {
      return null;
    }
  }

  private static Class<?> getGeneratedMessageClass() {
    try {
      // TODO(b/248560713) decide if we're keeping support for Full in schema classes and handle
      // this better.
      return Class.forName("com.google.protobuf.GeneratedMessageV3");
    } catch (Throwable e) {
      return null;
    }
  }

  private static Class<?> getUnknownFieldSetSchemaClass() {
    try {
      return Class.forName("com.google.protobuf.UnknownFieldSetSchema");
    } catch (Throwable e) {
      return null;
    }
  }

  static Object getMapDefaultEntry(Class<?> clazz, String name) {
    try {
      Class<?> holder =
          Class.forName(clazz.getName() + "$" + toCamelCase(name, true) + "DefaultEntryHolder");
      Field[] fields = holder.getDeclaredFields();
      if (fields.length != 1) {
        throw new IllegalStateException(
            "Unable to look up map field default entry holder class for "
                + name
                + " in "
                + clazz.getName());
      }
      return UnsafeUtil.getStaticObject(fields[0]);
    } catch (Throwable t) {
      throw new RuntimeException(t);
    }
  }

  static String toCamelCase(String name, boolean capNext) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < name.length(); ++i) {
      char c = name.charAt(i);
      // Matches protoc field name function:
      if ('a' <= c && c <= 'z') {
        if (capNext) {
          sb.append((char) (c + ('A' - 'a')));
        } else {
          sb.append(c);
        }
        capNext = false;
      } else if ('A' <= c && c <= 'Z') {
        if (i == 0 && !capNext) {
          // Force first letter to lower-case unless explicitly told to capitalize it.
          sb.append((char) (c - ('A' - 'a')));
        } else {
          sb.append(c);
        }
        capNext = false;
      } else if ('0' <= c && c <= '9') {
        sb.append(c);
        capNext = true;
      } else {
        capNext = true;
      }
    }
    return sb.toString();
  }

  /** Returns true if both are null or both are {@link Object#equals}. */
  static boolean safeEquals(Object a, Object b) {
    return a == b || (a != null && a.equals(b));
  }

  static <T> void mergeMap(MapFieldSchema mapFieldSchema, T message, T o, long offset) {
    Object merged =
        mapFieldSchema.mergeFrom(
            UnsafeUtil.getObject(message, offset), UnsafeUtil.getObject(o, offset));
    UnsafeUtil.putObject(message, offset, merged);
  }

  static <T, FT extends FieldDescriptorLite<FT>> void mergeExtensions(
      ExtensionSchema<FT> schema, T message, T other) {
    FieldSet<FT> otherExtensions = schema.getExtensions(other);
    if (!otherExtensions.isEmpty()) {
      FieldSet<FT> messageExtensions = schema.getMutableExtensions(message);
      messageExtensions.mergeFrom(otherExtensions);
    }
  }

  static <T, UT, UB> void mergeUnknownFields(
      UnknownFieldSchema<UT, UB> schema, T message, T other) {
    UT messageUnknowns = schema.getFromMessage(message);
    UT otherUnknowns = schema.getFromMessage(other);
    UT merged = schema.merge(messageUnknowns, otherUnknowns);
    schema.setToMessage(message, merged);
  }

  /** Filters unrecognized enum values in a list. */
  @CanIgnoreReturnValue
  static <UT, UB> UB filterUnknownEnumList(
      Object containerMessage,
      int number,
      List<Integer> enumList,
      EnumLiteMap<?> enumMap,
      UB unknownFields,
      UnknownFieldSchema<UT, UB> unknownFieldSchema) {
    if (enumMap == null) {
      return unknownFields;
    }
    // TODO(dweis): Specialize for IntArrayList to avoid boxing.
    if (enumList instanceof RandomAccess) {
      int writePos = 0;
      int size = enumList.size();
      for (int readPos = 0; readPos < size; ++readPos) {
        int enumValue = enumList.get(readPos);
        if (enumMap.findValueByNumber(enumValue) != null) {
          if (readPos != writePos) {
            enumList.set(writePos, enumValue);
          }
          ++writePos;
        } else {
          unknownFields =
              storeUnknownEnum(
                  containerMessage, number, enumValue, unknownFields, unknownFieldSchema);
        }
      }
      if (writePos != size) {
        enumList.subList(writePos, size).clear();
      }
    } else {
      for (Iterator<Integer> it = enumList.iterator(); it.hasNext(); ) {
        int enumValue = it.next();
        if (enumMap.findValueByNumber(enumValue) == null) {
          unknownFields =
              storeUnknownEnum(
                  containerMessage, number, enumValue, unknownFields, unknownFieldSchema);
          it.remove();
        }
      }
    }
    return unknownFields;
  }

  /** Filters unrecognized enum values in a list. */
  @CanIgnoreReturnValue
  static <UT, UB> UB filterUnknownEnumList(
      Object containerMessage,
      int number,
      List<Integer> enumList,
      EnumVerifier enumVerifier,
      UB unknownFields,
      UnknownFieldSchema<UT, UB> unknownFieldSchema) {
    if (enumVerifier == null) {
      return unknownFields;
    }
    // TODO(dweis): Specialize for IntArrayList to avoid boxing.
    if (enumList instanceof RandomAccess) {
      int writePos = 0;
      int size = enumList.size();
      for (int readPos = 0; readPos < size; ++readPos) {
        int enumValue = enumList.get(readPos);
        if (enumVerifier.isInRange(enumValue)) {
          if (readPos != writePos) {
            enumList.set(writePos, enumValue);
          }
          ++writePos;
        } else {
          unknownFields =
              storeUnknownEnum(
                  containerMessage, number, enumValue, unknownFields, unknownFieldSchema);
        }
      }
      if (writePos != size) {
        enumList.subList(writePos, size).clear();
      }
    } else {
      for (Iterator<Integer> it = enumList.iterator(); it.hasNext(); ) {
        int enumValue = it.next();
        if (!enumVerifier.isInRange(enumValue)) {
          unknownFields =
              storeUnknownEnum(
                  containerMessage, number, enumValue, unknownFields, unknownFieldSchema);
          it.remove();
        }
      }
    }
    return unknownFields;
  }

  /** Stores an unrecognized enum value as an unknown value. */
  static <UT, UB> UB storeUnknownEnum(
      Object containerMessage,
      int number,
      int enumValue,
      UB unknownFields,
      UnknownFieldSchema<UT, UB> unknownFieldSchema) {
    if (unknownFields == null) {
      unknownFields = unknownFieldSchema.getBuilderFromMessage(containerMessage);
    }
    unknownFieldSchema.addVarint(unknownFields, number, enumValue);
    return unknownFields;
  }
}
