Array.java

package zserio.runtime.array;

import java.io.IOException;

import zserio.runtime.BitPositionUtil;
import zserio.runtime.BitSizeOfCalculator;
import zserio.runtime.io.BitStreamReader;
import zserio.runtime.io.BitStreamWriter;

/**
 * Array wrapper which is used for mapping of all zserio arrays.
 *
 * Array wrapper is defined by:
 * - raw array
 * - array traits which define zserio methods for array elements of corresponding Java native types
 * - array type (normal, auto, implicit)
 * - offset initializer to set offsets for indexed offsets arrays
 * - offset checker to check offsets for indexed offsets arrays
 */
public final class Array
{
    /**
     * Constructor.
     *
     * @param rawArray    Raw array to construct from.
     * @param arrayTraits Array traits to construct from.
     * @param arrayType   Array type to construct from.
     */
    public Array(RawArray rawArray, ArrayTraits arrayTraits, ArrayType arrayType)
    {
        this(rawArray, arrayTraits, arrayType, null, null);
    }

    /**
     * Constructor.
     *
     * @param rawArray      Raw array to construct from.
     * @param arrayTraits   Array traits to construct from.
     * @param arrayType     Array type to construct from.
     * @param offsetChecker Offset checker to construct from.
     */
    public Array(RawArray rawArray, ArrayTraits arrayTraits, ArrayType arrayType, OffsetChecker offsetChecker)
    {
        this(rawArray, arrayTraits, arrayType, offsetChecker, null);
    }

    /**
     * Constructor.
     *
     * @param rawArray          Raw array to construct from.
     * @param arrayTraits       Array traits to construct from.
     * @param arrayType         Array type to construct from.
     * @param offsetChecker     Offset checker to construct from.
     * @param offsetInitializer Offset initializer to construct from.
     */
    public Array(RawArray rawArray, ArrayTraits arrayTraits, ArrayType arrayType, OffsetChecker offsetChecker,
            OffsetInitializer offsetInitializer)
    {
        this.rawArray = rawArray;
        this.arrayTraits = arrayTraits;
        this.packedArrayTraits = arrayTraits.getPackedArrayTraits();
        this.arrayType = arrayType;
        this.offsetChecker = offsetChecker;
        this.offsetInitializer = offsetInitializer;
    }

    @Override
    public boolean equals(java.lang.Object obj)
    {
        return (obj instanceof Array) ? rawArray.equals(((Array)obj).rawArray) : false;
    }

    @Override
    public int hashCode()
    {
        return rawArray.hashCode();
    }

    /**
     * Gets the underlying raw array.
     *
     * @param <T> Java array type to be returned.
     *
     * @return Underlying raw array.
     */
    public <T> T getRawArray()
    {
        return rawArray.getRawArray();
    }

    /**
     * Gets the underlying raw array size.
     *
     * @return The number of elements stored in the underlying raw array.
     */
    public int size()
    {
        return rawArray.size();
    }

    /**
     * Gets the bit size of the array if it is stored in the bit stream.
     *
     * @param bitPosition Current bit position in the bit stream.
     *
     * @return Bit size of the array if it is stored in the bit stream.
     */
    public int bitSizeOf(long bitPosition)
    {
        long endBitPosition = bitPosition;
        final int size = rawArray.size();
        if (arrayType == ArrayType.AUTO)
            endBitPosition += BitSizeOfCalculator.getBitSizeOfVarSize(size);

        if (size > 0)
        {
            if (arrayTraits.isBitSizeOfConstant())
            {
                final int elementSize = arrayTraits.bitSizeOf(endBitPosition, ArrayElement.Dummy);
                if (offsetInitializer == null)
                {
                    endBitPosition += size * elementSize;
                }
                else
                {
                    endBitPosition = BitPositionUtil.alignTo(Byte.SIZE, endBitPosition);
                    endBitPosition +=
                            elementSize + (size - 1) * BitPositionUtil.alignTo(Byte.SIZE, elementSize);
                }
            }
            else
            {
                for (int index = 0; index < size; ++index)
                {
                    if (offsetInitializer != null)
                        endBitPosition = BitPositionUtil.alignTo(Byte.SIZE, endBitPosition);

                    endBitPosition += arrayTraits.bitSizeOf(endBitPosition, rawArray.getElement(index));
                }
            }
        }

        return (int)(endBitPosition - bitPosition);
    }

