package org.hypergraphdb.storage.lmdb.type;

import java.math.BigDecimal;
import java.math.BigInteger;

import org.hypergraphdb.storage.lmdb.type.util.FastInputStream;
import org.hypergraphdb.storage.lmdb.type.util.PackedInteger;
import org.hypergraphdb.storage.lmdb.type.util.UtfOps;


/**
 * An <code>InputStream</code> with <code>DataInput</code>-like methods for
 * reading tuple fields.  It is used by <code>TupleBinding</code>.
 *
 * <p>This class has many methods that have the same signatures as methods in
 * the {@link java.io.DataInput} interface.  The reason this class does not
 * implement {@link java.io.DataInput} is because it would break the interface
 * contract for those methods because of data format differences.</p>
 *
 * @see <a href="package-summary.html#formats">Tuple Formats</a>
 *
 * @author Mark Hayes
 */
public class TupleInput extends FastInputStream {

    /**
     * Creates a tuple input object for reading a byte array of tuple data.  A
     * reference to the byte array will be kept by this object (it will not be
     * copied) and therefore the byte array should not be modified while this
     * object is in use.
     *
     * @param buffer is the byte array to be read and should contain data in
     * tuple format.
     */
    public TupleInput(byte[] buffer) {

        super(buffer);
    }

    /**
     * Creates a tuple input object for reading a byte array of tuple data at
     * a given offset for a given length.  A reference to the byte array will
     * be kept by this object (it will not be copied) and therefore the byte
     * array should not be modified while this object is in use.
     *
     * @param buffer is the byte array to be read and should contain data in
     * tuple format.
     *
     * @param offset is the byte offset at which to begin reading.
     *
     * @param length is the number of bytes to be read.
     */
    public TupleInput(byte[] buffer, int offset, int length) {

        super(buffer, offset, length);
    }

    /**
     * Creates a tuple input object from the data contained in a tuple output
     * object.  A reference to the tuple output's byte array will be kept by
     * this object (it will not be copied) and therefore the tuple output
     * object should not be modified while this object is in use.
     *
     * @param output is the tuple output object containing the data to be read.
     */
    public TupleInput(TupleOutput output) {

        super(output.getBufferBytes(), output.getBufferOffset(),
              output.getBufferLength());
    }

    // --- begin DataInput compatible methods ---

    /**
     * Reads a null-terminated UTF string from the data buffer and converts
     * the data from UTF to Unicode.
     * Reads values that were written using {@link
     * TupleOutput#writeString(String)}.
     *
     * @return the converted string.
     *
     * @throws IndexOutOfBoundsException if no null terminating byte is found
     * in the buffer.
     *
     * @throws IllegalArgumentException malformed UTF data is encountered.
     *
     * @see <a href="package-summary.html#stringFormats">String Formats</a>
     */
    public final String readString()
        throws IndexOutOfBoundsException, IllegalArgumentException  {

        byte[] myBuf = buf;
        int myOff = off;
        if (available() >= 2 &&
            myBuf[myOff] == TupleOutput.NULL_STRING_UTF_VALUE &&
            myBuf[myOff + 1] == 0) {
            skip(2);
            return null;
        } else {
            int byteLen = UtfOps.getZeroTerminatedByteLength(myBuf, myOff);
            skip(byteLen + 1);
            return UtfOps.bytesToString(myBuf, myOff, byteLen);
        }
    }

