/*
 * 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
//     12/14/2017-3.0 Tomas Kraus
//       - 522635: ConcurrentModificationException when triggering lazy load from conforming query
package org.eclipse.persistence.internal.identitymaps;

import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.indirection.ValueHolderInterface;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;

/**
 * <p><b>Purpose</b>: A FullIdentityMap holds all objects stored within it for the life of the application.
 * <p><b>Responsibilities</b>:<ul>
 *    <li> Guarantees identity
 * <li> Holds all cached objects indefinitely.
 * </ul>
 * @since TOPLink/Java 1.0
 */
public class FullIdentityMap extends AbstractIdentityMap {

    /** Map of CacheKeys stored using their key. */
    protected Map<Object, CacheKey> cacheKeys;

    /**
     * Used to allow subclasses to build different map type.
     */
    public FullIdentityMap() {
    }

    public FullIdentityMap(int size, ClassDescriptor descriptor, AbstractSession session, boolean isolated) {
        super(size, descriptor, session, isolated);
        this.cacheKeys = new ConcurrentHashMap(size);
    }

    /**
     * INTERNAL:
     * Clones itself.
     */
    @Override
    public Object clone() {
        FullIdentityMap clone = (FullIdentityMap)super.clone();
        clone.setCacheKeys(new ConcurrentHashMap(this.cacheKeys.size()));

        for (Iterator cacheKeysIterator = this.cacheKeys.values().iterator(); cacheKeysIterator.hasNext();) {
            CacheKey key = (CacheKey)((CacheKey)cacheKeysIterator.next()).clone();
            clone.getCacheKeys().put(key.getKey(), key);
        }

        return clone;
    }

    /**
     * INTERNAL:
     * Used to print all the Locks in every identity map in this session.
     */
    @Override
    public void collectLocks(HashMap threadList) {
        Iterator cacheKeyIterator = this.cacheKeys.values().iterator();
        while (cacheKeyIterator.hasNext()) {
            CacheKey cacheKey = (CacheKey)cacheKeyIterator.next();
            if (cacheKey.isAcquired()) {
                Thread activeThread = cacheKey.getActiveThread();
                Set set = (Set)threadList.get(activeThread);
                if (set == null) {
                    set = new HashSet();
                    threadList.put(activeThread, set);
                }
                set.add(cacheKey);
            }
        }
    }

    /**
     * Allow for the cache {@link CacheKey#getObject()} elements to be iterated.
     *
     * @return {@link Enumeration} of {@link CacheKey#getObject()} instances.
     */
    @Override
    public Enumeration elements() {
        return new IdentityMapEnumeration(this.getCacheKeys().values());
    }

    /**
     * Return the cache key matching the primary key of the searchKey.
     * If no object for the key exists, return null.
     */
    @Override
    public CacheKey getCacheKey(Object searchKey, boolean forMerge) {
        return this.cacheKeys.get(searchKey);
    }

    /**
     * Return the CacheKey (with object) matching the searchKey.
     * If the CacheKey is missing then put the searchKey in the map.
     * The searchKey should have already been locked.
     */
    @Override
    protected CacheKey putCacheKeyIfAbsent(CacheKey searchKey) {
        searchKey.setOwningMap(this);
        return (CacheKey)((ConcurrentMap)this.cacheKeys).putIfAbsent(searchKey.getKey(), searchKey);
    }

    /**
     * Return the cache keys.
     */
    public Map<Object, CacheKey> getCacheKeys() {
        return cacheKeys;
    }

    /**
     * Return the number of CacheKeys in the IdentityMap.
     * This may contain weak referenced objects that have been garbage collected.
     */
    @Override
    public int getSize() {
        return this.cacheKeys.size();
    }

