/*_############################################################################
  _## 
  _##  SNMP4J-Agent 3 - VacmMIB.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 java.util.*;

import org.snmp4j.log.*;
import org.snmp4j.agent.*;
import org.snmp4j.agent.mo.*;
import org.snmp4j.agent.mo.util.*;
import org.snmp4j.agent.security.*;
import org.snmp4j.mp.MessageProcessingModel;
import org.snmp4j.security.*;
import org.snmp4j.smi.*;

/**
 * This concrete implementation of the SNMP-VIEW-BASED-ACM-MIB (RFC 3415). The configuration of the view access model
 * can be changed programmatically (see {@link MutableVACM}) or via SNMP but an initial configuration must be created
 * programmatically in order to allow any access to the agent via SNMP.
 *
 * @author Frank Fock
 * @version 3.5
 */
public class VacmMIB implements MOGroup, MutableVACM {

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

    public static final OID vacmContextEntryOID =
            new OID(new int[]{1, 3, 6, 1, 6, 3, 16, 1, 1, 1});

    public static final int colVacmGroupName = 3;
    public static final int colVacmSecurityToGroupStorageType = 4;
    public static final int colVacmSecurityToGroupRowStatus = 5;

    public static final int idxVacmGroupName = 0;
    public static final int idxVacmSecurityToGroupStorageType = 1;
    public static final int idxVacmSecurityToGroupRowStatus = 2;

    public static final OID vacmSecurityToGroupEntryOID =
            new OID(new int[]{1, 3, 6, 1, 6, 3, 16, 1, 2, 1});

    public static final int colVacmAccessContextMatch = 4;
    public static final int colVacmAccessReadViewName = 5;
    public static final int colVacmAccessWriteViewName = 6;
    public static final int colVacmAccessNotifyViewName = 7;
    public static final int colVacmAccessStorageType = 8;
    public static final int colVacmAccessRowStatus = 9;

    //  private static final int idxVacmAccessGroupName = 0;
    public static final int idxVacmAccessContextPrefix = 1;
    public static final int idxVacmAccessSecurityModel = 2;
    public static final int idxVacmAccessSecurityLevel = 3;
    public static final int idxVacmAccessContextMatch = 0;
    public static final int idxVacmAccessReadViewName = 1;
    public static final int idxVacmAccessWriteViewName = 2;
    public static final int idxVacmAccessNotifyViewName = 3;
    public static final int idxVacmAccessStorageType = 4;
    public static final int idxVacmAccessRowStatus = 5;

    public static final OID vacmAccessEntryOID =
            new OID(new int[]{1, 3, 6, 1, 6, 3, 16, 1, 4, 1});

    public static final int vacmExactMatch = MutableVACM.VACM_MATCH_EXACT;
    public static final int vacmPrefixMatch = MutableVACM.VACM_MATCH_PREFIX;


    public static final OID vacmViewSpinLockOID =
            new OID(new int[]{1, 3, 6, 1, 6, 3, 16, 1, 5, 1, 0});

    public static final int colVacmViewTreeFamilyMask = 3;
    public static final int colVacmViewTreeFamilyType = 4;
    public static final int colVacmViewTreeFamilyStorageType = 5;
    public static final int colVacmViewTreeFamilyRowStatus = 6;

    //  private static final int idxVacmViewTreeViewName = 0;
    public static final int idxVacmViewTreeSubtree = 1;

    public static final int idxVacmViewTreeFamilyMask = 0;
    public static final int idxVacmViewTreeFamilyType = 1;
    public static final int idxVacmViewTreeFamilyStorageType = 2;
    public static final int idxVacmViewTreeFamilyRowStatus = 3;

    public static final OID vacmViewTreeFamilyEntryOID =
            new OID(new int[]{1, 3, 6, 1, 6, 3, 16, 1, 5, 2, 1});

    public static final int vacmViewIncluded = MutableVACM.VACM_VIEW_INCLUDED;
    public static final int vacmViewExcluded = MutableVACM.VACM_VIEW_EXCLUDED;

    private static final int[] vacmViewTreeFamilyTypeValues = {
            vacmViewIncluded, vacmViewExcluded
    };

    private static final MOTableSubIndex[] vacmViewTreeFamilyIndexes =
            new MOTableSubIndex[]{
                    new MOTableSubIndex(SMIConstants.SYNTAX_OCTET_STRING, 1, 32),
                    new MOTableSubIndex(SMIConstants.SYNTAX_OBJECT_IDENTIFIER, 0, 96)
            };

    private static final MOTableIndex vacmViewTreeFamilyIndex =
            new MOTableIndex(vacmViewTreeFamilyIndexes);