    /**
     * Reads a char (two byte) unsigned value from the buffer.
     * Reads values that were written using {@link TupleOutput#writeChar}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final char readChar()
        throws IndexOutOfBoundsException {

        return (char) readUnsignedShort();
    }

    /**
     * Reads a boolean (one byte) unsigned value from the buffer and returns
     * true if it is non-zero and false if it is zero.
     * Reads values that were written using {@link TupleOutput#writeBoolean}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final boolean readBoolean()
        throws IndexOutOfBoundsException {

        int c = readFast();
        if (c < 0) {
            throw new IndexOutOfBoundsException();
        }
        return (c != 0);
    }

    /**
     * Reads a signed byte (one byte) value from the buffer.
     * Reads values that were written using {@link TupleOutput#writeByte}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final byte readByte()
        throws IndexOutOfBoundsException {

        return (byte) (readUnsignedByte() ^ 0x80);
    }

    /**
     * Reads a signed short (two byte) value from the buffer.
     * Reads values that were written using {@link TupleOutput#writeShort}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final short readShort()
        throws IndexOutOfBoundsException {

        return (short) (readUnsignedShort() ^ 0x8000);
    }

    /**
     * Reads a signed int (four byte) value from the buffer.
     * Reads values that were written using {@link TupleOutput#writeInt}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int readInt()
        throws IndexOutOfBoundsException {

        return (int) (readUnsignedInt() ^ 0x80000000);
    }

    /**
     * Reads a signed long (eight byte) value from the buffer.
     * Reads values that were written using {@link TupleOutput#writeLong}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final long readLong()
        throws IndexOutOfBoundsException {

        return readUnsignedLong() ^ 0x8000000000000000L;
    }

    /**
     * Reads an unsorted float (four byte) value from the buffer.
     * Reads values that were written using {@link TupleOutput#writeFloat}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#floatFormats">Floating Point
     * Formats</a>
     */
    public final float readFloat()
        throws IndexOutOfBoundsException {

        return Float.intBitsToFloat((int) readUnsignedInt());
    }

    /**
     * Reads an unsorted double (eight byte) value from the buffer.
     * Reads values that were written using {@link TupleOutput#writeDouble}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#floatFormats">Floating Point
     * Formats</a>
     */
    public final double readDouble()
        throws IndexOutOfBoundsException {

        return Double.longBitsToDouble(readUnsignedLong());
    }

    /**
     * Reads a sorted float (four byte) value from the buffer.
     * Reads values that were written using {@link
     * TupleOutput#writeSortedFloat}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#floatFormats">Floating Point
     * Formats</a>
     */
    public final float readSortedFloat()
        throws IndexOutOfBoundsException {

        int val = (int) readUnsignedInt();
        val ^= (val < 0) ? 0x80000000 : 0xffffffff;
        return Float.intBitsToFloat(val);
    }

    /**
     * Reads a sorted double (eight byte) value from the buffer.
     * Reads values that were written using {@link
     * TupleOutput#writeSortedDouble}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#floatFormats">Floating Point
     * Formats</a>
     */
    public final double readSortedDouble()
        throws IndexOutOfBoundsException {

        long val = readUnsignedLong();
        val ^= (val < 0) ? 0x8000000000000000L : 0xffffffffffffffffL;
        return Double.longBitsToDouble(val);
    }

    /**
     * Reads an unsigned byte (one byte) value from the buffer.
     * Reads values that were written using {@link
     * TupleOutput#writeUnsignedByte}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int readUnsignedByte()
        throws IndexOutOfBoundsException {

        int c = readFast();
        if (c < 0) {
            throw new IndexOutOfBoundsException();
        }
        return c;
    }

    /**
     * Reads an unsigned short (two byte) value from the buffer.
     * Reads values that were written using {@link
     * TupleOutput#writeUnsignedShort}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int readUnsignedShort()
        throws IndexOutOfBoundsException {

        int c1 = readFast();
        int c2 = readFast();
        if ((c1 | c2) < 0) {
             throw new IndexOutOfBoundsException();
        }
        return ((c1 << 8) | c2);
    }

    // --- end DataInput compatible methods ---

    /**
     * Reads an unsigned int (four byte) value from the buffer.
     * Reads values that were written using {@link
     * TupleOutput#writeUnsignedInt}.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final long readUnsignedInt()
        throws IndexOutOfBoundsException {

        long c1 = readFast();
        long c2 = readFast();
        long c3 = readFast();
        long c4 = readFast();
        if ((c1 | c2 | c3 | c4) < 0) {
            throw new IndexOutOfBoundsException();
        }
        return ((c1 << 24) | (c2 << 16) | (c3 << 8) | c4);
    }

    /**
     * This method is private since an unsigned long cannot be treated as
     * such in Java, nor converted to a BigInteger of the same value.
     */
    private final long readUnsignedLong()
        throws IndexOutOfBoundsException {

        long c1 = readFast();
        long c2 = readFast();
        long c3 = readFast();
        long c4 = readFast();
        long c5 = readFast();
        long c6 = readFast();
        long c7 = readFast();
        long c8 = readFast();
        if ((c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8) < 0) {
             throw new IndexOutOfBoundsException();
        }
        return ((c1 << 56) | (c2 << 48) | (c3 << 40) | (c4 << 32) |
                (c5 << 24) | (c6 << 16) | (c7 << 8)  | c8);
    }

