package de.renew.ptchannel.single;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Vector;
import java.util.stream.Collectors;

import de.renew.expression.ConstantExpression;
import de.renew.expression.Expression;
import de.renew.expression.ListExpression;
import de.renew.expression.TupleExpression;
import de.renew.expression.TypeCheckingExpression;
import de.renew.expression.VariableExpression;
import de.renew.formalism.java.ParsedDeclarationNode;
import de.renew.formalism.java.SingleJavaNetCompiler;
import de.renew.net.Net;
import de.renew.net.Place;
import de.renew.net.Transition;
import de.renew.net.inscription.TransitionInscription;
import de.renew.net.inscription.transition.DownlinkInscription;
import de.renew.net.inscription.transition.UplinkInscription;
import de.renew.net.loading.NetLoader;
import de.renew.shadowcompiler.ShadowLookup;
import de.renew.simulatorontology.shadow.ShadowArc;
import de.renew.simulatorontology.shadow.ShadowDeclarationNode;
import de.renew.simulatorontology.shadow.ShadowInscription;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.ShadowNetElement;
import de.renew.simulatorontology.shadow.ShadowPlace;
import de.renew.simulatorontology.shadow.ShadowTransition;
import de.renew.simulatorontology.shadow.SyntaxException;
import de.renew.util.Value;

/**
 * The sole purpose of this class is to compile the transitions of a
 * {@link SinglePTNetWithChannelCompiler}.
 * It is NOT intended to function as a self-sufficient net single,
 * even though it extends a self-sufficient single.
 * It restricts the {@link SingleJavaNetCompiler} and does NOT fulfill
 * all of its contracts.
 * Thus, this class fulfills the bad smells of Refused Bequest and
 * Inheritance Abuse.
 * The main reason to use it is to have access to all
 * methods that are protected in the java net single.
 * <p>
 * A PTCTransitionCompiler compiles transitions and arcs by using
 * many methods of its parent and afterwards restricting the returned
 * results by throwing exceptions if the results contain invalid
 * elements.
 *
 * @author Lukas Voß
 */
public class PTCTransitionCompiler extends SingleJavaNetCompiler {

    /**
     * Creates a new <code>PTCTransitionCompiler</code> instance.
     *
     * @param allowDangerousArcs    (UNUSED) whether arcs without partial order
     *                              semantics (inhibitor and clear arcs) are allowed in the net.
     * @param allowTimeInscriptions whether time inscriptions are allowed
     *                              in the net.
     * @param wantEarlyTokens       whether instances of nets compiled by this
     **/
    public PTCTransitionCompiler(
        boolean allowDangerousArcs, boolean allowTimeInscriptions, boolean wantEarlyTokens)
    {
        super(allowDangerousArcs, allowTimeInscriptions, wantEarlyTokens);
    }

    /**
     * Compile an arc by turning it into a flexible arc for list unpacking
     * and compile it like an ordinary {@link SingleJavaNetCompiler}.
     *
     * @param shadowArc       the arc to compile
     * @param declarationNode declaration node of the shadow net
     * @param lookup          the net's lookup
     * @throws SyntaxException if the arc cannot be compiled correctly
     */
    public void compileArc(
        ShadowArc shadowArc, ParsedDeclarationNode declarationNode, ShadowLookup lookup)
        throws SyntaxException
    {
        this.declaration = declarationNode;
        this.lookup = lookup;

        // If an arc gets compiled by this compiler, it must be inscribed -> turn into flexible arc.
        shadowArc.setShadowArcType(ShadowArc.DOUBLE_ORDINARY);
        super.compileArc(shadowArc);
    }

    /**
     * Compile an arc between a place and a transitions.
     *
     * @param shadowArc the arc to compile
     * @param lookup the net's lookup
     * @throws SyntaxException if the arc cannot be compiled correctly
     */
    // NiN section.
    public void compileArc(ShadowArc shadowArc, ShadowLookup lookup) throws SyntaxException {
        this.lookup = lookup;
        super.compileArc(shadowArc);
    }

