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

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Handler;
import java.util.logging.Level;

import org.apache.commons.io.IOUtils;
import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;

import net.sourceforge.pmd.PMD;
import net.sourceforge.pmd.PMDConfiguration;
import net.sourceforge.pmd.Report;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.RulePriority;
import net.sourceforge.pmd.RuleSet;
import net.sourceforge.pmd.RuleSetFactory;
import net.sourceforge.pmd.RuleSetNotFoundException;
import net.sourceforge.pmd.RuleSets;
import net.sourceforge.pmd.ant.Formatter;
import net.sourceforge.pmd.ant.PMDTask;
import net.sourceforge.pmd.ant.SourceLanguage;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.renderers.AbstractRenderer;
import net.sourceforge.pmd.renderers.Renderer;
import net.sourceforge.pmd.util.IOUtil;
import net.sourceforge.pmd.util.StringUtil;
import net.sourceforge.pmd.util.datasource.DataSource;
import net.sourceforge.pmd.util.datasource.FileDataSource;
import net.sourceforge.pmd.util.log.AntLogHandler;
import net.sourceforge.pmd.util.log.ScopedLogHandlersManager;

public class PMDTaskImpl {

    private Path classpath;
    private Path auxClasspath;
    private final List<Formatter> formatters = new ArrayList<>();
    private final List<FileSet> filesets = new ArrayList<>();
    private final PMDConfiguration configuration = new PMDConfiguration();
    private boolean failOnError;
    private boolean failOnRuleViolation;
    private int maxRuleViolations = 0;
    private String failuresPropertyName;
    private Project project;

    public PMDTaskImpl(PMDTask task) {
        configuration.setReportShortNames(task.isShortFilenames());
        configuration.setSuppressMarker(task.getSuppressMarker());
        this.failOnError = task.isFailOnError();
        this.failOnRuleViolation = task.isFailOnRuleViolation();
        this.maxRuleViolations = task.getMaxRuleViolations();
        if (this.maxRuleViolations > 0) {
            this.failOnRuleViolation = true;
        }
        configuration.setRuleSets(task.getRulesetFiles());
        configuration.setRuleSetFactoryCompatibilityEnabled(!task.isNoRuleSetCompatibility());
        if (task.getEncoding() != null) {
            configuration.setSourceEncoding(task.getEncoding());
        }
        configuration.setThreads(task.getThreads());
        this.failuresPropertyName = task.getFailuresPropertyName();
        configuration.setMinimumPriority(RulePriority.valueOf(task.getMinimumPriority()));

        SourceLanguage version = task.getSourceLanguage();
        if (version != null) {
            LanguageVersion languageVersion = LanguageRegistry.findLanguageVersionByTerseName(version.getName() + " "
                    + version.getVersion());
            if (languageVersion == null) {
                throw new BuildException("The following language is not supported:" + version + ".");
            }
            configuration.setDefaultLanguageVersion(languageVersion);
        }

        classpath = task.getClasspath();
        auxClasspath = task.getAuxClasspath();

        filesets.addAll(task.getFilesets());
        formatters.addAll(task.getFormatters());

        project = task.getProject();
    }

