package de.ugoe.cs.eventbench.jfc;

import java.awt.event.FocusEvent;
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.security.InvalidParameterException;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

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.eventbench.jfc.data.JFCEvent;
import de.ugoe.cs.util.console.Console;

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

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

	/**
	 * <p>
	 * Internal handle to the event that is currently being parsed.
	 * </p>
	 */
	private JFCEvent currentEvent;

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

	/**
	 * <p>
	 * Internally used string to build a string representing a component-node.
	 * </p>
	 */
	private String componentString;

	/**
	 * <p>
	 * Enumeration to differentiate if a parameter belongs to an event, a source
	 * or the parent of a source.
	 * </p>
	 * 
	 * @author Steffen Herbold
	 * @version 1.0
	 */
	private enum ParamSource {
		EVENT, SOURCE, PARENT, COMPONENT
	};

	/**
	 * <p>
	 * Specifies whether the parameters that are currently being read belong the
	 * the event, the source or the parent.
	 * </p>
	 */
	ParamSource paramSource = null;

	private Collection<Integer> eventFilter;

	/**
	 * <p>
	 * Constructor. Creates a new JFCLogParser with a default event filter.
	 * This ignores focus events, mouse pressed, and mouse released events.
	 * </p>
	 */
	public JFCLogParser() {
		sequences = new LinkedList<List<JFCEvent>>();
		currentSequence = new LinkedList<JFCEvent>();
		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 JFCLogParser(Collection<Integer> ignoredEvents) {
		sequences = new LinkedList<List<JFCEvent>>();
		currentSequence = new LinkedList<JFCEvent>();
		eventFilter = ignoredEvents;
	}

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

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

	/*
	 * (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<JFCEvent>();
		}
		if (qName.equals("newsession")) {
			Console.traceln("start of session");
			if (currentSequence != null && !currentSequence.isEmpty()) {
				sequences.add(currentSequence);
			}
			currentSequence = new LinkedList<JFCEvent>();
		} else if (qName.equals("event")) {
			int eventId = Integer.parseInt(atts.getValue("id"));
			if ((eventFilter == null) || (!eventFilter.contains(eventId))) {
				currentEvent = new JFCEvent(atts.getValue("id"));
				paramSource = ParamSource.EVENT;
			}
		} else if (currentEvent != null) {
			if (qName.equals("param")) {
				if (paramSource == ParamSource.EVENT) {
					currentEvent.addParameter(atts.getValue("name"),
							atts.getValue("value"));
				} else if (paramSource == ParamSource.SOURCE) {
					currentEvent.addSourceInformation(atts.getValue("name"),
							atts.getValue("value"));
				} else if (paramSource == ParamSource.PARENT) {
					currentEvent.addParentInformation(atts.getValue("name"),
							atts.getValue("value"));
				} else if (paramSource == ParamSource.COMPONENT) {
					componentString += "'" + atts.getValue("value") + "',";
				}
			} else if (qName.equals("source")) {
				paramSource = ParamSource.SOURCE;
			} else if (qName.equals("parent")) {
				paramSource = ParamSource.PARENT;
			} else if (qName.equals("component")) {
				paramSource = ParamSource.COMPONENT;
				componentString = "[";
			}
		}
	}

	/*
	 * (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 (currentEvent != null) {
			if (qName.equals("event")) {
				currentSequence.add(currentEvent);
				currentEvent = null;
			} else if (qName.equals("source")) {
				paramSource = ParamSource.EVENT;
			} else if (qName.equals("parent")) {
				paramSource = ParamSource.SOURCE;
			} else if (qName.equals("component")) {
				paramSource.equals(ParamSource.SOURCE);
				currentEvent.extendTarget(componentString.substring(0,
						componentString.length() - 1) + "]");
			}
		}
	}

	/**
	 * <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 InvalidParameterException("filename 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(filename), "UTF-8"));
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		if (inputSource != null) {
			inputSource.setSystemId("file://"
					+ new File(filename).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()
						+ ".");
				e.printStackTrace();
			} catch (SAXException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

}
