package de.renew.faformalism.compiler;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;

import de.renew.expression.CallExpression;
import de.renew.expression.ConstantExpression;
import de.renew.expression.EqualsExpression;
import de.renew.expression.Expression;
import de.renew.expression.LocalVariable;
import de.renew.expression.TupleExpression;
import de.renew.expression.VariableExpression;
import de.renew.faformalism.shadow.FAShadowLookupExtension;
import de.renew.faformalism.shadow.ShadowFAArc;
import de.renew.faformalism.shadow.ShadowFAState;
import de.renew.faformalism.util.AutomatonDataStructure;
import de.renew.faformalism.util.FAAutomatonModelEnum;
import de.renew.faformalism.util.FAAutomatonSimulationHelper;
import de.renew.faformalism.util.FASyntaxChecker;
import de.renew.faformalism.util.SimulationSettingsManager;
import de.renew.faformalism.util.StackDataStructure;
import de.renew.formalism.function.DynamicStaticMethodFunction;
import de.renew.formalism.java.InscriptionParser;
import de.renew.formalism.java.JavaNetHelper;
import de.renew.formalism.java.ParseException;
import de.renew.formalism.java.SingleJavaNetCompiler;
import de.renew.formalism.java.TypedExpression;
import de.renew.net.ExpressionTokenSource;
import de.renew.net.Net;
import de.renew.net.NetElementID;
import de.renew.net.Place;
import de.renew.net.Transition;
import de.renew.net.inscription.arc.Arc;
import de.renew.net.inscription.transition.ActionInscription;
import de.renew.net.inscription.transition.GuardInscription;
import de.renew.net.inscription.transition.ManualInscription;
import de.renew.simulatorontology.shadow.ShadowInscription;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.ShadowNetElement;
import de.renew.simulatorontology.shadow.SyntaxException;
import de.renew.util.Types;


public class SingleAutomatonCompiler extends SingleJavaNetCompiler {
    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(SingleAutomatonCompiler.class);
    private static int _stateNum = 0;
    private static int _transNum = 0;

    public SingleAutomatonCompiler() {
        this(false, false, false);
    }

    public SingleAutomatonCompiler(
        boolean allowDangerousArcs, boolean allowTimeInscriptions, boolean wantEarlyTokens)
    {
        super(allowDangerousArcs, allowTimeInscriptions, wantEarlyTokens);
    }

    @Override
    public void compile(ShadowNet shadowNet) throws SyntaxException {

        if (hasMultipleStartstates(shadowNet.elements().iterator())) {
            //TODO
            LOGGER.debug("Found multiple startstate! ~~~~~~~~~~~~~«««");

            //            GuiPlugin.getCurrent()
            //                     .showStatus("Cannot simulate automaton with multiple startstates.");
            throw new SyntaxException("Cannot simulate automaton with multiple startstates.");
        }

        // Get the name of the net that is to be compiled.
        Net net = getLookup().getNet(shadowNet.getName());
        LOGGER.debug("compile(ShadowNet) compiling " + net);

        parseDeclarations(shadowNet);

        // Compile each shadow element
        // Compilation has to be done in the following total order

        // 1. ShadowFAStates
        Iterator<ShadowNetElement> iterator = shadowNet.elements().iterator();
        while (iterator.hasNext()) {
            ShadowNetElement elem = iterator.next();
            if (elem instanceof ShadowFAState faState) {
                try {
                    FASyntaxChecker.checkWordSyntax(faState._word);
                } catch (SyntaxException se) {
                    se.addObject(faState);
                    throw se;
                }
                compile(faState, net);
            }
        }
        LOGGER.debug("»~ All shadow states compiled ~«");

        // 2. ShadowFAArcs
        iterator = shadowNet.elements().iterator();
        while (iterator.hasNext()) {
            ShadowNetElement elem = iterator.next();
            if (elem instanceof ShadowFAArc shadowFAArc) {
                try {
                    compile(shadowFAArc, net);
                } catch (SyntaxException se) {
                    se.addObject(shadowFAArc);
                    throw se;
                }
            }
        }

        LOGGER.debug("»~ All shadow states compiled ~«");
        LOGGER.debug("»»~ Compilation of " + shadowNet.getName() + " finished! ~««");
    }

