/*
 * 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
 *
 *      http://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.
 */

/* $Id: PackedBitArray.java 1156213 2011-08-10 15:06:20Z jeremias $ */

package org.apache.pdfbox.io.ccitt;

/**
 * Represents an array of bits packed in a byte array of fixed size.
 * @version $Revision$
 */
public class PackedBitArray
{

    private int bitCount;
    private byte[] data;

    /**
     * Constructs a new bit array.
     * @param bitCount the number of bits to maintain
     */
    public PackedBitArray(int bitCount)
    {
        this.bitCount = bitCount;
        int byteCount = (bitCount + 7) / 8;
        this.data = new byte[byteCount];
    }

    private int byteOffset(int offset)
    {
        return offset / 8;
    }

    private int bitOffset(int offset)
    {
        return offset % 8;
    }

    /**
     * Sets a bit at the given offset.
     * @param offset the offset
     */
    public void set(int offset)
    {
        int byteOffset = byteOffset(offset);
        this.data[byteOffset] |= 1 << bitOffset(offset);
    }

    /**
     * Clears a bit at the given offset.
     * @param offset the offset
     */
    public void clear(int offset)
    {
        int byteOffset = byteOffset(offset);
        int bitOffset = bitOffset(offset);
        this.data[byteOffset] &= ~(1 << bitOffset);
    }

    /**
     * Sets a run of bits at the given offset to either 1 or 0.
     * @param offset the offset
     * @param length the number of bits to set
     * @param bit 1 to set the bit, 0 to clear it
     */
    public void setBits(int offset, int length, int bit)
    {
        if (bit == 0)
        {
            clearBits(offset, length);
        }
        else
        {
            setBits(offset, length);
        }
    }

    /**
     * Sets a run of bits at the given offset to either 1.
     * @param offset the offset
     * @param length the number of bits to set
     */
    public void setBits(int offset, int length)
    {
        if (length == 0)
        {
            return;
        }
        int startBitOffset = bitOffset(offset);
        int firstByte = byteOffset(offset);
        int lastBitOffset = offset + length;
        if (lastBitOffset > getBitCount())
        {
            throw new IndexOutOfBoundsException("offset + length > bit count");
        }
        int lastByte = byteOffset(lastBitOffset);
        int endBitOffset = bitOffset(lastBitOffset);

        if (firstByte == lastByte)
        {
            //Only one byte affected
            int mask = (1 << endBitOffset) - (1 << startBitOffset);
            this.data[firstByte] |= mask;
        }
        else
        {
            //Bits spanning multiple bytes
            this.data[firstByte] |= 0xFF << startBitOffset;
            for (int i = firstByte + 1; i < lastByte; i++)
            {
                this.data[i] = (byte)0xFF;
            }
            if (endBitOffset > 0)
            {
                this.data[lastByte] |= 0xFF >> (8 - endBitOffset);
            }
        }
    }

    /**
     * Clears a run of bits at the given offset.
     * @param offset the offset
     * @param length the number of bits to clear
     */
    public void clearBits(int offset, int length)
    {
        if (length == 0)
        {
            return;
        }
        int startBitOffset = offset % 8;
        int firstByte = byteOffset(offset);
        int lastBitOffset = offset + length;
        int lastByte = byteOffset(lastBitOffset);
        int endBitOffset = lastBitOffset % 8;

        if (firstByte == lastByte)
        {
            //Only one byte affected
            int mask = (1 << endBitOffset) - (1 << startBitOffset);
            this.data[firstByte] &= ~mask;
        }
        else
        {
            //Bits spanning multiple bytes
            this.data[firstByte] &= ~(0xFF << startBitOffset);
            for (int i = firstByte + 1; i < lastByte; i++)
            {
                this.data[i] = (byte)0x00;
            }
            if (endBitOffset > 0)
            {
                this.data[lastByte] &= ~(0xFF >> (8 - endBitOffset));
            }
        }
    }

    /**
     * Clear all bits in the array.
     */
    public void clear()
    {
        clearBits(0, getBitCount());
    }

    /**
     * Returns the number of bits maintained by this array.
     * @return the number of bits
     */
    public int getBitCount()
    {
        return this.bitCount;
    }

    /**
     * Returns the size of the byte buffer for this array.
     * @return the size of the byte buffer
     */
    public int getByteCount()
    {
        return this.data.length;
    }

    /**
     * Returns the underlying byte buffer.
     * <p>
     * Note: the actual buffer is returned. If it's manipulated
     * the content of the bit array changes.
     * @return the underlying data buffer
     */
    public byte[] getData()
    {
        return this.data;
    }

    /** {@inheritDoc} */
    public String toString()
    {
        return toBitString(this.data).substring(0, this.bitCount);
    }

    /**
     * Converts a byte to a "binary" String of 0s and 1s.
     * @param data the value to convert
     * @return the binary string
     */
    public static String toBitString(byte data)
    {
        byte[] buf = new byte[] {data};
        return toBitString(buf);
    }

    /**
     * Converts a series of bytes to a "binary" String of 0s and 1s.
     * @param data the data
     * @return the binary string
     */
    public static String toBitString(byte[] data)
    {
        return toBitString(data, 0, data.length);
    }

    /**
     * Converts a series of bytes to a "binary" String of 0s and 1s.
     * @param data the data
     * @param start the start offset
     * @param len the number of bytes to convert
     * @return the binary string
     */
    public static String toBitString(byte[] data, int start, int len)
    {
        StringBuffer sb = new StringBuffer();
        for (int x = start, end = start + len; x < end; x++)
        {
            for (int i = 0; i < 8; i++)
            {
                int mask = 1 << i;
                int value = data[x] & mask;
                sb.append(value != 0 ? '1' : '0');
            }
        }
        return sb.toString();
    }

}
