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

import jakarta.servlet.http.HttpServletResponse;

import org.apache.wicket.Application;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.IRequestMapper;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException;
import org.apache.wicket.request.mapper.parameter.INamedParameters;
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.request.resource.IResource;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.request.resource.caching.IResourceCachingStrategy;
import org.apache.wicket.request.resource.caching.IStaticCacheableResource;
import org.apache.wicket.request.resource.caching.ResourceUrl;
import org.apache.wicket.resource.ResourceUtil;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;

/**
 * A {@link IRequestMapper} to mount resources to a custom mount path
 * <ul>
 * <li>maps indexed parameters to path segments</li>
 * <li>maps named parameters to query string arguments or placeholder path segments</li>
 * </ul>
 *
 * <strong>Sample structure of url</strong>
 * <p/>
 * <pre>
 *    /myresources/${category}/images/[indexed-param-0]/[indexed-param-1]?[named-param-1=value]&amp;[named-param-2=value2]
 * </pre>
 *
 * <strong>Sample usage</strong>
 *
 * <p/>
 * in your wicket application's init() method use a statement like this
 * <p/>
 *
 * <pre>
 * mountResource(&quot;/images&quot;, new ImagesResourceReference()));
 * </pre>
 *
 * Note: Mounted this way the resource reference has application scope, i.e. it is shared between
 * all users of the application. It is recommended to not keep any state in it.
 *
 * @see org.apache.wicket.protocol.http.WebApplication#mountResource(String,
 *      org.apache.wicket.request.resource.ResourceReference)
 *
 * @author Peter Ertl
 */
public class ResourceMapper extends AbstractBookmarkableMapper
{
	// encode page parameters into url + decode page parameters from url
	private final IPageParametersEncoder parametersEncoder;

	// mount path (= segments) the resource is bound to
	private final String[] mountSegments;

	// resource that the mapper links to
	private final ResourceReference resourceReference;

	/**
	 * create a resource mapper for a resource
	 *
	 * @param path
	 *            mount path for the resource
	 * @param resourceReference
	 *            resource reference that should be linked to the mount path
	 *
	 * @see #ResourceMapper(String, org.apache.wicket.request.resource.ResourceReference,
	 *      org.apache.wicket.request.mapper.parameter.IPageParametersEncoder)
	 */
	public ResourceMapper(String path, ResourceReference resourceReference)
	{
		this(path, resourceReference, new PageParametersEncoder());
	}

	/**
	 * create a resource mapper for a resource
	 *
	 * @param path
	 *            mount path for the resource
	 * @param resourceReference
	 *            resource reference that should be linked to the mount path
	 * @param encoder
	 *            encoder for url parameters
	 */
	public ResourceMapper(String path, ResourceReference resourceReference,
		IPageParametersEncoder encoder)
	{
		super(path, encoder);
		Args.notNull(resourceReference, "resourceReference");

		this.resourceReference = resourceReference;
		mountSegments = getMountSegments(path);
		parametersEncoder = encoder;
	}

	@Override
	public IRequestHandler mapRequest(final Request request)
	{
		final Url url = new Url(request.getUrl());

		// now extract the page parameters from the request url
		PageParameters parameters = extractPageParameters(request, mountSegments.length,
			parametersEncoder);
		if (parameters != null)
		{
			parameters.setLocale(resolveLocale());
		}

		// remove caching information from current request
		removeCachingDecoration(url, parameters);

		// check if url matches mount path
		if (urlStartsWith(url, mountSegments) == false)
		{
			return null;
		}

		// check if there are placeholders in mount segments
		for (int index = 0; index < mountSegments.length; ++index)
		{
			String placeholder = getPlaceholder(mountSegments[index]);

			if (placeholder != null)
			{
				// extract the parameter from URL
				if (parameters == null)
				{
					parameters = newPageParameters();
				}
				parameters.add(placeholder, url.getSegments().get(index), INamedParameters.Type.PATH);
			}
		}
		return new ResourceReferenceRequestHandler(resourceReference, parameters);
	}

	@Override
	protected final UrlInfo parseRequest(final Request request) {
		throw new UnsupportedOperationException();
	}

	@Override
	protected final Url buildUrl(final UrlInfo info) {
		throw new UnsupportedOperationException();
	}

