/*
 * %CopyrightBegin%
 * 
 * Copyright Ericsson AB 2000-2009. All Rights Reserved.
 * 
 * The contents of this file are subject to the Erlang Public License,
 * Version 1.1, (the "License"); you may not use this file except in
 * compliance with the License. You should have received a copy of the
 * Erlang Public License along with this software. If not, it can be
 * retrieved online at http://www.erlang.org/.
 * 
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * %CopyrightEnd%
 */
package com.ericsson.otp.erlang;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigDecimal;

/**
 * Provides a stream for decoding Erlang terms from external format.
 * 
 * <p>
 * Note that this class is not synchronized, if you need synchronization you
 * must provide it yourself.
 */
public class OtpInputStream extends ByteArrayInputStream {

    public static int DECODE_INT_LISTS_AS_STRINGS = 1;

    private final int flags;

    /**
     * @param buf
     */
    public OtpInputStream(final byte[] buf) {
	this(buf, 0);
    }

    /**
     * Create a stream from a buffer containing encoded Erlang terms.
     * 
     * @param flags
     */
    public OtpInputStream(final byte[] buf, final int flags) {
	super(buf);
	this.flags = flags;
    }

    /**
     * Create a stream from a buffer containing encoded Erlang terms at the
     * given offset and length.
     * 
     * @param flags
     */
    public OtpInputStream(final byte[] buf, final int offset, final int length,
	    final int flags) {
	super(buf, offset, length);
	this.flags = flags;
    }

    /**
     * Get the current position in the stream.
     * 
     * @return the current position in the stream.
     */
    public int getPos() {
	return super.pos;
    }

    /**
     * Set the current position in the stream.
     * 
     * @param pos
     *            the position to move to in the stream. If pos indicates a
     *            position beyond the end of the stream, the position is move to
     *            the end of the stream instead. If pos is negative, the
     *            position is moved to the beginning of the stream instead.
     * 
     * @return the previous position in the stream.
     */
    public int setPos(int pos) {
	final int oldpos = super.pos;

	if (pos > super.count) {
	    pos = super.count;
	} else if (pos < 0) {
	    pos = 0;
	}

	super.pos = pos;

	return oldpos;
    }

    /**
     * Read an array of bytes from the stream. The method reads at most
     * buf.length bytes from the input stream.
     * 
     * @return the number of bytes read.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public int readN(final byte[] buf) throws OtpErlangDecodeException {
	return this.readN(buf, 0, buf.length);
    }

    /**
     * Read an array of bytes from the stream. The method reads at most len
     * bytes from the input stream into offset off of the buffer.
     * 
     * @return the number of bytes read.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public int readN(final byte[] buf, final int off, final int len)
	    throws OtpErlangDecodeException {
	if (len == 0 && available() == 0) {
	    return 0;
	}
	final int i = super.read(buf, off, len);
	if (i < 0) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}
	return i;
    }

    /**
     * Alias for peek1()
     */
    public int peek() throws OtpErlangDecodeException {
	return peek1();
    }