    /**
     * Return the number of actual objects of type myClass in the IdentityMap.
     * Recurse = true will include subclasses of myClass in the count.
     */
    @Override
    public int getSize(Class myClass, boolean recurse) {
        int count = 0;
        Iterator keys = this.cacheKeys.values().iterator();

        while (keys.hasNext()) {
            CacheKey key = (CacheKey)keys.next();
            Object object = key.getObject();

            if (object != null) {
                if (recurse && myClass.isInstance(object)) {
                    count++;
                } else if (object.getClass().equals(myClass)) {
                    count++;
                }
            }
        }

        return count;
    }

    /**
     * Allow for the cache keys to be iterated on.
     * Read locks will be checked.
     */
    @Override
    public Enumeration<CacheKey> keys() {
        return keys(true);
    }

    /**
     * Allow for the {@link CacheKey} elements to be iterated.
     * {@link CacheKey} {@link Collection} is reused so this iteration may not be thread safe.
     *
     * @param checkReadLocks value of {@code true} if read lock on the {@link CacheKey}
     *        instances should be checked or {@code false} otherwise
     * @return {@link Enumeration} of {@link CacheKey} instances.
     */
    public Enumeration<CacheKey> keys(boolean checkReadLocks) {
        return new IdentityMapKeyEnumeration(this.getCacheKeys().values(), checkReadLocks);
    }

    /**
     * Allow for the {@link CacheKey} elements to be iterated.
     * Clone of {@link CacheKey} {@link Collection} is returned so this iteration should
     * be thread safe.
     *
     * @return {@link Enumeration} with clone of the {@link CacheKey} {@link Collection}
     */
    @Override
    public Enumeration<CacheKey> cloneKeys() {
        return new IdentityMapKeyEnumeration(new ArrayList(this.getCacheKeys().values()), true);
    }

    /**
     * Notify the cache that a lazy relationship has been triggered in the object
     * and the cache may need to be updated
     */
    public void lazyRelationshipLoaded(Object object, ValueHolderInterface valueHolder, ForeignReferenceMapping mapping){
        //NO-OP
    }

    /**
     * Store the object in the cache at its primary key.
     * This is used by InsertObjectQuery, typically into the UnitOfWork identity map.
     * Merge and reads do not use put, but acquireLock.
     * Also an advanced (very) user API.
     * @param primaryKey is the primary key for the object.
     * @param object is the domain object to cache.
     * @param writeLockValue is the current write lock value of object, if null the version is ignored.
     */
    @Override
    public CacheKey put(Object primaryKey, Object object, Object writeLockValue, long readTime) {
        CacheKey newCacheKey = createCacheKey(primaryKey, object, writeLockValue, readTime);
        // Find the cache key in the map, reset it, or put the new one.
        CacheKey cacheKey = putCacheKeyIfAbsent(newCacheKey);
        if (cacheKey != null) {
            // The cache key is locked inside resetCacheKey() to keep other threads from accessing the object.
            resetCacheKey(cacheKey, object, writeLockValue, readTime);
        } else {
            return newCacheKey;
        }

        return cacheKey;
    }

    /**
     * Removes the CacheKey from the map.
     * @return the object held within the CacheKey or null if no object cached for given cacheKey.
     */
    @Override
    public Object remove(CacheKey cacheKey) {
        if (cacheKey != null) {
            // Cache key needs to be locked when removing from the map.
            cacheKey.acquire();
            this.cacheKeys.remove(cacheKey.getKey());
            cacheKey.setOwningMap(null);
            // Cache key needs to be released after removing from the map.
            cacheKey.setInvalidationState(CacheKey.CACHE_KEY_INVALID);
            cacheKey.release();
            return cacheKey.getObject();
        } else {
            return null;
        }
    }

    /**
     * Reset the cache key with new data.
     */
    public void resetCacheKey(CacheKey key, Object object, Object writeLockValue, long readTime) {
        key.acquire();
        key.setObject(object);
        key.setWriteLockValue(writeLockValue);
        key.setReadTime(readTime);
        key.release();
    }

    protected void setCacheKeys(Map<Object, CacheKey> cacheKeys) {
        this.cacheKeys = cacheKeys;
    }
}
