// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.util.cl;

import jodd.util.ArraysUtil;
import jodd.util.StringUtil;
import jodd.util.SystemUtil;
import jodd.util.Wildcard;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

/**
 * Class loader that offers two loading strategies: <b>parent-first</b> and <b>parent-last</b>.
 * The strategy defines the class loading order between this classloader and parent class loader;
 * i.e. defines the <i>first</i> class loader to be used for loading classes.
 * Extends <code>URLClassLoader</code> to provide just minimal set of modifications.
 * <p>
 * Either way, you can specify wildcard rules to force some behavior - like loading some package/class
 * exclusively with specific loader. There are the following groups:
 * <ul>
 *     <li>parent-only - classes will be loaded only with parent classloader.</li>
 *     <li>loader-only - classes will be loaded only with this loader.</li>
 *     <li>default - classes will be loaded as specified by strategy (no need to define).</li>
 * </ul>
 * <p>
 * The order of matching is also set by loading strategy. For <b>parent-first</b> strategy,
 * <b>loader</b> group rules will be checked first. For <b>parent-last</b> strategy,
 * <b>parent</b> group rules will be checked first.
 * <p>
 * By default, the list of <b>parent-only</b> group is populated with
 * {@link jodd.util.SystemUtil#jrePackages() JRE packages}.
 * <p>
 * When <b>parent-last</b> strategy is used, be aware how you use
 * {@link jodd.util.ClassLoaderUtil} as it is designed to follow <b>parent-first</b>
 * strategy. Use with caution as setting <b>parent-last</b> violates the
 * class loader hierarchy.
 */
public class ExtendedURLClassLoader extends URLClassLoader {

	protected final ClassLoader parentClassLoader;
	protected String[] parentOnlyRules;
	protected String[] loaderOnlyRules;
	protected final boolean parentFirst;
	protected boolean matchResourcesAsPackages = true;

	public ExtendedURLClassLoader(URL[] classpath, ClassLoader parent, boolean parentFirst) {
		this(classpath, parent, parentFirst, true);
	}

	/**
	 * Creates class loader with given loading strategy.
	 */
	public ExtendedURLClassLoader(
			URL[] classpath, ClassLoader parent,
			boolean parentFirst, boolean excludeJrePackagesFromLoader) {

		super(classpath, parent);

		this.parentFirst = parentFirst;

		if (parent == null) {
			throw new IllegalArgumentException("Parent classloader not specified");
		}
		parentClassLoader = parent;

		parentOnlyRules = new String[0];
		loaderOnlyRules = new String[0];

		if (excludeJrePackagesFromLoader) {
			String[] corePackages = SystemUtil.jrePackages();

			for (String corePackage : corePackages) {
				if (corePackage.equals("javax")) {
					// javax is NOT forbidden
					continue;
				}

				addParentOnlyRules(corePackage + ".*");
			}
		}
	}

	// ---------------------------------------------------------------- rules

	/**
	 * Adds parent only rules for classes which must be loaded on the
	 * parent loader.
	 */
	public void addParentOnlyRules(String... packages) {
		parentOnlyRules = ArraysUtil.join(parentOnlyRules, packages);
	}

	/**
	 * Adds loader-only rules for classes which must be loaded using this
	 * loader.
	 */
	public void addLoaderOnlyRules(String... packages) {
		loaderOnlyRules = ArraysUtil.join(loaderOnlyRules, packages);
	}

	/**
	 * When set, resources will be matched in the same way as packages.
	 * If disabled, resources must be matched only with separate rules
	 * that uses "/".
	 */
	public void setMatchResourcesAsPackages(boolean matchResourcesAsPackages) {
		this.matchResourcesAsPackages = matchResourcesAsPackages;
	}

	/**
	 * Returns <code>true</code> if class or resource name matches
	 * at least one package rule from the list.
	 */
	protected boolean isMatchingRules(String name, String... rules) {
		for (String rule : rules) {
			if (Wildcard.equalsOrMatch(name, rule)) {
				return true;
			}
		}
		return false;
	}

	// ---------------------------------------------------------------- resolver

	protected static class Loading {
		public Loading(boolean withParent, boolean withLoader) {
			this.withParent = withParent;
			this.withLoader = withLoader;
		}

		protected final boolean withParent;
		protected final boolean withLoader;
	}