    protected void compile(ShadowFAState shadowFAState, Net net) throws SyntaxException {
        LOGGER.debug("compile(ShadowFAState, Net) compiling " + shadowFAState);

        // Determine the name.
        String sname = shadowFAState.getName();
        if (sname == null) {
            _stateNum++;
            sname = "State" + _stateNum;
        }

        // Create the new place.
        Place place = new Place(net, sname, new NetElementID(shadowFAState.getID()));
        place.setTrace(shadowFAState.getTrace());

        // Mark startstates
        if (shadowFAState._stateType == ShadowFAState.START
            || shadowFAState._stateType == ShadowFAState.STARTEND) {
            String word = shadowFAState._word;
            AutomatonDataStructure dataStructure = null;
            if (SimulationSettingsManager.getAutomatonModel() == FAAutomatonModelEnum.PDA) {
                dataStructure = new StackDataStructure();
            }
            ConstantExpression wordExpression = new ConstantExpression(String.class, word);
            ConstantExpression stackExpression =
                new ConstantExpression(StackDataStructure.class, dataStructure);
            TupleExpression tupleExpression = new TupleExpression(wordExpression, stackExpression);
            ExpressionTokenSource tokenSource = new ExpressionTokenSource(tupleExpression);
            place.add(tokenSource);
        }

        // Add ShadowFAState and mapped Place to lookup
        FAShadowLookupExtension.lookup(lookup).set(shadowFAState, place);

        compileFAStateInscriptions(shadowFAState, place);
    }

    /**
     * Compiles the given FAArc.
     * <p>
     * Creates the corresponding compiled transition and adds arcs
     * to connect to the adjacent states.
     * </p>
     *
     * @param shadowFAArc
     *            ShadowFAArc to be compiled
     * @param net
     *            Net of that arc
     * @throws SyntaxException if the arc is invalid
     */
    protected void compile(ShadowFAArc shadowFAArc, Net net) throws SyntaxException {
        LOGGER.debug("compile(ShadowFAArc, Net) compiling " + shadowFAArc);

        // inscribe arc so it will output its name
        LOGGER.debug("ShadowFAArcs name is " + shadowFAArc.getName());
        //        String printInscr = arcInscriptionPattern.replace("ARCNAME", shadowFAArc.getName());
        //        String printInscr = String.format(arcInscriptionPattern, shadowFAArc.getName());
        //        new ShadowInscription(shadowFAArc, printInscr);

        // Determine the name.
        String tname = shadowFAArc.toString();
        if (tname == null) {
            _transNum++;
            tname = "Arc" + _transNum;
        }

        String arcInscr = getArcInscription(shadowFAArc);
        FASyntaxChecker.checkArcSyntax(arcInscr);

        // Create a transition
        Transition transition = new Transition(net, tname, new NetElementID(shadowFAArc.getID()));
        // the transition should take in the current word and produce the next word.
        // 1. the transition should be manual.
        // 2. the transition should only be able to fire if the current word and arc inscriptions allow it (according to the automaton model).
        // 3. the transition should produce the next word according to the automaton model.
        // P1 --x--> T --y--> P2        where T has the aforementioned inscriptions
        if (SimulationSettingsManager.getManualSimulation()) {
            // add manual inscription
            transition.add(ManualInscription.getInstance());
        }
        // add guard inscription: FAAutomatonSimulationHelper.canFire(x, arcInscription)
        VariableExpression currentWord =
            new VariableExpression(Types.UNTYPED, new LocalVariable("x"));
        ConstantExpression cE;
        String letter = getArcInscription(shadowFAArc);
        if (letter == null) {
            // no inscription found. We treat this as if the arc was inscribed with epsilon
            cE = new ConstantExpression(String.class, "");
        } else {
            // we found an inscription
            cE = new ConstantExpression(String.class, letter);
        }
        Expression[] params = { currentWord, cE };
        TupleExpression sig = new TupleExpression(params);
        DynamicStaticMethodFunction canFireCall =
            new DynamicStaticMethodFunction("canFire", FAAutomatonSimulationHelper.class);
        CallExpression canFire = new CallExpression(Types.UNTYPED, sig, canFireCall);
        GuardInscription guard = new GuardInscription(canFire);
        transition.add(guard);
        //add action inscription: action y=FAAutomatonSimulationHelper.canFire(x, arcInscription)
        VariableExpression left =
            new VariableExpression(Types.UNTYPED, new LocalVariable("y", true));
        DynamicStaticMethodFunction fireCall =
            new DynamicStaticMethodFunction("fire", FAAutomatonSimulationHelper.class);
        CallExpression right = new CallExpression(Types.UNTYPED, sig, fireCall);
        EqualsExpression eEx = new EqualsExpression(Types.UNTYPED, left, right);
        ActionInscription action = new ActionInscription(eEx, transition);
        transition.add(action);

        // Map ShadowFAArc to Transition in lookup
        FAShadowLookupExtension.lookup(lookup).set(shadowFAArc, transition);

        // create and add arcs from/to connected Place
        Place src = FAShadowLookupExtension.lookup(lookup).get(shadowFAArc._src);

        // we now add the inscriptions x and y to the incoming/outgoing arcs.
        // P1 --x--> T --y--> P2
        VariableExpression incomingX =
            new VariableExpression(Types.UNTYPED, new LocalVariable("x", true));
        Arc arc = new Arc(
            src, transition, Arc.Type.IN, incomingX, ConstantExpression.DOUBLE_ZERO_EXPRESSION);
        arc.setTrace(shadowFAArc.getTrace());
        transition.add(arc);

        Place dest = FAShadowLookupExtension.lookup(lookup).get(shadowFAArc._dest);

        // for the second arc inscription we use "y"
        VariableExpression incomingY =
            new VariableExpression(Types.UNTYPED, new LocalVariable("y", true));
        arc = new Arc(
            dest, transition, Arc.Type.OUT, incomingY, ConstantExpression.DOUBLE_ZERO_EXPRESSION);
        arc.setTrace(shadowFAArc.getTrace());
        transition.add(arc);
    }

