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


import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import javax.management.ListenerNotFoundException;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanServer;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.jboss.logging.Logger;
import org.jboss.mx.util.JBossNotificationBroadcasterSupport;
import org.jboss.remoting.InvocationRequest;
import org.jboss.remoting.InvokerLocator;
import org.jboss.remoting.InvokerRegistry;
import org.jboss.remoting.Subsystem;
import org.jboss.remoting.ident.Identity;
import org.jboss.remoting.network.NetworkRegistryFinder;
import org.jboss.remoting.transport.ClientInvoker;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import EDU.oswego.cs.dl.util.concurrent.SynchronizedLong;


/**
 * AbstractDetector
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @author <a href="mailto:telrod@e2technologies.net">Tom Elrod</a>
 * @version $Revision: 1.15.2.10 $
 */
public abstract class AbstractDetector implements Detector, AbstractDetectorMBean
{
   protected static final boolean DETECTOR_DEBUG = Boolean.getBoolean("jboss.remoting.detector.debug");
   private static final long DEFAULT_TIME_DELAY = Integer.parseInt(System.getProperty("jboss.remoting.detector.ping", "5000"));
   private static final long INVOCATION_DELAY = Integer.parseInt(System.getProperty("jboss.remoting.detector.invocation", "1000"));
   private static final long HEARTBEAT_TIME_DELAY = Integer.parseInt(System.getProperty("jboss.remoting.detector.heartbeat", "5000"));
   protected final Logger log = Logger.getLogger(getClass());
   protected MBeanServer mbeanserver;
   protected ObjectName objectName;
   protected ObjectName registryObjectName;

   private Identity myself;
   private Timer heartbeatTimer;
   private Timer failureTimer;
   private Map servers = new HashMap();
   private long maxTimeDelay = DEFAULT_TIME_DELAY;
   private long invocationDelay = INVOCATION_DELAY;
   private Element xml;
   private boolean running = false;
   private Set domains = new HashSet();

   private JBossNotificationBroadcasterSupport broadcaster = new JBossNotificationBroadcasterSupport();

   /**
    * Sequence number for jmx notifications we send out.
    */
   private SynchronizedLong sequenceNumber = new SynchronizedLong(0);


   public AbstractDetector()
   {
   }

   public boolean isRunning()
   {
      return running;
   }

   /**
    * called by MBeanServer to start the mbean lifecycle
    *
    * @throws Exception
    */
   public synchronized void start() throws Exception
   {
      running = true;

      // get our own identity
      myself = Identity.get(mbeanserver);

      // add my domain if domains empty and xml not set
      if (domains.isEmpty() && xml == null)
      {
         addDomain(myself.getDomain());
      }

      // find our NetworkRegistry
      registryObjectName = NetworkRegistryFinder.find(mbeanserver);
      if (registryObjectName == null)
      {
         log.warn("Detector: " + getClass().getName() + " could not be loaded because the NetworkRegistry isn't registered");
         return;
      }

      startPinger(getPingerDelay(), getPingerPeriod());
      startHeartbeat(getHeartbeatDelay(), getHeartbeatPeriod());
   }

   /**
    * return the delay in milliseconds between when the timer is created to when the first pinger thread runs.
    * defaults to <tt>5000</tt>
    *
    * @return
    */
   protected long getPingerDelay()
   {
      return maxTimeDelay * 2;
   }

   /**
    * return the period in milliseconds between checking lost servers against the last detection timestamp.
    * defaults to <tt>1500</tt>
    *
    * @return
    */
   protected long getPingerPeriod()
   {
      return maxTimeDelay;
   }

   /**
    * The <code>getNextNotificationSequenceNumber</code> method returns
    * the next sequence number for use in notifications.
    *
    * @return a <code>long</code> value
    */
   protected long getNextNotificationSequenceNumber()
   {
      return sequenceNumber.increment();
   }

   /**
    * start the pinger timer thread
    *
    * @param delay
    * @param period
    */
   protected void startPinger(long delay, long period)
   {
      failureTimer = new Timer(false);
      failureTimer.schedule(new FailureDetector(), delay, period);
   }

   /**
    * stop the pinger timer thread
    */
   protected void stopPinger()
   {
      if (failureTimer != null)
      {
         failureTimer.cancel();
         failureTimer = null;
      }
   }

   /**
    * called by the MBeanServer to stop the mbean lifecycle
    *
    * @throws Exception
    */
   public synchronized void stop() throws Exception
   {
      if (isRunning())
      {
         this.running = false;
         stopPinger();
         stopHeartbeat();
         stopPinger();
      }
   }

   public void postDeregister()
   {
   }

   public void postRegister(Boolean aBoolean)
   {
   }

   public void preDeregister() throws Exception
   {
   }