    /**
     * Returns length of the packed array stored in the bit stream in bits.
     *
     * @param bitPosition Current bit stream position.
     *
     * @return Length of the array stored in the bit stream in bits.
     */
    public int bitSizeOfPacked(long bitPosition)
    {
        checkIfPackable();

        long endBitPosition = bitPosition;
        final int size = rawArray.size();
        if (arrayType == ArrayType.AUTO)
            endBitPosition += BitSizeOfCalculator.getBitSizeOfVarSize(size);

        if (size > 0)
        {
            final PackingContext context = packedArrayTraits.createContext();
            for (int index = 0; index < size; ++index)
                packedArrayTraits.initContext(context, rawArray.getElement(index));

            for (int index = 0; index < size; ++index)
            {
                if (offsetInitializer != null)
                    endBitPosition = BitPositionUtil.alignTo(Byte.SIZE, endBitPosition);

                endBitPosition +=
                        packedArrayTraits.bitSizeOf(context, endBitPosition, rawArray.getElement(index));
            }
        }

        return (int)(endBitPosition - bitPosition);
    }

    /**
     * Initializes indexed offsets for the array.
     *
     * @param bitPosition Current bit position in the bit stream.
     *
     * @return Updated bit stream position which points to the first bit after the array.
     */
    public long initializeOffsets(long bitPosition)
    {
        long endBitPosition = bitPosition;
        final int size = rawArray.size();
        if (arrayType == ArrayType.AUTO)
            endBitPosition += BitSizeOfCalculator.getBitSizeOfVarSize(size);

        for (int index = 0; index < size; ++index)
        {
            if (offsetInitializer != null)
            {
                endBitPosition = BitPositionUtil.alignTo(Byte.SIZE, endBitPosition);
                offsetInitializer.setOffset(index, BitPositionUtil.bitsToBytes(endBitPosition));
            }

            endBitPosition = arrayTraits.initializeOffsets(endBitPosition, rawArray.getElement(index));
        }

        return endBitPosition;
    }

    /**
     * Initializes indexed offsets for the packed array.
     *
     * @param bitPosition Current bit stream position.
     *
     * @return Updated bit stream position which points to the first bit after the array.
     */
    public long initializeOffsetsPacked(long bitPosition)
    {
        checkIfPackable();

        long endBitPosition = bitPosition;
        final int size = rawArray.size();
        if (arrayType == ArrayType.AUTO)
            endBitPosition += BitSizeOfCalculator.getBitSizeOfVarSize(size);

        if (size > 0)
        {
            final PackingContext context = packedArrayTraits.createContext();
            for (int index = 0; index < size; ++index)
                packedArrayTraits.initContext(context, rawArray.getElement(index));

            for (int index = 0; index < size; ++index)
            {
                if (offsetInitializer != null)
                {
                    endBitPosition = BitPositionUtil.alignTo(Byte.SIZE, endBitPosition);
                    offsetInitializer.setOffset(index, BitPositionUtil.bitsToBytes(endBitPosition));
                }

                endBitPosition = packedArrayTraits.initializeOffsets(
                        context, endBitPosition, rawArray.getElement(index));
            }
        }

        return endBitPosition;
    }

    /**
     * Reads the array from the bit stream.
     *
     * @param reader Bit stream reader to read from.
     *
     * @throws IOException Failure during bit stream manipulation.
     */
    public void read(BitStreamReader reader) throws IOException
    {
        read(reader, -1);
    }

