/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.mx.remoting;

import EDU.oswego.cs.dl.util.concurrent.LinkedQueue;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedInt;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import org.jboss.remoting.*;
import org.jboss.remoting.invocation.NameBasedInvocation;
import org.jboss.remoting.transport.ClientInvoker;
import org.jboss.remoting.network.NetworkNotification;
import org.jboss.remoting.network.NetworkRegistryFinder;
import org.jboss.logging.Logger;

import javax.management.*;
import java.util.*;

/**
 * MBeanNotificationCache is an object that queues all the server side JMX notifications on behalf
 * of a client invoker.
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @version $Revision: 1.2.2.9 $
 */
public class MBeanNotificationCache implements NotificationListener
{
    private static final Logger log = Logger.getLogger(MBeanNotificationCache.class.getName());
    private final MBeanServer server;
    private final SynchronizedInt notifCounter=new SynchronizedInt(0);
    private final SynchronizedBoolean destroyed=new SynchronizedBoolean(false);
    private final List listeners=new ArrayList();
    private final Map queue=new HashMap();
    private final ObjectName networkRegistry;
    private final ServerInvoker serverInvoker;
    private final String localServerId;
    private final JMXSubsystemInvocationHandler handler;

    private ClientInvoker clientInvoker;
    private Client client;
    private boolean asyncSend = false;
    private int counter=0;
    private LinkedQueue asyncQueue;
    private BiDirectionClientNotificationSender biDirectionalSender;
    private InvokerLocator locator;
    private String sessionId;

    public MBeanNotificationCache (JMXSubsystemInvocationHandler handler, ServerInvoker invoker, MBeanServer server, InvokerLocator l, String sessionId)
        throws Exception
    {
        this.handler = handler;
        this.server = server;
        this.serverInvoker = invoker;
        this.localServerId = JMXUtil.getServerId(server);
        this.locator = l;
        this.sessionId = sessionId;

        if (log.isDebugEnabled())
        {
            log.debug("Creating notification cache to client locator: "+l+", for sessionid: "+sessionId+", local server locator: "+invoker.getLocator());
        }
        networkRegistry = NetworkRegistryFinder.find(server);
        if (networkRegistry==null)
        {
            throw new Exception("Couldn't find the required NetworkRegistryMBean in this MBeanServer");
        }
        // add ourself as a listener for detection failed events
        server.addNotificationListener(networkRegistry,this,null,this);
    }
    /**
     * sync invoke so that we can sync in case of destroy
     *
     * @param id
     * @param i
     * @throws Throwable
     */
    private void clientInvoke (String id, NameBasedInvocation i) throws Throwable
    {
        if (client==null) throw new ConnectionFailedException("client is null");
        client.setSessionId(id);
        client.invoke(i,null);
    }
    public synchronized boolean hasListeners ()
    {
        return listeners.isEmpty()==false;
    }

    /**
     * called internally to determine if we should attempt to send async notifications
     *
     * @return
     */
    private synchronized boolean shouldAsyncSend ()
    {
        return (asyncSend && biDirectionalSender!=null && biDirectionalSender.running);
    }
    /**
     * this is called by the jmx subsystem handler to determine if we should instruct the
     * remote client to continue polling for notifications
     *
     * @return
     */
    public synchronized boolean shouldClientPoll ()
    {
        return asyncSend==false || biDirectionalSender==null;
    }