   public ObjectName preRegister(MBeanServer mBeanServer, ObjectName objectName) throws Exception
   {
      this.mbeanserver = mBeanServer;
      this.objectName = objectName;
      return objectName;
   }

   /**
    * set the configuration for the domains to be recognized by detector
    *
    * @param xml
    * @jmx.managed-attribute description="Configuration is an xml element indicating domains to be recognized by detector"
    * access="read-write"
    */
   public void setConfiguration(Element xml)
         throws Exception
   {
      this.xml = xml;

      // check configuration xml
      if (xml != null)
      {
         // clearing collection of domains since have new ones to set
         domains.clear();

         NodeList domainNodes = xml.getElementsByTagName("domain");
         if (domainNodes == null || domainNodes.getLength() <= 0)
         {
            // no domains specified, so will accept all domains
            if (log.isDebugEnabled())
            {
               log.debug("No domains specified.  Will accept all domains.");
            }
         }
         int len = domainNodes.getLength();
         for (int c = 0; c < len; c++)
         {
            Node node = domainNodes.item(c);
            String domain = node.getFirstChild().getNodeValue();
            addDomain(domain);
            if (log.isDebugEnabled())
            {
               log.debug("Added domain " + domain + " to detector list.");
            }
         }
      }
   }

   /**
    * The <code>getConfiguration</code> method
    *
    * @return an <code>Element</code> value
    * @jmx.managed-attribute
    */
   public Element getConfiguration()
   {
      return xml;
   }

   //----------------------- protected

   /**
    * start heartbeating
    *
    * @param delay
    * @param period
    */
   protected void startHeartbeat(long delay, long period)
   {
      if (heartbeatTimer == null)
      {
         heartbeatTimer = new Timer(false);
      }
      heartbeatTimer.schedule(new Heartbeat(), delay, period);
   }

   /**
    * stop heartbeating
    */
   protected void stopHeartbeat()
   {
      if (heartbeatTimer != null)
      {
         try
         {
            heartbeatTimer.cancel();
         }
         catch (Exception eg)
         {
         }
         heartbeatTimer = null;
      }
   }

   /**
    * return the initial delay in milliseconds before the initial heartbeat is fired.
    * Defaults to <tt>0</tt>
    *
    * @return
    */
   protected long getHeartbeatDelay()
   {
      // fire the detection near when we start
      return 500;
   }

   /**
    * return the period in milliseconds between subsequent heartbeats. Defaults to
    * <tt>1000</tt>
    *
    * @return
    */
   protected long getHeartbeatPeriod()
   {
      return HEARTBEAT_TIME_DELAY;
   }

   /**
    * subclasses must implement to provide the specific heartbeat protocol
    * for this server to send out to other servers on the network
    */
   protected abstract void heartbeat();

   /**
    * called when a remote detection from a peer is received by a detector
    *
    * @param detection
    */
   protected void detect(Detection detection)
   {
      if (DETECTOR_DEBUG && log.isDebugEnabled())
      {
         log.debug("Detection message received. Local: " + myself + ", Remote: " + detection.getIdentity() + ", Remote? " + isRemoteDetection(detection));
      }
      // we only track detections within our own domain and not ourself
      if (isRemoteDetection(detection))
      {
         try
         {
            boolean found = false;
            Server server = null;

            synchronized (servers)
            {
               server = (Server) servers.get(detection);
               found = server != null;
               if (!found)
               {
                  // update either way the timestamp and the detection
                  servers.put(detection, (server = new Server(detection)));
                  if (log.isDebugEnabled()) log.debug("updated detection list = " + servers);
               }
               else
               {
                  server.lastDetection = System.currentTimeMillis();
               }
            }
            if (found == false)
            {
               if (DETECTOR_DEBUG && log.isDebugEnabled())
               {
                  log.debug("detected NEW server: " + detection.getIdentity());
               }
               mbeanserver.invoke(registryObjectName, "addServer", new Object[]{detection.getIdentity(), detection.getLocators()}, new String[]{Identity.class.getName(), InvokerLocator[].class.getName()});
               fireNewDetectionNotification(detection);
            }
            else
            {
               if (server.changed(detection))
               {
                  // update hash
                  server.rehash(detection);
                  if (DETECTOR_DEBUG && log.isDebugEnabled())
                  {
                     log.debug("detected UPDATE for server: " + detection.getIdentity());
                  }
                  mbeanserver.invoke(registryObjectName, "updateServer", new Object[]{detection.getIdentity(), detection.getLocators()}, new String[]{Identity.class.getName(), InvokerLocator[].class.getName()});
                  fireUpdatedDetectionNotification(detection);
               }
            }
         }
         catch (javax.management.InstanceNotFoundException inf)
         {
            return;
         }
         catch (Exception e)
         {
            log.error("Error during detection of: " + detection, e);
         }
      }
      else if (DETECTOR_DEBUG && log.isDebugEnabled())
      {
         log.debug("detection from myself - ignored");
      }
   }

