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

import org.snmp4j.agent.*;
import org.snmp4j.smi.*;
import org.snmp4j.transport.MessageLengthDecoder;
import org.snmp4j.transport.MessageLength;

import java.io.IOException;

import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;

/**
 * The {@link AgentXProtocol} class defines constants and operations specific to the AgentX protocol as defined
 * by RFC 2741.
 * @author Frank Fock
 */
public class AgentXProtocol implements MessageLengthDecoder {

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

    /**
     * Creates a new {@code AgentXProtocol} instance.
     */
    public AgentXProtocol() {
    }

    /**
     * AgentX protocol version.
     */
    public static final byte VERSION_1_0 = 1;

    /**
     * Close reason: None of the following reasons.
     */
    public static final byte REASON_OTHER = 1;
    /**
     * Close reason: Too many AgentX parse errors from peer.
     */
    public static final byte REASON_PARSE_ERROR = 2;
    /**
     * Close reason: Too many AgentX protocol errors from peer.
     */
    public static final byte REASON_PROTOCOL_ERROR = 3;
    /**
     * Close reason: Too many timeouts waiting for peer.
     */
    public static final byte REASON_TIMEOUTS = 4;
    /**
     * Close reason: Sending entity is shutting down.
     */
    public static final byte REASON_SHUTDOWN = 5;
    /**
     * Close reason: Due to Set operation; this reason code can be used only
     * by the master agent, in response to an SNMP management request.
     */
    public static final byte REASON_BY_MANAGER = 6;

    /**
     * AgentX error: open failed.
     */
    public static final int AGENTX_OPEN_FAILED = 256;
    /**
     * AgentX error: not open.
     */
    public static final int AGENTX_NOT_OPEN = 257;
    /**
     * AgentX error: index has wrong type.
     */
    public static final int AGENTX_INDEX_WRONG_TYPE = 258;
    /**
     * AgentX error: index already allocated.
     */
    public static final int AGENTX_INDEX_ALREADY_ALLOCATED = 259;
    /**
     * AgentX error: no index available.
     */
    public static final int AGENTX_INDEX_NONE_AVAILABLE = 260;
    /**
     * AgentX error: index not allocated.
     */
    public static final int AGENTX_INDEX_NOT_ALLOCATED = 261;
    /**
     * AgentX error: unsupported context.
     */
    public static final int AGENTX_UNSUPPORTED_CONTEXT = 262;
    /**
     * AgentX error: duplicate registration.
     */
    public static final int AGENTX_DUPLICATE_REGISTRATION = 263;
    /**
     * AgentX error: unknown registration.
     */
    public static final int AGENTX_UNKNOWN_REGISTRATION = 264;
    /**
     * AgentX error: unknown agent capabilities.
     */
    public static final int AGENTX_UNKNOWN_AGENTCAPS = 265;
    /**
     * AgentX error: parse error.
     */
    public static final int AGENTX_PARSE_ERROR = 266;
    /**
     * AgentX error: request denied.
     */
    public static final int AGENTX_REQUEST_DENIED = 267;
    /**
     * AgentX error: processing error.
     */
    public static final int AGENTX_PROCESSING_ERROR = 268;

    /**
     * The maximum OID length for AgentX transported OIDs according to RFC2741 §5.1.
     */
    public static final int AGENTX_MAX_OID_LENGTH = 128;

    /*  General errors  */

    /**
     * No error (success).
     */
    public static final int AGENTX_SUCCESS = 0;
    /**
     * General error.
     */
    public static final int AGENTX_ERROR = -1;
    /**
     * AentX disconnected.
     */
    public static final int AGENTX_DISCONNECT = -5;
    /**
     * Bad AgentX version.
     */
    public static final int AGENTX_BADVER = -10;
    /**
     * AgentX timeout.
     */
    public static final int AGENTX_TIMEOUT = -11;

    /*  User errors  */

    /**
     * No registration error.
     */
    public static final int AGENTX_NOREG = -40;
    /**
     * Duplicate mapping (not used)
     * @deprecated Will be removed in 3.0
     */
    public static final int AGENTX_DUPMAP = -41;

