/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.cache;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.Adler32;
import java.util.zip.CheckedInputStream;

import org.apache.commons.io.IOUtils;

import net.sourceforge.pmd.PMDVersion;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.RuleSets;
import net.sourceforge.pmd.RuleViolation;
import net.sourceforge.pmd.stat.Metric;

/**
 * Abstract implementation of the analysis cache. Handles all operations, except for persistence.
 */
public abstract class AbstractAnalysisCache implements AnalysisCache {

    protected static final Logger LOG = Logger.getLogger(AbstractAnalysisCache.class.getName());
    protected final String pmdVersion;
    protected final ConcurrentMap<String, AnalysisResult> fileResultsCache;
    protected final ConcurrentMap<String, AnalysisResult> updatedResultsCache;
    protected long rulesetChecksum;
    protected long auxClassPathChecksum;
    protected long executionClassPathChecksum;
    protected final CachedRuleMapper ruleMapper = new CachedRuleMapper();
    
    /**
     * Creates a new empty cache
     */
    public AbstractAnalysisCache() {
        pmdVersion = PMDVersion.VERSION;
        fileResultsCache = new ConcurrentHashMap<>();
        updatedResultsCache = new ConcurrentHashMap<>();
    }

    @Override
    public boolean isUpToDate(final File sourceFile) {
        // There is a new file being analyzed, prepare entry in updated cache
        final AnalysisResult updatedResult = new AnalysisResult(sourceFile);
        updatedResultsCache.put(sourceFile.getPath(), updatedResult);
        
        // Now check the old cache
        final AnalysisResult analysisResult = fileResultsCache.get(sourceFile.getPath());
        
        // is this a known file? has it changed?
        final boolean result = analysisResult != null
                && analysisResult.getFileChecksum() == updatedResult.getFileChecksum();

        if (LOG.isLoggable(Level.FINE)) {
            if (result) {
                LOG.fine("Incremental Analysis cache HIT");
            } else {
                LOG.fine("Incremental Analysis cache MISS - "
                        + (analysisResult != null ? "file changed" : "no previous result found"));
            }
        }

        return result;
    }

    @Override
    public List<RuleViolation> getCachedViolations(final File sourceFile) {
        final AnalysisResult analysisResult = fileResultsCache.get(sourceFile.getPath());

        if (analysisResult == null) {
            // new file, avoid nulls
            return Collections.emptyList();
        }

        return analysisResult.getViolations();
    }

    @Override
    public void analysisFailed(final File sourceFile) {
        updatedResultsCache.remove(sourceFile.getPath());
    }

    @Override
    public void checkValidity(final RuleSets ruleSets, final ClassLoader auxclassPathClassLoader) {
        boolean cacheIsValid = true;

        if (ruleSets.getChecksum() != rulesetChecksum) {
            LOG.info("Analysis cache invalidated, rulesets changed.");
            cacheIsValid = false;
        }

        final long currentAuxClassPathChecksum;
        if (auxclassPathClassLoader instanceof URLClassLoader) {
            final URLClassLoader urlClassLoader = (URLClassLoader) auxclassPathClassLoader;
            currentAuxClassPathChecksum = computeClassPathHash(urlClassLoader.getURLs());
            
            if (cacheIsValid && currentAuxClassPathChecksum != auxClassPathChecksum) {
                // Do we even care?
                for (final Rule r : ruleSets.getAllRules()) {
                    if (r.isDfa() || r.isTypeResolution()) {
                        LOG.info("Analysis cache invalidated, auxclasspath changed.");
                        cacheIsValid = false;
                        break;
                    }
                }
            }
        } else {
            currentAuxClassPathChecksum = 0;
        }
        
        final long currentExecutionClassPathChecksum = computeClassPathHash(getClassPathEntries());
        if (currentExecutionClassPathChecksum != executionClassPathChecksum) {
            LOG.info("Analysis cache invalidated, execution classpath changed.");
            cacheIsValid = false;
        }

        if (!cacheIsValid) {
            // Clear the cache
            fileResultsCache.clear();
        }

        // Update the local checksums
        rulesetChecksum = ruleSets.getChecksum();
        auxClassPathChecksum = currentAuxClassPathChecksum;
        executionClassPathChecksum = currentExecutionClassPathChecksum;
        ruleMapper.initialize(ruleSets);
    }

    private URL[] getClassPathEntries() {
        final String classpath = System.getProperty("java.class.path");
        final String[] classpathEntries = classpath.split(File.pathSeparator);
        final List<URL> entries = new ArrayList<>();
        
        try {
            for (final String entry : classpathEntries) {
                final File f = new File(entry);
                if (f.isFile()) {
                    entries.add(f.toURI().toURL());
                } else {
                    Files.walkFileTree(f.toPath(), EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
                        new SimpleFileVisitor<Path>() {
                            @Override
                            public FileVisitResult visitFile(final Path file,
                                    final BasicFileAttributes attrs) throws IOException {
                                if (!attrs.isSymbolicLink()) { // Broken link that can't be followed
                                    entries.add(file.toUri().toURL());
                                }
                                return FileVisitResult.CONTINUE;
                            }
                        });
                }
            }
        } catch (final IOException e) {
            LOG.log(Level.SEVERE, "Incremental analysis can't check execution classpath contents", e);
            throw new RuntimeException(e);
        }
        
        return entries.toArray(new URL[0]);
    }

    private long computeClassPathHash(final URL... classpathEntry) {
        final Adler32 adler32 = new Adler32();
        for (final URL url : classpathEntry) {
            try (CheckedInputStream inputStream = new CheckedInputStream(url.openStream(), adler32)) {
                // Just read it, the CheckedInputStream will update the checksum on it's own
                while (IOUtils.skip(inputStream, Long.MAX_VALUE) == Long.MAX_VALUE) {
                    // just loop
                }
            } catch (final FileNotFoundException ignored) {
                LOG.warning("Auxclasspath entry " + url.toString() + " doesn't exist, ignoring it");
            } catch (final IOException e) {
                // Can this even happen?
                LOG.log(Level.SEVERE, "Incremental analysis can't check auxclasspath contents", e);
                throw new RuntimeException(e);
            }
        }
        return adler32.getValue();
    }

    @Override
    public void ruleViolationAdded(final RuleViolation ruleViolation) {
        final AnalysisResult analysisResult = updatedResultsCache.get(ruleViolation.getFilename());
        
        analysisResult.addViolation(ruleViolation);
    }

    @Override
    public void metricAdded(final Metric metric) {
        // Not interested in metrics
    }

}
