/**
 * 
 * Copyright 2004 Protique Ltd
 * 
 * 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.codehaus.activemq;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.broker.Broker;
import org.codehaus.activemq.broker.BrokerConnector;
import org.codehaus.activemq.broker.BrokerContainer;
import org.codehaus.activemq.broker.BrokerContainerFactory;
import org.codehaus.activemq.broker.BrokerContext;
import org.codehaus.activemq.broker.impl.BrokerClientImpl;
import org.codehaus.activemq.broker.impl.BrokerConnectorImpl;
import org.codehaus.activemq.broker.impl.BrokerContainerFactoryImpl;
import org.codehaus.activemq.io.WireFormat;
import org.codehaus.activemq.io.impl.DefaultWireFormat;
import org.codehaus.activemq.jndi.JNDIBaseStorable;
import org.codehaus.activemq.management.JMSStatsImpl;
import org.codehaus.activemq.management.StatsCapable;
import org.codehaus.activemq.message.ActiveMQQueue;
import org.codehaus.activemq.message.ActiveMQTopic;
import org.codehaus.activemq.message.ConnectionInfo;
import org.codehaus.activemq.message.ConsumerInfo;
import org.codehaus.activemq.service.Service;
import org.codehaus.activemq.transport.TransportChannel;
import org.codehaus.activemq.transport.TransportChannelFactory;
import org.codehaus.activemq.transport.TransportChannelListener;
import org.codehaus.activemq.transport.TransportChannelProvider;
import org.codehaus.activemq.transport.vm.VmTransportChannel;
import org.codehaus.activemq.util.IdGenerator;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.TopicConnection;
import javax.jms.TopicConnectionFactory;
import javax.management.j2ee.statistics.Stats;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

/**
 * A ConnectionFactory is an an Administed object, and is used for creating
 * Connections.
 * <p/>
 * This class also implements QueueConnectionFactory and TopicConnectionFactory and is an Administered object.
 * You can use this connection to create both QueueConnections and TopicConnections.
 *
 * @version $Revision: 1.5.2.1 $
 * @see javax.jms.ConnectionFactory
 */
public class ActiveMQConnectionFactory extends JNDIBaseStorable implements ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory, Service, StatsCapable {

    private static final Log log = LogFactory.getLog(ActiveMQConnectionFactory.class);

    private BrokerContext brokerContext = BrokerContext.getInstance();
    private BrokerContainerFactory brokerContainerFactory;
    protected BrokerContainer brokerContainer;

    protected String userName;
    protected String password;
    protected String brokerURL;
    protected String clientID;
    protected String brokerName;
    private boolean useEmbeddedBroker;
    /**
     * Should we use an async send for persistent non transacted messages ?
     */
    protected boolean useAsyncSend = false;
    /* The list of emebeded brokers that this object started */
    private List startedEmbeddedBrokers = new ArrayList();

    private JMSStatsImpl stats = new JMSStatsImpl();
    private WireFormat wireFormat = new DefaultWireFormat();
    private IdGenerator idGenerator = new IdGenerator();
    private int connectionCount;
    private String brokerXmlConfig;

    /**
     * Default Constructor for ActiveMQConnectionFactory
     */
    public ActiveMQConnectionFactory() {
        this.userName = ActiveMQConnection.DEFAULT_USER;
        this.password = ActiveMQConnection.DEFAULT_PASSWORD;
        this.brokerURL = ActiveMQConnection.DEFAULT_URL;
    }


    public ActiveMQConnectionFactory(String brokerURL) {
        this();
        this.brokerURL = brokerURL;
    }

    public ActiveMQConnectionFactory(String userName, String password, String brokerURL) {
        this.userName = userName;
        this.password = password;
        this.brokerURL = brokerURL;
    }

    /**
     * Constructs a {@link ConnectionFactory} with an already configured and <b>started</b> {@link BrokerContainer}
     * ready for use in embedded mode.
     *
     * @param container
     */
    public ActiveMQConnectionFactory(BrokerContainer container) {
        this(container, "vm://" + container.getBroker().getName());
    }