    /**
     * Reads the specified number of bytes from the buffer, converting each
     * unsigned byte value to a character of the resulting string.
     * Reads values that were written using {@link TupleOutput#writeBytes}.
     *
     * @param length is the number of bytes to be read.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final String readBytes(int length)
        throws IndexOutOfBoundsException {

        StringBuilder buf = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            int c = readFast();
            if (c < 0) {
                throw new IndexOutOfBoundsException();
            }
            buf.append((char) c);
        }
        return buf.toString();
    }

    /**
     * Reads the specified number of characters from the buffer, converting
     * each two byte unsigned value to a character of the resulting string.
     * Reads values that were written using {@link TupleOutput#writeChars}.
     *
     * @param length is the number of characters to be read.
     *
     * @return the value read from the buffer.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final String readChars(int length)
        throws IndexOutOfBoundsException {

        StringBuilder buf = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            buf.append(readChar());
        }
        return buf.toString();
    }

    /**
     * Reads the specified number of bytes from the buffer, converting each
     * unsigned byte value to a character of the resulting array.
     * Reads values that were written using {@link TupleOutput#writeBytes}.
     *
     * @param chars is the array to receive the data and whose length is used
     * to determine the number of bytes to be read.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final void readBytes(char[] chars)
        throws IndexOutOfBoundsException {

        for (int i = 0; i < chars.length; i++) {
            int c = readFast();
            if (c < 0) {
                throw new IndexOutOfBoundsException();
            }
            chars[i] = (char) c;
        }
    }

    /**
     * Reads the specified number of characters from the buffer, converting
     * each two byte unsigned value to a character of the resulting array.
     * Reads values that were written using {@link TupleOutput#writeChars}.
     *
     * @param chars is the array to receive the data and whose length is used
     * to determine the number of characters to be read.
     *
     * @throws IndexOutOfBoundsException if not enough bytes are available in
     * the buffer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final void readChars(char[] chars)
        throws IndexOutOfBoundsException {

        for (int i = 0; i < chars.length; i++) {
            chars[i] = readChar();
        }
    }

    /**
     * Reads the specified number of UTF characters string from the data
     * buffer and converts the data from UTF to Unicode.
     * Reads values that were written using {@link
     * TupleOutput#writeString(char[])}.
     *
     * @param length is the number of characters to be read.
     *
     * @return the converted string.
     *
     * @throws IndexOutOfBoundsException if no null terminating byte is found
     * in the buffer.
     *
     * @throws IllegalArgumentException malformed UTF data is encountered.
     *
     * @see <a href="package-summary.html#stringFormats">String Formats</a>
     */
    public final String readString(int length)
        throws IndexOutOfBoundsException, IllegalArgumentException  {

        char[] chars = new char[length];
        readString(chars);
        return new String(chars);
    }

