
package com.esotericsoftware.kryo.serializers;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.NotNull;
import com.esotericsoftware.kryo.Registration;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.util.IntArray;
import com.esotericsoftware.kryo.util.ObjectMap;
import com.esotericsoftware.kryo.util.Util;
import com.esotericsoftware.reflectasm.FieldAccess;

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

// BOZO - Make primitive serialization with ReflectASM configurable?

/** Serializes objects using direct field assignment. No header or schema data is stored, only the data for each field. This
 * reduces output size but means if any field is added or removed, previously serialized bytes are invalidated. If fields are
 * public, bytecode generation will be used instead of reflection.
 * @see Serializer
 * @see Kryo#register(Class, Serializer)
 * @author Nathan Sweet <misc@n4te.com> */
public class FieldSerializer<T> extends Serializer<T> implements Comparator<FieldSerializer.CachedField> {
	final Kryo kryo;
	final Class type;
	private CachedField[] fields = new CachedField[0];
	Object access;
	private boolean fieldsCanBeNull = true, setFieldsAsAccessible = true;
	private boolean ignoreSyntheticFields = true;
	private boolean fixedFieldTypes;

	// BOZO - Get rid of kryo here?
	public FieldSerializer (Kryo kryo, Class type) {
		this.kryo = kryo;
		this.type = type;
		rebuildCachedFields();
	}

	/** Called when the list of cached fields must be rebuilt. This is done any time settings are changed that affect which fields
	 * will be used. It is called from the constructor for FieldSerializer, but not for subclasses. Subclasses must call this from
	 * their constructor. */
	private void rebuildCachedFields () {
		if (type.isInterface()) {
			fields = new CachedField[0]; // No fields to serialize.
			return;
		}

		// Collect all fields.
		ArrayList<Field> allFields = new ArrayList();
		Class nextClass = type;
		while (nextClass != Object.class) {
			Collections.addAll(allFields, nextClass.getDeclaredFields());
			nextClass = nextClass.getSuperclass();
		}

		ObjectMap context = kryo.getContext();

		IntArray useAsm = new IntArray();
		ArrayList<Field> validFields = new ArrayList(allFields.size());
		for (int i = 0, n = allFields.size(); i < n; i++) {
			Field field = allFields.get(i);

			int modifiers = field.getModifiers();
			if (Modifier.isTransient(modifiers)) continue;
			if (Modifier.isStatic(modifiers)) continue;
			if (field.isSynthetic() && ignoreSyntheticFields) continue;

			if (!field.isAccessible()) {
				if (!setFieldsAsAccessible) continue;
				try {
					field.setAccessible(true);
				} catch (AccessControlException ex) {
					continue;
				}
			}

			Optional optional = field.getAnnotation(Optional.class);
			if (optional != null && !context.containsKey(optional.value())) continue;

			validFields.add(field);

			// BOZO - Must be public?
			useAsm.add(!Modifier.isFinal(modifiers) && Modifier.isPublic(modifiers)
				&& Modifier.isPublic(field.getType().getModifiers()) ? 1 : 0);
		}

		// Use ReflectASM for any public fields.
		if (!Util.isAndroid && Modifier.isPublic(type.getModifiers()) && useAsm.indexOf(1) != -1) {
			try {
				access = FieldAccess.get(type);
			} catch (RuntimeException ignored) {
			}
		}

		ArrayList<CachedField> cachedFields = new ArrayList(validFields.size());
		for (int i = 0, n = validFields.size(); i < n; i++) {
			Field field = validFields.get(i);

			int accessIndex = -1;
			if (access != null && useAsm.get(i) == 1) accessIndex = ((FieldAccess)access).getIndex(field.getName());

			cachedFields.add(newCachedField(field, cachedFields.size(), accessIndex));
		}

		Collections.sort(cachedFields, this);
		fields = cachedFields.toArray(new CachedField[cachedFields.size()]);

		initializeCachedFields();
	}

	protected void initializeCachedFields () {
	}

