/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.apache.directory.api.asn1.util;


import org.apache.directory.api.i18n.I18n;


/**
 * Implement the Bit String primitive type. A BitString is internally stored as
 * an array of byte.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class BitString
{
    /** A null MutableString */
    public static final BitString EMPTY_STRING = new BitString( 1 );

    /** The number of unused bits in the last byte */
    private int nbUnusedBits;

    /** The string is stored in a byte array */
    private byte[] bytes;

    /** Actual length of the byte array */
    private int nbBytes;

    /** Actual length of the bit string */
    private int nbBits;


    /**
     * Creates a BitString with a specific length (length is the number of
     * bits).
     *
     * @param length The BitString length (it's a number of bits)
     */
    public BitString( int length )
    {
        if ( length <= 0 )
        {
            // This is not allowed
            throw new IndexOutOfBoundsException( I18n.err( I18n.ERR_00000_NULL_OR_NEG_LENGTH_NOT_ALLOWED ) );
        }

        nbBits = length;

        // As we store values in bytes, we must divide the length by 8
        nbBytes = length / 8;

        if ( ( length % 8 ) != 0 )
        {
            nbBytes += 1;
        }

        nbUnusedBits = ( 8 - ( length % 8 ) ) & 0x07;

        bytes = new byte[nbBytes];
    }


    /**
     * Creates a BitString from a byte[]. As the first byte is the number of unused bits
     * in the last byte, we have to ignore it.
     *
     * @param bytes The value to store. The first byte contains the number of
     * unused bits
     */
    public BitString( byte[] bytes )
    {
        if ( ( bytes == null ) || ( bytes.length == 0 ) )
        {
            nbBits = -1;
            return;
        }

        setData( bytes );
    }


    /**
     * Set a new BitString in the BitString. It will replace the old BitString,
     * and reset the current length with the new one.
     *
     * @param data The string to store
     */
    public void setData( byte[] data )
    {
        if ( ( data == null ) || ( data.length == 0 ) )
        {
            nbBits = -1;
            return;
        }

        // The first byte contains the number of unused bits
        nbUnusedBits = data[0] & 0x07;
        nbBytes = data.length - 1;
        nbBits = ( nbBytes * 8 ) - nbUnusedBits;
        this.bytes = new byte[nbBytes];

        // We have to transfer the data
        for ( int i = 0; i < nbBytes; i++ )
        {
            this.bytes[i] = data[i + 1];
        }
    }


    /**
     * Get the representation of a BitString. A first byte containing the number
     * of unused bits is added
     *
     * @return A byte array which represent the BitString
     */
    public byte[] getData()
    {
        byte[] copy = new byte[bytes.length + 1];

        System.arraycopy( bytes, 0, copy, 1, bytes.length );
        copy[0] = ( byte ) nbUnusedBits;

        return copy;
    }


    /**
     * Get the number of unused bits
     *
     * @return A byte which represent the number of unused bits
     */
    public byte getUnusedBits()
    {
        return ( byte ) nbUnusedBits;
    }


    /**
     * Set a bit at a specified position.
     * The bits are stored from left to right.
     * For instance, if we have 10 bits, then they are coded as b0 b1 b2 b3 b4 b5 b6 b7 - b8 b9 x x x x x x
     *
     * @param pos The bit to set
     */
    public void setBit( int pos )
    {
        if ( ( pos < 0 ) || ( pos > nbBits ) )
        {
            throw new IndexOutOfBoundsException( I18n.err( I18n.ERR_00001_BIT_NUMBER_OUT_OF_BOUND ) );
        }

        int posBytes = pos >>> 3;
        int bitNumber = 7 - pos % 8;
        byte mask = ( byte ) ( 1 << bitNumber );

        bytes[posBytes] |= mask;
    }


    /**
     * Clear a bit at a specified position.
     * The bits are stored from left to right.
     * For instance, if we have 10 bits, then they are coded
     * as b0 b1 b2 b3 b4 b5 b6 b7 - b8 b9 x x x x x x
     *
     * @param pos The bit to clear
     */
    public void clearBit( int pos )
    {
        if ( ( pos < 0 ) || ( pos > nbBits ) )
        {
            throw new IndexOutOfBoundsException( I18n.err( I18n.ERR_00001_BIT_NUMBER_OUT_OF_BOUND ) );
        }

        int posBytes = pos >>> 3;
        int bitNumber = 7 - pos % 8;
        byte mask = ( byte ) ( 1 << bitNumber );

        bytes[posBytes] &= ~mask;
    }


    /**
     * Get the bit stored into the BitString at a specific position.
     * The bits are stored from left to right, the LSB on the left and the
     * MSB on the right.<br>
     * For instance, if we have 10 bits, then they are coded as
     * b0 b1 b2 b3 - b4 b5 b6 b7 - b8 b9 x x - x x x x
     * <pre>
     * With '1001 000x', where x is an unused bit,
     *       ^ ^    ^
     *       | |    |
     *       | |    |
     *       | |    +----- getBit(6) = 0
     *       | +---------- getBit(2) = 0
     *       +------------ getBit(0) = 1
     * </pre>
     * @param pos The position of the requested bit.
     *
     * @return <code>true</code> if the bit is set, <code>false</code> otherwise
     */
    public boolean getBit( int pos )
    {
        if ( pos > nbBits )
        {
            throw new IndexOutOfBoundsException( I18n.err( I18n.ERR_00002_CANNOT_FIND_BIT, pos, nbBits ) );
        }

        int posBytes = pos >>> 3;
        int bitNumber = 7 - pos % 8;
        byte mask = ( byte ) ( 1 << bitNumber );

        int res = bytes[posBytes] & mask;

        return res != 0;
    }


    /**
     * @return The number of bits stored in this BitString
     */
    public int size()
    {
        return nbBits;
    }


    /**
     * Return a native String representation of the BitString.
     *
     * @return A String representing the BitString
     */
    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder();

        for ( int i = 0; i < nbBits; i++ )
        {
            if ( getBit( i ) )
            {
                sb.append( '1' );
            }
            else
            {
                sb.append( '0' );
            }
        }

        return sb.toString();
    }
}