   private void fireNewDetectionNotification(Detection detection)
   {
      Notification not = new Notification(SERVER_FOUND, this, getNextNotificationSequenceNumber(),
            System.currentTimeMillis(), "Found a new server on the network.");
      not.setUserData(detection);
      broadcaster.sendNotification(not);
   }

   private void fireUpdatedDetectionNotification(Detection detection)
   {
      Notification not = new Notification(SERVER_UPDATED, this, getNextNotificationSequenceNumber(),
            System.currentTimeMillis(), "Updated a server on the network.");
      not.setUserData(detection);
      broadcaster.sendNotification(not);
   }

   protected boolean isRemoteDetection(Detection detection)
   {
      String domain = detection.getIdentity().getDomain();
      // is detection domain in accepted domain collection and not local
      // if domains empty, then accept all
      return (domains.isEmpty() || domains.contains(domain)) &&
            myself.isSameJVM(detection.getIdentity()) == false;
   }

   protected boolean checkInvokerServer(Detection detection, ClassLoader cl)
   {
      boolean ok = false;
      boolean domainChange = false;
      InvokerLocator il[] = detection.getLocators();
      for (int c = 0; c < il.length; c++)
      {
         try
         {
            ClientInvoker ci = InvokerRegistry.createClientInvoker(il[c]);

            if (ci.isDestroyed())
            {
               // check the next invoker, this one's no good
               InvokerRegistry.destroyClientInvoker(il[c]);
               continue;
            }

            if (ci.getLastInvocationTimestamp() > 0)
            {
               long invocationAge = System.currentTimeMillis() - ci.getLastInvocationTimestamp();

               if (invocationAge < invocationDelay)
               {
                  // we've made an invocation within our quiet period, don't ping
                  ok = true;
                  break;
               }
            }

            if (ci.isConnected() == false)
            {
               ci.connect();
            }

            Identity ident = (Identity) ci.invoke(new InvocationRequest(getClass().getName(), Subsystem.SELF, "$PING$", null, null, null));
            if (detection.getIdentity().equals(ident))
            {
               ok = true;
               break;
            }
            else
            {
               if (log.isDebugEnabled())
               {
                  log.debug("detection for: " + ident + " didn't match what we expected: " + detection.getIdentity() + ", this is most likely because the locator is still valid but the JMX server restarted since we last detected it");
               }
               if (detection.getIdentity().getInstanceId().equals(ident.getInstanceId()) &&
                     detection.getIdentity().getAddress().equals(ident.getAddress()) &&
                     detection.getIdentity().getDomain().equals(ident.getDomain()) == false)
               {
                  if (log.isDebugEnabled())
                  {
                     log.debug("detection for: " + ident + " has changed domain from: " + detection.getIdentity().getDomain() + " to: " + ident.getDomain());
                  }
                  domainChange = true;
                  break;
               }
               break;
            }
         }
         catch (Throwable ig)
         {
            log.debug("failed calling ping on " + detection, ig);
            // remove the client invoker, it's not any good anymore
            InvokerRegistry.destroyClientInvoker(il[c]);
            // will be aggressive with removal.  if any fail, remove it.
            // if still good, will pick up detection again next go around.
            break;
         }
      }
      if (ok == false)
      {
         // the server is down!
         removeServer(detection, domainChange);
      }

      return ok;
   }

   /**
    * Remove the server from the registry and fire lost detection notification that is
    * associated with the specified detection.
    *
    * @param detection
    */
   private void removeServer(Detection detection, boolean domainChange)
   {
      try
      {
         if (domainChange == false)
         {
            mbeanserver.invoke(registryObjectName, "removeServer", new Object[]{detection.getIdentity()}, new String[]{Identity.class.getName()});
            if (log.isDebugEnabled())
            {
               log.debug("Removed detection " + detection);
            }
         }
      }
      catch (Exception ex)
      {
         log.warn("Error removing server: " + detection, ex);
      }
      finally
      {
         // remove this server, it isn't available any more
         servers.remove(detection);
         if (domainChange == false)
         {
            fireLostDetectionNotification(detection);
         }
         else
         {
            try
            {
               mbeanserver.invoke(registryObjectName, "updateServer", new Object[]{detection.getIdentity(), detection.getLocators()}, new String[]{Identity.class.getName(), InvokerLocator[].class.getName()});
            }
            catch (Exception ex)
            {
               log.warn("Error updating server with: " + detection, ex);
            }
         }
      }
   }

   private void fireLostDetectionNotification(Detection detection)
   {
      Notification not = new Notification(SERVER_LOST, this, getNextNotificationSequenceNumber(),
            System.currentTimeMillis(), "Lost server on the network.");
      not.setUserData(detection);
      broadcaster.sendNotification(not);
   }

