import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.caucho.hessian.util.IdentityIntMap;


/**
 * Demonstrates the bug in HessianIdentityInMap.
 * @author matthias.meier@abraxas.ch
 */
public final class HessianIdentityIntMapBug {

	// ---- Main Method

	public static void main (final String[] args) {
		// Create some strings to represent objects which are getting "(de)serialized":
		final String[] objects = new String[16];

		// The first few objects are "new":
		for (int i = 0; i < 12; i++) {
			objects[i] = String.format("Object-%02d", Integer.valueOf(i));
		}

		// In the end, we repeat some objects which already have been "(de)serialized" before:
		objects[12] = objects[6];
		objects[13] = objects[9];
		objects[14] = objects[2];
		objects[15] = objects[8];

		// Some of the objects get "replaced" during serialization:
		final Set<Integer> indizesOfReplacedObjects = new HashSet<Integer>();
		indizesOfReplacedObjects.add(Integer.valueOf(3));
		indizesOfReplacedObjects.add(Integer.valueOf(5));

		// Simulate the serialization process:
		simulateSerialization(objects, indizesOfReplacedObjects);
	}

	// ---- Serialization

	public static void simulateSerialization (final String[] objects, final Set<Integer> indizesOfReplacedObjects) {
		System.out.println("==== Serializing Objects ... ====");

		final IdentityIntMap refs = new IdentityIntMap(8);
		for (int i = 0; i < objects.length; i++) {
			// Simulate "serializing" of 'objects[i]':

			// Note: The following does more or less whats implemented in writeObject(Object) in com.caucho.hessian.io.AbstractSerializer ...

			if (addRef(refs, objects[i])) {
				// If the object has already been serialized before, 'addRef(...)' will write the reference and we
				// continue with the next object to serialize:
				continue;
			}

			// Simulate replacement of some objects:
			if (indizesOfReplacedObjects.contains(Integer.valueOf(i))) {
				// ... Here the replacement would be written to the Hessian2Output ...
				// Then the reference is replaced in the 'refs' map:
				final Object newRef = "Replacement for [" + objects[i] + "]";
				replaceRef(refs, objects[i], newRef);
				// Continue with the next object to serialize:
				System.out.println("[" + objects[i] + "]: has been replaced with: " + newRef);
				continue;
			}

			// ... Here the object to serialize would actually be written to the stream ...
			System.out.println("[" + objects[i] + "]: has been written to the output stream");
		}
		System.out.println();

		// Print the results:
		printMap(refs);
		System.out.println();
	}

	/**
	 * This more or less copied is from addRef(Object) in com.caucho.hessian.io.Hessian2Output.
	 */
	public static boolean addRef (final IdentityIntMap refs, final Object object) {
		int newRef = refs.size();
		int ref = refs.put(object, newRef, false);
		if (ref != newRef) {
			System.out.println("[" + object + "]: has already been serialized; writing reference to #" + ref
				+ " instead of serializing again");
			return true;
		} else {
			return false;
		}
	}

	/**
	 * This more or less copied is from replaceRef(Object,Object) in com.caucho.hessian.io.Hessian2Output.
	 */
	public static boolean replaceRef (final IdentityIntMap refs, final Object oldRef, final Object newRef) {
		int value = refs.get(oldRef);
		if (value >= 0) {
			refs.put(newRef, value, true);
			refs.remove(oldRef);
			return true;
		} else {
			return false;
		}
	}

	// ---- Helper Methods

	/**
	 * This prints the actual contents of the IdentityIntMap.
	 */
	public static void printMap (final IdentityIntMap map) {
		try {
			// get all entries and sort them:
			final List<MapEntry> entries = new ArrayList<MapEntry>();
			final Field f = map.getClass().getDeclaredField("_keys");
			f.setAccessible(true);
			final Object[] keys = (Object[]) f.get(map);
			for (final Object k : keys) {
				if (k != null) {
					entries.add(new MapEntry(k, map.get(k)));
				}
			}
			Collections.sort(entries);

			// print the entries:
			System.out.println("==== IdentityIntMap contents: ====");
			for (final MapEntry e : entries) {
				System.out.println(e.toString());
			}
		} catch (final Exception exc) {
			System.out.println("Uuuups ...");
			exc.printStackTrace(System.out);
		}
	}

	/**
	 * Helper class to sort the map entries according to value.
	 */
	private static final class MapEntry implements Comparable<MapEntry> {

		private final int value;

		private final Object key;

		private MapEntry (final Object k, final int v) {
			value = v;
			key = k;
		}

		public int compareTo (final MapEntry o) {
			if (value < o.value) {
				return -1;
			} else if (value > o.value) {
				return 1;
			} else {
				return key.toString().compareTo(o.key.toString());
			}
		}

		@Override
		public String toString () {
			return String.format("%02d: %s", Integer.valueOf(value), key.toString());
		}
	}

}