    public void handleNotification (Notification notification, Object o)
    {
        if (notification instanceof NetworkNotification && o!=null && this.equals(o))
        {
            String type = notification.getType();
            if (type.equals(NetworkNotification.SERVER_REMOVED))
            {
                // server has failed
                NetworkNotification nn=(NetworkNotification)notification;
                String sessionId = nn.getIdentity().getJMXId();
                if (sessionId.equals(this.sessionId)==false)
                {
                    if (log.isDebugEnabled()) log.debug("server with id: "+sessionId+" has failed ... but it doesn't match our session: "+this.sessionId+", ignoring");
                    return;
                }
                if (log.isDebugEnabled()) log.debug("server with id: "+sessionId+" has failed ... removing listeners");
                List failed = new ArrayList();
                synchronized(listeners)
                {
                    Iterator iter = listeners.iterator();
                    while(iter.hasNext())
                    {
                        Listener listener=(Listener)iter.next();
                        // just put into a list, so we only sync min time
                        failed.add(listener);
                    }
                }
                if (failed.isEmpty()==false)
                {
                    // walk through and remove each listener that has failed
                    Iterator iter = failed.iterator();
                    while(iter.hasNext())
                    {
                        Listener listener=(Listener)iter.next();
                        if (log.isDebugEnabled())
                        {
                            log.debug("++ Removed orphaned listener: "+listener+" because server failed: "+nn.getIdentity());
                        }
                        try
                        {
                            // this will remove from listeners list
                            removeNotificationListener(listener.objectName,listener.handback);
                        }
                        catch (Exception ig) { }
                        iter.remove();
                        listener = null;
                    }
                    failed = null;
                }

                // destroy ourself
                ////destroy();
            }
        }
    }
    private void emptyQueue ()
    {
        if (biDirectionalSender!=null)
        {
            biDirectionalSender.flush = true;
            if (log.isDebugEnabled())
            {
                log.debug("empty queue called ... waiting for the event queue to flush ");
            }
            while (biDirectionalSender.running && biDirectionalSender.isKilled()==false)
            {
                synchronized(biDirectionalSender)
                {
                    try
                    {
                        biDirectionalSender.wait(200);
                    }
                    catch (InterruptedException ex)
                    {
                        break;
                    }
                }
            }
            if (log.isDebugEnabled())
            {
                log.debug("empty queue called flush is finished");
            }
        }
    }
    /**
     * returns true if destroyed
     *
     * @return
     */
    public boolean isDestroyed ()
    {
        return destroyed.get();
    }
    public void destroy ()
    {
        if (destroyed.get())
        {
            return;
        }
        destroyed.set(true);
        handler.destroyed(locator,this);

        if (log.isDebugEnabled())
        {
            log.debug("destroy call on notification cache for client: "+clientInvoker+", sessionid: "+sessionId);
        }
        try
        {
            server.removeNotificationListener(networkRegistry,this,null,this);
        }
        catch (Exception ig) { }
        // make a copy, remove might generate a list remove
        List list = null;
        synchronized(listeners)
        {
            list = new ArrayList(listeners);
        }
        Iterator iter = list.iterator();
        while(iter.hasNext())
        {
            Listener l=(Listener)iter.next();
            iter.remove();
            try
            {
                removeNotificationListener(l.objectName,l.handback);
            }
            catch (Exception e)
            {
            }
            // remove will remove from the listeners list
        }
        list=null;
        synchronized(listeners)
        {
            listeners.clear();
        }
        if (biDirectionalSender!=null)
        {
            // empty queue will set flag to flush and wait for the queue to drain
            emptyQueue();
            biDirectionalSender.kill();
            biDirectionalSender=null;
            asyncQueue=null;
        }
        synchronized (queue)
        {
            queue.clear();
        }
    }
    public synchronized void addNotificationListener (ObjectName objectName, NotificationFilter filter, Object handback)
        throws InstanceNotFoundException
    {
        if (log.isDebugEnabled())
        {
            log.debug("addNotificationListener added for client ["+locator+"] on objectName ["+objectName+"] and mbeanServerId ["+sessionId+"], filter: "+filter+", handback: "+handback);
        }
        if (hasListeners()==false)
        {
            if (serverInvoker.isTransportBiDirectional())
            {
                // attempt connection
                connectAsync();
            }
        }
        Listener l = new Listener(objectName, filter, handback);
        synchronized(this.listeners)
        {
            this.listeners.add(l);
            server.addNotificationListener(objectName,l,filter,handback);
        }
    }

