/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.qpid.jms.message;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Enumeration;

import jakarta.jms.BytesMessage;
import jakarta.jms.Destination;
import jakarta.jms.JMSException;
import jakarta.jms.MapMessage;
import jakarta.jms.Message;
import jakarta.jms.MessageEOFException;
import jakarta.jms.ObjectMessage;
import jakarta.jms.Queue;
import jakarta.jms.StreamMessage;
import jakarta.jms.TemporaryQueue;
import jakarta.jms.TemporaryTopic;
import jakarta.jms.TextMessage;
import jakarta.jms.Topic;

import org.apache.qpid.jms.JmsConnection;
import org.apache.qpid.jms.JmsDestination;
import org.apache.qpid.jms.JmsQueue;
import org.apache.qpid.jms.JmsTemporaryQueue;
import org.apache.qpid.jms.JmsTemporaryTopic;
import org.apache.qpid.jms.JmsTopic;

/**
 * A helper class for converting normal JMS interfaces into the QpidJMS specific
 * versions.
 */
public final class JmsMessageTransformation {

    private static JmsUnresolvedDestinationTransformer unresolvedDestinationHandler =
        new JmsDefaultUnresolvedDestinationTransformer();

    /**
     * Creates a an available JMS message from another provider.
     *
     * @param connection
     *      The Connection instance that is requesting the transformation.
     * @param destination
     *      Destination to be converted into Jms's implementation.
     *
     * @return JmsDestination - Jms's implementation of the destination.
     *
     * @throws JMSException if an error occurs during the transformation.
     */
    public static JmsDestination transformDestination(JmsConnection connection, Destination destination) throws JMSException {
        JmsDestination result = null;

        if (destination != null) {

            if (destination instanceof JmsDestination) {
                result = (JmsDestination) destination;
            } else if (destination instanceof Queue && destination instanceof Topic) {
                String queueName = ((Queue) destination).getQueueName();
                String topicName = ((Topic) destination).getTopicName();
                if (queueName != null && topicName == null) {
                    result = new JmsQueue(queueName);
                } else if (queueName == null && topicName != null) {
                    result = new JmsTopic(topicName);
                } else {
                    result = unresolvedDestinationHandler.transform(destination);
                }
            } else {
                if (destination instanceof TemporaryQueue) {
                    result = new JmsTemporaryQueue(((TemporaryQueue) destination).getQueueName());
                } else if (destination instanceof TemporaryTopic) {
                    result = new JmsTemporaryTopic(((TemporaryTopic) destination).getTopicName());
                } else if (destination instanceof Queue) {
                    result = new JmsQueue(((Queue) destination).getQueueName());
                } else if (destination instanceof Topic) {
                    result = new JmsTopic(((Topic) destination).getTopicName());
                } else {
                    result = unresolvedDestinationHandler.transform(destination);
                }
            }
        }

        return result;
    }

    /**
     * Creates a new JmsMessage object and populates it using the details of
     * the given Message.
     *
     * @param connection
     *        The JmsConnection where this transformation is being initiated.
     * @param message
     *        Message to be converted into the clients implementation.
     *
     * @return JmsMessage
     *         The client's implementation object for the incoming message.
     *
     * @throws JMSException if an error occurs during the transform.
     */
    public static JmsMessage transformMessage(JmsConnection connection, Message message) throws JMSException {
        JmsMessage jmsMessage = null;
        JmsMessageFactory factory = connection.getMessageFactory();

        if (message instanceof BytesMessage) {
            BytesMessage bytesMsg = (BytesMessage) message;
            bytesMsg.reset();
            JmsBytesMessage msg = factory.createBytesMessage();
            try {
                for (;;) {
                    // Reads a byte from the message stream until the stream is empty
                    msg.writeByte(bytesMsg.readByte());
                }
            } catch (MessageEOFException e) {
                // Indicates all the bytes have been read from the source.
            }

            jmsMessage = msg;
        } else if (message instanceof MapMessage) {
            MapMessage mapMsg = (MapMessage) message;
            JmsMapMessage msg = factory.createMapMessage();
            Enumeration<?> iter = mapMsg.getMapNames();

            while (iter.hasMoreElements()) {
                String name = iter.nextElement().toString();
                msg.setObject(name, mapMsg.getObject(name));
            }

            jmsMessage = msg;
        } else if (message instanceof ObjectMessage) {
            ObjectMessage objMsg = (ObjectMessage) message;
            JmsObjectMessage msg = factory.createObjectMessage();
            msg.setObject(objMsg.getObject());
            jmsMessage = msg;
        } else if (message instanceof StreamMessage) {
            StreamMessage streamMessage = (StreamMessage) message;
            streamMessage.reset();
            JmsStreamMessage msg = factory.createStreamMessage();
            Object obj = null;

            try {
                while ((obj = streamMessage.readObject()) != null) {
                    msg.writeObject(obj);
                }
            } catch (MessageEOFException e) {
                // Indicates all the stream values have been read from the source.
            }

            jmsMessage = msg;
        } else if (message instanceof TextMessage) {
            TextMessage textMsg = (TextMessage) message;
            JmsTextMessage msg = factory.createTextMessage();
            msg.setText(textMsg.getText());
            jmsMessage = msg;
        } else {
            jmsMessage = factory.createMessage();
        }

        copyProperties(connection, message, jmsMessage);

        return jmsMessage;
    }

