/*_############################################################################
  _## 
  _##  SNMP4J-Agent 3 - SNMPv2MIB.java  
  _## 
  _##  Copyright (C) 2005-2025  Frank Fock (SNMP4J.org)
  _##  
  _##  Licensed under the Apache License, Version 2.0 (the "License");
  _##  you may not use this file except in compliance with the License.
  _##  You may obtain a copy of the License at
  _##  
  _##      http://www.apache.org/licenses/LICENSE-2.0
  _##  
  _##  Unless required by applicable law or agreed to in writing, software
  _##  distributed under the License is distributed on an "AS IS" BASIS,
  _##  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  _##  See the License for the specific language governing permissions and
  _##  limitations under the License.
  _##  
  _##########################################################################*/


package org.snmp4j.agent.mo.snmp;

import org.snmp4j.agent.*;
import org.snmp4j.agent.mo.*;
import org.snmp4j.mp.*;
import org.snmp4j.smi.*;
import org.snmp4j.event.CounterListener;
import org.snmp4j.event.CounterEvent;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Collections;

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

/**
 * The {@code SystemGroup} implements the objects in the SNMPv2-MIB specification. Since version 2.0, the SysUpTime
 * class uses {@link System#nanoTime()} instead {@link System#currentTimeMillis()} for up-time computation which makes
 * the implementation stable against system time changes.
 *
 * @author Frank Fock
 * @version 2.0
 */
public class SNMPv2MIB implements MOGroup, CounterListener, AgentCapabilityList {

    private static final LogAdapter LOGGER = LogFactory.getLogger(SNMPv2MIB.class);

    // Constants
    private static final OID snmpStatsPrefix =
            new OID(new int[]{1, 3, 6, 1, 2, 1, 11});
    private static final OID usmStatsPrefix =
            new OID(new int[]{1, 3, 6, 1, 6, 3, 15, 1, 1});


    private static final OID oidSnmpInPkts =
            SnmpConstants.snmpInPkts;
    private static final OID oidSnmpInBadVersions =
            SnmpConstants.snmpInBadVersions;
    private static final OID oidSnmpInBadCommunityNames =
            SnmpConstants.snmpInBadCommunityNames;
    private static final OID oidSnmpInBadCommunityUses =
            SnmpConstants.snmpInBadCommunityUses;
    private static final OID oidSnmpInASNParseErrs =
            SnmpConstants.snmpInASNParseErrs;
    private static final OID oidSnmpSilentDrops =
            SnmpConstants.snmpSilentDrops;
    private static final OID oidSnmpProxyDrops =
            SnmpConstants.snmpProxyDrops;

    private static final OID oidSysORLastChange =
            new OID(new int[]{1, 3, 6, 1, 2, 1, 1, 8, 0});

    private static final OID oidSnmpEnableAuthenTraps =
            new OID(new int[]{1, 3, 6, 1, 2, 1, 11, 30, 0});

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


    private MOScalar<OctetString> descr;
    private SysUpTimeImpl upTime;
    private MOScalar<OID> objectID;
    private MOScalar<OctetString> contact;
    private MOScalar<OctetString> name;
    private MOScalar<OctetString> location;
    private MOScalar<Integer32> services;
    private MOScalar<Integer32> snmpEnableAuthenTraps;
    private TestAndIncr snmpSetSerialNo;
    private MOScalar<TimeTicks> sysORLastChange;

    private static final OID[] snmpStatsOIDs = {
            oidSnmpInPkts, oidSnmpInBadVersions, oidSnmpInBadCommunityNames,
            oidSnmpInBadCommunityUses, oidSnmpInASNParseErrs,
            oidSnmpSilentDrops, oidSnmpProxyDrops
    };

    private MOScalar<?>[] snmpStats;

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

    // Column sub-identifier definitions for sysOREntry:
    private static final int colSysORID = 2;
    private static final int colSysORDescr = 3;
    private static final int colSysORUpTime = 4;

    // Column index definitions for sysOREntry:
    private static final int idxSysORID = 0;
    private static final int idxSysORDescr = 1;
    private static final int idxSysORUpTime = 2;
    private static MOTableSubIndex[] sysOREntryIndexes =
            new MOTableSubIndex[]{
                    new MOTableSubIndex(SMIConstants.SYNTAX_INTEGER, 1, 1)};

    private static MOTableIndex sysOREntryIndex =
            new MOTableIndex(sysOREntryIndexes, false);

