/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */
package de.rcenvironment.core.communication.transport.jms.common;

import java.net.ProtocolException;
import java.util.Map;

import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageEOFException;
import javax.jms.MessageProducer;
import javax.jms.ObjectMessage;
import javax.jms.Session;
import javax.jms.TextMessage;

import de.rcenvironment.core.communication.common.CommunicationException;
import de.rcenvironment.core.communication.common.SerializationException;
import de.rcenvironment.core.communication.model.InitialNodeInformation;
import de.rcenvironment.core.communication.model.NetworkRequest;
import de.rcenvironment.core.communication.model.NetworkResponse;
import de.rcenvironment.core.communication.model.impl.InitialNodeInformationImpl;
import de.rcenvironment.core.communication.protocol.NetworkRequestFactory;
import de.rcenvironment.core.communication.protocol.NetworkResponseFactory;
import de.rcenvironment.core.communication.transport.spi.HandshakeInformation;
import de.rcenvironment.core.communication.utils.MessageUtils;

/**
 * Utility class providing the mapping between RCE entities and JMS messages, plus related message-related settings.
 * 
 * @author Robert Mischke
 */
public final class JmsProtocolUtils {

    private JmsProtocolUtils() {
        // prevent instantiation
    }

    /**
     * Creates a new JMS {@link Message} with the provided {@link InitialNodeInformation} as the handshake content, and a message type field
     * of {@link JmsProtocolConstants#MESSAGE_TYPE_INITIAL}.
     * 
     * @param handshakeInformation the node information to use as body
     * @param session the JMS session to use
     * @return the created JMS {@link Message}
     * @throws JMSException on JMS errors
     */
    public static Message createHandshakeMessage(JMSHandshakeInformation handshakeInformation, Session session) throws JMSException {
        ObjectMessage initialMessage = session.createObjectMessage();
        initialMessage.setStringProperty(JmsProtocolConstants.MESSAGE_FIELD_MESSAGE_TYPE, JmsProtocolConstants.MESSAGE_TYPE_INITIAL);
        initialMessage.setStringProperty(JmsProtocolConstants.MESSAGE_FIELD_PROTOCOL_VERSION,
            handshakeInformation.getProtocolVersionString());
        initialMessage.setStringProperty(JmsProtocolConstants.MESSAGE_FIELD_CHANNEL_ID, handshakeInformation.getChannelId());
        initialMessage.setStringProperty(JmsProtocolConstants.MESSAGE_FIELD_REMOTE_INITIATED_REQUEST_INBOX,
            handshakeInformation.getTemporaryQueueInformation());

        InitialNodeInformation initialNodeInformation = handshakeInformation.getInitialNodeInformation();
        if (initialNodeInformation != null) {
            // TODO replace by JSON data?
            byte[] handshakeBytes = MessageUtils.serializeSafeObject(initialNodeInformation);
            initialMessage.setObject(handshakeBytes);
        }
        return initialMessage;
    }

    /**
     * Extracts the {@link HandshakeInformation} from a received initial JMS handshake response.
     * 
     * @param message the received message
     * @param expectedProtocolVersion the protocol version string that the response must match to continue parsing; made a parameter to
     *        allow unit testing
     * @return the extracted {@link HandshakeInformation}
     * @throws JMSException on JMS errors
     * @throws ProtocolException on a protocol version mismatch, or deserialization errors
     */
    public static JMSHandshakeInformation parseHandshakeMessage(Message message, String expectedProtocolVersion) throws JMSException,
        ProtocolException {
        JMSHandshakeInformation result = new JMSHandshakeInformation();

        result.setProtocolVersionString(message.getStringProperty(JmsProtocolConstants.MESSAGE_FIELD_PROTOCOL_VERSION));
        result.setTemporaryQueueInformation(message
            .getStringProperty(JmsProtocolConstants.MESSAGE_FIELD_REMOTE_INITIATED_REQUEST_INBOX));

        if (!result.matchesVersion(expectedProtocolVersion)) {
            // on an incompatible version, do not continue parsing; return result containing only
            // the version information
            return result;
        }

        result.setChannelId(message.getStringProperty(JmsProtocolConstants.MESSAGE_FIELD_CHANNEL_ID));

        byte[] handshakeRequestBytes = (byte[]) ((ObjectMessage) message).getObject();
        if (handshakeRequestBytes == null || handshakeRequestBytes.length == 0) {
            throw new ProtocolException("Received handshake request without payload");
        }
        try {
            result.setInitialNodeInformation(MessageUtils.deserializeObject(handshakeRequestBytes, InitialNodeInformationImpl.class));
        } catch (SerializationException e) {
            throw new ProtocolException("Failed to deserialize initial node information from handshake message: " + e.toString());
        }
        return result;
    }

    /**
     * Creates a JMS message from a given {@link NetworkRequest}.
     * 
     * @param request the request to transform
     * @param session the JMS session to use
     * @return the equivalent JMS message
     * @throws JMSException on JMS errors
     */
    public static Message createMessageFromNetworkRequest(final NetworkRequest request, Session session) throws JMSException {
        Map<String, String> metadata = request.accessRawMetaData();
        ObjectMessage jmsRequest = session.createObjectMessage();
        jmsRequest.setObject(request.getContentBytes());
        jmsRequest.setObjectProperty(JmsProtocolConstants.MESSAGE_FIELD_METADATA, metadata);
        jmsRequest.setStringProperty(JmsProtocolConstants.MESSAGE_FIELD_MESSAGE_TYPE, JmsProtocolConstants.MESSAGE_TYPE_REQUEST);
        return jmsRequest;
    }

