package de.ugoe.cs.eventbench;

import java.io.IOException;
import java.security.InvalidParameterException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;

import de.ugoe.cs.eventbench.data.Event;
import de.ugoe.cs.eventbench.data.ReplayableEvent;
import de.ugoe.cs.eventbench.data.WindowTree;
import de.ugoe.cs.eventbench.data.WindowTreeNode;
import de.ugoe.cs.eventbench.data.WindowsMessage;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * Translates sequences of windows messages into events that can be used by the
 * Logalyzer core libraries for usage analysis.
 * </p>
 * 
 * @author Steffen Herbold
 * 
 */
public class EventGenerator {

	/**
	 * <p>
	 * Helper method that fetches the document node of an XML file.
	 * </p>
	 * 
	 * @param filename
	 *            name of the XML file
	 * @return the document node
	 */
	private static Document getDocument(String filename) {
		SAXBuilder builder = new SAXBuilder();
		Document doc = null;

		try {
			doc = builder.build(filename);
			rulesNamespace = Namespace.getNamespace("ul:rules");
		} catch (JDOMException e) {
			System.err.println("Invalid rules file.");
			e.printStackTrace();
		} catch (IOException e) {
			System.err.println("Invalid rules file.");
			e.printStackTrace();
		}

		return doc;
	}

	/**
	 * <p>
	 * Name and path of the XML files containing the rules.
	 * </p>
	 */
	private String rulesFile;

	/**
	 * <p>
	 * Iterator used for the current sequence.
	 * </p>
	 */
	private ListIterator<WindowsMessage> sequenceIterator;

	/**
	 * <p>
	 * Token that is currently being generated.
	 * </p>
	 */
	private ReplayableEvent<WindowsMessage> currentToken;

	/**
	 * <p>
	 * Reference to the ul:rules namespace.
	 * </p>
	 */
	private static Namespace rulesNamespace;

	/**
	 * <p>
	 * The name of the rule that is currently being evaluated.
	 * </p>
	 */
	private String currentRuleName;

	/**
	 * <p>
	 * Internal message storage. Used to implement the
	 * <code>{@literal <store>}</code> and <code>{@literal <storeSeq>}</code>
	 * tags.
	 * </p>
	 */
	private Map<String, Object> messageStorage;

	/**
	 * <p>
	 * Creates a new EventGenerator. Sets "rules/rules.xml" as default file for
	 * the rules.
	 * </p>
	 */
	public EventGenerator() {
		rulesFile = "rules/rules.xml";
	}