    @SuppressWarnings("rawtypes")
    private DefaultMOTable<DefaultMOTableRow, MOColumn, DefaultMOMutableTableModel<DefaultMOTableRow>> sysOREntry;
    private DefaultMOMutableTableModel<DefaultMOTableRow> sysOREntryModel;

    // Notification originator for authenticationFailure trap
    private NotificationOriginator notificationOriginator;

    // Context to sysUpTime Map
    private static Map<OctetString, SysUpTimeImpl> sysUpTimes =
            Collections.synchronizedMap(new HashMap<OctetString, SysUpTimeImpl>());

    /**
     * Create a new SNMPv2MIB instance with description, OID, services indicator, and system up-time which starts when
     * this constuctor is being called.
     *
     * @param sysDescr
     *         the description of the system.
     * @param sysOID
     *         the (unique) OID of the OBJECT-IDENTITY construct identifying this system.
     * @param sysServices
     *         the services indicator that roughly describes which network services this entity supports. See
     *         sysServices MIB description.
     */
    public SNMPv2MIB(OctetString sysDescr,
                     OID sysOID,
                     Integer32 sysServices) {
        this(sysDescr, sysOID, sysServices, null);
    }

    /**
     * Create a new SNMPv2MIB instance with description, OID, services indicator, and system up-time.
     *
     * @param sysDescr
     *         the description of the system.
     * @param sysOID
     *         the (unique) OID of the OBJECT-IDENTITY construct identifying this system.
     * @param sysServices
     *         the services indicator that roughly describes which network services this entity supports. See
     *         sysServices MIB description.
     * @param sysUpTime
     *         the up-time service whose {@link SysUpTime#get()} is called when the system up-time needs to be
     *         determined.
     *
     * @since 2.4.1
     */
    protected SNMPv2MIB(OctetString sysDescr,
                        OID sysOID,
                        Integer32 sysServices,
                        final SysUpTime sysUpTime) {
        this.descr = new MOScalar<OctetString>(new OID(SnmpConstants.sysDescr),
                MOAccessImpl.ACCESS_READ_ONLY,
                sysDescr);
        this.descr.setVolatile(true);
        if (sysUpTime == null) {
            this.upTime = new SysUpTimeImpl();
        } else {
            this.upTime = new SysUpTimeImpl() {
                @Override
                public TimeTicks getValue() {
                    return sysUpTime.get();
                }
            };
        }
        this.upTime.setVolatile(true);
        this.objectID = new MOScalar<>(new OID(SnmpConstants.sysObjectID),
                MOAccessImpl.ACCESS_READ_ONLY,
                sysOID);
        this.objectID.setVolatile(true);
        this.contact =
                new DisplayStringScalar<OctetString>(new OID(SnmpConstants.sysContact),
                        MOAccessImpl.ACCESS_READ_WRITE,
                        new OctetString());
        this.name =
                new DisplayStringScalar<OctetString>(new OID(SnmpConstants.sysName),
                        MOAccessImpl.ACCESS_READ_WRITE,
                        new OctetString());
        this.location =
                new DisplayStringScalar<OctetString>(new OID(SnmpConstants.sysLocation),
                        MOAccessImpl.ACCESS_READ_WRITE,
                        new OctetString());
        this.services = new MOScalar<Integer32>(new OID(SnmpConstants.sysServices),
                MOAccessImpl.ACCESS_READ_ONLY,
                sysServices);
        this.services.setVolatile(true);
        snmpEnableAuthenTraps =
                new EnumeratedScalar<Integer32>(oidSnmpEnableAuthenTraps,
                        MOAccessImpl.ACCESS_READ_WRITE,
                        new Integer32(SnmpEnableAuthenTrapsEnum.enabled),
                        new int[]{SnmpEnableAuthenTrapsEnum.enabled,
                                SnmpEnableAuthenTrapsEnum.disabled});
        snmpSetSerialNo = new TestAndIncr(oidSnmpSetSerialNo);
        snmpSetSerialNo.setVolatile(true);

        sysORLastChange =
                new TimeStampScalar(oidSysORLastChange,
                        MOAccessImpl.ACCESS_READ_ONLY, upTime);
        sysORLastChange.setVolatile(true);
        createSysOREntry();
        createSnmpStats();
    }

