/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 org.apache.axis2.transport.xmpp;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.soap.SOAP12Version;
import org.apache.axiom.soap.SOAPVersion;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.client.Options;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.AxisMessage;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.TransportOutDescription;
import org.apache.axis2.description.WSDL2Constants;
import org.apache.axis2.handlers.AbstractHandler;
import org.apache.axis2.transport.OutTransportInfo;
import org.apache.axis2.transport.TransportSender;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.axis2.transport.xmpp.util.XMPPClientResponseManager;
import org.apache.axis2.transport.xmpp.util.XMPPConnectionFactory;
import org.apache.axis2.transport.xmpp.util.XMPPConstants;
import org.apache.axis2.transport.xmpp.util.XMPPOutTransportInfo;
import org.apache.axis2.transport.xmpp.util.XMPPServerCredentials;
import org.apache.axis2.transport.xmpp.util.XMPPUtils;
import org.apache.axis2.util.Utils;
import org.apache.axis2.wsdl.WSDLConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ws.commons.schema.*;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ChatManager;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;

import javax.xml.namespace.QName;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;

public class XMPPSender extends AbstractHandler implements TransportSender {
	static Log log = null;
	
    XMPPServerCredentials serverCredentials;    
    private XMPPClientResponseManager xmppClientSidePacketListener;
    private XMPPConnectionFactory defaultConnectionFactory;
	
    public XMPPSender() {
        log = LogFactory.getLog(XMPPSender.class);
        xmppClientSidePacketListener  = new XMPPClientResponseManager();
    }
    
	public void cleanup(MessageContext msgContext) throws AxisFault {	
	}

    /**
     * Initialize the transport sender by reading pre-defined connection factories for
     * outgoing messages. These will create sessions (one per each destination dealt with)
     * to be used when messages are being sent.
     * @param confContext the configuration context
     * @param transportOut the transport sender definition from axis2.xml
     * @throws AxisFault on error
     */	
	public void init(ConfigurationContext confContext,
			TransportOutDescription transportOut) throws AxisFault {
		//if connection details are available from axis configuration
		//use those & connect to jabber server(s)
		serverCredentials = new XMPPServerCredentials();
		getConnectionDetailsFromAxisConfiguration(transportOut);	
		
		defaultConnectionFactory = new XMPPConnectionFactory();
	}

	
	public void stop() {}

	public InvocationResponse invoke(MessageContext msgContext)
			throws AxisFault {
        String targetAddress = (String) msgContext.getProperty(
                Constants.Configuration.TRANSPORT_URL);
            if (targetAddress != null) {
                sendMessage(msgContext, targetAddress, null);
            } else if (msgContext.getTo() != null && !msgContext.getTo().hasAnonymousAddress()) {
                targetAddress = msgContext.getTo().getAddress();

                if (!msgContext.getTo().hasNoneAddress()) {
                    sendMessage(msgContext, targetAddress, null);
                } else {
                    //Don't send the message.
                    return InvocationResponse.CONTINUE;
                }
            } else if (msgContext.isServerSide()) {
                // get the out transport info for server side when target EPR is unknown
                sendMessage(msgContext, null,
                    (OutTransportInfo) msgContext.getProperty(Constants.OUT_TRANSPORT_INFO));
            }
            return InvocationResponse.CONTINUE;
	}

