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

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import org.apache.commons.lang.mutable.MutableInt;
import org.eclipse.uml2.uml.CallConcurrencyKind;
import org.eclipse.uml2.uml.Component;
import org.eclipse.uml2.uml.Connector;
import org.eclipse.uml2.uml.DataType;
import org.eclipse.uml2.uml.InstanceSpecification;
import org.eclipse.uml2.uml.InstanceValue;
import org.eclipse.uml2.uml.Interaction;
import org.eclipse.uml2.uml.Interface;
import org.eclipse.uml2.uml.Lifeline;
import org.eclipse.uml2.uml.LiteralBoolean;
import org.eclipse.uml2.uml.LiteralInteger;
import org.eclipse.uml2.uml.LiteralReal;
import org.eclipse.uml2.uml.LiteralString;
import org.eclipse.uml2.uml.Message;
import org.eclipse.uml2.uml.MessageOccurrenceSpecification;
import org.eclipse.uml2.uml.MessageSort;
import org.eclipse.uml2.uml.Model;
import org.eclipse.uml2.uml.Operation;
import org.eclipse.uml2.uml.Package;
import org.eclipse.uml2.uml.Parameter;
import org.eclipse.uml2.uml.PrimitiveType;
import org.eclipse.uml2.uml.Property;
import org.eclipse.uml2.uml.Slot;
import org.eclipse.uml2.uml.UMLPackage;
import org.w3c.dom.Element;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.plugin.http.SOAPUtils;
import de.ugoe.cs.autoquest.plugin.http.eventcore.SimpleSOAPEventType.CallType;
import de.ugoe.cs.util.StringTools;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * Utility class to create UML Interactions from event sequences
 * </p>
 * 
 * @author Steffen Herbold
 */
class UMLInteractionCreator {

    /**
     * model for which the interactions are created
     */
    private final Model model;

    /**
     * name of the test context in which the interactions are created
     */
    private final String testContextName;

    /**
     * defines whether random message bodies are used or the ones associated with the SOAP events
     */
    private final boolean useRandomMsgBodies;
    
    /**
     * TODO
     */
    private final Map<String, InstanceSpecification> instanceSpecificationCache;
    
    /**
     * <p>
     * Creates a new UMLInteractionCreator
     * </p>
     * 
     * @param model
     *            UML model to which the interaction is added
     * @param testContextName
     *            Name of the test context that should be used. If this value is null, the first
     *            test context found is used.
     * @param useRandomRequestBodies
     *            defines is random request bodies are used or the body of the associated event
     */
    public UMLInteractionCreator(Model model, String testContextName, boolean useRandomMsgBodies) {
        this.model = model;
        this.testContextName = testContextName;
        this.useRandomMsgBodies = useRandomMsgBodies;
        instanceSpecificationCache = new HashMap<String, InstanceSpecification>();
    }

