/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.sessions;

import java.util.*;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.descriptors.PersistenceEntity;
import org.eclipse.persistence.internal.identitymaps.*;
import org.eclipse.persistence.internal.sessions.IdentityMapAccessor;
import org.eclipse.persistence.expressions.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.sessions.Record;

/**
 * INTERNAL:
 * IdentityMapAccessor subclass for UnitOfWork
 * Overrides some initialization functionality and some behavior having to do with
 * getting objects from identity maps.
 */
public class UnitOfWorkIdentityMapAccessor extends IdentityMapAccessor {

    public UnitOfWorkIdentityMapAccessor() {
    }

    public UnitOfWorkIdentityMapAccessor(AbstractSession session, IdentityMapManager identityMapManager) {
        super(session, identityMapManager);
    }

    /**
     * ADVANCED:
     * Clear all the query caches
     */
    @Override
    public void clearQueryCache() {
        ((UnitOfWorkImpl)this.session).getParent().getIdentityMapAccessor().clearQueryCache();
    }

    /**
     * Invalidate/remove any results for the class from the query cache.
     * This is used to invalidate the query cache on any change.
     */
    public void invalidateQueryCache(Class classThatChanged) {
        ((UnitOfWorkImpl)this.session).getParent().getIdentityMapAccessor().invalidateQueryCache(classThatChanged);
    }

    /**
     * ADVANCED:
     * Clear the query class associated with the passed-in read query
     */
    @Override
    public void clearQueryCache(ReadQuery query) {
        ((UnitOfWorkImpl)this.session).getParent().getIdentityMapAccessor().clearQueryCache(query);
    }

    /**
     * ADVANCED:
     * Clear the query cache associated with the named query on the session
     */
    @Override
    public void clearQueryCache(String sessionQueryName) {
        ((UnitOfWorkImpl)this.session).getParent().getIdentityMapAccessor().clearQueryCache((ReadQuery)session.getQuery(sessionQueryName));
    }

    /**
     * ADVANCED:
     * Clear the query cache associated with the named query on the descriptor for the given class
     */
    @Override
    public void clearQueryCache(String descriptorQueryName, Class queryClass) {
        ((UnitOfWorkImpl)this.session).getParent().getIdentityMapAccessor().clearQueryCache((ReadQuery)session.getDescriptor(queryClass).getQueryManager().getQuery(descriptorQueryName));
    }

    /**
     * INTERNAL:
     * Return if their is an object for the primary key.
     */
    @Override
    public boolean containsObjectInIdentityMap(Object primaryKey, Class theClass, ClassDescriptor descriptor) {
        if (getIdentityMapManager().containsKey(primaryKey, theClass, descriptor)) {
            return true;
        }
        return ((UnitOfWorkImpl)this.session).getParent().getIdentityMapAccessorInstance().containsObjectInIdentityMap(primaryKey, theClass, descriptor);
    }

    /**
     * INTERNAL:
     * This method overrides the getAllFromIdentityMap method in Session.  Invalidated Objects
     * will always be returned from a UnitOfWork.
     */
    @Override
    public Vector getAllFromIdentityMap(Expression selectionCriteria, Class theClass, Record translationRow, int valueHolderPolicy, boolean shouldReturnInvalidatedObjects) throws QueryException {
        return super.getAllFromIdentityMap(selectionCriteria, theClass, translationRow, valueHolderPolicy, true);
    }

    /**
     * INTERNAL:
     * Override the getFromIdentityMapWithDeferredLock method on the session to ensure that
     * invalidated objects are always returned since this is a UnitOfWork
     */
    @Override
    public Object getFromIdentityMapWithDeferredLock(Object primaryKey, Class theClass, boolean shouldReturnInvalidatedObjects, ClassDescriptor descriptor) {
        Object objectFromCache = super.getFromIdentityMapWithDeferredLock(primaryKey, theClass, true, descriptor);
        if (objectFromCache != null){
            return objectFromCache;
        }
        return getAndCloneCacheKeyFromParent(primaryKey, null, theClass, shouldReturnInvalidatedObjects, descriptor);
    }

    /**
     * INTERNAL:
     * Return the object from the identity map with the primary key and class.
     * The parent's cache must be checked after the child's,
     * if found in the parent, it must be registered/cloned (but must avoid looping).
     * Note: in a UnitOfWork, invalidated objects will always be returned from the identity map
     * In the parent session, only return the object if it has not been Invalidated
     */
    @Override
    public Object getFromIdentityMap(Object primaryKey, Object object, Class theClass, boolean shouldReturnInvalidatedObjects, ClassDescriptor descriptor) {
        Object objectFromCache = super.getFromIdentityMap(primaryKey, object, theClass, true, descriptor);

        if (objectFromCache != null) {
            return objectFromCache;
        }
        //Bug#4613774  In the parent session, only return the object if it has not been Invalidated
        return getAndCloneCacheKeyFromParent(primaryKey, object, theClass, shouldReturnInvalidatedObjects, descriptor);
    }

