/*
 * 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.core.request.handler;

import org.apache.wicket.Application;
import org.apache.wicket.core.request.mapper.IPageSource;
import org.apache.wicket.core.request.mapper.StalePageException;
import org.apache.wicket.page.IPageManager;
import org.apache.wicket.protocol.http.PageExpiredException;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.IRequestMapper;
import org.apache.wicket.request.component.IRequestablePage;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.io.IClusterable;
import org.apache.wicket.util.lang.Args;

/**
 * Provides page instance for request handlers. Each of the constructors has just enough information
 * to get existing or create new page instance. Requesting or creating page instance is deferred
 * until {@link #getPageInstance()} is called.
 * <p>
 * Purpose of this class is to reduce complexity of both {@link IRequestMapper}s and
 * {@link IRequestHandler}s. {@link IRequestMapper} examines the URL, gathers all relevant
 * information about the page in the URL (combination of page id, page class, page parameters and
 * render count), creates {@link PageProvider} object and creates a {@link IRequestHandler} instance
 * that can use the {@link PageProvider} to access the page.
 * <p>
 * Apart from simplifying {@link IRequestMapper}s and {@link IRequestHandler}s {@link PageProvider}
 * also helps performance because creating or obtaining page from {@link IPageManager} is delayed
 * until the {@link IRequestHandler} actually requires the page.
 * 
 * @author Matej Knopp
 */
public class PageProvider implements IPageProvider, IClusterable
{
	private static final long serialVersionUID = 1L;

	private final Integer renderCount;

	private final Integer pageId;

	private transient IPageSource pageSource;

	private Class<? extends IRequestablePage> pageClass;

	private PageParameters pageParameters;

	private transient Provision provision;

	/**
	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
	 * will return page instance with specified id.
	 * 
	 * @param pageId
	 * @param renderCount
	 *            optional argument
	 */
	public PageProvider(final Integer pageId, final Integer renderCount)
	{
		this.pageId = pageId;
		this.renderCount = renderCount;
	}

	/**
	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
	 * will return page instance with specified id if it exists and it's class matches pageClass. If
	 * none of these is true new page instance will be created.
	 * 
	 * @param pageId
	 * @param pageClass
	 * @param renderCount
	 *            optional argument
	 */
	public PageProvider(final Integer pageId, final Class<? extends IRequestablePage> pageClass,
		Integer renderCount)
	{
		this(pageId, pageClass, new PageParameters(), renderCount);
	}

	/**
	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
	 * will return page instance with specified id if it exists and it's class matches pageClass. If
	 * none of these is true new page instance will be created.
	 * 
	 * @param pageId
	 * @param pageClass
	 * @param pageParameters
	 * @param renderCount
	 *            optional argument
	 */
	public PageProvider(final Integer pageId, final Class<? extends IRequestablePage> pageClass,
		final PageParameters pageParameters, final Integer renderCount)
	{
		this.pageId = pageId;
		setPageClass(pageClass);
		setPageParameters(pageParameters);
		this.renderCount = renderCount;
	}

	/**
	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
	 * will return new instance of page with specified class.
	 * 
	 * @param pageClass
	 * @param pageParameters
	 */
	public PageProvider(final Class<? extends IRequestablePage> pageClass,
		final PageParameters pageParameters)
	{
		setPageClass(pageClass);
		if (pageParameters != null)
		{
			setPageParameters(pageParameters);
		}
		pageId = null;
		renderCount = null;
	}

	/**
	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
	 * will return new instance of page with specified class.
	 * 
	 * @param pageClass
	 */
	public PageProvider(Class<? extends IRequestablePage> pageClass)
	{
		this(pageClass, null);
	}

	/**
	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
	 * will return the given page instance.
	 * 
	 * @param page
	 */
	public PageProvider(IRequestablePage page)
	{
		Args.notNull(page, "page");

		provision = new Provision().resolveTo(page);
		pageId = page.getPageId();
		renderCount = page.getRenderCount();
	}

	private Provision getProvision()
	{
		if (provision == null)
		{
			provision = new Provision().resolve();
		}
		return provision;
	}

	@Override
	public IRequestablePage getPageInstance() throws PageExpiredException
	{
		IRequestablePage page = getProvision().getPage();
		
		if (page == null && wasExpired()) 
		{
			throw new PageExpiredException("Page with id '" + pageId + "' has expired.");
		}
		
		return page;
	}

	@Override
	public PageParameters getPageParameters() throws PageExpiredException
	{
		if (pageParameters != null)
		{
			return pageParameters;
		}
		else if (hasPageInstance())
		{
			return getPageInstance().getPageParameters();
		}
		else
		{
			return null;
		}
	}

	/**
	 * @return negates {@link PageProvider#hasPageInstance()}
	 * @deprecated use {@link PageProvider#hasPageInstance()} negation instead
	 */
	@Override
	public boolean isNewPageInstance()
	{
		return !hasPageInstance();
	}

