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

import java.io.*;
import java.util.Collection;
import java.util.Collections;

import org.snmp4j.log.*;
import org.snmp4j.smi.*;

/**
 * The {@code DefaultMOInput} class implements the {@link MOInput} interface to provide functionality
 * for reading managed object states, variables, and other elements from a persistent source
 * using an {@link ObjectInputStream}. It uses strict deserialization filters to ensure
 * that only authorized classes are read, improving the security and consistency of input handling.
 *
 * @author Frank Fock
 * @version 3.12.0
 */
public class DefaultMOInput implements MOInput {

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

    private int importMode;
    private final ObjectInputStream ois;

    /**
     * Creates a {@code DefaultMOInput} for the supplied {@link ObjectInputStream} and installs a strict
     * {@link AgentStateDeserializationFilter} on it that allows only the agent state classes written by
     * {@link DefaultMOOutput} to be deserialized.
     *
     * @param ois
     *    the {@link ObjectInputStream} to read the persistent agent state from.
     */
    public DefaultMOInput(ObjectInputStream ois) {
        this(ois, Collections.emptyList());
    }

    /**
     * Creates a {@code DefaultMOInput} for the supplied {@link ObjectInputStream} and installs a strict
     * {@link AgentStateDeserializationFilter} on it that allows the agent state classes written by
     * {@link DefaultMOOutput} plus the supplied additional classes to be deserialized. Use this
     * constructor to restore persistent state that contains custom (e.g. user defined) {@link Variable}
     * or {@link Context} subclasses.
     *
     * @param ois
     *    the {@link ObjectInputStream} to read the persistent agent state from.
     * @param additionalAllowedClasses
     *    additional classes (matched by exact identity) to allow for deserialization in addition to the
     *    default agent state classes, or {@code null}/empty for the default set only.
     * @since 3.9.0
     */
    public DefaultMOInput(ObjectInputStream ois, Collection<Class<?>> additionalAllowedClasses) {
        this.ois = ois;
        applyDeserializationFilter(ois, new AgentStateDeserializationFilter(additionalAllowedClasses));
    }

    /**
     * Installs the supplied {@link ObjectInputFilter} on the {@link ObjectInputStream}. If the stream
     * already has a filter, the two filters are combined with logical AND semantics so that an object is
     * only allowed if neither filter rejects it (the existing filter can only tighten, not loosen, the
     * strict agent state filter).
     *
     * @param ois
     *    the stream to protect.
     * @param filter
     *    the strict agent state filter to apply.
     */
    private static void applyDeserializationFilter(ObjectInputStream ois, ObjectInputFilter filter) {
        try {
            ObjectInputFilter existing = ois.getObjectInputFilter();
            ObjectInputFilter effective = (existing == null) ? filter : andCombine(existing, filter);
            ois.setObjectInputFilter(effective);
        }
        catch (SecurityException | IllegalStateException ex) {
            logger.warn("Could not install deserialization filter on ObjectInputStream: " + ex.getMessage());
        }
    }

    /**
     * Combines two {@link ObjectInputFilter}s with logical AND semantics: the result rejects an object
     * if either filter rejects it, allows it if either filter allows it (and the other does not reject),
     * and is undecided otherwise.
     */
    private static ObjectInputFilter andCombine(ObjectInputFilter first, ObjectInputFilter second) {
        return filterInfo -> {
            ObjectInputFilter.Status s1 = first.checkInput(filterInfo);
            ObjectInputFilter.Status s2 = second.checkInput(filterInfo);
            if ((s1 == ObjectInputFilter.Status.REJECTED) || (s2 == ObjectInputFilter.Status.REJECTED)) {
                return ObjectInputFilter.Status.REJECTED;
            }
            if ((s1 == ObjectInputFilter.Status.ALLOWED) || (s2 == ObjectInputFilter.Status.ALLOWED)) {
                return ObjectInputFilter.Status.ALLOWED;
            }
            return ObjectInputFilter.Status.UNDECIDED;
        };
    }

    public int getImportMode() {
        return importMode;
    }

    public Context readContext() throws IOException {
        try {
            return (Context) ois.readObject();
        }
        catch (ClassNotFoundException ex) {
            logger.error("Failed to load Context: " + ex.getMessage());
            return null;
        }
    }

    public IndexedVariables readIndexedVariables() throws IOException {
        try {
            return (IndexedVariables) ois.readObject();
        }
        catch (ClassNotFoundException ex) {
            logger.error("Failed to load IndexedVariables: " + ex.getMessage());
            return null;
        }
    }

    public MOInfo readManagedObject() throws IOException {
        try {
            return (MOInfo) ois.readObject();
        }
        catch (ClassNotFoundException ex) {
            logger.error("Failed to load MOInfo: " + ex.getMessage());
            return null;
        }
    }

    public Sequence readSequence() throws IOException {
        try {
            return (Sequence) ois.readObject();
        }
        catch (ClassNotFoundException ex) {
            logger.error("Failed to load Sequence: " + ex.getMessage());
            return null;
        }
    }

    public Variable readVariable() throws IOException {
        try {
            return (Variable) ois.readObject();
        }
        catch (ClassNotFoundException ex) {
            logger.error("Failed to load Variable: " + ex.getMessage());
            return null;
        }
    }

    public void skipContext(Context context) throws IOException {
        Object next = null;
        try {
            next = ois.readObject();
        }
        catch (ClassNotFoundException ex) {
            logger.error("Failed to skip Context: " + ex.getMessage());
        }
        while ((!(next instanceof Context)) || (!((Context)next).equals(context))) {
            try {
                next = ois.readObject();
            }
            catch (ClassNotFoundException ex1) {
                logger.error("Failed to skip Context: " + ex1.getMessage());
            }
        }
    }

    public void skipManagedObject(MOInfo moInfo) throws IOException {
        Object next = null;
        try {
            next = ois.readObject();
        }
        catch (ClassNotFoundException ex) {
            logger.error("Failed to skip Context: " + ex.getMessage());
        }
        while ((!(next instanceof MOInfo)) || (!((MOInfo)next).equals(moInfo))) {
            try {
                next = ois.readObject();
            }
            catch (ClassNotFoundException ex1) {
                logger.error("Failed to skip Context: " + ex1.getMessage());
            }
        }
    }

    public void setOverwriteMode(int importMode) {
        this.importMode = importMode;
    }

    public void close() throws IOException {
        ois.close();
    }

}
