//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package de.ugoe.cs.autoquest.plugin.jfc;

import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.eventcore.gui.IInteraction;
import de.ugoe.cs.autoquest.eventcore.gui.KeyPressed;
import de.ugoe.cs.autoquest.eventcore.gui.KeyReleased;
import de.ugoe.cs.autoquest.eventcore.gui.KeyboardFocusChange;
import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonDown;
import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonInteraction;
import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonUp;
import de.ugoe.cs.autoquest.eventcore.gui.MouseClick;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIElementTree;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModelException;
import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement;
import de.ugoe.cs.autoquest.keyboardmaps.VirtualKey;
import de.ugoe.cs.autoquest.plugin.jfc.eventcore.JFCEventId;
import de.ugoe.cs.autoquest.plugin.jfc.guimodel.JFCGUIElement;
import de.ugoe.cs.autoquest.plugin.jfc.guimodel.JFCGUIElementSpec;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * This class provides functionality to parse XML log files generated by the JFCMonitor of
 * autoquest. The result of parsing a file is a collection of event sequences.
 * </p>
 * 
 * @author Fabian Glaser
 * @author Steffen Herbold
 * @version 1.0
 */
public class JFCSimplifiedLogParser extends DefaultHandler {

    /**
     * <p>
     * Map that holds events that had no registered target GUI element during parsing. Keys are the
     * IDs of the unregistered targets.
     * </p>
     */
    private Map<Long, List<Event>> eventsWithoutTargets;

    /**
     * <p>
     * Collection of event sequences that is contained in the log file, which is parsed.
     * </p>
     */
    private Collection<List<Event>> sequences;

    /**
     * <p>
     * Internal handle to the id of the event that is currently being parsed.
     * </p>
     */
    private JFCEventId currentEventId;

    /**
     * 
     * <p>
     * Internal handle to the hashcode of the GUI element, that is currently parsed.
     * </p>
     */
    private Long currentGUIElementHash;

    /**
     * 
     * <p>
     * Internal handle to the hashcode of the parent of the GUI element, that is currently parsed.
     * </p>
     */
    private Long currentParentHash;

    /**
     * <p>
     * Internal handle to the source of the event that is currently being parsed.
     * </p>
     */
    private Long currentEventSource;

    /**
     * <p>
     * Internal handle to the timestamp of the event that is currently being parsed.
     */
    private Long currentEventTimestamp = -1l;

    /**
     * <p>
     * Internal handle to the parameters of the event currently being parsed.
     * </p>
     */
    private Map<String, String> currentEventParameters;

    /**
     * <p>
     * Internal handle to the event sequence that is currently being parsed.
     * </p>
     */
    private List<Event> currentSequence;

    /**
     * <p>
     * internal handle to the specification currently parsed for a GUI element
     * </p>
     */
    private JFCGUIElementSpec currentGuiElementSpec;

    /**
     * <p>
     * internal handle to the parsed GUI structure, stored in a GUIElementTree
     * </p>
     */
    private GUIElementTree<Long> currentGUIElementTree;

    /**
     * <p>
     * internal handle to the class ancestors
     * </p>
     */
    private List<String> currentTypeHierarchy;

    /**
     * <p>
     * this is used to check, if for every pressed key, there is a release of it
     * </p>
     */
    private List<VirtualKey> mPressedKeys = new ArrayList<VirtualKey>();

    /**
     * <p>
     * a specification for event ids to be omitted by the parser
     * </p>
     */
    private Collection<JFCEventId> eventFilter;

    /**
     * <p>
     * Constructor. Creates a new JFCLogParser with a default event filter. This ignores focus
     * events, mouse pressed, and mouse released events.
     * </p>
     */
    public JFCSimplifiedLogParser() {
        sequences = new LinkedList<List<Event>>();
        currentSequence = null;
        eventsWithoutTargets = new HashMap<Long, List<Event>>();
        // setupDefaultEventFilter();
    }

    /**
     * <p>
     * Constructor. Creates a new JFCLogParser with a specific event filter. The events in the
     * provided collection are ignored by the parser. As events, the constants of the different
     * event classes must be used. E.g. creating a collection and putting
     * <code>MouseEvent.MOUSE_PRESSED</code> will cause the parser to ignore all mouse pressed
     * events. If the provided collection is null, no event is ignored.
     * </p>
     * 
     * @param ignoredEvents
     *            the events to be ignored by the parser, can be null
     */
    public JFCSimplifiedLogParser(Collection<JFCEventId> ignoredEvents) {
        sequences = new LinkedList<List<Event>>();
        currentSequence = null;
        eventsWithoutTargets = new HashMap<Long, List<Event>>();
        eventFilter = ignoredEvents;
    }