	/**
	 * <p>
	 * Tries to match the rules to the given sequence to generate an
	 * {@link Event}.
	 * </p>
	 * <p>
	 * The rules are matched the order, in which they are defined in the XML
	 * file. Therefore, the order of the rules in the file defines priorities,
	 * when multiple rules could be matched to the same sequence.
	 * </p>
	 * 
	 * @param sequence
	 *            sequence of message for which an event will be generated
	 * @return event that matches the messages; null, if no rule can be matched
	 */
	@SuppressWarnings("unchecked")
	public Event<WindowsMessage> generateEvent(List<WindowsMessage> sequence) {
		Document rulesDoc = getDocument(rulesFile);
		Element rulesRoot = rulesDoc.getRootElement();

		List<Element> ruleElements = rulesRoot.getChildren("rule",
				rulesNamespace);

		boolean isMatch = false;

		for (int ruleIndex = 0; ruleIndex < ruleElements.size() && !isMatch; ruleIndex++) {
			Element currentRule = ruleElements.get(ruleIndex);
			currentToken = new ReplayableEvent<WindowsMessage>(currentRuleName);
			currentRuleName = currentRule.getAttributeValue("name");
			isMatch = true;
			messageStorage = new HashMap<String, Object>();
			sequenceIterator = sequence.listIterator();
			List<Element> ruleChildrenMsg = currentRule.getChildren("msg",
					rulesNamespace);

			int i = 0;
			while (isMatch && i < ruleChildrenMsg.size()) {
				Element messageElement = ruleChildrenMsg.get(i);
				if ("true".equals(messageElement.getAttributeValue("multiple"))) {
					Element nextMessageElement = null;
					if (i + 1 < ruleChildrenMsg.size()) {
						nextMessageElement = ruleChildrenMsg.get(i + 1);
					}
					try {
						isMatch = matchMultipleMessages(messageElement,
								nextMessageElement);
					} catch (InvalidParameterException e) {
						Console.printerrln(e.getMessage());
					}
				} else {
					try {
						isMatch = matchSingleMessage(messageElement);
					} catch (InvalidParameterException e) {
						Console.printerrln(e.getMessage());
					}
				}
				i++;
			}
			if (isMatch) {
				List<Element> ruleChildren = currentRule.getChildren();
				for (Element genMsgElement : ruleChildren) {
					if (genMsgElement.getName().equals("genMsg")) {
						try {
							generateReplayMessage(genMsgElement);
						} catch (InvalidParameterException e) {
							Console.printerrln(e.getMessage());
							currentToken.invalidateReplay();
						}
					} else if (genMsgElement.getName().equals("genMsgSeq")) {
						try {
							generateReplaySequence(genMsgElement);
							currentToken.invalidateReplay();
						} catch (InvalidParameterException e) {
							Console.printerrln(e.getMessage());
							currentToken.invalidateReplay();
						}
					}
				}
				Element idinfoElement = currentRule.getChild("idinfo",
						rulesNamespace);
				if (idinfoElement != null) {
					// cannot be empty if document is valid
					List<Element> valueElements = idinfoElement.getChildren();
					currentToken.setIdInfo(getTermValue(null,
							valueElements.get(0)));
				}
				Console.traceln(currentRule.getAttributeValue("name")
						+ currentToken.getIdInfo() + " matched");
			} else {
				currentToken = null;
			}
		}
		if (!isMatch) {
			Console.traceln("no match found for sequence: "
					+ sequence.toString());
		}
		return currentToken;
	}

	private boolean createSequenceLParam(
			List<WindowsMessage> generatedMessageSeq, boolean msgsGenerated,
			int constMsgType, Element termElement)
			throws NoSuchElementException {
		Iterator<WindowsMessage> seqIterator = generatedMessageSeq.iterator();
		if (termElement.getName().equals("seqValue")) {
			String obj = termElement.getAttributeValue("seqObj");
			List<WindowsMessage> seqVar = getStoredSeqVariable(obj);
			if (msgsGenerated && seqVar.size() != generatedMessageSeq.size()) {
				throw new InvalidParameterException(
						"Failure generating replay sequence for rule "
								+ currentRuleName
								+ ": One or more of the sequence variables used to generate a sequence have different lenghts.");
			}
			for (WindowsMessage msg : seqVar) {
				WindowsMessage currentSeqMsg = getCurrentSeqMsg(
						generatedMessageSeq, msgsGenerated, constMsgType,
						seqIterator);
				String paramValueStr = msg.getParameter(termElement
						.getAttributeValue("param"));
				int paramValue = 0;
				try {
					paramValue = Integer.parseInt(paramValueStr);
					currentSeqMsg.setLPARAM(paramValue);
				} catch (NumberFormatException e) {
					currentSeqMsg.setLPARAMasWindowDesc(paramValueStr);
				}
			}
			if (seqIterator.hasNext()) {
				// the first seq-var has a different number of elements than the
				// current one
				throw new NoSuchElementException();
			}
			msgsGenerated = true;
		} else { // const value
			int paramValue = Integer.parseInt(getTermValue(null, termElement));
			while (seqIterator.hasNext()) {
				seqIterator.next().setLPARAM(paramValue);
			}
		}
		return msgsGenerated;
	}

