/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander 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 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander 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 program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.commons.io;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * <code>BoundedInputStream</code> is an InputStream that has a set limit to the number of bytes that can be read or
 * skipped from it. What happens when the limit is reached is controlled at creation time: <code>read</code> and
 * <code>skip</code> methods can either throw a {@link StreamOutOfBoundException} or simply return <code>-1</code>.
 *
 * <p>The limit has no effect if it is set to a value that is higher than the number of bytes remaining in the
 * underlying stream.</p>
 *
 * <p>This class is particularly useful for reading archives that are a concatenation of files, tarballs for instance.</p>
 *
 * @author Maxence Bernard
 * @see BoundedReader
 * @see BoundedOutputStream
 * @see StreamOutOfBoundException
 */
public class BoundedInputStream extends FilterInputStream implements Bounded {

    private long totalRead;
    private long allowedBytes;
    private boolean throwStreamOutOfBoundException;

    /**
     * Creates a new <code>BoundedInputStream</code> over the specified stream, allowing a maximum of
     * <code>allowedBytes</code> to be read or skipped. If <code>allowedBytes</code> is equal to <code>-1</code>, this
     * stream is not bounded and acts as a normal stream.
     *
     * <p>If the <code>throwStreamOutOfBoundException</code> parameter is <code>true</code>, <code>read</code> and
     * <code>skip</code> methods will throw a {@link StreamOutOfBoundException} when an attempt to read or skip beyond
     * that limit is made. If <code>false</code>, <code>-1</code> will be returned.</p>
     *
     * @param in the stream to be bounded
     * @param allowedBytes the total number of bytes that are allowed to be read or skipped, <code>-1</code> for no limit
     * @param throwStreamOutOfBoundException <code>true</code> to throw when an attempt to read or skip beyond the byte
     * limit is made, <code>false</code> to simply return <code>-1</code>
     */
    public BoundedInputStream(InputStream in, long allowedBytes, boolean throwStreamOutOfBoundException) {
        super(in);

        this.allowedBytes = allowedBytes;
        this.throwStreamOutOfBoundException = throwStreamOutOfBoundException;
    }

    /**
     * Called when an attempt to read out of the stream's bound has been made. This method will either throw a
     * {@link StreamOutOfBoundException} or return <code>-1</code>, depending on how the <code>BoundedInputStream</code>
     * was created.
     *
     * @return -1 if this BoundedInputStream was configured not to throw a StreamOutOfBoundException
     * @throws StreamOutOfBoundException if this BoundedInputStream was configured to throw a StreamOutOfBoundException  
     */
    protected int handleStreamOutOfBound() throws StreamOutOfBoundException {
        if(throwStreamOutOfBoundException)
            throw new StreamOutOfBoundException(allowedBytes);

        return -1;
    }


    ////////////////////////////
    // Bounded implementation //
    ////////////////////////////

    public long getAllowedBytes() {
        return allowedBytes;
    }

    public synchronized long getProcessedBytes() {
        return totalRead;
    }

    public synchronized long getRemainingBytes() {
        return allowedBytes<=-1?Long.MAX_VALUE:allowedBytes-totalRead;
    }


    ////////////////////////
    // Overridden methods //
    ////////////////////////

    @Override
    public synchronized int read() throws IOException {
        if(getRemainingBytes()==0)
            return handleStreamOutOfBound();

        int i = in.read();
        totalRead++;

        return i;
    }

    @Override
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    @Override
    public synchronized int read(byte b[], int off, int len) throws IOException {
        int canRead = (int)Math.min(getRemainingBytes(), len);
        if(canRead==0)
            return handleStreamOutOfBound();

        int nbRead = in.read(b, off, canRead);
        if(nbRead>0)
            totalRead += nbRead;

        return nbRead;
    }

    @Override
    public synchronized long skip(long n) throws IOException {
        int canSkip = (int)Math.min(getRemainingBytes(), n);
        if(canSkip==0)
            return handleStreamOutOfBound();

        long nbSkipped = in.skip(canSkip);
        if(nbSkipped>0)
            totalRead += nbSkipped;

        return nbSkipped;
    }

    @Override
    public synchronized int available() throws IOException {
        return Math.min(in.available(), (int)getRemainingBytes());
    }

    // Methods not implemented

    /**
     * Always returns <code>false</code>, even if the underlying stream supports it.
     *
     * @return always returns <code>false</code>, even if the underlying stream supports it
     */
    @Override
    public boolean markSupported() {
        // Todo: in theory we could support mark/reset
        return false;
    }

    /**
     * Implemented as a no-op: the call is *not* delegated to the underlying stream.
     */
    @Override
    public synchronized void mark(int readlimit) {
        // Todo: in theory we could support mark/reset
        // No-op
    }

    /**
     * Always throws an <code>IOException</code>: the call is *not* delegated to the underlying stream.
     */
    @Override
    public synchronized void reset() throws IOException {
        // Todo: in theory we could support mark/reset
        // No-op
    }
}
