/**
 *
 */

package de.renew.lola2;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import CH.ifa.draw.figures.AttributeFigure;
import CH.ifa.draw.figures.TextFigure;
import CH.ifa.draw.framework.FigureWithID;
import CH.ifa.draw.standard.FilteredFigureEnumerator;
import de.renew.draw.storables.ontology.Drawing;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureEnumeration;
import de.renew.formalism.FormalismPlugin;
import de.renew.gui.ArcConnection;
import de.renew.gui.CPNDrawing;
import de.renew.gui.CPNTextFigure;
import de.renew.gui.PlaceFigure;
import de.renew.gui.TransitionFigure;
import de.renew.gui.VirtualPlaceFigure;
import de.renew.lola2.analysis.LolaHelper;


/**
 * This class is responsible for creating a Lola net file from a CPNDrawing.
 * It extracts places, transitions, and initial markings, and writes them to
 * the specified output stream in the Lola format.
 */
public class LolaFileCreator {
    private static org.apache.log4j.Logger _logger =
        org.apache.log4j.Logger.getLogger(LolaFileCreator.class);
    private Map<Figure, String> _usedFigureNames = new HashMap<Figure, String>();
    /**
     * This map contains the preset of all transitions in the Petri net.
     */
    protected Map<TransitionFigure, HashMap<PlaceFigure, Integer>> _preAll =
        new HashMap<TransitionFigure, HashMap<PlaceFigure, Integer>>();
    /**
     * This map contains the postset of all transitions in the Petri net.
     */
    protected Map<TransitionFigure, HashMap<PlaceFigure, Integer>> _postAll =
        new HashMap<TransitionFigure, HashMap<PlaceFigure, Integer>>();
    private boolean _ptnetcompiler;

    /**
     * Constructor for LolaFileCreator.
     * Initializes the creator and checks if the PT formalism is selected.
     * If so, it sets the _ptnetcompiler flag to true.
     */
    public LolaFileCreator() {
        super();
        // if the PT formalism is selected calculate weight using integers
        FormalismPlugin plugin = FormalismPlugin.getCurrent();
        if (plugin != null) {
            String compiler = plugin.getCompiler();
            if (_logger.isInfoEnabled()) {
                _logger.info("[Lola] Using compiler: " + compiler);
                if (compiler.equals(FormalismPlugin.PT_COMPILER)) {
                    _ptnetcompiler = true;
                }
            }
        } else {
            _ptnetcompiler = true;
        }
    }

    /**
     * Writes the drawing into the stream.
     *
     * @param stream  stream to write drawing into
     * @param drawing drawing to write into stream
     */
    public void writeLolaFile(OutputStream stream, CPNDrawing drawing) {
        writeLolaFile(stream, drawing, null);
    }

    /**
     * Writes the drawing into the stream. Additional parameter to manually add
     * an initial marking
     *
     * @param stream         stream to write drawing into
     * @param drawing        drawing to write into stream
     * @param initialMarking may manually add an initial marking
     */
    public void writeLolaFile(OutputStream stream, CPNDrawing drawing, String initialMarking) {
        // check if net is non-empty
        if (emptyNet(drawing)) {
            _logger.error("[Lola Export] Net drawing needs at least 1 place and 1 transition.");
            return;
        }
        _logger.info("[Lola Export] Converting drawing to lola net file format");
        if (_logger.isDebugEnabled()) {
            _logger.debug("  Extracting Places");
        }
        StringBuffer placeBuffer = extractPlaces(drawing);
        String marking = initialMarking;
        if (initialMarking == null) {
            if (_logger.isDebugEnabled()) {
                _logger.debug("  Extracting Marking");
            }
            marking = extractMarking(drawing);
        }
        marking += ";\n\n";

        if (_logger.isDebugEnabled()) {
            _logger.debug("  Extracting Transitions");
        }
        StringBuffer transitionsBuffer = extractTransitions(drawing);
        try {
            stream.write(placeBuffer.toString().getBytes());
            stream.write(marking.getBytes());
            stream.write(transitionsBuffer.toString().getBytes());
        } catch (IOException e) {
            _logger.error("[Lola] Writing to output stream failed");
            e.printStackTrace();
        }
    }

