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

import java.util.Locale;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.apache.wicket.request.resource.ResourceReference.Key;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Generics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Allows to register and lookup {@link ResourceReference}s per Application.
 * 
 * @see org.apache.wicket.Application#getResourceReferenceRegistry()
 * @see org.apache.wicket.Application#newResourceReferenceRegistry()
 * 
 * @author Matej Knopp
 * @author Juergen Donnerstag
 */
public class ResourceReferenceRegistry
{
	/** Log for reporting. */
	private static final Logger log = LoggerFactory.getLogger(ResourceReferenceRegistry.class);

	// Scan classes and its superclasses for static ResourceReference fields. For each
	// RR field found, the callback method is called and the RR gets registered. It's kind of
	// auto-register all RRs in your Component hierarchy.
	private ClassScanner scanner = new ClassScanner()
	{
		@Override
		boolean foundResourceReference(final ResourceReference reference)
		{
			// register the RR found (static field of Scope class)
			return registerResourceReference(reference);
		}
	};

	// The Map (registry) maintaining the resource references
	private final ConcurrentHashMap<Key, ResourceReference> map = Generics.newConcurrentHashMap();

	// If combinations of parameters (Key) have no registered resource reference yet, a default
	// resource reference can be created and added to the registry. The following list keeps track
	// of all auto added references.
	private Queue<Key> autoAddedQueue;

	// max entries. If the queue is full and new references are auto generated, references are
	// removed starting with the first entry and unregistered from the registry.
	private int autoAddedCapacity = 1000;

	/**
	 * A simple implementation of {@link IResourceReferenceFactory} that creates
	 * {@link PackageResourceReference}
	 */
	public static class DefaultResourceReferenceFactory implements IResourceReferenceFactory
	{
		@Override
		public ResourceReference create(Key key)
		{
			ResourceReference result = null;
			if (PackageResource.exists(key))
			{
				result = new PackageResourceReference(key);
			}
			return result;
		}
	}

	/**
	 * The factory to use when a ResourceReference is not previously
	 * registered and a new instance should be create
	 */
	private IResourceReferenceFactory resourceReferenceFactory;

	/**
	 * Constructor.
	 *
	 * <p>Uses DefaultResourceReferenceFactory to create ResourceReference when there is
	 * no registered one for the requested attributes</p>
	 */
	public ResourceReferenceRegistry()
	{
		this(new DefaultResourceReferenceFactory());
	}

	/**
	 * Constructor
	 *
	 * @param resourceReferenceFactory
	 *      The factory that will create ResourceReference by Key when there is no registered one
	 */
	public ResourceReferenceRegistry(IResourceReferenceFactory resourceReferenceFactory)
	{
		this.resourceReferenceFactory = Args.notNull(resourceReferenceFactory, "resourceReferenceFactory");

		// Initial the auto-add list for a maximum of 1000 entries
		setAutoAddedCapacity(autoAddedCapacity);
	}

	/**
	 * Registers the given {@link ResourceReference}.
	 * <p>
	 * {@link ResourceReference#canBeRegistered()} must return <code>true</code>. Else, the resource
	 * reference will not be registered.
	 * 
	 * @param reference
	 *      the reference to register
	 * @return {@code true} if the resource was registered successfully or has been registered previously
	 *         already.
	 */
	public final boolean registerResourceReference(final ResourceReference reference)
	{
		return null != _registerResourceReference(reference);
	}

	/**
	 * Registers the given {@link ResourceReference}.
	 * <p>
	 * {@link ResourceReference#canBeRegistered()} must return <code>true</code>. Else, the resource
	 * reference will not be registered.
	 * 
	 * @param reference
	 *      the reference to register
	 * @return {@code true} if the resource was registered successfully or has been registered previously
	 *         already.
	 */
	private Key _registerResourceReference(final ResourceReference reference)
	{
		Args.notNull(reference, "reference");

		if (reference.canBeRegistered())
		{
			Key key = reference.getKey();
			map.putIfAbsent(key, reference);
			return key;
		}

		log.warn("{} cannot be added to the registry.", reference.getClass().getName());
		return null;
	}

	/**
	 * Unregisters a {@link ResourceReference} by its identifier.
	 * 
	 * @param key
	 *            the {@link ResourceReference}'s identifier
	 * @return The removed ResourceReference or {@code null} if the registry did not contain an entry for this key.
	 */
	public final ResourceReference unregisterResourceReference(final Key key)
	{
		Args.notNull(key, "key");

		// remove from registry
		ResourceReference removed = map.remove(key);

		// remove from auto-added list, in case the RR was auto-added
		if (autoAddedQueue != null)
		{
			autoAddedQueue.remove(key);
		}

		return removed;
	}

