package org.unsynchronized;

import java.io.*;
import java.util.*;

/**
 * <p>
 * An InputStream designed to record data passing through the stream after a call to
 * record() is made.  After record() is called, the results from every read will 
 * be stored in an * internal buffer.  The contents of the buffer can be 
 * retrieved by getRecordedData(); to stop recording and clear the internal 
 * buffer, call stopRecording().
 * </p>
 *
 * <p>
 * <b>Note</b>: calls to mark() and reset() are merely passed through to the inner stream; if
 * recording is active, the buffer won't be backtracked by reset().
 * </p>
 */
public class LoggerInputStream extends InputStream {
    private InputStream innerStream = null;
    private ByteArrayOutputStream baos = null;
    private boolean recording = false; 

    public LoggerInputStream(InputStream innerStream) {
        super();
        this.innerStream = innerStream;
    }
    public synchronized int read() throws IOException {
        int i = innerStream.read();
        if(recording && i != -1) {
            if(i > 255 || i < 0) {
                throw new IOException("non-byte, non--1 value read from inner stream: " + i);
            }
            baos.write((byte)i);
        }
        return i;
    }
    public synchronized int read(byte[] b) throws IOException {
        return this.read(b, 0, b.length);
    }
    public synchronized int read(byte[] b, int off, int len) throws IOException {
        int retval = innerStream.read(b, off, len);
        if(recording && retval > 0) {
            if(retval > len) {
                throw new IOException("inner stream read(byte[], int, int) violating contract; return value > len: " + retval);
            }
            baos.write(b, off, retval);
        }
        return retval;
    }
    public synchronized long skip(long n) throws IOException {
        if(n < 0) {
            throw new IOException("can't skip negative number of bytes");
        }
        if(recording == false) {
            return innerStream.skip(n);
        }
        long nskipped = 0;
        while(n > Integer.MAX_VALUE) {
            long ret = skip(Integer.MAX_VALUE);
            nskipped += ret;
            if(ret < Integer.MAX_VALUE) {
                return nskipped;
            }
            n -= ret;
        }
        int toread = (int)n, actuallyread = 0;
        byte[] buf = new byte[10240];
        while(toread > 0) {
            int r = Math.min(toread, buf.length);
            int rret = this.read(buf, 0, r);
            actuallyread += rret;
            toread -= rret;
            if(rret < r) {
                break;
            }
        }
        return actuallyread;
    }
    public synchronized int available() throws IOException {
        return innerStream.available();
    }
    public synchronized void close() throws IOException {
        innerStream.close();
    }
    public synchronized void mark(int readlimit) {
        innerStream.mark(readlimit);
    }
    public synchronized void reset() throws IOException {
        innerStream.reset();
    }
    public boolean markSupported() {
        return innerStream.markSupported();
    }
    /**
     * If not currently recording, start recording.  If the stream is currently recording,
     * the current buffer is cleared.
     */
    public synchronized void record() {
        recording = true;
        baos = new ByteArrayOutputStream();
    }
    /**
     * Stops recording and clears the internal buffer.  If recording is not active, an
     * IOException is thrown.
     *
     * @throws IOException if recording is not currently active
     */
    public synchronized void stopRecording() throws IOException {
        if(recording == false) {
            throw new IOException("recording not active");
        }
        try {
            baos.close();
        } catch (IOException ignore) {}
        baos = null;
        recording = false;
    }
    /**
     * Returns the data recorded so far; if recording is not active, an empty buffer
     * is returned.
     */
    public synchronized byte[] getRecordedData() {
        if(recording == false) {
            return new byte[0];
        }
        return baos.toByteArray();
    }
}