    /**
     * Tests whether the given drawing has no places or no transitions. In both
     * cases lola can't handle the exported net file.
     *
     * @param drawing the CPNDrawing to check for emptiness
     * @return true if net is empty (has less than 1 place or less than 1 transition), false otherwise
     */
    private boolean emptyNet(CPNDrawing drawing) {
        int p = 0;
        int t = 0;
        FigureEnumeration figs = drawing.figures();
        while (figs.hasMoreElements()) {
            Figure fig = figs.nextElement();
            if (fig instanceof PlaceFigure) {
                p++;
            }
            if (fig instanceof TransitionFigure) {
                t++;
            }
        }
        if (_logger.isDebugEnabled()) {
            _logger.debug(
                "[Lola] Net " + drawing.getName() + " has " + p + " Places and " + t
                    + " Transitions");
        }
        return (p < 1 || t < 1);
    }

    /**
     * This method takes care of the creation of a temporary lola file for a
     * given CPNDrawing.
     *
     * @param drawing the CPNDrawing to be exported to lola format and written into a net file
     * @return the created temporary LolaFile
     */
    public File writeTemporaryLolaFile(CPNDrawing drawing) {
        return writeTemporaryLolaFile(drawing, null);
    }

    /**
     * This method takes care of the creation of a temporary lola file for a
     * given CPNDrawing. This method also uses a custum, manually input initial
     * marking, which is ignored if it is the empty string.
     *
     * @param drawing the CPNDrawing to be exported to lola format and written into a net file
     * @param initMarking a manually input initial marking; may be null to use default marking
     * @return the created temporary LolaFile
     */
    public File writeTemporaryLolaFile(CPNDrawing drawing, String initMarking) {
        File netFile = null;
        OutputStream fileStream = null;

        String netName = drawing.getName();
        try {
            netFile = File.createTempFile("renew" + netName, ".net", LolaHelper.findTmpDir());
            if (_logger.isInfoEnabled()) {
                _logger.info("[Lola] temporay lola net file: " + netFile.getAbsolutePath());
            }
            fileStream = new FileOutputStream(netFile);
            writeLolaFile(fileStream, drawing, initMarking);
        } catch (FileNotFoundException e) {
            _logger.error("[Lola] Could not create stream to write lola file");
            e.printStackTrace();
        } catch (IOException e) {
            _logger.error(
                "[Lola] Could not create temporary lola file in directory "
                    + LolaHelper.findTmpDir());
            e.printStackTrace();
        }
        return netFile;
    }

    /**
     * @param drawing the CPNDrawing containing the transitions to extract
     * @return StringBuffer containing the Lola-formatted transition definitions
     */
    private StringBuffer extractTransitions(CPNDrawing drawing) {
        StringBuffer buffer = new StringBuffer();
        Boolean check = false;
        for (FigureEnumeration it = drawing.figures(); it.hasMoreElements();) {
            Figure figure = it.nextFigure();
            if (figure instanceof TransitionFigure) {
                TransitionFigure trans = (TransitionFigure) figure;
                buffer.append(
                    "TRANSITION " + name(trans) + "{" + "x:" + (int) trans.displayBox().getX()
                        + "y:" + (int) trans.displayBox().getY() + "}" + "\n");

                HashMap<String, Integer> preset = preset(drawing, trans);
                buffer.append("CONSUME\n  ");
                check = false;
                for (Iterator<String> iter = preset.keySet().iterator(); iter.hasNext();) {
                    if (check) {
                        buffer.append(",\n  ");
                    } else {
                        check = true;
                    }
                    String placeName = iter.next();
                    buffer.append(placeName + ": " + preset.get(placeName));

                }
                buffer.append(";\n\n");

                HashMap<String, Integer> postset = postset(drawing, trans);
                buffer.append("PRODUCE\n  ");
                check = false;
                for (Iterator<String> iter = postset.keySet().iterator(); iter.hasNext();) {
                    if (check) {
                        buffer.append(",\n  ");
                    } else {
                        check = true;
                    }
                    String placeName = iter.next();
                    buffer.append(placeName + ": " + postset.get(placeName));

                }
                buffer.append(";\n\n");

            }
        }
        return buffer;
    }

