/*
 * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.internal.util;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.Objects;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jdk.internal.access.SharedSecrets;

/**
 * This class provides management of {@link Map maps} where it is desirable to
 * remove entries automatically when the key is garbage collected. This is
 * accomplished by using a backing map where the keys are either a
 * {@link WeakReference} or a {@link SoftReference}.
 * <p>
 * To create a {@link ReferencedKeyMap} the user must provide a {@link Supplier}
 * of the backing map and whether {@link WeakReference} or
 * {@link SoftReference} is to be used.
 *
 * {@snippet :
 * // Use HashMap and WeakReference
 * Map<Long, String> map = ReferencedKeyMap.create(false, HashMap::new);
 * map.put(10_000_000L, "a");
 * map.put(10_000_001L, "b");
 * map.put(10_000_002L, "c");
 * map.put(10_000_003L, "d");
 * map.put(10_000_004L, "e");
 *
 * // Use ConcurrentHashMap and SoftReference
 * map = ReferencedKeyMap.create(true, ConcurrentHashMap::new);
 * map.put(20_000_000L, "v");
 * map.put(20_000_001L, "w");
 * map.put(20_000_002L, "x");
 * map.put(20_000_003L, "y");
 * map.put(20_000_004L, "z");
 * }
 *
 * @implNote Care must be given that the backing map does replacement by
 * replacing the value in the map entry instead of deleting the old entry and
 * adding a new entry, otherwise replaced entries may end up with a strongly
 * referenced key. {@link HashMap} and {@link ConcurrentHashMap} are known
 * to be safe.
 *
 * @param <K> the type of keys maintained by this map
 * @param <V> the type of mapped values
 *
 * @since 21
 */
public final class ReferencedKeyMap<K, V> implements Map<K, V> {
    /**
     * true if {@link SoftReference} keys are to be used,
     * {@link WeakReference} otherwise.
     */
    private final boolean isSoft;

    /**
     * Backing {@link Map}.
     */
    private final Map<ReferenceKey<K>, V> map;

    /**
     * {@link ReferenceQueue} for cleaning up entries.
     */
    private final ReferenceQueue<K> stale;

    /**
     * Private constructor.
     *
     * @param isSoft          true if {@link SoftReference} keys are to
     *                        be used, {@link WeakReference} otherwise.
     * @param map             backing map
     * @param stale           {@link ReferenceQueue} for cleaning up entries
     */
    private ReferencedKeyMap(boolean isSoft, Map<ReferenceKey<K>, V> map, ReferenceQueue<K> stale) {
        this.isSoft = isSoft;
        this.map = map;
        this.stale = stale;
    }

    /**
     * Create a new {@link ReferencedKeyMap} map.
     *
     * @param isSoft          true if {@link SoftReference} keys are to
     *                        be used, {@link WeakReference} otherwise.
     * @param supplier        {@link Supplier} of the backing map
     *
     * @return a new map with {@link Reference} keys
     *
     * @param <K> the type of keys maintained by the new map
     * @param <V> the type of mapped values
     */
    // Android-changed: made private so no one tries to create a map with a non-native queue.
    // public static <K, V> ReferencedKeyMap<K, V>
    private static <K, V> ReferencedKeyMap<K, V>
    create(boolean isSoft, Supplier<Map<ReferenceKey<K>, V>> supplier) {
        // Android-changed: only native monitors based queues are available.
        // return create(isSoft, false, supplier);
        return create(isSoft, true, supplier);
    }

    /**
     * Create a new {@link ReferencedKeyMap} map.
     *
     * @param isSoft          true if {@link SoftReference} keys are to
     *                        be used, {@link WeakReference} otherwise.
     * @param useNativeQueue  true if uses NativeReferenceQueue
     *                        otherwise use {@link ReferenceQueue}.
     * @param supplier        {@link Supplier} of the backing map
     *
     * @return a new map with {@link Reference} keys
     *
     * @param <K> the type of keys maintained by the new map
     * @param <V> the type of mapped values
     */
    // Android-changed: made package-private so no one tries to create a map with a non-native queue
    // public static <K, V> ReferencedKeyMap<K, V>
    static <K, V> ReferencedKeyMap<K, V>
    create(boolean isSoft, boolean useNativeQueue, Supplier<Map<ReferenceKey<K>, V>> supplier) {
        // Android-added: non-native queue is not available.
        if (!useNativeQueue) {
            throw new IllegalArgumentException("Only native queue is supported");
        }
        return new ReferencedKeyMap<K, V>(isSoft, supplier.get(),
                // Android-changed: non-native queue is not available and ReferenceQueue is
                // native monitors based.
                // useNativeQueue ? SharedSecrets.getJavaLangRefAccess().newNativeReferenceQueue()
                //                : new ReferenceQueue<>()
                new ReferenceQueue<>()
                );
    }

