/* 
 * FSCodec.java
 * Transform Utilities
 *
 * Copyright (c) 2001-2008 Flagstone Software Ltd. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 *  * Neither the name of Flagstone Software Ltd. nor the names of its contributors 
 *    may be used to endorse or promote products derived from this software 
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.flagstone.transform.util;

/**
 * @deprecated FSCoded is replaced by FSCoder from com.flagstone.transform so
 * there is only one class for reading and writing data.
 * 
 * FSCodec is a similar to Java stream classes, allowing words and bit fields to be read
 * and written from an internal array of bytes. FSCodec supports both little-endian
 * and big-endian byte ordering. The primary use of the class is to support post-processing
 * of sound files loaded using the FSSoundConstructor class where the encoded sound samples
 * contain bits fields. For example compressing 16-bit, byte-aligned sound samples to the 
 * ADPCM format. However the class may be used in any situation where data must be encoded
 * or decoded to an array of bytes.
 * 
 * The FSCodec class maintains an internal pointer which points to the next bit in the internal
 * array where data will be read or written. When calculating an offset in bytes to jump to
 * simply multiply the offset by 8 for the correct bit position. The class provides accessor
 * methods, getPosition() and setPosition() to change the location of the internal pointer.
 * 
 * When writing to an array the size of the array is changed dynamically should a write 
 * operation cause a buffer overflow. For reads if an overflow results then the bits/bytes
 * that overflowed will be set to zero, rather than throwing an exception. The eof() method 
 * can be used to determine whether the end of the buffer has been reached.
 */
public class FSCodec
{
    /** 
     * Identifies that multibyte words are stored in little-endian format with the least
     * significant byte in a word stored first.
     */
    public static final int LITTLE_ENDIAN = 0;

    /** 
     * Identifies that multibyte words are stored in big-endian format with the most
     * significant byte in a word stored first.
     */
    public static final int BIG_ENDIAN = 1;
    
    private int byteOrder = LITTLE_ENDIAN;
    private byte[] data = null;
    private int ptr = 0;
    private int end = 0;
    
    /**
     * Constructs and FSCodec object containing an array of bytes with the specified byte 
     * ordering.
     * 
     * @param order the byte-order for words, either FSCodec.LITTLE_ENDIAN or FSCodec.BIG_ENDIAN.
     * @param bytes an array of bytes where the data will be read or written.
     */
    public FSCodec(int order, byte[] bytes)
    {
        byteOrder = order;
        
        data = new byte[bytes.length];
        System.arraycopy(bytes, 0, data, 0, bytes.length);
        end = data.length << 3;
    }
    
    /**
     * Constructs and FSCodec object containing an array of bytes with the specified byte 
     * ordering.
     * 
     * @param order the byte-order for words, either FSCodec.LITTLE_ENDIAN or FSCodec.BIG_ENDIAN.
     * @param size the size of the internal buffer to be created.
     */
    public FSCodec(int order, int size)
    {
        byteOrder = order;
        
        data = new byte[size];
        end = data.length << 3;
    }
    
    /**
     * Returns a copy of the array of bytes.
     * 
     * @return a copy of the internal buffer.
     */
    public byte[] getData()
    {
        int length = (ptr + 7) >> 3;
        
        byte[] bytes = new byte[length];
        System.arraycopy(data, 0, bytes, 0, length);
        
        return bytes;
    }

    /**
     * Sets the array of bytes used to read or write data to. The size of the array must be 
     * calculated in advance to avoid buffer overflows.
     * 
     * @param bytes a byte array that will be used as the internal buffer.
     */
    public void setData(byte[] bytes)
    {
        data = bytes;
        ptr = 0;
        end = data.length << 3;
    }
    
    /**
     * Sets the array of bytes used to read or write data to. The byte-order for words read
     * or written is also specified. The size of the array must be calculated in advance to 
     * avoid buffer overflows.
     * 
     * @param order the byte-order for words, either FSCodec.LITTLE_ENDIAN or FSCodec.BIG_ENDIAN.
     * @param bytes a byte array that will be used as the internal buffer.
     */
    public void setData(int order, byte[] bytes)
    {
        byteOrder = order;
        data = bytes;
        ptr = 0;
        end = data.length << 3;
    }
    
