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

import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

import org.snmp4j.agent.mo.*;
import org.snmp4j.agent.mo.lock.DefaultMOLockStrategy;
import org.snmp4j.agent.mo.lock.LockRequest;
import org.snmp4j.agent.mo.lock.MOLockStrategy;
import org.snmp4j.log.*;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.smi.*;

/**
 * The default MO server implementation uses a sorted map for the managed object
 * registry.
 *
 * @author Frank Fock
 * @version 3.1.0
 */
public class DefaultMOServer implements MOServer {

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

    private final Set<OctetString> contexts;
    private final SortedMap<MOScope, ManagedObject<?>> registry;
    private final Map<ManagedObject<?>, Lock> lockList;
    private Map<ManagedObject<?>, List<MOServerLookupListener>> lookupListener;
    private transient List<ContextListener> contextListeners;
    private UpdateStrategy updateStrategy;
    private MOLockStrategy lockStrategy = new DefaultMOLockStrategy();

    /**
     * Enable or disable deadlock prevention (disabled by default).
     */
    private boolean deadlockPreventionEnabled = false;
    private Lock superLock;
    private final Map<LockRequest, Integer> waitingLockRequests = new WeakHashMap<>();
    private final Map<LockRequest, Integer> activeLockRequests = new WeakHashMap<>();

    public DefaultMOServer() {
        this.registry = new TreeMap<>(new MOScopeComparator());
        this.contexts = new LinkedHashSet<>(10);
        this.lockList = new Hashtable<>(10);
    }


    @Override
    @SuppressWarnings("rawtypes")
    public ManagedObject lookup(MOQuery query) {
        return lookup(query, false, null,
                new MOServerLookupEvent(this, null, query, MOServerLookupEvent.IntendedUse.undefined),
                ManagedObject.class);
    }

    /**
     * Lookup the first (lexicographically ordered) managed object that matches
     * the supplied query and implements the given {@link ManagedObject} class.
     * Locking will be performed according to the set {@link MOLockStrategy} before the lookup
     * listener is fired.
     * CAUTION: To make sure that the acquired lock is released after the
     * using of the managed object has been finished, the {@link #unlock(Object, ManagedObject)}
     * method must be called then.
     *
     * @param query
     *         a {@code MOQuery} instance.
     * @param lockRequest
     *         the {@link LockRequest} that holds the lock owner and the timeout for
     *         acquiring a lock and returns whether a lock has been acquired or not
     *         on behalf of this lookup operation.
     * @param lookupEvent
     *         provides additional information about the intended use and optionally a callback to be informed about
     *         the completion of the use, including a reference to its result.
     * @param managedObjectType
     *         the {@link ManagedObject} type filter. Only objects of this type will be looked up and returned.
     *
     * @return the {@code ManagedObject} that matches the query and
     * {@code null} if no such object exists.
     * @since 3.1
     */
    @Override
    public <MO extends ManagedObject<?>>
    MO lookup(MOQuery query, LockRequest lockRequest, MOServerLookupEvent lookupEvent, Class<MO> managedObjectType) {
        return lookup(query, false, lockRequest, lookupEvent, managedObjectType);
    }

    /**
     * Return the locking strategy for this server. The strategy defines if a lock is acquired before
     * a looked up {@link ManagedObject} is returned (and before the corresponding lookup event is being fired) or not.
     *
     * @return the managed object locking strategy instance used by this server. If {@code null}, no locking is performed
     * at all (which is only recommended for static content servers).
     * @since 2.4.0
     */
    public MOLockStrategy getLockStrategy() {
        return lockStrategy;
    }

    /**
     * Sets the lock strategy for this server. The strategy defines if a lock is acquired before
     * a looked up {@link ManagedObject} is returned (and before the corresponding lookup event is being fired) or not.
     * By default, only write access needs a lock.
     *
     * @param lockStrategy
     *         a {@link MOLockStrategy} instance or {@code null} to suppress any locking.
     *
     * @since 2.4.0
     */
    public void setLockStrategy(MOLockStrategy lockStrategy) {
        this.lockStrategy = lockStrategy;
    }

    private <MO extends ManagedObject<?>> MO lookup(MOQuery query,
                                                    boolean specificRegistrationsOnly, LockRequest lockRequest,
                                                    MOServerLookupEvent event, Class<MO> managedObjectType) {

        SortedMap<MOScope, ManagedObject<?>> scope = registry.tailMap(query);
        boolean timedOut = false;
        if (lockRequest != null) {
            lockRequest.setLockRequestStatus(LockRequest.LockStatus.notRequired);
        }
        for (Entry<MOScope, ManagedObject<?>> entry : scope.entrySet()) {
            MOScope key = entry.getKey();
            if (!MOScopeComparator.isQueryContextMatching(query, key)) {
                continue;
            }
            if ((specificRegistrationsOnly) &&
                    (!(key instanceof MOContextScope) || (((MOContextScope) key).getContext() == null))) {
                continue;
            }
            ManagedObject<?> managedObject = entry.getValue();
            MOScope moScope = managedObject.getScope();
            if ((managedObjectType.isInstance(managedObject)) && query.getScope().isOverlapping(moScope)) {
                MO mo = managedObjectType.cast(managedObject);
                event.setLookupResult(mo);
                fireQueryEvent(mo, event);
                // apply locking if needed
                if ((lockStrategy != null) && (lockRequest != null) && (lockRequest.getLockOwner() != null) &&
                        lockStrategy.isLockNeeded(mo, query)) {
                    if (!lock(lockRequest.getLockOwner(), mo, lockRequest.getTimeoutMillis(), lockRequest)) {
                        timedOut = true;
                        continue;
                    } else {
                        lockRequest.setLockRequestStatus((timedOut) ?
                                LockRequest.LockStatus.lockedAfterTimeout : LockRequest.LockStatus.locked);
                    }
                }
                if (mo instanceof UpdatableManagedObject) {
                    checkForUpdate((UpdatableManagedObject<?>) mo, query);
                }
                if (query.matchesQuery(mo)) {
                    event.setLookupResult(mo);
                    fireLookupEvent(mo, event);
                    return mo;
                } else if (lockRequest != null) {
                    unlock(lockRequest.getLockOwner(), mo);
                }
            }
        }
        if (timedOut) {
            lockRequest.setLockRequestStatus(LockRequest.LockStatus.lockTimedOut);
        }
        return null;
    }