    /**
     * Reads the specified number of UTF characters string from the data
     * buffer and converts the data from UTF to Unicode.
     * Reads values that were written using {@link
     * TupleOutput#writeString(char[])}.
     *
     * @param chars is the array to receive the data and whose length is used
     * to determine the number of characters to be read.
     *
     * @throws IndexOutOfBoundsException if no null terminating byte is found
     * in the buffer.
     *
     * @throws IllegalArgumentException malformed UTF data is encountered.
     *
     * @see <a href="package-summary.html#stringFormats">String Formats</a>
     */
    public final void readString(char[] chars)
        throws IndexOutOfBoundsException, IllegalArgumentException  {

        off = UtfOps.bytesToChars(buf, off, chars, 0, chars.length, false);
    }

    /**
     * Returns the byte length of a null-terminated UTF string in the data
     * buffer, including the terminator.  Used with string values that were
     * written using {@link TupleOutput#writeString(String)}.
     *
     * @throws IndexOutOfBoundsException if no null terminating byte is found
     * in the buffer.
     *
     * @throws IllegalArgumentException malformed UTF data is encountered.
     *
     * @see <a href="package-summary.html#stringFormats">String Formats</a>
     */
    public final int getStringByteLength()
        throws IndexOutOfBoundsException, IllegalArgumentException  {

        if (available() >= 2 &&
            buf[off] == TupleOutput.NULL_STRING_UTF_VALUE &&
            buf[off + 1] == 0) {
            return 2;
        } else {
            return UtfOps.getZeroTerminatedByteLength(buf, off) + 1;
        }
    }

    /**
     * Reads an unsorted packed integer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int readPackedInt() {

        int len = PackedInteger.getReadIntLength(buf, off);
        int val = PackedInteger.readInt(buf, off);

        off += len;
        return val;
    }

    /**
     * Returns the byte length of a packed integer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int getPackedIntByteLength() {
        return PackedInteger.getReadIntLength(buf, off);
    }

    /**
     * Reads an unsorted packed long integer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final long readPackedLong() {

        int len = PackedInteger.getReadLongLength(buf, off);
        long val = PackedInteger.readLong(buf, off);

        off += len;
        return val;
    }

    /**
     * Returns the byte length of a packed long integer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int getPackedLongByteLength() {
        return PackedInteger.getReadLongLength(buf, off);
    }

    /**
     * Reads a sorted packed integer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int readSortedPackedInt() {

        int len = PackedInteger.getReadSortedIntLength(buf, off);
        int val = PackedInteger.readSortedInt(buf, off);

        off += len;
        return val;
    }

    /**
     * Returns the byte length of a sorted packed integer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int getSortedPackedIntByteLength() {
        return PackedInteger.getReadSortedIntLength(buf, off);
    }

    /**
     * Reads a sorted packed long integer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final long readSortedPackedLong() {

        int len = PackedInteger.getReadSortedLongLength(buf, off);
        long val = PackedInteger.readSortedLong(buf, off);

        off += len;
        return val;
    }

    /**
     * Returns the byte length of a sorted packed long integer.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int getSortedPackedLongByteLength() {
        return PackedInteger.getReadSortedLongLength(buf, off);
    }

    /**
     * Reads a {@code BigInteger}.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final BigInteger readBigInteger() {
    
        int len = readShort();
        if (len < 0) {
            len = (- len);
        }
        byte[] a = new byte[len];
        a[0] = readByte();
        readFast(a, 1, a.length - 1);
        return new BigInteger(a);
    }

    /**
     * Returns the byte length of a {@code BigInteger}.
     *
     * @see <a href="package-summary.html#integerFormats">Integer Formats</a>
     */
    public final int getBigIntegerByteLength() {
    
        int saveOff = off;
        int len = readShort();
        off = saveOff;
        if (len < 0) {
            len = (- len);
        }
        return len + 2;
    }
    
    /**
     * Reads an unsorted {@code BigDecimal}.
     *
     * @see <a href="package-summary.html#bigDecimalFormats">BigDecimal
     * Formats</a>
     */
    public final BigDecimal readBigDecimal() {
    
        int scale = readPackedInt();
        int len = readPackedInt();
        byte[] a = new byte[len];
        readFast(a, 0, len);
        BigInteger unscaledVal = new BigInteger(a);
        return new BigDecimal(unscaledVal, scale);
    }
    
