/*_############################################################################
  _## 
  _##  SNMP4J-AgentX - IndexRegistryEntry.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.index;

import org.snmp4j.agent.agentx.AgentXProtocol;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable;
import org.snmp4j.smi.VariableBinding;

import java.util.Comparator;
import java.util.Iterator;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * A {@link IndexRegistryEntry} represents an index value registry for a specific table index type, represented
 * by a {@link VariableBinding}.
 */
public class IndexRegistryEntry implements Comparable<IndexRegistryEntry> {

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

    private final OctetString context;
    private final VariableBinding indexType;

    /**
     * Registry for active index values.
     */
    protected SortedMap<IndexEntry, IndexEntry> indexValues = new TreeMap<>(new IndexComparator());
    /**
     * Registry for used (old) index values.
     */
    protected SortedMap<IndexEntry, IndexEntry> usedValues = new TreeMap<>(new IndexComparator());

    /**
     * Creates a new registry based on an index type.
     * @param context
     *    the SNMPv3 context.
     * @param indexType
     *    the index type.
     */
    public IndexRegistryEntry(OctetString context, VariableBinding indexType) {
        this.context = (context == null) ? new OctetString() : context;
        this.indexType = indexType;
    }

    /**
     * Creates a new {@link IndexEntry} for the given session and index value. Subclasses may override
     * this factory method to provide a specialized index entry implementation.
     * @param sessionID
     *    the ID of the session allocating the index.
     * @param indexValue
     *    the index value of the index instance.
     * @return
     *    a new {@link IndexEntry}.
     */
    protected IndexEntry newIndexEntry(int sessionID, Variable indexValue) {
        return new IndexEntry(sessionID, indexValue);
    }

    /**
     * Called when an already allocated index instance is allocated again. The default implementation
     * does nothing; subclasses may override this method to react on duplicate allocations.
     * @param entry
     *    the index entry that has been allocated more than once.
     */
    protected void duplicateAllocation(IndexEntry entry) {

    }

    /**
     * Allocate an INDEX instance this index type.
     * @param sessionID
     *    the session allocating the index.
     * @param indexValue
     *    the index value of the requested index instance.
     * @param testOnly
     *    if {@code true}, only test if allocation could be done, otherwise directly allocate the index.
     * @return
     *    {@link AgentXProtocol#AGENTX_SUCCESS} if the allocation was successful,
     *    {@link AgentXProtocol#AGENTX_INDEX_WRONG_TYPE}, {@link AgentXProtocol#AGENTX_INDEX_NONE_AVAILABLE},
     *    and {@link AgentXProtocol#AGENTX_INDEX_ALREADY_ALLOCATED} on
     *    error.
     */
    public synchronized int allocate(int sessionID, Variable indexValue, boolean testOnly) {
        IndexEntry newEntry = newIndexEntry(sessionID, indexValue);
        if (indexValue.getSyntax() != indexType.getSyntax()) {
            return AgentXProtocol.AGENTX_INDEX_WRONG_TYPE;
        } else {
            IndexEntry oldEntry = indexValues.get(newEntry);
            if (oldEntry != null) {
                if (!testOnly) {
                    duplicateAllocation(oldEntry);
                }
                return AgentXProtocol.AGENTX_INDEX_ALREADY_ALLOCATED;
            } else {
                if (!testOnly) {
                    indexValues.put(newEntry, newEntry);
                }
                return AgentXProtocol.AGENTX_SUCCESS;
            }
        }
    }

    /**
     * Release a previously allocated INDEX instance.
     * @param sessionID
     *    the session allocating the index.
     * @param indexValue
     *    the variable with the index value to release.
     * @param testOnly
     *    if {@code true}, only test if release could be done, otherwise directly release the index.
     * @return
     *    {@link AgentXProtocol#AGENTX_SUCCESS} if the allocation was successful,
     *    {@link AgentXProtocol#AGENTX_INDEX_NOT_ALLOCATED}, {@link AgentXProtocol#AGENTX_INDEX_ALREADY_ALLOCATED} on
     *    error.
     */
    public synchronized int release(int sessionID, Variable indexValue, boolean testOnly) {
        IndexEntry removeKey = newIndexEntry(sessionID, indexValue);
        IndexEntry contained = indexValues.get(removeKey);
        if ((contained != null) && (contained.getSessionID() == sessionID)) {
            if (!testOnly) {
                if (!removeEntry(contained)) {
                    return AgentXProtocol.AGENTX_INDEX_ALREADY_ALLOCATED;
                }
            }
            return AgentXProtocol.AGENTX_SUCCESS;
        }
        return AgentXProtocol.AGENTX_INDEX_NOT_ALLOCATED;
    }

