/*
 *
 * 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.server;

import org.apache.qpid.framing.ProtocolVersionList;
import org.apache.qpid.pool.ReadWriteThreadModel;
import org.apache.qpid.server.protocol.AMQPFastProtocolHandler;
import org.apache.qpid.server.protocol.AMQPProtocolProvider;
import org.apache.qpid.server.registry.ApplicationRegistry;
import org.apache.qpid.server.registry.ConfigurationFileApplicationRegistry;
import org.apache.qpid.server.registry.IApplicationRegistry;
import org.apache.qpid.server.transport.ConnectorConfiguration;
import org.apache.qpid.server.configuration.VirtualHostConfiguration;
import org.apache.qpid.server.management.*;
import org.apache.qpid.server.queue.QueueRegistry;
import org.apache.qpid.server.queue.AMQQueue;
import org.apache.qpid.server.exchange.ExchangeRegistry;
import org.apache.qpid.server.exchange.ExchangeFactory;
import org.apache.qpid.server.exchange.Exchange;
import org.apache.qpid.server.store.MessageStore;
import org.apache.qpid.AMQException;
import org.apache.qpid.url.URLSyntaxException;
import org.apache.commons.cli.*;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import org.apache.mina.common.ByteBuffer;
import org.apache.mina.common.IoAcceptor;
import org.apache.mina.common.SimpleByteBufferAllocator;
import org.apache.mina.transport.socket.nio.SocketAcceptorConfig;
import org.apache.mina.transport.socket.nio.SocketSessionConfig;

import javax.management.JMException;
import javax.management.MBeanException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.management.MalformedObjectNameException;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.StringTokenizer;
import java.util.Collection;
import java.util.List;

/**
 * Main entry point for AMQPD.
 *
 */
public class Main implements ProtocolVersionList
{
    private static final Logger _logger = Logger.getLogger(Main.class);

    private static final String DEFAULT_CONFIG_FILE = "etc/config.xml";

    private static final String DEFAULT_LOG_CONFIG_FILENAME = "log4j.xml";

    protected static class InitException extends Exception
    {
        InitException(String msg)
        {
            super(msg);
        }
    }

    protected final Options options = new Options();
    protected CommandLine commandLine;

    protected Main(String[] args)
    {
        setOptions(options);
        if (parseCommandline(args))
        {
            execute();
        }
    }

    protected boolean parseCommandline(String[] args)
    {
        try
        {
            commandLine = new PosixParser().parse(options, args);
            return true;
        }
        catch (ParseException e)
        {
            System.err.println("Error: " + e.getMessage());
            HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp("Qpid", options, true);
            return false;
        }
    }

    protected void setOptions(Options options)
    {
        Option help = new Option("h", "help", false, "print this message");
        Option version = new Option("v", "version", false, "print the version information and exit");
        Option configFile = OptionBuilder.withArgName("file").hasArg().withDescription("use given configuration file").
                withLongOpt("config").create("c");
        Option port = OptionBuilder.withArgName("port").hasArg().withDescription("listen on the specified port. Overrides any value in the config file").
                withLongOpt("port").create("p");
        Option bind = OptionBuilder.withArgName("bind").hasArg().withDescription("bind to the specified address. Overrides any value in the config file").
                withLongOpt("bind").create("b");
        Option logconfig = OptionBuilder.withArgName("logconfig").hasArg().withDescription("use the specified log4j xml configuration file. By " +
                "default looks for a file named " + DEFAULT_LOG_CONFIG_FILENAME + " in the same directory as the configuration file").
                withLongOpt("logconfig").create("l");
        Option logwatchconfig = OptionBuilder.withArgName("logwatch").hasArg().withDescription("monitor the log file configuration file for changes. Units are seconds. " +
                "Zero means do not check for changes.").withLongOpt("logwatch").create("w");

        options.addOption(help);
        options.addOption(version);
        options.addOption(configFile);
        options.addOption(logconfig);
        options.addOption(logwatchconfig);
        options.addOption(port);
        options.addOption(bind);
    }

