/*
 * Copyright 2005 Ralf Joachim, Gregory Bock, Werner Guttmann
 *
 * 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.castor.persist;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.castor.persist.proxy.LazyCGLIB;
import org.castor.util.IdentityMap;
import org.castor.util.IdentitySet;
import org.castor.util.Messages;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.persist.ClassMolder;
import org.exolab.castor.persist.LockEngine;
import org.exolab.castor.persist.OID;
import org.exolab.castor.persist.spi.Identity;

/**
 * A transaction records all objects accessed during the lifetime
 * of the transaction in this record (queries and created).
 * 
 * This information, stored on a per-object basis within the
 * ObjectTracker, covers the database engine used
 * to persist the object, the object's OID, the object itself, and
 * whether the object has been deleted in this transaction,
 * created in this transaction. 
 * 
 * Sidenote: Objects identified as read only are not updated 
 * when the transaction commits.
 * 
 * @author <a href="mailto: ralf DOT joachim AT syscon-world DOT de">Ralf Joachim</a>
 * @author <a href="mailto:werner DOT guttmann AT gmx DOT net">Werner Guttmann</a>
 * @author <a href="mailto:gblock AT ctoforaday DOT COM">Gregory Block</a>
 * @version $Revision: 5980 $ $Date: 2006-04-29 09:15:50 -0600 (Sat, 29 Apr 2006) $
 * @since 0.9.9
 */
public final class ObjectTracker {
    //--------------------------------------------------------------------------

    /** Map of Object->ClassMolder. */
    private final Map _objectToMolder = new IdentityMap();
    
    /** Map of Engine -> OID -> Object. */
    private final Map _engineToOIDToObject = new HashMap();
    
    /** Map of Object->OID. */
    private final Map _objectToOID = new IdentityMap();
    
    /** Set of all objects marked 'deleted'. */
    private final Map _deletedMap = new IdentityMap();
    
    /** Set of all objects marked 'creating'. */
    private final Map _creatingMap = new IdentityMap();
    
    /** Set of all objects marked 'created'. */
    private final Set _createdSet = new IdentitySet();
    
    /** Set of all objects for which we need to update persistence. */
    private final Set _updatePersistNeededSet = new IdentitySet();
    
    /** Set of all objects for which we need to update cache. */
    private final Set _updateCacheNeededSet = new IdentitySet();
    
    /** Set of all objects marked read only in this transaction. */
    private final Set _readOnlySet = new IdentitySet();
    
    /** Set of all objects marked read-write in this transaction. */
    private final Set _readWriteSet = new IdentitySet();
    
    /** Operation counter of ObjectTracker. */
    private long _operation = Long.MIN_VALUE;
    
    //--------------------------------------------------------------------------

    /**
     * Retrieve the object for a given OID.
     * 
     * @param engine Lock engine mapped to oid
     * @param oid Object id specified
     * @param allowReadOnly Allow (or ignore, if false) read-only objects to be returned.
     * @return The object associated with this oid.
     */
    public Object getObjectForOID(final LockEngine engine, 
            final OID oid, 
            final boolean allowReadOnly) {
        Map oidToObject = (Map) _engineToOIDToObject.get(engine);
        if (oidToObject != null) {
            Object found = oidToObject.get(oid);
            if (!allowReadOnly) {
                if (_readOnlySet.contains(found)) {
                    return null;
                }
            }
            return found;
        }
        
        // We don't know about this engine in this transaction.
        // Hence, to us, there is no mapping.  Yet.
        return null;
    }
    
    /**
     * Returns true if the specified object is tracked as a read-write object.
     * @param object Object instance for which it should be determined whether 
     * it's tracked as read-write object
     * @return True if the specified object is tracked as a read-write object
     */
    public boolean isReadWrite(final Object object) {
        Object aObject = supportCGLibObject(object);
        return (_readWriteSet.contains(aObject));
    }
    
    public void unmarkAllDeleted() {
        _operation++;
        _deletedMap.clear();
    }
    
    /**
     * Reset ObjectTracker's state.
     */
    public void clear() {
        _operation++;
        _createdSet.clear();
        _creatingMap.clear();
        _deletedMap.clear();
        _engineToOIDToObject.clear();
        _objectToMolder.clear();
        _objectToOID.clear();
        _readOnlySet.clear();
        _readWriteSet.clear();
        _updateCacheNeededSet.clear();
        _updatePersistNeededSet.clear();
    }
    
    /**
     * Returns true if the cache needs to be updated for the given object
     * @param object An object instance
     * @return true if the cache needs to be updated; false, otherwise.
     */
    public boolean isUpdateCacheNeeded(final Object object) {
        Object aObject = supportCGLibObject(object);
        return _updateCacheNeededSet.contains(aObject);
    }
    
