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

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

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

    /** The Reader that's currently being processed */
    private Reader currentReader;

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

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


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

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

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

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

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

        // Close the current reader
        if(currentReader !=null) {
            try {
                closeCurrentReader();
            }
            catch(IOException e) {
                // Fail silently
            }
        }

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

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


    /**
     * Checks the current reader and returns <code>true</code> if the current reader 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 reader 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 reader.
     */
    private boolean checkReader() throws IOException {
        if(globalEOFReached)
            return true;

        if(currentReader ==null)
            if(!advanceReader())
                return true;

        return false;
    }


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

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


    ///////////////////////////
    // Reader implementation //
    ///////////////////////////

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

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

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

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

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

        int ret = currentReader.read(c, off, len);

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

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

            return -1;
        }

        return ret;
    }

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

        long ret = currentReader.skip(n);

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

                return currentReader.skip(n);
            }

            return -1;
        }

        return ret;
    }

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

    /**
     * Delegates to the current <code>Reader</code>.
     */
    @Override
    public boolean ready() throws IOException {
        return !checkReader() && currentReader.ready();
    }

    /**
     * Delegates to the current <code>Reader</code>.
     */
    @Override
    public void mark(int readlimit) throws IOException {
        if(!checkReader())
            currentReader.mark(readlimit);
    }

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

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