/*_############################################################################
  _## 
  _##  SNMP4J-AgentX - AgentXNode.java  
  _## 
  _##  Copyright (C) 2005-2026  Frank Fock (SNMP4J.org)
  _##  
  _##  This program is free software; you can redistribute it and/or modify
  _##  it under the terms of the GNU General Public License version 2 as 
  _##  published by the Free Software Foundation.
  _##
  _##  This program is distributed in the hope that it will be useful,
  _##  but WITHOUT ANY WARRANTY; without even the implied warranty of
  _##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  _##  GNU General Public License for more details.
  _##
  _##  You should have received a copy of the GNU General Public License
  _##  along with this program; if not, write to the Free Software
  _##  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
  _##  MA  02110-1301  USA
  _##  
  _##########################################################################*/

package org.snmp4j.agent.agentx.master;

import java.util.TreeSet;
import java.util.function.Function;

import org.snmp4j.agent.MOScope;
import org.snmp4j.agent.ManagedObject;
import org.snmp4j.agent.agentx.AgentXRegion;
import org.snmp4j.agent.mo.GenericManagedObject;
import org.snmp4j.agent.request.SnmpRequest;
import org.snmp4j.agent.request.SubRequest;
import org.snmp4j.smi.OID;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;
import org.snmp4j.smi.Null;
import org.snmp4j.PDU;
import org.snmp4j.agent.request.SubRequestIterator;

/**
 * A {@code AgentXNode} represents an atomic registration
 * region within the master agents {@link ManagedObject}s.
 * There can be several AgentXNodes for a single AgentX
 * region registration.
 *
 * @author Frank Fock
 * @version 3.0
 */
public class AgentXNode implements GenericManagedObject {

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

    private TreeSet<AgentXRegEntry<?>> registrations = new TreeSet<>();
    private final AgentXRegion region;

    /**
     * Creates an {@link AgentXNode} for a region and corresponding registration.
     * @param region
     *    the region (scope) for the new {@link AgentXNode}.
     * @param registration
     *    the registration associated with {@code region}.
     */
    public AgentXNode(AgentXRegion region, AgentXRegEntry<?> registration) {
        this.region = new AgentXRegion(region);
        this.registrations.add(registration);
    }

    /**
     * Creates an {@link AgentXNode} for a region and corresponding registration.
     * @param region
     *    the region (scope) for the new {@link AgentXNode}.
     * @param registrations
     *    the registrations associated with {@code region}.
     */
    protected AgentXNode(AgentXRegion region, TreeSet<AgentXRegEntry<?>> registrations) {
        this.region = new AgentXRegion(region);
        this.registrations = registrations;
    }

    /**
     * Gets a deep clone of this node but with a new region.
     * @param region
     *    the new region for this clone which will be copied into the new node.
     * @return
     *    a new {@link AgentXNode} with the provided region and its registrations. Both as a copy of the original
     *    references.
     */
    @SuppressWarnings("unchecked")
    public AgentXNode getClone(AgentXRegion region) {
        return new AgentXNode(new AgentXRegion(region),
                (TreeSet<AgentXRegEntry<?>>) registrations.clone());
    }

    /**
     * Gets the number of registrations for this region/node.
     * @return
     *    number of registrations.
     */
    public int getRegistrationCount() {
        return registrations.size();
    }

