/*_############################################################################
  _## 
  _##  SNMP4J-AgentX - AgentX.java  
  _## 
  _##  Copyright (C) 2005-2026  Frank Fock (SNMP4J.org)
  _##  
  _##  This program is free software; you can redistribute it and/or modify
  _##  it under the terms of the GNU General Public License version 2 as 
  _##  published by the Free Software Foundation.
  _##
  _##  This program 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 General Public License for more details.
  _##
  _##  You should have received a copy of the GNU General Public License
  _##  along with this program; if not, write to the Free Software
  _##  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
  _##  MA  02110-1301  USA
  _##  
  _##########################################################################*/

package org.snmp4j.agent.agentx;

import java.io.IOException;
import java.util.*;

import org.snmp4j.TransportMapping;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;
import org.snmp4j.mp.PduHandle;
import org.snmp4j.mp.PduHandleCallback;
import org.snmp4j.smi.Address;

/**
 * The {@code AgentX} class implements the AgentX protocol that sends
 * and receives AgentX PDUs over one or more transport mappings.
 *
 * @author Frank Fock
 * @version 3.0.0
 */
public class AgentX implements AgentXCommandListener {

    private static final LogAdapter logger = LogFactory.getLogger(AgentX.class);

    private final AgentXMessageDispatcher messageDispatcher;

    /**
     * The {@code pendingRequests} table contains pending requests
     * accessed through the key {@code PduHandle}
     */
    private final Map<PduHandle, PendingRequest<?>> pendingRequests =
            Collections.synchronizedMap(new HashMap<>(50));

    /**
     * The {@code asyncRequests} table contains pending requests
     * accessed trough the key userObject
     */
    private final Map<AsyncRequestKey, PduHandle> asyncRequests =
            Collections.synchronizedMap(new HashMap<>(50));

    // Timer for timing out pending requests
    private final Timer timer = new Timer(true);
    private ArrayList<AgentXCommandListener> commandListeners;

    /**
     * Create an {@link AgentX} protocol engine based on the given {@link AgentXMessageDispatcher}.
     * @param dispatcher
     *    a {@link AgentXMessageDispatcher} for dispatching AgentX messages.
     */
    public AgentX(AgentXMessageDispatcher dispatcher) {
        this.messageDispatcher = dispatcher;
        this.messageDispatcher.addCommandListener(this);
    }

    /**
     * Add a {@link TransportMapping} for sending/receiving AgentX messages.
     * @param transport
     *    a {@link TransportMapping}.
     */
    public void addTransportMapping(TransportMapping<?> transport) {
        messageDispatcher.addTransportMapping(transport);
        transport.addTransportListener(messageDispatcher);
    }

    /**
     * Remove a {@link TransportMapping} for sending/receiving AgentX messages.
     * @param transport
     *    the {@link TransportMapping} to be removed.
     */
    public void removeTransportMapping(TransportMapping<?> transport) {
        transport.removeTransportListener(messageDispatcher);
        messageDispatcher.removeTransportMapping(transport);
    }

    /**
     * Removes a {@code AgentXCommandListener} from this AgentX session.
     *
     * @param listener
     *         a previously added {@code AgentXCommandListener} instance.
     */
    public synchronized void removeCommandResponder(AgentXCommandListener listener) {
        ArrayList<AgentXCommandListener> l = commandListeners;
        if (l != null &&
                l.contains(listener)) {
            l = new ArrayList<AgentXCommandListener>(l);
            l.remove(listener);
            commandListeners = l;
        }
    }

    /**
     * Adds a {@code AgentXCommandListener} to this AgentX session.
     * The command responder will then be informed about incoming SNMP PDUs of
     * any kind that are not related to any outstanding requests of this SNMP
     * session.
     *
     * @param listener
     *         the {@code AgentXCommandListener} instance to be added.
     */
    public synchronized void addCommandResponder(AgentXCommandListener listener) {
        ArrayList<AgentXCommandListener> l = (commandListeners == null) ?
                new ArrayList<AgentXCommandListener>(2) : commandListeners;
        if (!l.contains(listener)) {
            l = new ArrayList<AgentXCommandListener>(l);
            l.add(listener);
        }
        commandListeners = l;
    }

