/**
 *  SimpleARC
 *  a Simple Adaptive Replacement Cache
 *  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.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
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.
 *
 * This class is defined abstract because it shall be used with either the HashARC or the ComparableARC classes
 */

abstract class SimpleARC<K, V> extends AbstractMap<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>>, ARC<K, V> {

    protected int cacheSize;
    protected Map<K, V> levelA, levelB; // we can assume that these maps are synchronized

    /**
     * put a value to the cache.
     * @param s
     * @param v
     */
    @Override
    public final synchronized void insert(final K s, final V v) {
        if (this.levelB.containsKey(s)) {
        	this.levelB.put(s, v);
            assert (this.levelB.size() <= this.cacheSize); // the cache should shrink automatically
        } else {
        	this.levelA.put(s, v);
            assert (this.levelA.size() <= this.cacheSize); // the cache should shrink automatically
        }
    }

    /**
     * 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) {
        if (this.levelB.containsKey(s)) {
            return;
        } else if (this.levelA.containsKey(s)) {
            return;
        } else {
            synchronized (this) {
                // we must repeat the tests again because we did this in a not synchronized environment
                if (this.levelB.containsKey(s)) {
                    return;
                } else if (this.levelA.containsKey(s)) {
                    return;
                } else {
                    this.levelA.put(s, v);
                    assert (this.levelA.size() <= this.cacheSize); // the cache should shrink automatically
                }
            }
        }
    }

    /**
     * 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) {
        synchronized (this) {
            V o = this.levelB.get(s);
            if (o != null) return o;
            o = this.levelA.get(s);
            if (o != null) return o;
            this.levelA.put(s, v);
            assert (this.levelA.size() <= this.cacheSize); // the cache should shrink automatically
            return null;
        }
    }

    /**
     * put a value to the cache.
     * @param s
     * @param v
     */
    @Override
    public final synchronized V put(final K s, final V v) {
        if (this.levelB.containsKey(s)) {
            final V r = this.levelB.put(s, v);
            assert (this.levelB.size() <= this.cacheSize); // the cache should shrink automatically
            return r;
        }
        final V r = this.levelA.put(s, v);
        assert (this.levelA.size() <= this.cacheSize); // the cache should shrink automatically
        return r;
    }

    /**
     * get a value from the cache.
     * @param s
     * @return the value
     */
    @SuppressWarnings("unchecked")
    @Override
    public final V get(final Object s) {
        V v = this.levelB.get(s);
        if (v != null) return v;
        synchronized (this) {
            // we must repeat the get here because another thread may have moved the
            // entry from A to B meanwhile
            v = this.levelB.get(s);
            if (v != null) return v;
            // now get and move the entry to B
            v = this.levelA.remove(s);
            if (v == null) return null;
            // move value from A to B; since it was already removed from A, just put it to B
            //System.out.println("ARC: moving A->B, size(A) = " + this.levelA.size() + ", size(B) = " + this.levelB.size());
            this.levelB.put((K) s, v);
            assert (this.levelB.size() <= this.cacheSize); // the cache should shrink automatically
        }
        return v;
    }

    /**
     * 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>();
        synchronized (this.levelB) {
            for (final Map.Entry<K, V> entry: this.levelB.entrySet()) {
                if (value.equals(entry.getValue())) keys.add(entry.getKey());
            }
        }
        synchronized (this) {
            for (final Map.Entry<K, V> entry: this.levelA.entrySet()) {
                if (value.equals(entry.getValue())) keys.add(entry.getKey());
            }
        }
        return keys;
    }

    /**
     * check if the map contains the key
     * @param s
     * @return
     */
    @Override
    public final boolean containsKey(final Object s) {
        if (this.levelB.containsKey(s)) return true;
        return this.levelA.containsKey(s);
    }


    /**
     * remove an entry from the cache
     * @param s
     * @return the old value
     */
    @Override
    public final synchronized V remove(final Object s) {
        final V r = this.levelB.remove(s);
        if (r != null) return r;
        return this.levelA.remove(s);
    }

    /**
     * clear the cache
     */
    @Override
    public final synchronized void clear() {
        this.levelA.clear();
        this.levelB.clear();
    }

    /**
     * get the size of the ARC. this returns the sum of main and ghost cache
     * @return the complete number of entries in the ARC cache
     */
    @Override
    public final synchronized int size() {
        return this.levelA.size() + this.levelB.size();
    }

    /**
     * iterator implements the Iterable interface
     */
    @Override
    public final Iterator<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 final synchronized Set<Map.Entry<K, V>> entrySet() {
        final Set<Map.Entry<K, V>> m = new HashSet<Map.Entry<K, V>>();
        for (final Map.Entry<K, V> entry: this.levelA.entrySet()) m.add(entry);
        for (final Map.Entry<K, V> entry: this.levelB.entrySet()) m.add(entry);
        return m;
    }

    /**
     * a hash code for this ARC
     * @return the hash code of one of the ARC partial hash tables
     */
    @Override
    public final int hashCode() {
        return this.levelA.hashCode();
    }

}
