/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2001-06 Wolfgang M. Meier
 *  wolfgang@exist-db.org
 *  http://exist.sourceforge.net
 *  
 *  This program 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
 *  of the License, or (at your option) any later version.
 *  
 *  This program 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; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *  
 *  $Id: LRUCache.java 2436 2006-01-07 21:47:15Z brihaye $
 */
package org.exist.storage.cache;

import org.exist.storage.CacheManager;
import org.exist.util.hashtable.SequencedLongHashMap;

/**
 * A simple cache implementing a Last Recently Used policy. This
 * cache implementation is based on a 
 * {@link org.exist.util.hashtable.SequencedLongHashMap}. Contrary
 * to the other {@link org.exist.storage.cache.Cache} implementations,
 * LRUCache ignores reference counts or timestamps.
 * 
 * @author wolf
 */
public class LRUCache implements Cache {
    
	private int max;
	private SequencedLongHashMap map;
	
    private Accounting accounting;
	
    private int hitsOld = -1;
	
    private double growthFactor;
    
    private String fileName;
    
    private CacheManager cacheManager = null;
    
	public LRUCache(int size, double growthFactor, double growthThreshold) {
		max = size;
        this.growthFactor = growthFactor;
		map = new SequencedLongHashMap(size * 2);
        accounting = new Accounting(growthThreshold);
        accounting.setTotalSize(max);
	}
	
	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#add(org.exist.storage.cache.Cacheable, int)
	 */
	public void add(Cacheable item, int initialRefCount) {
		add(item);
	}
		
	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#add(org.exist.storage.cache.Cacheable)
	 */
	public void add(Cacheable item) {
		if(map.size() == max) {
			removeOne(item);
		}
		map.put(item.getKey(), item);
	}

	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#get(org.exist.storage.cache.Cacheable)
	 */
	public Cacheable get(Cacheable item) {
		return get(item.getKey());
	}

	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#get(long)
	 */
	public Cacheable get(long key) {
		Cacheable obj = (Cacheable) map.get(key);
		if(obj == null)
			accounting.missesIncrement();
		else
			accounting.hitIncrement();
		return obj;
	}

	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#remove(org.exist.storage.cache.Cacheable)
	 */
	public void remove(Cacheable item) {
		map.remove(item.getKey());
	}

	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#flush()
	 */
	public boolean flush() {
		boolean flushed = false;
		Cacheable cacheable;
		SequencedLongHashMap.Entry next = map.getFirstEntry();
		while(next != null) {
			cacheable = (Cacheable)next.getValue();
			if(cacheable.isDirty()) {
				flushed = flushed | cacheable.sync(false);
			}
			next = next.getNext();
		}
		return flushed;
	}

	
    /* (non-Javadoc)
     * @see org.exist.storage.cache.Cache#hasDirtyItems()
     */
    public boolean hasDirtyItems() {
        Cacheable cacheable;
        SequencedLongHashMap.Entry next = map.getFirstEntry();
        while(next != null) {
        	cacheable = (Cacheable)next.getValue();
        	if(cacheable.isDirty())
        		return true;
        	next = next.getNext();
        }
		return false;
    }
    
	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#getBuffers()
	 */
	public int getBuffers() {
		return max;
	}

	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#getUsedBuffers()
	 */
	public int getUsedBuffers() {
		return map.size();
	}

	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#getHits()
	 */
	public int getHits() {
		return accounting.getHits();
	}

	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#getFails()
	 */
	public int getFails() {
		return accounting.getMisses();
	}
 
    public int getThrashing() {
        return accounting.getThrashing();
    }
    
	/* (non-Javadoc)
	 * @see org.exist.storage.cache.Cache#setFileName(java.lang.String)
	 */
	public void setFileName(String fileName) {
		this.fileName = fileName;
	}
	
    public String getFileName() {
        return fileName;
    }
    
    public SequencedLongHashMap.Entry getFirst() {
        return map.getFirstEntry();
    }
    
	private final void removeOne(Cacheable item) {
		boolean removed = false;
		SequencedLongHashMap.Entry next = map.getFirstEntry();
		do {
			Cacheable cached = (Cacheable)next.getValue();
			if(cached.allowUnload() && cached.getKey() != item.getKey()) {
				cached.sync(true);
				map.remove(next.getKey());
				removed = true;
			} else {
				next = next.getNext();
				if(next == null) {
					LOG.debug("Unable to remove entry");
					next = map.getFirstEntry();
				}
			}
		} while(!removed);
        accounting.replacedPage(item);
        if (growthFactor > 1.0 && accounting.resizeNeeded()) {
            cacheManager.requestMem(this);
        }
	}

    /* (non-Javadoc)
     * @see org.exist.storage.cache.Cache#getGrowthFactor()
     */
    public double getGrowthFactor() {
        return growthFactor;
    }

    /* (non-Javadoc)
     * @see org.exist.storage.cache.Cache#setCacheManager(org.exist.storage.CacheManager)
     */
    public void setCacheManager(CacheManager manager) {
        this.cacheManager = manager;
    }
    
    /* (non-Javadoc)
     * @see org.exist.storage.cache.Cache#resize(int)
     */
    public void resize(int newSize) {
        if (newSize < max) {
            shrink(newSize);
        } else {
            SequencedLongHashMap newMap = new SequencedLongHashMap(newSize * 2);
            SequencedLongHashMap.Entry next = map.getFirstEntry();
            Cacheable cacheable;
            while(next != null) {
                cacheable = (Cacheable)next.getValue();
                newMap.put(cacheable.getKey(), cacheable);
                next = next.getNext();
            }
            max = newSize;
            map = newMap;
            accounting.reset();
            accounting.setTotalSize(max);
        }
    }
    
    private void shrink(int newSize) {
        flush();
        this.map = new SequencedLongHashMap(newSize);
        this.max = newSize;
        accounting.reset();
        accounting.setTotalSize(max);
    }

    public int getLoad() {
        if (hitsOld == 0) {
            hitsOld = accounting.getHits();
            return Integer.MAX_VALUE;
        }
        int load = accounting.getHits() - hitsOld;
        hitsOld = accounting.getHits();
        return load;
    }
}