    /**
     * Returns true if the given object needs to be written to the persistence store
     * @param object An object instance
     * @return true if the object needs to be written to the persistence store
     */
    public boolean isUpdatePersistNeeded(final Object object) {
        Object aObject = supportCGLibObject(object);
        return _updatePersistNeededSet.contains(aObject);
    }
    
    public void markUpdateCacheNeeded(final Object object) {
        _operation++;
        Object aObject = supportCGLibObject(object);
        if (!isTracking(aObject)) {
            return;
        }
        
        _updateCacheNeededSet.add(aObject);
    }
    
    public void unmarkUpdateCacheNeeded(final Object object) {
        _operation++;
        Object aObject = supportCGLibObject(object);
        _updateCacheNeededSet.remove(aObject);
    }
    
    public Collection getObjectsWithUpdateCacheNeededState() {
        return new ArrayList(_updateCacheNeededSet);
    }
    
    public void markUpdatePersistNeeded(final Object object) {
        _operation++;
        Object aObject = supportCGLibObject(object);
        if (!isTracking(aObject)) {
            return;
        }
        
        _updatePersistNeededSet.add(aObject);
        // updatePersistNeeded implies updateCacheNeeded (from comment in persist())
        _updateCacheNeededSet.add(aObject);
    }
    
    public void unmarkUpdatePersistNeeded(final Object object) {
        _operation++;
        Object aObject = supportCGLibObject(object);
        _updatePersistNeededSet.remove(aObject);
    }
    
    public void markCreating(final Object object) throws PersistenceException {
        _operation++;
        Object aObject = supportCGLibObject(object);
        if (!isTracking(aObject)) {
            return;
        }
        
        if (_createdSet.contains(aObject)) {
            throw new PersistenceException("Invalid state change; can't mark something " 
                    + " creating which is already marked created.");
        }
        _creatingMap.put(aObject, new Long(_operation));
    }
    
    public void markCreated(final Object object) {
        _operation++;
        Object aObject = supportCGLibObject(object);
        if (!isTracking(aObject)) {
            return;
        }
        
        _createdSet.add(aObject);
        _creatingMap.remove(aObject);
    }
    
    public void markDeleted(final Object object) {
        _operation++;
        Object aObject = supportCGLibObject(object);
        if (!isTracking(aObject)) {
            return;
        }
        
        _deletedMap.put(aObject, new Long(_operation));
    }
    
    public void unmarkDeleted(final Object object) {
        _operation++;
        Object aObject = supportCGLibObject(object);
        _deletedMap.remove(aObject);
    }
    
    /**
     * Determine whether an object is being tracked within this tracking manager.
     * @param object The object for which it should be determined whether it is tracked.
     * @return True if the object specified is tracked; false otherwise
     */
    public boolean isTracking(final Object object) {
        Object aObject = supportCGLibObject(object);
        return _objectToOID.containsKey(aObject);
    }
    
    /**
     * Record changes to an OID by re-tracking the OID information.  When an object's 
     * OID can change, ensure that the object is retracked.
     * 
     * @param obj The object to record a tracking change for.
     * @param engine The engine which is responsible for the old and new OID
     * @param oldoid The old oid.
     * @param newoid The new oid.
     */
    public void trackOIDChange(final Object obj, 
            final LockEngine engine, 
            final OID oldoid, 
            final OID newoid) {
        _operation++;
        Object object = supportCGLibObject(obj);
        
        removeOIDForObject(engine, oldoid);
        setOIDForObject(object, engine, newoid);
    }
    
    
    /**
     * For a given lockengine and OID, set the object in the maps.  Note that an OID
     * can only be accessed via the LockManager which manages it.
     * @param obj The object to track
     * @param engine The engine to which the OID belongs
     * @param oid The OID of the object to track
     */
    public void setOIDForObject(final Object obj, 
            final LockEngine engine, 
            final OID oid) {
        _operation++;
        Object object = supportCGLibObject(obj);
        
        // Remove any current mapping.
        removeOIDForObject(engine, oid);
        
        // Now add it in.
        Map oidToObject = (Map) _engineToOIDToObject.get(engine);
        if (oidToObject == null) {
            oidToObject = new HashMap();
            _engineToOIDToObject.put(engine, oidToObject);
        }
        oidToObject.put(oid, obj);
        
        // Add the reversal.
        _objectToOID.put(object, oid);
    }
    
    /**
     * For a given lockengine and OID, remove references to an object in the maps.
     * This eliminates both the engine->oid->object and the object->oid.
     * @param engine The engine to stop tracking the OID for
     * @param oid The oid of the object to stop tracking on.
     */
    public void removeOIDForObject(final LockEngine engine, final OID oid) {
        _operation++;
        Object found = null;
        Map oidToObject = (Map) _engineToOIDToObject.get(engine);
        if (oidToObject != null) {
            found = oidToObject.get(oid);
            oidToObject.remove(oid);
        }
        if (found != null) {
            _objectToOID.remove(found);
        }
    }
    