    private void createSnmpStats() {
        snmpStats = new MOScalar<?>[snmpStatsOIDs.length];
        for (int i = 0; i < snmpStatsOIDs.length; i++) {
            snmpStats[i] = new MOScalar<>(snmpStatsOIDs[i], MOAccessImpl.ACCESS_READ_ONLY, new Counter32(0));
        }
    }

    private void createSysOREntry() {
        MOColumn<?>[] sysOREntryColumns = new MOColumn<?>[3];
        sysOREntryColumns[idxSysORID] =
                new MOColumn<OID>(colSysORID,
                        SMIConstants.SYNTAX_OBJECT_IDENTIFIER,
                        MOAccessImpl.ACCESS_READ_ONLY);
        sysOREntryColumns[idxSysORDescr] =
                new DisplayString<OctetString>(colSysORDescr,
                        MOAccessImpl.ACCESS_READ_ONLY,
                        null,
                        true);
        sysOREntryColumns[idxSysORUpTime] =
                new MOColumn<TimeTicks>(colSysORUpTime,
                        SMIConstants.SYNTAX_TIMETICKS,
                        MOAccessImpl.ACCESS_READ_ONLY);

        sysOREntry =
                new SysOREntry(oidSysOREntry,
                        sysOREntryIndex,
                        sysOREntryColumns);
        sysOREntryModel = new DefaultMOMutableTableModel<DefaultMOTableRow>();
        sysOREntryModel.setRowFactory(new MOTableRowFactory<DefaultMOTableRow>() {
            @Override
            public DefaultMOTableRow createRow(OID index, Variable[] values) throws UnsupportedOperationException {
                return new DefaultMOTableRow(index, values);
            }

            @Override
            public void freeRow(DefaultMOTableRow row) {
            }
        });
        sysOREntry.setModel(sysOREntryModel);
        sysOREntry.setVolatile(true);
    }

    protected void updateSysORLastChange() {
        sysORLastChange.setValue(getUpTime());
    }

    public OID addSysOREntry(OID sysORID, OctetString sysORDescr) {
        OID index = new OID(new int[]{sysOREntryModel.getRowCount() + 1});
        Variable[] values = new Variable[sysOREntry.getColumnCount()];
        int n = 0;
        values[n++] = sysORID;
        values[n++] = sysORDescr;
        values[n] = upTime.get();
        DefaultMOTableRow row = new DefaultMOTableRow(index, values);
        sysOREntry.addRow(row);
        updateSysORLastChange();
        return index;
    }

    public MOTableRow removeSysOREntry(OID index) {
        updateSysORLastChange();
        return sysOREntry.removeRow(index);
    }

    public void registerMOs(MOServer server, OctetString context)
            throws DuplicateRegistrationException {
        server.register(descr, context);
        server.register(upTime, context);
        sysUpTimes.put(context, upTime);
        server.register(objectID, context);
        server.register(contact, context);
        server.register(name, context);
        server.register(location, context);
        server.register(services, context);
        server.register(snmpEnableAuthenTraps, context);
        server.register(snmpSetSerialNo, context);
        server.register(sysORLastChange, context);
        for (MOScalar<?> snmpStat : snmpStats) {
            server.register(snmpStat, context);
        }
        server.register(sysOREntry, context);
    }

    public void unregisterMOs(MOServer server, OctetString context) {
        server.unregister(descr, context);
        server.unregister(upTime, context);
        sysUpTimes.remove(context);
        server.unregister(objectID, context);
        server.unregister(contact, context);
        server.unregister(name, context);
        server.unregister(location, context);
        server.unregister(services, context);
        server.unregister(snmpEnableAuthenTraps, context);
        server.unregister(snmpSetSerialNo, context);
        server.unregister(sysORLastChange, context);
        for (MOScalar<?> snmpStat : snmpStats) {
            server.unregister(snmpStat, context);
        }
        server.unregister(sysOREntry, context);
    }

    public static final class SnmpEnableAuthenTrapsEnum {
        public static final int enabled = 1;
        public static final int disabled = 2;
    }

    public static class SysUpTimeImpl extends MOScalar<TimeTicks> implements SysUpTime, Serializable {

        private static final long serialVersionUID = 1;
        private long startTime;

        public SysUpTimeImpl() {
            super(new OID(SnmpConstants.sysUpTime), MOAccessImpl.ACCESS_READ_ONLY,
                    new TimeTicks(0));
            startTime = System.nanoTime();
        }

        public TimeTicks getValue() {
            long ticks = (System.nanoTime() - startTime) / 10000000L;
            return new TimeTicks(ticks & 0xFFFFFFFFL);
        }