    /**
     * <p>
     * Creates the UML Interaction for the given sequence
     * </p>
     * 
     * @param sequence
     *            for which the interaction is created
     * @param interactionName
     *            name of the interaction
     * @return created interaction
     */
    public Interaction createInteraction(List<Event> sequence, String interactionName) {
        final Component testContext = UMLUtils.fetchTestContext(model, testContextName);
        if (testContext == null) {
            throw new RuntimeException("Could not find any test context in the model");
        }

        final Operation operation = testContext.createOwnedOperation(interactionName, null, null);
        operation.applyStereotype(UTPUtils.getTestCaseStereotype(model));

        final Interaction interaction =
            (Interaction) testContext.createPackagedElement(interactionName + "_Impl",
                                                            UMLPackage.Literals.INTERACTION);
        operation.getMethods().add(interaction);

        // create lifelines
        Lifeline userLifeline = null;

        for (Property property : UMLUtils.fetchAllSUTProperties(testContext)) {
            String serviceName = property.getName();
            Lifeline targetLifeline = interaction.createLifeline(serviceName);
            targetLifeline.setRepresents(property);
        }
        for (Property property : UMLUtils.fetchAllTestComponentProperties(testContext)) {
            userLifeline = interaction.createLifeline(property.getName());
            userLifeline.setRepresents(property);
        }

        if (userLifeline == null) {
            throw new RuntimeException("No TestComponent found, could not create user lifeline.");
        }
        if (interaction.getLifelines().size() < 2) {
            throw new RuntimeException("Fewer than two lifelines created. No SUT found.");
        }

        int i = 0;
        for (Event event : sequence) {
            if (!(event.equals(Event.STARTEVENT) || event.equals(Event.ENDEVENT))) {
                String serviceName = SOAPUtils.getServiceNameFromEvent(event);
                String methodName = SOAPUtils.getCalledMethodFromEvent(event);
                String clientName = SOAPUtils.getClientNameFromEvent(event);
                String prefix = interactionName + "_" + i + "_" + methodName + "_";
                // determine lifelines
                Lifeline msgTargetLifeline;
                Lifeline msgSourceLifeline;

                msgSourceLifeline = interaction.getLifeline(clientName);
                msgTargetLifeline = interaction.getLifeline(serviceName);

                if (msgSourceLifeline == null) {
                    throw new RuntimeException(
                                               "Error creating message: could not determine source lifeline for component: " +
                                                   clientName);
                }
                if (msgTargetLifeline == null) {
                    throw new RuntimeException(
                                               "Error creating message: could not determine target lifeline for component: " +
                                                   serviceName);
                }
                // determine correct target interface
                Set<Interface> targetInterfaces =
                    UMLUtils.getRealizedInterfacesFromProperty((Property) msgTargetLifeline
                        .getRepresents());
                if (targetInterfaces.isEmpty()) {
                    throw new RuntimeException("no interface associated with the property " +
                        msgTargetLifeline.getRepresents().getName());
                }
                Interface targetInterface = null;
                for (Interface intface : targetInterfaces) {
                    if (UMLUtils.getOperationFromName(intface.getOperations(), methodName) != null)
                    {
                        // interface found
                        targetInterface = intface;
                        break;
                    }
                }
                if (targetInterface == null) {
                    StringBuilder errStrBuilder = new StringBuilder();
                    errStrBuilder
                        .append("Error creating message: operation not found in the implementing interfaces (");
                    Iterator<Interface> iter = targetInterfaces.iterator();
                    while (iter.hasNext()) {
                        String interfaceName = iter.next().getName();
                        errStrBuilder.append(interfaceName);
                        if (iter.hasNext()) {
                            errStrBuilder.append(",");
                        }
                        else {
                            errStrBuilder.append("): " + methodName);
                        }
                    }
                    throw new RuntimeException(errStrBuilder.toString());
                }

                Operation calledOperation =
                    UMLUtils.getOperationFromName(targetInterface.getOperations(), methodName);
                // get connector
                Connector connector =
                    UMLUtils.inferConnector(msgSourceLifeline, msgTargetLifeline, targetInterface);
                if (connector == null) {
                    throw new RuntimeException(
                                               "Error creating message: could not find connector between the two life lines that supports the target interface at the target lifeline");
                }

                boolean asynch = false;
                if (calledOperation.getConcurrency() == CallConcurrencyKind.CONCURRENT_LITERAL) {
                    asynch = true;
                }

                if (SOAPUtils.isSOAPRequest(event)) {
                    // setup for both SYNCH and ASYNCH calls
                    MessageOccurrenceSpecification callSendFragment =
                        (MessageOccurrenceSpecification) interaction
                            .createFragment(prefix + "callSendFragment",
                                            UMLPackage.Literals.MESSAGE_OCCURRENCE_SPECIFICATION);
                    MessageOccurrenceSpecification callRecvFragment =
                        (MessageOccurrenceSpecification) interaction
                            .createFragment(prefix + "callRecvFragment",
                                            UMLPackage.Literals.MESSAGE_OCCURRENCE_SPECIFICATION);

                    callSendFragment.setCovered(msgSourceLifeline);
                    callRecvFragment.setCovered(msgTargetLifeline);

                    // create call
                    Message callMessage = interaction.createMessage(prefix + "call");
                    callMessage.setSignature(calledOperation);
                    setMessageParameters(callMessage, calledOperation, event, prefix);
                    callMessage.setConnector(connector);
                    callMessage.setSendEvent(callSendFragment);
                    callMessage.setReceiveEvent(callRecvFragment);
                    callSendFragment.setMessage(callMessage);
                    callRecvFragment.setMessage(callMessage);

                    if (asynch) {
                        // Create ASYNCH call
                        callMessage.setMessageSort(MessageSort.ASYNCH_CALL_LITERAL);
                    }
                    else {
                        // SYNCH call
                        callMessage.setMessageSort(MessageSort.SYNCH_CALL_LITERAL);
                    }
                }
                if (!asynch && SOAPUtils.isSOAPResponse(event)) {
                    // setup reply and behavior execution specifications
                    MessageOccurrenceSpecification replySendFragment =
                        (MessageOccurrenceSpecification) interaction
                            .createFragment(prefix + "replySendFragment",
                                            UMLPackage.Literals.MESSAGE_OCCURRENCE_SPECIFICATION);
                    MessageOccurrenceSpecification replyRecvFragment =
                        (MessageOccurrenceSpecification) interaction
                            .createFragment(prefix + "replyRecvFragment",
                                            UMLPackage.Literals.MESSAGE_OCCURRENCE_SPECIFICATION);

                    replySendFragment.setCovered(msgTargetLifeline);
                    replyRecvFragment.setCovered(msgSourceLifeline);

                    /*
                     * BehaviorExecutionSpecification sourceBehaviorExecutionSpecification =
                     * (BehaviorExecutionSpecification) interaction .createFragment(":" + methodName
                     * + "_sourceBhvExecSpec",
                     * UMLPackage.Literals.BEHAVIOR_EXECUTION_SPECIFICATION);
                     * BehaviorExecutionSpecification targetBehaviorExecutionSpecification =
                     * (BehaviorExecutionSpecification) interaction .createFragment(":" + methodName
                     * + "_targetBhvExecSpec",
                     * UMLPackage.Literals.BEHAVIOR_EXECUTION_SPECIFICATION);
                     * 
                     * sourceBehaviorExecutionSpecification.setStart(callSendFragment);
                     * sourceBehaviorExecutionSpecification.setFinish(replyRecvFragment);
                     * targetBehaviorExecutionSpecification.setStart(callRecvFragment);
                     * targetBehaviorExecutionSpecification.setFinish(replySendFragment);
                     */

                    // create reply
                    Message replyMessage = interaction.createMessage(prefix + "_reply");
                    replyMessage.setMessageSort(MessageSort.REPLY_LITERAL);
                    replyMessage.setSignature(calledOperation);
                    // setReplyMessageParameters(replyMessage, calledOperation);
                    setMessageParameters(replyMessage, calledOperation, event, prefix);
                    replyMessage.setConnector(connector);
                    replyMessage.setSendEvent(replySendFragment);
                    replyMessage.setReceiveEvent(replyRecvFragment);
                    replySendFragment.setMessage(replyMessage);
                    replyRecvFragment.setMessage(replyMessage);
                }

                i++;
            }
        }
        return interaction;
    }

