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

import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.wicket.Application;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.listener.IChangeListener;
import org.apache.wicket.util.watch.IModifiable;
import org.apache.wicket.util.watch.IModificationWatcher;
import org.apache.wicket.util.watch.ModificationWatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is Wicket's default IMarkupCache implementation. It will load the markup and cache it for
 * fast retrieval.
 * <p>
 * If the application is in development mode and a markup file changes, it'll automatically be
 * removed from the cache and reloaded when needed.
 * <p>
 * MarkupCache is registered with {@link MarkupFactory} which in turn is registered with
 * {@link org.apache.wicket.settings.MarkupSettings} and thus can be replaced with a sub-classed version.
 * 
 * @see org.apache.wicket.settings.MarkupSettings
 * @see MarkupFactory
 * 
 * @author Jonathan Locke
 * @author Juergen Donnerstag
 */
public class MarkupCache implements IMarkupCache
{
	/** Log for reporting. */
	private static final Logger log = LoggerFactory.getLogger(MarkupCache.class);

	/** The actual cache: location => Markup */
	private final ICache<String, Markup> markupCache;

	/**
	 * Add extra indirection to the cache: key => location
	 * <p>
	 * Since ConcurrentHashMap does not allow to store null values, we are using Markup.NO_MARKUP
	 * instead.
	 */
	private final ICache<String, String> markupKeyCache;

	/** The markup cache key provider used by MarkupCache */
	private IMarkupCacheKeyProvider markupCacheKeyProvider;

	/**
	 * Note that you can not use Application.get() since removeMarkup() will be called from a
	 * ModificationWatcher thread which has no associated Application.
	 */
	private final Application application;

	/**
	 * A convenient helper to get the markup cache registered with the application.
	 * 
	 * @see Application#getMarkupSettings()
	 * @see MarkupFactory#getMarkupCache()
	 * 
	 * @return The markup cache registered with the {@link Application}
	 */
	public static IMarkupCache get()
	{
		return Application.get().getMarkupSettings().getMarkupFactory().getMarkupCache();
	}

	/**
	 * Constructor.
	 */
	protected MarkupCache()
	{
		application = Application.get();

		markupCache = newCacheImplementation();
		if (markupCache == null)
		{
			throw new WicketRuntimeException("The map used to cache markup must not be null");
		}

		markupKeyCache = newCacheImplementation();
	}

	@Override
	public void clear()
	{
		markupCache.clear();
		markupKeyCache.clear();
	}

	@Override
	public void shutdown()
	{
		markupCache.shutdown();
		markupKeyCache.shutdown();
	}

	/**
	 * Note that this method will be called from a "cleanup" thread which might not have a thread
	 * local application.
	 */
	@Override
	public final IMarkupFragment removeMarkup(final String cacheKey)
	{
		Args.notNull(cacheKey, "cacheKey");

		if (log.isDebugEnabled())
		{
			log.debug("Removing from cache: " + cacheKey);
		}

		// Remove the markup from the cache
		String locationString = markupKeyCache.get(cacheKey);
		IMarkupFragment markup = (locationString != null ? markupCache.get(locationString) : null);
		if (markup == null)
		{
			return null;
		}

		// Found an entry: actual markup or Markup.NO_MARKUP. Null values are not possible
		// because of ConcurrentHashMap.
		markupCache.remove(locationString);

		if (log.isDebugEnabled())
		{
			log.debug("Removed from cache: " + locationString);
		}

		// If a base markup file has been removed from the cache then
		// the derived markup should be removed as well.
		removeMarkupWhereBaseMarkupIsNoLongerInTheCache();

		// And now remove all watcher entries associated with markup
		// resources no longer in the cache.

		// Note that you can not use Application.get() since removeMarkup() will be called from a
		// ModificationWatcher thread which has no associated Application.

		IModificationWatcher watcher = application.getResourceSettings().getResourceWatcher(false);
		if (watcher != null)
		{
			Iterator<IModifiable> iter = watcher.getEntries().iterator();
			while (iter.hasNext())
			{
				IModifiable modifiable = iter.next();
				if (modifiable instanceof MarkupResourceStream)
				{
					if (!isMarkupCached((MarkupResourceStream)modifiable))
					{
						iter.remove();

						if (log.isDebugEnabled())
						{
							log.debug("Removed from watcher: " + modifiable);
						}
					}
				}
			}
		}

		return markup;
	}

