package pdftk.org.bouncycastle.crypto.io;

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

import pdftk.org.bouncycastle.crypto.BufferedBlockCipher;
import pdftk.org.bouncycastle.crypto.StreamCipher;

/**
 * A CipherInputStream is composed of an InputStream and a BufferedBlockCipher so
 * that read() methods return data that are read in from the
 * underlying InputStream but have been additionally processed by the
 * Cipher.  The Cipher must be fully initialized before being used by
 * a CipherInputStream.
 * <p>
 * For example, if the Cipher is initialized for decryption, the
 * CipherInputStream will attempt to read in data and decrypt them,
 * before returning the decrypted data.
 */
public class CipherInputStream
    extends FilterInputStream
{
    private BufferedBlockCipher bufferedBlockCipher;
    private StreamCipher streamCipher;

    private byte[] buf;
    private byte[] inBuf;

    private int bufOff;
    private int maxBuf;
    private boolean finalized;

    private static final int INPUT_BUF_SIZE = 2048;

    /**
     * Constructs a CipherInputStream from an InputStream and a
     * BufferedBlockCipher.
     */
    public CipherInputStream(
        InputStream is,
        BufferedBlockCipher cipher)
    {
        super(is);

        this.bufferedBlockCipher = cipher;

        buf = new byte[cipher.getOutputSize(INPUT_BUF_SIZE)];
        inBuf = new byte[INPUT_BUF_SIZE];
    }

    public CipherInputStream(
        InputStream is,
        StreamCipher cipher)
    {
        super(is);

        this.streamCipher = cipher;

        buf = new byte[INPUT_BUF_SIZE];
        inBuf = new byte[INPUT_BUF_SIZE];
    }

    /**
     * grab the next chunk of input from the underlying input stream
     */
    private int nextChunk()
        throws IOException
    {
        int available = super.available();

        // must always try to read 1 byte!
        // some buggy InputStreams return < 0!
        if (available <= 0)
        {
            available = 1;
        }

        if (available > inBuf.length)
        {
            available = super.read(inBuf, 0, inBuf.length);
        }
        else
        {
            available = super.read(inBuf, 0, available);
        }

        if (available < 0)
        {
            if (finalized)
            {
                return -1;
            }

            try
            {
                if (bufferedBlockCipher != null)
                {
                    maxBuf = bufferedBlockCipher.doFinal(buf, 0);
                }
                else
                {
                    maxBuf = 0; // a stream cipher
                }
            }
            catch (Exception e)
            {
                throw new IOException("error processing stream: " + e.toString());
            }

            bufOff = 0;

            finalized = true;

            if (bufOff == maxBuf)
            {
                return -1;
            }
        }
        else
        {
            bufOff = 0;

            try
            {
                if (bufferedBlockCipher != null)
                {
                    maxBuf = bufferedBlockCipher.processBytes(inBuf, 0, available, buf, 0);
                }
                else
                {
                    streamCipher.processBytes(inBuf, 0, available, buf, 0);
                    maxBuf = available;
                }
            }
            catch (Exception e)
            {
                throw new IOException("error processing stream: " + e.toString());
            }

            if (maxBuf == 0)    // not enough bytes read for first block...
            {
                return nextChunk();
            }
        }

        return maxBuf;
    }

    public int read()
        throws IOException
    {
        if (bufOff == maxBuf)
        {
            if (nextChunk() < 0)
            {
                return -1;
            }
        }

        return buf[bufOff++] & 0xff;
    }

    public int read(
        byte[] b)
        throws IOException
    {
        return read(b, 0, b.length);
    }

    public int read(
        byte[] b,
        int off,
        int len)
        throws IOException
    {
        if (bufOff == maxBuf)
        {
            if (nextChunk() < 0)
            {
                return -1;
            }
        }

        int available = maxBuf - bufOff;

        if (len > available)
        {
            System.arraycopy(buf, bufOff, b, off, available);
            bufOff = maxBuf;

            return available;
        }
        else
        {
            System.arraycopy(buf, bufOff, b, off, len);
            bufOff += len;

            return len;
        }
    }

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

        int available = maxBuf - bufOff;

        if (n > available)
        {
            bufOff = maxBuf;

            return available;
        }
        else
        {
            bufOff += (int)n;

            return (int)n;
        }
    }

    public int available()
        throws IOException
    {
        return maxBuf - bufOff;
    }

    public void close()
        throws IOException
    {
        super.close();
    }

    public boolean markSupported()
    {
        return false;
    }
}