    /**
     * Sends a {@code AgentXPDU} to the given target and returns the response
     * synchronously.
     *
     * @param pdu
     *         a {@code AgentXPDU} instance.
     * @param target
     *         the AgentXTarget instance representing the target AgentX entity
     *         where to send the {@code pdu}.
     * @param transport
     *         specifies the {@code TransportMapping} to be used when sending
     *         the PDU. If {@code transport} is {@code null}, the associated
     *         message dispatcher will try to determine the transport mapping by the
     *         {@code target}'s address.
     * @param <A> address type associated with the provided {@link TransportMapping}.
     *
     * @return the received response encapsulated in a {@code AgentXResponseEvent}
     * instance. To obtain the received response {@code AgentXPDU} call
     * {@link AgentXResponseEvent#getResponse()}. If the request timed out,
     * that method will return {@code null}.
     * @throws IOException
     *         if the message could not be sent.
     */
    public <A extends Address> AgentXResponseEvent<A> send(AgentXPDU pdu, AgentXTarget<A> target,
                                    TransportMapping<? super A> transport) throws IOException {
        SyncResponseListener syncResponse = new SyncResponseListener();
        if (!pdu.isConfirmedPDU()) {
            sendMessage(pdu, target, transport, null);
            return null;
        }
        synchronized (syncResponse) {
            PendingRequest<A> request = new PendingRequest<>(null, syncResponse, target, pdu, target);
            sendMessage(pdu, target, transport, request);
            try {
                while (syncResponse.response == null) {
                    syncResponse.wait();
                }
            } catch (InterruptedException iex) {
                logger.warn(iex);
                // ignore
            }
            if (syncResponse.response != null && logger.isDebugEnabled()) {
                logger.debug("Received AgentX response: " + syncResponse.response.getResponse());
            }
        }
        @SuppressWarnings("unchecked")
        AgentXResponseEvent<A> responseEvent = (AgentXResponseEvent<A>) syncResponse.response;
        return responseEvent;
    }

    /**
     * Sends a {@code AgentXPDU} to the given target and returns the response
     * asynchronously.
     *
     * @param pdu
     *         a {@code AgentXPDU} instance.
     * @param target
     *         the AgentXTarget instance representing the target AgentX entity
     *         where to send the {@code pdu}.
     * @param transport
     *         specifies the {@code TransportMapping} to be used when sending
     *         the PDU. If {@code transport} is {@code null}, the associated
     *         message dispatcher will try to determine the transport mapping by the
     *         {@code target}'s address.
     * @param userHandle
     *         an arbitrary user handle which is transparently returned to the response
     *         listener.
     * @param listener
     *         the listener that should be informed about the reponse or timeout.
     * @param <A> address type associated with the provided {@link TransportMapping}.
     *
     * @throws IOException
     *         if the message could not be sent.
     */
    public <A extends Address> void send(AgentXPDU pdu, AgentXTarget<A> target, TransportMapping<? super A> transport,
                                         Object userHandle, AgentXResponseListener listener) throws IOException {
        if (listener != null) {
            PendingRequest<A> request = new PendingRequest<>(null, listener, userHandle, pdu, target);
            sendMessage(pdu, target, transport, request);
        } else {
            sendMessage(pdu, target, transport, null);
        }
    }

    /**
     * Actually sends a PDU to a target and returns a handle for the sent PDU.
     *
     * @param pdu
     *         the {@code PDU} instance to be sent.
     * @param target
     *         a {@code AgentXTarget} instance denoting the target AgentX entity.
     * @param transport
     *         the (optional) transport mapping to be used to send the request.
     *         If {@code transport} is {@code null} a suitable transport
     *         mapping is determined from the {@code target} address.
     * @param pduHandleCallback
     *         an optional callback instance that is informed (if not
     *         {@code null}) about the newly assigned PduHandle just before the
     *         message is sent out.
     * @param <A> address type associated with the provided {@link TransportMapping}.
     *
     * @return PduHandle
     * that uniquely identifies the sent PDU for further reference.
     * @throws IOException
     *         if the transport fails to send the PDU or the if the message cannot
     *         be BER encoded.
     */
    protected <A extends Address> PduHandle sendMessage(AgentXPDU pdu, AgentXTarget<A> target, TransportMapping<? super A> transport,
                                    PduHandleCallback<AgentXPDU> pduHandleCallback)
            throws IOException {
        return messageDispatcher.send(transport, target.getAddress(), pdu, pduHandleCallback);
    }

