package de.renew.lola2.parser;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Scanner;

import CH.ifa.draw.figures.AttributeFigure;
import de.renew.gui.ArcConnection;
import de.renew.gui.CPNDrawing;
import de.renew.gui.CPNDrawingHelper;
import de.renew.gui.PlaceFigure;
import de.renew.gui.TransitionFigure;

/**
 * This class parses Lola net files and creates a CPNDrawing
 */
public class LolaParser {
    private static org.apache.log4j.Logger _logger =
        org.apache.log4j.Logger.getLogger(LolaParser.class);
    private List<Transition> _transitions = new ArrayList<Transition>();
    private List<Place> _places = new ArrayList<Place>();
    private Map<String, Node> _elementsMap = new HashMap<String, Node>();
    private Map<Node, AttributeFigure> _figureMap = new HashMap<Node, AttributeFigure>();

    /**
     * instead of using ShadowArc.ordinary
     */
    private final int _ordinaryArc = 1;

    /**
     * Here the parsing takes place, during the process it fills the
     * instance variables.
     * @param stream the input stream containing the Lola net file format
     * @throws NetFileParseError if parsing fails due to invalid format
     */
    public void parse(InputStream stream) throws NetFileParseError {
        _logger.info("[Lola] Lola Import: Starting parser.");
        Scanner scanner = new Scanner(new BufferedReader(new InputStreamReader(stream)));
        scanner.useDelimiter(";");
        try {
            scanner.findWithinHorizon("PLACE", 0);
            _places = parsePlaces(scanner.next());
            for (Place p : _places) {
                _elementsMap.put(p.getName(), p);
            }
            scanner.findWithinHorizon("MARKING", 0);
            Map<Place, Integer> markings = parseMarking(scanner.next());
            for (Place p : markings.keySet()) {
                p.setInitialMarking(markings.get(p));
            }
            scanner.useDelimiter(";?\\s*TRANSITION");
            while (scanner.hasNext()) {
                Transition t = parseTransition(scanner.next());
                _transitions.add(t);
                _elementsMap.put(t.getName(), t);
            }
        } catch (NetFileParseError | NoSuchElementException e) {
            _logger.error("[Lola] An error occurred while trying to parse the net.");
        }
    }

    /**
     * Parses a single transition definition from the Lola format.
     * @param transition the string containing the transition definition
     * @return the parsed Transition object
     * @throws NetFileParseError if the transition format is invalid
     */
    public Transition parseTransition(String transition) throws NetFileParseError {
        if (transition.trim().endsWith(";")) {
            transition = transition.substring(0, transition.trim().length());
        }
        if (_logger.isDebugEnabled()) {
            _logger.debug("Now parsing the transition\n" + transition);
        }
        Scanner transitionScanner = new Scanner(transition);
        String nameLine = transitionScanner.next();
        if (_logger.isDebugEnabled()) {
            _logger.debug(nameLine);
        }
        String[] nameAndCoordinates = nameLine.split("\\{");
        String transitionName = nameAndCoordinates[0].trim();
        Transition result = new Transition(transitionName);
        if (nameLine.contains("{") && nameAndCoordinates.length > 1) {
            int[] coords = parseCoordinates(nameAndCoordinates[1].trim());
            if (coords != null) {
                result.setX(coords[0]);
                result.setY(coords[1]);
            }
        }
        if (transitionScanner.next().equals("CONSUME")) {
            transitionScanner.useDelimiter(";\\s*PRODUCE");
            String consumeBlock = transitionScanner.next();
            if (consumeBlock != null) {
                Map<Place, Integer> pre = parseMarking(consumeBlock);
                result.setPre(pre);
            }
        } else {
            throw (new NetFileParseError("Missing CONSUME keyword"));
        }
        if (transitionScanner.hasNext()) {
            Map<Place, Integer> post = parseMarking(transitionScanner.next());
            result.setPost(post);
        } else {
            throw (new NetFileParseError("Missing PRODUCE keyword"));
        }
        return result;
    }