    /**
     * After editing, this method checks the syntax of a transition's inscription.
     * Since the class is used as a single for the {@link SinglePTNetWithChannelCompiler},
     * the only viable inscriptions are {@link UplinkInscription} and {@link DownlinkInscription}.
     * All other inscriptions will throw an exception.
     * A transition may only be inscribed with one inscription and
     * downlink parameters must be integers.
     * Also, currently synchronous communication is only supported within
     * one net instance. Thus, a downlink's callee must be "this".
     * After checking all inscriptions, it returns the first inscription of the transition.
     *
     * @param inscription the syntax-checked inscription
     * @param special     whether it is special
     * @param shadowNet   the associated shadow net
     * @return the type of the first inscription
     * @throws SyntaxException if any inscription other then up-/downlink are present
     */
    @Override
    public String checkTransitionInscription(
        String inscription, boolean special, ShadowNet shadowNet) throws SyntaxException
    {
        makeDeclarationNode(shadowNet);
        // Parse the inscription
        Collection<TransitionInscription> inscriptions = makeInscriptions(inscription, null, false);

        if (inscriptions.size() == 0) {
            throw new SyntaxException("Invalid inscription");
        }

        // Only one inscription per transition is allowed.
        if (inscriptions.size() > 1) {
            throw new SyntaxException("Multiple inscriptions not supported");
        }

        // Throw exception if any inscription is not valid.
        for (TransitionInscription transitionInscription : inscriptions) {
            throwExceptionIfNotChannel(transitionInscription);
            if (transitionInscription instanceof DownlinkInscription) {
                throwExceptionIfCalleeNotThis((DownlinkInscription) transitionInscription);
            }
        }

        // Return the type of the first inscription
        Object res = inscriptions.iterator().next();
        if (res instanceof UplinkInscription) {
            return "uplink";
        } else if (res instanceof DownlinkInscription) {
            return "downlink";
        } else {
            throw new SyntaxException("Unsupported inscription");
        }
    }

    /**
     * Method for initiating the compilation of transition inscriptions,
     * when the single's declarationNode and NetLoader are not set.
     * Since this single is not a self-sufficient single, those
     * are set from this method, allowing the compilation of inscriptions.
     *
     * @param inscription     the inscription to compile
     * @param lookup          the lookup
     * @param create          if its a create inscription
     * @param declarationNode a single's declaration node
     * @param loader          a single's loader
     * @return the parsed inscriptions
     * @throws SyntaxException if any inscription is not an up-/downlink
     */
    public Collection<TransitionInscription> makeInscriptions(
        ShadowInscription inscription, ShadowLookup lookup, boolean create,
        ParsedDeclarationNode declarationNode, NetLoader loader) throws SyntaxException
    {

        // The input string must be parsed, it denotes either an uplink or a downlink
        //
        // Syntax:
        //   x:channel(...)         downlink
        //   :channel(...)          uplink
        String str = inscription.getInscription();
        Transition transition = lookup.get((ShadowTransition) inscription.getInscribable());

        this.declaration = declarationNode;
        this._loopbackNetLoader = loader;

        Collection<TransitionInscription> inscriptions = makeInscriptions(str, transition, create);

        // Throw exceptions if inscription is not supported
        throwExceptionIfMultipleInscriptions(inscription, inscriptions);
        for (TransitionInscription transitionInscription : inscriptions) {
            throwExceptionIfNotChannel(transitionInscription);
            if (transitionInscription instanceof DownlinkInscription) {
                throwExceptionIfCalleeNotThis((DownlinkInscription) transitionInscription);
            }
        }
        return convertIntegerInscriptions(inscriptions);
    }