	private boolean createSequenceTarget(
			List<WindowsMessage> generatedMessageSeq, boolean msgsGenerated,
			int constMsgType, Element termElement)
			throws NoSuchElementException {
		Iterator<WindowsMessage> seqIterator = generatedMessageSeq.iterator();
		if (termElement.getName().equals("seqValue")) {
			String obj = termElement.getAttributeValue("seqObj");
			List<WindowsMessage> seqVar = getStoredSeqVariable(obj);
			if (msgsGenerated && seqVar.size() != generatedMessageSeq.size()) {
				throw new InvalidParameterException(
						"Failure generating replay sequence for rule "
								+ currentRuleName
								+ ": One or more of the sequence variables used to generate a sequence have different lenghts.");
			}
			for (WindowsMessage msg : seqVar) {
				WindowsMessage currentSeqMsg = getCurrentSeqMsg(
						generatedMessageSeq, msgsGenerated, constMsgType,
						seqIterator);
				String targetString = msg.getParameter(termElement
						.getAttributeValue("param"));
				currentSeqMsg.setXmlWindowDescription(targetString);
			}
			msgsGenerated = true;
		} else { // const value
			throw new AssertionError("target must be a sequence variable!");
			/*
			 * If target would not be a variable, the message-elements could not
			 * yet be created and the whole sequence might be broken. If this is
			 * to be changed, createSequenceLParam and createSequenceWParam need
			 * to be addepted, too.
			 */
		}
		return msgsGenerated;
	}

	private boolean createSequenceWParam(
			List<WindowsMessage> generatedMessageSeq, boolean msgsGenerated,
			int constMsgType, Element termElement)
			throws NoSuchElementException {
		Iterator<WindowsMessage> seqIterator = generatedMessageSeq.iterator();
		if (termElement.getName().equals("seqValue")) {
			String obj = termElement.getAttributeValue("seqObj");
			List<WindowsMessage> seqVar = getStoredSeqVariable(obj);
			if (msgsGenerated && seqVar.size() != generatedMessageSeq.size()) {
				throw new InvalidParameterException(
						"Failure generating replay sequence for rule "
								+ currentRuleName
								+ ": One or more of the sequence variables used to generate a sequence have different lenghts.");
			}
			for (WindowsMessage msg : seqVar) {
				WindowsMessage currentSeqMsg = getCurrentSeqMsg(
						generatedMessageSeq, msgsGenerated, constMsgType,
						seqIterator);
				String paramValueStr = msg.getParameter(termElement
						.getAttributeValue("param"));
				int paramValue = 0;
				try {
					paramValue = Integer.parseInt(paramValueStr);
					currentSeqMsg.setWPARAM(paramValue);
				} catch (NumberFormatException e) {
					currentSeqMsg.setWPARAMasWindowDesc(paramValueStr);
				}
			}
			if (seqIterator.hasNext()) {
				// the first seq-var has a different number of elements than the
				// current one
				throw new NoSuchElementException();
			}
			msgsGenerated = true;
		} else { // const value
			int paramValue = Integer.parseInt(getTermValue(null, termElement));
			while (seqIterator.hasNext()) {
				seqIterator.next().setWPARAM(paramValue);
			}
		}
		return msgsGenerated;
	}

	@SuppressWarnings("unchecked")
	private boolean evalEqualRestrictions(WindowsMessage currentMessage,
			Element messageElement) {
		boolean isMatch = true;
		for (Element childElement : (List<Element>) messageElement.getChildren(
				"equals", rulesNamespace)) {
			List<Element> termElements = childElement.getChildren();
			// the size 2 of termElements is guaranteed by the XML schema
			String value1 = getTermValue(currentMessage, termElements.get(0));
			String value2 = getTermValue(currentMessage, termElements.get(1));
			if (value1 == null || value2 == null) {
				isMatch = false;
			} else {
				isMatch = isMatch && value1.equals(value2);
			}
		}
		for (Element childElement : (List<Element>) messageElement.getChildren(
				"equalsSeq", rulesNamespace)) {
			List<Element> termElements = childElement.getChildren();
			List<String> values1 = getTermValueSeq(currentMessage,
					termElements.get(0));
			List<String> values2 = getTermValueSeq(currentMessage,
					termElements.get(0));
			if (values1 == null || values2 == null) {
				isMatch = false;
			} else {
				isMatch = isMatch && values1.equals(values2);
			}
		}
		return isMatch;
	}

