//   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.mfc;

import java.io.IOException;
import java.util.ArrayList;
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 java.util.logging.Level;

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.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.eventcore.IEventType;
import de.ugoe.cs.autoquest.plugin.mfc.EventGenerationRule.Term;
import de.ugoe.cs.autoquest.plugin.mfc.eventcore.MFCEventTypeFactory;
import de.ugoe.cs.autoquest.plugin.mfc.eventcore.ReplayWindowsMessage;
import de.ugoe.cs.autoquest.plugin.mfc.eventcore.WindowsMessage;
import de.ugoe.cs.autoquest.plugin.mfc.eventcore.WindowsMessageType;
import de.ugoe.cs.autoquest.plugin.mfc.guimodel.MFCGUIElement;
import de.ugoe.cs.autoquest.plugin.mfc.guimodel.MFCWindowTree;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * Translates sequences of windows messages into {@link Event}s that can be used by the
 * AutoQUEST core libraries.
 * </p>
 * 
 * @version 1.0
 * @author Steffen Herbold, Patrick Harms
 */
public class EventGenerator {

    /**
     * <p>
     * the list of all event generation rules available
     * </p>
     */
    private List<EventGenerationRule> generationRules;
    
    /**
     * <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 Event currentEvent;

    /**
     * <p>
     * Event type of the current token. Stored as a member to be able to update it during the
     * parsing of the idinfo tag.
     * </p>
     */
    private IEventType currentType;

    /**
     * <p>
     * 
     * </p>
     */
    private MFCGUIElement currentTarget;

    /**
     * <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>
     * reference to the window tree created during parsing
     * </p>
     */
    private MFCWindowTree windowTree;

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

    /**
     * <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
     */
    public Event generateEvent(List<WindowsMessage> sequence) {
        if (generationRules == null) {
            parseGenerationRules();
        }
        
        
        /*System.out.println("generating event for ");
        for (WindowsMessage msg : sequence) {
            System.out.println("    " + msg + "  " + msg.getParameter("scrollBarHandle") + "  " + msg.getTargetXML());
        }*/

        boolean isMatch = false;

        for (int ruleIndex = 0; ruleIndex < generationRules.size() && !isMatch; ruleIndex++) {
            EventGenerationRule currentRule = generationRules.get(ruleIndex);
            currentRuleName = currentRule.getName();
            
            //System.out.println("checking rule " + currentRuleName);
            
            messageStorage = new HashMap<String, Object>();
            sequenceIterator = sequence.listIterator();
            
            isMatch = evaluateMessageConditions(currentRule);

            if (isMatch) {
                currentType = MFCEventTypeFactory.getInstance().getEventType
                    (currentRuleName, resolveParameters(currentRule.getEventParameters()));
                
                currentEvent = new Event(currentType, currentTarget);
                
                for (EventGenerationRule.ReplayMessageSpec replayMessageSpec :
                      currentRule.getReplayMessageSpecifications())
                {
                    if (replayMessageSpec.generateSingleMessage()) {
                        try {
                            generateReplayMessage(replayMessageSpec);
                        }
                        catch (IllegalArgumentException e) {
                            Console.printerrln(e.getMessage());
                            // TODO currentToken.invalidateReplay();
                        }
                    }
                    else {
                        try {
                            generateReplaySequence(replayMessageSpec);
                            // TODO currentToken.invalidateReplay();
                        }
                        catch (IllegalArgumentException e) {
                            Console.printerrln(e.getMessage());
                            // TODO currentToken.invalidateReplay();
                        }
                    }
                }

                Console.traceln(Level.FINE, currentEvent.getType().toString() + " matched");
            }
            else {
                currentEvent = null;
            }
        }
        if (!isMatch) {
            Console.traceln(Level.WARNING, "no match found for sequence: " + sequence.toString());
        }
        
        
        /*if (currentEvent != null && currentEvent.getReplayables() != null) {
            System.out.println("replay messages are ");
            for (de.ugoe.cs.autoquest.eventcore.IReplayable msg : currentEvent.getReplayables()) {
                System.out.println("    " + msg + "  " + msg.getReplay());
            }
        }

        System.out.println();*/


        return currentEvent;
    }