    /**
     * Loop through all inscriptions in a given collection and change
     * those inscriptions which are {@link DownlinkInscription DownlinkInscriptions}
     * or {@link UplinkInscription UplinkInscriptions} and
     * have integers as parameter values.
     * Those inscriptions are changed to lists with as many black tokens
     * in them as given by the integer value.
     *
     * @param inscriptions collection of inscriptions to change
     * @return a collection in which all integer downlinks are changed
     */
    private Collection<TransitionInscription> convertIntegerInscriptions(
        Collection<TransitionInscription> inscriptions)
    {
        return inscriptions.stream().map(inscription -> {
            if (inscription instanceof DownlinkInscription
                || inscription instanceof UplinkInscription) {
                return transformIntegerInscriptions(inscription);
            }
            return inscription;
        }).collect(Collectors.toCollection(ArrayList::new));
    }

    /**
     * Transform each integer parameter of an inscription
     * into a List with black tokens.
     * All other expressions of the inscription
     * remain unchanged.
     *
     * @param inscription the inscription to transform.
     *                    Must either be an up-/ or downlink.
     * @return a new inscription with a potentially changed expression array
     */
    private TransitionInscription transformIntegerInscriptions(TransitionInscription inscription) {
        TupleExpression oldTuple;
        if (inscription instanceof DownlinkInscription) {
            oldTuple =
                (TupleExpression) ((DownlinkInscription) inscription).getParameterExpression();
        } else {
            oldTuple = (TupleExpression) ((UplinkInscription) inscription).getParams();
        }
        Expression[] expressions = new Expression[oldTuple.getExpressions().length];
        for (int i = 0; i < oldTuple.getExpressions().length; i++) {
            if (oldTuple.getExpressions()[i] instanceof TypeCheckingExpression) {
                expressions[i] = buildBlackTokenExpression(
                    (TypeCheckingExpression) oldTuple.getExpressions()[i]);
            } else {
                expressions[i] = oldTuple.getExpressions()[i];
            }
        }
        if (inscription instanceof DownlinkInscription) {
            DownlinkInscription oldDownlink = (DownlinkInscription) inscription;
            return new DownlinkInscription(
                oldDownlink.getName(), new TupleExpression(expressions), oldDownlink.getCallee(),
                false, oldDownlink.getTransition());
        } else {
            UplinkInscription oldUplink = (UplinkInscription) inscription;
            return new UplinkInscription(oldUplink.getName(), new TupleExpression(expressions));
        }
    }

    /**
     * This method parses a type checking expression for its
     * integer value and build a new expression which contains
     * that many black tokens instead.
     *
     * @param tce an integer inscription parameter
     * @return an expression with n black tokens in a list
     */
    private TypeCheckingExpression buildBlackTokenExpression(TypeCheckingExpression tce) {
        ConstantExpression constantExpression = (ConstantExpression) tce.getArgument();
        Value value = (Value) constantExpression.getConstant();
        int insc = value.intValue();
        TupleExpression[] emptyTuples = buildAggregatedTuples(insc);
        ListExpression list = new ListExpression(emptyTuples, false);
        return new TypeCheckingExpression(list.getType(), list);
    }

    /**
     * Build a tuple expression that consists of n empty tuples
     * (i.e. black tokens), where n is equal to inscrValue.
     *
     * @param inscrValue that many black tokens get added
     * @return The expression with inscrValue-many black tokens
     */
    private TupleExpression[] buildAggregatedTuples(int inscrValue) {
        TupleExpression[] expressions = new TupleExpression[inscrValue];
        for (int i = 0; i < inscrValue; i++) {
            Expression[] empty = new TupleExpression[0];
            expressions[i] = new TupleExpression(empty);
        }
        return expressions;
    }

