//   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.util.ArrayList;
import java.util.List;

import org.jdom.Element;
import org.jdom.Namespace;

import de.ugoe.cs.autoquest.plugin.mfc.eventcore.WindowsMessageType;

/**
 * <p>
 * This class defines rules for the generation of MFC events.
 * </p>
 * 
 * @version 1.0
 * @author Steffen Herbold, Patrick Harms
 */
class EventGenerationRule {

    /**
     * <p>
     * Namespace used for parsing the rule.
     * </p>
     */
    private Namespace namespace;

    /**
     * <p>
     * Name of the rule.
     * </p>
     */
    private String name;

    /**
     * <p>
     * List of conditions for the rule to be matched.
     * </p>
     */
    private List<MessageCondition> messageConditions;

    /**
     * <p>
     * List of parameters to be provided to the generated event.
     * </p>
     */
    private List<Term> eventParameters;

    /**
     * <p>
     * List of replay message generation rules.
     * </p>
     */
    private List<ReplayMessageSpec> replayMessageSpecifications;

    /**
     * <p>
     * Constructor. Creates a new EventGenerationRule.
     * </p>
     * 
     * @param ruleElement
     *            the JDOM element that descripes the rule
     * @param rulesNamespace
     *            the XML namespace the rule is defined in
     */
    @SuppressWarnings("unchecked")
    EventGenerationRule(Element ruleElement, Namespace rulesNamespace) {
        this.namespace = rulesNamespace;

        this.name = ruleElement.getAttributeValue("name");

        this.messageConditions = new ArrayList<MessageCondition>();
        this.eventParameters = new ArrayList<Term>();
        this.replayMessageSpecifications = new ArrayList<ReplayMessageSpec>();

        for (Element child : (List<Element>) ruleElement.getChildren()) {
            if ("msg".equals(child.getName()) && namespace.equals(child.getNamespace())) {
                messageConditions.add(new MessageCondition(child));
            }
            else if ("idinfo".equals(child.getName()) && namespace.equals(child.getNamespace())) {
                for (Element parameterElements : (List<Element>) child.getChildren()) {
                    eventParameters.add(new Term(parameterElements));
                }
            }
            else if ("genMsg".equals(child.getName()) && namespace.equals(child.getNamespace())) {
                replayMessageSpecifications.add(new ReplayMessageSpec(child));
            }
            else if ("genMsgSeq".equals(child.getName()) && namespace.equals(child.getNamespace()))
            {
                replayMessageSpecifications.add(new ReplayMessageSpec(child));
            }
            else {
                throw new IllegalArgumentException(
                                                   "the provided rules can not be parsed: unknown element " +
                                                       child.getName());
            }
        }
    }

    /**
     * <p>
     * Returns the name of the rule.
     * </p>
     * 
     * @return the name
     */
    String getName() {
        return name;
    }

    /**
     * <p>
     * Returns the conditions on the matched messages defined by this rule.
     * </p>
     * 
     * @return the message conditions
     */
    List<MessageCondition> getMessageConditions() {
        return messageConditions;
    }

    /**
     * <p>
     * Returns the parameters of the event generated by this rule.
     * </p>
     * 
     * @return the event parameters
     */
    List<Term> getEventParameters() {
        return eventParameters;
    }

    /**
     * <p>
     * Returns the replay specification defined by this rule.
     * </p>
     * 
     * @return the replay specification
     */
    List<ReplayMessageSpec> getReplayMessageSpecifications() {
        return replayMessageSpecifications;
    }

    /**
     * <p>
     * Helper class that describes conditions on the message sequence when matching this rule.
     * </p>
     * 
     * @version 1.0
     * @author Steffen Herbold, Patrick Harms
     */
    class MessageCondition {

        /**
         * <p>
         * True, if the condition defines to match several conditions
         * </p>
         */
        private boolean matchMultiple;

        /**
         * <p>
         * Type of the message matched by the condition
         * </p>
         */
        private WindowsMessageType messageType;

