/*
 * 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 org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.AddressingConstants;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.util.JavaUtils;
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.client.SandeshaClient;
import org.apache.sandesha2.client.SandeshaClientConstants;
import org.apache.sandesha2.client.SandeshaListener;
import org.apache.sandesha2.client.SequenceReport;
import org.apache.sandesha2.i18n.SandeshaMessageHelper;
import org.apache.sandesha2.i18n.SandeshaMessageKeys;
import org.apache.sandesha2.policy.SandeshaPolicyBean;
import org.apache.sandesha2.security.SecurityManager;
import org.apache.sandesha2.security.SecurityToken;
import org.apache.sandesha2.storage.StorageManager;
import org.apache.sandesha2.storage.beans.RMDBean;
import org.apache.sandesha2.storage.beans.RMSBean;
import org.apache.sandesha2.workers.SequenceEntry;
import org.apache.sandesha2.wsrm.CreateSequence;

/**
 * This is used to set up a new sequence, both at the sending side and the
 * receiving side.
 */

public class SequenceManager {

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

	/**
	 * Set up a new inbound sequence, triggered by the arrival of a create sequence message. As this
	 * is an inbound sequence, the sequencePropertyKey is the sequenceId.
	 */
	public static RMDBean setupNewSequence(RMMsgContext createSequenceMsg, StorageManager storageManager, SecurityManager securityManager, SecurityToken token)
			throws AxisFault {
		if (log.isDebugEnabled())
			log.debug("Enter: SequenceManager::setupNewSequence");
		
		String sequenceId = SandeshaUtil.getUUID();

		// Generate the new RMD Bean
		RMDBean rmdBean = new RMDBean();

		EndpointReference to = createSequenceMsg.getTo();
		if (to == null) {
			String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.toEPRNotValid, null);
			log.debug(message);
			throw new AxisFault(message);
		}

		EndpointReference replyTo = createSequenceMsg.getReplyTo();