    // Android-added: add a separate method with explicit name instead of passing boolean arg.
    public static <K, V> ReferencedKeyMap<K, V>
    createWithNativeQueue(boolean isSoft, Supplier<Map<ReferenceKey<K>, V>> supplier) {
        return create(isSoft, /* useNativeQueue= */ true, supplier);
    }

    /**
     * {@return a key suitable for a map entry}
     *
     * @param key unwrapped key
     */
    @SuppressWarnings("unchecked")
    private ReferenceKey<K> entryKey(Object key) {
        if (isSoft) {
            return new SoftReferenceKey<>((K)key, stale);
        } else {
            return new WeakReferenceKey<>((K)key, stale);
        }
    }

    /**
     * {@return a key suitable for lookup}
     *
     * @param key unwrapped key
     */
    @SuppressWarnings("unchecked")
    private ReferenceKey<K> lookupKey(Object key) {
        return new StrongReferenceKey<>((K)key);
    }

    @Override
    public int size() {
        removeStaleReferences();
        return map.size();
    }

    @Override
    public boolean isEmpty() {
        removeStaleReferences();
        return map.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        Objects.requireNonNull(key, "key must not be null");
        removeStaleReferences();
        return map.containsKey(lookupKey(key));
    }

    @Override
    public boolean containsValue(Object value) {
        Objects.requireNonNull(value, "value must not be null");
        removeStaleReferences();
        return map.containsValue(value);
    }

    @Override
    public V get(Object key) {
        Objects.requireNonNull(key, "key must not be null");
        removeStaleReferences();
        return map.get(lookupKey(key));
    }

    @Override
    public V put(K key, V newValue) {
        Objects.requireNonNull(key, "key must not be null");
        Objects.requireNonNull(newValue, "value must not be null");
        removeStaleReferences();
        ReferenceKey<K> entryKey = entryKey(key);
        // If {@code put} returns non-null then was actually a {@code replace}
        // and older key was used. In that case the new key was not used and the
        // reference marked stale.
        V oldValue = map.put(entryKey, newValue);
        if (oldValue != null) {
            entryKey.unused();
        }
        return oldValue;
    }

