/*
 * Copyright 1999-2004 The Apache Software Foundation.
 * 
 * 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 org.apache.sandesha2.msgprocessors;

import java.util.Iterator;

import org.apache.axiom.om.OMElement;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.OperationContext;
import org.apache.axis2.context.OperationContextFactory;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.engine.AxisEngine;
import org.apache.axis2.transport.TransportUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sandesha2.RMMsgContext;
import org.apache.sandesha2.Sandesha2Constants;
import org.apache.sandesha2.SandeshaException;
import org.apache.sandesha2.i18n.SandeshaMessageHelper;
import org.apache.sandesha2.i18n.SandeshaMessageKeys;
import org.apache.sandesha2.security.SecurityManager;
import org.apache.sandesha2.security.SecurityToken;
import org.apache.sandesha2.storage.StorageManager;
import org.apache.sandesha2.storage.Transaction;
import org.apache.sandesha2.storage.beanmanagers.RMDBeanMgr;
import org.apache.sandesha2.storage.beans.RMDBean;
import org.apache.sandesha2.storage.beans.RMSBean;
import org.apache.sandesha2.storage.beans.SenderBean;
import org.apache.sandesha2.util.AcknowledgementManager;
import org.apache.sandesha2.util.FaultManager;
import org.apache.sandesha2.util.MessageRetransmissionAdjuster;
import org.apache.sandesha2.util.MsgInitializer;
import org.apache.sandesha2.util.RMMsgCreator;
import org.apache.sandesha2.util.SandeshaUtil;
import org.apache.sandesha2.util.SpecSpecificConstants;
import org.apache.sandesha2.util.TerminateManager;
import org.apache.sandesha2.util.WSRMMessageSender;
import org.apache.sandesha2.wsrm.SequenceAcknowledgement;
import org.apache.sandesha2.wsrm.TerminateSequence;

/**
 * Responsible for processing an incoming Terminate Sequence message.
 */

public class TerminateSeqMsgProcessor extends WSRMMessageSender implements MsgProcessor {

	private static final Log log = LogFactory.getLog(TerminateSeqMsgProcessor.class);

	public boolean processInMessage(RMMsgContext terminateSeqRMMsg, Transaction transaction) throws AxisFault {

		if (log.isDebugEnabled())
			log.debug("Enter: TerminateSeqMsgProcessor::processInMessage");

		MessageContext terminateSeqMsg = terminateSeqRMMsg.getMessageContext();

		// Processing the terminate message
		// TODO Add terminate sequence message logic.
		TerminateSequence terminateSequence = (TerminateSequence) terminateSeqRMMsg
				.getMessagePart(Sandesha2Constants.MessageParts.TERMINATE_SEQ);
		if (terminateSequence == null) {
			String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.noTerminateSeqPart);
			log.debug(message);
			throw new SandeshaException(message);
		}

