/*
 * DeltaInputStream
 *
 * Author: Lasse Collin <lasse.collin@tukaani.org>
 *
 * This file has been put into the public domain.
 * You can do whatever you want with this file.
 */

package org.tukaani.xz;

import java.io.InputStream;
import java.io.IOException;
import org.tukaani.xz.delta.DeltaDecoder;

/**
 * Decodes raw Delta-filtered data (no XZ headers).
 * <p>
 * The delta filter doesn't change the size of the data and thus it
 * cannot have an end-of-payload marker. It will simply decode until
 * its input stream indicates end of input.
 */
public class DeltaInputStream extends InputStream {
    /**
     * Smallest supported delta calculation distance.
     */
    public static final int DISTANCE_MIN = 1;

    /**
     * Largest supported delta calculation distance.
     */
    public static final int DISTANCE_MAX = 256;

    private InputStream in;
    private final DeltaDecoder delta;

    private IOException exception = null;

    private final byte[] tempBuf = new byte[1];

    /**
     * Creates a new Delta decoder with the given delta calculation distance.
     *
     * @param       in          input stream from which Delta filtered data
     *                          is read
     *
     * @param       distance    delta calculation distance, must be in the
     *                          range [<code>DISTANCE_MIN</code>,
     *                          <code>DISTANCE_MAX</code>]
     */
    public DeltaInputStream(InputStream in, int distance) {
        // Check for null because otherwise null isn't detect
        // in this constructor.
        if (in == null)
            throw new NullPointerException();

        this.in = in;
        this.delta = new DeltaDecoder(distance);
    }

    /**
     * Decode the next byte from this input stream.
     *
     * @return      the next decoded byte, or <code>-1</code> to indicate
     *              the end of input on the input stream <code>in</code>
     *
     * @throws      IOException may be thrown by <code>in</code>
     */
    public int read() throws IOException {
        return read(tempBuf, 0, 1) == -1 ? -1 : (tempBuf[0] & 0xFF);
    }

    /**
     * Decode into an array of bytes.
     * <p>
     * This calls <code>in.read(buf, off, len)</code> and defilters the
     * returned data.
     *
     * @param       buf         target buffer for decoded data
     * @param       off         start offset in <code>buf</code>
     * @param       len         maximum number of bytes to read
     *
     * @return      number of bytes read, or <code>-1</code> to indicate
     *              the end of the input stream <code>in</code>
     *
     * @throws      XZIOException if the stream has been closed
     *
     * @throws      IOException may be thrown by underlaying input
     *                          stream <code>in</code>
     */
    public int read(byte[] buf, int off, int len) throws IOException {
        if (len == 0)
            return 0;

        if (in == null)
            throw new XZIOException("Stream closed");

        if (exception != null)
            throw exception;

        int size;
        try {
            size = in.read(buf, off, len);
        } catch (IOException e) {
            exception = e;
            throw e;
        }

        if (size == -1)
            return -1;

        delta.decode(buf, off, size);
        return size;
    }

    /**
     * Calls <code>in.available()</code>.
     *
     * @return      the value returned by <code>in.available()</code>
     */
    public int available() throws IOException {
        if (in == null)
            throw new XZIOException("Stream closed");

        if (exception != null)
            throw exception;

        return in.available();
    }

    /**
     * Closes the stream and calls <code>in.close()</code>.
     * If the stream was already closed, this does nothing.
     *
     * @throws  IOException if thrown by <code>in.close()</code>
     */
    public void close() throws IOException {
        if (in != null) {
            try {
                in.close();
            } finally {
                in = null;
            }
        }
    }
}
