package de.is24.deadcode4j.analyzer.javassist;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import de.is24.deadcode4j.CodeContext;
import de.is24.deadcode4j.Repository;
import javassist.ClassPool;
import javassist.NotFoundException;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.util.Set;

import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.of;
import static com.google.common.collect.Sets.newHashSet;

public final class ClassPoolAccessor {
    @Nonnull
    private final ClassPool classPool;
    @Nonnull
    private final LoadingCache<String, Optional<String>> classResolver;
    @Nonnull
    private final Set<String> knownPackages = newHashSet();

    public ClassPoolAccessor(@Nonnull CodeContext codeContext) {
        this.classPool = createClassPool(codeContext);
        this.classResolver = createResolverCache();
    }

    @Nonnull
    public static ClassPoolAccessor classPoolAccessorFor(@Nonnull CodeContext codeContext) {
        ClassPoolAccessor classPoolAccessor = (ClassPoolAccessor) codeContext.getCache().get(ClassPoolAccessor.class);
        if (classPoolAccessor == null) {
            classPoolAccessor = new ClassPoolAccessor(codeContext);
            codeContext.getCache().put(ClassPoolAccessor.class, classPoolAccessor);
        }
        return classPoolAccessor;
    }

    /**
     * Returns the <code>ClassPool</code> used for examining classes.
     *
     * @since 1.6
     */
    @Nonnull
    public ClassPool getClassPool() {
        return this.classPool;
    }

    /**
     * Returns the "resolved" class name for the given qualifier.
     * "Resolved" in this case means that if the qualifier refers to an existing class, the class'
     * {@link java.lang.ClassLoader binary name} is returned.
     *
     * @since 1.6
     */
    @Nonnull
    public Optional<String> resolveClass(@Nonnull CharSequence qualifier) {
        return classResolver.getUnchecked(qualifier.toString());
    }

    @Nonnull
    private ClassPool createClassPool(CodeContext codeContext) {
        ClassPool classPool = new ClassPool(true);
        try {
            Repository outputRepository = codeContext.getModule().getOutputRepository();
            if (outputRepository != null) {
                classPool.appendClassPath(outputRepository.getDirectory().getAbsolutePath());
            }
            for (File file : codeContext.getModule().getClassPath()) {
                classPool.appendClassPath(file.getAbsolutePath());
            }
        } catch (NotFoundException e) {
            throw new RuntimeException("Failed to set up ClassPool!", e);
        }
        return classPool;
    }

    private LoadingCache<String, Optional<String>> createResolverCache() {
        return CacheBuilder.newBuilder().concurrencyLevel(1).build(CacheLoader.from(new Function<String, Optional<String>>() {
            @Nullable
            @Override
            public Optional<String> apply(@Nullable String input) {
                if (input == null) {
                    return absent();
                }
                for (; ; ) {
                    if (classPool.getOrNull(input) != null) {
                        addToKnownPackages(input);
                        return of(input);
                    }
                    int dotIndex = input.lastIndexOf('.');
                    if (dotIndex < 0) {
                        return absent();
                    }
                    String potentialPackage = input.substring(0, dotIndex);
                    if (knownPackages.contains(potentialPackage)) {
                        // no need to look for inner classes
                        return absent();
                    }
                    input = potentialPackage + "$" + input.substring(dotIndex + 1);
                }
            }
        }));
    }

    private void addToKnownPackages(String className) {
        for (; ; ) {
            int dotIndex = className.lastIndexOf('.');
            if (dotIndex < 0) {
                return;
            }
            className = className.substring(0, dotIndex);
            if (!knownPackages.add(className)) {
                return;
            }
        }

    }

}

@@ -849,81 +849,8 @@
 ver;
-%0A    @Nonnull%0A    private final Set%3CString%3E knownPackages = newHashSet();
 %0A%0A  
@@ -3015,24 +3015,114 @@
 tring%3E%3E() %7B%0A
+            @Nonnull%0A            private final Set%3CString%3E knownPackages = newHashSet();%0A%0A
             
@@ -4069,28 +4069,17 @@
   %7D%0A
- 
+%0A
        
-%7D));%0A    %7D%0A%0A
+ 
     
@@ -4110,16 +4110,25 @@
 ackages(
+@Nonnull 
 String c
@@ -4139,32 +4139,40 @@
 Name) %7B%0A        
+  
+      
 for (; ; ) %7B%0A   
@@ -4172,32 +4172,40 @@
 ) %7B%0A            
+  
+      
 int dotIndex = c
@@ -4235,32 +4235,40 @@
 ');%0A            
+        
 if (dotIndex %3C 0
@@ -4279,32 +4279,40 @@
                 
+  
+      
 return;%0A        
@@ -4307,34 +4307,44 @@
 rn;%0A            
-%7D%0A
+        %7D%0A  
             clas
@@ -4335,24 +4335,30 @@
             
+      
 className = 
@@ -4399,24 +4399,32 @@
             
+        
 if (!knownPa
@@ -4460,32 +4460,40 @@
             
+  
+      
 return;%0A
             
@@ -4472,32 +4472,73 @@
         return;%0A
+                    %7D%0A                %7D%0A%0A
             %7D%0A  
@@ -4527,32 +4527,33 @@
 %0A%0A            %7D%0A
+%0A
         %7D%0A%0A    %7D
@@ -4541,23 +4541,25 @@
 %7D%0A%0A        %7D
-%0A
+));
 %0A    %7D%0A%0A%7D%0A

clean code: keep things together