    /**
     * <p>
     * Parses a log file written by the JFCMonitor and creates a collection of event sequences.
     * </p>
     * 
     * @param filename
     *            name and path of the log file
     */
    public void parseFile(String filename) {
        if (filename == null) {
            throw new IllegalArgumentException("filename must not be null");
        }

        parseFile(new File(filename));
    }

    /**
     * <p>
     * Parses a log file written by the JFCMonitor and creates a collection of event sequences.
     * </p>
     * 
     * @param file
     *            name and path of the log file
     */
    public void parseFile(File file) {
        if (file == null) {
            throw new IllegalArgumentException("file must not be null");
        }

        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setValidating(true);

        SAXParser saxParser = null;
        InputSource inputSource = null;
        try {
            saxParser = spf.newSAXParser();
            inputSource =
                new InputSource(new InputStreamReader(new FileInputStream(file), "UTF-8"));
        }
        catch (UnsupportedEncodingException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return;
        }
        catch (ParserConfigurationException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return;
        }
        catch (SAXException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return;
        }
        catch (FileNotFoundException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return;
        }
        if (inputSource != null) {
            inputSource.setSystemId("file://" + file.getAbsolutePath());
            try {
                if (saxParser == null) {
                    throw new RuntimeException("SAXParser creation failed");
                }
                saxParser.parse(inputSource, this);
            }
            catch (SAXParseException e) {
                Console.printerrln("Failure parsing file in line " + e.getLineNumber() +
                    ", column " + e.getColumnNumber() + ".");
                Console.logException(e);
                return;
            }
            catch (SAXException e) {
                Console.printerr("Error parsing file + " + file.getName());
                Console.logException(e);
                return;
            }
            catch (IOException e) {
                Console.printerr("Error parsing file + " + file.getName());
                Console.logException(e);
                return;
            }
        }
        if (!eventsWithoutTargets.isEmpty()) {
            Console.printerr("Some events reference GUI elements that are not part of logfile. " +
            		"These events have been parsed without target.");
        }
    }

    /**
     * <p>
     * Returns the collection of event sequences that is obtained from parsing log files.
     * </p>
     * 
     * @return collection of event sequences
     */
    public Collection<List<Event>> getSequences() {
        return sequences;
    }