    /**
     * Constructs a {@link ConnectionFactory} with an already configured and <b>started</b> {@link BrokerContainer}
     * ready for use in embedded mode and the brokerURL connection.
     *
     * @param container
     */
    public ActiveMQConnectionFactory(BrokerContainer container, String brokerURL) {
        this();
        this.brokerContainer = container;
        this.useEmbeddedBroker = true;
        this.brokerURL = brokerURL;
    }


    public Stats getStats() {
        return stats;
    }

    public JMSStatsImpl getFactoryStats() {
        return stats;
    }

    /**
     * @return Returns the brokerURL.
     */
    public String getBrokerURL() {
        return brokerURL;
    }

    /**
     * @param brokerURL The brokerURL to set.
     */
    public void setBrokerURL(String brokerURL) {
        this.brokerURL = brokerURL;
    }

    /**
     * @return Returns the clientID.
     */
    public String getClientID() {
        return clientID;
    }

    /**
     * @param clientID The clientID to set.
     */
    public void setClientID(String clientID) {
        this.clientID = clientID;
    }

    /**
     * @return Returns the password.
     */
    public String getPassword() {
        return password;
    }

    /**
     * @param password The password to set.
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * @return Returns the userName.
     */
    public String getUserName() {
        return userName;
    }

    /**
     * @param userName The userName to set.
     */
    public void setUserName(String userName) {
        this.userName = userName;
    }

    /**
     * Is an embedded broker used by this connection factory
     *
     * @return true if an embedded broker will be used by this connection factory
     */
    public boolean isUseEmbeddedBroker() {
        return useEmbeddedBroker;
    }

    /**
     * Allows embedded brokers to be associated with a connection factory
     *
     * @param useEmbeddedBroker
     */
    public void setUseEmbeddedBroker(boolean useEmbeddedBroker) {
        this.useEmbeddedBroker = useEmbeddedBroker;
    }

    /**
     * The name of the broker to use if creating an embedded broker
     *
     * @return
     */
    public String getBrokerName() {
        if (brokerName == null) {
            // lets auto-create a broker name
            brokerName = idGenerator.generateId();
        }
        return brokerName;
    }
    
    /**
     * The name of the broker to use if creating an embedded broker
     *
     * @return
     */
    public String getBrokerName(String url) {
        if (brokerName == null) {
            brokerName = url;
        }
        return brokerName;
    }

    public void setBrokerName(String brokerName) {
        this.brokerName = brokerName;
    }

    /**
     * @return Returns the useAsyncSend.
     */
    public boolean isUseAsyncSend() {
        return useAsyncSend;
    }

    /**
     * @param useAsyncSend The useAsyncSend to set.
     */
    public void setUseAsyncSend(boolean useAsyncSend) {
        this.useAsyncSend = useAsyncSend;
    }

    public WireFormat getWireFormat() {
        return wireFormat;
    }

    /**
     * Allows a custom wire format to be used; otherwise the default Java wire format is used
     * which is designed for minimum size and maximum speed on the Java platform
     *
     * @param wireFormat
     */
    public void setWireFormat(WireFormat wireFormat) {
        this.wireFormat = wireFormat;
    }

    public String getBrokerXmlConfig() {
        return brokerXmlConfig;
    }

    public BrokerContainer getBrokerContainer() {
        return brokerContainer;
    }

    /**
     * Sets the <a href="http://activemq.codehaus.org/Xml+Configuration">XML configuration file</a>
     * used to configure the ActiveMQ broker via Spring if using embedded mode.
     *
     * @param brokerXmlConfig is the filename which is assumed to be on the classpath unless a URL
     *                        is specified. So a value of <code>foo/bar.xml</code> would be assumed to be on the classpath
     *                        whereas <code>file:dir/file.xml</code> would use the file system.
     *                        Any valid URL string is supported.
     * @see #setUseEmbeddedBroker(boolean)
     */
    public void setBrokerXmlConfig(String brokerXmlConfig) {
        this.brokerXmlConfig = brokerXmlConfig;
    }