        /**
         * <p>
         * Term conditions associated with the rule condition
         * </p>
         */
        private List<AttributeCondition> attributeConditions;

        /**
         * <p>
         * List of messages to be stored, if the message matches, for continuing the rule
         * application
         * </p>
         */
        private ArrayList<Term> messagesToStore;

        /**
         * <p>
         * Constructor. Creates a new MessageCondition.
         * </p>
         * 
         * @param msgChild
         *            JDOM element that describes the message condition
         */
        @SuppressWarnings("unchecked")
        private MessageCondition(Element msgChild) {
            this.matchMultiple = "true".equals(msgChild.getAttributeValue("multiple"));
            this.messageType =
                WindowsMessageType.parseMessageType(msgChild.getAttributeValue("type"));

            this.attributeConditions = new ArrayList<AttributeCondition>();
            for (Element childElement : (List<Element>) msgChild.getChildren("equals", namespace)) {
                attributeConditions.add(new AttributeCondition(childElement));
            }

            this.messagesToStore = new ArrayList<Term>();
            for (Element childElement : (List<Element>) msgChild.getChildren("store", namespace)) {
                messagesToStore.add(new Term(childElement));
            }
            for (Element childElement : (List<Element>) msgChild.getChildren("storeSeq", namespace))
            {
                messagesToStore.add(new Term(childElement));
            }
        }

        /**
         * <p>
         * Returns whether a single message is matched to the condition or a whole sequence can be
         * matched.
         * </p>
         * 
         * @return true if multiple message shall be matched, false if only a single message is
         *         matched
         */
        boolean matchMultiple() {
            return matchMultiple;
        }

        /**
         * <p>
         * Returns the type of the matched messages.
         * </p>
         * 
         * @return the message type
         */
        WindowsMessageType getMessageType() {
            return messageType;
        }

        /**
         * <p>
         * Returns the attribute conditions of the message condition.
         * </p>
         * 
         * @return the attribute conditions
         */
        List<AttributeCondition> getAttributeConditions() {
            return attributeConditions;
        }

        /**
         * <p>
         * Returns messages, that have eventually been stored as part of the condition.
         * </p>
         * 
         * @return the stored messages
         */
        ArrayList<Term> getMessagesToStore() {
            return messagesToStore;
        }

    }

    /**
     * <p>
     * Helper class that defines attribute conditions for matching messages.
     * </p>
     * 
     * @version 1.0
     * @author Steffen Herbold, Patrick Harms
     */
    class AttributeCondition {

        /**
         * <p>
         * Left hand side of the condition.
         * </p>
         */
        private Term leftHandSide;

        /**
         * <p>
         * Reft hand side of the condition.
         * </p>
         */
        private Term rightHandSide;

        /**
         * <p>
         * Constructor. Creates a new AttributeCondition.
         * </p>
         * 
         * @param conditionElement
         *            JDOM element that describes the condition
         */
        private AttributeCondition(Element conditionElement) {
            this.leftHandSide = new Term((Element) conditionElement.getChildren().get(0));
            this.rightHandSide = new Term((Element) conditionElement.getChildren().get(1));
        }

        /**
         * <p>
         * Returns the left hand side of the condition.
         * </p>
         * 
         * @return the left hand side
         */
        Term getLeftHandSide() {
            return leftHandSide;
        }

        /**
         * <p>
         * Returns the right hand side of the condition.
         * </p>
         * 
         * @return the right hand side
         */
        Term getRightHandSide() {
            return rightHandSide;
        }

    }

    /**
     * <p>
     * Helper class that defines terms to define conditions.
     * </p>
     * 
     * @version 1.0
     * @author Steffen Herbold, Patrick Harms
     */
    class Term {

        /**
         * <p>
         * Name of the term.
         * </p>
         */
        private String name;

        /**
         * <p>
         * Value of the term, if it is a constValue; null otherwise.
         * </p>
         */
        private String value;

        /**
         * <p>
         * Variable name of the object, i.e. a message, of which a parameter is identified if the
         * term is a winInfoValue or a msgInfoValue; null otherwise.
         * </p>
         */
        private String messageId;