    public synchronized void removeNotificationListener (ObjectName objectName, Object handback)
        throws InstanceNotFoundException, ListenerNotFoundException
    {
        if (log.isDebugEnabled())
        {
            log.debug("removeNotificationListener called with clientLocator: "+locator+", sessionId: "+sessionId+", objectName: "+objectName+", handback: "+handback);
        }
        boolean empty = false;
        synchronized(this.listeners)
        {
            Iterator iter = listeners.iterator();
            while (iter.hasNext())
            {
                Listener l=(Listener)iter.next();
                if (l.objectName.equals(objectName) && l.handback.equals(handback))
                {
                    if (log.isDebugEnabled())
                    {
                        log.debug("remote notification listener removed for client ["+locator+"] on objectName ["+objectName+"] and MBeanServerId ["+sessionId+"]");
                    }
                    iter.remove();
                    server.removeNotificationListener(objectName,l,l.filter,handback);
                    l.destroy();
                    l=null;
                    break;
                }
            }
            if (listeners.isEmpty() && biDirectionalSender!=null)
            {
                empty=true;
            }
        }
        if (empty)
        {
            // empty the queue
            emptyQueue();
            // destroy the cache
            biDirectionalSender.kill();
            biDirectionalSender = null;
            asyncQueue = null;

            if (log.isDebugEnabled()) log.debug("removeNotificationListener: listener collection is now empty - killing sender, nulling asyncQueue...");
        }
    }

    /**
     * pull notifications for a given sessionId and return the queue or null if none pending
     *
     * @param sessionId
     * @return
     */
    public NotificationQueue getNotifications (String sessionId)
    {
        synchronized(queue)
        {
            // remove the queue object each time, if it exists, the
            // listener will re-create a new one on each notification
            return (NotificationQueue)queue.remove(sessionId);
        }
    }

    private final class BiDirectionClientNotificationSender extends Thread
    {
        private boolean running = true;
        private boolean flush = false;

        public BiDirectionClientNotificationSender()
        {
            super("BiDirectionClientNotificationSender-" + clientInvoker.getLocator());
            // don't let the remote notification sender consume more CPU than
            // local threads - allow the system to override it with a property
            String norm = String.valueOf(Thread.NORM_PRIORITY - 2);
            String prop = System.getProperty("jboss.jmx.remoting.notificationcache.priority", norm);
            setPriority(Integer.parseInt(prop));
        }