    public BrokerContainerFactory getBrokerContainerFactory() throws JMSException {
        if (brokerContainerFactory == null) {
            brokerContainerFactory = createBrokerContainerFactory();
        }
        return brokerContainerFactory;
    }

    public void setBrokerContainerFactory(BrokerContainerFactory brokerContainerFactory) {
        this.brokerContainerFactory = brokerContainerFactory;
    }

    /**
     * Returns the context used to store broker containers and connectors which defaults
     * to using the singleton
     */
    public BrokerContext getBrokerContext() {
        return brokerContext;
    }

    public void setBrokerContext(BrokerContext brokerContext) {
        this.brokerContext = brokerContext;
    }

    /**
     * Create a JMS Connection
     *
     * @return the JMS Connection
     * @throws JMSException if an error occurs creating the Connection
     */
    public Connection createConnection() throws JMSException {
        return this.createConnection(this.userName, this.password);
    }

    /**
     * @param userName
     * @param password
     * @return the Connection
     * @throws JMSException if an error occurs creating the Connection
     */
    public Connection createConnection(String userName, String password) throws JMSException {
        ActiveMQConnection connection = new ActiveMQConnection(this, userName, password, createTransportChannel(this.brokerURL));
        connection.setUseAsyncSend(isUseAsyncSend());
        if (this.clientID != null && this.clientID.length() > 0) {
            connection.setClientID(this.clientID);
        }
        return connection;
    }

    /**
     * Create a JMS QueueConnection
     *
     * @return the JMS QueueConnection
     * @throws JMSException if an error occurs creating the Connection
     */
    public QueueConnection createQueueConnection() throws JMSException {
        return this.createQueueConnection(this.userName, this.password);
    }

    /**
     * @param userName
     * @param password
     * @return the QueueConnection
     * @throws JMSException if an error occurs creating the Connection
     */
    public QueueConnection createQueueConnection(String userName, String password) throws JMSException {
        return (QueueConnection) createConnection(userName, password);
    }

    /**
     * Create a JMS TopicConnection
     *
     * @return the JMS TopicConnection
     * @throws JMSException if an error occurs creating the Connection
     */
    public TopicConnection createTopicConnection() throws JMSException {
        return this.createTopicConnection(this.userName, this.password);
    }

    /**
     * @param userName
     * @param password
     * @return the TopicConnection
     * @throws JMSException if an error occurs creating the Connection
     */
    public TopicConnection createTopicConnection(String userName, String password) throws JMSException {
        return (TopicConnection) createConnection(userName, password);
    }


    public void start() throws JMSException {
    }

    /**
     * A hook to allow any embedded JMS Broker's to be closed down
     *
     * @throws JMSException
     */
    public synchronized void stop() throws JMSException {
        // Stop all embded brokers that we started.
        for (Iterator iter = startedEmbeddedBrokers.iterator(); iter.hasNext();) {
            String uri = (String) iter.next();
            brokerContext.deregisterConnector(uri);
        }
        if (brokerContainer != null) {
            brokerContainer.stop();
            brokerContainer = null;
        }
    }


    public Broker getEmbeddedBroker() throws JMSException {
        if (isUseEmbeddedBroker()) {
            return getContainer(getBrokerName()).getBroker();
        }
        return null;
    }

    public static synchronized void registerBroker(String theURLString, BrokerConnector brokerConnector) {
        BrokerContext.getInstance().registerConnector(theURLString, brokerConnector);
    }

    public static synchronized void unregisterBroker(String theURLString) {
        BrokerContext.getInstance().deregisterConnector(theURLString);
    }


    // Implementation methods
    //-------------------------------------------------------------------------


    /**
     * Set the properties that will represent the instance in JNDI
     *
     * @param props
     */
    protected void buildFromProperties(Properties props) {
        this.userName = props.getProperty("userName", this.userName);
        this.password = props.getProperty("password", this.password);
        this.brokerURL = props.getProperty("brokerURL", this.brokerURL);
        this.brokerName = props.getProperty("brokerName", this.brokerName);
        this.clientID = props.getProperty("clientID");
        this.useAsyncSend = getBoolean(props, "useAsyncSend", true);
        this.useEmbeddedBroker = getBoolean(props, "useEmbeddedBroker");
        this.brokerXmlConfig = props.getProperty("brokerXmlConfig", this.brokerXmlConfig);
    }