    /**
     * <p>
     * Returns the GUI model that is obtained from parsing log files.
     * </p>
     * 
     * @return GUIModel
     */
    public GUIModel getGuiModel() {
        return currentGUIElementTree.getGUIModel();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String,
     * java.lang.String, org.xml.sax.Attributes)
     */
    public void startElement(String uri, String localName, String qName, Attributes atts)
        throws SAXException
    {
        if (qName.equals("sessions")) {
            currentSequence = new LinkedList<Event>();
            if (currentGUIElementTree == null)
                currentGUIElementTree = new GUIElementTree<Long>();
        }
        if (qName.equals("newsession")) {
            Console.traceln(Level.FINE, "start of session");
            if (currentSequence != null && !currentSequence.isEmpty()) {
                // create a copy of the list just to have a correctly typed one.
                sequences.add(currentSequence.subList(0, currentSequence.size() - 1));
            }
            currentSequence = new LinkedList<Event>();
        }
        else if (qName.equals("component")) {
            currentGUIElementHash = Long.parseLong(atts.getValue("hash"), 16);
            currentGuiElementSpec = new JFCGUIElementSpec();
            currentGuiElementSpec.setElementHash((int) currentGUIElementHash.longValue());
        }
        else if (qName.equals("event")) {
            JFCEventId eventId = JFCEventId.parseEventId(atts.getValue("id"));
            if ((eventFilter == null) || (!eventFilter.contains(eventId))) {
                currentEventId = eventId;
                currentEventParameters = new HashMap<String, String>();
            }
        }
        else if (qName.equals("componentNameChange")) {
            long sourceHash = Long.parseLong(atts.getValue("hash"), 16);
            String newName = atts.getValue("newName");
            // int titleSource = Integer.parseInt(atts.getValue("titleSource"));
            JFCGUIElement sourceElement = (JFCGUIElement) currentGUIElementTree.find(sourceHash);
            JFCGUIElementSpec sourceSpec = (JFCGUIElementSpec) sourceElement.getSpecification();
            sourceSpec.setName(newName);
        }
        else if (qName.equals("param")) {
            if (currentEventId != null) {
                if ("source".equals(atts.getValue("name"))) {
                    currentEventSource = Long.parseLong(atts.getValue("value"), 16);
                }
                if ("timestamp".equals(atts.getValue("name"))) {
                    currentEventTimestamp = Long.parseLong(atts.getValue("value"));
                }
                currentEventParameters.put(atts.getValue("name"), atts.getValue("value"));
            }
            else if (currentGUIElementHash != null) {
                if ("title".equals(atts.getValue("name"))) {
                    currentGuiElementSpec.setName(atts.getValue("value"));
                }
                else if ("class".equals(atts.getValue("name"))) {
                    currentGuiElementSpec.setType(atts.getValue("value"));
                }
                else if ("icon".equals(atts.getValue("name"))) {
                    currentGuiElementSpec.setIcon(atts.getValue("value"));
                }
                else if ("index".equals(atts.getValue("name"))) {
                    currentGuiElementSpec.setIndex(Integer.parseInt(atts.getValue("value")));
                }
                else if ("parent".equals(atts.getValue("name"))) {
                    currentParentHash = Long.parseLong(atts.getValue("value"), 16);
                }
            }
        }
        else if (qName.equals("ancestor")) {
            currentTypeHierarchy.add(atts.getValue("name"));
        }
        else if (qName.equals("ancestors")) {
            currentTypeHierarchy = new LinkedList<String>();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String,
     * java.lang.String)
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if (qName.equals("sessions")) {
            if (currentSequence != null && !currentSequence.isEmpty()) {
                sequences.add(currentSequence);
            }
            currentSequence = null;
        }
        else if (qName.equals("ancestors")) {
            currentGuiElementSpec.setTypeHierarchy(currentTypeHierarchy);
        }
        else if (qName.equals("component") && currentGUIElementHash != null) {
            try {
                currentGUIElementTree.add(currentGUIElementHash, currentParentHash,
                                          currentGuiElementSpec);
            }
            catch (GUIModelException e) {
                throw new SAXException("could not handle GUI element with hash " +
                                       currentGUIElementHash + ": " + e.getMessage(), e);
            }
            List<Event> unhandledEvents = eventsWithoutTargets.get(currentGUIElementHash);
            if (unhandledEvents != null) {
                JFCGUIElement guiElement =
                    (JFCGUIElement) currentGUIElementTree.find(currentGUIElementHash);
                for (Event event : unhandledEvents) {
                    event.setTarget(guiElement);
                    guiElement.markUsed();
                }
                eventsWithoutTargets.remove(currentGUIElementHash);
            }
            currentGUIElementHash = null;
            currentParentHash = null;
            currentTypeHierarchy = null;
        }
        else if (currentEventId != null) {
            if (qName.equals("event")) {

                IGUIElement currentGUIElement;
                currentGUIElement = currentGUIElementTree.find(currentEventSource);
                Event event;
                // in some rare cases the target GUI element of the event is not
                // known yet
                if (currentGUIElement == null) {
                    event = new Event(instantiateInteraction(currentEventId, currentEventParameters));
                    List<Event> eventList = eventsWithoutTargets.get(currentEventSource);
                    if (eventList == null) {
                        eventList = new ArrayList<Event>();
                        eventsWithoutTargets.put(currentEventSource, eventList);
                    }
                    eventList.add(event);
                }
                else {
                    event = new Event(instantiateInteraction(currentEventId, currentEventParameters),
                                  currentGUIElement);
                    JFCGUIElement currentEventTarget = (JFCGUIElement) event.getTarget();
                    currentEventTarget.markUsed();
                }
                event.setTimestamp(currentEventTimestamp);
                currentSequence.add(event);

                currentEventParameters = null;
                currentEventId = null;
                currentEventTimestamp = -1l;
            }
        }
    }

    /**
     * <p>
     * depending on the event id and the event parameters, this method instantiates the concrete
     * interaction, that took place, i.e. the event type
     * </p>
     * 
     * @param eventId
     *            the id of the event
     * @param eventParameters
     *            the parameters provided for the event
     * 
     * @return as described
     * 
     * @throws SAXException
     *             thrown if the provided event id is unknown
     */
    private IInteraction instantiateInteraction(JFCEventId eventId,
                                                Map<String, String> eventParameters)
        throws SAXException
    {
        switch (eventId)
        {
            case FOCUS_GAINED:
                return handleNewFocus(eventId, eventParameters);

            case KEY_PRESSED:
            case KEY_RELEASED:
            case KEY_TYPED:
                return handleKeyAction(eventId, eventParameters);

            case MOUSE_CLICKED:
            case MOUSE_PRESSED:
            case MOUSE_RELEASED:
            case MOUSE_MOVED:
            case MOUSE_ENTERED:
            case MOUSE_EXITED:
            case MOUSE_DRAGGED:
            case MOUSE_WHEEL:
                return handleMouseAction(eventId, eventParameters);

            default:
                throw new SAXException("unhandled event id " + eventId);
        }
    }

