//$Id: PersistentMap.java,v 1.26 2005/08/01 16:29:30 oneovthafew Exp $
package org.hibernate.collection;

import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.loader.CollectionAliases;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.type.Type;


/**
 * A persistent wrapper for a <tt>java.util.Map</tt>. Underlying collection
 * is a <tt>HashMap</tt>.
 *
 * @see java.util.HashMap
 * @author Gavin King
 */
public class PersistentMap extends AbstractPersistentCollection implements Map {

	protected Map map;

	public Serializable getSnapshot(CollectionPersister persister) 
	throws HibernateException {
		EntityMode entityMode = getSession().getEntityMode();
		
		HashMap clonedMap = new HashMap( map.size() );
		Iterator iter = map.entrySet().iterator();
		while ( iter.hasNext() ) {
			Map.Entry e = (Map.Entry) iter.next();
			final Object copy = persister.getElementType()
				.deepCopy( e.getValue(), entityMode, persister.getFactory() );
			clonedMap.put( e.getKey(), copy );
		}
		return clonedMap;
	}

	public Collection getOrphans(Serializable snapshot, String entityName) throws HibernateException {
		Map sn = (Map) snapshot;
		return getOrphans( sn.values(), map.values(), entityName, getSession() );
	}

	public boolean equalsSnapshot(CollectionPersister persister) throws HibernateException {
		Type elementType = persister.getElementType();
		Map xmap = (Map) getSnapshot();
		if ( xmap.size()!=this.map.size() ) return false;
		Iterator iter = map.entrySet().iterator();
		while ( iter.hasNext() ) {
			Map.Entry entry = (Map.Entry) iter.next();
			if ( elementType.isDirty( entry.getValue(), xmap.get( entry.getKey() ), getSession() ) ) return false;
		}
		return true;
	}

	public boolean isSnapshotEmpty(Serializable snapshot) {
		return ( (Map) snapshot ).isEmpty();
	}

	public boolean isWrapper(Object collection) {
		return map==collection;
	}
	
	public PersistentMap(SessionImplementor session) {
		super(session);
	}

	public PersistentMap() {} //needed for SOAP libraries, etc

	public void beforeInitialize(CollectionPersister persister) {
		this.map = (Map) persister.getCollectionType().instantiate();
	}

	public PersistentMap(SessionImplementor session, Map map) {
		super(session);
		this.map = map;
		setInitialized();
		setDirectlyAccessible(true);
	}

	/**
	 * @see java.util.Map#size()
	 */
	public int size() {
		return readSize() ? getCachedSize() : map.size();
	}

	/**
	 * @see java.util.Map#isEmpty()
	 */
	public boolean isEmpty() {
		return readSize() ? getCachedSize()==0 : map.isEmpty();
	}

	/**
	 * @see java.util.Map#containsKey(Object)
	 */
	public boolean containsKey(Object key) {
		Boolean exists = readIndexExistence(key);
		return exists==null ? map.containsKey(key) : exists.booleanValue();
	}

	/**
	 * @see java.util.Map#containsValue(Object)
	 */
	public boolean containsValue(Object value) {
		Boolean exists = readElementExistence(value);
		return exists==null ? 
				map.containsValue(value) : 
				exists.booleanValue();
	}

	/**
	 * @see java.util.Map#get(Object)
	 */
	public Object get(Object key) {
		Object result = readElementByIndex(key);
		return result==UNKNOWN ? map.get(key) : result;
	}

	/**
	 * @see java.util.Map#put(Object, Object)
	 */
	public Object put(Object key, Object value) {
		Object old = isPutQueueEnabled() ?
				readElementByIndex(key) : UNKNOWN;
		if ( old==UNKNOWN ) {
			write();
			return map.put(key, value);
		}
		else {
			queueOperation( new Put(key, value, old) );
			return old;
		}
	}

