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

package edu.emory.mathcs.util.io;

import java.io.*;

/**
 * Virtual input stream that reads up to the specified number of bytes from the
 * underlying stream and then reports EOF. Application: handle sequence of
 * streams over a single physical "keepalive-type" stream.
 *
 * @author Dawid Kurzyniec
 * @version 1.0
 */
public class FragmentInputStream extends FilterInputStream {

    /**
     * Closing policy that leaves the underlying stream opened at the current
     * position.
     */
    public static final int STAY_ON_CLOSE = 0;

    /**
     * Closing policy that closes the the underlying stream.
     */
    public static final int CLOSE_ON_CLOSE = 1;

    /**
     * Closing policy that leaves the underlying stream opened but advances it
     * to the end of this virtual stream. If less bytes were read than the
     * length of the stream as specified at the construction time, the
     * remaining bytes are skipped. Close may block if the data is not yet
     * available.
     */
    public static final int ADVANCE_ON_CLOSE = 2;

    long remaining;
    final int closingPolicy;
    long marked = -1;

    /**
     * Creates new fragment input stream over a specified physical stream and
     * with specified fragment length and {@link #STAY_ON_CLOSE} closing policy.
     *
     * @param in the underlying stream to read from
     * @param len the total length of this fragment stream
     */
    public FragmentInputStream(InputStream in, long len) {
        this(in, len, STAY_ON_CLOSE);
    }

    /**
     * Creates new fragment input stream over a specified physical stream and
     * with specified fragment length and specified closing policy.
     *
     * @param in the underlying stream to read from
     * @param len the total length of this fragment stream
     * @param closingPolicy policy that decides what to do when this stream
     *        is closed
     */
    public FragmentInputStream(InputStream in, long len, int closingPolicy) {
        super(in);
        this.remaining = len;
        if (closingPolicy < STAY_ON_CLOSE || closingPolicy > ADVANCE_ON_CLOSE) {
            throw new IllegalArgumentException("Unrecognized closing policy");
        }
        this.closingPolicy = closingPolicy;
    }

    public synchronized int read() throws IOException {
        if (remaining <= 0) return -1;
        int b = super.read();
        if (b >= 0) {
            remaining--;
        }
        return b;
    }

    public synchronized int read(byte[] buf, int off, int len) throws IOException {
        if (remaining <= 0) return -1;
        if (len > remaining) len = (int)remaining;
        int read = super.read(buf, off, len);
        if (read >= 0) {
            remaining -= read;
        }
        return read;
    }

    public int available() throws IOException {
        if (remaining <= 0) return 0;
        int available = super.available();
        if (available > remaining) available = (int)remaining;
        return available;
    }

    public long skip(long n) throws IOException {
        if (remaining <= 0) return 0;
        if (n > remaining) n = remaining;
        return super.skip(n);
    }

    public void mark(int readlimit) {
        super.mark(readlimit);
        marked = remaining;
    }

    public void reset(int readlimit) throws IOException {
        if (marked < 0) throw new IllegalStateException("Not marked");
        super.reset();
        remaining = marked;
    }

    public void close() throws IOException {
        switch (closingPolicy) {
            case CLOSE_ON_CLOSE:
                super.close();
                break;
            case STAY_ON_CLOSE:
                return;
            case ADVANCE_ON_CLOSE:
                while (remaining > 0) {
                    long skipped = super.skip(remaining);
                    if (skipped > 0) {
                        remaining -= skipped;
                    }
                    else {
                        // avoid spinning; block until at least one byte is available
                        read();
                    }
                }
                break;
            default:
                throw new RuntimeException();
        }
    }
}
