/*
 * Copyright 2009 the original author or authors.
 *
 * 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.powermock.classloading;

import org.powermock.api.support.ClassLoaderUtil;
import org.powermock.api.support.SafeExceptionRethrower;
import org.powermock.classloading.spi.DeepClonerSPI;
import org.powermock.classloading.spi.DoNotClone;
import org.powermock.core.ListMap;
import org.powermock.reflect.Whitebox;
import sun.misc.Unsafe;

import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;

/**
 * The purpose of the deep cloner is to create a deep clone of an object. An
 * object can also be cloned to a different class-loader.
 * <p>
 */
public class DeepCloner implements DeepClonerSPI {

	private final ClassLoader targetCL;
	private final Map<Object, Object> referenceMap = new ListMap<Object, Object>();
	private final Class<DoNotClone> doNotClone;

	/**
	 * Clone using the supplied ClassLoader.
	 */
	public DeepCloner(ClassLoader classLoader) {
		this.targetCL = classLoader;
		doNotClone = getDoNotClone(targetCL);
	}

	/**
	 * Clone using the current ContextClassLoader.
	 */
	public DeepCloner() {
		this(Thread.currentThread().getContextClassLoader());
	}

	private Class<DoNotClone> getDoNotClone(ClassLoader targetCL) {
		return ClassLoaderUtil.loadClass(DoNotClone.class, targetCL);
	}

	/**
	 * Clones an object.
	 * 
	 * @return A deep clone of the object to clone.
	 */
	@Override
	public <T> T clone(T objectToClone) {
		return clone(objectToClone, true);
	}

	/**
	 * 
	 * @param includeStandardJavaType
	 *            <code>true</code> also clones standard java types (using
	 *            simple serialization), <code>false</code> simply reference to
	 *            these objects (will be same instance).
	 * @return A deep clone of the object to clone.
	 */
	public <T> T clone(T objectToClone, boolean includeStandardJavaType) {
		assertObjectNotNull(objectToClone);
		return performClone(ClassLoaderUtil.loadClass(getType(objectToClone), targetCL), objectToClone,
				includeStandardJavaType);
	}

	@SuppressWarnings("unchecked")
	private static <T> Class<T> getType(T object) {
		if (object == null) {
			return null;
		}
		return (Class<T>) (object instanceof Class ? object : object.getClass());
	}

	private static boolean isClass(Object object) {
		if (object == null) {
			return false;
		}
		return object instanceof Class<?>;
	}

	private static void assertObjectNotNull(Object object) {
		if (object == null) {
			throw new IllegalArgumentException("Object to clone cannot be null");
		}
	}

	@SuppressWarnings("unchecked")
	private <T> T performClone(Class<T> targetClass, Object source, boolean shouldCloneStandardJavaTypes) {
		Object target = null;
		if (targetClass.isArray() && !isClass(source)) {
			return (T) instantiateArray(targetCL, targetClass, source, shouldCloneStandardJavaTypes);
		} else if (isJavaReflectMethod(targetClass)) {
			return (T) cloneJavaReflectMethod(source);
		} else if (targetClass.isPrimitive() || isSunClass(targetClass) || isJavaReflectClass(targetClass)) {
			return (T) source;
		} else if (isSerializableCandidate(targetClass, source)) {
			return (T) serializationClone(source);
		} else if (targetClass.isEnum()) {
			return (T) cloneEnum(targetCL, source);
		} else if (isClass(source)) {
			return (T) ClassLoaderUtil.loadClass(getType(source), targetCL);
		} else {
			target = isClass(source) ? source : Whitebox.newInstance(targetClass);
		}

		if (target != null) {
			referenceMap.put(source, target);
			cloneFields(targetCL, targetClass, source, target, referenceMap, shouldCloneStandardJavaTypes);
		}
		return (T) target;
	}

	private Object cloneJavaReflectMethod(Object source) {
		Method sourceMethod = (Method) source;
		Class<?> declaringClass = sourceMethod.getDeclaringClass();
		Class<?> targetClassLoadedWithTargetCL = ClassLoaderUtil.loadClass(declaringClass, targetCL);
		Method targetMethod = null;
		try {
			targetMethod = targetClassLoadedWithTargetCL.getDeclaredMethod(sourceMethod.getName(),
					sourceMethod.getParameterTypes());
		} catch (Exception e) {
			SafeExceptionRethrower.safeRethrow(e);
		}
		if (sourceMethod.isAccessible()) {
			targetMethod.setAccessible(true);
		}
		return targetMethod;
	}