		CreateSequence createSequence = (CreateSequence) createSequenceMsg
				.getMessagePart(Sandesha2Constants.MessageParts.CREATE_SEQ);
		if (createSequence == null) {
			String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.createSeqEntryNotFound);
			log.debug(message);
			throw new AxisFault(message);
		}

		EndpointReference acksTo = createSequence.getAcksTo().getEPR();

		if (acksTo == null) {
			FaultManager.makeCreateSequenceRefusedFault(createSequenceMsg, SandeshaMessageHelper.getMessage(SandeshaMessageKeys.noAcksToPartInCreateSequence), new Exception());
		} else if (acksTo.getAddress().equals(AddressingConstants.Final.WSA_NONE_URI)){
			FaultManager.makeCreateSequenceRefusedFault(createSequenceMsg, "AcksTo can not be " + AddressingConstants.Final.WSA_NONE_URI, new Exception());
		}

		MessageContext createSeqContext = createSequenceMsg.getMessageContext();
		
		// If this create is the result of a MakeConnection, then we must have a related
		// outbound sequence.
		SequenceEntry entry = (SequenceEntry) createSeqContext.getProperty(Sandesha2Constants.MessageContextProperties.MAKECONNECTION_ENTRY);
		if(log.isDebugEnabled()) log.debug("This message is associated with sequence entry: " + entry);
		if(entry != null && entry.isRmSource()) {
			rmdBean.setOutboundInternalSequence(entry.getSequenceId());
		}

		rmdBean.setServerCompletedMessages(new RangeString());
		
		rmdBean.setReplyToEPR(to.getAddress());
		rmdBean.setAcksToEPR(acksTo.getAddress());

		// If no replyTo value. Send responses as sync.
		if (replyTo != null)
			rmdBean.setToEPR(replyTo.getAddress());

		// Store the security token alongside the sequence
		if(token != null) {
			String tokenData = securityManager.getTokenRecoveryData(token);
			rmdBean.setSecurityTokenData(tokenData);
		}		
		
		rmdBean.setSequenceID(sequenceId);
		rmdBean.setNextMsgNoToProcess(1);
		
		rmdBean.setToAddress(to.getAddress());
		
		// If this sequence has a 'To' address that is anonymous then we must have got the
		// message as a response to a poll. We need to make sure that we keep polling until
		// the sequence is closed.
		if(to.hasAnonymousAddress()) {
			String newKey = SandeshaUtil.getUUID();
			rmdBean.setPollingMode(true);
			rmdBean.setReferenceMessageKey(newKey);
			storageManager.storeMessageContext(newKey, createSeqContext);
		}

		String messageRMNamespace = createSequence.getNamespaceValue();

		String specVersion = null;
		if (Sandesha2Constants.SPEC_2005_02.NS_URI.equals(messageRMNamespace)) {
			specVersion = Sandesha2Constants.SPEC_VERSIONS.v1_0;
		} else if (Sandesha2Constants.SPEC_2007_02.NS_URI.equals(messageRMNamespace)) {
			specVersion = Sandesha2Constants.SPEC_VERSIONS.v1_1;
		} else {
			throw new SandeshaException(SandeshaMessageHelper.getMessage(SandeshaMessageKeys.cannotDecideRMVersion));
		}

		rmdBean.setRMVersion(specVersion);
		rmdBean.setLastActivatedTime(System.currentTimeMillis());

		storageManager.getRMDBeanMgr().insert(rmdBean);

		// TODO get the SOAP version from the create seq message.

		if (log.isDebugEnabled())
			log.debug("Exit: SequenceManager::setupNewSequence, " + rmdBean);
		return rmdBean;
	}

	public void removeSequence(String sequence) {

	}

	public static RMSBean setupNewClientSequence(MessageContext firstAplicationMsgCtx,
			String internalSequenceId, StorageManager storageManager) throws SandeshaException {
		if (log.isDebugEnabled())
			log.debug("Enter: SequenceManager::setupNewClientSequence");
		
		RMSBean rmsBean = new RMSBean();
		rmsBean.setInternalSequenceID(internalSequenceId);

		// If we are server-side, we use the details from the inbound sequence to help set
		// up the reply sequence.
		String inboundSequence = null;
		RMDBean inboundBean = null;
		if(firstAplicationMsgCtx.isServerSide()) {
			inboundSequence = (String) firstAplicationMsgCtx.getProperty(Sandesha2Constants.MessageContextProperties.INBOUND_SEQUENCE_ID);
			if(inboundSequence != null) {
				inboundBean = SandeshaUtil.getRMDBeanFromSequenceId(storageManager, inboundSequence);
			}
		}
		
		// Finding the spec version
		String specVersion = getSpecVersion(firstAplicationMsgCtx, storageManager);
		rmsBean.setRMVersion(specVersion);

		// Set up the To EPR
		EndpointReference toEPR = firstAplicationMsgCtx.getTo();

		if (toEPR == null) {
			String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.toEPRNotValid, null);
			log.debug(message);
			throw new SandeshaException(message);
		}

		rmsBean.setToEPR(toEPR.getAddress());

		// Discover the correct acksTo and replyTo EPR for this RMSBean
		EndpointReference acksToEPR = null;
		EndpointReference replyToEPR = null;

		if (firstAplicationMsgCtx.isServerSide()) {
			// Server side, we want the replyTo and AcksTo EPRs to point into this server.
			// We can work that out by looking at the RMD bean that pulled the message in,
			// and copying its 'ReplyTo' address.
			if(inboundBean != null && inboundBean.getReplyToEPR() != null) {
				acksToEPR = new EndpointReference(inboundBean.getReplyToEPR());
				replyToEPR = new EndpointReference(inboundBean.getReplyToEPR());
			} else {
				String beanInfo = (inboundBean == null) ? "null" : inboundBean.toString();
				String message = SandeshaMessageHelper.getMessage(
						SandeshaMessageKeys.cannotChooseAcksTo, inboundSequence, beanInfo);
				SandeshaException e = new SandeshaException(message);
				if(log.isDebugEnabled()) log.debug("Throwing", e);
				throw e;
			}

		} else {
			replyToEPR = firstAplicationMsgCtx.getReplyTo();
			
			// If the replyTo is the none URI, we need to rewrite it as the
			// anon replyTo. Setting the value to null should have that effect.
			if(replyToEPR != null && replyToEPR.hasNoneAddress()) {
				replyToEPR = null;
			}

			// For client-side sequences there are 3 options:
			// 1) An explict AcksTo, set via the client API
			// 2) The replyTo from the app message
			// 3) The anonymous URI (for which we can leave a null EPR)
			String acksTo = (String) firstAplicationMsgCtx.getProperty(SandeshaClientConstants.AcksTo);
			if (acksTo != null) {
				if (log.isDebugEnabled())
					log.debug("Using explicit AcksTo, addr=" + acksTo);
				acksToEPR = new EndpointReference(acksTo);
			} else if(replyToEPR != null) {
				if (log.isDebugEnabled())
					log.debug("Using replyTo EPR as AcksTo, addr=" + replyToEPR.getAddress());
				acksToEPR = replyToEPR;
			}
		}
		// In case either of the replyTo or AcksTo is anonymous, rewrite them using the AnonURI template
		//(this should be done only for RM 1.1)
		ConfigurationContext config = firstAplicationMsgCtx.getConfigurationContext();
		
		if (Sandesha2Constants.SPEC_VERSIONS.v1_1.equals(specVersion)) {
			replyToEPR = SandeshaUtil.rewriteEPR(rmsBean, replyToEPR, config);
			acksToEPR = SandeshaUtil.rewriteEPR(rmsBean, acksToEPR, config);
		}
		
		// Store both the acksTo and replyTo 
		if(replyToEPR != null) rmsBean.setReplyToEPR(replyToEPR.getAddress());
		if(acksToEPR  != null) rmsBean.setAcksToEPR(acksToEPR.getAddress());
		
		// New up the client completed message ranges list
		rmsBean.setClientCompletedMessages(new RangeString());

		// saving transportTo value;
		String transportTo = (String) firstAplicationMsgCtx.getProperty(Constants.Configuration.TRANSPORT_URL);
		if (transportTo != null) {
			rmsBean.setTransportTo(transportTo);
		}

		// Set the soap version use by this client
		rmsBean.setSoapVersion(SandeshaUtil.getSOAPVersion(firstAplicationMsgCtx.getEnvelope()));

		//setting the autoTermination property for the client side.
		if (!firstAplicationMsgCtx.isServerSide()) {
			Object avoidAutoTermination = firstAplicationMsgCtx.getProperty(SandeshaClientConstants.AVOID_AUTO_TERMINATION);
			if (avoidAutoTermination!=null && JavaUtils.isTrueExplicitly(avoidAutoTermination))
				rmsBean.setAvoidAutoTermination(true);
		}
		// updating the last activated time.
		rmsBean.setLastActivatedTime(System.currentTimeMillis());
		
		if (log.isDebugEnabled())
			log.debug("Exit: SequenceManager::setupNewClientSequence " + rmsBean);
		return rmsBean;
	}

	public static boolean hasSequenceTimedOut(RMSBean rmsBean, String internalSequenceId, StorageManager storageManager)
			throws SandeshaException {

		//FIXME - get this policy from the service level.
		SandeshaPolicyBean propertyBean = 
			SandeshaUtil.getPropertyBean(storageManager.getContext().getAxisConfiguration());

		if (propertyBean.getInactivityTimeoutInterval() <= 0)
			return false;

		boolean sequenceTimedOut = false;
		
		long lastActivatedTime = rmsBean.getLastActivatedTime();
		long timeNow = System.currentTimeMillis();
		if (lastActivatedTime > 0 && (lastActivatedTime + propertyBean.getInactivityTimeoutInterval() < timeNow))
			sequenceTimedOut = true;

		return sequenceTimedOut;
	}
	
	public static void finalizeTimedOutSequence(String internalSequenceID, MessageContext messageContext,
			StorageManager storageManager) throws SandeshaException {
		ConfigurationContext configurationContext = null;
		if (messageContext == null)
			configurationContext = storageManager.getContext();
		else 
			configurationContext = messageContext.getConfigurationContext();			

		// Notify the clients of a timeout
		AxisFault fault = new AxisFault(
				SandeshaMessageHelper.getMessage(SandeshaMessageKeys.cannotSendMsgAsSequenceTimedout, internalSequenceID));
		// Notify any waiting clients that the sequence has timeed out.
		FaultManager.notifyClientsOfFault(internalSequenceID, storageManager, configurationContext, fault);
		
		// Already an active transaction, so don't want a new one
		TerminateManager.timeOutSendingSideSequence(internalSequenceID, storageManager);

		if (messageContext != null) {
			SandeshaListener listener = (SandeshaListener) messageContext
					.getProperty(SandeshaClientConstants.SANDESHA_LISTENER);
			if (listener != null) {
				SequenceReport report = SandeshaClient.getOutgoingSequenceReport(internalSequenceID, configurationContext, false);
				listener.onTimeOut(report);
			}
		}
	}

	public static String getSpecVersion(MessageContext applicationMessage, StorageManager storageManager)
	throws SandeshaException
	{
		String specVersion = null;
		if (applicationMessage.isServerSide()) {
			String inboundSequence = null;
			RMDBean inboundBean = null;
			if(applicationMessage.isServerSide()) {
				inboundSequence = (String) applicationMessage.getProperty(Sandesha2Constants.MessageContextProperties.INBOUND_SEQUENCE_ID);
				if(inboundSequence != null) {
					inboundBean = SandeshaUtil.getRMDBeanFromSequenceId(storageManager, inboundSequence);
				}
			}

			// in the server side, get the RM version from the request sequence.
			if(inboundBean == null || inboundBean.getRMVersion() == null) {
				String beanInfo = (inboundBean == null) ? "null" : inboundBean.toString();
				String message = SandeshaMessageHelper.getMessage(
						SandeshaMessageKeys.cannotChooseSpecLevel, inboundSequence, beanInfo );
				SandeshaException e = new SandeshaException(message);
				if(log.isDebugEnabled()) log.debug("Throwing", e);
				throw e;
			}

			specVersion = inboundBean.getRMVersion();
		} else {
			// in the client side, user will set the RM version.
			specVersion = (String) applicationMessage.getProperty(SandeshaClientConstants.RM_SPEC_VERSION);
			
			// If the spec version is null, look in the axis operation to see value has been set
			Parameter opLevel = applicationMessage.getAxisOperation().getParameter(SandeshaClientConstants.RM_SPEC_VERSION);
			if (specVersion == null && opLevel != null)	specVersion = (String) opLevel.getValue();						
		}

		if (specVersion == null)
			// TODO change the default to v1_1
			specVersion = SpecSpecificConstants.getDefaultSpecVersion(); 

		return specVersion;
	}
}