    /**
     * @param drawing the CPNDrawing to extract the marking from
     * @return String containing the Lola-formatted initial marking
     */
    private String extractMarking(CPNDrawing drawing) {
        StringBuffer buffer = new StringBuffer();
        Boolean check = false;
        buffer.append("MARKING\n");
        for (FigureEnumeration iterator = drawing.figures(); iterator.hasMoreElements();) {
            Figure fig = iterator.nextFigure();
            if (fig instanceof PlaceFigure) {
                PlaceFigure place = (PlaceFigure) fig;
                if (_logger.isDebugEnabled() && place instanceof VirtualPlaceFigure) { // warn about virtual places
                    _logger.debug(
                        "[Lola Export] Place " + name(place)
                            + " is virtual.\n It is not allowed that both virtual and original place are marked.");
                }
                FigureEnumeration childs = place.children();
                StringBuffer marking = new StringBuffer();
                Boolean hasMarking = false;
                int tokenCounter = 0;
                // are we first entry, otherwise add colon and line break
                marking.append(check ? ",\n  " : "  ");
                marking.append(
                    (place instanceof VirtualPlaceFigure) ? name(place.getSemanticFigure())
                        : name(place));
                marking.append(": ");
                while (childs.hasMoreElements()) {
                    Figure child = childs.nextFigure();
                    if (child instanceof CPNTextFigure) { // only Textfigures...
                        String text = ((CPNTextFigure) child).getText();
                        if (((CPNTextFigure) child).getType() == CPNTextFigure.INSCRIPTION) {
                            if (_logger.isDebugEnabled()) {
                                _logger.debug(
                                    "[Lola Export: Extract Marking] Place " + name(place)
                                        + " has inscription " + text);
                            }
                            if (_ptnetcompiler) {
                                if (_logger.isInfoEnabled()) {
                                    _logger.info(
                                        LolaFileCreator.class.getSimpleName()
                                            + ": Encountered ptnetcomp and number: " + text);
                                }
                                // this might throw a Numberformat exception, if the int is to big
                                tokenCounter = (text.matches("^[0-9]*$"))
                                    ? tokenCounter + Integer.parseInt(text)
                                    : tokenCounter + 1;
                            } else {
                                tokenCounter++;
                            }
                            hasMarking = true;
                        }
                    }
                }

                if (_logger.isDebugEnabled()) {
                    _logger.debug(
                        "[Lola Export: Extract Marking] Place " + name(place) + " has "
                            + tokenCounter + " token.");
                }
                marking.append(tokenCounter);
                if (hasMarking) {
                    check = true;
                    buffer.append(marking);
                }
            }
        }

        //        buffer.append(";\n\n");
        return buffer.toString();
    }

