/*
 *
 * Copyright (c) 2007, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
// Copyright (c) 1995-96 by Cisco Systems, Inc.

package com.sun.jmx.snmp.daemon;

// java imports
//
import java.util.Hashtable;
import java.util.logging.Level;
import java.util.Stack;
import java.net.InetAddress;
import java.net.SocketException;

// jmx imports
//
import static com.sun.jmx.defaults.JmxProperties.SNMP_ADAPTOR_LOGGER;
import com.sun.jmx.snmp.SnmpDefinitions;
import com.sun.jmx.snmp.SnmpStatusException;
import com.sun.jmx.snmp.SnmpVarBindList;

/**
 * This class is used for sending INFORM REQUESTS from an agent to a manager.
 *
 * Creates, controls, and manages one or more inform requests.
 *
 * The SnmpSession maintains the list of all active inform requests and inform responses.
 * Each SnmpSession has a dispatcher that is a thread used to service all the inform requests it creates
 * and each SnmpSession uses a separate socket for sending/receiving inform requests/responses.
 *
 * An SnmpSession object is associated with an SNMP adaptor server.
 * It is created the first time an inform request is sent by the SNMP adaptor server
 * and is destroyed (with its associated SnmpSocket) when the SNMP adaptor server is stopped.
 *
 */

class SnmpSession implements SnmpDefinitions, Runnable {

    // PRIVATE VARIABLES
    //------------------

    /**
     * The SNMP adaptor associated with this SnmpSession.
     */
    protected transient SnmpAdaptorServer adaptor;
    /**
     * The SnmpSocket to be used to communicate with the manager
     * by all inform requests created in this session.
     */
    protected transient SnmpSocket informSocket = null;
    /**
     * This table maintains the list of inform requests.
     */
    private transient Hashtable<SnmpInformRequest, SnmpInformRequest> informRequestList =
            new Hashtable<>();
    /**
     * This table maintains the list of inform responses.
     * A FIFO queue is needed here.
     */
    private transient Stack<SnmpInformRequest> informRespq =
            new Stack<>();
    /**
     * The dispatcher that will service all inform responses to inform requests generated
     * using this session object. An SnmpSession object creates one or more inform requests.
     * Thus it services all inform requests, which are created by this session object,
     * when an inform response arrives for an inform request generated by the session.
     */
    private transient Thread myThread = null;
    /**
     * Request being synchronized from session thread. This happens when
     * a user does sync operation from a callback.
     */
    private transient SnmpInformRequest syncInformReq ;

    SnmpQManager snmpQman = null;

    private boolean isBeingCancelled = false;

    // PUBLIC CONSTRUCTORS
    //--------------------

    /**
     * Constructor for creating a new session.
     * @param adp The SNMP adaptor associated with this SnmpSession.
     * @exception SocketException Unable to initialize the SnmpSocket.
     */
    public SnmpSession(SnmpAdaptorServer adp) throws SocketException {
        adaptor = adp;
        snmpQman = new SnmpQManager();
        SnmpResponseHandler snmpRespHdlr = new SnmpResponseHandler(adp, snmpQman);
        initialize(adp, snmpRespHdlr);
    }
    /**
     * Constructor for creating a new session. Allows subclassing.
     */
    public SnmpSession() throws SocketException {
    }
    // OTHER METHODS
    //--------------
    /**
     * Initializes the SnmpSession.
     * @param adp The SNMP adaptor associated with this SnmpSession.
     * @exception SocketException Unable to initialize the SnmpSocket.
     */
    protected synchronized void initialize(SnmpAdaptorServer adp,
                                           SnmpResponseHandler snmpRespHdlr)
        throws SocketException {
        informSocket = new SnmpSocket(snmpRespHdlr, adp.getAddress(), adp.getBufferSize().intValue());

        myThread = new Thread(this, "SnmpSession");
        myThread.start();
    }

    /**
     * Indicates whether the thread for this session is active and the SNMP adaptor server ONLINE.
     * @return true if active, false otherwise.
     */
    synchronized boolean isSessionActive() {
        //return ((myThread != null) && (myThread.isAlive()));
        return ((adaptor.isActive()) && (myThread != null) && (myThread.isAlive()));
    }

    /**
     * Gets the SnmpSocket which will be used by inform requests created in this session.
     * @return The socket which will be used in this session.
     */
    SnmpSocket getSocket() {
        return informSocket;
    }

    /**
     * Gets the SnmpQManager which will be used by inform requests created in this session.
     * @return The SnmpQManager which will be used in this session.
     */
    SnmpQManager getSnmpQManager() {
        return snmpQman;
    }

