/*
 * Copyright 2010-2012 VMware and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springsource.loaded;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Able to read or write a particular field in a type. Knows nothing about the instance upon which the read/write may be getting
 * done.
 * 
 * @author Andy Clement
 * @since 0.5.0
 */
public class FieldReaderWriter {

	private static Logger log = Logger.getLogger(FieldReaderWriter.class.getName());

	/**
	 * The type descriptor for the type that defines the field we want to access
	 */
	protected TypeDescriptor typeDescriptor;

	protected FieldMember theField;

	public FieldReaderWriter(FieldMember theField, TypeDescriptor typeDescriptor) {
		this.theField = theField;
		this.typeDescriptor = typeDescriptor;
		assert theField.typename == typeDescriptor.typename;
	}

	protected FieldReaderWriter() {
	}

	/**
	 * Set the value of an instance field on the specified instance to the specified value. If a state manager is passed in things
	 * can be done in a more optimal way, otherwise the state manager has to be discovered from the instance.
	 * 
	 * @param instance the object instance upon which to set the field
	 * @param newValue the new value for that field
	 * @param stateManager the optional state manager for this instance, which will be looked up (expensive) if not passed in
	 * @throws IllegalAccessException if the field value cannot be set
	 */
	public void setValue(Object instance, Object newValue, ISMgr stateManager) throws IllegalAccessException {
		if (typeDescriptor.isReloadable()) {
			if (stateManager == null) {
				// Look it up using reflection
				stateManager = findInstanceStateManager(instance);
			}
			String declaringTypeName = typeDescriptor.getName();
			Map<String, Object> typeLevelValues = stateManager.getMap().get(declaringTypeName);
			if (typeLevelValues == null) {
				// first time we've accessed this type for an instance field
				typeLevelValues = new HashMap<String, Object>();
				stateManager.getMap().put(declaringTypeName, typeLevelValues);
			}
			typeLevelValues.put(theField.getName(), newValue);
		} else { // the type is not reloadable, must use reflection to access the value
			// TODO generate get/set in the topmost reloader for these kinds of field and use them?
			if (typeDescriptor.isInterface()) {
				// field resolution has left us with an interface field, those can't be set like this
				throw new IncompatibleClassChangeError("Expected non-static field " + instance.getClass().getName() + "."
						+ theField.getName());
			} else {
				findAndSetFieldValueInHierarchy(instance, newValue);
			}
		}
	}

	public void setStaticFieldValue(Class<?> clazz, Object newValue, SSMgr stateManager) throws IllegalAccessException {
		if (clazz == null) {
			throw new IllegalStateException();
		}
		// First decision - is the field part of a reloadable type or not?  The typeDescriptor here is the actual owner
		// of the field, at this point we *know* this class has this field.
		if (typeDescriptor.isReloadable()) {
			if (stateManager == null) {
				// need to go and find it, there *will* be one but it will be slow to retrieve (reflection)
				stateManager = findStaticStateManager(clazz);
			}
			String declaringTypeName = typeDescriptor.getName();
			Map<String, Object> typeLevelValues = stateManager.getMap().get(declaringTypeName);
			if (typeLevelValues == null) {
				typeLevelValues = new HashMap<String, Object>();
				stateManager.getMap().put(declaringTypeName, typeLevelValues);
			}
			typeLevelValues.put(theField.getName(), newValue);
		} else { // the type is not reloadable, must use reflection to access the value
			try {
				Field f = locateFieldByReflection(clazz, typeDescriptor.getDottedName(), typeDescriptor.isInterface(),
						theField.getName());
				f.setAccessible(true);
				f.set(null, newValue);
				// cant cache result - we dont control the sets so won't know it is happening anyway
			} catch (Exception e) {
				throw new IllegalStateException("Unexpectedly unable to reflectively set the field " + theField.getName()
						+ " on the type " + clazz.getName());
			}
		}
	}

