/*
 * Copyright 2011 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.codenarc.ant;

import org.apache.log4j.Logger;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.FileSet;
import org.codenarc.analyzer.AbstractSourceAnalyzer;
import org.codenarc.results.DirectoryResults;
import org.codenarc.results.FileResults;
import org.codenarc.results.Results;
import org.codenarc.rule.Violation;
import org.codenarc.ruleset.RuleSet;
import org.codenarc.source.SourceFile;
import org.codenarc.util.PathUtil;

import java.io.File;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * SourceAnalyzer implementation that gets source files from one or more Ant FileSets.
 *
 * @author Chris Mair
 */
public class AntFileSetSourceAnalyzer extends AbstractSourceAnalyzer {

    private static final Logger LOG = Logger.getLogger(AntFileSetSourceAnalyzer.class);
    private static final int POOL_TIMEOUT_SECONDS = 60 * 60;

    private final Project project;
    protected final List<FileSet> fileSets;

    // Concurrent shared state
    private final ConcurrentMap<String, List<FileResults>> resultsMap = new ConcurrentHashMap<String, List<FileResults>>();
    private final ConcurrentMap<String, AtomicInteger> fileCountMap = new ConcurrentHashMap<String, AtomicInteger>();

    /**
     * Construct a new instance on the specified Ant FileSet.
     *
     * @param project - the Ant Project; must not be null
     * @param fileSet - the Ant FileSet; must not be null
     */
    public AntFileSetSourceAnalyzer(Project project, FileSet fileSet) {
        if (fileSet == null) {
            throw new IllegalArgumentException("Null: fileSet");
        }
        if (project == null) {
            throw new IllegalArgumentException("Null: project");
        }
        this.project = project;
        this.fileSets = Arrays.asList(fileSet);
    }

    /**
     * Construct a new instance on the specified List of Ant FileSets.
     *
     * @param project  - the Ant Project
     * @param fileSets - the List of Ant FileSet; my be empty; must not be null
     */
    AntFileSetSourceAnalyzer(Project project, List<FileSet> fileSets) {
        if (fileSets == null) {
            throw new IllegalArgumentException("Null: fileSets");
        }
        if (project == null) {
            throw new IllegalArgumentException("Null: project");
        }

        this.project = project;
        this.fileSets = new ArrayList<FileSet>(fileSets);
    }

    /**
     * Analyze all source code using the specified RuleSet and return the report results.
     *
     * @param ruleSet - the RuleSet to apply to each source component; must not be null.
     * @return the results from applying the RuleSet to all of the source
     */
    public Results analyze(RuleSet ruleSet) {
        long startTime = System.currentTimeMillis();
        DirectoryResults reportResults = new DirectoryResults();

        int numThreads = Runtime.getRuntime().availableProcessors() + 1;
        ExecutorService pool = Executors.newFixedThreadPool(numThreads);

        for (FileSet fileSet : fileSets) {
            processFileSet(fileSet, ruleSet, pool);
        }

        pool.shutdown();

        try {
            boolean completed = pool.awaitTermination(POOL_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            if (!completed) {
                throw new IllegalStateException("Thread Pool terminated before comp<FileResults>letion");
            }
        } catch (InterruptedException e) {
            throw new IllegalStateException("Thread Pool interrupted before completion");
        }

        addDirectoryResults(reportResults);
        LOG.info("Analysis time=" + (System.currentTimeMillis() - startTime) + "ms");
        return reportResults;
    }

    public List getSourceDirectories() {
        String baseDir = project.getBaseDir().getAbsolutePath();

        List<String> result = new ArrayList<String>();
        for (FileSet fileSet : fileSets) {
            String path = fileSet.getDir(project).getPath();
            String trimmedPath = PathUtil.removePathPrefix(baseDir, path);
            result.add(trimmedPath);
        }
        return result;
    }

    //--------------------------------------------------------------------------
    // Internal Helper Methods
    //--------------------------------------------------------------------------

    private void processFileSet(FileSet fileSet, RuleSet ruleSet, ExecutorService pool) {
        DirectoryScanner dirScanner = fileSet.getDirectoryScanner(project);
        File baseDir = fileSet.getDir(project);
        String[] includedFiles = dirScanner.getIncludedFiles();

        if (includedFiles == null || includedFiles.length == 0) {
            LOG.info("No matching files found for FileSet with basedir [" + baseDir + "]");
            return;
        }

        for (String filePath : includedFiles) {
            Runnable task = buildTask(baseDir, filePath, ruleSet);
            pool.submit(task);
        }
    }

    private Runnable buildTask(final File baseDir, final String filePath, final RuleSet ruleSet) {
        return new Runnable() {
            public void run() {
                try {
                    processFile(baseDir, filePath, ruleSet);
                } catch (Throwable t) {
                    LOG.info("Error processing filePath: '" + filePath + "'", t);
                }
            }
        };
    }

    private void processFile(File baseDir, String filePath, RuleSet ruleSet) {
        File file = new File(baseDir, filePath);
        SourceFile sourceFile = new SourceFile(file);
        List<Violation> allViolations = collectViolations(sourceFile, ruleSet);
        FileResults fileResults = null;
        if (allViolations != null && !allViolations.isEmpty()) {
            fileResults = new FileResults(PathUtil.normalizePath(filePath), allViolations);
        }
        String parentPath = PathUtil.getParentPath(filePath);
        String safeParentPath = parentPath != null ? parentPath : "";
        addToResultsMap(safeParentPath, fileResults);
        incrementFileCount(safeParentPath);
    }

    private void incrementFileCount(String parentPath) {
        AtomicInteger initialZeroCount = new AtomicInteger(0);
        fileCountMap.putIfAbsent(parentPath, initialZeroCount);
        AtomicInteger fileCount = fileCountMap.get(parentPath);
        fileCount.incrementAndGet();
    }

    private void addToResultsMap(String parentPath, FileResults results) {
        List<FileResults> initialEmptyResults = Collections.synchronizedList(new ArrayList<FileResults>());
        resultsMap.putIfAbsent(parentPath, initialEmptyResults);

        if (results != null) {
            List<FileResults> dirResults = resultsMap.get(parentPath);
            dirResults.add(results);
        }
    }

    private void addToParentResults(DirectoryResults reportResults, Results results) {
        String parentPath = PathUtil.getParentPath(results.getPath());
        if (parentPath == null) {
            reportResults.addChild(results);
            return;
        }
        DirectoryResults parent = (DirectoryResults) reportResults.findResultsForPath(parentPath);
        if (parent == null) {
            parent = new DirectoryResults(parentPath);
            addToParentResults(reportResults, parent);
        }
        parent.addChild(results);
    }

    private void addDirectoryResults(DirectoryResults reportResults) {
        Set<String> set = resultsMap.keySet();
        ArrayList<String> allPaths = new ArrayList<String>(set);
        Collections.sort(allPaths);

        for (String path : allPaths) {
            DirectoryResults dirResults = new DirectoryResults(path);
            List<FileResults> allResults = resultsMap.get(path);
            Collections.sort(allResults, new Comparator<FileResults>() {
                public int compare(FileResults o1, FileResults o2) {
                    return o1.getPath().compareTo(o2.getPath());
                }
            });

            for (FileResults child : allResults) {
                dirResults.addChild(child);
            }
            AtomicInteger cnt = fileCountMap.get(path);
            dirResults.setNumberOfFilesInThisDirectory(cnt != null ? cnt.get() : 0);
            addToParentResults(reportResults, dirResults);
        }
    }
}