/*
 *  Copyright 2005-2006 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.commons.collections.buffer;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Iterator;

import org.apache.commons.collections.BoundedCollection;
import org.apache.commons.collections.Buffer;
import org.apache.commons.collections.BufferOverflowException;
import org.apache.commons.collections.BufferUnderflowException;
import org.apache.commons.collections.iterators.AbstractIteratorDecorator;

/**
 * Decorates another <code>Buffer</code> to ensure a fixed maximum size.
 * <p>
 * Note: This class should only be used if you need to add bounded
 * behaviour to another buffer. If you just want a bounded buffer then
 * you should use {@link BoundedFifoBuffer} or {@link CircularFifoBuffer}.
 * <p>
 * The decoration methods allow you to specify a timeout value.
 * This alters the behaviour of the add methods when the buffer is full.
 * Normally, when the buffer is full, the add method will throw an exception.
 * With a timeout, the add methods will wait for up to the timeout period
 * to try and add the elements.
 *
 * @author James Carman
 * @author Stephen Colebourne
 * @version $Revision: 405927 $ $Date: 2006-05-12 23:57:03 +0100 (Fri, 12 May 2006) $
 * @since Commons Collections 3.2
 */
public class BoundedBuffer extends SynchronizedBuffer implements BoundedCollection {

    /** The serialization version. */
    private static final long serialVersionUID = 1536432911093974264L;

    /** The maximum size. */
    private final int maximumSize;
    /** The timeout milliseconds. */
    private final long timeout;

    /**
     * Factory method to create a bounded buffer.
     * <p>
     * When the buffer is full, it will immediately throw a
     * <code>BufferOverflowException</code> on calling <code>add()</code>.
     *
     * @param buffer  the buffer to decorate, must not be null
     * @param maximumSize  the maximum size, must be size one or greater
     * @return a new bounded buffer
     * @throws IllegalArgumentException if the buffer is null
     * @throws IllegalArgumentException if the maximum size is zero or less
     */
    public static BoundedBuffer decorate(Buffer buffer, int maximumSize) {
        return new BoundedBuffer(buffer, maximumSize, 0L);
    }

    /**
     * Factory method to create a bounded buffer that blocks for a maximum
     * amount of time.
     *
     * @param buffer  the buffer to decorate, must not be null
     * @param maximumSize  the maximum size, must be size one or greater
     * @param timeout  the maximum amount of time to wait in milliseconds
     * @return a new bounded buffer
     * @throws IllegalArgumentException if the buffer is null
     * @throws IllegalArgumentException if the maximum size is zero or less
     */
    public static BoundedBuffer decorate(Buffer buffer, int maximumSize, long timeout) {
        return new BoundedBuffer(buffer, maximumSize, timeout);
    }

    //-----------------------------------------------------------------------
    /**
     * Constructor that wraps (not copies) another buffer, making it bounded
     * waiting only up to a maximum amount of time.
     *
     * @param buffer  the buffer to wrap, must not be null
     * @param maximumSize  the maximum size, must be size one or greater
     * @param timeout  the maximum amount of time to wait
     * @throws IllegalArgumentException if the buffer is null
     * @throws IllegalArgumentException if the maximum size is zero or less
     */
    protected BoundedBuffer(Buffer buffer, int maximumSize, long timeout) {
        super(buffer);
        if (maximumSize < 1) {
            throw new IllegalArgumentException();
        }
        this.maximumSize = maximumSize;
        this.timeout = timeout;
    }

    //-----------------------------------------------------------------------
    public Object remove() {
        synchronized (lock) {
            Object returnValue = getBuffer().remove();
            lock.notifyAll();
            return returnValue;
        }
    }

    public boolean add(Object o) {
        synchronized (lock) {
            timeoutWait(1);
            return getBuffer().add(o);
        }
    }

    public boolean addAll(final Collection c) {
        synchronized (lock) {
            timeoutWait(c.size());
            return getBuffer().addAll(c);
        }
    }

    public Iterator iterator() {
        return new NotifyingIterator(collection.iterator());
    }

    private void timeoutWait(final int nAdditions) {
        // method synchronized by callers
        if (nAdditions > maximumSize) {
            throw new BufferOverflowException(
                    "Buffer size cannot exceed " + maximumSize);
        }
        if (timeout <= 0) {
            // no wait period (immediate timeout)
            if (getBuffer().size() + nAdditions > maximumSize) {
                throw new BufferOverflowException(
                        "Buffer size cannot exceed " + maximumSize);
            }
            return;
        }
        final long expiration = System.currentTimeMillis() + timeout;
        long timeLeft = expiration - System.currentTimeMillis();
        while (timeLeft > 0 && getBuffer().size() + nAdditions > maximumSize) {
            try {
                lock.wait(timeLeft);
                timeLeft = expiration - System.currentTimeMillis();
            } catch (InterruptedException ex) {
                PrintWriter out = new PrintWriter(new StringWriter());
                ex.printStackTrace(out);
                throw new BufferUnderflowException(
                    "Caused by InterruptedException: " + out.toString());
            }
        }
        if (getBuffer().size() + nAdditions > maximumSize) {
            throw new BufferOverflowException("Timeout expired");
        }
    }

    public boolean isFull() {
        // size() is synchronized
        return (size() == maxSize());
    }

    public int maxSize() {
        return maximumSize;
    }

    //-----------------------------------------------------------------------
    /**
     * BoundedBuffer iterator.
     */
    private class NotifyingIterator extends AbstractIteratorDecorator {

        public NotifyingIterator(Iterator it) {
            super(it);
        }

        public void remove() {
            synchronized (lock) {
                iterator.remove();
                lock.notifyAll();
            }
        }
    }
}
