/*
 * Copyright (c) 1998, 2021 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.identitymaps;

import org.eclipse.persistence.exceptions.ConcurrencyException;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.queries.ObjectBuildingQuery;
import org.eclipse.persistence.sessions.DatabaseRecord;
import org.eclipse.persistence.sessions.Record;

/**
 * <p><b>Purpose</b>: Container class for storing objects in an IdentityMap.
 * <p><b>Responsibilities</b>:<ul>
 * <li> Hold key and object.
 * <li> Maintain and update the current writeLockValue.
 * </ul>
 * @since TOPLink/Java 1.0
 */
public class CacheKey extends ConcurrencyManager implements Cloneable {

    //These constants are used in extended cache logging to compare cache item creation thread and thread which picking item from the cache
    public final long CREATION_THREAD_ID = Thread.currentThread().getId();
    public final String CREATION_THREAD_NAME = String.copyValueOf(Thread.currentThread().getName().toCharArray());
    public final long CREATION_THREAD_HASHCODE = Thread.currentThread().hashCode();

    /** The key holds the vector of primary key values for the object. */
    protected Object key;

    protected Object object;

    //used to store a reference to the map this cachekey is in in cases where the
    //cache key is to be removed, prevents us from having to track down the owning
    //map
    protected IdentityMap mapOwner;

    /** The writeLock value is being held as an object so that it might contain a number or timestamp. */
    protected Object writeLockValue;

    /** The cached wrapper for the object, used in EJB. */
    protected Object wrapper;

    /** This is used for Document Preservation to cache the record that this object was built from */
    protected Record record;

    /** This attribute is the system time in milli seconds that the object was last refreshed on */

    //CR #4365
    // CR #2698903 - fix for the previous fix. No longer using millis.
    protected long lastUpdatedQueryId;

    /** Invalidation State can be used to indicate whether this cache key is considered valid */
    protected int invalidationState = CHECK_INVALIDATION_POLICY;

    /** The following constants are used for the invalidationState variable */
    public static final int CHECK_INVALIDATION_POLICY = 0;
    public static final int CACHE_KEY_INVALID = -1;

    public static final int MAX_WAIT_TRIES = 10000;

    /** The read time stores the millisecond value of the last time the object help by
    this cache key was confirmed as up to date. */
    protected long readTime = 0;

    /**
     * Stores if this CacheKey instance is a wrapper for the underlying CacheKey.  CacheKey wrappers
     * may be used with cache interceptors.
     */
    protected boolean isWrapper = false;

    /**
     * Stores retrieved FK values for relationships that are not stored in the Entity
     */
    protected AbstractRecord protectedForeignKeys;

    /**
     * Set to true if this CacheKey comes from an IsolatedClientSession, or DatabaseSessionImpl.
     */
    protected boolean isIsolated;

    /**
     * The ID of the database transaction that last wrote the object.
     * This is used for database change notification.
     */
    protected Object transactionId;

    /**
     * Internal:
     * Only used by subclasses that may want to wrap the cache key.  Could be replaced
     * by switching to an interface.
     */
    protected CacheKey(){
    }

    public CacheKey(Object primaryKey) {
        this.key = primaryKey;
    }

    public CacheKey(Object primaryKey, Object object, Object lockValue) {
        this.key = primaryKey;
        this.writeLockValue = lockValue;
        //bug4649617  use setter instead of this.object = object to avoid hard reference on object in subclasses
        if (object != null) {
            setObject(object);
        }
    }

    public CacheKey(Object primaryKey, Object object, Object lockValue, long readTime, boolean isIsolated) {
        this.key = primaryKey;
        this.writeLockValue = lockValue;
        //bug4649617  use setter instead of this.object = object to avoid hard reference on object in subclasses
        if (object != null) {
            setObject(object);
        }
        this.readTime = readTime;
        this.isIsolated = isIsolated;
    }

    /**
     * Acquire the lock on the cache key object.
     */
    public void acquire() {
        if (this.isIsolated) {
            this.depth.incrementAndGet();
            return;
        }
        super.acquire(false);
    }

    /**
     * Acquire the lock on the cache key object. For the merge process
     * called with true from the merge process, if true then the refresh will not refresh the object
     */
    public void acquire(boolean forMerge) {
        if (this.isIsolated) {
            this.depth.incrementAndGet();
            return;
        }
        super.acquire(forMerge);
    }

    /**
     * Acquire the lock on the cache key object. But only if the object has no lock on it
     * Added for CR 2317
     */
    public boolean acquireNoWait() {
        if (this.isIsolated) {
            this.depth.incrementAndGet();
            return true;
        }
        return super.acquireNoWait(false);
    }

    /**
     * Acquire the lock on the cache key object. Only acquire a lock if the cache key's
     * active thread is not set.
     * Added for Bug 5840635
     */

    public boolean acquireIfUnownedNoWait() {
        if (this.isIsolated) {
            if (this.depth.get() > 0) {
                return false;
            }
            this.depth.incrementAndGet();
            return true;
        }
        return super.acquireIfUnownedNoWait(false);
    }

