package de.renew.unify;

import java.util.Set;

import org.apache.log4j.Logger;

import de.renew.util.Types;
import de.renew.util.Value;

/**
 * This class is used to constrain the type of value.
 * It checks if the value is of the expected type and throws an
 * {@link Impossible} if there is a type mismatch.
 */
public class TypeConstrainer implements Referer {
    /**
     * The logger for this class.
     */
    public static final Logger LOGGER = Logger.getLogger(TypeConstrainer.class);

    /**
     * The type of the value.
     */
    private final Class<?> _type;

    /**
     * The reference to the value.
     */
    private final Reference _reference;

    /**
     * The constructor for the TypeConstrainer class.
     * It initializes the type and reference fields.
     *
     * @param type the type of the value
     * @param value the value to be constrained
     * @param recorder the state recorder
     */
    private TypeConstrainer(Class<?> type, Object value, IStateRecorder recorder) {
        _type = type;
        _reference = new Reference(value, this, recorder);
    }

    /**
     * This method is used to constrain the type of value.
     * It checks if the value is of the expected type and throws an
     * {@code Impossible} if there is a type mismatch.
     *
     * @param type the type of the value
     * @param value the value to be constrained
     * @param recorder the state recorder
     * @throws Impossible if there is a type mismatch
     */
    public static void constrain(Class<?> type, Object value, IStateRecorder recorder)
        throws Impossible
    {
        if (type == Types.UNTYPED) {
            return;
        }

        if (Unify.isComplete(value)) {
            // Check value now, no need to create a type constrainer.
            check(type, value);
        } else {
            new TypeConstrainer(type, value, recorder);
        }
    }

    /**
     * This method checks if the value is of the expected type.
     * It throws an Impossible exception if there is a type mismatch.
     *
     * @param type the type of the value
     * @param value the value to be checked
     * @throws Impossible if there is a type mismatch
     */
    private static void check(Class<?> type, Object value) throws Impossible {
        if (value instanceof Calculator calculator) {
            Class<?> valueType = calculator.getType();

            if (type != valueType
                && (type.isPrimitive() || !Types.allowsReferenceWidening(valueType, type))) {
                try {
                    String message =
                        "Type mismatch: Primitive " + type + ", calculation result: " + valueType;
                    LOGGER.debug(message);
                    throw new Impossible(message);
                } catch (RuntimeException e) {
                    String message = "Type mismatch (diagnostics not available).";
                    LOGGER.debug(message, e);
                    throw new Impossible(message, e);
                }
            }
        } else if (value instanceof Value) {
            if (!type.isPrimitive() || !Types.objectify(type).isInstance(((Value) value).value)) {
                try {
                    String message = "Type mismatch: Class " + type + ", primitive value: " + value;
                    LOGGER.debug(message);
                    throw new Impossible(message);
                } catch (RuntimeException e) {
                    String message = "Type mismatch (diagnostics not available).";
                    LOGGER.debug(message, e);
                    throw new Impossible(message, e);
                }
            }
        } else if (value == null) {
            if (type.isPrimitive()) {
                try {
                    String message = "Type mismatch: Primitive " + type + ", null reference";
                    LOGGER.debug(message);
                    throw new Impossible(message);
                } catch (RuntimeException e) {
                    String message = "Type mismatch (diagnostics not available).";
                    LOGGER.debug(message, e);
                    throw new Impossible(message, e);
                }
            }
        } else if (!type.isInstance(value)) {
            try {
                String message = "Type mismatch: Class " + type + ", object: " + value;
                LOGGER.debug(message);
                throw new Impossible(message);
            } catch (RuntimeException e) {
                String message = "Type mismatch (diagnostics not available).";
                LOGGER.debug(message, e);
                throw new Impossible(message, e);
            }
        }
    }

    @Override
    public void possiblyCompleted(Set<Notifiable> listeners, IStateRecorder recorder)
        throws Impossible
    {
        // I own only one reference, therefore that reference must have become
        // completed.
        check(_type, _reference.getValue());
    }
}