    private static final MOTableSubIndex[] vacmAccessIndexes = new MOTableSubIndex[]{
            new MOTableSubIndex(SMIConstants.SYNTAX_OCTET_STRING, 1, 32),
            new MOTableSubIndex(SMIConstants.SYNTAX_OCTET_STRING, 0, 32),
            new MOTableSubIndex(SMIConstants.SYNTAX_INTEGER, 1, 1),
            new MOTableSubIndex(SMIConstants.SYNTAX_INTEGER, 1, 1)
    };

    private final MOTableIndex vacmAccessIndex =
            new MOTableIndex(vacmAccessIndexes) {
                public boolean isValidIndex(OID index) {
                    boolean ok = super.isValidIndex(index);
                    if (ok) {
                        Integer32 secModel = new Integer32(index.get(index.size() - 2));
                        int secModelID = secModel.getValue();
                        if ((secModelID > SecurityModel.SECURITY_MODEL_SNMPv2c) &&
                                (securityModels.getSecurityModel(secModel) == null)) {
                            return false;
                        }
                        int secLevel = index.get(index.size() - 1);
                        if ((secLevel < 1) || (secLevel > 3)) {
                            return false;
                        }
                    }
                    return ok;
                }
            };

    private final MOServer[] server;

    @SuppressWarnings("rawtypes")
    private DefaultMOTable<MOTableRow, MOColumn, VacmContextTableModel> vacmContextTable;

    @SuppressWarnings("rawtypes")
    private DefaultMOTable<DefaultMOMutableRow2PC, MOColumn, DefaultMOMutableTableModel<DefaultMOMutableRow2PC>>
            vacmSecurityToGroupTable;
    private DefaultMOMutableTableModel<DefaultMOMutableRow2PC> vacmSecurityToGroupTableModel;

    @SuppressWarnings("rawtypes")
    private DefaultMOTable<DefaultMOMutableRow2PC, MOColumn, DefaultMOMutableTableModel<DefaultMOMutableRow2PC>>
            vacmAccessTable;
    private DefaultMOMutableTableModel<DefaultMOMutableRow2PC> vacmAccessTableModel;

    private final TestAndIncr vacmViewSpinLock;

    @SuppressWarnings("rawtypes")
    private DefaultMOTable<DefaultMOMutableRow2PC, MOColumn, DefaultMOMutableTableModel<DefaultMOMutableRow2PC>>
            vacmViewTreeFamilyTable;
    private DefaultMOMutableTableModel<DefaultMOMutableRow2PC> vacmViewTreeFamilyTableModel;

    private SecurityModels securityModels;


    public VacmMIB(MOServer[] server) {
        this.server = server;
        createVacmContextTable();
        createVacmSecuritToGroupTable();
        createVacmAccessTable();
        createVacmViewTreeFamilyTable();
        vacmViewSpinLock = new TestAndIncr(vacmViewSpinLockOID);
        securityModels = SecurityModels.getInstance();
    }

    public VacmMIB(MOServer[] server, SecurityModels securityModels) {
        this(server);
        this.securityModels = securityModels;
    }

    public void registerMOs(MOServer server, OctetString context) throws
            DuplicateRegistrationException {
        server.register(vacmContextTable, context);
        server.register(vacmSecurityToGroupTable, context);
        server.register(vacmAccessTable, context);
        server.register(vacmViewSpinLock, context);
        server.register(vacmViewTreeFamilyTable, context);
    }

    private void createVacmContextTable() {
        MOTableSubIndex[] vacmContextTableIndexes = new MOTableSubIndex[]{
                new MOTableSubIndex(SMIConstants.SYNTAX_OCTET_STRING, 0, 32)};
        MOTableIndex vacmContextTableIndex =
                new MOTableIndex(vacmContextTableIndexes);
        MOColumn<?>[] vacmContextColumns = new MOColumn<?>[]{
                new MOColumn<OctetString>(1, SMIConstants.SYNTAX_OCTET_STRING, MOAccessImpl.ACCESS_READ_ONLY)};

        this.vacmContextTable = new DefaultMOTable<>(vacmContextEntryOID, vacmContextTableIndex, vacmContextColumns);
        this.vacmContextTable.setVolatile(true);
        this.vacmContextTable.setConfigurable(false);
        this.vacmContextTable.setModel(new VacmContextTableModel());
    }

