/*_############################################################################
  _## 
  _##  SNMP4J-AgentX - AgentXMessageDispatcherImpl.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.nio.*;
import java.util.*;

import org.snmp4j.*;
import org.snmp4j.smi.*;

import java.io.IOException;

import org.snmp4j.log.LogFactory;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.mp.PduHandle;
import org.snmp4j.mp.PduHandleCallback;
import org.snmp4j.transport.ConnectionOrientedTransportMapping;

/**
 * The {@link AgentXMessageDispatcherImpl} implements an AgentX {@link AgentXMessageDispatcher}.
 */
public class AgentXMessageDispatcherImpl implements AgentXMessageDispatcher {

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

    private final List<TransportMapping<?>> transportMappings = new ArrayList<>();
    private final List<AgentXCommandListener> commandListener = new ArrayList<>();
    private volatile int nextPacketID = 0;

    /**
     * Creates a {@link AgentXMessageDispatcherImpl}.
     */
    public AgentXMessageDispatcherImpl() {
    }

    /**
     * Increments the internal packet ID counter and returns the next ID.
     * @return
     *    a new (positive) packet ID. If there is an overrun, 1 is returned.
     */
    public synchronized int getNextPacketID() {
        int nextID = ++nextPacketID;
        if (nextID <= 0) {
            nextID = nextPacketID = 1;
        }
        return nextID;
    }

    /**
     * Creates a new PDU handle.
     * @return
     *    a new {@link PduHandle}.
     */
    protected PduHandle createPduHandle() {
        return new PduHandle(getNextPacketID());
    }

    @Override
    public synchronized void addTransportMapping(TransportMapping<?> transport) {
        transportMappings.add(transport);
        transport.addTransportListener(this);
    }

    @Override
    public Collection<TransportMapping<?>> getTransportMappings() {
        return new ArrayList<>(transportMappings);
    }

    public <A extends Address> void processMessage(TransportMapping<? super A> sourceTransport,
                                                   A incomingAddress,
                                                   ByteBuffer wholeMessage, TransportStateReference tmStateReference) {
        @SuppressWarnings("unchecked")
        ConnectionOrientedTransportMapping<A> connectionOrientedTM =
                (ConnectionOrientedTransportMapping<A>) sourceTransport;
        try {
            AgentXPDU pdu = AgentXPDU.decode(wholeMessage);
            AgentXCommandEvent<A> event =
                    new AgentXCommandEvent<>(this, this, incomingAddress, connectionOrientedTM, pdu,
                            tmStateReference);
            fireCommandEvent(event);
        } catch (IOException ex) {
            if (logger.isDebugEnabled()) {
                ex.printStackTrace();
            }
            logger.warn(ex);
            if (ex instanceof AgentXParseException) {
                // exception can be handled on AgentX protocol level
                AgentXCommandEvent<A> event = new AgentXCommandEvent<>(this, this, incomingAddress,
                        connectionOrientedTM, (AgentXParseException) ex, tmStateReference);
                fireCommandEvent(event);
            }
        }
    }

    @Override
    public TransportMapping<?> removeTransportMapping(TransportMapping<?> transport) {
        if (transportMappings.remove(transport)) {
            transport.removeTransportListener(this);
            return transport;
        }
        return null;
    }

    /**
     * Sends a {@link AgentXPDU} to a target.
     * @param transport
     *    the {@link TransportMapping}.
     * @param address
     *    the target address.
     * @param message
     *    the {@link AgentXPDU} to be sent.
     * @param callback
     *    the {@link PduHandleCallback} to generate a {@link PduHandle} whe required.
     * @param <A>
     *     the address type.
     * @throws IOException
     *     if sending fails because of an {@link IOException}.
     */
    public <A extends Address> PduHandle send(TransportMapping<? super A> transport, A address, AgentXPDU message,
                          PduHandleCallback<AgentXPDU> callback) throws IOException {
        PduHandle handle;
        if (message instanceof AgentXResponsePDU) {
            handle = new PduHandle(message.getPacketID());
        } else {
            handle = createPduHandle();
            message.setPacketID(handle.getTransactionID());
        }
        if (callback != null) {
            callback.pduHandleAssigned(handle, message);
        }
        if (transport != null) {
            sendPDU(address, message, transport);
            return handle;
        } else {
            for (TransportMapping<?> t : transportMappings) {
                if (t.isAddressSupported(address)) {
                    @SuppressWarnings("unchecked")
                    TransportMapping<? super Address> ta = (TransportMapping<? super Address>) t;
                    sendPDU(address, message, ta);
                    return handle;
                }
            }
        }
        return null;
    }

    private <A extends Address> void sendPDU(A address, AgentXPDU message, TransportMapping<? super A> transport)
            throws IOException {
        ByteBuffer buf = ByteBuffer.allocate(message.getPayloadLength() + AgentXProtocol.HEADER_LENGTH);
        message.encode(buf);
        send(address, transport, buf, null);
    }

    /**
     * Sends a {@link AgentXPDU} encoded in a {@link ByteBuffer} to a target.
     * @param address
     *    the target address.
     * @param transport
     *    the {@link TransportMapping}.
     * @param message
     *    the message to be sent.
     * @param tmStateReference
     *    the {@link TransportStateReference} to hold SNMPv3 state reference.
     * @param <A>
     *     the address type.
     * @throws IOException
     *     if sending fails because of an {@link IOException}.
     */
    public <A extends Address> void send(A address, TransportMapping<? super A> transport, ByteBuffer message,
                                         TransportStateReference tmStateReference) throws IOException {
        message.flip();
        byte[] bytes = new byte[message.limit()];
        message.get(bytes);
        transport.sendMessage(address, bytes, tmStateReference, 0, 0);
    }

    /**
     * Fires a {@link AgentXCommandEvent}
     * @param event
     *   the {@link AgentXCommandEvent} to be fired.
     */
    protected synchronized void fireCommandEvent(AgentXCommandEvent<?> event) {
        for (AgentXCommandListener aCommandListener : commandListener) {
            aCommandListener.processCommand(event);
            if (event.isProcessed()) {
                return;
            }
        }
    }

    @Override
    public synchronized void addCommandListener(AgentXCommandListener l) {
        commandListener.add(l);
    }

    @Override
    public synchronized void removeCommandListener(AgentXCommandListener l) {
        commandListener.remove(l);
    }
}
