package weka.classifiers.rules;

import java.io.*;

public class BinaryFile {

    /**
     * Use this constant to specify big-endian integers.
     */
    public static final short BIG_ENDIAN = 1;

    /**
     * Use this constant to specify litte-endian constants.
     */
    public static final short LITTLE_ENDIAN = 2;

    /**
     * The underlying file.
     */
    protected RandomAccessFile _file;

    /**
     * Are we in LITTLE_ENDIAN or BIG_ENDIAN mode.
     */
    protected short _endian;

    /**
     * Are we reading signed or unsigned numbers.
     */
    protected boolean _signed;

    /**
     * The constructor.  Use to specify the underlying file.
     *
     * @param f The file to read/write from/to.
     */
    public BinaryFile(RandomAccessFile f) {
        _file = f;
        _endian = LITTLE_ENDIAN;
        _signed = false;
    }

    /**
     * Set the endian mode for reading integers.
     *
     * @param i Specify either LITTLE_ENDIAN or BIG_ENDIAN.
     * @exception java.lang.Exception Will be thrown if this method is not passed either BinaryFile.LITTLE_ENDIAN or BinaryFile.BIG_ENDIAN.
     */
    public void setEndian(short i) throws Exception {
        if ((i == BIG_ENDIAN) || (i == LITTLE_ENDIAN))
            _endian = i;
        else
            throw (new Exception(
                       "Must be BinaryFile.LITTLE_ENDIAN or BinaryFile.BIG_ENDIAN"));
    }

    /**
     * Returns the endian mode.  Will be either BIG_ENDIAN or LITTLE_ENDIAN.
     *
     * @return BIG_ENDIAN or LITTLE_ENDIAN to specify the current endian mode.
     */
    public int getEndian() {
        return _endian;
    }

    /**
     * Sets the signed or unsigned mode for integers.  true for signed, false for unsigned.
     *
     * @param b True if numbers are to be read/written as signed, false if unsigned.
     */
    public void setSigned(boolean b) {
        _signed = b;
    }

    /**
     * Returns the signed mode.
     *
     * @return Returns true for signed, false for unsigned.
     */
    public boolean getSigned() {
        return _signed;
    }

    /**
     * Reads a fixed length ASCII string.
     *
     * @param length How long of a string to read.
     * @return The number of bytes read.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public String readFixedString(int length) throws java.io.IOException {
        String rtn = "";

        for (int i = 0; i < length; i++)
            rtn += (char) _file.readByte();
        return rtn;
    }

    /**
     * Writes a fixed length ASCII string.  Will truncate the string if it does not fit in the specified buffer.
     *
     * @param str The string to be written.
     * @param length The length of the area to write to.  Should be larger than the length of the string being written.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public void writeFixedString(String str, int length)
    throws java.io.IOException {
        int i;

        // trim the string back some if needed

        if (str.length() > length)
            str = str.substring(0, length);

        // write the string

        for (i = 0; i < str.length(); i++)
            _file.write(str.charAt(i));

        // buffer extra space if needed

        i = length - str.length();
        while ((i--) > 0)
            _file.write(0);
    }

    /**
     * Reads a string that stores one length byte before the string.  This string can be up to 255 characters long.  Pascal stores strings this way.
     *
     * @return The string that was read.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public String readLengthPrefixString() throws java.io.IOException {
        short len = readUnsignedByte();
        return readFixedString(len);
    }

    /**
     * Writes a string that is prefixed by a single byte that specifies the length of the string.  This is how Pascal usually stores strings.
     *
     * @param str The string to be written.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public void writeLengthPrefixString(String str) throws java.io.IOException {
        writeByte((byte) str.length());
        for (int i = 0; i < str.length(); i++)
            _file.write(str.charAt(i));
    }

    /**
     * Reads a fixed length string that is zero(NULL) terminated.  This is a type of string used by C/C++.  For example char str[80].
     *
     * @param length The length of the string.
     * @return The string that was read.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public String readFixedZeroString(int length) throws java.io.IOException {
        String rtn = readFixedString(length);
        int i = rtn.indexOf(0);
        if (i != -1)
            rtn = rtn.substring(0, i);
        return rtn;
    }

    /**
     * Writes a fixed length string that is zero terminated.  This is the format generally used by C/C++ for string storage.
     *
     * @param str The string to be written.
     * @param length The length of the buffer to receive the string.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public void writeFixedZeroString(String str, int length)
    throws java.io.IOException {
        writeFixedString(str, length);
    }

    /**
     * Reads an unlimited length zero(null) terminated string.
     *
     * @return The string that was read.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public String readZeroString() throws java.io.IOException {
        String rtn = "";
        char ch;

        do {
            ch = (char) _file.read();
            if (ch != 0)
                rtn += ch;
        } while (ch != 0);
        return rtn;
    }

    /**
     * Writes an unlimited zero(NULL) terminated string to the file.
     *
     * @param str The string to be written.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public void writeZeroString(String str) throws java.io.IOException {
        for (int i = 0; i < str.length(); i++)
            _file.write(str.charAt(i));
        writeByte((byte) 0);
    }

    /**
     * Internal function used to read an unsigned byte.  External classes should use the readByte function.
     *
     * @return The byte, unsigned, as a short.
     * @exception java.io.IOException If an IO exception occurs.
     */
    protected short readUnsignedByte() throws java.io.IOException {
        return (short) (_file.readByte() & 0xff);
    }