        /**
         * <p>
         * Name of the parameter of the object, i.e., a message, of which a parameter is identified
         * if the term is a paramValue; null otherwise.
         * </p>
         */
        private String messageParameterName;

        /**
         * <p>
         * Variable name of the message sequence denoted by the term in case of a seqValue; null
         * otherwise.
         * </p>
         */
        private String sequenceId;

        /**
         * <p>
         * Name of the parameter of the sequence of which a parameter is identified if the term is a
         * seqValue; null otherwise.
         * </p>
         */
        private String sequenceParameterName;

        /**
         * <p>
         * Name of the parameter of the window of the object, e.g. a message, of which a parameter
         * is identified if the term is a winInfoValue; null otherwise.
         * </p>
         */
        private String windowParameterName;

        /**
         * <p>
         * Name of the info of the message of which a parameter is identified if the term is a
         * msgInfoValue; null otherwise.
         * </p>
         */
        private String messageInfoName;

        /**
         * <p>
         * Name of the parameter of the message into which a value shall be stored if the term is a
         * resolveHwnd, null otherwise
         * </p>
         */
        private String storeParameterName;

        /**
         * <p>
         * List of handles to be resolved in case the term is a store or storeSeq; null otherwise.
         * </p>
         */
        private List<Term> resolveHandles;

        /**
         * <p>
         * Constructor. Creates a new Term.
         * </p>
         * 
         * @param termElement
         *            JDOM element that describes the term
         */
        @SuppressWarnings("unchecked")
        private Term(Element termElement) {
            this.name = termElement.getName();

            if ("constValue".equals(name)) {
                this.value = termElement.getAttributeValue("value");
            }
            else if ("paramValue".equals(name)) {
                this.messageId = termElement.getAttributeValue("obj");
                this.messageParameterName = termElement.getAttributeValue("param");
            }
            else if ("winInfoValue".equals(name)) {
                this.messageId = termElement.getAttributeValue("obj");
                this.windowParameterName = termElement.getAttributeValue("winParam");
            }
            else if ("msgInfoValue".equals(name)) {
                this.messageId = termElement.getAttributeValue("obj");
                this.messageInfoName = termElement.getAttributeValue("msgParam");
            }
            else if ("seqValue".equals(name)) {
                this.sequenceId = termElement.getAttributeValue("seqObj");
                this.sequenceParameterName = termElement.getAttributeValue("param");
            }
            else if ("store".equals(name)) {
                this.messageId = termElement.getAttributeValue("var");
                if ((termElement.getChildren() != null) && (termElement.getChildren().size() > 0)) {
                    this.resolveHandles = new ArrayList<Term>();
                    for (Element child : (List<Element>) termElement.getChildren()) {
                        this.resolveHandles.add(new Term(child));
                    }
                }
            }
            else if ("storeSeq".equals(name)) {
                this.sequenceId = termElement.getAttributeValue("varSeq");
                if ((termElement.getChildren() != null) && (termElement.getChildren().size() > 0)) {
                    this.resolveHandles = new ArrayList<Term>();
                    for (Element child : (List<Element>) termElement.getChildren()) {
                        this.resolveHandles.add(new Term(child));
                    }
                }
            }
            else if ("resolveHwnd".equals(name)) {
                this.messageParameterName = termElement.getAttributeValue("param");
                this.storeParameterName = termElement.getAttributeValue("storeParam");
            }
        }

        /**
         * <p>
         * Returns the name of the term.
         * </p>
         * 
         * @return the name
         */
        String getName() {
            return name;
        }

        /**
         * <p>
         * Returns the value of the term.
         * </p>
         * 
         * @return the value
         */
        String getValue() {
            return value;
        }

        /**
         * <p>
         * Returns the object Id of the message, which is resolved as part of this term.
         * </p>
         * 
         * @return the object Id
         */
        String getMessageId() {
            return messageId;
        }

