package org.jacorb.notification.servant;

/*
 *        JacORB - a free Java ORB
 *
 *   Copyright (C) 1997-2014 Gerald Brose / The JacORB Team.
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Library General Public
 *   License as published by the Free Software Foundation; either
 *   version 2 of the License, or (at your option) any later version.
 *
 *   This library is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *   Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this library; if not, write to the Free
 *   Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import java.util.List;

import org.jacorb.config.*;
import org.jacorb.notification.OfferManager;
import org.jacorb.notification.SubscriptionManager;
import org.jacorb.notification.conf.Attributes;
import org.jacorb.notification.conf.Default;
import org.jacorb.notification.engine.TaskProcessor;
import org.jacorb.notification.interfaces.Message;
import org.jacorb.notification.interfaces.MessageConsumer;
import org.jacorb.notification.queue.EventQueueFactory;
import org.jacorb.notification.queue.MessageQueue;
import org.jacorb.notification.queue.MessageQueueAdapter;
import org.jacorb.notification.queue.RWLockEventQueueDecorator;
import org.jacorb.notification.util.CollectionsWrapper;
import org.jacorb.notification.util.PropertySet;
import org.jacorb.notification.util.PropertySetAdapter;
import org.omg.CORBA.Any;
import org.omg.CORBA.NO_IMPLEMENT;
import org.omg.CORBA.ORB;
import org.omg.CosNotification.DiscardPolicy;
import org.omg.CosNotification.EventType;
import org.omg.CosNotification.MaxEventsPerConsumer;
import org.omg.CosNotification.OrderPolicy;
import org.omg.CosNotification.Property;
import org.omg.CosNotifyChannelAdmin.ConsumerAdmin;
import org.omg.CosNotifyChannelAdmin.ObtainInfoMode;
import org.omg.CosNotifyComm.InvalidEventType;
import org.omg.CosNotifyComm.NotifyPublish;
import org.omg.CosNotifyComm.NotifyPublishHelper;
import org.omg.CosNotifyComm.NotifyPublishOperations;
import org.omg.CosNotifyComm.NotifySubscribeOperations;
import org.omg.PortableServer.POA;

/**
 * Abstract base class for ProxySuppliers. This class provides base functionality
 * for the different ProxySuppliers:
 * <ul>
 * <li>queue management,
 * <li>error threshold settings.
 * </ul>
 *
 * @jmx.mbean extends = "AbstractProxyMBean"
 * @jboss.xmbean
 *
 * @--jmx.notification    name = "notification.proxy.message_discarded"
 *                      description = "queue overflow causes messages to be discarded"
 *                      notificationType = "java.lang.String"
 *
 * @author Alphonse Bendt
 */

