package de.renew.unify;

import java.util.HashSet;
import java.util.Set;

/**
 * This class offers static methods to unify two objects and check their status.
 * It also provides methods to create reference arrays and
 * clean up reference arrays.
 */
public final class Unify {
    /**
     * The copier for this class that can be used to copy bound values.
     */
    private static final Copier TEMP_COPIER = new Copier();

    /**
     * As this is a static class, no instances should be created.
     */
    private Unify() {}

    /**
     * This method is used to clean up a reference array.
     * It takes an original array of objects and creates
     * an array of {@link Reference} objects from it.
     *
     * @param orgArray the original array of objects
     * @param referer the referer for the references
     * @param recorder the state recorder used to track changes
     * @return an array of {@link Reference} objects created from the original array
     */
    static Reference[] cleanupReferenceArray(
        Object[] orgArray, Referer referer, IStateRecorder recorder)
    {
        Reference[] references = new Reference[orgArray.length];
        for (int i = 0; i < orgArray.length; i++) {
            references[i] = new Reference(orgArray[i], referer, recorder);
        }
        return references;
    }

    /**
     * This method is used to create a reference array with a single element.
     * It takes an initial value and creates a {@link Reference} object from it.
     *
     * @param initValue the initial value for the reference
     * @param referer the referer for the reference
     * @param recorder the state recorder used to track changes
     * @return an array of the {@code Reference} object created from the initial value
     */
    static Reference[] makeReferenceArray(
        Object initValue, Referer referer, IStateRecorder recorder)
    {
        Reference[] references = new Reference[1];
        references[0] = new Reference(initValue, referer, recorder);
        return references;
    }

    /**
     * This method is used to create a reference array with a specified arity.
     * It creates an array of {@link Reference} objects referring to {@link Unknown} objects.
     *
     * @param arity the number of elements in the array
     * @param referer the referer for the references
     * @return an array of {@code Reference} objects with unknown values
     */
    static Reference[] makeUnknownReferenceArray(int arity, Referer referer) {
        Reference[] references = new Reference[arity];
        for (int i = 0; i < arity; i++) {
            references[i] = new Reference(new Unknown(), referer, null);
        }
        return references;
    }

    /**
     * This method checks if an object is complete.
     * Together with {@link #isBound(Object)}, it probes
     * the status of a unifiable object.
     * <p>
     * An object is complete when no further unification
     * within its scope can change its state, i.e. it is
     * fully calculatable.
     * An object is bound if it is fully calculated.
     * </p>
     * An object may be complete and not bound, if it depends on
     * calculations registered with the checker.
     * A bound object is always complete.
     *
     * @param o the object to check
     * @return {@code true} if the object is complete, {@code false} otherwise
     */
    static boolean isComplete(Object o) {
        if (o instanceof Unifiable unifiable) {
            // Let's ask the object.
            return unifiable.isComplete();
        }
        // Ordinary objects are always complete.
        return true;
    }

    /**
     * This method checks if an object is bound.
     * Together with {@link #isComplete(Object)}, it probes
     * the status of a unifiable object.
     * <p>
     * An object is bound if it is fully calculated.
     * </p>
     * An object may be complete and not bound, if it depends on
     * calculations registered with the checker.
     * A bound object is always complete.
     *
     * @param o the object to check
     * @return {@code true} if the object is bound, {@code false} otherwise
     */
    public static boolean isBound(Object o) {
        if (o instanceof Unifiable unifiable) {
            // Let's ask the object.
            return unifiable.isBound();
        }
        // Ordinary objects are always bound.
        return true;
    }

    /**
     * This method is used to unify two objects silently.
     * <p>
     * It is used internally to perform the unification
     * without notifying listeners immediately.
     * It assumes that all listeners will be notified later
     * on.
     * </p>
     *
     * @param left the first object to unify
     * @param right the second object to unify
     * @param recorder the state recorder used to track changes
     * @param listeners the set of listeners to be notified later on
     * @throws Impossible if unification is not possible
     */
    static void unifySilently(
        Object left, Object right, IStateRecorder recorder, Set<Notifiable> listeners)
        throws Impossible
    {
        // Remove variables, because they are only wrappers
        // for the real values.
        if (left instanceof Variable leftVariable) {
            left = leftVariable.getValue();
        }
        if (right instanceof Variable rightVariable) {
            right = rightVariable.getValue();
        }

        // Now do the unification.
        if (left == right) {
            return;
        }

        if (left instanceof SilentlyUnifiable unifiable) {
            unifiable.unifySilently(right, recorder, listeners);
        } else if (left instanceof Unknown unknown) {
            unknown.unifySilently(right, recorder, listeners);
        } else if (right instanceof Unknown unknown) {
            unknown.unifySilently(left, recorder, listeners);
        } else if (left instanceof Tuple leftTuple) {
            if (right instanceof Tuple rightTuple) {
                leftTuple.unifySilently(rightTuple, recorder, listeners);
            } else {
                throw new Impossible();
            }
        } else if (left instanceof List leftList) {
            if (right instanceof List rightList) {
                leftList.unifySilently(rightList, recorder, listeners);
            } else {
                throw new Impossible();
            }
        } else if (left == null || right == null || left instanceof Calculator
            || right instanceof Calculator) {
            throw new Impossible(); // We already checked for identity.
        } else {
            if (!left.equals(right)) {
                throw new Impossible();
            }
        }
    }

    /**
     * This method is used to unify two objects and notify listeners.
     * <p>
     * It performs the unification and notifies all listeners
     * that are registered for this unification immediately.
     * </p>
     *
     * @param left the first object to unify
     * @param right the second object to unify
     * @param recorder the state recorder used to track changes
     * @throws Impossible if unification is not possible
     */
    static public void unify(Object left, Object right, IStateRecorder recorder) throws Impossible {
        // Prepare a set of listeners that will hold the
        // listeners to invoke.
        Set<Notifiable> listeners = new HashSet<>();

        // Make the unification.
        unifySilently(left, right, recorder, listeners);

        // Notify all listeners.
        for (Notifiable listener : listeners) {
            listener.boundNotify(recorder);
        }
    }

    /**
     * This method is used to copy a bound value.
     * It throws an exception if the value is not bound.
     *
     * @param val the value to copy
     * @return the copied value
     * @throws RuntimeException if value is not bound
     */
    static public Object copyBoundValue(Object val) {
        if (!isBound(val)) {
            throw new RuntimeException("To copy unbound values, use a copier.");
        }
        return TEMP_COPIER.copy(val);
    }
}