	/**
	 * Resolves loading rules.
	 */
	protected Loading resolveLoading(boolean parentFirstStrategy, String className) {
		boolean withParent = true;
		boolean withLoader = true;

		if (parentFirstStrategy) {
			if (isMatchingRules(className, loaderOnlyRules)) {
				withParent = false;
			}
			else if (isMatchingRules(className, parentOnlyRules)) {
				withLoader = false;
			}
		}
		else {
			if (isMatchingRules(className, parentOnlyRules)) {
				withLoader = false;
			}
			else if (isMatchingRules(className, loaderOnlyRules)) {
				withParent = false;
			}
		}

		return new Loading(withParent, withLoader);
	}

	/**
	 * Resolves resources.
	 */
	protected Loading resolveResourceLoading(boolean parentFirstStrategy, String resourceName) {
		if (matchResourcesAsPackages) {
			resourceName = StringUtil.replaceChar(resourceName, '/', '.');
		}

		return resolveLoading(parentFirstStrategy, resourceName);
	}


	// ---------------------------------------------------------------- overrides

	/**
	 * Loads class using parent-first or parent-last strategy.
	 */
	@Override
	protected synchronized Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {

		// check first if the class has already been loaded

		Class<?> c = findLoadedClass(className);

		if (c != null) {
			if (resolve) {
				resolveClass(c);
			}

			return c;
		}

		// class not loaded yet

		Loading loading = resolveLoading(parentFirst, className);

		if (parentFirst) {
			// PARENT FIRST
			if (loading.withParent) {
				try {
					c = parentClassLoader.loadClass(className);
				}
				catch (ClassNotFoundException ignore) {
				}
			}

			if (c == null) {
				if (loading.withLoader) {
					c = this.findClass(className);
				}
				else {
					throw new ClassNotFoundException("Class not found: " + className);
				}
			}
		} else {
			// THIS FIRST
			if (loading.withLoader) {
				try {
					c = this.findClass(className);
				}
				catch (ClassNotFoundException ignore) {
				}
			}

			if (c == null) {
				if (loading.withParent) {
					c = parentClassLoader.loadClass(className);
				}
				else {
					throw new ClassNotFoundException("Class not found: " + className);
				}
			}
		}

		if (resolve) {
			resolveClass(c);
		}
		return c;
	}

	/**
	 * Returns a resource using parent-first or parent-last strategy.
	 */
	@Override
	public URL getResource(String resourceName) {

		URL url = null;

		Loading loading = resolveResourceLoading(parentFirst, resourceName);

		if (parentFirst) {
			// PARENT FIRST
			if (loading.withParent) {
				url = parentClassLoader.getResource(resourceName);
			}

			if (url == null) {
				if (loading.withLoader) {
					url = this.findResource(resourceName);
				}
			}
		} else {
			// THIS FIRST
			if (loading.withLoader) {
				url = this.findResource(resourceName);
			}

			if (url == null) {
				if (loading.withParent) {
					url = parentClassLoader.getResource(resourceName);
				}
			}
		}

		return url;
	}

	@Override
	public Enumeration<URL> getResources(String resourceName) throws IOException {

		final List<URL> urls = new ArrayList<>();

		Enumeration<URL> loaderUrls = this.findResources(resourceName);
		Enumeration<URL> parentUrls = parentClassLoader.getResources(resourceName);

		Loading loading = resolveResourceLoading(parentFirst, resourceName);

		if (parentFirst) {
			if (loading.withParent) {
				while (parentUrls.hasMoreElements()) {
					urls.add(parentUrls.nextElement());
				}
			}
			if (loading.withLoader) {
				while (loaderUrls.hasMoreElements()) {
					urls.add(loaderUrls.nextElement());
				}
			}
		}
		else {
			if (loading.withLoader) {
				while (loaderUrls.hasMoreElements()) {
					urls.add(loaderUrls.nextElement());
				}
			}
			if (loading.withParent) {
				while (parentUrls.hasMoreElements()) {
					urls.add(parentUrls.nextElement());
				}
			}
		}
		return new Enumeration<URL>() {
			Iterator<URL> iterator = urls.iterator();

			public boolean hasMoreElements() {
				return iterator.hasNext();
			}

			public URL nextElement() {
				return iterator.next();
			}
		};
	}

}