    protected void execute()
    {
        // note this understands either --help or -h. If an option only has a long name you can use that but if
        // an option has a short name and a long name you must use the short name here.
        if (commandLine.hasOption("h"))
        {
            HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp("Qpid", options, true);
        }
        else if (commandLine.hasOption("v"))
        {
            String ver = "Qpid 0.9.0.0";
            String protocol = "AMQP version(s) [major.minor]: ";
            for (int i=0; i<pv.length; i++)
            {
                if (i > 0)
                    protocol += ", ";
                protocol += pv[i][PROTOCOL_MAJOR] + "." + pv[i][PROTOCOL_MINOR];
            }
            System.out.println(ver + " (" + protocol + ")");
        }
        else
        {
            try
            {
                startup();
            }
            catch (InitException e)
            {
                System.out.println(e.getMessage());
            }
            catch (ConfigurationException e)
            {
                System.out.println("Error configuring message broker: " + e);
                e.printStackTrace();
            }
            catch (Exception e)
            {
                System.out.println("Error intialising message broker: " + e);
                e.printStackTrace();
            }
        }
    }


    protected void startup() throws InitException, ConfigurationException, Exception
    {
        final String QpidHome = System.getProperty("QPID_HOME");
        final File defaultConfigFile = new File(QpidHome, DEFAULT_CONFIG_FILE);
        final File configFile = new File(commandLine.getOptionValue("c", defaultConfigFile.getPath()));
        if (!configFile.exists())
        {
            String error = "File " + configFile + " could not be found. Check the file exists and is readable.";

            if (QpidHome == null)
            {
                error = error + "\nNote: Qpid_HOME is not set.";
            }

            throw new InitException(error);
        }
        else
        {
            System.out.println("Using configuration file " + configFile.getAbsolutePath());
        }

        String logConfig = commandLine.getOptionValue("l");
        String logWatchConfig = commandLine.getOptionValue("w", "0");
        if (logConfig != null)
        {
            File logConfigFile = new File(logConfig);
            configureLogging(logConfigFile, logWatchConfig);
        }
        else
        {
            File configFileDirectory = configFile.getParentFile();
            File logConfigFile = new File(configFileDirectory, DEFAULT_LOG_CONFIG_FILENAME);
            configureLogging(logConfigFile, logWatchConfig);
        }

        ApplicationRegistry.initialise(new ConfigurationFileApplicationRegistry(configFile));

        _logger.info("Starting Qpid.AMQP broker");

        ConnectorConfiguration connectorConfig = ApplicationRegistry.getInstance().
                getConfiguredObject(ConnectorConfiguration.class);

        ByteBuffer.setUseDirectBuffers(connectorConfig.enableDirectBuffers);

        // the MINA default is currently to use the pooled allocator although this may change in future
        // once more testing of the performance of the simple allocator has been done
        if (!connectorConfig.enablePooledAllocator)
        {
            ByteBuffer.setAllocator(new SimpleByteBufferAllocator());
        }

        int port = connectorConfig.port;

        String portStr = commandLine.getOptionValue("p");
        if (portStr != null)
        {
            try
            {
                port = Integer.parseInt(portStr);
            }
            catch (NumberFormatException e)
            {
                throw new InitException("Invalid port: " + portStr);
            }
        }

        String VIRTUAL_HOSTS = "virtualhosts";

        Object virtualHosts = ApplicationRegistry.getInstance().getConfiguration().getProperty(VIRTUAL_HOSTS);

        if (virtualHosts != null)
        {
            if (virtualHosts instanceof Collection)
            {
                int totalVHosts = ((Collection) virtualHosts).size();
                for (int vhost = 0; vhost < totalVHosts; vhost++)
                {
                    setupVirtualHosts(configFile.getParent() , (String)((List)virtualHosts).get(vhost));
                }
            }
            else
            {
               setupVirtualHosts(configFile.getParent() , (String)virtualHosts);
            }
        }
        bind(port, connectorConfig);

        createAndRegisterBrokerMBean();
    }

    protected void setupVirtualHosts(String configFileParent, String configFilePath) throws ConfigurationException, AMQException, URLSyntaxException
    {
        String configVar = "${conf}";

        if (configFilePath.startsWith(configVar))
        {
            configFilePath = configFileParent + configFilePath.substring(configVar.length());
        }

        if (configFilePath.indexOf(".xml") != -1 )
        {
            VirtualHostConfiguration vHostConfig = new VirtualHostConfiguration(configFilePath);
            vHostConfig.performBindings();
        }
        else
        {
            // the virtualhosts value is a path. Search it for XML files.

            File virtualHostDir = new File(configFilePath);

            String[] fileNames = virtualHostDir.list();

            for (int each=0; each < fileNames.length; each++)
            {
                if (fileNames[each].endsWith(".xml"))
                {
                    VirtualHostConfiguration vHostConfig = new VirtualHostConfiguration(configFilePath+"/"+fileNames[each]);
                    vHostConfig.performBindings();
                }
            }
        }
    }

