/*_############################################################################
  _## 
  _##  SNMP4J-AgentX - AgentXPDU.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.io.Serial;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.snmp4j.log.LogFactory;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.smi.OctetString;

/**
 * The {@link AgentXPDU} implements the common characteristics of all AgentX PDUs (see RFC 2741).
 * @author Frank Fock
 */
public abstract class AgentXPDU implements Serializable {

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

    /**
     * The reserved byte position.
     */
    public static final byte RESERVED = (byte)0;

    /**
     * The agentx-Open-PDU ID.
     */
    public static final byte AGENTX_OPEN_PDU		= 1;
    /**
     * The agentx-Close-PDU ID.
     */
    public static final byte AGENTX_CLOSE_PDU		= 2;
    /**
     * The agentx-Register-PDU ID.
     */
    public static final byte AGENTX_REGISTER_PDU	= 3;
    /**
     * The agentx-Unregister-PDU ID.
     */
    public static final byte AGENTX_UNREGISTER_PDU	= 4;
    /**
     * The agentx-Get-PDU ID.
     */
    public static final byte AGENTX_GET_PDU		    = 5;
    /**
     * The agentx-GetNext-PDU ID.
     */
    public static final byte AGENTX_GETNEXT_PDU		= 6;
    /**
     * The agentx-GetBulk-PDU ID.
     */
    public static final byte AGENTX_GETBULK_PDU		= 7;
    /**
     * The agentx-TestSet-PDU ID.
     */
    public static final byte AGENTX_TESTSET_PDU		= 8;
    /**
     * The agentx-CommitSet-PDU ID.
     */
    public static final byte AGENTX_COMMITSET_PDU	= 9;
    /**
     * The agentx-UndoSet-PDU ID.
     */
    public static final byte AGENTX_UNDOSET_PDU		=10;
    /**
     * The agentx-CleanupSet-PDU ID.
     */
    public static final byte AGENTX_CLEANUPSET_PDU	=11;
    /**
     * The agentx-Notify-PDU ID.
     */
    public static final byte AGENTX_NOTIFY_PDU		=12;
    /**
     * The agentx-Ping-PDU ID.
     */
    public static final byte AGENTX_PING_PDU		=13;
    /**
     * The agentx-IndexAllocate-PDU ID.
     */
    public static final byte AGENTX_INDEXALLOCATE_PDU	=14;
    /**
     * The agentx-IndexDeallocate-PDU ID.
     */
    public static final byte AGENTX_INDEXDEALLOCATE_PDU	=15;
    /**
     * The agentx-AddAgentCaps-PDU ID.
     */
    public static final byte AGENTX_ADDAGENTCAPS_PDU	=16;
    /**
     * The agentx-RemoveAgentCaps-PDU ID.
     */
    public static final byte AGENTX_REMOVEAGENTCAPS_PDU   =17;
    /**
     * The agentx-Response-PDU ID.
     */
    public static final byte AGENTX_RESPONSE_PDU     	=18;

    @Serial
    private static final long serialVersionUID = -1162042633774482416L;

    /**
     * AgentX PDU type.
     */
    protected byte type;
    /**
     * AgentX protocol version
     */
    protected byte version = AgentXProtocol.VERSION_1_0;
    /**
     * Session ID
     */
    protected int sessionID;
    /**
     * Transaction ID.
     */
    protected int transactionID;
    /**
     * Packet ID
     */
    protected int packetID;
    /**
     * Flags (bitwise OR), see {@link AgentXProtocol#FLAG_INSTANCE_REGISTRATION} for example.
     */
    protected byte flags;
    /**
     * The byte order.
     */
    protected ByteOrder byteOrder;

    /**
     * Create a {@link AgentXPDU} from its type.
     * @param type
     *    a AgentX PDU type from {@link #AGENTX_OPEN_PDU} to {@link AGENTX_RESPONSE_PDU}.
     */
    protected AgentXPDU(byte type) {
        this.type = type;
    }

    /**
     * Create a {@link AgentXPDU} from type and other header attributes.
     * @param type
     *    a AgentX PDU type from {@link #AGENTX_OPEN_PDU} to {@link AGENTX_RESPONSE_PDU}.
     * @param flags
     *    a set of flags (bit-or), see also {@link AgentXProtocol#FLAG_NETWORK_BYTE_ORDER}.
     * @param sessionID
     *    a session ID.
     * @param transactionID
     *    a transaction ID.
     * @param packetID
     *    a packet ID.
     */
    protected AgentXPDU(byte type, byte flags,
                        int sessionID, int transactionID, int packetID) {
        this.type = type;
        this.flags = flags;
        this.sessionID = sessionID;
        this.transactionID = transactionID;
        this.packetID = packetID;
        byteOrder = isFlagSet(AgentXProtocol.FLAG_NETWORK_BYTE_ORDER) ?
                ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
    }