    // ////////////////////////////////////////////////////////////
    // Helper functions for matching of events, i.e., msg-nodes //
    // ////////////////////////////////////////////////////////////

    /**
     * <p>
     * Evaluates whether the current message sequence matches a given rule.
     * </p>
     *
     * @param currentRule rule that is matched
     * @return true if the message sequence matches the rule; false otherwise
     */
    private boolean evaluateMessageConditions(EventGenerationRule currentRule) {
        boolean isMatch = true;
        List<EventGenerationRule.MessageCondition> messageConditions =
            currentRule.getMessageConditions();

        int i = 0;
        while (isMatch && i < messageConditions.size()) {
            EventGenerationRule.MessageCondition messageCondition = messageConditions.get(i);
            if (messageCondition.matchMultiple()) {
                EventGenerationRule.MessageCondition nextMessageCondition = null;
                if (i + 1 < messageConditions.size()) {
                    nextMessageCondition = messageConditions.get(i + 1);
                }
                try {
                    isMatch = matchMultipleConditions(messageCondition, nextMessageCondition);
                }
                catch (IllegalArgumentException e) {
                    Console.printerrln(e.getMessage());
                }
            }
            else {
                try {
                    isMatch = matchSingleMessage(messageCondition);
                }
                catch (IllegalArgumentException e) {
                    Console.printerrln(e.getMessage());
                }
            }
            i++;
        }
        
        return isMatch;
    }