	private CachedField newCachedField (Field field, int fieldIndex, int accessIndex) {
		Class fieldClass = field.getType();

		CachedField cachedField;
		if (accessIndex != -1) {
			if (fieldClass.isPrimitive()) {
				if (fieldClass == boolean.class)
					cachedField = new BooleanField();
				else if (fieldClass == byte.class)
					cachedField = new ByteField();
				else if (fieldClass == char.class)
					cachedField = new CharField();
				else if (fieldClass == short.class)
					cachedField = new ShortField();
				else if (fieldClass == int.class)
					cachedField = new IntField();
				else if (fieldClass == long.class)
					cachedField = new LongField();
				else if (fieldClass == float.class)
					cachedField = new FloatField();
				else if (fieldClass == double.class)
					cachedField = new DoubleField();
				else
					cachedField = new ObjectField();
			} else if (fieldClass == String.class
				&& (!kryo.getReferences() || !kryo.getReferenceResolver().useReferences(String.class))) {
				cachedField = new StringField();
			} else
				cachedField = new ObjectField();
		} else {
			cachedField = new ObjectField();
			((ObjectField)cachedField).generics = Kryo.getGenerics(field.getGenericType());
		}

		cachedField.field = field;
		cachedField.accessIndex = accessIndex;
		cachedField.canBeNull = fieldsCanBeNull && !fieldClass.isPrimitive() && !field.isAnnotationPresent(NotNull.class);

		// Always use the same serializer for this field if the field's class is final.
		if (kryo.isFinal(fieldClass) || fixedFieldTypes) cachedField.valueClass = fieldClass;

		return cachedField;
	}

	public int compare (CachedField o1, CachedField o2) {
		// Fields are sorted by alpha so the order of the data is known.
		return o1.field.getName().compareTo(o2.field.getName());
	}

	/** Sets the default value for {@link CachedField#setCanBeNull(boolean)}. Calling this method resets the {@link #getFields()
	 * cached fields}.
	 * @param fieldsCanBeNull False if none of the fields are null. Saves 0-1 byte per field. True if it is not known (default). */
	public void setFieldsCanBeNull (boolean fieldsCanBeNull) {
		this.fieldsCanBeNull = fieldsCanBeNull;
		rebuildCachedFields();
	}

	/** Controls which fields are serialized. Calling this method resets the {@link #getFields() cached fields}.
	 * @param setFieldsAsAccessible If true, all non-transient fields (inlcuding private fields) will be serialized and
	 *           {@link Field#setAccessible(boolean) set as accessible} if necessary (default). If false, only fields in the public
	 *           API will be serialized. */
	public void setFieldsAsAccessible (boolean setFieldsAsAccessible) {
		this.setFieldsAsAccessible = setFieldsAsAccessible;
		rebuildCachedFields();
	}

	/** Controls if synthetic fields are serialized. Default is true. Calling this method resets the {@link #getFields() cached
	 * fields}.
	 * @param ignoreSyntheticFields If true, only non-synthetic fields will be serialized. */
	public void setIgnoreSyntheticFields (boolean ignoreSyntheticFields) {
		this.ignoreSyntheticFields = ignoreSyntheticFields;
		rebuildCachedFields();
	}

	/** Sets the default value for {@link CachedField#setClass(Class)} to the field's declared type. This allows FieldSerializer to
	 * be more efficient, since it knows field values will not be a subclass of their declared type. Default is false. Calling this
	 * method resets the {@link #getFields() cached fields}. */
	public void setFixedFieldTypes (boolean fixedFieldTypes) {
		this.fixedFieldTypes = fixedFieldTypes;
		rebuildCachedFields();
	}

	public void write (Kryo kryo, Output output, T object) {
		CachedField[] fields = this.fields;
		for (int i = 0, n = fields.length; i < n; i++)
			fields[i].write(output, object);
	}

	public T read (Kryo kryo, Input input, Class<T> type) {
		T object = create(kryo, input, type);
		kryo.reference(object);
		CachedField[] fields = this.fields;
		for (int i = 0, n = fields.length; i < n; i++)
			fields[i].read(input, object);
		return object;
	}

	/** Used by {@link #read(Kryo, Input, Class)} to create the new object. This can be overridden to customize object creation, eg
	 * to call a constructor with arguments. The default implementation uses {@link Kryo#newInstance(Class)}. */
	protected T create (Kryo kryo, Input input, Class<T> type) {
		return kryo.newInstance(type);
	}

	/** Allows specific fields to be optimized. */
	public CachedField getField (String fieldName) {
		for (CachedField cachedField : fields)
			if (cachedField.field.getName().equals(fieldName)) return cachedField;
		throw new IllegalArgumentException("Field \"" + fieldName + "\" not found on class: " + type.getName());
	}

