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

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

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.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.storage.SandeshaStorageException;
import org.apache.sandesha2.storage.StorageManager;
import org.apache.sandesha2.storage.beanmanagers.InvokerBeanMgr;
import org.apache.sandesha2.storage.beanmanagers.RMDBeanMgr;
import org.apache.sandesha2.storage.beanmanagers.SenderBeanMgr;
import org.apache.sandesha2.storage.beans.RMSBean;
import org.apache.sandesha2.storage.beans.InvokerBean;
import org.apache.sandesha2.storage.beans.RMDBean;
import org.apache.sandesha2.storage.beans.SenderBean;

/**
 * Contains logic to remove all the storad data of a sequence. Methods of this
 * are called by sending side and the receiving side when appropriate
 */

public class TerminateManager {

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

	private static String CLEANED_ON_TERMINATE_MSG = "CleanedOnTerminateMsg";

	private static String CLEANED_AFTER_INVOCATION = "CleanedAfterInvocation";

	public static HashMap receivingSideCleanMap = new HashMap();

	public static void checkAndTerminate(ConfigurationContext configurationContext, StorageManager storageManager, RMSBean rmsBean)
	throws SandeshaStorageException, AxisFault {
		if(log.isDebugEnabled()) log.debug("Enter: TerminateManager::checkAndTerminate " +rmsBean);

		long lastOutMessage = rmsBean.getLastOutMessage ();

		if (lastOutMessage > 0 && !rmsBean.isTerminateAdded()) {
			
			boolean complete = AcknowledgementManager.verifySequenceCompletion(rmsBean.getClientCompletedMessages(), lastOutMessage);
			
			//If this is RM 1.1 and RMAnonURI scenario, dont do the termination unless the response side createSequence has been
			//received (RMDBean has been created) through polling, in this case termination will happen in the create sequence response processor.
			String rmVersion = rmsBean.getRMVersion();
			String replyToAddress = rmsBean.getReplyToEPR();

			if (complete &&
					Sandesha2Constants.SPEC_VERSIONS.v1_1.equals(rmVersion) && SandeshaUtil.isWSRMAnonymous(replyToAddress)) {
				RMDBean findBean = new RMDBean ();
				findBean.setPollingMode(true);
				findBean.setToAddress(replyToAddress);

				RMDBeanMgr rmdBeanMgr = storageManager.getRMDBeanMgr();
				List beans = rmdBeanMgr.find(findBean);
				if(beans.isEmpty()) {
					rmsBean.setTerminationPauserForCS(true);
					storageManager.getRMSBeanMgr().update(rmsBean);
					complete = false;
				}
			}
			
			// If we are doing sync 2-way over WSRM 1.0 then we may need to keep sending messages,
			// so check to see if all the senders have been removed
			EndpointReference replyTo = new EndpointReference (replyToAddress);
			if (complete &&
					Sandesha2Constants.SPEC_VERSIONS.v1_0.equals(rmVersion) && (replyToAddress==null || replyTo.hasAnonymousAddress())) {
				SenderBean matcher = new SenderBean();
				matcher.setMessageType(Sandesha2Constants.MessageTypes.APPLICATION);
				matcher.setSequenceID(rmsBean.getSequenceID());
				
				List matches = storageManager.getSenderBeanMgr().find(matcher);
				if(!matches.isEmpty()) complete = false;
			}
			
			if (complete) {
				
				String referenceMsgKey = rmsBean.getReferenceMessageStoreKey();
				if (referenceMsgKey==null) {
					String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.referenceMessageNotSetForSequence,rmsBean.getSequenceID());
					throw new SandeshaException (message);
				}
				
				MessageContext referenceMessage = storageManager.retrieveMessageContext(referenceMsgKey, configurationContext);
				
				if (referenceMessage==null) {
					String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.referencedMessageNotFound, rmsBean.getSequenceID());
					throw new SandeshaException (message);
				}
				
				RMMsgContext referenceRMMsg = MsgInitializer.initializeMessage(referenceMessage);
				addTerminateSequenceMessage(referenceRMMsg, rmsBean.getInternalSequenceID(), rmsBean.getSequenceID(), storageManager);
			}
			
		}

		if(log.isDebugEnabled()) log.debug("Exit: TerminateManager::checkAndTerminate");
	}
	
	
	/**
	 * Called by the receiving side to remove data related to a sequence. e.g.
	 * After sending the TerminateSequence message. Calling this methods will
	 * complete all the data if InOrder invocation is not sequired.
	 * 
	 * @param configContext
	 * @param sequenceID
	 * @throws SandeshaException
	 */
	public static void cleanReceivingSideOnTerminateMessage(ConfigurationContext configContext, String sequenceId,
			StorageManager storageManager) throws SandeshaException {

		// clean senderMap

		//removing any un-sent ack messages.
		SenderBean findAckBean = new SenderBean ();
		findAckBean.setSequenceID(sequenceId);
		findAckBean.setMessageType(Sandesha2Constants.MessageTypes.ACK);
		
		SenderBeanMgr senderBeanMgr = storageManager.getSenderBeanMgr();
		Iterator ackBeans = senderBeanMgr.find(findAckBean).iterator();
		while (ackBeans.hasNext()) {
			SenderBean ackBean = (SenderBean) ackBeans.next();
			senderBeanMgr.delete(ackBean.getMessageID());
			
			storageManager.removeMessageContext(ackBean.getMessageContextRefKey());
		}
		
		// Currently in-order invocation is done for default values.
		boolean inOrderInvocation = SandeshaUtil.getDefaultPropertyBean(configContext.getAxisConfiguration())
				.isInOrder();

		if (!inOrderInvocation) {
			// there is no invoking by Sandesha2. So clean invocations storages.
			
			receivingSideCleanMap.put(sequenceId, CLEANED_ON_TERMINATE_MSG);
			cleanReceivingSideAfterInvocation(sequenceId, storageManager);
		} else {

			String cleanStatus = (String) receivingSideCleanMap.get(sequenceId);
			if (cleanStatus != null
					&& CLEANED_AFTER_INVOCATION.equals(cleanStatus))
				// Remove the sequence from the map
				receivingSideCleanMap.remove(sequenceId);
				//completeTerminationOfReceivingSide(configContext,
				//		sequenceId, storageManager);
			else
				receivingSideCleanMap.put(sequenceId, CLEANED_ON_TERMINATE_MSG);
		}
	}

	/**
	 * When InOrder invocation is anabled this had to be called to clean the
	 * data left by the above method. This had to be called after the Invocation
	 * of the Last Message.
	 * 
	 * @param sequenceID
	 * @throws SandeshaException
	 */
	public static void cleanReceivingSideAfterInvocation(String sequenceId,
			StorageManager storageManager) throws SandeshaException {
		if(log.isDebugEnabled()) log.debug("Enter: TerminateManager::cleanReceivingSideAfterInvocation " +sequenceId);
		
		InvokerBeanMgr invokerBeanMgr = storageManager.getInvokerBeanMgr();

		// removing InvokerBean entries
		InvokerBean invokerFindBean = new InvokerBean();
		invokerFindBean.setSequenceID(sequenceId);
		Collection collection = invokerBeanMgr.find(invokerFindBean);
		Iterator iterator = collection.iterator();
		while (iterator.hasNext()) {
			InvokerBean invokerBean = (InvokerBean) iterator.next();
			String messageStoreKey = invokerBean.getMessageContextRefKey();
			invokerBeanMgr.delete(messageStoreKey);

			// removing the respective message context from the message store.
			storageManager.removeMessageContext(messageStoreKey);
		}

		String cleanStatus = (String) receivingSideCleanMap.get(sequenceId);
		if (cleanStatus != null && CLEANED_ON_TERMINATE_MSG.equals(cleanStatus))
			// Remove the sequence id from the map
			receivingSideCleanMap.remove(sequenceId);
			//completeTerminationOfReceivingSide(configContext, sequenceId, storageManager);
		else 
			receivingSideCleanMap.put(sequenceId, CLEANED_AFTER_INVOCATION);		
		
		if(log.isDebugEnabled()) log.debug("Exit: TerminateManager::cleanReceivingSideAfterInvocation");
	}

	/**
	 * This is called by the sending side to clean data related to a sequence.
	 * e.g. after sending the TerminateSequence message.
	 * 
	 * @param configContext
	 * @param sequenceID
	 * @throws SandeshaException
	 */
	public static void terminateSendingSide(RMSBean rmsBean, 
			StorageManager storageManager) throws SandeshaException {

		// Indicate that the sequence is terminated
		rmsBean.setTerminated(true);		
		storageManager.getRMSBeanMgr().update(rmsBean);
		
		cleanSendingSideData (rmsBean.getInternalSequenceID(), storageManager);
	}

	public static void timeOutSendingSideSequence(String internalSequenceId,
			StorageManager storageManager) throws SandeshaException {

		RMSBean rmsBean = SandeshaUtil.getRMSBeanFromInternalSequenceId(storageManager, internalSequenceId);
		rmsBean.setTimedOut(true);
		rmsBean.setLastActivatedTime(System.currentTimeMillis());
		storageManager.getRMSBeanMgr().update(rmsBean);

		cleanSendingSideData(internalSequenceId, storageManager);
	}

	private static void cleanSendingSideData(String internalSequenceId, StorageManager storageManager) throws SandeshaException {

		SenderBeanMgr retransmitterBeanMgr = storageManager.getSenderBeanMgr();

		// removing retransmitterMgr entries and corresponding message contexts.
		Collection collection = retransmitterBeanMgr.find(internalSequenceId);
		Iterator iterator = collection.iterator();
		while (iterator.hasNext()) {
			SenderBean retransmitterBean = (SenderBean) iterator.next();
			retransmitterBeanMgr.delete(retransmitterBean.getMessageID());

			String messageStoreKey = retransmitterBean.getMessageContextRefKey();
			storageManager.removeMessageContext(messageStoreKey);
		}
	}

	public static void addTerminateSequenceMessage(RMMsgContext referenceMessage, String internalSequenceID, String outSequenceId, StorageManager storageManager) throws AxisFault {
	
		if(log.isDebugEnabled())
			log.debug("Enter: TerminateManager::addTerminateSequenceMessage " + outSequenceId + ", " + internalSequenceID);

		RMSBean rmsBean = SandeshaUtil.getRMSBeanFromInternalSequenceId(storageManager, internalSequenceID);

		if (rmsBean.isTerminateAdded()) {
			if(log.isDebugEnabled())
				log.debug("Exit: TerminateManager::addTerminateSequenceMessage - terminate was added previously.");
			return;
		}

		RMMsgContext terminateRMMessage = RMMsgCreator.createTerminateSequenceMessage(referenceMessage, rmsBean, storageManager);
		terminateRMMessage.setFlow(MessageContext.OUT_FLOW);
		terminateRMMessage.setProperty(Sandesha2Constants.APPLICATION_PROCESSING_DONE, "true");

		
		//setting the To EPR.
		//First try to get it from an Endpoint property.
		//If not get it from the To property.
		
		EndpointReference toEPR = null;
		
		if (rmsBean.getOfferedEndPoint() != null)
			toEPR = new EndpointReference (rmsBean.getOfferedEndPoint());
		
		if (toEPR==null) {

			if (rmsBean.getToEPR()!=null) {
				toEPR = new EndpointReference(rmsBean.getToEPR());
				if (toEPR == null) {
					String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.toEPRNotValid, null);
					throw new SandeshaException(message);
				}
			}
		}

		if (toEPR!=null)
			terminateRMMessage.setTo(toEPR);
		
		if (rmsBean.getReplyToEPR()!=null) {
			terminateRMMessage.setReplyTo(new EndpointReference (rmsBean.getReplyToEPR()));
		}
		
		String rmVersion = rmsBean.getRMVersion();
		terminateRMMessage.setWSAAction(SpecSpecificConstants.getTerminateSequenceAction(rmVersion));
		terminateRMMessage.setSOAPAction(SpecSpecificConstants.getTerminateSequenceSOAPAction(rmVersion));

		if (rmsBean.getTransportTo() != null) {
			terminateRMMessage.setProperty(Constants.Configuration.TRANSPORT_URL, rmsBean.getTransportTo());
		}

		terminateRMMessage.addSOAPEnvelope();

		String key = SandeshaUtil.getUUID();

		SenderBean terminateBean = new SenderBean();
		terminateBean.setInternalSequenceID(internalSequenceID);
		terminateBean.setSequenceID(outSequenceId);
		terminateBean.setMessageContextRefKey(key);
		terminateBean.setMessageType(Sandesha2Constants.MessageTypes.TERMINATE_SEQ);

		// 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.
		terminateBean.setTimeToSend(System.currentTimeMillis() + Sandesha2Constants.TERMINATE_DELAY);

		terminateBean.setMessageID(terminateRMMessage.getMessageId());

		// this will be set to true at the sender.
		terminateBean.setSend(true);

		terminateRMMessage.getMessageContext().setProperty(Sandesha2Constants.QUALIFIED_FOR_SENDING,
				Sandesha2Constants.VALUE_FALSE);

		terminateBean.setReSend(false);
		
		terminateBean.setSequenceID(outSequenceId);
		
		terminateBean.setMessageType(Sandesha2Constants.MessageTypes.TERMINATE_SEQ);
		terminateBean.setInternalSequenceID(internalSequenceID);
		
		
		EndpointReference to = terminateRMMessage.getTo();
		if (to!=null)
			terminateBean.setToAddress(to.getAddress());

		// If this message is targetted at an anonymous address then we must not have a transport
		// ready for it, as the terminate sequence is not a reply.
		if(to == null || to.hasAnonymousAddress())
			terminateBean.setTransportAvailable(false);

		rmsBean.setTerminateAdded(true);

		storageManager.getRMSBeanMgr().update(rmsBean);

		terminateRMMessage.setProperty(Sandesha2Constants.SET_SEND_TO_TRUE, Sandesha2Constants.VALUE_TRUE);
		
		//the propertyKey of the ackMessage will be the propertyKey for the terminate message as well.
//		terminateRMMessage.setProperty(Sandesha2Constants.MessageContextProperties.SEQUENCE_PROPERTY_KEY, sequencePropertyKey);
		
		// / addTerminateSeqTransaction.commit();
		SandeshaUtil.executeAndStore(terminateRMMessage, key);
		
		SenderBeanMgr retramsmitterMgr = storageManager.getSenderBeanMgr();
		
		
		retramsmitterMgr.insert(terminateBean);

		if(log.isDebugEnabled())
			log.debug("Exit: TerminateManager::addTerminateSequenceMessage");
	}

}
