/* Copyright (c) 2008, Nathan Sweet
 * All rights reserved.
 * 
 * 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 Esoteric Software 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 HOLDER 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.esotericsoftware.kryo.serializers;

import static com.esotericsoftware.minlog.Log.*;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.serializers.FieldSerializer.CachedField;

/** A few utility methods for using generic type parameters, mostly by FieldSerializer
 * 
 * @author Roman Levenstein <romixlev@gmail.com> */
final class FieldSerializerGenericsUtil {
	private Kryo kryo;
	private FieldSerializer serializer;

	public FieldSerializerGenericsUtil (FieldSerializer serializer) {
		this.serializer = serializer;
		this.kryo = serializer.getKryo();
	}

	/*** Create a mapping from type variable names (which are declared as type parameters of a generic class) to the concrete
	 * classes used for type instantiation.
	 * 
	 * @param clazz class with generic type arguments
	 * @param generics concrete types used to instantiate the class
	 * @return new scope for type parameters */
	Generics buildGenericsScope (Class clazz, Class[] generics) {
		Class typ = clazz;
		TypeVariable[] typeParams = null;

		while (typ != null) {
			if (typ == this.serializer.type)
				typeParams = this.serializer.typeParameters;
			else
				typeParams = typ.getTypeParameters();
			if (typeParams == null || typeParams.length == 0) {
				if (typ == this.serializer.type) {
					typ = this.serializer.componentType;
					if (typ != null) continue;
					// This is not a generic type.
					// Check if its superclass is generic.
					typ = this.serializer.type;
					Type superclass = null;
					do {
						superclass = typ.getGenericSuperclass();
						typ = typ.getSuperclass();
					} while (superclass != null && !(superclass instanceof ParameterizedType));
					if (superclass == null) break;
					ParameterizedType pt = (ParameterizedType)superclass;
					Type[] typeArgs = pt.getActualTypeArguments();
					typeParams = typ.getTypeParameters();
					generics = new Class[typeArgs.length];
					for (int i = 0; i < typeArgs.length; i++) {
						generics[i] = (typeArgs[i] instanceof Class) ? (Class)typeArgs[i] : Object.class;
					}
					break;
				} else
					typ = typ.getComponentType();
			} else
				break;
		}

		if (typeParams != null && typeParams.length > 0) {
			Generics genScope;
			if (TRACE) trace("kryo", "Class " + clazz.getName() + " has generic type parameters");
			int typeVarNum = 0;
			Map<String, Class> typeVar2concreteClass;
			typeVar2concreteClass = new HashMap<String, Class>();
			for (TypeVariable typeVar : typeParams) {
				String typeVarName = typeVar.getName();
				if (TRACE) {
					trace("kryo",
						"Type parameter variable: name=" + typeVarName + " type bounds=" + Arrays.toString(typeVar.getBounds()));
				}

				final Class<?> concreteClass = getTypeVarConcreteClass(generics, typeVarNum, typeVarName);
				if (concreteClass != null) {
					typeVar2concreteClass.put(typeVarName, concreteClass);
					if (TRACE) trace("kryo", "Concrete type used for " + typeVarName + " is: " + concreteClass.getName());
				}

				typeVarNum++;
			}
			genScope = new Generics(typeVar2concreteClass);
			return genScope;
		} else
			return null;
	}

	private Class<?> getTypeVarConcreteClass (Class[] generics, int typeVarNum, String typeVarName) {
		if (generics != null && generics.length > typeVarNum) {
			// If passed concrete classes are known explicitly, use this information
			return generics[typeVarNum];
		} else {
			// Otherwise try to derive the information from the current GenericScope
			if (TRACE) trace("kryo", "Trying to use kryo.getGenericScope");
			GenericsResolver scope = kryo.getGenericsResolver();
			if (scope.isSet()) {
				return scope.getConcreteClass(typeVarName);
			}
		}
		return null;
	}

