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

import java.util.ArrayList;
import java.util.List;

import org.apache.wicket.RequestListenerInterface;
import org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.component.IRequestablePage;
import org.apache.wicket.request.mapper.info.ComponentInfo;
import org.apache.wicket.request.mapper.info.PageComponentInfo;
import org.apache.wicket.request.mapper.info.PageInfo;
import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
import org.apache.wicket.util.ClassProvider;
import org.apache.wicket.util.IProvider;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.reference.ClassReference;
import org.apache.wicket.util.string.Strings;

/**
 * Encoder for mounted URL. The mount path can contain parameter placeholders, i.e.
 * <code>/mount/${foo}/path</code>. In that case the appropriate segment from the URL will be
 * accessible as named parameter "foo" in the {@link PageParameters}. Similarly when the URL is
 * constructed, the second segment will contain the value of the "foo" named page parameter.
 * Optional parameters are denoted by using a # instead of $: <code>/mount/#{foo}/path/${bar}</code>
 * has an optional {@code foo} parameter, a fixed {@code /path/} part and a required {@code bar}
 * parameter. When in doubt, parameters are matched from left to right, where required parameters
 * are matched before optional parameters, and optional parameters eager (from left to right).
 * <p>
 * Decodes and encodes the following URLs:
 * 
 * <pre>
 *  Page Class - Render (BookmarkablePageRequestHandler for mounted pages)
 *  /mount/point
 *  (these will redirect to hybrid alternative if page is not stateless)
 * 
 *  IPage Instance - Render Hybrid (RenderPageRequestHandler for mounted pages)
 *  /mount/point?2
 * 
 *  IPage Instance - Bookmarkable Listener (BookmarkableListenerInterfaceRequestHandler for mounted pages)
 *  /mount/point?2-click-foo-bar-baz
 *  /mount/point?2-5.click.1-foo-bar-baz (1 is behavior index, 5 is render count)
 *  (these will redirect to hybrid if page is not stateless)
 * </pre>
 * 
 * @author Matej Knopp
 */
public class MountedMapper extends AbstractBookmarkableMapper
{
	private final IPageParametersEncoder pageParametersEncoder;

	private static class MountPathSegment
	{
		private int segmentIndex;
		private String fixedPart;
		private int minParameters;
		private int optionalParameters;

		public MountPathSegment(int segmentIndex)
		{
			this.segmentIndex = segmentIndex;
		}

		public void setFixedPart(String fixedPart)
		{
			this.fixedPart = fixedPart;
		}

		public void addRequiredParameter()
		{
			minParameters++;
		}

		public void addOptionalParameter()
		{
			optionalParameters++;
		}

		public int getSegmentIndex()
		{
			return segmentIndex;
		}

		public String getFixedPart()
		{
			return fixedPart;
		}

		public int getMinParameters()
		{
			return minParameters;
		}

		public int getOptionalParameters()
		{
			return optionalParameters;
		}

		public int getMaxParameters()
		{
			return getOptionalParameters() + getMinParameters();
		}

		public int getFixedPartSize()
		{
			return getFixedPart() == null ? 0 : 1;
		}

		@Override
		public String toString()
		{
			return "(" + getSegmentIndex() + ") " + getMinParameters() + "-" + getMaxParameters() +
				" " + (getFixedPart() == null ? "(end)" : getFixedPart());
		}
	}

	private final List<MountPathSegment> pathSegments;
	private final String[] mountSegments;

	/** bookmarkable page class. */
	private final IProvider<Class<? extends IRequestablePage>> pageClassProvider;

	/**
	 * Construct.
	 * 
	 * @param mountPath
	 * @param pageClass
	 */
	public MountedMapper(String mountPath, Class<? extends IRequestablePage> pageClass)
	{
		this(mountPath, pageClass, new PageParametersEncoder());
	}

	/**
	 * Construct.
	 * 
	 * @param mountPath
	 * @param pageClassProvider
	 */
	@Deprecated
	public MountedMapper(String mountPath,
		ClassProvider<? extends IRequestablePage> pageClassProvider)
	{
		this(mountPath, new ClassReference(pageClassProvider.get()), new PageParametersEncoder());
	}