    /**
     * Returns the offset, in bits, from the start of the buffer where the next value will be 
     * read or written.
     * 
     * @return the offset in bits where the next value will be read or written. 
     */
    public int getPosition()
    {
        return ptr;
    }

    /**
     * Sets the offset, in bits, from the start of the buffer where the next value will be 
     * read or written. If the offset falls outside of the bits range supported by the buffer 
     * then the pointer is clamped to either the start or end of the buffer.
     *  
     * @param offset the offset in bits from the start of the array of bytes.
     */
    public void setPosition(int offset)
    {
        if (offset < 0)
            ptr = 0;
        else if (offset > end)
            ptr = end;
        else
            ptr = offset;
    }

    /**
     * Moves the internal pointer forward so it is aligned on a byte boundary. All word
     * values read and written to the internal buffer must be byte-aligned.
     */
    public void alignToByte()
    {
        ptr = (ptr+7) & ~7;
    }
    
    /**
     * Returns true of the internal pointer is at the end of the buffer.
     * 
     * @return true if the pointer is at the end of the buffer, false otherwise.
     */
    public boolean eof()
    {
        return ptr >= end;
    }

    /**
     * Searches the internal buffer for a bit pattern and advances the pointer to
     * the start of the bit field, returning true to signal a successful search.
     * If the bit pattern cannot be found then the method returns false and the 
     * position of the internal pointer is not changed.
     * 
     * The step, in bits, added to the pointer can be specified, allowing the
     * number of bits being searched to be independent of the location in the 
     * internal buffer. This is useful for example when searching for a bit 
     * field that begins on a byte or word boundary.
     *  
     * @param value an integer containing the bit patter to search for.
     * @param numberOfBits least significant n bits in the value to search for.
     * @param step the increment in bits to add to the internal pointer as the buffer
     * is searched.
     * @return true if the pattern was found, false otherwise.
     */
    public boolean findBits(int value, int numberOfBits, int step)
    {
        boolean found = false;
        int start = ptr;
        int val = 0;
        
        if (numberOfBits < 1 || numberOfBits > 32)
            throw new IllegalArgumentException("Number of bits must be in the range 1..32.");
            
        for (; ptr < end; ptr += step)
        {
            val = readBits(numberOfBits, false);
            ptr -= numberOfBits;
            
            if (val == value)
            {
                found = true;
                break;
            }
        }
        
        if (found == false)
            ptr = start;
            
        return found;
    }

    /**
     * Searches the internal buffer for a word and advances the pointer to the location
     * where the word was found, returning true to signal a successful search. The 
     * search will begin on the next byte boundary. If word cannot be found then the 
     * method returns false and the position of the internal pointer is not changed.
     * 
     * Specifying the number of bytes in the search value allows word of either 8, 16,
     * 24 or 32 bits to be searched for. Searches for words are performed faster than 
     * using the findBits() method.
     * 
     * @param value an integer containing the word to search for.
     * @param numberOfBytes least significant n bytes in the value to search for.
     * @param step the increment in bits to add to the internal pointer as the buffer
     * is searched.
     * @return true if the pattern was found, false otherwise.
     */
    public boolean findWord(int value, int numberOfBytes, int step)
    {
        boolean found = false;
        int start = ptr;
        int val = 0;
        
        if (numberOfBytes < 1 || numberOfBytes > 4)
            throw new IllegalArgumentException("Number of bytes must be in the range 1..4.");
        
        if ((ptr & 7) > 0)
            ptr = (ptr+7) & ~7;
        
        for (; ptr < end; ptr += step)
        {
            val = readWord(numberOfBytes, false);
            ptr -= (numberOfBytes*8);
            
            if (val == value)
            {
                found = true;
                break;
            }
        }
        
        if (found == false)
            ptr = start;
            
        return found;
    }