    @Override
    public V remove(Object key) {
        // Rely on gc to clean up old key.
        return map.remove(lookupKey(key));
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        removeStaleReferences();
        for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
            K key = entry.getKey();
            V value = entry.getValue();
            put(key, value);
        }
    }

    @Override
    public void clear() {
        removeStaleReferences();
        // Rely on gc to clean up old keys.
        map.clear();
    }

    /**
     * Common routine for collecting the current set of keys.
     *
     * @return {@link Stream} of valid keys (unwrapped)
     */
    private Stream<K> filterKeySet() {
        return map.keySet()
                .stream()
                .map(ReferenceKey::get)
                .filter(Objects::nonNull);
    }

    @Override
    public Set<K> keySet() {
        removeStaleReferences();
        return filterKeySet().collect(Collectors.toSet());
    }

    @Override
    public Collection<V> values() {
        removeStaleReferences();
        return map.values();
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        removeStaleReferences();
        return filterKeySet()
                .map(k -> new AbstractMap.SimpleEntry<>(k, get(k)))
                .collect(Collectors.toSet());
    }

    @Override
    public V putIfAbsent(K key, V newValue) {
        removeStaleReferences();
        ReferenceKey<K> entryKey = entryKey(key);
        // If {@code putIfAbsent} returns non-null then was actually a
        // {@code replace}  and older key was used. In that case the new key was
        // not used and the reference marked stale.
        V oldValue = map.putIfAbsent(entryKey, newValue);
        if (oldValue != null) {
            entryKey.unused();
        }
        return oldValue;
    }

    @Override
    public boolean remove(Object key, Object value) {
        // Rely on gc to clean up old key.
        return map.remove(lookupKey(key), value);
    }

    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        removeStaleReferences();
        // If replace is successful then the older key will be used and the
        // lookup key will suffice.
        return map.replace(lookupKey(key), oldValue, newValue);
    }

    @Override
    public V replace(K key, V value) {
        removeStaleReferences();
        // If replace is successful then the older key will be used and the
        // lookup key will suffice.
        return map.replace(lookupKey(key), value);
    }

    @Override
    public String toString() {
        removeStaleReferences();
        return filterKeySet()
                .map(k -> k + "=" + get(k))
                .collect(Collectors.joining(", ", "{", "}"));
    }

    /**
     * Removes enqueued weak references from map.
     */
    public void removeStaleReferences() {
        while (true) {
            Object key = stale.poll();
            if (key == null) {
                break;
            }
            map.remove(key);
        }
    }

    /**
     * Puts an entry where the key and the value are the same. Used for
     * interning values in a set.
     *
     * @implNote Requires a {@link ReferencedKeyMap} whose {@code V} type
     * is a {@code ReferenceKey<K>}. Otherwise, a {@link ClassCastException} will
     * be thrown.
     *
     * @param setMap    {@link ReferencedKeyMap} where interning takes place
     * @param key       key to add
     *
     * @param <T> type of key
     *
     * @return the old key instance if found otherwise the new key instance
     *
     * @throws ClassCastException if {@code V} is not {@code EntryKey<T>}
     */
    static <T> T intern(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key) {
        T value = existingKey(setMap, key);
        if (value != null) {
            return value;
        }
        return internKey(setMap, key);
    }

    /**
     * Puts an entry where the key and the value are the same. Used for
     * interning values in a set.
     *
     * @implNote Requires a {@link ReferencedKeyMap} whose {@code V} type
     * is a {@code ReferenceKey<K>}. Otherwise, a {@link ClassCastException} will
     * be thrown.
     *
     * @param setMap    {@link ReferencedKeyMap} where interning takes place
     * @param key       key to add
     * @param interner  operation to apply to key before adding to map
     *
     * @param <T> type of key
     *
     * @return the old key instance if found otherwise the new key instance
     *
     * @throws ClassCastException if {@code V} is not {@code EntryKey<T>}
     *
     * @implNote This version of intern should not be called during phase1
     * using a lambda. Use an UnaryOperator instance instead.
     */
    static <T> T intern(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key, UnaryOperator<T> interner) {
        T value = existingKey(setMap, key);
        if (value != null) {
            return value;
        }
        key = interner.apply(key);
        return internKey(setMap, key);
    }

    /**
     * Check if the key already exists in the map.
     *
     * @param setMap    {@link ReferencedKeyMap} where interning takes place
     * @param key       key to test
     *
     * @param <T> type of key
     *
     * @return key if found otherwise null
     */
    private static <T> T existingKey(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key) {
        setMap.removeStaleReferences();
        ReferenceKey<T> entryKey = setMap.get(setMap.lookupKey(key));
        return entryKey != null ? entryKey.get() : null;
    }

    /**
     * Attempt to add key to map.
     *
     * @param setMap    {@link ReferencedKeyMap} where interning takes place
     * @param key       key to add
     *
     * @param <T> type of key
     *
     * @return the old key instance if found otherwise the new key instance
     */
    private static <T> T internKey(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key) {
        ReferenceKey<T> entryKey = setMap.entryKey(key);
        T interned;
        do {
            setMap.removeStaleReferences();
            ReferenceKey<T> existing = setMap.map.putIfAbsent(entryKey, entryKey);
            if (existing == null) {
                return key;
            } else {
                // If {@code putIfAbsent} returns non-null then was actually a
                // {@code replace} and older key was used. In that case the new
                // key was not used and the reference marked stale.
                interned = existing.get();
                if (interned != null) {
                    entryKey.unused();
                }
            }
        } while (interned == null);
        return interned;
    }


    /**
     * Attempt to add key to map if absent.
     *
     * @param setMap    {@link ReferencedKeyMap} where interning takes place
     * @param key       key to add
     *
     * @param <T> type of key
     *
     * @return true if the key was added
     */
    static <T> boolean internAddKey(ReferencedKeyMap<T, ReferenceKey<T>> setMap, T key) {
        ReferenceKey<T> entryKey = setMap.entryKey(key);
        setMap.removeStaleReferences();
        ReferenceKey<T> existing = setMap.map.putIfAbsent(entryKey, entryKey);
        if (existing == null) {
            return true;
        } else {
            // If {@code putIfAbsent} returns non-null then was actually a
            // {@code replace} and older key was used. In that case the new
            // key was not used and the reference marked stale.
            entryKey.unused();
            return false;
        }
     }

}
