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

// Misc Imports
import java.util.Collections;
import java.util.Map;

// Simple Logging Facade 4 Java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// QMF2 Imports
import org.apache.qpid.qmf2.agent.Agent;
import org.apache.qpid.qmf2.agent.QmfAgentData;
import org.apache.qpid.qmf2.common.Handle;
import org.apache.qpid.qmf2.common.ObjectId;
import org.apache.qpid.qmf2.common.QmfData;
/*import org.apache.qpid.qmf2.common.QmfEvent;
import org.apache.qpid.qmf2.common.QmfEventListener;
import org.apache.qpid.qmf2.common.QmfException;
import org.apache.qpid.qmf2.common.QmfType;*/
import org.apache.qpid.qmf2.common.SchemaEventClass;
import org.apache.qpid.qmf2.common.SchemaMethod;
import org.apache.qpid.qmf2.common.SchemaObjectClass;
import org.apache.qpid.qmf2.common.SchemaProperty;

// Java Broker model Imports
import org.apache.qpid.server.model.Binding;
import org.apache.qpid.server.model.Exchange;
import org.apache.qpid.server.model.LifetimePolicy;
import org.apache.qpid.server.model.Port;
import org.apache.qpid.server.model.Protocol;
import org.apache.qpid.server.model.Queue;
import org.apache.qpid.server.model.State;
import org.apache.qpid.server.model.VirtualHost;

/**
 * This class provides a concrete implementation of QmfAgentData for the Broker Management Object.
 * In general it's possible to use QmfAgentData without sub-classing as it's really a "bean" style class
 * that retains its properties in a Map, but in the case of the Java Broker Management Agent it's useful
 * to sub-class as we need to map between the properties/statistics as specified in the Java Broker
 * management model and those specified in qpid/spec/management-schema.xml which is what the C++ broker 
 * uses. This class retains a reference to its peer org.apache.qpid.server.model.Broker and does the
 * necessary mapping when its mapEncode() method is called (which is used to serialise the QmfAgentData).
 *
 * @author Fraser Adams
 */
public class Broker extends QmfAgentData
{
    private static final Logger _log = LoggerFactory.getLogger(Broker.class);

    /**
     * This static initialiser block initialises the QMF2 Schema information needed by the Agent to find
     * QmfAgentData Objects of a given type.
     */
    private static final SchemaObjectClass _schema;
    public static SchemaObjectClass getSchema()
    {
        return _schema;
    }

    static
    {
        // Declare the schema for the QMF2 broker class.
        _schema = new SchemaObjectClass("org.apache.qpid.broker", "broker");

        // TODO
        //_schema.addProperty(new SchemaProperty("whatHappened", QmfType.TYPE_STRING));
    }

    private final org.apache.qpid.server.model.Broker _broker; // Passed in by Plugin bootstrapping.
    private final String _defaultVirtualHost; // Pulled from the broker attributes.

    /**
     * This inner class parses the name String that was passed in as a QMF method argument.
     * There are a few quirks with this name. In the first instance it may be prefixed with Virtual Host information
     * e.g. [vhost:<vhost-name>/]<queue-name> this is because in order to allow Java Broker QMF to work with things like
     * qpid-config which are not particularly Virtual Host aware prefixing the names seemed to be the most natural,
     * if slightly ugly approach. In addition the way bindings are named is of the form:
     * <exchange-name>/<queue-name>[/<binding-key>] so we need a mechanism to parse the relevant information.
     * <p>
     * N.B. the parsing that takes place in this class makes an assumption that there are no valid exchange or queue
     * names that contain a "/". This is probably a reasonable assumption given the way that a binding name is
     * constructed, but it's worth recording the restriction here in case such a beast crops up.
     * <p>
     * This class also provides accessors that allow the Exchange, Queue and Binding ConfiguredObjects for the
     * name parsed in the Constructor to be retrieved. This is generally useful because although in QMF the create
     * and delete methods are invoked on the Broker object in the Java Broker ConfiguredObject model the underlying
     * methods are distributed across a number of different classes.
     */
    private class NameParser
    {
        private String _vhostName = _defaultVirtualHost;
        private VirtualHost _vhost = null;
        private String _exchangeName = "";
        private Exchange _exchange = null;
        private String _queueName = "";
        private Queue _queue = null;
        private String _bindingKey = "";
        private Binding _binding = null;

