package org.mockejb.jms;

import java.util.*;
import javax.jms.*;

/**
 * <code>StreamMessage</code> implementation.
 * @author Dimitar Gospodinov
 */
public class StreamMessageImpl extends MessageImpl implements StreamMessage {

    private final List streamData = new ArrayList();

    // Shows what should be read next from the stream
    private int position;
    // Shows what should be read next from current byte[]
    private int byteArrayPosition;

    /**
     * Creates empty <code>StreamMessageImpl</code>. 
     */
    public StreamMessageImpl()  {
        super();
    }

    /**
     * Creates new <code>StreamMessageImpl</code> initialized with
     * header, properties and body from <code>msg</code>.
     * The state of <code>msg</code> is not changed. 
     * @param msg message to copy from
     * @throws JMSException
     */
    public StreamMessageImpl(StreamMessage msg) throws JMSException {
        super(msg);
        setBody(msg);
    }

    /**
     * Sets the body of this message to the body of <code>msg</code>.
     * The state of <code>msg</code> is not changed. 
     * @param msg
     * @throws JMSException
     */
    private void setBody(StreamMessage msg) throws JMSException {
        // Number of elements remaining until the end of the stream
        int elementsRemaining = 0;

        while (true) {
            try {
                msg.readObject();
                elementsRemaining++;
            } catch (MessageEOFException ex) {
                /*
                 * Reached EOF.
                 * Now <code>bytesRemaining</code> contains number of bytes
                 * remaining in the byte stream of <code>msg</code>
                 * Next - reset <code>msg</code> and "extract" all bytes into
                 * <code>sourceBytes</code>
                 */
                extractElements(msg);
                // Reset the message and move to the original position in the stream 
                msg.reset();
                int moveTo = streamData.size() - elementsRemaining;
                while (moveTo-- > 0) {
                    msg.readObject();
                }
                break;
            } catch (MessageNotReadableException ex) {
                // Message is in Write-Only mode
                extractElements(msg);
                /*
                 *  At this point <code>msg</code> is in Read-Only mode
                 *  Switch to Write-Only mode and restore the original stream.
                 */
                msg.clearBody();
                for (int i = 0; i < streamData.size(); i++) {
                    msg.writeObject(streamData.get(i));
                }
                break;
            }
        }
    }

    /**
     * Calls <code>msg.reset</code> and reads all elements from its body.
     * The elements are added to the body of this message.
     * @param msg
     * @throws JMSException
     */
    private void extractElements(StreamMessage msg) throws JMSException {
        msg.reset();
        while (true) {
            try {
                writeObject(msg.readObject());
            } catch (MessageEOFException ex) {
                break;
            }
        }
    }


    /**
     * @see javax.jms.StreamMessage#clearBody()
     */
    public void clearBody() throws JMSException {
        super.clearBody();
        streamData.clear();
    }

    /**
     * @see javax.jms.StreamMessage#readBoolean()
     */
    public boolean readBoolean() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object value = streamData.get(position);
        boolean result;