	@SuppressWarnings("unchecked")
	private void generateReplayMessage(Element genMsgElement) {
		List<Element> genMsgChildren = genMsgElement.getChildren();
		WindowsMessage generatedMessage = null;
		if (genMsgChildren.size() == 1) { // replay stored message without
											// change
			String obj = genMsgChildren.get(0).getAttributeValue("obj");
			generatedMessage = getStoredMessageVariable(null, obj);
		} else { // generate message according to the rule
			for (Element genMsgChild : genMsgChildren) {
				Element termElement = (Element) genMsgChild.getChildren()
						.get(0);
				if (genMsgChild.getName().equals("type")) {
					try {
						int msgType = Integer.parseInt(getTermValue(null,
								termElement));
						generatedMessage = new WindowsMessage(msgType);
					} catch (NumberFormatException e) {
						throw new InvalidParameterException(
								"Failure generating replay sequence for rule "
										+ currentRuleName
										+ ": Defined type is not an integer.");
					}
				} else if (genMsgChild.getName().equals("target")) {
					String targetString = getTermValue(null, termElement);
					generatedMessage.setXmlWindowDescription(targetString);
				} else if (genMsgChild.getName().equals("LPARAM")) {
					String paramValueStr = getTermValue(null, termElement);
					int paramValue = 0;
					try {
						paramValue = Integer.parseInt(paramValueStr);
						generatedMessage.setLPARAM(paramValue);
					} catch (NumberFormatException e) {
						generatedMessage.setLPARAMasWindowDesc(paramValueStr);
					}
				} else if (genMsgChild.getName().equals("WPARAM")) {
					String paramValueStr = getTermValue(null, termElement);
					int paramValue = 0;
					try {
						paramValue = Integer.parseInt(paramValueStr);
						generatedMessage.setWPARAM(paramValue);
					} catch (NumberFormatException e) {
						generatedMessage.setWPARAMasWindowDesc(paramValueStr);
					}
				}
			}
		}
		if (generatedMessage != null) {
			int delay = Integer.parseInt(genMsgElement
					.getAttributeValue("delay"));
			generatedMessage.setDelay(delay);
		} else {
			currentToken.invalidateReplay();
		}
		currentToken.addReplayEvent(generatedMessage);
	}

	@SuppressWarnings("unchecked")
	private void generateReplaySequence(Element genMsgElement) {
		List<Element> genMsgSeqChildren = genMsgElement.getChildren();
		List<WindowsMessage> generatedMessageSeq = new LinkedList<WindowsMessage>();
		if (genMsgSeqChildren.size() == 1) {
			String obj = genMsgSeqChildren.get(0).getAttributeValue("seqObj");
			generatedMessageSeq = getStoredSeqVariable(obj);
		} else {
			boolean msgsGenerated = false;
			int constMsgType = 0;
			for (Element genMsgSeqChild : genMsgSeqChildren) {
				Element termElement = (Element) genMsgSeqChild.getChildren()
						.get(0);
				if (genMsgSeqChild.getName().equals("type")) {
					// note: cannot easily be extracted because of mulitple
					// return values
					if (termElement.getName().equals("seqValue")) {
						String obj = termElement.getAttributeValue("seqObj");
						List<WindowsMessage> seqVar = getStoredSeqVariable(obj);
						for (WindowsMessage msg : seqVar) {
							generatedMessageSeq.add(new WindowsMessage(msg
									.getType()));
						}
						msgsGenerated = true;
					} else { // constValue type
						constMsgType = Integer.parseInt(getTermValue(null,
								termElement));
					}
				} else if (genMsgSeqChild.getName().equals("target")) {
					msgsGenerated = createSequenceTarget(generatedMessageSeq,
							msgsGenerated, constMsgType, termElement);
				} else if (genMsgSeqChild.getName().equals("LPARAM")) {
					msgsGenerated = createSequenceLParam(generatedMessageSeq,
							msgsGenerated, constMsgType, termElement);
				} else if (genMsgSeqChild.getName().equals("WPARAM")) {
					msgsGenerated = createSequenceWParam(generatedMessageSeq,
							msgsGenerated, constMsgType, termElement);
				}
			}
		}
		currentToken.addReplaySequence(generatedMessageSeq);
	}