	/** Removes a field so that it won't be serialized. */
	public void removeField (String fieldName) {
		for (int i = 0; i < fields.length; i++) {
			CachedField cachedField = fields[i];
			if (cachedField.field.getName().equals(fieldName)) {
				CachedField[] newFields = new CachedField[fields.length - 1];
				System.arraycopy(fields, 0, newFields, 0, i);
				System.arraycopy(fields, i + 1, newFields, i, newFields.length - i);
				fields = newFields;
				return;
			}
		}
		throw new IllegalArgumentException("Field \"" + fieldName + "\" not found on class: " + type.getName());
	}

	public CachedField[] getFields () {
		return fields;
	}

	public Class getType () {
		return type;
	}

	/** Used by {@link #copy(Kryo, Object)} to create the new object. This can be overridden to customize object creation, eg to
	 * call a constructor with arguments. The default implementation uses {@link Kryo#newInstance(Class)}. */
	protected T createCopy (Kryo kryo, T original) {
		return (T)kryo.newInstance(original.getClass());
	}

	public T copy (Kryo kryo, T original) {
		T copy = createCopy(kryo, original);
		kryo.reference(copy);
		for (int i = 0, n = fields.length; i < n; i++)
			fields[i].copy(original, copy);
		return copy;
	}

	/** Controls how a field will be serialized. */
	public abstract class CachedField<X> {
		Field field;
		Class valueClass;
		Serializer serializer;
		boolean canBeNull;
		int accessIndex = -1;

		/** @param valueClass The concrete class of the values for this field. This saves 1-2 bytes. The serializer registered for the
		 *           specified class will be used. Only set to a non-null value if the field type in the class definition is final
		 *           or the values for this field will not vary. */
		public void setClass (Class valueClass) {
			this.valueClass = valueClass;
			this.serializer = null;
		}

		/** @param valueClass The concrete class of the values for this field. This saves 1-2 bytes. Only set to a non-null value if
		 *           the field type in the class definition is final or the values for this field will not vary. */
		public void setClass (Class valueClass, Serializer serializer) {
			this.valueClass = valueClass;
			this.serializer = serializer;
		}

		public void setSerializer (Serializer serializer) {
			this.serializer = serializer;
		}

		public void setCanBeNull (boolean canBeNull) {
			this.canBeNull = canBeNull;
		}

		public Field getField () {
			return field;
		}

		public String toString () {
			return field.getName();
		}

		abstract public void write (Output output, Object object);

		abstract public void read (Input input, Object object);

		abstract public void copy (Object original, Object copy);
	}

	abstract class AsmCachedField extends CachedField {
		FieldAccess access = (FieldAccess)FieldSerializer.this.access;
	}

	class IntField extends AsmCachedField {
		public void write (Output output, Object object) {
			if (TRACE) {
				trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Write int: " + access.getInt(object, accessIndex));
			}
			output.writeInt(access.getInt(object, accessIndex), false);
		}

		public void read (Input input, Object object) {
			if (TRACE) {
				int value = input.readInt(false);
				access.setInt(object, accessIndex, value);
				trace("kryo", "Read field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Read int: " + value);
			} else
				access.setInt(object, accessIndex, input.readInt(false));
		}

		public void copy (Object original, Object copy) {
			access.setInt(copy, accessIndex, access.getInt(original, accessIndex));
		}
	}

	class FloatField extends AsmCachedField {
		public void write (Output output, Object object) {
			if (TRACE) {
				trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Write float: " + access.getFloat(object, accessIndex));
			}
			output.writeFloat(access.getFloat(object, accessIndex));
		}

		public void read (Input input, Object object) {
			if (TRACE) {
				float value = input.readFloat();
				access.setFloat(object, accessIndex, value);
				trace("kryo", "Read field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Read float: " + value);
			} else
				access.setFloat(object, accessIndex, input.readFloat());
		}

		public void copy (Object original, Object copy) {
			access.setFloat(copy, accessIndex, access.getFloat(original, accessIndex));
		}
	}

	class ShortField extends AsmCachedField {
		public void write (Output output, Object object) {
			if (TRACE) {
				trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Write short: " + access.getShort(object, accessIndex));
			}
			output.writeShort(access.getShort(object, accessIndex));
		}

		public void read (Input input, Object object) {
			if (TRACE) {
				short value = input.readShort();
				access.setShort(object, accessIndex, value);
				trace("kryo", "Read field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Read short: " + value);
			} else
				access.setShort(object, accessIndex, input.readShort());
		}

