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

import org.apache.log4j.Logger;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoSession;
import org.apache.mina.transport.vmpipe.VmPipeAddress;
import org.apache.qpid.AMQChannelException;
import org.apache.qpid.AMQException;
import org.apache.qpid.codec.AMQCodecFactory;
import org.apache.qpid.codec.AMQDecoder;
import org.apache.qpid.framing.AMQDataBlock;
import org.apache.qpid.framing.AMQFrame;
import org.apache.qpid.framing.AMQMethodBody;
import org.apache.qpid.framing.ConnectionStartBody;
import org.apache.qpid.framing.ContentBody;
import org.apache.qpid.framing.ContentHeaderBody;
import org.apache.qpid.framing.HeartbeatBody;
import org.apache.qpid.framing.ProtocolInitiation;
import org.apache.qpid.framing.ProtocolVersionList;
import org.apache.qpid.server.AMQChannel;
import org.apache.qpid.server.RequiredDeliveryException;
import org.apache.qpid.server.exchange.ExchangeRegistry;
import org.apache.qpid.server.management.AMQManagedObject;
import org.apache.qpid.server.management.MBeanConstructor;
import org.apache.qpid.server.management.MBeanDescription;
import org.apache.qpid.server.management.Managable;
import org.apache.qpid.server.management.ManagedObject;
import org.apache.qpid.server.queue.QueueRegistry;
import org.apache.qpid.server.registry.ApplicationRegistry;
import org.apache.qpid.server.state.AMQStateManager;

import javax.management.JMException;
import javax.management.MBeanException;
import javax.management.MBeanNotificationInfo;
import javax.management.NotCompliantMBeanException;
import javax.management.Notification;
import javax.management.monitor.MonitorNotification;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;
import javax.security.sasl.SaslServer;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