    /**
     * <p>
     * Sets values for the parameters of a call message. The values are, if possible, inferred from
     * the event that is provided.
     * </p>
     * 
     * @param message
     *            call message for which the parameters are set
     * @param calledOperation
     *            operation that is called by the message
     * @param event
     *            event that provides the parameters; in case of null, default values are assumed
     * @param prefix
     *            prefix of the call message; used to create good warnings and debugging information
     */
    private void setMessageParameters(Message message,
                                      Operation calledOperation,
                                      Event event,
                                      String prefix)
    {
        Element requestBody;
        if (SOAPUtils.isSOAPRequest(event)) {
            requestBody =
                SOAPUtils.getSoapBodyFromEvent(event, useRandomMsgBodies, CallType.REQUEST);
        }
        else {
            requestBody =
                SOAPUtils.getSoapBodyFromEvent(event, useRandomMsgBodies, CallType.RESPONSE);
        }
        Package instSpecPkg = null;
        MutableInt instSpecNumber = new MutableInt(0);

        // Set parameters of operation
        for (Parameter param : calledOperation.getOwnedParameters()) {
            if (instSpecPkg == null) {
                instSpecPkg = getOrCreateInstanceSpecificationPackage(event);
            }

            String path = calledOperation.getName() + ":" + param.getType().getName();
            if ((UMLUtils.isInParameter(param) && SOAPUtils.isSOAPRequest(event)) ||
                (UMLUtils.isOutParameter(param) && SOAPUtils.isSOAPResponse(event)))
            {

                // create parameters node
                if (!(param.getType() instanceof DataType)) {
                    throw new RuntimeException("TODO error handling; parameters missing");
                }
                DataType parametersNode = (DataType) param.getType();
                InstanceSpecification instSpecParameters =
                    (InstanceSpecification) instSpecPkg
                        .createPackagedElement(prefix + "instspec" + instSpecNumber.intValue() +
                                                   "_" + param.getType().getName(),
                                               UMLPackage.Literals.INSTANCE_SPECIFICATION);
                instSpecParameters.getClassifiers().add((DataType) param.getType());
                instSpecNumber.setValue(instSpecNumber.intValue() + 1);
                InstanceValue instanceValue =
                    (InstanceValue) message.createArgument(param.getName(), param.getType(),
                                                           UMLPackage.Literals.INSTANCE_VALUE);
                instanceValue.setInstance(instSpecParameters);

                for (Property internalParameter : parametersNode.getAllAttributes()) {
                    if (internalParameter.getType() instanceof DataType) {
                        List<org.w3c.dom.Element> paramNodes =
                            SOAPUtils.getMatchingChildNode(internalParameter.getType().getName(),
                                                           requestBody);
                        int multiplicityChosen = paramNodes.size();

                        if (multiplicityChosen == 0 && internalParameter.getLower() > 0) {
                            Console
                                .traceln(Level.WARNING,
                                         "required attribute not found in SOAP message: " + path);
                            Console
                                .traceln(Level.WARNING,
                                         "setting default values for this attribute and all its children");
                            Console.traceln(Level.FINE, "XML structure of path:" +
                                StringTools.ENDLINE + SOAPUtils.getSerialization(requestBody));
                            multiplicityChosen = internalParameter.getLower();
                        }
                        for (int i = 0; i < multiplicityChosen; i++) {
                            org.w3c.dom.Element paramNode = null;
                            if (!paramNodes.isEmpty()) {
                                paramNode = paramNodes.get(i);
                            }

                            Slot slot = instSpecParameters.createSlot();
                            slot.setDefiningFeature(internalParameter);

                            InstanceValue value =
                                (InstanceValue) slot
                                    .createValue(internalParameter.getName() + "_" + i,
                                                 internalParameter.getType(),
                                                 UMLPackage.Literals.INSTANCE_VALUE);
                            InstanceSpecification instSpec = createInstanceSpecification((DataType) internalParameter
                                                                                        .getType(),
                                                                                        instSpecPkg, prefix,
                                                                                        instSpecNumber, paramNode,
                                                                                        path);
                            value.setInstance(instSpec);
                            /*
                             * InstanceValue value = (InstanceValue) argument .createOperand(null,
                             * internalParameter.getType(), UMLPackage.Literals.INSTANCE_VALUE);
                             * value.setInstance(instSpec);
                             */
                        }
                    }
                    else if (internalParameter.getType() instanceof PrimitiveType) {
                        createSlotPrimitiveType(instSpecParameters, internalParameter, requestBody,
                                                path);
                    }
                }
            }
            else {
                // set literalNull for out and return parameters
                // argument.createOperand(null, param.getType(), UMLPackage.Literals.LITERAL_NULL);
                message.createArgument(param.getName(), param.getType(),
                                       UMLPackage.Literals.LITERAL_NULL);
            }
        }
    }

