package de.renew.unify;

import java.io.Serializable;
import java.util.Set;

/**
 * Represents a reference to an object that tracks its binding and completeness status.
 * <p>
 * Acts as a wrapper around objects and {@link Referable}s and is responsible for:
 * <ul>
 *   <li>Tracking whether the value is bound or complete.</li>
 *   <li>Maintaining a backlink to the {@link Referer} that uses this reference.</li>
 *   <li>Updates its {@link Referer} about a possible completion.</li>
 * </ul>
 */
public class Reference implements Serializable {
    // Tried to delay the field value
    // to cut down recursion depth on serialization.
    // But that did cause a problem with the
    // TupleIndexes of PlaceInstances: at the time
    // a tuple was included into the index, its
    // components were still missing, so the index
    // was incomplete.

    /** The object referenced by this Reference. */
    private Object _value;
    /**
     * {@code true} if reference's value is bound,
     * i.e. it is fully calculated.
     * */
    private boolean _bound;
    /**
     * {@code true} if reference's value is complete,
     * i.e. the state will not be changed by further unification,
     * which makes it fully calculable.
     */
    private boolean _complete;
    /** The object that uses this reference. */
    private final Referer _referer;

    /**
     * Creates a new {@code Reference} with the given value and {@code Referer},
     * registering itself as a backlink, if the given object is a {@link Referable}.
     *
     * @param value the value being referenced (may be a {@link Referable} or {@link Variable})
     * @param referer the object that uses this reference and should be notified of changes
     * @param recorder the recorder of the states
     */
    public Reference(Object value, Referer referer, IStateRecorder recorder) {
        // Get rid of variables.
        if (value instanceof Variable) {
            value = ((Variable) value).getValue();
        }


        // Initialize.
        _value = value;
        _complete = Unify.isComplete(value);
        _bound = Unify.isBound(value);

        _referer = referer;


        // If I reference a referable that is not complete,
        // I have to register as a backlink. However, if the referable
        // is complete, I need not expect a notification, so why bother?
        // Adding a backlink would only make garbage collection
        // more difficult.
        //
        // Note that the special case of complete referenced objects
        // occurs when complete unifiable objects are send though
        // a copier.
        if (!_complete && value instanceof Referable referable) {
            referable.addBacklink(this, recorder);
        }
    }

    /**
     * Performs an occurrence check to detect circular references involving a given unknown.
     *
     * @param that the unknown to check for cycles
     * @param visited a set used to track visited references and prevent infinite loops
     * @throws Impossible if a circular reference is detected
     */
    public void occursCheck(Unknown that, Set<IdentityWrapper> visited) throws Impossible {
        // Now let's test the referenced object.
        if (_value instanceof Referable referable) {
            referable.occursCheck(that, visited);
        }
    }

    /**
     * Updates this reference's value to the given new value. This method is
     * called when a {@link Referable}'s value is changed by unification.
     * If the new value is complete, the {@link Referer} is notified about the update.
     *
     * @param oldValue the original value
     * @param newValue the new value replacing the old
     * @param listeners a set of listeners that should be notified about the changes
     * @param recorder state recorder to track the updates for backtracking
     * @throws Impossible if the new value is incomplete and not referable
     */
    public void update(
        final Referable oldValue, final Object newValue, Set<Notifiable> listeners,
        IStateRecorder recorder) throws Impossible
    {
        // A strange phenomenon: The completion of a subobject might
        // be indicated multiple times. Scenario: A=[B,C], B=[C]
        // and C becomes complete. Now C notifies B, which becomes
        // complete and notifies A in turn, which is complete now, too.
        // Now the notification from C to A (C occurs in A)
        // remains to be done, but A is already complete. Therefore,
        // I must be prepared to be notified, although I am already
        // complete. I simply discard any late notifications.
        if (_complete) {
            return;
        }

        if (_value == oldValue && oldValue != newValue) {
            if (recorder != null) {
                recorder.record(() -> _value = oldValue);
            }
            _value = newValue;
        }

        if (!Unify.isComplete(_value)) {
            if (_value instanceof Referable referable) {
                referable.addBacklink(this, recorder);
                return;
            } else {
                throw new RuntimeException(
                    "An incomplete value that is referred to was not referable. Strange");
            }
        }

        if (recorder != null) {
            recorder.record(() -> _complete = false);
        }
        _complete = true;

        if (Unify.isBound(_value)) {
            if (recorder != null) {
                recorder.record(() -> _bound = false);
            }
            _bound = true;
        }

        // Notify the referer about the change of completeness.
        _referer.possiblyCompleted(listeners, recorder);
    }

    /**
     * Returns the referer that uses this reference.
     *
     * @return the referer associated with this reference
     */
    public Referer getReferer() {
        return _referer;
    }

    /**
     * Returns whether the reference is bound, i.e. fully calculated.
     *
     * @return {@code true} if the reference is bound, {@code false} otherwise
     */
    public boolean isBound() {
        return _bound;
    }

    /**
     * Returns whether the reference is complete, i.e. the state will not be changed by further unification,
     * which makes it fully calculable.
     *
     * @return {@code true} if the reference is complete, {@code false} otherwise
     */
    public boolean isComplete() {
        return _complete;
    }

    /**
     * Returns the value being referenced.
     *
     * @return the current value of this reference
     */
    public Object getValue() {
        return _value;
    }
}