/*
 * Copyright 2002-2011 the original author or authors.
 * 
 * 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.springframework.flex.messaging.jms;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.jms.ConnectionFactory;
import javax.jms.Destination;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.flex.messaging.SubscribeEvent;
import org.springframework.flex.messaging.UnsubscribeEvent;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.listener.DefaultMessageListenerContainer;
import org.springframework.jms.listener.adapter.MessageListenerAdapter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.destination.DestinationResolver;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.Assert;

import flex.messaging.FlexContext;
import flex.messaging.messages.CommandMessage;
import flex.messaging.messages.Message;
import flex.messaging.services.MessageService;
import flex.messaging.services.messaging.adapters.MessagingAdapter;

/**
 * A {@link MessagingAdapter} implementation that enables sending and receiving messages via JMS.
 * 
 * @author Mark Fisher
 * @author Jeremy Grelle
 */
public class JmsAdapter extends MessagingAdapter implements InitializingBean, BeanNameAware, ApplicationEventPublisherAware {

    private final Log logger = LogFactory.getLog(getClass());

    private volatile ConnectionFactory connectionFactory;

    private volatile Object destination;

    private volatile boolean pubSubDomain;

    private volatile MessageConverter messageConverter;
    
    private volatile ApplicationEventPublisher applicationEventPublisher;

    private final JmsTemplate jmsTemplate = new JmsTemplate();

    private final DefaultMessageListenerContainer messageListenerContainer = new DefaultMessageListenerContainer();

    private final Set<Object> subscriberIds = new HashSet<Object>();
    
    private final Map<Object, String> clientSubscriptions = new HashMap<Object, String>();

    /**
     * 
     * {@inheritDoc}
     */
    public void afterPropertiesSet() {
        Assert.notNull(this.connectionFactory, "connectionFactory is required");
        Assert.notNull(this.destination, "destination or destination name is required");
        this.jmsTemplate.setConnectionFactory(this.connectionFactory);
        MessageConverter converterToSet = this.messageConverter;
        if (converterToSet == null || !(converterToSet instanceof FlexMessageConverter)) {
            converterToSet = new FlexMessageConverter(converterToSet);
        }
        this.jmsTemplate.setMessageConverter(converterToSet);
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter();
        messageListenerAdapter.setMessageConverter(converterToSet);
        messageListenerAdapter.setDelegate(this);
        this.messageListenerContainer.setConnectionFactory(this.connectionFactory);
        this.messageListenerContainer.setMessageListener(messageListenerAdapter);
        this.messageListenerContainer.setAutoStartup(false);
        if (this.destination instanceof Destination) {
            this.jmsTemplate.setDefaultDestination((Destination) this.destination);
            this.messageListenerContainer.setDestination((Destination) this.destination);
        } else {
            this.jmsTemplate.setPubSubDomain(this.pubSubDomain);
            this.jmsTemplate.setDefaultDestinationName((String) this.destination);
            this.messageListenerContainer.setPubSubDomain(this.pubSubDomain);
            this.messageListenerContainer.setDestinationName((String) this.destination);
        }
        this.jmsTemplate.afterPropertiesSet();
        this.messageListenerContainer.afterPropertiesSet();
    }

    /**
     * Returns the {@link JmsTemplate} used by this adapter
     * 
     * @return the jms template
     */
    public JmsTemplate getJmsTemplate() {
        return this.jmsTemplate;
    }

    /**
     * 
     * {@inheritDoc}
     */
    @Override
    public boolean handlesSubscriptions() {
        return true;
    }

    /**
     * Invoked when a Message is received from a Flex client.
     */
    @Override
    public Object invoke(Message flexMessage) {
        this.jmsTemplate.convertAndSend(flexMessage);
        return null;
    }