    /**
     * Initialize the instance from properties stored in JNDI
     *
     * @param props
     */
    protected void populateProperties(Properties props) {
        props.put("userName", this.userName);
        props.put("password", this.password);
        props.put("brokerURL", this.brokerURL);
        props.put("brokerName", this.brokerName);
        if (this.clientID != null) {
            props.put("clientID", this.clientID);
        }
        props.put("useAsyncSend", (useAsyncSend) ? "true" : "false");
        props.put("useEmbeddedBroker", (useEmbeddedBroker) ? "true" : "false");
        if (this.brokerXmlConfig != null) {
            props.put("brokerXmlConfig", this.brokerXmlConfig);
        }
    }

    /**
     * Helper method to return the property value as a boolean flag
     *
     * @param props
     * @param key
     * @return
     */
    protected boolean getBoolean(Properties props, String key) {
        return getBoolean(props, key, false);
    }

    /**
     * Helper method to return the property value as a boolean flag
     *
     * @param props
     * @param key
     * @param defaultValue
     * @return
     */
    protected boolean getBoolean(Properties props, String key, boolean defaultValue) {
        String value = props.getProperty(key);
        return value != null ? value.equalsIgnoreCase("true") : defaultValue;
    }

    protected BrokerContainerFactory createBrokerContainerFactory() throws JMSException {
        if (brokerXmlConfig != null) {
            return XmlConfigHelper.createBrokerContainerFactory(brokerXmlConfig);
        }
        return new BrokerContainerFactoryImpl();
    }

    /**
     * Factory method to create a TransportChannel from a URL
     * @param theURLString
     * @return the TransportChannel to use with the embedded broker
     * @throws JMSException
     */
    protected TransportChannel createTransportChannel(String theURLString) throws JMSException {
        URI uri = createURI(theURLString);
        TransportChannelFactory factory = TransportChannelProvider.getFactory(uri);
        BrokerConnector brokerConnector = null;
        boolean created = false;
        TransportChannel transportChannel = null;
        boolean embedServer = isUseEmbeddedBroker() || factory.requiresEmbeddedBroker();
        if (embedServer) {
            synchronized (this) {
                if (factory.requiresEmbeddedBroker()) {
                    transportChannel = factory.create(getWireFormat(), uri);
                    brokerConnector = transportChannel.getEmbeddedBrokerConnector();
                }
                if (brokerConnector == null) {
                    brokerConnector = brokerContext.getConnectorByURL(theURLString);
                    if (brokerConnector == null) {
                        brokerConnector = createBrokerConnector(theURLString);
                        brokerContext.registerConnector(theURLString, brokerConnector);
                        startedEmbeddedBrokers.add(theURLString);
                        created = true;
                    }
                }
                else {
                    created = true;
                }
            }
        }
        if (transportChannel == null){
            transportChannel = factory.create(getWireFormat(), uri);
        }
       
        if (embedServer) {
            return ensureServerIsAvailable(uri, transportChannel, brokerConnector, created);
        }
        return transportChannel;
    }

    protected synchronized BrokerContainer getContainer(String brokerName) throws JMSException {
        if (brokerContainer == null) {
            brokerContainer = brokerContext.getBrokerContainerByName(brokerName, getBrokerContainerFactory());
        }
        return brokerContainer;
    }