        /**
         * NameParser Constructor.
         * The Constructor actually does the majority of the parsing, the remaining method are largely just accessors.
         *
         * @param name the name argument that was retrieved from the QMF method inArgs. This will be the exchange name,
         * the queue name or the binding name (which is of the form <exchange-name>/<queue-name>[/<binding-key>])
         * exchange and queue names may be prefixed by a Virtual Host name e.g. [vhost:<vhost-name>/]<queue-name>
         * @param type the type argument that was retrieved from the QMF method inArgs. Valid types are "exchange, 
         * "queue" or "binding".
         */
        public NameParser(final String name, final String type)
        {
            boolean malformedVHostName = false;
            String[] splitName = name.split("/"); // A slash is used as a separator in a couple of scenarios.
            if (name.startsWith("vhost:"))
            {
                if (splitName.length == 1) // If it starts with vhost: the name should also contain at least one "/".
                {
                    malformedVHostName = true;
                    _vhostName = name;
                }
                else
                {
                    _vhostName = splitName[0];
                    _vhostName = _vhostName.substring(6, _vhostName.length());
                }
            }

            // If the vhostName isn't malformed then try to find the actual Virtual Host that it relates to.
            // If it is malformed the vhost stays set to null, which will cause an exception to be returned later.
            if (!malformedVHostName)
            {
                for (VirtualHost vhost : _broker.getVirtualHosts())
                {
                    if (vhost.getName().equals(_vhostName))
                    {
                        _vhost = vhost;
                        break;
                    }
                }
            }

            // Populate the exchange, queue and binding names. We only populate the names in the constructor
            // when we actually want to find the Object associated with the name we do it "on demand" in the
            // relevant accessor and cache the result.
            if (type.equals("exchange"))
            {
                _exchangeName = splitName[splitName.length - 1];
            }
            else if (type.equals("queue"))
            {
                _queueName = splitName[splitName.length - 1];
            }
            else if (type.equals("binding"))
            { // TODO is there a way to make this parse less nasty and a bit more elegant....
                int i = 0;
                String vhost1Name = _defaultVirtualHost; // The exchange and queue vhostName need to be the same.
                if (splitName[i].startsWith("vhost:")) // Does the exchange name specify a vhost?
                {
                    vhost1Name = splitName[i];
                    i++;
                }

                if (i < splitName.length) // Extract the exchange name sans vhost part.
                {
                    _exchangeName = splitName[i];
                    i++;
                }

                String vhost2Name = _defaultVirtualHost;
                if (i < splitName.length && splitName[i].startsWith("vhost:")) // Does the queue name specify a vhost?
                {
                    vhost2Name = splitName[i];
                    i++;
                }

                // If the exchange and queue vhost names differ set _vhost and _vhostName to null which causes
                // an exception that says "VirtualHost names for exchange and queue must match.".
                if (!vhost2Name.equals(vhost1Name))
                {
                    _vhost = null;
                    _vhostName = null;
                }

                if (i < splitName.length) // Extract the queue name sans vhost part.
                {
                    _queueName = splitName[i];
                    i++;
                }

                if (i < splitName.length) // Extract the binding key if present (it's optional).
                {
                    _bindingKey = splitName[i];
                    i++;
                }
            } // End of binding name parse.
        }

        // NameParser accessors.

        /**
         * Retrieves the name of the Virtual Host that was parsed from the name supplied in the Constructor.
         * @return the parsed Virtual Host name (may be an empty String).
         */
        public String getVirtualHostName()
        {
            return _vhostName;
        }