        if (value instanceof Boolean) {
            result = ((Boolean) value).booleanValue();
        } else if (value == null || value instanceof String) {
            result = Boolean.valueOf((String) value).booleanValue();
        } else {
            throw new MessageFormatException("Current position does not contain valid Boolean value!");
        }
        position++;
        return result;
    }

    /**
     * @see javax.jms.StreamMessage#readByte()
     */
    public byte readByte() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object value = streamData.get(position);
        byte result;

        if (value instanceof Byte) {
            result = ((Byte) value).byteValue();
        } else if (value == null || value instanceof String) {
            result = Byte.valueOf((String) value).byteValue();
        } else {
            throw new MessageFormatException("Current position does not contain valid Byte value!");
        }
        position++;
        return result;
    }

    /**
     * @see javax.jms.StreamMessage#readShort()
     */
    public short readShort() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object value = streamData.get(position);
        short result;

        if (value instanceof Byte || value instanceof Short) {
            result = ((Number) value).shortValue();
        } else if (value == null || value instanceof String) {
            result = Short.valueOf((String) value).shortValue();
        } else {
            throw new MessageFormatException("Current position does not contain valid Short value!");
        }
        position++;
        return result;
    }

    /**
     * @see javax.jms.StreamMessage#readChar()
     */
    public char readChar() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object value = streamData.get(position);
        char result;

        if (value == null) {
            throw new NullPointerException();
        } else if (value instanceof Character) {
            result = ((Character) value).charValue();
        } else {
            throw new MessageFormatException("Current position does not contain valid Char value!");
        }
        position++;
        return result;
    }

    /**
     * @see javax.jms.StreamMessage#readInt()
     */
    public int readInt() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object value = streamData.get(position);
        int result;

        if (value instanceof Byte
            || value instanceof Short
            || value instanceof Integer) {
            result = ((Number) value).intValue();
        } else if (value == null || value instanceof String) {
            result = Integer.valueOf((String) value).intValue();
        } else {
            throw new MessageFormatException("Current position does not contain valid Integer value!");
        }
        position++;
        return result;
    }

    /**
     * @see javax.jms.StreamMessage#readLong()
     */
    public long readLong() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object value = streamData.get(position);
        long result;

        if (value instanceof Byte
            || value instanceof Short
            || value instanceof Integer
            || value instanceof Long) {

            result = ((Number) value).longValue();
        } else if (value == null || value instanceof String) {
            result = Long.valueOf((String) value).longValue();
        } else {
            throw new MessageFormatException("Current position does not contain valid Long value!");
        }
        position++;
        return result;
    }

    /**
     * @see javax.jms.StreamMessage#readFloat()
     */
    public float readFloat() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object value = streamData.get(position);
        float result;

        if (value instanceof Float) {
            result = ((Float) value).floatValue();
        } else if (value == null || value instanceof String) {
            result = Float.valueOf((String) value).floatValue();
        } else {
            throw new MessageFormatException("Current position does not contain valid Float value!");
        }
        position++;
        return result;
    }

    /**
     * @see javax.jms.StreamMessage#readDouble()
     */
    public double readDouble() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object value = streamData.get(position);
        double result;

        if (value instanceof Float || value instanceof Double) {
            result = ((Number) value).doubleValue();
        } else if (value == null || value instanceof String) {
            result = Double.valueOf((String) value).doubleValue();
        } else {
            throw new MessageFormatException("Current position does not contain valid Double value!");
        }
        position++;
        return result;
    }

    /**
     * @see javax.jms.StreamMessage#readString()
     */
    public String readString() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object value = streamData.get(position);
        if (value instanceof byte[]) {
            throw new MessageFormatException("Current position does not contain valid UTF value!");
        }
        position++;
        if (value == null) {
            return null;
        }
        return value.toString();
    }

    /**
     * @see javax.jms.StreamMessage#readBytes(byte[])
     */
    public int readBytes(byte[] bytes) throws JMSException {
        checkBodyReadable();
        checkEOF();

        if (bytes == null) {
            throw new NullPointerException();
        }

        Object value = streamData.get(position);
        if (byteArrayPosition == -1 || value == null) {
            position++;
            byteArrayPosition = 0;
            return -1;
        }
        if (!(value instanceof byte[])) {
            throw new MessageFormatException("Current position does not contain valid byte[] value!");
        }

        byte[] byteArray = (byte[]) value;
        if (byteArray.length == 0) {
            byteArrayPosition = -1;
            return 0;
        }

        // Determine how many bytes to copy into <code>bytes</code>
        int numOfBytesToCopy;
        int remainingBytesToCopy = byteArray.length - byteArrayPosition;
        int startOffset = byteArrayPosition;

        if (remainingBytesToCopy <= bytes.length) {
            numOfBytesToCopy = remainingBytesToCopy;
            byteArrayPosition = -1;
        } else {
            numOfBytesToCopy = bytes.length;
            byteArrayPosition += numOfBytesToCopy;
        }

        int i = 0;
        int j = numOfBytesToCopy;
        while (j > 0) {
            bytes[i++] = byteArray[startOffset++];
            j--;
        }
        return numOfBytesToCopy;
    }

    /**
     * @see javax.jms.StreamMessage#readObject()
     */
    public Object readObject() throws JMSException {
        checkBodyReadable();
        checkEOF();

        Object result = streamData.get(position++);
        if (result instanceof byte[]) {
            result = ((byte[]) result).clone();
        }
        return result;
    }

    /**
     * @see javax.jms.StreamMessage#writeBoolean(boolean)
     */
    public void writeBoolean(boolean value) throws JMSException {
        checkBodyWriteable();
        streamData.add(new Boolean(value));
    }

    /**
     * @see javax.jms.StreamMessage#writeByte(byte)
     */
    public void writeByte(byte value) throws JMSException {
        checkBodyWriteable();
        streamData.add(new Byte(value));
    }

    /**
     * @see javax.jms.StreamMessage#writeShort(short)
     */
    public void writeShort(short value) throws JMSException {
        checkBodyWriteable();
        streamData.add(new Short(value));
    }

    /**
     * @see javax.jms.StreamMessage#writeChar(char)
     */
    public void writeChar(char value) throws JMSException {
        checkBodyWriteable();
        streamData.add(new Character(value));
    }

    /**
     * @see javax.jms.StreamMessage#writeInt(int)
     */
    public void writeInt(int value) throws JMSException {
        checkBodyWriteable();
        streamData.add(new Integer(value));
    }

    /**
     * @see javax.jms.StreamMessage#writeLong(long)
     */
    public void writeLong(long value) throws JMSException {
        checkBodyWriteable();
        streamData.add(new Long(value));
    }

    /**
     * @see javax.jms.StreamMessage#writeFloat(float)
     */
    public void writeFloat(float value) throws JMSException {
        checkBodyWriteable();
        streamData.add(new Float(value));
    }

    /**
     * @see javax.jms.StreamMessage#writeDouble(double)
     */
    public void writeDouble(double value) throws JMSException {
        checkBodyWriteable();
        streamData.add(new Double(value));
    }

    /**
     * @see javax.jms.StreamMessage#writeString(java.lang.String)
     */
    public void writeString(String value) throws JMSException {
        checkBodyWriteable();
        streamData.add(value);
    }

    /**
     * @see javax.jms.StreamMessage#writeBytes(byte[])
     */
    public void writeBytes(byte[] bytes) throws JMSException {
        checkBodyWriteable();
        if (bytes == null) {
            streamData.add(null);
            return;
        }
        writeBytes(bytes, 0, bytes.length);
    }

    /**
     * @see javax.jms.StreamMessage#writeBytes(byte[], int, int)
     */
    public void writeBytes(byte[] bytes, int offset, int length)
        throws JMSException {

        checkBodyWriteable();
        if (bytes == null) {
            streamData.add(null);
            return;
        }
        if (offset < 0 || length < 0 || (offset + length) > bytes.length) {
            throw new IllegalArgumentException();
        }

        byte[] bytesToAdd = new byte[length];
        for (int i = 0; i < length; bytesToAdd[i++] = bytes[offset++]);
        streamData.add(bytesToAdd);
    }

    /**
     * @see javax.jms.StreamMessage#writeObject(java.lang.Object)
     */
    public void writeObject(Object value) throws JMSException {
        checkBodyWriteable();

        if (!(value == null
            || value instanceof Boolean
            || value instanceof Byte
            || value instanceof Short
            || value instanceof Integer
            || value instanceof Long
            || value instanceof Float
            || value instanceof Double
            || value instanceof String
            || value instanceof Character
            || value instanceof byte[])) {
            throw new MessageFormatException("Incorrect object type!");
        }
        if (value instanceof byte[]) {
            value = ((byte[]) value).clone();
        }
        streamData.add(value);
    }

    /**
     * @see javax.jms.StreamMessage#reset()
     */
    public void reset() throws JMSException {
        setBodyReadOnly();
        position = 0;
        byteArrayPosition = 0;
    }

    // Non standard methods

    /**
     * Checks if EOF has been reached and throws
     * <code>MessageEOFException</code> if yes.
     * @throws MessageEOFException if EOF has been reached.
     */
    private void checkEOF() throws MessageEOFException {
        if (position >= streamData.size()) {
            throw new MessageEOFException("EOF reached!");
        }
    }
    
    /**
     * Returns array containing all elements from the stream, in
     * the order they were added.
     * @return
     */
    Object[] getStreamData() {
        return streamData.toArray();
    }

    // Non-standard methods

    /**
      * Sets message body in read-only mode.
      * @throws JMSException
      */
    void resetBody() throws JMSException {
        reset();
    }

}