/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Nam Nguyen
 */

package com.caucho.server.snmp;

import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.webbeans.In;

import com.caucho.jmx.Jmx;
import com.caucho.server.connection.Connection;
import com.caucho.server.port.ServerRequest;
import com.caucho.server.snmp.types.*;
import com.caucho.server.thread.ResinThreadPoolExecutor;
import com.caucho.util.L10N;
import com.caucho.vfs.ReadStream;

/*
 * Responds to SNMP requests.
 */
public class SnmpRequest implements ServerRequest
{
  private static final Logger log
    = Logger.getLogger(SnmpRequest.class.getName());
  
  private static final L10N L = new L10N(SnmpRequest.class);
  
  public static final int NO_ERROR = 0;
  public static final int TOO_BIG = 1;
  public static final int NO_SUCH_NAME = 2;
  public static final int BAD_VALUE = 3;
  public static final int READ_ONLY = 4;
  public static final int GENERAL_ERROR = 5;
  
  private final Connection _connection;

  private IntegerValue _version = IntegerValue.ZERO;
  private final OctetStringValue _communityString;
  
  private HashMap<String, Oid> _mibMap;
  
  public SnmpRequest(Connection connection,
                     HashMap<String, Oid> mibMap,
                     OctetStringValue community)
  {
    _connection = connection;
    _mibMap = mibMap;
    
    _communityString = community;
  }

  /**
   * Initialize the connection.  At this point, the current thread is the
   * connection thread.
   */
  public void init()
  {
  }

  /**
   * Return true if the connection should wait for a read before
   * handling the request.
   */
  public boolean isWaitForRead()
  {
    return true;
  }
  
  /**
   * Handles a new connection.  The controlling TcpServer may call
   * handleConnection again after the connection completes, so 
   * the implementation must initialize any variables for each connection.
   */
  public boolean handleRequest() 
    throws IOException
  {
    ReadStream in = _connection.getReadStream();
    
    SnmpParser parser = new SnmpParser(in);
    SnmpMessageValue req = parser.readMessage();
    
    checkVersion(req);
    authenticate(req);
    
    SnmpMessageValue response = composeResponse(req);

    sendResponse(response);
    
    return true;
  }
  
  final protected void checkVersion(SnmpMessageValue req)
  {
    if (! _version.equals(req.getVersion())) {
      log.fine(L.l("expected version {0} != {1}",
                   _version,
                   req.getVersion()));
      
      throw new SnmpRuntimeException(L.l("expected version {0} != {1}",
                                         _version,
                                         req.getVersion()));
    }
  }
  
  final protected void authenticate(SnmpMessageValue req)
  {
    if (! _communityString.equals(req.getCommunityString())) {
      log.fine(L.l("non-matching community string"));
      
      throw new SnmpRuntimeException(L.l("non-matching community string"));
    }
  }
  
  final protected void sendResponse(SnmpValue response)
    throws IOException
  {
    OutputStream out = _connection.getWriteStream();
    
    StringBuilder sb = new StringBuilder();
    response.toAsn1(sb);

    for (int i = 0; i < sb.length(); i++) {
      out.write((byte) sb.charAt(i));
    }
    out.flush();
  }
  
  final protected SnmpMessageValue composeResponse(SnmpMessageValue req)
  {
    IntegerValue error = IntegerValue.ZERO;
    IntegerValue errorIndex = IntegerValue.ZERO;
    VarBindListValue varBindList = new VarBindListValue();
    
    int i = -1;
    try {
      switch (req.getPdu().getType()) {
        case SnmpValue.GET_REQUEST_PDU:
        {
          ObjectIdentifierValue []oids = req.getVarBindList().getNames();
          
          for (i = 0; i < oids.length; i++) {
            SnmpValue attr = getMBean(oids[i]);

            VarBindValue varBind;
            
            if (attr != null)
              varBind = new VarBindValue(oids[i], attr);
            else {
              varBind = new VarBindValue(oids[i], NullValue.NULL);
              
              if (error.getLong() == 0) {
                error = new IntegerValue(NO_SUCH_NAME);
                errorIndex = new IntegerValue(i + 1);
              }
            }
            
            varBindList.add(varBind);
          }

          break;
        }
        case SnmpValue.GET_NEXT_REQUEST_PDU:
        {
          break;
        }
        case SnmpValue.SET_REQUEST_PDU:
        {
          break;
        }
        default:
          log.fine(L.l("invalid pdu type {0}", req.getType()));
          
          throw new SnmpRuntimeException(L.l("invalid pdu type {0}",
                                             req.getType()));
      }
    }
    catch (Exception e) {
      if (error.getLong() == 0) {
        error = new IntegerValue(GENERAL_ERROR);
        errorIndex = new IntegerValue(i + 1);
      }
      
      if (req.getVarBindList().size() > i) {
        VarBindValue varBind
          = new VarBindValue(req.getVarBindList().get(i).getName(),
                             new OctetStringValue(e.getMessage()));
        
        varBindList.add(varBind);
      }
    }

    GetResponsePduValue pdu = new GetResponsePduValue(req.getRequestId(),
                                                      error,
                                                      errorIndex,
                                                      varBindList);

    return new SnmpMessageValue(req.getVersion(),
                                req.getCommunityString(),
                                pdu);
    
  }
  
  //@In MBeanServer _jmx;
  protected SnmpValue getMBean(ObjectIdentifierValue objectIdentifier)
  {
    MBeanServer mbeanServer = getMBeanServer();

    Oid oid = _mibMap.get(objectIdentifier.getString());

    if (oid == null)
      return null;

    try {
      ObjectName mbean = new ObjectName(oid.getMbean());

      Object attr = mbeanServer.getAttribute(mbean, oid.getAttribute());
      
      //XXX: complex attributes that are not either Integer or String
      //     i.e. from java.lang:type=Memory mbean
      
      return SnmpValue.create(attr, oid.getType());
    }
    catch (MalformedObjectNameException e) {
      log.fine(e.getMessage());
      
      throw new SnmpRuntimeException(e);
    }
    catch (AttributeNotFoundException e) {
      log.fine(e.getMessage());
      
      return null;
    }
    catch (InstanceNotFoundException e) {
      log.fine(e.getMessage());
      
      return null;
    }
    catch (MBeanException e) {
      log.fine(e.getMessage());
      
      throw new SnmpRuntimeException(e);
    }
    catch (ReflectionException e) {
      log.fine(e.getMessage());
      
      throw new SnmpRuntimeException(e);
    }
    catch (Exception e) {
      log.fine(e.getMessage());
      
      return null;
    }
  }
  
  //@In MBeanServer _jmx;
  protected MBeanServer getMBeanServer()
  {
    return Jmx.getMBeanServer();
  }

  /**
   * Resumes processing after a wait.
   */
  public boolean handleResume() 
    throws IOException
  {
    return false;
  }

  /**
   * Handles a close event when the connection is closed.
   */
  public void protocolCloseEvent()
  {
  }

  /*
   * Sets the SNMP version.
   */
  public void setVersion(int version)
  {
    _version = new IntegerValue(version);
  }
  
  public HashMap<String, Oid> getMib()
  {
    return _mibMap;
  }
}