    /**
     * INTERNAL:
     * This method will return the object from the parent and clone it.
     */
    protected Object getAndCloneCacheKeyFromParent(Object primaryKey, Object objectToClone, Class theClass, boolean shouldReturnInvalidatedObjects, ClassDescriptor descriptor) {
        // Note: Objects returned from the parent's identity map should include invalidated
        // objects. This is important because this internal method is used in the existence
        // check in the UnitOfWork.
        UnitOfWorkImpl unitOfWork = (UnitOfWorkImpl)this.session;
        CacheKey cacheKey = null;
        if (objectToClone != null && objectToClone instanceof PersistenceEntity){
            cacheKey = ((PersistenceEntity)objectToClone)._persistence_getCacheKey();
        }

        if (cacheKey == null || cacheKey.getOwningMap() == null){
            org.eclipse.persistence.internal.sessions.IdentityMapAccessor parentIdentityMapAccessor = unitOfWork.getParentIdentityMapSession(descriptor, false, false).getIdentityMapAccessorInstance();
            cacheKey = parentIdentityMapAccessor.getCacheKeyForObject(primaryKey, theClass, descriptor, false);
        }
        if ((cacheKey == null) && unitOfWork.getParentIdentityMapSession(descriptor, false, false).isUnitOfWork()) {
            //for nested unit of work
            //make parent clone and register object
            org.eclipse.persistence.internal.sessions.IdentityMapAccessor parentIdentityMapAccessor = unitOfWork.getParentIdentityMapSession(descriptor, false, false).getIdentityMapAccessorInstance();
            ((UnitOfWorkIdentityMapAccessor)parentIdentityMapAccessor).getAndCloneCacheKeyFromParent(primaryKey, null, theClass, shouldReturnInvalidatedObjects, descriptor);
            //get the cachekey that was created in the parent.
            cacheKey = parentIdentityMapAccessor.getCacheKeyForObject(primaryKey, theClass, descriptor, false);
        }

        Object objectFromCache = null;
        // this check could be simplified to one line but would create a window
        // in which GC could remove the object and we would end up with a null pointer
        // as well we must inspect the cacheKey without locking on it.
        if ((cacheKey != null) && (shouldReturnInvalidatedObjects || !descriptor.getCacheInvalidationPolicy().isInvalidated(cacheKey))) {
            synchronized (cacheKey) {
                //if the object in the cachekey is null but the key is acquired then
                //someone must be rebuilding it or creating a new one.  Sleep until
                // it's finished. A plain wait here would be more efficient but we may not
                // get notified for quite some time (ie deadlock) if the other thread
                //is building the object.  Must wait and not sleep in order for the monitor to be released
                objectFromCache = cacheKey.getObject();
                try {
                    while (cacheKey.isAcquired() && (objectFromCache == null)) {
                        cacheKey.wait(5);
                    }
                } catch (InterruptedException ex) {
                }
            }

            // check for inheritance.
            objectFromCache = checkForInheritance(objectFromCache, theClass, descriptor);
            if (objectFromCache == null) {
                return null;
            }
        } else {
            return null;
        }

        // Consider read-only class CR#4094
        if (this.session.isClassReadOnly(theClass, descriptor)) {
            // PERF: Just return the original object.
            return objectFromCache;
        }

        if (this.session instanceof RepeatableWriteUnitOfWork) {
            Object unregisteredDeletedClone = ((RepeatableWriteUnitOfWork)this.session).getUnregisteredDeletedCloneForOriginal(objectFromCache);
            if(unregisteredDeletedClone != null) {
                return unregisteredDeletedClone;
            }
        }

        return unitOfWork.cloneAndRegisterObject(objectFromCache, cacheKey, descriptor);
    }

    /**
     * INTERNAL:
     * Get the cached results associated with a query.  Results are cached by the
     * values of the parameters to the query so different parameters will have
     * different cached results.
     *
     * results are only cached in the parent session for UnitOfWorks
     */
    @Override
    public Object getQueryResult(ReadQuery query, List parameters, boolean checkExpiry) {
        return ((UnitOfWorkImpl)this.session).getParent().getIdentityMapAccessorInstance().getQueryResult(query, parameters, checkExpiry);
    }

    /**
     *  INTERNAL:
     *  Set the results for a query.
     *  Query results are cached based on the parameter values provided to the query
     *  different parameter values access different caches.
     *
     *  Results are only cached in the parent session for UnitOfWorks
     */
    @Override
    public void putQueryResult(ReadQuery query, List parameters, Object results) {
        ((UnitOfWorkImpl)this.session).getParent().getIdentityMapAccessorInstance().putQueryResult(query, parameters, results);
    }

    /**
     * INTERNAL:
     * Reset the entire object cache,
     * ** be careful using this.
     * This method blows away both this session's and its parents caches,
     * this includes the server cache or any other cache.
     * This throws away any objects that have been read in.
     * Extreme caution should be used before doing this because object identity will no longer
     * be maintained for any objects currently read in.  This should only be called
     * if the application knows that it no longer has references to object held in the cache.
     */
    @Override
    public void initializeAllIdentityMaps() {
        super.initializeAllIdentityMaps();
        ((UnitOfWorkImpl)this.session).getParent().getIdentityMapAccessor().initializeAllIdentityMaps();
    }

    /**
     * This method is used to resolve the inheritance issues arise while trying to get object from
     * identity map of parent session. Avoid reading the unintended subclass during in-memory query
     * (e.g. when querying on large project, do not want to check small project, both are inherited
     * from the project, and stored in the same identity map).
     */
    protected Object checkForInheritance(Object domainObject, Class superClass, ClassDescriptor descriptor) {
        if ((domainObject != null) && ((domainObject.getClass() != superClass) && (!superClass.isInstance(domainObject)))) {
            if (descriptor.hasInheritance() && descriptor.getInheritancePolicy().getUseDescriptorsToValidateInheritedObjects()) {
                if (descriptor.getInheritancePolicy().getSubclassDescriptor(domainObject.getClass()) == null) {
                    return null;
                }
                return domainObject;
            }
            return null;
        }
        return domainObject;
    }
}