	private void removeMarkupWhereBaseMarkupIsNoLongerInTheCache()
	{
		// Repeat until all dependent resources have been removed (count == 0)
		int count = 1;
		while (count > 0)
		{
			// Reset prior to next round
			count = 0;

			// Iterate though all entries of the cache
			Iterator<Markup> iter = markupCache.getValues().iterator();
			while (iter.hasNext())
			{
				Markup markup = iter.next();

				if ((markup != null) && (markup != Markup.NO_MARKUP))
				{
					// Check if the markup associated with key has a base markup. And if yes, test
					// if that is cached. If the base markup has been removed, than remove the
					// derived markup as well.

					MarkupResourceStream resourceStream = markup.getMarkupResourceStream();
					if (resourceStream != null)
					{
						resourceStream = resourceStream.getBaseMarkupResourceStream();
					}

					// Is the base markup available in the cache?
					if ((resourceStream != null) && !isMarkupCached(resourceStream))
					{
						iter.remove();
						count++;

						if (log.isDebugEnabled())
						{
							log.debug("Removed derived markup from cache: " +
								markup.getMarkupResourceStream());
						}
					}
				}
			}
		}
	}

	/**
	 * @param resourceStream
	 * @return True if the markup is cached
	 */
	private boolean isMarkupCached(final MarkupResourceStream resourceStream)
	{
		if (resourceStream != null)
		{
			String key = resourceStream.getCacheKey();
			if (key != null)
			{
				String locationString = markupKeyCache.get(key);
				if ((locationString != null) && (markupCache.get(locationString) != null))
				{
					return true;
				}
			}
		}
		return false;
	}

	@Override
	public final int size()
	{
		return markupCache.size();
	}

	/**
	 * Get a unmodifiable map which contains the cached data. The map key is of type String and the
	 * value is of type Markup.
	 * <p>
	 * May be used to debug or iterate the cache content.
	 * 
	 * @return cache implementation
	 */
	public final ICache<String, Markup> getMarkupCache()
	{
		return markupCache;
	}

	@Override
	public final Markup getMarkup(final MarkupContainer container, final Class<?> clazz,
		final boolean enforceReload)
	{
		Class<?> containerClass = MarkupFactory.get().getContainerClass(container, clazz);

		// Get the cache key to be associated with the markup resource stream.
		// If the cacheKey returned == null, than caching is disabled for the resource stream.
		final String cacheKey = getMarkupCacheKeyProvider(container).getCacheKey(container,
			containerClass);

		// Is the markup already in the cache?
		Markup markup = null;
		if ((enforceReload == false) && (cacheKey != null))
		{
			markup = getMarkupFromCache(cacheKey, container);
		}

		// If markup not found in cache or cache disabled, than ...
		if (markup == null)
		{
			if (log.isDebugEnabled())
			{
				log.debug("Load markup: cacheKey=" + cacheKey);
			}

			// Get the markup resource stream for the container
			final MarkupResourceStream resourceStream = MarkupFactory.get()
				.getMarkupResourceStream(container, containerClass);

			// Found markup?
			if (resourceStream != null)
			{
				resourceStream.setCacheKey(cacheKey);

				// load the markup and watch for changes
				markup = loadMarkupAndWatchForChanges(container, resourceStream, enforceReload);
			}
			else
			{
				markup = onMarkupNotFound(cacheKey, container, Markup.NO_MARKUP);
			}
		}

		// NO_MARKUP should only be used inside the Cache.
		if (markup == Markup.NO_MARKUP)
		{
			markup = null;
		}

		return markup;
	}

	/**
	 * Will be called if the markup was not in the cache yet and could not be found either.
	 * <p>
	 * Subclasses may change the default implementation. E.g. they might choose not to update the
	 * cache to enforce reloading of any markup not found. This might be useful in very dynamic
	 * environments. Additionally a non-caching IResourceStreamLocator should be used.
	 * 
	 * @param cacheKey
	 * @param container
	 * @param markup
	 *            Markup.NO_MARKUP
	 * @return Same as parameter "markup"
	 * @see org.apache.wicket.settings.ResourceSettings#setResourceStreamLocator(org.apache.wicket.core.util.resource.locator.IResourceStreamLocator)
	 */
	protected Markup onMarkupNotFound(final String cacheKey, final MarkupContainer container,
		final Markup markup)
	{
		if (log.isDebugEnabled())
		{
			log.debug("Markup not found: " + cacheKey);
		}

		// If cacheKey == null then caching is disabled for the component
		if (cacheKey != null)
		{
			// flag markup as non-existent
			markupKeyCache.put(cacheKey, cacheKey);
			putIntoCache(cacheKey, container, markup);
		}

		return markup;
	}