        public TimeTicks get() {
            return getValue();
        }

    }

    public OctetString getContact() {
        return contact.getValue();
    }

    public void setContact(OctetString contact) {
        this.contact.setValue(contact);
    }

    public OctetString getDescr() {
        return descr.getValue();
    }

    public OctetString getLocation() {
        return location.getValue();
    }

    public void setLocation(OctetString sysLocation) {
        this.location.setValue(sysLocation);
    }

    public OctetString getName() {
        return name.getValue();
    }

    public void setName(OctetString sysName) {
        this.name.setValue(sysName);
    }

    public OID getObjectID() {
        return objectID.getValue();
    }

    public Integer32 getServices() {
        return services.getValue();
    }

    public TimeTicks getUpTime() {
        return upTime.getValue();
    }

    public SysUpTime getSysUpTime() {
        return upTime;
    }

    public void incrementCounter(CounterEvent event) {
        if ((event.getOid().startsWith(snmpStatsPrefix)) &&
                (event.getOid().size() > snmpStatsPrefix.size())) {
            int suffix = event.getOid().get(snmpStatsPrefix.size());
            for (int i = 0; i < snmpStatsOIDs.length; i++) {
                if (suffix == snmpStatsOIDs[i].get(snmpStatsPrefix.size())) {
                    Counter32 current = (Counter32) snmpStats[i].getValue();
                    current.increment();
                    event.setCurrentValue((Counter32) current.clone());
                }
            }
        }
        OID eventOID = event.getOid();
        if ((eventOID.equals(SnmpConstants.snmpInBadCommunityNames)) ||
                ((eventOID.startsWith(usmStatsPrefix)) &&
                        !SnmpConstants.usmStatsNotInTimeWindows.equals(eventOID) &&
                        !SnmpConstants.usmStatsUnknownEngineIDs.equals(eventOID))) {
            if ((snmpEnableAuthenTraps.getValue()).getValue() ==
                    SnmpEnableAuthenTrapsEnum.enabled) {
                if (notificationOriginator == null) {
                    LOGGER.debug("Authentication failure trap not sent because " +
                            "NotificationOriginator not set in SNMPv2MIB");
                } else {
                    notificationOriginator.notify(new OctetString(),
                            SnmpConstants.authenticationFailure,
                            new VariableBinding[0]);
                }
            }
        }
    }

    /**
     * Returns the sysUpTime for the supplied context.
     *
     * @param context
     *         a context or {@code null} for the default context.
     *
     * @return the SysUpTime instance associated with the given context or
     * {@code null} if such a sysUpTime instance has not been
     * registered yet.
     */
    public static SysUpTime getSysUpTime(OctetString context) {
        return sysUpTimes.get(context);
    }

    @SuppressWarnings("rawtypes")
    public class SysOREntry extends DefaultMOTable<DefaultMOTableRow, MOColumn,
            DefaultMOMutableTableModel<DefaultMOTableRow>> implements AgentCapabilityList {

        public SysOREntry(OID oid, MOTableIndex index, MOColumn<?>[] columns) {
            super(oid, index, columns);
        }

        public OID addSysOREntry(OID sysORID, OctetString sysORDescr) {
            return SNMPv2MIB.this.addSysOREntry(sysORID, sysORDescr);
        }

        public MOTableRow removeSysOREntry(OID index) {
            return SNMPv2MIB.this.removeSysOREntry(index);
        }

    }

    /**
     * Sets the {@code NotificationOriginator} to be used for sending the authenticationFailure trap.
     *
     * @param notificationOriginator
     *         a NotificationOriginator instance or {@code null} to disable authenticationFailure traps (default).
     */
    public void setNotificationOriginator(NotificationOriginator
                                                  notificationOriginator) {
        this.notificationOriginator = notificationOriginator;
    }

    /**
     * Gets the notification originator used for sending authenticationFailure traps.
     *
     * @return a NotificationOriginator.
     * @since 1.2
     */
    public NotificationOriginator getNotificationOriginator() {
        return notificationOriginator;
    }

    /**
     * Gets the snmpEnableAuthenTraps {@link MOScalar} object.
     *
     * @return a scalar managed object that represents the snmpEnableAuthenTraps instance.
     * @since 3.0
     */
    public MOScalar<Integer32> getSnmpEnableAuthenTraps() {
        return snmpEnableAuthenTraps;
    }
}