    /**
     * Checks {@link #updateStrategy} whether the queried managed object needs
     * to be updated. This method is called on behalf of
     * {@link #lookup(MOQuery query)} after {@link #fireQueryEvent} and before
     * {@link #fireLookupEvent} is being called.
     *
     * @param mo
     *         an UpdatableManagedObject instance.
     * @param query
     *         the query that is interested in content of {@code mo}.
     *
     * @since 1.2
     */
    protected void checkForUpdate(UpdatableManagedObject<?> mo, MOQuery query) {
        if (updateStrategy != null) {
            if (updateStrategy.isUpdateNeeded(this, mo, query)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Updating UpdatableManagedObject " + mo +
                            " on behalf of query " + query);
                }
                mo.update(query);
            }
        }
    }

    /**
     * Returns the {@code ManagedObject} with the specified {@code OID}
     * as ID returned by {@link RegisteredManagedObject#getID()} or the lower bound
     * (regardless whether the ManagedObject's scope includes it or not)
     * when registered in the supplied context.
     * <p>
     * Note: The query used to look up the managed object will indicate intended
     * read-only access for the {@link MOServerLookupEvent}s fired on behalf of
     * this method.
     *
     * @param key
     *         the OID identifying the key (lower bound) of the
     *         {@code ManagedObject}.
     * @param context
     *         the optional context to look in. A {@code null} value searches
     *         in all contexts.
     * @param fireLookupEvents
     *         if {@code true} lookup and query events will be fired as if the managed
     *         objects has been looked up by {@link #lookup(MOQuery)}. In addition, if
     *         the looked up managed object is an {@link UpdatableManagedObject} it will be locked
     *         if the lock strategy of this server requires it. The lock is active until the look-up
     *         events have been fired completely. The lock operation waits without timeout for the lock
     *         to become available.
     *         Otherwise, no events will be fired at all.
     *
     * @return the {@code ManagedObject} instance or {@code null} if such
     * an instance does not exist.
     * @since 2.3
     */
    public ManagedObject<?> getManagedObject(OID key, OctetString context, boolean fireLookupEvents) {
        MOContextScope scope =
                new DefaultMOContextScope(context, key, true, key, true);
        MOQuery query = new DefaultMOQuery(scope);
        SortedMap<MOScope, ManagedObject<?>> reducedScope = registry.tailMap(query);
        for (Entry<MOScope, ManagedObject<?>> entry : reducedScope.entrySet()) {
            MOScope compareScope = entry.getKey();
            if (!MOScopeComparator.isQueryContextMatching(query, compareScope)) {
                continue;
            }
            ManagedObject<?> mo = entry.getValue();
            if (((mo instanceof RegisteredManagedObject) && ((RegisteredManagedObject<?>) mo).getID().equals(key)) ||
                    (!(mo instanceof RegisteredManagedObject) && key.equals(mo.getScope().getLowerBound()))) {
                if (fireLookupEvents) {
                    MOServerLookupEvent event =
                            new MOServerLookupEvent(this, mo, query, MOServerLookupEvent.IntendedUse.undefined);
                    fireQueryEvent(mo, event);
                    boolean locked = false;
                    if (mo instanceof UpdatableManagedObject) {
                        locked = lock(this, mo);
                        if (locked) {
                            checkForUpdate((UpdatableManagedObject<?>) mo, query);
                        }
                    }
                    fireLookupEvent(mo, event);
                    if (locked) {
                        unlock(this, mo);
                    }
                }
                return mo;
            }
        }
        return null;
    }

    /**
     * Returns the {@code ManagedObject} with the specified {@code OID}
     * as ID returned by {@link RegisteredManagedObject#getID()} or the lower bound
     * (regardless whether the ManagedObject's scope includes it or not)
     * when registered in the supplied context.
     * <p>
     * Note: The query used to look up the managed object will indicate intended
     * read-only access for the {@link MOServerLookupEvent}s fired on behalf of
     * this method.
     *
     * @param key
     *         the OID identifying the key (lower bound) of the
     *         {@code ManagedObject}.
     * @param context
     *         the optional context to look in. A {@code null} value searches
     *         in all contexts.
     *
     * @return the {@code ManagedObject} instance or {@code null} if such
     * an instance does not exist.
     * @since 1.1
     */
    public ManagedObject<?> getManagedObject(OID key, OctetString context) {
        return getManagedObject(key, context, true);
    }

    /**
     * Returns the value of a particular MIB object instance using the
     * {@link ManagedObjectValueAccess} interface. If a {@link ManagedObject}
     * does not support this interface, its value cannot be returned and
     * {@code null} will be returned instead.
     * Note: This method does not perform any locking based on the {@link MOLockStrategy}.
     *
     * @param server
     *         the {@code MOServer} where to lookup the value.
     * @param context
     *         the optional context to look in. A {@code null} value searches
     *         in all contexts.
     * @param key
     *         the OID identifying the variable instance to return.
     *
     * @return the {@code Variable} associated with {@code OID} and
     * {@code context} in {@code server} or {@code null} if
     * no such variable exists.
     * @since 1.4
     */
    public static Variable getValue(MOServer server, OctetString context, OID key) {
        MOContextScope scope =
                new DefaultMOContextScope(context, key, true, key, true);
        MOServerLookupEvent lookupEvent =
                new MOServerLookupEvent(server, null, new DefaultMOQuery(scope),
                        MOServerLookupEvent.IntendedUse.get, true);
        ManagedObject<?> mo = server.lookup(lookupEvent.getQuery(), null, lookupEvent);
        if (mo instanceof ManagedObjectValueAccess) {
            Variable variable = ((ManagedObjectValueAccess<?>) mo).getValue(key);
            lookupEvent.completedUse(variable);
            return variable;
        }
        return null;
    }

    /**
     * Sets the value of a particular MIB object instance using the
     * {@link ManagedObjectValueAccess} interface. If a {@link ManagedObject}
     * does not support this interface, its value cannot be set and
     * {@code false} will be returned.
     * Note: This method does not perform any locking based on the {@link MOLockStrategy}.
     *
     * @param server
     *         the {@code MOServer} where to lookup the value.
     * @param context
     *         the optional context to look in. A {@code null} value searches
     *         in all contexts.
     * @param newValueAndKey
     *         the OID identifying the variable instance to set and its new value.
     *
     * @return the {@code true} if the value has been set successfully,
     * {@code false} otherwise.
     * @since 1.4
     */
    public static boolean setValue(MOServer server, OctetString context,
                                   VariableBinding newValueAndKey) {
        OID key = newValueAndKey.getOid();
        MOContextScope scope =
                new DefaultMOContextScope(context, key, true, key, true);
        MOServerLookupEvent lookupEvent = new MOServerLookupEvent(server, null,
                new DefaultMOQuery(scope), MOServerLookupEvent.IntendedUse.update, true);
        ManagedObject<?> mo = server.lookup(lookupEvent.getQuery(), null, lookupEvent);
        if (mo instanceof ManagedObjectValueAccess) {
            boolean variableSet = ((ManagedObjectValueAccess<?>) mo).setValue(newValueAndKey);
            if (variableSet) {
                lookupEvent.completedUse(newValueAndKey);
            } else {
                lookupEvent.completedUse(null);
            }
            return variableSet;
        }
        lookupEvent.completedUse(null);
        return false;
    }

    protected void fireLookupEvent(ManagedObject<?> mo, MOServerLookupEvent event) {
        if (lookupListener != null) {
            List<MOServerLookupListener> l = lookupListener.get(mo);
            if (l != null) {
                callLookupListeners(event, l);
            }
            l = lookupListener.get(null);
            if (l != null) {
                callLookupListeners(event, l);
            }
        }
    }

    private void callLookupListeners(MOServerLookupEvent event, List<MOServerLookupListener> l) {
        // avoid concurrent modification exception
        ArrayList<MOServerLookupListener> listCopy = new ArrayList<>(l);
        for (MOServerLookupListener item : listCopy) {
            item.lookupEvent(event);
        }
    }

    protected void fireQueryEvent(ManagedObject<?> mo, MOServerLookupEvent event) {
        if (lookupListener != null) {
            List<MOServerLookupListener> l = lookupListener.get(mo);
            if (l != null) {
                // avoid concurrent modification exception
                l = new ArrayList<>(l);
                for (MOServerLookupListener item : l) {
                    item.queryEvent(event);
                }
            }
        }
    }

    public OctetString[] getContexts() {
        return contexts.toArray(new OctetString[0]);
    }

    public boolean isContextSupported(OctetString context) {
        if ((context == null) || (context.length() == 0)) {
            return true;
        }
        return contexts.contains(context);
    }

    public SortedMap<MOScope, ManagedObject<?>> getRegistry() {
        return registry;
    }

    /**
     * Gets the update strategy for {@link UpdatableManagedObject}s. If the
     * strategy is {@code null} no updates will be performed on behalf
     * of calls to {@link #lookup}.
     *
     * @return the current UpdateStrategy instance or {@code null} if no
     * strategy is active.
     * @see #lookup
     * @since 1.2
     */
    public UpdateStrategy getUpdateStrategy() {
        return updateStrategy;
    }

    /**
     * Sets the update strategy for {@link UpdatableManagedObject}s. If the
     * strategy is {@code null} no updates will be performed on behalf
     * of calls to {@link #lookup(MOQuery)}.
     *
     * @param updateStrategy
     *         the new UpdateStrategy instance or {@code null} if no
     *         updates should be performed.
     * @return this object for fluently connecting config settings.
     * @see #lookup(MOQuery)
     * @since 1.2
     */
    public DefaultMOServer setUpdateStrategy(UpdateStrategy updateStrategy) {
        this.updateStrategy = updateStrategy;
        return this;
    }

    @Override
    public void register(ManagedObject<?> mo, OctetString context) throws DuplicateRegistrationException {
        if ((context == null) || (context.length() == 0)) {
            MOContextScope contextScope =
                    new DefaultMOContextScope(null, mo.getScope());
            MOServerLookupEvent lookupEvent =
                    new MOServerLookupEvent(this, mo, new DefaultMOQuery(contextScope),
                            MOServerLookupEvent.IntendedUse.register);
            ManagedObject<?> other = lookup(new DefaultMOQuery(contextScope), null, lookupEvent);
            if (other != null) {
                throw new DuplicateRegistrationException(contextScope, other.getScope());
            }
            registry.put(mo.getScope(), mo);
            if (logger.isInfoEnabled()) {
                logger.info("Registered MO [" + mo.getClass().getSimpleName() + "] in default context with scope " +
                        mo.getScope());
            }
        } else {
            DefaultMOContextScope contextScope =
                    new DefaultMOContextScope(context, mo.getScope());
            MOServerLookupEvent lookupEvent =
                    new MOServerLookupEvent(this, mo, new DefaultMOQuery(contextScope),
                            MOServerLookupEvent.IntendedUse.register);
            ManagedObject<?> other =
                    lookup(lookupEvent.getQuery(), true, null, lookupEvent, ManagedObject.class);
            if (other != null) {
                throw new DuplicateRegistrationException(contextScope, other.getScope());
            }
            registry.put(contextScope, mo);
            if (logger.isInfoEnabled()) {
                logger.info("Registered MO [" + mo.getClass().getSimpleName() + "] in context " + context + " with scope " +
                        contextScope);
            }
        }
    }

    @Override
    public ManagedObject<?> unregister(ManagedObject<?> mo, OctetString context) {
        MOScope key;
        if ((context == null) || (context.length() == 0)) {
            key = mo.getScope();
        } else {
            key = new DefaultMOContextScope(context, mo.getScope());
        }
        ManagedObject<?> r = registry.remove(key);
        if (r == null) {
            // OK, may be the upper bound of the scope has been adjusted so we need to
            // check that by iterating
            SortedMap<MOScope, ManagedObject<?>> tailMap = registry.tailMap(key);
            for (Iterator<Entry<MOScope, ManagedObject<?>>> it = tailMap.entrySet().iterator(); it.hasNext(); ) {
                Entry<MOScope, ManagedObject<?>> entry = it.next();
                MOScope entryKey = entry.getKey();
                if ((entry.getValue().equals(mo)) &&
                        (context == null || ((entryKey instanceof MOContextScope) &&
                                (context.equals(((MOContextScope) entryKey).getContext()))))) {
                    r = entry.getValue();
                    it.remove();
                    break;
                }
            }
        }
        if (r != null) {
            MOServerLookupEvent event =
                    new MOServerLookupEvent(this, r, new DefaultMOQuery(key instanceof MOContextScope ?
                            (MOContextScope) key : new DefaultMOContextScope(null, key)),
                            MOServerLookupEvent.IntendedUse.unregister);
            fireLookupEvent(r, event);
        }
        if (logger.isInfoEnabled()) {
            if (r != null) {
                logger.info("Removed registration " + r + " for " + mo.getScope() +
                        " in context '" + context + "' successfully");
            } else {
                logger.warn("Removing registration failed for " + mo.getScope() +
                        " in context '" + context + "'");
            }
        }
        return r;
    }

    public void addContext(OctetString context) {
        contexts.add(context);
        fireContextChanged(new ContextEvent(this, ContextEvent.CONTEXT_ADDED, context));
    }

    public void removeContext(OctetString context) {
        contexts.remove(context);
        fireContextChanged(new ContextEvent(this, ContextEvent.CONTEXT_REMOVED, context));
    }

    public boolean lock(Object owner, ManagedObject<?> managedObject) {
        return lock(owner, managedObject, 0, new LockRequest(owner, 0));
    }

    public boolean lock(Object owner, ManagedObject<?> managedObject, long timeoutMillis) {
        return lock(owner, managedObject, timeoutMillis, new LockRequest(owner, timeoutMillis));
    }

    /**
     * Resets the lock list and all active and waiting locks. This might cause threads / {@link LockRequest}
     * (= transactions) waiting for locks to continue without still having locked the locks they already acquired.
     * To cleanly reset the locks, first set the {@link #setLockStrategy(MOLockStrategy)} to
     * {@code (managedObjectLookedUp, query) -> false} and then wait for {@link #getActiveLockRequests()} become zero.
     * {@since 3.7.0}
     */
    public synchronized void resetLocks() {
        waitingLockRequests.clear();
        activeLockRequests.clear();
        lockList.clear();
        superLock = null;
        notifyAll();
    }

    /**
     * Wait until this server has not active and pending locks anymore.
     * @param waitTimeoutMillis
     *   the maximum time in milliseconds to wait for this server to become lock free.
     * @return
     *   {@code true} if server has no active and waiting locks anymore, {@code false} otherwise.
     * @since 3.7.0
     */
    @Override
    public synchronized boolean waitForUnlockedState(long waitTimeoutMillis) {
        if (logger.isInfoEnabled()) {
            logger.info("Waiting for unlocked state with timeout millis "+waitTimeoutMillis+
                    ", active locks: "+lockList+", waitingLockRequests: "+ waitingLockRequests);
        }
        long waitStart = System.nanoTime();
        while ((!lockList.isEmpty() || (!waitingLockRequests.isEmpty())) &&
                ((waitTimeoutMillis <= 0) ||
                ((System.nanoTime() - waitStart) > TimeUnit.MICROSECONDS.toNanos(waitTimeoutMillis)))) {
            try {
            if (waitTimeoutMillis <= 0) {
                    wait(0);
                }
                else {
                    long waitTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - waitStart);
                    wait(waitTime);
                }
            } catch (InterruptedException e) {
                return false;
            }
        }
        if (logger.isInfoEnabled()) {
            logger.info("Finished waiting for unlocked state in "+
                    TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-waitStart)+ "ms"+
                    ", active locks: "+lockList+", waitingLockRequests: "+ waitingLockRequests);
        }
        return lockList.isEmpty() && waitingLockRequests.isEmpty();
    }

    /**
     * Returns a readonly map of waiting {@link LockRequest}s and their locks or an empty set
     * if {@link #isDeadlockPreventionEnabled()} is {@code false}.
     * @return
     *    the lock requests (i.e., transactions) that wait for a lock.
     * @since 3.7.0
     */
    public Map<LockRequest, Integer> getWaitingLockRequests() {
        return Collections.unmodifiableMap(waitingLockRequests);
    }

    /**
     * Returns a readonly set of active {@link LockRequest}s or an empty set if {@link #isDeadlockPreventionEnabled()}
     * is {@code false}.
     * @return
     *    the lock requests (i.e., transactions) that hold at least one lock.
     * @since 3.7.0
     */
    public Map<LockRequest, Integer> getActiveLockRequests() {
        return Collections.unmodifiableMap(activeLockRequests);
    }

    public synchronized boolean lock(Object owner, ManagedObject<?> managedObject, long timeoutMillis,
                                     LockRequest lockRequest) {
        Lock lock;
        long start = System.nanoTime();
        lockRequest.setLockRequestStatus(LockRequest.LockStatus.lockRequired);
        do {
            if (deadlockPreventionEnabled && superLock != null && superLock.getOwner() != owner) {
                if (!waitForLock(managedObject, timeoutMillis, superLock, start)) {
                    return false;
                }
            }
            else if (deadlockPreventionEnabled && superLock != null && superLock.getOwner() == owner) {
                superLock.add();
                if (logger.isDebugEnabled()) {
                    logger.debug("Acquired super lock "+superLock+" on " + managedObject + " for " + owner);
                }
                return true;
            }
            else if (superLock == null) {
                lock = lockList.get(managedObject);
                if ((lock == null) || ((lock.getOwner() != owner) && (lock.getCount() <= 0))) {
                    Lock newLock = new Lock(lockRequest);
                    lockRequest.setLockRequestStatus(LockRequest.LockStatus.locked);
                    lockList.put(managedObject, newLock);
                    if (deadlockPreventionEnabled) {
                        incLockRequestCounter(activeLockRequests, lockRequest);
                    }
                    if (logger.isDebugEnabled()) {
                        logger.debug("Acquired lock "+newLock+" on " + managedObject + " for " + owner +
                                " (waiting="+waitingLockRequests.size()+",active="+activeLockRequests.size()+")");
                    }
                    return true;
                } else if (lock.getOwner() != owner) {
                    // deadlock detection
                    int waitingCount = waitingLockRequests.size()+1;
                    int activeCount = activeLockRequests.size();
                    if (deadlockPreventionEnabled && (!waitingLockRequests.isEmpty() && waitingCount >= activeCount)) {
//                        if (deadlockPreventionEnabled) activeLockRequests.add(lockRequest);
                        // deadlock condition
                        switch (lock.getLockRequest().getLockRequestStatus()) {
                            case waitingForLock:
                            case lockRequired:
                            case unlocked:
                                superLock = new Lock(lockRequest);
                                lockRequest.setLockRequestStatus(LockRequest.LockStatus.locked);
                                if (logger.isDebugEnabled()) {
                                    logger.debug("Deadlock condition detected on "+lock+" (waiting="+waitingCount+
                                            " >= active="+activeCount+
                                            ", acquired super "+superLock+" for " + owner);
                                }
                                return true;
                            default:
                                if (logger.isDebugEnabled()) {
                                    logger.debug("Waiting for running thread on "+lock+
                                            " to unlock it before entering in super lock mode, waitingCount=" +
                                            waitingLockRequests.size() + ", activeCount=" + activeLockRequests.size());
                                }
                                lockRequest.setLockRequestStatus(LockRequest.LockStatus.waitingForLock);
                                if (!waitForLock(managedObject, timeoutMillis, lock, start)) return false;
                        }
                    }
                    else {
                        lockRequest.setLockRequestStatus(LockRequest.LockStatus.waitingForLock);
                        if (!waitForLock(managedObject, timeoutMillis, lock, start)) break;
                        if (logger.isDebugEnabled()) {
                            logger.debug("Waiting thread on "+lock+" was notified, waitingCount=" +
                                    waitingLockRequests.size() + ", activeCount=" + activeLockRequests.size());
                        }
                    }
                } else {
                    lock.add();
                    if (deadlockPreventionEnabled) {
                        incLockRequestCounter(activeLockRequests, lockRequest);
                    }
                    if (logger.isDebugEnabled()) {
                        logger.debug("Added lock on " + managedObject + " for " + owner);
                    }
                    return true;
                }
            }
        }
        while (((timeoutMillis <= 0) ||
                ((System.nanoTime() - start) / SnmpConstants.MILLISECOND_TO_NANOSECOND < timeoutMillis)));
        return false;
    }

    private synchronized void incLockRequestCounter(Map<LockRequest, Integer> counter, LockRequest lockRequest) {
        int counterValue = counter.getOrDefault(lockRequest, 0);
        counterValue++;
        counter.put(lockRequest, counterValue);
    }

    private synchronized void decLockRequestCounter(Map<LockRequest, Integer> counter, LockRequest lockRequest) {
        counter.computeIfPresent(lockRequest, (lr, counterValue) -> {
            int updatedCount = counterValue - 1;
            if (updatedCount <= 0) return null;
            return updatedCount;
        });
    }

    private synchronized boolean waitForLock(ManagedObject<?> managedObject, long timeoutMillis, Lock lock, long start) {
        try {
            if (logger.isDebugEnabled()) {
                logger.debug("Waiting for "+lock+" on " + managedObject + " with timeout (ms) "+ timeoutMillis);
            }
            if (deadlockPreventionEnabled && lock != superLock) {
                incLockRequestCounter(waitingLockRequests, lock.getLockRequest());
            }
            if (timeoutMillis <= 0) {
                wait();
            } else {
                wait(Math.max(timeoutMillis -
                        (System.nanoTime() - start) / SnmpConstants.MILLISECOND_TO_NANOSECOND, 1));
            }
        } catch (InterruptedException ex) {
            logger.warn("Waiting for lock "+lock+" on " + managedObject + " has been interrupted!");
            return false;
        }
        finally {
            if (deadlockPreventionEnabled && lock != superLock) {
                decLockRequestCounter(waitingLockRequests, lock.getLockRequest());
            }
        }
        return true;
    }

    public synchronized boolean unlockNow(Object owner, ManagedObject<?> managedObject) {
        return unlock(owner, managedObject, true);
    }

    public synchronized boolean unlock(Object owner, ManagedObject<?> managedObject) {
        return unlock(owner, managedObject, false);
    }

    protected synchronized boolean unlock(Object owner, ManagedObject<?> managedObject, boolean forceUnlock) {
        if (managedObject != null) {
            Lock lock = lockList.get(managedObject);
            if (lock != null) {
                if (lock.getOwner() != owner) {
                    if ((superLock == null) && logger.isDebugEnabled()) {
                        logger.debug("Object '" + owner + "' is not owner of lock: " + lock);
                    }
                } else if (lock.remove() || forceUnlock) {
                    lockList.remove(managedObject);
                    if (deadlockPreventionEnabled && lock != superLock) {
                        decLockRequestCounter(activeLockRequests, lock.getLockRequest());
                    }
                    if (logger.isDebugEnabled()) {
                        logger.debug("Removed "+lock+" on " + managedObject + " by " + owner);
                    }
                    notifyAll();
                    return true;
                }
            }
            if (superLock != null && superLock.getOwner() == owner) {
                superLock.remove();
                if (superLock.getCount() <= 0 || forceUnlock) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("End of deadlock prevention: Removed super lock "+superLock+
                                " on " + managedObject + " by " + owner);
                    }
                    superLock = null;
                }
                else if (logger.isDebugEnabled()) {
                    logger.debug("Removed super lock "+superLock+" on " + managedObject + " by " + owner);
                }
                notifyAll();
                return true;
            }
        }
        return false;
    }

    @Override
    public Iterator<Entry<MOScope, ManagedObject<?>>> iterator() {
        return iterator(new MOScopeComparator(), null);
    }

    @Override
    public synchronized Iterator<Entry<MOScope, ManagedObject<?>>> iterator(Comparator<MOScope> comparator, MOFilter moFilter) {
        synchronized (registry) {
            SortedMap<MOScope, ManagedObject<?>> r = new TreeMap<>(comparator);
            if (moFilter != null) {
                for (Map.Entry<MOScope, ManagedObject<?>> entry : registry.entrySet()) {
                    if (moFilter.passesFilter(entry.getValue())) {
                        r.put(entry.getKey(), entry.getValue());
                    }
                }
            }
            else {
                r.putAll(registry);
            }
            return r.entrySet().iterator();
        }
    }

    public synchronized void addLookupListener(MOServerLookupListener listener,
                                               ManagedObject<?> mo) {
        if (lookupListener == null) {
            lookupListener = Collections.synchronizedMap(new HashMap<>());
        }
        List<MOServerLookupListener> l =
                lookupListener.computeIfAbsent(mo, k -> Collections.synchronizedList(new LinkedList<>()));
        l.add(listener);
    }

    public synchronized boolean removeLookupListener(MOServerLookupListener listener, ManagedObject<?> mo) {
        if (lookupListener != null) {
            List<MOServerLookupListener> l = lookupListener.get(mo);
            if (l != null) {
                return l.remove(listener);
            }
        }
        return false;
    }

    public synchronized void addContextListener(ContextListener l) {
        if (contextListeners == null) {
            contextListeners = new ArrayList<>(2);
        }
        contextListeners.add(l);
    }

    public synchronized void removeContextListener(ContextListener l) {
        if (contextListeners != null) {
            contextListeners.remove(l);
        }
    }

    protected void fireContextChanged(ContextEvent event) {
        if (contextListeners != null) {
            List<ContextListener> listeners = contextListeners;
            for (ContextListener listener : listeners) {
                listener.contextChanged(event);
            }
        }
    }

    public String toString() {
        StringBuilder buf = new StringBuilder(getClass().getName());
        buf.append("[contexts=");
        buf.append(contexts);
        buf.append("[keys={");
        for (Iterator<MOScope> it = registry.keySet().iterator(); it.hasNext(); ) {
            MOScope scope = it.next();
            buf.append(scope.getLowerBound());
            if (scope.isLowerIncluded()) {
                buf.append("+");
            }
            buf.append("-");
            buf.append(scope.getUpperBound());
            if (scope.isUpperIncluded()) {
                buf.append("+");
            }
            if (scope instanceof MOContextScope) {
                buf.append("(").append(((MOContextScope) scope).getContext()).append(")");
            }
            if (it.hasNext()) {
                buf.append(",");
            }
        }
        buf.append("}");
        buf.append(",registry=").append(registry);
        buf.append(",lockList=").append(lockList);
        buf.append(",lookupListener=").append(lookupListener);
        buf.append("]");
        return buf.toString();
    }

    @Override
    public OctetString[] getRegisteredContexts(ManagedObject<?> managedObject) {
        Set<OctetString> contextSet = new HashSet<>();
        SortedMap<MOScope, ManagedObject<?>> scope = registry.tailMap(new DefaultMOContextScope(null,
                managedObject.getScope()));
        for (Entry<MOScope, ManagedObject<?>> entry : scope.entrySet()) {
            MOScope key = entry.getKey();
            ManagedObject<?> o = entry.getValue();
            if (managedObject.equals(o)) {
                if (key instanceof MOContextScope) {
                    contextSet.add(((MOContextScope) key).getContext());
                } else {
                    contextSet.add(null);
                }
            }
        }
        return contextSet.toArray(new OctetString[0]);
    }

    @Override
    public Map<OctetString, MOScope> getRegisteredScopes(ManagedObject<?> managedObject) {
        Map<OctetString, MOScope> scopes = new HashMap<>();
        SortedMap<MOScope, ManagedObject<?>> scope = registry.tailMap(new DefaultMOContextScope(null,
                managedObject.getScope()));
        for (Entry<MOScope, ManagedObject<?>> entry : scope.entrySet()) {
            MOScope key = entry.getKey();
            ManagedObject<?> o = entry.getValue();
            if (managedObject.equals(o)) {
                if (key instanceof MOContextScope) {
                    scopes.put(((MOContextScope)key).getContext(), key);
                }
                else {
                    scopes.put(null, key);
                }
            }
        }
        return scopes;
    }

    /**
     * Register a single {@link MOTableRowListener} with all tables in the
     * specified {@link MOServer}. This overall registration can be used,
     * for example, to apply table size limits to all tables in an agent.
     * See {@link org.snmp4j.agent.mo.util.MOTableSizeLimit} for details.
     * <p>
     * Note: The server must not change its registration content while this
     * method is being called, otherwise a
     * {@link java.util.ConcurrentModificationException} might be thrown.
     *
     * @param server
     *         a {@code MOServer} instance.
     * @param listener
     *         the {@code MOTableRowListener} instance to register.
     * @param <R>
     *         the {@link MOTableRow} type supported by the table row listener to register.
     * @param <T>
     *         the {@link MOTable} type supported by the table row listener to register.
     *
     * @since 1.4
     */
    @SuppressWarnings({"unchecked","rawtypes"})
    public static <R extends MOTableRow, T extends MOTable<R, ? extends MOColumn, ? extends MOTableModel<R>>>
    void registerTableRowListener(MOServer server, MOTableRowListener<R> listener) {
        for (Iterator<Map.Entry<MOScope, ManagedObject<?>>> it = server.iterator(); it.hasNext(); ) {
            ManagedObject<?> mo = it.next().getValue();
            if (mo instanceof MOTable) {
                ((T) mo).addMOTableRowListener(listener);
            }
        }
    }

    /**
     * Unregister a single {@link MOTableRowListener} with all tables in the
     * specified {@link MOServer}. This overall unregistration can be used,
     * for example, to remove table size limits from all tables in an agent.
     * See {@link org.snmp4j.agent.mo.util.MOTableSizeLimit} for details.
     * <p>
     * Note: The server must not change its registration content while this
     * method is being called, otherwise a
     * {@link java.util.ConcurrentModificationException} might be thrown.
     *
     * @param server
     *         a {@code MOServer} instance.
     * @param listener
     *         the {@code MOTableRowListener} instance to unregister.
     * @param <R>
     *         the {@link MOTableRow} type supported by the table row listener to register.
     * @param <T>
     *         the {@link MOTable} type supported by the table row listener to register.
     *
     * @since 1.4
     */
    @SuppressWarnings({"unchecked","rawtypes"})
    public static <R extends MOTableRow, T extends MOTable<R, ? extends MOColumn, ? extends MOTableModel<R>>>
    void unregisterTableRowListener(MOServer server, MOTableRowListener<R> listener) {
        for (Iterator<Map.Entry<MOScope, ManagedObject<?>>> it = server.iterator(); it.hasNext(); ) {
            ManagedObject<?> mo = it.next().getValue();
            if (mo instanceof MOTable) {
                ((T) mo).removeMOTableRowListener(listener);
            }
        }
    }

    /**
     * Register a single {@link MOChangeListener} with all objects matching the given filter in the
     * specified {@link MOServer}. This overall registration can be used,
     * for example, to listen for object changes.
     * <p>
     * Note: The server must not change its registration content while this
     * method is being called, otherwise a
     * {@link java.util.ConcurrentModificationException} might be thrown.
     *
     * @param server
     *         a {@code MOServer} instance.
     * @param listener
     *         the {@code MOTableRowListener} instance to register.
     * @param moFilter
     *         an optional filter to select objects which should register the provided listener.
     *
     * @since 3.0
     */
    public static void registerChangeListener(MOServer server, MOChangeListener listener, MOFilter moFilter) {
        for (Iterator<Map.Entry<MOScope, ManagedObject<?>>> it = server.iterator(); it.hasNext(); ) {
            ManagedObject<?> mo = it.next().getValue();
            if (mo instanceof ChangeableManagedObject && (moFilter == null || moFilter.passesFilter(mo))) {
                ((ChangeableManagedObject<?>) mo).addMOChangeListener(listener);
            }
        }
    }

    /**
     * Unregister a single {@link MOChangeListener} from all objects matching the given filter in the
     * specified {@link MOServer}.
     * <p>
     * Note: The server must not change its registration content while this
     * method is being called, otherwise a
     * {@link java.util.ConcurrentModificationException} might be thrown.
     *
     * @param server
     *         a {@code MOServer} instance.
     * @param listener
     *         the {@code MOTableRowListener} instance to unregister.
     * @param moFilter
     *         an optional filter to select objects which should no longer register the provided listener.
     *
     * @since 3.0
     */
    public static void unregisterChangeListener(MOServer server, MOChangeListener listener, MOFilter moFilter) {
        for (Iterator<Map.Entry<MOScope, ManagedObject<?>>> it = server.iterator(); it.hasNext(); ) {
            ManagedObject<?> mo = it.next().getValue();
            if (mo instanceof ChangeableManagedObject && (moFilter == null || moFilter.passesFilter(mo))) {
                ((ChangeableManagedObject<?>) mo).removeMOChangeListener(listener);
            }
        }
    }

    /**
     * Returns {@code true} if the {@link #lookup(MOQuery, LockRequest, MOServerLookupEvent, Class)} method uses
     * deadlock prevention using a super thread, if a deadlock situation is detected (i.e.,
     * waiting {@link LockRequest}s equal active {@link LockRequest}s. Using the super thread could reduce parallelism
     * unnecessarily to one and detecting (possible) deadlock situations creates additional overhead.
     * For most use cases, setting
     * {@link CommandProcessor#setLockNonNextRequestsSortedByVbOid(boolean)}
     * to {@code true} is sufficient to prevent deadlocks, if API calls to
     * {@link #lookup(MOQuery, LockRequest, MOServerLookupEvent, Class)} are not locking more than one
     * {@link ManagedObject} at once for a single {@link LockRequest}.
     * @return
     *    {@code true} if super-thread based deadlock prevention is enabled or {@code false} otherwise (default).
     * @since 3.7.0
     */
    public boolean isDeadlockPreventionEnabled() {
        return deadlockPreventionEnabled;
    }

    /**
     * Enable or disable the super-thread based deadlock prevention for the
     * {@link #lookup(MOQuery, LockRequest, MOServerLookupEvent, Class)} method. If enabled and a deadlock situation
     * is detected (i.e., the number of waiting {@link LockRequest}s equals active {@link LockRequest}s, a super-thread
     * will be used to run the latest lookup while all other lookups are blocked until this one is unlocked (finished).
     * Using the super thread could reduce parallelism unnecessarily to one and detecting (possible) deadlock situations
     * creates additional overhead.
     * For most use cases, setting
     * {@link CommandProcessor#setLockNonNextRequestsSortedByVbOid(boolean)}
     * to {@code true} is sufficient to prevent deadlocks, if API calls to
     * {@link #lookup(MOQuery, LockRequest, MOServerLookupEvent, Class)} are not locking more than one
     * {@link ManagedObject} at once for a single {@link LockRequest}.
     * @param deadlockPreventionEnabled
     *    {@code true} to enable super-thread based deadlock prevention. Deadlock prevention is disabled by default.
     * @return this object instance for fluently connecting config settings.
     * @since 3.7.0
     */
    public DefaultMOServer setDeadlockPreventionEnabled(boolean deadlockPreventionEnabled) {
        this.deadlockPreventionEnabled = deadlockPreventionEnabled;
        return this;
    }


    static class Lock {
        private final long creationTime;
        private volatile int count = 0;
        private LockRequest lockRequest;

        private Lock() {
            this.creationTime = System.currentTimeMillis();
            this.count = 0;
        }

        Lock(LockRequest lockRequest) {
            this();
            this.lockRequest = lockRequest;
            this.count = 1;
        }

        public long getCreationTime() {
            return creationTime;
        }

        public int getCount() {
            return count;
        }

        public synchronized void add() {
            count++;
        }

        public synchronized boolean remove() {
            if ((--count) <= 0) {
                if (count != 0) {
                    count = 0;
                }
                return true;
            }
            return false;
        }

        public Object getOwner() {
            return lockRequest.getLockOwner();
        }

        public LockRequest getLockRequest() {
            return lockRequest;
        }

        @Override
        public String toString() {
            return "Lock[" +
                    ",creationTime=" + creationTime +
                    ",count=" + count +
                    ",lockRequest=" + lockRequest +
                    ']';
        }
    }

}