	/**
	 * If this provider returns existing page, regardless if it was already created by PageProvider
	 * itself or is or can be found in the data store. The only guarantee is that by calling
	 * {@link PageProvider#getPageInstance()} this provider will return an existing instance and no
	 * page will be created.
	 * 
	 * @return if provides an existing page
	 */
	@Override
	public final boolean hasPageInstance()
	{
		if (provision != null || pageId != null)
		{
			return getProvision().didResolveToPage();
		}
		else
		{
			return false;
		}
	}

	/**
	 * Returns whether or not the page instance held by this provider has been instantiated by the
	 * provider.
	 * 
	 * @return {@code true} iff the page instance held by this provider was instantiated by the
	 *         provider
	 */
	@Override
	public final boolean doesProvideNewPage()
	{
		return getProvision().doesProvideNewPage();
	}

	@Override
	public boolean wasExpired()
	{
		return pageId != null && getProvision().didFailToFindStoredPage();
	}

	@Override
	public Class<? extends IRequestablePage> getPageClass() throws PageExpiredException
	{
		if (pageClass != null)
		{
			return pageClass;
		}
		else
		{
			return getPageInstance().getClass();
		}
	}

	protected IPageSource getPageSource()
	{
		if (pageSource != null)
		{
			return pageSource;
		}
		if (Application.exists())
		{
			return Application.get().getMapperContext();
		}
		else
		{
			throw new IllegalStateException(
				"No application is bound to current thread. Call setPageSource() to manually assign pageSource to this provider.");
		}
	}


	/**
	 * Detaches the page if it has been loaded (that means either
	 * {@link #PageProvider(IRequestablePage)} constructor has been used or
	 * {@link #getPageInstance()} has been called).
	 */
	@Override
	public void detach()
	{
		if (provision != null)
		{
			provision.detach();
			provision = null;
		}
	}

	/**
	 * If the {@link PageProvider} is used outside request thread (thread that does not have
	 * application instance assigned) it is necessary to specify a {@link IPageSource} instance so
	 * that {@link PageProvider} knows how to get a page instance.
	 * 
	 * @param pageSource
	 */
	public void setPageSource(IPageSource pageSource)
	{
		if (provision != null)
		{
			throw new IllegalStateException("A page was already provided.");
		}
		this.pageSource = pageSource;
	}

	/**
	 * 
	 * @param pageClass
	 */
	private void setPageClass(Class<? extends IRequestablePage> pageClass)
	{
		Args.notNull(pageClass, "pageClass");

		this.pageClass = pageClass;
	}

	/**
	 * 
	 * @param pageParameters
	 */
	protected void setPageParameters(PageParameters pageParameters)
	{
		this.pageParameters = pageParameters;
	}

	/**
	 * 
	 * @return page id
	 */
	@Override
	public Integer getPageId()
	{
		return pageId;
	}

	@Override
	public Integer getRenderCount()
	{
		return renderCount;
	}

	@Override
	public String toString()
	{
		return "PageProvider{" + "renderCount=" + renderCount + ", pageId=" + pageId
			+ ", pageClass=" + pageClass + ", pageParameters=" + pageParameters + '}';
	}

	/**
	 * A provision is the work necessary to provide a page. It includes to resolve parameters to a
	 * page, to track the resolution metadata and to keep a reference of the resolved page.
	 * 
	 * The logic based on {@link PageProvider}'s parameters:
	 * 
	 * - having an stored page id, the stored page is provided
	 * 
	 * - having only a page class, a new instance of it is provided
	 * 
	 * - having non stored page id plus page class, a new instance of the page class is provided
	 * 
	 * - having non stored page id and no page class, no page is provided
	 * 
	 * - being a page instance, the instance itself will be the provided page
	 *
	 * @author pedro
	 */
	private class Provision
	{
		transient IRequestablePage page;
		boolean failedToFindStoredPage;

		IRequestablePage getPage()
		{
			if (page == null && doesProvideNewPage())
			{
				page = getPageSource().newPageInstance(pageClass, pageParameters);
			}
			return page;
		}

		boolean didResolveToPage()
		{
			return page != null;
		}

		boolean doesProvideNewPage()
		{
			return (pageId == null || failedToFindStoredPage) && pageClass != null;
		}

		boolean didFailToFindStoredPage()
		{
			return failedToFindStoredPage;
		}

		Provision resolveTo(IRequestablePage page)
		{
			this.page = page;

			return this;
		}

		Provision resolve()
		{

			if (pageId != null)
			{
				IRequestablePage stored = getPageSource().getPageInstance(pageId);
				if (stored != null && (pageClass == null || pageClass.equals(stored.getClass())))
				{

					page = stored;

					if (renderCount != null && page.getRenderCount() != renderCount)
						throw new StalePageException(page);
				}

				failedToFindStoredPage = page == null;
			}

			return this;
		}

		void detach()
		{
			if (page != null)
			{
				page.detach();
			}
		}

	}
}