        /**
         * Retrieves the Virtual Host with the name that was parsed from the name supplied in the Constructor.
         * @return the Virtual Host with the name that was parsed from the name supplied in the Constructor (may be null).
         */
        public VirtualHost getVirtualHost()
        {
            return _vhost;
        }

        /**
         * Retrieves the name of the Exchange that was parsed from the name supplied in the Constructor.
         * @return the parsed Exchange name (may be an empty String).
         */
        public String getExchangeName()
        {
            return _exchangeName;
        }

        /**
         * Retrieves the Exchange with the name that was parsed from the name supplied in the Constructor.
         * @return the Exchange with the name that was parsed from the name supplied in the Constructor (may be null).
         */
        public Exchange getExchange()
        {
            // If we've not previously cached the _exchange and the previously parsed Virtual Host isn't null we do a 
            // look up for the actual Exchange with the name _exchangeName and cache it.
            if (_exchange == null && _vhost != null)
            {
                for (Exchange exchange : _vhost.getExchanges())
                {
                    if (exchange.getName().equals(_exchangeName))
                    {
                        _exchange = exchange;
                        break;
                    }
                }
            }

            return _exchange;
        }

        /**
         * Retrieves the name of the Queue that was parsed from the name supplied in the Constructor.
         * @return the parsed Queue name (may be an empty String).
         */
        public String getQueueName()
        {
            return _queueName;
        }

        /**
         * Retrieves the Queue with the name that was parsed from the name supplied in the Constructor.
         * @return the Queue with the name that was parsed from the name supplied in the Constructor (may be null).
         */
        public Queue getQueue()
        {
            // If we've not previously cached the _queue and the previously parsed Virtual Host isn't null we do a 
            // look up for the actual Queue with the name _queueName and cache it.
            if (_queue == null && _vhost != null)
            {
                for (Queue queue : _vhost.getQueues())
                {
                    if (queue.getName().equals(_queueName))
                    {
                        _queue = queue;
                        break;
                    }
                }
            }

            return _queue;
        }

        /**
         * Retrieves the name of the Binding that was parsed from the name supplied in the Constructor.
         * @return the parsed Binding name (may be an empty String).
         */
        public String getBindingKey()
        {
            return _bindingKey;
        }

        /**
         * Retrieves the Binding with the name that was parsed from the name supplied in the Constructor.
         * @return the Binding with the name that was parsed from the name supplied in the Constructor (may be null).
         */
        public Binding getBinding()
        {
            // In order to retrieve a Binding it's first necessary to get the Exchange (or Queue) ConfiguredObject.
            _exchange = getExchange(); // Need to get it via the accessor as it's initialised by lazy evaluation.

            // If we've not previously cached the _binding and the previously retrieved Exchange isn't null we do a 
            // look up for the actual Binding with the name _bindingKey and cache it.
            if (_binding == null && _exchange != null)
            {
                for (Binding binding : _exchange.getBindings())
                {
                    if (binding.getName().equals(_bindingKey))
                    {
                        _binding = binding;
                        break;
                    }
                }
            }

            return _binding;
        }
    } // End of class NameParser

