//  Copyright 2004 The Apache Software Foundation
//
// 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.apache.tapestry.util.pool;

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

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tapestry.ApplicationRuntimeException;
import org.apache.tapestry.IMarkupWriter;
import org.apache.tapestry.Tapestry;
import org.apache.tapestry.util.AdaptorRegistry;
import org.apache.tapestry.util.ICleanable;
import org.apache.tapestry.util.IRenderDescription;
import org.apache.tapestry.util.JanitorThread;

/**
 *  A Pool is used to pool instances of a useful class.  It uses
 *  keys, much like a {@link Map}, to identify a list of pooled objects.
 *  Retrieving an object from the Pool atomically removes it from the
 *  pool.  It can then be stored again later.  In this way, a single
 *  Pool instance can manage many different types of pooled objects,
 *  filed under different keys.
 * 
 *  <p>
 *  Unlike traditional Pools, this class does not create new instances of
 *  the objects it stores (with the exception of simple Java Beans,
 *  via {@link #retrieve(Class)}.  The usage pattern is to retrieve an instance
 *  from the Pool, and if the instance is null, create a new instance.
 *
 *  <p>The implementation of Pool is threadsafe.
 *
 *  <p>Pool implements {@link ICleanable}, with a goal of
 *  only keeping pooled objects that have been needed within
 *  a recent time frame.  A generational system is used, where each
 *  pooled object is assigned a generation count.  {@link #executeCleanup}
 *  discards objects whose generation count is too old (outside of a
 *  {@link #getWindow() window}).
 * 
 *  <p>
 *  Objects in the pool can receive two notifications: one notification
 *  when they are {@link #store(Object, Object) stored} into the pool,
 *  and one when they are discarded from the pool.
 * 
 *  <p>
 *  Classes that implement {@link org.apache.tapestry.util.pool.IPoolable}
 *  receive notifications directly, as per the two methods
 *  of that interface.
 * 
 *  <p>
 *  Alternately, an adaptor for the other classes can be
 *  registerered (using {@link #registerAdaptor(Class, IPoolableAdaptor)}.
 *  The adaptor will be invoked to handle the notification when a 
 *  pooled object is stored or discarded.
 *
 *  @author Howard Lewis Ship
 *  @version $Id: Pool.java 243791 2004-02-19 17:38:13Z hlship $
 *
 **/

public class Pool implements ICleanable, IRenderDescription
{
    private static final Log LOG = LogFactory.getLog(Pool.class);

    private AdaptorRegistry _adaptors = new AdaptorRegistry();

    /**
     *  The generation, used to cull unused pooled items.
     *
     *  @since 1.0.5
     **/

    private int _generation;

    /**
     *  The generation window, used to identify which
     *  items should be culled.
     *
     *  @since 1.0.5
     **/

    private int _window = 10;

    /**
     *  The number of objects pooled.
     *
     **/

    private int _pooledCount;

    /**
     *  A map of PoolLists, keyed on an arbitrary object.
     *
     **/

    private Map _map;

    /**
     *  Creates a new Pool using the default map size.  Creation of the map is deferred.
     *
     *
     **/

    public Pool()
    {
        this(true);
    }

    /**
     *  Creates a new Pool using the specified map size.  The map is created immediately.
     *
     *  @deprecated Use {@link #Pool()} instead.
     * 
     **/

    public Pool(int mapSize)
    {
        this(mapSize, true);
    }

    /**
     *  @param useSharedJanitor if true, then the Pool is added to
     *  the {@link JanitorThread#getSharedJanitorThread() shared janitor}.
     *
     *  @since 1.0.5
     *
     **/

    public Pool(boolean useSharedJanitor)
    {
        if (useSharedJanitor)
            JanitorThread.getSharedJanitorThread().add(this);

        registerAdaptors();
    }

    /**
     *  Standard constructor.
     *
     *  @param mapSize initial size of the map.
     *  @param useSharedJanitor if true, then the Pool is added to
     *  the {@link JanitorThread#getSharedJanitorThread() shared janitor}.
     *
     *  @since 1.0.5
     *  @deprecated Use {@link #Pool(boolean)} instead.
     * 
     **/