    /**
     * Throws an exception if a transition is inscribed
     * with multiple inscriptions.
     *
     * @param inscription  this inscription's transition shall not have multiple inscriptions
     * @param inscriptions this collection shall only contain one inscription
     * @throws SyntaxException if transition is inscribed with multiple inscriptions
     */
    private void throwExceptionIfMultipleInscriptions(
        ShadowInscription inscription, Collection<TransitionInscription> inscriptions)
        throws SyntaxException
    {
        // Catch multiple different inscriptions
        boolean inscriptionFound = false;
        Iterator<ShadowNetElement> iterator = inscription.getInscribable().elements().iterator();
        while (iterator.hasNext()) {
            Object elem = iterator.next();
            if (elem instanceof ShadowInscription) {
                if (inscriptionFound) {
                    throw new SyntaxException("Multiple inscriptions not supported");
                }
                inscriptionFound = true;
            }
        }
        // Catch multiple inscriptions in one inscription
        if (inscriptions.size() > 1) {
            throw new SyntaxException("Multiple inscriptions not supported");
        }
    }

    /**
     * Throws an exception if a transition is inscribed
     * with something other than up-/downlink.
     *
     * @param insc this inscription shall either be an up- or downlink.
     * @throws SyntaxException if transition is inscribed with something other than up-/downlink
     */
    private void throwExceptionIfNotChannel(TransitionInscription insc) throws SyntaxException {
        if (!((insc instanceof DownlinkInscription) || (insc instanceof UplinkInscription))) {
            throw new SyntaxException("Only up- and downlinks are supported");
        }
    }

    /**
     * Throw an exception if the callee of a downlink is not this.
     *
     * @param downlink inscription to check
     * @throws SyntaxException if a callee is not this
     */
    private void throwExceptionIfCalleeNotThis(DownlinkInscription downlink)
        throws SyntaxException
    {
        if (!(downlink.getCallee() instanceof VariableExpression)) {
            throw new SyntaxException("Callee must be this.");
        } else if (!((VariableExpression) downlink.getCallee()).getVariable().getName()
            .equals("this")) {
            throw new SyntaxException("Callee must be this.");
        }
    }

    // NiN section.
    // All methods in this section are added to ensure
    // that the NiN compiler can access the methods for
    // compiling various net elements.

    public void compilePlaceInscriptions(ShadowPlace shadowPlace, Place place)
        throws SyntaxException
    {
        super.compilePlaceInscriptions(shadowPlace, place);
    }

    /**
     * Compile a place into a net with a given group ID.
     *
     * @param shadowPlace the place to compile
     * @param net the net to compile the place into
     * @param groupId the group ID of the net
     */
    public void compile(ShadowPlace shadowPlace, Net net, Serializable groupId)
        throws SyntaxException
    {
        super.compile(shadowPlace, net, groupId);
    }

    /*
     * Compile a transition into a net with a given group ID.
     *
     * @param shadowTransition the transition to compile
     * @param net the net to compile the transition into
     * @param groupId the group ID of the net
     */
    public void compile(ShadowTransition shadowTransition, Net net, Serializable groupId)
        throws SyntaxException
    {
        super.compile(shadowTransition, net, groupId);
    }

    /**
     * Compile a transition inscription.
     *
     * @param inscription the inscription to compile
     * @param lookup the net's lookup
     * @return a collection of transition inscriptions
     * @throws SyntaxException if the inscription cannot be compiled correctly
     */
    public Collection<TransitionInscription> compileTransitionInscription(
        ShadowInscription inscription, ShadowLookup lookup) throws SyntaxException
    {
        this.lookup = lookup;
        return super.compileTransitionInscription(inscription);
    }

    public void compileTransitionInscriptions(
        ShadowTransition shadowTransition, Vector<TransitionInscription> parsedInscriptions,
        Vector<ShadowInscription> errorShadows) throws SyntaxException
    {
        super.compileTransitionInscriptions(shadowTransition, parsedInscriptions, errorShadows);
    }


    public ShadowDeclarationNode findDeclarationNode(ShadowNet shadowNet) throws SyntaxException {
        return super.findDeclarationNode(shadowNet);
    }
}