    protected void compileFAStateInscriptions(ShadowFAState shadowFAState, Place place)
        throws SyntaxException
    {
        LOGGER.debug(
            "compileFAStateInscriptions(ShadowFAState, Place) called with " + shadowFAState
                + " and " + place);


        // Insert the inscriptions.
        Iterator<ShadowNetElement> iterator = shadowFAState.elements().iterator();
        while (iterator.hasNext()) {
            ShadowNetElement elem = iterator.next();
            LOGGER.debug(this + " has " + elem + " as child");

            if (elem instanceof ShadowInscription) {
                String inscr = ((ShadowInscription) elem).getInscription();

                //                logger.debug("On " + shadowFAState + " a ShadowInscription "
                //                             + elem + " with " + inscr + " of type "
                //                             + ((FATextFigure) elem.context).getType()
                //                             + " was found");
                //
                //                FATextFigure faText = (FATextFigure) elem.context;
                //                if (!(faText.getType() == CPNTextFigure.NAME)) {
                try {
                    Iterator<Object> exprEnum = parseFAStateInscription(inscr).iterator();
                    while (exprEnum.hasNext()) {
                        Object expr = exprEnum.next();
                        if (expr instanceof TypedExpression) {
                            TypedExpression typedExpr = (TypedExpression) expr;

                            Expression castedExpression = null;
                            try {
                                castedExpression = JavaNetHelper
                                    .makeCastedOutputExpression(Types.UNTYPED, typedExpr);
                            } catch (SyntaxException e) {
                                throw e.addObject(elem);
                            }
                            place.add(new ExpressionTokenSource(castedExpression));
                            LOGGER.debug("Added " + castedExpression + " to " + shadowFAState);
                        }
                    }
                } catch (SyntaxException e) {
                    throw e.addObject(elem);
                    //                    }

                    //Only Marking parsing:
                    //                        TypedExpression typedExpr;
                    //                        if ("[]".equals(inscr)) {
                    //                            typedExpr = (TypedExpression) parseFAStateInscription(inscr);
                    //                            // token = value1;
                    //                        } else {
                    //                            throw new SyntaxException("Expected '[]'").addObject(elem);
                    //                        }
                    //                        Expression expr = typedExpr.getExpression();
                    //                        place.add(new ExpressionTokenSource(expr));
                }
            } else if (elem instanceof ShadowFAArc) {
                // ignore it.
            } else {
                throw new SyntaxException("Unsupported place inscription: " + elem.getClass())
                    .addObject(shadowFAState).addObject(elem);
            }
        }
    }

    private Collection<Object> parseFAStateInscription(String inscr) throws SyntaxException {
        LOGGER.debug("parseFAStateInscription(String) called with " + inscr);
        if ((inscr != null) && !inscr.equals("")) {
            LOGGER.debug("FAState has inscription » " + inscr + " «");
            InscriptionParser parser = makeParser(inscr);
            parser.setDeclarationNode(declaration);
            try {
                return parser.PlaceInscription();
            } catch (ParseException e) {
                // I really do not know what this could be.
                // I'll give out the error message of the
                // second parse, although it might be
                // appropriate to tell the user that I also
                // tried to parse a type.
                throw makeSyntaxException(e);
            }
        }

        return Collections.emptySet();
    }


    private String getArcInscription(ShadowFAArc shadowFAArc) {
        Set<ShadowNetElement> insc = shadowFAArc.elements();
        String letter = null;
        //find the FAArc's inscription
        for (ShadowNetElement snw : insc) {
            if (snw instanceof ShadowInscription si) {
                letter = si.getInscription();
            }
        }
        return letter;
    }

    private boolean hasMultipleStartstates(Iterator<ShadowNetElement> shadowNetElements) {
        int startstateCount = 0;

        while (shadowNetElements.hasNext()) {
            ShadowNetElement elem = shadowNetElements.next();
            if (elem instanceof ShadowFAState) {
                ShadowFAState shFAState = (ShadowFAState) elem;
                if (shFAState._stateType == ShadowFAState.START
                    || shFAState._stateType == ShadowFAState.STARTEND) {
                    startstateCount++;
                    if (startstateCount > 1) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    @Override
    public String toString() {
        return this.getClass().getName();
    }
}