    /**
     * Look ahead one position in the stream without consuming the byte found
     * there.
     * 
     * @return the next byte in the stream, as an integer.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public int peek1() throws OtpErlangDecodeException {
	int i;
	try {
	    i = super.buf[super.pos];
	    if (i < 0) {
		i += 256;
	    }

	    return i;
	} catch (final Exception e) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}
    }

    public int peek1skip_version() throws OtpErlangDecodeException {
	int i = peek1();
	if (i == OtpExternal.versionTag) {
	    read1();
	    i = peek1();
	}
	return i;
    }

    /**
     * Read a one byte integer from the stream.
     * 
     * @return the byte read, as an integer.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public int read1() throws OtpErlangDecodeException {
	int i;
	i = super.read();

	if (i < 0) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}

	return i;
    }

    public int read1skip_version() throws OtpErlangDecodeException {
	int tag = read1();
	if (tag == OtpExternal.versionTag) {
	    tag = read1();
	}
	return tag;
    }

    /**
     * Read a two byte big endian integer from the stream.
     * 
     * @return the bytes read, converted from big endian to an integer.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public int read2BE() throws OtpErlangDecodeException {
	final byte[] b = new byte[2];
	try {
	    super.read(b);
	} catch (final IOException e) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}
	;
	return (b[0] << 8 & 0xff00) + (b[1] & 0xff);
    }

    /**
     * Read a four byte big endian integer from the stream.
     * 
     * @return the bytes read, converted from big endian to an integer.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public int read4BE() throws OtpErlangDecodeException {
	final byte[] b = new byte[4];
	try {
	    super.read(b);
	} catch (final IOException e) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}
	;
	return (b[0] << 24 & 0xff000000) + (b[1] << 16 & 0xff0000)
		+ (b[2] << 8 & 0xff00) + (b[3] & 0xff);
    }

    /**
     * Read a two byte little endian integer from the stream.
     * 
     * @return the bytes read, converted from little endian to an integer.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public int read2LE() throws OtpErlangDecodeException {
	final byte[] b = new byte[2];
	try {
	    super.read(b);
	} catch (final IOException e) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}
	;
	return (b[1] << 8 & 0xff00) + (b[0] & 0xff);
    }

    /**
     * Read a four byte little endian integer from the stream.
     * 
     * @return the bytes read, converted from little endian to an integer.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public int read4LE() throws OtpErlangDecodeException {
	final byte[] b = new byte[4];
	try {
	    super.read(b);
	} catch (final IOException e) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}
	;
	return (b[3] << 24 & 0xff000000) + (b[2] << 16 & 0xff0000)
		+ (b[1] << 8 & 0xff00) + (b[0] & 0xff);
    }

    /**
     * Read a little endian integer from the stream.
     * 
     * @param n
     *            the number of bytes to read
     * 
     * @return the bytes read, converted from little endian to an integer.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public long readLE(int n) throws OtpErlangDecodeException {
	final byte[] b = new byte[n];
	try {
	    super.read(b);
	} catch (final IOException e) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}
	;
	long v = 0;
	while (n-- > 0) {
	    v = v << 8 | (long) b[n] & 0xff;
	}
	return v;
    }

    /**
     * Read a bigendian integer from the stream.
     * 
     * @param n
     *            the number of bytes to read
     * 
     * @return the bytes read, converted from big endian to an integer.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public long readBE(final int n) throws OtpErlangDecodeException {
	final byte[] b = new byte[n];
	try {
	    super.read(b);
	} catch (final IOException e) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}
	;
	long v = 0;
	for (int i = 0; i < n; i++) {
	    v = v << 8 | (long) b[i] & 0xff;
	}
	return v;
    }

    /**
     * Read an Erlang atom from the stream and interpret the value as a boolean.
     * 
     * @return true if the atom at the current position in the stream contains
     *         the value 'true' (ignoring case), false otherwise.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not an atom.
     */
    public boolean read_boolean() throws OtpErlangDecodeException {
	return Boolean.valueOf(read_atom()).booleanValue();
    }

    /**
     * Read an Erlang atom from the stream.
     * 
     * @return a String containing the value of the atom.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not an atom.
     */
    public String read_atom() throws OtpErlangDecodeException {
	int tag;
	int len;
	byte[] strbuf;
	String atom;

	tag = read1skip_version();

	if (tag != OtpExternal.atomTag) {
	    throw new OtpErlangDecodeException(
		    "wrong tag encountered, expected " + OtpExternal.atomTag
			    + ", got " + tag);
	}

	len = read2BE();

	strbuf = new byte[len];
	this.readN(strbuf);
	atom = OtpErlangString.newString(strbuf);

	if (atom.length() > OtpExternal.maxAtomLength) {
	    atom = atom.substring(0, OtpExternal.maxAtomLength);
	}

	return atom;
    }

    /**
     * Read an Erlang binary from the stream.
     * 
     * @return a byte array containing the value of the binary.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not a binary.
     */
    public byte[] read_binary() throws OtpErlangDecodeException {
	int tag;
	int len;
	byte[] bin;

	tag = read1skip_version();

	if (tag != OtpExternal.binTag) {
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected " + OtpExternal.binTag
			    + ", got " + tag);
	}