    /**
     * Remove an index value from the active index values.
     * @param entryKey
     *    the index value entry.
     * @return
     *    {@code true} if the removal was successful.
     */
    protected boolean removeEntry(IndexEntry entryKey) {
        IndexEntry r = indexValues.remove(entryKey);
        if (r == null) {
            return false;
        }
        addUsed(r);
        return true;
    }

    /**
     * Add a used index value to the used registry.
     * @param entry
     *    the index value entry.
     */
    protected void addUsed(IndexEntry entry) {
        usedValues.put(entry, entry);
    }

    /**
     * Release all index values of the specified session ID.
     * @param sessionID
     *   the session to release index values for.
     */
    public synchronized void release(int sessionID) {
        for (Iterator<IndexEntry> it = indexValues.values().iterator(); it.hasNext(); ) {
            IndexEntry entry = it.next();
            if (entry.getSessionID() == sessionID) {
                it.remove();
                addUsed(entry);
            }
        }
    }

    @Override
    public int compareTo(IndexRegistryEntry other) {
        int c = context.compareTo(other.context);
        if (c == 0) {
            c = indexType.getOid().compareTo(other.indexType.getOid());
        }
        return c;
    }

    @Override
    public int hashCode() {
        return indexType.getOid().hashCode() + context.hashCode();
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof IndexRegistryEntry) {
            return indexType.getOid().equals(
                    ((IndexRegistryEntry) o).indexType.getOid());
        }
        return false;
    }

    /**
     * Gets the index type identifier.
     * @return
     *    an index type identifier.
     */
    public VariableBinding getIndexType() {
        return indexType;
    }

    /**
     * Gets the context.
     * @return
     *    a SNMPv3 context.
     */
    public OctetString getContext() {
        return context;
    }

    /**
     * Creates a new (never used before) index value for this index type.
     * @param sessionID
     *    the session ID requesting the index.
     * @param testOnly
     *    if {@code true}, only test if allocation could be done, otherwise directly allocate the index.
     * @return
     *    the allocated index value.
     */
    public synchronized Variable newIndex(int sessionID, boolean testOnly) {
        try {
            IndexEntry last = null;
            if (!usedValues.isEmpty()) {
                last = usedValues.lastKey();
            }
            if (!indexValues.isEmpty()) {
                IndexEntry lastAllocated = indexValues.lastKey();
                if (last == null) {
                    last = lastAllocated;
                } else if (lastAllocated.compareTo(last) > 0) {
                    last = lastAllocated;
                }
            } else if (last == null) {
                return anyIndex(sessionID, testOnly);
            }
            OID nextIndex = last.getIndexValue().toSubIndex(true);
            nextIndex = nextIndex.nextPeer();
            Variable nextIndexValue = (Variable) last.getIndexValue().clone();
            nextIndexValue.fromSubIndex(nextIndex, true);
            int status = allocate(sessionID, nextIndexValue, testOnly);
            if (status != AgentXProtocol.AGENTX_SUCCESS) {
                return null;
            }
            return nextIndexValue;
        } catch (Exception ex) {
            LOGGER.error("Exception occurred while creating/allocating new index:" +
                    ex.getMessage(), ex);
            return null;
        }
    }

    /**
     * Creates any (used before or new) index value for this index type.
     * @param sessionID
     *    the session ID requesting the index.
     * @param testOnly
     *    if {@code true}, only test if allocation could be done, otherwise directly allocate the index.
     * @return
     *    the allocated index value.
     */
    public synchronized Variable anyIndex(int sessionID, boolean testOnly) {
        try {
            OID nextIndex;
            if (usedValues.isEmpty()) {
                if (indexValues.isEmpty()) {
                    nextIndex = indexType.getVariable().toSubIndex(true);
                } else {
                    nextIndex = (indexValues.lastKey()).
                            getIndexValue().toSubIndex(true);
                }
            } else {
                IndexEntry last = usedValues.firstKey();
                nextIndex = last.getIndexValue().toSubIndex(true);
            }
            nextIndex = nextIndex.nextPeer();
            Variable nextIndexValue = (Variable) indexType.getVariable().clone();
            nextIndexValue.fromSubIndex(nextIndex, true);
            int status = allocate(sessionID, nextIndexValue, testOnly);
            if (status != AgentXProtocol.AGENTX_SUCCESS) {
                return null;
            }
            return nextIndexValue;
        } catch (Exception ex) {
            LOGGER.error("Exception occurred while creating/allocating" +
                    " any new index:" + ex.getMessage(), ex);
            return null;
        }
    }

    private static class IndexComparator implements Comparator<IndexEntry> {

        public int compare(IndexEntry o1, IndexEntry o2) {
            Variable c1, c2;
            c1 = o1.getIndexValue();
            c2 = o2.getIndexValue();
            return c1.compareTo(c2);
        }

    }
}