	/**
	 * Construct.
	 * 
	 * @param mountPath
	 * @param pageClassProvider
	 */
	public MountedMapper(String mountPath,
		IProvider<Class<? extends IRequestablePage>> pageClassProvider)
	{
		this(mountPath, pageClassProvider, new PageParametersEncoder());
	}

	/**
	 * Construct.
	 * 
	 * @param mountPath
	 * @param pageClass
	 * @param pageParametersEncoder
	 */
	public MountedMapper(String mountPath, Class<? extends IRequestablePage> pageClass,
		IPageParametersEncoder pageParametersEncoder)
	{
		this(mountPath, new ClassReference(pageClass), pageParametersEncoder);
	}

	/**
	 * Construct.
	 * 
	 * @param mountPath
	 * @param pageClassProvider
	 * @param pageParametersEncoder
	 */
	@Deprecated
	public MountedMapper(String mountPath,
		ClassProvider<? extends IRequestablePage> pageClassProvider,
		IPageParametersEncoder pageParametersEncoder)
	{
		this(mountPath, new ClassReference(pageClassProvider.get()), pageParametersEncoder);
	}

	/**
	 * Construct.
	 * 
	 * @param mountPath
	 * @param pageClassProvider
	 * @param pageParametersEncoder
	 */
	public MountedMapper(String mountPath,
		IProvider<Class<? extends IRequestablePage>> pageClassProvider,
		IPageParametersEncoder pageParametersEncoder)
	{
		Args.notEmpty(mountPath, "mountPath");
		Args.notNull(pageClassProvider, "pageClassProvider");
		Args.notNull(pageParametersEncoder, "pageParametersEncoder");

		this.pageParametersEncoder = pageParametersEncoder;
		this.pageClassProvider = pageClassProvider;
		mountSegments = getMountSegments(mountPath);
		pathSegments = getPathSegments(mountSegments);
	}

	private List<MountPathSegment> getPathSegments(String[] segments)
	{
		List<MountPathSegment> ret = new ArrayList<MountPathSegment>();
		int segmentIndex = 0;
		MountPathSegment curPathSegment = new MountPathSegment(segmentIndex);
		ret.add(curPathSegment);
		for (String curSegment : segments)
		{
			if (isFixedSegment(curSegment))
			{
				curPathSegment.setFixedPart(curSegment);
				curPathSegment = new MountPathSegment(segmentIndex + 1);
				ret.add(curPathSegment);
			}
			else if (getPlaceholder(curSegment) != null)
			{
				curPathSegment.addRequiredParameter();
			}
			else
			{
				curPathSegment.addOptionalParameter();
			}
			segmentIndex++;
		}
		return ret;
	}

	private boolean isFixedSegment(String segment)
	{
		return getOptionalPlaceholder(segment) == null && getPlaceholder(segment) == null;
	}

	/**
	 * @see AbstractBookmarkableMapper#parseRequest(org.apache.wicket.request.Request)
	 */
	@Override
	protected UrlInfo parseRequest(Request request)
	{
		Url url = request.getUrl();

		// when redirect to buffer/render is active and redirectFromHomePage returns true
		// check mounted class against the home page class. if it matches let wicket redirect
		// to the mounted URL
		if (redirectFromHomePage() && checkHomePage(url))
		{
			return new UrlInfo(null, getContext().getHomePageClass(), newPageParameters());
		}
		// check if the URL starts with the proper segments
		else if (urlStartsWith(url, mountSegments))
		{
			// try to extract page and component information from URL
			PageComponentInfo info = getPageComponentInfo(url);
			Class<? extends IRequestablePage> pageClass = getPageClass();
			PageParameters pageParameters = extractPageParameters(request, url);

			return new UrlInfo(info, pageClass, pageParameters);
		}
		else
		{
			return null;
		}
	}