    /**
     * Acquire the lock on the cache key object. But only if the object has no lock on it
     * Added for CR 2317
     * called with true from the merge process, if true then the refresh will not refresh the object
     */
    public boolean acquireNoWait(boolean forMerge) {
        if (this.isIsolated) {
            this.depth.incrementAndGet();
            return true;
        }
        return super.acquireNoWait(forMerge);
    }

    /**
     * Acquire the lock on the cache key object. But only if the object has no lock on it
     * Added for CR 2317
     * called with true from the merge process, if true then the refresh will not refresh the object
     */
    public boolean acquireWithWait(boolean forMerge, int wait) {
        if (this.isIsolated) {
            this.depth.incrementAndGet();
            return true;
        }
        return super.acquireWithWait(forMerge, wait);
    }

    /**
     * Acquire the deferred lock.
     */
    public void acquireDeferredLock() {
        if (this.isIsolated) {
            this.depth.incrementAndGet();
            return;
        }
        super.acquireDeferredLock();
    }

    public void acquireLock(ObjectBuildingQuery query){

        // PERF: Only use deferred locking if required.
        // CR#3876308 If joining is used, deferred locks are still required.
        if (query.requiresDeferredLocks()) {
            this.acquireDeferredLock();

            int counter = 0;
            while ((this.object == null) && (counter < 1000)) {
                if (this.getActiveThread() == Thread.currentThread()) {
                    break;
                }
                //must release lock here to prevent acquiring multiple deferred locks but only
                //releasing one at the end of the build object call.
                //bug 5156075
                this.releaseDeferredLock();
                //sleep and try again if we are not the owner of the lock for CR 2317
                // prevents us from modifying a cache key that another thread has locked.
                try {
                    Thread.sleep(10);
                } catch (InterruptedException exception) {
                }
                this.acquireDeferredLock();
                counter++;
            }
            if (counter == 1000) {
                throw ConcurrencyException.maxTriesLockOnBuildObjectExceded(this.getActiveThread(), Thread.currentThread());
            }
        } else {
            this.acquire();
        }
    }

    /**
     * Check the read lock on the cache key object.
     * This can be called to ensure the cache key has a valid built object.
     * It does not hold a lock, so the object could be refreshed afterwards.
     */
    public void checkReadLock() {
        if (this.isIsolated) {
            return;
        }
        super.checkReadLock();
    }

    /**
     * Check the deferred lock on the cache key object.
     * This can be called to ensure the cache key has a valid built object.
     * It does not hold a lock, so the object could be refreshed afterwards.
     */
    public void checkDeferredLock() {
        if (this.isIsolated) {
            return;
        }
        super.checkDeferredLock();
    }

    /**
     * Acquire the read lock on the cache key object.
     */
    public void acquireReadLock() {
        if (this.isIsolated) {
            return;
        }
        super.acquireReadLock();
    }

    /**
     * Acquire the read lock on the cache key object.  Return true if acquired.
     */
    public boolean acquireReadLockNoWait() {
        if (this.isIsolated) {
            return true;
        }
        return super.acquireReadLockNoWait();
    }

    /**
     * INTERNAL:
     * Clones itself.
     */
    public Object clone() {
        Object object = null;

        try {
            object = super.clone();
        } catch (Exception exception) {
            throw new InternalError(exception.toString());
        }

        return object;
    }

    /**
     * Determine if the receiver is equal to anObject.
     * If anObject is a CacheKey, do further comparison, otherwise, return false.
     * @see CacheKey#equals(CacheKey)
     */
    public boolean equals(Object object) {
        try {
            return equals((CacheKey)object);
        } catch (ClassCastException incorrectType) {
            return false;
        }
    }

    /**
     * Determine if the receiver is equal to key.
     * Use an index compare, because it is much faster than enumerations.
     */
    public boolean equals(CacheKey key) {
        if (key.key == null || this.key == null) {
            return false;
        }
        return this.key.equals(key.key);
    }

    /**
     * INTERNAL:
     * This method returns the system time in millis seconds at which this object was last refreshed
     * CR #4365
     * CR #2698903 ... instead of using millis we will now use id's instead. Method
     * renamed appropriately.
     */
    public long getLastUpdatedQueryId() {
        return this.lastUpdatedQueryId;
    }

    public Object getKey() {
        return key;
    }

    /**
     * Return the active thread.
     */
    public Thread getActiveThread() {
        if (this.isIsolated) {
            if (this.depth.get() > 0) {
                return Thread.currentThread();
            } else {
                return null;
            }
        }
        return super.getActiveThread();
    }

    public Object getObject() {
        return object;
    }

    public IdentityMap getOwningMap(){
        return this.mapOwner;
    }

    /**
     * INTERNAL:
     * Return the current value of the Read Time variable
     */
    public long getReadTime() {
        return readTime;
    }

    public Record getRecord() {
        return record;
    }

    public Object getWrapper() {
        return wrapper;
    }

    /**
     * If a Wrapper subclasses this CacheKey this method will be used to unwrap the cache key.
     * @return
     */
    public CacheKey getWrappedCacheKey(){
        return this;
    }

