/*
 * Written by Dawid Kurzyniec and released to the public domain, as explained
 * at http://creativecommons.org/licenses/publicdomain
 */

package edu.emory.mathcs.util.allocator;

/**
 * Implements the {@link Allocator} using memory buffer pool. Able to enforce
 * memory usage limits; attempts to avoid OutOfMemoryErrors by postponing the
 * allocate requests when there is insufficient memory available in the system.
 * Parameters of the pool are: (1) reserved capacity --
 * if the current memory usage of the pool is below this value, the allocate
 * request will be always attempted without blocking; (2) maximum capacity --
 * if the allocate request would inflate the current memory usage above this
 * value, the call will block until more memory is available; (3) security
 * margin -- if the memory available in the system is below this value, the
 * allocate request will block unless current usage is below reserved capacity.
 *
 * <p>
 * Note: the purpose of this class is to enable control of memory footprint;
 * pooling is NOT intended mainly as a performance optimization. In typical
 * scenarios, allocating memory directly would be faster than reusing buffers
 * through this class, because of associated costs of synchronization.
 * The only possible exception is when buffers are large (in the order
 * of megabytes), when zeroing-out the newly allocated buffer dominates the
 * allocation costs.

 * @author Dawid Kurzyniec
 * @version 1.0
 */

public class PoolingAllocator implements Allocator {
    final BufferPool bufpool;

    private final Runtime runtime;

    private final long reserved, max;
    private final long minLeft;

    private long memOccupied = 0;

    // minimize freeMemory() calls
    private long lastFreeMem = 0;
    private long occupiedOnLastFreeMem = 0;

    /**
     * Constructs pooling allocator with default reserved capacity of
     * 1 MB, unlimited maximum capacity and default security margin
     * of 2 MB.
     */
    public PoolingAllocator() {
        this(1*1024*1024, -1);
    }

   /**
     * Constructs pooling allocator with specified reserved and maximum capacity
     * and default security margin of 2 MB.
     *
     * @param reserved the reserved capacity.
     * @param max the maximum capacity.
     */
    public PoolingAllocator(long reserved, long max) {
        this(reserved, max, 2*1024*1024);
    }

   /**
     * Constructs pooling allocator with specified reserved and maximum capacity
     * and specified security margin.
     *
     * @param reserved the reserved capacity.
     * @param max the maximum capacity.
     * @param minLeft the security margin.
     */
    public PoolingAllocator(long reserved, long max, int minLeft) {
        this(reserved, max, minLeft, new BufferPool());
    }

   /**
     * Constructs pooling allocator with specified reserved and maximum capacity
     * and specified security margin, as well as given buffer pool.
     *
     * @param reserved the reserved capacity.
     * @param max the maximum capacity.
     * @param minLeft the security margin.
     * @param bufpool the buffer pool to use.
     */
    public PoolingAllocator(long reserved, long max, int minLeft, BufferPool bufpool) {
        this.bufpool = bufpool;
        this.reserved = reserved;
        this.max = max;
        this.minLeft = minLeft;
        this.runtime = Runtime.getRuntime();
    }

    public Allocator.Buffer allocate(int size, boolean clear, long timeout)
        throws InterruptedException
    {
        long endTime = -1;
        synchronized (this) {
            while (!canAlloc(size)) {
                // need to wait
                if (timeout < 0) {
                    // wait until notification
                    wait();
                } else if (timeout == 0) {
                    // do not wait
                    return null;
                } else {
                    // wait remaining time
                    long remaining;
                    if (endTime < 0) {
                        endTime = System.currentTimeMillis() + timeout;
                        remaining = timeout;
                    } else {
                        remaining = endTime - System.currentTimeMillis();
                    }
                    if (remaining <= 0) return null;
                    wait(remaining);
                }
            }

            // green light
            memOccupied += size;

            // continue unsynchronized
        }

        byte[] data = bufpool.get(size, clear);
        Buffer buf = new Buffer(data, size);
        if (data.length > size) {
            synchronized (this) {
                memOccupied += (data.length - size);
            }
        }
        return buf;
    }

    private boolean canAlloc(int size) {
        if (max >= 0 && memOccupied + size > max) return false;
        if (memOccupied + size <= reserved) return true;
        if (minLeft < 0) return true;
        long morenew = memOccupied-occupiedOnLastFreeMem;
        // minimize calls to freeMemory(); do it only if memory allocated
        // since the last call exceeded 1MB or 1/16 of the remaining,
        // or if about to return false
        if (morenew > 1024*1024 || morenew > lastFreeMem / 16) {
            lastFreeMem = runtime.freeMemory();
            occupiedOnLastFreeMem = memOccupied;
        }
        if (lastFreeMem >= size + minLeft) return true;
        lastFreeMem = runtime.freeMemory();
        occupiedOnLastFreeMem = memOccupied;
        if (lastFreeMem >= size + minLeft) return true;
        return false;
    }

    private void reclaim(byte[] data) {
        synchronized (this) {
            memOccupied -= data.length;
            notifyAll();
        }
        bufpool.reclaim(data);
    }

    private class Buffer extends Allocator.Buffer {
        Buffer(byte[] data, int size) {
            super(data, size);
        }
        protected void reclaim() {
            PoolingAllocator.this.reclaim(this.data);
        }
    }
}