        /**
         * <p>
         * Returns the name of the message parameter that is resolved as part of this term.
         * </p>
         * 
         * @return the message parameter name
         */
        String getMessageParameterName() {
            return messageParameterName;
        }

        /**
         * <p>
         * Returns the object Id of the message sequence, which is resolved as part of this term.
         * </p>
         * 
         * @return the object Id
         */
        String getSequenceId() {
            return sequenceId;
        }

        /**
         * <p>
         * Returns the name of the message parameter that is resolved as part of this term.
         * </p>
         * 
         * @return the sequenceParameter
         */
        String getSequenceParameterName() {
            return sequenceParameterName;
        }

        /**
         * <p>
         * Returns the window parameter name that is resolved as part of this term.
         * </p>
         * 
         * @return the name of the window parameter
         */
        String getWindowParameterName() {
            return windowParameterName;
        }

        /**
         * <p>
         * Returns the name of the message info value that is resolved as part of this term.
         * </p>
         * 
         * @return the name of the message info value
         */
        String getMessageInfoName() {
            return messageInfoName;
        }

        /**
         * <p>
         * Returns the object Id under which a message will be stored.
         * </p>
         * 
         * @return the object Id
         */
        String getStoreParameterName() {
            return storeParameterName;
        }

        /**
         * <p>
         * Returns all terms that are responsible to resolve HWNDs.
         * </p>
         * 
         * @return the terms
         */
        List<Term> getResolveHandles() {
            return resolveHandles;
        }

    }

    /**
     * <p>
     * Helper class that defines the replay specification part of rules.
     * </p>
     * 
     * @version 1.0
     * @author Steffen Herbold, Patrick Harms
     */
    class ReplayMessageSpec {

        /**
         * <p>
         * Determines if this specification defines one, or a sequence of messages.
         * </p>
         */
        private boolean generateSingleMessage;

        /**
         * <p>
         * Object Id of a concrete message of message sequence to be replayed as is.
         * </p>
         */
        private String replayObjectId;

        /**
         * <p>
         * Term describing the type of the generated message.
         * </p>
         */
        private Term type;

        /**
         * <p>
         * Term describing the target of the generated message.
         * </p>
         */
        private Term target;

        /**
         * <p>
         * Term describing the LO word of the LParam of the generated message.
         * </p>
         */
        private Term lparamLoWord;

        /**
         * <p>
         * Term describing the HI word of the LParam of the generated message.
         * </p>
         */
        private Term lparamHiWord;

        /**
         * <p>
         * Term describing the LParam of the generated message.
         * </p>
         */
        private Term lparam;

        /**
         * <p>
         * Term describing the LO word of the WParam of the generated message.
         * </p>
         */
        private Term wparamLoWord;

        /**
         * <p>
         * Term describing the HI word of the WParam of the generated message.
         * </p>
         */
        private Term wparamHiWord;

        /**
         * <p>
         * Term describing the WParam of the generated message.
         * </p>
         */
        private Term wparam;

        /**
         * <p>
         * Value in milliseconds that the replay waits until the the next message is replayed.
         * </p>
         */
        private int delay;

