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

import java.net.URLConnection;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import org.apache.wicket.application.ComponentInitializationListenerCollection;
import org.apache.wicket.application.ComponentInstantiationListenerCollection;
import org.apache.wicket.application.ComponentOnAfterRenderListenerCollection;
import org.apache.wicket.application.ComponentOnBeforeRenderListenerCollection;
import org.apache.wicket.application.ComponentOnConfigureListenerCollection;
import org.apache.wicket.application.HeaderContributorListenerCollection;
import org.apache.wicket.application.IComponentInitializationListener;
import org.apache.wicket.application.IComponentInstantiationListener;
import org.apache.wicket.application.OnComponentTagListenerCollection;
import org.apache.wicket.core.request.mapper.IMapperContext;
import org.apache.wicket.core.util.lang.PropertyResolver;
import org.apache.wicket.core.util.lang.WicketObjects;
import org.apache.wicket.core.util.resource.ClassPathResourceFinder;
import org.apache.wicket.event.IEvent;
import org.apache.wicket.event.IEventSink;
import org.apache.wicket.javascript.DefaultJavaScriptCompressor;
import org.apache.wicket.markup.MarkupFactory;
import org.apache.wicket.markup.head.HeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.ResourceAggregator;
import org.apache.wicket.markup.html.HeaderResponseDecoratorCollection;
import org.apache.wicket.markup.html.IHeaderContributor;
import org.apache.wicket.markup.html.IHeaderResponseDecorator;
import org.apache.wicket.markup.html.image.resource.DefaultButtonImageResourceFactory;
import org.apache.wicket.markup.parser.filter.EnclosureHandler;
import org.apache.wicket.markup.parser.filter.InlineEnclosureHandler;
import org.apache.wicket.markup.parser.filter.RelativePathPrefixHandler;
import org.apache.wicket.markup.parser.filter.WicketLinkTagHandler;
import org.apache.wicket.markup.parser.filter.WicketMessageTagHandler;
import org.apache.wicket.markup.resolver.HtmlHeaderResolver;
import org.apache.wicket.markup.resolver.WicketContainerResolver;
import org.apache.wicket.markup.resolver.WicketMessageResolver;
import org.apache.wicket.page.IPageManager;
import org.apache.wicket.pageStore.IPageStore;
import org.apache.wicket.protocol.http.IRequestLogger;
import org.apache.wicket.protocol.http.RequestLogger;
import org.apache.wicket.protocol.http.RequestLoggerRequestCycleListener;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.WebSession;
import org.apache.wicket.request.IExceptionMapper;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.IRequestMapper;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.cycle.IRequestCycleListener;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.cycle.RequestCycleContext;
import org.apache.wicket.request.cycle.RequestCycleListenerCollection;
import org.apache.wicket.request.mapper.ICompoundRequestMapper;
import org.apache.wicket.request.resource.ResourceReferenceRegistry;
import org.apache.wicket.response.filter.EmptySrcAttributeCheckFilter;
import org.apache.wicket.session.DefaultPageFactory;
import org.apache.wicket.session.ISessionStore;
import org.apache.wicket.session.ISessionStore.UnboundListener;
import org.apache.wicket.settings.ApplicationSettings;
import org.apache.wicket.settings.DebugSettings;
import org.apache.wicket.settings.ExceptionSettings;
import org.apache.wicket.settings.FrameworkSettings;
import org.apache.wicket.settings.JavaScriptLibrarySettings;
import org.apache.wicket.settings.MarkupSettings;
import org.apache.wicket.settings.PageSettings;
import org.apache.wicket.settings.RequestCycleSettings;
import org.apache.wicket.settings.RequestLoggerSettings;
import org.apache.wicket.settings.ResourceSettings;
import org.apache.wicket.settings.SecuritySettings;
import org.apache.wicket.settings.StoreSettings;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Generics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Base class for all Wicket applications. To create a Wicket application, you generally should
 * <i>not </i> directly subclass this class. Instead, you will want to subclass some subclass of
 * Application, like WebApplication, which is appropriate for the protocol and markup type you are
 * working with.
 * <p>
 * Application has the following interesting features / attributes:
 * <ul>
 * <li><b>Name </b>- The Application's name, which is the same as its class name.
 * 
 * <li><b>Home Page </b>- The Application's home Page class. Subclasses must override getHomePage()
 * to provide this property value.
 * 
 * <li><b>Settings </b>- Application settings are partitioned into sets of related settings using
 * interfaces in the org.apache.wicket.settings package. These interfaces are returned by the
 * following methods, which should be used to configure framework settings for your application:
 * getApplicationSettings(), getDebugSettings(), getExceptionSettings(), getMarkupSettings(),
 * getPageSettings(), getRequestCycleSettings(), getSecuritySettings and getSessionSettings(). These
 * settings are configured by default through the constructor or internalInit methods. Default the
 * application is configured for DEVELOPMENT. You can configure this globally to DEPLOYMENT or
 * override specific settings by implementing the init() method.
 * 
 * <li><b>Shared Resources </b>- Resources added to an Application's SharedResources have
 * application-wide scope and can be referenced using a logical scope and a name with the
 * ResourceReference class. ResourceReferences can then be used by multiple components in the same
 * application without additional overhead (beyond the ResourceReference instance held by each
 * referee) and will yield a stable URL, permitting efficient browser caching of the resource (even
 * if the resource is dynamically generated). Resources shared in this manner may also be localized.
 * See {@link org.apache.wicket.request.resource.ResourceReference} for more details.
 * 
 * <li><b>Custom Session Subclasses</b>- In order to install your own {@link Session} subclass you
 * must override Application{@link #newSession(Request, Response)}. For subclasses of
 * {@link WebApplication} you will want to subclass {@link WebSession}.
 * 
 * </ul>
 * 
 * @see org.apache.wicket.protocol.http.WebApplication
 * @author Jonathan Locke
 */
public abstract class Application implements UnboundListener, IEventSink, IMetadataContext<Object, Application>
{
	/** Configuration constant for the 2 types */
	public static final String CONFIGURATION = "configuration";

	/**
	 * Applications keyed on the {@link #getApplicationKey()} so that they can be retrieved even
	 * without being in a request/ being set in the thread local (we need that e.g. for when we are
	 * in a destruction thread).
	 */
	private static final Map<String, Application> applicationKeyToApplication = Generics.newHashMap(1);

	/** Log. */
	private static final Logger log = LoggerFactory.getLogger(Application.class);

	/** root mapper */
	private IRequestMapper rootRequestMapper;

	/** The converter locator instance. */
	private IConverterLocator converterLocator;

	/** list of initializers. */
	private final List<IInitializer> initializers = Generics.newArrayList();

	/** Application level meta data. */
	private final ConcurrentHashMap<MetaDataKey<?>, Object> metaData = new ConcurrentHashMap<>();

	/** Name of application subclass. */
	private String name;

	/** Request logger instance. */
	private IRequestLogger requestLogger;

	/** The session facade. */
	private volatile ISessionStore sessionStore;

	/** page renderer provider */
	private IPageRendererProvider pageRendererProvider;

	/** request cycle provider */
	private IRequestCycleProvider requestCycleProvider;

	/** exception mapper provider */
	private Supplier<IExceptionMapper> exceptionMapperProvider;

	/** session store provider */
	private Supplier<ISessionStore> sessionStoreProvider;

	/**
	 * The decorator this application uses to decorate any header responses created by Wicket
	 */
	private HeaderResponseDecoratorCollection headerResponseDecorators =
		new HeaderResponseDecoratorCollection();

	/**
	 * Checks if the <code>Application</code> threadlocal is set in this thread
	 * 
	 * @return true if {@link Application#get()} can return the instance of application, false
	 *         otherwise
	 */
	public static boolean exists()
	{
		return ThreadContext.getApplication() != null;
	}

	/**
	 * Get Application for current thread.
	 * 
	 * @return The current thread's Application
	 */
	public static Application get()
	{
		Application application = ThreadContext.getApplication();
		if (application == null)
		{
			throw new WicketRuntimeException("There is no application attached to current thread " +
				Thread.currentThread().getName());
		}
		return application;
	}

	/**
	 * Gets the Application based on the application key of that application. You typically never
	 * have to use this method unless you are working on an integration project.
	 * 
	 * @param applicationKey
	 *            The unique key of the application within a certain context (e.g. a web
	 *            application)
	 * @return The application or <code>null</code> if application has not been found
	 */
	public static Application get(final String applicationKey)
	{
		return applicationKeyToApplication.get(applicationKey);
	}

	/**
	 * Gets the keys of the currently registered Wicket applications for this web application. You
	 * typically never have to use this method unless you are working on an integration project.
	 * 
	 * @return unmodifiable set with keys that correspond with {@link #getApplicationKey()}. Never
	 *         null, but possibly empty
	 */
	public static Set<String> getApplicationKeys()
	{
		return Collections.unmodifiableSet(applicationKeyToApplication.keySet());
	}

	/**
	 * Constructor. <strong>Use {@link #init()} for any configuration of your application instead of
	 * overriding the constructor.</strong>
	 */
	public Application()
	{
		// Install default component instantiation listener that uses
		// authorization strategy to check component instantiations.
		getComponentInstantiationListeners().add(new IComponentInstantiationListener()
		{
			/**
			 * @see org.apache.wicket.application.IComponentInstantiationListener#onInstantiation(org.apache.wicket.Component)
			 */
			@Override
			public void onInstantiation(final Component component)
			{
				final Class<? extends Component> cl = component.getClass();
				// If component instantiation is not authorized
				if (!Session.get().getAuthorizationStrategy().isInstantiationAuthorized(cl))
				{
					// then call any unauthorized component instantiation
					// listener
					getSecuritySettings().getUnauthorizedComponentInstantiationListener()
						.onUnauthorizedInstantiation(component);
				}
			}
		});
	}

	/**
	 * Configures application settings to good defaults.
	 */
	public final void configure()
	{
		// As long as this is public api the development and deployment mode
		// should counter act each other for all properties.
		switch (getConfigurationType())
		{
			case DEVELOPMENT : {
				getResourceSettings().setResourcePollFrequency(Duration.ofSeconds(1));
				getResourceSettings().setJavaScriptCompressor(null);
				getResourceSettings().setUseMinifiedResources(false);
				getMarkupSettings().setStripWicketTags(false);
				getExceptionSettings().setUnexpectedExceptionDisplay(
					ExceptionSettings.SHOW_EXCEPTION_PAGE);
				getDebugSettings().setComponentUseCheck(true);
				getDebugSettings().setAjaxDebugModeEnabled(true);
				getDebugSettings().setDevelopmentUtilitiesEnabled(true);
				// getDebugSettings().setOutputMarkupContainerClassName(true);
				getRequestCycleSettings().addResponseFilter(EmptySrcAttributeCheckFilter.INSTANCE);
				break;
			}
			case DEPLOYMENT : {
				getResourceSettings().setResourcePollFrequency(null);
				getResourceSettings().setJavaScriptCompressor(new DefaultJavaScriptCompressor());
				getMarkupSettings().setStripWicketTags(true);
				getExceptionSettings().setUnexpectedExceptionDisplay(
					ExceptionSettings.SHOW_INTERNAL_ERROR_PAGE);
				getDebugSettings().setComponentUseCheck(false);
				getDebugSettings().setAjaxDebugModeEnabled(false);
				getDebugSettings().setDevelopmentUtilitiesEnabled(false);
				break;
			}
		}
	}

	/**
	 * Gets the unique key of this application within a given context (like a web application). NOT
	 * INTENDED FOR FRAMEWORK CLIENTS.
	 * 
	 * @return The unique key of this application
	 */
	public abstract String getApplicationKey();

	/**
	 * Gets the configuration mode to use for configuring the app, either
	 * {@link RuntimeConfigurationType#DEVELOPMENT} or {@link RuntimeConfigurationType#DEPLOYMENT}.
	 * <p>
	 * The configuration type. Must currently be either DEVELOPMENT or DEPLOYMENT. Currently, if the
	 * configuration type is DEVELOPMENT, resources are polled for changes, component usage is
	 * checked, wicket tags are not stripped from output and a detailed exception page is used. If
	 * the type is DEPLOYMENT, component usage is not checked, wicket tags are stripped from output
	 * and a non-detailed exception page is used to display errors.
	 * <p>
	 * Note that you should not run Wicket in DEVELOPMENT mode on production servers - the various
	 * debugging checks and resource polling is inefficient and may leak resources, particularly on
	 * webapp redeploy.
	 * 
	 * <div style="border-style:solid;">
	 * <p>
	 * To change the deployment mode, add the following to your web.xml, inside your <servlet>
	 * mapping (or <filter> mapping if you're using 1.3.x):
	 * 
	 * <pre>
	 * &lt;init-param&gt;
	 *             &lt;param-name&gt;configuration&lt;/param-name&gt;
	 *             &lt;param-value&gt;deployment&lt;/param-value&gt;
	 * &lt;/init-param&gt;
	 * </pre>
	 * 
	 * <p>
	 * You can alternatively set this as a &lt;context-param&gt; on the whole context.
	 * 
	 * <p>
	 * Another option is to set the "wicket.configuration" system property to either "deployment" or
	 * "development". The value is not case-sensitive.
	 * 
	 * <p>
	 * The system property is checked first, allowing you to add a web.xml param for deployment, and
	 * a command-line override when you want to run in development mode during development.
	 * 
	 * <p>
	 * You may also override Application.getConfigurationType() to provide your own custom switch,
	 * in which case none of the above logic is used. </div>
	 * 
	 * <p>
	 * IMPORTANT NOTE
	 * </p>
	 * THIS METHOD IS CALLED OFTEN FROM MANY DIFFERENT POINTS IN CODE, INCLUDING DURING THE RENDER
	 * PROCESS, THEREFORE THE IMPLEMENTATION SHOULD BE FAST - PREFERRABLY USING A FAST-TO-RETRIEVE
	 * CACHED VALUE
	 * 
	 * @return configuration
	 * @since 1.2.3 (function existed as a property getter)
	 * @since 1.3.0 (abstract, used to configure things)
	 */
	public abstract RuntimeConfigurationType getConfigurationType();

	/**
	 * Application subclasses must specify a home page class by implementing this abstract method.
	 * 
	 * @return Home page class for this application
	 */
	public abstract Class<? extends Page> getHomePage();

	/**
	 * @return The converter locator for this application
	 */
	public final IConverterLocator getConverterLocator()
	{
		return converterLocator;
	}

	/**
	 * Gets metadata for this application using the given key.
	 * 
	 * @param <T>
	 * @param key
	 *            The key for the data
	 * @return The metadata
	 * @see MetaDataKey
	 */
	@Override
	@SuppressWarnings("unchecked")
	public final <T> T getMetaData(final MetaDataKey<T> key)
	{
		return (T)metaData.get(key);
	}

	/**
	 * Gets the name of this application.
	 * 
	 * @return The application name.
	 */
	public final String getName()
	{
		return name;
	}

	/**
	 * Gets the {@link IRequestLogger}.
	 * 
	 * @return The RequestLogger
	 */
	public final IRequestLogger getRequestLogger()
	{
		if (getRequestLoggerSettings().isRequestLoggerEnabled())
		{
			if (requestLogger == null)
			{
				requestLogger = newRequestLogger();
			}
		}
		else
		{
			requestLogger = null;
		}
		return requestLogger;
	}

	/**
	 * Gets the facade object for working getting/ storing session instances.
	 * 
	 * @return The session facade
	 */
	public final ISessionStore getSessionStore()
	{
		if (sessionStore == null)
		{
			synchronized (this)
			{
				if (sessionStore == null)
				{
					sessionStore = sessionStoreProvider.get();
					sessionStore.registerUnboundListener(this);
				}
			}
		}
		return sessionStore;
	}

	/**
	 * @see org.apache.wicket.session.ISessionStore.UnboundListener#sessionUnbound(java.lang.String)
	 */
	@Override
	public void sessionUnbound(final String sessionId)
	{
		getSessionListeners().onUnbound(sessionId);
	}

	/**
	 * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL.
	 * 
	 * @param target
	 */
	public void logEventTarget(final IRequestHandler target)
	{
	}

	/**
	 * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL.
	 * 
	 * @param requestTarget
	 */
	public void logResponseTarget(final IRequestHandler requestTarget)
	{
	}

	/**
	 * Creates a new session. Override this method if you want to provide a custom session.
	 * 
	 * @param request
	 *            The request that will create this session.
	 * @param response
	 *            The response to initialize, for example with cookies. This is important to use
	 *            cases involving unit testing because those use cases might want to be able to sign
	 *            a user in automatically when the session is created.
	 * 
	 * @return The session
	 * 
	 * @since 1.3
	 */
	public abstract Session newSession(Request request, Response response);

	/**
	 * Sets the metadata for this application using the given key. If the metadata object is not of
	 * the correct type for the metadata key, an IllegalArgumentException will be thrown. For
	 * information on creating MetaDataKeys, see {@link MetaDataKey}.
	 * 
	 * @param <T>
	 * @param key
	 *            The singleton key for the metadata
	 * @param object
	 *            The metadata object
	 * @throws IllegalArgumentException
	 * @see MetaDataKey
	 */
	@Override
	public final <T> Application setMetaData(final MetaDataKey<T> key, final T object)
	{
		metaData.put(key, object);
		return this;
	}

	/**
	 * Construct and add initializer from the provided class name.
	 * 
	 * @param className
	 */
	private void addInitializer(final String className)
	{
		IInitializer initializer = WicketObjects.newInstance(className);
		if (initializer != null)
		{
			initializers.add(initializer);
		}
	}

	/**
	 * Iterate initializers list, calling their {@link IInitializer#destroy(Application) destroy}
	 * methods.
	 */
	private void destroyInitializers()
	{
		for (IInitializer initializer : initializers)
		{
			log.info("[{}] destroy: {}", getName(), initializer);
			initializer.destroy(this);
		}
	}

	/**
	 * Iterate initializers list, calling {@link IInitializer#init(Application)} on any instances
	 * found in it.
	 */
	private void initInitializers()
	{
		for (IInitializer initializer : initializers)
		{
			log.info("[{}] init: {}", getName(), initializer);
			initializer.init(this);
		}

		final ServiceLoader<IInitializer> serviceLoaderInitializers = ServiceLoader.load(IInitializer.class);
		for (IInitializer serviceLoaderInitializer : serviceLoaderInitializers) {
			log.info("[{}] init: {}", getName(), serviceLoaderInitializer);
			serviceLoaderInitializer.init(this);
			initializers.add(serviceLoaderInitializer);
		}
	}

	/**
	 * Called when wicket servlet is destroyed. Overrides do not have to call super.
	 */
	protected void onDestroy()
	{
	}

	/**
	 * Allows for initialization of the application by a subclass. <strong>Use this method for any
	 * application setup instead of the constructor.</strong>
	 */
	protected void init()
	{
	}

	/**
	 * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
	 */
	public void internalDestroy()
	{
		applicationListeners.onBeforeDestroyed(this);

		// destroy detach listener
		final IDetachListener detachListener = getFrameworkSettings().getDetachListener();
		if (detachListener != null)
		{
			detachListener.onDestroyListener();
		}

		// Clear caches of Class keys so the classloader can be garbage
		// collected (WICKET-625)
		PropertyResolver.destroy(this);
		MarkupFactory markupFactory = getMarkupSettings().getMarkupFactory();

		if (markupFactory.hasMarkupCache())
		{
			markupFactory.getMarkupCache().shutdown();
		}

		onDestroy();

		destroyInitializers();

		internalGetPageManager().destroy();
		getSessionStore().destroy();

		applicationKeyToApplication.remove(getApplicationKey());
	}

	/**
	 * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT OVERRIDE OR CALL.
	 * 
	 * Internal initialization.
	 */
	protected void internalInit()
	{
		settingsAccessible = true;
		PageSettings pageSettings = getPageSettings();

		// Install default component resolvers
		pageSettings.addComponentResolver(new HtmlHeaderResolver());
		pageSettings.addComponentResolver(new WicketLinkTagHandler());
		pageSettings.addComponentResolver(new WicketMessageResolver());
		pageSettings.addComponentResolver(new RelativePathPrefixHandler());
		pageSettings.addComponentResolver(new EnclosureHandler());
		pageSettings.addComponentResolver(new InlineEnclosureHandler());
		pageSettings.addComponentResolver(new WicketMessageTagHandler());
		pageSettings.addComponentResolver(new WicketContainerResolver());

		getResourceSettings().getResourceFinders().add(new ClassPathResourceFinder(""));

		// Install button image resource factory
		getResourceSettings().addResourceFactory("buttonFactory",
			new DefaultButtonImageResourceFactory());

		String applicationKey = getApplicationKey();
		applicationKeyToApplication.put(applicationKey, this);

		converterLocator = newConverterLocator();

		setPageManagerProvider(new DefaultPageManagerProvider(this));
		resourceReferenceRegistry = newResourceReferenceRegistry();
		sharedResources = newSharedResources(resourceReferenceRegistry);
		resourceBundles = newResourceBundles(resourceReferenceRegistry);

		// set up default request mapper
		setRootRequestMapper(new SystemMapper(this));

		pageFactory = newPageFactory();

		requestCycleProvider = RequestCycle::new;
		exceptionMapperProvider = DefaultExceptionMapper::new;

		// add a request cycle listener that logs each request for the requestlogger.
		getRequestCycleListeners().add(new RequestLoggerRequestCycleListener());
	}

	/**
	 * Returns a supplier of {@link IExceptionMapper} that will be used to
	 * handle exceptions which were not handled by any
	 * {@link IRequestCycleListener#onException(RequestCycle, Exception) request cycle listener}.
	 *
	 * @return the exception mapper supplier
	 * @see IRequestCycleListener#onException(RequestCycle, Exception)
	 */
	public Supplier<IExceptionMapper> getExceptionMapperProvider()
	{
		return exceptionMapperProvider;
	}

	public void setExceptionMapperProvider(Supplier<IExceptionMapper> exceptionMapperProvider) {
		this.exceptionMapperProvider = Args.notNull(exceptionMapperProvider, "exceptionMapperProvider");
	}

	/**
	 * 
	 * @return Session state provider
	 */
	public final Supplier<ISessionStore> getSessionStoreProvider()
	{
		return sessionStoreProvider;
	}

	/**
	 * 
	 * @param sessionStoreProvider
	 */
	public final Application setSessionStoreProvider(final Supplier<ISessionStore> sessionStoreProvider)
	{
		this.sessionStoreProvider = Args.notNull(sessionStoreProvider, "sessionStoreProvider");
		this.sessionStore = null;
		return this;
	}

	/**
	 * Creates and returns a new instance of {@link IConverterLocator}.
	 * 
	 * @return A new {@link IConverterLocator} instance
	 */
	protected IConverterLocator newConverterLocator()
	{
		return new ConverterLocator();
	}

	/**
	 * creates a new request logger when requests logging is enabled.
	 * 
	 * @return The new request logger
	 * 
	 */
	protected IRequestLogger newRequestLogger()
	{
		return new RequestLogger();
	}

	/**
	 * Converts the root mapper to a {@link ICompoundRequestMapper} if necessary and returns the
	 * converted instance.
	 * 
	 * @return compound instance of the root mapper
	 */
	public final ICompoundRequestMapper getRootRequestMapperAsCompound()
	{
		IRequestMapper root = getRootRequestMapper();
		if (!(root instanceof ICompoundRequestMapper))
		{
			root = new SystemMapper(this).add(root);
			setRootRequestMapper(root);
		}
		return (ICompoundRequestMapper)root;
	}

	/**
	 * @return The root request mapper
	 */
	public final IRequestMapper getRootRequestMapper()
	{
		return rootRequestMapper;
	}

	/**
	 * Sets the root request mapper
	 * 
	 * @param rootRequestMapper
	 */
	public final Application setRootRequestMapper(final IRequestMapper rootRequestMapper)
	{
		this.rootRequestMapper = rootRequestMapper;
		return this;
	}

	/**
	 * Initialize the application
	 */
	public final void initApplication()
	{
		if (name == null)
		{
			throw new IllegalStateException("setName must be called before initApplication");
		}
		internalInit();
		initInitializers();
		init();
		applicationListeners.onAfterInitialized(this);

		validateInit();
	}

	/**
	 * Gives the Application object a chance to validate if it has been properly initialized
	 */
	protected void validateInit()
	{
		if (getPageRendererProvider() == null)
		{
			throw new IllegalStateException(
				"An instance of IPageRendererProvider has not yet been set on this Application. @see Application#setPageRendererProvider");
		}
		if (getSessionStoreProvider() == null)
		{
			throw new IllegalStateException(
				"An instance of ISessionStoreProvider has not yet been set on this Application. @see Application#setSessionStoreProvider");
		}
		if (getPageManagerProvider() == null)
		{
			throw new IllegalStateException(
				"An instance of IPageManagerProvider has not yet been set on this Application. @see Application#setPageManagerProvider");
		}
	}

	/**
	 * Sets application name. This method must be called before any other methods are invoked and
	 * can only be called once per application instance.
	 * 
	 * @param name
	 *            unique application name
	 */
	public final void setName(final String name)
	{
		Args.notEmpty(name, "name");

		if (this.name != null)
		{
			throw new IllegalStateException("Application name can only be set once.");
		}

		if (applicationKeyToApplication.get(name) != null)
		{
			throw new IllegalStateException("Application with name '" + name + "' already exists.'");
		}

		this.name = name;
		applicationKeyToApplication.put(name, this);
	}

	/**
	 * Returns the mime type for given filename.
	 * 
	 * @param fileName
	 * @return mime type
	 */
	public String getMimeType(final String fileName)
	{
		return URLConnection.getFileNameMap().getContentTypeFor(fileName);
	}

	/** {@inheritDoc} */
	@Override
	public void onEvent(final IEvent<?> event)
	{
	}

	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//
	// Listeners
	//
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	/** */
	private final ComponentOnBeforeRenderListenerCollection componentPreOnBeforeRenderListeners = new ComponentOnBeforeRenderListenerCollection();

	/** */
	private final ComponentOnBeforeRenderListenerCollection componentPostOnBeforeRenderListeners = new ComponentOnBeforeRenderListenerCollection();

	/** */
	private final ComponentOnAfterRenderListenerCollection componentOnAfterRenderListeners = new ComponentOnAfterRenderListenerCollection();

	/** */
	private final RequestCycleListenerCollection requestCycleListeners = new RequestCycleListenerCollection();

	private final ApplicationListenerCollection applicationListeners = new ApplicationListenerCollection();

	private final SessionListenerCollection sessionListeners = new SessionListenerCollection();

	/** list of {@link IComponentInstantiationListener}s. */
	private final ComponentInstantiationListenerCollection componentInstantiationListeners = new ComponentInstantiationListenerCollection();

	/** list of {@link IComponentInitializationListener}s. */
	private final ComponentInitializationListenerCollection componentInitializationListeners = new ComponentInitializationListenerCollection();

	/** list of {@link org.apache.wicket.application.IComponentOnConfigureListener}s. */
	private final ComponentOnConfigureListenerCollection componentOnConfigureListeners = new ComponentOnConfigureListenerCollection();

	/** list of {@link IHeaderContributor}s. */
	private final HeaderContributorListenerCollection headerContributorListeners = new HeaderContributorListenerCollection();

	private final BehaviorInstantiationListenerCollection behaviorInstantiationListeners = new BehaviorInstantiationListenerCollection();

	private final OnComponentTagListenerCollection onComponentTagListeners = new OnComponentTagListenerCollection();

	/**
	 * @return Gets the application's {@link HeaderContributorListenerCollection}
	 */
	public final HeaderContributorListenerCollection getHeaderContributorListeners()
	{
		return headerContributorListeners;
	}

	/**
	 * @return collection of initializers
	 */
	public final List<IInitializer> getInitializers()
	{
		return Collections.unmodifiableList(initializers);
	}

	/**
	 * @return collection of application listeners
	 */
	public final ApplicationListenerCollection getApplicationListeners()
	{
		return applicationListeners;
	}

	/**
	 * @return collection of session listeners
	 */
	public final SessionListenerCollection getSessionListeners()
	{
		return sessionListeners;
	}

	/**
	 * @return collection of behavior instantiation listeners
	 */
	public final BehaviorInstantiationListenerCollection getBehaviorInstantiationListeners()
	{
		return behaviorInstantiationListeners;
	}

	/**
	 * @return collection of application's on-component-tag listeners
	 */
	public final OnComponentTagListenerCollection getOnComponentTagListeners() {
		return onComponentTagListeners;
	}

	/**
	 * @return Gets the application's ComponentInstantiationListenerCollection
	 */
	public final ComponentInstantiationListenerCollection getComponentInstantiationListeners()
	{
		return componentInstantiationListeners;
	}

	/**
	 * @return Gets the application's ComponentInitializationListeners
	 */
	public final ComponentInitializationListenerCollection getComponentInitializationListeners()
	{
		return componentInitializationListeners;
	}

	/**
	 * @return Gets the application's ComponentOnConfigureListeners
	 */
	public final ComponentOnConfigureListenerCollection getComponentOnConfigureListeners()
	{
		return componentOnConfigureListeners;
	}

	/**
	 * 
	 * @return ComponentOnBeforeRenderListenerCollection
	 */
	public final ComponentOnBeforeRenderListenerCollection getComponentPreOnBeforeRenderListeners()
	{
		return componentPreOnBeforeRenderListeners;
	}

	/**
	 * 
	 * @return ComponentOnBeforeRenderListenerCollection
	 */
	public final ComponentOnBeforeRenderListenerCollection getComponentPostOnBeforeRenderListeners()
	{
		return componentPostOnBeforeRenderListeners;
	}

	/**
	 * @return on after render listeners collection
	 */
	public final ComponentOnAfterRenderListenerCollection getComponentOnAfterRenderListeners()
	{
		return componentOnAfterRenderListeners;
	}

	/**
	 * @return the unmodifiable request list of {@link IRequestCycleListener}s in this application
	 */
	public RequestCycleListenerCollection getRequestCycleListeners()
	{
		return requestCycleListeners;
	}

	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//
	// Settings
	//
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	/** Application settings */
	private ApplicationSettings applicationSettings;

	/** JavaScriptLibrary settings */
	private JavaScriptLibrarySettings javaScriptLibrarySettings;

	/** Debug Settings */
	private DebugSettings debugSettings;

	/** Exception Settings */
	private ExceptionSettings exceptionSettings;

	/** Framework Settings */
	private FrameworkSettings frameworkSettings;

	/** The Markup Settings */
	private MarkupSettings markupSettings;

	/** The Page Settings */
	private PageSettings pageSettings;

	/** The Request Cycle Settings */
	private RequestCycleSettings requestCycleSettings;

	/** The Request Logger Settings */
	private RequestLoggerSettings requestLoggerSettings;

	/** The Resource Settings */
	private ResourceSettings resourceSettings;

	/** The Security Settings */
	private SecuritySettings securitySettings;

	/** The settings for {@link IPageStore} and {@link IPageManager} */
	private StoreSettings storeSettings;

	/** can the settings object be set/used. */
	private boolean settingsAccessible;

	/**
	 * @return Application's application-wide settings
	 * @since 1.2
	 */
	public final ApplicationSettings getApplicationSettings()
	{
		checkSettingsAvailable();
		if (applicationSettings == null)
		{
			applicationSettings = new ApplicationSettings();
		}
		return applicationSettings;
	}

	/**
	 * 
	 * @param applicationSettings
	 */
	public final Application setApplicationSettings(final ApplicationSettings applicationSettings)
	{
		this.applicationSettings = applicationSettings;
		return this;
	}

	/**
	 * @return Application's JavaScriptLibrary settings
	 * @since 6.0
	 */
	public final JavaScriptLibrarySettings getJavaScriptLibrarySettings()
	{
		checkSettingsAvailable();
		if (javaScriptLibrarySettings == null)
		{
			javaScriptLibrarySettings = new JavaScriptLibrarySettings();
		}
		return javaScriptLibrarySettings;
	}

	/**
	 * 
	 * @param javaScriptLibrarySettings
	 */
	public final Application setJavaScriptLibrarySettings(
		final JavaScriptLibrarySettings javaScriptLibrarySettings)
	{
		this.javaScriptLibrarySettings = javaScriptLibrarySettings;
		return this;
	}

	/**
	 * @return Application's debug related settings
	 */
	public final DebugSettings getDebugSettings()
	{
		checkSettingsAvailable();
		if (debugSettings == null)
		{
			debugSettings = new DebugSettings();
		}
		return debugSettings;
	}

	/**
	 * 
	 * @param debugSettings
	 */
	public final Application setDebugSettings(final DebugSettings debugSettings)
	{
		this.debugSettings = debugSettings;
		return this;
	}

	/**
	 * @return Application's exception handling settings
	 */
	public final ExceptionSettings getExceptionSettings()
	{
		checkSettingsAvailable();
		if (exceptionSettings == null)
		{
			exceptionSettings = new ExceptionSettings();
		}
		return exceptionSettings;
	}

	/**
	 * 
	 * @param exceptionSettings
	 */
	public final Application setExceptionSettings(final ExceptionSettings exceptionSettings)
	{
		this.exceptionSettings = exceptionSettings;
		return this;
	}

	/**
	 * @return Wicket framework settings
	 */
	public final FrameworkSettings getFrameworkSettings()
	{
		checkSettingsAvailable();
		if (frameworkSettings == null)
		{
			frameworkSettings = new FrameworkSettings(this);
		}
		return frameworkSettings;
	}

	/**
	 * 
	 * @param frameworkSettings
	 */
	public final Application setFrameworkSettings(final FrameworkSettings frameworkSettings)
	{
		this.frameworkSettings = frameworkSettings;
		return this;
	}

	/**
	 * @return Application's page related settings
	 */
	public final PageSettings getPageSettings()
	{
		checkSettingsAvailable();
		if (pageSettings == null)
		{
			pageSettings = new PageSettings();
		}
		return pageSettings;
	}

	/**
	 * 
	 * @param pageSettings
	 */
	public final Application setPageSettings(final PageSettings pageSettings)
	{
		this.pageSettings = pageSettings;
		return this;
	}

	/**
	 * @return Application's request cycle related settings
	 */
	public RequestCycleSettings getRequestCycleSettings()
	{
		checkSettingsAvailable();
		if (requestCycleSettings == null)
		{
			requestCycleSettings = new RequestCycleSettings();
		}
		return requestCycleSettings;
	}

	/**
	 * 
	 * @param requestCycleSettings
	 */
	public final Application setRequestCycleSettings(final RequestCycleSettings requestCycleSettings)
	{
		this.requestCycleSettings = requestCycleSettings;
		return this;
	}

	/**
	 * @return Application's markup related settings
	 */
	public MarkupSettings getMarkupSettings()
	{
		checkSettingsAvailable();
		if (markupSettings == null)
		{
			markupSettings = new MarkupSettings();
		}
		return markupSettings;
	}

	/**
	 * 
	 * @param markupSettings
	 */
	public final Application setMarkupSettings(final MarkupSettings markupSettings)
	{
		this.markupSettings = markupSettings;
		return this;
	}

	/**
	 * @return Application's request logger related settings
	 */
	public final RequestLoggerSettings getRequestLoggerSettings()
	{
		checkSettingsAvailable();
		if (requestLoggerSettings == null)
		{
			requestLoggerSettings = new RequestLoggerSettings();
		}
		return requestLoggerSettings;
	}

	/**
	 * 
	 * @param requestLoggerSettings
	 */
	public final Application setRequestLoggerSettings(final RequestLoggerSettings requestLoggerSettings)
	{
		this.requestLoggerSettings = requestLoggerSettings;
		return this;
	}

	/**
	 * @return Application's resources related settings
	 */
	public final ResourceSettings getResourceSettings()
	{
		checkSettingsAvailable();
		if (resourceSettings == null)
		{
			resourceSettings = new ResourceSettings(this);
		}
		return resourceSettings;
	}

	/**
	 * 
	 * @param resourceSettings
	 */
	public final Application setResourceSettings(final ResourceSettings resourceSettings)
	{
		this.resourceSettings = resourceSettings;
		return this;
	}

	/**
	 * @return Application's security related settings
	 */
	public final SecuritySettings getSecuritySettings()
	{
		checkSettingsAvailable();
		if (securitySettings == null)
		{
			securitySettings = new SecuritySettings();
		}
		return securitySettings;
	}

	/**
	 * 
	 * @param securitySettings
	 */
	public final Application setSecuritySettings(final SecuritySettings securitySettings)
	{
		this.securitySettings = securitySettings;
		return this;
	}

	/**
	 * @return Application's stores related settings
	 */
	public final StoreSettings getStoreSettings()
	{
		checkSettingsAvailable();
		if (storeSettings == null)
		{
			storeSettings = new StoreSettings(this);
		}
		return storeSettings;
	}

	/**
	 * 
	 * @param storeSettings
	 */
	public final Application setStoreSettings(final StoreSettings storeSettings)
	{
		this.storeSettings = storeSettings;
		return this;
	}

	/**
	 *
	 */
	protected void checkSettingsAvailable()
	{
		if (!settingsAccessible)
		{
			throw new WicketRuntimeException(
				"Use Application.init() method for configuring your application object");
		}
	}

	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//
	// Page Manager
	//
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	private volatile IPageManager pageManager;
	private IPageManagerProvider pageManagerProvider;

	/**
	 * 
	 * @return PageManagerProvider
	 */
	public final IPageManagerProvider getPageManagerProvider()
	{
		return pageManagerProvider;
	}

	/**
	 * Set the provider of an {@link IPageManager}.
	 * 
	 * @param provider
	 * 
	 * @see DefaultPageManagerProvider
	 */
	public final Application setPageManagerProvider(final IPageManagerProvider provider)
	{
		pageManagerProvider = provider;
		return this;
	}

	/**
	 * Returns an unsynchronized version of page manager
	 * 
	 * @return the page manager
	 */
	final IPageManager internalGetPageManager()
	{
		if (pageManager == null)
		{
			synchronized (this)
			{
				if (pageManager == null)
				{
					pageManager = pageManagerProvider.get();
				}
			}
		}
		return pageManager;
	}

	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//
	// Page Rendering
	//
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * 
	 * @return PageRendererProvider
	 */
	public final IPageRendererProvider getPageRendererProvider()
	{
		return pageRendererProvider;
	}

	/**
	 * 
	 * @param pageRendererProvider
	 */
	public final Application setPageRendererProvider(final IPageRendererProvider pageRendererProvider)
	{
		Args.notNull(pageRendererProvider, "pageRendererProvider");
		this.pageRendererProvider = pageRendererProvider;
		return this;
	}


	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//
	// Request Handler encoding
	//
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	private ResourceReferenceRegistry resourceReferenceRegistry;

	private SharedResources sharedResources;

	private ResourceBundles resourceBundles;

	private IPageFactory pageFactory;

	private IMapperContext encoderContext;

	/**
	 * Override to create custom {@link ResourceReferenceRegistry}.
	 * 
	 * @return new {@link ResourceReferenceRegistry} instance.
	 */
	protected ResourceReferenceRegistry newResourceReferenceRegistry()
	{
		return new ResourceReferenceRegistry();
	}

	/**
	 * Returns {@link ResourceReferenceRegistry} for this application.
	 * 
	 * @return ResourceReferenceRegistry
	 */
	public final ResourceReferenceRegistry getResourceReferenceRegistry()
	{
		return resourceReferenceRegistry;
	}

	/**
	 * 
	 * @param registry
	 * @return SharedResources
	 */
	protected SharedResources newSharedResources(final ResourceReferenceRegistry registry)
	{
		return new SharedResources(registry);
	}

	/**
	 * 
	 * @return SharedResources
	 */
	public SharedResources getSharedResources()
	{
		return sharedResources;
	}

	protected ResourceBundles newResourceBundles(final ResourceReferenceRegistry registry)
	{
		return new ResourceBundles(registry);
	}

	/**
	 * @return The registry for resource bundles
	 */
	public ResourceBundles getResourceBundles()
	{
		return resourceBundles;
	}

	/**
	 * Override to create custom {@link IPageFactory}
	 * 
	 * @return new {@link IPageFactory} instance.
	 */
	protected IPageFactory newPageFactory()
	{
		return new DefaultPageFactory();
	}

	/**
	 * Returns {@link IPageFactory} for this application.
	 * 
	 * @return page factory
	 */
	public final IPageFactory getPageFactory()
	{
		return pageFactory;
	}

	/**
	 * 
	 * @return mapper context
	 */
	public final IMapperContext getMapperContext()
	{
		if (encoderContext == null)
		{
			encoderContext = newMapperContext();
		}
		return encoderContext;
	}

	/**
	 * Factory method for {@link IMapperContext} implementations. {@link DefaultMapperContext} may
	 * be a good starting point for custom implementations.
	 * 
	 * @return new instance of mapper context to be used in the application
	 */
	protected IMapperContext newMapperContext()
	{
		return new DefaultMapperContext(this);
	}

	/**
	 * 
	 * @param requestCycle
	 * @return Session
	 */
	public Session fetchCreateAndSetSession(final RequestCycle requestCycle)
	{
		Args.notNull(requestCycle, "requestCycle");

		Session session = getSessionStore().lookup(requestCycle.getRequest());
		if (session == null)
		{
			session = newSession(requestCycle.getRequest(), requestCycle.getResponse());
			ThreadContext.setSession(session);
			internalGetPageManager().clear();
			sessionListeners.onCreated(session);
		}
		else
		{
			ThreadContext.setSession(session);
		}
		return session;
	}

	/**
	 * 
	 * @return RequestCycleProvider
	 */
	public final IRequestCycleProvider getRequestCycleProvider()
	{
		return requestCycleProvider;
	}

	/**
	 * 
	 * @param requestCycleProvider
	 */
	public final Application setRequestCycleProvider(final IRequestCycleProvider requestCycleProvider)
	{
		this.requestCycleProvider = requestCycleProvider;
		return this;
	}

	/**
	 * 
	 * @param request
	 * @param response
	 * @return request cycle
	 */
	public final RequestCycle createRequestCycle(final Request request, final Response response)
	{
		RequestCycleContext context = new RequestCycleContext(request, response,
			getRootRequestMapper(), getExceptionMapperProvider().get());

		RequestCycle requestCycle = getRequestCycleProvider().apply(context);
		requestCycle.getListeners().add(requestCycleListeners);
		requestCycle.getListeners().add(new IRequestCycleListener()
		{
			@Override
			public void onEndRequest(RequestCycle cycle)
			{
				internalGetPageManager().end();
			}
			
			@Override
			public void onDetach(final RequestCycle requestCycle)
			{
				internalGetPageManager().detach();

				IRequestLogger requestLogger = getRequestLogger();
				if (requestLogger != null)
				{
					requestLogger.requestTime((System.currentTimeMillis() - requestCycle.getStartTime()));
				}
			}
		});
		return requestCycle;
	}

	/**
	 * Returns the {@link HeaderResponseDecoratorCollection} used by this application. On this
	 * collection you can add additional decorators, which will be nested in the order added.
	 * 
	 * @return The {@link HeaderResponseDecoratorCollection} used by this application.
	 */
	public HeaderResponseDecoratorCollection getHeaderResponseDecorators()
	{
		return headerResponseDecorators;
	}
	
	/**
	 * INTERNAL METHOD - You shouldn't need to call this. This is called every time Wicket creates
	 * an IHeaderResponse. It gives you the ability to incrementally add features to an
	 * IHeaderResponse implementation by wrapping it in another implementation.
	 * 
	 * To decorate an IHeaderResponse in your application, set the {@link IHeaderResponseDecorator}
	 * on the application.
	 * 
	 * @see IHeaderResponseDecorator
	 * @param response
	 *            the response Wicket created
	 * @return the response Wicket should use in IHeaderContributor traversal
	 */
	public final IHeaderResponse decorateHeaderResponse(final IHeaderResponse response)
	{
		return headerResponseDecorators.decorate(response);
	}

	/**
	 * 
	 * @return true, of app is in Development mode
	 */
	public final boolean usesDevelopmentConfig()
	{
		return RuntimeConfigurationType.DEVELOPMENT.equals(getConfigurationType());
	}

	/**
	 * 
	 * @return true, of app is in Deployment mode
	 */
	public final boolean usesDeploymentConfig()
	{
		return RuntimeConfigurationType.DEPLOYMENT.equals(getConfigurationType());
	}
}