	len = read4BE();

	bin = new byte[len];
	this.readN(bin);

	return bin;
    }

    /**
     * Read an Erlang bitstr from the stream.
     * 
     * @param pad_bits
     *            an int array whose first element will be set to the number of
     *            pad bits in the last byte.
     * 
     * @return a byte array containing the value of the bitstr.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not a bitstr.
     */
    public byte[] read_bitstr(final int pad_bits[])
	    throws OtpErlangDecodeException {
	int tag;
	int len;
	byte[] bin;

	tag = read1skip_version();

	if (tag != OtpExternal.bitBinTag) {
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected " + OtpExternal.bitBinTag
			    + ", got " + tag);
	}

	len = read4BE();
	bin = new byte[len];
	final int tail_bits = read1();
	if (tail_bits < 0 || 7 < tail_bits) {
	    throw new OtpErlangDecodeException(
		    "Wrong tail bit count in bitstr: " + tail_bits);
	}
	if (len == 0 && tail_bits != 0) {
	    throw new OtpErlangDecodeException(
		    "Length 0 on bitstr with tail bit count: " + tail_bits);
	}
	this.readN(bin);

	pad_bits[0] = 8 - tail_bits;
	return bin;
    }

    /**
     * Read an Erlang float from the stream.
     * 
     * @return the float value.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not a float.
     */
    public float read_float() throws OtpErlangDecodeException {
	final double d = read_double();
	return (float) d;
    }

    /**
     * Read an Erlang float from the stream.
     * 
     * @return the float value, as a double.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not a float.
     */
    public double read_double() throws OtpErlangDecodeException {
	int tag;

	// parse the stream
	tag = read1skip_version();

	switch (tag) {
	case OtpExternal.newFloatTag: {
	    return Double.longBitsToDouble(readBE(8));
	}
	case OtpExternal.floatTag: {
	    BigDecimal val;
	    int epos;
	    int exp;
	    final byte[] strbuf = new byte[31];
	    String str;

	    // get the string
	    this.readN(strbuf);
	    str = OtpErlangString.newString(strbuf);

	    // find the exponent prefix 'e' in the string
	    epos = str.indexOf('e', 0);

	    if (epos < 0) {
		throw new OtpErlangDecodeException("Invalid float format: '"
			+ str + "'");
	    }

	    // remove the sign from the exponent, if positive
	    String estr = str.substring(epos + 1).trim();

	    if (estr.substring(0, 1).equals("+")) {
		estr = estr.substring(1);
	    }

	    // now put the mantissa and exponent together
	    exp = Integer.valueOf(estr).intValue();
	    val = new BigDecimal(str.substring(0, epos)).movePointRight(exp);

	    return val.doubleValue();
	}
	default:
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected "
			    + OtpExternal.newFloatTag + ", got " + tag);
	}
    }

    /**
     * Read one byte from the stream.
     * 
     * @return the byte read.
     * 
     * @exception OtpErlangDecodeException
     *                if the next byte cannot be read.
     */
    public byte read_byte() throws OtpErlangDecodeException {
	final long l = this.read_long(false);
	final byte i = (byte) l;

	if (l != i) {
	    throw new OtpErlangDecodeException("Value does not fit in byte: "
		    + l);
	}

	return i;
    }

    /**
     * Read a character from the stream.
     * 
     * @return the character value.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not an integer that can
     *                be represented as a char.
     */
    public char read_char() throws OtpErlangDecodeException {
	final long l = this.read_long(true);
	final char i = (char) l;

	if (l != (i & 0xffffL)) {
	    throw new OtpErlangDecodeException("Value does not fit in char: "
		    + l);
	}

	return i;
    }

    /**
     * Read an unsigned integer from the stream.
     * 
     * @return the integer value.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream can not be represented as a
     *                positive integer.
     */
    public int read_uint() throws OtpErlangDecodeException {
	final long l = this.read_long(true);
	final int i = (int) l;

	if (l != (i & 0xFFFFffffL)) {
	    throw new OtpErlangDecodeException("Value does not fit in uint: "
		    + l);
	}

	return i;
    }

    /**
     * Read an integer from the stream.
     * 
     * @return the integer value.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream can not be represented as
     *                an integer.
     */
    public int read_int() throws OtpErlangDecodeException {
	final long l = this.read_long(false);
	final int i = (int) l;

	if (l != i) {
	    throw new OtpErlangDecodeException("Value does not fit in int: "
		    + l);
	}

	return i;
    }

    /**
     * Read an unsigned short from the stream.
     * 
     * @return the short value.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream can not be represented as a
     *                positive short.
     */
    public short read_ushort() throws OtpErlangDecodeException {
	final long l = this.read_long(true);
	final short i = (short) l;

	if (l != (i & 0xffffL)) {
	    throw new OtpErlangDecodeException("Value does not fit in ushort: "
		    + l);
	}

	return i;
    }

    /**
     * Read a short from the stream.
     * 
     * @return the short value.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream can not be represented as a
     *                short.
     */
    public short read_short() throws OtpErlangDecodeException {
	final long l = this.read_long(false);
	final short i = (short) l;

	if (l != i) {
	    throw new OtpErlangDecodeException("Value does not fit in short: "
		    + l);
	}

	return i;
    }

    /**
     * Read an unsigned long from the stream.
     * 
     * @return the long value.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream can not be represented as a
     *                positive long.
     */
    public long read_ulong() throws OtpErlangDecodeException {
	return this.read_long(true);
    }

    /**
     * Read a long from the stream.
     * 
     * @return the long value.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream can not be represented as a
     *                long.
     */
    public long read_long() throws OtpErlangDecodeException {
	return this.read_long(false);
    }

    public long read_long(final boolean unsigned)
	    throws OtpErlangDecodeException {
	final byte[] b = read_integer_byte_array();
	return OtpInputStream.byte_array_to_long(b, unsigned);
    }

    /**
     * Read an integer from the stream.
     * 
     * @return the value as a big endian 2's complement byte array.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not an integer.
     */
    public byte[] read_integer_byte_array() throws OtpErlangDecodeException {
	int tag;
	byte[] nb;

	tag = read1skip_version();

	switch (tag) {
	case OtpExternal.smallIntTag:
	    nb = new byte[2];
	    nb[0] = 0;
	    nb[1] = (byte) read1();
	    break;

	case OtpExternal.intTag:
	    nb = new byte[4];
	    if (this.readN(nb) != 4) { // Big endian
		throw new OtpErlangDecodeException(
			"Cannot read from intput stream");
	    }
	    break;

	case OtpExternal.smallBigTag:
	case OtpExternal.largeBigTag:
	    int arity;
	    int sign;
	    if (tag == OtpExternal.smallBigTag) {
		arity = read1();
		sign = read1();
	    } else {
		arity = read4BE();
		sign = read1();
		if (arity + 1 < 0) {
		    throw new OtpErlangDecodeException(
			    "Value of largeBig does not fit in BigInteger, arity "
				    + arity + " sign " + sign);
		}
	    }
	    nb = new byte[arity + 1];
	    // Value is read as little endian. The big end is augumented
	    // with one zero byte to make the value 2's complement positive.
	    if (this.readN(nb, 0, arity) != arity) {
		throw new OtpErlangDecodeException(
			"Cannot read from intput stream");
	    }
	    // Reverse the array to make it big endian.
	    for (int i = 0, j = nb.length; i < j--; i++) {
		// Swap [i] with [j]
		final byte b = nb[i];
		nb[i] = nb[j];
		nb[j] = b;
	    }
	    if (sign != 0) {
		// 2's complement negate the big endian value in the array
		int c = 1; // Carry
		for (int j = nb.length; j-- > 0;) {
		    c = (~nb[j] & 0xFF) + c;
		    nb[j] = (byte) c;
		    c >>= 8;
		}
	    }
	    break;

	default:
	    throw new OtpErlangDecodeException("Not valid integer tag: " + tag);
	}

	return nb;
    }

    public static long byte_array_to_long(final byte[] b, final boolean unsigned)
	    throws OtpErlangDecodeException {
	long v;
	switch (b.length) {
	case 0:
	    v = 0;
	    break;
	case 2:
	    v = ((b[0] & 0xFF) << 8) + (b[1] & 0xFF);
	    v = (short) v; // Sign extend
	    if (v < 0 && unsigned) {
		throw new OtpErlangDecodeException("Value not unsigned: " + v);
	    }
	    break;
	case 4:
	    v = ((b[0] & 0xFF) << 24) + ((b[1] & 0xFF) << 16)
		    + ((b[2] & 0xFF) << 8) + (b[3] & 0xFF);
	    v = (int) v; // Sign extend
	    if (v < 0 && unsigned) {
		throw new OtpErlangDecodeException("Value not unsigned: " + v);
	    }
	    break;
	default:
	    int i = 0;
	    final byte c = b[i];
	    // Skip non-essential leading bytes
	    if (unsigned) {
		if (c < 0) {
		    throw new OtpErlangDecodeException("Value not unsigned: "
			    + b);
		}
		while (b[i] == 0) {
		    i++; // Skip leading zero sign bytes
		}
	    } else {
		if (c == 0 || c == -1) { // Leading sign byte
		    i = 1;
		    // Skip all leading sign bytes
		    while (i < b.length && b[i] == c) {
			i++;
		    }
		    if (i < b.length) {
			// Check first non-sign byte to see if its sign
			// matches the whole number's sign. If not one more
			// byte is needed to represent the value.
			if (((c ^ b[i]) & 0x80) != 0) {
			    i--;
			}
		    }
		}
	    }
	    if (b.length - i > 8) {
		// More than 64 bits of value
		throw new OtpErlangDecodeException(
			"Value does not fit in long: " + b);
	    }
	    // Convert the necessary bytes
	    for (v = c < 0 ? -1 : 0; i < b.length; i++) {
		v = v << 8 | b[i] & 0xFF;
	    }
	}
	return v;
    }

    /**
     * Read a list header from the stream.
     * 
     * @return the arity of the list.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not a list.
     */
    public int read_list_head() throws OtpErlangDecodeException {
	int arity = 0;
	final int tag = read1skip_version();

	switch (tag) {
	case OtpExternal.nilTag:
	    arity = 0;
	    break;

	case OtpExternal.stringTag:
	    arity = read2BE();
	    break;

	case OtpExternal.listTag:
	    arity = read4BE();
	    break;

	default:
	    throw new OtpErlangDecodeException("Not valid list tag: " + tag);
	}

	return arity;
    }

    /**
     * Read a tuple header from the stream.
     * 
     * @return the arity of the tuple.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not a tuple.
     */
    public int read_tuple_head() throws OtpErlangDecodeException {
	int arity = 0;
	final int tag = read1skip_version();

	// decode the tuple header and get arity
	switch (tag) {
	case OtpExternal.smallTupleTag:
	    arity = read1();
	    break;

	case OtpExternal.largeTupleTag:
	    arity = read4BE();
	    break;

	default:
	    throw new OtpErlangDecodeException("Not valid tuple tag: " + tag);
	}

	return arity;
    }

    /**
     * Read an empty list from the stream.
     * 
     * @return zero (the arity of the list).
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not an empty list.
     */
    public int read_nil() throws OtpErlangDecodeException {
	int arity = 0;
	final int tag = read1skip_version();

	switch (tag) {
	case OtpExternal.nilTag:
	    arity = 0;
	    break;

	default:
	    throw new OtpErlangDecodeException("Not valid nil tag: " + tag);
	}

	return arity;
    }

    /**
     * Read an Erlang PID from the stream.
     * 
     * @return the value of the PID.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not an Erlang PID.
     */
    public OtpErlangPid read_pid() throws OtpErlangDecodeException {
	String node;
	int id;
	int serial;
	int creation;
	int tag;

	tag = read1skip_version();

	if (tag != OtpExternal.pidTag) {
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected " + OtpExternal.pidTag
			    + ", got " + tag);
	}

	node = read_atom();
	id = read4BE() & 0x7fff; // 15 bits
	serial = read4BE() & 0x1fff; // 13 bits
	creation = read1() & 0x03; // 2 bits

	return new OtpErlangPid(node, id, serial, creation);
    }

    /**
     * Read an Erlang port from the stream.
     * 
     * @return the value of the port.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not an Erlang port.
     */
    public OtpErlangPort read_port() throws OtpErlangDecodeException {
	String node;
	int id;
	int creation;
	int tag;

	tag = read1skip_version();

	if (tag != OtpExternal.portTag) {
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected " + OtpExternal.portTag
			    + ", got " + tag);
	}

	node = read_atom();
	id = read4BE() & 0xfffffff; // 28 bits
	creation = read1() & 0x03; // 2 bits

	return new OtpErlangPort(node, id, creation);
    }

    /**
     * Read an Erlang reference from the stream.
     * 
     * @return the value of the reference
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not an Erlang reference.
     */
    public OtpErlangRef read_ref() throws OtpErlangDecodeException {
	String node;
	int id;
	int creation;
	int tag;

	tag = read1skip_version();

	switch (tag) {
	case OtpExternal.refTag:
	    node = read_atom();
	    id = read4BE() & 0x3ffff; // 18 bits
	    creation = read1() & 0x03; // 2 bits
	    return new OtpErlangRef(node, id, creation);

	case OtpExternal.newRefTag:
	    final int arity = read2BE();
	    node = read_atom();
	    creation = read1() & 0x03; // 2 bits

	    final int[] ids = new int[arity];
	    for (int i = 0; i < arity; i++) {
		ids[i] = read4BE();
	    }
	    ids[0] &= 0x3ffff; // first id gets truncated to 18 bits
	    return new OtpErlangRef(node, ids, creation);

	default:
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected ref, got " + tag);
	}
    }

    public OtpErlangFun read_fun() throws OtpErlangDecodeException {
	final int tag = read1skip_version();
	if (tag == OtpExternal.funTag) {
	    final int nFreeVars = read4BE();
	    final OtpErlangPid pid = read_pid();
	    final String module = read_atom();
	    final long index = read_long();
	    final long uniq = read_long();
	    final OtpErlangObject[] freeVars = new OtpErlangObject[nFreeVars];
	    for (int i = 0; i < nFreeVars; ++i) {
		freeVars[i] = read_any();
	    }
	    return new OtpErlangFun(pid, module, index, uniq, freeVars);
	} else if (tag == OtpExternal.newFunTag) {
	    final int n = read4BE();
	    final int arity = read1();
	    final byte[] md5 = new byte[16];
	    readN(md5);
	    final int index = read4BE();
	    final int nFreeVars = read4BE();
	    final String module = read_atom();
	    final long oldIndex = read_long();
	    final long uniq = read_long();
	    final OtpErlangPid pid = read_pid();
	    final OtpErlangObject[] freeVars = new OtpErlangObject[nFreeVars];
	    for (int i = 0; i < nFreeVars; ++i) {
		freeVars[i] = read_any();
	    }
	    return new OtpErlangFun(pid, module, arity, md5, index, oldIndex,
		    uniq, freeVars);
	} else {
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected fun, got " + tag);
	}
    }

    public OtpErlangExternalFun read_external_fun()
	    throws OtpErlangDecodeException {
	final int tag = read1skip_version();
	if (tag != OtpExternal.externalFunTag) {
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected external fun, got " + tag);
	}
	final String module = read_atom();
	final String function = read_atom();
	final int arity = (int) read_long();
	return new OtpErlangExternalFun(module, function, arity);
    }

    /**
     * Read a string from the stream.
     * 
     * @return the value of the string.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not a string.
     */
    public String read_string() throws OtpErlangDecodeException {
	int tag;
	int len;
	byte[] strbuf;
	int[] intbuf;
	tag = read1skip_version();
	switch (tag) {
	case OtpExternal.stringTag:
	    len = read2BE();
	    strbuf = new byte[len];
	    this.readN(strbuf);
	    return OtpErlangString.newString(strbuf);
	case OtpExternal.nilTag:
	    return "";
	case OtpExternal.listTag: // List when unicode +
	    len = read4BE();
	    intbuf = new int[len];
	    for (int i = 0; i < len; i++) {
		intbuf[i] = read_int();
		if (! OtpErlangString.isValidCodePoint(intbuf[i])) {
		    throw new OtpErlangDecodeException
			("Invalid CodePoint: " + intbuf[i]);
		}
	    }
	    read_nil();
	    return new String(intbuf, 0, intbuf.length);
	default:
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected " + OtpExternal.stringTag
			    + " or " + OtpExternal.listTag + ", got " + tag);
	}
    }

    /**
     * Read a compressed term from the stream
     * 
     * @return the resulting uncompressed term.
     * 
     * @exception OtpErlangDecodeException
     *                if the next term in the stream is not a compressed term.
     */
    public OtpErlangObject read_compressed() throws OtpErlangDecodeException {
	final int tag = read1skip_version();

	if (tag != OtpExternal.compressedTag) {
	    throw new OtpErlangDecodeException(
		    "Wrong tag encountered, expected "
			    + OtpExternal.compressedTag + ", got " + tag);
	}

	final int size = read4BE();
	final byte[] buf = new byte[size];
	final java.util.zip.InflaterInputStream is = 
	    new java.util.zip.InflaterInputStream(this);
	try {
	    final int dsize = is.read(buf, 0, size);
	    if (dsize != size) {
		throw new OtpErlangDecodeException("Decompression gave "
			+ dsize + " bytes, not " + size);
	    }
	} catch (final IOException e) {
	    throw new OtpErlangDecodeException("Cannot read from input stream");
	}

	final OtpInputStream ois = new OtpInputStream(buf, flags);
	return ois.read_any();
    }

    /**
     * Read an arbitrary Erlang term from the stream.
     * 
     * @return the Erlang term.
     * 
     * @exception OtpErlangDecodeException
     *                if the stream does not contain a known Erlang type at the
     *                next position.
     */
    public OtpErlangObject read_any() throws OtpErlangDecodeException {
	// calls one of the above functions, depending on o
	final int tag = peek1skip_version();

	switch (tag) {
	case OtpExternal.smallIntTag:
	case OtpExternal.intTag:
	case OtpExternal.smallBigTag:
	case OtpExternal.largeBigTag:
	    return new OtpErlangLong(this);

	case OtpExternal.atomTag:
	    return new OtpErlangAtom(this);

	case OtpExternal.floatTag:
	case OtpExternal.newFloatTag:
	    return new OtpErlangDouble(this);

	case OtpExternal.refTag:
	case OtpExternal.newRefTag:
	    return new OtpErlangRef(this);

	case OtpExternal.portTag:
	    return new OtpErlangPort(this);

	case OtpExternal.pidTag:
	    return new OtpErlangPid(this);

	case OtpExternal.stringTag:
	    return new OtpErlangString(this);

	case OtpExternal.listTag:
	case OtpExternal.nilTag:
	    if ((flags & DECODE_INT_LISTS_AS_STRINGS) != 0) {
		final int savePos = getPos();
		try {
		    return new OtpErlangString(this);
		} catch (final OtpErlangDecodeException e) {
		}
		setPos(savePos);
	    }
	    return new OtpErlangList(this);

	case OtpExternal.smallTupleTag:
	case OtpExternal.largeTupleTag:
	    return new OtpErlangTuple(this);

	case OtpExternal.binTag:
	    return new OtpErlangBinary(this);

	case OtpExternal.bitBinTag:
	    return new OtpErlangBitstr(this);

	case OtpExternal.compressedTag:
	    return read_compressed();

	case OtpExternal.newFunTag:
	case OtpExternal.funTag:
	    return new OtpErlangFun(this);

	default:
	    throw new OtpErlangDecodeException("Uknown data type: " + tag);
	}
    }
}
