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

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.plugin.http.eventcore.SOAPEventType;
import de.ugoe.cs.autoquest.plugin.http.eventcore.SimpleSOAPEventType;

/**
 * <p>
 * Utilities for working with SOAP events. Their main task is to simply working with
 * {@link SimpleSOAPEventType} and {@link SOAPEventType}.
 * </p>
 * 
 * @author Steffen Herbold
 */
public class SOAPUtils {

    /**
     * <p>
     * Replaces events with a {@link SOAPEventType} with new events of {@link SimpleSOAPEventType}
     * and no target.
     * </p>
     * 
     * @param sequence
     *            sequence where the events are replaced
     * @param keepOnlySOAPEvents
     *            if true, all events that are not of SOAPEventType or SimpleSOAPEventType are
     *            removed
     * @return sequence with {@link SimpleSOAPEventType}s instead of {@link SOAPEventType}s
     */
    public static List<Event> convertToSimpleSOAPEvent(List<Event> sequence,
                                                       boolean keepOnlySOAPEvents)
    {
        List<Event> newSequence = null;
        if (sequence != null) {
            newSequence = new LinkedList<>();
            for (Event event : sequence) {
                if (event.getType() instanceof SOAPEventType) {
                    SOAPEventType eventType = (SOAPEventType) event.getType();
                    newSequence.add(new Event(new SimpleSOAPEventType(eventType.getCalledMethod(),
                                                                      eventType.getServiceName(),
                                                                      eventType.getClientName(),
                                                                      eventType
                                                                          .getSoapRequestBody())));
                }
                else {
                    if (!keepOnlySOAPEvents || event.getType() instanceof SimpleSOAPEventType) {
                        newSequence.add(event);
                    }
                }
            }
        }
        return newSequence;
    }

    /**
     * <p>
     * Removes all non SOAP events from a sequence. Warning: this change is performed in-place!
     * </p>
     * 
     * @param sequence
     *            sequence where the events are replaced
     */
    public static void removeNonSOAPEvents(List<Event> sequence) {
        for (Iterator<Event> eventIter = sequence.iterator(); eventIter.hasNext();) {
            Event event = eventIter.next();
            if (!(event.getType() instanceof SOAPEventType)) {
                eventIter.remove();
            }
        }
    }

    /**
     * <p>
     * Helper function to get the name of a service from a SOAP event.
     * </p>
     * 
     * @param event
     *            event for which the service name is retrieved
     * @return service name
     */
    public static String getServiceNameFromEvent(Event event) {
        if (event.getType() instanceof SOAPEventType) {
            return ((SOAPEventType) event.getType()).getServiceName();
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            return ((SimpleSOAPEventType) event.getType()).getServiceName();
        }
        else {
            throw new RuntimeException(
                                       "Wrong event type. Only SOAPEventType and SimpleSOAPEventType supported but was: " +
                                           event.getType().getClass().getName());
        }
    }

    /**
     * 
     * <p>
     * Helper function to get the called method from a SOAP event
     * </p>
     * 
     * @param event
     *            event for which the called method is retrieved
     * @return called method
     */
    public static String getCalledMethodFromEvent(Event event) {
        if (event.getType() instanceof SOAPEventType) {
            return ((SOAPEventType) event.getType()).getCalledMethod();
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            return ((SimpleSOAPEventType) event.getType()).getCalledMethod();
        }
        else {
            throw new RuntimeException(
                                       "Wrong event type. Only SOAPEventType and SimpleSOAPEventType supported but was: " +
                                           event.getType().getClass().getName());
        }
    }

    /**
     * <p>
     * Helper function to get the name of a client from a SOAP event.
     * </p>
     * 
     * @param event
     *            event for which the client name is retrieved
     * @return service name
     */
    public static String getClientNameFromEvent(Event event) {
        if (event.getType() instanceof SOAPEventType) {
            return ((SOAPEventType) event.getType()).getClientName();
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            return ((SimpleSOAPEventType) event.getType()).getClientName();
        }
        else {
            throw new RuntimeException(
                                       "Wrong event type. Only SOAPEventType and SimpleSOAPEventType supported but was: " +
                                           event.getType().getClass().getName());
        }
    }

