package com.badlogic.gdx.lang;

import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.function.Consumer;
import com.badlogic.gdx.function.Predicate;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * Utility class to filter and iterate classes registered to a {@link ClassLoader}.
 */
public class ClassFinder {

	private Array<URL> urls = new Array<>();

	/**
	 * Only uses the same URL as the given class was loaded from.
	 * <p>
	 * Must be called before {@link ClassFinder#process(Predicate, Consumer)}.
	 */
	public ClassFinder filterURLforClass(Class<?> clazz) {

		// todo: this may not work because of SecurityManager
		URL url = clazz.getProtectionDomain().getCodeSource().getLocation();
		urls.add(url);

		return this;
	}

	/**
	 * Iterates classes of all URLs filtered by a previous call of {@link ClassFinder#filterURLforClass(Class)}.
	 */
	public ClassFinder process(Predicate<String> filter, Consumer<Class<?>> processor) {

		for (URL url : urls) {

			Path path;

			// required for Windows paths with spaces
			try {
				path = Paths.get(url.toURI());
			} catch (URISyntaxException e) {
				throw new GdxRuntimeException(e);
			}

			FileHandle file = new FileHandle(path.toFile());

			if (file.isDirectory()) {

				processDirectory(file, file, filter, processor);

			} else if (file.extension().equals("jar")) {

				try (JarFile jar = new JarFile(file.file())) {

					Enumeration<JarEntry> entries = jar.entries();

					while (entries.hasMoreElements()) {

						JarEntry entry = entries.nextElement();

						if (entry.isDirectory()) {
							continue;
						}

						FileHandle entryFile = new FileHandle(entry.getName());

						process(null, entryFile, filter, processor);
					}

				} catch (IOException ignored) {

				}
			}

		}

		return this;
	}

	/**
	 * Iterates classes from a {@link ClassFinderCache}.
	 * <p>
	 * This is faster than walking URLs/directories. It's also more secure/portable.
	 */
	public ClassFinder process(ClassFinderCache cache, String groupName,
							   Predicate<String> filter, Consumer<Class<?>> processor) {

		Array<String> classNames = cache.get(groupName);

		if (classNames == null) {
			return this;
		}

		for (String className : classNames) {
			processClass(className, filter, processor);
		}

		return this;
	}

	private void processDirectory(FileHandle root, FileHandle directory,
								  Predicate<String> filter, Consumer<Class<?>> processor) {

		FileHandle[] files = directory.list();

		for (FileHandle file : files) {

			if (file.isDirectory()) {
				processDirectory(root, new FileHandle(new File(directory.file(), file.name())), filter, processor);
			} else {
				process(root, file, filter, processor);
			}

		}
	}

	private void process(FileHandle root, FileHandle file, Predicate<String> filter, Consumer<Class<?>> processor) {

		if (!file.extension().equals("class")) {
			return;
		}

		FileHandle relative = relativeTo(file, root);

		String className = relative.pathWithoutExtension()
				.replace("/", ".");//.replaceAll("\\$.", "");

		processClass(className, filter, processor);
	}

	private void processClass(String className, Predicate<String> filter, Consumer<Class<?>> processor) {

		if (!filter.test(className)) {
			return;
		}

		try {

			Class<?> clazz = Class.forName(className);
			processClass(clazz, filter, processor);

		} catch (ClassNotFoundException ignored) {

		}

	}

	private void processClass(Class<?> clazz, Predicate<String> filter, Consumer<Class<?>> processor) {

		if (!filter.test(clazz.getName())) {
			return;
		}

		processor.accept(clazz);

		for (Class<?> inner : clazz.getDeclaredClasses()) {
			processClass(inner, filter, processor);
		}

	}

	private FileHandle relativeTo(FileHandle file, FileHandle root) {

		if (root == null) {
			return file;
		}

		String r = root.path();
		String f = file.path();

		if (!f.startsWith(r)) {
			return file;
		}

		return new FileHandle(f.substring(r.length() + 1));
	}
}