    private void createVacmSecuritToGroupTable() {
        MOTableSubIndex[] vacmSecurityToGroupIndexes = new MOTableSubIndex[]{
                new MOTableSubIndex(SMIConstants.SYNTAX_INTEGER, 1, 1),
                new MOTableSubIndex(SMIConstants.SYNTAX_OCTET_STRING, 1, 32)};

        MOTableIndex vacmSecurityToGroupIndex =
                new MOTableIndex(vacmSecurityToGroupIndexes) {
                    public boolean isValidIndex(OID index) {
                        boolean ok = super.isValidIndex(index);
                        int securityModel = index.get(0);
                        if (ok && (securityModel > SecurityModel.SECURITY_MODEL_SNMPv2c)) {
                            if (securityModels.getSecurityModel(new Integer32(securityModel)) == null) {
                                return false;
                            }
                        }
                        return ok;
                    }
                };

        MOColumn<?>[] vacmSecurityToGroupColumns = new MOColumn<?>[]{
                new SnmpAdminString(colVacmGroupName,
                        MOAccessImpl.ACCESS_READ_CREATE, null, true, 1, 32),
                new StorageType(colVacmSecurityToGroupStorageType,
                        MOAccessImpl.ACCESS_READ_CREATE,
                        new Integer32(StorageType.nonVolatile), true),
                new RowStatus<>(colVacmSecurityToGroupRowStatus)
        };

        this.vacmSecurityToGroupTable =
                new DefaultMOTable<>(vacmSecurityToGroupEntryOID, vacmSecurityToGroupIndex, vacmSecurityToGroupColumns);
        vacmSecurityToGroupTableModel = new DefaultMOMutableTableModel<>();
        vacmSecurityToGroupTableModel.setRowFactory(new DefaultMOMutableRow2PCFactory());
        this.vacmSecurityToGroupTable.setModel(vacmSecurityToGroupTableModel);
    }

    private void createVacmAccessTable() {
        MOColumn<?>[] vacmAccessColumns = new MOColumn<?>[]{
                new Enumerated<Integer32>(colVacmAccessContextMatch,
                        SMIConstants.SYNTAX_INTEGER32,
                        MOAccessImpl.ACCESS_READ_CREATE,
                        new Integer32(vacmExactMatch), true,
                        new int[]{vacmExactMatch, vacmPrefixMatch}),
                new SnmpAdminString(colVacmAccessReadViewName,
                        MOAccessImpl.ACCESS_READ_CREATE,
                        new OctetString(), true, 0, 32),
                new SnmpAdminString(colVacmAccessWriteViewName,
                        MOAccessImpl.ACCESS_READ_CREATE,
                        new OctetString(), true, 0, 32),
                new SnmpAdminString(colVacmAccessNotifyViewName,
                        MOAccessImpl.ACCESS_READ_CREATE,
                        new OctetString(), true, 0, 32),
                new StorageType(colVacmAccessStorageType,
                        MOAccessImpl.ACCESS_READ_CREATE,
                        new Integer32(StorageType.nonVolatile), true),
                new RowStatus<DefaultMOMutableRow2PC>(colVacmAccessRowStatus)
        };

        vacmAccessTable = new DefaultMOTable<>(vacmAccessEntryOID, vacmAccessIndex, vacmAccessColumns);
        vacmAccessTableModel = new DefaultMOMutableTableModel<DefaultMOMutableRow2PC>();
        vacmAccessTableModel.setRowFactory(new DefaultMOMutableRow2PCFactory());
        vacmAccessTable.setModel(vacmAccessTableModel);
    }

    private void createVacmViewTreeFamilyTable() {
        MOColumn<?>[] vacmViewTreeFamilyColumns = new MOColumn<?>[]{
                new SnmpAdminString(colVacmViewTreeFamilyMask,
                        MOAccessImpl.ACCESS_READ_CREATE,
                        new OctetString(), true, 0, 16),
                new Enumerated<Integer32>(colVacmViewTreeFamilyType,
                        SMIConstants.SYNTAX_INTEGER32,
                        MOAccessImpl.ACCESS_READ_CREATE,
                        new Integer32(vacmViewIncluded), true,
                        vacmViewTreeFamilyTypeValues),
                new StorageType(colVacmViewTreeFamilyStorageType,
                        MOAccessImpl.ACCESS_READ_CREATE,
                        new Integer32(StorageType.nonVolatile), true),
                new RowStatus<DefaultMOMutableRow2PC>(colVacmViewTreeFamilyRowStatus)
        };

        vacmViewTreeFamilyTable =
                new DefaultMOTable<>(vacmViewTreeFamilyEntryOID, vacmViewTreeFamilyIndex, vacmViewTreeFamilyColumns);
        vacmViewTreeFamilyTableModel = new DefaultMOMutableTableModel<>();
        vacmViewTreeFamilyTableModel.setRowFactory(new DefaultMOMutableRow2PCFactory());
        vacmViewTreeFamilyTable.setModel(vacmViewTreeFamilyTableModel);
    }

