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

import javax.enterprise.context.Conversation;
import javax.enterprise.context.ConversationScoped;
import javax.enterprise.context.NonexistentConversationException;
import javax.inject.Inject;

import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.MetaDataKey;
import org.apache.wicket.Page;
import org.apache.wicket.core.request.handler.BufferedResponseRequestHandler;
import org.apache.wicket.core.request.handler.IPageClassRequestHandler;
import org.apache.wicket.core.request.handler.IPageRequestHandler;
import org.apache.wicket.core.request.mapper.StalePageException;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.IRequestHandlerDelegate;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.component.IRequestablePage;
import org.apache.wicket.request.cycle.AbstractRequestCycleListener;
import org.apache.wicket.request.cycle.IRequestCycleListener;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;
import org.apache.wicket.util.visit.Visits;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A request cycle listener that takes care of propagating persistent conversations.
 * 
 * @see ConversationScoped
 * 
 * @author igor
 */
public class ConversationPropagator extends AbstractRequestCycleListener
{
	private static final Logger logger = LoggerFactory.getLogger(ConversationPropagator.class);

	private static final MetaDataKey<String> CID_KEY = ConversationIdMetaKey.INSTANCE;

	private static final MetaDataKey<Boolean> CONVERSATION_STARTED_KEY = new MetaDataKey<Boolean>()
	{
	};


	public static final String CID = "cid";

	private final CdiContainer container;

	/** propagation mode to use */
	private final IConversationPropagation propagation;

	private final Application application;

	private final boolean auto;

	@Inject
	Conversation conversation_;

	@Inject
	AutoConversation autoConversation;

	/**
	 * Constructor
	 * 
	 * @param container
	 * @param propagation
	 */
	public ConversationPropagator(Application application, CdiContainer container,
		IConversationPropagation propagation)
	{
		this(application, container, propagation, false);
	}


	/**
	 * Constructor
	 * 
	 * @param container
	 * @param propagation
	 */
	public ConversationPropagator(Application application, CdiContainer container,
		IConversationPropagation propagation, boolean auto)
	{
		Args.notNull(application, "application");
		Args.notNull(container, "container");
		Args.notNull(propagation, "propagation");

		if (propagation == ConversationPropagation.NONE)
		{
			throw new IllegalArgumentException(
				"If propagation is NONE do not set up the propagator");
		}

		this.application = application;
		this.container = container;
		this.propagation = propagation;
		this.auto = auto;

		container.getNonContextualManager().postConstruct(this);
	}

	private Conversation getConversation(RequestCycle cycle)
	{
		return Boolean.TRUE.equals(cycle.getMetaData(CONVERSATION_STARTED_KEY)) ? conversation_
			: null;
	}