	@Override
	protected final boolean pageMustHaveBeenCreatedBookmarkable() {
		throw new UnsupportedOperationException();
	}

	@Override
	public int getCompatibilityScore(Request request)
	{
		Url originalUrl = new Url(request.getUrl());
		PageParameters parameters = extractPageParameters(request, mountSegments.length, parametersEncoder);
		if (parameters != null)
		{
			parameters.setLocale(resolveLocale());
		}
		removeCachingDecoration(originalUrl, parameters);
		Request requestWithoutDecoration = request.cloneWithUrl(originalUrl);

		int score = super.getCompatibilityScore(requestWithoutDecoration);
		if (score > 0)
		{
			score--; // pages always have priority over resources
		}
		else
		{
			score = -1;
		}
		return score;
	}

	@Override
	public Url mapHandler(IRequestHandler requestHandler)
	{
		if ((requestHandler instanceof ResourceReferenceRequestHandler) == false)
		{
			return null;
		}

		ResourceReferenceRequestHandler handler = (ResourceReferenceRequestHandler)requestHandler;

		// see if request handler addresses the resource reference we serve
		if (resourceReference.equals(handler.getResourceReference()) == false)
		{
			return null;
		}

		Url url = new Url();

		// add mount path segments
		for (String segment : mountSegments)
		{
			url.getSegments().add(segment);
		}

		// replace placeholder parameters
		PageParameters parameters = newPageParameters();
		parameters.mergeWith(handler.getPageParameters());

		for (int index = 0; index < mountSegments.length; ++index)
		{
			String placeholder = getPlaceholder(mountSegments[index]);

			if (placeholder != null)
			{
				url.getSegments().set(index, parameters.get(placeholder).toString(""));
				parameters.remove(placeholder);
			}
		}

		// add caching information
		addCachingDecoration(url, parameters);

		ResourceUtil.encodeResourceReferenceAttributes(url, resourceReference);
		// create url
		return encodePageParameters(url, parameters, parametersEncoder);
	}

	protected IResourceCachingStrategy getCachingStrategy()
	{
		return Application.get().getResourceSettings().getCachingStrategy();
	}

	protected void addCachingDecoration(Url url, PageParameters parameters)
	{
		final List<String> segments = url.getSegments();
		final int lastSegmentAt = segments.size() - 1;
		final String filename = segments.get(lastSegmentAt);

		if (Strings.isEmpty(filename) == false)
		{
			final IResource resource = resourceReference.getResource();

			if (resource instanceof IStaticCacheableResource)
			{
				final IStaticCacheableResource cacheable = (IStaticCacheableResource)resource;
				
				if(cacheable.isCachingEnabled())
				{
					final ResourceUrl cacheUrl = new ResourceUrl(filename, parameters);
	
					getCachingStrategy().decorateUrl(cacheUrl, cacheable);
	
					if (Strings.isEmpty(cacheUrl.getFileName()))
					{
						if (Application.exists() && Application.get().usesDeploymentConfig())
						{
							throw new AbortWithHttpErrorCodeException(HttpServletResponse.SC_NOT_FOUND,
								"caching strategy returned empty name for " + resource);
						}
						else
						{
							throw new IllegalStateException(
									"caching strategy returned empty name for " + resource);
						}
					}
					segments.set(lastSegmentAt, cacheUrl.getFileName());
				}
			}
		}
	}

	protected void removeCachingDecoration(Url url, PageParameters parameters)
	{
		final List<String> segments = url.getSegments();

		if (segments.isEmpty() == false)
		{
			// get filename (the last segment)
			final int lastSegmentAt = segments.size() - 1;
			String filename = segments.get(lastSegmentAt);

			// ignore requests with empty filename
			if (Strings.isEmpty(filename))
			{
				return;
			}

			// create resource url from filename and query parameters
			final ResourceUrl resourceUrl = new ResourceUrl(filename, parameters);

			// remove caching information from request
			getCachingStrategy().undecorateUrl(resourceUrl);

			// check for broken caching strategy (this must never happen)
			if (Strings.isEmpty(resourceUrl.getFileName()))
			{
				throw new IllegalStateException("caching strategy returned empty name for " +
					resourceUrl);
			}

			segments.set(lastSegmentAt, resourceUrl.getFileName());
		}
	}
}