		public void copy (Object original, Object copy) {
			access.setShort(copy, accessIndex, access.getShort(original, accessIndex));
		}
	}

	class ByteField extends AsmCachedField {
		public void write (Output output, Object object) {
			if (TRACE) {
				trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Write byte: " + access.getByte(object, accessIndex));
			}
			output.writeByte(access.getByte(object, accessIndex));
		}

		public void read (Input input, Object object) {
			if (TRACE) {
				byte value = input.readByte();
				access.setByte(object, accessIndex, value);
				trace("kryo", "Read field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Read byte: " + value);
			} else
				access.setByte(object, accessIndex, input.readByte());
		}

		public void copy (Object original, Object copy) {
			access.setByte(copy, accessIndex, access.getByte(original, accessIndex));
		}
	}

	class BooleanField extends AsmCachedField {
		public void write (Output output, Object object) {
			if (TRACE) {
				trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Write boolean: " + access.getBoolean(object, accessIndex));
			}
			output.writeBoolean(access.getBoolean(object, accessIndex));
		}

		public void read (Input input, Object object) {
			if (TRACE) {
				boolean value = input.readBoolean();
				access.setBoolean(object, accessIndex, value);
				trace("kryo", "Read field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Read boolean: " + value);
			} else
				access.setBoolean(object, accessIndex, input.readBoolean());
		}

		public void copy (Object original, Object copy) {
			access.setBoolean(copy, accessIndex, access.getBoolean(original, accessIndex));
		}
	}

	class CharField extends AsmCachedField {
		public void write (Output output, Object object) {
			if (TRACE) {
				trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Write char: " + access.getChar(object, accessIndex));
			}
			output.writeChar(access.getChar(object, accessIndex));
		}

		public void read (Input input, Object object) {
			if (TRACE) {
				char value = input.readChar();
				access.setChar(object, accessIndex, value);
				trace("kryo", "Read field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Read char: " + value);
			} else
				access.setChar(object, accessIndex, input.readChar());
		}

		public void copy (Object original, Object copy) {
			access.setChar(copy, accessIndex, access.getChar(original, accessIndex));
		}
	}

	class LongField extends AsmCachedField {
		public void write (Output output, Object object) {
			if (TRACE) {
				trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Write long: " + access.getLong(object, accessIndex));
			}
			output.writeLong(access.getLong(object, accessIndex), false);
		}

		public void read (Input input, Object object) {
			if (TRACE) {
				long value = input.readLong(false);
				access.setLong(object, accessIndex, value);
				trace("kryo", "Read field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Read long: " + value);
			} else
				access.setLong(object, accessIndex, input.readLong(false));
		}

		public void copy (Object original, Object copy) {
			access.setLong(copy, accessIndex, access.getLong(original, accessIndex));
		}
	}

	class DoubleField extends AsmCachedField {
		public void write (Output output, Object object) {
			if (TRACE) {
				trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Write double: " + access.getDouble(object, accessIndex));
			}
			output.writeDouble(access.getDouble(object, accessIndex));
		}

		public void read (Input input, Object object) {
			if (TRACE) {
				double value = input.readDouble();
				access.setDouble(object, accessIndex, value);
				trace("kryo", "Read field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Read double: " + value);
			} else
				access.setDouble(object, accessIndex, input.readDouble());
		}

		public void copy (Object original, Object copy) {
			access.setDouble(copy, accessIndex, access.getDouble(original, accessIndex));
		}
	}

	class StringField extends AsmCachedField {
		public void write (Output output, Object object) {
			if (TRACE) {
				trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Write string: " + access.getString(object, accessIndex));
			}
			output.writeString(access.getString(object, accessIndex));
		}

		public void read (Input input, Object object) {
			if (TRACE) {
				String value = input.readString();
				access.set(object, accessIndex, value);
				trace("kryo", "Read field: " + this + " (" + object.getClass().getName() + ")");
				trace("kryo", "Read string: " + value);
			} else
				access.set(object, accessIndex, input.readString());
		}

		public void copy (Object original, Object copy) {
			access.set(copy, accessIndex, access.getString(original, accessIndex));
		}
	}

	class ObjectField extends CachedField {
		Class[] generics;