	@Override
	public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler)
	{
		String cid = cycle.getRequest().getRequestParameters().getParameterValue(CID).toString();
		Page page = getPage(handler);

		if (cid == null && page != null)
		{
			cid = page.getMetaData(CID_KEY);
		}

		Conversation current = getConversation(cycle);
		if (current != null && !Objects.isEqual(current.getId(), cid))
		{
			logger.info("Conversation {} has expired for {}", cid, page);
			throw new ConversationExpiredException(null, cid, page, handler);
		}

		activateConversationIfNeeded(cycle, handler, cid);
	}

	@Override
	public IRequestHandler onException(RequestCycle cycle, Exception ex)
	{
		// if we are handling a stale page exception then use its conversation since we are most
		// likely about to rerender it.

		if (ex instanceof StalePageException)
		{
			IRequestablePage requestable = ((StalePageException)ex).getPage();
			if (requestable instanceof Page)
			{
				String cid = container.getConversationMarker((Page)requestable);
				if (cid != null)
				{
					try
					{
						activateConversationIfNeeded(cycle, null, cid);
						return null;
					}
					catch (ConversationExpiredException e)
					{
						// ignore, we will start a new one below
					}
				}
			}
		}

		activateConversationIfNeeded(cycle, null, null);
		return null;
	}

	private void activateConversationIfNeeded(RequestCycle cycle, IRequestHandler handler,
		String cid)
	{
		Conversation current = getConversation(cycle);

		if (current != null || !activateForHandler(handler))
		{
			return;
		}

		logger.debug("Activating conversation {}", cid);

		try
		{
			container.activateConversationalContext(cycle, cid);
			fireOnAfterConversationStarted(cycle);
		}
		catch (NonexistentConversationException e)
		{
			logger.info("Unable to restore conversation with id {}", cid, e.getMessage());
			logger.debug("Unable to restore conversation", e);
			fireOnAfterConversationStarted(cycle);
			throw new ConversationExpiredException(e, cid, getPage(handler), handler);
		}

		cycle.setMetaData(CONVERSATION_STARTED_KEY, true);
	}

	private void fireOnAfterConversationStarted(RequestCycle cycle)
	{
		for (IRequestCycleListener listener : application.getRequestCycleListeners())
		{
			if (listener instanceof ICdiAwareRequestCycleListener)
			{
				((ICdiAwareRequestCycleListener)listener).onAfterConversationActivated(cycle);
			}
		}
	}

	@Override
	public void onRequestHandlerExecuted(RequestCycle cycle, IRequestHandler handler)
	{
		Conversation conversation = getConversation(cycle);

		if (conversation == null)
		{
			return;
		}

		Page page = getPage(handler);

		if (page == null)
		{
			return;
		}

		// apply auto semantics

		autoEndIfNecessary(page, handler, conversation);
		autoBeginIfNecessary(page, handler, conversation);

		if (propagation.propagatesViaPage(page, handler))
		{
			// propagate a conversation across non-bookmarkable page instances
			setConversationOnPage(conversation, page);
		}
	}

	@Override
	public void onRequestHandlerScheduled(RequestCycle cycle, IRequestHandler handler)
	{
		// propagate current non-transient conversation to the newly scheduled page

		Conversation conversation = getConversation(cycle);

		if (conversation == null || conversation.isTransient())
		{
			return;
		}

		Page page = getPage(handler);
		if (page != null)
		{
			if (propagation.propagatesViaPage(page, handler))
			{
				// propagate a conversation across non-bookmarkable page instances
				setConversationOnPage(conversation, page);
			}
		}

		if (propagation.propagatesViaParameters(handler))
		{
			// propagate cid to a scheduled bookmarkable page

			logger.debug(
				"Propagating non-transient conversation {} via page parameters of handler {}",
				conversation.getId(), handler);

			PageParameters parameters = getPageParameters(handler);
			if (parameters != null)
			{
				parameters.set(CID, conversation.getId());
			}
		}
	}

	protected void setConversationOnPage(Conversation conversation, Page page)
	{
		if (conversation == null || conversation.isTransient())
		{
			logger.debug("Detaching transient conversation {} via meta of page instance {}",
				(conversation == null ? "null" : conversation.getId()), page);

			page.setMetaData(CID_KEY, null);
		}
		else
		{

			logger.debug("Propagating non-transient conversation {} via meta of page instance {}",
				conversation.getId(), page);

			page.setMetaData(CID_KEY, conversation.getId());
		}
	}

	@Override
	public void onUrlMapped(RequestCycle cycle, IRequestHandler handler, Url url)
	{
		// no need to propagate the conversation to packaged resources, they should never change
		if (handler instanceof ResourceReferenceRequestHandler)
		{
			if (((ResourceReferenceRequestHandler)handler).getResourceReference() instanceof PackageResourceReference)
			{
				return;
			}
		}

		Conversation conversation = getConversation(cycle);

		if (conversation == null || conversation.isTransient())
		{
			return;
		}

		if (propagation.propagatesViaParameters(handler))
		{
			// propagate cid to bookmarkable pages via urls

			logger.debug("Propagating non-transient conversation {} via url", conversation.getId());

			url.setQueryParameter(CID, conversation.getId());
		}
	}

	@Override
	public void onDetach(RequestCycle cycle)
	{
		Conversation conversation = getConversation(cycle);
		if (conversation != null)
		{
			logger.debug("Deactivating conversation {}", conversation.getId());

			for (IRequestCycleListener listener : application.getRequestCycleListeners())
			{
				if (listener instanceof ICdiAwareRequestCycleListener)
				{
					((ICdiAwareRequestCycleListener)listener).onBeforeConversationDeactivated(cycle);
				}
			}
			container.deactivateConversationalContext(cycle);

			cycle.setMetaData(CONVERSATION_STARTED_KEY, null);
		}
	}

	/**
	 * Determines whether or not a conversation should be activated fro the specified handler. This
	 * method is used to filter out conversation activation for utility handlers such as the
	 * {@link BufferedResponseRequestHandler}
	 * 
	 * @param handler
	 * @return {@code true} iff a conversation should be activated
	 */
	protected boolean activateForHandler(IRequestHandler handler)
	{
		if (handler != null)
		{
			if (handler instanceof BufferedResponseRequestHandler)
			{
				// we do not care about pages that are being rendered from a buffer
				return false;
			}
		}
		return true;
	}

	protected void autoBeginIfNecessary(Page page, IRequestHandler handler,
		Conversation conversation)
	{
		if (!auto || conversation == null || !conversation.isTransient() || page == null ||
			!propagation.propagatesViaPage(page, handler) || !hasConversationalComponent(page))
		{
			return;
		}

		// auto activate conversation

		conversation.begin();
		autoConversation.setAutomatic(true);

		logger.debug("Auto-began conversation {} for page {}", conversation.getId(), page);
	}

	protected void autoEndIfNecessary(Page page, IRequestHandler handler, Conversation conversation)
	{
		if (!auto || conversation == null || conversation.isTransient() || page == null ||
			!propagation.propagatesViaPage(page, handler) || hasConversationalComponent(page) ||
			autoConversation.isAutomatic() == false)
		{
			return;
		}

		// auto de-activate conversation

		String cid = conversation.getId();

		autoConversation.setAutomatic(false);
		conversation.end();

		logger.debug("Auto-ended conversation {} for page {}", cid, page);
	}


	protected boolean hasConversationalComponent(Page page)
	{
		Boolean hasConversational = Visits.visit(page, new IVisitor<Component, Boolean>()
		{
			@Override
			public void component(Component object, IVisit<Boolean> visit)
			{
				if (object instanceof ConversationalComponent)
				{
					visit.stop(true);
				}
			}
		});

		return hasConversational == null ? false : hasConversational;
	}

	/**
	 * Resolves a page instance from the request handler iff the page instance is already created
	 * 
	 * @param handler
	 * @return page or {@code null} if none
	 */
	public static Page getPage(IRequestHandler handler)
	{
		while (handler instanceof IRequestHandlerDelegate)
		{
			handler = ((IRequestHandlerDelegate)handler).getDelegateHandler();
		}

		if (handler instanceof IPageRequestHandler)
		{
			IPageRequestHandler pageHandler = (IPageRequestHandler)handler;
			if (pageHandler.isPageInstanceCreated())
			{
				return (Page)pageHandler.getPage();
			}
		}
		return null;
	}

	/**
	 * Resolves page parameters from a request handler
	 * 
	 * @param handler
	 * @return page parameters or {@code null} if none
	 */
	protected PageParameters getPageParameters(IRequestHandler handler)
	{
		if (handler instanceof IPageClassRequestHandler)
		{
			IPageClassRequestHandler pageHandler = (IPageClassRequestHandler)handler;
			return pageHandler.getPageParameters();
		}
		return null;
	}
}