    /**
     * Broker Constructor.
     * @param broker the root Broker Management Object from which the other Management Objects may be obtained.
     */
    public Broker(final org.apache.qpid.server.model.Broker broker)
    {
        super(getSchema());
        _broker = broker;
        _defaultVirtualHost = (String)broker.getAttribute("defaultVirtualHost");
        int amqpPort = 5672; // Default AMQP Port.

        // Search through the available Ports on this Broker looking for the AMQP Port. When we find the
        // AMQP Port we record that in amqpPort;
        for (Port port : _broker.getPorts())
        {
            boolean isAMQP = false;
            for (Protocol protocol : port.getProtocols())
            {
                isAMQP = protocol.isAMQP();
                if (isAMQP)
                {
                    break;
                }
            }

            if (isAMQP)
            {
                amqpPort = port.getPort();
                break;
            }
        }

        String port = "" + amqpPort;

        // systemRef is ignored in this implementation.
        // stagingThreshold is ignored in this implementation (it's deprecated anyway I believe).

        // Use this name to be fairly consistent with C++ broker which uses "amqp-broker".
        // N.B. although it's useful to be able to distinguish between C++ and Java brokers note that the
        // _object_name in the ObjectId that we set below uses actually uses "amqp-broker" vice "amqp-java-broker",
        // this is because qpid-config uses a "hardcoded" ObjectId to invoke methods so we need to use the same name.
        setValue("name", "amqp-java-broker");
        setValue("port", port);

        // workerThreads doesn't *appear* to be configurable in the Java Broker, looks like there's no pool and the
        // Threads just grow with the number of Connections?
        setValue("workerThreads", 0);

        // maxConns doesn't *appear* to be configurable in the Java Broker.
        setValue("maxConns", 0);

        // The Java Broker ServerSocket seems to be created in org.apache.qpid.transport.network.io.IoNetworkTransport
        // In AcceptingThread. The does't appear to use any configuration for ServerSocket, which suggests that the
        // backlog is the default value which is assumed to be 10.
        setValue("connBacklog", 10);

        // "Technically" this isn't quite the same as for the C++ broker, which pushes management data to a particular 
        // topic - the subscription "qmf.default.topic/agent.ind.#" grabs that plus heartbeats for the C++ broker.
        // This Agent allows the use of the QMF2 Query Subscriptions (which the C++ broker does not!! - the Console
        // class fakes this client side for the C++ broker. TODO make sure that the Console does not fake for Java broker.
        setValue("mgmtPublish", true);
        setValue("mgmtPubInterval", 10);

        setValue("version", org.apache.qpid.common.QpidProperties.getReleaseVersion());
        setValue("dataDir", System.getProperty("QPID_WORK"));

        // ObjectId needs to be set here in Broker because the QMF2 version of qpid-config uses a hardcoded
        // _object_name of "org.apache.qpid.broker:broker:amqp-broker" in the _object_id that it sets.
        // It *shouldn't* do this and should really use the _object_id of the broker object returned by
        // getObjects("broker"), but it does. The following line causes the Agent to use the explicit
        // ObjectId below rather than constructing its own, which fixes the qpid-config issue.
        // Note we use "amqp-broker" in the ObjectId to be compatible with qpid-config but we set the actual
        // name to "amqp-java-broker" as it's useful to be able to distinguish between C++ and Java Brokers.
        setObjectId(new ObjectId("", "org.apache.qpid.broker:broker:amqp-broker", 0));
    }

    /**
     * This helper method checks the supplied properties Map for the "alternate-exchange" property, if it is present
     * the property is removed from the map and the alternate exchange is parsed to recover the Virtual Host name
     * and the actual alternate exchange name. If the alternate exchange Virtual Host name is not the same as the
     * supplied vhostName this method returns "invalid" otherwise it returns the alternate exchange name or null.
     *
     * @param vhostName the Virtual Host name that we want to compare the alternate exchange's Virtual Host name with.
     * @param properties a Map of properties that might contain "alternate-exchange".
     * @return the alternate exchange name if present, null if not present or "invalid" if the Virtual Host name that
     * was parsed from the alternate exchange doesn't match the name supplied in the vhostName parameter.
     */
    private String parseAlternateExchange(String vhostName, Map<String, Object> properties)
    {
        String alternateExchange = null;
        Object property = properties.get("alternate-exchange");
        if (property != null && property instanceof String) // Alternate exchange has been specified.
        {
            alternateExchange = property.toString();
            properties.remove("alternate-exchange");

            String altExVhostName = _defaultVirtualHost;
            String[] splitName = alternateExchange.split("/"); 
            if (alternateExchange.startsWith("vhost:"))
            {
                altExVhostName = splitName[0];
                altExVhostName = altExVhostName.substring(6, altExVhostName.length());
            }

            // If the Virtual Hosts differ raise an exception and return.
            if (!altExVhostName.equals(vhostName))
            {
                return "invalid";
            }
        }

        return alternateExchange;
    }

