package org.mockejb.jms;

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

/**
 * Collection of utility methods.
 * 
 * @author D11
 * Dimitar Gospodinov
 */
public final class MessageUtility {

    /**
     * Objects can not be created.
     */
    private MessageUtility() {
        // Does nothing
    }

    /**
     * Compares two strings. Allows <code>null</code> values.
     * @param s1
     * @param s2
     * @return <code>true</code> if <code>s1</code> and
     * <code>s2</code> are <code>null</code> or
     * <code>s1.equals(s2)</code> evaluates to <code>true</code>.
     * In all other cases returns <code>false</code>.
     */
    public static boolean compare(String s1, String s2) {
        if (s1 == null && s2 == null) {
            return true;
        }
        if (s1 != null && s2 != null) {
            return s1.equals(s2);
        }
        return false;
    }

    /**
     * Compares two byte arrays. Disallow <code>null</code> values.
     * @param bytes1
     * @param bytes2
     * @return <code>true</code> if <code>bytes1</code> and
     * <code>bytes2</code> have the same lengths and content.
     * In all other cases returns <code>false</code>.
     */
    public static boolean compare(byte[] bytes1, byte[] bytes2) {
        if (bytes1.length != bytes2.length) {
            return false;
        }
        for (int i = 0; i < bytes1.length; i++) {
            if (bytes1[i] != bytes2[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Compares two messages. Messages are considered equal if
     * they have the same header values and properties.
     * @param msg1
     * @param msg2
     * @return <code>true</code> if all header fields in <code>msg1</code>
     * have the same values as the corresponded header fields in
     * <code>msg2</code>, and all properties from <code>msg1</code>
     * exists and have the same values in <code>msg2</code>, and
     * <code>msg2</code> does not have properties that do not exist in
     * <code>msg1</code>.
     * @throws JMSException
     */
    public static boolean compare(Message msg1, Message msg2)
        throws JMSException {

        // Compare header fields

        if (compare(msg1.getJMSCorrelationID(), msg2.getJMSCorrelationID())
            && msg1.getJMSDeliveryMode() == msg2.getJMSDeliveryMode()
            && msg1.getJMSDestination() == msg2.getJMSDestination()
            && msg1.getJMSExpiration() == msg2.getJMSExpiration()
            && compare(msg1.getJMSMessageID(), msg2.getJMSMessageID())
            && msg1.getJMSPriority() == msg2.getJMSPriority()
            && msg1.getJMSReplyTo() == msg2.getJMSReplyTo()
            && msg1.getJMSTimestamp() == msg2.getJMSTimestamp()
            && compare(msg1.getJMSType(), msg2.getJMSType())
            && msg1.getJMSRedelivered() == msg2.getJMSRedelivered()) {

            // Compare properties
            Enumeration names = msg1.getPropertyNames();
            int numOfProperties1 = 0;
            while (names.hasMoreElements()) {
                numOfProperties1++;
                String name = (String) names.nextElement();
                if (!msg2.propertyExists(name)) {
                    return false;
                }
                Object value1 = msg1.getObjectProperty(name);
                Object value2 = msg2.getObjectProperty(name);
                if ((value1 == null && value2 == null)
                    || (value1 != null
                        && value2 != null
                        && value1.equals(value2))) {
                    continue;
                }
                return false;
            }
            // Determine number of properties in msg2
            names = msg2.getPropertyNames();
            int numOfProperties2 = 0;
            while (names.hasMoreElements()
                && numOfProperties2 <= numOfProperties1) {

                numOfProperties2++;
                names.nextElement();
            }
            return numOfProperties1 == numOfProperties2;
        }
        return false;
    }

    /**
     * Compares two bytes messages. Messages are considered equal if
     * they have the same value for their bodies and their
     * header fields and properties values are the same.
     * @param msg1
     * @param msg2
     * @return <code>true</code> if the body of <code>msg1</code>
     * has the same value as the body of <code>msg2</code>, and
     * <code>compare((Message)msg1, msg2)</code> evaluates to <code>true</code>.
     * In all other cases returns <code>false</code>.
     * @throws JMSException
     */
    public static boolean compare(BytesMessage msg1, BytesMessage msg2)
        throws JMSException {

        // Create copy of the messages to be compared so we can freely manipulate them
        BytesMessageImpl m1 = new BytesMessageImpl(msg1);
        BytesMessageImpl m2 = new BytesMessageImpl(msg2);

        m1.reset();
        m2.reset();

        // Lengths are less than 2^31, otherwise m1 and m2 would not be created.
        int length = (int) m1.getBodyLength();
        if (length != (int) m2.getBodyLength()) {
            return false;
        }
        while (length-- > 0) {
            if (m1.readByte() != m2.readByte()) {
                return false;
            }
        }
        return compare((Message) m1, m2);
    }

    /**
     * Compares two map messages. Messages are considered equal if
     * they have the same value for their bodies and their
     * header fields and properties values are the same.
     * Bodies of map messages are considered equal if they contain
     * the same set of name-value pairs.
     * @param msg1
     * @param msg2
     * @return <code>true</code> if the body of <code>msg1</code>
     * has the same value as the body of <code>msg2</code>, and
     * <code>compare((Message)msg1, msg2)</code> evaluates to <code>true</code>.
     * In all other cases returns <code>false</code>.
     * @throws JMSException
     */
    public static boolean compare(MapMessage msg1, MapMessage msg2)
        throws JMSException {

        // Compare properties
        Enumeration names = msg1.getMapNames();
        int numOfNames1 = 0;
        while (names.hasMoreElements()) {
            numOfNames1++;
            String name = (String) names.nextElement();
            if (!msg2.itemExists(name)) {
                return false;
            }
            Object value1 = msg1.getObject(name);
            Object value2 = msg2.getObject(name);
            if ((value1 == null && value2 == null)
                || ((value1 != null && value2 != null)
                    && ((value1 instanceof byte[]
                        && value2 instanceof byte[]
                        && compare((byte[]) value1, (byte[]) value2))
                        || value1.equals(value2)))) {
                continue;
            }
            return false;
        }
        // Determine number of properties in msg2
        names = msg2.getMapNames();
        int numOfNames2 = 0;
        while (names.hasMoreElements() && numOfNames2 <= numOfNames1) {
            numOfNames2++;
            names.nextElement();
        }
        return numOfNames1 == numOfNames2 && compare((Message) msg1, msg2);
    }

    /**
     * Compares two object messages. Messages are considered equal if
     * they have the same value for their bodies and their
     * header fields and properties values are the same.
     * Bodies of object messages are considered the same if after
     * deserialization <code>msg1.getObject().equals(msg2.getObject)</code> evaluates to
     * <code>true</code> or both bodies are <code>null</code>.
     * @param msg1
     * @param msg2
     * @return <code>true</code> if the body of <code>msg1</code>
     * has the same value as the body of <code>msg2</code>, and
     * <code>compare((Message)msg1, msg2)</code> evaluates to <code>true</code>.
     * In all other cases returns <code>false</code>.
     * @throws JMSException
     */
    public static boolean compare(ObjectMessage msg1, ObjectMessage msg2)
        throws JMSException {

        Object o1 = msg1.getObject();
        Object o2 = msg2.getObject();

        if (o1 == null && o2 == null) {
            return compare((Message) msg1, msg2);
        }
        if (o1 != null && o2 != null) {
            return o1.equals(o2) && compare((Message) msg1, msg2);
        }
        return false;
    }

    /**
     * Compares two stream messages. Messages are considered equal if
     * they have the same value for their bodies and their
     * header fields and properties values are the same.
     * Bodies of stream messages are considered the same if they have
     * the same number of elements and corresponding elements (elements that
     * have the same position in the stream) are from the
     * same type and have the same values. 
     * @param msg1
     * @param msg2
     * @return <code>true</code> if the body of <code>msg1</code>
     * has the same value as the body of <code>msg2</code>, and
     * <code>compare((Message)msg1, msg2)</code> evaluates to <code>true</code>.
     * In all other cases returns <code>false</code>.
     * @throws JMSException
     */
    public static boolean compare(StreamMessage msg1, StreamMessage msg2)
        throws JMSException {

        StreamMessageImpl m1 = new StreamMessageImpl(msg1);
        StreamMessageImpl m2 = new StreamMessageImpl(msg2);

        Object[] data1 = m1.getStreamData();
        Object[] data2 = m2.getStreamData();

        if (data1.length != data2.length) {
            return false;
        }
        for (int i = 0; i < data1.length; i++) {
            if (data1[i] == null && data2[i] == null) {
                continue;
            }
            if (data1[i] != null
                && data2[i] != null
                && ((data1[i] instanceof byte[]
                    && data2[i] instanceof byte[]
                    && compare((byte[]) data1[i], (byte[]) data2[i]))
                    || data1[i].equals(data2[i]))) {
                continue;
            }
            return false;
        }
        return compare((Message) m1, m2);
    }

    /**
     * Compares two text messages. Messages are considered equal if
     * they have the same value for their bodies and their
     * header fields and properties values are the same.
     * Bodies of text messages are considered the same if
     * <code>msg1.getText().equals(msg2.getText())</code> evaluates to
     * <code>true</code> or both bodies are <code>null</code>.
     * @param msg1
     * @param msg2
     * @return <code>true</code> if the body of <code>msg1</code>
     * has the same value as the body of <code>msg2</code>, and
     * <code>compare((Message)msg1, msg2)</code> evaluates to <code>true</code>.
     * In all other cases returns <code>false</code>.
     * @throws JMSException
     */
    public static boolean compare(TextMessage msg1, TextMessage msg2)
        throws JMSException {

        return compare(msg1.getText(), msg2.getText())
            && compare((Message) msg1, msg2);
    }

    /**
       * Copies header, properties and body of <code>msg</code> to a new
       * message of the same type.
       * @param msg to be copied from
       * @param reset <code>true</code> if the copy should be in read only mode. If <code>msg</code> is already in read-only
       * mode this parameter is redundant.
       * @return copy of <code>msg</code>
       * @throws JMSException
       */
    public static MessageImpl copyMessage(Message msg, boolean reset) throws JMSException {
        MessageImpl result;
        if (msg instanceof BytesMessage) {
            result = new BytesMessageImpl((BytesMessage) msg);
        } else if (msg instanceof MapMessage) {
            result = new MapMessageImpl((MapMessage) msg);
        } else if (msg instanceof ObjectMessage) {
            result = new ObjectMessageImpl(((ObjectMessage) msg).getObject());
        } else if (msg instanceof StreamMessage) {
            result = new StreamMessageImpl((StreamMessage) msg);
        } else if (msg instanceof TextMessage) {
            result = new TextMessageImpl((TextMessage) msg);
        } else {
            result = new MessageImpl(msg);
        }
        
        if (reset) {
            result.setPropertiesNotWriteable();
            result.resetBody();
        }
        return result;
    }
    
}