/*
 * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors.
 *
 * Licensed 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.switchyard.component.jca.endpoint;

import java.io.InputStream;
import java.util.Properties;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.naming.InitialContext;

import org.jboss.logging.Logger;
import org.switchyard.Context;
import org.switchyard.Exchange;
import org.switchyard.ExchangePattern;
import org.switchyard.ExchangeState;
import org.switchyard.common.type.Classes;
import org.switchyard.component.common.SynchronousInOutHandler;
import org.switchyard.component.common.composer.MessageComposer;
import org.switchyard.component.jca.JCALogger;
import org.switchyard.component.jca.JCAMessages;
import org.switchyard.component.jca.composer.JMSBindingData;
import org.switchyard.SwitchYardException;
import org.switchyard.selector.OperationSelector;

/**
 * Concrete message endpoint class for JCA message inflow using JMS MessageListener interface.
 * 
 * @author <a href="mailto:tm.igarashi@gmail.com">Tomohisa Igarashi</a>
 *
 */
public class JMSEndpoint extends AbstractInflowEndpoint implements MessageListener {

    /** prefix for the context property. */
    public static final String CONTEXT_PROPERTY_PREFIX = "org.switchyard.component.jca.endpoint.";
    /** key for JNDI properties file. */
    public static final String KEY_JNDI_PROPERTIES_FILE = "jndiPropertiesFileName";
    /** key for JNDI properties file to look up the JMS destination. */
    public static final String KEY_DESTINATION_JNDI_PROPERTIES_FILE = "destinationJndiPropertiesFileName";
    /** key for ConnectionFactory JNDI name. */
    public static final String KEY_CONNECTION_FACTORY_JNDI_NAME = "connectionFactoryJndiName";
    /** key for replyTo/faultTo destination type. */
    public static final String KEY_DESTINATION_TYPE = "destinationType";
    /** key for replyTo destination name. */
    public static final String KEY_REPLY_TO = "replyTo";
    /** key for faultTo destination name. */
    public static final String KEY_FAULT_TO = "faultTo";
    /** key for output message type. */
    public static final String KEY_MESSAGE_TYPE = "messageType";
    /** key for user name. */
    public static final String KEY_USERNAME = "userName";
    /** key for password. */
    public static final String KEY_PASSWORD = "password";
    /** key for Delivery Mode. */
    public static final String KEY_DELIVERY_MODE = "deliveryMode";
    /** key for priority. */
    public static final String KEY_PRIORITY = "priority";
    /** key for Time-to-Live. */
    public static final String KEY_TIME_TO_LIVE = "timeToLive";

    private Logger _logger = Logger.getLogger(JMSEndpoint.class);
    private MessageComposer<JMSBindingData> _composer;
    private OperationSelector<JMSBindingData> _selector;
    private String _jndiPropertiesFileName;
    private String _destinationJndiPropertiesFileName;
    private String _connectionFactoryJNDIName;
    private DestinationType _defaultDestinationType = DestinationType.JNDI;
    private String _defaultFaultTo;
    private String _defaultReplyTo;

    private String _userName;
    private String _password;
    private Properties _jndiProperties;
    private Properties _destinationJndiProperties;
    private ConnectionFactory _connectionFactory;
    private Destination _defaultFaultToJMSDestination;
    private Destination _defaultReplyToJMSDestination;
    private MessageType _defaultOutMessageType = MessageType.Object;

    private enum DestinationType {
        Queue, Topic, JNDI
    }

    private enum MessageType {
        Stream, Map, Text, Object, Bytes, Plain 
    }