    /**
     * Reads the array from the bit stream.
     *
     * @param reader Bit stream reader to read from.
     * @param size   Number of elements stored in the array which shall be read.
     *
     * @throws IOException Failure during bit stream manipulation.
     */
    public void read(BitStreamReader reader, int size) throws IOException
    {
        int readSize = size;
        if (arrayType == ArrayType.IMPLICIT)
        {
            if (!arrayTraits.isBitSizeOfConstant())
            {
                throw new UnsupportedOperationException(
                        "Array: Implicit array elements must have constant bit size!");
            }

            final long readerBitPosition = reader.getBitPosition();
            final int elementSize = arrayTraits.bitSizeOf(readerBitPosition, ArrayElement.Dummy);
            final long remainingBits = reader.getBufferBitSize() - readerBitPosition;
            readSize = (int)(remainingBits / elementSize);
        }
        else if (arrayType == ArrayType.AUTO)
        {
            readSize = reader.readVarSize();
        }

        rawArray.reset(readSize);

        for (int index = 0; index < readSize; ++index)
        {
            if (offsetChecker != null)
            {
                reader.alignTo(Byte.SIZE);
                offsetChecker.checkOffset(index, reader.getBytePosition());
            }

            final ArrayElement element = arrayTraits.read(reader, index);
            rawArray.setElement(element, index);
        }
    }

    /**
     * Reads packed array from the bit stream.
     *
     * This method has all possible arguments and from generated code is used for aligned object arrays.
     *
     * @param reader Bit stream from which to read.
     *
     * @throws IOException Failure during bit stream manipulation.
     */
    public void readPacked(BitStreamReader reader) throws IOException
    {
        readPacked(reader, -1);
    }

    /**
     * Reads packed array from the bit stream.
     *
     * This method has all possible arguments and from generated code is used for aligned object arrays.
     *
     * @param reader Bit stream from which to read.
     * @param size Number of elements to read.
     *
     * @throws IOException Failure during bit stream manipulation.
     */
    public void readPacked(BitStreamReader reader, int size) throws IOException
    {
        checkIfPackable();

        final int readSize = (arrayType == ArrayType.AUTO) ? reader.readVarSize() : size;

        rawArray.reset(readSize);

        if (readSize > 0)
        {
            final PackingContext context = packedArrayTraits.createContext();

            for (int index = 0; index < readSize; ++index)
            {
                if (offsetChecker != null)
                {
                    reader.alignTo(Byte.SIZE);
                    offsetChecker.checkOffset(index, reader.getBytePosition());
                }

                final ArrayElement element = packedArrayTraits.read(context, reader, index);
                rawArray.setElement(element, index);
            }
        }
    }

    /**
     * Writes the array element to the bit stream.
     *
     * @param writer Bit stream write to write to.
     *
     * @throws IOException Failure during bit stream manipulation.
     */
    public void write(BitStreamWriter writer) throws IOException
    {
        final int size = rawArray.size();
        if (arrayType == ArrayType.AUTO)
            writer.writeVarSize(size);

        for (int index = 0; index < size; ++index)
        {
            if (offsetChecker != null)
            {
                writer.alignTo(Byte.SIZE);
                offsetChecker.checkOffset(index, writer.getBytePosition());
            }

            arrayTraits.write(writer, rawArray.getElement(index));
        }
    }

    /**
     * Writes packed array to the bit stream.
     *
     * @param writer Bit stream where to write.
     *
     * @throws IOException Failure during bit stream manipulation.
     */
    public void writePacked(BitStreamWriter writer) throws IOException
    {
        checkIfPackable();

        final int size = rawArray.size();
        if (arrayType == ArrayType.AUTO)
            writer.writeVarSize(size);

        if (size > 0)
        {
            final PackingContext context = packedArrayTraits.createContext();
            for (int index = 0; index < size; ++index)
                packedArrayTraits.initContext(context, rawArray.getElement(index));

            for (int index = 0; index < size; ++index)
            {
                if (offsetChecker != null)
                {
                    writer.alignTo(Byte.SIZE);
                    offsetChecker.checkOffset(index, writer.getBytePosition());
                }

                packedArrayTraits.write(context, writer, rawArray.getElement(index));
            }
        }
    }

    private void checkIfPackable()
    {
        if (packedArrayTraits == null)
            throw new UnsupportedOperationException("Array: The array is not packable!");

        if (arrayType == ArrayType.IMPLICIT)
            throw new UnsupportedOperationException("Array: Implicit array cannot be packed!");
    }

    private final RawArray rawArray;
    private final ArrayTraits arrayTraits;
    private final PackedArrayTraits packedArrayTraits;
    private final ArrayType arrayType;
    private final OffsetChecker offsetChecker;
    private final OffsetInitializer offsetInitializer;
}