	/**
	 * Get a resource reference matching the parameters from the registry or if not found and
	 * requested, create an default resource reference and add it to the registry.
	 * <p>
	 * Part of the search is scanning the class (scope) and it's superclass for static
	 * ResourceReference fields. Found fields get registered automatically (but are different from
	 * auto-generated ResourceReferences).
	 * 
	 * @see #createDefaultResourceReference(org.apache.wicket.request.resource.ResourceReference.Key)
	 * @see ClassScanner
	 * 
	 * @param scope
	 *            The scope of resource reference (e.g. the Component's class)
	 * @param name
	 *            The name of resource reference (e.g. filename)
	 * @param locale
	 *            see Component
	 * @param style
	 *            see Component
	 * @param variation
	 *            see Component
	 * @param strict
	 *            If true, "weaker" combination of scope, name, locale etc. are not tested
	 * @param createIfNotFound
	 *            If true a default resource reference is created if no entry can be found in the
	 *            registry. The newly created resource reference will be added to the registry.
	 * @return Either the resource reference found in the registry or, if requested, a resource
	 *         reference automatically created based on the parameters provided. The automatically
	 *         created resource reference will automatically be added to the registry.
	 */
	public final ResourceReference getResourceReference(final Class<?> scope, final String name,
		final Locale locale, final String style, final String variation, final boolean strict,
		final boolean createIfNotFound)
	{
		return getResourceReference(new Key(scope.getName(), name, locale, style, variation),
			strict, createIfNotFound);
	}

	/**
	 * Get a resource reference matching the parameters from the registry or if not found and
	 * requested, create an default resource reference and add it to the registry.
	 * <p>
	 * Part of the search is scanning the class (scope) and it's superclass for static
	 * ResourceReference fields. Found fields get registered automatically (but are different from
	 * auto-generated ResourceReferences).
	 * 
	 * @see #createDefaultResourceReference(org.apache.wicket.request.resource.ResourceReference.Key)
	 * @see ClassScanner
	 * 
	 * @param key
	 *            The data making up the resource reference
	 * @param strict
	 *            If true, "weaker" combination of scope, name, locale etc. are not tested
	 * @param createIfNotFound
	 *            If true a default resource reference is created if no entry can be found in the
	 *            registry. The newly created resource reference will be added to the registry.
	 * @return Either the resource reference found in the registry or, if requested, a resource
	 *         reference automatically created based on the parameters provided. The automatically
	 *         created resource reference will automatically be added to the registry.
	 */
	public final ResourceReference getResourceReference(final Key key, final boolean strict,
		final boolean createIfNotFound)
	{
		ResourceReference resource = _getResourceReference(key.getScope(), key.getName(),
			key.getLocale(), key.getStyle(), key.getVariation(), strict);

		// Nothing found so far?
		if (resource == null)
		{
			// Scan the class (scope) and it's super classes for static fields containing resource
			// references. Such resources are registered as normal resource reference (not
			// auto-added).
			if (scanner.scanClass(key.getScopeClass()) > 0)
			{
				// At least one new resource reference got registered => Search the registry again
				resource = _getResourceReference(key.getScope(), key.getName(), key.getLocale(),
					key.getStyle(), key.getVariation(), strict);
			}

			// Still nothing found => Shall a new reference be auto-created?
			if ((resource == null) && createIfNotFound)
			{
				resource = addDefaultResourceReference(key);
			}
		}

		return resource;
	}

	/**
	 * Get a resource reference matching the parameters from the registry.
	 * 
	 * @param scope
	 *            The scope of resource reference (e.g. the Component's class)
	 * @param name
	 *            The name of resource reference (e.g. filename)
	 * @param locale
	 *            see Component
	 * @param style
	 *            see Component
	 * @param variation
	 *            see Component
	 * @param strict
	 *            If true, "weaker" combination of scope, name, locale etc. are not tested
	 * @return Either the resource reference found in the registry or null if not found
	 */
	private ResourceReference _getResourceReference(final String scope, final String name,
		final Locale locale, final String style, final String variation, final boolean strict)
	{
		// Create a registry key containing all of the relevant attributes
		Key key = new Key(scope, name, locale, style, variation);

		// Get resource reference matching exactly the attrs provided
		ResourceReference res = map.get(key);
		if ((res != null) || strict)
		{
			return res;
		}

		res = _getResourceReference(scope, name, locale, style, null, true);
		if (res == null)
		{
			res = _getResourceReference(scope, name, locale, null, variation, true);
		}
		if (res == null)
		{
			res = _getResourceReference(scope, name, locale, null, null, true);
		}
		if (res == null)
		{
			res = _getResourceReference(scope, name, null, style, variation, true);
		}
		if (res == null)
		{
			res = _getResourceReference(scope, name, null, style, null, true);
		}
		if (res == null)
		{
			res = _getResourceReference(scope, name, null, null, variation, true);
		}
		if (res == null)
		{
			res = _getResourceReference(scope, name, null, null, null, true);
		}
		return res;
	}

