/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.jmx.adaptor.snmp.agent;

import java.io.InputStream;
import java.net.InetAddress;
import java.util.Iterator;
import java.util.List;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.management.Attribute;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.jboss.jmx.adaptor.snmp.config.attribute.AttributeMappings;
import org.jboss.jmx.adaptor.snmp.config.attribute.ManagedBean;
import org.jboss.jmx.adaptor.snmp.config.attribute.MappedAttribute;
import org.jboss.logging.Logger;
import org.jboss.xb.binding.ObjectModelFactory;
import org.jboss.xb.binding.Unmarshaller;
import org.jboss.xb.binding.UnmarshallerFactory;
import org.opennms.protocols.snmp.SnmpAgentSession;
import org.opennms.protocols.snmp.SnmpInt32;
import org.opennms.protocols.snmp.SnmpNull;
import org.opennms.protocols.snmp.SnmpObjectId;
import org.opennms.protocols.snmp.SnmpOctetString;
import org.opennms.protocols.snmp.SnmpPduPacket;
import org.opennms.protocols.snmp.SnmpPduRequest;
import org.opennms.protocols.snmp.SnmpSyntax;
import org.opennms.protocols.snmp.SnmpUInt32;
import org.opennms.protocols.snmp.SnmpVarBind;

/**
 * Implement RequestHandler with mapping of snmp get/set requests
 * to JMX mbean attribute gets/sets
 *
 * @author <a href="mailto:hwr@pilhuhn.de>">Heiko W. Rupp</a>
 * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
 * @version $Revision: 57210 $
 */
