/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.wicket.core.util.lang;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.HashMap;

import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.application.IClassResolver;
import org.apache.wicket.model.IDetachable;
import org.apache.wicket.serialize.ISerializer;
import org.apache.wicket.serialize.java.JavaSerializer;
import org.apache.wicket.settings.ApplicationSettings;
import org.apache.wicket.util.io.ByteCountingOutputStream;
import org.apache.wicket.util.lang.Generics;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Object (de)serialization utilities.
 */
public class WicketObjects
{
	/** log. */
	private static final Logger log = LoggerFactory.getLogger(WicketObjects.class);

	private WicketObjects()
	{
	}

	/**
	 * @param <T>
	 *            class type
	 * @param className
	 *            Class to resolve
	 * @return Resolved class
	 */
	@SuppressWarnings("unchecked")
	public static <T> Class<T> resolveClass(final String className)
	{
		Class<T> resolved = null;
		try
		{
			if (Application.exists())
			{
				resolved = (Class<T>)Application.get()
					.getApplicationSettings()
					.getClassResolver()
					.resolveClass(className);
			}

			if (resolved == null)
			{
				resolved = (Class<T>)Class.forName(className, false, Thread.currentThread()
					.getContextClassLoader());
			}
		}
		catch (ClassNotFoundException cnfx)
		{
			log.warn("Could not resolve class [" + className + "]", cnfx);
		}
		return resolved;
	}

	/**
	 * Interface that enables users to plugin the way object sizes are calculated with Wicket.
	 */
	public static interface IObjectSizeOfStrategy
	{
		/**
		 * Computes the size of an object. This typically is an estimation, not an absolute accurate
		 * size.
		 *
		 * @param object
		 *            The serializable object to compute size of
		 * @return The size of the object in bytes.
		 */
		long sizeOf(Serializable object);
	}

	/**
	 * {@link IObjectSizeOfStrategy} that works by serializing the object to an instance of
	 * {@link ByteCountingOutputStream}, which records the number of bytes written to it. Hence,
	 * this gives the size of the object as it would be serialized,including all the overhead of
	 * writing class headers etc. Not very accurate (the real memory consumption should be lower)
	 * but the best we can do in a cheap way pre JDK 5.
	 */
	public static final class SerializingObjectSizeOfStrategy implements IObjectSizeOfStrategy
	{
		@Override
		public long sizeOf(Serializable object)
		{
			if (object == null)
			{
				return 0;
			}

			ISerializer serializer = null;
			if (Application.exists())
			{
				serializer = Application.get().getFrameworkSettings().getSerializer();
			}

			if (serializer == null || serializer instanceof JavaSerializer)
			{
				// WICKET-6334 create a new instance of JavaSerializer that doesn't use custom IObjectCheckers
				serializer = new JavaSerializer(SerializingObjectSizeOfStrategy.class.getName());
			}

			byte[] serialized = serializer.serialize(object);
			int size = -1;
			if (serialized != null)
			{
				size = serialized.length;
			}
			return size;
		}

	}

	private static final class ReplaceObjectInputStream extends ObjectInputStream
	{
		private final ClassLoader classloader;
		private final HashMap<String, Component> replacedComponents;

		private ReplaceObjectInputStream(InputStream in,
			HashMap<String, Component> replacedComponents, ClassLoader classloader)
			throws IOException
		{
			super(in);
			this.replacedComponents = replacedComponents;
			this.classloader = classloader;
			enableResolveObject(true);
		}

		// This override is required to resolve classes inside in different
		// bundle, i.e.
		// The classes can be resolved by OSGI classresolver implementation
		@Override
		protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
			ClassNotFoundException
		{
			String className = desc.getName();

			try
			{
				return Class.forName(className, true, classloader);
			}
			catch (ClassNotFoundException ex1)
			{
				// ignore this exception.
				log.debug("Class not found by using objects own classloader, trying the IClassResolver");
			}

			Application application = Application.get();
			ApplicationSettings applicationSettings = application.getApplicationSettings();
			IClassResolver classResolver = applicationSettings.getClassResolver();

			Class<?> candidate = null;
			try
			{
				candidate = classResolver.resolveClass(className);
				if (candidate == null)
				{
					candidate = super.resolveClass(desc);
				}
			}
			catch (WicketRuntimeException ex)
			{
				if (ex.getCause() instanceof ClassNotFoundException)
				{
					throw (ClassNotFoundException)ex.getCause();
				}
			}
			return candidate;
		}

		@Override
		protected Object resolveObject(Object obj) throws IOException
		{
			Object replaced = replacedComponents.get(obj);
			if (replaced != null)
			{
				return replaced;
			}
			return super.resolveObject(obj);
		}
	}

	private static final class ReplaceObjectOutputStream extends ObjectOutputStream
	{
		private final HashMap<String, Component> replacedComponents;

		private ReplaceObjectOutputStream(OutputStream out,
			HashMap<String, Component> replacedComponents) throws IOException
		{
			super(out);
			this.replacedComponents = replacedComponents;
			enableReplaceObject(true);
		}