	/*
	 * extract the PageParameters from URL if there are any
	 */
	private PageParameters extractPageParameters(Request request, Url url)
	{
		int[] matchedParameters = getMatchedSegmentSizes(url);
		int total = 0;
		for (int curMatchSize : matchedParameters)
			total += curMatchSize;
		PageParameters pageParameters = extractPageParameters(request, total, pageParametersEncoder);

		int skippedParameters = 0;
		for (int pathSegmentIndex = 0; pathSegmentIndex < pathSegments.size(); pathSegmentIndex++)
		{
			MountPathSegment curPathSegment = pathSegments.get(pathSegmentIndex);
			int matchSize = matchedParameters[pathSegmentIndex] - curPathSegment.getFixedPartSize();
			int optionalParameterMatch = matchSize - curPathSegment.getMinParameters();
			for (int matchSegment = 0; matchSegment < matchSize; matchSegment++)
			{
				if (pageParameters == null)
				{
					pageParameters = new PageParameters();
				}

				int curSegmentIndex = matchSegment + curPathSegment.getSegmentIndex();
				String curSegment = mountSegments[curSegmentIndex];
				String placeholder = getPlaceholder(curSegment);
				String optionalPlaceholder = getOptionalPlaceholder(curSegment);
				// extract the parameter from URL
				if (placeholder != null)
				{
					pageParameters.add(placeholder,
						url.getSegments().get(curSegmentIndex - skippedParameters));
				}
				else if (optionalPlaceholder != null && optionalParameterMatch > 0)
				{
					pageParameters.add(optionalPlaceholder,
						url.getSegments().get(curSegmentIndex - skippedParameters));
					optionalParameterMatch--;
				}
			}
			skippedParameters += curPathSegment.getMaxParameters() - matchSize;
		}
		return pageParameters;
	}

	@Override
	protected boolean urlStartsWith(Url url, String... segments)
	{
		if (url == null)
		{
			return false;
		}
		else
		{
			return getMatchedSegmentSizes(url) != null;
		}
	}

	protected int[] getMatchedSegmentSizes(Url url)
	{
		int[] ret = new int[pathSegments.size()];
		int segmentIndex = 0;
		int pathSegmentIndex = 0;
		for (MountPathSegment curPathSegment : pathSegments.subList(0, pathSegments.size() - 1))
		{
			boolean foundFixedPart = false;
			segmentIndex += curPathSegment.getMinParameters();
			int max = Math.min(curPathSegment.getOptionalParameters() + 1,
				url.getSegments().size() - segmentIndex);

			for (int count = max - 1; count >= 0; count--)
			{
				if (url.getSegments()
					.get(segmentIndex + count)
					.equals(curPathSegment.getFixedPart()))
				{
					foundFixedPart = true;
					segmentIndex += count + 1;
					ret[pathSegmentIndex] = count + curPathSegment.getMinParameters() + 1;
					break;
				}
			}
			if (!foundFixedPart)
				return null;
			pathSegmentIndex++;
		}
		MountPathSegment lastSegment = pathSegments.get(pathSegments.size() - 1);
		segmentIndex += lastSegment.getMinParameters();
		if (segmentIndex > url.getSegments().size())
			return null;
		ret[pathSegmentIndex] = Math.min(lastSegment.getMaxParameters(), url.getSegments().size() -
			segmentIndex + lastSegment.getMinParameters());
		return ret;
	}

	protected PageParameters newPageParameters()
	{
		return new PageParameters();
	}

	@Override
	public Url mapHandler(IRequestHandler requestHandler)
	{
		Url url = super.mapHandler(requestHandler);

		if (url == null && requestHandler instanceof ListenerInterfaceRequestHandler &&
			getRecreateMountedPagesAfterExpiry())
		{
			ListenerInterfaceRequestHandler handler = (ListenerInterfaceRequestHandler)requestHandler;
			IRequestablePage page = handler.getPage();
			if (checkPageInstance(page))
			{
				String componentPath = handler.getComponentPath();
				RequestListenerInterface listenerInterface = handler.getListenerInterface();

				Integer renderCount = null;
				if (listenerInterface.isIncludeRenderCount())
				{
					renderCount = page.getRenderCount();
				}

				PageInfo pageInfo = getPageInfo(handler);
				ComponentInfo componentInfo = new ComponentInfo(renderCount,
					requestListenerInterfaceToString(listenerInterface), componentPath,
					handler.getBehaviorIndex());
				PageComponentInfo pageComponentInfo = new PageComponentInfo(pageInfo, componentInfo);
				PageParameters parameters = new PageParameters(page.getPageParameters());
				UrlInfo urlInfo = new UrlInfo(pageComponentInfo, page.getClass(),
					parameters.mergeWith(handler.getPageParameters()));
				url = buildUrl(urlInfo);
			}
		}

		return url;
	}

