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

import org.apache.wicket.Component;
import org.apache.wicket.Page;
import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
import org.apache.wicket.markup.MarkupType;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.html.internal.HtmlHeaderContainer;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.parser.filter.HtmlHeaderSectionHandler;
import org.apache.wicket.markup.renderStrategy.AbstractHeaderRenderStrategy;
import org.apache.wicket.model.IModel;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.response.StringResponse;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.wicket.request.IRequestHandlerDelegate.unwrap;


/**
 * Base class for HTML pages. This subclass of Page simply returns HTML when asked for its markup
 * type. It also has a method which subclasses can use to retrieve a bookmarkable link to the
 * application's home page.
 * <p>
 * WebPages can be constructed with any constructor when they are being used in a Wicket session,
 * but if you wish to link to a Page using a URL that is "bookmarkable" (which implies that the URL
 * will not have any session information encoded in it, and that you can call this page directly
 * without having a session first directly from your browser), you need to implement your Page with
 * a no-arg constructor or with a constructor that accepts a PageParameters argument (which wraps
 * any query string parameters for a request). In case the page has both constructors, the
 * constructor with PageParameters will be used.
 * 
 * @author Jonathan Locke
 * @author Eelco Hillenius
 * @author Juergen Donnerstag
 * @author Gwyn Evans
 */
public class WebPage extends Page
{
	private static final Logger log = LoggerFactory.getLogger(WebPage.class);

	private static final long serialVersionUID = 1L;

	/**
	 * Constructor.
	 *
	 * Having this constructor public means that your page is 'bookmarkable' and hence
	 * can be called/ created from anywhere.
	 */
	protected WebPage()
	{
	}

	protected WebPage(final IModel<?> model)
	{
		super(model);
	}

	/**
	 * Constructor which receives wrapped query string parameters for a request. Having this
	 * constructor public means that your page is 'bookmarkable' and hence can be called/ created
	 * from anywhere. For bookmarkable pages (as opposed to when you construct page instances
	 * yourself, this constructor will be used in preference to a no-arg constructor, if both exist.
	 * Note that nothing is done with the page parameters argument. This constructor is provided so
	 * that tools such as IDEs will include it their list of suggested constructors for derived
	 * classes.
	 * 
	 * Please call this constructor if you want to remember the pageparameters
	 * {@link #getPageParameters()}. So that they are reused for stateless links.
	 * 
	 * @param parameters
	 *            Wrapped query string parameters.
	 */
	protected WebPage(final PageParameters parameters)
	{
		super(parameters);
	}

	/**
	 * Gets the markup type for a WebPage, which is "html" by default. Support for pages in another
	 * markup language, such as VXML, would require the creation of a different Page subclass in an
	 * appropriate package under org.apache.wicket.markup. To support VXML (voice markup), one might
	 * create the package org.apache.wicket.markup.vxml and a subclass of Page called VoicePage.
	 * 
	 * @return Markup type for HTML
	 */
	@Override
	public MarkupType getMarkupType()
	{
		return MarkupType.HTML_MARKUP_TYPE;
	}

	@Override
	protected void onRender()
	{
		// Configure the response such as headers etc.
		configureResponse((WebResponse)RequestCycle.get().getResponse());

		// The rules if and when to insert an xml decl in the response are a bit tricky. Allow the
		// user to replace the default per page and per application.
		renderXmlDecl();

		super.onRender();
	}

	/**
	 * The rules if and when to insert an xml decl in the response are a bit tricky. Allow the user
	 * to replace the default per page and per application.
	 */
	protected void renderXmlDecl()
	{
		WebApplication.get().renderXmlDecl(this, false);
	}

	/**
	 * Set-up response with appropriate content type, locale and encoding. The locale is set equal
	 * to the session's locale. The content type header contains information about the markup type
	 * (@see #getMarkupType()) and the encoding. The response (and request) encoding is determined
	 * by an application setting (@see ApplicationSettings#getResponseRequestEncoding()). If null,
	 * no xml decl will be written.
	 * 
	 * @param response
	 *            The WebResponse object
	 */
	protected void configureResponse(final WebResponse response)
	{
		var cspSettings = WebApplication.get().getCspSettings();
		var handler = RequestCycle.get().getActiveRequestHandler();
		if (cspSettings.isEnabled() && unwrap(handler) instanceof RenderPageRequestHandler)
		{
			cspSettings.getHeaderWriter().write(response, handler);
		}

		// Users may subclass setHeader() to set there own headers
		setHeaders(response);

		// The response encoding is an application setting
		final String encoding = getApplication().getRequestCycleSettings()
			.getResponseRequestEncoding();
		final boolean validEncoding = (Strings.isEmpty(encoding) == false);
		final String contentType;
		if (validEncoding)
		{
			contentType = getMarkupType().getMimeType() + "; charset=" + encoding;
		}
		else
		{
			contentType = getMarkupType().getMimeType();
		}

		response.setContentType(contentType);
	}

