package org.bouncycastle.asn1;

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

import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Exceptions;
import org.bouncycastle.util.Integers;

/**
 * Base class for BIT STRING objects
 */
public abstract class ASN1BitString
    extends ASN1Primitive
    implements ASN1String, ASN1BitStringParser
{
    static final ASN1UniversalType TYPE = new ASN1UniversalType(ASN1BitString.class, BERTags.BIT_STRING)
    {
        ASN1Primitive fromImplicitPrimitive(DEROctetString octetString)
        {
            return createPrimitive(octetString.getOctets());
        }

        ASN1Primitive fromImplicitConstructed(ASN1Sequence sequence)
        {
            return sequence.toASN1BitString();
        }
    };

    public static ASN1BitString getInstance(Object obj)
    {
        if (obj == null || obj instanceof ASN1BitString)
        {
            return (ASN1BitString)obj;
        }
//      else if (obj instanceof ASN1BitStringParser)
        else if (obj instanceof ASN1Encodable)
        {
            ASN1Primitive primitive = ((ASN1Encodable)obj).toASN1Primitive();
            if (primitive instanceof ASN1BitString)
            {
                return (ASN1BitString)primitive;
            }
        }
        else if (obj instanceof byte[])
        {
            try
            {
                return (ASN1BitString)TYPE.fromByteArray((byte[])obj);
            }
            catch (IOException e)
            {
                throw Exceptions.illegalArgumentException("failed to construct BIT STRING from byte[]", e);
            }
        }

        throw new IllegalArgumentException("illegal object in getInstance: " + obj.getClass().getName());
    }

    public static ASN1BitString getInstance(ASN1TaggedObject taggedObject, boolean declaredExplicit)
    {
        return (ASN1BitString)TYPE.getContextTagged(taggedObject, declaredExplicit);
    }

    public static ASN1BitString getTagged(ASN1TaggedObject taggedObject, boolean declaredExplicit)
    {
        return (ASN1BitString)TYPE.getTagged(taggedObject, declaredExplicit);
    }

    static final byte[] EMPTY_OCTETS_CONTENTS = new byte[]{ 0x00 };

    private static final char[]  table = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

    /**
     * @param bitString an int containing the BIT STRING
     * @return the correct number of pad bits for a bit string defined in
     * a 32 bit constant
     * 
     * @deprecated Will be removed
     */
    static protected int getPadBits(int bitString)
    {
        int val = 0;
        for (int i = 3; i >= 0; i--)
        {
            //
            // this may look a little odd, but if it isn't done like this pre jdk1.2
            // JVM's break!
            //
            if (i != 0)
            {
                if ((bitString >> (i * 8)) != 0)
                {
                    val = (bitString >> (i * 8)) & 0xFF;
                    break;
                }
            }
            else
            {
                if (bitString != 0)
                {
                    val = bitString & 0xFF;
                    break;
                }
            }
        }

        if (val == 0)
        {
            return 0;
        }


        int bits = 1;

        while (((val <<= 1) & 0xFF) != 0)
        {
            bits++;
        }

        return 8 - bits;
    }

    /**
     * @param bitString an int containing the BIT STRING
     * @return the correct number of bytes for a bit string defined in
     * a 32 bit constant
     * 
     * @deprecated Will be removed
     */
    static protected byte[] getBytes(int bitString)
    {
        if (bitString == 0)
        {
            return new byte[0];
        }

        int bytes = 4;
        for (int i = 3; i >= 1; i--)
        {
            if ((bitString & (0xFF << (i * 8))) != 0)
            {
                break;
            }
            bytes--;
        }

        byte[] result = new byte[bytes];
        for (int i = 0; i < bytes; i++)
        {
            result[i] = (byte) ((bitString >> (i * 8)) & 0xFF);
        }

        return result;
    }

    final byte[] contents;

    ASN1BitString(byte data, int padBits)
    {
        if (padBits > 7 || padBits < 0)
        {
            throw new IllegalArgumentException("pad bits cannot be greater than 7 or less than 0");
        }

        this.contents = new byte[]{ (byte)padBits, data };
    }

    /**
     * Base constructor.
     *
     * @param data the octets making up the bit string.
     * @param padBits the number of extra bits at the end of the string.
     */
    ASN1BitString(byte[] data, int padBits)
    {
        if (data == null)
        {
            throw new NullPointerException("'data' cannot be null");
        }
        if (padBits > 7 || padBits < 0)
        {
            throw new IllegalArgumentException("pad bits cannot be greater than 7 or less than 0");
        }
        if (data.length == 0 && padBits != 0)
        {
            throw new IllegalArgumentException("zero length data with non-zero pad bits");
        }

        this.contents = Arrays.prepend(data, (byte)padBits);
    }

    ASN1BitString(int namedBits)
    {
        if (namedBits == 0)
        {
            this.contents = EMPTY_OCTETS_CONTENTS;
            return;
        }

        int bits = Integers.bitLength(namedBits);
        int bytes = (bits + 7) / 8;
//        assert 0 < bytes && bytes <= 4;

        byte[] data = new byte[1 + bytes];

        for (int i = 1; i < bytes; i++)
        {
            data[i] = (byte)namedBits;
            namedBits >>>= 8;
        }

//        assert (namedBits & 0xFF) != 0;
        data[bytes] = (byte)namedBits;

        int padBits = Integers.numberOfTrailingZeros(namedBits);
//        assert padBits < 8;
        data[0] = (byte)padBits;

        this.contents = data;
    }

    ASN1BitString(byte[] contents, boolean check)
    {
        if (check)
        {
            if (null == contents)
            {
                throw new NullPointerException("'contents' cannot be null");
            }
            if (contents.length < 1)
            {
                throw new IllegalArgumentException("'contents' cannot be empty");
            }

            int padBits = contents[0] & 0xFF;
            if (padBits > 0)
            {
                if (contents.length < 2)
                {
                    throw new IllegalArgumentException("zero length data with non-zero pad bits");
                }
                if (padBits > 7)
                {
                    throw new IllegalArgumentException("pad bits cannot be greater than 7 or less than 0");
                }
            }
        }

        this.contents = contents;
    }

    /**
     * Build the contents octets for a BIT STRING holding the DER encoding of 'obj' - the
     * pad-count octet (zero) followed by the encoding - in a single allocation.
     */
    static byte[] derEncodedContents(ASN1Encodable obj) throws IOException
    {
        ASN1Primitive primitive = obj.toASN1Primitive().toDERObject();

        byte[] contents = new byte[1 + primitive.encodedLength(true)];

        ContentsOutputStream cOut = new ContentsOutputStream(contents, 1);
        primitive.encodeTo(cOut, ASN1Encoding.DER);
        if (cOut.getPosition() != contents.length)
        {
            throw new IllegalStateException("encoded length did not match actual encoding");
        }

        return contents;
    }

    private static final class ContentsOutputStream
        extends OutputStream
    {
        private final byte[] buf;
        private int pos;

        ContentsOutputStream(byte[] buf, int pos)
        {
            this.buf = buf;
            this.pos = pos;
        }

        public void write(int b)
        {
            buf[pos++] = (byte)b;
        }

        public void write(byte[] b, int off, int len)
        {
            System.arraycopy(b, off, buf, pos, len);
            pos += len;
        }

        int getPosition()
        {
            return pos;
        }
    }

    public InputStream getBitStream() throws IOException
    {
        return new ByteArrayInputStream(contents, 1, contents.length - 1);
    }

    public InputStream getOctetStream() throws IOException
    {
        int padBits = contents[0] & 0xFF;
        if (0 != padBits)
        {
            throw new IOException("expected octet-aligned bitstring, but found padBits: " + padBits);
        }

        return getBitStream();
    }

    public ASN1BitStringParser parser()
    {
        return this;
    }

    /**
     * Return a String representation of this BIT STRING
     *
     * @return a String representation.
     */
    public String getString()
    {
        byte[] string;
        try
        {
            string = getEncoded();
        }
        catch (IOException e)
        {
            throw new ASN1ParsingException("Internal error encoding BitString", e);
        }

        StringBuilder buf = new StringBuilder(1 + string.length * 2);
        buf.append('#');

        for (int i = 0; i != string.length; i++)
        {
            byte b = string[i];
            buf.append(table[(b >>> 4) & 0xf]);
            buf.append(table[b & 0xf]);
        }

        return buf.toString();
    }

    /**
     * @return the value of the bit string as an int (truncating if necessary)
     */
    public int intValue()
    {
        int value = 0;
        int end = Math.min(5, contents.length - 1);
        for (int i = 1; i < end; ++i)
        {
            value |= (contents[i] & 0xFF) << (8 * (i - 1));
        }
        if (1 <= end && end < 5)
        {
            int padBits = contents[0] & 0xFF;
            byte der = (byte)(contents[end] & (0xFF << padBits));
            value |= (der & 0xFF) << (8 * (end - 1));
        }
        return value;
    }

    /**
     * Return the octets contained in this BIT STRING, checking that this BIT STRING really
     * does represent an octet aligned string. Only use this method when the standard you are
     * following dictates that the BIT STRING will be octet aligned.
     *
     * @return a copy of the octet aligned data.
     */
    public byte[] getOctets()
    {
        if (contents[0] != 0)
        {
            throw new IllegalStateException("attempt to get non-octet aligned data from BIT STRING");
        }

        if (contents.length == 1)
        {
            return ASN1OctetString.EMPTY_OCTETS;
        }

        return Arrays.copyOfRange(contents, 1, contents.length);
    }

    public byte[] getBytes()
    {
        if (contents.length == 1)
        {
            return ASN1OctetString.EMPTY_OCTETS;
        }

        int padBits = contents[0] & 0xFF;
        byte[] rv = Arrays.copyOfRange(contents, 1, contents.length);
        // DER requires pad bits be zero
        rv[rv.length - 1] &= (byte)(0xFF << padBits);
        return rv;
    }

    public int getBytesLength()
    {
        return contents.length - 1;
    }

    public boolean isOctetAligned()
    {
        return getPadBits() == 0;
    }

    public int getPadBits()
    {
        return contents[0] & 0xFF;
    }

    public String toString()
    {
        return getString();
    }

    public int hashCode()
    {
        if (contents.length < 2)
        {
            return 1;
        }

        int padBits = contents[0] & 0xFF;
        int last = contents.length - 1;

        byte lastOctetDER = (byte)(contents[last] & (0xFF << padBits));

        int hc = Arrays.hashCode(contents, 0, last);
        hc *= 257;
        hc ^= lastOctetDER;
        return hc;
    }

    boolean asn1Equals(ASN1Primitive other)
    {
        if (!(other instanceof ASN1BitString))
        {
            return false;
        }

        ASN1BitString that = (ASN1BitString)other;
        byte[] thisContents = this.contents, thatContents = that.contents;

        int length = thisContents.length;
        if (thatContents.length != length)
        {
            return false;
        }
        if (length == 1)
        {
            return true;
        }

        int last = length - 1;
        for (int i = 0; i < last; ++i)
        {
            if (thisContents[i] != thatContents[i])
            {
                return false;
            }
        }

        int padBits = thisContents[0] & 0xFF;
        byte thisLastOctetDER = (byte)(thisContents[last] & (0xFF << padBits));
        byte thatLastOctetDER = (byte)(thatContents[last] & (0xFF << padBits));

        return thisLastOctetDER == thatLastOctetDER;
    }

    public ASN1Primitive getLoadedObject()
    {
        return this.toASN1Primitive();
    }

    ASN1Primitive toDERObject()
    {
        return new DERBitString(contents, false);
    }

    ASN1Primitive toDLObject()
    {
        return new DLBitString(contents, false);
    }

    static ASN1BitString createPrimitive(byte[] contents)
    {
        int length = contents.length;
        if (length < 1)
        {
            throw new IllegalArgumentException("truncated BIT STRING detected");
        }

        int padBits = contents[0] & 0xFF;
        if (padBits > 0)
        {
            if (padBits > 7 || length < 2)
            {
                throw new IllegalArgumentException("invalid pad bits detected");
            }

            byte finalOctet = contents[length - 1];
            if (finalOctet != (byte)(finalOctet & (0xFF << padBits)))
            {
                return new DLBitString(contents, false);
            }
        }

        return new DERBitString(contents, false);
    }
}
