/**
 *  ConcurrentARC
 *  Copyright 2009 by Michael Peter Christen, mc@yacy.net, Frankfurt a. M., Germany
 *  First released 17.04.2009 at https://yacy.net
 *
 *  $LastChangedDate$
 *  $LastChangedRevision$
 *  $LastChangedBy$
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program in the file lgpl21.txt
 *  If not, see <http://www.gnu.org/licenses/>.
 */

package net.yacy.cora.storage;

import java.lang.reflect.Array;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;


/**
 * This is a simple cache using two generations of hashtables to store the content with a LFU strategy.
 * The Algorithm is described in a slightly more complex version as Adaptive Replacement Cache, "ARC".
 * For details see http://www.almaden.ibm.com/cs/people/dmodha/ARC.pdf
 * or http://en.wikipedia.org/wiki/Adaptive_Replacement_Cache
 * This version omits the ghost entry handling which is described in ARC, and keeps both cache levels
 * at the same size.
 */

public final class ConcurrentARC<K, V> extends AbstractMap<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>>, ARC<K, V> {

    private final int mask;
    private final ARC<K, V> arc[];

    /**
     * create a concurrent ARC based on a HashARC. The type of the key elements must implement a hashing function
     * @param cacheSize the number of maximum entries
     * @param partitions the number of partitions
     */
    @SuppressWarnings("unchecked")
	public ConcurrentARC(final int cacheSize, final int partitions) {
    	int m = 1;
    	while (m < partitions) m = m * 2;
        int partitionSize = cacheSize / m;
        if (partitionSize < 4) partitionSize = 4;
    	this.arc = (ARC<K, V>[]) Array.newInstance(HashARC.class, m);
    	for (int i = 0; i < this.arc.length; i++) this.arc[i] = new HashARC<K, V>(partitionSize);
    	m -= 1;
    	this.mask = m;
    }

    /**
     * create a concurrent ARC based on a ComparableARC
     * @param cacheSize the number of maximum entries
     * @param partitions the number of partitions
     * @param comparator a comparator for the key object which may be of type byte[]
     */
    @SuppressWarnings("unchecked")
    public ConcurrentARC(final int cacheSize, final int partitions, final Comparator<? super K> comparator) {
        int m = 1;
        while (m < partitions) m = m * 2;
        int partitionSize = cacheSize / m;
        if (partitionSize < 4) partitionSize = 4;
        this.arc = (ARC<K, V>[]) Array.newInstance(ComparableARC.class, m);
        for (int i = 0; i < this.arc.length; i++) this.arc[i] = new ComparableARC<K, V>(partitionSize, comparator);
        m -= 1;
        this.mask = m;
    }

    /**
     * put a value to the cache.
     * @param s
     * @param v
     */
    @Override
    public final void insert(final K s, final V v) {
        this.arc[getPartition(s)].insert(s, v);
    }

    /**
     * put a value to the cache if there was not an entry before
     * do not return a previous content value
     * @param s
     * @param v
     */
    @Override
    public void insertIfAbsent(final K s, final V v) {
        this.arc[getPartition(s)].insertIfAbsent(s, v);
    }

    /**
     * put a value to the cache if there was not an entry before
     * return a previous content value
     * @param s
     * @param v
     * @return the value before inserting the new value
     */
    @Override
    public V putIfAbsent(final K s, final V v) {
        return this.arc[getPartition(s)].putIfAbsent(s, v);
    }

    /**
     * put a value to the cache.
     * @param s
     * @param v
     */
    @Override
    public final V put(final K s, final V v) {
        return this.arc[getPartition(s)].put(s, v);
    }

    /**
     * get a value from the cache.
     * @param s
     * @return the value
     */
    @SuppressWarnings("unchecked")
    @Override
    public final V get(final Object s) {
    	return this.arc[getPartition(s)].get((K) s);
    }

    /**
     * check if the map contains the value
     * @param value
     * @return the keys that have the given value
     */
    @Override
    public Collection<K> getKeys(final V value) {
        final ArrayList<K> keys = new ArrayList<K>();
        for (final ARC<K, V> element : this.arc)
            keys.addAll(element.getKeys(value));
        return keys;
    }