	private boolean isJavaReflectMethod(Class<?> cls) {
		return cls.getName().equals(Method.class.getName());
	}

	private boolean isSunClass(Class<?> cls) {
		return cls.getName().startsWith("sun.");
	}

	private boolean isJavaReflectClass(Class<?> cls) {
		return cls.getName().startsWith("java.lang.reflect");
	}

	private <T> boolean isSerializableCandidate(Class<T> targetClass, Object source) {
		return isStandardJavaType(targetClass)
				&& (isSerializable(targetClass) || isImpliticlySerializable(targetClass))
				&& !Map.class.isAssignableFrom(source.getClass())
				&& !Iterable.class.isAssignableFrom(source.getClass());
	}

	private static boolean isImpliticlySerializable(Class<?> cls) {
		return cls.isPrimitive();
	}

	private static boolean isSerializable(Class<?> cls) {
		return Serializable.class.isAssignableFrom(cls);
	}

	/*
	 * Perform simple serialization
	 */
	private Object serializationClone(Object source) {
		ObjectOutputStream oos = null;
		ObjectInputStream ois = null;
		try {
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			oos = new ObjectOutputStream(bos);
			oos.writeObject(source);
			oos.flush();
			ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
			ois = new ObjectInputStream(bin);
			return ois.readObject();
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			close(oos);
			close(ois);
		}
	}

