//jTDS JDBC Driver for Microsoft SQL Server and Sybase
//Copyright (C) 2004 The jTDS Project
//
//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 library; if not, write to the Free Software
//Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
package net.sourceforge.jtds.jdbc.cache;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;

import net.sourceforge.jtds.jdbc.ProcEntry;

/**
 * LRU cache for procedures and statement handles.
 *
 * @version $Id: ProcedureCache.java,v 1.0, 2006-03-15 17:47:17Z, Robert Worsnop$
 */
public class ProcedureCache implements StatementCache {

    /**
     * Encapsulates the cached Object and implements the linked list used to
     * implement the LRU logic.
     */
    private static class CacheEntry {
        String key;
        ProcEntry value;
        CacheEntry next;
        CacheEntry prior;

        /**
         * Constructs a new cache entry encapsulating the supplied key and
         * value.
         *
         * @param key   key used to identify the cache entry
         * @param value object being cached
         */
        CacheEntry(String key, ProcEntry value) {
            this.key = key;
            this.value = value;
        }

        /**
         * Unlinks this CacheEntry from the linked list.
         */
        void unlink() {
            next.prior = prior;
            prior.next = next;
        }

        /**
         * Links this CacheEntry into the linked list after the node specified.
         *
         * @param ce node after which this entry will be linked
         */
        void link(CacheEntry ce) {
            next = ce.next;
            prior = ce;
            next.prior = this;
            ce.next = this;
        }
    }

    /** The maximum initial HashMap size. */
    private static final int MAX_INITIAL_SIZE = 50;
    /** The actual cache instance. */
    private HashMap cache;
    /** Maximum cache size or 0 to disable. */
    int cacheSize;
    /** Head node of the linked list. */
    CacheEntry head;
    /** Tail node of the linked list. */
    CacheEntry tail;
    /** List of redundant cache entries. */
    ArrayList free;

    /**
     * Constructs a new statement cache.
     *
     * @param cacheSize maximum cache size or 0 to disable caching
     */
    public ProcedureCache(int cacheSize) {
        this.cacheSize = cacheSize;
        cache = new HashMap(Math.min(MAX_INITIAL_SIZE, cacheSize) + 1);
        head  = new CacheEntry(null, null);
        tail  = new CacheEntry(null, null);
        head.next = tail;
        tail.prior = head;
        free = new ArrayList();
    }

    /**
     * Retrieves a ProcEntry object from the cache.
     * <p/>
     * If the entry exists it is moved to the front of the linked list to keep
     * it alive as long as possible.
     *
     * @param key the key value identifying the required entry
     * @return the keyed entry as an <code>Object</code> or null if the entry
     *         does not exist
     */
    public synchronized Object get(String key) {
        CacheEntry ce = (CacheEntry) cache.get(key);
        if (ce != null) {
            // remove entry from linked list
            ce.unlink();
            // Relink at Head
            ce.link(head);
            // Increment usage count
            ce.value.addRef();

            return ce.value;
        }
        return null;
    }

    /**
     * Inserts a new entry, identified by a key, into the cache.
     * <p/>
     * If the cache is full then one or more entries are removed and
     * transferred to a list for later destruction.
     *
     * @param key    value used to identify the entry
     * @param handle proc entry to be inserted into the cache
     */
    public synchronized void put(String key, Object handle) {
        // Increment usage count
        ((ProcEntry) handle).addRef();

        // Add new entry to cache
        CacheEntry ce = new CacheEntry(key, (ProcEntry) handle);
        cache.put(key, ce);
        ce.link(head);

        // See if we need to scavenge some existing entries
        scavengeCache();
    }

    /**
     * Removes a redundant entry from the cache.
     *
     * @param key value that identifies the cache entry
     */
    public synchronized void remove(String key) {
        CacheEntry ce = (CacheEntry) cache.get(key);
        if (ce != null) {
            // remove entry from linked list
            ce.unlink();
            // Remove from HashMap
            cache.remove(key);
        }
    }

    /**
     * Obtains a list of statement handles or procedures that can now be
     * dropped.
     *
     * @param handles a collection of single use statements that will be
     *                returned for dropping if the cache is disabled
     * @return the collection of redundant statments for dropping
     */
    public synchronized Collection getObsoleteHandles(Collection handles) {
        if (handles != null) {
            // Update the usage count for handles belonging to statements
            // that are being closed.
            for (Iterator iterator = handles.iterator(); iterator.hasNext();) {
                ProcEntry handle = (ProcEntry) iterator.next();
                handle.release();
            }
        }

        // Scavenge some existing entries
        scavengeCache();

        if (free.size() > 0) {
            // There are redundant entries to drop
            Collection list = free;
            free = new ArrayList();
            return list;
        } else {
            // Nothing to do this time
            return null;
        }
    }

    /**
     * Removes unused entries trying to bring down the cache to the requested
     * size. The removed entries are placed in the {@link #free} list.
     * <p/>
     * <b>Note:</b> entries that are in use will not be removed so it is
     * possible for the cache to still be larger than {@link #cacheSize} after
     * the call finishes.
     */
    private void scavengeCache() {
        CacheEntry ce = tail.prior;
        while (ce != head && cache.size() > cacheSize) {
            if (ce.value.getRefCount() == 0) {
                // remove entry from linked list
                ce.unlink();
                // Add to free list for reclaiming
                free.add(ce.value);
                // Remove from HashMap
                cache.remove(ce.key);
            }
            ce = ce.prior;
        }
    }
}