    protected BrokerConnector createBrokerConnector(String url) throws JMSException {
        BrokerConnector brokerConnector;
        brokerConnector = new BrokerConnectorImpl(getContainer(getBrokerName(url)), url, getWireFormat());
        brokerConnector.start();

        // lets wait a little for the server to startup
        log.info("Embedded JMS Broker has started");
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException e) {
            System.out.println("Caught: " + e);
            e.printStackTrace();
        }
        return brokerConnector;
    }


    protected TransportChannel ensureServerIsAvailable(URI remoteLocation, TransportChannel channel, BrokerConnector brokerConnector, boolean created) throws JMSException {
        ensureVmServerIsAvailable(channel, brokerConnector);
        if (channel.isMulticast()) {
            return ensureMulticastChannelIsAvailable(remoteLocation, channel, brokerConnector, created);
        }
        return channel;
    }

    private void ensureVmServerIsAvailable(TransportChannel channel, BrokerConnector brokerConnector) throws JMSException {
        if (channel instanceof VmTransportChannel && brokerConnector instanceof TransportChannelListener) {
            VmTransportChannel answer = (VmTransportChannel) channel;
            answer.connect(brokerConnector);
        }
    }

    protected TransportChannel ensureMulticastChannelIsAvailable(URI remoteLocation, TransportChannel channel, BrokerConnector brokerConnector, boolean created) throws JMSException {
        if (created) {
            BrokerConnectorImpl brokerImpl = (BrokerConnectorImpl) brokerConnector;

            BrokerClientImpl client = new BrokerClientImpl();
            client.initialize(brokerImpl, channel);
            channel.start();
            String brokerClientID = createMulticastClientID();
            channel.setClientID(brokerClientID);

            // lets spoof a consumer for topics which will replicate messages
            // over the multicast transport
            ConnectionInfo info = new ConnectionInfo();
            info.setHostName(IdGenerator.getHostName());
            info.setClientId(brokerClientID);
            info.setStarted(true);
            client.consumeConnectionInfo(info);

            ConsumerInfo consumerInfo = new ConsumerInfo();
            consumerInfo.setDestination(new ActiveMQTopic(">"));
            consumerInfo.setNoLocal(true);
            consumerInfo.setClientId(brokerClientID);
            consumerInfo.setConsumerId(idGenerator.generateId());
            consumerInfo.setId(consumerInfo.getConsumerId());
            consumerInfo.setStarted(true);
            client.consumeConsumerInfo(consumerInfo);

            consumerInfo = new ConsumerInfo();
            consumerInfo.setDestination(new ActiveMQQueue(">"));
            consumerInfo.setNoLocal(true);
            consumerInfo.setClientId(brokerClientID);
            consumerInfo.setConsumerId(idGenerator.generateId());
            consumerInfo.setId(consumerInfo.getConsumerId());
            consumerInfo.setStarted(true);
            client.consumeConsumerInfo(consumerInfo);
        }

        // now lets create a VM channel that the JMS client will use
        // to connect to the embedded brokerConnector
        URI localURI = createURI("vm", remoteLocation);
        TransportChannel localChannel = TransportChannelProvider.create(getWireFormat(), localURI);
        ensureVmServerIsAvailable(localChannel, brokerConnector);
        return localChannel;
    }

    /**
     * Creates the clientID for the multicast client (used to dispatch local
     * messages over a multicast bus)
     */
    protected String createMulticastClientID() {
        return idGenerator.generateId();
    }

    protected URI createURI(String protocol, URI uri) throws JMSException {
        try {
            return new URI(protocol, uri.getRawSchemeSpecificPart(), uri.getFragment());
        }
        catch (URISyntaxException e) {
            JMSException jmsEx = new JMSException("the URL string is badly formated:", e.getMessage());
            jmsEx.setLinkedException(e);
            throw jmsEx;

        }
    }

    protected URI createURI(String uri) throws JMSException {
        try {
            if (uri == null) {
                throw new JMSException("The connection URI must be specified!");
            }
            return new URI(uri);
        }
        catch (URISyntaxException e) {
            e.printStackTrace();
            JMSException jmsEx = new JMSException("the URL string is badly formated:", e.getMessage());
            jmsEx.setLinkedException(e);
            throw jmsEx;

        }
    }

    /**
     * Called when a connection is closed so that we can shut down any embedded brokers cleanly
     *
     * @param connection
     */
    synchronized void onConnectionClose(ActiveMQConnection connection) throws JMSException {
        if (--connectionCount <= 0) {
            // close any broker if we've got one
            stop();
        }

    }

    synchronized void onConnectionCreate(ActiveMQConnection connection) {
        ++connectionCount;
    }
}