    /**
     * Parses a marking, in the context of Lola this is a comma-separated
     * list of place names followed by a colon (:) and a number of tokens.
     * Markings occur in the MARKINGS section of a net file and also in the
     * CONSUME and PRODUCE blocks of transitions.
     * The returned map may be empty (if a transition has no pre- or post-set).
     * @param marking - a String containing the marking definition
     * @return map of {@link Place}s to Integers representing token counts
     * @throws NetFileParseError if the marking format is invalid
     */
    public Map<Place, Integer> parseMarking(String marking) throws NetFileParseError {
        if (_logger.isDebugEnabled()) {
            _logger.debug("Now parse the marking");
        }
        Scanner markingScanner = new Scanner(marking);
        markingScanner.useDelimiter(",");
        Map<Place, Integer> markings = new HashMap<Place, Integer>();
        while (markingScanner.hasNext()) {
            try {
                Marking found = parseToken(markingScanner.next());
                if (found != null) {
                    Place place = (Place) forName(found.getName());
                    markings.put(place, found.getTokens());
                }
            } catch (NetFileParseError e) {
                e.printStackTrace();
                throw (e);
            }
        }
        return markings;
    }

    /**
     * Gets the node for a name.
     * @param name name the name of the node to retrieve
     * @return the node (place or transition) named 'name'
     */
    public Node forName(String name) {
        return _elementsMap.get(name);
    }

    /**
     * Parses a single token on a place and returns a {@link Marking}
     * or {@code null} if the token is an empty String.
     * @param token a String token in the format "placeName:tokenCount"
     * @throws NetFileParseError if the token format is invalid
     * @return a Marking object representing the place and its token count
     */
    public static Marking parseToken(String token) throws NetFileParseError {
        if (token != null && !token.trim().isEmpty()) {
            String[] placeTokens = token.split(":");
            if (placeTokens.length > 1) {
                if (_logger.isDebugEnabled()) {
                    _logger.debug(placeTokens[0] + " has " + placeTokens[1] + " tokens.");
                }
                try {
                    Integer tokenCount = Integer.parseInt(placeTokens[1].trim());
                    Marking marking = new Marking(placeTokens[0].trim(), tokenCount);
                    return marking;
                } catch (NumberFormatException e) {
                    throw (new NetFileParseError("Could not parse token number"));
                }
            } else {
                throw (new NetFileParseError(
                    "Place without marking not allowed in MARKING section"));
            }
        } else {
            // the token is an empty string, return null
            return null;
        }
    }

    /**
     * Parses a comma-separated list of places
     * @param places places a String containing comma-separated place definitions
     * @return a List of parsed Place objects
     * @throws NetFileParseError if any place definition is invalid
     */
    public static List<Place> parsePlaces(String places) throws NetFileParseError {
        if (_logger.isDebugEnabled()) {
            _logger.debug("First parse the places");
        }
        Scanner placeScanner = new Scanner(places);
        placeScanner.useDelimiter(",");
        List<Place> result = new ArrayList<Place>();
        while (placeScanner.hasNext()) {
            result.add(parsePlace(placeScanner.next()));
        }
        return result;
    }


    /**
     * Parses a single place with optional location comment
     * @param place a String containing the place definition with optional coordinates
     * @return the parsed Place object
     * @throws NetFileParseError if the place format is invalid
     */
    public static Place parsePlace(String place) throws NetFileParseError {
        if (_logger.isDebugEnabled()) {
            _logger.debug(place);
        }
        Scanner placeScanner = new Scanner(place);
        placeScanner.useDelimiter("\\{");
        String placeName = placeScanner.next().trim();
        Place result = new Place(placeName);
        if (placeScanner.hasNext()) {
            int[] coords = parseCoordinates(placeScanner.next());
            if (coords != null) {
                result.setX(coords[0]);
                result.setY(coords[1]);
            }
        }
        return result;
    }