    /**
     * Reads an 8-bit byte.  Can be signed or unsigned depending on the signed property.
     *
     * @return A byte stored in a short.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public short readByte() throws java.io.IOException {
        if (_signed)
            return (short) _file.readByte();
        else
            return (short) _file.readUnsignedByte();
    }

    /**
     * Writes a single byte to the file.
     *
     * @param b The byte to be written.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public void writeByte(short b) throws java.io.IOException {
        _file.write(b & 0xff);
    }

    /**
     * Reads a 16-bit word.  Can be signed or unsigned depending on the signed property.  Can be little or big endian depending on the endian property.
     *
     * @return A word stored in an int.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public int readWord() throws java.io.IOException {
        short a, b;
        int result;

        a = readUnsignedByte();
        b = readUnsignedByte();

        if (_endian == BIG_ENDIAN)
            result = ((a << 8) | b);
        else
            result = (a | (b << 8));

        if (_signed)
            if ((result & 0x8000) == 0x8000)
                result = -(0x10000 - result);

        return result;
    }

    /**
     * Write a word to the file.
     *
     * @param w The word to be written to the file.
     * @exception java.io.IOException If an IO exception occurs.
     */

    public void writeWord(int w) throws java.io.IOException {
        if (_endian == BIG_ENDIAN) {
            _file.write((w & 0xff00) >> 8);
            _file.write(w & 0xff);
        } else {
            _file.write(w & 0xff);
            _file.write((w & 0xff00) >> 8);
        }
    }

    /**
     * Reads a 32-bit double word.  Can be signed or unsigned depending on the signed property.  Can be little or big endian depending on the endian property.
     *
     * @return A double world stored in a long.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public long readDWord() throws java.io.IOException {
        short a, b, c, d;
        long result;

        a = readUnsignedByte();
        b = readUnsignedByte();
        c = readUnsignedByte();
        d = readUnsignedByte();

        if (_endian == BIG_ENDIAN)
            result = ((a << 24) | (b << 16) | (c << 8) | d);
        else
            result = (a | (b << 8) | (c << 16) | (d << 24));

        if (_signed)
            if ((result & 0x80000000L) == 0x80000000L)
                result = -(0x100000000L - result);

        return result;
    }

    /**
     * Writes a double word to the file.
     *
     * @param d The double word to be written to the file.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public void writeDWord(long d) throws java.io.IOException {
        if (_endian == BIG_ENDIAN) {
            _file.write((int) (d & 0xff000000) >> 24);
            _file.write((int) (d & 0xff0000) >> 16);
            _file.write((int) (d & 0xff00) >> 8);
            _file.write((int) (d & 0xff));
        } else {
            _file.write((int) (d & 0xff));
            _file.write((int) (d & 0xff00) >> 8);
            _file.write((int) (d & 0xff0000) >> 16);
            _file.write((int) (d & 0xff000000) >> 24);
        }
    }

    /**
     * Allows the file to be aligned to a specified byte boundary.  For example, if a 4(double word) is specified, the file pointer will be moved to the next double word boundary.
     *
     * @param a The byte-boundary to align to.
     * @exception java.io.IOException If an IO exception occurs.
     */
    public void align(int a) throws java.io.IOException {
        if ((_file.getFilePointer() % a) > 0) {
            long pos = _file.getFilePointer() / a;
            _file.seek((pos + 1) * a);
        }
    }
}

