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

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.collections4.set.ListOrderedSet;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import de.ugoe.cs.autoquest.eventcore.IEventType;
import de.ugoe.cs.autoquest.plugin.http.HTTPUtils;
import de.ugoe.cs.autoquest.plugin.http.SOAPUtils;

/**
 * <p>
 * A simplified SOAP event type that only contains the name of the called method and the name of the
 * service.
 * </p>
 * 
 * @author Steffen Herbold
 */
public class SimpleSOAPEventType implements IEventType {

    /**  */
    private static final long serialVersionUID = 1L;
    
    /**
     * see {@link RequestBodyMode}
     */
    private static RequestBodyMode requestBodyMode = RequestBodyMode.LOCALEVENT;

    /**
     * <p>
     * the SOAP method called in this request
     * </p>
     */
    private final String calledMethod;

    /**
     * <p>
     * the name of the service; this is either the path or, if a path map is available
     * </p>
     */
    private final String serviceName;

    /**
     * <p>
     * the name of the client; this is either the host/ip and port of the sender or, if a path map
     * is available, a human readable name that may be based also on the receiver
     * </p>
     */
    private final String clientName;

    /**
     * <p>
     * the body of the SOAP request; storage as serialized XML string to allow object serialization
     * </p>
     */
    private final String soapRequestBody;

    /**
     * <p>
     * Constructor. Creates a new SimpleSOAPEventType.
     * </p>
     * 
     */
    public SimpleSOAPEventType(String calledMethod,
                               String serviceName,
                               String clientName,
                               Element soapRequestBody)
    {
        if (calledMethod == null) {
            throw new IllegalArgumentException("called method must not be null");
        }
        if (serviceName == null) {
            throw new IllegalArgumentException("serviceName must not be null");
        }
        if (clientName == null) {
            throw new IllegalArgumentException("clientName must not be null");
        }
        this.calledMethod = calledMethod;
        this.serviceName = serviceName;
        this.clientName = clientName;
        this.soapRequestBody = SOAPUtils.getSerialization(soapRequestBody);
        EqualSOAPDataMap.INSTANCE.add(this, this.soapRequestBody);
    }

    /**
     * <p>
     * the SOAP method called in this request
     * </p>
     * 
     * @return the SOAP method called in this request
     */
    public String getCalledMethod() {
        return calledMethod;
    }

    /**
     * <p>
     * the name of the service called in this request
     * </p>
     * 
     * @return the name of the service called in this request
     */
    public String getServiceName() {
        return serviceName;
    }

    /**
     * <p>
     * the name of the client calling in this request
     * </p>
     * 
     * @return the name of the client calling in this request
     */
    public String getClientName() {
        return clientName;
    }