	/**
	 * Put the markup into the cache if cacheKey is not null and the cache does not yet contain the
	 * cacheKey. Return the markup stored in the cache if cacheKey is present already.
	 * 
	 * More sophisticated implementations may call a container method to e.g. cache it per container
	 * instance.
	 * 
	 * @param locationString
	 *            If {@code null} then ignore the cache
	 * @param container
	 *            The container this markup is for.
	 * @param markup
	 * @return markup The markup provided, except if the cacheKey already existed in the cache, then
	 *         the markup from the cache is provided.
	 */
	protected Markup putIntoCache(final String locationString, final MarkupContainer container,
		Markup markup)
	{
		if (locationString != null)
		{
			if (markupCache.containsKey(locationString) == false)
			{
				// The default cache implementation is a ConcurrentHashMap. Thus neither the key nor
				// the value can be null.
				if (markup == null)
				{
					markup = Markup.NO_MARKUP;
				}

				markupCache.put(locationString, markup);
			}
			else
			{
				// We don't lock the cache while loading a markup. Thus it may
				// happen that the very same markup gets loaded twice (the first
				// markup being loaded, but not yet in the cache, and another
				// request requesting the very same markup). Since markup
				// loading in avg takes less than 100ms, it is not really an
				// issue. For consistency reasons however, we should always use
				// the markup loaded first which is why it gets returned.
				markup = markupCache.get(locationString);
			}
		}
		return markup;
	}

	/**
	 * Wicket's default implementation just uses the cacheKey to retrieve the markup from the cache.
	 * More sophisticated implementations may call a container method to e.g. ignore the cached
	 * markup under certain situations.
	 * 
	 * @param cacheKey
	 *            If null, than the cache will be ignored
	 * @param container
	 * @return null, if not found or to enforce reloading the markup
	 */
	protected Markup getMarkupFromCache(final String cacheKey, final MarkupContainer container)
	{
		if (cacheKey != null)
		{
			String locationString = markupKeyCache.get(cacheKey);
			if (locationString != null)
			{
				return markupCache.get(locationString);
			}
		}
		return null;
	}

	/**
	 * Loads markup from a resource stream.
	 * 
	 * @param container
	 *            The original requesting markup container
	 * @param markupResourceStream
	 *            The markup resource stream to load
	 * @param enforceReload
	 *            The cache will be ignored and all, including inherited markup files, will be
	 *            reloaded. Whatever is in the cache, it will be ignored
	 * @return The markup. Markup.NO_MARKUP, if not found.
	 */
	private Markup loadMarkup(final MarkupContainer container,
		final MarkupResourceStream markupResourceStream, final boolean enforceReload)
	{
		String cacheKey = markupResourceStream.getCacheKey();
		String locationString = markupResourceStream.locationAsString();
		if (locationString == null)
		{
			// set the cache key as location string, because location string
			// couldn't be resolved.
			locationString = cacheKey;
		}

		Markup markup = MarkupFactory.get().loadMarkup(container, markupResourceStream,
			enforceReload);
		if (markup != null)
		{
			if (cacheKey != null)
			{
				String temp = markup.locationAsString();
				if (temp != null)
				{
					locationString = temp;
				}

				// add the markup to the cache.
				markupKeyCache.put(cacheKey, locationString);
				return putIntoCache(locationString, container, markup);
			}
			return markup;
		}

		// In case the markup could not be loaded (without exception) then ..
		if (cacheKey != null)
		{
			removeMarkup(cacheKey);
		}

		return Markup.NO_MARKUP;
	}

	/**
	 * Load markup from an IResourceStream and add an {@link IChangeListener}to the
	 * {@link ModificationWatcher} so that if the resource changes, we can remove it from the cache
	 * automatically and subsequently reload when needed.
	 * 
	 * @param container
	 *            The original requesting markup container
	 * @param markupResourceStream
	 *            The markup stream to load and begin to watch
	 * @param enforceReload
	 *            The cache will be ignored and all, including inherited markup files, will be
	 *            reloaded. Whatever is in the cache, it will be ignored
	 * @return The markup in the stream
	 */
	private Markup loadMarkupAndWatchForChanges(final MarkupContainer container,
		final MarkupResourceStream markupResourceStream, final boolean enforceReload)
	{
		// @TODO the following code sequence looks very much like in loadMarkup. Can it be
		// optimized?
		final String cacheKey = markupResourceStream.getCacheKey();
		if (cacheKey != null)
		{
			if (enforceReload == false)
			{
				// get the location String
				String locationString = markupResourceStream.locationAsString();
				if (locationString == null)
				{
					// set the cache key as location string, because location string
					// couldn't be resolved.
					locationString = cacheKey;
				}
				Markup markup = markupCache.get(locationString);
				if (markup != null)
				{
					markupKeyCache.put(cacheKey, locationString);
					return markup;
				}
			}

			// Watch file in the future
			final IModificationWatcher watcher = application.getResourceSettings()
				.getResourceWatcher(true);
			if (watcher != null)
			{
				watcher.add(markupResourceStream, new IChangeListener<IModifiable>()
				{
					@Override
					public void onChange(IModifiable modifiable)
					{
						if (log.isDebugEnabled())
						{
							log.debug("Remove markup from watcher: " + markupResourceStream);
						}

						// Remove the markup from the cache. It will be reloaded
						// next time when the markup is requested.
						watcher.remove(markupResourceStream);
						removeMarkup(cacheKey);
					}
				});
			}
		}

		if (log.isDebugEnabled())
		{
			log.debug("Loading markup from " + markupResourceStream);
		}
		return loadMarkup(container, markupResourceStream, enforceReload);
	}

