/*
 * 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.util;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.Executor;

import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.stream.XMLStreamException;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMException;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.builder.BuilderUtil;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.TransportInDescription;
import org.apache.axis2.description.TransportOutDescription;
import org.apache.axis2.engine.AxisEngine;
import org.apache.axis2.transport.TransportUtils;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.axis2.transport.xmpp.XMPPSender;
import org.apache.axis2.util.MessageContextBuilder;
import org.apache.axis2.util.MultipleEntryHashMap;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;

public class XMPPPacketListener implements PacketListener {
	private static final Log log = LogFactory.getLog(XMPPPacketListener.class);
	private XMPPConnectionFactory xmppConnectionFactory = null;
	private ConfigurationContext configurationContext = null;
	private Executor workerPool = null;
    
    public final static String CONTENT_TYPE = "mail.contenttype";

    public XMPPPacketListener(XMPPConnectionFactory xmppConnectionFactory, ConfigurationContext configurationContext, Executor workerPool) {
		this.xmppConnectionFactory = xmppConnectionFactory;
		this.configurationContext = configurationContext;
		this.workerPool = workerPool;
	}

	/**
	 * This method gets triggered when server side gets a message
	 */
	public void processPacket(Packet packet) {
		log.debug("Received : "+packet.toXML());
		if(packet instanceof Message){
			workerPool.execute(new Worker(packet));			
		}
	}

	/**
	 * Creates message context using values received in XMPP packet
	 * @param packet
	 * @return MessageContext
	 * @throws AxisFault
	 */
	private MessageContext createMessageContext(Packet packet) throws AxisFault {
		Message message = (Message) packet;		

		Boolean isServerSide = (Boolean) message
				.getProperty(XMPPConstants.IS_SERVER_SIDE);
		String serviceName = (String) message
				.getProperty(XMPPConstants.SERVICE_NAME);
		String action = (String) message.getProperty(XMPPConstants.ACTION);
		MessageContext msgContext = null;

		TransportInDescription transportIn = configurationContext
				.getAxisConfiguration().getTransportIn("xmpp");
		TransportOutDescription transportOut = configurationContext
				.getAxisConfiguration().getTransportOut("xmpp");
		if ((transportIn != null) && (transportOut != null)) {
			msgContext = configurationContext.createMessageContext();
			msgContext.setTransportIn(transportIn);
			msgContext.setTransportOut(transportOut);
			if (isServerSide != null) {
				msgContext.setServerSide(isServerSide.booleanValue());
			}
			msgContext.setProperty(
					CONTENT_TYPE,
					"text/xml");
			msgContext.setProperty(
					Constants.Configuration.CHARACTER_SET_ENCODING, "UTF-8");
			msgContext.setIncomingTransportName("xmpp");

			Map services = configurationContext.getAxisConfiguration()
					.getServices();

			AxisService axisService = (AxisService) services.get(serviceName);
			msgContext.setAxisService(axisService);
			msgContext.setSoapAction(action);

			// pass the configurationFactory to transport sender
			msgContext.setProperty("XMPPConfigurationFactory",
					this.xmppConnectionFactory);

			if (packet.getFrom() != null) {
				msgContext.setFrom(new EndpointReference(packet.getFrom()));
			}
			if (packet.getTo() != null) {
				msgContext.setTo(new EndpointReference(packet.getTo()));
			}

			XMPPOutTransportInfo xmppOutTransportInfo = new XMPPOutTransportInfo();
			xmppOutTransportInfo
					.setConnectionFactory(this.xmppConnectionFactory);

			String packetFrom = packet.getFrom();
			if (packetFrom != null) {
				EndpointReference fromEPR = new EndpointReference(packetFrom);
				xmppOutTransportInfo.setFrom(fromEPR);
				xmppOutTransportInfo.setDestinationAccount(packetFrom);
			}

			// Save Message-Id to set as In-Reply-To on reply
			String xmppMessageId = packet.getPacketID();
			if (xmppMessageId != null) {
				xmppOutTransportInfo.setInReplyTo(xmppMessageId);
			}
			xmppOutTransportInfo.setSequenceID((String)message.getProperty(XMPPConstants.SEQUENCE_ID));
			msgContext.setProperty(
					org.apache.axis2.Constants.OUT_TRANSPORT_INFO,
					xmppOutTransportInfo);
			buildSOAPEnvelope(packet, msgContext);
		} else {
			throw new AxisFault("Either transport in or transport out is null");
		}
		return msgContext;
	}

    /**
     * builds SOAP envelop using message contained in packet
     * @param packet
     * @param msgContext
     * @throws AxisFault
     */
	private void buildSOAPEnvelope(Packet packet, MessageContext msgContext) throws AxisFault{
		Message message = (Message)packet;		
		String logMsg = "Trying to create " +
		"message content using XMPP message received :"+packet.toXML();
			
		String messageBody = StringEscapeUtils.unescapeXml(message.getBody());
		if(msgContext.isServerSide()){
			log.debug("Received Envelope : "+messageBody);
		}
		
		InputStream inputStream = new ByteArrayInputStream(messageBody.getBytes());
		SOAPEnvelope envelope = null;
		try {
			Object obj = message.getProperty(XMPPConstants.CONTAINS_SOAP_ENVELOPE); 
			if(obj != null && ((Boolean)obj).booleanValue()){
				String contentType = (String)message.getProperty(XMPPConstants.CONTENT_TYPE);
				if(contentType == null){
					throw new AxisFault("Can not Find Content type Property in the XMPP Message");
				}
				envelope = TransportUtils.createSOAPMessage(msgContext, inputStream, contentType);
				msgContext.setProperty(XMPPConstants.CONTAINS_SOAP_ENVELOPE, new Boolean(true));
			}else{
				//A text message has been received from a chat client
				//This message could either be a service call or a help command
				if(!(messageContainsCommandsFromChat(messageBody,msgContext))){
					envelope = createSOAPEnvelopeForRawMessage(msgContext, messageBody);					
				}				
			}
			if(envelope != null){
				msgContext.setEnvelope(envelope);				
			}
		}catch (OMException e) {
			log.error(logMsg, e);
			throw new AxisFault(logMsg);
		}catch (XMLStreamException e) {
			log.error(logMsg, e);
			throw new AxisFault(logMsg);
		}catch (FactoryConfigurationError e) {
			log.error(logMsg, e);
			throw new AxisFault(logMsg);
		}catch (AxisFault e){
			log.error(logMsg, e);
			throw new AxisFault(logMsg);
		}
	}

	/**
	 * In the direct chat client scenario, client can send commands & retrieve details
	 * on available services, operations,etc. This method checks if a client has sent
	 * such command. Only limited set of commands are available as of now. 
	 * @param message
	 * @param msgContext
	 * @return
	 */
	private boolean messageContainsCommandsFromChat(String message,MessageContext msgContext){
		boolean containsKnownCommand = false;
		if(message.trim().startsWith("help")){
			containsKnownCommand = true;						
		}else if(message.trim().startsWith("listServices")){
			containsKnownCommand = true;
		}else if (message.trim().startsWith("getOperations")){
			containsKnownCommand = true;
		}
		
		if(containsKnownCommand){
			msgContext.setProperty(XMPPConstants.MESSAGE_FROM_CHAT,message.trim());	
		}
		return containsKnownCommand;
	}
	
	/**
	 * Creates a SOAP envelope using details found in chat message.
	 * @param msgCtx
	 * @param chatMessage
	 * @return
	 */
	private SOAPEnvelope createSOAPEnvelopeForRawMessage(MessageContext msgCtx,String chatMessage)
	throws AxisFault{
		//TODO : need to add error handling logic 
    	String callRemoved = chatMessage.replaceFirst("call", "");
    	//extract Service name
    	String serviceName = callRemoved.trim().substring(0, callRemoved.indexOf(":")-1);
    	String operationName = callRemoved.trim().substring(callRemoved.indexOf(":"), callRemoved.indexOf("(")-1);

    	//Extract parameters from IM message
    	String parameterList = callRemoved.trim().substring(callRemoved.indexOf("("),callRemoved.trim().length()-1); 	
    	StringTokenizer st = new StringTokenizer(parameterList,",");
		MultipleEntryHashMap parameterMap = new MultipleEntryHashMap();
    	while(st.hasMoreTokens()){
    		String token = st.nextToken();
    		String name = token.substring(0, token.indexOf("="));
    		String value = token.substring(token.indexOf("=")+1);
    		parameterMap.put(name, value);
    	}
    	
		SOAPEnvelope envelope = null;
		try {
			msgCtx.setProperty(XMPPConstants.CONTAINS_SOAP_ENVELOPE, new Boolean(true));
			if(serviceName != null && serviceName.trim().length() > 0){
				AxisService axisService = msgCtx.getConfigurationContext().getAxisConfiguration().getService(serviceName);
				msgCtx.setAxisService(axisService);	
				
				AxisOperation axisOperation = axisService.getOperationBySOAPAction("urn:"+operationName);
				if(axisOperation != null){
					msgCtx.setAxisOperation(axisOperation);
				}
			}
	    	
			if(operationName != null && operationName.trim().length() > 0){
				msgCtx.setSoapAction("urn:"+operationName);
			}
			
			XMPPOutTransportInfo xmppOutTransportInfo = (XMPPOutTransportInfo)msgCtx.getProperty(
					org.apache.axis2.Constants.OUT_TRANSPORT_INFO);
			//This should be only set for messages received via chat.
			//TODO : need to read from a constant
			xmppOutTransportInfo.setContentType("xmpp/text");
			
			msgCtx.setServerSide(true);
			
			//TODO : need to support SOAP12 as well
			SOAPFactory soapFactory = OMAbstractFactory.getSOAP11Factory();
			envelope = BuilderUtil.buildsoapMessage(msgCtx, parameterMap,
                    soapFactory);
			//TODO : improve error handling & messages
		} catch (AxisFault e) {
			throw new AxisFault(e.getMessage());
		} catch (OMException e) {
			throw new AxisFault(e.getMessage());
		} catch (FactoryConfigurationError e) {
			throw new AxisFault(e.getMessage());
		}		
		return envelope;
	}

	/**
	 * The actual Runnable Worker implementation which will process the
	 * received XMPP messages in the worker thread pool
	 */
	class Worker implements Runnable {
		private Packet packet = null;
		Worker(Packet packet) {
			this.packet = packet;
		}

		public void run() {
			MessageContext msgCtx = null;
			try {
				msgCtx = createMessageContext(packet);
				Object obj = msgCtx.getProperty(XMPPConstants.CONTAINS_SOAP_ENVELOPE);
				if(obj != null && ((Boolean)obj).booleanValue()){
					if(msgCtx.isProcessingFault() && msgCtx.isServerSide()){
						AxisEngine.sendFault(msgCtx);
					}else{
						AxisEngine.receive(msgCtx);
					}					
				}else{
					//Send a text reply message to command received from chat client
					XMPPSender.processChatMessage(msgCtx);
				}
			} catch (AxisFault e) {
				log.error("Error occurred while sending message"+e);
   				if (msgCtx != null && msgCtx.isServerSide()) {
    				MessageContext faultContext;
					try {
						faultContext = MessageContextBuilder.createFaultMessageContext(msgCtx, e);
	    				AxisEngine.sendFault(faultContext);
					} catch (AxisFault e1) {
						log.error("Error occurred while creating SOAPFault message"+e1);
					}
   				}
			}
		}
	}
}