    /**
     * <p>
     * Helper function to get the body of a SOAP request.
     * </p>
     * 
     * @param event
     *            event for which the SOAP request body is retrieved
     * @return body of the SOAP event
     */
    public static Element getSoapRequestBodyFromEvent(Event event) {
        Element requestBody = null;
        if (event.getType() instanceof SOAPEventType) {
            requestBody = ((SOAPEventType) event.getType()).getSoapRequestBody();
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            requestBody = ((SimpleSOAPEventType) event.getType()).getSoapRequestBody();
        }
        return requestBody;
    }

    /**
     * <p>
     * returns the XML serialization of a DOM node; located here because it is used for SOAP request
     * bodies
     * </p>
     * 
     * @param node
     *            DOM node that is serialized
     * @return XML serialization as String; null if node is null
     */
    public static String getSerialization(Node node) {
        if (node == null) {
            return null;
        }
        try {
            StringWriter writer = new StringWriter();
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.transform(new DOMSource(node), new StreamResult(writer));
            return writer.toString();
        }
        catch (TransformerFactoryConfigurationError | TransformerException e) {
            throw new IllegalArgumentException(
                                               "could not create serialization for SOAP request body.",
                                               e);
        }
    }

    /**
     * <p>
     * Fetches all {@link Element}s that are direct children of the parent node, whose name matches
     * the given name.
     * </p>
     * 
     * @param typeNameRaw
     *            name of elements that are looked for
     * @param parentNode
     *            DOM node in which the elements are searched for
     * @return found {@link Element}s
     */
    public static List<Element> getMatchingChildNode(String typeNameRaw, Element parentNode) {
        List<Element> matchingNodes = new ArrayList<>();
        Node parameterNode = null;
        if (parentNode != null) {
            NodeList parameterNodes = parentNode.getChildNodes();
            String[] typeNameSplit = typeNameRaw.split(":");
            String typeName = typeNameSplit[typeNameSplit.length - 1];
            for (int i = 0; i < parameterNodes.getLength(); i++) {
                parameterNode = parameterNodes.item(i);
                if (parameterNode.getNodeType() == Node.ELEMENT_NODE) {
                    String[] parameterNodeSplit = parameterNode.getNodeName().split(":");
                    String parameterNodeName = parameterNodeSplit[parameterNodeSplit.length - 1];
                    if (typeName.equals(parameterNodeName)) {
                        matchingNodes.add((Element) parameterNode);
                    }
                }
            }
        }
        return matchingNodes;
    }

    /**
     * <p>
     * Returns the values found in a currentNode for a defined valueName. To this aim, this methods
     * first determines if there is an attribute with the name "valueName". If this is the case, the
     * value of this attribute is returned. Otherwise, the methods looks for {@link Element}s that
     * are direct children of the provided DOM node with the given name and returns the text content
     * of those nodes.
     * </p>
     * <p>
     * In case no values can be found, an empty list is returned
     * 
     * @param valueName
     *            name of the value that is retrieved
     * @param node
     *            node for which the value is retrieved
     * @return list of the found values.
     */
    public static List<String> getValuesFromElement(String valueName, Element node) {
        List<String> attributeValues = new LinkedList<>();

        if (node != null) {
            // first check attributes of the node
            Attr attribute = node.getAttributeNode(valueName);
            if (attribute != null) {
                attributeValues.add(attribute.getValue());
            }
            else {
                // now check elements
                List<Element> elements = getMatchingChildNode(valueName, node);
                for (Element element : elements) {
                    attributeValues.add(element.getTextContent());
                }
            }
        }

        return attributeValues;
    }

    /**
     * <p>
     * prevent instantiation
     * </p>
     */
    private SOAPUtils() {}
}