    /**
     * Copies the standard JMS and user defined properties from the given source
     * message to the specified target message.  The copy can only handle the JMS
     * specific message properties and known JMS Headers, any headers that are
     * specific to the foreign message may be lost if not returned directly via
     * the <code>propertyNames</code> method.
     *
     * @param connection
     *        The Connection instance that is requesting the transformation.
     * @param source
     *        the message to take the properties from
     * @param target
     *        the message to add the properties to
     *
     * @throws JMSException if an error occurs during the copy of message properties.
     */
    public static void copyProperties(JmsConnection connection, Message source, JmsMessage target) throws JMSException {
        target.setJMSMessageID(source.getJMSMessageID());
        target.setJMSCorrelationID(source.getJMSCorrelationID());
        target.setJMSReplyTo(transformDestination(connection, source.getJMSReplyTo()));
        target.setJMSDestination(transformDestination(connection, source.getJMSDestination()));
        target.setJMSDeliveryMode(source.getJMSDeliveryMode());
        target.setJMSDeliveryTime(getForeignMessageDeliveryTime(source));
        target.setJMSRedelivered(source.getJMSRedelivered());
        target.setJMSType(source.getJMSType());
        target.setJMSExpiration(source.getJMSExpiration());
        target.setJMSPriority(source.getJMSPriority());
        target.setJMSTimestamp(source.getJMSTimestamp());

        Enumeration<?> propertyNames = source.getPropertyNames();

        while (propertyNames.hasMoreElements()) {
            String name = propertyNames.nextElement().toString();
            Object obj = source.getObjectProperty(name);
            target.setObjectProperty(name, obj);
        }
    }

    public static void setUnresolvedDestinationHandler(JmsUnresolvedDestinationTransformer handler) {
        unresolvedDestinationHandler = handler;
    }

    public static JmsUnresolvedDestinationTransformer getUnresolvedDestinationTransformer() {
        if (unresolvedDestinationHandler == null) {
            unresolvedDestinationHandler = new JmsDefaultUnresolvedDestinationTransformer();
        }

        return unresolvedDestinationHandler;
    }

    private static long getForeignMessageDeliveryTime(Message foreignMessage) throws JMSException {
        // Verify if the getJMSDeliveryTime method exists, i.e the foreign provider isn't only JMS 1.1.
        Method deliveryTimeMethod = null;
        try {
            Class<?> clazz = foreignMessage.getClass();
            Method method = clazz.getMethod("getJMSDeliveryTime", (Class[]) null);
            if (!Modifier.isAbstract(method.getModifiers())) {
                deliveryTimeMethod = method;
            }
        } catch (NoSuchMethodException e) {
            // Assume its a JMS 1.1 Message, we will return 0.
        }

        if (deliveryTimeMethod != null) {
            // Method exists, isn't abstract, so use it.
            return foreignMessage.getJMSDeliveryTime();
        }

        return 0;
    }
}