        synchronized void kill ()
        {
            if (log.isDebugEnabled())
               log.debug("kill called on BiDirectionClientNotificationSender - running="+running+", client: "+client+", sessionid: "+sessionId);
            if (this.running)
            {
                this.running = false;
                interrupt();
            }
        }
        synchronized boolean isKilled ()
        {
            return !running || asyncQueue==null || MBeanNotificationCache.this.isDestroyed();
        }
        public void run ()
        {
            try
            {
                if (log.isDebugEnabled()) log.debug("BiDirectionClientNotificationSender::run: thread is starting");

                int fullQueueCount = Math.min(1,Integer.parseInt(System.getProperty("jboss.jmx.remoting.notificationcache.fullQueueCount","10")));
                long fullQueueTimeoutWhenFilled = Math.min(25,Long.parseLong(System.getProperty("jboss.jmx.remoting.notificationcache.fullQueueTimeoutWhenFilled","100")));
                long fullQueueTimeoutWhenEmpty = Math.min(25,Long.parseLong(System.getProperty("jboss.jmx.remoting.notificationcache.fullQueueTimeoutWhenEmpty","500")));
                int count = 0;
                long lastTx = 0;
                String PARAM[] = new String[]{NotificationQueue[].class.getName()};
                Set q = new HashSet(fullQueueCount);
                while (!isKilled())
                {
                    try
                    {
                        NotificationQueue pollq = getNotifications(sessionId);
                        if (pollq!=null)
                        {
                            count++;
                            q.add(pollq);
                            pollq=null;
                        }

//                    System.err.println(System.currentTimeMillis()+" top of the while loop");

                        // this is simple - try and collect as many notifications to be sent as
                        // possible during a small window / total count before attempting the network IO
                        while (!isKilled())
                        {
                            if (flush && queue.isEmpty())
                            {
                                break;
                            }
                            // check to see if we've timed out after getting at least 1 (and we're waiting for more)
                            boolean timedout = !flush && (System.currentTimeMillis()-lastTx>fullQueueTimeoutWhenFilled) && count>0;

                            // if so, break
                            if (timedout) break;

                            // if we have exceeded out fullQueueCount
                            timedout = !flush && count>=fullQueueCount;

                            if (timedout) break;

                            // as long as we have entries w/o blocking, add them  - we wait a little differently depending
                            // on if we have an empty queue (first time) or if we have some count
                            long timeout = (count>0) ?  fullQueueTimeoutWhenFilled :
                                    asyncQueue.isEmpty() && q.isEmpty() ? fullQueueTimeoutWhenEmpty : fullQueueTimeoutWhenFilled;

                            NotificationQueue nq=(NotificationQueue)asyncQueue.poll(timeout);
                            //System.err.println(System.currentTimeMillis()+" poll returned: "+nq);
                            if (nq==null)
                            {
                                if (count>0)
                                {
                                    // we timed out, but we at least have one, send it
                                    break;
                                }
                                else
                                {
                                    if (flush)
                                    {
                                        break;
                                    }
                                    // we timed out, but we don't have anything still .. continue
                                    continue;
                                }
                            }
                            else
                            {
                                if (nq.isEmpty()==false)
                                {
                                    // not null
                                    count++;
                                    q.add(nq);
                                }
                            }
                        }
                        if (isKilled())
                        {
                            if (log.isDebugEnabled()) log.debug(getName()+" is killed, returning, queue is dropping: "+q);
                            return;
                        }

                        boolean send = q.isEmpty()==false && isKilled()==false;

                        if (send)
                        {
//                        System.err.println(System.currentTimeMillis()+" sending ... count="+q.size()+", queue: "+q);

                            // send back to client
                            try
                            {
                                if (log.isDebugEnabled())
                                {
                                    long duration = System.currentTimeMillis() - lastTx;
                                    log.debug("sending notification queue("+q.size()+") ["+q+"] to client ["+locator+"] with sessionId ["+sessionId+"], counter="+counter+" ,count="+count+", lastTx="+lastTx+", "+duration+" ms since last tx");
                                }
                                // update the last time we transmitted
                                lastTx = System.currentTimeMillis();
                                NotificationQueue queues[]=new NotificationQueue[q.size()];
                                q.toArray(queues);
                                clientInvoke(localServerId,new NameBasedInvocation("$NOTIFICATIONS$",
                                                                      new Object[]{queues},PARAM
                                                                      ));
                                synchronized(this)
                                {
                                   asyncSend = true;
                                }
                            }
                            catch (Throwable t)
                            {
                                if (t instanceof ConnectionFailedException || t instanceof java.io.EOFException)
                                {
                                    if (log.isDebugEnabled())
                                    {
                                        log.debug("Client is dead during invocation");
                                    }
                                    MBeanNotificationCache.this.destroy();
                                    break;
                                }
                                else
                                {
                                    if (isKilled())
                                    {
                                        break;
                                    }
                                    log.warn("Error sending async notifications to client: "+locator,t);
                                }
                            }
                            finally
                            {
                                // clear the items in the queue, if any
                                count=0;
                                q.clear();
                            }
                        }
                        else if (flush && q.isEmpty())
                        {
                            if (log.isDebugEnabled()) log.debug("queue instructed to be flushed and its empty");
                            break;
                        }
                        else if (isKilled() && q.isEmpty()==false)
                        {
                            if (log.isDebugEnabled()) log.debug("queue is destroyed with notifications pending: "+q);
                        }
                    }
                    catch (InterruptedException ex)
                    {
                         break;
                    }
                    catch (Throwable err)
                    {
                        log.error("Unhandled Error caught",err);
                    }
                }

                if (log.isDebugEnabled()) log.debug(getName()+" is killed, returning");
            }
            finally
            {
                running = false;
                synchronized(this)
                {
                    notifyAll();
                }

                if (log.isDebugEnabled()) log.debug("BiDirectionClientNotificationSender::run: thread is exiting");
            }
        }
    }
    private synchronized void connectAsync ()
    {
        try
        {
            if (log.isDebugEnabled())
            {
                log.debug("attempting an bi-directional connection back to client ["+locator+"], server id ["+sessionId+"]");
            }
            asyncSend = false;
            // attempt connection back
            clientInvoker = InvokerRegistry.createClientInvoker(locator);
            clientInvoker.connect();
            client=new Client(Thread.currentThread().getContextClassLoader(),clientInvoker,Subsystem.JMX);
            asyncQueue = new LinkedQueue();
            biDirectionalSender = new BiDirectionClientNotificationSender();
            biDirectionalSender.start();
            asyncSend = true;
            if (log.isDebugEnabled())
            {
                log.debug("bi-directional connection back to client ["+locator+"] successful for "+sessionId+", async?"+shouldAsyncSend());
            }
        }
        catch ( Throwable e )
        {
            log.debug("attempted a bi-directional connection back to client ["+locator+"], but it failed",e);
        }
    }