    private void doTask() {
        setupClassLoader();

        // Setup RuleSetFactory and validate RuleSets
        RuleSetFactory ruleSetFactory = new RuleSetFactory();
        ruleSetFactory.setClassLoader(configuration.getClassLoader());
        if (!configuration.isRuleSetFactoryCompatibilityEnabled()) {
            ruleSetFactory.disableCompatibilityFilter();
        }
        try {
            // This is just used to validate and display rules. Each thread will
            // create its own ruleset
            ruleSetFactory.setMinimumPriority(configuration.getMinimumPriority());
            ruleSetFactory.setWarnDeprecated(true);
            String ruleSets = configuration.getRuleSets();
            if (StringUtil.isNotEmpty(ruleSets)) {
                // Substitute env variables/properties
                configuration.setRuleSets(project.replaceProperties(ruleSets));
            }
            RuleSets rules = ruleSetFactory.createRuleSets(configuration.getRuleSets());
            ruleSetFactory.setWarnDeprecated(false);
            logRulesUsed(rules);
        } catch (RuleSetNotFoundException e) {
            throw new BuildException(e.getMessage(), e);
        }

        if (configuration.getSuppressMarker() != null) {
            project.log("Setting suppress marker to be " + configuration.getSuppressMarker(), Project.MSG_VERBOSE);
        }

        // Start the Formatters
        for (Formatter formatter : formatters) {
            project.log("Sending a report to " + formatter, Project.MSG_VERBOSE);
            formatter.start(project.getBaseDir().toString());
        }

        // log("Setting Language Version " + languageVersion.getShortName(),
        // Project.MSG_VERBOSE);

        // TODO Do we really need all this in a loop over each FileSet? Seems
        // like a lot of redundancy
        RuleContext ctx = new RuleContext();
        Report errorReport = new Report();
        final AtomicInteger reportSize = new AtomicInteger();
        final String separator = System.getProperty("file.separator");

        for (FileSet fs : filesets) {
            List<DataSource> files = new LinkedList<>();
            DirectoryScanner ds = fs.getDirectoryScanner(project);
            String[] srcFiles = ds.getIncludedFiles();
            for (String srcFile : srcFiles) {
                File file = new File(ds.getBasedir() + separator + srcFile);
                files.add(new FileDataSource(file));
            }

            final String inputPaths = ds.getBasedir().getPath();
            configuration.setInputPaths(inputPaths);

            Renderer logRenderer = new AbstractRenderer("log", "Logging renderer") {
                public void start() {
                    // Nothing to do
                }

                public void startFileAnalysis(DataSource dataSource) {
                    project.log("Processing file " + dataSource.getNiceFileName(false, inputPaths), Project.MSG_VERBOSE);
                }

                public void renderFileReport(Report r) {
                    int size = r.size();
                    if (size > 0) {
                        reportSize.addAndGet(size);
                    }
                }

                public void end() {
                    // Nothing to do
                }

                public String defaultFileExtension() {
                    return null;
                } // not relevant
            };
            List<Renderer> renderers = new LinkedList<>();
            renderers.add(logRenderer);
            for (Formatter formatter : formatters) {
                renderers.add(formatter.getRenderer());
            }
            try {
                PMD.processFiles(configuration, ruleSetFactory, files, ctx, renderers);
            } catch (RuntimeException pmde) {
                handleError(ctx, errorReport, pmde);
            }
        }

        int problemCount = reportSize.get();
        project.log(problemCount + " problems found", Project.MSG_VERBOSE);

        for (Formatter formatter : formatters) {
            formatter.end(errorReport);
        }

        if (failuresPropertyName != null && problemCount > 0) {
            project.setProperty(failuresPropertyName, String.valueOf(problemCount));
            project.log("Setting property " + failuresPropertyName + " to " + problemCount, Project.MSG_VERBOSE);
        }

        if (failOnRuleViolation && problemCount > maxRuleViolations) {
            throw new BuildException("Stopping build since PMD found " + problemCount + " rule violations in the code");
        }
    }

    private void handleError(RuleContext ctx, Report errorReport, RuntimeException pmde) {

        pmde.printStackTrace();
        project.log(pmde.toString(), Project.MSG_VERBOSE);

        Throwable cause = pmde.getCause();

        if (cause != null) {
            StringWriter strWriter = new StringWriter();
            PrintWriter printWriter = new PrintWriter(strWriter);
            cause.printStackTrace(printWriter);
            project.log(strWriter.toString(), Project.MSG_VERBOSE);
            IOUtils.closeQuietly(printWriter);

            if (StringUtil.isNotEmpty(cause.getMessage())) {
                project.log(cause.getMessage(), Project.MSG_VERBOSE);
            }
        }

        if (failOnError) {
            throw new BuildException(pmde);
        }
        errorReport.addError(new Report.ProcessingError(pmde.getMessage(), ctx.getSourceCodeFilename()));
    }

    private void setupClassLoader() {
        if (classpath == null) {
            classpath = new Path(project);
        }
        /*
         * 'basedir' is added to the path to make sure that relative paths
         * such as "<ruleset>resources/custom_ruleset.xml</ruleset>" still
         * work when ant is invoked from a different directory using "-f"
         */
        classpath.add(new Path(null, project.getBaseDir().toString()));

        project.log("Using the AntClassLoader: " + classpath, Project.MSG_VERBOSE);
        // must be true, otherwise you'll get ClassCastExceptions as classes
        // are loaded twice
        // and exist in multiple class loaders
        boolean parentFirst = true;
        configuration.setClassLoader(new AntClassLoader(Thread.currentThread().getContextClassLoader(), project,
                classpath, parentFirst));

        try {
            if (auxClasspath != null) {
                project.log("Using auxclasspath: " + auxClasspath, Project.MSG_VERBOSE);
                configuration.prependClasspath(auxClasspath.toString());
            }
        } catch (IOException ioe) {
            throw new BuildException(ioe.getMessage(), ioe);
        }
    }

    public void execute() throws BuildException {
        final Handler antLogHandler = new AntLogHandler(project);
        final ScopedLogHandlersManager logManager = new ScopedLogHandlersManager(Level.FINEST, antLogHandler);
        try {
            doTask();
        } finally {
            logManager.close();
            IOUtil.tryCloseClassLoader(configuration.getClassLoader());
        }
    }

    private void logRulesUsed(RuleSets rules) {
        project.log("Using these rulesets: " + configuration.getRuleSets(), Project.MSG_VERBOSE);

        RuleSet[] ruleSets = rules.getAllRuleSets();
        for (RuleSet ruleSet : ruleSets) {
            for (Rule rule : ruleSet.getRules()) {
                project.log("Using rule " + rule.getName(), Project.MSG_VERBOSE);
            }
        }
    }
}