    /**
     * AgentX flag: ALLOCATE_INDEX.
     */
    public static final int FLAG_ALLOCATE_INDEX = 0;
    /**
     * AgentX flag: INSTANCE_REGISTRATION.
     */
    public static final byte FLAG_INSTANCE_REGISTRATION = 0x01;
    /**
     * AgentX flag: NEW_INDEX.
     */
    public static final byte FLAG_NEW_INDEX = 0x02;
    /**
     * AgentX flag: ANY_INDEX.
     */
    public static final byte FLAG_ANY_INDEX = 0x04;
    /**
     * AgentX flag: NON_DEFAULT_CONTEXT.
     */
    public static final byte FLAG_NON_DEFAULT_CONTEXT = 0x08;
    /**
     * AgentX flag: NETWORK_BYTE_ORDER.
     */
    public static final byte FLAG_NETWORK_BYTE_ORDER = 0x10;

    private static final OID INTERNET = new OID(new int[]{1, 3, 6, 1});

    private static final int IPADDRESS_OCTETS = 4;

    /**
     * Number of bytes for an AgentX INTEGER.
     */
    protected static final int AGENTX_INT_SIZE = 4;

    /**
     * AgentX header length = 5 * {@link #AGENTX_INT_SIZE}.
     */
    public static final int HEADER_LENGTH = 5 * AGENTX_INT_SIZE;

    /**
     * Default AgentX timeout in seconds.
     */
    public static final int DEFAULT_TIMEOUT_SECONDS = 5;
    /**
     * Default maximum of consecutive timeouts before connection closing.
     */
    public static final int DEFAULT_MAX_CONSECUTIVE_TIMEOUTS = 3;
    /**
     * Default maximum number of parse errors before connection closing.
     */
    public static final int DEFAULT_MAX_PARSE_ERRORS = -1;
    /**
     * Maximum timeout seconds.
     */
    public static final int MAX_TIMEOUT_SECONDS = 255;

    /**
     * The default priority for registrations.
     */
    public static final byte DEFAULT_PRIORITY = 127;


    private static boolean nonDefaultContextEnabled = true;


    /**
     * AgentX encode an OID
     * @param buf
     *    the target byte buffer.
     * @param oid
     *    the OID to encode
     * @param include
     *    {@code true} to add include flag and {@code false} to set it to 0.
     */
    public static void encodeOID(ByteBuffer buf,
                                 OID oid,
                                 boolean include) {
        if (oid == null) {
            buf.put(new byte[]{0, 0, 0, 0});
        } else {
            int startPos = 0;
            int size = oid.size();
            if (size > AGENTX_MAX_OID_LENGTH || size < 0) {
                size = AGENTX_MAX_OID_LENGTH;
                logger.warn("Too long OID is trimmed to " + AGENTX_MAX_OID_LENGTH +
                        " sub-identifiers allowed by AgentX: " + oid);
            }
            if ((size > INTERNET.size()) && (oid.startsWith(INTERNET))) {
                buf.put((byte) (size - (INTERNET.size() + 1)));
                buf.put((byte) oid.get(INTERNET.size()));
                startPos = INTERNET.size() + 1;
            } else {
                buf.put((byte) size);
                buf.put((byte) 0);
            }
            if ((include) && (size > 0)) {
                buf.put((byte) 1);
                buf.put((byte) 0);
            } else {
                buf.put(new byte[]{0, 0});
            }
            for (int i = startPos; i < size; i++) {
                buf.putInt(oid.get(i));
            }
        }
    }

    /**
     * Get the encoded length of an OID.
     * @param oid
     *    the OID.
     * @return
     *    the encoding length.
     */
    public static int getOIDLength(OID oid) {
        if (oid == null) {
            return AGENTX_INT_SIZE;
        }
        int startPos = 0;
        int size = Math.min(oid.size(), AGENTX_MAX_OID_LENGTH);
        if ((size > INTERNET.size()) && (oid.startsWith(INTERNET))) {
            startPos = INTERNET.size() + 1;
        }
        return AGENTX_INT_SIZE + (AGENTX_INT_SIZE * (size - startPos));
    }