        /**
         * <p>
         * Constructor. Creates a new ReplayMessageSpec.
         * </p>
         * 
         * @param replayMessageSpecElement
         *            JDOM element that describes the replay message specification
         */
        @SuppressWarnings("unchecked")
        private ReplayMessageSpec(Element replayMessageSpecElement) {
            List<Element> children = replayMessageSpecElement.getChildren();
            if ("genMsg".equals(replayMessageSpecElement.getName())) {
                generateSingleMessage = true;
                if (children.size() == 1) {
                    replayObjectId = children.get(0).getAttributeValue("obj");
                }
            }
            else {
                generateSingleMessage = false;
                if (children.size() == 1) {
                    replayObjectId = children.get(0).getAttributeValue("seqObj");
                }
            }

            this.delay = Integer.parseInt(replayMessageSpecElement.getAttributeValue("delay"));

            if (children.size() > 1) {
                for (Element child : children) {
                    Element termElement = (Element) child.getChildren().get(0);

                    if (child.getName().equals("type")) {
                        this.type = new Term(termElement);
                    }
                    else if (child.getName().equals("target")) {
                        this.target = new Term(termElement);

                        if (!generateSingleMessage) {
                            // in this case, the target is always a sequence value term, i.e.
                            // the targets of the originally recorded sequence. So the
                            // replay object id is set to this sequence
                            replayObjectId = target.getSequenceId();
                        }
                    }
                    else if (child.getName().equals("LPARAM")) {
                        Element loWordElement = child.getChild("LOWORD", namespace);
                        if (loWordElement != null) {
                            this.lparamLoWord =
                                new Term((Element) loWordElement.getChildren().get(0));
                        }

                        Element hiWordElement = child.getChild("HIWORD", namespace);
                        if (hiWordElement != null) {
                            this.lparamHiWord =
                                new Term((Element) hiWordElement.getChildren().get(0));
                        }

                        if ((lparamLoWord == null) && (lparamHiWord == null)) {
                            this.lparam = new Term(termElement);
                        }
                    }
                    else if (child.getName().equals("WPARAM")) {
                        Element loWordElement = child.getChild("LOWORD", namespace);
                        if (loWordElement != null) {
                            this.wparamLoWord =
                                new Term((Element) loWordElement.getChildren().get(0));
                        }

                        Element hiWordElement = child.getChild("HIWORD", namespace);
                        if (hiWordElement != null) {
                            this.wparamHiWord =
                                new Term((Element) hiWordElement.getChildren().get(0));
                        }

                        if ((wparamLoWord == null) && (wparamHiWord == null)) {
                            this.wparam = new Term(termElement);
                        }
                    }
                }
            }
        }

        /**
         * <p>
         * Determines if this specification defines one, or a sequence of messages.
         * </p>
         * 
         * @return true if only a single message is generated; false if a sequence is generated
         */
        boolean generateSingleMessage() {
            return generateSingleMessage;
        }

        /**
         * <p>
         * Returns the object Id from which the message is generated.
         * </p>
         * 
         * @return the object Id
         */
        String getReplayObjectId() {
            return replayObjectId;
        }

        /**
         * <p>
         * Returns the term that describes the type of the generated message.
         * </p>
         * 
         * @return the type term
         */
        Term getType() {
            return type;
        }

        /**
         * <p>
         * Returns the term that describes the target of the generated message.
         * </p>
         * 
         * @return the target term
         */
        Term getTarget() {
            return target;
        }

        /**
         * <p>
         * Returns the term that describes the LO word of the LParam of the generated message.
         * </p>
         * 
         * @return the LParam LO word term
         */
        Term getLparamLoWord() {
            return lparamLoWord;
        }

        /**
         * <p>
         * Returns the term that describes the HI word of the LParam of the generated message.
         * </p>
         * 
         * @return the LParam HI word term
         */
        Term getLparamHiWord() {
            return lparamHiWord;
        }

        /**
         * <p>
         * Returns the term that describes the LParam of the generated message.
         * </p>
         * 
         * @return the LParam term
         */
        Term getLparam() {
            return lparam;
        }

        /**
         * <p>
         * Returns the term that describes the LO word of the WParam of the generated message.
         * </p>
         * 
         * @return the WParam LO word term
         */
        Term getWparamLoWord() {
            return wparamLoWord;
        }

        /**
         * <p>
         * Returns the term that describes the HI word of the WParam of the generated message.
         * </p>
         * 
         * @return the WParam HI word term
         */
        Term getWparamHiWord() {
            return wparamHiWord;
        }

        /**
         * <p>
         * Returns the term that describes the WParam of the generated message.
         * </p>
         * 
         * @return the WParam term
         */
        Term getWparam() {
            return wparam;
        }

        /**
         * <p>
         * Returns the delay during the replay after this message is sent.
         * </p>
         * 
         * @return the delay
         */
        int getDelay() {
            return delay;
        }

    }
}
