package freenet.support;

import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import freenet.support.Logger.LogLevel;

/**
 * An LRU map from K to V. That is, when a mapping is added, it is 
 * pushed to the top of the queue, even if it was already present, and 
 * pop/peek operate from the bottom of the queue i.e. the least recently 
 * pushed. The caller must implement any size limit needed.
 * FIXME most callers should be switched to LinkedHashMap.
 * Does not support null keys.
 * @param <K> The key type.
 * @param <V> The value type.
 */
public class LRUMap<K, V> {
	private static volatile boolean logMINOR;

	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	/** We use our own DoublyLinkedList implementation because it improves 
	 * performance to be able to inherit from and refer to QItem's directly. */
	private final DoublyLinkedListImpl<QItem<K, V>> list = new DoublyLinkedListImpl<QItem<K, V>>();
    private final Map<K, QItem<K, V>> hash;
    
    public LRUMap() {
    	hash = new HashMap<K, QItem<K, V>>();
    }
    
    /** Takes an arbitrary map */
    private LRUMap(Map<K, QItem<K, V>> map) {
    	hash = map;
    }
    
    /** Create a LRUMap that is safe to use with keys that can be
     * controlled by an attacker. Meaning one based on a TreeMap, not a 
     * HashMap (think hash collision DoS's). */
    public static<K extends Comparable<K>,V> LRUMap<K,V> createSafeMap() {
    	return new LRUMap<K,V>(new TreeMap<K, QItem<K,V>>());
    }
    
    /** Create a LRUMap that is safe to use with keys that can be
     * controlled by an attacker. Meaning one based on a TreeMap, not a 
     * HashMap (think hash collision DoS's). */
    public static<K,V> LRUMap<K,V> createSafeMap(Comparator<K> comparator) {
    	return new LRUMap<K,V>(new TreeMap<K, QItem<K,V>>(comparator));
    }
    
    /**
     *       push()ing an object that is already in
     *       the queue moves that object to the most
     *       recently used position, but doesn't add
     *       a duplicate entry in the queue.
     */
    public final synchronized V push(K key, V value) {
    	if(key == null)
    		throw new NullPointerException();
    	V old = null;
        QItem<K,V> insert = hash.get(key);
        if (insert == null) {
            insert = new QItem<K, V>(key, value);
            hash.put(key,insert);
        } else {
            old = insert.value;
        	insert.value = value;
            list.remove(insert);
        }
        if(logMINOR)
        	Logger.minor(this, "Pushed "+insert+" ( "+key+ ' ' +value+" )");

        list.unshift(insert);
        return old;
    } 

    /**
     *  @return Least recently pushed key.
     */
    public final synchronized K popKey() {
        if (!list.isEmpty()) {
			return hash.remove(list.pop().obj).obj;
        } else {
            return null;
        }
    }

    /**
     * @return Least recently pushed value.
     */
    public final synchronized V popValue() {
        if (!list.isEmpty()) {
			return hash.remove(list.pop().obj).value;
        } else {
            return null;
        }
    }
    
	public final synchronized V peekValue() {
        if (!list.isEmpty()) {
			return hash.get(list.tail().obj).value;
        } else {
            return null;
        }
	}

	public final synchronized K peekKey() {
        if (!list.isEmpty()) {
			return hash.get(list.tail().obj).obj;
        } else {
            return null;
        }
	}

    public final int size() {
        return list.size();
    }
    
    public final synchronized boolean removeKey(K key) {
    	if(key == null)
    		throw new NullPointerException();
	QItem<K,V> i = (hash.remove(key));
	if(i != null) {
	    list.remove(i);
	    return true;
	} else {
	    return false;
	}
    }
    
    /**
     * Check if this queue contains obj
     * @param obj Object to match
     * @return true if this queue contains obj.
     */
    public final synchronized boolean containsKey(K key) {
    	if(key == null)
    		throw new NullPointerException();
        return hash.containsKey(key);
    }
    
    /**
     * Note that this does not automatically promote the key. You have
     * to do that by hand with push(key, value).
     */
    public final synchronized V get(K key) {
    	if(key == null)
    		throw new NullPointerException();
    	QItem<K,V> q = hash.get(key);
    	if(q == null) return null;
    	return q.value;
    }
    
	public Enumeration<K> keys() {
        return new ItemEnumeration();
    }
    
	public Enumeration<V> values() {
    	return new ValuesEnumeration();
    }

	private class ItemEnumeration implements Enumeration<K> {
		private Enumeration<QItem<K, V>> source = list.reverseElements();
       
        @Override
        public boolean hasMoreElements() {
        	synchronized(LRUMap.this) {
        		return source.hasMoreElements();
        	}
        }

		@Override
		public K nextElement() {
        	synchronized(LRUMap.this) {
        		return source.nextElement().obj;
        	}
        }
    }

	private class ValuesEnumeration implements Enumeration<V> {
		private Enumeration<QItem<K, V>> source = list.reverseElements();
       
        @Override
        public boolean hasMoreElements() {
        	synchronized(LRUMap.this) {
        		return source.hasMoreElements();
        	}
        }

		@Override
		public V nextElement() {
        	synchronized(LRUMap.this) {
        		return source.nextElement().value;
        	}
        }
    }

	public static class QItem<K, V> extends DoublyLinkedListImpl.Item<QItem<K, V>> {
        public K obj;
        public V value;

        public QItem(K key, V val) {
            this.obj = key;
            this.value = val;
        }
        
		@Override
        public String toString() {
        	return super.toString()+": "+obj+ ' ' +value;
        }
    }

	public boolean isEmpty() {
		return list.isEmpty();
	}

	/**
	 * Note that unlike the java.util versions, this will not reallocate (hence it doesn't return), 
	 * so pass in an appropriately big array, and make sure you hold the lock!
	 * @param entries
	 * @return
	 */
	public synchronized void valuesToArray(V[] entries) {
		Enumeration<V> values = values();
		int i=0;
		while(values.hasMoreElements())
			entries[i++] = values.nextElement();
	}

	public synchronized void clear() {
		list.clear();
		hash.clear();
	}
}