    /**
     * @param drawing the CPNDrawing containing the places to extract
     * @return StringBuffer containing the Lola-formatted place definitions
     */
    private StringBuffer extractPlaces(CPNDrawing drawing) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("PLACE\n");
        boolean check = false;
        for (FigureEnumeration iterator = drawing.figures(); iterator.hasMoreElements();) {
            Figure fig = iterator.nextFigure();
            if (fig instanceof PlaceFigure && !(fig instanceof VirtualPlaceFigure)) {
                if (check) {
                    buffer.append(",\n");
                } else {
                    check = true;
                }
                PlaceFigure place = (PlaceFigure) fig;
                buffer.append(
                    "  " + this.name(place) + "{" + "x:" + (int) place.displayBox().getX() + "y:"
                        + (int) place.displayBox().getY() + "}");

            }
        }
        buffer.append(";\n\n");
        return buffer;
    }

    /**
     * Creates HashMap with names of the places that are connected with an arc
     * to this Transition trans in the Petri Net as the keys. They are either
     * the source of the arc or it is double arc/test arc in which case they
     * should be in the preset and the postset. So the source/destination
     * doesn't matter. (source/destination depends on where you started drawing)
     *
     * @param drawing the Drawing containing the Petri net
     * @param trans   the TransitionFigure to find the preset for
     * @return HashMap with place names as keys and arc weights as values representing the preset
     */
    private HashMap<String, Integer> preset(Drawing drawing, TransitionFigure trans) {
        HashMap<String, Integer> result = new HashMap<String, Integer>();
        for (FigureEnumeration iterator = drawing.figures(); iterator.hasMoreElements();) {
            Figure fig = iterator.nextFigure();
            if (fig instanceof ArcConnection) { // look for arcs...
                ArcConnection arc = (ArcConnection) fig;
                Integer arcType = arc.getArcType();
                Figure end = arc.endFigure();
                Figure start = arc.startFigure();

                // ... ending at trans ...
                if (end.equals(trans) && start instanceof PlaceFigure) {
                    // arcSource = The place the arc starts at.
                    String arcSource = name(unvirtualize((PlaceFigure) start));
                    if (result.get(arcSource) == null) { // Check for multiple arcs consuming marks from this place.
                        result.put(arcSource, parseArcInscription(arc));
                    } else {
                        result.put(arcSource, parseArcInscription(arc) + result.get(arcSource));
                    }
                } // ... or being double-headed and starting at trans
                else if (start.equals(trans) && (arcType == 0 || arcType == 2)
                    && end instanceof PlaceFigure) {
                    // doubleArcDestination = Place the double arc "ends" at.
                    String doubleArcDestination = name(unvirtualize((PlaceFigure) end));
                    if (result.get(doubleArcDestination) == null) { // Check for multiple arcs consuming marks from this place.
                        result.put(doubleArcDestination, parseArcInscription(arc));
                    } else {
                        result.put(
                            doubleArcDestination,
                            parseArcInscription(arc) + result.get(doubleArcDestination));
                    }
                }
            }
        }
        if (_logger.isDebugEnabled()) {
            _logger.debug("[Lola Export] Preset of " + name(trans) + " is: " + result.toString());
        }
        return result;
    }

    /**
     * Converts a virtual place figure to its semantic figure if virtual, otherwise returns the original figure.
     *
     * @param fig the PlaceFigure to unvirtualize
     * @return the semantic PlaceFigure if input was virtual, otherwise the original figure
     */
    private PlaceFigure unvirtualize(PlaceFigure fig) {
        return (fig instanceof VirtualPlaceFigure) ? (PlaceFigure) fig.getSemanticFigure() : fig;
    }

    /**
     * Parses the inscription on an arc connection to determine its weight/cardinality.
     *
     * @param arc the ArcConnection to parse the inscription from
     * @return Integer representing the weight/cardinality of the arc (default is 1)
     */
    private Integer parseArcInscription(ArcConnection arc) {
        Integer cardinality = 1;
        boolean check = false;
        int weight = 0;
        for (FilteredFigureEnumerator textChilds =
            new FilteredFigureEnumerator(arc.children(), CPNTextFigure::isInscription); textChilds
                .hasMoreElements();) {
            check = true;
            Figure child = textChilds.nextFigure();
            String text = ((TextFigure) child).getText();
            if (_ptnetcompiler) {
                //just take the first found inscription and return the value
                int ret = (text.matches("^[0-9]*$")) ? Integer.parseInt(text) : 1;
                return ret;
            } else {
            }
            String[] split = text.split(";");
            String emptyString = "";
            for (String string : split) {
                if (!(string.equals(emptyString))) {
                    weight++;
                }
            }
            if (_logger.isDebugEnabled()) {
                _logger.debug(
                    "[Lola Export] Parsing arc (" + name((FigureWithID) arc.startFigure()) + ")--"
                        + text + "-->(" + name((FigureWithID) arc.endFigure())
                        + ")  -- Cardinality is " + cardinality);
            }
        }

        if (check) {
            cardinality = weight;
        }

        // TODO take care of arcs with multiple inscriptions
        return cardinality;
    }

    /**
     * Creates a HashMap with the names of the places that are connected with an
     * arc to this Transition trans as the keys. They are either the destination
     * of the arc or it is a double arc/test arc in which case it they should be
     * in the preset and the postset. So the source/destination doesn't matter.
     * (source/destination depends on where you started drawing)
     *
     * @param drawing the Drawing containing the Petri net
     * @param trans   the TransitionFigure to find the postset for
     * @return HashMap with place names as keys and arc weights as values representing the postset
     */
    private HashMap<String, Integer> postset(Drawing drawing, TransitionFigure trans) {
        HashMap<String, Integer> result = new HashMap<String, Integer>();
        for (FigureEnumeration iterator = drawing.figures(); iterator.hasMoreElements();) {
            Figure fig = iterator.nextFigure();
            if (fig instanceof ArcConnection) { // looking for arcs ...
                ArcConnection arc = (ArcConnection) fig;
                Integer arcType = arc.getArcType();
                Figure end = arc.endFigure();
                Figure start = arc.startFigure();

                // ... starting at trans ...
                if (start.equals(trans) && end instanceof PlaceFigure) {
                    // arcDestination = the place the arc ends at.
                    String arcDestination = name(unvirtualize((PlaceFigure) end));
                    if (result.get(arcDestination) == null) {
                        result.put(arcDestination, parseArcInscription(arc));
                    } else {
                        result.put(
                            arcDestination, parseArcInscription(arc) + result.get(arcDestination));
                    }
                } // ... or being double-headed and starting at trans
                else if (end.equals(trans) && (arcType == 0 || arcType == 2)
                    && start instanceof PlaceFigure) {
                    // doubleArcSource = the Place the double arc was drawn from.
                    String doubleArcSource = name(unvirtualize((PlaceFigure) start));
                    if (result.get(doubleArcSource) == null) {
                        result.put(doubleArcSource, parseArcInscription(arc));
                    } else {
                        result.put(
                            doubleArcSource,
                            parseArcInscription(arc) + result.get(doubleArcSource));
                    }
                }
            }
        }
        if (_logger.isDebugEnabled()) {
            _logger.debug("[Lola Export] Postset of " + name(trans) + " is: " + result.toString());
        }
        return result;
    }

    /**
     * Provides a name for given figure. If it has a user defined and unique
     * name, this is used. If some figures share a user defined name, the unique
     * figure id is added. Otherwise (no user defined name) we return the id
     * preceded with "p" or "t" for place, transition respectively.
     *
     * @param figure the FigureWithID to generate a name for
     * @return String a unique name for the figure
     */
    public String name(FigureWithID figure) {
        StringBuffer retString = new StringBuffer();
        Boolean named = false;

        if (figure instanceof PlaceFigure || figure instanceof TransitionFigure) {
            //already named, use this name
            int id = figure.getID();
            if (_usedFigureNames.containsKey(figure)) {
                retString.append(_usedFigureNames.get(figure));
                // we have a name
                named = true;
            } else { // figure yet unnamed, search for name
                FigureEnumeration childs = ((AttributeFigure) figure).children();
                while (childs.hasMoreElements() && named == false) { // loop for child elements...
                    Figure child = childs.nextElement();
                    if (child instanceof CPNTextFigure
                        && ((CPNTextFigure) child).getType() == CPNTextFigure.NAME) { // that are text figures of type name)
                        String figureName = ((CPNTextFigure) child).getText();
                        /*
                         * check for valid name: starts with letter, followed by
                         * letter or digit && is not one of the reserved
                         * keywords
                         */
                        String validName = "^\\p{Alpha}[\\p{Alnum}_-]*[0-9]*\\b";
                        String[] reserved = new String[] {
                            "IF", "RECORD", "END", "SORT", "FUNCTION", "SAFE", "DO", "ARRAY",
                            "STRONG", "WEAK", "FAIR", "ENUMERATE", "CONSTANT", "BOOLEAN", "OF",
                            "BEGIN", "WHILE", "IF", "THEN", "ELSE", "SWITCH", "CASE", "NEXTSTEP",
                            "REPEAT", "FOR", "TO", "ALL", "EXIT", "EXISTS", "RETURN", "TRUE",
                            "FALSE", "MOD", "VAR", "GUARD", "STATE", "PATH", "GENERATOR", "ANALYSE",
                            "PLACE", "TRANSITION", "MARKING", "CONSUME", "PRODUCE", "FORMULA",
                            "EXPATH", "ALLPATH", "ALWAYS", "UNTIL", "EVENTUALLY", "AND", "OR",
                            "NOT" };
                        Boolean notReserved = true;
                        for (String r : reserved) {
                            if (figureName.equals(r)) {
                                notReserved = false;
                                break;
                            }
                        }

                        //String reserved = "^(IF|RECORD|END|SORT|FUNCTION|SAFE|DO|ARRAY|STRONG|WEAK|FAIR|ENUMERATE|CONSTANT|BOOLEAN|OF|BEGIN|WHILE|IF|THEN|ELSE|SWITCH|CASE|NEXTSTEP|REPEAT|FOR|TO|ALL|EXIT|EXISTS|RETURN|TRUE|FALSE|MOD|VAR|GUARD|STATE|PATH|GENERATOR|ANALYSE|PLACE|TRANSITION|MARKING|CONSUME|PRODUCE|FORMULA|EXPATH|ALLPATH|ALWAYS|UNTIL|EVENTUALLY|AND|OR|NOT)$";
                        if (figureName.matches(validName) && notReserved) {
                            if (_logger.isDebugEnabled()) {
                                _logger.debug(
                                    "[Lola Export] Name of figure seems to be okay: " + figureName);
                            }
                            if (_usedFigureNames.containsValue(figureName)) { //name already used by other figure
                                figureName = figureName + id;
                                if (_usedFigureNames.containsValue(figureName)) {
                                    _logger
                                        .error("Exception: Name " + figureName + " already exists");
                                }
                            }
                            // remember figure and given name
                            retString.append(figureName);
                            _usedFigureNames.put(figure, figureName);
                            named = true;
                        } else {
                            _logger.error(
                                "[Lola Export] Name " + figureName
                                    + " is not valid syntactically.");
                        }
                    }
                }
            }

            // no user defined name at figure, invent one
            if (named == false) {
                // "p" if place, "t" if transition
                retString.append(figure instanceof PlaceFigure ? "p" : "t");
                retString.append(id);
                // if *accidentally* the invented name is already in use ...
                if (_usedFigureNames.containsValue("p" + id)
                    || _usedFigureNames.containsValue("t" + id)) {
                    retString.append("Exception42");
                }
            }
        }

        // TODO restrict method to only Transition- and PlaceFigures, so far other FiguresWithID return ""
        return retString.toString();
    }

    /**
     * For a figure get the corresponding unique name in the LoLA net.
     * @param figure the figure to get the name for
     * @return the name of the figure in the LoLA net.
     */
    public String getNameForFigure(Figure figure) {
        return this._usedFigureNames.get(figure);
    }

    /**
     * Uses the private extractMarking method to determine a HashMap of the
     * places (key) and initial marking (value) for the drawing. This is more
     * useful then the Stringbuffer returned by extractMarking
     *
     * @param drawing the CPNDrawing to extract initial marking from
     * @return a HashMap with place names as keys and token counts as values
     */
    public HashMap<String, String> getInitialMarking(CPNDrawing drawing) {
        HashMap<String, String> result = new HashMap<String, String>();

        // cut off MARKING from start
        String marking = extractMarking(drawing).substring(8);
        String[] markingAssignments = marking.split(",");
        try {
            for (String markingAssignment : markingAssignments) {
                String[] split = markingAssignment.split(":");
                String place = split[0].trim();
                String tokens = split[1].trim();
                result.put(place, tokens);
            }
            return result;
        } catch (ArrayIndexOutOfBoundsException e) {
            _logger
                .error("[Lola] an Index out of bounds exception occurred. Results may be screwed.");
            return result;
        }
    }

    /**
     * Gets all tasks out of a drawing and returns them as vector
     *
     * @param drawing the CPNDrawing to extract tasks from
     * @return a Vector of LolaTask objects found in the drawing
     */
    public Vector<LolaTask> parseTasks(CPNDrawing drawing) {
        Vector<LolaTask> result = new Vector<LolaTask>();
        FigureEnumeration figs = drawing.figures();
        while (figs.hasMoreElements()) {
            Figure fig = figs.nextElement();
            if (fig instanceof TextFigure) {
                TextFigure textFig = (TextFigure) fig;
                String text = textFig.getText();
                if (text.toLowerCase().startsWith("formula")) {
                    LolaTask tmpTask = new LolaTask((TextFigure) fig, drawing);
                    tmpTask.resetColor();
                    result.add(tmpTask);
                }
            }
        }
        drawing.checkDamage();
        return result;
    }
}