	private WindowsMessage getCurrentSeqMsg(
			List<WindowsMessage> generatedMessageSeq, boolean msgsGenerated,
			int constMsgType, Iterator<WindowsMessage> seqIterator) {
		WindowsMessage currentSeqMsg = null;
		if (msgsGenerated) {
			currentSeqMsg = seqIterator.next();
		} else {
			currentSeqMsg = new WindowsMessage(constMsgType);
			generatedMessageSeq.add(currentSeqMsg);
		}
		return currentSeqMsg;
	}

	private WindowsMessage getStoredMessageVariable(
			WindowsMessage currentMessage, String obj)
			throws InvalidParameterException {
		WindowsMessage varMessage = null;
		if (obj.equals("this")) {
			if (currentMessage == null) {
				throw new InvalidParameterException(
						"Failure obtaining term value for rule "
								+ currentRuleName
								+ ": \"this\" is not a valid name for generating runtime messages.");
			}
			varMessage = currentMessage;
		} else {
			Object tmp = messageStorage.get(obj);
			if (tmp instanceof WindowsMessage) {
				varMessage = (WindowsMessage) tmp;
			} else {
				throw new InvalidParameterException(
						"Failure obtaining term value for rule "
								+ currentRuleName + ": No message \"" + obj
								+ "\" stored.");
			}
		}
		return varMessage;
	}

	@SuppressWarnings("unchecked")
	private List<WindowsMessage> getStoredSeqVariable(String obj)
			throws InvalidParameterException {
		List<WindowsMessage> varMsgSeq = null;
		Object tmp = messageStorage.get(obj);
		if (tmp instanceof List<?>) {
			varMsgSeq = (List<WindowsMessage>) tmp;
		} else {
			throw new InvalidParameterException(
					"Failure obtaining term value for rule " + currentRuleName
							+ ": No sequence \"" + obj + "\" store.");
		}
		return varMsgSeq;
	}

	private String getTermValue(WindowsMessage currentMessage,
			Element termElement) {
		String value = null;
		WindowsMessage varMessage = null;
		if (termElement.getName().equals("constValue")) {
			value = termElement.getAttributeValue("value");
		} else if (termElement.getName().equals("paramValue")) {
			String objectName = termElement.getAttributeValue("obj");
			varMessage = getStoredMessageVariable(currentMessage, objectName);
			if (varMessage != null) {
				String param = termElement.getAttributeValue("param");
				value = varMessage.getParameter(param);
			}
		} else if (termElement.getName().equals("winInfoValue")) {
			String objectName = termElement.getAttributeValue("obj");
			varMessage = getStoredMessageVariable(currentMessage, objectName);
			if (varMessage != null) {
				String paramString = termElement.getAttributeValue("winParam");
				if (paramString.equals("class")) {
					value = varMessage.getWindowClass();
				} else if (paramString.equals("resourceId")) {
					value = "" + varMessage.getWindowResourceId();
				} else if (paramString.equals("hwnd")) {
					value = "" + varMessage.getHwnd();
				}
			}
		} else if (termElement.getName().equals("msgInfoValue")) {
			String objectName = termElement.getAttributeValue("obj");
			varMessage = getStoredMessageVariable(currentMessage, objectName);
			if (varMessage != null) {
				String paramString = termElement.getAttributeValue("msgParam");
				if (paramString.equals("type")) {
					value = "" + varMessage.getType();
				} else if (paramString.equals("target")) {
					value = varMessage.getXmlWindowDescription();
				}
			}
		}
		return value;
	}

	private List<String> getTermValueSeq(WindowsMessage currentMessage,
			Element termElement) {
		List<String> values = new LinkedList<String>();
		if (termElement.getName().equals("seqValue")) {
			String obj = termElement.getAttributeValue("seqObj");
			String param = termElement.getAttributeValue("param");
			List<WindowsMessage> seqVar = getStoredSeqVariable(obj);

			for (WindowsMessage msg : seqVar) {
				// msg.getParameter returns null, if parameter is not found,
				// therefore the List can contain null-values
				values.add(msg.getParameter(param));
			}
		}
		return values;
	}