		String sequenceId = terminateSequence.getIdentifier().getIdentifier();
		if (sequenceId == null || "".equals(sequenceId)) {
			String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.invalidSequenceID, null);
			log.debug(message);
			throw new SandeshaException(message);
		}
		
		ConfigurationContext context = terminateSeqMsg.getConfigurationContext();
		StorageManager storageManager = SandeshaUtil.getSandeshaStorageManager(context,context.getAxisConfiguration());
		
		// Check that the sender of this TerminateSequence holds the correct token
		RMDBean rmdBean = SandeshaUtil.getRMDBeanFromSequenceId(storageManager, sequenceId);
		if(rmdBean != null && rmdBean.getSecurityTokenData() != null) {
			SecurityManager secManager = SandeshaUtil.getSecurityManager(context);
			OMElement body = terminateSeqRMMsg.getSOAPEnvelope().getBody();
			SecurityToken token = secManager.recoverSecurityToken(rmdBean.getSecurityTokenData());
			secManager.checkProofOfPossession(token, body, terminateSeqRMMsg.getMessageContext());
		}

		if (FaultManager.checkForUnknownSequence(terminateSeqRMMsg, sequenceId, storageManager, false)) {
			if (log.isDebugEnabled())
				log.debug("Exit: TerminateSeqMsgProcessor::processInMessage, unknown sequence");
			return false;
		}

		// add the terminate sequence response if required.
		RMMsgContext terminateSequenceResponse = null;
		if (SpecSpecificConstants.isTerminateSequenceResponseRequired(terminateSeqRMMsg.getRMSpecVersion()))
			terminateSequenceResponse = getTerminateSequenceResponse(terminateSeqRMMsg, rmdBean, sequenceId, storageManager);

		setUpHighestMsgNumbers(context, storageManager, sequenceId, terminateSeqRMMsg);
		
		
		
		boolean inOrderInvocation = SandeshaUtil.getDefaultPropertyBean(context.getAxisConfiguration()).isInOrder();
		
		
		//if the invocation is inOrder and if this is RM 1.1 there is a posibility of all the messages having eleady being invoked.
		//In this case we should do the full termination.
		
		boolean doFullTermination = false;
		
		if (inOrderInvocation) {

			long highestMsgNo = rmdBean.getHighestInMessageNumber();
			long nextMsgToProcess = rmdBean.getNextMsgNoToProcess();
			
			if (nextMsgToProcess>highestMsgNo) {
				//all the messages have been invoked, u can do the full termination
				doFullTermination = true;
			}
		} else {
			//for not-inorder case, always do the full termination.
			doFullTermination = true;
		}
		
		if (doFullTermination) {
			TerminateManager.cleanReceivingSideAfterInvocation(sequenceId, storageManager);
			TerminateManager.cleanReceivingSideOnTerminateMessage(context, sequenceId, storageManager);
		} else
			TerminateManager.cleanReceivingSideOnTerminateMessage(context, sequenceId, storageManager);

		rmdBean.setTerminated(true);		
		rmdBean.setLastActivatedTime(System.currentTimeMillis());
		storageManager.getRMDBeanMgr().update(rmdBean);


		//sending the terminate sequence response
		if (terminateSequenceResponse != null) {
			//
			// As we have processed the input and prepared the response we can commit the
			// transaction now.
			if(transaction != null && transaction.isActive()) transaction.commit();
			
			MessageContext outMessage = terminateSequenceResponse.getMessageContext();
			EndpointReference toEPR = outMessage.getTo();
			
			outMessage.setServerSide(true);
						
			try {							
				AxisEngine.send(outMessage);
			} catch (AxisFault e) {
				if (log.isDebugEnabled())
					log.debug("Unable to send terminate sequence response", e);
				
				throw new SandeshaException(
						SandeshaMessageHelper.getMessage(SandeshaMessageKeys.couldNotSendTerminateResponse), e);
			}
			
			if (toEPR.hasAnonymousAddress()) {
				TransportUtils.setResponseWritten(terminateSeqMsg, true);
			}

		} else {
			//if RM 1.0 Anonymous scenario we will be trying to attache the TerminateSequence of the response side 
			//as the response message.
			
			String outgoingSideInternalSeqId = SandeshaUtil.getOutgoingSideInternalSequenceID(sequenceId);
			SenderBean senderFindBean = new SenderBean ();
			senderFindBean.setInternalSequenceID(outgoingSideInternalSeqId);
			senderFindBean.setMessageType(Sandesha2Constants.MessageTypes.TERMINATE_SEQ);
			senderFindBean.setSend(true);
			senderFindBean.setReSend(false);
			
			SenderBean outgoingSideTerminateBean = storageManager.getSenderBeanMgr().findUnique(senderFindBean);
			if (outgoingSideTerminateBean!=null) {
			
				EndpointReference toEPR = new EndpointReference (outgoingSideTerminateBean.getToAddress());
				if (toEPR.hasAnonymousAddress()) {
					String messageKey = outgoingSideTerminateBean
							.getMessageContextRefKey();
					MessageContext message = storageManager
							.retrieveMessageContext(messageKey, context);

					RMMsgContext rmMessage = MsgInitializer.initializeMessage(message);
					
					// attaching the this outgoing terminate message as the
					// response to the incoming terminate message.
					message.setTransportOut(terminateSeqMsg.getTransportOut());
					message.setProperty(MessageContext.TRANSPORT_OUT,terminateSeqMsg.getProperty(MessageContext.TRANSPORT_OUT));
					message.setProperty(Constants.OUT_TRANSPORT_INFO, terminateSeqMsg.getProperty(Constants.OUT_TRANSPORT_INFO));
	                
					try {							
						AxisEngine.send(message);
						TransportUtils.setResponseWritten(terminateSeqMsg, true);
					} catch (AxisFault e) {
						if (log.isDebugEnabled())
							log.debug("Unable to send terminate sequence response", e);
						
						throw new SandeshaException(
								SandeshaMessageHelper.getMessage(SandeshaMessageKeys.couldNotSendTerminateResponse), e);
					}
					
					// TODO - should this be here?
					MessageRetransmissionAdjuster.adjustRetransmittion(rmMessage, outgoingSideTerminateBean, context, storageManager);
				}
				
			}

		}
		
		terminateSeqMsg.pause();

		if (log.isDebugEnabled())
			log.debug("Exit: TerminateSeqMsgProcessor::processInMessage " + Boolean.TRUE);
		return true;
	}

	private void setUpHighestMsgNumbers(ConfigurationContext configCtx, StorageManager storageManager,
			String sequenceId, RMMsgContext terminateRMMsg) throws SandeshaException {

		if (log.isDebugEnabled())
			log.debug("Enter: TerminateSeqMsgProcessor::setUpHighestMsgNumbers, " + sequenceId);

		RMDBeanMgr mgr = storageManager.getRMDBeanMgr();
		RMDBean bean = mgr.retrieve(sequenceId);

		long highestInMsgNo = bean.getHighestInMessageNumber();

		// following will be valid only for the server side, since the obtained
		// int. seq ID is only valid there.
		String responseSideInternalSequenceId = SandeshaUtil.getOutgoingSideInternalSequenceID(sequenceId);
		
		
		long highestOutMsgNo = 0;
		try {
			boolean addResponseSideTerminate = false;
			if (highestInMsgNo == 0) {
				addResponseSideTerminate = false;
			} else {
				// Mark up the highest inbound message as if it had the last message flag on it.
				// 
				String inMsgId = bean.getHighestInMessageId();
				bean.setLastInMessageId(inMsgId);
				
				// Update the RMDBean
				storageManager.getRMDBeanMgr().update(bean);
				
				// If an outbound message has already gone out with that relatesTo, then we can terminate
				// right away.
				RMSBean rmsBean = SandeshaUtil.getRMSBeanFromInternalSequenceId(storageManager, responseSideInternalSequenceId);

				if(rmsBean != null) {
					String highestOutRelatesTo = rmsBean.getHighestOutRelatesTo();
					if (highestOutRelatesTo != null && highestOutRelatesTo.equals(inMsgId)) {
						highestOutMsgNo = rmsBean.getHighestOutMessageNumber();
						addResponseSideTerminate = true;
						
						// It is possible that the message has gone out, but not been acked yet. In that case
						// we can store the HIGHEST_OUT_MSG_NUMBER as the LAST_OUT_MESSAGE_NO, so that when the
						// ack arrives we will terminate the sequence
						rmsBean.setLastOutMessage(highestOutMsgNo);
						storageManager.getRMSBeanMgr().update(rmsBean);
					}
				}
			}

			// If all the out message have been acked, add the outgoing
			// terminate seq msg.
			String outgoingSequnceID = SandeshaUtil.getSequenceIDFromInternalSequenceID(responseSideInternalSequenceId, storageManager); 

			if (addResponseSideTerminate && highestOutMsgNo > 0 && responseSideInternalSequenceId != null
					&& outgoingSequnceID != null) {
				boolean allAcked = SandeshaUtil.isAllMsgsAckedUpto(highestOutMsgNo, responseSideInternalSequenceId, storageManager);

				if (allAcked)
				{
					RMSBean rmsBean = SandeshaUtil.getRMSBeanFromSequenceId(storageManager, outgoingSequnceID);
					if (!rmsBean.isTerminateAdded()) {
						TerminateManager.addTerminateSequenceMessage(terminateRMMsg, rmsBean.getInternalSequenceID(), outgoingSequnceID , storageManager);
						String referenceMsgKey = rmsBean.getReferenceMessageStoreKey();
						if (referenceMsgKey==null) {
							String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.referenceMessageNotSetForSequence,rmsBean.getSequenceID());
							throw new SandeshaException (message);
						}
						
						MessageContext referenceMessage = storageManager.retrieveMessageContext(referenceMsgKey, configCtx);
						
						if (referenceMessage==null) {
							String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.referencedMessageNotFound, rmsBean.getSequenceID());
							throw new SandeshaException (message);
						}
						
						//RMMsgContext referenceRMMsg = MsgInitializer.initializeMessage(referenceMessage);
									
					}
				}
			}
		} catch (AxisFault e) {
			throw new SandeshaException(e);
		}
		if (log.isDebugEnabled())
			log.debug("Exit: TerminateSeqMsgProcessor::setUpHighestMsgNumbers");
	}

	private RMMsgContext getTerminateSequenceResponse(RMMsgContext terminateSeqRMMsg, RMDBean rmdBean, String sequenceId,
			StorageManager storageManager) throws AxisFault {

		if (log.isDebugEnabled())
			log.debug("Enter: TerminateSeqMsgProcessor::addTerminateSequenceResponse, " + sequenceId);

		RMMsgContext terminateSeqResponseRMMsg = RMMsgCreator.createTerminateSeqResponseMsg(terminateSeqRMMsg, rmdBean);
		MessageContext outMessage = terminateSeqResponseRMMsg.getMessageContext();

		RMMsgContext ackRMMessage = AcknowledgementManager.generateAckMessage(terminateSeqRMMsg, rmdBean, 
				sequenceId,	storageManager, true);
		
		// copy over the ack parts
		Iterator iter = ackRMMessage.getMessageParts(Sandesha2Constants.MessageParts.SEQ_ACKNOWLEDGEMENT);
		while (iter.hasNext()) {
			SequenceAcknowledgement seqAck = (SequenceAcknowledgement) iter.next();
			terminateSeqResponseRMMsg.setMessagePart(Sandesha2Constants.MessageParts.SEQ_ACKNOWLEDGEMENT, seqAck);
		}
		
		terminateSeqResponseRMMsg.addSOAPEnvelope();

		terminateSeqResponseRMMsg.setFlow(MessageContext.OUT_FLOW);
		terminateSeqResponseRMMsg.setProperty(Sandesha2Constants.APPLICATION_PROCESSING_DONE, "true");

		outMessage.setResponseWritten(true);
		
		if (log.isDebugEnabled())
			log.debug("Exit: TerminateSeqMsgProcessor::addTerminateSequenceResponse");

		return terminateSeqResponseRMMsg;
	}

	public boolean processOutMessage(RMMsgContext rmMsgCtx) throws AxisFault {

		if (log.isDebugEnabled())
			log.debug("Enter: TerminateSeqMsgProcessor::processOutMessage");

		// Get the parent processor to setup the out message
		setupOutMessage(rmMsgCtx);
		
		RMSBean rmsBean = SandeshaUtil.getRMSBeanFromInternalSequenceId(getStorageManager(), getInternalSequenceID());
		
		// Check if the sequence is already terminated (stored on the internal sequenceid)
		if (rmsBean.isTerminateAdded()) {
			String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.terminateAddedPreviously);
			log.debug(message);
			if (log.isDebugEnabled())
				log.debug("Exit: TerminateSeqMsgProcessor::processOutMessage, sequence previously terminated");
			return true;
		}

		AxisOperation terminateOp = SpecSpecificConstants.getWSRMOperation(
				Sandesha2Constants.MessageTypes.TERMINATE_SEQ,
				rmMsgCtx.getRMSpecVersion(),
				getMsgContext().getAxisService());
	
		OperationContext opcontext = OperationContextFactory.createOperationContext(terminateOp.getAxisSpecificMEPConstant(), terminateOp, getMsgContext().getServiceContext());
		opcontext.setParent(getMsgContext().getServiceContext());

		getConfigurationContext().registerOperationContext(rmMsgCtx.getMessageId(),	opcontext);

		getMsgContext().setOperationContext(opcontext);
		getMsgContext().setAxisOperation(terminateOp);

		TerminateSequence terminateSequencePart = (TerminateSequence) rmMsgCtx
				.getMessagePart(Sandesha2Constants.MessageParts.TERMINATE_SEQ);
		terminateSequencePart.getIdentifier().setIndentifer(getOutSequenceID());

		rmMsgCtx.setWSAAction(SpecSpecificConstants.getTerminateSequenceAction(getRMVersion()));
		rmMsgCtx.setSOAPAction(SpecSpecificConstants.getTerminateSequenceSOAPAction(getRMVersion()));
		
		rmsBean.setTerminateAdded(true);

		// Update the RMSBean with the terminate added flag
		getStorageManager().getRMSBeanMgr().update(rmsBean);

		// Send the outgoing message
		// Set a retransmitter lastSentTime so that terminate will be send with
		// some delay.
		// Otherwise this get send before return of the current request (ack).
		// TODO: refine the terminate delay.
		sendOutgoingMessage(rmMsgCtx, Sandesha2Constants.MessageTypes.TERMINATE_SEQ, Sandesha2Constants.TERMINATE_DELAY);		

		// Pause the message context
		rmMsgCtx.pause();

		if (log.isDebugEnabled())
			log.debug("Exit: TerminateSeqMsgProcessor::processOutMessage " + Boolean.TRUE);
		return true;
	}
}