    @Override
    public void initialize() {
        super.initialize();
        _composer = getMessageComposer(JMSBindingData.class);
        _selector = getOperationSelector(JMSBindingData.class);

        try {
            InitialContext cfic = null;
            if (getJndiProperties() != null) {
                cfic = new InitialContext(getJndiProperties());
            } else {
                cfic = new InitialContext();
            }
            if (_connectionFactoryJNDIName != null) {
                _connectionFactory = (ConnectionFactory) cfic.lookup(_connectionFactoryJNDIName);
            }

            // caching replyTo/faultTo destination if its type is JNDI
            if (_defaultDestinationType == DestinationType.JNDI) {
                InitialContext destic = null;
                if (getDestinationJndiProperties() != null) {
                    cfic.close();
                    destic = new InitialContext(getDestinationJndiProperties());
                } else {
                    destic = cfic;
                }

                if (_defaultFaultTo != null) {
                    try {
                        _defaultFaultToJMSDestination = (Destination) destic.lookup(_defaultFaultTo);
                    } catch (Exception e) {
                        JCALogger.ROOT_LOGGER.destinationNotFound(_defaultFaultTo, "faultTo");
                    }
                }
                if (_defaultReplyTo != null) {
                    try {
                        _defaultReplyToJMSDestination = (Destination) destic.lookup(_defaultReplyTo);
                    } catch (Exception e) {
                        JCALogger.ROOT_LOGGER.destinationNotFound(_defaultReplyTo, "replyTo");
                    }
                }
                destic.close();
            }
        } catch (Exception e) {
            throw JCAMessages.MESSAGES.failedToInitialize(this.getClass().getName(), e);
        }
        
        if (_logger.isDebugEnabled()) {
            StringBuilder msg = new StringBuilder()
                .append("Initialized with: {")
                .append("Connection Factory:").append(_connectionFactoryJNDIName)
                .append(", Destination Type:").append(_defaultDestinationType)
                .append(", replyTo Name:").append(_defaultReplyTo)
                .append(", faultTo Name:").append(_defaultReplyTo)
                .append(", User Name:").append(_userName)
                .append(", Output Message Type:").append(_defaultOutMessageType)
                .append(", JNDI Properties File:").append(_jndiPropertiesFileName)
                .append(", Destination JNDI Properties File:").append(_destinationJndiPropertiesFileName)
                .append("}");
            _logger.debug(msg.toString());
        }
    }

    @Override
    public void onMessage(Message message) {

        try {
            JMSBindingData bindingData = new JMSBindingData(message);
            final String operation = _selector != null ? _selector.selectOperation(bindingData).getLocalPart() : null;
            SynchronousInOutHandler replyHandler = new SynchronousInOutHandler();
            Exchange exchange = createExchange(operation, replyHandler);
            exchange.send(_composer.compose(bindingData, exchange));

            if (_connectionFactory == null) {
                return;
            }
            
            // Process replyTo and faultTo if ConnectionFactory is available 
            Context context = exchange.getContext();
            Connection connection = null;
            
            try {
                if (_userName != null) {
                    connection = _connectionFactory.createConnection(_userName, _password);
                } else {
                    connection = _connectionFactory.createConnection();
                }
                Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
                
                Destination faultTo = getFaultToDestinationFromContext(session, context);
                Destination replyTo = getReplyToDestinationFromContext(session, context);
                if (faultTo != null && ExchangeState.FAULT.equals(exchange.getState())) {
                    if (exchange.getMessage() != null) {
                        sendJMSMessage(session, faultTo, exchange, getOutputMessageTypeFromContext(context));
                    }
                } else if (replyTo != null && ExchangePattern.IN_OUT.equals(exchange.getPattern())) {
                        exchange = replyHandler.waitForOut();
                        if (exchange.getMessage() != null) {
                            sendJMSMessage(session, replyTo, exchange, getOutputMessageTypeFromContext(context));
                        }
                }
            } finally {
                if (connection != null) {
                    connection.close();
                }
            }
        } catch (Exception e) {
            throw new SwitchYardException(e);
        }
    }