	/**
	 * Subclasses can override this to set there headers when the Page is being served. By default
	 * these headers are set:
	 * 
	 * <pre>
	 * response.setHeader(&quot;Date&quot;, &quot;[now]&quot;);
	 * response.setHeader(&quot;Expires&quot;, &quot;[0]&quot;);
	 * response.setHeader(&quot;Pragma&quot;, &quot;no-cache&quot;);
	 * response.setHeader(&quot;Cache-Control&quot;, &quot;no-cache&quot;);
	 * </pre>
	 * 
	 * So if a Page wants to control this or doesn't want to set this info it should override this
	 * method and don't call super.
	 * 
	 * @param response
	 *            The WebResponse where set(Date)Header can be called on.
	 */
	protected void setHeaders(WebResponse response)
	{
		response.disableCaching();
	}

	/**
	 * 
	 * @see org.apache.wicket.Component#onAfterRender()
	 */
	@Override
	protected void onAfterRender()
	{
		// only in development mode validate the headers
		if (getApplication().usesDevelopmentConfig())
		{
			// check headers only when page was completely rendered
			if (wasRendered(this))
			{
				validateHeaders();
			}
		}

		super.onAfterRender();
	}

	/**
	 * Validate that each component which wanted to contribute to the header section actually was
	 * able to do so.
	 */
	private void validateHeaders()
	{
		// search for HtmlHeaderContainer in the first level of children or deeper
		// if there are transparent resolvers used
		HtmlHeaderContainer header = visitChildren(new IVisitor<Component, HtmlHeaderContainer>()
		{
			@Override
			public void component(final Component component, final IVisit<HtmlHeaderContainer> visit)
			{
				if (component instanceof HtmlHeaderContainer)
				{
					visit.stop((HtmlHeaderContainer)component);
				}
				else if (component instanceof TransparentWebMarkupContainer == false)
				{
					visit.dontGoDeeper();
				}
			}
		});

		if (header == null)
		{
			// the markup must at least contain a <body> tag for wicket to automatically
			// create a HtmlHeaderContainer. Log an error if no header container
			// was created but any of the components or behaviors want to contribute
			// something to the header.
			header = new HtmlHeaderContainer(HtmlHeaderSectionHandler.HEADER_ID);
			add(header);

			RequestCycle requestCycle = getRequestCycle();
			Response orgResponse = requestCycle.getResponse();
			try
			{
				StringResponse tempResponse = new StringResponse();
				requestCycle.setResponse(tempResponse);

				// Render all header sections of all components on the page
				AbstractHeaderRenderStrategy.get().renderHeader(header, null, getPage());

				IHeaderResponse headerResponse = header.getHeaderResponse();
				headerResponse.close();
				CharSequence collectedHeaderOutput = tempResponse.getBuffer();
				if (collectedHeaderOutput.length() > 0)
				{
					reportMissingHead(collectedHeaderOutput);
				}
			}
			catch (Exception e)
			{
				// just swallow this exception, there isn't much we can do about.
				log.error("header/body check throws exception", e);
			}
			finally
			{
				this.remove(header);
				requestCycle.setResponse(orgResponse);
			}
		}
	}

	/**
	 * Reports an error that there is no &lt;head&gt; and/or &lt;body&gt; in the page and
	 * there is no where to write the header response.
	 * 
	 * @param collectedHeaderOutput
	 *          The collected response that should have been written to the &lt;head&gt;
	 * @see #validateHeaders()
	 */
	protected void reportMissingHead(final CharSequence collectedHeaderOutput)
	{
		log.error("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^");
		log.error("You probably forgot to add a <body> or <head> tag to your markup since no Header Container was \n" +
				"found but components were found which want to write to the <head> section.\n" +
				collectedHeaderOutput);
		log.error("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^");
	}

	/**
	 * Creates and returns a bookmarkable link to this application's home page.
	 * 
	 * @param id
	 *            Name of link
	 * @return Link to home page for this application
	 */
	protected final BookmarkablePageLink<Void> homePageLink(final String id)
	{
		return new BookmarkablePageLink<>(id, getApplication().getHomePage());
	}

	/**
	 * Prevents page from get dirty inside an AJAX request.
	 */
	@Override
	public final void dirty(boolean isInitialization)
	{
		Request request = getRequest();
		if (isInitialization == false && request instanceof WebRequest &&
			((WebRequest)request).isAjax())
		{
			return;
		}
		super.dirty(isInitialization);
	}
}