    /**
     * Create a {@link AgentXPDU} from an {@link AgentXMessageHeader}.
     * @param header
     *    an {@link AgentXMessageHeader}.
     */
    protected AgentXPDU(AgentXMessageHeader header) {
        this(header.getType(), header.getFlags(), header.getSessionID(),
                header.getTransactionID(), header.getPacketID());
    }

    /**
     * Adds a flag to the existing flags (bit-wise OR).
     * @param flag
     *    a AgentX flag, see {@link AgentXProtocol#FLAG_NETWORK_BYTE_ORDER}
     */
    public final void addFlag(byte flag) {
        this.flags |= flag;
    }

    /**
     * Check if the specified flag is set, see {@link AgentXProtocol#FLAG_INSTANCE_REGISTRATION} for example.
     * @param flag
     *    the flag to test.
     * @return
     *    {@code true} if it is set.
     */
    public final boolean isFlagSet(int flag) {
        return ((this.flags & flag) != 0);
    }

    /**
     * Gets all flags.
     * @return
     *    a bit-wise OR flag set.
     */
    public final byte getFlags() {
        return flags;
    }

    /**
     * Gets the packet ID.
     * @return
     *    the packet ID.
     */
    public final int getPacketID() {
        return packetID;
    }

    /**
     * Gets the session ID.
     * @return
     *    the session ID.
     */
    public final int getSessionID() {
        return sessionID;
    }

    /**
     * Get the PDU type.
     * @return
     *    the type like {@link #AGENTX_OPEN_PDU}.
     */
    public final byte getType() {
        return type;
    }

    /**
     * Gets the protcol version.
     * @return
     *    the version info.
     */
    public final byte getVersion() {
        return version;
    }

    /**
     * Gets the byte order.
     * @return
     *    the byte order.
     */
    public final ByteOrder getByteOrder() {
        return byteOrder;
    }

    /**
     * Gets the transaction ID.
     * @return
     *    the transaction ID.
     */
    public final int getTransactionID() {
        return transactionID;
    }

    /**
     * Sets all flags at once.
     * @param flags
     *    a bit-wise OR combinaed set of flags.
     */
    public void setFlags(byte flags) {
        this.flags = flags;
    }

    /**
     * Sets the packet ID.
     * @param packetID
     *    the packet ID.
     */
    public void setPacketID(int packetID) {
        this.packetID = packetID;
    }

    /**
     * Sets the session ID.
     * @param sessionID
     *    the session ID.
     */
    public void setSessionID(int sessionID) {
        this.sessionID = sessionID;
    }

    /**
     * Sets the PDU type.
     * @param type
     *    the type.
     */
    public void setType(byte type) {
        this.type = type;
    }

    /**
     * Sets the version.
     * @param version
     *    the version.
     */
    public void setVersion(byte version) {
        this.version = version;
    }

    /**
     * Sets the byte order.
     * @param byteOrder
     *    the byte order.
     */
    public void setByteOrder(ByteOrder byteOrder) {
        this.byteOrder = byteOrder;
    }

    /**
     * Sets the transaction ID
     * @param transactionID
     *    the transaction ID.
     */
    public void setTransactionID(int transactionID) {
        this.transactionID = transactionID;
    }

    /**
     * Sets the {@link AgentXSession} specific attributes: session ID and byte order.
     * @param session
     *    an {@link AgentXSession}.
     */
    public void setSessionAttributes(AgentXSession<?> session) {
        setSessionID(session.getSessionID());
        setByteOrder(session.getByteOrder());
    }

    /**
     * Encode the payload to the given byte buffer.
     * @param buf
     *    the buffer.
     */
    protected abstract void encodePayload(ByteBuffer buf);

    /**
     * Gets the payload length.
     * @return
     *    the payload length.
     */
    public abstract int getPayloadLength();

    /**
     * Encode this PDU into the given byte buffer.
     * @param buf
     *    the {@link ByteBuffer} to hold the BER encoded PDU.
     */
    public final void encode(ByteBuffer buf) {
        beforeEncode();
        buf.put(version);
        buf.put(type);
        if (byteOrder == null) {
            byteOrder = ByteOrder.nativeOrder();
        }
        if (byteOrder == ByteOrder.BIG_ENDIAN) {
            flags |= AgentXProtocol.FLAG_NETWORK_BYTE_ORDER;
        }
        else {
            flags &= ~AgentXProtocol.FLAG_NETWORK_BYTE_ORDER;
        }
        buf.order(byteOrder);
        buf.put(flags);
        buf.put(RESERVED);
        buf.putInt(sessionID);
        buf.putInt(transactionID);
        buf.putInt(packetID);
        buf.putInt(getPayloadLength());
        encodePayload(buf);
    }