public class AMQMinaProtocolSession implements AMQProtocolSession,
                                               ProtocolVersionList,
                                               Managable
{
    private static final Logger _logger = Logger.getLogger(AMQProtocolSession.class);

    private final IoSession _minaProtocolSession;

    private String _contextKey;

    private final Map<Integer, AMQChannel> _channelMap = new HashMap<Integer, AMQChannel>();

    private final CopyOnWriteArraySet<AMQMethodListener> _frameListeners = new CopyOnWriteArraySet<AMQMethodListener>();

    private final AMQStateManager _stateManager;

    private final QueueRegistry _queueRegistry;

    private final ExchangeRegistry _exchangeRegistry;

    private AMQCodecFactory _codecFactory;

    private ManagedAMQProtocolSession _managedObject;

    private SaslServer _saslServer;

    private Object _lastReceived;

    private Object _lastSent;

    private boolean _closed;

    private long _maxNoOfChannels = 1000;
    
    /* AMQP Version for this session */
    
    private byte _major;
    private byte _minor;

    public ManagedObject getManagedObject()
    {
        return _managedObject;
    }

    /**
     * This class implements the management interface (is an MBean). In order to
     * make more attributes, operations and notifications available over JMX simply
     * augment the ManagedConnection interface and add the appropriate implementation here.
     */
    @MBeanDescription("Management Bean for an AMQ Broker Connection")
    private final class ManagedAMQProtocolSession extends AMQManagedObject
                                                  implements ManagedConnection
    {
        private String _name = null;
        /**
         * Represents the channel attributes sent with channel data.
         */
        private String[] _channelAtttibuteNames = { "ChannelId",
                                                    "Transactional",
                                                    "DefaultQueue",
                                                    "UnacknowledgedMessageCount"};
        private String[] _channelAttributeDescriptions = { "Channel Identifier",
                                                           "is Channel Transactional?",
                                                           "Default Queue Name",
                                                           "Unacknowledged Message Count"};
        private OpenType[] _channelAttributeTypes = { SimpleType.INTEGER,
                                                      SimpleType.BOOLEAN,
                                                      SimpleType.STRING,
                                                      SimpleType.INTEGER};

        private String[] _indexNames = { "ChannelId" };  //Channels in the list will be indexed according to channelId.
        private CompositeType _channelType = null;       // represents the data type for channel data
        private TabularType  _channelsType = null;       // Datatype for list of channelsType
        private TabularDataSupport _channelsList = null;

        @MBeanConstructor("Creates an MBean exposing an AMQ Broker Connection")
        public ManagedAMQProtocolSession() throws NotCompliantMBeanException
        {
            super(ManagedConnection.class, ManagedConnection.TYPE);
            init();
        }

        /**
         * initialises the CompositeTypes and TabularType attributes.
         */
        private void init()
        {
            String remote = getRemoteAddress();
            remote = "anonymous".equals(remote) ? remote + hashCode() : remote;
            _name = jmxEncode(new StringBuffer(remote), 0).toString();

            try
            {
                _channelType = new CompositeType("channel",
                                              "Channel Details",
                                              _channelAtttibuteNames,
                                              _channelAttributeDescriptions,
                                              _channelAttributeTypes);

                _channelsType = new TabularType("channelsType",
                                       "List of available channels",
                                       _channelType,
                                       _indexNames);
            }
            catch(OpenDataException ex)
            {
                // It should never occur.
                _logger.error("OpenDataTypes could not be created.", ex);
                throw new RuntimeException(ex);
            }
        }

        public Date getLastIoTime()
        {
            return new Date(_minaProtocolSession.getLastIoTime());
        }

        public String getRemoteAddress()
        {
            return _minaProtocolSession.getRemoteAddress().toString();
        }

        public Long getWrittenBytes()
        {
            return _minaProtocolSession.getWrittenBytes();
        }

        public Long getReadBytes()
        {
            return _minaProtocolSession.getReadBytes();
        }

        public Long getMaximumNumberOfAllowedChannels()
        {
            return _maxNoOfChannels;
        }

        public void setMaximumNumberOfAllowedChannels(Long value)
        {
            _maxNoOfChannels = value;
        }

        public String getObjectInstanceName()
        {
            return _name;
        }

        public void commitTransactions(int channelId) throws JMException
        {
            try
            {
                AMQChannel channel = _channelMap.get(channelId);
                if (channel == null)
                {
                    throw new JMException("The channel (channel Id = " + channelId + ") does not exist");
                }
                if (channel.isTransactional())
                {
                    channel.commit();
                }
            }
            catch(AMQException ex)
            {
                throw new MBeanException(ex, ex.toString());
            }
        }

        public void rollbackTransactions(int channelId) throws JMException
        {
            try
            {
                AMQChannel channel = _channelMap.get(channelId);
                if (channel == null)
                {
                    throw new JMException("The channel (channel Id = " + channelId + ") does not exist");
                }
                if (channel.isTransactional())
                {
                    channel.rollback();
                }
            }
            catch(AMQException ex)
            {
                throw new MBeanException(ex, ex.toString());
            }
        }

        /**
         * Creates the list of channels in tabular form from the _channelMap.
         * @return  list of channels in tabular form.
         * @throws OpenDataException
         */
        public TabularData getChannels() throws OpenDataException
        {
            _channelsList = new TabularDataSupport(_channelsType);

            for (Map.Entry<Integer, AMQChannel> entry : _channelMap.entrySet())
            {
                AMQChannel channel = entry.getValue();
                Object[] itemValues = {channel.getChannelId(),
                                       channel.isTransactional(),
                                       (channel.getDefaultQueue() != null) ? channel.getDefaultQueue().getName() : null,
                                       channel.getUnacknowledgedMessageMap().size()};

                CompositeData channelData = new CompositeDataSupport(_channelType,
                                                            _channelAtttibuteNames,
                                                            itemValues);

                _channelsList.put(channelData);
            }

            return _channelsList;
        }

        public void closeChannel(int id)
            throws Exception
        {
            try
            {
                AMQMinaProtocolSession.this.closeChannel(id);
            }
            catch (AMQException ex)
            {
                throw new Exception(ex.toString());
            }
        }

        public void closeConnection()
            throws Exception
        {
            try
            {
                AMQMinaProtocolSession.this.closeSession();
            }
            catch (AMQException ex)
            {
                throw new Exception(ex.toString());
            }
        }

        @Override
        public MBeanNotificationInfo[] getNotificationInfo()
        {
            String[] notificationTypes = new String[]
                                {MonitorNotification.THRESHOLD_VALUE_EXCEEDED};
            String name = MonitorNotification.class.getName();
            String description = "An attribute of this MBean has reached threshold value";
            MBeanNotificationInfo info1 = new MBeanNotificationInfo(notificationTypes,
                                                                    name,
                                                                    description);

            return new MBeanNotificationInfo[] {info1};
        }

        private void checkForNotification()
        {
            int channelsCount = _channelMap.size();
            if (channelsCount >= getMaximumNumberOfAllowedChannels())
            {
                Notification n = new Notification(
                        MonitorNotification.THRESHOLD_VALUE_EXCEEDED,
                        this,
                        ++_notificationSequenceNumber,
                        System.currentTimeMillis(),
                        "ChannelsCount = " + channelsCount + ", ChannelsCount has reached the threshold value");

                _broadcaster.sendNotification(n);
            }
        }

    } // End of MBean class

    public AMQMinaProtocolSession(IoSession session, QueueRegistry queueRegistry, ExchangeRegistry exchangeRegistry,
                                  AMQCodecFactory codecFactory)
            throws AMQException
    {
        this(session, queueRegistry, exchangeRegistry, codecFactory, new AMQStateManager());
    }

    public AMQMinaProtocolSession(IoSession session, QueueRegistry queueRegistry, ExchangeRegistry exchangeRegistry,
                                  AMQCodecFactory codecFactory, AMQStateManager stateManager)
            throws AMQException
    {
        _stateManager = stateManager;
        _minaProtocolSession = session;
        session.setAttachment(this);
        _frameListeners.add(_stateManager);
        _queueRegistry = queueRegistry;
        _exchangeRegistry = exchangeRegistry;
        _codecFactory = codecFactory;
        _managedObject = createMBean();
        _managedObject.register();
    }

    private ManagedAMQProtocolSession createMBean() throws AMQException
    {
        try
        {
            return new ManagedAMQProtocolSession();
        }
        catch(NotCompliantMBeanException ex)
        {
            _logger.error("AMQProtocolSession MBean creation has failed.", ex);
            throw new AMQException("AMQProtocolSession MBean creation has failed.", ex);
        }
    }

    public static AMQProtocolSession getAMQProtocolSession(IoSession minaProtocolSession)
    {
        return (AMQProtocolSession) minaProtocolSession.getAttachment();
    }

    public void dataBlockReceived(AMQDataBlock message)
            throws Exception
    {
        _lastReceived = message;
        if (message instanceof ProtocolInitiation)
        {
            ProtocolInitiation pi = (ProtocolInitiation) message;
            // this ensures the codec never checks for a PI message again
            ((AMQDecoder)_codecFactory.getDecoder()).setExpectProtocolInitiation(false);
            try {
                pi.checkVersion(this); // Fails if not correct
                // This sets the protocol version (and hence framing classes) for this session.
                _major = pi.protocolMajor;
                _minor = pi.protocolMinor;
                String mechanisms = ApplicationRegistry.getInstance().getAuthenticationManager().getMechanisms();
                String locales = "en_US";
                AMQFrame response = ConnectionStartBody.createAMQFrame((short)0, pi.protocolMajor, pi.protocolMinor, null,
                                                                       mechanisms.getBytes(), locales.getBytes());
                _minaProtocolSession.write(response);
            } catch (AMQException e) {
                _logger.error("Received incorrect protocol initiation", e);
                /* Find last protocol version in protocol version list. Make sure last protocol version
                listed in the build file (build-module.xml) is the latest version which will be used
                here. */
                int i = pv.length - 1;
                _minaProtocolSession.write(new ProtocolInitiation(pv[i][PROTOCOL_MAJOR], pv[i][PROTOCOL_MINOR]));
                // TODO: Close connection (but how to wait until message is sent?)
            }
        }
        else
        {
            AMQFrame frame = (AMQFrame) message;

            if (frame.bodyFrame instanceof AMQMethodBody)
            {
                methodFrameReceived(frame);
            }
            else
            {
                try
                {
                    contentFrameReceived(frame);
                }
                catch (RequiredDeliveryException e)
                {
                    //need to return the message:
                    _logger.info("Returning message to " + this + " channel " + frame.channel
                                 + ": " + e.getMessage());
                    writeFrame(e.getReturnMessage(frame.channel));
                }
            }
        }
    }

    private void methodFrameReceived(AMQFrame frame)
    {
        if (_logger.isDebugEnabled())
        {
            _logger.debug("Method frame received: " + frame);
        }
        final AMQMethodEvent<AMQMethodBody> evt = new AMQMethodEvent<AMQMethodBody>(frame.channel,
                                                                                    (AMQMethodBody)frame.bodyFrame);
        try
        {
            boolean wasAnyoneInterested = false;
            for (AMQMethodListener listener : _frameListeners)
            {
                wasAnyoneInterested = listener.methodReceived(evt, this, _queueRegistry, _exchangeRegistry) ||
                                      wasAnyoneInterested;
            }
            if (!wasAnyoneInterested)
            {
                throw new AMQException("AMQMethodEvent " + evt + " was not processed by any listener.");
            }
        }
        catch (AMQChannelException e)
        {
            _logger.error("Closing channel due to: " + e.getMessage());
            writeFrame(e.getCloseFrame(frame.channel));
        }
        catch (AMQException e)
        {
            for (AMQMethodListener listener : _frameListeners)
            {
                listener.error(e);
            }
            _minaProtocolSession.close();
        }
    }

    private void contentFrameReceived(AMQFrame frame) throws AMQException
    {
        if (frame.bodyFrame instanceof ContentHeaderBody)
        {
            contentHeaderReceived(frame);
        }
        else if (frame.bodyFrame instanceof ContentBody)
        {
            contentBodyReceived(frame);
        }
        else if (frame.bodyFrame instanceof HeartbeatBody)
        {
            _logger.debug("Received heartbeat from client");
        }
        else
        {
            _logger.warn("Unrecognised frame " + frame.getClass().getName());
        }
    }

    private void contentHeaderReceived(AMQFrame frame) throws AMQException
    {
        if (_logger.isDebugEnabled())
        {
            _logger.debug("Content header frame received: " + frame);
        }
        getChannel(frame.channel).publishContentHeader((ContentHeaderBody)frame.bodyFrame);
    }

    private void contentBodyReceived(AMQFrame frame) throws AMQException
    {
        if (_logger.isDebugEnabled())
        {
            _logger.debug("Content body frame received: " + frame);
        }
        getChannel(frame.channel).publishContentBody((ContentBody)frame.bodyFrame);
    }

    /**
     * Convenience method that writes a frame to the protocol session. Equivalent
     * to calling getProtocolSession().write().
     *
     * @param frame the frame to write
     */
    public void writeFrame(AMQDataBlock frame)
    {
        _lastSent = frame;
        _minaProtocolSession.write(frame);
    }

    public String getContextKey()
    {
        return _contextKey;
    }

    public void setContextKey(String contextKey)
    {
        _contextKey = contextKey;
    }

    public AMQChannel getChannel(int channelId) throws AMQException
    {
        return _channelMap.get(channelId);
    }

    public void addChannel(AMQChannel channel)
    {
        _channelMap.put(channel.getChannelId(), channel);
        _managedObject.checkForNotification();
    }

    /**
     * Close a specific channel. This will remove any resources used by the channel, including:
     * <ul><li>any queue subscriptions (this may in turn remove queues if they are auto delete</li>
     * </ul>
     * @param channelId id of the channel to close
     * @throws AMQException if an error occurs closing the channel
     * @throws IllegalArgumentException if the channel id is not valid
     */
    public void closeChannel(int channelId) throws AMQException
    {
        final AMQChannel channel = _channelMap.get(channelId);
        if (channel == null)
        {
            throw new IllegalArgumentException("Unknown channel id");
        }
        else
        {
            try
            {
                channel.close(this);
            }
            finally
            {
                _channelMap.remove(channelId);
            }
        }
    }

    /**
     * In our current implementation this is used by the clustering code.
     * @param channelId
     */
    public void removeChannel(int channelId)
    {
        _channelMap.remove(channelId);
    }

    /**
     * Initialise heartbeats on the session.
     * @param delay delay in seconds (not ms)
     */
    public void initHeartbeats(int delay)
    {
        if(delay > 0)
        {
            _minaProtocolSession.setIdleTime(IdleStatus.WRITER_IDLE, delay);
            _minaProtocolSession.setIdleTime(IdleStatus.READER_IDLE, HeartbeatConfig.getInstance().getTimeout(delay));
        }
    }

    /**
     * Closes all channels that were opened by this protocol session. This frees up all resources
     * used by the channel.
     * @throws AMQException if an error occurs while closing any channel
     */
    private void closeAllChannels() throws AMQException
    {
        for (AMQChannel channel : _channelMap.values())
        {
            channel.close(this);
        }
    }

    /**
     * This must be called when the session is _closed in order to free up any resources
     * managed by the session.
     */
    public void closeSession() throws AMQException
    {
        if(!_closed)
        {
            _closed = true;
            closeAllChannels();
            if (_managedObject != null)
            {
                _managedObject.unregister();
            }
        }
    }

    public String toString()
    {
        return "AMQProtocolSession(" + _minaProtocolSession.getRemoteAddress() + ")";
    }

    public String dump()
    {
        return this + " last_sent=" + _lastSent + " last_received=" + _lastReceived;
    }

    /**
     * @return an object that can be used to identity
     */
    public Object getKey()
    {
        return _minaProtocolSession.getRemoteAddress();
    }

    /**
     * Get the fully qualified domain name of the local address to which this session is bound. Since some servers
     * may be bound to multiple addresses this could vary depending on the acceptor this session was created from.
     *
     * @return a String FQDN
     */
    public String getLocalFQDN()
    {
        SocketAddress address = _minaProtocolSession.getLocalAddress();
        // we use the vmpipe address in some tests hence the need for this rather ugly test. The host
        // information is used by SASL primary.
        if (address instanceof InetSocketAddress)
        {
            return ((InetSocketAddress)address).getHostName();
        }
        else if (address instanceof VmPipeAddress)
        {
            return "vmpipe:" + ((VmPipeAddress)address).getPort();
        }
        else
        {
            throw new IllegalArgumentException("Unsupported socket address class: " + address);
        }
    }

    public SaslServer getSaslServer()
    {
        return _saslServer;
    }

    public void setSaslServer(SaslServer saslServer)
    {
        _saslServer = saslServer;
    }
    
    /**
     * Convenience methods for managing AMQP version.
     * NOTE: Both major and minor will be set to 0 prior to protocol initiation.
     */
    
    public byte getAmqpMajor()
    {
        return _major;
    }
    
    public byte getAmqpMinor()
    {
        return _minor;
    }
    
    public boolean amqpVersionEquals(byte major, byte minor)
    {
        return _major == major && _minor == minor;
    }
}