    public Pool(int mapSize, boolean useSharedJanitor)
    {
        this(useSharedJanitor);

        _map = new HashMap(mapSize);
    }

    /**
     *  Returns the window used to cull pooled objects during a cleanup.
     *  The default is 10, which works out to about five minutes with
     *  a standard janitor (on a 30 second cycle).
     *
     *  @since 1.0.5
     *
     **/

    public int getWindow()
    {
        return _window;
    }

    /**
     *  Sets the window, or number of generations that an object may stay
     *  in the pool before being culled.
     *
     *  @throws IllegalArgumentException if value is less than 1.
     *
     *  @since 1.0.5
     **/

    public void setWindow(int value)
    {
        if (value < 1)
            throw new IllegalArgumentException("Pool window may not be less than 1.");

        _window = value;
    }

    /**
     *  Returns a previously pooled object with the given key, or null if no
     *  such object exists.  Getting an object from a Pool removes it from the Pool,
     *  but it can later be re-added with {@link #store(Object,Object)}.
     *
     **/

    public synchronized Object retrieve(Object key)
    {
        Object result = null;

        if (_map == null)
            _map = new HashMap();

        PoolList list = (PoolList) _map.get(key);

        if (list != null)
            result = list.retrieve();

        if (result != null)
            _pooledCount--;

        if (LOG.isDebugEnabled())
            LOG.debug("Retrieved " + result + " from " + key);

        return result;
    }

    /**
     *  Retrieves an instance of the named class.  If no pooled
     *  instance is available, a new instance is created
     *  (using the no arguments constructor).  Objects are
     *  pooled using their actual class as a key.
     * 
     *  <p>
     *  However, don't be fooled by false economies.  Unless
     *  an object is very expensive to create, pooling is 
     *  <em>more</em> expensive than simply instantiating temporary
     *  instances and letting the garbage collector deal with it
     *  (this is counter intuitive, but true).  For example,
     *  this method was originally created to allow pooling
     *  of {@link StringBuffer}, but testing showed that it
     *  was a net defecit.
     * 
     **/

    public Object retrieve(Class objectClass)
    {
        Object result = retrieve((Object) objectClass);

        if (result == null)
        {
            if (LOG.isDebugEnabled())
                LOG.debug("No instance of " + objectClass.getName() + " is available, instantiating one.");

            try
            {
                result = objectClass.newInstance();
            }
            catch (Exception ex)
            {
                throw new ApplicationRuntimeException(
                    Tapestry.format("Pool.unable-to-instantiate-instance", objectClass.getName()),
                    ex);
            }
        }

        return result;
    }

    /**
     *  Stores an object using its class as a key.
     * 
     *  @see #retrieve(Class)
     * 
     **/

    public void store(Object object)
    {
        store(object.getClass(), object);
    }

    /**
     *  Stores an object in the pool for later retrieval, resetting
     *  the object for storage within the pool.
     *
     **/

    public synchronized void store(Object key, Object object)
    {
        getAdaptor(object).resetForPool(object);

        if (_map == null)
            _map = new HashMap();

        PoolList list = (PoolList) _map.get(key);

        if (list == null)
        {
            list = new PoolList(this);
            _map.put(key, list);
        }

        int count = list.store(_generation, object);

        _pooledCount++;

        if (LOG.isDebugEnabled())
            LOG.debug("Stored " + object + " into " + key + " (" + count + " pooled)");
    }

    /**
     *  Removes all previously pooled objects from this Pool.
     *
     **/

    public synchronized void clear()
    {
        if (_map != null)
        {
            Iterator i = _map.values().iterator();

            while (i.hasNext())
            {
                PoolList list = (PoolList) i.next();

                list.discardAll();
            }

            _map.clear();
        }

        _pooledCount = 0;

        if (LOG.isDebugEnabled())
            LOG.debug("Cleared");
    }