    /**
     * Send the given message over XMPP transport
     *
     * @param msgCtx the axis2 message context
     * @throws AxisFault on error
     */
    public void sendMessage(MessageContext msgCtx, String targetAddress,
            
        OutTransportInfo outTransportInfo) throws AxisFault {
		XMPPConnection xmppConnection = null;
		XMPPOutTransportInfo xmppOutTransportInfo = null;
		XMPPConnectionFactory connectionFactory;
		
		//if on client side,create connection to xmpp server
		if(msgCtx.isServerSide()){
		    xmppOutTransportInfo = (XMPPOutTransportInfo)msgCtx.getProperty(org.apache.axis2.Constants.OUT_TRANSPORT_INFO);
		    connectionFactory = xmppOutTransportInfo.getConnectionFactory();
		}else{
		    getConnectionDetailsFromClientOptions(msgCtx);
		    connectionFactory = defaultConnectionFactory;
		}
		
		synchronized (this) {
            xmppConnection = connectionFactory.getXmppConnection();
            if(xmppConnection == null){
                connectionFactory.connect(serverCredentials);   
                xmppConnection = connectionFactory.getXmppConnection();
            }
        }
		
		Message message = new Message();
		Options options = msgCtx.getOptions();    	
    	String serviceName = XMPPUtils.getServiceName(targetAddress);    
    	
    	SOAPVersion version = msgCtx.getEnvelope().getVersion();
    	if(version instanceof SOAP12Version){
    		message.setProperty(XMPPConstants.CONTENT_TYPE, HTTPConstants.MEDIA_TYPE_APPLICATION_SOAP_XML+ "; action="+ msgCtx.getSoapAction());
    	}else{
    		message.setProperty(XMPPConstants.CONTENT_TYPE, HTTPConstants.MEDIA_TYPE_TEXT_XML);
    	}
    	
    	
		if (targetAddress != null) {
			xmppOutTransportInfo = new XMPPOutTransportInfo(targetAddress);
			xmppOutTransportInfo.setConnectionFactory(defaultConnectionFactory);
		} else if (msgCtx.getTo() != null &&
				!msgCtx.getTo().hasAnonymousAddress()) {
			//TODO 
		} else if (msgCtx.isServerSide()) {
			xmppOutTransportInfo = (XMPPOutTransportInfo)
			msgCtx.getProperty(Constants.OUT_TRANSPORT_INFO);
		}
		try{
		if(msgCtx.isServerSide()){
			message.setProperty(XMPPConstants.IS_SERVER_SIDE, new Boolean(false));
			message.setProperty(XMPPConstants.IN_REPLY_TO, xmppOutTransportInfo.getInReplyTo());
			message.setProperty(XMPPConstants.SEQUENCE_ID, xmppOutTransportInfo.getSequenceID());
		}else{
			//message is going to be processed on server side
			message.setProperty(XMPPConstants.IS_SERVER_SIDE,new Boolean(true));
			//we are sending a soap envelope as a message
			message.setProperty(XMPPConstants.CONTAINS_SOAP_ENVELOPE, new Boolean(true));
			message.setProperty(XMPPConstants.SERVICE_NAME, serviceName);
			String action = options.getAction();
			if (action == null) {
				AxisOperation axisOperation = msgCtx.getAxisOperation();
				if (axisOperation != null) {
					action = axisOperation.getSoapAction();
				}
			}
			if (action != null) {
				message.setProperty(XMPPConstants.ACTION, action);
			}
		}		
    	if(xmppConnection == null){
    		handleException("Connection to XMPP Server is not established.");    		
    	}
		
    	
    	
		//initialize the chat manager using connection
		ChatManager chatManager = xmppConnection.getChatManager();
		Chat chat = chatManager.createChat(xmppOutTransportInfo.getDestinationAccount(), null);
		
			boolean waitForResponse =
				msgCtx.getOperationContext() != null &&
				WSDL2Constants.MEP_URI_OUT_IN.equals(
						msgCtx.getOperationContext().getAxisOperation().getMessageExchangePattern());
			
			OMElement msgElement;			
			String messageToBeSent = "";
			
			if(XMPPConstants.XMPP_CONTENT_TYPE_STRING.equals(xmppOutTransportInfo.getContentType())){
				//if request is received from a chat client, whole soap envelope
				//should not be sent.
				OMElement soapBodyEle = msgCtx.getEnvelope().getBody();
				OMElement responseEle = soapBodyEle.getFirstElement();
				if(responseEle != null){
					msgElement = responseEle.getFirstElement();					
				}else{
					msgElement = responseEle;
				}
			}else{
				//if request received from a ws client whole soap envelope 
				//must be sent.
				msgElement = msgCtx.getEnvelope();
			}	
			messageToBeSent = msgElement.toString();
			message.setBody(messageToBeSent);
			
			String key = null;
			if(waitForResponse && !msgCtx.isServerSide()){
				PacketFilter filter = new PacketTypeFilter(message.getClass());				
				xmppConnection.addPacketListener(xmppClientSidePacketListener,filter);
				key = UUID.randomUUID().toString();
				xmppClientSidePacketListener.listenForResponse(key, msgCtx);
				message.setProperty(XMPPConstants.SEQUENCE_ID, key);
			}			

			chat.sendMessage(message);
			log.debug("Sent message :"+message.toXML());

			//If this is on client side, wait for the response from server.
			//Is this the best way to do this?
			if(waitForResponse && !msgCtx.isServerSide()){
			    xmppClientSidePacketListener.waitFor(key);
				//xmppConnection.disconnect();
			    log.debug("Received response sucessfully");
			}
			

		} catch (XMPPException e) {
			log.error("Error occurred while sending the message : "+message.toXML(),e);
			handleException("Error occurred while sending the message : "+message.toXML(),e);
		} catch (InterruptedException e) {
		    log.error("Error occurred while sending the message : "+message.toXML(),e);
            handleException("Error occurred while sending the message : "+message.toXML(),e);
        }finally{
//			if(xmppConnection != null && !msgCtx.isServerSide()){
//				xmppConnection.disconnect();
//			}
		}
    }	
    
