// 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 java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.List;

/** Enumeration identifying all relevant type information for a protobuf field. */
@ExperimentalApi
public enum FieldType {
  DOUBLE(0, Collection.SCALAR, JavaType.DOUBLE),
  FLOAT(1, Collection.SCALAR, JavaType.FLOAT),
  INT64(2, Collection.SCALAR, JavaType.LONG),
  UINT64(3, Collection.SCALAR, JavaType.LONG),
  INT32(4, Collection.SCALAR, JavaType.INT),
  FIXED64(5, Collection.SCALAR, JavaType.LONG),
  FIXED32(6, Collection.SCALAR, JavaType.INT),
  BOOL(7, Collection.SCALAR, JavaType.BOOLEAN),
  STRING(8, Collection.SCALAR, JavaType.STRING),
  MESSAGE(9, Collection.SCALAR, JavaType.MESSAGE),
  BYTES(10, Collection.SCALAR, JavaType.BYTE_STRING),
  UINT32(11, Collection.SCALAR, JavaType.INT),
  ENUM(12, Collection.SCALAR, JavaType.ENUM),
  SFIXED32(13, Collection.SCALAR, JavaType.INT),
  SFIXED64(14, Collection.SCALAR, JavaType.LONG),
  SINT32(15, Collection.SCALAR, JavaType.INT),
  SINT64(16, Collection.SCALAR, JavaType.LONG),
  GROUP(17, Collection.SCALAR, JavaType.MESSAGE),
  DOUBLE_LIST(18, Collection.VECTOR, JavaType.DOUBLE),
  FLOAT_LIST(19, Collection.VECTOR, JavaType.FLOAT),
  INT64_LIST(20, Collection.VECTOR, JavaType.LONG),
  UINT64_LIST(21, Collection.VECTOR, JavaType.LONG),
  INT32_LIST(22, Collection.VECTOR, JavaType.INT),
  FIXED64_LIST(23, Collection.VECTOR, JavaType.LONG),
  FIXED32_LIST(24, Collection.VECTOR, JavaType.INT),
  BOOL_LIST(25, Collection.VECTOR, JavaType.BOOLEAN),
  STRING_LIST(26, Collection.VECTOR, JavaType.STRING),
  MESSAGE_LIST(27, Collection.VECTOR, JavaType.MESSAGE),
  BYTES_LIST(28, Collection.VECTOR, JavaType.BYTE_STRING),
  UINT32_LIST(29, Collection.VECTOR, JavaType.INT),
  ENUM_LIST(30, Collection.VECTOR, JavaType.ENUM),
  SFIXED32_LIST(31, Collection.VECTOR, JavaType.INT),
  SFIXED64_LIST(32, Collection.VECTOR, JavaType.LONG),
  SINT32_LIST(33, Collection.VECTOR, JavaType.INT),
  SINT64_LIST(34, Collection.VECTOR, JavaType.LONG),
  DOUBLE_LIST_PACKED(35, Collection.PACKED_VECTOR, JavaType.DOUBLE),
  FLOAT_LIST_PACKED(36, Collection.PACKED_VECTOR, JavaType.FLOAT),
  INT64_LIST_PACKED(37, Collection.PACKED_VECTOR, JavaType.LONG),
  UINT64_LIST_PACKED(38, Collection.PACKED_VECTOR, JavaType.LONG),
  INT32_LIST_PACKED(39, Collection.PACKED_VECTOR, JavaType.INT),
  FIXED64_LIST_PACKED(40, Collection.PACKED_VECTOR, JavaType.LONG),
  FIXED32_LIST_PACKED(41, Collection.PACKED_VECTOR, JavaType.INT),
  BOOL_LIST_PACKED(42, Collection.PACKED_VECTOR, JavaType.BOOLEAN),
  UINT32_LIST_PACKED(43, Collection.PACKED_VECTOR, JavaType.INT),
  ENUM_LIST_PACKED(44, Collection.PACKED_VECTOR, JavaType.ENUM),
  SFIXED32_LIST_PACKED(45, Collection.PACKED_VECTOR, JavaType.INT),
  SFIXED64_LIST_PACKED(46, Collection.PACKED_VECTOR, JavaType.LONG),
  SINT32_LIST_PACKED(47, Collection.PACKED_VECTOR, JavaType.INT),
  SINT64_LIST_PACKED(48, Collection.PACKED_VECTOR, JavaType.LONG),
  GROUP_LIST(49, Collection.VECTOR, JavaType.MESSAGE),
  MAP(50, Collection.MAP, JavaType.VOID);