public abstract class AbstractProxySupplier extends AbstractProxy implements MessageConsumer,
        NotifySubscribeOperations, AbstractProxySupplierMBean
{
    private static final String EVENT_MESSAGE_DISCARDED = "notification.proxy.message_discarded";

    private int numberOfDiscardedMessages_ = 0;

    private MessageQueue.DiscardListener discardListener_ = new MessageQueue.DiscardListener()
    {
        private long sendTimestamp_;
        private int discardedMessagesSinceLastBroadcast_ = 1;

        public void messageDiscarded(int maxSize)
        {
            numberOfDiscardedMessages_++;

            // max. one notification every five second
            if (!((System.currentTimeMillis() - sendTimestamp_) < 5000))
            {
                sendNotification(EVENT_MESSAGE_DISCARDED, discardedMessagesSinceLastBroadcast_
                        + " Message(s) discarded. Queue Limit: " + maxSize);
                sendTimestamp_ = System.currentTimeMillis();
                discardedMessagesSinceLastBroadcast_ = 1;

                if (logger_.isInfoEnabled())
                {
                    logger_.info(discardedMessagesSinceLastBroadcast_
                            + " Message(s) discarded. Queue Limit: " + maxSize);
                }
            }
            else
            {
                ++discardedMessagesSinceLastBroadcast_;
            }
        }
    };

    private static final Runnable EMPTY_RUNNABLE = new Runnable()
    {
        public void run()
        {
            // no operation
        }
    };

    private static final EventType[] EMPTY_EVENT_TYPE_ARRAY = new EventType[0];

    private static final Message[] EMPTY_MESSAGE = new Message[0];

    // //////////////////////////////////////

    private final RWLockEventQueueDecorator pendingMessages_;

    private final int errorThreshold_;

    private final ConsumerAdmin consumerAdmin_;

    private final EventQueueFactory eventQueueFactory_;

    private NotifyPublishOperations proxyOfferListener_;

    private NotifyPublish offerListener_;

    // //////////////////////////////////////

    protected AbstractProxySupplier(IAdmin admin, ORB orb, POA poa, Configuration conf,
            TaskProcessor taskProcessor, OfferManager offerManager,
            SubscriptionManager subscriptionManager, ConsumerAdmin consumerAdmin)
            throws ConfigurationException
    {
        super(admin, orb, poa, conf, taskProcessor, offerManager, subscriptionManager);

        consumerAdmin_ = consumerAdmin;

        eventQueueFactory_ = new EventQueueFactory(conf);

        errorThreshold_ = conf.getAttributeAsInteger(Attributes.EVENTCONSUMER_ERROR_THRESHOLD,
                Default.DEFAULT_EVENTCONSUMER_ERROR_THRESHOLD);

        if (logger_.isInfoEnabled())
        {
            logger_.info("set Error Threshold to : " + errorThreshold_);
        }

        qosSettings_.addPropertySetListener(new String[] { OrderPolicy.value, DiscardPolicy.value,
                MaxEventsPerConsumer.value }, eventQueueConfigurationChangedCB);

        final MessageQueueAdapter initialEventQueue =
            getMessageQueueFactory().newMessageQueue(qosSettings_);

        pendingMessages_ = new RWLockEventQueueDecorator(initialEventQueue);

        pendingMessages_.addDiscardListener(discardListener_);

        eventTypes_.add(EVENT_MESSAGE_DISCARDED);
    }

    // //////////////////////////////////////

    protected EventQueueFactory getMessageQueueFactory()
    {
        return eventQueueFactory_;
    }

    /**
     * configure pending messages queue. the queue is reconfigured according to the current QoS
     * Settings. the contents of the queue are reorganized according to the new OrderPolicy.
     */
    private final void configureEventQueue()
    {
        final MessageQueueAdapter _newQueue = getMessageQueueFactory().newMessageQueue(qosSettings_);

        try
        {
            pendingMessages_.replaceDelegate(_newQueue);
        } catch (InterruptedException e)
        {
            // ignored
        }
    }

    private PropertySetAdapter eventQueueConfigurationChangedCB = new PropertySetAdapter()
    {
        public void actionPropertySetChanged(PropertySet source)
        {
            configureEventQueue();
        }
    };

    /**
     * @jmx.managed-attribute description = "Number of Pending Messages"
     *                        access = "read-only"
     */
    public int getPendingMessagesCount()
    {
        try
        {
            return pendingMessages_.getPendingMessagesCount();
        } catch (InterruptedException e)
        {
            return -1;
        }
    }

    /**
     * @jmx.managed-attribute description = "current OrderPolicy"
     *                        access = "read-only"
     */
    public final String getOrderPolicy()
    {
        return pendingMessages_.getOrderPolicyName();
    }

    /**
     * @jmx.managed-attribute description = "current DiscardPolicy"
     *                        access = "read-only"
     */
    public final String getDiscardPolicy()
    {
        return pendingMessages_.getDiscardPolicyName();
    }

    /**
     * @jmx.managed-attribute description = "maximum number of events that may be queued per consumer"
     *                        access = "read-write"
     */
    public final int getMaxEventsPerConsumer()
    {
        return qosSettings_.get(MaxEventsPerConsumer.value).extract_long();
    }

    /**
     * @jmx.managed-attribute access = "read-write"
     */
    public void setMaxEventsPerConsumer(int max)
    {
        final Any any = getORB().create_any();
        any.insert_long(max);
        final Property prop = new Property(MaxEventsPerConsumer.value, any);
        qosSettings_.set_qos(new Property[] { prop });
    }

    /**
     * @jmx.managed-attribute access = "read-only"
     */
    public int getNumberOfDiscardedMessages()
    {
        return numberOfDiscardedMessages_;
    }

    public boolean hasPendingData()
    {
        try
        {
            return pendingMessages_.hasPendingMessages();
        } catch (InterruptedException e)
        {
            return false;
        }
    }

    /**
     * put a copy of the Message in the queue of pending Messages.
     *
     * @param message
     *            the <code>Message</code> to queue.
     */
    protected void enqueue(Message message)
    {
        Message _copy = (Message) message.clone();

        try
        {
            pendingMessages_.enqeue(_copy);

            if (logger_.isDebugEnabled())
            {
                logger_.debug("enqueue " + message + " to pending Messages.");
            }
        } catch (InterruptedException e)
        {
            _copy.dispose();
            logger_.info("enqueue was interrupted", e);
        }
    }

    public Message getMessageBlocking() throws InterruptedException
    {
        return pendingMessages_.getMessageBlocking();
    }

    protected Message getMessageNoBlock()
    {
        try
        {
            return pendingMessages_.getMessageNoBlock();
        } catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();

            return null;
        }
    }

    protected Message[] getAllMessages()
    {
        try
        {
            return pendingMessages_.getAllMessages();
        } catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();

            return EMPTY_MESSAGE;
        }
    }

    public void queueMessage(final Message message)
    {
        if (logger_.isDebugEnabled())
        {
            logger_.debug("queueMessage() connected=" + getConnected() + " suspended="
                    + isSuspended());
        }

        if (getConnected())
        {
            enqueue(message);

            messageQueued();
        }
    }

    /**
     * this is an extension point.
     */
    protected void messageQueued()
    {
        // no operation
    }

    /**
     * @param max maximum number of messages
     * @return an array containing at most max Messages
     */
    protected Message[] getUpToMessages(int max)
    {
        try
        {
            return pendingMessages_.getUpToMessages(max);
        } catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();

            return EMPTY_MESSAGE;
        }
    }

    /**
     * @param min
     *            minimum number of messages
     * @return an array containing the requested number of Messages or null
     */
    protected Message[] getAtLeastMessages(int min)
    {
        try
        {
            return pendingMessages_.getAtLeastMessages(min);
        } catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();

            return EMPTY_MESSAGE;
        }
    }

    public int getErrorThreshold()
    {
        return errorThreshold_;
    }

    public final void dispose()
    {
        super.dispose();

        pendingMessages_.clear();

        // insert an empty command into the taskProcessor's queue.
        // otherwise queue seems to contain old entries that prevent GC'ing
        getTaskProcessor().executeTaskAfterDelay(1000, EMPTY_RUNNABLE);
    }

    public final ConsumerAdmin MyAdmin()
    {
        return consumerAdmin_;
    }

    public final void subscription_change(EventType[] added, EventType[] removed)
            throws InvalidEventType
    {
        subscriptionManager_.subscription_change(added, removed);
    }

    public final EventType[] obtain_offered_types(ObtainInfoMode obtainInfoMode)
    {
        EventType[] _offeredTypes = EMPTY_EVENT_TYPE_ARRAY;

        switch (obtainInfoMode.value()) {
        case ObtainInfoMode._ALL_NOW_UPDATES_ON:
            registerListener();
            _offeredTypes = offerManager_.obtain_offered_types();
            break;
        case ObtainInfoMode._ALL_NOW_UPDATES_OFF:
            _offeredTypes = offerManager_.obtain_offered_types();
            removeListener();
            break;
        case ObtainInfoMode._NONE_NOW_UPDATES_ON:
            registerListener();
            break;
        case ObtainInfoMode._NONE_NOW_UPDATES_OFF:
            removeListener();
            break;
        default:
            throw new IllegalArgumentException("Illegal ObtainInfoMode");
        }

        return _offeredTypes;
    }

    private void registerListener()
    {
        if (proxyOfferListener_ == null)
        {
            final NotifyPublishOperations _listener = getOfferListener();

            if (_listener != null)
            {
                proxyOfferListener_ = new NotifyPublishOperations()
                {
                    public void offer_change(EventType[] added, EventType[] removed)
                    {
                        try
                        {
                            _listener.offer_change(added, removed);
                        } catch (NO_IMPLEMENT e)
                        {
                            logger_.info("disable offer_change for connected Consumer.", e);

                            removeListener();
                        } catch (InvalidEventType e)
                        {
                            logger_.warn("invalid event type", e);
                        } catch (Exception e)
                        {
                            logger_.warn("offer_change failed", e);
                        }
                    }
                };

                offerManager_.addListener(proxyOfferListener_);
            }
        }
    }

    protected void removeListener()
    {
        if (proxyOfferListener_ != null)
        {
            offerManager_.removeListener(proxyOfferListener_);
            proxyOfferListener_ = null;
        }
    }

    final NotifyPublishOperations getOfferListener()
    {
        return offerListener_;
    }

    protected final void clientDisconnected()
    {
        offerListener_ = null;
    }

    public void connectClient(org.omg.CORBA.Object client)
    {
        super.connectClient(client);

        try
        {
            offerListener_ = NotifyPublishHelper.narrow(client);

            logger_.debug("successfully narrowed connecting Client to IF NotifyPublish");
        } catch (Exception t)
        {
            logger_.info("disable offer_change for connecting Consumer");
        }
    }

    public boolean isRetryAllowed()
    {
        return !isDestroyed() && getErrorCounter() < getErrorThreshold();
    }

    protected abstract long getCost();

    public int compareTo(Object o)
    {
        AbstractProxySupplier other = (AbstractProxySupplier) o;

        return (int) (getCost() - other.getCost());
    }

    public final boolean hasMessageConsumer()
    {
        return true;
    }

    public final List getSubsequentFilterStages()
    {
        return CollectionsWrapper.singletonList(this);
    }

    public final MessageConsumer getMessageConsumer()
    {
        return this;
    }

    /**
     * @jmx.managed-operation   impact = "ACTION"
     *                          description = "delete all queued Messages"
     */
    public void clearPendingMessageQueue()
    {
        pendingMessages_.clear();
    }
}