		@Override
		protected Object replaceObject(Object obj) throws IOException
		{
			if (obj instanceof Component)
			{
				final Component component = (Component)obj;
				String name = component.getPath();
				replacedComponents.put(name, component);
				return name;
			}
			return super.replaceObject(obj);
		}
	}

	/**
	 * Makes a deep clone of an object by serializing and deserializing it. The object must be fully
	 * serializable to be cloned. This method will not clone wicket Components, it will just reuse
	 * those instances so that the complete component tree is not copied over only the model data.
	 *
	 * <strong>Warning</strong>: this method uses Java Serialization APIs to be able to avoid cloning
	 * of {@link org.apache.wicket.Component} instances. If the application uses custom
	 * {@link org.apache.wicket.serialize.ISerializer} then most probably this method cannot be used.
	 *
	 * @param object
	 *            The object to clone
	 * @return A deep copy of the object
	 * @deprecated Use {@linkplain #cloneObject(Object)} instead
	 */
	@Deprecated
	public static <T> T cloneModel(final T object)
	{
		if (object == null)
		{
			return null;
		}
		else
		{
			try
			{
				final ByteArrayOutputStream out = new ByteArrayOutputStream(256);
				final HashMap<String, Component> replacedObjects = Generics.newHashMap();
				ObjectOutputStream oos = new ReplaceObjectOutputStream(out, replacedObjects);
				oos.writeObject(object);
				ObjectInputStream ois = new ReplaceObjectInputStream(new ByteArrayInputStream(
					out.toByteArray()), replacedObjects, object.getClass().getClassLoader());
				return (T) ois.readObject();
			}
			catch (ClassNotFoundException | IOException e)
			{
				throw new WicketRuntimeException("Internal error cloning object", e);
			}
		}
	}

	/**
	 * Strategy for calculating sizes of objects. Note: I didn't make this an application setting as
	 * we have enough of those already, and the typical way this probably would be used is that
	 * install a different one according to the JDK version used, so varying them between
	 * applications doesn't make a lot of sense.
	 */
	private static IObjectSizeOfStrategy objectSizeOfStrategy = new SerializingObjectSizeOfStrategy();

	/**
	 * Makes a deep clone of an object by serializing and deserializing it. The object must be fully
	 * serializable to be cloned. No extra debug info is gathered.
	 *
	 * @param object
	 *            The object to clone
	 * @return A deep copy of the object
	 * @see #cloneModel(Object)
	 */
	public static <T> T cloneObject(final T object)
	{
		if (object == null)
		{
			return null;
		}
		else
		{
			ISerializer serializer = null;
			if (Application.exists())
			{
				serializer = Application.get().getFrameworkSettings().getSerializer();
			}

			if (serializer == null || serializer instanceof JavaSerializer)
			{
				// WICKET-6334 create a new instance of JavaSerializer that doesn't use custom IObjectCheckers
				serializer = new JavaSerializer(SerializingObjectSizeOfStrategy.class.getName());
			}

			byte[] serialized = serializer.serialize(object);
			if (serialized == null)
			{
				throw new IllegalStateException("A problem occurred while serializing an object. " +
						"Please check the earlier logs for more details. Problematic object: " + object);
			}
			Object deserialized = serializer.deserialize(serialized);
			return (T) deserialized;
		}
	}

	/**
	 * Creates a new instance using the current application's class resolver. Returns null if
	 * className is null.
	 *
	 * @param className
	 *            The full class name
	 * @return The new object instance
	 */
	public static <T> T newInstance(final String className)
	{
		if (!Strings.isEmpty(className))
		{
			try
			{
				Class<?> c = WicketObjects.resolveClass(className);
				return (T) c.newInstance();
			}
			catch (Exception e)
			{
				throw new WicketRuntimeException("Unable to create " + className, e);
			}
		}
		return null;
	}

	/**
	 * Sets the strategy for determining the sizes of objects.
	 *
	 * @param objectSizeOfStrategy
	 *            the strategy. Pass null to reset to the default.
	 */
	public static void setObjectSizeOfStrategy(IObjectSizeOfStrategy objectSizeOfStrategy)
	{
		if (objectSizeOfStrategy == null)
		{
			WicketObjects.objectSizeOfStrategy = new SerializingObjectSizeOfStrategy();
		}
		else
		{
			WicketObjects.objectSizeOfStrategy = objectSizeOfStrategy;
		}
		log.info("using " + objectSizeOfStrategy + " for calculating object sizes");
	}

	/**
	 * Computes the size of an object. Note that this is an estimation, never an absolute accurate
	 * size.
	 *
	 * @param object
	 *            Object to compute size of
	 * @return The size of the object in bytes
	 */
	public static long sizeof(final Serializable object)
	{
		Serializable target = object;

		if (object instanceof Component)
		{
			// clone to not detach the original component (WICKET-5013, 5014)
			Component clone = (Component) cloneObject(object);
			clone.detach();

			target = clone;
		}
		else if (object instanceof IDetachable)
		{
			// clone to not detach the original IDetachable (WICKET-5013, 5014)
			IDetachable clone = (IDetachable) cloneObject(object);
			clone.detach();

			target = clone;
		}

		return objectSizeOfStrategy.sizeOf(target);
	}
}