  private final JavaType javaType;
  private final int id;
  private final Collection collection;
  private final Class<?> elementType;
  private final boolean primitiveScalar;

  FieldType(int id, Collection collection, JavaType javaType) {
    this.id = id;
    this.collection = collection;
    this.javaType = javaType;

    switch (collection) {
      case MAP:
        elementType = javaType.getBoxedType();
        break;
      case VECTOR:
        elementType = javaType.getBoxedType();
        break;
      case SCALAR:
      default:
        elementType = null;
        break;
    }

    boolean primitiveScalar = false;
    if (collection == Collection.SCALAR) {
      switch (javaType) {
        case BYTE_STRING:
        case MESSAGE:
        case STRING:
          break;
        default:
          primitiveScalar = true;
          break;
      }
    }
    this.primitiveScalar = primitiveScalar;
  }

  /** A reliable unique identifier for this type. */
  public int id() {
    return id;
  }

  /**
   * Gets the {@link JavaType} for this field. For lists, this identifies the type of the elements
   * contained within the list.
   */
  public JavaType getJavaType() {
    return javaType;
  }

  /** Indicates whether a list field should be represented on the wire in packed form. */
  public boolean isPacked() {
    return Collection.PACKED_VECTOR.equals(collection);
  }

  /**
   * Indicates whether this field type represents a primitive scalar value. If this is {@code true},
   * then {@link #isScalar()} will also be {@code true}.
   */
  public boolean isPrimitiveScalar() {
    return primitiveScalar;
  }

  /** Indicates whether this field type represents a scalar value. */
  public boolean isScalar() {
    return collection == Collection.SCALAR;
  }

  /** Indicates whether this field represents a list of values. */
  public boolean isList() {
    return collection.isList();
  }

  /** Indicates whether this field represents a map. */
  public boolean isMap() {
    return collection == Collection.MAP;
  }

  /** Indicates whether or not this {@link FieldType} can be applied to the given {@link Field}. */
  public boolean isValidForField(Field field) {
    if (Collection.VECTOR.equals(collection)) {
      return isValidForList(field);
    } else {
      return javaType.getType().isAssignableFrom(field.getType());
    }
  }

  private boolean isValidForList(Field field) {
    Class<?> clazz = field.getType();
    if (!javaType.getType().isAssignableFrom(clazz)) {
      // The field isn't a List type.
      return false;
    }
    Type[] types = EMPTY_TYPES;
    Type genericType = field.getGenericType();
    if (genericType instanceof ParameterizedType) {
      types = ((ParameterizedType) field.getGenericType()).getActualTypeArguments();
    }
    Type listParameter = getListParameter(clazz, types);
    if (!(listParameter instanceof Class)) {
      // It's a wildcard, we should allow anything in the list.
      return true;
    }
    return elementType.isAssignableFrom((Class<?>) listParameter);
  }

  /**
   * Looks up the appropriate {@link FieldType} by it's identifier.
   *
   * @return the {@link FieldType} or {@code null} if not found.
   */
  public static FieldType forId(int id) {
    if (id < 0 || id >= VALUES.length) {
      return null;
    }
    return VALUES[id];
  }

  private static final FieldType[] VALUES;
  private static final Type[] EMPTY_TYPES = new Type[0];