    /**
     * Get the encoded length of an OID.
     * @param oid
     *    the OID.
     * @return
     *    the encoding length.
     */
    public static int getOIDLength(int[] oid) {
        if (oid == null) {
            return AGENTX_INT_SIZE;
        }
        int startPos = 0;
        int size = Math.min(oid.length, AGENTX_MAX_OID_LENGTH);
        if (size > INTERNET.size()) {
            boolean ok = true;
            for (int i = 0; ok && i < INTERNET.size(); i++) {
                ok = (oid[i] == INTERNET.get(i));
            }
            if (ok) {
                startPos = INTERNET.size() + 1;
            }
        }
        return AGENTX_INT_SIZE + (AGENTX_INT_SIZE * (size - startPos));
    }

    /**
     * Decode an OID from an AgentX encoded message (byte buffer)
     * @param buf
     *   the byte buffer containing the encoded OID.
     * @param oid
     *    the OID to return (data will be written into this object).
     * @return
     *    {@code true} if the include flag is set, {@code false} otherwise.
     */
    public static boolean decodeOID(ByteBuffer buf, OID oid) {
        int size = Math.min(0xFF & ((int) buf.get()), AGENTX_MAX_OID_LENGTH);
        int first = buf.get();
        int[] value = new int[size + ((first != 0) ? INTERNET.size() + 1 : 0)];
        int startPos = 0;
        if (first != 0) {
            System.arraycopy(INTERNET.getValue(), 0, value, 0, INTERNET.size());
            value[INTERNET.size()] = first;
            startPos = INTERNET.size() + 1;
        }
        boolean include = (buf.get() != 0);
        buf.get(); // reserved
        for (int i = 0; i < size; i++) {
            value[startPos + i] = buf.getInt();
        }
        oid.setValue(value);
        return include;
    }

    /**
     * Encode a {@link Variable}.
     * @param buf
     *    the target byte buffer.
     * @param v
     *    the variable to encode.
     */
    public static void encodeVariableData(ByteBuffer buf, Variable v) {
        if (v == null) {
            return;
        }
        switch (v.getSyntax()) {
            //case sNMP_SYNTAX_INT:
            case SMIConstants.SYNTAX_GAUGE32:
            case SMIConstants.SYNTAX_TIMETICKS:
            case SMIConstants.SYNTAX_COUNTER32: {
                buf.putInt((int) (((AssignableFromLong) v).toLong() & 0xFFFFFFFFL));
                break;
            }
            case SMIConstants.SYNTAX_INTEGER32: {
                buf.putInt(((AssignableFromInteger) v).toInt());
                break;
            }
            case SMIConstants.SYNTAX_COUNTER64: {
                buf.putLong(((AssignableFromLong) v).toLong());
                break;
            }
            case SMIConstants.SYNTAX_OCTET_STRING:
            case SMIConstants.SYNTAX_OPAQUE: {
                encodeOctetString(buf, (OctetString) v);
                break;
            }
            case SMIConstants.SYNTAX_IPADDRESS: {
                encodeOctetString(buf,
                        new OctetString(((IpAddress) v).getInetAddress().getAddress()));
                //buf.put(((IpAddress)v).getInetAddress().getAddress());
                break;
            }
            case SMIConstants.SYNTAX_OBJECT_IDENTIFIER: {
                encodeOID(buf, (OID) v, false);
                break;
            }
            default:
                break;
        }
    }

