package de.renew.formalism.function;

import java.io.IOException;
import java.io.Serial;

import de.renew.expression.Function;
import de.renew.unify.Calculator;
import de.renew.unify.Impossible;
import de.renew.util.ReflectionSerializer;
import de.renew.util.Types;
import de.renew.util.Value;

/**
 * A function that checks if an object is an instance of a given class.
 */
public class InstanceofFunction implements Function {

    /**
     * This field is not really transient, but as <code>java.lang.Class
     * </code>is not always serializable, we have to store it by
     * ourselves.
     **/
    private transient Class<?> _clazz;

    /**
     * Specifies if null is considered a valid instance.
     */
    private final boolean _allowsNull;

    /**
     * Specifies if Calculator objects are allowed and checked for type compatibility.
     */
    private final boolean _allowsCalculator;

    /**
     * Creates an instance of this class for a given class.
     *
     * @param clazz the class
     * @param allowsNull whether to allow {@code null} objects
     * @param allowsCalculator whether to allow calculator objects
     */
    public InstanceofFunction(Class<?> clazz, boolean allowsNull, boolean allowsCalculator) {
        if (clazz == null) {
            throw new NullPointerException();
        }
        this._clazz = clazz;
        this._allowsNull = allowsNull;
        this._allowsCalculator = allowsCalculator;
    }

    @Override
    public Object function(Object param) throws Impossible {
        boolean result;
        if (param instanceof Calculator) {
            if (_allowsCalculator) {
                Class<?> paramType = ((Calculator) param).getType();
                if (_clazz.isPrimitive()) {
                    result = (_clazz == paramType);
                } else {
                    result =
                        (_clazz == paramType) || Types.allowsReferenceWidening(paramType, _clazz);
                }
            } else {
                result = false;
            }
        } else if (param instanceof Value) {
            if (_clazz.isPrimitive()) {
                result = Types.objectify(_clazz).isInstance(((Value) param).value);
            } else {
                result = false;
            }
        } else if (param == null) {
            result = _allowsNull;
        } else {
            result = _clazz.isInstance(param);
        }
        return new Value(result);
    }

    /**
     * Serialization method, behaves like default writeObject
     * method. Stores the not-really-transient clazz field.
     *
     * @param out the output stream to write to
     * @throws IOException if writing the object fails
     **/
    @Serial
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        ReflectionSerializer.writeClass(out, _clazz);
    }

    /**
     * Deserialization method, behaves like default readObject
     * method. Restores the not-really-transient clazz field.
     *
     * @param in the input stream to read from
     * @throws IOException if reading the object fails
     * @throws ClassNotFoundException if a required class is not found during reading
     **/
    @Serial
    private void readObject(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        in.defaultReadObject();
        _clazz = ReflectionSerializer.readClass(in);
    }
}