  static {
    FieldType[] values = values();
    VALUES = new FieldType[values.length];
    for (FieldType type : values) {
      VALUES[type.id] = type;
    }
  }

  /**
   * Given a class, finds a generic super class or interface that extends {@link List}.
   *
   * @return the generic super class/interface, or {@code null} if not found.
   */
  private static Type getGenericSuperList(Class<?> clazz) {
    // First look at interfaces.
    Type[] genericInterfaces = clazz.getGenericInterfaces();
    for (Type genericInterface : genericInterfaces) {
      if (genericInterface instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
        Class<?> rawType = (Class<?>) parameterizedType.getRawType();
        if (List.class.isAssignableFrom(rawType)) {
          return genericInterface;
        }
      }
    }

    // Try the subclass
    Type type = clazz.getGenericSuperclass();
    if (type instanceof ParameterizedType) {
      ParameterizedType parameterizedType = (ParameterizedType) type;
      Class<?> rawType = (Class<?>) parameterizedType.getRawType();
      if (List.class.isAssignableFrom(rawType)) {
        return type;
      }
    }

    // No super class/interface extends List.
    return null;
  }

  /**
   * Inspects the inheritance hierarchy for the given class and finds the generic type parameter for
   * {@link List}.
   *
   * @param clazz the class to begin the search.
   * @param realTypes the array of actual type parameters for {@code clazz}. These will be used to
   *     substitute generic parameters up the inheritance hierarchy. If {@code clazz} does not have
   *     any generic parameters, this list should be empty.
   * @return the {@link List} parameter.
   */
  private static Type getListParameter(Class<?> clazz, Type[] realTypes) {
    top:
    while (clazz != List.class) {
      // First look at generic subclass and interfaces.
      Type genericType = getGenericSuperList(clazz);
      if (genericType instanceof ParameterizedType) {
        // Replace any generic parameters with the real values.
        ParameterizedType parameterizedType = (ParameterizedType) genericType;
        Type[] superArgs = parameterizedType.getActualTypeArguments();
        for (int i = 0; i < superArgs.length; ++i) {
          Type superArg = superArgs[i];
          if (superArg instanceof TypeVariable) {
            // Get the type variables for this class so that we can match them to the variables
            // used on the super class.
            TypeVariable<?>[] clazzParams = clazz.getTypeParameters();
            if (realTypes.length != clazzParams.length) {
              throw new RuntimeException("Type array mismatch");
            }

            // Replace the variable parameter with the real type.
            boolean foundReplacement = false;
            for (int j = 0; j < clazzParams.length; ++j) {
              if (superArg == clazzParams[j]) {
                Type realType = realTypes[j];
                superArgs[i] = realType;
                foundReplacement = true;
                break;
              }
            }
            if (!foundReplacement) {
              throw new RuntimeException("Unable to find replacement for " + superArg);
            }
          }
        }

        Class<?> parent = (Class<?>) parameterizedType.getRawType();

        realTypes = superArgs;
        clazz = parent;
        continue;
      }

      // None of the parameterized types inherit List. Just continue up the inheritance hierarchy
      // toward the List interface until we can identify the parameters.
      realTypes = EMPTY_TYPES;
      for (Class<?> iface : clazz.getInterfaces()) {
        if (List.class.isAssignableFrom(iface)) {
          clazz = iface;
          continue top;
        }
      }
      clazz = clazz.getSuperclass();
    }

    if (realTypes.length != 1) {
      throw new RuntimeException("Unable to identify parameter type for List<T>");
    }
    return realTypes[0];
  }

  enum Collection {
    SCALAR(false),
    VECTOR(true),
    PACKED_VECTOR(true),
    MAP(false);

    private final boolean isList;

    Collection(boolean isList) {
      this.isList = isList;
    }

    /** @return the isList */
    public boolean isList() {
      return isList;
    }
  }
}