	private void close(Closeable closeable) {
		try {
			if (closeable != null) {
				closeable.close();
			}
		} catch (IOException e) {
		}
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private Object cloneEnum(ClassLoader targetCL, Object source) {
		Object target;
		final Class enumClassLoadedByTargetCL = ClassLoaderUtil.loadClass(getType(source), targetCL);
		target = getEnumValue(source, enumClassLoadedByTargetCL);
		return target;
	}

	@SuppressWarnings("unchecked")
	private <T> void cloneFields(ClassLoader targetCL, Class<T> targetClass, Object source, Object target,
			Map<Object, Object> referenceMap, boolean cloneStandardJavaTypes) {
		Class<?> currentTargetClass = targetClass;
		while (currentTargetClass != null) {
			for (Field field : currentTargetClass.getDeclaredFields()) {
				if (field.getAnnotation(doNotClone) != null) {
					continue;
				}
				field.setAccessible(true);
				try {
					final Field declaredField = Whitebox.getField(getType(source), field.getName());
					declaredField.setAccessible(true);
					final Object object = declaredField.get(source);
					final Object instantiatedValue;
					if (object == source) {
						instantiatedValue = target;
					} else if (referenceMap.containsKey(object)) {
						instantiatedValue = referenceMap.get(object);
					} else {
						if (object == null && !isIterable(object)) {
							instantiatedValue = object;
						} else {
							Class<Object> type = getType(object);
							if (type.getName().equals("void")) {
								type = Class.class.cast(Class.class);
							}
							final Class<Object> typeLoadedByCL = ClassLoaderUtil.loadClass(type, targetCL
                            );
							if (type.isEnum()) {
								instantiatedValue = getEnumValue(object, typeLoadedByCL);
							} else {
								instantiatedValue = performClone(typeLoadedByCL, object, cloneStandardJavaTypes);
							}
						}
					}

					final boolean needsUnsafeWrite = field.isEnumConstant() || isStaticFinalModifier(field);
					if (needsUnsafeWrite) {
						UnsafeFieldWriter.write(field, target, instantiatedValue);
					} else {
						field.set(target, instantiatedValue);
					}
				} catch (RuntimeException e) {
					throw e;
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
			currentTargetClass = currentTargetClass.getSuperclass();
		}
	}

	private static <T> boolean isStandardJavaType(Class<T> targetClass) {
		return targetClass.getName().startsWith("java.");
	}

	private static boolean isStaticFinalModifier(final Field field) {
		final int modifiers = field.getModifiers();
		return Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers)
				|| field.getDeclaringClass().equals(Character.class) && field.getName().equals("MIN_RADIX");
	}

	private static boolean isIterable(final Object object) {
		return object != null && isIterable(object.getClass());
	}

	private static boolean isIterable(final Class<?> cls) {
		return Iterable.class.isAssignableFrom(cls);
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private static Enum getEnumValue(final Object enumValueOfSourceClassloader,
			final Class<Object> enumTypeLoadedByTargetCL) {
		return Enum.valueOf((Class) enumTypeLoadedByTargetCL, ((Enum) enumValueOfSourceClassloader).toString());
	}

	private Object instantiateArray(ClassLoader targetCL, Class<?> arrayClass, Object objectToClone,
                                    boolean cloneStandardJavaTypes) {
		final int arrayLength = Array.getLength(objectToClone);
		final Object array = Array.newInstance(arrayClass.getComponentType(), arrayLength);
		for (int i = 0; i < arrayLength; i++) {
			final Object object = Array.get(objectToClone, i);
			final Object performClone;
			if (object == null) {
				performClone = null;
			} else {
				performClone = performClone(ClassLoaderUtil.loadClass(getType(object), targetCL),
						object, cloneStandardJavaTypes);
			}
			Array.set(array, i, performClone);
		}
		return array;
	}

	/**
	 * Most of this code has been copied from the Sun14ReflectionProvider in the
	 * XStream project. Some changes has been made, namely if the field is
	 * static final then the {@link Unsafe#staticFieldOffset(Field)} method is
	 * used instead of {@link Unsafe#objectFieldOffset(Field)}.
	 * 
	 * @author Joe Walnes
	 * @author Brian Slesinsky
	 * @author Johan Haleby
	 */
	private static class UnsafeFieldWriter {
		private final static Unsafe unsafe;
		private final static Exception exception;
		static {
			Unsafe u = null;
			Exception ex = null;
			try {
				Class<?> objectStreamClass = Class.forName("sun.misc.Unsafe");
				Field unsafeField = objectStreamClass.getDeclaredField("theUnsafe");
				unsafeField.setAccessible(true);
				u = (Unsafe) unsafeField.get(null);
			} catch (Exception e) {
				ex = e;
			}
			exception = ex;
			unsafe = u;
		}

		public static void write(Field field, Object object, Object value) {
			if (exception != null) {
				throw new RuntimeException("Could not set field " + object.getClass() + "." + field.getName(),
						exception);
			}
			try {
				final long offset;
				if (DeepCloner.isStaticFinalModifier(field)) {
					offset = unsafe.staticFieldOffset(field);
				} else {
					offset = unsafe.objectFieldOffset(field);
				}
				Class<?> type = field.getType();
				if (type.isPrimitive()) {
					if (type.equals(Integer.TYPE)) {
						unsafe.putInt(object, offset, ((Integer) value).intValue());
					} else if (type.equals(Long.TYPE)) {
						unsafe.putLong(object, offset, ((Long) value).longValue());
					} else if (type.equals(Short.TYPE)) {
						unsafe.putShort(object, offset, ((Short) value).shortValue());
					} else if (type.equals(Character.TYPE)) {
						unsafe.putChar(object, offset, ((Character) value).charValue());
					} else if (type.equals(Byte.TYPE)) {
						unsafe.putByte(object, offset, ((Byte) value).byteValue());
					} else if (type.equals(Float.TYPE)) {
						unsafe.putFloat(object, offset, ((Float) value).floatValue());
					} else if (type.equals(Double.TYPE)) {
						unsafe.putDouble(object, offset, ((Double) value).doubleValue());
					} else if (type.equals(Boolean.TYPE)) {
						unsafe.putBoolean(object, offset, ((Boolean) value).booleanValue());
					} else {
						throw new RuntimeException("Could not set field " + object.getClass() + "." + field.getName()
								+ ": Unknown type " + type);
					}
				} else {
					unsafe.putObject(object, offset, value);
				}
			} catch (IllegalArgumentException e) {
				throw new RuntimeException("Could not set field " + object.getClass() + "." + field.getName(), e);
			}
		}
	}
}
