package de.renew.unify;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.Serial;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.stream.IntStream;

/**
 * This is the abstract superclass of {@link Tuple} and {@link List}.
 * It captures the common properties of both classes.
 * If further subtypes are added, it is
 * necessary to update the {@link TupleIndex} class.
 * <p>
 * Serialization Notes:
 * Only complete aggregates are allowed to be serialized.
 * As a consequence, deserialized tuples do not need
 * a recorderChecker: the field is transient and does
 * not get a value on deserialization.
 */
public abstract class Aggregate implements Unifiable, Referable, Referer, Serializable {
    /**
     * Stores backlinks of the aggregate
     */
    private BacklinkSet _backlinkSet;
    private final transient RecorderChecker _recorderChecker;
    /**
     * The array of references that refer to the actual values of this aggregate.
     */
    private final Reference[] _references;
    /**
     * {@code true} if every component of this aggregate is complete,
     * i.e. the state will not be changed by further unification,
     * which makes it fully calculable.
     */
    private boolean _complete;
    /**
     * {@code true} if every component of this aggregate is bound,
     * i.e. it is fully calculated.
     */
    private boolean _bound;

    /**
     * Constructor for the abstract class Aggregate.
     * Fills the Aggregate with unknowns.
     *
     * @param arity the number of elements in the aggregate
     */
    Aggregate(int arity) {
        _recorderChecker = new RecorderChecker(null);
        _references = Unify.makeUnknownReferenceArray(arity, this);

        _complete = (arity == 0);
        _bound = (arity == 0);
        if (!_complete) {
            _backlinkSet = new BacklinkSet();
        }
    }

    /**
     * Constructor that initialises the Aggregate with the given initial values.
     *
     * @param initValues initial values of the aggregate
     * @param recorder the {@link IStateRecorder} to record state changes
     */
    Aggregate(Object[] initValues, IStateRecorder recorder) {
        _recorderChecker = new RecorderChecker(recorder);
        _references = Unify.cleanupReferenceArray(initValues, this, recorder);

        _complete = calculateComplete();
        _bound = calculateBound();
        if (!_complete) {
            _backlinkSet = new BacklinkSet();
        }
    }

    /**
     * Returns the values of the references as an object array in the specified type.
     *
     * @param destType type of the objects in the array
     * @return Object array with the references
     */
    public Object[] asArray(Class<?> destType) {
        return copyInto((Object[]) Array.newInstance(destType, _references.length));
    }

    /**
     * Returns the values of the references as an object array.
     *
     * @return Object array with the references
     */
    public Object[] asArray() {
        return copyInto(new Object[_references.length]);
    }

    /**
     * Extracts the referred values from the given array of references.
     *
     * @param result array that will contain the references,
     * needs to be at least as long as the references
     * @return an Object array with the references
     */
    public Object[] copyInto(Object[] result) {
        for (int i = 0; i < _references.length; i++) {
            result[i] = _references[i].getValue();
        }
        return result;
    }

    /**
     * Returns the array of references to the actual values of this aggregate.
     *
     * @return the reference array
     */
    public Reference[] getReferences() {
        return _references;
    }

    private boolean calculateComplete() {
        return Arrays.stream(_references).allMatch(Reference::isComplete);
    }

    private boolean calculateBound() {
        return Arrays.stream(_references).allMatch(Reference::isBound);
    }

    @Override
    public boolean isComplete() {
        return _complete;
    }

    @Override
    public boolean isBound() {
        return _bound;
    }

    @Override
    public void possiblyCompleted(Set<Notifiable> listeners, IStateRecorder recorder)
        throws Impossible
    {
        if (_references.length == 0) {
            // This should not happen.
            throw new RuntimeException("An empty aggregate became completed.");
        }

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

            // Maybe I am even bound?
            if (calculateBound()) {
                if (recorder != null) {
                    recorder.record(() -> _bound = false);
                }
                _bound = true;
            }

            // I any case, I have a notification to send to my backlinked objects.
            // I am notifying with myself as the new value.
            checkBacklinkSet();
            _backlinkSet.updateBacklinked(this, this, listeners, recorder);

            // I have now done the update, so the backlinked objects should be
            // forgotten. In fact they have to be dumped into the state
            // recorder for future backtracking.
            final BacklinkSet oldBacklinkSet = _backlinkSet;
            if (recorder != null) {
                recorder.record(() -> _backlinkSet = oldBacklinkSet);
            }
            _backlinkSet = null;
        }
    }

    private void checkBacklinkSet() {
        if (_backlinkSet == null) {
            if (!_complete) {
                throw new RuntimeException("A complete object received a backlink. Strange");
            }
            throw new RuntimeException("An incomplete object lacks a backlink set. Strange");
        }
    }

    @Override
    public void addBacklink(Reference reference, IStateRecorder recorder) {
        _recorderChecker.checkRecorder(recorder);
        checkBacklinkSet();
        _backlinkSet.addBacklink(reference, recorder);
    }

    // Only call this method when it is clear that the concrete subtypes of
    // this and that allow a unification.
    void unifySilently(Aggregate that, IStateRecorder recorder, Set<Notifiable> listeners)
        throws Impossible
    {
        if (that._references.length != _references.length) {
            throw new Impossible();
        }
        for (int i = 0; i < _references.length; i++) {
            Unify.unifySilently(
                _references[i].getValue(), that._references[i].getValue(), recorder, listeners);
        }
    }

    @Override
    public void occursCheck(Unknown that, Set<IdentityWrapper> visited) throws Impossible {
        // Am I complete? If yes, no unknown can possibly
        // be contained within me.
        if (_complete) {
            return;
        }

        // Did I check myself earlier?
        IdentityWrapper thisWrapper = new IdentityWrapper(this);
        if (visited.contains(thisWrapper)) {
            return;
        }

        // I do not want to check me again.
        visited.add(thisWrapper);

        // Now let's test all the referenced objects.
        for (Reference reference : _references) {
            reference.occursCheck(that, visited);
        }
    }

    boolean matches(Aggregate that) {
        if (_references.length != that._references.length) {
            return false;
        }

        return IntStream.range(0, _references.length).allMatch(
            index -> Objects
                .equals(_references[index].getValue(), that._references[index].getValue()));
    }

    /**
     * Computes a hash code for the value of the reference at the specified index
     * <p>
     * If the value of the reference at the given index is {@code null},
     * this method returns 1. Otherwise, it returns the hash code
     * of the value.
     *
     * @param i the index of the reference the hash should be calculated for
     * @return the hash code of the reference's value, or 1 if the value is {@code null}
     */
    protected int refHash(int i) {
        if (_references[i].getValue() == null) {
            return 1;
        }
        return _references[i].getValue().hashCode();
    }

    /**
     * Serialization method, behaves like default writeObject
     * method except checking additional error conditions.
     *
     * @param out the {@link java.io.ObjectOutputStream} to write the object to
     * @throws NotSerializableException if not complete.
     **/
    @Serial
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        if (isComplete()) {
            out.defaultWriteObject();
        } else {
            throw new NotSerializableException(
                "de.renew.unify.Aggregate: " + this + " is not complete.");
        }
    }

    /**
     * Returns an iterator over the elements in this aggregate.
     *
     * @return an {@link Iterator} over the elements in this aggregate
     */
    public abstract Iterator<Object> iterator();

    /**
     * Returns the number of elements in this aggregate.
     *
     * @return the number of elements in this aggregate
     */
    public abstract int length();
}