	@SuppressWarnings("unchecked")
	private void handleStorage(Element messageElement,
			WindowsMessage currentMessage) {
		for (Element childElement : (List<Element>) messageElement.getChildren(
				"store", rulesNamespace)) {
			String identifier = childElement.getAttributeValue("var");
			messageStorage.put(identifier, currentMessage);
			resolveHwnd(currentMessage, childElement);
		}
		for (Element childElement : (List<Element>) messageElement.getChildren(
				"storeSeq", rulesNamespace)) {
			String identifier = childElement.getAttributeValue("varSeq");
			Object tmp = messageStorage.get(identifier);
			List<WindowsMessage> storedSequence;
			if (tmp == null || tmp instanceof WindowsMessage) {
				storedSequence = new LinkedList<WindowsMessage>();
				storedSequence.add(currentMessage);
				messageStorage.put(identifier, storedSequence);
			} else if (tmp instanceof List<?>) {
				storedSequence = (List<WindowsMessage>) tmp;
				storedSequence.add(currentMessage);
				messageStorage.put(identifier, storedSequence);
			}
			resolveHwnd(currentMessage, childElement);
		}
	}

	private boolean matchMultipleMessages(Element messageElement,
			Element nextMessageElement) {
		boolean isMatch = false;
		boolean isCurrentMatch = false;
		boolean nextMatchFound = false;
		WindowsMessage currentMessage = null;
		WindowsMessage nextMessage = null;

		int type = Integer.parseInt(messageElement.getAttributeValue("type"));

		int nextType = -1;
		if (nextMessageElement != null) {
			nextType = Integer.parseInt(nextMessageElement
					.getAttributeValue("type"));
		}

		while (!nextMatchFound && sequenceIterator.hasNext()) {
			currentMessage = sequenceIterator.next();
			if (type == currentMessage.getType()) {
				isCurrentMatch = evalEqualRestrictions(currentMessage,
						messageElement);
				isMatch = isMatch || isCurrentMatch;

				if (isCurrentMatch) {
					handleStorage(messageElement, currentMessage);
					currentToken.setTarget(currentMessage
							.getXmlWindowDescription());
					currentToken
							.setTargetShort(currentMessage.getParentNames());
				}
			}
			if (nextMessageElement != null && isMatch) {
				// peek next message to check if the sequence ends and the next
				// match is found
				if (!sequenceIterator.hasNext()) {
					return false; // sequence is over, but not all messages are
									// found
				}
				nextMessage = sequenceIterator.next();
				sequenceIterator.previous();

				if (nextType == nextMessage.getType()) {
					nextMatchFound = evalEqualRestrictions(nextMessage,
							nextMessageElement);
				}

			}
		}

		return isMatch;
	}

	private boolean matchSingleMessage(Element messageElement) {
		boolean isMatch = false;
		WindowsMessage currentMessage = null;

		int type = Integer.parseInt(messageElement.getAttributeValue("type"));

		while (!isMatch && sequenceIterator.hasNext()) {
			// traverses the messages from the current position forward till a
			// message with the correct type is found
			currentMessage = sequenceIterator.next();
			if (type == currentMessage.getType()) {
				// message with the correct type found
				// eval child nodes for further matching/storing
				isMatch = evalEqualRestrictions(currentMessage, messageElement);

				// in case the message is a match, eval storage children
				if (isMatch) {
					handleStorage(messageElement, currentMessage);
					currentToken.setTarget(currentMessage
							.getXmlWindowDescription());
					currentToken
							.setTargetShort(currentMessage.getParentNames());
				}
			}
		}

		return isMatch;
	}

	@SuppressWarnings("unchecked")
	private void resolveHwnd(WindowsMessage currentMessage, Element childElement) {
		List<Element> resolveElements = childElement.getChildren("resolveHwnd",
				rulesNamespace);
		for (Element resolveElement : resolveElements) {
			String param = resolveElement.getAttributeValue("param");
			String storeParam = resolveElement.getAttributeValue("storeParam");
			int paramHwnd = Integer
					.parseInt(currentMessage.getParameter(param));
			WindowTreeNode node = WindowTree.getInstance().find(paramHwnd);
			if (node != null) {
				currentMessage.addParameter(storeParam,
						node.xmlRepresentation());
			}
		}
	}

}