    /**
     * <p>
     * Handles msg-nodes where multiple is not true, i.e., not a sequences.
     * </p>
     * 
     * @param messageElement
     *            {@link Element} representing the msg-node
     * @return true, if a match is found; false otherwise
     */
    private boolean matchSingleMessage(EventGenerationRule.MessageCondition condition) {
        boolean isMatch = false;
        WindowsMessage currentMessage = null;

        WindowsMessageType type = condition.getMessageType();

        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 = evaluateMessageCondition(currentMessage, condition);

                // in case the message is a match, eval storage children
                if (isMatch) {
                    handleStorage(condition, currentMessage);
                    currentTarget = currentMessage.getTarget();
                    // TODO currentToken.setTargetShort(currentMessage.getParentNames());
                }
            }
        }

        return isMatch;
    }

    /**
     * <p>
     * Handles msg-nodes where multiple is true, i.e., sequences. Requires knowledge about the next
     * msg-node to determine the end of the sequence.
     * </p>
     * 
     * @param messageElement
     *            {@link Element} representing the msg-node
     * @param nextMessageElement
     *            {@link Element} representing the next msg-node; {@code null} if the current node
     *            is the last one
     * @return true, if a sequence is matched; false otherwise
     */
    private boolean matchMultipleConditions(EventGenerationRule.MessageCondition condition,
                                            EventGenerationRule.MessageCondition nextCondition)
    {
        boolean isMatch = false;
        boolean isCurrentMatch = false;
        boolean nextMatchFound = false;
        WindowsMessage currentMessage = null;
        WindowsMessage nextMessage = null;

        WindowsMessageType type = condition.getMessageType();

        WindowsMessageType nextType = null;
        if (nextCondition != null) {
            nextType = nextCondition.getMessageType();
        }

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

                if (isCurrentMatch) {
                    handleStorage(condition, currentMessage);
                    currentTarget = currentMessage.getTarget();
                    // TODO currentToken.setTargetShort(currentMessage.getParentNames());
                }
            }
            if (nextCondition != 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 = evaluateMessageCondition(nextMessage, nextCondition);
                }

            }
        }

        return isMatch;
    }

    /**
     * <p>
     * Handles equals-nodes.
     * </p>
     * 
     * @param currentMessage
     *            {@link Element} representing the msg-node the equals-node belongs to
     * @param messageElement
     *            {@link Element} representing the equals-node to be evaluated
     * @return true, if constraint is fulfilled; false otherwise
     */
    private boolean evaluateMessageCondition(WindowsMessage                       currentMessage,
                                             EventGenerationRule.MessageCondition condition)
    {
        boolean isMatch = true;
        for (int i = 0; isMatch && (i < condition.getAttributeConditions().size()); i++)
        {
            EventGenerationRule.AttributeCondition attrCond =
                condition.getAttributeConditions().get(i);
            
            // the size 2 of termElements is guaranteed by the XML schema
            Object value1 = getTermValue(currentMessage, attrCond.getLeftHandSide(), Object.class);
            Object value2 = getTermValue(currentMessage, attrCond.getRightHandSide(), Object.class);
            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(termElements.get(0));
//            List<String> values2 = getTermValueSeq(termElements.get(0));
//            if (values1 == null || values2 == null) {
//                isMatch = false;
//            }
//            else {
//                isMatch = isMatch && values1.equals(values2);
//            }
//        }
        return isMatch;
    }

    /**
     * <p>
     * Handles store-nodes and storeSeq-nodes.
     * </p>
     * 
     * @param messageElement
     *            {@link Element} representing the msg-node that is currently being evaluated
     * @param currentMessage
     *            current message in the message sequence that is matched; this is the message that
     *            is stored
     */
    @SuppressWarnings("unchecked")
    private void handleStorage(EventGenerationRule.MessageCondition messageCondition,
                               WindowsMessage                       currentMessage)
    {
        for (Term valueToStore : messageCondition.getMessagesToStore())
        {
            if (valueToStore.getMessageId() != null) {
              ReplayWindowsMessage replayMessage = new ReplayWindowsMessage(currentMessage);
              messageStorage.put(valueToStore.getMessageId(), replayMessage);
              resolveHwnd(replayMessage, valueToStore.getResolveHandles());
            }
            else if (valueToStore.getSequenceId() != null) {
                Object tmp = messageStorage.get(valueToStore.getSequenceId());
                ReplayWindowsMessage replayMessage = new ReplayWindowsMessage(currentMessage);
                List<ReplayWindowsMessage> storedSequence;
                if (tmp == null || tmp instanceof ReplayWindowsMessage) {
                    storedSequence = new LinkedList<ReplayWindowsMessage>();
                    storedSequence.add(replayMessage);
                    messageStorage.put(valueToStore.getSequenceId(), storedSequence);
                }
                else if (tmp instanceof List<?>) {
                    storedSequence = (List<ReplayWindowsMessage>) tmp;
                    storedSequence.add(replayMessage);
                    messageStorage.put(valueToStore.getSequenceId(), storedSequence);
                }
                resolveHwnd(replayMessage, valueToStore.getResolveHandles());
            }
        }
    }

    /**
     * <p>
     * Resolves a parameter that contains a HWND of a message to the target string of the HWND and
     * stores it.
     * </p>
     * 
     * @param currentMessage
     *            message whose HWND is resolved
     * @param list
     *            child element of the store node that represents the resolve
     */
    private void resolveHwnd(ReplayWindowsMessage currentMessage, List<Term> resolveHandles) {
        if (resolveHandles != null) {
            for (Term resolveElement : resolveHandles) {
                String param = resolveElement.getMessageParameterName();
                String storeParam = resolveElement.getStoreParameterName();
                long paramHwnd = (Long) currentMessage.getParameter(param);
                MFCGUIElement guiElement = windowTree.find(paramHwnd);
                if (guiElement != null) {
                    currentMessage.addParameter(storeParam, "" + guiElement.toXML());
                }
            }
        }
    }

    // /////////////////////////////////////////////////////
    // Helper functions for generating the replay, i.e.,
    // parsing of genMsg und genMsgSeq-nodes
    // /////////////////////////////////////////////////////

    /**
     * <p>
     * Handles genMsg-nodes and adds the replay to the {@link Event} that is generated.
     * </p>
     * 
     * @param genMsgElement
     *            {@link Element} representing the genMsg-node
     */
    private void generateReplayMessage(EventGenerationRule.ReplayMessageSpec messageSpec) {
        ReplayWindowsMessage generatedMessage = null;
        if (messageSpec.getReplayObjectId() != null) { // replay stored message without change
            generatedMessage = getStoredMessageVariable(null, messageSpec.getReplayObjectId());
        }
        else { // generate message according to the rule
            generatedMessage = new ReplayWindowsMessage
                (getTermValue(messageSpec.getType(), WindowsMessageType.class));
            generatedMessage.setTargetXML(getTermValue(messageSpec.getTarget(), String.class));

            if ((messageSpec.getLparamHiWord() != null) ||
                (messageSpec.getLparamLoWord() != null))
            {
                generatedMessage.setLPARAM
                    (loHiWord(messageSpec.getLparamLoWord(), messageSpec.getLparamHiWord()));
            }
            else if (messageSpec.getLparam() != null) {
                try {
                    generatedMessage.setLPARAM(getTermValue(messageSpec.getLparam(), Long.class));
                }
                catch (IllegalArgumentException e) {
                    generatedMessage.setLPARAMasWindowDesc
                        (getTermValue(messageSpec.getLparam(), String.class));
                }
            }

            if ((messageSpec.getWparamHiWord() != null) ||
                (messageSpec.getWparamLoWord() != null))
            {
                generatedMessage.setWPARAM
                    (loHiWord(messageSpec.getWparamLoWord(), messageSpec.getWparamHiWord()));
            }
            else if (messageSpec.getWparam() != null) {
                try {
                    generatedMessage.setWPARAM(getTermValue(messageSpec.getWparam(), Long.class));
                }
                catch (IllegalArgumentException e) {
                    generatedMessage.setWPARAMasWindowDesc
                        (getTermValue(messageSpec.getWparam(), String.class));
                }
            }
            
            
        }

        generatedMessage.setDelay(messageSpec.getDelay());

        currentEvent.addReplayable(generatedMessage);
    }

    /**
     * Handles genMsgSeq-nodes and adds the replay to the {@link Event} that is generated.</p>
     * 
     * @param genMsgElement
     *            {@link Element} representing the genMsgSeq-node.
     */
    private void generateReplaySequence(EventGenerationRule.ReplayMessageSpec messageSpec)
    {
        List<ReplayWindowsMessage> generatedMessageSeq = new LinkedList<ReplayWindowsMessage>();
        if (messageSpec.getReplayObjectId() != null) { // replay stored sequence without changes
            generatedMessageSeq = getStoredSeqVariable(messageSpec.getReplayObjectId());
        }
        else {
            List<ReplayWindowsMessage> seqVar =
                getStoredSeqVariable(messageSpec.getReplayObjectId());
            
            Term typeTerm = messageSpec.getType();
            if (typeTerm.getSequenceId() != null) {
                seqVar = getStoredSeqVariable(typeTerm.getSequenceId());
                for (WindowsMessage msg : seqVar) {
                    ReplayWindowsMessage replayMessage = new ReplayWindowsMessage(msg.getType());
                    replayMessage.setDelay(messageSpec.getDelay());
                    generatedMessageSeq.add(replayMessage);
                }
            }
            else { // constValue type
                WindowsMessageType constMsgType = getTermValue(typeTerm, WindowsMessageType.class);
                for (int i = 0; i < seqVar.size(); i++) {
                    ReplayWindowsMessage replayMessage = new ReplayWindowsMessage(constMsgType);
                    replayMessage.setDelay(messageSpec.getDelay());
                    generatedMessageSeq.add(replayMessage);
                }
            }

            createSequenceTarget(generatedMessageSeq, messageSpec);
            createSequenceLParam(generatedMessageSeq, messageSpec);
            createSequenceWParam(generatedMessageSeq, messageSpec);
        }
        
        currentEvent.addReplayableSequence(generatedMessageSeq);
    }

    /**
     * <p>
     * Creates the targets for replay sequences generated with genMsgSeq-nodes.
     * </p>
     * 
     * @param generatedMessageSeq
     *            list of the messages that is being generated
     * @param msgsGenerated
     *            boolean stating if the list of messages is already generated or if the generation
     *            has to be handles by this method
     * @param constMsgType
     *            a constant message type that is used for message generation, in case the list of
     *            message is generated by this method
     * @param termElement
     *            {@link Element} representing the term-node describing the target
     * @return true, if the list of message is generated after calling this method; false otherwise
     * @throws NoSuchElementException
     *             thrown if the seqVar referred to in the termElement contains a different number
     *             of messages than is contained in messageSeq
     */
    private void createSequenceTarget(List<ReplayWindowsMessage>            generatedMessageSeq,
                                      EventGenerationRule.ReplayMessageSpec messageSpec)
        throws NoSuchElementException
    {
        Iterator<ReplayWindowsMessage> seqIterator = generatedMessageSeq.iterator();
        if (messageSpec.getTarget().getSequenceId() != null) {
            List<ReplayWindowsMessage> seqVar =
                getStoredSeqVariable(messageSpec.getTarget().getSequenceId());

            if (seqVar.size() != generatedMessageSeq.size()) {
                throw new IllegalArgumentException
                    ("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) {
                seqIterator.next().setTarget(msg.getTarget());
            }
        }
        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.
             */
        }
    }

    /**
     * <p>
     * Creates the LPARAMs for replay sequences generated with genMsgSeq-nodes.
     * </p>
     * 
     * @param generatedMessageSeq
     *            list of the messages that is being generated
     * @param msgsGenerated
     *            boolean stating if the list of messages is already generated or if the generation
     *            has to be handles by this method
     * @param constMsgType
     *            a constant message type that is used for message generation, in case the list of
     *            message is generated by this method
     * @param termElement
     *            {@link Element} representing the term-node describing the LPARAM
     * @return true, if the list of message is generated after calling this method; false otherwise
     * @throws NoSuchElementException
     *             thrown if the seqVar referred to in the termElement contains a different number
     *             of messages than is contained in messageSeq
     */
    private void createSequenceLParam(List<ReplayWindowsMessage>            generatedMessageSeq,
                                      EventGenerationRule.ReplayMessageSpec messageSpec)
        throws NoSuchElementException
    {
        Iterator<ReplayWindowsMessage> seqIterator = generatedMessageSeq.iterator();
        if (messageSpec.getLparam().getSequenceId() != null) {
            List<ReplayWindowsMessage> seqVar =
                getStoredSeqVariable(messageSpec.getLparam().getSequenceId());
            
            if (seqVar.size() != generatedMessageSeq.size()) {
                throw new IllegalArgumentException
                    ("Failure generating replay sequence for rule " + currentRuleName +
                     ": One or more of the sequence variables used to generate a sequence have " +
                     "different lengths.");
            }
            for (WindowsMessage msg : seqVar) {
                ReplayWindowsMessage currentSeqMsg = seqIterator.next();
                Object paramValue =
                    msg.getParameter(messageSpec.getLparam().getSequenceParameterName());
                if (paramValue instanceof Long) {
                    currentSeqMsg.setLPARAM((Long) paramValue);
                }
                else {
                    currentSeqMsg.setLPARAMasWindowDesc((String) paramValue);
                }
            }
        }
        else { // const value
            int paramValue = getTermValue(messageSpec.getLparam(), int.class);
            while (seqIterator.hasNext()) {
                seqIterator.next().setLPARAM(paramValue);
            }
        }

    }

    /**
     * <p>
     * Creates the WPARAMs for replay sequences generated with genMsgSeq-nodes.
     * </p>
     * 
     * @param generatedMessageSeq
     *            list of the messages that is being generated
     * @param msgsGenerated
     *            boolean stating if the list of messages is already generated or if the generation
     *            has to be handles by this method
     * @param constMsgType
     *            a constant message type that is used for message generation, in case the list of
     *            message is generated by this method
     * @param termElement
     *            {@link Element} representing the term-node describing the WPARAM
     * @return true, if the list of message is generated after calling this method; false otherwise
     * @throws NoSuchElementException
     *             thrown if the seqVar referred to in the termElement contains a different number
     *             of messages than is contained in messageSeq
     */
    private void createSequenceWParam(List<ReplayWindowsMessage>            generatedMessageSeq,
                                      EventGenerationRule.ReplayMessageSpec messageSpec)
        throws NoSuchElementException
    {
        Iterator<ReplayWindowsMessage> seqIterator = generatedMessageSeq.iterator();
        if (messageSpec.getWparam().getSequenceId() != null) {
            List<ReplayWindowsMessage> seqVar =
                getStoredSeqVariable(messageSpec.getWparam().getSequenceId());
            
            if (seqVar.size() != generatedMessageSeq.size()) {
                throw new IllegalArgumentException
                    ("Failure generating replay sequence for rule " + currentRuleName +
                     ": One or more of the sequence variables used to generate a sequence have " +
                     "different lengths.");
            }
            for (WindowsMessage msg : seqVar) {
                ReplayWindowsMessage currentSeqMsg = seqIterator.next();
                Object paramValue =
                    msg.getParameter(messageSpec.getWparam().getSequenceParameterName());
                if (paramValue instanceof Long) {
                    currentSeqMsg.setWPARAM((Long) paramValue);
                }
                else {
                    currentSeqMsg.setWPARAMasWindowDesc((String) paramValue);
                }
            }
        }
        else { // const value
            long paramValue = getTermValue(messageSpec.getWparam(), Long.class);
            while (seqIterator.hasNext()) {
                seqIterator.next().setWPARAM(paramValue);
            }
        }
    }

    // ////////////////////////////
    // General helper functions //
    // ////////////////////////////

    /**
     * <p>
     * Resolves the parameters described by {@link Term}s.
     * </p>
     *
     * @param eventParameters terms whose parameters are resolved
     * @return resolved parameters
     */
    private Map<String, String> resolveParameters(List<Term> eventParameters) {
        Map<String, String> resultParameters = null;
        
        if ((eventParameters != null) && (eventParameters.size() > 0)) {
            resultParameters = new HashMap<String, String>();
            
            for (Term term : eventParameters) {
                if ("seqValue".equals(term.getName())) {
                    List<String> values = getTermValueAsList(term, String.class);
                                        
                    resultParameters.put
                        (term.getSequenceParameterName(), (String) values.get(values.size() - 1));
                }
                else {
                    resultParameters.put
                        (term.getMessageParameterName(), getTermValue(term, String.class));
                }
            }
        }
        
        return resultParameters;
    }

    /**
     * <p>
     * Retrieves a message from the storage for, e.g., comparison or replay. "this" is used to refer
     * to the current message.
     * </p>
     * 
     * @param currentMessage
     *            current message during the parsing; passed to handle "this"
     * @param obj
     *            object identifier in the storage
     * @return message retrieved from the storage
     * @throws IllegalArgumentException
     *             thrown in case of invalid uses of "this" or if no message with the identifier obj
     *             is found in the storage
     */
    private ReplayWindowsMessage getStoredMessageVariable(WindowsMessage currentMessage, String obj)
        throws IllegalArgumentException
    {
        ReplayWindowsMessage varMessage = null;
        if (obj.equals("this")) {
            if (currentMessage == null) {
                throw new IllegalArgumentException("Failure obtaining term value for rule " +
                    currentRuleName +
                    ": \"this\" is not a valid name for generating runtime messages.");
            }
            varMessage = new ReplayWindowsMessage(currentMessage);
        }
        else {
            Object tmp = messageStorage.get(obj);
            if (tmp instanceof ReplayWindowsMessage) {
                varMessage = (ReplayWindowsMessage) tmp;
            }
            else {
                throw new IllegalArgumentException("Failure obtaining term value for rule " +
                    currentRuleName + ": No message \"" + obj + "\" stored.");
            }
        }
        return varMessage;
    }

    /**
     * <p>
     * Retrieves a stored message sequence from the storage.
     * </p>
     * 
     * @param obj
     *            object identifier in the storage
     * @return message sequence retrieved from the storage
     * @throws IllegalArgumentException
     *             thrown if no message sequences with the identifier obj is found in the storage
     */
    @SuppressWarnings("unchecked")
    private List<ReplayWindowsMessage> getStoredSeqVariable(String obj)
        throws IllegalArgumentException
    {
        List<ReplayWindowsMessage> varMsgSeq = null;
        Object tmp = messageStorage.get(obj);
        if (tmp instanceof List<?>) {
            varMsgSeq = (List<ReplayWindowsMessage>) tmp;
        }
        else {
            throw new IllegalArgumentException("Failure obtaining term value for rule " +
                                               currentRuleName + ": No sequence \"" + obj +
                                               "\" store.");
        }
        return varMsgSeq;
    }

    /**
     * <p>
     * convenience method for {@link #getTermValue(WindowsMessage, Term)} with current message is
     * null.
     * </p>
     * 
     * @param termElement
     *            {@link Element} representing the term node
     * @return value of the term or {@code null} of the term node could not be evaluated
     */
    private <T> T getTermValue(EventGenerationRule.Term term, Class<T> expectedType) {
        return getTermValue(null, term, expectedType);
    }

    /**
     * <p>
     * Handles term-nodes and returns the value of the described term.
     * </p>
     * 
     * @param currentMessage
     *            current message during the parsing; required to resolve references to "this" in a
     *            term
     * @param termElement
     *            {@link Element} representing the term node
     * @return value of the term or {@code null} of the term node could not be evaluated
     */
    private <T> T getTermValue(WindowsMessage           currentMessage,
                               EventGenerationRule.Term term,
                               Class<T>                 expectedType)
    {
        T value = null;
        
        if ("constValue".equals(term.getName())) {
            value = getValueAsType(term.getValue(), expectedType);
        }
        else if ("paramValue".equals(term.getName())) {
            String objectName = term.getMessageId();
            WindowsMessage varMessage = getStoredMessageVariable(currentMessage, objectName);
            if (varMessage != null) {
                String param = term.getMessageParameterName();
                value = getValueAsType(varMessage.getParameter(param), expectedType);
            }
        }
        else if ("winInfoValue".equals(term.getName())) {
            String objectName = term.getMessageId();
            WindowsMessage varMessage = getStoredMessageVariable(currentMessage, objectName);
            if (varMessage != null) {
                String paramString = term.getWindowParameterName();
                if (paramString.equals("class")) {
                    value = getValueAsType
                        (((MFCGUIElement) varMessage.getTarget()).getType(), expectedType);
                }
                else if (paramString.equals("resourceId")) {
                    value = getValueAsType
                        (((MFCGUIElement) varMessage.getTarget()).getResourceId(), expectedType);
                }
                else if (paramString.equals("hwnd")) {
                    value = getValueAsType
                        (((MFCGUIElement) varMessage.getTarget()).getId(), expectedType);
                }
                else if (paramString.equals("parentTarget")) {
                    String target = varMessage.getTargetXML();
                    int index = target.lastIndexOf("<");
                    if (index == 0) {
                        Console.traceln(Level.WARNING, "Trying to adress parent of top-level " +
                                        "window! Replay probably invalid!");
                    }
                    value =  getValueAsType(target.substring(0, index), expectedType);
                }
                else if (paramString.equals("parentClass")) {
                    value =  getValueAsType
                        (((MFCGUIElement) varMessage.getTarget())
                        .getParent().getSpecification().getType(), expectedType);
                }
            }
        }
        else if ("msgInfoValue".equals(term.getName())) {
            String objectName = term.getMessageId();
            WindowsMessage varMessage = getStoredMessageVariable(currentMessage, objectName);
            if (varMessage != null) {
                String paramString = term.getMessageInfoName();
                if (paramString.equals("type")) {
                    value = getValueAsType(varMessage.getType(), expectedType);
                }
                else if (paramString.equals("target")) {
                    value = getValueAsType(varMessage.getTargetXML(), expectedType);
                }
            }
        }
        else if ("msgInfoValue".equals(term.getName())) {
            String objectName = term.getMessageId();
            WindowsMessage varMessage = getStoredMessageVariable(currentMessage, objectName);
            if (varMessage != null) {
                String paramString = term.getMessageInfoName();
                if (paramString.equals("type")) {
                    value = getValueAsType(varMessage.getType(), expectedType);
                }
                else if (paramString.equals("target")) {
                    value = getValueAsType(varMessage.getTargetXML(), expectedType);
                }
            }
        }
        
        return value;
    }
    
    /**
     * <p>
     * Convenience method for {@link #getTermValueAsList(WindowsMessage, Term)} with current
     * message is null.
     * </p>
     * 
     * @param termElement
     *            {@link Element} representing the term node
     * @return value of the term or {@code null} of the term node could not be evaluated
     */
    private <T> List<T> getTermValueAsList(EventGenerationRule.Term term, Class<T> expectedType) {
        return getTermValueAsList(null, term, expectedType);
    }

    /**
     * <p>
     * Handles term-nodes and returns the value of the described term.
     * </p>
     * 
     * @param currentMessage
     *            current message during the parsing; required to resolve references to "this" in a
     *            term
     * @param termElement
     *            {@link Element} representing the term node
     * @return value of the term or {@code null} of the term node could not be evaluated
     */
    private <T> List<T> getTermValueAsList(WindowsMessage           currentMessage,
                                           EventGenerationRule.Term term,
                                           Class<T>                 expectedElementType)
    {
        List<T> values = new ArrayList<T>();
        if ("seqValue".equals(term.getName())) {
            List<ReplayWindowsMessage> seqVar = getStoredSeqVariable(term.getSequenceId());
            Object value;
            
            for (ReplayWindowsMessage msg : seqVar) {
                // msg.getParameter returns null, if parameter is not found,
                // therefore the List can contain null-values
                value = msg.getParameter(term.getSequenceParameterName());
                values.add(getValueAsType(value, expectedElementType));
            }
        }
        else {
            values.add(getTermValue(currentMessage, term, expectedElementType));
        }
        
        return values;
    }
    
    /**
     * <p>
     * Resolves term values.
     * </p>
     *
     * @param value value to be resolved
     * @param expectedType class defining the expected type
     * @return resolved value
     */
    @SuppressWarnings("unchecked")
    private <T> T getValueAsType(Object value, Class<T> expectedType) {
        if (expectedType.isInstance(value)) {
            return (T) value;
        }
        else if (value instanceof String) {
            try {
                if (WindowsMessageType.class.equals(expectedType)) {
                    return (T) WindowsMessageType.parseMessageType((String) value);
                }
                else if (Short.class.equals(expectedType)) {
                    return (T) (Short) Short.parseShort((String) value);
                }
                else if (Long.class.equals(expectedType)) {
                    return (T) (Long) Long.parseLong((String) value);
                }
            }
            catch (Exception e) {
                // in this case, the value can not be transformed to the expected value. So ignore
                // the exception and fall through to the exception thrown anyway
            }
        }
        else if (value instanceof Long) {
            try {
                if (Short.class.equals(expectedType)) {
                    return (T) (Short) ((Long) value).shortValue();
                }
                else if (String.class.equals(expectedType)) {
                    return (T) ((Long) value).toString();
                }
            }
            catch (Exception e) {
                // in this case, the value can not be transformed to the expected value. So ignore
                // the exception and fall through to the exception thrown anyway
            }
        }
        
        throw new IllegalArgumentException("the term value is not of the expected type " +
                                           expectedType + " but a " +
                                           (value != null ? value.getClass() : "null"));
    }

    /**
     * <p>
     * Handles LOWORD and HIWORD child nodes of LPARAM and WPARAM nodes. The returned value is the
     * LPARAM/WPARAM value based on the LOWORD and HIWORD.
     * </p>
     * 
     * @param param
     *            {@link Element} representing the LPARAM/WPARAM node
     * @return value of the LPARAM/WPARAM
     */
    private long loHiWord(EventGenerationRule.Term lword, EventGenerationRule.Term hword) {
        return MAKEPARAM(getTermValue(lword, Short.class), getTermValue(hword, Short.class));
    }

    /**
     * <p>
     * Takes to short integers and combines them into the high and low order bits of an integer.
     * </p>
     * 
     * @param loword
     *            low word
     * @param hiword
     *            high word
     * @return combined integer
     */
    private static int MAKEPARAM(short loword, short hiword) {
        return loword | ((int) hiword) << Short.SIZE;
    }

    /**
     * <p>
     * Parses the rules.
     * </p>
     *
     */
    @SuppressWarnings("unchecked")
    private void parseGenerationRules() {
        SAXBuilder builder = new SAXBuilder();
        Document doc = null;

        try {
            doc = builder.build(rulesFile);
            rulesNamespace = Namespace.getNamespace("ul:rules");
        }
        catch (JDOMException e) {
            Console.printerrln("Invalid rules file.");
            Console.logException(e);
            return;
        }
        catch (IOException e) {
            Console.printerrln("Invalid rules file.");
            Console.logException(e);
            return;
        }

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

        generationRules = new ArrayList<EventGenerationRule>();

        for (Element ruleElement : ruleElements) {
            generationRules.add(new EventGenerationRule(ruleElement, rulesNamespace));
        }
    }

}
