package de.is24.deadcode4j.analyzer.javassist;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.cache.LoadingCache;
import de.is24.deadcode4j.AnalysisContext;
import de.is24.deadcode4j.Repository;
import de.is24.guava.NonNullFunction;
import de.is24.guava.SequentialLoadingCache;
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;

/**
 * The <code>ClassPoolAccessor</code> provides access to a Javassist {@link javassist.ClassPool} with fully configured
 * class path. It also provides some convenience methods to deal with loading & resolving classes.
 *
 * @since 1.6
 */
public final class ClassPoolAccessor {
    @Nonnull
    private static final NonNullFunction<AnalysisContext, ClassPoolAccessor> SUPPLIER = new NonNullFunction<AnalysisContext, ClassPoolAccessor>() {
        @Nonnull
        @Override
        public ClassPoolAccessor apply(@Nonnull AnalysisContext input) {
            return new ClassPoolAccessor(input);
        }
    };
    @Nonnull
    private final ClassPool classPool;
    @Nonnull
    private final LoadingCache<String, Optional<String>> classResolver;

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

    /**
     * Creates or retrieves the <code>ClassPoolAccessor</code> for the given analysis context.<br/>
     * A new instance will be put in the analysis context's cache and subsequently retrieved from there.
     *
     * @since 1.6
     */
    @Nonnull
    public static ClassPoolAccessor classPoolAccessorFor(@Nonnull AnalysisContext analysisContext) {
        return analysisContext.getOrCreateCacheEntry(ClassPoolAccessor.class, SUPPLIER);
    }

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

    /**
     * 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 LoadingCache<String, Optional<String>> createResolverCache() {
        return new SequentialLoadingCache<String, String>(new Function<String, Optional<String>>() {
            @Nonnull
            private final Set<String> knownPackages = newHashSet();

            @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(@Nonnull String className) {
                for (; ; ) {
                    int dotIndex = className.lastIndexOf('.');
                    if (dotIndex < 0) {
                        return;
                    }
                    className = className.substring(0, dotIndex);
                    if (!knownPackages.add(className)) {
                        return;
                    }
                }

            }

        });
    }

}

@@ -2778,32 +2778,589 @@
 assPool;%0A    %7D%0A%0A
+    private static String prepareQualifier(CharSequence qualifier) %7B%0A        String preparedQualifier = qualifier.toString();%0A        for (; ; ) %7B%0A            int lastDot = preparedQualifier.lastIndexOf('.');%0A            int lastDollar = preparedQualifier.lastIndexOf('$');%0A            if (lastDollar %3C 0 %7C%7C lastDot %3C 0 %7C%7C lastDollar %3E lastDot) %7B%0A                break;%0A            %7D%0A            preparedQualifier = preparedQualifier.substring(0, lastDot) + %22$%22 + preparedQualifier.substring(lastDot + 1);%0A        %7D%0A        return preparedQualifier;%0A    %7D%0A%0A
     /**%0A     * R
@@ -3942,35 +3942,42 @@
 checked(
-qualifier.toString(
+prepareQualifier(qualifier
 ));%0A    

correct inner class qualifier from the start