    protected void sendJMSMessage(Session session, Destination destination, Exchange exchange, MessageType type) {
        try {
            MessageProducer producer = session.createProducer(destination);
            Context context = exchange.getContext();

            Message msg;
            switch (type) {
            case Stream:
                msg = session.createStreamMessage();
                break;
            case Map:
                msg = session.createMapMessage();
                break;
            case Text:
                msg = session.createTextMessage();
                break;
            case Bytes:
                msg = session.createBytesMessage();
                break;
            case Plain:
                msg = session.createMessage();
                break;
            default:
                msg = session.createObjectMessage();
            }
            producer.send(_composer.decompose(exchange, new JMSBindingData(msg)).getMessage(),
                    getDeliveryModeFromContext(context, producer),
                    getPriorityFromContext(context, producer),
                    getTimeToLiveFromContext(context, producer));
        } catch (Exception e) {
            JCALogger.ROOT_LOGGER.failedToSendMessage(destination.toString(), e.getMessage());
            if (_logger.isDebugEnabled()) {
                _logger.debug(e);
            }
        }
    }

    protected Destination getReplyToDestinationFromContext(Session session, Context ctx) {
        Destination replyToDestination = null;
        
        String key = CONTEXT_PROPERTY_PREFIX + KEY_REPLY_TO;
        if (ctx.getProperty(key) != null) {
            String replyToName = ctx.getPropertyValue(key);
            DestinationType replyToType = getDestinationTypeFromContext(ctx);
            
            try {
                switch (replyToType) {
                case JNDI:
                    replyToDestination = lookupDestinationFromJNDI(replyToName);
                    break;
                case Queue:
                    replyToDestination = session.createQueue(replyToName);
                    break;
                case Topic:
                    replyToDestination = session.createTopic(replyToName);
                    break;
                }
                if (_logger.isDebugEnabled()) {
                    _logger.debug("replyTo is set to '" + replyToName + "'");
                }
            } catch (Exception e) {
                JCALogger.ROOT_LOGGER.contextDestinationNotFound(replyToName, _defaultReplyTo);
                if (_logger.isDebugEnabled()) {
                    _logger.debug(e);
                }
            }
        }
        
        // No valid replyTo is specified in a context property - use default
        if (replyToDestination == null) {
            try {
                switch (_defaultDestinationType) {
                case JNDI:
                    replyToDestination = _defaultReplyToJMSDestination;
                    break;
                case Queue:
                    replyToDestination = session.createQueue(_defaultReplyTo);
                    break;
                case Topic:
                    replyToDestination = session.createTopic(_defaultReplyTo);
                    break;
                }
            } catch (Exception e) {
                JCALogger.ROOT_LOGGER.destinationNotFound(_defaultReplyTo, "replyTo");
                if (_logger.isDebugEnabled()) {
                    _logger.debug(e);
                }
            }
        }
        return replyToDestination;
    }

    protected Destination getFaultToDestinationFromContext(Session session, Context ctx) {
        Destination faultToDestination = null;
        
        String key = CONTEXT_PROPERTY_PREFIX + KEY_FAULT_TO;
        if (ctx.getProperty(key) != null) {
            String faultToName = ctx.getPropertyValue(key);
            DestinationType faultToType = getDestinationTypeFromContext(ctx);
            
            try {
                switch (faultToType) {
                case JNDI:
                    faultToDestination = lookupDestinationFromJNDI(faultToName);
                    break;
                case Queue:
                    faultToDestination = session.createQueue(faultToName);
                    break;
                case Topic:
                    faultToDestination = session.createTopic(faultToName);
                    break;
                }
                if (_logger.isDebugEnabled()) {
                    _logger.debug("faultTo is set to '" + faultToName + "'");
                }
            } catch (Exception e) {
                JCALogger.ROOT_LOGGER.contextDestinationNotFound(faultToName, _defaultFaultTo);
                if (_logger.isDebugEnabled()) {
                    _logger.debug(e);
                }
            }
        }
        
        // No valid faultTo is specified in a context property - use default
        if (faultToDestination == null) {
            try {
                switch (_defaultDestinationType) {
                case JNDI:
                    faultToDestination = _defaultFaultToJMSDestination;
                    break;
                case Queue:
                    faultToDestination = session.createQueue(_defaultFaultTo);
                    break;
                case Topic:
                    faultToDestination = session.createTopic(_defaultFaultTo);
                    break;
                }
            } catch (Exception e) {
                JCALogger.ROOT_LOGGER.destinationNotFound(_defaultFaultTo, "faultTo");
                if (_logger.isDebugEnabled()) {
                    _logger.debug(e);
                }
            }
        }
        return faultToDestination;
    }