	/**
	 * Return the value of the field for which is reader-writer exists. To improve performance a fieldAccessor can be supplied but
	 * if it is missing the code will go and discover it.
	 * 
	 * @param instance the instance for which the field should be fetched
	 * @param stateManager an optional state manager containing the map of values (will be discovered if not supplied)
	 * @return the value of the field
	 * @throws IllegalAccessException if there is a problem accessing the field value
	 */
	public Object getValue(Object instance, ISMgr stateManager) throws IllegalAccessException, IllegalArgumentException {
		Object result = null;
		String fieldname = theField.getName();
		if (typeDescriptor.isReloadable()) {
			if (stateManager == null) {
				// find it using reflection
				stateManager = findInstanceStateManager(instance);
			}
			String declaringTypeName = typeDescriptor.getName();
			Map<String, Object> typeLevelValues = stateManager.getMap().get(declaringTypeName);
			boolean knownField = false;
			if (typeLevelValues != null) {
				knownField = typeLevelValues.containsKey(fieldname);
			}
			if (knownField) {
				result = typeLevelValues.get(fieldname);
			}

			// If a field has been deleted it may 'reveal' a field in a supertype.  The revealed field may be in a type
			// not yet dealt with.  In this case typeLevelValues may be null (type not seen before) or the typelevelValues
			// may not have heard of our field name.  In these cases we need to go and find the field and 'relocate' it
			// into our map, where it will be processed from now on.
			if (typeLevelValues == null || !knownField) {

				FieldMember fieldOnOriginalType = typeDescriptor.getReloadableType().getTypeRegistry()
						.getReloadableType(declaringTypeName).getTypeDescriptor().getField(fieldname);

				if (fieldOnOriginalType != null) {
					// Copy the field into the map - that is where it will live from now on
					ReloadableType rt = typeDescriptor.getReloadableType();
					try {
						Field f = rt.getClazz().getDeclaredField(fieldname);
						f.setAccessible(true);
						result = f.get(instance);
						if (typeLevelValues == null) {
							typeLevelValues = new HashMap<String, Object>();
							stateManager.getMap().put(declaringTypeName, typeLevelValues);
						}
						typeLevelValues.put(fieldname, result);
					} catch (Exception e) {
						throw new IllegalStateException("Unexpectedly unable to access field " + fieldname + " on class "
								+ rt.getClazz(), e);
					}
				} else {
					// The field was not on the original type.  As not seen before, can default it
					result = Utils.toResultCheckIfNull(null, theField.getDescriptor());
					if (typeLevelValues == null) {
						typeLevelValues = new HashMap<String, Object>();
						stateManager.getMap().put(declaringTypeName, typeLevelValues);
					}
					typeLevelValues.put(fieldname, result);
					return result;
				}
			}
			if (result != null) {
				result = Utils.checkCompatibility(typeDescriptor.getTypeRegistry(), result, theField.getDescriptor());
				if (result == null) {
					// Was not compatible, forget it
					typeLevelValues.remove(fieldname);
				}
			}
			result = Utils.toResultCheckIfNull(result, theField.getDescriptor());
		} else {
			// the type is not reloadable, must use reflection to access the value.
			// TODO measure how often we hit the reflection path, should never happen unless reflection is already on the frame

			if (typeDescriptor.isInterface()) { // cant be an instance field if it is found to be on an interface
				throw new IncompatibleClassChangeError("Expected non-static field " + instance.getClass().getName() + "."
						+ fieldname);
			} else {
				result = findAndGetFieldValueInHierarchy(instance);
			}
		}

		if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.FINER)) {
			log.finer("<getValue() value of " + theField + " is " + result);
		}
		return result;
	}

	public Object getStaticFieldValue(Class<?> clazz, SSMgr stateManager) throws IllegalAccessException, IllegalArgumentException {
		Object result = null;
		if (clazz == null) {
			throw new IllegalStateException();
		}
		// First decision - is the field part of a reloadable type or not?  The typeDescriptor here is the actual owner
		// of the field, at this point we *know* this class has this field.
		if (typeDescriptor.isReloadable()) {
			if (stateManager == null) {
				// need to go and find it, there *will* be one but it will be slow to retrieve (reflection)
				stateManager = findStaticStateManager(clazz);
				if (stateManager == null) {
					return Utils.toResultCheckIfNull(null, theField.descriptor);
				}
			}
			String declaringTypeName = typeDescriptor.getName();
			Map<String, Object> typeLevelValues = stateManager.getMap().get(declaringTypeName);
			String fieldname = theField.getName();
			boolean knownField = false;
			if (typeLevelValues != null) {
				knownField = typeLevelValues.containsKey(fieldname);
			}
			if (knownField) {
				result = typeLevelValues.get(fieldname);
			}
			// If a field has been deleted it may 'reveal' a field in a supertype.  The revealed field may be in a type
			// not yet dealt with.  In this case typeLevelValues may be null (type not seen before) or the typelevelValues
			// may not have heard of our field name.  In these cases we need to go and find the field and 'relocate' it
			// into our map, where it will be processed from now on.

			// These revealed fields are not necessarily in the original form of the type so cannot always be accessed via reflection
			if (typeLevelValues == null || !knownField) {

				FieldMember fieldOnOriginalType = typeDescriptor.getReloadableType().getTypeRegistry()
						.getReloadableType(declaringTypeName).getTypeDescriptor().getField(fieldname);

				if (fieldOnOriginalType != null) { // && fieldOnOriginalType.isStatic()
					// can use reflection
					ReloadableType rt = typeDescriptor.getReloadableType();
					try {
						Field f = rt.getClazz().getDeclaredField(theField.getName());
						if (!Modifier.isStatic(f.getModifiers())) {
							// need to default it anyway, cant see that original value
							// TODO  this is a dup of the code below, refactor
							result = Utils.toResultCheckIfNull(null, theField.getDescriptor());
							if (typeLevelValues == null) {
								typeLevelValues = new HashMap<String, Object>();
								stateManager.getMap().put(declaringTypeName, typeLevelValues);
							}
							typeLevelValues.put(fieldname, result);
							return result;
						}
						f.setAccessible(true);
						// TODO can fail on this next line if the field we've found is non-static
						result = f.get(null);
						if (typeLevelValues == null) {
							typeLevelValues = new HashMap<String, Object>();
							stateManager.getMap().put(declaringTypeName, typeLevelValues);
						}
						typeLevelValues.put(theField.getName(), result);
					} catch (Exception e) {
						throw new IllegalStateException("Unexpectedly unable to read field " + theField.getName() + " on type "
								+ rt.getClazz(), e);
					}
				} else {
					// The field was not on the original type.  As not seen before, can default it
					result = Utils.toResultCheckIfNull(null, theField.getDescriptor());
					if (typeLevelValues == null) {
						typeLevelValues = new HashMap<String, Object>();
						stateManager.getMap().put(declaringTypeName, typeLevelValues);
					}
					typeLevelValues.put(fieldname, result);
					return result;
				}
			}
			// A problem that can occur is if a fields type is changed on a reload.  If the field
			// was previously written to we can then retrieve it and attempt to pass it back to the caller
			// and they'll get something unexpected.  
			if (result != null) {
				result = Utils.checkCompatibility(typeDescriptor.getTypeRegistry(), result, theField.getDescriptor());
				if (result == null) {
					typeLevelValues.remove(theField.getName());
				}
			}
			result = Utils.toResultCheckIfNull(result, theField.getDescriptor());
		} else { // the type is not reloadable, must use reflection to access the value
			// TODO measure how often this code gets hit - ensure it does not in the common (non reflective) case
			try {
				Field f = locateFieldByReflection(clazz, typeDescriptor.getDottedName(), typeDescriptor.isInterface(),
						theField.getName());
				f.setAccessible(true);
				result = f.get(null);
				// cant cache result - we dont control the sets so won't know it is happening anyway
			} catch (Exception e) {
				throw new IllegalStateException("Unexpectedly unable to set static field " + theField.getName() + " on type "
						+ typeDescriptor.getDottedName());
			}

		}

		if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.FINER)) {
			log.finer("<getValue() value of " + theField + " is " + result);
		}
		return result;
	}

	/**
	 * Discover the named field in the hierarchy using the standard rules of resolution.
	 * 
	 * @param clazz the class upon which to start looking
	 * @param typeWanted where the field is!
	 * @param name the name of the field to find
	 * @return the jlrField object representing that field, or null if not found
	 */
	private Field locateFieldByReflection(Class<?> clazz, String typeWanted, boolean isInterface, String name) {
		if (clazz.getName().equals(typeWanted)) {
			Field[] fs = clazz.getDeclaredFields();
			if (fs != null) {
				for (Field f : fs) {
					if (f.getName().equals(name)) {
						return f;
					}
				}
			}
		}
		// Check interfaces
		if (!isInterface) { // not worth looking!
			Class<?>[] interfaces = clazz.getInterfaces();
			if (interfaces != null) {
				for (Class<?> intface : interfaces) {
					Field f = locateFieldByReflection(intface, typeWanted, isInterface, name);
					if (f != null) {
						return f;
					}
				}
			}
		}
		// Check superclass
		Class<?> superclass = clazz.getSuperclass();
		if (superclass == null) {
			return null;
		} else {
			return locateFieldByReflection(superclass, typeWanted, isInterface, name);
		}
	}

	/**
	 * Discover the instance state manager for the specific object instance. Will fail by exception rather than returning null.
	 * 
	 * @param instance the object instance on which to look
	 * @return the discovered state manager
	 */
	private ISMgr findInstanceStateManager(Object instance) {
		Class<?> clazz = typeDescriptor.getReloadableType().getClazz();
		try {
			Field fieldAccessorField = clazz.getField(Constants.fInstanceFieldsName);
			if (fieldAccessorField == null) {
				throw new IllegalStateException("Cant find field accessor for type " + clazz.getName());
			}
			ISMgr stateManager = (ISMgr) fieldAccessorField.get(instance);
			if (stateManager == null) {
				// Looks to not have been initialized yet, this can happen if a non standard ctor was used.
				// We could push this step into the generated ctors...
				ISMgr instanceStateManager = new ISMgr(instance, typeDescriptor.getReloadableType());
				fieldAccessorField.set(instance,instanceStateManager);
				stateManager = (ISMgr) fieldAccessorField.get(instance);
				// For some reason it didn't stick!
				if (stateManager == null) {
					throw new IllegalStateException("The class '" + clazz.getName()
							+ "' has a null instance state manager object, instance is " + instance);
				}
			}
			return stateManager;
		} catch (Exception e) {
			throw new IllegalStateException("Unexpectedly unable to find instance state manager on class " + clazz.getName(), e);
		}
	}

	/**
	 * Discover the static state manager on the specified class and return it. Will fail by exception rather than returning null.
	 * 
	 * @param clazz the class on which to look
	 * @return the discovered state manager
	 */
	private SSMgr findStaticStateManager(Class<?> clazz) {
		try {
			Field stateManagerField = clazz.getField(Constants.fStaticFieldsName);
			if (stateManagerField == null) {
				throw new IllegalStateException("Cant find field accessor for type " + typeDescriptor.getReloadableType().getName());
			}
			SSMgr stateManager = (SSMgr) stateManagerField.get(null);
			// Field should always have been initialized - it is done at the start of the top most reloadable type <clinit>
			if (stateManager == null) {
				throw new IllegalStateException("Instance of this class has no state manager: " + clazz.getName());
			}
			return stateManager;
		} catch (Exception e) {
			throw new IllegalStateException("Unexpectedly unable to find static state manager on class " + clazz.getName(), e);
		}
	}

	public boolean isStatic() {
		return theField.isStatic();
	}

	/**
	 * Walk up the instance hierarchy looking for the field, and when it is found access it and return the result. Will exit via
	 * exception if it cannot find the field or something goes wrong when accessing it.
	 * 
	 * @param instance the object instance upon which the field is being accessed
	 * @return the value of the field
	 */
	private Object findAndGetFieldValueInHierarchy(Object instance) {
		Class<?> clazz = instance.getClass();
		String fieldname = theField.getName();
		String searchName = typeDescriptor.getName().replace('/', '.');
		while (clazz != null && !clazz.getName().equals(searchName)) {
			clazz = clazz.getSuperclass();
		}
		if (clazz == null) {
			throw new IllegalStateException("Failed to find " + searchName + " in hierarchy of " + instance.getClass());
		}
		try {
			Field f = clazz.getDeclaredField(fieldname);
			f.setAccessible(true);
			return f.get(instance);
		} catch (Exception e) {
			throw new IllegalStateException("Unexpectedly could not access field named " + fieldname + " on class "
					+ clazz.getName());
		}
	}

	/**
	 * Walk up the instance hierarchy looking for the field, and when it is found set it. Will exit via exception if it cannot find
	 * the field or something goes wrong when accessing it.
	 * 
	 * @param instance the object instance upon which the field is being set
	 * @param newValue the new value for the field
	 */
	private void findAndSetFieldValueInHierarchy(Object instance, Object newValue) {
		Class<?> clazz = instance.getClass();
		String fieldname = theField.getName();
		String searchName = typeDescriptor.getName().replace('/', '.');
		while (clazz != null && !clazz.getName().equals(searchName)) {
			clazz = clazz.getSuperclass();
		}
		if (clazz == null) {
			throw new IllegalStateException("Failed to find " + searchName + " in hierarchy of " + instance.getClass());
		}
		try {
			Field f = clazz.getDeclaredField(fieldname);
			f.setAccessible(true);
			f.set(instance, newValue);
		} catch (Exception e) {
			throw new IllegalStateException("Unexpectedly could not access field named " + fieldname + " on class "
					+ clazz.getName());
		}

	}
}