    /**
     * Gets the encoded variable data length for a given {@link Variable}.
     * @param v
     *    the {@link Variable}.
     * @return
     *    the encoded length of the given variable.
     */
    public static int getVariableDataLength(Variable v) {
        if (v == null) {
            return 0;
        }
        switch (v.getSyntax()) {
            //case sNMP_SYNTAX_INT:
            case SMIConstants.SYNTAX_GAUGE32:
            case SMIConstants.SYNTAX_TIMETICKS:
            case SMIConstants.SYNTAX_COUNTER32:
            case SMIConstants.SYNTAX_INTEGER32: {
                return AGENTX_INT_SIZE;
            }
            case SMIConstants.SYNTAX_COUNTER64: {
                return 2 * AGENTX_INT_SIZE;
            }
            case SMIConstants.SYNTAX_OCTET_STRING:
            case SMIConstants.SYNTAX_OPAQUE: {
                if (v instanceof OctetString) {
                    return getOctetStringLength(((OctetString) v).length());
                } else if (v instanceof AssignableFromByteArray) {
                    return getOctetStringLength(
                            ((AssignableFromByteArray) v).toByteArray().length);
                }
                break;
            }
            case SMIConstants.SYNTAX_IPADDRESS: {
                return 2 * AGENTX_INT_SIZE;
            }
            case SMIConstants.SYNTAX_OBJECT_IDENTIFIER: {
                return getOIDLength(((AssignableFromIntArray) v).toIntArray());
            }
            default:
                break;
        }
        return 0;
    }

    /**
     * Decode a {@link Variable} from an AgentX message provided as byte buffer.
     * @param buf
     *    the AgentX message (portion) to decode.
     * @param syntax
     *    the syntax ID specifying the variable type.
     * @return
     *    the decoded {@link Variable}.
     */
    public static Variable decodeVariableData(ByteBuffer buf, int syntax) {
        switch (syntax) {
            //case sNMP_SYNTAX_INT:
            case SMIConstants.SYNTAX_GAUGE32:
                return new Gauge32((buf.getInt() & 0xFFFFFFFFL));
            case SMIConstants.SYNTAX_TIMETICKS:
                return new TimeTicks((buf.getInt() & 0xFFFFFFFFL));
            case SMIConstants.SYNTAX_COUNTER32:
                return new Counter32((buf.getInt() & 0xFFFFFFFFL));
            case SMIConstants.SYNTAX_INTEGER32:
                return new Integer32(buf.getInt());
            case SMIConstants.SYNTAX_COUNTER64:
                return new Counter64(buf.getLong());
            case SMIConstants.SYNTAX_OCTET_STRING:
                return decodeOctetString(buf);
            case SMIConstants.SYNTAX_OPAQUE:
                return new Opaque(decodeOctetString(buf).getValue());
            case SMIConstants.SYNTAX_IPADDRESS: {
                byte[] addrBytes = decodeOctetString(buf).getValue();
                // Workaround for incorrectly implemented sub-agents like
                // NET-SNMP 5.4, that return addresses with more than 4 bytes
                if (addrBytes.length > IPADDRESS_OCTETS) {
                    logger.warn("Subagent returned IpAddress with length " +
                            addrBytes.length +
                            " > " + IPADDRESS_OCTETS +
                            " which violates AgentX protocol specification");
                    byte[] fourBytes = new byte[IPADDRESS_OCTETS];
                    System.arraycopy(addrBytes, 0, fourBytes, 0, IPADDRESS_OCTETS);
                    addrBytes = fourBytes;
                }
                InetAddress addr = null;
                try {
                    addr = InetAddress.getByAddress(addrBytes);
                } catch (UnknownHostException ex) {
                    logger.error("Failed to create IpAddress from address bytes " +
                            " with length " + addrBytes.length +
                            ", using default IpAddress instead", ex);
                    return new IpAddress();
                }
                return new IpAddress(addr);
            }
            case SMIConstants.SYNTAX_OBJECT_IDENTIFIER: {
                OID oid = new OID();
                decodeOID(buf, oid);
                return oid;
            }
            case SMIConstants.EXCEPTION_END_OF_MIB_VIEW:
            case SMIConstants.EXCEPTION_NO_SUCH_INSTANCE:
            case SMIConstants.EXCEPTION_NO_SUCH_OBJECT: {
                return new Null(syntax);
            }
            case SMIConstants.SYNTAX_NULL: {
                return new Null();
            }
            default: {
                logger.error("Unknown AgentX variable syntax '" + syntax +
                        "', using Null instead");
                return new Null();
            }
        }
    }