	Class[] computeFieldGenerics (Type fieldGenericType, Field field, Class[] fieldClass) {
		Class[] fieldGenerics = null;
		if (fieldGenericType != null) {
			if (fieldGenericType instanceof TypeVariable && serializer.getGenericsScope() != null) {
				TypeVariable typeVar = (TypeVariable)fieldGenericType;
				// Obtain information about a concrete type of a given variable from the environment
				Class concreteClass = serializer.getGenericsScope().getConcreteClass(typeVar.getName());
				if (concreteClass != null) {
					fieldClass[0] = concreteClass;
					fieldGenerics = new Class[] {fieldClass[0]};
					if (TRACE)
						trace("kryo", "Determined concrete class of '" + field.getName() + "' to be " + fieldClass[0].getName());
				}
			} else if (fieldGenericType instanceof ParameterizedType) {
				ParameterizedType parameterizedType = (ParameterizedType)fieldGenericType;
				// Get actual type arguments of the current field's type
				Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
				// if(actualTypeArguments != null && generics != null) {
				if (actualTypeArguments != null) {
					fieldGenerics = new Class[actualTypeArguments.length];
					for (int i = 0; i < actualTypeArguments.length; ++i) {
						Type t = actualTypeArguments[i];
						if (t instanceof Class)
							fieldGenerics[i] = (Class)t;
						else if (t instanceof ParameterizedType)
							fieldGenerics[i] = (Class)((ParameterizedType)t).getRawType();
						else if (t instanceof TypeVariable && serializer.getGenericsScope() != null) {
							fieldGenerics[i] = serializer.getGenericsScope().getConcreteClass(((TypeVariable)t).getName());
							if (fieldGenerics[i] == null) fieldGenerics[i] = Object.class;
						} else if (t instanceof WildcardType)
							fieldGenerics[i] = Object.class;
						else if (t instanceof GenericArrayType) {
							Type componentType = ((GenericArrayType)t).getGenericComponentType();
							if (componentType instanceof Class)
								fieldGenerics[i] = Array.newInstance((Class)componentType, 0).getClass();
							else if (componentType instanceof TypeVariable) {
								Generics scope = serializer.getGenericsScope();
								if (scope != null) {
									Class clazz = scope.getConcreteClass(((TypeVariable)componentType).getName());
									if (clazz != null) {
										fieldGenerics[i] = Array.newInstance(clazz, 0).getClass();
									}
								}
							}
						} else
							fieldGenerics[i] = null;
					}
					if (TRACE && fieldGenerics != null) {
						trace("kryo", "Determined concrete class of parametrized '" + field.getName() + "' to be " + fieldGenericType
							+ " where type parameters are " + Arrays.toString(fieldGenerics));
					}
				}
			} else if (fieldGenericType instanceof GenericArrayType) {
				// TODO: store generics for arrays as well?
				GenericArrayType arrayType = (GenericArrayType)fieldGenericType;
				Type genericComponentType = arrayType.getGenericComponentType();
				Class[] tmpFieldClass = new Class[] {fieldClass[0]};
				fieldGenerics = computeFieldGenerics(genericComponentType, field, tmpFieldClass);
				// Kryo.getGenerics(fieldGenericType);
				if (TRACE && fieldGenerics != null) {
					trace("kryo", "Determined concrete class of a generic array '" + field.getName() + "' to be " + fieldGenericType
						+ " where type parameters are " + Arrays.toString(fieldGenerics));
				} else if (TRACE) trace("kryo", "Determined concrete class of '" + field.getName() + "' to be " + fieldGenericType);
			}
		}

		return fieldGenerics;
	}

