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

import java.io.*;
import java.util.*;
import java.util.concurrent.*;

import org.apache.juneau.internal.*;

/**
 * Class for retrieving and caching resource files from the classpath.
 */
public final class ClasspathResourceManager {

	// Maps resource names+locales to found resources.
	private final ConcurrentHashMap<ResourceKey,byte[]> byteCache;
	private final ConcurrentHashMap<ResourceKey,String> stringCache;

	private final Class<?> baseClass;
	private final ClasspathResourceFinder resourceFinder;
	private final boolean useCache;

	/**
	 * Constructor.
	 *
	 * @param baseClass The default class to use for retrieving resources from the classpath.
	 * @param resourceFinder The resource finder implementation.
	 * @param useCache If <jk>true</jk>, retrieved resources are stored in an in-memory cache for fast lookup.
	 */
	public ClasspathResourceManager(Class<?> baseClass, ClasspathResourceFinder resourceFinder, boolean useCache) {
		this.baseClass = baseClass;
		this.resourceFinder = resourceFinder;
		this.useCache = useCache;
		if (useCache) {
			this.byteCache = new ConcurrentHashMap<>();
			this.stringCache = new ConcurrentHashMap<>();
		} else {
			this.byteCache = null;
			this.stringCache = null;
		}
	}

	/**
	 * Constructor.
	 *
	 * <p>
	 * Uses default {@link ClasspathResourceFinderBasic} for finding resources.
	 *
	 * @param baseClass The default class to use for retrieving resources from the classpath.
	 */
	public ClasspathResourceManager(Class<?> baseClass) {
		this(baseClass, new ClasspathResourceFinderBasic(), false);
	}

	/**
	 * Finds the resource with the given name.
	 *
	 * @param name Name of the desired resource.
	 * @return An input stream to the object, or <jk>null</jk> if the resource could not be found.
	 * @throws IOException Thrown by underlying stream.
	 */
	public InputStream getStream(String name) throws IOException {
		return getStream(name, null);
	}

	/**
	 * Finds the resource with the given name for the specified locale and returns it as an input stream.
	 *
	 * @param name Name of the desired resource.
	 * @param locale The locale.  Can be <jk>null</jk>.
	 * @return An input stream to the object, or <jk>null</jk> if the resource could not be found.
	 * @throws IOException Thrown by underlying stream.
	 */
	public InputStream getStream(String name, Locale locale) throws IOException {
		return getStream(baseClass, name, locale);
	}

	/**
	 * Finds the resource with the given name for the specified locale and returns it as an input stream.
	 *
	 * @param baseClass
	 * 	Overrides the default class to use for retrieving the classpath resource.
	 * 	<br>If <jk>null<jk>, uses the base class passed in through the constructor of this class.
	 * @param name Name of the desired resource.
	 * @param locale The locale.  Can be <jk>null</jk>.
	 * @return An input stream to the object, or <jk>null</jk> if the resource could not be found.
	 * @throws IOException Thrown by underlying stream.
	 */
	public InputStream getStream(Class<?> baseClass, String name, Locale locale) throws IOException {

		if (baseClass == null)
			baseClass = this.baseClass;

		if (! useCache)
			return resourceFinder.findResource(baseClass, name, locale);

		ResourceKey key = new ResourceKey(name, locale);

		byte[] r = byteCache.get(key);
		if (r == null) {
			try (InputStream is = resourceFinder.findResource(baseClass, name, locale)) {
				if (is != null)
					byteCache.putIfAbsent(key, IOUtils.readBytes(is, 1024));
			}
		}

		r = byteCache.get(key);
		return r == null ? null : new ByteArrayInputStream(r);
	}

	/**
	 * Finds the resource with the given name and converts it to a simple string.
	 *
	 * @param name Name of the desired resource.
	 * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
	 * @throws IOException Thrown by underlying stream.
	 */
	public String getString(String name) throws IOException {
		return getString(baseClass, name, null);
	}

	/**
	 * Finds the resource with the given name and converts it to a simple string.
	 *
	 * @param baseClass
	 * 	Overrides the default class to use for retrieving the classpath resource.
	 * 	<br>If <jk>null<jk>, uses the base class passed in through the constructor of this class.
	 * @param name Name of the desired resource.
	 * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
	 * @throws IOException Thrown by underlying stream.
	 */
	public String getString(Class<?> baseClass, String name) throws IOException {
		return getString(baseClass, name, null);
	}

	/**
	 * Finds the resource with the given name and converts it to a simple string.
	 *
	 * @param name Name of the desired resource.
	 * @param locale The locale.  Can be <jk>null</jk>.
	 * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
	 * @throws IOException Thrown by underlying stream.
	 */
	public String getString(String name, Locale locale) throws IOException {
		return getString(baseClass, name, locale);
	}

	/**
	 * Finds the resource with the given name and converts it to a simple string.
	 *
	 * @param baseClass
	 * 	Overrides the default class to use for retrieving the classpath resource.
	 * 	<br>If <jk>null<jk>, uses the base class passed in through the constructor of this class.
	 * @param name Name of the desired resource.
	 * @param locale The locale.  Can be <jk>null</jk>.
	 * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
	 * @throws IOException Thrown by underlying stream.
	 */
	public String getString(Class<?> baseClass, String name, Locale locale) throws IOException {

		if (baseClass == null)
			baseClass = this.baseClass;

		if (! useCache) {
			try (InputStream is = resourceFinder.findResource(baseClass, name, locale)) {
				return IOUtils.read(is, IOUtils.UTF8);
			}
		}

		ResourceKey key = new ResourceKey(name, locale);

		String r = stringCache.get(key);
		if (r == null) {
			try (InputStream is = resourceFinder.findResource(baseClass, name, locale)) {
				if (is != null)
					stringCache.putIfAbsent(key, IOUtils.read(is, IOUtils.UTF8));
			}
		}

		return stringCache.get(key);
	}

	private class ResourceKey {
		final String name;
		final Locale locale;

		ResourceKey(String name, Locale locale) {
			this.name = name;
			this.locale = locale;
		}

		@Override
		public int hashCode() {
			return name.hashCode() + (locale == null ? 0 : locale.hashCode());
		}

		@Override
		public boolean equals(Object o) {
			if (o == null)
				return false;
			ResourceKey ok = (ResourceKey)o;
			return ObjectUtils.equals(name, ok.name) && ObjectUtils.equals(locale, ok.locale);
		}
	}
}
