/** 
 * 
 * Copyright 2004 Hiram Chirino
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 * 
 **/
package org.activemq.io.util;

import java.util.HashMap;
import java.util.Iterator;

import org.activemq.message.ActiveMQMessage;
import org.activemq.service.QueueListEntry;
import org.activemq.service.impl.DefaultQueueList;
import org.activemq.store.cache.MessageCache;

/**
 * A simple cache that stores messages in memory.  Cache entries are evicted 
 * when the memoryManager starts to run short on memory (A LRU cache is used).
 *
 * @version $Revision: 1.1.1.1 $
 */
public class MemoryBoundedMessageCache implements MessageCache, MemoryBoundedObject {

    private static final int OBJECT_OVERHEAD = 50;

    private final MemoryBoundedObjectManager memoryManager;
    
    /** msgId -> LRUNode */
    private final HashMap messages = new HashMap();
    /** Ordered list of messageIds recently used at the front */
    private final DefaultQueueList lruList = new DefaultQueueList();

    private int memoryUsedByThisCache;
    private float growthLimit = 0.75f;
    private boolean closed;
    
    /** Used associate the a Message to it's QueueListEntry in the lruList */
    private static class CacheNode {        
        ActiveMQMessage message;
        QueueListEntry entry;
    }
	
	public MemoryBoundedMessageCache(MemoryBoundedObjectManager memoryManager) {
        this.memoryManager = memoryManager;
        this.memoryManager.add(this);
	}
	
	/**
	 * Gets a message that was previously <code>put</code> into this object.   
	 * 
	 * @param msgid
	 * @return null if the message was not previously put or if the message has expired out of the cache.
	 */
	synchronized public ActiveMQMessage get(String msgid) {
        CacheNode rc  = (CacheNode) messages.get(msgid);
        if( rc != null ) {
            // Move to front (the recently used part of the list).
            lruList.remove(rc.entry);
            rc.entry = lruList.addFirst(msgid);
            return rc.message;
        }
        return null;
	}

	/**
	 * Puts a message into the cache.
	 * 
	 * @param messageID
	 * @param message
	 */
	synchronized public void put(String messageID, ActiveMQMessage message) {
        
        // Drop old messages until there is space.
        while( isFull() && !messages.isEmpty() ) {
            removeOldest();
        }
        
        if( !isFull() ) {
            incrementMemoryUsed(message);
            CacheNode newNode = new CacheNode();
            newNode.message = message;
            newNode.entry = lruList.addFirst(messageID);            
            CacheNode oldNode = (CacheNode) messages.put(messageID, newNode);
            if( oldNode !=null ) {
                lruList.remove(oldNode);
                decrementMemoryUsed(oldNode.message);
            }        
        }
	}

    private void removeOldest() {
        String messageID = (String) lruList.removeLast();
        CacheNode node = (CacheNode) messages.remove(messageID);
        decrementMemoryUsed(node.message);
    }

    private boolean isFull() {
        return memoryManager.getPercentFull() > growthLimit;
    }

    /**
	 * Remvoes a message from the cache.
	 * 
	 * @param messageID
	 */
	synchronized public void remove(String messageID) {
        CacheNode node = (CacheNode) messages.remove(messageID);
        if( node !=null ) {
            lruList.remove(node.entry);
            decrementMemoryUsed(node.message);
        }        
	}
    
    private void incrementMemoryUsed(ActiveMQMessage packet) {
        if (packet != null) {
            int size = OBJECT_OVERHEAD;
            if (packet != null) {
                if (packet.incrementMemoryReferenceCount() == 1) {
                    size += packet.getMemoryUsage();
                }
            }
            synchronized( this ) {
                memoryUsedByThisCache += size;
            }
            memoryManager.incrementMemoryUsed(size);
        }
    }

    private void decrementMemoryUsed(ActiveMQMessage packet) {
        if (packet != null) {
            int size = OBJECT_OVERHEAD;
            if (packet != null) {
                if (packet.decrementMemoryReferenceCount() == 0) {
                    size += packet.getMemoryUsage();
                }
            }
                        
            synchronized( this ) {
                memoryUsedByThisCache -= size;
            }
            memoryManager.decrementMemoryUsed(size);
        }
    }

    /**
     * @return returns the percentage of memory usage at which that cache will stop to grow.
     */
    public float getGrowthLimit() {
        return growthLimit;
    }
    
    /**
     * @param growTillFence the percentage of memory usage at which that cache will stop to grow.
     */
    public void setGrowthLimit(float growTillFence) {
        this.growthLimit = growTillFence;
    }

    synchronized public void close() {
        if( closed ) 
            return;
        closed=true;
        
        for (Iterator iter = messages.values().iterator(); iter.hasNext();) {
            CacheNode node = (CacheNode) iter.next();
            decrementMemoryUsed(node.message);
        }
        messages.clear();
        lruList.clear();
        
        memoryManager.remove(this);
    }
}