    /**
     * Decode an array of {@link VariableBinding}.
     * @param buf
     *    the AgentX message (portion) to decode.
     * @return
     *    the decoded array of {@link VariableBinding} instances.
     */
    public static VariableBinding[] decodeVariableBindings(ByteBuffer buf) {
        ArrayList<VariableBinding> vbs = new ArrayList<VariableBinding>();
        while (buf.remaining() > 0) {
            int type = buf.getShort() & 0xFFFF;
            buf.getShort();
            OID oid = new OID();
            decodeOID(buf, oid);
            Variable v = decodeVariableData(buf, type);
            vbs.add(new VariableBinding(oid, v));
        }
        return vbs.toArray(new VariableBinding[0]);
    }

    /**
     * Encode an array of {@link VariableBinding} to an AgentX message buffer.
     * @param buf
     *    the AgentX message (portion) to encode.
     * @param vbs
     *    an array of {@link VariableBinding} instances.
     */
    public static void encodeVaribleBindings(ByteBuffer buf, VariableBinding[] vbs) {
        for (VariableBinding vb : vbs) {
            buf.putShort((short) vb.getSyntax());
            buf.put(new byte[]{0, 0}); // reserved
            encodeOID(buf, vb.getOid(), false);
            encodeVariableData(buf, vb.getVariable());
        }
    }

    /**
     * Encode search ranges.
     * @param buf
     *    the AgentX message (portion) to encode.
     * @param searchRanges
     *    an array of search ranges.
     */
    public static void encodeRanges(ByteBuffer buf, MOScope[] searchRanges) {
        for (MOScope searchRange : searchRanges) {
            encodeOID(buf, searchRange.getLowerBound(),
                    searchRange.isLowerIncluded());
            if (searchRange.isUpperIncluded()) {
                encodeOID(buf, searchRange.getUpperBound().successor(), false);
            } else {
                encodeOID(buf, searchRange.getUpperBound(), false);
            }
        }
    }

    /**
     * Gets the encoded length of an encoded {@link OctetString}.
     * @param length
     *    the length in bytes of the {@link OctetString}.
     * @return
     *    the encoded length in bytes.
     */
    public static int getOctetStringLength(int length) {
        int padding = 0;
        if ((length % AGENTX_INT_SIZE) > 0) {
            padding = AGENTX_INT_SIZE - (length % AGENTX_INT_SIZE);
        }
        return AGENTX_INT_SIZE + length + padding;
    }

    /**
     * Encode an {@link OctetString}.
     * @param buf
     *    the AgentX message (portion) to encode.
     * @param os
     *    an {@link OctetString} to encode.
     */
    public static void encodeOctetString(ByteBuffer buf, OctetString os) {
        buf.putInt(os.length());
        buf.put(os.getValue());
        if ((os.length() % AGENTX_INT_SIZE) > 0) {
            for (int i = 0; i < AGENTX_INT_SIZE - (os.length() % AGENTX_INT_SIZE); i++) {
                buf.put((byte) 0);
            }
        }
    }

    /**
     * Decode an {@link OctetString}.
     * @param buf
     *    the AgentX message (portion) to decode.
     * @return os
     *    the decoded {@link OctetString}.
     */
    public static OctetString decodeOctetString(ByteBuffer buf) {
        int size = buf.getInt();
        byte[] value = new byte[size];
        buf.get(value);
        if ((size % AGENTX_INT_SIZE) > 0) {
            for (int i = 0; i < AGENTX_INT_SIZE - (size % AGENTX_INT_SIZE); i++) {
                buf.get(); // skip 0 bytes
            }
        }
        return new OctetString(value);
    }

    /**
     * Decode search ranges.
     * @param buf
     *    the AgentX message (portion) to decode.
     * @return
     *    the decoded array of {@link MOScope} instances representing the search ranges.
     */
    public static MOScope[] decodeRanges(ByteBuffer buf) {
        return decodeRanges(buf, false);
    }