    /**
     * Shrink a node's region the new upper bound.
     * @param upper
     *    the new upper bound.
     * @return
     *    {@code true} if the new upper bound is covered by the current region (thus shrinking had an effect).
     */
    public synchronized boolean shrink(OID upper) {
        if (region.covers(upper)) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Shrinking node " + toString() + " to " + upper);
            }
            this.region.setUpperBound(upper);
            return true;
        }
        return false;
    }

    /**
     * Expand a node's region the new upper bound.
     * @param upper
     *    the new upper bound.
     * @param inclusive
     *    determines of {@code upper} itself is included or not by the expanded region.
     * @return
     *    {@code true} if the new upper bound is not covered by the current region (thus shrinking had an effect).
     */
    public synchronized boolean expand(OID upper, boolean inclusive) {
        if ((!region.covers(upper)) && (region.getUpperBound().compareTo(upper) >= 0)) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Expanding node " + toString() + " to " + upper +
                        ", inclusive=" + inclusive);
            }
            this.region.setUpperBound(upper);
            this.region.setUpperIncluded(inclusive);
            return true;
        }
        return false;
    }

    /**
     * Adds a registration.
     * @param entry
     *   a new registration to be added to the node.
     */
    public synchronized void addRegistration(AgentXRegEntry<?> entry) {
        registrations.add(entry);
    }

    /**
     * Removes a registration.
     * @param entry
     *   the registration to remove.
     * @return
     *   {@code true} if successfully removed.
     */
    public synchronized boolean removeRegistration(AgentXRegEntry<?> entry) {
        boolean removed = registrations.remove(entry);
        if (LOGGER.isDebugEnabled()) {
            if (removed) {
                LOGGER.debug("Removed registration " + entry +
                        " from AgentX node " + toString());
            } else {
                LOGGER.debug("Removing registration failed for " + entry +
                        " from AgentX node " + toString());
            }
        }
        return removed;
    }

    @Override
    public OID find(MOScope range) {
        OID next = OID.max(range.getLowerBound(), region.getLowerBound());
        if (region.covers(next) && (getActiveRegistration() != null)) {
            return next;
        }
        return null;
    }

    /**
     * Gets the active (i.e. with the highest precedence) registration.
     * @return
     *    the registration entry with the highest precedence or {@code null} if there is no registration for any
     *    active session left.
     */
    public final synchronized AgentXRegEntry<?> getActiveRegistration() {
        AgentXRegEntry<?> activeReg = null;
        while (!registrations.isEmpty() && (activeReg == null)) {
            activeReg = registrations.first();
            if (activeReg.getSession().isClosed()) {
                registrations.remove(activeReg);
                LOGGER.warn("Removed registration from already closed session: " +
                        activeReg);
                activeReg = null;
            }
        }
        return activeReg;
    }

    @Override
    public void get(SubRequest<?> request) {
        get((SnmpRequest.SnmpSubRequest)request);
    }

    /**
     * Processes an SNMP GET {@link SubRequest} by sending a AgentX request to the appropriate sub-agent(s).
     * @param request
     *    a GET sub-request.
     */
    public void get(SnmpRequest.SnmpSubRequest request) {
        AgentXRegEntry<?> activeReg = getActiveRegistration();
        if (activeReg == null) {
            request.getVariableBinding().setVariable(Null.noSuchObject);
            request.getStatus().setPhaseComplete(true);
            return;
        }
        AgentXQueue queue = activeReg.getSession().getQueue();
        AgentXSearchRange searchRange =
                new AgentXSearchRange(request.getScope().getLowerBound(),
                        request.getScope().isLowerIncluded(),
                        request.getScope().getUpperBound(),
                        request.getScope().isUpperIncluded(),
                        request);
        queue.add(searchRange, activeReg, false);
        markAsProcessed(request);
    }

    @Override
    public MOScope getScope() {
        return region;
    }

    @Override
    public boolean next(SubRequest<?> request) {
        return next((SnmpRequest.SnmpSubRequest)request);
    }

    /**
     * Processes an SNMP NEXT {@link SubRequest} by sending a AgentX request to the appropriate sub-agent(s).
     * @param request
     *    a GETNEXT sub-request.
     * @return
     *    {@code true} if a AgentX request has been created.
     */
    public boolean next(SnmpRequest.SnmpSubRequest request) {
        return next(request, null);
    }

    @Override
    public boolean next(SubRequest<?> request, Function<OID, Boolean> filter) {
        return next((SnmpRequest.SnmpSubRequest)request, filter);
    }

    /**
     * Processes a GETNEXT/GETBULK sub-request against the AgentX registrations managed by this node.
     *
     * @param request
     *    the SNMP sub-request providing the search scope to process.
     * @param filter
     *    an optional filter to skip OIDs (may be {@code null} for no filtering).
     * @return
     *    {@code true} if the sub-request could be dispatched to a matching AgentX registration,
     *    {@code false} otherwise.
     */
    public boolean next(SnmpRequest.SnmpSubRequest request, Function<OID, Boolean> filter) {
        AgentXRegEntry<?> activeReg = getActiveRegistration();
        if (activeReg == null) {
            return false;
        }
        AgentXQueue queue = activeReg.getSession().getQueue();
        AgentXSearchRange searchRange =
                new AgentXSearchRange(request.getScope().getLowerBound(),
                        request.getScope().isLowerIncluded(),
                        region.getUpperBound(),
                        region.isUpperIncluded(),
                        request);
        OID upperRequestBound = request.getScope().getUpperBound();
        if ((upperRequestBound != null) &&
                (upperRequestBound.compareTo(region.getUpperBound()) < 0)) {
            searchRange.setUpperBound(upperRequestBound);
            searchRange.setUpperIncluded(request.getScope().isUpperIncluded());
        }
        if (searchRange.isEmpty()) {
            return false;
        }
        int nonRepeaters = request.getRequest().getNonRepeaters();
        if (queue.add(searchRange, activeReg, request.getIndex() >= nonRepeaters, filter)) {
            if (request.getRequest().getSource().getPDU().getType() == PDU.GETBULK) {
                // need to set also repetitions to processed
                for (SubRequestIterator<? extends SubRequest<?>> it = request.repetitions(); it.hasNext(); ) {
                    SubRequest<?> sreq = it.next();
                    sreq.getStatus().setProcessed(true);
                }
            }
        }
        markAsProcessed(request);
        return true;
    }

    @Override
    public void prepare(SubRequest<?> request) {
        prepare((SnmpRequest.SnmpSubRequest)request);
    }

    /**
     * Processes an SNMP SET {@link SubRequest} preparation by sending a AgentX request to the appropriate sub-agent(s).
     * @param request
     *    a SET sub-request.
     */
    public void prepare(SnmpRequest.SnmpSubRequest request) {
        addAgentXSet2Queue(request);
        markAsProcessed(request);
    }

    @Override
    public void undo(SubRequest<?> request) {
        undo((SnmpRequest.SnmpSubRequest)request);
    }

    /**
     * Processes an SNMP SET {@link SubRequest} undo by sending a AgentX request to the appropriate sub-agent(s).
     * @param request
     *    a SET sub-request.
     */
    public void undo(SnmpRequest.SnmpSubRequest request) {
        addAgentXSet2Queue(request);
        markAsProcessed(request);
    }

    @Override
    public void cleanup(SubRequest<?> request) {
        cleanup((SnmpRequest.SnmpSubRequest)request);
    }

    /**
     * Processes an SNMP SET {@link SubRequest} cleanup by sending a AgentX request to the appropriate sub-agent(s).
     * @param request
     *    a SET sub-request.
     */
    public void cleanup(SnmpRequest.SnmpSubRequest request) {
        addAgentXSet2Queue(request);
        markAsProcessed(request);
    }

    @Override
    public void commit(SubRequest<?> request) {
        commit((SnmpRequest.SnmpSubRequest)request);
    }

    /**
     * Processes an SNMP SET {@link SubRequest} commit by sending a AgentX request to the appropriate sub-agent(s).
     * @param request
     *    a SET sub-request.
     */
    public void commit(SnmpRequest.SnmpSubRequest request) {
        addAgentXSet2Queue(request);
        markAsProcessed(request);
    }

    private static void markAsProcessed(SubRequest<?> request) {
        request.getStatus().setProcessed(true);
    }

    private void addAgentXSet2Queue(SnmpRequest.SnmpSubRequest request) {
        AgentXRegEntry<?> activeReg = getActiveRegistration();
        if (activeReg != null) {
            AgentXMasterSession<?> session = activeReg.getSession();
            if (session != null) {
                AgentXQueue queue = session.getQueue();
                if (queue != null) {
                    queue.add(request.getVariableBinding(), request,
                            activeReg);
                } else if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("No queue for session " + session);
                }
            } else if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("No session for registration entry " + activeReg);
            }
        } else if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("No active registration left for " + request);
        }
    }

    @Override
    public String toString() {
        return getClass().getName() + "[region=" + region +
                ",registrations=" + registrations + "]";
    }

}