    public boolean isCreating(final Object o) {
        Object object = supportCGLibObject(o);
        return _creatingMap.containsKey(object);
    }
    
    public boolean isCreated(final Object o) {
        Object object = supportCGLibObject(o);
        return _createdSet.contains(object);
    }
    
    public boolean isDeleted(final Object o) {
        Object object = supportCGLibObject(o);
        return _deletedMap.containsKey(object);
    }
    
    /** 
     * Retrieve the ClassMolder associated with a specific object. 
     * @param o Object instance the associated ClassMolder should be retrieved. 
     * @return The ClassMolder instance associated with the Object instance specified.
     */
    public ClassMolder getMolderForObject(final Object o) {
        Object object = supportCGLibObject(o);
        return (ClassMolder) _objectToMolder.get(object);
    }
    
    private void setMolderForObject(final Object obj, final ClassMolder molder) {
        _operation++;
        Object object = supportCGLibObject(obj);
        
        // remove if exists
        removeMolderForObject(object);
        // Update objectToMolder (handles both new and pre-existing mappings)
        _objectToMolder.put(object, molder);
    }
    
    private void removeMolderForObject(final Object obj) {
        _operation++;
        Object object = supportCGLibObject(obj);
        _objectToMolder.remove(object);
    }
    
    /** 
     * Retrieve the list of all read-write objects being tracked. 
     * @return List of all read-write objects being currently tracked.
     */
    public Collection getReadWriteObjects() {
        ArrayList returnedList = new ArrayList(_readWriteSet);
        return returnedList;
    }
    
    /** 
     * Retrieve the list of all read-only objects being tracked. 
     * @return List of all read-only objects being currently tracked
     */
    public Collection getReadOnlyObjects() {
        ArrayList returnedList = new ArrayList(_readOnlySet);
        return returnedList;
    }
    
    /** 
     * Retrieve the list of 'creating' objects (to be created), sorted in the 
     * order they should be created. 
     * @return List of objects to be created, sorted in the order they should 
     * be created. 
     */
    public Collection getObjectsWithCreatingStateSortedByLowestMolderPriority() {
        ArrayList entryList = new ArrayList(_creatingMap.entrySet());
        Collections.sort(entryList, new ObjectMolderPriorityComparator(this, false));
        ArrayList returnedList = new ArrayList(entryList.size());
        for (int i = 0; i < entryList.size(); i++) {
            returnedList.add(((Map.Entry) entryList.get(i)).getKey());
        }
        return returnedList;
    }
    
    /** 
     * Retrieve the list of 'deleted' objects, sorted in the order they should be 
     * deleted. 
     * @return List of 'deleted' objects, sorted in the order they should be 
     * deleted. 
     */
    public Collection getObjectsWithDeletedStateSortedByHighestMolderPriority() {
        ArrayList entryList = new ArrayList(_deletedMap.entrySet());
        Collections.sort(entryList, new ObjectMolderPriorityComparator(this, true));
        ArrayList returnedList = new ArrayList(entryList.size());
        for (int i = 0; i < entryList.size(); i++) {
            returnedList.add(((Map.Entry) entryList.get(i)).getKey());
        }
        return returnedList;
    }
    
    public void trackObject(
            final ClassMolder molder, 
            final OID oid, 
            final Object object) {
        _operation++;
        Object aObject = supportCGLibObject(object);
        
        setMolderForObject(aObject, molder);
        setOIDForObject(aObject, molder.getLockEngine(), oid);
        _readWriteSet.add(aObject);
    }
    
    
    public void untrackObject(final Object object) {
        _operation++;
        Object aObject = supportCGLibObject(object);
        
        // Grab any lockengine/OID information for removal of the nested 
        // engine-oid-object maps.
        LockEngine engine = getMolderForObject(aObject).getLockEngine();
        OID oid = getOIDForObject(aObject);
        
        removeMolderForObject(aObject);
        removeOIDForObject(engine, oid);
        
        _deletedMap.remove(aObject);
        _creatingMap.remove(aObject);
        _createdSet.remove(aObject);
        _updatePersistNeededSet.remove(aObject);
        _updateCacheNeededSet.remove(aObject);
        _readOnlySet.remove(aObject);
        _readWriteSet.remove(aObject);
    }
    
    public OID getOIDForObject(final Object o) {
        Object object = supportCGLibObject(o);
        return (OID) _objectToOID.get(object);
    }
    
    public boolean isReadOnly(final Object o) {
        Object object = supportCGLibObject(o);
        return (_readOnlySet.contains(object));
    }
    