    /**
     * This method acts as a single entry point for QMF methods invoked on the Broker Object.
     *
     * @param agent the org.apache.qpid.qmf2.agent.Agent instance that we call methodResponse() and raiseException() on.
     * @param handle the reply handle used by methodResponse() and raiseException().
     * @param methodName the name of the QMF method being invoked.
     * @param inArgs a Map of input arguments wrapped in a QmfData Object.
     */
    public void invokeMethod(Agent agent, Handle handle, String methodName, QmfData inArgs)
    {
        if (methodName.equals("create") || methodName.equals("delete"))
        {
            QmfData outArgs = new QmfData();

            String name = inArgs.getStringValue("name");
            String type = inArgs.getStringValue("type");

            NameParser nameParser = new NameParser(name, type);
            String vhostName = nameParser.getVirtualHostName();
            VirtualHost vhost = nameParser.getVirtualHost();

            if (vhost == null)
            {
                if (vhostName == null)
                {
                    agent.raiseException(handle, "VirtualHost names for exchange and queue must match.");
                }
                else
                {
                    agent.raiseException(handle, "VirtualHost " + vhostName + " not found.");
                }
            }
            else
            {
                if (methodName.equals("create")) // method = create
                {
                    try
                    {
                        //boolean strict = inArgs.getBooleanValue("strict");
                        Map<String, Object> properties = inArgs.getValue("properties");

                        boolean durable = false;
                        Object property = properties.get("durable");
                        if (property != null && property instanceof Boolean)
                        {
                            Boolean durableProperty = (Boolean)property;
                            durable = durableProperty.booleanValue();
                            properties.remove("durable");
                        }

                        if (type.equals("exchange")) // create exchange.
                        {
/*
System.out.println("Create Exchange");
System.out.println("vhostName = " + vhostName);
System.out.println("exchange name = " + nameParser.getExchangeName());
System.out.println("properties = " + properties);
*/
                            String exchangeType = "";
                            property = properties.get("exchange-type");
                            if (property != null && property instanceof String)
                            {
                                exchangeType = property.toString();
                                properties.remove("exchange-type");
                            }

                            String alternateExchange = parseAlternateExchange(vhostName, properties);
                            if (alternateExchange != null && alternateExchange.equals("invalid"))
                            {
                                agent.raiseException(handle, "Alternate Exchange must belong to the same Virtual Host as the Exchange being added.");
                                return;
                            }

                            // TODO delete this block when adding an AlternateExchange is implemented.
                            if (alternateExchange != null)
                            {
                                agent.raiseException(handle,
                                    "Setting an Alternate Exchange on an Exchange is not yet implemented.");
                                return;
                            }

                            // Note that for Qpid 0.20 the "qpid.msg_sequence=1" and "qpid.ive=1" properties are
                            // not suppored, indeed no exchange properties seem to be supported yet.
                            vhost.createExchange(nameParser.getExchangeName(), State.ACTIVE, durable,
                                                 LifetimePolicy.PERMANENT, 0l, exchangeType, properties);
                            if (alternateExchange != null)
                            {
                                // TODO set Alternate Exchange. There doesn't seem to be a way to do this yet!!!
                            }
                        } // End of create exchange.
                        else if (type.equals("queue")) // create queue.
                        {
/*
System.out.println("Create Queue");
System.out.println("vhostName = " + vhostName);
System.out.println("queue name = " + nameParser.getQueueName());
System.out.println("properties = " + properties);
*/

                            // TODO Try to map from the QMF create queue properties to the closest equivalents on
                            // the Java Broker. Unfortunately there are a *lot* of frustrating little differences.


                            String alternateExchange = parseAlternateExchange(vhostName, properties);
                            if (alternateExchange != null && alternateExchange.equals("invalid"))
                            {
                                agent.raiseException(handle, "Alternate Exchange must belong to the same Virtual Host as the Queue being added.");
                                return;
                            }

                            // I don't *think* that it make sense to allow setting exclusive or autoDelete to
                            // a queue created from config.
                            Queue queue = vhost.createQueue(nameParser.getQueueName(), State.ACTIVE, durable, false,
                                                            LifetimePolicy.PERMANENT, 0l, properties);

                            // Set the queue's alternateExchange, which is just a little bit involved......
                            // The queue.setAttribute() method needs an org.apache.qpid.server.model.Exchange instance
                            // not just a name, so we look up org.apache.qpid.server.qmf2.agentdata.Exchange by ID
                            // and get its associated org.apache.qpid.server.model.Exchange. We can do a look up by ID
                            // because we needed to use ObjectIds that were based on names in order to allow qpid-config
                            // to work, so we may as well make use of this convenience here too.
                            if (alternateExchange != null)
                            {
                                ObjectId objectId =
                                        new ObjectId("", "org.apache.qpid.broker:exchange:" + alternateExchange, 0);

                                // Look up Exchange QmfAgentData by ObjectId from the Agent's internal Object store.
                                QmfAgentData object = agent.getObject(objectId);
                                if (object != null)
                                {
                                    org.apache.qpid.server.qmf2.agentdata.Exchange ex = 
                                        (org.apache.qpid.server.qmf2.agentdata.Exchange)object;

                                    Exchange altEx = ex.getExchange();
                                    queue.setAttribute("alternateExchange", null, altEx);
                                }
                            }
                        }
                        else if (type.equals("binding")) // create binding.
                        {
                            Exchange exchange = nameParser.getExchange();
                            if (exchange == null)
                            {
                                agent.raiseException(handle, "Cannot create binding on Exchange " +
                                                     nameParser.getExchangeName());
                                return;
                            }
                            else
                            {
                                Map<String, Object> attributes = Collections.emptyMap();
                                exchange.createBinding(nameParser.getBindingKey(), nameParser.getQueue(),
                                                       properties, attributes);
                            }
                        }

                        agent.methodResponse(methodName, handle, outArgs, null);
                    }
                    catch (Exception e)
                    {
                        agent.raiseException(handle, e.getMessage());
                    }
                }
                else // method = delete
                {
                    try
                    {
                        if (type.equals("exchange")) // delete exchange.
                        {
                            Exchange exchange = nameParser.getExchange();
                            if (exchange != null)
                            {
                                exchange.delete();
                            }
                        }
                        else if (type.equals("queue")) // delete queue.
                        {
                            Queue queue = nameParser.getQueue();
                            if (queue != null)
                            {
                                queue.delete();
                            }
                        }
                        else if (type.equals("binding")) // delete binding.
                        {
                            Binding binding = nameParser.getBinding();
                            if (binding != null)
                            {
                                binding.delete();
                            }
                        }

                        agent.methodResponse(methodName, handle, outArgs, null);
                    }
                    catch (Exception e)
                    {
                        agent.raiseException(handle, e.getMessage());
                    }
                }
            }
        }
        else // If methodName is not create or delete.
        {
            agent.raiseException(handle, methodName + " not yet implemented on Broker.");
        }
    } // End of invokeMethod.

    /**
     * This method maps the org.apache.qpid.server.model.Broker to QMF2 broker properties where possible then
     * serialises into the underlying Map for transmission via AMQP. This method is called by handleQueryRequest()
     * in the org.apache.qpid.qmf2.agent.Agent class implementing the main QMF2 Agent behaviour.
     * 
     * @return the underlying map. 
     */
    @Override
    public Map<String, Object> mapEncode()
    {
        update(); // Need to do update before setting uptime in order to get the latest getUpdateTime() value.

        // Not sure if there's an "official" broker uptime anywhere, but as the QmfManagementAgent is created when
        // the broker is and the Broker object is created then too the following approach should be good enough.
        setValue("uptime", getUpdateTime() - getCreateTime());

        return super.mapEncode();
    }
}
