package com.srbenoit.pool;

import java.util.logging.Level;
import com.srbenoit.log.LoggedObject;

/**
 * A pool of objects that can be checked out for use, then checked back in when no longer needed.
 * Objects managed by the pool must be subclasses of <code>AbstractPoolObject</code>.
 *
 * <p>All methods in this class are thread-safe. A request to check out a new object when there are
 * no more available in the pool will result in a new object being created by cloning a prototype
 * object provided in the pool constructor. Therefore, every subclass of <code>
 * AbstractPoolObject</code> must provide a clone method that creates a completely independent
 * copy.
 *
 * <p>If the number of objects in the pool exceeds some threshold amount, some will be released to
 * be garbage collected to bring the total back down to that upper limit.
 *
 * @param  <E>  the type of object this pool manages
 */
public final class Pool<E extends AbstractPoolObject> extends LoggedObject {

    /** a prototype object that we can clone to make new ones */
    private final transient E prototype;

    /** an object on which to synchronize access to this object's data */
    private final transient Object synch;

    /** the maximum size of the pool */
    private final transient int maxPoolSize;

    /** the total number of objects in existence */
    private transient int total;

    /** the total number of available objects */
    private transient int available;

    /** the pool of objects */
    private transient AbstractPoolObject[] objectPool;

    /**
     * Constructs a new <code>Pool</code>.
     *
     * @param  proto        a prototype object that we can clone to make new ones
     * @param  initialSize  the initial number of objects created and placed in the pool
     * @param  maxSize      the maximum number of objects that the pool will store in an available
     *                      state (checked out objects do not count against this maximum)
     */
    public Pool(final E proto, final int initialSize, final int maxSize) {

        super();

        assert (proto != null);
        assert (initialSize >= 0);
        assert (maxSize >= initialSize);

        this.synch = new Object();
        this.prototype = proto;

        // Ensure we're working with a ground-state object as our prototype
        this.prototype.toVirginState();
        this.prototype.setFromPool(null);

        this.maxPoolSize = maxSize;

        this.objectPool = new AbstractPoolObject[initialSize];

        for (int i = 0; i < initialSize; i++) {
            this.objectPool[i] = proto.copy();
        }

        this.total = initialSize;
        this.available = initialSize;
    }

    /**
     * Gets the total number of objects this pool has created but not destroyed.
     *
     * @return  the total number of objects
     */
    public int getTotal() {

        synchronized (this.synch) {
            return this.total;
        }
    }

    /**
     * Gets the number of objects currently available in this pool.
     *
     * @return  the available number of objects
     */
    public int getAvailable() {

        synchronized (this.synch) {
            return this.available;
        }
    }

    /**
     * Checks out an object, creating a new one if the pool is empty.
     *
     * @return  the checked out object
     */
    @SuppressWarnings("unchecked")
    public E checkOut() {

        AbstractPoolObject object;

        synchronized (this.synch) {

            if (this.available > 0) {
                this.available--;
                object = this.objectPool[this.available];
                this.objectPool[this.available] = null;
            } else {
                object = this.prototype.copy();
                this.total++;
            }
        }

        assert object.getFromPool() == null;
        object.setFromPool(this);

        return (E) object;
    }

    /**
     * Checks in a object.
     *
     * @param  obj  the object to check in
     */
    public void checkIn(final AbstractPoolObject obj) {

        int size;
        AbstractPoolObject[] newPool;

        assert obj.getFromPool() == this;
        obj.setFromPool(null);

        synchronized (this.synch) {

            if (this.available == this.maxPoolSize) {

                // Don't put back in pool - allow garbage collection
                obj.die();
                this.total--;
            } else {
                obj.toVirginState();

                if (this.available == this.objectPool.length) {
                    size = this.objectPool.length << 1;

                    if (size > this.maxPoolSize) {
                        size = this.maxPoolSize;
                    }

                    newPool = new AbstractPoolObject[size];
                    System.arraycopy(this.objectPool, 0, newPool, 0, this.available);
                    this.objectPool = newPool;
                }

                this.objectPool[this.available] = obj;
                this.available++;
            }
        }
    }

    /**
     * Instructs the pool to release all pooled objects for garbage collection, then deallocate its
     * internal objects. The pool may not be reused after this method is called.
     */
    public void die() {

        synchronized (this.synch) {

            for (int i = 0; i < this.available; i++) {
                this.objectPool[i].die();
                this.objectPool[i] = null;
            }

            if (this.total > this.available) {
                LOG.log(Level.WARNING,
                    "Pool of {0} terminated while {1} objects were still checked out",
                    new Object[] {
                        this.prototype.getClass().getName(), this.total - this.available
                    });
            }

            this.prototype.die();
            this.objectPool = null;
            this.available = 0;
        }
    }
}