    /**
     * Read a bit field from the internal buffer.
     * 
     * If a buffer overflow occurs then the number of bits which cause the overflow will 
     * be set to zero.
     * 
     * @param numberOfBits the number of bits to read.
     * @param signed a boolean flag indicating whether the value read should be sign extended.
     * @return the value read.
     */
    public int readBits(int numberOfBits, boolean signed)
    {
        int value = 0;
    
        if (numberOfBits < 1 || numberOfBits > 32)
            throw new IllegalArgumentException("Number of bits must be in the range 1..32.");
        
        int index = ptr >> 3;
        int base = (data.length - index > 4) ? 0  : (4 - (data.length - index))*8;
        
        for (int i=32; i>base; i-=8, index++)
            value |= (data[index] & 0x000000FF) << (i-8);

        value <<= ptr % 8;

        if (signed)
            value >>= 32 - numberOfBits;
        else 
            value >>>= 32 - numberOfBits;

        ptr += numberOfBits;

        if (ptr > (data.length << 3))
            ptr = data.length << 3;

        return value;
    }
    
    /**
     * Write a bit value to the internal buffer. The buffer will resize automatically
     * if required.
     * 
     * @param value an integer containing the value to be written.
     * @param numberOfBits the least significant n bits from the value that will be 
     * written to the buffer.
     */
    public void writeBits(int value, int numberOfBits)
    {
        if (ptr + numberOfBits + 24 > end)
        {
            int length = ((numberOfBits > data.length) ? numberOfBits : data.length) + data.length;
            byte[] bytes = new byte[length];
            
            System.arraycopy(data, 0, bytes, 0, data.length);
            data = bytes;
        }
        
        if (numberOfBits < 1 || numberOfBits > 32)
            throw new IllegalArgumentException("Number of bits must be in the range 1..32.");
        
        int index = ptr >> 3;

        value <<= (32 - numberOfBits);    
        value = value >>> (ptr % 8);
        value = value | (data[index] << 24);

        for (int i=24; i>=0; i-=8, index++)
            data[index] = (byte)(value >>> i);
        
        ptr += numberOfBits;

        if (ptr > (data.length << 3))
            ptr = data.length << 3;
    }
    
    /**
     * Read a word from the internal buffer.
     * 
     * If a buffer overflow occurs then the number of bytes which cause the overflow will 
     * be set to zero.
     * 
     * @param numberOfBytes the number of bytes read in the range 1..4.
     * @param signed a boolean flag indicating whether the value read should be sign extended.
     * @return the value read.
     */
    public int readWord(int numberOfBytes, boolean signed)
    {
        int value = 0;
    
        if (numberOfBytes < 1 || numberOfBytes > 4)
            throw new IllegalArgumentException("Number of bytes must be in the range 1..4.");
        
        int index = ptr >> 3;
        
        if (index + numberOfBytes > data.length)
            numberOfBytes = data.length - index;
        
        int numberOfBits = numberOfBytes*8;

        if (byteOrder == LITTLE_ENDIAN)
        {
            for (int i=0; i<numberOfBits; i+=8, ptr+=8, index++)
                value += (data[index] & 0x000000FF) << i;
        }
        else
        {
            for (int i=0; i<numberOfBits; i+=8, ptr+=8, index++)
            {
                value = value << 8;
                value += data[index] & 0x000000FF;
            }
        }
        
        if (signed)
        {
            value <<= 32 - numberOfBits;
            value >>= 32 - numberOfBits;
        }

        return value;
    }

    /**
     * Write a word to the internal buffer. The buffer will resize automatically
     * if required.
     * 
     * @param value an integer containing the value to be written.
     * @param numberOfBytes the least significant n bytes from the value that will be 
     * written to the buffer.
     */
    public void writeWord(int value, int numberOfBytes)
    {
        if (numberOfBytes < 1 || numberOfBytes > 4)
            throw new IllegalArgumentException("Number of bytes must be in the range 1..4.");
            
        int numberOfBits = numberOfBytes*8;
        
        if (ptr + numberOfBits > end)
        {
            int length = ((numberOfBits > data.length) ? numberOfBits : data.length) + data.length;
            byte[] bytes = new byte[length];
            
            System.arraycopy(data, 0, bytes, 0, data.length);
            data = bytes;
            end = data.length << 3;
        }
        
        if (byteOrder == LITTLE_ENDIAN)
        {
            int index = ptr >>> 3;
            
            for (int i=0; i<numberOfBits; i+=8, ptr+=8, value >>>= 8, index++)
                data[index] = (byte)value;
        }
        else
        {
            int index = (ptr + numberOfBits - 8) >>> 3;
            
            for (int i=0; i<numberOfBits; i+=8, ptr+=8, value >>>= 8, index--)
                data[index] = (byte)value;
        }
    }