    /**
     * <p>
     * handles a mouse interaction. The method determines based on the event id and the parameters
     * which mouse button is pressed, released or clicked.
     * </p>
     * 
     * @param eventId
     *            the id of the event
     * @param eventParameters
     *            the parameters provided for the event
     * 
     * @return as described
     * 
     * @throws SAXException
     *             thrown if the provided event id or button index is unknown
     */
    private IInteraction handleMouseAction(JFCEventId eventId, Map<String, String> eventParameters)
        throws SAXException
    {
        MouseButtonInteraction.Button button = null;

        if (eventParameters.get("Button") != null) {
            int buttonId = Integer.parseInt(eventParameters.get("Button"));
            if (buttonId == MouseEvent.BUTTON1) {
                button = MouseButtonInteraction.Button.LEFT;
            }
            else if (buttonId == MouseEvent.BUTTON2) {
                button = MouseButtonInteraction.Button.MIDDLE;
            }
            else if (buttonId == MouseEvent.BUTTON3) {
                button = MouseButtonInteraction.Button.RIGHT;
            }
            else {
                throw new SAXException("unknown mouse button index " + buttonId);
            }
        }

        if (JFCEventId.MOUSE_CLICKED == eventId) {
            int x = Integer.parseInt(eventParameters.get("X"));
            int y = Integer.parseInt(eventParameters.get("Y"));
            return new MouseClick(button, x, y);
        }
        else if (JFCEventId.MOUSE_PRESSED == eventId) {
            int x = Integer.parseInt(eventParameters.get("X"));
            int y = Integer.parseInt(eventParameters.get("Y"));
            return new MouseButtonDown(button, x, y);
        }
        else if (JFCEventId.MOUSE_RELEASED == eventId) {
            int x = Integer.parseInt(eventParameters.get("X"));
            int y = Integer.parseInt(eventParameters.get("Y"));
            return new MouseButtonUp(button, x, y);
        }
        else {
            throw new SAXException("unknown event id " + eventId);
        }
    }

    /**
     * <p>
     * handles a keyboard interaction. The method determines based on the event id and the
     * parameters which key on the keyboard is pressed or released. It further checks, if for every
     * released key there is also a pressed event
     * </p>
     * 
     * @param eventId
     *            the id of the event
     * @param eventParameters
     *            the parameters provided for the event
     * 
     * @return as described
     * 
     * @throws SAXException
     *             thrown if the provided event id is unknown or if there is a key release without a
     *             preceding press of the same key
     */
    private IInteraction handleKeyAction(JFCEventId eventId, Map<String, String> eventParameters)
        throws SAXException
    {
        // TODO handle shortcuts
        if (JFCEventId.KEY_PRESSED == eventId) {
            VirtualKey key = VirtualKey.parseVirtualKey(eventParameters.get("KeyCode"));
            mPressedKeys.add(key);

            return new KeyPressed(key);
        }
        else if (JFCEventId.KEY_RELEASED == eventId) {
            VirtualKey key = VirtualKey.parseVirtualKey(eventParameters.get("KeyCode"));
            if (mPressedKeys.contains(key)) {
                mPressedKeys.remove(key);
            }
            else {
                Console.traceln(Level.SEVERE,
                                "log file has an error, as it contains a key up event on key " +
                                    key + " for which there is no preceeding key down event");
            }

            return new KeyReleased(key);
        }

        throw new SAXException("unknown event id " + eventId);
    }

    /**
     * <p>
     * handles explicit keyboard focus changes.
     * </p>
     * 
     * @param eventId
     *            the id of the event
     * @param eventParameters
     *            the parameters provided for the event
     * 
     * @return as described
     * 
     * @throws SAXException
     *             thrown if the provided event id is unknown
     */
    private IInteraction handleNewFocus(JFCEventId eventId, Map<String, String> eventParameters)
        throws SAXException
    {
        if (JFCEventId.FOCUS_GAINED == eventId) {
            return new KeyboardFocusChange();
        }
        else {
            throw new SAXException("unknown event id " + eventId);
        }
    }

    /**
     * <p>
     * creates a default event filter that ignores focus changes, mouse pressed and mouse released
     * events.
     * </p>
     */
    /*
     * private void setupDefaultEventFilter() { eventFilter = new HashSet<JFCEventId>();
     * eventFilter.add(JFCEventId.MOUSE_PRESSED); eventFilter.add(JFCEventId.MOUSE_RELEASED);
     * eventFilter.add(JFCEventId.FOCUS_GAINED); }
     */
}