    /**
     * check if the map contains the key
     * @param s
     * @return
     */
    @SuppressWarnings("unchecked")
    @Override
    public final boolean containsKey(final Object s) {
    	return this.arc[getPartition(s)].containsKey((K) s);
    }

    /**
     * remove an entry from the cache
     * @param s
     * @return the old value
     */
    @SuppressWarnings("unchecked")
    @Override
    public final V remove(final Object s) {
    	return this.arc[getPartition(s)].remove((K) s);
    }

    /**
     * clear the cache
     */
    @Override
    public final void clear() {
    	for (final ARC<K, V> a: this.arc) a.clear();
    }

    /**
     * get the size of the ARC.
     * @return the complete number of entries in the ARC cache
     */
    @Override
    public final int size() {
        int s = 0;
        for (final ARC<K, V> a: this.arc) s += a.size();
        return s;
    }

    /**
     * iterator implements the Iterable interface
     */
    @Override
    public Iterator<java.util.Map.Entry<K, V>> iterator() {
        return entrySet().iterator();
    }

    /**
     * Return a Set view of the mappings contained in this map.
     * This method is the basis for all methods that are implemented
     * by a AbstractMap implementation
     *
     * @return a set view of the mappings contained in this map
     */
    @Override
    public Set<java.util.Map.Entry<K, V>> entrySet() {
        final Set<Map.Entry<K, V>> m = new HashSet<Map.Entry<K, V>>();
        for (final ARC<K, V> a: this.arc) {
            for (final Map.Entry<K, V> entry: a.entrySet()) m.add(entry);
        }
        return m;
    }

    /**
     * a hash code for this ARC
     * @return a hash code
     */
    @Override
    public int hashCode() {
        return this.arc.hashCode();
    }

    //private static String latestObject = "";
    /**
     * return in which partition the Object belongs
     * This function uses the objects hashCode() function
     * except for byte[] keys
     * @return the partition number
     */
    private int getPartition(final Object x) {
        if (x instanceof byte[]) {
            int h = 0;
            for (final byte c: (byte[])x) h = 31 * h + (c & 0xFF);
            final int p = h & this.mask;
            //String o = UTF8.String((byte[]) x); try { if (o.equals(latestObject)) throw new RuntimeException("ConcurrentARC: p = " + p + ", objectb = " + o); } catch (final Exception e) { Log.logException(e); } latestObject = o;
            return p;
        }
        final int p = x.hashCode() & this.mask;
        //String o = x.toString(); try { if (o.equals(latestObject)) throw new RuntimeException("ConcurrentARC: p = " + p + ", objecto = " + o); } catch (final Exception e) { Log.logException(e); } latestObject = o;
        return p;
    }

    public static void main(final String[] args) {
        final Random r = new Random();
        final int testsize = 10000;
        final ARC<String, String> a = new ConcurrentARC<String, String>(testsize * 3, Runtime.getRuntime().availableProcessors());
        final Map<String, String> b = new HashMap<String, String>();
        String key, value;
        for (int i = 0; i < testsize; i++) {
            key = "k" + r.nextInt();
            value = "v" + r.nextInt();
            a.insertIfAbsent(key, value);
            b.put(key, value);
        }

        // now put half of the entries AGAIN into the ARC
        int h = testsize / 2;
        for (final Map.Entry<String, String> entry: b.entrySet()) {
            a.put(entry.getKey(), entry.getValue());
            if (h-- <= 0) break;
        }

        // test correctness
        for (final Map.Entry<String, String> entry: b.entrySet()) {
            if (!a.containsKey(entry.getKey())) {
                System.out.println("missing: " + entry.getKey());
                continue;
            }
            if (!a.get(entry.getKey()).equals(entry.getValue())) {
                System.out.println("wrong: a = " + entry.getKey() + "," + a.get(entry.getKey()) + "; v = " + entry.getValue());
            }
        }
        System.out.println("finished test!");
    }

}