    /**
     * Indicates whether this session is performing synchronous operation for an inform request.
     * @return <CODE>true</CODE> if the session is performing synchronous operation, <CODE>false</CODE> otherwise.
     */
    private synchronized boolean syncInProgress() {
        return syncInformReq != null ;
    }

    private synchronized void setSyncMode(SnmpInformRequest req) {
        syncInformReq = req ;
    }

    private synchronized void resetSyncMode() {
        if (syncInformReq == null)
            return ;
        syncInformReq = null ;
        if (thisSessionContext())
            return ;
        this.notifyAll() ;
    }

    /**
     * Returns <CODE>true</CODE> if the current executing thread is this session's dispatcher.
     * Typically used to detect whether the user is doing a sync operation from
     * this dispatcher context. For instance, a user gives a sync command
     * from within a request callback using its associated session.
     * @return <CODE>true</CODE> if current thread is this session's dispatcher, <CODE>false</CODE> otherwise.
     */
    boolean thisSessionContext() {
        return (Thread.currentThread() == myThread) ;
    }

    /**
     * Sends an inform request to the specified InetAddress destination using the specified community string.
     * @param addr The InetAddress destination for this inform request.
     * @param cs The community string to be used for the inform request.
     * @param cb The callback that is invoked when a request is complete.
     * @param vblst A list of SnmpVarBind instances or null.
     * @exception SnmpStatusException SNMP adaptor is not ONLINE or session
     *            is dead.
     */
    SnmpInformRequest makeAsyncRequest(InetAddress addr, String cs,
                                       SnmpInformHandler cb,
                                       SnmpVarBindList vblst, int port)
        throws SnmpStatusException {

        if (!isSessionActive()) {
            throw new SnmpStatusException("SNMP adaptor server not ONLINE");
        }
        SnmpInformRequest snmpreq = new SnmpInformRequest(this, adaptor, addr, cs, port, cb);
        snmpreq.start(vblst);
        return snmpreq;
    }