    /**
     * Process message requests that came in through chat clients
     * @param msgCtx
     * @throws AxisFault
     */
    public static void processChatMessage(MessageContext msgCtx) throws AxisFault {
    	Object obj = msgCtx.getProperty(XMPPConstants.MESSAGE_FROM_CHAT);
    	if(obj != null){
        	String message = (String)obj;
        	String response = "";       	
        	
    		if(message.trim().startsWith("help")){
    			response = prepareHelpTextForChat();						
    		}else if(message.trim().startsWith("listServices")){
    			response = prepareServicesList(msgCtx);
    		}else if (message.trim().startsWith("getOperations")){
    			response = prepareOperationList(msgCtx,message);
        	}else{
        		//TODO add support for more help commands
        	}
    		sendChatMessage(msgCtx,response);       	
    	}
    }    
   
    /**
     * Prepares a list of service names deployed in current runtime
     * @param msgCtx
     * @return
     */
	private static String prepareOperationList(MessageContext msgCtx,String chatMessage) {
		StringBuffer sb = new StringBuffer();
		//extract service name
		String serviceName = chatMessage.replace("getOperations", "");
		serviceName = serviceName.replaceAll(" ", "");
		if(log.isDebugEnabled()){
			log.debug("Finding operations for service :"+ serviceName);	
		}
		
		try {
			AxisService service = msgCtx.getConfigurationContext().getAxisConfiguration().getService(serviceName);
			Iterator itrOperations = service.getOperations();
			int index = 1;
			while(itrOperations.hasNext()){
				AxisOperation operation = (AxisOperation)itrOperations.next();
				String parameterList = getParameterListForOperation(operation);				
				sb.append(index +"."+operation.getName().getLocalPart()+"("+parameterList+")"+"\n");
				index++;
			}
		} catch (AxisFault e) {
			log.error("Error occurred while retreiving AxisService : "+serviceName,e);
			sb.append("Error occurred while retrieving operations for service : "+serviceName);
		}		
		return sb.toString();
	}

