/*_############################################################################
  _##
  _##  SNMP4J-Agent 3 - MIBTree.java
  _##
  _##  Copyright (C) 2005-2026  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.io.prop;

import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * An in-memory representation of the managed objects configured by a
 * {@link org.snmp4j.agent.io.MOInput} source (for example a {@link PropertyMOInput} properties
 * file), organised as an OID prefix tree (trie) per SNMP context.
 * <p>
 * Each configured managed object becomes a terminal {@link Node} located at its OID. A node is
 * either a <em>scalar</em> (carrying a single {@link Variable} value) or a <em>table</em>
 * (carrying a {@link Table} with one {@link Row} per configured row). Intermediate nodes model the
 * OID hierarchy and carry no value.
 * <p>
 * Values that could not be resolved during parsing are preserved as
 * {@link UnresolvedReference} instances so that their configured reference remains visible.
 *
 * @author SNMP4J.org
 * @version 3.9.2
 * @since 3.9.2
 */
public class MIBTree {

    /** Context to root node. The {@code null} key denotes the context-independent (default) data. */
    private final Map<OctetString, Node> roots = new LinkedHashMap<>();

    /**
     * Returns the contexts present in this tree, in insertion order. The set may contain a
     * {@code null} element denoting the context-independent (default) data.
     *
     * @return the set of context names (possibly including {@code null}).
     */
    public Set<OctetString> getContexts() {
        return roots.keySet();
    }

    /**
     * Returns the root node for the given context, or {@code null} if that context is not present.
     *
     * @param context
     *    the context name, or {@code null} for the context-independent data.
     * @return the context root {@link Node}, or {@code null}.
     */
    public Node getRoot(OctetString context) {
        return roots.get(context);
    }

    /**
     * Adds a scalar managed object at the given OID within the given context.
     *
     * @param context
     *    the context name, or {@code null} for the context-independent data.
     * @param oid
     *    the object instance OID.
     * @param value
     *    the configured value ({@code null} if the configuration left the value empty).
     */
    public void addScalar(OctetString context, OID oid, Variable value) {
        Node node = pathTo(context, oid);
        node.managedObject = true;
        node.scalar = true;
        node.scalarValue = value;
    }

    /**
     * Adds a conceptual table (row entry) managed object at the given OID within the given context.
     *
     * @param context
     *    the context name, or {@code null} for the context-independent data.
     * @param oid
     *    the table entry OID.
     * @param columnCount
     *    the number of columns configured per row.
     * @param rows
     *    the configured rows.
     */
    public void addTable(OctetString context, OID oid, int columnCount, List<Row> rows) {
        Node node = pathTo(context, oid);
        node.managedObject = true;
        node.table = new Table(columnCount, rows);
    }

    private Node pathTo(OctetString context, OID oid) {
        Node current = roots.computeIfAbsent(context, k -> new Node(-1, null));
        int[] arcs = oid.getValue();
        for (int i = 0; i < arcs.length; i++) {
            OID prefix = new OID(Arrays.copyOfRange(arcs, 0, i + 1));
            final int subId = arcs[i];
            current = current.children.computeIfAbsent(subId, k -> new Node(subId, prefix));
        }
        return current;
    }

    /**
     * A node in the OID tree. Non-terminal nodes model the OID hierarchy; terminal nodes
     * ({@link #isManagedObject()}) carry either a scalar value or a table.
     */
    public static final class Node {

        private final int subId;
        private final OID oid;
        private final TreeMap<Integer, Node> children = new TreeMap<>();

        private boolean managedObject;
        private boolean scalar;
        private Variable scalarValue;
        private Table table;

        Node(int subId, OID oid) {
            this.subId = subId;
            this.oid = oid;
        }

        /**
         * @return the last sub-identifier of this node relative to its parent, or {@code -1} for the root.
         */
        public int getSubID() {
            return subId;
        }

        /**
         * @return the full OID up to and including this node, or {@code null} for the root.
         */
        public OID getOID() {
            return oid;
        }

        /**
         * @return the child nodes, ordered by sub-identifier.
         */
        public Collection<Node> getChildren() {
            return children.values();
        }

        /**
         * @return {@code true} if this node has at least one child.
         */
        public boolean hasChildren() {
            return !children.isEmpty();
        }

        /**
         * @return {@code true} if a managed object is configured at this node.
         */
        public boolean isManagedObject() {
            return managedObject;
        }

        /**
         * @return {@code true} if this node is a scalar managed object.
         */
        public boolean isScalar() {
            return scalar;
        }

        /**
         * @return {@code true} if this node is a table (row entry) managed object.
         */
        public boolean isTable() {
            return table != null;
        }

        /**
         * @return the scalar value (may be {@code null}); only meaningful when {@link #isScalar()}.
         */
        public Variable getScalarValue() {
            return scalarValue;
        }

        /**
         * @return the table (may be {@code null}); only present when {@link #isTable()}.
         */
        public Table getTable() {
            return table;
        }
    }

    /**
     * The configured rows of a conceptual table (SMI table row entry).
     */
    public static final class Table {

        private final int columnCount;
        private final List<Row> rows;

        Table(int columnCount, List<Row> rows) {
            this.columnCount = columnCount;
            this.rows = (rows == null) ? new ArrayList<>() : rows;
        }

        /**
         * @return the number of columns configured per row.
         */
        public int getColumnCount() {
            return columnCount;
        }

        /**
         * @return the configured rows (never {@code null}).
         */
        public List<Row> getRows() {
            return rows;
        }
    }

    /**
     * A single configured table row: an index OID and the per-column values.
     */
    public static final class Row {

        private final OID index;
        private final Variable[] values;

        /**
         * Creates a new row.
         *
         * @param index
         *    the row index OID (relative to the table entry).
         * @param values
         *    the per-column values; individual elements may be {@code null} if left empty in the
         *    configuration.
         */
        public Row(OID index, Variable[] values) {
            this.index = index;
            this.values = values;
        }

        /**
         * @return the row index OID.
         */
        public OID getIndex() {
            return index;
        }

        /**
         * @return the per-column values (elements may be {@code null}).
         */
        public Variable[] getValues() {
            return values;
        }
    }
}
