package org.jboss.windup.rules.apps.java.scan.ast;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import org.apache.commons.collections.map.LRUMap;
import org.jboss.windup.ast.java.data.TypeReferenceLocation;
import org.jboss.windup.util.ExecutionStatistics;
import org.jboss.windup.util.Logging;

/**
 * Static store for type interest information. E.g. Which classes to scan and report on.
 *
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
public final class TypeInterestFactory {
    private static final Logger LOG = Logging.get(TypeInterestFactory.class);

    // Keep track of each pattern, as well as an identifier of who gave the pattern to us (so that we can update it)
    private static final Map<String, PatternAndLocation> patternsBySource = new HashMap<>();

    // The full list of patterns, organized by location (including null for the case of no location specified)
    private static final Map<TypeReferenceLocation, TypeInterestFactoryTrie> trieByLocation = new HashMap<>();

    private static final Set<String> ignorePatternSet = Collections.synchronizedSet(new HashSet<String>());

    static {
        ignorePatternSet.add("void");
        ignorePatternSet.add("String");
        ignorePatternSet.add("java.lang.String");
        ignorePatternSet.add("boolean");
        ignorePatternSet.add("Boolean");
        ignorePatternSet.add("java.lang.Boolean");
        ignorePatternSet.add("int");
        ignorePatternSet.add("Integer");
        ignorePatternSet.add("java.lang.Integer");
        ignorePatternSet.add("long");
        ignorePatternSet.add("Long");
        ignorePatternSet.add("java.lang.Long");
        ignorePatternSet.add("double");
        ignorePatternSet.add("Double");
        ignorePatternSet.add("java.lang.Double");
        ignorePatternSet.add("float");
        ignorePatternSet.add("Float");
        ignorePatternSet.add("java.lang.Float");
    }

    // cache these lookups in an LRU cache, as there are frequent duplicates (and the regex comparisons are much slower
    // than a cache lookup)
    private static final Map<String, Boolean> resultsCache = Collections.synchronizedMap(new LRUMap(8000));
    private static final AtomicLong cacheLookupCount = new AtomicLong(0);
    private static final AtomicLong cacheHitCount = new AtomicLong(0);

    // cache the words from the patterns and use this to filter out obvious non-matches very quickly
    private static final Set<String> prescanMap = new TreeSet<>();
    private static final AtomicInteger totalPrescans = new AtomicInteger();
    private static final AtomicInteger totalPrescanHits = new AtomicInteger();

    static void clear() {
        patternsBySource.clear();
        trieByLocation.clear();
        resultsCache.clear();
        cacheLookupCount.set(0);
        cacheHitCount.set(0);
        prescanMap.clear();
        totalPrescans.set(0);
        totalPrescanHits.set(0);
    }

    /**
     * Register a regex pattern to filter interest in certain Java types.
     *
     * @param sourceKey Identifier of who gave the pattern to us (so that we can update it)
     */
    public static void registerInterest(String sourceKey, String regex, String rewritePattern, List<TypeReferenceLocation> locations) {
        registerInterest(sourceKey, regex, rewritePattern, locations.toArray(new TypeReferenceLocation[locations.size()]));
    }

    /**
     * Register a regex pattern to filter interest in certain Java types.
     *
     * @param sourceKey Identifier of who gave the pattern to us (so that we can update it).
     *                  This can be any arbitrary string.
     */
    public static void registerInterest(String sourceKey, String regex, String rewritePattern, TypeReferenceLocation... locations) {
        patternsBySource.put(sourceKey, new PatternAndLocation(locations, regex, rewritePattern));
    }

    private static String getCacheKey(TypeReferenceLocation location, String text) {
        return location + "_" + text;
    }

    private static Boolean checkCacheForMatches(String inputText, TypeReferenceLocation location) {
        String key = getCacheKey(location, inputText);
        Boolean cachedResult = (Boolean) resultsCache.get(key);
        Long lookupCount = cacheLookupCount.incrementAndGet();
        Long hitCount = cacheHitCount.get();
        if (cachedResult != null) {
            hitCount = cacheHitCount.incrementAndGet();
        }
        if (lookupCount % 100000L == 0) {
            long hitPercentage = Math.round(((double) hitCount / (double) lookupCount) * 100);
            LOG.fine("There have been " + lookupCount + " lookups with " + hitCount
                    + " hits, for a hit percentage of: " + hitPercentage);
        }
        return cachedResult;
    }

    private static TypeInterestFactoryTrie getTrieByLocation(TypeReferenceLocation typeReferenceLocation) {
        TypeInterestFactoryTrie result = trieByLocation.get(typeReferenceLocation);
        if (result == null) {
            result = TypeInterestFactoryTrie.newDefaultInstance();
            for (PatternAndLocation patternKey : patternsBySource.values()) {
                String entryRegex = patternKey.regex;
                TypeReferenceLocation[] entryLocations = patternKey.locations;
                String rewritePattern = patternKey.rewritePattern;
                /*if (result.containsKey(entryRegex))
                {
                TODO
                    continue;
                }*/

                boolean shouldAdd = false;
                if (entryLocations == null || entryLocations.length == 0) {
                    shouldAdd = true;
                } else {
                    for (TypeReferenceLocation entryLocation : entryLocations) {
                        if (typeReferenceLocation.equals(entryLocation)) {
                            shouldAdd = true;
                            break;
                        }
                    }
                }

                if (shouldAdd) {
                    result.addInterest(new RewritePatternToRegex(rewritePattern, Pattern.compile(entryRegex)));
                }
            }
            trieByLocation.put(typeReferenceLocation, result);
        }
        return result;
    }

    public static boolean matchesAny(String text, TypeReferenceLocation typeReferenceLocation) {
        ExecutionStatistics.get().begin("TypeInterestFactory.matchesAny(text)");
        synchronized (prescanMap) {
            if (prescanMap.isEmpty()) {
                for (PatternAndLocation patternKey : patternsBySource.values()) {
                    String pattern = patternKey.rewritePattern;
                    pattern = replaceRegexWithDot(pattern);
                    StringTokenizer stk = new StringTokenizer(pattern, ".");
                    while (stk.hasMoreTokens()) {
                        prescanMap.add(stk.nextToken());
                    }
                }
            }
        }

        StringTokenizer stk = new StringTokenizer(text, ".");
        boolean foundPotentialMatch = false;
        totalPrescans.incrementAndGet();
        while (stk.hasMoreTokens()) {
            if (prescanMap.contains(stk.nextToken())) {
                foundPotentialMatch = true;
                break;
            }
        }

        if (totalPrescans.get() % 25000 == 0) {
            int perc = (int) (((double) totalPrescanHits.get() / (double) totalPrescans.get()) * 100);
            LOG.fine("Prescan hit ratio " + totalPrescanHits.get() + " / " + totalPrescans.get() + "; " + perc + "%");
        }

        if (!foundPotentialMatch) {
            totalPrescanHits.incrementAndGet();
            return false;
        }

        try {
            if (ignorePatternSet.contains(text)) {
                return false;
            }
            ExecutionStatistics.get().begin("TypeInterestFactory.matchesAny(text).cacheCheck");
            try {
                Boolean cachedResult = checkCacheForMatches(text, typeReferenceLocation);
                if (cachedResult != null) {
                    return cachedResult;
                }
            } finally {
                ExecutionStatistics.get().end("TypeInterestFactory.matchesAny(text).cacheCheck");
            }

            ExecutionStatistics.get().begin("TypeInterestFactory.matchesAny(text).manualSearch");
            try {
                //TODO: Improve this with trie structure for every location
                if (getTrieByLocation(typeReferenceLocation).matches(text)) {
                    resultsCache.put(text, true);
                    return true;
                }
                resultsCache.put(getCacheKey(typeReferenceLocation, text), false);
                return false;
            } finally {
                ExecutionStatistics.get().end("TypeInterestFactory.matchesAny(text).manualSearch");
            }
        } finally {
            ExecutionStatistics.get().end("TypeInterestFactory.matchesAny(text)");
        }
    }

    private static String replaceRegexWithDot(String input) {
        return input.replaceAll("\\{.*\\}", ".");
    }

    private static class PatternAndLocation {
        private TypeReferenceLocation[] locations;
        private String regex;
        private String rewritePattern;

        private PatternAndLocation(TypeReferenceLocation[] locations, String regex, String rewritePattern) {
            this.locations = locations;
            this.regex = regex;
            this.rewritePattern = rewritePattern;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + Arrays.hashCode(locations);
            result = prime * result + ((regex == null) ? 0 : regex.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            PatternAndLocation other = (PatternAndLocation) obj;
            if (!Arrays.equals(locations, other.locations))
                return false;
            if (regex == null) {
                if (other.regex != null)
                    return false;
            } else if (!regex.equals(other.regex))
                return false;
            return true;
        }
    }

}