    /**
     * 
     * {@inheritDoc}
     */
    @Override
    public Object manage(CommandMessage commandMessage) {
        String clientId = (String) commandMessage.getClientId();
        if (commandMessage.getOperation() == CommandMessage.SUBSCRIBE_OPERATION) {
            this.subscriberIds.add(clientId);
            synchronized (this.messageListenerContainer) {
                if (!this.messageListenerContainer.isActive()) {
                    this.messageListenerContainer.initialize();
                }
                if (!this.messageListenerContainer.isRunning()) {
                    this.messageListenerContainer.start();
                }
            }
            if (this.logger.isInfoEnabled()) {
                this.logger.info("client [" + clientId + "] subscribed to destination [" + this.getDestination().getId() + "]");
            }
            String flexClientId = FlexContext.getFlexClient().getId();
            this.clientSubscriptions.put(clientId, flexClientId);
            this.applicationEventPublisher.publishEvent(new SubscribeEvent(flexClientId, clientId, this.getDestination().getId()));
        } else if (commandMessage.getOperation() == CommandMessage.UNSUBSCRIBE_OPERATION) {
            this.subscriberIds.remove(clientId);
            synchronized (this.messageListenerContainer) {
                if (this.subscriberIds.isEmpty() && this.messageListenerContainer.isActive()){ 
                    this.messageListenerContainer.shutdown();
                }
            }
            if (this.logger.isInfoEnabled()) {
                this.logger.info("client [" + clientId + "] unsubscribed from destination [" + this.getDestination().getId() + "]");
            }
            String flexClientId = this.clientSubscriptions.remove(clientId);
            this.applicationEventPublisher.publishEvent(new UnsubscribeEvent(flexClientId, clientId, this.getDestination().getId()));
        }
        return null;
    }

    /**
     * 
     * {@inheritDoc}
     */
    public void setBeanName(String beanName) {
        this.setId(beanName);
    }

    /**
     * Sets the {@link ConnectionFactory} to use for sending and receiving JMS messages
     * 
     * @param connectionFactory the connection factory
     */
    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }

    /**
     * Sets the {@link DestinationResolver} for resolving the JMS destination for this adapter
     * 
     * @param destinationResolver the destination resolver
     */
    public void setDestinationResolver(DestinationResolver destinationResolver) {
        Assert.notNull(destinationResolver, "destinationResolver must not be null");
        this.jmsTemplate.setDestinationResolver(destinationResolver);
        this.messageListenerContainer.setDestinationResolver(destinationResolver);
    }

    /**
     * Sets the JMS {@link Destination} for messages sent and received by this adapter
     * 
     * @param destination the destination
     */
    public void setJmsDestination(Destination destination) {
        this.destination = destination;
    }

    /**
     * Sets the {@link MessageConverter} for messages sent and received by this adapter.
     * 
     * @param messageConverter the message converter
     */
    public void setMessageConverter(MessageConverter messageConverter) {
        this.messageConverter = messageConverter;
    }

    /**
     * Sets the JMS queue name for messages sent and received by this adapter.
     * 
     * @param queueName the JMS queue name
     */
    public void setQueueName(String queueName) {
        this.destination = queueName;
    }

    /**
     * Sets the JMS topic name for messages sent and received by this adapter.
     * 
     * @param topicName the JMS topic name
     */
    public void setTopicName(String topicName) {
        this.pubSubDomain = true;
        this.destination = topicName;
    }

    /**
     * Sets the {@link PlatformTransactionManager} to be used when sending and receiving messages
     * 
     * @param transactionManager the transaction manager
     */
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        Assert.notNull(transactionManager, "transactionManager must not be null");
        this.messageListenerContainer.setTransactionManager(transactionManager);
    }

    /**
     * 
     * {@inheritDoc}
     */
    @Override
    public void start() {
        if (!this.messageListenerContainer.isActive()) {
            this.messageListenerContainer.initialize();
        }
        super.start();
    }
    
    /**
     * 
     * {@inheritDoc}
     */
    @Override
    public void stop() {
        this.messageListenerContainer.shutdown();
        super.stop();
    }

    DefaultMessageListenerContainer getMessageListenerContainer() {
        return this.messageListenerContainer;
    }

    /**
     * Invoked when a Message is received from a JMS client.
     */
    void handleMessage(Message flexMessage) {
        flexMessage.setDestination(this.getDestination().getId());
        MessageService messageService = (MessageService) getDestination().getService();
        messageService.pushMessageToClients(flexMessage, true);
        messageService.sendPushMessageFromPeer(flexMessage, true);
    }

	public void setApplicationEventPublisher(
			ApplicationEventPublisher applicationEventPublisher) {
		this.applicationEventPublisher = applicationEventPublisher;
	}

}
