package org.apache.commons.ssl.asn1;

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

public class ASN1StreamParser {
    InputStream _in;

    private int _limit;
    private boolean _eofFound;

    public ASN1StreamParser(
        InputStream in) {
        this(in, Integer.MAX_VALUE);
    }

    public ASN1StreamParser(
        InputStream in,
        int limit) {
        this._in = in;
        this._limit = limit;
    }

    public ASN1StreamParser(
        byte[] encoding) {
        this(new ByteArrayInputStream(encoding), encoding.length);
    }

    InputStream getParentStream() {
        return _in;
    }

    private int readLength()
        throws IOException {
        int length = _in.read();
        if (length < 0) {
            throw new EOFException("EOF found when length expected");
        }

        if (length == 0x80) {
            return -1;      // indefinite-length encoding
        }

        if (length > 127) {
            int size = length & 0x7f;

            if (size > 4) {
                throw new IOException("DER length more than 4 bytes");
            }

            length = 0;
            for (int i = 0; i < size; i++) {
                int next = _in.read();

                if (next < 0) {
                    throw new EOFException("EOF found reading length");
                }

                length = (length << 8) + next;
            }

            if (length < 0) {
                throw new IOException("corrupted stream - negative length found");
            }

            if (length >= _limit)   // after all we must have read at least 1 byte
            {
                throw new IOException("corrupted stream - out of bounds length found");
            }
        }

        return length;
    }

    public DEREncodable readObject()
        throws IOException {
        int tag = _in.read();
        if (tag == -1) {
            if (_eofFound) {
                throw new EOFException("attempt to read past end of file.");
            }

            _eofFound = true;

            return null;
        }

        //
        // turn of looking for "00" while we resolve the tag
        //
        set00Check(false);

        //
        // calculate tag number
        //
        int baseTagNo = tag & ~DERTags.CONSTRUCTED;
        int tagNo = baseTagNo;

        if ((tag & DERTags.TAGGED) != 0) {
            tagNo = tag & 0x1f;

            //
            // with tagged object tag number is bottom 5 bits, or stored at the start of the content
            //
            if (tagNo == 0x1f) {
                tagNo = 0;

                int b = _in.read();

                while ((b >= 0) && ((b & 0x80) != 0)) {
                    tagNo |= (b & 0x7f);
                    tagNo <<= 7;
                    b = _in.read();
                }

                if (b < 0) {
                    _eofFound = true;

                    throw new EOFException("EOF encountered inside tag value.");
                }

                tagNo |= (b & 0x7f);
            }
        }

        //
        // calculate length
        //
        int length = readLength();

        if (length < 0)  // indefinite length
        {
            IndefiniteLengthInputStream indIn = new IndefiniteLengthInputStream(_in);

            switch (baseTagNo) {
                case DERTags.NULL:
                    while (indIn.read() >= 0) {
                        // make sure we skip to end of object
                    }
                    return BERNull.INSTANCE;
                case DERTags.OCTET_STRING:
                    return new BEROctetStringParser(new ASN1ObjectParser(tag, tagNo, indIn));
                case DERTags.SEQUENCE:
                    return new BERSequenceParser(new ASN1ObjectParser(tag, tagNo, indIn));
                case DERTags.SET:
                    return new BERSetParser(new ASN1ObjectParser(tag, tagNo, indIn));
                default:
                    return new BERTaggedObjectParser(tag, tagNo, indIn);
            }
        } else {
            DefiniteLengthInputStream defIn = new DefiniteLengthInputStream(_in, length);

            switch (baseTagNo) {
                case DERTags.INTEGER:
                    return new DERInteger(defIn.toByteArray());
                case DERTags.NULL:
                    defIn.toByteArray(); // make sure we read to end of object bytes.
                    return DERNull.INSTANCE;
                case DERTags.OBJECT_IDENTIFIER:
                    return new DERObjectIdentifier(defIn.toByteArray());
                case DERTags.OCTET_STRING:
                    return new DEROctetString(defIn.toByteArray());
                case DERTags.SEQUENCE:
                    return new DERSequence(loadVector(defIn, length)).parser();
                case DERTags.SET:
                    return new DERSet(loadVector(defIn, length)).parser();
                default:
                    return new BERTaggedObjectParser(tag, tagNo, defIn);
            }
        }
    }

    private void set00Check(boolean enabled) {
        if (_in instanceof IndefiniteLengthInputStream) {
            ((IndefiniteLengthInputStream) _in).setEofOn00(enabled);
        }
    }

    private ASN1EncodableVector loadVector(InputStream in, int length)
        throws IOException {
        ASN1InputStream aIn = new ASN1InputStream(in, length);
        ASN1EncodableVector v = new ASN1EncodableVector();

        DERObject obj;
        while ((obj = aIn.readObject()) != null) {
            v.add(obj);
        }

        return v;
    }
}