    public void unregisterMOs(MOServer server, OctetString context) {
        server.unregister(this.vacmContextTable, context);
        server.unregister(this.vacmSecurityToGroupTable, context);
        server.unregister(this.vacmAccessTable, context);
        server.unregister(vacmViewSpinLock, context);
        server.unregister(vacmViewTreeFamilyTable, context);
    }

    public int isAccessAllowed(OctetString context, OctetString securityName,
                               int securityModel, int securityLevel, int viewType,
                               OID oid) {
        if (logger.isDebugEnabled()) {
            logger.debug("VACM access requested for context=" + context +
                    ", securityName=" + securityName +
                    ", securityModel=" + securityModel +
                    ", securityLevel=" + securityLevel +
                    ", viewType=" + viewType +
                    ", OID=" + oid);
        }
        boolean supported = isContextSupported(context);
        if (!supported) {
            if (logger.isDebugEnabled()) {
                logger.debug("Context '" + context + "' ist not supported");
            }
            return VACM.VACM_NO_SUCH_CONTEXT;
        }
        OctetString groupName = getGroupName(securityName, securityModel);
        if (groupName == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("No group name for securityName=" + securityName +
                        " and securityModel=" + securityModel);
            }
            return VACM.VACM_NO_GROUP_NAME;
        }
        OctetString viewName = getViewNameByGroup(context, securityModel,
                securityLevel, viewType,
                groupName);
        if (viewName == null) {
            return VACM.VACM_NO_ACCESS_ENTRY;
        }
        if (viewName.length() == 0) {
            return VACM.VACM_NO_SUCH_VIEW;
        }
        return isAccessAllowed(viewName, oid);
    }

    private boolean isContextSupported(OctetString context) {
        boolean supported = false;
        for (MOServer aServer : server) {
            if (aServer.isContextSupported(context)) {
                supported = true;
                break;
            }
        }
        return supported;
    }

    public OctetString getViewName(OctetString context,
                                   OctetString securityName,
                                   int securityModel,
                                   int securityLevel,
                                   int viewType) {
        // Defense in depth: gate on context support here as well, so that callers reusing this
        // method as an entry point (e.g. the notification originator) cannot look up a view for a
        // context that is not served by this agent. isAccessAllowed(..) performs the same check.
        if (!isContextSupported(context)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Context '" + context + "' is not supported; denying view lookup");
            }
            return null;
        }
        OctetString groupName = getGroupName(securityName, securityModel);
        if (groupName == null) {
            return null;
        }
        return getViewNameByGroup(context, securityModel, securityLevel,
                viewType, groupName);
    }

    @Override
    public boolean hasContext(OctetString contextName) {
        return isContextSupported(contextName);
    }

    private OctetString getViewNameByGroup(OctetString context, int securityModel,
                                           int securityLevel, int viewType,
                                           OctetString groupName) {
        List<MOTableRow> accessEntries = getAccessEntries(groupName);

        if (logger.isDebugEnabled()) {
            logger.debug("Got views " + accessEntries +
                    " for group name '" + groupName + "'");
        }

        MOTableRow possibleMatch = null;
        long bestScore = -1L;

        for (MOTableRow accessEntry : accessEntries) {
            if (((Integer32) accessEntry.getValue(idxVacmAccessRowStatus)).getValue() !=
                    RowStatus.active) {
                continue;
            }
            Variable[] indexValues = vacmAccessIndex.getIndexValues(accessEntry.getIndex());
            OctetString rowContext =
                    (OctetString) indexValues[idxVacmAccessContextPrefix];
            int rowSecurityModel =
                    ((Integer32) indexValues[idxVacmAccessSecurityModel]).getValue();
            int rowSecurityLevel =
                    ((Integer32) indexValues[idxVacmAccessSecurityLevel]).getValue();
            int rowContextMatch =
                    ((Integer32) accessEntry.getValue(idxVacmAccessContextMatch)).getValue();
            boolean exactContextMatch = rowContext.equals(context);
            boolean prefixMatch = (!exactContextMatch) &&
                    ((rowContextMatch == vacmPrefixMatch) &&
                            (context.startsWith(rowContext)));
            boolean exactSecModel = (rowSecurityModel == securityModel);
            boolean matchSecModel = exactSecModel
                    || (rowSecurityModel == SecurityModel.SECURITY_MODEL_ANY);
            boolean matchSecLevel = (rowSecurityLevel <= securityLevel);
            if (logger.isDebugEnabled()) {
                logger.debug("Matching against access entry " + accessEntry +
                        " with exactContextMatch=" + exactContextMatch +
                        ", prefixMatch=" + prefixMatch +
                        ", exactSecModel=" + exactSecModel +
                        ", matchSecModel=" + matchSecModel +
                        " and matchSecLevel=" + matchSecLevel);
            }
            if ((exactContextMatch || prefixMatch) && matchSecModel && matchSecLevel) {
                // Score each candidate and keep the highest scoring one. The score encodes the
                // RFC 3415 section 4.4.2 selection preference, from most to least significant:
                //   1) an exact context match is preferred over a prefix match,
                //   2) a longer matching context prefix is preferred,
                //   3) an exact securityModel is preferred over anySecurityModel, and
                //   4) a higher securityLevel is preferred.
                long score = accessMatchScore(exactContextMatch, rowContext.length(),
                        exactSecModel, rowSecurityLevel);
                if (possibleMatch == null || score > bestScore) {
                    possibleMatch = accessEntry;
                    bestScore = score;
                }
            }
        }
        if (possibleMatch != null) {
            OctetString viewName = null;
            switch (viewType) {
                case VACM.VIEW_READ: {
                    viewName =
                            (OctetString) possibleMatch.getValue(idxVacmAccessReadViewName);
                    break;
                }
                case VACM.VIEW_WRITE: {
                    viewName = (OctetString)
                            possibleMatch.getValue(idxVacmAccessWriteViewName);
                    break;
                }
                case VACM.VIEW_NOTIFY: {
                    viewName = (OctetString)
                            possibleMatch.getValue(idxVacmAccessNotifyViewName);
                    break;
                }
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Matching view found for group name '" + groupName + "' is '" +
                        viewName + "'");
            }
            return viewName;
        }
        return null;
    }

    /**
     * Computes a comparable score for a matching {@code vacmAccessTable} entry that encodes the
     * RFC 3415 section 4.4.2 selection preference between multiple matching entries. A higher score
     * denotes a better (more specific) match. The score packs, from most to least significant:
     * <ol>
     * <li>whether the context matched exactly (preferred over a prefix match),</li>
     * <li>the length of the matching context prefix (a longer prefix is preferred),</li>
     * <li>whether the {@code securityModel} matched exactly (preferred over
     *     {@link SecurityModel#SECURITY_MODEL_ANY}), and</li>
     * <li>the entry's {@code securityLevel} (a higher level is preferred).</li>
     * </ol>
     * This makes the previously order-dependent, chained-condition preference deterministic and
     * auditable, and in particular guarantees an exact security-model match is chosen over an
     * {@code anySecurityModel} entry regardless of table iteration order.
     *
     * @param exactContextMatch
     *    whether the entry's context prefix equals the requested context.
     * @param contextPrefixLength
     *    the length of the entry's (matching) context prefix.
     * @param exactSecurityModel
     *    whether the entry's securityModel equals the requested one (as opposed to
     *    {@link SecurityModel#SECURITY_MODEL_ANY}).
     * @param securityLevel
     *    the entry's securityLevel.
     * @return a non-negative score; larger values denote a better match.
     * @since 3.9.2
     */
    private static long accessMatchScore(boolean exactContextMatch, int contextPrefixLength,
                                         boolean exactSecurityModel, int securityLevel) {
        long score = (exactContextMatch ? 1L : 0L);
        score = (score << 8) | (contextPrefixLength & 0xFFL);
        score = (score << 1) | (exactSecurityModel ? 1L : 0L);
        score = (score << 8) | (securityLevel & 0xFFL);
        return score;
    }

    private OctetString getGroupName(OctetString securityName, int securityModel) {
        OID index = new OID();
        index.append(securityModel);
        index.append(securityName.toSubIndex(false));
        MOTableRow row = vacmSecurityToGroupTableModel.getRow(index);
        if (row != null) {
            OctetString groupName = (OctetString) row.getValue(idxVacmGroupName);
            if (logger.isDebugEnabled()) {
                logger.debug("Found group name '" + groupName + "' for secName '" +
                        securityName + "' and secModel " + securityModel);
            }
            return groupName;
        }
        if (logger.isDebugEnabled()) {
            logger.debug("No group found for secName '" + securityName + "' and secModel " + securityModel);
        }
        return null;
    }

    public int isAccessAllowed(OctetString viewName, OID oid) {
        List<MOTableRow> views = getViews(viewName);
        if (views.isEmpty()) {
            if (logger.isDebugEnabled()) {
                logger.debug("No view tree family entry for view '" + viewName + "'");
            }
            return VACM.VACM_NO_SUCH_VIEW;
        }
        // iterate from back to forth because the views list must be ordered by
        // subtree length (view name is the same for all entries) which is the
        // criteria to find the appropriate view access entry.
        for (int v = views.size() - 1; v >= 0; v--) {
            MOTableRow row = views.get(v);
            if (((Integer32) row.getValue(idxVacmViewTreeFamilyRowStatus)).getValue() != RowStatus.active) {
                // only active rows are relevant
                continue;
            }
            OID index = row.getIndex();
            Variable[] indexValues = vacmViewTreeFamilyIndex.getIndexValues(index);
            OID subtree = (OID) indexValues[idxVacmViewTreeSubtree];
            if (oid.size() < subtree.size()) {
                // no match
                continue;
            }
            OctetString mask = (OctetString) row.getValue(idxVacmViewTreeFamilyMask);
            boolean match = true;
            for (int i = 0; i < subtree.size(); i++) {
                if ((subtree.get(i) != oid.get(i)) && isBitSet(i, mask)) {
                    match = false;
                    break;
                }
            }
            if (match) {
                // we found the matching entry
                if (((Integer32) row.getValue(idxVacmViewTreeFamilyType)).getValue() ==
                        vacmViewIncluded) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Access allowed for view '" + viewName + "' by subtree " +
                                subtree + " for OID " + oid + " and mask " + mask.toHexString());
                    }
                    return VACM.VACM_OK;
                } else {
                    // excluded
                    if (logger.isDebugEnabled()) {
                        logger.debug("Access denied for view '" + viewName + "' by subtree " +
                                subtree + " for OID " + oid + " and mask " + mask.toHexString());
                    }
                    return VACM.VACM_NOT_IN_VIEW;
                }
            }
        }
        return VACM.VACM_NOT_IN_VIEW;
    }

    /**
     * Adds a security model and name to group name mapping to this VACM. Any already existing mapping for the security
     * name and model will be silently replaced.
     *
     * @param securityModel
     *         the security model.
     * @param securityName
     *         the security name.
     * @param groupName
     *         the group name.
     * @param storageType
     *         the storage type for the new entry.
     */
    public void addGroup(int securityModel, OctetString securityName,
                         OctetString groupName, int storageType) {
        OID index = createGroupIndex(securityModel, securityName);
        Variable[] values = new Variable[vacmSecurityToGroupTable.getColumnCount()];
        values[idxVacmGroupName] = groupName;
        values[idxVacmSecurityToGroupStorageType] = new Integer32(storageType);
        values[idxVacmSecurityToGroupRowStatus] = new Integer32(RowStatus.active);
        DefaultMOMutableRow2PC row = vacmSecurityToGroupTable.createRow(index, values);
        vacmSecurityToGroupTable.addRow(row);
    }

    private static OID createGroupIndex(int securityModel,
                                        OctetString securityName) {
        OID index = new OID();
        index.append(securityModel);
        index.append(securityName.toSubIndex(false));
        return index;
    }

    /**
     * Removes a security model and name to group name mapping from this VACM.
     *
     * @param securityModel
     *         the security model.
     * @param securityName
     *         the security name.
     *
     * @return {@code true} when the entry has been removed or {@code false}
     * if such a mapping could not be found.
     */
    public boolean removeGroup(int securityModel, OctetString securityName) {
        OID index = createGroupIndex(securityModel, securityName);
        return (vacmSecurityToGroupTable.removeRow(index) != null);
    }

    @Override
    public boolean hasSecurityToGroupMapping(int securityModel, OctetString securityName) {
        OID index = createGroupIndex(securityModel, securityName);
        return vacmSecurityToGroupTableModel.containsRow(index);
    }

    /**
     * Adds an access entry to this VACM and thus adds access rights for a group.
     *
     * @param groupName
     *         the group for which access rights are to be added.
     * @param contextPrefix
     *         the context or context prefix.
     * @param securityModel
     *         the security model
     * @param securityLevel
     *         the security level
     * @param match
     *         indicates whether exact context match ({@link #vacmExactMatch}) or prefix context match ({@link
     *         #vacmPrefixMatch}) should be used by the new entry.
     * @param readView
     *         the view name for read access (use a zero length OctetString to disable access).
     * @param writeView
     *         the view name for write access (use a zero length OctetString to disable access).
     * @param notifyView
     *         the view name for notify access (use a zero length OctetString to disable access).
     * @param storageType
     *         the {@link StorageType} for this access entry.
     */
    public void addAccess(OctetString groupName, OctetString contextPrefix,
                          int securityModel, int securityLevel,
                          int match,
                          OctetString readView, OctetString writeView,
                          OctetString notifyView, int storageType) {
        OID index = createAccessIndex(groupName, contextPrefix, securityModel,
                securityLevel);
        Variable[] values = new Variable[vacmAccessTable.getColumnCount()];
        values[idxVacmAccessContextMatch] = new Integer32(match);
        values[idxVacmAccessReadViewName] = readView;
        values[idxVacmAccessWriteViewName] = writeView;
        values[idxVacmAccessNotifyViewName] = notifyView;
        values[idxVacmAccessStorageType] = new Integer32(storageType);
        values[idxVacmAccessRowStatus] = new Integer32(RowStatus.active);
        vacmAccessTable.addRow(vacmAccessTableModel.createRow(index, values));
    }

    /**
     * Removes an access entry from this VACM.
     *
     * @param groupName
     *         the group for which access rights are to be added.
     * @param contextPrefix
     *         the context or context prefix.
     * @param securityModel
     *         the security model
     * @param securityLevel
     *         the security level
     *
     * @return {@code true} when the entry has been removed or {@code false}
     * if no such entry could be found.
     */
    public boolean removeAccess(OctetString groupName, OctetString contextPrefix,
                                int securityModel, int securityLevel) {
        OID index = createAccessIndex(groupName, contextPrefix, securityModel,
                securityLevel);
        return (vacmAccessTable.removeRow(index) != null);
    }

    @Override
    public int accessEntryCount(OctetString groupName) {
        OID lowerBound = groupName.toSubIndex(false);
        OID upperBound = new OID(lowerBound).nextPeer();
        return vacmAccessTableModel.getRows(lowerBound, upperBound).size();
    }

    private static OID createAccessIndex(OctetString groupName,
                                         OctetString contextPrefix,
                                         int securityModel, int securityLevel) {
        OID index = groupName.toSubIndex(false);
        if (contextPrefix == null) {
            index.append(0);
        } else {
            index.append(contextPrefix.toSubIndex(false));
        }
        index.append(securityModel);
        index.append(securityLevel);
        return index;
    }

    /**
     * Adds a new view to this VACM. An already existing entry with the same view name and subtree OID will be replaced
     * silently.
     *
     * @param viewName
     *         the view name.
     * @param subtree
     *         the subtree OID.
     * @param mask
     *         the bit mask which, in combination with {@code subtree}, defines a family of view subtrees.
     * @param type
     *         indicates whether the view defined by {@code subtree} and
     *         {@code mask} is included ({@link #vacmViewIncluded}) or excluded
     *         (@link #vacmViewExcluded}) from the MIB view.
     * @param storageType
     *         the {@link StorageType} for this access entry.
     */
    public void addViewTreeFamily(OctetString viewName, OID subtree,
                                  OctetString mask, int type, int storageType) {
        OID index = createViewIndex(viewName, subtree);
        Variable[] values = new Variable[vacmViewTreeFamilyTable.getColumnCount()];
        values[idxVacmViewTreeFamilyMask] = mask;
        values[idxVacmViewTreeFamilyType] = new Integer32(type);
        values[idxVacmViewTreeFamilyStorageType] = new Integer32(storageType);
        values[idxVacmViewTreeFamilyRowStatus] = new Integer32(RowStatus.active);
        DefaultMOMutableRow2PC row = vacmViewTreeFamilyTableModel.createRow(index, values);
        vacmViewTreeFamilyTable.addRow(row);
    }

    /**
     * Removes a view tree family from this VACM.
     *
     * @param viewName
     *         the view name.
     * @param subtree
     *         the subtree OID.
     *
     * @return {@code true} when the entry has been removed or {@code false}
     * if no such entry could be found.
     */
    public boolean removeViewTreeFamily(OctetString viewName, OID subtree) {
        OID index = createViewIndex(viewName, subtree);
        return (vacmViewTreeFamilyTable.removeRow(index) != null);
    }

    @Override
    public int viewTreeFamilyEntryCount(OctetString viewName) {
        OID lowerBound = viewName.toSubIndex(false);
        OID upperBound = new OID(lowerBound).nextPeer();
        return vacmViewTreeFamilyTableModel.getRows(lowerBound, upperBound).size();
    }

    private static OID createViewIndex(OctetString viewName, OID subtree) {
        OID index = viewName.toSubIndex(false);
        index.append(subtree.toSubIndex(false));
        return index;
    }

    /**
     * Checks whether bit n of the supplied OctetString is set or not.
     *
     * @param n
     *         denotes the bit to check starting from zero.
     * @param os
     *         OctetString
     *
     * @return boolean
     */
    public static boolean isBitSet(final int n, final OctetString os) {
        return (os.length() <= n / 8) || ((os.get(n / 8) & (0x01 << (7 - (n % 8)))) > 0);
    }

    private List<MOTableRow> getAccessEntries(OctetString groupName) {
        OctetString upperBound = new OctetString(groupName);
        byte last = -1;
        if (upperBound.length() > 0) {
            last = upperBound.get(upperBound.length() - 1);
        }
        if (last == -1) {
            upperBound.append((byte) 0);
        } else {
            upperBound.set(upperBound.length() - 1, (byte) (last + 1));
        }
        OID lowerOID = groupName.toSubIndex(false);
        OID upperOID = upperBound.toSubIndex(false);
        return vacmAccessTableModel.getRows(lowerOID, upperOID);
    }

    protected List<MOTableRow> getViews(OctetString viewName) {
        if (viewName.length() == 0) {
            return Collections.emptyList();
        }
        OctetString upperBound = new OctetString(viewName);
        byte last = upperBound.get(upperBound.length() - 1);
        if (last == -1) {
            upperBound.append((byte) 0);
        } else {
            upperBound.set(upperBound.length() - 1, (byte) (last + 1));
        }
        OID lowerOID = viewName.toSubIndex(false);
        OID upperOID = upperBound.toSubIndex(false);
        return vacmViewTreeFamilyTableModel.getRows(lowerOID, upperOID);
    }

    public static class VacmContextIterator implements Iterator<MOTableRow> {

        private int index = 0;
        private final OctetString[] contexts;

        VacmContextIterator(OctetString[] contexts, int offset) {
            this.contexts = contexts;
            this.index = offset;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

        public boolean hasNext() {
            return (index < contexts.length);
        }

        public MOTableRow next() {
            if (index < contexts.length) {
                OctetString context = contexts[index++];
                return new DefaultMOTableRow(context.toSubIndex(false),
                        new Variable[]{context});
            }
            throw new NoSuchElementException();
        }

    }

    @SuppressWarnings("rawtypes")
    public DefaultMOTable<DefaultMOMutableRow2PC, MOColumn, DefaultMOMutableTableModel<DefaultMOMutableRow2PC>>
    getVacmSecurityToGroupTable() {
        return vacmSecurityToGroupTable;
    }


    @SuppressWarnings("rawtypes")
    DefaultMOTable<DefaultMOMutableRow2PC, MOColumn, DefaultMOMutableTableModel<DefaultMOMutableRow2PC>>
    getVacmViewTreeFamilyTable() {
        return vacmViewTreeFamilyTable;
    }


    private static OctetString getContextFromIndex(OID index) {
        if (index.size() > 0) {
            return new OctetString(index.toByteArray(), 1, index.size() - 1);
        }
        return new OctetString();
    }


    class VacmContextTableModel implements MOTableModel<MOTableRow> {

        public int getColumnCount() {
            return 1;
        }

        public int getRowCount() {
            int n = 0;
            for (MOServer aServer : server) {
                n += aServer.getContexts().length;
            }
            return n;
        }

        @Override
        public boolean isEmpty() {
            for (MOServer aServer : server) {
                if (aServer.getContexts().length > 0) {
                    return false;
                }
            }
            return true;
        }

        public boolean containsRow(OID index) {
            return isContextSupported(getContextFromIndex(index));
        }

        public MOTableRow getRow(OID index) {
            if (index == null) {
                return null;
            }
            OctetString context = getContextFromIndex(index);
            if (isContextSupported(context)) {
                return new DefaultMOTableRow(index, new Variable[]{context});
            }
            return null;
        }

        public Iterator<MOTableRow> iterator() {
            return tailIterator(new OID());
        }

        public Iterator<MOTableRow> tailIterator(OID lowerBound) {
            OctetString[] contexts = getContexts();
            LexicographicOctetStringComparator contextComparator = new LexicographicOctetStringComparator();
            Arrays.<OctetString>sort(contexts, contextComparator);
            int offset = 0;
            if (lowerBound != null) {
                offset = Arrays.binarySearch(contexts, getContextFromIndex(lowerBound), contextComparator);
            }
            if (offset < 0) {
                offset = -(offset + 1);
            }
            return new VacmContextIterator(contexts, offset);
        }

        private OctetString[] getContexts() {
            ArrayList<OctetString> ctx = new ArrayList<OctetString>();
            for (MOServer aServer : server) {
                ctx.addAll(Arrays.asList(aServer.getContexts()));
            }
            return ctx.toArray(new OctetString[0]);
        }

        public OID lastIndex() {
            OctetString[] contexts = getContexts();
            if (contexts.length == 0) {
                return null;
            }
            Arrays.sort(contexts, new LexicographicOctetStringComparator());
            return contexts[contexts.length - 1].toSubIndex(false);
        }

        public OID firstIndex() {
            OctetString[] contexts = getContexts();
            if (contexts.length == 0) {
                return null;
            }
            Arrays.sort(contexts, new LexicographicOctetStringComparator());
            return contexts[0].toSubIndex(false);
        }

        public MOTableRow firstRow() {
            return getRow(firstIndex());
        }

        public MOTableRow lastRow() {
            return getRow(lastIndex());
        }
    }

}
