package org.bouncycastle.bcpg;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;

import org.bouncycastle.util.io.Streams;

/**
 * Stream reader for PGP objects
 */
public class BCPGInputStream
    extends InputStream implements PacketTags
{
    /**
     * If the argument is a {@link BCPGInputStream}, return it.
     * Otherwise wrap it in a {@link BCPGInputStream} and then return the result.
     *
     * @param in input stream
     * @return BCPGInputStream
     */
    public static BCPGInputStream wrap(InputStream in)
    {
        if (in instanceof BCPGInputStream)
        {
            return (BCPGInputStream)in;
        }
        return new BCPGInputStream(in);
    }

    InputStream    in;
    boolean        next = false;
    int            nextB;

    boolean        mNext = false;
    int            mNextB;

    public BCPGInputStream(
        InputStream    in)
    {
        this.in = in;
    }

    public int available()
        throws IOException
    {
        return in.available();
    }

    public boolean markSupported()
    {
        return in.markSupported();
    }

    public synchronized void mark(int readLimit)
    {
        mNext = next;
        mNextB = nextB;
        in.mark(readLimit);
    }

    public synchronized void reset()
        throws IOException
    {
        next = mNext;
        nextB = mNextB;
        in.reset();
    }

    public int read()
        throws IOException
    {
        if (next)
        {
            next = false;

            return nextB;
        }
        else
        {
            return in.read();
        }
    }

    public int read(
        byte[] buf,
        int off,
        int len)
        throws IOException
    {
        if (len == 0)
        {
            return 0;
        }

        if (!next)
        {
            return in.read(buf, off, len);
        }

        // We have next byte waiting, so return it

        if (nextB < 0)
        {
            return -1; // EOF
        }

        buf[off] = (byte)nextB;  // May throw NullPointerException...
        next = false;            // ...so only set this afterwards

        return 1;
    }

    public void readFully(
        byte[]    buf,
        int       off,
        int       len)
        throws IOException
    {
        if (Streams.readFully(this, buf, off, len) < len)
        {
            throw new EOFException();
        }
    }

    public byte[] readAll()
        throws IOException
    {
        return Streams.readAll(this);
    }

    public void readFully(
        byte[]    buf)
        throws IOException
    {
        readFully(buf, 0, buf.length);
    }

    /**
     * Obtains the tag of the next packet in the stream.
     *
     * @return the {@link PacketTags tag number}.
     *
     * @throws IOException if an error occurs reading the tag from the stream.
     */
    public int nextPacketTag()
        throws IOException
    {
        if (!next)
        {
            try
            {
                nextB = in.read();
            }
            catch (EOFException e)
            {
                nextB = -1;
            }

            next = true;
        }

        if (nextB < 0)
        {
            return nextB;
        }

        int maskB = nextB & 0x3f;
        if ((nextB & 0x40) == 0)    // old
        {
            maskB >>= 2;
        }
        return maskB;
    }

    /**
     * Reads the next packet from the stream.
     * @throws IOException
     */
    public Packet readPacket()
        throws IOException
    {
        int    hdr = this.read();

        if (hdr < 0)
        {
            return null;
        }

        if ((hdr & 0x80) == 0)
        {
            throw new IOException("invalid header encountered");
        }

        boolean    newPacket = (hdr & 0x40) != 0;
        int        tag = 0;
        int        bodyLen = 0;
        boolean    partial = false;

        if (newPacket)
        {
            tag = hdr & 0x3f;

            int    l = this.read();

            if (l < 192)
            {
                bodyLen = l;
            }
            else if (l <= 223)
            {
                int b = in.read();

                bodyLen = ((l - 192) << 8) + (b) + 192;
            }
            else if (l == 255)
            {
                bodyLen = (in.read() << 24) | (in.read() << 16) |  (in.read() << 8)  | in.read();
            }
            else
            {
                partial = true;
                bodyLen = 1 << (l & 0x1f);
            }
        }
        else
        {
            int lengthType = hdr & 0x3;

            tag = (hdr & 0x3f) >> 2;

            switch (lengthType)
            {
            case 0:
                bodyLen = this.read();
                break;
            case 1:
                bodyLen = (this.read() << 8) | this.read();
                break;
            case 2:
                bodyLen = (this.read() << 24) | (this.read() << 16) | (this.read() << 8) | this.read();
                break;
            case 3:
                partial = true;
                break;
            default:
                throw new IOException("unknown length type encountered");
            }
        }

        BCPGInputStream    objStream;

            objStream = this;

        switch (tag)
        {
        case RESERVED:
            return new InputStreamPacket(objStream);
        case PUBLIC_KEY_ENC_SESSION:
            return new PublicKeyEncSessionPacket(objStream);
        case SIGNATURE:
            return new SignaturePacket(objStream);
        case SYMMETRIC_KEY_ENC_SESSION:
            return new SymmetricKeyEncSessionPacket(objStream);
        case ONE_PASS_SIGNATURE:
            return new OnePassSignaturePacket(objStream);
        case SECRET_KEY:
            return new SecretKeyPacket(objStream);
        case PUBLIC_KEY:
            return new PublicKeyPacket(objStream);
        case SECRET_SUBKEY:
            return new SecretSubkeyPacket(objStream);
        case COMPRESSED_DATA:
            return new CompressedDataPacket(objStream);
        case SYMMETRIC_KEY_ENC:
            return new SymmetricEncDataPacket(objStream);
        case MARKER:
            return new MarkerPacket(objStream);
        case LITERAL_DATA:
            return new LiteralDataPacket(objStream);
        case TRUST:
            return new TrustPacket(objStream);
        case USER_ID:
            return new UserIDPacket(objStream);
        case USER_ATTRIBUTE:
            return new UserAttributePacket(objStream);
        case PUBLIC_SUBKEY:
            return new PublicSubkeyPacket(objStream);
        case SYM_ENC_INTEGRITY_PRO:
            return new SymmetricEncIntegrityPacket(objStream);
        case MOD_DETECTION_CODE:
            return new ModDetectionCodePacket(objStream);
        case EXPERIMENTAL_1:
        case EXPERIMENTAL_2:
        case EXPERIMENTAL_3:
        case EXPERIMENTAL_4:
            return new ExperimentalPacket(tag, objStream);
        default:
            throw new IOException("unknown packet type encountered: " + tag);
        }
    }

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

    public int skipMarkerPackets()
        throws IOException
    {
        int tag;
        while ((tag = nextPacketTag()) == PacketTags.MARKER)
        {
            readPacket();
        }

        return tag;
    }

    /**
     * a stream that overlays our input stream, allowing the user to only read a segment of it.
     *
     * NB: dataLength will be negative if the segment length is in the upper range above 2**31.
     */
    private static class PartialInputStream
        extends InputStream
    {
        private BCPGInputStream     in;
        private boolean             partial;
        private int                 dataLength;

        PartialInputStream(
            BCPGInputStream  in,
            boolean          partial,
            int              dataLength)
        {
            this.in = in;
            this.partial = partial;
            this.dataLength = dataLength;
        }

        public int available()
            throws IOException
        {
            int avail = in.available();

            if (avail <= dataLength || dataLength < 0)
            {
                return avail;
            }
            else
            {
                if (partial && dataLength == 0)
                {
                    return 1;
                }
                return dataLength;
            }
        }

        private int loadDataLength()
            throws IOException
        {
            int            l = in.read();

            if (l < 0)
            {
                return -1;
            }

            partial = false;
            if (l < 192)
            {
                dataLength = l;
            }
            else if (l <= 223)
            {
                dataLength = ((l - 192) << 8) + (in.read()) + 192;
            }
            else if (l == 255)
            {
                dataLength = (in.read() << 24) | (in.read() << 16) |  (in.read() << 8)  | in.read();
            }
            else
            {
                partial = true;
                dataLength = 1 << (l & 0x1f);
            }

            return dataLength;
        }

        public int read(byte[] buf, int offset, int len)
            throws IOException
        {
            do
            {
                if (dataLength != 0)
                {
                    int readLen = (dataLength > len || dataLength < 0) ? len : dataLength;
                    readLen = in.read(buf, offset, readLen);
                    if (readLen < 0)
                    {
                        throw new EOFException("premature end of stream in PartialInputStream");
                    }
                    dataLength -= readLen;
                    return readLen;
                }
            }
            while (partial && loadDataLength() >= 0);

            return -1;
        }

        public int read()
            throws IOException
        {
            do
            {
                if (dataLength != 0)
                {
                    int ch = in.read();
                    if (ch < 0)
                    {
                        throw new EOFException("premature end of stream in PartialInputStream");
                    }
                    dataLength--;
                    return ch;
                }
            }
            while (partial && loadDataLength() >= 0);

            return -1;
        }
    }
}
