/*
 * 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.compound;

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

/**
 * <code>CompoundInputStream</code> concatenates several input streams into one. It can operate in two modes:
 * <dl>
 *   <dt>Merged</dt>
 *   <dd>the compound stream acts as a single, global input stream, merging the contents of underlying streams. The
 * compound stream can be read just like a regular <code>InputStream</code> -- streams are advanced automatically as EOF
 * of individual streams are reached.</dd>
 *
 *   <dt>Unmerged</dt>
 *   <dd>the compound stream has be advanced manually. EOF are signaled individually for each underlying stream.
 * After EOF is reached, the current stream has to be advanced to the next one using {@link #advanceInputStream()}.</dd>
 * </dl>
 *
 * <p>
 * This class is abstract, with a single method to implement: {@link #getNextInputStream()}.
 * See {@link IteratorCompoundInputStream} for an <code>Iterator</code>-backed implementation.
 * </p>
 *
 * @see IteratorCompoundInputStream
 * @see CompoundReader
 * @author Maxence Bernard
 */
public abstract class CompoundInputStream extends InputStream {

    /** True if this CompoundInputStream operates in 'merged' mode */
    private boolean merged;

    /** The InputStream that's currently being processed */
    private InputStream currentIn;

    /** Used by {@link #read()} */
    private byte oneByteBuf[];

    /** <code>true</code> if the global EOF has been reached */
    private boolean globalEOFReached;


    /**
     * Creates a new <code>CompoundInputStream</code> operating in the specified mode.
     *
     * @param merged <code>true</code> if the streams should be merged, acting as a single stream, or considered
     * as separate streams that have to be {@link #advanceInputStream() advanced manually}.
     */
    public CompoundInputStream(boolean merged) {
        this.merged = merged;
    }

    /**
     * Returns:
     * <ul>
     *   <li><code>true</code> if this stream acts as a single, global input stream, merging the contents of underlying
     * streams. In this mode, the compound stream can be read just like a regular InputStream -- streams are advanced
     * automatically as EOF of individual streams are reached.</li>
     *   <li><code>false</code> if this stream has be advanced manually. In this mode, EOF are signaled individually
     * for each underlying stream. After EOF has been reached, the current stream has to be advanced to the next one
     * using {@link #advanceInputStream()}.</li>
     * </ul>
     *
     * @return <code>true</code> if this stream acts as a global input stream, <code>false</code> if this stream has
     * to be advanced manually.
     */
    public boolean isMerged() {
        return merged;
    }

    /**
     * Returns the <code>InputStream</code> this compound stream is currently reading, <code>null</code> if this stream
     * hasn't read anything yet, or if it has no underlying stream to read.
     *
     * @return <code>InputStream</code> this compound stream is currently reading
     */
    public InputStream getCurrentInputStream() {
        return currentIn;
    }

    /**
     * Closes the current input stream, if any. This method has no effect if there is no current input stream.
     *
     * @throws IOException if an error occurred while closing the current input stream.
     */
    public void closeCurrentInputStream() throws IOException {
        if(currentIn!=null)
            currentIn.close();
    }

    /**
     * Tries to advances the current stream to the next one, causing subsequent calls to <code>InputStream</code>
     * methods to operate on the new stream. Returns <code>true</code> if there was a next stream, <code>false</code>
     * otherwise.
     * <p>
     * Note: the current stream (if any) will be closed by this method.
     * </p>
     *
     * @return <code>true</code> if there was a next stream, <code>false</code> otherwise
     * @throws IOException if an error occurred while trying to advancing the current stream. This
     * <code>CompoundInputStream</code> can't be used after that and must be closed.
     */
    public boolean advanceInputStream() throws IOException {
        // Return immediately (don't close the stream) if this method is global EOF has already been reached
        if(globalEOFReached)
            return false;

        // Close the current stream
        if(currentIn!=null) {
            try {
                closeCurrentInputStream();
            }
            catch(IOException e) {
                // Fail silently
            }
        }

        // Try to advance the current InputStream to the next
        try {
            currentIn = getNextInputStream();
        }
        catch(IOException e) {
            // Can't recover from this, this is the end of this stream
            globalEOFReached = true;
            throw e;
        }

        if(currentIn==null) {
            // Global EOF reached
            globalEOFReached = true;
            return false;
        }
        return true;
    }


