/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.reflection.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Set;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import net.ranides.assira.collection.iterators.ForwardSpliterator;
import net.ranides.assira.text.StringUtils;
import net.ranides.assira.io.uri.URIUtils;
import net.ranides.assira.trace.ExceptionUtils;


public final class PackageScanner {

    private PackageScanner() {
        /* utility class */
    }
    
    public static Set<Class<?>> getTypes(String root) throws IOException {
        return getTypes(root, ClassUtils.loader());
    }
    
    public static Set<Class<?>> getTypes(String root, ClassLoader loader) throws IOException {
        return getTypeStream(root, loader).collect(Collectors.toSet());
    }
    
    public static Stream<Class<?>> getTypeStream(String root) throws IOException {
        return getTypeStream(root, ClassUtils.loader());
    }
    
    public static Stream<Class<?>> getTypeStream(String root, ClassLoader loader) throws IOException {
        return getNameStream(root, loader).map(toclass(loader));
    }
    
    public static Set<String> getNames(String root) throws IOException {
        return getNames(root, ClassUtils.loader());
    }
    
    public static Set<String> getNames(String root, ClassLoader loader) throws IOException {
        return getNameStream(root, loader).collect(Collectors.toSet());
    }
    
    public static Stream<String> getNameStream(String root) throws IOException {
        return getNameStream(root, ClassUtils.loader());
    }
    
    public static Stream<String> getNameStream(String root, ClassLoader loader) throws IOException {
        return StreamSupport.stream(new PackageSpliterator(root, loader), false);
    }
    
    private static final class PackageSpliterator extends ForwardSpliterator<String> {
		
        private final String nroot;
        private final String croot;
        private final Enumeration<URL> resources;
        
        private Spliterator<String> current;
		
		public PackageSpliterator(String root, ClassLoader loader) throws IOException {
			this.nroot = root + ".";
			this.croot = StringUtils.replace(root, '.', '/');
            this.resources =  loader.getResources(croot);
            this.current = Collections.<String>emptyList().spliterator();
		}

		@Override
		public boolean tryAdvance(Consumer<? super String> action) {
            return current.tryAdvance(action) || tryAdvanceResource(action);
		}
        
        private boolean tryAdvanceResource(Consumer<? super String> action) {
            if(!resources.hasMoreElements()) {
                return false;
            }
            String str = resources.nextElement().getPath();
            String uri = URIUtils.decodeURL(str, Charset.defaultCharset());
            File file = new File(uri);
            current = file.isDirectory() ? forDirectory(file.toPath()) : forJAR(uri);
            return current.tryAdvance(action);
        }
        
        private Spliterator<String> forJAR(String uri) {
            try {
                String jar = StringUtils.between(uri, ":", "!");
                return new JARSpliterator(new JarInputStream(new FileInputStream(jar)));
            } catch (IOException cause) {
                throw ExceptionUtils.rethrow(cause);
            }
        }
        
        private Spliterator<String> forDirectory(Path file) {
            try {
                return Files
                    .walk(file)
                    .filter(PackageScanner::isclass)
                    .map(s -> nroot + tocname(file.relativize(s)))
                    .spliterator();
            } catch(IOException cause) {
                throw ExceptionUtils.rethrow(cause);
            }
        }
        
        private final class JARSpliterator extends ForwardSpliterator<String> {

            private final JarInputStream istream;

            public JARSpliterator(JarInputStream istream) throws IOException {
                this.istream = istream;
            }

            @Override
            public boolean tryAdvance(Consumer<? super String> action) {
                try {
                    JarEntry entry;
                    while(null != (entry = istream.getNextJarEntry())) {
                        String ename = entry.getName();
                        if( ename.endsWith(".class") && ename.startsWith(croot) ) {
                            action.accept(tocname(ename));
                            return true;
                        }
                    }
                    istream.close();
                    return false;
                } catch(IOException cause) {
                    throw ExceptionUtils.rethrow(cause);
                }
            }

        }

	}

    private static boolean isclass(Path path) {
        return path.getFileName().toString().endsWith(".class");
    }
    
    private static Function<String, Class<?>> toclass(ClassLoader loader) {
        return name -> {
            try {
                return Class.forName(name, false, loader);
            } catch(ClassNotFoundException cause) {
                throw ExceptionUtils.rethrow(cause);
            }
        };
    }
    
    private static String tocname(Path path) {
        return tocname(path.toString());
    }
    
    private static String tocname(String path) {
        String fname = StringUtils.removeSuffix(path, ".class");
        return StringUtils.replace(StringUtils.replace(fname, '/', '.'), '\\', '.');
    }

}