    @Override
    public <A extends Address> void processCommand(AgentXCommandEvent<A> event) {
        AgentXPDU pdu = event.getCommand();
        PduHandle handle = new PduHandle(pdu.getPacketID());
        if (pdu.getType() == AgentXPDU.AGENTX_RESPONSE_PDU) {
            event.setProcessed(true);
            if (logger.isDebugEnabled()) {
                logger.debug("Removing pending request with handle " + handle);
            }
            @SuppressWarnings("unchecked")
            PendingRequest<A> request = (PendingRequest<A>) pendingRequests.remove(handle);
            if (request == null) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Received response that cannot be matched to any " +
                            "outstanding request, address=" +
                            event.getPeerAddress() +
                            ", packetID=" + pdu.getPacketID());
                }
            } else {
                // return response
                request.finished = true;
                request.listener.onResponse(new AgentXResponseEvent<A>(this,
                        request.target,
                        event.getPeerAddress(),
                        request.pdu,
                        (AgentXResponsePDU) pdu,
                        request.userObject));
            }
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Fire process PDU event: " + event.toString());
            }
            fireProcessPdu(event);
        }
    }

    /**
     * Throws {@link UnsupportedOperationException} because AgentX sessions are not managed on protocol level. Use
     * {@link AgentXCommandListener} implementations on agent/application level instead.
     *
     * @return nothing, instead {@link UnsupportedOperationException} is thrown.
     * @since 3.0.0
     */
    @Override
    public Map<AgentXSession<?>, Integer> closeAllSessions(byte reason) {
        throw new UnsupportedOperationException();
    }

    /**
     * Fires a {@code CommandResponderEvent} event to inform listeners about
     * a received PDU. If a listener has marked the event as processed further
     * listeners will not be informed about the event.
     *
     * @param event
     *         a {@code CommandResponderEvent}.
     */
    protected void fireProcessPdu(AgentXCommandEvent<?> event) {
        if (commandListeners != null) {
            ArrayList<AgentXCommandListener> listeners = commandListeners;
            for (AgentXCommandListener listener : listeners) {
                listener.processCommand(event);
                // if event is marked as processed the event is not forwarded to
                // remaining listeners
                if (event.isProcessed()) {
                    return;
                }
            }
        }
    }


    static class AsyncRequestKey {
        private final AgentXPDU request;
        private final AgentXResponseListener listener;

        public AsyncRequestKey(AgentXPDU request, AgentXResponseListener listener) {
            this.request = request;
            this.listener = listener;
        }

        /**
         * Indicates whether some other object is "equal to" this one.
         *
         * @param obj
         *         the reference object with which to compare.
         *
         * @return {@code true} if this object is the same as the obj argument;
         * {@code false} otherwise.
         */
        public boolean equals(Object obj) {
            if (obj instanceof AsyncRequestKey) {
                AsyncRequestKey other = (AsyncRequestKey) obj;
                return (request.equals(other.request) && listener.equals(other.listener));
            }
            return false;
        }

        public int hashCode() {
            return request.hashCode();
        }
    }

    class PendingRequest<A extends Address> extends TimerTask implements PduHandleCallback<AgentXPDU> {

        protected PduHandle key;
        protected AgentXResponseListener listener;
        protected Object userObject;

        protected AgentXPDU pdu;
        protected AgentXTarget<A> target;

        private volatile boolean finished = false;


        public PendingRequest(PduHandle key, AgentXResponseListener listener,
                              Object userObject, AgentXPDU pdu, AgentXTarget<A> target) {
            this.key = key;
            this.userObject = userObject;
            this.listener = listener;
            this.pdu = pdu;
            this.target = target;
        }

        protected void registerRequest(PduHandle handle) {
        }

        public synchronized void run() {
            pendingRequests.remove(key);
            // request timed out
            if (!finished) {
                if (logger.isDebugEnabled()) {
                    logger.debug("AgentX request timed out: " + key.getTransactionID());
                }
                finished = true;
                if (listener != null) {
                    listener.onResponse(new AgentXResponseEvent<>(AgentX.this, target, null,
                            pdu, null, userObject));
                }
            }
        }

        public synchronized boolean setFinished() {
            boolean currentState = finished;
            this.finished = true;
            return currentState;
        }

        public synchronized void pduHandleAssigned(PduHandle handle, AgentXPDU pdu) {
            if (key == null) {
                key = handle;
                if (logger.isDebugEnabled()) {
                    logger.debug("New pending request " + pdu + " with handle " + handle);
                }
                registerRequest(handle);
                pendingRequests.put(handle, this);
                long delay = target.getTimeout();
                if (delay < 1000) {
                    delay = AgentXProtocol.DEFAULT_TIMEOUT_SECONDS * 1000;
                }
                timer.schedule(this, delay);
            }
        }
    }

    class AsyncPendingRequest<A extends Address> extends PendingRequest<A> {

        public AsyncPendingRequest(PduHandle key,
                                   AgentXResponseListener listener,
                                   Object userObject,
                                   AgentXPDU pdu,
                                   AgentXTarget<A> target) {
            super(key, listener, userObject, pdu, target);
        }

        protected void registerRequest(PduHandle handle) {
            asyncRequests.put(new AsyncRequestKey(pdu, listener), handle);
        }
    }

    static class SyncResponseListener implements AgentXResponseListener {

        private AgentXResponseEvent<?> response = null;

        public synchronized void onResponse(AgentXResponseEvent<?> event) {
            this.response = event;
            this.notify();
        }

        public AgentXResponseEvent<?> getResponse() {
            return response;
        }

    }

    /**
     * Get the AgentX message dispatcher.
     * @return
     *    the associated {@link AgentXMessageDispatcher}.
     */
    public AgentXMessageDispatcher getMessageDispatcher() {
        return messageDispatcher;
    }
}