    /**
     * <p>
     * returns the body of the SOAP request; how the body is determined is defined by the {@link RequestBodyMode}. 
     * </p>
     *
     * @return body of the SOAP request
     */
    public Element getSoapRequestBody() {
        String requestBody;
        switch (requestBodyMode)
        {
            case LOCALEVENT:
                requestBody = soapRequestBody;
                break;
            case RANDOM:
                requestBody = EqualSOAPDataMap.INSTANCE.getRandom(this);
                break;
            default:
                throw new RuntimeException("undefined RequestBodyMode");
        }
        if( requestBody==null ) {
            System.err.println("foobar" + this);
            System.err.println(EqualSOAPDataMap.INSTANCE.getAll(this));
        }
        try {
            return DocumentBuilderFactory.newInstance().newDocumentBuilder()
                .parse(new ByteArrayInputStream(requestBody.getBytes())).getDocumentElement();
        }
        catch (SAXException | IOException | ParserConfigurationException e) {
            return null;
        }
    }
    
    

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.autoquest.eventcore.IEventType#getName()
     */
    @Override
    public String getName() {
        return "(" + serviceName + ", " + calledMethod + ")";
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.autoquest.plugin.http.eventcore.HTTPEventType#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        else if (obj instanceof SimpleSOAPEventType) {
            return HTTPUtils.equals(calledMethod, ((SimpleSOAPEventType) obj).calledMethod) &&
                HTTPUtils.equals(serviceName, ((SimpleSOAPEventType) obj).serviceName) &&
                HTTPUtils.equals(clientName, ((SimpleSOAPEventType) obj).clientName);
        }
        else {
            return false;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.autoquest.plugin.http.eventcore.HTTPEventType#hashCode()
     */
    @Override
    public int hashCode() {
        int hashCode = calledMethod.hashCode() + serviceName.hashCode() + clientName.hashCode();
        return hashCode;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "SimpleSOAPEventType(" + serviceName + ", " + calledMethod + ")";
    }
    
    /**
     * <p>
     * returns the current {@link RequestBodyMode}
     * </p>
     *
     * @return the requestBodyMode
     */
    public static RequestBodyMode getRequestBodyMode() {
        return requestBodyMode;
    }
    
    /**
     * <p>
     * sets the {@link RequestBodyMode}
     * </p>
     *
     * @param new requestBodyMode
     */
    public static void setRequestBodyMode(RequestBodyMode requestBodyMode) {
        SimpleSOAPEventType.requestBodyMode = requestBodyMode;
    }
    
    /**
     * <p>
     * Determines how getSoapRequestBody works.
     * <ul>
     * <li>LOCALEVENT: returns the request body of the event type itself</li>
     * <li>RANDOM: returns a randomly draw request body for the called method using {@link EqualSOAPDataMap#getRandom(SimpleSOAPEventType)}.
     * </ul> 
     * </p>
     * 
     * @author Steffen Herbold
     */
    public static enum RequestBodyMode {LOCALEVENT, RANDOM};
    
    /**
     * <p>
     * Handles all request bodies of equal SOAP events. Can be used to view either all request bodies sent to an operation or to randomly draw one of the bodies.
     * </p>
     * 
     * @author Steffen Herbold
     */
    public static class EqualSOAPDataMap {
        
        /**
         * Singleton. Handle to only instance of this class
         */
        public final static EqualSOAPDataMap INSTANCE = new EqualSOAPDataMap();
        
        /**
         * Map with all soapRequestBodies for all equal {@link SimpleSOAPEventType}s
         */
        private final Map<SimpleSOAPEventType, ListOrderedSet<String>> soapRequestBodies = new HashMap<>();
        
        /**
         * random number generator for picking a random request body
         */
        private final Random random = new Random(1); // TODO static seed for testing
        
        private EqualSOAPDataMap() {
            // private constructor to avoid initialization
        }

        /**
         * <p>
         * Adds a new body to the map.
         * </p>
         *
         * @param simpleSOAPEventType
         */
        public void add(SimpleSOAPEventType simpleSOAPEventType, String soapRequestBody) {
            if( soapRequestBody!=null ) {
                ListOrderedSet<String> requestBodySet = soapRequestBodies.get(simpleSOAPEventType);
                if( requestBodySet==null ) {
                    requestBodySet = new ListOrderedSet<>();
                    soapRequestBodies.put(simpleSOAPEventType, requestBodySet);
                }
                requestBodySet.add(soapRequestBody);
            }
        }
        
        /**
         * <p>
         * Retrieves all bodies associated with the simpleSoapEventType; null if not found
         * </p>
         *
         * @param simpleSoapEventType
         * @return
         */
        public Set<String> getAll(SimpleSOAPEventType simpleSoapEventType) {
            return Collections.unmodifiableSet(soapRequestBodies.get(simpleSoapEventType));
        }
        
        /**
         * <p>
         * Randomly draws one of the SOAP event type bodies associated with the event
         * </p>
         *
         * @param simpleSOAPEventType
         * @return
         */
        public String getRandom(SimpleSOAPEventType simpleSOAPEventType) {
            ListOrderedSet<String> requestBodySet = soapRequestBodies.get(simpleSOAPEventType);
            if( requestBodySet==null || requestBodySet.isEmpty() ) {
                throw new RuntimeException("no request body known for SimpleSOAPEventType: " + simpleSOAPEventType);
            }
            
            return requestBodySet.get(random.nextInt(requestBodySet.size()));
        }
    }
}