	/**
	 * @see AbstractBookmarkableMapper#buildUrl(AbstractBookmarkableMapper.UrlInfo)
	 */
	@Override
	protected Url buildUrl(UrlInfo info)
	{
		Url url = new Url();
		for (String s : mountSegments)
		{
			url.getSegments().add(s);
		}
		encodePageComponentInfo(url, info.getPageComponentInfo());

		PageParameters copy = new PageParameters(info.getPageParameters());

		int dropped = 0;
		for (int i = 0; i < mountSegments.length; ++i)
		{
			String placeholder = getPlaceholder(mountSegments[i]);
			String optionalPlaceholder = getOptionalPlaceholder(mountSegments[i]);
			if (placeholder != null)
			{
				if (!copy.getNamedKeys().contains(placeholder))
				{
					// no value for placeholder - cannot mount
					return null;
				}
				url.getSegments().set(i - dropped, copy.get(placeholder).toString(""));
				copy.remove(placeholder);
			}
			else if (optionalPlaceholder != null)
			{
				if (copy.getNamedKeys().contains(optionalPlaceholder))
				{
					url.getSegments().set(i - dropped, copy.get(optionalPlaceholder).toString(""));
					copy.remove(optionalPlaceholder);
				}
				else
				{
					url.getSegments().remove(i - dropped);
					dropped++;
				}
			}
		}

		return encodePageParameters(url, copy, pageParametersEncoder);
	}

	/**
	 * Check if the URL is for home page and the home page class match mounted class. If so,
	 * redirect to mounted URL.
	 * 
	 * @param url
	 * @return request handler or <code>null</code>
	 */
	private boolean checkHomePage(Url url)
	{
		if (url.getSegments().isEmpty() && url.getQueryParameters().isEmpty())
		{
			// this is home page
			if (getPageClass().equals(getContext().getHomePageClass()) && redirectFromHomePage())
			{
				return true;
			}
		}
		return false;
	}

	/**
	 * If this method returns <code>true</code> and application home page class is same as the class
	 * mounted with this encoder, request to home page will result in a redirect to the mounted
	 * path.
	 * 
	 * @return whether this encode should respond to home page request when home page class is same
	 *         as mounted class.
	 */
	protected boolean redirectFromHomePage()
	{
		return true;
	}

	/**
	 * @see AbstractBookmarkableMapper#pageMustHaveBeenCreatedBookmarkable()
	 */
	@Override
	protected boolean pageMustHaveBeenCreatedBookmarkable()
	{
		return false;
	}

	/**
	 * @see AbstractBookmarkableMapper#getCompatibilityScore(org.apache.wicket.request.Request)
	 */
	@Override
	public int getCompatibilityScore(Request request)
	{
		if (urlStartsWith(request.getUrl(), mountSegments))
		{
			/* see WICKET-5056 - alter score with pathSegment type */
			int countOptional = 0;
			int fixedSegments = 0;
			for (MountPathSegment pathSegment : pathSegments)
			{
				fixedSegments += pathSegment.getFixedPartSize();
				countOptional += pathSegment.getOptionalParameters();
			}
			return mountSegments.length - countOptional + fixedSegments;
		}
		else
		{
			return 0;
		}
	}

	/**
	 * @see AbstractBookmarkableMapper#checkPageClass(java.lang.Class)
	 */
	@Override
	protected boolean checkPageClass(Class<? extends IRequestablePage> pageClass)
	{
		return pageClass.equals(this.getPageClass());
	}

	private Class<? extends IRequestablePage> getPageClass()
	{
		return pageClassProvider.get();
	}

	@Override
	public String toString()
	{
		return "MountedMapper [mountSegments=" + Strings.join("/", mountSegments) + "]";
	}
}