    protected void bind(int port, ConnectorConfiguration connectorConfig)
    {
        String bindAddr = commandLine.getOptionValue("b");
        if (bindAddr == null)
        {
            bindAddr = connectorConfig.bindAddress;
        }

        try
        {
            //IoAcceptor acceptor = new SocketAcceptor(connectorConfig.processors);
            IoAcceptor acceptor = connectorConfig.createAcceptor();
            SocketAcceptorConfig sconfig = (SocketAcceptorConfig) acceptor.getDefaultConfig();
            SocketSessionConfig sc = (SocketSessionConfig) sconfig.getSessionConfig();

            sc.setReceiveBufferSize(connectorConfig.socketReceiveBufferSize);
            sc.setSendBufferSize(connectorConfig.socketWriteBuferSize);
            sc.setTcpNoDelay(connectorConfig.tcpNoDelay);

            // if we do not use the executor pool threading model we get the default leader follower
            // implementation provided by MINA
            if (connectorConfig.enableExecutorPool)
            {
                sconfig.setThreadModel(new ReadWriteThreadModel());
            }

            if (connectorConfig.enableNonSSL)
            {
                AMQPFastProtocolHandler handler = new AMQPProtocolProvider().getHandler();
                InetSocketAddress bindAddress;
                if (bindAddr.equals("wildcard"))
                {
                    bindAddress = new InetSocketAddress(port);
                }
                else
                {
                    bindAddress = new InetSocketAddress(InetAddress.getByAddress(parseIP(bindAddr)), port);
                }
                acceptor.bind(bindAddress, handler, sconfig);
                _logger.info("Qpid.AMQP listening on non-SSL address " + bindAddress);
            }

            if (connectorConfig.enableSSL)
            {
                AMQPFastProtocolHandler handler = new AMQPProtocolProvider().getHandler();
                handler.setUseSSL(true);
                try
                {
                    acceptor.bind(new InetSocketAddress(connectorConfig.sslPort),
                                  handler, sconfig);
                    _logger.info("Qpid.AMQP listening on SSL port " + connectorConfig.sslPort);
                }
                catch (IOException e)
                {
                    _logger.error("Unable to listen on SSL port: " + e, e);
                }
            }
        }
        catch (Exception e)
        {
            _logger.error("Unable to bind service to registry: " + e, e);
        }
    }

    public static void main(String[] args)
    {

        new Main(args);
    }

    private byte[] parseIP(String address) throws Exception
    {
        StringTokenizer tokenizer = new StringTokenizer(address, ".");
        byte[] ip = new byte[4];
        int index = 0;
        while (tokenizer.hasMoreTokens())
        {
            String token = tokenizer.nextToken();
            try
            {
                ip[index++] = Byte.parseByte(token);
            }
            catch (NumberFormatException e)
            {
                throw new Exception("Error parsing IP address: " + address, e);
            }
        }
        if (index != 4)
        {
            throw new Exception("Invalid IP address: " + address);
        }
        return ip;
    }

    private void configureLogging(File logConfigFile, String logWatchConfig)
    {
        int logWatchTime = 0;
        try
        {
            logWatchTime = Integer.parseInt(logWatchConfig);
        }
        catch (NumberFormatException e)
        {
            System.err.println("Log watch configuration value of " + logWatchConfig + " is invalid. Must be " +
                    "a non-negative integer. Using default of zero (no watching configured");
        }
        if (logConfigFile.exists() && logConfigFile.canRead())
        {
            System.out.println("Configuring logger using configuration file " + logConfigFile.getAbsolutePath());
            if (logWatchTime > 0)
            {
                System.out.println("log file " + logConfigFile.getAbsolutePath() + " will be checked for changes every " +
                        logWatchTime + " seconds");
                // log4j expects the watch interval in milliseconds
                DOMConfigurator.configureAndWatch(logConfigFile.getAbsolutePath(), logWatchTime * 1000);
            }
            else
            {
                DOMConfigurator.configure(logConfigFile.getAbsolutePath());
            }
        }
        else
        {
            System.err.println("Logging configuration error: unable to read file " + logConfigFile.getAbsolutePath());
            System.err.println("Using basic log4j configuration");
            BasicConfigurator.configure();
        }
    }

    private void createAndRegisterBrokerMBean() throws AMQException
    {
        try
        {
            new AMQBrokerManager().register();
        }
        catch (NotCompliantMBeanException ex)
        {
            throw new AMQException("Exception occured in creating AMQBrokerManager MBean.");    
        }
    }