    /**
     * Initialize flags and other things before a PDU is encoded.
     */
    protected abstract void beforeEncode();

    /**
     * Decode this PDU's payload.
     * @param buf
     *    the buffer positioned at the end of the encoded context.
     * @param length
     *    the length of the PDU to be decoded.
     * @throws IOException
     *    if the buffer contains less data than expected.
     */
    public abstract void decodePayload(ByteBuffer buf, int length)
            throws IOException;

    /**
     * Decode {@link AgentXPDU} from a byte buffer.
     * @param buf
     *    the {@link ByteBuffer} that contains the BER encoded PDU.
     * @return
     *    the decoded {@link AgentXPDU}.
     * @throws IOException
     *    if the buffer contains less than encoded data or if the is a parse error.
     */
    public static AgentXPDU decode(ByteBuffer buf) throws IOException {
        AgentXMessageHeader header = AgentXProtocol.decodeHeader(buf);
        if (buf.remaining() < header.getPayloadLength()) {
            throw new IOException("Short AgentX PDU with payload length="+
                    header.getPayloadLength()+"<"+
                    buf.remaining()+" remaining length");
        }
        try {
            AgentXPDU pdu = createAgentXPDU(header);
            pdu.decodePayload(buf, header.getPayloadLength());
            return pdu;
        }
        catch (IOException iox) {
            logger.warn("IO Exception while parsing AgentX PDU with header "+header+
                    ", exception is: "+iox.getMessage());
            throw new AgentXParseException(header, iox);
        }
    }

    private static AgentXPDU createAgentXPDU(AgentXMessageHeader header) {
        AgentXPDU pdu = null;
        switch (header.getType()) {
            case AGENTX_OPEN_PDU: {
                pdu = new AgentXOpenPDU(header);
                break;
            }
            case AGENTX_CLOSE_PDU: {
                pdu = new AgentXClosePDU(header);
                break;
            }
            case AGENTX_RESPONSE_PDU: {
                pdu = new AgentXResponsePDU(header);
                break;
            }
            case AGENTX_ADDAGENTCAPS_PDU: {
                pdu = new AgentXAddAgentCapsPDU(header);
                break;
            }
            case AGENTX_CLEANUPSET_PDU: {
                pdu = new AgentXCleanupSetPDU(header);
                break;
            }
            case AGENTX_COMMITSET_PDU: {
                pdu = new AgentXCommitSetPDU(header);
                break;
            }
            case AGENTX_GET_PDU: {
                pdu = new AgentXGetPDU(header);
                break;
            }
            case AGENTX_GETBULK_PDU: {
                pdu = new AgentXGetBulkPDU(header);
                break;
            }
            case AGENTX_GETNEXT_PDU: {
                pdu = new AgentXGetNextPDU(header);
                break;
            }
            case AGENTX_INDEXALLOCATE_PDU: {
                pdu = new AgentXIndexAllocatePDU(header);
                break;
            }
            case AGENTX_INDEXDEALLOCATE_PDU: {
                pdu = new AgentXIndexDeallocatePDU(header);
                break;
            }
            case AGENTX_NOTIFY_PDU: {
                pdu = new AgentXNotifyPDU(header);
                break;
            }
            case AGENTX_PING_PDU: {
                pdu = new AgentXPingPDU(header);
                break;
            }
            case AGENTX_REGISTER_PDU: {
                pdu = new AgentXRegisterPDU(header);
                break;
            }
            case AGENTX_REMOVEAGENTCAPS_PDU: {
                pdu = new AgentXRemoveAgentCapsPDU(header);
                break;
            }
            case AGENTX_TESTSET_PDU: {
                pdu = new AgentXTestSetPDU(header);
                break;
            }
            case AGENTX_UNDOSET_PDU: {
                pdu = new AgentXUndoSetPDU(header);
                break;
            }
            case AGENTX_UNREGISTER_PDU: {
                pdu = new AgentXUnregisterPDU(header);
                break;
            }
            default:
                break;
        }
        return pdu;
    }

    /**
     * Check if this PDU is a confirmed PDU (i.e. requires a response).
     * @return
     *    {@code true} if a repsone is required.
     */
    public final boolean isConfirmedPDU() {
        return getType() != AGENTX_RESPONSE_PDU;
    }

    /**
     * Returns a string representation of the PDU members. An empty string is returned by this base class.
     * @return
     *    a string representation of the PDU members.
     */
    protected String toStringExtMembers() {
        return "";
    }

    @Override
    public String toString() {
        return getClass().getName()+"[type="+type+",version="+version+
                ",sessionID="+sessionID+",transactionID="+transactionID+
                ",packetID="+packetID+",byteOrder="+byteOrder+toStringExtMembers()+"]";
    }

}
