package de.renew.unify;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.IntStream;

class BacklinkSet implements Serializable {
    /**
     * The maximum number of backlinks that can be handled without a special data structure.
     */
    private static final int MAX_BACKLINKS = 6;
    /**
     * Up to six backlinked objects can be handled without
     * a special data structure.
     **/
    private int _cnt = 0;
    private final Reference[] _backlinks = new Reference[MAX_BACKLINKS];
    private Set<IdentityWrapper> _externalSet = null;

    private boolean includes(Reference reference) {
        if (_externalSet != null) {
            return _externalSet.contains(new IdentityWrapper(reference));
        }

        return IntStream.range(0, _cnt).anyMatch(index -> _backlinks[index] == reference);
    }

    /**
     * Include an element. This element must not be
     * included in the set so far.
     **/
    private void includeNonElement(Reference reference) {
        if (_externalSet == null && _cnt == MAX_BACKLINKS) {
            _externalSet = new HashSet<>();
            for (int i = 0; i < MAX_BACKLINKS; i++) {
                _externalSet.add(new IdentityWrapper(_backlinks[i]));
            }
        }
        if (_externalSet != null) {
            _externalSet.add(new IdentityWrapper(reference));
        } else {
            _backlinks[_cnt++] = reference;
        }
    }

    /**
     * Exclude an element. This element must be
     * included in the set.
     **/
    private void exclude(Reference reference) {
        if (_externalSet != null) {
            // I do not switch back to array once I have created
            // the hashed set. It is probable that the number
            // of links will grow again in this case.
            _externalSet.remove(new IdentityWrapper(reference));
        } else {
            _cnt--;
            if (_backlinks[_cnt] != reference) {
                if (_backlinks[0] == reference) {
                    _backlinks[0] = _backlinks[_cnt];
                } else if (_backlinks[1] == reference) {
                    _backlinks[1] = _backlinks[_cnt];
                } else if (_backlinks[2] == reference) {
                    _backlinks[2] = _backlinks[_cnt];
                } else {
                    throw new RuntimeException("Backlink not found. Strange. ");
                }
            }


            // Enable garbage collection.
            _backlinks[_cnt] = null;
        }
    }

    // Return the number of references known to this object.
    // Knowing this number is useful to optimize reference
    // redirection by minimizing the number of references changed.
    int size() {
        if (_externalSet != null) {
            return _externalSet.size();
        }

        return _cnt;
    }

    void addBacklink(final Reference reference, IStateRecorder recorder) {
        if (!includes(reference)) {
            includeNonElement(reference);
            if (recorder != null) {
                recorder.record(() -> exclude(reference));
            }
        }
    }

    void updateBacklinked(
        Referable oldValue, Object newValue, Set<Notifiable> listeners, IStateRecorder recorder)
        throws Impossible
    {
        // I will tell all my backlinked objects that the given
        // value is now relevant instead of myself. (Maybe I *am* the
        // new object, but who knows.)
        //
        // I assume that no new backlinks will be added to myself
        // during this procedure.
        if (_externalSet != null) {
            for (IdentityWrapper wrapper : _externalSet) {
                Reference reference = (Reference) wrapper.getObject();
                reference.update(oldValue, newValue, listeners, recorder);
            }
        } else {
            for (int i = 0; i < _cnt; i++) {
                _backlinks[i].update(oldValue, newValue, listeners, recorder);
            }
        }
    }
}