package de.ugoe.cs.eventbench.windows;

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.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

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.windows.data.WindowTree;
import de.ugoe.cs.eventbench.windows.data.WindowsEvent;
import de.ugoe.cs.eventbench.windows.data.WindowsMessage;
import de.ugoe.cs.util.StringTools;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * This class provides functionality to parse XML log files generated by the
 * MFCUsageMonitor of EventBench. The result of parsing a file is a collection
 * of event sequences. It uses the {@link SequenceSplitter} and the
 * {@link EventGenerator} as well as custom defined {@link MessageHandler} for
 * the parsing.
 * </p>
 * 
 * @author Steffen Herbold
 * @version 1.0
 */
public class LogParser extends DefaultHandler {

	/**
	 * <p>
	 * If a custom message handler is used, this field contains its handle.
	 * Otherwise this field is {@code null}.
	 * </p>
	 */
	private MessageHandler currentHandler;

	/**
	 * <p>
	 * Handle to the message that is currently parsed.
	 * </p>
	 */
	private WindowsMessage currentMessage;

	/**
	 * <p>
	 * {@link SequenceSplitter} instance used by the {@link LogParser}.
	 * </p>
	 */
	private SequenceSplitter sequenceSplitter;

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

	/**
	 * <p>
	 * Debugging variable that allows the analysis which message type occurs how
	 * often in the log file. Can be used to enhance the message filter.
	 * </p>
	 */
	private SortedMap<Integer, Integer> typeCounter;

	/**
	 * <p>
	 * Debugging variable that enables the counting of the occurrences of each
	 * message. Used in combination with {@link #typeCounter}.
	 * </p>
	 */
	private boolean countMessageOccurences;

	/**
	 * <p>
	 * Constructor. Creates a new LogParser that does not count message
	 * occurrences.
	 * </p>
	 */
	public LogParser() {
		this(false);
	}

	/**
	 * <p>
	 * Constructor. Creates a new LogParser.
	 * </p>
	 * 
	 * @param countMessageOccurences
	 *            if true, the occurrences of each message type in the log is
	 *            counted.
	 */
	public LogParser(boolean countMessageOccurences) {
		sequenceSplitter = new SequenceSplitter();
		sequences = new LinkedList<List<WindowsEvent>>();
		currentHandler = null;
		this.countMessageOccurences = countMessageOccurences;
		if (countMessageOccurences) {
			typeCounter = new TreeMap<Integer, Integer>();
		}

	}

	/**
	 * <p>
	 * Returns the collection of event sequences that is obtained from parsing
	 * log files.
	 * </p>
	 * 
	 * @return collection of event sequences
	 */
	public Collection<List<WindowsEvent>> 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)
	 */
	@Override
	public void startElement(String uri, String localName, String qName,
			Attributes atts) throws SAXException {
		if (qName.equals("session")) {
			Console.traceln("start of session");
			sequenceSplitter = new SequenceSplitter();
		} else if (qName.equals("msg")) {
			String msgType = atts.getValue("type");
			int msgInt = -1;
			try {
				msgInt = Integer.parseInt(msgType);

				if (countMessageOccurences) {
					Integer currentCount = typeCounter.get(msgInt);
					if (currentCount == null) {
						typeCounter.put(msgInt, 1);
					} else {
						typeCounter.put(msgInt, currentCount + 1);
					}
				}

				if (msgInt == MessageDefs.WM_CREATE) {
					currentHandler = new HandlerCreate();
					currentHandler.onStartElement();
				} else if (msgInt == MessageDefs.WM_DESTROY) {
					currentHandler = new HandlerDestroy();
					currentHandler.onStartElement();
				} else if (msgInt == MessageDefs.WM_SETTEXT) {
					currentHandler = new HandlerSetText();
					currentHandler.onStartElement();
				} else {
					currentMessage = new WindowsMessage(msgInt);
				}
			} catch (NumberFormatException e) {
				Console.printerrln("Invalid message type: type not a number");
				e.printStackTrace();
			}
		} else if (qName.equals("param")) {
			if (currentHandler != null) {
				currentHandler.onParameter(atts.getValue("name"),
						atts.getValue("value"));
			} else {
				currentMessage.addParameter(atts.getValue("name"),
						atts.getValue("value"));
			}
		}
	}

	/*
	 * (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("msg")) {
			if (currentHandler != null) {
				currentHandler.onEndElement();
				currentHandler = null;
			} else {
				try {
					currentMessage.setTarget(WindowTree.getInstance());
					sequenceSplitter.addMessage(currentMessage);
				} catch (InvalidParameterException e) {
					Console.traceln(e.getMessage() + " WindowsMessage "
							+ currentMessage + " ignored.");
				}
			}
		} else if (qName.equals("session")) {
			sequenceSplitter.endSession();
			sequences.add(sequenceSplitter.getSequence());
			Console.traceln("end of session");
		}
	}

	/**
	 * <p>
	 * Parses a given log file and adds its contents to the 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-16"));
		} 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();
			}
		}
		if (countMessageOccurences) {
			Console.println("Message statistics:");
			Console.println(typeCounter.toString()
					.replace(" ", StringTools.ENDLINE)
					.replaceAll("[\\{\\}]", ""));
		}
	}
}