	/**
	 * @see java.util.Map#remove(Object)
	 */
	public Object remove(Object key) {
		Object old = isPutQueueEnabled() ?
				readElementByIndex(key) : UNKNOWN;
		if ( old==UNKNOWN ) {
			write();
			return map.remove(key);
		}
		else {
			queueOperation( new Remove(key, old) );
			return old;
		}
	}

	/**
	 * @see java.util.Map#putAll(java.util.Map puts)
	 */
	public void putAll(Map puts) {
		if ( puts.size()>0 ) {
			write();
			map.putAll(puts);
		}
	}

	/**
	 * @see java.util.Map#clear()
	 */
	public void clear() {
		if ( isClearQueueEnabled() ) {
			queueOperation( new Clear() );
		}
		else {
			write();
			map.clear();
		}
	}

	/**
	 * @see java.util.Map#keySet()
	 */
	public Set keySet() {
		read();
		return new SetProxy( map.keySet() );
	}

	/**
	 * @see java.util.Map#values()
	 */
	public Collection values() {
		read();
		return new SetProxy( map.values() );
	}

	/**
	 * @see java.util.Map#entrySet()
	 */
	public Set entrySet() {
		read();
		return new EntrySetProxy( map.entrySet() );
	}

	public boolean empty() {
		return map.isEmpty();
	}

	public String toString() {
		read();
		return map.toString();
	}

	public Object readFrom(ResultSet rs, CollectionPersister persister, CollectionAliases descriptor, Object owner)
	throws HibernateException, SQLException {
		Object element = persister.readElement( rs, owner, descriptor.getSuffixedElementAliases(), getSession() );
		Object index = persister.readIndex( rs, descriptor.getSuffixedIndexAliases(), getSession() );
		if ( element!=null ) map.put(index, element);
		return element;
	}

	public Iterator entries(CollectionPersister persister) {
		return map.entrySet().iterator();
	}

	/** a wrapper for Map.Entry sets */
	class EntrySetProxy implements Set {
		private final Set set;
		EntrySetProxy(Set set) {
			this.set=set;
		}
		public boolean add(Object entry) {
			//write(); -- doesn't
			return set.add(entry);
		}
		public boolean addAll(Collection entries) {
			//write(); -- doesn't
			return set.addAll(entries);
		}
		public void clear() {
			write();
			set.clear();
		}
		public boolean contains(Object entry) {
			return set.contains(entry);
		}
		public boolean containsAll(Collection entries) {
			return set.containsAll(entries);
		}
		public boolean isEmpty() {
			return set.isEmpty();
		}
		public Iterator iterator() {
			return new EntryIteratorProxy( set.iterator() );
		}
		public boolean remove(Object entry) {
			write();
			return set.remove(entry);
		}
		public boolean removeAll(Collection entries) {
			write();
			return set.removeAll(entries);
		}
		public boolean retainAll(Collection entries) {
			write();
			return set.retainAll(entries);
		}
		public int size() {
			return set.size();
		}
		// amazingly, these two will work because AbstractCollection
		// uses iterator() to fill the array
		public Object[] toArray() {
			return set.toArray();
		}
		public Object[] toArray(Object[] array) {
			return set.toArray(array);
		}
	}
	final class EntryIteratorProxy implements Iterator {
		private final Iterator iter;
		EntryIteratorProxy(Iterator iter) {
			this.iter=iter;
		}
		public boolean hasNext() {
			return iter.hasNext();
		}
		public Object next() {
			return new MapEntryProxy( (Map.Entry) iter.next() );
		}
		public void remove() {
			write();
			iter.remove();
		}
	}

	final class MapEntryProxy implements Map.Entry {
		private final Map.Entry me;
		MapEntryProxy( Map.Entry me ) {
			this.me = me;
		}
		public Object getKey() { return me.getKey(); }
		public Object getValue() { return me.getValue(); }
		public boolean equals(Object o) { return me.equals(o); }
		public int hashCode() { return me.hashCode(); }
		// finally, what it's all about...
		public Object setValue(Object value) {
			write();
			return me.setValue(value);
		}
	}