    /**
     * Returns the byte length of an unsorted {@code BigDecimal}.
     *
     * @see <a href="package-summary.html#bigDecimalFormats">BigDecimal
     * Formats</a>
     */
    public final int getBigDecimalByteLength() {
    
        /* First get the length of the scale. */
        int scaleLen = getPackedIntByteLength();
        int saveOff = off;
        off += scaleLen;
        
        /* 
         * Then get the length of the value which store the length of the 
         * following bytes. 
         */
        int lenOfUnscaleValLen = getPackedIntByteLength();
        
        /* Finally get the length of the following bytes. */
        int unscaledValLen = readPackedInt();
        off = saveOff;
        return scaleLen + lenOfUnscaleValLen + unscaledValLen;
    }
    
    /**
     * Reads a sorted {@code BigDecimal}, with support for correct default 
     * sorting.
     *
     * @see <a href="package-summary.html#bigDecimalFormats">BigDecimal
     * Formats</a>
     */
    public final BigDecimal readSortedBigDecimal() {    
        /* Get the sign of the BigDecimal. */
        int sign = readByte();
        
        /* Get the exponent of the BigDecimal. */
        int exponent = readSortedPackedInt();
        
        /*Get the normalized BigDecimal. */
        BigDecimal normalizedVal = readSortedNormalizedBigDecimal();
        
        /* 
         * After getting the normalized BigDecimal, we need to scale the value
         * with the exponent.
         */
        return normalizedVal.scaleByPowerOfTen(exponent * sign);
    }
    
    /**
     * Reads a sorted {@code BigDecimal} in normalized format with a single
     * digit to the left of the decimal point.
     */
    private final BigDecimal readSortedNormalizedBigDecimal() { 

        StringBuilder valStr = new StringBuilder(32);
        int subVal = readSortedPackedInt();
        int sign = subVal < 0 ? -1 : 1;
        
        /* Read through the buf, until we meet the terminator byte. */
        while (subVal != -1) {
        
            /* Adjust the sub-value back to the original. */
            subVal = subVal < 0 ? subVal + 1 : subVal;
            String groupDigits = Integer.toString(Math.abs(subVal));
        
            /* 
             * subVal < 100000000 means some leading zeros have been removed,
             * we have to add them back.
             */ 
            if (groupDigits.length() < 9) {
                final int insertLen = 9 - groupDigits.length();
                for (int i = 0; i < insertLen; i++) {
                    valStr.append("0");
                }
            }
            valStr.append(groupDigits);
            subVal = readSortedPackedInt();
        }

        BigInteger digitsVal = new BigInteger(valStr.toString());
        if (sign < 0) {
            digitsVal = digitsVal.negate();
        }
        /* The normalized decimal has 1 digits in the int part. */
        int scale = valStr.length() - 1;
        
        /* 
         * Since we may pad trailing zeros for serialization, when doing 
         * de-serialization, we need to delete the trailing zeros. 
         */
        return new BigDecimal(digitsVal, scale).stripTrailingZeros();
    }
    
    /**
     * Returns the byte length of a sorted {@code BigDecimal}.
     *
     * @see <a href="package-summary.html#bigDecimalFormats">BigDecimal
     * Formats</a>
     */
    public final int getSortedBigDecimalByteLength() {
    
        /* Save the original position, and read past the sigh byte. */
        int saveOff = off++;

        /* Get the length of the exponent. */
        int len = getSortedPackedIntByteLength(); /* the exponent */

        /* Skip to the digit part. */
        off += len;
        
        /* 
         * Travel through the following SortedPackedIntegers, until we meet the 
         * terminator byte. 
         */
        int subVal = readSortedPackedInt();
        while (subVal != -1) {
            subVal = readSortedPackedInt();
        }
        
        /* 
         * off is the value of end offset, while saveOff is the beginning 
         * offset.
         */
        len = off - saveOff;
        off = saveOff;
        return len;
    }
}