    /**
     * Checks the current stream and returns <code>true</code> if the current stream is in a state where it can be
     * accessed, <code>false</code> if global EOF has been reached.
     *
     * @return <code>true</code> if the current stream is in a state where it can be accessed, <code>false</code> if
     * global EOF has been reached.
     * @throws IOException if an error occurred while trying to advancing the current stream.
     */
    private boolean checkStream() throws IOException {
        if(globalEOFReached)
            return true;

        if(currentIn==null)
            if(!advanceInputStream())
                return true;

        return false;
    }


    //////////////////////
    // Abstract methods //
    //////////////////////

    /**
     * Returns the next <code>InputStream</code>, <code>null</code> if there is none.
     * <p>
     * Before calling this method, {@link #advanceInputStream()} closes the current stream (if any). In other words,
     * implementations do not have to worry about closing previously-returned streams.
     * </p>
     *
     * @return the next <code>InputStream</code>, <code>null</code> if there is none.
     * @throws IOException if an error occurred while retrieving the next input stream 
     */
    public abstract InputStream getNextInputStream() throws IOException;


    ////////////////////////////////
    // InputStream implementation //
    ////////////////////////////////

    /**
     * Delegates to {@link #read(byte[], int, int)} with a 1-byte buffer.
     */
    @Override
    public int read() throws IOException {
        if(oneByteBuf==null)
            oneByteBuf = new byte[1];

        int ret = read(oneByteBuf, 0, 1);

        return ret<=0?ret:oneByteBuf[0];
    }

    /**
     * Delegates to {@link #read(byte[], int, int)} with a <code>0</code> offset and the whole buffer's length.
     */
    @Override
    public int read(byte[] b) throws IOException {
        return read(b, 0, b.length);
    }

    /**
     * Reads up to <code>len-off</code> bytes and stores them in the specified byte buffer, starting at <code>off</code>.
     * Returns the number of bytes that were actually read, or <code>-1</code> to signal:
     * <ul>
     *   <li>if {@link #isMerged()} is <code>true</code>, the end of the compound stream as a whole</li>
     *   <li>if {@link #isMerged ()} is <code>false</code>, the end of the current stream, which may or may not coincide
     * with the end of the stream as a whole.</li>
     * </ul>
     */
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if(checkStream())
            return -1;

        int ret = currentIn.read(b, off, len);

        if(ret==-1) {
            // read the next stream
            if(merged) {
                if(!advanceInputStream())
                    return -1;      // Global EOF reached

                // Recurse
                return read(b, off, len);
            }

            return -1;
        }

        return ret;
    }

    /**
     * Skips up to <code>n</code> bytes and returns the number of bytes that were actually skipped, or <code>-1</code>
     * to signal:
     * <ul>
     *   <li>if {@link #isMerged()} is enabled, the end of the compound stream as a whole</li>
     *   <li>if {@link #isMerged ()} is disabled, the end of the current stream, which may or may not coincide
     * with the end of the stream as a whole.</li>
     * </ul>
     */
    @Override
    public long skip(long n) throws IOException {
        if(checkStream())
            return -1;

        long ret = currentIn.skip(n);

        if(ret==-1) {
            // read the next stream
            if(merged) {
                if(!advanceInputStream())
                    return -1;      // Global EOF reac  hed

                return currentIn.skip(n);
            }

            return -1;
        }

        return ret;
    }

    /**
     * Closes the current <code>InputStream</code> and this <code>CompoundInputStream</code> a whole.
     * The current stream can no longer be advanced after this method has been called.
     *
     * @throws IOException if the current stream could not be closed.  
     */
    @Override
    public void close() throws IOException {
        try {
            if(currentIn!=null)
                closeCurrentInputStream();
        }
        finally {
            globalEOFReached = true;
        }
    }

    /**
     * Delegates to the current <code>InputStream</code>.
     */
    @Override
    public int available() throws IOException {
        if(checkStream())
            return 0;

        return currentIn.available();
    }

    /**
     * Delegates to the current <code>InputStream</code>.
     */
    @Override
    public void mark(int readlimit) {
        try {
            if(!checkStream())
                currentIn.mark(readlimit);
        }
        catch(IOException e) {
            // Can't throw an IOException here unfortunately, fail silently
        }
    }

    /**
     * Delegates to the current <code>InputStream</code>.
     */
    @Override
    public void reset() throws IOException {
        if(!checkStream())
            currentIn.reset();
    }

    /**
     * Delegates to the current <code>InputStream</code>.
     */
    @Override
    public boolean markSupported() {
        try {
            return !checkStream() && currentIn.markSupported();
        }
        catch(IOException e) {
            return false;
        }
    }
}