    public Object getWriteLockValue() {
        return writeLockValue;
    }

    /**
     * Overrides hashCode() in Object to use the primaryKey's hashCode for storage in data structures.
     */
    public int hashCode() {
        return this.key.hashCode();
    }

    /**
     * Returns true if the protectedForeignKeys record is non-null and non-empty, false otherwise.
     */
    public boolean hasProtectedForeignKeys() {
        return (this.protectedForeignKeys != null) && (this.protectedForeignKeys.size() > 0);
    }

    /**
     * Returns true if this CacheKey is from an IsolatedClientSession
     */
    public boolean isIsolated() {
        return isIsolated;
    }

    /**
     * Returns true if this Instance of CacheKey is a wrapper and should be unwrapped before passing
     * to IdentityMap APIs.  Wrapped CacheKeys may be used in the Cache Interceptors.
     */
    public boolean isWrapper(){
        return this.isWrapper;
    }

    /**
     * INTERNAL:
     * Return the FK cache
     */
    public AbstractRecord getProtectedForeignKeys(){
        if (this.protectedForeignKeys == null){
            this.protectedForeignKeys = new DatabaseRecord();
        }
        return this.protectedForeignKeys;
    }

    /**
     * INTERNAL:
     * Return the value of the invalidationState Variable
     * The return value will be a constant
     * CHECK_INVALIDATION_POLICY - The Invalidation policy is must be checked for this cache key's sate
     * CACHE_KEY_INVALID - This cache key has been labeled invalid.
     */
    public int getInvalidationState() {
        return invalidationState;
    }

    /**
     * Release the lock on the cache key object.
     */
    public void release() {
        if (this.isIsolated) {
            this.depth.decrementAndGet();
            return;
        }
        super.release();
    }

    /**
     * Release the deferred lock
     */
    public void releaseDeferredLock() {
        if (this.isIsolated) {
            this.depth.decrementAndGet();
            return;
        }
        super.releaseDeferredLock();
    }

    /**
     * Release the read lock on the cache key object.
     */
    public void releaseReadLock() {
        if (this.isIsolated) {
            return;
        }
        super.releaseReadLock();
    }

    /**
     * Removes this cacheKey from the owning map
     */
    public Object removeFromOwningMap(){
        if (getOwningMap() != null){
            return getOwningMap().remove(this);
        }
        return null;
    }

    /**
     * INTERNAL:
     * Set the value of the invalidationState Variable
     * The possible values are from an enumeration of constants
     * CHECK_INVALIDATION_POLICY - The invalidation policy is must be checked for this cache key's sate
     * CACHE_KEY_INVALID - This cache key has been labelled invalid.
     */
    public void setInvalidationState(int invalidationState) {
        this.invalidationState = invalidationState;
    }

    /**
     * INTERNAL:
     * This method sets the system time in millis seconds at which this object was last refreshed
     * CR #4365
     * CR #2698903 ... instead of using millis we will now use ids instead. Method
     * renamed appropriately.
     */
    public void setLastUpdatedQueryId(long id) {
        this.lastUpdatedQueryId = id;
    }

    public void setKey(Object key) {
        this.key = key;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    public void setOwningMap(IdentityMap map){
        this.mapOwner = map;
    }

    public void setProtectedForeignKeys(AbstractRecord protectedForeignKeys) {
        this.protectedForeignKeys = protectedForeignKeys;
    }

    /**
     * INTERNAL:
     * Set the read time of this cache key
     */
    public void setReadTime(long readTime) {
        this.readTime = readTime;
        invalidationState = CHECK_INVALIDATION_POLICY;
    }

    public void setRecord(Record newRecord) {
        this.record = newRecord;
    }

    public void setWrapper(Object wrapper) {
        this.wrapper = wrapper;
    }

    public void setWriteLockValue(Object writeLockValue) {
        this.writeLockValue = writeLockValue;
    }

    public String toString() {
        int hashCode = 0;
        if (getObject() != null) {
            hashCode = getObject().hashCode();
        }

        return "[" + getKey() + ": " + hashCode + ": " + getWriteLockValue() + ": " + getReadTime() + ": " + getObject() + "]";
    }

    /**
     * Notifies that cache key that it has been accessed.
     * Allows the LRU sub-cache to be maintained.
     */
    public void updateAccess() {
        // Nothing required by default.
    }

    public void setIsolated(boolean isIsolated) {
        this.isIsolated = isIsolated;
    }

    public void setIsWrapper(boolean isWrapper) {
        this.isWrapper = isWrapper;
    }

    public Object getTransactionId() {
        return transactionId;
    }

    public void setTransactionId(Object transactionId) {
        this.transactionId = transactionId;
    }

    public synchronized Object waitForObject(){
        try {
            int count = 0;
            while (this.object == null && isAcquired()) {
                if (count > MAX_WAIT_TRIES)
                    throw ConcurrencyException.maxTriesLockOnBuildObjectExceded(getActiveThread(), Thread.currentThread());
                wait(10);
                ++count;
            }
        } catch(InterruptedException ex) {
            //ignore as the loop is broken
        }
        return this.object;
    }
}