	/** Special processing for fiels of generic types */
	CachedField newCachedFieldOfGenericType (Field field, int accessIndex, Class[] fieldClass, Type fieldGenericType) {
		Class[] fieldGenerics;
		CachedField cachedField;
		// This is a field with generic type parameters
		if (TRACE) {
			trace("kryo", "Field '" + field.getName() + "' of type " + fieldClass[0] + " of generic type " + fieldGenericType);
		}

		if (TRACE && fieldGenericType != null)
			trace("kryo", "Field generic type is of class " + fieldGenericType.getClass().getName());

		// Get set of provided type parameters

		// Get list of field specific concrete classes passed as generic parameters
		Class[] cachedFieldGenerics = FieldSerializerGenericsUtil.getGenerics(fieldGenericType, kryo);

		// Build a generics scope for this field
		Generics scope = buildGenericsScope(fieldClass[0], cachedFieldGenerics);

		// Is it a field of a generic parameter type, i.e. "T field"?
		if (fieldClass[0] == Object.class && fieldGenericType instanceof TypeVariable && serializer.getGenericsScope() != null) {
			TypeVariable typeVar = (TypeVariable)fieldGenericType;
			// Obtain information about a concrete type of a given variable from the environment
			Class concreteClass = serializer.getGenericsScope().getConcreteClass(typeVar.getName());
			if (concreteClass != null) {
				scope = new Generics();
				scope.add(typeVar.getName(), concreteClass);
			}
		}

		if (TRACE) {
			trace("kryo", "Generics scope of field '" + field.getName() + "' of class " + fieldGenericType + " is " + scope);
		}

		fieldGenerics = computeFieldGenerics(fieldGenericType, field, fieldClass);
		cachedField = serializer.newMatchingCachedField(field, accessIndex, fieldClass[0], fieldGenericType, fieldGenerics);

		if (fieldGenerics != null && cachedField instanceof ObjectField) {
			if (fieldGenerics.length > 0 && fieldGenerics[0] != null) {
				// If any information about concrete types for generic arguments of current field's type
				// was deriver, remember it.
				((ObjectField)cachedField).generics = fieldGenerics;
				if (TRACE) trace("kryo", "Field generics: " + Arrays.toString(fieldGenerics));
			}
		}
		return cachedField;
	}

	/** Returns the first level of classes or interfaces for a generic type.
	 * @return null if the specified type is not generic or its generic types are not classes. */
	public static Class[] getGenerics (Type genericType, Kryo kryo) {
		if (genericType instanceof GenericArrayType) {
			Type componentType = ((GenericArrayType)genericType).getGenericComponentType();
			if (componentType instanceof Class)
				return new Class[] {(Class)componentType};
			else
				return getGenerics(componentType, kryo);
		}
		if (!(genericType instanceof ParameterizedType)) return null;
		if (TRACE) trace("kryo", "Processing generic type " + genericType);
		Type[] actualTypes = ((ParameterizedType)genericType).getActualTypeArguments();
		Class[] generics = new Class[actualTypes.length];
		int count = 0;
		for (int i = 0, n = actualTypes.length; i < n; i++) {
			Type actualType = actualTypes[i];
			if (TRACE) trace("kryo", "Processing actual type " + actualType + " (" + actualType.getClass().getName() + ")");
			generics[i] = Object.class;
			if (actualType instanceof Class)
				generics[i] = (Class)actualType;
			else if (actualType instanceof ParameterizedType)
				generics[i] = (Class)((ParameterizedType)actualType).getRawType();
			else if (actualType instanceof TypeVariable) {
				GenericsResolver scope = kryo.getGenericsResolver();
				if (scope.isSet()) {
					Class clazz = scope.getConcreteClass(((TypeVariable)actualType).getName());
					if (clazz != null) {
						generics[i] = clazz;
					} else
						continue;
				} else
					continue;
			} else if (actualType instanceof GenericArrayType) {
				Type componentType = ((GenericArrayType)actualType).getGenericComponentType();
				if (componentType instanceof Class)
					generics[i] = Array.newInstance((Class)componentType, 0).getClass();
				else if (componentType instanceof TypeVariable) {
					GenericsResolver scope = kryo.getGenericsResolver();
					if (scope.isSet()) {
						Class clazz = scope.getConcreteClass(((TypeVariable)componentType).getName());
						if (clazz != null) {
							generics[i] = Array.newInstance(clazz, 0).getClass();
						}
					}
				} else {
					Class[] componentGenerics = getGenerics(componentType, kryo);
					if (componentGenerics != null) generics[i] = componentGenerics[0];
				}
			} else
				continue;
			count++;
		}
		if (count == 0) return null;
		return generics;
	}
}