    /**
     * Restores a {@link NetworkRequest} from its JMS message form.
     * 
     * @param jmsRequest the JMS message
     * @return the reconstructed {@link NetworkRequest}
     * @throws JMSException on JMS errors
     * @throws CommunicationException on message format errors
     */
    public static NetworkRequest createNetworkRequestFromMessage(Message jmsRequest) throws JMSException, CommunicationException {
        byte[] content = (byte[]) ((ObjectMessage) jmsRequest).getObject();
        if (content.length == 0) {
            throw new CommunicationException("Received message with zero-length payload");
        }
        @SuppressWarnings("unchecked") Map<String, String> requestMetadata = (Map<String, String>) jmsRequest
            .getObjectProperty(JmsProtocolConstants.MESSAGE_FIELD_METADATA);
        NetworkRequest originalRequest = NetworkRequestFactory.reconstructNetworkRequest(content, requestMetadata);
        return originalRequest;
    }

    /**
     * Creates a JMS message from a given {@link NetworkResponse}.
     * 
     * @param response the response to transform
     * @param session the JMS session to use
     * @return the equivalent JMS message
     * @throws JMSException on JMS errors
     */
    public static Message createMessageFromNetworkResponse(NetworkResponse response, Session session) throws JMSException {
        ObjectMessage jmsResponse = session.createObjectMessage();
        jmsResponse.setObject(response.getContentBytes());
        jmsResponse.setIntProperty(JmsProtocolConstants.MESSAGE_FIELD_RESULT_CODE, response.getResultCode().getCode());
        // TODO add metadata?
        return jmsResponse;
    }

    /**
     * Restores a {@link NetworkResponse} from its JMS message form.
     * 
     * @param jmsResponse the JMS message
     * @param request the {@link NetworkRequest} this response is associated with
     * @return the reconstructed {@link NetworkResponse}
     * @throws JMSException on JMS errors
     */
    public static NetworkResponse createNetworkResponseFromMessage(Message jmsResponse, final NetworkRequest request) throws JMSException {
        byte[] content = (byte[]) ((ObjectMessage) jmsResponse).getObject();
        if (jmsResponse.propertyExists(JmsProtocolConstants.MESSAGE_FIELD_RESULT_CODE)) {
            int resultCode = jmsResponse.getIntProperty(JmsProtocolConstants.MESSAGE_FIELD_RESULT_CODE);
            return NetworkResponseFactory.generateResponseWithResultCode(request, content, resultCode);
        } else {
            // backwards compatibility for remote 6.0.x - 6.2.x RCE nodes; can be dropped in 7.0.0
            return NetworkResponseFactory.generateSuccessResponse(request, content);
        }
    }

    /**
     * Creates a JMS message to send to a JMS queue to terminate one {@link AbstractJmsQueueConsumer} listening on this queue ("poison pill"
     * pattern).
     * 
     * @param session the JMS session to use
     * @param channelId the channel id to send with the message
     * @param securityToken the shared-secret security token to prevent unauthorized shutdown
     * @return the shutdown message
     * @throws JMSException on JMS errors
     */
    public static Message createChannelShutdownMessage(Session session, String channelId, String securityToken) throws JMSException {
        TextMessage poisonPill = session.createTextMessage();
        poisonPill.setStringProperty(JmsProtocolConstants.MESSAGE_FIELD_MESSAGE_TYPE,
            JmsProtocolConstants.MESSAGE_TYPE_CHANNEL_CLOSING);
        poisonPill.setStringProperty(JmsProtocolConstants.MESSAGE_FIELD_CHANNEL_ID,
            channelId);
        poisonPill.setText(securityToken);
        return poisonPill;
    }

    /**
     * Creates a JMS message to send to a JMS queue to terminate one {@link AbstractJmsQueueConsumer} listening on this queue ("poison pill"
     * pattern).
     * 
     * @param session the JMS session to use
     * @param securityToken the shared-secret security token to prevent unauthorized queue shutdown
     * @return the shutdown message
     * @throws JMSException on JMS errors
     */
    public static Message createQueueShutdownMessage(Session session, String securityToken) throws JMSException {
        TextMessage poisonPill = session.createTextMessage();
        poisonPill.setStringProperty(JmsProtocolConstants.MESSAGE_FIELD_MESSAGE_TYPE, JmsProtocolConstants.MESSAGE_TYPE_QUEUE_SHUTDOWN);
        poisonPill.setText(securityToken);
        return poisonPill;
    }

    /**
     * Applies common settings (like message timeouts etc.) to a JMS {@link MessageProducer}. Should be invoked on any
     * {@link MessageProducer} before it is used for sending messages.
     * 
     * @param producer the producer to configure
     * @throws JMSException on JMS errors
     */
    public static void configureMessageProducer(MessageProducer producer) throws JMSException {
        // set the maximum time that messages from this producer are preserved
        producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
    }

    /**
     * Wraps the common JMS pattern where a temporary {@link MessageProducer} is created from a session, used to send a single
     * {@link MessageEOFException}, and then disposed.
     * 
     * @param session the session to create the producer from
     * @param message the message to send
     * @param destination the JMS destination
     * @throws JMSException on JMS errors
     */
    public static void sendWithTransientProducer(Session session, Message message, Destination destination) throws JMSException {
        MessageProducer producer = session.createProducer(destination);
        try {
            JmsProtocolUtils.configureMessageProducer(producer);
            producer.send(message);
        } finally {
            producer.close();
        }
    }
}