	/**
	 * Get the markup cache key provider to be used
	 * 
	 * @param container
	 *            The MarkupContainer requesting the markup resource stream
	 * @return IMarkupResourceStreamProvider
	 */
	public IMarkupCacheKeyProvider getMarkupCacheKeyProvider(final MarkupContainer container)
	{
		if (container instanceof IMarkupCacheKeyProvider)
		{
			return (IMarkupCacheKeyProvider)container;
		}

		if (markupCacheKeyProvider == null)
		{
			markupCacheKeyProvider = new DefaultMarkupCacheKeyProvider();
		}
		return markupCacheKeyProvider;
	}

	/**
	 * Allows you to change the map implementation which will hold the cache data. By default it is
	 * a ConcurrentHashMap() in order to allow multiple thread to access the data in a secure way.
	 * 
	 * @param <K>
	 * @param <V>
	 * @return new instance of cache implementation
	 */
	protected <K, V> ICache<K, V> newCacheImplementation()
	{
		return new DefaultCacheImplementation<K, V>();
	}

	/**
	 * MarkupCache allows you to implement you own cache implementation. ICache is the interface the
	 * implementation must comply with.
	 * 
	 * @param <K>
	 *            The key type
	 * @param <V>
	 *            The value type
	 */
	public interface ICache<K, V>
	{
		/**
		 * Clear the cache
		 */
		void clear();

		/**
		 * Remove an entry from the cache.
		 * 
		 * @param key
		 * @return true, if found and removed
		 */
		boolean remove(K key);

		/**
		 * Get the cache element associated with the key
		 * 
		 * @param key
		 * @return cached object for key <code>key</code> or null if no matches
		 */
		V get(K key);

		/**
		 * Get all the keys referencing cache entries
		 * 
		 * @return collection of cached keys
		 */
		Collection<K> getKeys();

		/**
		 * Get all the values referencing cache entries
		 * 
		 * @return collection of cached keys
		 */
		Collection<V> getValues();

		/**
		 * Check if key is in the cache
		 * 
		 * @param key
		 * @return true if cache contains key <code>key</code>
		 */
		boolean containsKey(K key);

		/**
		 * Get the number of cache entries
		 * 
		 * @return number of cache entries
		 */
		int size();

		/**
		 * Put an entry into the cache
		 * 
		 * @param key
		 *            The reference key to find the element. Must not be null.
		 * @param value
		 *            The element to be cached. Must not be null.
		 */
		void put(K key, V value);

		/**
		 * Cleanup and shutdown
		 */
		void shutdown();
	}

	/**
	 * @param <K>
	 * @param <V>
	 */
	public static class DefaultCacheImplementation<K, V> implements ICache<K, V>
	{
		// Neither key nor value are allowed to be null with ConcurrentHashMap
		private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<K, V>();

		/**
		 * Construct.
		 */
		public DefaultCacheImplementation()
		{
		}

		@Override
		public void clear()
		{
			cache.clear();
		}

		@Override
		public boolean containsKey(final Object key)
		{
			if (key == null)
			{
				return false;
			}
			return cache.containsKey(key);
		}

		@Override
		public V get(final Object key)
		{
			if (key == null)
			{
				return null;
			}
			return cache.get(key);
		}

		@Override
		public Collection<K> getKeys()
		{
			return cache.keySet();
		}

		@Override
		public Collection<V> getValues()
		{
			return cache.values();
		}

		@Override
		public void put(K key, V value)
		{
			// Note that neither key nor value are allowed to be null with ConcurrentHashMap
			cache.put(key, value);
		}

		@Override
		public boolean remove(K key)
		{
			if (key == null)
			{
				return false;
			}
			return cache.remove(key) == null;
		}

		@Override
		public int size()
		{
			return cache.size();
		}

		@Override
		public void shutdown()
		{
			clear();
		}
	}
}