    /**
     *  Returns the number of object pooled, the sum of the number
     *  of objects in pooled under each key. 
     *
     *  @since 1.0.2
     **/

    public synchronized int getPooledCount()
    {
        return _pooledCount;
    }

    /**
     *  Returns the number of keys within the pool.
     *
     *  @since 1.0.2
     **/

    public synchronized int getKeyCount()
    {
        if (_map == null)
            return 0;

        return _map.size();
    }

    /**
     *  Peforms culling of unneeded pooled objects.
     *
     *  @since 1.0.5
     *
     **/

    public synchronized void executeCleanup()
    {
        if (_map == null)
            return;

        if (LOG.isDebugEnabled())
            LOG.debug("Executing cleanup of " + this);

        _generation++;

        int oldestGeneration = _generation - _window;

        if (oldestGeneration < 0)
            return;

        int oldCount = _pooledCount;
        int culledKeys = 0;

        // During the cleanup, we keep the entire instance synchronized
        // (meaning other threads will block when trying to store
        // or retrieved pooled objects).  Fortunately, this
        // should be pretty darn quick!

        int newCount = 0;

        Iterator i = _map.entrySet().iterator();
        while (i.hasNext())
        {
            Map.Entry e = (Map.Entry) i.next();

            PoolList list = (PoolList) e.getValue();

            int count = list.cleanup(oldestGeneration);

            if (count == 0)
            {
                i.remove();
                culledKeys++;
            }
            else
                newCount += count;
        }

        _pooledCount = newCount;

        if (LOG.isDebugEnabled())
            LOG.debug("Culled " + (oldCount - _pooledCount) + " pooled objects and " + culledKeys + " keys.");
    }

    public synchronized String toString()
    {
        ToStringBuilder builder = new ToStringBuilder(this);

        builder.append("generation", _generation);
        builder.append("pooledCount", _pooledCount);

        return builder.toString();
    }

    /** @since 1.0.6 **/

    public synchronized void renderDescription(IMarkupWriter writer)
    {
        writer.begin("table");
        writer.attribute("border", "1");
        writer.println();

        writer.begin("tr");
        writer.begin("th");
        writer.attribute("colspan", "2");
        writer.print(toString());
        writer.end();
        writer.end();
        writer.println();

        if (_map != null)
        {
            Iterator i = _map.entrySet().iterator();

            while (i.hasNext())
            {
                Map.Entry entry = (Map.Entry) i.next();
                PoolList list = (PoolList) entry.getValue();

                writer.begin("tr");
                writer.begin("td");
                writer.print(entry.getKey().toString());
                writer.end();
                writer.begin("td");
                writer.print(list.getPooledCount());
                writer.end();
                writer.end();

                writer.println();
            }
        }
    }

    /**
     *  Invoked from the constructor to register the default set of
     *  {@link org.apache.tapestry.util.pool.IPoolableAdaptor}.  Subclasses
     *  may override this to register a different set.
     * 
     *  <p>
     *  Registers:
     *  <ul>
     *  <li>{@link NullPoolableAdaptor} for class Object
     *  <li>{@link DefaultPoolableAdaptor} for interface {@link IPoolable}
     *  <li>{@link StringBufferAdaptor} for {@link StringBuffer}
     *  </ul>
     * 
     *  @since 3.0
     * 
     **/

    protected void registerAdaptors()
    {
        registerAdaptor(Object.class, new NullPoolableAdaptor());
        registerAdaptor(IPoolable.class, new DefaultPoolableAdaptor());
        registerAdaptor(StringBuffer.class, new StringBufferAdaptor());
    }

    /**
     *  Registers an adaptor for a particular class (or interface).
     * 
     *  @since 3.0
     * 
     **/

    public void registerAdaptor(Class registrationClass, IPoolableAdaptor adaptor)
    {
        _adaptors.register(registrationClass, adaptor);
    }

    /**
     *  Returns an adaptor appropriate to the object.
     * 
     **/

    public IPoolableAdaptor getAdaptor(Object object)
    {
        return (IPoolableAdaptor) _adaptors.getAdaptor(object.getClass());
    }
}