package de.renew.net.inscription.arc;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Vector;

import org.apache.log4j.Logger;

import de.renew.engine.searcher.Binder;
import de.renew.engine.searcher.BindingBadness;
import de.renew.engine.searcher.Searcher;
import de.renew.net.TokenReserver;
import de.renew.unify.Impossible;
import de.renew.unify.List;
import de.renew.unify.Unify;
import de.renew.util.Value;

/**
 * A {@code FlexibleArcBinder} finds the binding for the tokens that will be moved by a
 * {@code FlexibleArcOccurrence} that belongs to an input arc.
 */
class FlexibleArcBinder implements Binder {
    /** The logger used by all {@code FlexibleArcBinder} instances.*/
    public static final Logger LOGGER = Logger.getLogger(FlexibleArcBinder.class);

    /**
     * This is the flexible arc occurrence that created this binder and
     * wants to be informed about the tokens that are actually moved.
     * This information is passed via the field {@link FlexibleArcOccurrence#getInTokens()}  inTokens.
     */
    private final FlexibleArcOccurrence _occurrence;

    /**
     * Constructs a new {@code FlexibleArcBinder} based on the {@code FlexibleArcOccurrence} that it allows to act.
     * @param occurrence the {@code FlexibleArcOccurrence} whose executables require a binding from the
     *                   {@code FlexibleArcBinder} in order to run.
     */
    FlexibleArcBinder(FlexibleArcOccurrence occurrence) {
        this._occurrence = occurrence;
    }

    @Override
    public int bindingBadness(Searcher searcher) {
        if (Unify.isBound(_occurrence.getTokenVar())) {
            return 1;
        }
        // We must not try to bind the variable.
        return BindingBadness.MAX;
    }

    private void rememberSingleToken(Object token) {
        _occurrence.getInTokens().addElement(Unify.copyBoundValue(token));
    }

    private Object applyConversionFunction(Object token) throws Impossible {
        if (_occurrence.getArc().getForwardFunction() == null) {
            return token;
        }

        Object convertedToken = _occurrence.getArc().getForwardFunction().function(token);

        if (_occurrence.getArc().getBackwardFunction() != null) {
            Object restoredToken =
                _occurrence.getArc().getBackwardFunction().function(convertedToken);
            Unify.unify(token, restoredToken, null);
        }

        return convertedToken;
    }

    @Override
    public void bind(Searcher searcher) {
        if (!Unify.isBound(_occurrence.getTokenVar())) {
            // This should not happen.
            throw new RuntimeException(
                "Flexible arc binder was invoked for an " + "incomplete value.");
        }

        // The array is analysed locally and need not be copied.
        Object tokens = _occurrence.getTokenVar().getValue();

        _occurrence.setInTokens(new Vector<>());

        //We can not check for Enumerations and Iterators here as we would use them
        //while checking for possible bindings.
        if (tokens == null) {
            // Let's ignore this arc. Probably the null value is meant as
            // a substitute for an empty array.

            /*} else if (tokens instanceof Iterator) {
                for (Iterator i = (Iterator) tokens; i.hasNext();) {
                                rememberSingleToken(i.next());
                }
            } else if (tokens instanceof Enumeration) {
                Enumeration enumeration = (Enumeration) tokens;
                while (enumeration.hasMoreElements()) {
                                rememberSingleToken(enumeration.nextElement());
                }*/
        } else if (tokens.getClass().isArray()) {
            Class<?> elementType = tokens.getClass().getComponentType();

            int n = Array.getLength(tokens);

            for (int i = 0; i < n; i++) {
                Object token = null;
                try {
                    // Extract a single token and copy it, so that
                    // backtracking does not hurt.
                    token = Array.get(tokens, i);

                    if (elementType.isPrimitive()) {
                        token = new Value(token);
                    }

                    token = applyConversionFunction(token);
                    rememberSingleToken(token);

                } catch (Exception e) {
                    // Abort this search process.
                    // A conversion did not succeed.
                    return;
                }
            }
        } else if (tokens instanceof List current) {
            while (!current.isNull()) {
                rememberSingleToken(current.head());
                if (!(current.tail() instanceof List)) { // stop if open/corrupted list
                    break;
                }
                current = (List) current.tail();
            }
        } else if (tokens instanceof Collection<?> coll) {
            for (Object o : coll) {
                rememberSingleToken(o);
            }
        }

        if (!_occurrence.getArc().isOutputArc() && !_occurrence.getInTokens().isEmpty()) {
            // Make sure that the place instance notifies
            // the searchable if its marking changes,
            // because in that case the possible binding would have
            // to be rechecked. This is only required for input
            // arcs, because output arcs cannot disable or enable
            // a transition, and only if a token is actually moved.
            searcher.insertTriggerable(_occurrence.getPlaceInstance().triggerables());
        }

        // Now we start computation with side effects.
        boolean success = true;
        int maxOk = 0;
        TokenReserver tokenReserver = TokenReserver.getInstance(searcher);
        while (success && maxOk < _occurrence.getInTokens().size()) {
            try {
                success = tokenReserver.removeToken(
                    _occurrence.getPlaceInstance(), _occurrence.getInTokens().elementAt(maxOk), 0);
                if (success) {
                    // Reservation was successful.
                    maxOk++;
                }
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
                success = false;
            }
        }

        if (success) {
            // All reservations succeeded.
            searcher.search();
        }

        // Undo the successful reservations.
        while (maxOk > 0) {
            maxOk--;
            tokenReserver.unremoveToken(
                _occurrence.getPlaceInstance(), _occurrence.getInTokens().elementAt(maxOk), 0);
        }


        // The set of tokens has become invalid.
        _occurrence.setInTokens(null);
    }
}