    /**
     * Reads an array of bytes from the internal buffer. If a read overflow occurs
     * while reading the internal buffer then the remaining bytes in the array will
     * not be filled. The method returns the number of bytes read.
     * 
     * @param bytes the array that will contain the bytes read.
     * @return the number of bytes read from the buffer.
     */
    public int readBytes(byte[] bytes)
    {
        int bytesRead = 0;
    
        if (bytes == null || bytes.length == 0)
            return bytesRead;

        int index = ptr >>> 3;
        int numberOfBytes = bytes.length;
        
        if (index + numberOfBytes > data.length)
            numberOfBytes = data.length - index;

        for (int i=0; i<numberOfBytes; i++, ptr+=8, index++, bytesRead++)
            bytes[i] = data[index];

        return bytesRead;
    }

    /**
     * Writes an array of bytes from the internal buffer. The internal buffer will
     * be resized automatically if required.
     * 
     * @param bytes the array containing the data to be written.
     * @return the number of bytes written to the buffer.
     */
    public int writeBytes(byte[] bytes)
    {
        int bytesWritten = 0;
    
        if (ptr + (bytes.length << 3) > end)
        {
            int length = ((bytes.length > data.length) ? bytes.length : data.length) + data.length;
            byte[] buffer = new byte[length];
            
            System.arraycopy(data, 0, buffer, 0, data.length);
            data = bytes;
            end = data.length << 3;
        }
        
        if (bytes == null || bytes.length == 0)
            return bytesWritten;

        int index = ptr >>> 3;
        int numberOfBytes = bytes.length;

        for (int i=0; i<numberOfBytes; i++, ptr+=8, index++, bytesWritten++)
            data[index] = bytes[i];
        
        return bytesWritten;
    }
    
    byte[] getBuffer()
    {
        return data;
    }

    int getPointer()
    {
        return ptr;
    }

    void setPointer(int location)
    {
        if (location < 0)
            location = 0;
        else if (location > end)
            location = end;

        ptr = location;
    }

    void adjustPointer(int offset)
    {
        ptr += offset;
        
        if (ptr < 0)
            ptr = 0;
        else if (ptr >= end)
            ptr = end;
    }
    
    int readBits(int numberOfBits)
    {
        int value = 0;
    
        if (numberOfBits == 0)
            return value;
        
        int index = ptr >> 3;
        int base = (data.length - index > 4) ? 0  : (4 - (data.length - index))*8;
        
        for (int i=32; i>base; i-=8, index++)
            value |= (data[index] & 0x000000FF) << (i-8);

        value <<= ptr % 8;
        value >>>= 32 - numberOfBits;

        ptr += numberOfBits;

        if (ptr > (data.length << 3))
            ptr = data.length << 3;

        return value;
    }
    
    int readByte()
    {
        int value = data[ptr>>3] & 0xFF;
    
        ptr += 8;
        
        return value;
    }

    int readShort(boolean signed)
    {
        int value = 0;
        int index = ptr >> 3;
        
        if (byteOrder == LITTLE_ENDIAN)
        {
            value = (data[index++] & 0xFF);    
            value += (data[index] & 0xFF) << 8;
        }
        else
        {
            value = (data[index++] & 0xFF) << 8;
            value += (data[index] & 0xFF);    
        }

        if (signed)
        {
            value <<= 16;
            value >>= 16;
        }
        ptr += 16;
        
        return value;
    }

    int readInt()
    {
        int value = 0;
        int index = ptr >> 3;
        
        if (byteOrder == LITTLE_ENDIAN)
        {
            value = (data[index++] & 0xFF);    
            value += (data[index++] & 0xFF) << 8;
            value += (data[index++] & 0xFF) << 16;
            value += (data[index] & 0xFF) << 24;
        }
        else
        {
            value = (data[index++] & 0xFF) << 24;
            value += (data[index++] & 0xFF) << 16;
            value += (data[index++] & 0xFF) << 8;
            value += (data[index] & 0xFF);    
        }
        ptr += 32;
        
        return value;
    }

    float readFixedBits(int numberOfBits, int fractionSize)
    {
        float value = 0;
        float divisor = (float)(1 << fractionSize);
    
        value = ((float)readBits(numberOfBits, true)) / divisor;
        
        return value;
    }
}