   /**
    * Will add the specified domain to the list of domains that the
    * detector will look for when doing detection.
    *
    * @param domainName
    */
   public void addDomain(String domainName)
   {
      domains.add(domainName);
   }

   /**
    * Will remove the specified domain from the list of domains that the
    * detector will look for when doing detection.  Note, this will
    * cause servers already found by the detector to be invalidated and will
    * appear as though they have gone down.
    *
    * @param domainName
    */
   public void removeDomain(String domainName)
   {
      domains.remove(domainName);
      removeServersByDomain(domainName);
   }

   /**
    * Search the collection of servers already found and remove
    * any that have a domain matching the one specified.
    *
    * @param domainName
    */
   private void removeServersByDomain(String domainName)
   {
      synchronized (servers)
      {
         Set detections = servers.keySet();
         Iterator itr = detections.iterator();
         while (itr.hasNext())
         {
            Detection det = (Detection) itr.next();
            if (det.getIdentity().getDomain().equals(domainName))
            {
               // need to remove it
               removeServer(det, false);
            }
         }
      }
   }

   /**
    * Add a listener to an MBean.
    *
    * @param listener implementation of the listener object
    * @param filter   implementation of the filter object or <tt>null</tt>
    *                 if no filtering is required
    * @param handback A handback object associated with each notification
    *                 sent by this notification broadcaster.
    * @throws IllegalArgumentException if listener is <tt>null</tt>
    */
   public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws IllegalArgumentException
   {
      broadcaster.addNotificationListener(listener, filter, handback);
   }

   /**
    * Removes a listener from an MBean.
    *
    * @param listener the listener object to remove
    * @throws javax.management.ListenerNotFoundException
    *          if the listener was not found
    */
   public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException
   {
      broadcaster.removeNotificationListener(listener);
   }

   /**
    * Returns the notification metadata associated with the MBean.
    *
    * @return MBean's notification metadata
    * @see javax.management.MBeanNotificationInfo
    */
   public MBeanNotificationInfo[] getNotificationInfo()
   {
      return new MBeanNotificationInfo[0];
   }

   private final class FailureDetector extends TimerTask
   {
      public void run()
      {
         if (servers.isEmpty())
         {
            return;
         }
         // make a copy so we don't have to block incoming
         // notifications during failure check
         Map map = null;
         synchronized (servers)
         {
            map = new HashMap(servers);
         }
         ClassLoader cl = AbstractDetector.this.getClass().getClassLoader();
         // walk through each detection and see if it needs checking up on ...
         Iterator iter = map.keySet().iterator();
         while (iter.hasNext())
         {
            Detection detection = (Detection) iter.next();
            long lastDetection = 0;
            Server server = null;
            synchronized (servers)
            {
               server = (Server) map.get(detection);
               lastDetection = server.lastDetection;
            }
            long duration = System.currentTimeMillis() - lastDetection;
            if (duration >= maxTimeDelay)
            {
               if (log.isDebugEnabled())
               {
                  log.debug("detection for: " + detection + " has not been received in: " + maxTimeDelay + " ms, contacting..");
               }
               // OK, we've exceeded the time delay since the last time we've detected
               // this dude, he might be down, let's walk through each of his transports and
               // see if any of them lead to a valid invocation
               if (checkInvokerServer(detection, cl))
               {
                  if (log.isDebugEnabled())
                  {
                     log.debug("detection for: " + detection + " recovered on ping");
                  }
                  server.lastDetection = System.currentTimeMillis();
               }
            }
         }
      }

   }

   private final class Server
   {
      Detection detection;
      private int hashCode = 0;
      long lastDetection = System.currentTimeMillis();

      Server(Detection detection)
      {
         rehash(detection);
      }


      private void rehash(Detection d)
      {
         this.hashCode = hash(d);
      }

      private int hash(Detection d)
      {
         int hc = 0;
         InvokerLocator locators[] = d.getLocators();
         if (locators != null)
         {
            for (int c = 0; c < locators.length; c++)
            {
               hc += locators[c].hashCode();
            }
         }
         return hc;
      }

      boolean changed(Detection detection)
      {
         return hashCode != hash(detection);
      }

      public boolean equals(Object obj)
      {
         return obj instanceof Server && hashCode == obj.hashCode();
      }

      public int hashCode()
      {
         return hashCode;
      }
   }

   private final class Heartbeat extends TimerTask
   {
      public void run()
      {
         InvokerLocator il[] = InvokerRegistry.getRegisteredServerLocators();
         if (il != null && il.length > 0)
         {
            // we only heartbeat if we have connectors and the ability for a
            // client to reach us back, otherwise its sort of a mute point ..
            heartbeat();
         }
      }
   }
}