    protected DestinationType getDestinationTypeFromContext(Context ctx) {
        DestinationType destType = _defaultDestinationType;
        String key = CONTEXT_PROPERTY_PREFIX + KEY_DESTINATION_TYPE;
        if (ctx.getProperty(key) != null) {
            String type = ctx.getPropertyValue(key);
            DestinationType ctxType = parseDestinationType(type);
            if (ctxType != null) {
                destType = ctxType;
                if (_logger.isDebugEnabled()) {
                    _logger.debug("Destination type is set to '" + destType.toString() + "'");
                }
            }
        }
        return destType;
    }

    protected Destination lookupOrCreateDestination(Session session, DestinationType type, String destName) throws Exception {
        switch (type) {
        case JNDI:
            return lookupDestinationFromJNDI(destName);
        case Queue:
            return session.createQueue(destName);
        case Topic:
            return session.createTopic(destName);
        default:
            return null;
        }
    }

    protected Destination lookupDestinationFromJNDI(String destName) throws Exception {
        InitialContext ic = null;
        try {
            if (getDestinationJndiProperties() != null) {
                ic = new InitialContext(getDestinationJndiProperties());
            } else if (getJndiProperties() != null) {
                ic = new InitialContext(getJndiProperties());
            } else {
                ic = new InitialContext();
            }
            return (Destination) ic.lookup(destName);
        } finally {
            if (ic != null) {
                try {
                    ic.close();
                } catch (Exception e) {
                    if (_logger.isDebugEnabled()) {
                        _logger.debug(e);
                    }
                }
            }
        }
    }

    protected MessageType getOutputMessageTypeFromContext(Context ctx) {
        String key = CONTEXT_PROPERTY_PREFIX + KEY_MESSAGE_TYPE;
        if (ctx.getProperty(key) != null) {
            MessageType type = MessageType.valueOf(ctx.getPropertyValue(key).toString());
            if (_logger.isDebugEnabled()) {
                _logger.debug("Output message type is set to '" + type.toString() + "'");
            }
            return type;
        }
        return _defaultOutMessageType;
    }

    protected int getDeliveryModeFromContext(Context ctx, MessageProducer producer) throws Exception {
        String key = CONTEXT_PROPERTY_PREFIX + KEY_DELIVERY_MODE;
        if (ctx.getProperty(key) != null) {
            int deliveryMode = Integer.parseInt(ctx.getPropertyValue(key).toString());
            if (_logger.isDebugEnabled()) {
                _logger.debug("Delivery Mode is set to '" + deliveryMode + "'");
            }
            return deliveryMode;
        }
        return producer.getDeliveryMode();
    }

    protected int getPriorityFromContext(Context ctx, MessageProducer producer) throws Exception {
        String key = CONTEXT_PROPERTY_PREFIX + KEY_PRIORITY;
        if (ctx.getProperty(key) != null) {
            int priority = Integer.parseInt(ctx.getPropertyValue(key).toString());
            if (_logger.isDebugEnabled()) {
                _logger.debug("Priority is set to '" + priority + "'");
            }
            return priority;
        }
        return producer.getPriority();
    }