    /**
     * Parses coordinate information from a coordinate string.
     * @param coordinates a String containing coordinate information in format "x:value y:value}"
     * @return an array containing [x, y] coordinates
     * @throws NetFileParseError if the coordinate format is invalid
     */
    private static int[] parseCoordinates(String coordinates) throws NetFileParseError {
        if (_logger.isDebugEnabled()) {
            _logger.debug(coordinates);
        }
        if (coordinates.startsWith("x") && coordinates.contains("y")) {
            int[] coordArray = new int[2];
            try {
                String xs =
                    coordinates.substring(coordinates.indexOf(":") + 1, coordinates.indexOf("y"));
                String ys = coordinates
                    .substring(coordinates.lastIndexOf(":") + 1, coordinates.indexOf("}"));
                coordArray[0] = Integer.parseInt(xs);
                coordArray[1] = Integer.parseInt(ys);
                if (_logger.isDebugEnabled()) {
                    _logger.debug("Coordinates: (x=" + xs + "|y=" + ys + ")");
                }
            } catch (NumberFormatException e) {
                throw (new NetFileParseError("Coordinates could not be parsed"));
            }
            return coordArray;
        } else {
            throw (new NetFileParseError("Comment does not contain coordinates"));
        }
    }

    /**
     * Imports places, transitions, arcs and their names from a stream which
     * needs to be in Lolas net file format.
     * It returns a CPNDrawing corresponding to the net file.
     *
     * @param stream the input stream containing the Lola net file
     * @return a CPNDrawing object representing the parsed net
     */
    public CPNDrawing importNet(InputStream stream) {
        try {
            parse(stream);
        } catch (NetFileParseError e) {
            _logger.error("[LolaParser] : " + e.getMessage());
            return new CPNDrawing();
        }
        CPNDrawing drawing = new CPNDrawing();
        CPNDrawingHelper drawer = new CPNDrawingHelper();

        // draw places
        if (_logger.isDebugEnabled()) {
            _logger.debug("Drawing places");
        }
        for (Place p : _places) {
            if (_logger.isDebugEnabled()) {
                _logger.debug("Drawing place " + p + " (" + p.getX() + "|" + p.getY());
            }
            PlaceFigure place = drawer.createPlace();
            if (p.hasCoordinates()) {
                place.moveBy(p.getX(), p.getY());
            }
            drawing.add(place);
            drawing.add(drawer.createNameTextFigure(p.getName(), place));
            if (_logger.isDebugEnabled()) {
                _logger.debug("Added and named place " + p);
            }
            _figureMap.put(p, place);
            // add initial marking
            if (p.initiallyMarked()) {
                if (_logger.isDebugEnabled()) {
                    _logger.debug("Adding marking " + p.getInitialMarking());
                }
                for (int j = 0; j < p.getInitialMarking(); j++) {
                    drawing.add(drawer.createInscription("[]", place));
                }
            }
        }

        // draw transitions and arcs
        if (_logger.isDebugEnabled()) {
            _logger.debug("Drawing transitions");
        }
        for (Transition t : _transitions) {
            TransitionFigure transition = drawer.createTransition();
            if (t.hasCoordinates()) {
                transition.moveBy(t.getX(), t.getY());
            }
            drawing.add(transition);
            _figureMap.put(t, transition);
            drawing.add(drawer.createNameTextFigure(t.getName(), transition));
            for (Place p : t.getPre().keySet()) {
                ArcConnection arc =
                    drawer.createArcConnection(figureOf(p), transition, _ordinaryArc);
                drawing.add(arc);
                int tokenpre;
                if ((tokenpre = t.getPre().get(p)) > 1) {
                    drawing.add(drawer.createWeightTextFigure(arc, tokenpre));
                }
            }
            for (Place p : t.getPost().keySet()) {
                ArcConnection arc =
                    drawer.createArcConnection(transition, figureOf(p), _ordinaryArc);
                drawing.add(arc);
                int tokenpost;
                if ((tokenpost = t.getPost().get(p)) > 1) {
                    drawing.add(drawer.createWeightTextFigure(arc, tokenpost));
                }
            }
        }
        return drawing;
    }

    /**
     * Gets the AttributeFigure associated with a given Node.
     * @param p the Node to get the figure for
     * @return the AttributeFigure associated with the Node
     */
    private AttributeFigure figureOf(Node p) {
        return _figureMap.get(p);
    }

    /**
     * Gets the list of parsed transitions.
     * @return the list of Transition objects
     */
    public List<Transition> getTransitions() {
        return _transitions;
    }

    /**
     * Gets the list of parsed places.
     * @return the list of Place objects
     */
    public List<Place> getPlaces() {
        return _places;
    }
}