    public void markReadOnly(final Object o) {
        _operation++;
        Object object = supportCGLibObject(o);
        
        if (!isTracking(object)) {
            throw new IllegalStateException(Messages.format("persist.internal",
            "Attempt to make read-only object that is not in transaction"));
        }
        
        // Add it to our list of read only objects.
        _readOnlySet.add(object);
        _readWriteSet.remove(object);
    }
    
    public void unmarkReadOnly(final Object o) {
        _operation++;
        Object object = supportCGLibObject(o);
        
        if (!isTracking(object)) {
            throw new IllegalStateException(Messages.format("persist.internal",
            "Attempt to make read-write object that is not in transaction"));
        }
        
        // Add it to our list of read only objects.
        _readWriteSet.add(object);
        _readOnlySet.remove(object);
    }
    
    public int readOnlySize() {
        return _readOnlySet.size();
    }
    
    public int readWriteSize() {
        return _readWriteSet.size();
    }
    
    /**
     * "Transform" an object provided as a LazyCGLib into a 'natural' object within 
     * the transaction, undecorated. This allows us to find the true object associated 
     * with a decorated object in the transaction.
     * 
     * We do this by asking the decorated object for enough information to re-generate 
     * its proper OID; we then go looking for that OID in the system to find the object 
     * in the transaction.
     * 
     * @param object
     * @return
     */
    private Object supportCGLibObject(final Object object) {
        if (object instanceof LazyCGLIB) {
            LazyCGLIB cgObject = (LazyCGLIB) object;
            
            // TODO [WG] We still might have an option for some serious optimization
            // here if the instance has not been materialized yet.
            Identity identity = cgObject.interceptedIdentity();
            ClassMolder molder = cgObject.interceptedClassMolder();
            LockEngine engine = molder.getLockEngine();
            
            // Get the OID we're looking for.
            OID oid = new OID(molder, identity);
            
            // Retrieve the object for this OID; returns null if there ain't such object 
            return getObjectForOID(engine, oid, true);
        }

        return object;
    }
    
    public String allObjectStates() {
        StringBuffer sb = new StringBuffer();
        Iterator it = _objectToOID.keySet().iterator();
        while (it.hasNext()) {
            sb.append(objectStateToString(it.next()));
            sb.append("\n");
        }
        return sb.toString();
    }
    
    /**
     * Returns the object's state.
     * @param obj Object for which its state should be output.
     * @return The state of the object specified
     */
    public String objectStateToString(final Object obj) {
        StringBuffer sb = new StringBuffer();
        sb.append(getOIDForObject(obj));
        sb.append('\t'); 
        sb.append("deleted: ");  
        sb.append(_deletedMap.containsKey(obj));
        sb.append('\t'); sb.append("creating: "); 
        sb.append(_creatingMap.containsKey(obj));
        sb.append('\t'); 
        sb.append("created: ");  
        sb.append(_createdSet.contains(obj));
        return sb.toString();
    }
    
    //--------------------------------------------------------------------------

    private static final class ObjectMolderPriorityComparator implements Comparator {
        
        private ObjectTracker _tracker;
        private boolean _reverseOrder;
        
        /**
         * Creates an instance if this class.
         * @param tracker The asociated ObjectTracker instance
         * @param reverseOrder True if reverse order should be applied.
         */
        public ObjectMolderPriorityComparator(
                final ObjectTracker tracker, final boolean reverseOrder) {
            _tracker = tracker;
            _reverseOrder = reverseOrder;
        }
        
        public int compare(final Object object1, final Object object2) {
            Map.Entry entry1 = (Map.Entry) object1;
            Map.Entry entry2 = (Map.Entry) object2;
            
            long oper1 = ((Long) entry1.getValue()).longValue();
            long oper2 = ((Long) entry2.getValue()).longValue();
            
            ClassMolder molder1 = _tracker.getMolderForObject(entry1.getKey());
            ClassMolder molder2 = _tracker.getMolderForObject(entry2.getKey());
            
            if (molder1 == null || molder2 == null) {
                if (oper1 > oper2) {
                    return 1;
                } else if (oper1 < oper2) {
                    return -1;
                } else {
                    return 0;
                }
            }
            
            int pri1 = molder1.getPriority();
            int pri2 = molder2.getPriority();
            
            if (pri1 == pri2) {
                if (oper1 > oper2) {
                    return 1;
                } 
                if (oper1 < oper2) {
                    return -1;
                }    
                return 0;
            }
            
            if (_reverseOrder) {
                return (pri1 < pri2) ? 1 : -1;
            }
            return (pri1 < pri2) ? -1 : 1;
        }
    }

    //--------------------------------------------------------------------------
}