    /**
     * AMQPBrokerMBean implements the broker management interface and exposes the
     * Broker level management features like creating and deleting exchanges and queue.
     */
    @MBeanDescription("This MBean exposes the broker level management features")
    private final class AMQBrokerManager extends AMQManagedObject
                                         implements ManagedBroker
    {
        private final QueueRegistry    _queueRegistry;
        private final ExchangeRegistry _exchangeRegistry;
        private final ExchangeFactory  _exchangeFactory;
        private final MessageStore     _messageStore;

        @MBeanConstructor("Creates the Broker Manager MBean")
        protected AMQBrokerManager()  throws NotCompliantMBeanException
        {
            super(ManagedBroker.class, ManagedBroker.TYPE);

            IApplicationRegistry appRegistry = ApplicationRegistry.getInstance();
            _queueRegistry    = appRegistry.getQueueRegistry();
            _exchangeRegistry = appRegistry.getExchangeRegistry();
            _exchangeFactory  = ApplicationRegistry.getInstance().getExchangeFactory();
            _messageStore     = ApplicationRegistry.getInstance().getMessageStore();
       }

        public String getObjectInstanceName()
        {
            return this.getClass().getName();
        }

        /**
         * Creates new exchange and registers it with the registry.
         * @param exchangeName
         * @param type
         * @param durable
         * @param autoDelete
         * @throws JMException
         */
        public void createNewExchange(String exchangeName,
                                      String type,
                                      boolean durable,
                                      boolean autoDelete)
            throws JMException
        {
            try
            {
                synchronized(_exchangeRegistry)
                {
                    Exchange exchange = _exchangeRegistry.getExchange(exchangeName);

                    if (exchange == null)
                    {
                        exchange = _exchangeFactory.createExchange(exchangeName,
                                                               type,        //eg direct
                                                               durable,
                                                               autoDelete,
                                                               0);         //ticket no
                        _exchangeRegistry.registerExchange(exchange);
                    }
                    else
                    {
                        throw new JMException("The exchange \"" + exchangeName + "\" already exists.");
                    }
                }
            }
            catch(AMQException ex)
            {
                _logger.error("Error in creating exchange " + exchangeName, ex);
                throw new MBeanException(ex, ex.toString());
            }
        }

        /**
         * Unregisters the exchange from registry.
         * @param exchangeName
         * @throws JMException
         */
        public void unregisterExchange(String exchangeName)
            throws JMException
        {
            boolean inUse = false;
            // TODO
            // Check if the exchange is in use.
            // Check if there are queue-bindings with the exchnage and unregister
            // when there are no bindings.
            try
            {
                _exchangeRegistry.unregisterExchange(exchangeName, false);
            }
            catch(AMQException ex)
            {
                _logger.error("Error in unregistering exchange " + exchangeName, ex);
                throw new MBeanException(ex, ex.toString());
            }
        }

        /**
         * Creates a new queue and registers it with the registry and puts it
         * in persistance storage if durable queue.
         * @param queueName
         * @param durable
         * @param owner
         * @param autoDelete
         * @throws JMException
         */
        public void createQueue(String queueName,
                                boolean durable,
                                String owner,
                                boolean autoDelete)
            throws JMException
        {
            AMQQueue queue = _queueRegistry.getQueue(queueName);
            if (queue == null)
            {
                try
                {
                    queue = new AMQQueue(queueName, durable, owner, autoDelete, _queueRegistry);
                    if (queue.isDurable() && !queue.isAutoDelete())
                    {
                        _messageStore.createQueue(queue);
                    }
                    _queueRegistry.registerQueue(queue);
                }
                catch (AMQException ex)
                {
                    _logger.error("Error in creating queue " + queueName, ex);
                    throw new MBeanException(ex, ex.toString());
                }
            }
            else
            {
                throw new JMException("The queue \"" + queueName + "\" already exists.");
            }
        }

        /**
         * Deletes the queue from queue registry and persistant storage.
         * @param queueName
         * @throws JMException
         */
        public void deleteQueue(String queueName) throws JMException
        {
            AMQQueue queue = _queueRegistry.getQueue(queueName);
            if (queue == null)
            {
                throw new JMException("The Queue " + queueName + " is not a registerd queue.");
            }

            try
            {
                queue.delete();
                _messageStore.removeQueue(queueName);

            }
            catch (AMQException ex)
            {
                throw new MBeanException(ex, ex.toString());
            }
        }

        public ObjectName getObjectName() throws MalformedObjectNameException
        {
            StringBuffer objectName = new StringBuffer(ManagedObject.DOMAIN);
            objectName.append(":type=").append(getType());

            return new ObjectName(objectName.toString());
        }
    } // End of MBean class
}