public class RequestHandlerImpl extends RequestHandlerSupport
   implements Reconfigurable
{
	// Protected Data ------------------------------------------------

	private static final String NO_ENTRY_FOUND_FOR_OID = "No entry found for oid ";
	private static final String SKIP_ENTRY = " - skipping entry";


	/** Bindings from oid to mbean */
	protected SortedMap bindings = new TreeMap();

	private SortedSet oidKeys = null;

	/** Has this RequestHandler instance been initialized? */
	private boolean initialized = false;

	// Constructors --------------------------------------------------

	/**
	 * Default CTOR
	 */
	public RequestHandlerImpl() 
	{
		bindings = new TreeMap();
		oidKeys = new TreeSet();
	}

	// RequestHandler Implementation ---------------------------------

	/**
	 * Initialize
	 * 
	 * @param resourceName A file containing get/set mappings
	 * @param server Our MBean-Server
	 * @param log The logger we use
	 * @param uptime The uptime of the snmp-agent subsystem.
	 */
	public void initialize(String resourceName, MBeanServer server, Logger log, Clock uptime)
      throws Exception
   {
      log.debug("initialize() with res=" + resourceName);
	   super.initialize(resourceName, server, log, uptime);
		if (resourceName != null)
			initializeBindings();
		else
			log.warn("No RequestHandlerResName configured, disabling snmp-get");

		initialized = true;
	}

   // Reconfigurable Implementation ---------------------------------
   /**
    * Reconfigures the RequestHandler
    */
   public void reconfigure(String resName) throws Exception
   {
      if (resName == null || resName.equals(""))
         throw new IllegalArgumentException("Null or empty resName, cannot reconfigure");

      if (initialized == false)
         throw new IllegalStateException("Cannot reconfigure, not initialized yet");
      
      this.resourceName = resName;
   
      // Wipe out old entries
      bindings.clear();
      
      // Fetch them again
      initializeBindings();
   }
   
	// SnmpAgentHandler Implementation -------------------------------

	/**
	 * <P>
	 * This method is defined to handle SNMP Get requests that are received by
	 * the session. The request has already been validated by the system. This
	 * routine will build a response and pass it back to the caller.
	 * </P>
	 * 
	 * @param pdu
	 *            The SNMP pdu
	 * @param getNext
	 *            The agent is requesting the lexically NEXT item after each
	 *            item in the pdu.
	 * 
	 * @return SnmpPduRequest filled in with the proper response, or null if
	 *         cannot process NOTE: this might be changed to throw an exception.
	 */
	public SnmpPduRequest snmpReceivedGet(SnmpPduPacket pdu, boolean getNext)
	{
		try
		{
			SnmpPduRequest response = null;
			int pduLength = pdu.getLength();
			final boolean trace = log.isTraceEnabled();

			if (trace)
				log.trace("requestId=" + pdu.getRequestId() + ", pduLength="
						+ pduLength + ", getNext=" + getNext);

			SnmpVarBind[] vblist = new SnmpVarBind[pduLength];
			int errorStatus = SnmpPduPacket.ErrNoError;
			int errorIndex = 0;

			// Process for each varibind in the request
			for (int i = 0; i < pduLength; i++)
			{
				boolean good = true;
				SnmpVarBind vb = pdu.getVarBindAt(i);
				SnmpObjectId oid = vb.getName();
				if (getNext)
				{
					/*
					 * We call getNextOid() to find out what is the next valid OID
					 * instance in the supported MIB (sub-)tree. Assign that OID to the
					 * VB List and then proceed same as that of get request. If the
					 * passed oid is already the last, we flag it.
					 */
					ComparableSnmpObjectId coid = new ComparableSnmpObjectId(oid);
					oid = getNextOid(coid, true);
					if (oid == null)
					{
						good = false;
					}
					else
					{
						pdu.setVarBindAt(i, new SnmpVarBind(oid));
					}
				}
				if (oid!=null)
					vblist[i] = new SnmpVarBind(oid);
				else
					vblist[i] = new SnmpVarBind(vb.getName()); // oid passed in
				

				if (trace)
					log.trace("oid=" + oid);

				SnmpSyntax result = null;
				if (good && bindings != null)
					result = getValueFor(oid);

				if (trace)
					log.trace("got result of " + result);

				if (result == null || !good)
				{
					errorStatus = SnmpPduPacket.ErrNoSuchName;
					errorIndex = i + 1;
					log.debug("Error Occured " + vb.getName().toString());
				} 
				else
				{
					vblist[i].setValue(result);
					log.debug("Varbind[" + i + "] := "
									+ vblist[i].getName().toString());
					log.debug(" --> " + vblist[i].getValue().toString());
				}
			} // for ...
			response = new SnmpPduRequest(SnmpPduPacket.RESPONSE, vblist);
			response.setErrorStatus(errorStatus);
			response.setErrorIndex(errorIndex);
			return response;
		} catch (Exception e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * <P>
	 * This method is defined to handle SNMP Set requests that are received by
	 * the session. The request has already been validated by the system. This
	 * routine will build a response and pass it back to the caller.
	 * </P>
	 * 
	 * @param pdu
	 *           The SNMP pdu
	 * 
	 * @return SnmpPduRequest filled in with the proper response, or null if
	 *         cannot process NOTE: this might be changed to throw an exception.
	 */
	public SnmpPduRequest snmpReceivedSet(SnmpPduPacket pdu)
   {
		final boolean trace = log.isTraceEnabled();
		SnmpPduRequest response = null;
		int errorStatus = SnmpPduPacket.ErrNoError;
		int errorIndex = 0;
		int k = pdu.getLength();
		SnmpVarBind[] vblist = new SnmpVarBind[k];

		for (int i = 0; i < k; i++)
      {
			SnmpVarBind vb = pdu.getVarBindAt(i);
			vblist[i] = new SnmpVarBind(vb);
			SnmpObjectId oid = vb.getName();
			SnmpSyntax newVal = vb.getValue();
			if (trace)
				log.trace("set: received oid " + oid.toString() + " with value " + newVal.toString());
			SnmpSyntax result = null;
			try
         {
				result = setValueFor(oid,newVal);
			}
         catch (ReadOnlyException e)
         {
				errorStatus = SnmpPduPacket.ErrReadOnly;
				errorIndex = i + 1;
			}

			 if (result != null)
			 {
				errorStatus = SnmpPduPacket.ErrReadOnly;
				errorIndex = i + 1;
				log.debug("Error occured " + vb.getName().toString());
			 }

			 if (trace)
          {
				 log.trace("Varbind[" + i + "] := " + vb.getName().toString());
				 log.trace(" --> " + vb.getValue().toString());
			 }
		}
		response = new SnmpPduRequest(SnmpPduPacket.RESPONSE, vblist);
		response.setErrorStatus(errorStatus);
		response.setErrorIndex(errorIndex);

		return response;
	}

	/**
	 * <P>
	 * This method is defined to handle SNMP requests that are received by the
	 * session. The parameters allow the handler to determine the host, port,
	 * and community string of the received PDU
	 * </P>
	 * 
	 * @param session
	 *            The SNMP session
	 * @param manager
	 *            The remote sender
	 * @param port
	 *            The remote senders port
	 * @param community
	 *            The community string
	 * @param pdu
	 *            The SNMP pdu
	 * 
	 */
	public void snmpReceivedPdu(SnmpAgentSession session, InetAddress manager,
			int port, SnmpOctetString community, SnmpPduPacket pdu)
   {
		log.error("Message from manager " + manager.toString() + " on port " + port);
		int cmd = pdu.getCommand();
		log.error("Unsupported PDU command......... " + cmd);
	}

	/**
	 * <P>
	 * This method is invoked if an error occurs in the session. The error code
	 * that represents the failure will be passed in the second parameter,
	 * 'error'. The error codes can be found in the class SnmpAgentSession
	 * class.
	 * </P>
	 * 
	 * <P>
	 * If a particular PDU is part of the error condition it will be passed in
	 * the third parameter, 'pdu'. The pdu will be of the type SnmpPduRequest or
	 * SnmpPduTrap object. The handler should use the "instanceof" operator to
	 * determine which type the object is. Also, the object may be null if the
	 * error condition is not associated with a particular PDU.
	 * </P>
	 * 
	 * @param session
	 *            The SNMP Session
	 * @param error
	 *            The error condition value.
	 * @param ref
	 *            The PDU reference, or potentially null. It may also be an
	 *            exception.
	 */
	public void SnmpAgentSessionError(SnmpAgentSession session, int error, Object ref)
   {
		log.error("An error occured in the trap session");
		log.error("Session error code = " + error);
		if (ref != null)
      {
			log.error("Session error reference: " + ref.toString());
		}

		if (error == SnmpAgentSession.ERROR_EXCEPTION)
      {
			synchronized (session)
         {
				session.notify(); // close the session
			}
		}
	}

   // Private -------------------------------------------------------
   
	/**
	 * Initialize the bindings from the file given in resourceName
	 */
	private void initializeBindings() throws Exception
   {
      log.debug("Reading resource: '" + resourceName + "'");
      
      ObjectModelFactory omf = new AttributeMappingsBinding();
      InputStream is = null;
      AttributeMappings mappings = null;
      try
      {
         // locate resource
         is = getClass().getResourceAsStream(resourceName);
         
         // create unmarshaller
         Unmarshaller unmarshaller = UnmarshallerFactory.newInstance().newUnmarshaller();

         // let JBossXB do it's magic using the AttributeMappingsBinding
         mappings = (AttributeMappings)unmarshaller.unmarshal(is, omf, null);         
      }
      catch (Exception e)
      {
         log.error("Accessing resource '" + resourceName + "'");
         throw e;
      }
      finally
      {
         if (is != null)
         {
            // close the XML stream
            is.close();            
         }
      }
      if (mappings == null)
      {
         log.warn("No bindings found in " + resourceName);
         return;         
      }
      log.debug("Found " + mappings.size() + " attribute mappings"); 		
		/**
		 * We have the MBeans now. Put them into the bindungs.
		 */

		Iterator it = mappings.iterator();
		while (it.hasNext())
      {
		   ManagedBean mmb = (ManagedBean)it.next();
		   String oidPrefix = mmb.getOidPrefix();
		   List attrs = mmb.getAttributes();
		   Iterator aIt = attrs.iterator();
		   while (aIt.hasNext())
		   {
		      MappedAttribute ma = (MappedAttribute)aIt.next();
		      String oid;
		      if (oidPrefix != null)
		         oid = oidPrefix + ma.getOid();
		      else
		         oid = ma.getOid();
           
		      BindEntry be = new BindEntry(oid, mmb.getName(), ma.getName());
		      be.isReadWrite = ma.isReadWrite();
  			  
  			  ComparableSnmpObjectId coid = new ComparableSnmpObjectId(oid);
  			  
		      if (log.isTraceEnabled())
		         log.trace("New bind entry   " + be);
  			  if (bindings.containsKey(coid)) {
		         log.info("Duplicate oid " + oid + SKIP_ENTRY);
		         continue;
		      }
		      if (mmb.getName() == null || mmb.getName().equals(""))
		      {
		         log.info("Invalid mbean name for oid " + oid + SKIP_ENTRY);
		         continue;
		      }
		      if (ma.getName() == null || ma.getName().equals(""))
		      {
		         log.info("Invalid attribute name " + ma.getName() + " for oid " + oid + SKIP_ENTRY);
		         continue;
		      }
  			  bindings.put(coid, be);
  			  oidKeys.add(coid);
  			  
		   }
      }
   }

	/**
	 * Return the current value for the given oid
	 * 
	 * @param oid
	 *            The oid we want a value for
	 * @return SnmpNull if no value present
	 */
	private SnmpSyntax getValueFor(final SnmpObjectId oid) {

		BindEntry be = findBindEntryForOid(oid);
		SnmpSyntax ssy = null;
		if (be != null)
      {
			if (log.isTraceEnabled())
				log.trace("Found entry " + be.toString() + " for oid " + oid);
         
			try
         {
			   Object val = server.getAttribute(be.mbean, be.attr.getName());

				if (val instanceof Long)
            {
					Long uin = (Long) val;
					ssy = new SnmpUInt32(uin);
				}
            else if (val instanceof String)
            {
					String in = (String) val;
					ssy = new SnmpOctetString(in.getBytes());
				}
            else if (val instanceof Integer)
            {
					Integer in = (Integer) val;
					ssy = new SnmpInt32(in);
				}
            else if (val instanceof SnmpObjectId)
            {
					ssy = (SnmpObjectId)val;
				}
            else
					log.info("Unknown type for " + be);
			}
         catch (Exception e)
         {
				log.warn("getValueFor (" + be.mbean.toString() + ", "
						+ be.attr.getName() + ": " + e.toString());
         }
      }
      else
      {
			ssy = new SnmpNull();
			log.info(NO_ENTRY_FOUND_FOR_OID + oid);
		}
		return ssy;
	}
	
	/**
	 * Set a jmx attribute
	 * @param oid The oid to set. This is translated into a mbean / attribute pair
	 * @param newVal The new value to set
	 * @return null on success, non-null on failure
	 * @throws ReadOnlyException If the referred entry is read only.
	 */
	private SnmpSyntax setValueFor(final SnmpObjectId oid, final SnmpSyntax newVal) throws ReadOnlyException
   {
		final boolean trace = log.isTraceEnabled();
		
		BindEntry be = findBindEntryForOid(oid);
		
		if (trace)
			log.trace("setValueFor: found bind entry for " + oid);
		
		SnmpSyntax ssy = null;
		if (be != null)
      {
			if (trace)
				log.trace("setValueFor: " + be.toString());
         
			if (be.isReadWrite == false)
         {
				if (trace)
					log.trace("setValueFor: this is marked read only");
            
				throw new ReadOnlyException(oid);
			}
			try
         {
				Object val = null;
				if (newVal instanceof SnmpOctetString)
            {
					val = newVal.toString();
				}
				else if (newVal instanceof SnmpInt32)
            {
					val = new Integer(((SnmpInt32)newVal).getValue());
				}
				else if (newVal instanceof SnmpUInt32)
            {
					val = new Long(((SnmpUInt32)newVal).getValue());
				}
				// TODO do more mumbo jumbo for type casting / changing
				
				if (val != null)
            { 
					Attribute at = new Attribute(be.attr.getName(), val);
					server.setAttribute(be.mbean, at);
					if (trace)
						log.trace("setValueFor: set attribute in mbean-Server");
				}
				else
            {
					log.debug("Did not find a suitable data type for newVal " + newVal);
					ssy = new SnmpNull();
				}
				// TODO
			}
			catch (Exception e )
         {
				log.debug("setValueFor: exception " + e.getMessage());
				ssy = new SnmpNull();
			}
		}
		else
      {
			ssy = new SnmpNull();
			log.info(NO_ENTRY_FOUND_FOR_OID + oid);
		}
		return ssy;
	}


	/**
	 * Lookup a BinEntry on the given oid. If the oid ends in .0,
	 * then the .0 will be stripped of before the search.
	 * @param oid The oid look up.
	 * @return a bind entry or null.
	 */
	private BindEntry findBindEntryForOid(final SnmpObjectId oid) {
		
		ComparableSnmpObjectId coid= new ComparableSnmpObjectId(oid);
		
		if (coid.isLeaf())
		{
			coid = coid.removeLastPart();
		}
		BindEntry be = (BindEntry)bindings.get(coid);

		return be;
	}

	/**
	 * Return the next oid that is larger than ours.
	 * @param oid the starting oid
	 * @param stayInSubtree if true, the next oid will not have a different prefix than the one of oid.
	 * @return the next oid or null if none found.
	 */
	private ComparableSnmpObjectId getNextOid(final ComparableSnmpObjectId oid, boolean stayInSubtree) {
		ComparableSnmpObjectId coid = new ComparableSnmpObjectId(oid);


		if (coid.isLeaf())
			coid = coid.removeLastPart();

		SortedSet ret;
		ret= oidKeys.tailSet(oid);  // get oids >= oid
		Iterator it = ret.iterator();
		ComparableSnmpObjectId roid=null;
		
		/*
		 * If there are elements in the tail set, then
		 * - get first one.
		 * - if first is input (which it is supposed to be according to the contract of
		 *   SortedSet.tailSet() , then get next, which is the 
		 *   one we look for.
		 */
		if (it.hasNext()) 
		{
			roid = (ComparableSnmpObjectId)it.next(); // oid
		}
		
		if (roid == null)
		{
			return null; // roid is null, 
		}
		
		if (roid.compareTo(coid)==0) // input elment
		{
			// if there is a next element, then it is ours.
			if (it.hasNext()) 
			{
				roid = (ComparableSnmpObjectId)it.next();
			}
			else
			{
				roid = null; // end of list
			}
		}
      
		/*
		 * Check if still in subtree if requested to stay within
		 */
		if (stayInSubtree && roid != null)
		{
			ComparableSnmpObjectId parent = coid.removeLastPart();
			if (!parent.isRootOf(roid))
				roid = null;
		}

		return roid;
	}


   // Inner Class ---------------------------------------------------
   
	/**
	 * An entry containing the mapping between oid and mbean/attribute
	 * 
	 * @author <a href="mailto:pilhuhn@user.sf.net>">Heiko W. Rupp</a>
	 */
	private class BindEntry implements Comparable {
		private final ComparableSnmpObjectId oid;

		private ObjectName mbean;
      private Attribute attr;
		private String mName;
      private String aName;      
		private boolean isReadWrite = false;

		/**
		 * Constructs a new BindEntry
		 * 
		 * @param oid
		 *            The SNMP-oid, this entry will use.
		 * @param mbName
		 *            The name of an MBean with attribute to query
		 * @param attrName
		 *            The name of the attribute to query
		 */
		BindEntry(final String oidString, final String mbName, final String attrName) {
			this(new ComparableSnmpObjectId(oidString), 
					mbName,
					attrName);
		}
		
		/**
		 * Constructs a new BindEntry.
		 * @param coid The SNMP-oid, this entry will use.
		 * @param mbName The name of an MBean with attribute to query
		 * @param attrName The name of the attribute to query
		 */
		BindEntry(final ComparableSnmpObjectId coid, final String mbName, final String attrName) {
			oid = coid;
			this.mName = mbName;
			this.aName = attrName;
			try
         {
			   mbean = new ObjectName(mbName);
				attr = new Attribute(attrName, null);

			}
         catch (Exception e)
         {
            log.warn(e.toString());
				mName = "-unset-";
				aName = "-unset-";
			}
		}

		/**
		 * A string representation of this BindEntry
		 */
		public String toString() {
			StringBuffer buf = new StringBuffer();
			buf.append("[oid=");
			buf.append(oid).append(", mbean=");
			buf.append(mName).append(", attr=");
			buf.append(aName).append(", rw=");
			buf.append(isReadWrite).append("]");

			return buf.toString();
		}

		public Attribute getAttr() {
			return attr;
		}

		public ObjectName getMbean()
      {
			return mbean;
		}

		public ComparableSnmpObjectId getOid()
      {
			return oid;
		}


		/**
		 * Compare two BindEntries. Ordering is defined at oid-level.
		 * 
		 * @param other
		 *            The BindEntry to compare to.
		 * @return 0 on equals, 1 if this is bigger than other
		 */
		public int compareTo(Object other)
      {
			if (other == null)
				throw new NullPointerException("Can't compare to NULL");

			if (!(other instanceof BindEntry))
				throw new ClassCastException("Parameter is no BindEntry");

			// trivial case
			if (this.equals(other))
				return 0;
         
			BindEntry obe = (BindEntry) other;
			if (getOid().equals(obe.getOid()))
				return 0;

			int res =oid.compare(obe.getOid());
			return res;
		}

	}

}