    /**
     * Decode search ranges.
     * @param buf
     *    the AgentX message (portion) to decode.
     * @param lowerAlwaysIncluded
     *    {@code true} to interpret lower bounds as always included, {@code false} to include lower bound if
     *    encoded as included.
     * @return
     *    the decoded array of {@link MOScope} instances representing the search ranges.
     */
    public static MOScope[] decodeRanges(ByteBuffer buf,
                                         boolean lowerAlwaysIncluded) {
        ArrayList<MOScope> ranges = new ArrayList<MOScope>();
        while (buf.hasRemaining()) {
            OID lowerBound = new OID();
            boolean isLowerIncluded = lowerAlwaysIncluded | decodeOID(buf, lowerBound);
            OID upperBound = new OID();
            decodeOID(buf, upperBound);
            if (upperBound.size() == 0) {
                upperBound = null;
            }
            ranges.add(new DefaultMOScope(lowerBound, isLowerIncluded,
                    upperBound, false));
        }
        return ranges.toArray(new MOScope[0]);
    }

    /**
     * Gets the encoded length of the given ranges.
     * @param ranges
     *    an array of {@link MOScope} instances representing search ranges.
     * @return
     *    the encoded length in bytes.
     */
    public static int getRangesLength(MOScope[] ranges) {
        int length = 0;
        for (MOScope range : ranges) {
            length += AgentXProtocol.getOIDLength(range.getLowerBound());
            if (range.isUpperIncluded()) {
                length +=
                        AgentXProtocol.getOIDLength(range.getUpperBound().successor());
            } else {
                length += AgentXProtocol.getOIDLength(range.getUpperBound());
            }
        }
        return length;
    }

    /**
     * Gets the encoded length of the given {@link VariableBinding}s.
     * @param vbs
     *    the {@link VariableBinding}s.
     * @return
     *    the encoded length in bytes.
     */
    public static int getVariableBindingsLength(VariableBinding[] vbs) {
        int length = 0;
        for (VariableBinding vb : vbs) {
            length += AGENTX_INT_SIZE + getOIDLength(vb.getOid()) +
                    getVariableDataLength(vb.getVariable());
        }
        return length;
    }

    /**
     * Gets the minimum header length.
     * @return
     *    {@link #HEADER_LENGTH}.
     */
    public int getMinHeaderLength() {
        return HEADER_LENGTH;
    }

    /**
     * Gets the encoded message length.
     * @param buf
     *    the AgentX message buffer containing the AgentX message to decode.
     * @return
     *    the length of the message.
     * @throws IOException
     *    if the header could not be decoded.
     */
    public MessageLength getMessageLength(ByteBuffer buf) throws IOException {
        return decodeHeader(buf);
    }

    /**
     * Decodes the header of an AgentX message.
     * @param buf
     *    the AgentX message buffer containing the AgentX message to decode.
     * @return
     *    the decoded {@link AgentXMessageHeader}.
     * @throws IOException
     *    if the header could not be decoded.
     */
    public static AgentXMessageHeader decodeHeader(ByteBuffer buf)
            throws IOException {
        byte version = buf.get();
        if (version != AgentXProtocol.VERSION_1_0) {
            throw new IOException("Unknown AgentX version: " + version);
        }
        byte type = buf.get();
        byte flags = buf.get();
        buf.get();
        ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN;
        if ((flags & AgentXProtocol.FLAG_NETWORK_BYTE_ORDER) != 0) {
            byteOrder = ByteOrder.BIG_ENDIAN;
        }
        buf.order(byteOrder);
        int sessionID = buf.getInt();
        int transactionID = buf.getInt();
        int packetID = buf.getInt();
        int length = buf.getInt();
        return new AgentXMessageHeader(type, flags, sessionID, transactionID,
                packetID, length);
    }

    /**
     * Activate/deactivate non-default context support.
     * @param enabled
     *    {@code false} to disable non-default context support for this {@link AgentXProtocol}, enabled is the default.
     */
    public static void setNonDefaultContextsEnabled(boolean enabled) {
        nonDefaultContextEnabled = enabled;
    }

    /**
     * Checks if non-default context support is enabled or not.
     * @return
     *    {@code true} if non-default context support is enabled (default).
     */
    public static boolean isNonDefaultContextsEnabled() {
        return nonDefaultContextEnabled;
    }

}