    /**
     * <p>
     * Creates an {@link InstanceSpecification} for a data type in the given package. The values are
     * inferred, if possible, from the DOM node. The prefix and the path are used for naming the
     * instance specification and to provide good warnings and debug information in case of
     * problems.
     * </p>
     * 
     * @param type
     *            DataType for which the {@link InstanceSpecification} is created
     * @param pkg
     *            package in which the {@link InstanceSpecification} is created
     * @param prefix
     *            prefix used for naming the {@link InstanceSpecification}
     * @param currentNode
     *            node of a DOM from which values are inferred
     * @param path
     *            used for warnings and debug information
     * @return {@link InstanceSpecification} for the given type
     */
    private InstanceSpecification createInstanceSpecification(DataType type,
                                                              Package pkg,
                                                              String prefix,
                                                              MutableInt instSpecNumber,
                                                              org.w3c.dom.Element currentNode,
                                                              String path)
    {
        if( instanceSpecificationCache.containsKey(SOAPUtils.getSerialization(currentNode)) ) {
            return instanceSpecificationCache.get(SOAPUtils.getSerialization(currentNode));
        }
        if ("".equals(path)) {
            path = type.getName();
        }

        InstanceSpecification instSpec =
            (InstanceSpecification) pkg
                .createPackagedElement(prefix + "instspec" + instSpecNumber.intValue() + "_" +
                                           type.getName(),
                                       UMLPackage.Literals.INSTANCE_SPECIFICATION);
        instSpec.getClassifiers().add(type);
        instSpecNumber.setValue(instSpecNumber.intValue() + 1);
        for (Property prop : type.getAllAttributes()) {
            if (prop.getType() instanceof PrimitiveType) {
                createSlotPrimitiveType(instSpec, prop, currentNode, path);
            }
            else if (prop.getType() instanceof DataType) {
                List<org.w3c.dom.Element> attributeNodes = null;
                int multiplicityChosen = 0;
                if (currentNode != null) {
                    attributeNodes = SOAPUtils.getMatchingChildNode(prop.getName(), currentNode);
                    multiplicityChosen = attributeNodes.size();
                }

                if (multiplicityChosen == 0 && prop.getLower() > 0) {
                    if (currentNode != null) {
                        Console.traceln(Level.WARNING,
                                        "required attribute not found in SOAP message: " + path +
                                            "." + prop.getName());
                        Console
                            .traceln(Level.WARNING,
                                     "setting default values for this attribute and all its children");
                        Console.traceln(Level.FINE, "XML structure of path:" + StringTools.ENDLINE +
                            SOAPUtils.getSerialization(currentNode));
                    }
                    multiplicityChosen = prop.getLower();
                }
                for (int i = 0; i < multiplicityChosen; i++) {
                    org.w3c.dom.Element attributeNode = null;
                    if (attributeNodes != null && !attributeNodes.isEmpty()) {
                        attributeNode = attributeNodes.get(i);
                    }

                    Slot slot = instSpec.createSlot();
                    slot.setDefiningFeature(prop);

                    InstanceValue value =
                        (InstanceValue) slot.createValue(prop.getName() + "_" + i, prop.getType(),
                                                         UMLPackage.Literals.INSTANCE_VALUE);
                    value.setInstance(createInstanceSpecification((DataType) prop.getType(), pkg,
                                                                  prefix, instSpecNumber,
                                                                  attributeNode,
                                                                  path + "." + prop.getName()));
                }
            }
            else {
                Console.traceln(Level.SEVERE, "property neither DataType nor PrimitiveType: " +
                    prop.getType());
                throw new RuntimeException(
                                           "can only handle DataType and PrimitiveType properties but was: " +
                                               prop.getType().getClass().getName());
            }
        }
        instanceSpecificationCache.put(SOAPUtils.getSerialization(currentNode), instSpec);
        return instSpec;
    }