	/**
	 * Creates a default resource reference and registers it.
	 * 
	 * @param key
	 *      the data making up the resource reference
	 * @return The default resource created
	 */
	private ResourceReference addDefaultResourceReference(final Key key)
	{
		// Can be subclassed to create other than PackagedResourceReference
		ResourceReference reference = createDefaultResourceReference(key);

		if (reference != null)
		{
			// number of RRs which can be auto-added is restricted (cache size). Remove entries, and
			// unregister excessive ones, if needed.
			enforceAutoAddedCacheSize(getAutoAddedCapacity());

			// Register the new RR
			_registerResourceReference(reference);

			// Add it to the auto-added list
			if (autoAddedQueue != null)
			{
				autoAddedQueue.add(key);
			}
		}
		else
		{
			log.warn("A ResourceReference wont be created for a resource with key [{}] because it cannot be located.", key);
		}
		return reference;
	}

	/**
	 * The number of {@link ResourceReference}s which can be auto-added is restricted (cache size). Remove entries, and
	 * unregister excessive ones, if needed.
	 * 
	 * @param maxSize
	 *            Remove all entries, head first, until auto-added cache is smaller than maxSize.
	 */
	private void enforceAutoAddedCacheSize(int maxSize)
	{
		if (autoAddedQueue != null)
		{
			while (autoAddedQueue.size() > maxSize)
			{
				// remove entry from auto-added list
				Key first = autoAddedQueue.remove();

				// remove entry from registry
				map.remove(first);
			}
		}
	}

	/**
	 * Creates a default resource reference in case no registry entry and it was requested to create
	 * one.
	 * <p>
	 * A {@link PackageResourceReference} will be created by default
	 * 
	 * @param key
	 *      the data making up the resource reference
	 * @return The {@link ResourceReference} created or {@code null} if not successful
	 */
	protected ResourceReference createDefaultResourceReference(final Key key)
	{
		IResourceReferenceFactory factory = getResourceReferenceFactory();
		if (factory == null)
		{
			factory = new DefaultResourceReferenceFactory();
		}
		return factory.create(key);
	}

	/**
	 * Set the cache size in number of entries
	 * 
	 * @param autoAddedCapacity
	 *            A value < 0 will disable aging of auto-create resource references. They will be
	 *            created, added to the registry and live their until manually removed or the
	 *            application shuts down.
	 */
	public final void setAutoAddedCapacity(final int autoAddedCapacity)
	{
		// Disable aging of auto-added references?
		if (autoAddedCapacity < 0)
		{
			// unregister all auto-added references
			clearAutoAddedEntries();

			// disable aging from now on
			autoAddedQueue = null;
		}
		else
		{
			this.autoAddedCapacity = autoAddedCapacity;

			if (autoAddedQueue == null)
			{
				autoAddedQueue = new ConcurrentLinkedQueue<Key>();
			}
			else
			{
				// remove all extra entries if necessary
				enforceAutoAddedCacheSize(autoAddedCapacity);
			}
		}
	}

	/**
	 * Gets cache size in number of entries
	 * 
	 * @return capacity
	 */
	public final int getAutoAddedCapacity()
	{
		return autoAddedCapacity;
	}

	/**
	 * Unregisters all auto-added Resource References
	 */
	public final void clearAutoAddedEntries()
	{
		enforceAutoAddedCacheSize(0);
	}

	/**
	 * @return Number of auto-generated (and registered) resource references.
	 */
	public final int getAutoAddedCacheSize()
	{
		return autoAddedQueue == null ? -1 : autoAddedQueue.size();
	}

	/**
	 * @return Number of registered resource references (normal and auto-generated)
	 */
	public final int getSize()
	{
		return map.size();
	}

	/**
	 * @return the factory that will create the resource reference by using the parsed
	 *          {@link org.apache.wicket.request.resource.ResourceReference.Key}
	 */
	public IResourceReferenceFactory getResourceReferenceFactory()
	{
		return resourceReferenceFactory;
	}

	/**
	 * Sets the factory to use when a ResourceReference is not previously
	 * registered and a new instance should be created
	 *
	 * @param resourceReferenceFactory
	 *          the factory that will create the resource reference by using the parsed
	 *          {@link org.apache.wicket.request.resource.ResourceReference.Key}
	 */
	public void setResourceReferenceFactory(IResourceReferenceFactory resourceReferenceFactory)
	{
		this.resourceReferenceFactory = resourceReferenceFactory;
	}
}
