// SPDX-License-Identifier: 0BSD
// SPDX-FileCopyrightText: The XZ for Java authors and contributors
// SPDX-FileContributor: Lasse Collin <lasse.collin@tukaani.org>

package org.tukaani.xz;

import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * A basic {@link ArrayCache} implementation.
 * <p>
 * This caches exact array sizes, that is, {@code getByteArray} will return
 * an array whose size is exactly the requested size. A limited number
 * of different array sizes are cached at the same time; least recently used
 * sizes will be dropped from the cache if needed (can happen if several
 * different (de)compression options are used with the same cache).
 * <p>
 * The current implementation uses
 * {@link java.util.LinkedHashMap LinkedHashMap} to map different array sizes
 * to separate array-based data structures which hold
 * {@link java.lang.ref.SoftReference SoftReferences} to the cached arrays.
 * In the common case this should give good performance and fairly low
 * memory usage overhead.
 * <p>
 * A statically allocated global {@code BasicArrayCache} instance is
 * available via {@link #getInstance()} which is a good choice in most
 * situations where caching is wanted.
 *
 * @since 1.7
 */
public class BasicArrayCache extends ArrayCache {
    /**
     * Arrays smaller than this many elements will not be cached.
     */
    private static final int CACHEABLE_SIZE_MIN = 32 << 10;

    /**
     * Number of stacks i.e. how many different array sizes to cache.
     */
    private static final int STACKS_MAX = 32;

    /**
     * Number of arrays of the same type and size to keep in the cache.
     * (ELEMENTS_PER_STACK - 1) is used as a bit mask so ELEMENTS_PER_STACK
     * must be a power of two!
     */
    private static final int ELEMENTS_PER_STACK = 512;

    /**
     * A thread-safe stack-like data structure whose {@code push} method
     * overwrites the oldest element in the stack if the stack is full.
     */
    private static class CyclicStack<T> {
        /**
         * Array that holds the elements in the cyclic stack.
         */
        @SuppressWarnings("unchecked")
        private final T[] elements = (T[])new Object[ELEMENTS_PER_STACK];

        /**
         * Read-write position in the {@code refs} array.
         * The most recently added element is in {@code refs[pos]}.
         * If it is {@code null}, then the stack is empty and all
         * elements in {@code refs} are {@code null}.
         * <p>
         * Note that {@code pop()} always modifies {@code pos}, even if
         * the stack is empty. This means that when the first element is
         * added by {@code push(T)}, it can get added in any position in
         * {@code refs} and the stack will start growing from there.
         */
        private int pos = 0;

        /**
         * Gets the most recently added element from the stack.
         * If the stack is empty, {@code null} is returned.
         */
        public synchronized T pop() {
            T e = elements[pos];
            elements[pos] = null;
            pos = (pos - 1) & (ELEMENTS_PER_STACK - 1);
            return e;
        }

        /**
         * Adds a new element to the stack. If the stack is full, the oldest
         * element is overwritten.
         */
        public synchronized void push(T e) {
            pos = (pos + 1) & (ELEMENTS_PER_STACK - 1);
            elements[pos] = e;
        }
    }

    /**
     * Maps Integer (array size) to stacks of references to arrays. At most
     * STACKS_MAX number of stacks are kept in the map (LRU cache).
     */
    private static class CacheMap<T>
            extends LinkedHashMap<Integer, CyclicStack<Reference<T>>> {
        /**
         * This class won't be serialized but this is needed
         * to silence a compiler warning.
         */
        private static final long serialVersionUID = 1L;

        /**
         * Creates a new CacheMap.
         */
        public CacheMap() {
            // The map may momentarily have at most STACKS_MAX + 1 entries
            // when put(K,V) has inserted a new entry but hasn't called
            // removeEldestEntry yet. Using 2 * STACKS_MAX as the initial
            // (and the final) capacity should give good performance. 0.75 is
            // the default load factor and in this case it guarantees that
            // the map will never need to be rehashed because
            // (STACKS_MAX + 1) / 0.75 < 2 * STACKS_MAX.
            //
            // That last argument is true to get LRU cache behavior together
            // with the overridden removeEldestEntry method.
            super(2 * STACKS_MAX, 0.75f, true);
        }

        /**
         * Returns true if the map is full and the least recently used stack
         * should be removed.
         */
        @Override
        protected boolean removeEldestEntry(
                Map.Entry<Integer, CyclicStack<Reference<T>>> eldest) {
            return size() > STACKS_MAX;
        }
    }

    /**
     * Helper class for the singleton instance.
     * This is allocated only if {@code getInstance()} is called.
     */
    private static final class LazyHolder {
        static final BasicArrayCache INSTANCE = new BasicArrayCache();
    }

    /**
     * Returns a statically-allocated {@code BasicArrayCache} instance.
     * This is often a good choice when a cache is needed.
     */
    public static BasicArrayCache getInstance() {
        return LazyHolder.INSTANCE;
    }

    /**
     * Stacks for cached byte arrays.
     */
    private final CacheMap<byte[]> byteArrayCache = new CacheMap<byte[]>();

    /**
     * Stacks for cached int arrays.
     */
    private final CacheMap<int[]> intArrayCache = new CacheMap<int[]>();

    /**
     * Gets {@code T[size]} from the given {@code cache}.
     * If no such array is found, {@code null} is returned.
     */
    private static <T> T getArray(CacheMap<T> cache, int size) {
        // putArray doesn't add small arrays to the cache and so it's
        // pointless to look for small arrays here.
        if (size < CACHEABLE_SIZE_MIN)
            return null;

        // Try to find a stack that holds arrays of T[size].
        CyclicStack<Reference<T>> stack;
        synchronized(cache) {
            stack = cache.get(size);
        }

        if (stack == null)
            return null;

        // Try to find a non-cleared Reference from the stack.
        T array;
        do {
            Reference<T> r = stack.pop();
            if (r == null)
                return null;

            array = r.get();
        } while (array == null);

        return array;
    }

    /**
     * Puts the {@code array} of {@code size} elements long into
     * the {@code cache}.
     */
    private static <T> void putArray(CacheMap<T> cache, T array, int size) {
        // Small arrays aren't cached.
        if (size < CACHEABLE_SIZE_MIN)
            return;

        CyclicStack<Reference<T>> stack;

        synchronized(cache) {
            // Get a stack that holds arrays of T[size]. If no such stack
            // exists, allocate a new one. If the cache already had STACKS_MAX
            // number of stacks, the least recently used stack is removed by
            // cache.put (it calls removeEldestEntry).
            stack = cache.get(size);
            if (stack == null) {
                stack = new CyclicStack<Reference<T>>();
                cache.put(size, stack);
            }
        }

        stack.push(new SoftReference<T>(array));
    }

    /**
     * Allocates a new byte array, hopefully reusing an existing
     * array from the cache.
     *
     * @param       size        size of the array to allocate
     *
     * @param       fillWithZeros
     *                          if true, all the elements of the returned
     *                          array will be zero; if false, the contents
     *                          of the returned array is undefined
     */
    @Override
    public byte[] getByteArray(int size, boolean fillWithZeros) {
        byte[] array = getArray(byteArrayCache, size);

        if (array == null)
            array = new byte[size];
        else if (fillWithZeros)
            Arrays.fill(array, (byte)0x00);

        return array;
    }

    /**
     * Puts the given byte array to the cache. The caller must no longer
     * use the array.
     * <p>
     * Small arrays aren't cached and will be ignored by this method.
     */
    @Override
    public void putArray(byte[] array) {
        putArray(byteArrayCache, array, array.length);
    }

    /**
     * This is like getByteArray but for int arrays.
     */
    @Override
    public int[] getIntArray(int size, boolean fillWithZeros) {
        int[] array = getArray(intArrayCache, size);

        if (array == null)
            array = new int[size];
        else if (fillWithZeros)
            Arrays.fill(array, 0);

        return array;
    }

    /**
     * Puts the given int array to the cache. The caller must no longer
     * use the array.
     * <p>
     * Small arrays aren't cached and will be ignored by this method.
     */
    @Override
    public void putArray(int[] array) {
        putArray(intArrayCache, array, array.length);
    }
}