	public void initializeFromCache(CollectionPersister persister, Serializable disassembled, Object owner)
	throws HibernateException {
		beforeInitialize(persister);
		Serializable[] array = (Serializable[]) disassembled;
		for (int i=0; i<array.length; i+=2 ) {
			map.put(
					persister.getIndexType().assemble( array[i], getSession(), owner ),
					persister.getElementType().assemble( array[i+1], getSession(), owner )
				);
		}
	}

	public Serializable disassemble(CollectionPersister persister) throws HibernateException {

		Serializable[] result = new Serializable[ map.size() * 2 ];
		Iterator iter = map.entrySet().iterator();
		int i=0;
		while ( iter.hasNext() ) {
			Map.Entry e = (Map.Entry) iter.next();
			result[i++] = persister.getIndexType().disassemble( e.getKey(), getSession(), null );
			result[i++] = persister.getElementType().disassemble( e.getValue(), getSession(), null );
		}
		return result;

	}

	public Iterator getDeletes(CollectionPersister persister, boolean indexIsFormula) 
	throws HibernateException {
		List deletes = new ArrayList();
		Iterator iter = ( (Map) getSnapshot() ).entrySet().iterator();
		while ( iter.hasNext() ) {
			Map.Entry e = (Map.Entry) iter.next();
			Object key = e.getKey();
			if ( e.getValue()!=null && map.get(key)==null ) {
				deletes.add( indexIsFormula ? e.getValue() : key );
			}
		}
		return deletes.iterator();
	}

	public boolean needsInserting(Object entry, int i, Type elemType) 
	throws HibernateException {
		final Map sn = (Map) getSnapshot();
		Map.Entry e = (Map.Entry) entry;
		return e.getValue()!=null && sn.get( e.getKey() )==null;
	}

	public boolean needsUpdating(Object entry, int i, Type elemType) 
	throws HibernateException {
		final Map sn = (Map) getSnapshot();
		Map.Entry e = (Map.Entry) entry;
		Object snValue = sn.get( e.getKey() );
		return e.getValue()!=null &&
			snValue!=null &&
			elemType.isDirty( snValue, e.getValue(), getSession() );
	}


	public Object getIndex(Object entry, int i, CollectionPersister persister) {
		return ( (Map.Entry) entry ).getKey();
	}

	public Object getElement(Object entry) {
		return ( (Map.Entry) entry ).getValue();
	}

	public Object getSnapshotElement(Object entry, int i) {
		final Map sn = (Map) getSnapshot();
		return sn.get( ( (Map.Entry) entry ).getKey() );
	}

	public boolean equals(Object other) {
		read();
		return map.equals(other);
	}

	public int hashCode() {
		read();
		return map.hashCode();
	}

	public boolean entryExists(Object entry, int i) {
		return ( (Map.Entry) entry ).getValue()!=null;
	}

	final class Clear implements DelayedOperation {
		public void operate() {
			map.clear();
		}
		public Object getAddedInstance() {
			return null;
		}
		public Object getOrphan() {
			throw new UnsupportedOperationException("queued clear cannot be used with orphan delete");
		}
	}

	final class Put implements DelayedOperation {
		private Object index;
		private Object value;
		private Object old;
		
		public Put(Object index, Object value, Object old) {
			this.index = index;
			this.value = value;
			this.old = old;
		}
		public void operate() {
			map.put(index, value);
		}
		public Object getAddedInstance() {
			return value;
		}
		public Object getOrphan() {
			return old;
		}
	}

	final class Remove implements DelayedOperation {
		private Object index;
		private Object old;
		
		public Remove(Object index, Object old) {
			this.index = index;
			this.old = old;
		}
		public void operate() {
			map.remove(index);
		}
		public Object getAddedInstance() {
			return null;
		}
		public Object getOrphan() {
			return old;
		}
	}
}