    /**
     * <p>
     * Creates a {@link Slot} in an {@link InstanceSpecification} for a primitive type.
     * </p>
     * 
     * @param instSpec
     *            instance specification to which the slot is added
     * @param prop
     *            property that describes the slot
     * @param currentNode
     *            DOM node from which is value for the slot is inferred
     * @param path
     *            used for warnings and debug information
     */
    private static void createSlotPrimitiveType(InstanceSpecification instSpec,
                                                Property prop,
                                                org.w3c.dom.Element currentNode,
                                                String path)
    {
        List<String> attributeValues = SOAPUtils.getValuesFromElement(prop.getName(), currentNode);

        if (attributeValues.isEmpty()) {
            if (prop.getLower() == 0) {
                // ignoring optional attribute
                return;
            }
            else {
                if (currentNode != null) {
                    Console.traceln(Level.WARNING,
                                    "required attribute not found in SOAP message: " + path + "." +
                                        prop.getName());
                    Console.traceln(Level.WARNING, "setting default values for this attribute");
                    Console.traceln(Level.FINE, "XML structure of path:" + StringTools.ENDLINE +
                        SOAPUtils.getSerialization(currentNode));
                }
                attributeValues.add(null);
            }
        }
        for (String attributeValue : attributeValues) {
            Slot slot = instSpec.createSlot();
            slot.setDefiningFeature(prop);
            if ("String".equals(prop.getType().getName())) {
                LiteralString value =
                    (LiteralString) slot.createValue(prop.getName(), null,
                                                     UMLPackage.Literals.LITERAL_STRING);
                if (attributeValue != null) {
                    value.setValue(attributeValue);
                }
                else {
                    value.setValue("foobar");
                }
            }
            else if ("Integer".equals(prop.getType().getName())) {
                LiteralInteger value =
                    (LiteralInteger) slot.createValue(prop.getName(), null,
                                                      UMLPackage.Literals.LITERAL_INTEGER);
                if (attributeValue != null) {
                    value.setValue(Integer.parseInt(attributeValue));
                }
                else {
                    value.setValue(42);
                }
            }
            else if ("Boolean".equals(prop.getType().getName())) {
                LiteralBoolean value =
                    (LiteralBoolean) slot.createValue(prop.getName(), null,
                                                      UMLPackage.Literals.LITERAL_BOOLEAN);
                if (attributeValue != null) {
                    value.setValue(Boolean.parseBoolean(attributeValue));
                }
                else {
                    value.setValue(true);
                }
            }
            else if ("Real".equals(prop.getType().getName())) {
                LiteralReal value =
                    (LiteralReal) slot.createValue(prop.getName(), null,
                                                   UMLPackage.Literals.LITERAL_REAL);
                if (attributeValue != null) {
                    value.setValue(Double.parseDouble(attributeValue));
                }
                else {
                    value.setValue(3.14);
                }
            }
            else {
                Console.traceln(Level.SEVERE, "could not create literal for primitive type: " +
                    prop.getType().getName());
                throw new RuntimeException("unknown primitive type: " + prop.getType().getName());
            }
        }
    }

    /**
     * <p>
     * Gets or creates a {@link Package} for {@link InstanceSpecification} created by the
     * usage-based testing. Each service gets its own sub-package within a package called
     * UBT_InstanceSpecifications. "
     * </p>
     * 
     * @param event
     *            event from which the service name is inferred
     * @return package for the {@link InstanceSpecification}s
     */
    private Package getOrCreateInstanceSpecificationPackage(Event event) {
        String pkgUBTInstSpecs = "UBT_InstanceSpecifications";
        Package ubtInstSpecPkg = (Package) model.getOwnedMember(pkgUBTInstSpecs);
        if (ubtInstSpecPkg == null) {
            ubtInstSpecPkg =
                (Package) model.createPackagedElement(pkgUBTInstSpecs, UMLPackage.Literals.PACKAGE);
        }
        String serviceName = SOAPUtils.getServiceNameFromEvent(event);
        Package serviceInstSpecPkg = (Package) ubtInstSpecPkg.getOwnedMember(serviceName);
        if (serviceInstSpecPkg == null) {
            serviceInstSpecPkg =
                (Package) ubtInstSpecPkg.createPackagedElement(serviceName,
                                                               UMLPackage.Literals.PACKAGE);
        }
        return serviceInstSpecPkg;
    }

}