    /**
     * Performs sync operations on active requests. Any number of inform requests
     * can be done in sync mode but only one per thread.
     * The user can do synchronous operation using the request handle only.
     */
    void waitForResponse(SnmpInformRequest req, long waitTime) {

        if (! req.inProgress())
            return ;
        setSyncMode(req) ;
        if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
            SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
                "waitForResponse", "Session switching to sync mode for inform request " + req.getRequestId());
        }
        long maxTime ;
        if (waitTime <= 0)
            maxTime = System.currentTimeMillis() + 6000 * 1000 ;
        else
            maxTime = System.currentTimeMillis() + waitTime ;

        while (req.inProgress() || syncInProgress()) {
            waitTime = maxTime - System.currentTimeMillis() ;
            if (waitTime <= 0)
                break ;
            synchronized (this) {
                if (! informRespq.removeElement(req)) {
                    try {
                        this.wait(waitTime) ;
                    } catch(InterruptedException e) {
                    }
                    continue ;
                }
            }
            try {
                processResponse(req) ;
            } catch (Exception e) {
                if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
                    SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
                        "waitForResponse", "Got unexpected exception", e);
                }
            }
        }
        resetSyncMode() ;
    }

    /**
     * Dispatcher method for this session thread. This is the dispatcher method
     * which goes in an endless-loop and waits for servicing inform requests
     * which received a reply from the manager.
     */
    @Override
    public void run() {
        myThread = Thread.currentThread();
        myThread.setPriority(Thread.NORM_PRIORITY);

        SnmpInformRequest reqc = null;
        while (myThread != null) {
            try {
                reqc = nextResponse();
                if (reqc != null) {
                    processResponse(reqc);
                }
            } catch (ThreadDeath d) {
                myThread = null;
                if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
                    SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
                        "run", "ThreadDeath, session thread unexpectedly shutting down");
                }
                throw d ;
            }
        }
        if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
            SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
                "run", "Session thread shutting down");
        }
        myThread = null ;
    }

    private void processResponse(SnmpInformRequest reqc) {

        while (reqc != null && myThread != null) {
            try {
                if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
                    SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
                            "processResponse", "Processing response to req = " + reqc.getRequestId());
                }
                reqc.processResponse() ;  // Handles out of memory.
                reqc = null ;  // finished processing.
            } catch (Exception e) {
                if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
                    SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
                        "processResponse", "Got unexpected exception", e);
                }
                reqc = null ;
            } catch (OutOfMemoryError ome) {
                if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
                    SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
                        "processResponse", "Out of memory error in session thread", ome);
                }
                Thread.yield();
                continue ;   // re-process the request.
            }
        }
    }

    // HANDLING INFORM REQUESTS LIST AND INFORM RESPONSES LIST
    //--------------------------------------------------------

    /**
     * Adds an inform request.
     * @param snmpreq The inform request to add.
     * @exception SnmpStatusException SNMP adaptor is not ONLINE or session is dead.
     */
    synchronized void addInformRequest(SnmpInformRequest snmpreq) throws SnmpStatusException {

        // If the adaptor is not ONLINE, stop adding requests.
        //
        if (!isSessionActive()) {
            throw new SnmpStatusException("SNMP adaptor is not ONLINE or session is dead...") ;
        }
        informRequestList.put(snmpreq, snmpreq);
    }

    /**
     * Deletes an inform request.
     * @param snmpreq The inform request to delete.
     */
    synchronized void removeInformRequest(SnmpInformRequest snmpreq) {
        // deleteRequest can be called from destroySnmpSession.
        //In such a case remove is done in cancelAllRequest method.
        if(!isBeingCancelled)
            informRequestList.remove(snmpreq) ;

        if (syncInformReq != null && syncInformReq == snmpreq) {
            resetSyncMode() ;
        }
    }

    /**
     * Cancels all pending inform requests in this session.
     */
    private void cancelAllRequests() {
        final SnmpInformRequest[] list;

        synchronized(this) {

            if (informRequestList.isEmpty()) {
                return ;
            }

            isBeingCancelled = true;

            list = new SnmpInformRequest[informRequestList.size()];
            java.util.Iterator<SnmpInformRequest> it = informRequestList.values().iterator();
            int i = 0;
            while(it.hasNext()) {
                SnmpInformRequest req = it.next();
                list[i++] = req;
                it.remove();
            }
            informRequestList.clear();
        }

        for(int i = 0; i < list.length; i++)
            list[i].cancelRequest();
    }

    /**
     * Adds the inform request object which received a response to an inform request
     * generated by the session.  This is added to a private store, which
     * will be eventually picked up by the dispatcher for processing.
     * @param reqc The inform request that received the response from the manager.
     */
    void addResponse(SnmpInformRequest reqc) {

        SnmpInformRequest snmpreq = reqc;
        if (isSessionActive()) {
            synchronized(this) {
                informRespq.push(reqc) ;
                this.notifyAll() ;
            }
        } else {
            if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINEST)) {
                SNMP_ADAPTOR_LOGGER.logp(Level.FINEST, SnmpSession.class.getName(),
                    "addResponse", "Adaptor not ONLINE or session thread dead, so inform response is dropped..." + reqc.getRequestId());
            }
        }
    }

    private synchronized SnmpInformRequest nextResponse() {

        if (informRespq.isEmpty()) {
            try {
                if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
                    SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
                       "nextResponse", "Blocking for response");
                }
                this.wait();
            } catch(InterruptedException e) {
            }
        }
        if (informRespq.isEmpty())
            return null;
        SnmpInformRequest reqc = informRespq.firstElement();
        informRespq.removeElementAt(0) ;
        return reqc ;
    }

    private synchronized void cancelAllResponses() {
        if (informRespq != null) {
            syncInformReq = null ;
            informRespq.removeAllElements() ;
            this.notifyAll() ;
        }
    }

    /**
     * Destroys any pending inform requests and then stops the session.
     * The session will not be usable after this method returns.
     */
    final void destroySession() {

        cancelAllRequests() ;
        cancelAllResponses() ;
        synchronized(this) {
            informSocket.close() ;
            informSocket = null ;
        }
        snmpQman.stopQThreads() ;
        snmpQman = null ;
        killSessionThread() ;
    }

    /**
     * Make sure you are killing the thread when it is active. Instead
     * prepare for a graceful exit.
     */
    private synchronized void killSessionThread() {

        if ((myThread != null) && (myThread.isAlive())) {
            if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
                SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
                   "killSessionThread", "Destroying session");
            }
            if (!thisSessionContext()) {
                myThread = null ;
                this.notifyAll() ;
            } else
                myThread = null ;
        }
    }

    /**
     * Finalizer of the <CODE>SnmpSession</CODE> objects.
     * This method is called by the garbage collector on an object
     * when garbage collection determines that there are no more references to the object.
     * <P>Removes all the requests for this SNMP session, closes the socket and
     * sets all the references to the <CODE>SnmpSession</CODE> object to <CODE>null</CODE>.
     */
    @Override
    protected void finalize() {

        if (SNMP_ADAPTOR_LOGGER.isLoggable(Level.FINER)) {
            SNMP_ADAPTOR_LOGGER.logp(Level.FINER, SnmpSession.class.getName(),
               "finalize", "Shutting all servers");
        }

        if (informRespq != null)
            informRespq.removeAllElements() ;
        informRespq = null ;
        if (informSocket != null)
            informSocket.close() ;
        informSocket = null ;

        snmpQman = null ;
    }

}