	/**
	 * Retrieves list of parameter names & their type for a given operation
	 * @param operation
	 */
	private static String getParameterListForOperation(AxisOperation operation) {
		//Logic copied from BuilderUtil.buildsoapMessage(...)
		StringBuffer paramList = new StringBuffer();
		AxisMessage axisMessage =
		    operation.getMessage(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
		XmlSchemaElement xmlSchemaElement = axisMessage.getSchemaElement();
		if(xmlSchemaElement != null){			
		    XmlSchemaType schemaType = xmlSchemaElement.getSchemaType();
		    if (schemaType instanceof XmlSchemaComplexType) {
		        XmlSchemaComplexType complexType = ((XmlSchemaComplexType)schemaType);
		        XmlSchemaParticle particle = complexType.getParticle();
		        if (particle instanceof XmlSchemaSequence || particle instanceof XmlSchemaAll) {

                    if (particle instanceof XmlSchemaSequence) {
                        XmlSchemaSequence sequence = (XmlSchemaSequence) particle;
                        for (XmlSchemaSequenceMember member : sequence.getItems()) {
                            XmlSchemaElement innerElement = (XmlSchemaElement) member;
                            QName qName = innerElement.getQName();
                            if (qName == null && innerElement.getSchemaTypeName()
                                    .equals(org.apache.ws.commons.schema.constants.Constants.XSD_ANYTYPE)) {
                                break;
                            }
                            String name =
                                    qName != null ? qName.getLocalPart() : innerElement.getName();
                            String type = innerElement.getSchemaTypeName().toString();
                            paramList.append(","+type +" " +name);
                        }
                    }  else {
                        XmlSchemaAll xmlSchemaAll = (XmlSchemaAll) particle;
                        for (XmlSchemaAllMember member : xmlSchemaAll.getItems()) {
							XmlSchemaElement innerElement = (XmlSchemaElement) member;
                            QName qName = innerElement.getQName();
                            if (qName == null && innerElement.getSchemaTypeName()
                                    .equals(org.apache.ws.commons.schema.constants.Constants.XSD_ANYTYPE)) {
                                break;
                            }
                            String name =
                                    qName != null ? qName.getLocalPart() : innerElement.getName();
                            String type = innerElement.getSchemaTypeName().toString();
                            paramList.append("," + type + " " + name);
                        }
                    }
                }
		   }	            	
		}
		//remove first ","
		String list = paramList.toString();		
		return list.replaceFirst(",", "");
	}

	
    /**
     * Prepares a list of service names deployed in current runtime
     * @param msgCtx
     * @return
     */
	private static String prepareServicesList(MessageContext msgCtx) {
		Map services = msgCtx.getConfigurationContext().getAxisConfiguration().getServices();
		StringBuffer sb = new StringBuffer();
		if(services != null && services.size() > 0){
			Iterator itrServiceNames = services.keySet().iterator();			
			int index = 1;
			while (itrServiceNames.hasNext()) {
				String serviceName = (String) itrServiceNames.next();
				sb.append(index+"."+serviceName+"\n");
				index++;
			}
		}
		return sb.toString();
	}
    
    
    /**
     * Generate help text for chat client
     * @return {@link String}
     */
    private static String prepareHelpTextForChat(){
    	StringBuffer helpText = new StringBuffer();
    	helpText.append("Following commands are supported :"+"\n");
    	helpText.append("-----------------------------------"+"\n");
    	helpText.append("1. listServices"+"\n");
    	helpText.append("2. getOperations <service-name>"+"\n");
    	helpText.append("3. call <service-name>:<operation>(<param1>,<param2>,...)"+"\n");
    	return helpText.toString();
    }
    
    /**
     * Replies to IM clients via a chat message. The reply contains the invocation response as a string. 
     * @param msgCtx
     * @param responseMsg
     * @throws AxisFault
     */
    private static void sendChatMessage(MessageContext msgCtx,String responseMsg) throws AxisFault {
    		XMPPConnection xmppConnection = null;
    		XMPPOutTransportInfo xmppOutTransportInfo = null;    		
    		Message message = new Message();
        	
   			xmppOutTransportInfo = (XMPPOutTransportInfo)msgCtx.getProperty(Constants.OUT_TRANSPORT_INFO);
   			if(xmppOutTransportInfo != null){
   				message.setProperty(XMPPConstants.IN_REPLY_TO, xmppOutTransportInfo.getInReplyTo());
   	    		xmppConnection = xmppOutTransportInfo.getConnectionFactory().getXmppConnection();
   	        	if(xmppConnection == null){
   	        		handleException("Connection to XMPP Server is not established.");    		
   	        	}   	    		
   			}else{
   				handleException("Could not find message sender details.");
   			}   			
    		
    		//initialize the chat manager using connection
    		ChatManager chatManager = xmppConnection.getChatManager();
    		Chat chat = chatManager.createChat(xmppOutTransportInfo.getDestinationAccount(), null);    		
    		try{   			
    		    message.setProperty(XMPPConstants.SEQUENCE_ID, 
    		            xmppOutTransportInfo.getSequenceID());
   				message.setBody(responseMsg);		
    			chat.sendMessage(message);
    			log.debug("Sent message :"+message.toXML());
    		} catch (XMPPException e) {
    			XMPPSender.handleException("Error occurred while sending the message : "+message.toXML(),e);
    		}
        }	

    /**
     * Extract connection details from axis2.xml's transportsender section
     * @param serverCredentials
     * @param transportOut
     */
    private void getConnectionDetailsFromAxisConfiguration(TransportOutDescription transportOut){
    	if(transportOut != null){
			Parameter serverUrl = transportOut.getParameter(XMPPConstants.XMPP_SERVER_URL);
			if (serverUrl != null) {
				serverCredentials.setServerUrl(Utils.getParameterValue(serverUrl));
			}
			
			Parameter userName = transportOut.getParameter(XMPPConstants.XMPP_SERVER_USERNAME);
			if (userName != null) {
				serverCredentials.setAccountName(Utils.getParameterValue(userName));
			}
		
			Parameter password = transportOut.getParameter(XMPPConstants.XMPP_SERVER_PASSWORD);
			if (password != null) {
				serverCredentials.setPassword(Utils.getParameterValue(password));
			}

			Parameter serverType = transportOut.getParameter(XMPPConstants.XMPP_SERVER_TYPE);			
			if (serverType != null) {
				serverCredentials.setServerType(Utils.getParameterValue(serverType));
			}	
			
			Parameter domainName = transportOut.getParameter(XMPPConstants.XMPP_DOMAIN_NAME);
			if (serverUrl != null) {
				serverCredentials.setDomainName(Utils.getParameterValue(domainName));
			}
		}
	}
	
	/**
	 * Extract connection details from client options
	 * @param serverCredentials
	 * @param msgContext
	 */
    private void getConnectionDetailsFromClientOptions(MessageContext msgContext){
    	Options clientOptions = msgContext.getOptions();

		if (clientOptions.getProperty(XMPPConstants.XMPP_SERVER_USERNAME) != null){
			serverCredentials.setAccountName((String)clientOptions.getProperty(XMPPConstants.XMPP_SERVER_USERNAME));
		}
		if (clientOptions.getProperty(XMPPConstants.XMPP_SERVER_PASSWORD) != null){
			serverCredentials.setPassword((String)clientOptions.getProperty(XMPPConstants.XMPP_SERVER_PASSWORD));
		}
		if (clientOptions.getProperty(XMPPConstants.XMPP_SERVER_URL) != null){
			serverCredentials.setServerUrl((String)clientOptions.getProperty(XMPPConstants.XMPP_SERVER_URL));
		}
		if (clientOptions.getProperty(XMPPConstants.XMPP_SERVER_TYPE) != null){
			serverCredentials.setServerType((String)clientOptions.getProperty(XMPPConstants.XMPP_SERVER_TYPE));
		}		
	}  
	
    private static void handleException(String msg, Exception e) throws AxisFault {
        log.error(msg, e);
        throw new AxisFault(msg, e);
    }
    private static void handleException(String msg) throws AxisFault {
        log.error(msg);
        throw new AxisFault(msg);
    }
}