    protected long getTimeToLiveFromContext(Context ctx, MessageProducer producer) throws Exception {
        String key = CONTEXT_PROPERTY_PREFIX + KEY_TIME_TO_LIVE;
        if (ctx.getProperty(key) != null) {
            long ttl = Long.parseLong(ctx.getPropertyValue(key).toString());
            if (_logger.isDebugEnabled()) {
                _logger.debug("Time to Live is set to '" + ttl + "'");
            }
            return ttl;
        }
        return producer.getTimeToLive();
    }

    /**
     * set ConnectionFactory JNDI name.
     * 
     * @param name ConnectionFactory JNDI name
     */
    public void setConnectionFactoryJNDIName(String name) {
        _connectionFactoryJNDIName = name;
    }

    /**
     * Set destination type.
     * @param type destination type
     */
    public void setDestinationType(String type) {
        DestinationType destType = parseDestinationType(type);
        if (destType == null) {
            JCALogger.ROOT_LOGGER.invalidDestinationType(type, DestinationType.JNDI.toString());
            destType = DestinationType.JNDI;
        }
        _defaultDestinationType = destType;
    }

    protected DestinationType parseDestinationType(String type) {
        DestinationType destType = null;
        if (type.equals(javax.jms.Queue.class.getName()) || type.equalsIgnoreCase("queue")) {
            destType = DestinationType.Queue;
        } else if (type.equals(javax.jms.Topic.class.getName()) || type.equalsIgnoreCase("Topic")) {
            destType = DestinationType.Topic;
        } else if (type.equalsIgnoreCase("JNDI")) {
            destType = DestinationType.JNDI;
        }
        return destType;
    }

    /**
     * set replyTo destination name.
     * 
     * @param name replyTo destination name
     */
    public void setReplyTo(String name) {
        _defaultReplyTo = name;
    }

    /**
     * set faultTo destination name.
     * 
     * @param name faultTo destination name
     */
    public void setFaultTo(String name) {
        _defaultFaultTo = name;
    }

    /**
     * set user name.
     * 
     * @param user user name
     */
    public void setUsername(String user) {
        _userName = user;
    }

    /**
     * set password.
     * 
     * @param passwd password
     */
    public void setPassword(String passwd) {
        _password = passwd;
    }

    /**
     * set message type.
     * @param type message type
     */
    public void setMessageType(String type) {
        _defaultOutMessageType = MessageType.valueOf(type);
    }

    /**
     * set JNDI properties file name.
     * @param name file name
     */
    public void setJndiPropertiesFileName(String name) {
            _jndiPropertiesFileName = name;
    }

    /**
     * get JNDI properties.
     * @return JNDI properties
     */
    public Properties getJndiProperties() {
        if (_jndiPropertiesFileName != null && _jndiProperties == null) {
            try {
                InputStream is = Classes.getResourceAsStream(_jndiPropertiesFileName);
                Properties props = new Properties();
                props.load(is);
                is.close();
                _jndiProperties = props;
            } catch (Exception e) {
                JCALogger.ROOT_LOGGER.failedToLoadJndiPropertiesFile(_jndiPropertiesFileName, e);
            }
        }
        return _jndiProperties;
    }

    /**
     * set JNDI properties file name for destination lookup.
     * @param name filename
     */
    public void setDestinationJndiPropertiesFileName(String name) {
        _destinationJndiPropertiesFileName = name;
    }

    /**
     * get JNDI properties for destination lookup.
     * @return InitialContext properties for destination lookup
     */
    public Properties getDestinationJndiProperties() {
        if (_destinationJndiPropertiesFileName != null && _destinationJndiProperties == null) {
            try {
                InputStream is = Classes.getResourceAsStream(_destinationJndiPropertiesFileName);
                Properties props = new Properties();
                props.load(is);
                is.close();
                _destinationJndiProperties = props;
            } catch (Exception e) {
                JCALogger.ROOT_LOGGER.failedToLoadJndiPropertiesFile(_destinationJndiPropertiesFileName, e);
            }
        }
        return _destinationJndiProperties;
    }
}