		public void write (Output output, Object object) {
			try {
				if (TRACE) trace("kryo", "Write field: " + this + " (" + object.getClass().getName() + ")");

				Object value;
				if (accessIndex != -1)
					value = ((FieldAccess)access).get(object, accessIndex);
				else
					value = field.get(object);

				Serializer serializer = this.serializer;
				if (valueClass == null) {
					// The concrete type of the field is unknown, write the class first.
					if (value == null) {
						kryo.writeClass(output, null);
						return;
					}
					Registration registration = kryo.writeClass(output, value.getClass());
					if (serializer == null) serializer = registration.getSerializer();
					if (generics != null) serializer.setGenerics(kryo, generics);
					kryo.writeObject(output, value, serializer);
				} else {
					// The concrete type of the field is known, always use the same serializer.
					if (serializer == null) this.serializer = serializer = kryo.getSerializer(valueClass);
					if (generics != null) serializer.setGenerics(kryo, generics);
					if (canBeNull) {
						kryo.writeObjectOrNull(output, value, serializer);
					} else {
						if (value == null) {
							throw new KryoException("Field value is null but canBeNull is false: " + this + " ("
								+ object.getClass().getName() + ")");
						}
						kryo.writeObject(output, value, serializer);
					}
				}
			} catch (IllegalAccessException ex) {
				throw new KryoException("Error accessing field: " + this + " (" + object.getClass().getName() + ")", ex);
			} catch (KryoException ex) {
				ex.addTrace(this + " (" + object.getClass().getName() + ")");
				throw ex;
			} catch (RuntimeException runtimeEx) {
				KryoException ex = new KryoException(runtimeEx);
				ex.addTrace(this + " (" + object.getClass().getName() + ")");
				throw ex;
			}
		}

		public void read (Input input, Object object) {
			try {
				if (TRACE) trace("kryo", "Read field: " + this + " (" + type.getName() + ")");
				Object value;

				Class concreteType = valueClass;
				Serializer serializer = this.serializer;
				if (concreteType == null) {
					Registration registration = kryo.readClass(input);
					if (registration == null)
						value = null;
					else {
						if (serializer == null) serializer = registration.getSerializer();
						if (generics != null) serializer.setGenerics(kryo, generics);
						value = kryo.readObject(input, registration.getType(), serializer);
					}
				} else {
					if (serializer == null) this.serializer = serializer = kryo.getSerializer(valueClass);
					if (generics != null) serializer.setGenerics(kryo, generics);
					if (canBeNull)
						value = kryo.readObjectOrNull(input, concreteType, serializer);
					else
						value = kryo.readObject(input, concreteType, serializer);
				}

				if (accessIndex != -1)
					((FieldAccess)access).set(object, accessIndex, value);
				else
					field.set(object, value);
			} catch (IllegalAccessException ex) {
				throw new KryoException("Error accessing field: " + this + " (" + type.getName() + ")", ex);
			} catch (KryoException ex) {
				ex.addTrace(this + " (" + type.getName() + ")");
				throw ex;
			} catch (RuntimeException runtimeEx) {
				KryoException ex = new KryoException(runtimeEx);
				ex.addTrace(this + " (" + type.getName() + ")");
				throw ex;
			}
		}

		public void copy (Object original, Object copy) {
			try {
				if (accessIndex != -1) {
					FieldAccess access = (FieldAccess)FieldSerializer.this.access;
					access.set(copy, accessIndex, kryo.copy(access.get(original, accessIndex)));
				} else
					field.set(copy, kryo.copy(field.get(original)));
			} catch (IllegalAccessException ex) {
				throw new KryoException("Error accessing field: " + this + " (" + type.getName() + ")", ex);
			} catch (KryoException ex) {
				ex.addTrace(this + " (" + type.getName() + ")");
				throw ex;
			} catch (RuntimeException runtimeEx) {
				KryoException ex = new KryoException(runtimeEx);
				ex.addTrace(this + " (" + type.getName() + ")");
				throw ex;
			}
		}
	}

	/** Indicates a field should be ignored when its declaring class is registered unless the {@link Kryo#getContext() context} has
	 * a value set for the specified key. This can be useful when a field must be serialized for one purpose, but not for another.
	 * Eg, a class for a networked application could have a field that should not be serialized and sent to clients, but should be
	 * serialized when stored on the server.
	 * @author Nathan Sweet <misc@n4te.com> */
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	static public @interface Optional {
		public String value();
	}
}