    private static boolean eq(Object o1, Object o2)
    {
       if (o1 == null && o2 != null) return false;
       return o1.equals(o2);
    }

    private static int hc(Object o)
    {
       return (o == null) ? 0 : o.hashCode();
    }

    private final class Listener implements NotificationListener
    {

        final ObjectName objectName;
        final Object handback;
        final NotificationFilter filter;

        Listener (ObjectName objectName, NotificationFilter filter, Object handback)
        {
            this.objectName = objectName;
            this.filter = filter;
            this.handback = handback;
        }

        public String toString ()
        {
			      return "Listener [objectName="+objectName+",handback="+handback+",filter="+filter+"]";
		    }

        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (!(o instanceof Listener)) return false;

            final Listener listener = (Listener) o;

            return eq(filter, listener.filter) &&
              eq(handback, listener.handback) &&
              eq(objectName, listener.objectName);
        }

        public int hashCode()
        {
            return hc(objectName) ^ hc(handback) ^ hc(filter);
        }

        synchronized void destroy ()
        {
            if (log.isDebugEnabled())
            {
                log.debug("destroy called on client ["+locator+"], session id ["+sessionId+"], removing: "+this);
            }
            try
            {
                removeNotificationListener(objectName,handback);
            }
            catch (Throwable e)
            {
            }
        }



        public void handleNotification (Notification notification, Object o)
        {
            // increment our handy notification counter
            notifCounter.increment();

            boolean poll = !shouldAsyncSend();

            if (log.isDebugEnabled())
            {
                log.debug("("+(poll?"polling":"async")+") notification ("+notifCounter.get()+") received ..."+notification+" for client ["+locator+"]");
            }
            if (poll)
            {
                // not async, we are going to queue for polling ...
                NotificationQueue q = null;

                synchronized(queue)
                {
                    // get the queue
                    q = (NotificationQueue)queue.get(sessionId);
                    if (q==null)
                    {
                        // doesn't exist, create it
                        q = new NotificationQueue(sessionId);
                        queue.put(sessionId,q);
                    }
                    if (log.isDebugEnabled())
                    {
                        log.debug("added notification to polling queue: "+notification+" for sessionId: "+sessionId+", current contents (before add) ="+q);
                    }
                    q.add (new NotificationEntry(notification,handback));
                }
            }
            else
            {
                if (asyncQueue!=null)
                {
                    // this is a bi-directional client, send it immediately by
                    // placing it in the async queue and the async notification thread
                    // will pick it up and sent it
                    try
                    {

                        NotificationQueue q = new NotificationQueue(sessionId);
                        q.add(new NotificationEntry(notification,handback));
                        asyncQueue.put(q);
                    }
                    catch (InterruptedException ie)
                    {

                    }
                }
                else
                {
                    log.debug("notification received - but our asyncQueue is null for: "+locator+", sessionid: "+sessionId);
                }
            }
        }
    }
}

