/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2003-2008 University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.dom4j.DocumentException;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.AnalysisFeatures;
import edu.umd.cs.findbugs.config.AnalysisFeatureSetting;
import edu.umd.cs.findbugs.config.CommandLine.HelpRequestedException;
import edu.umd.cs.findbugs.filter.Filter;
import edu.umd.cs.findbugs.filter.FilterException;
import edu.umd.cs.findbugs.internalAnnotations.StaticConstant;
import edu.umd.cs.findbugs.updates.UpdateChecker;
import edu.umd.cs.findbugs.updates.UpdateChecker.PluginUpdate;
import edu.umd.cs.findbugs.util.FutureValue;

/**
 * Static methods and fields useful for working with instances of
 * IFindBugsEngine.
 *
 * This class was previously the main driver for FindBugs analyses, but has been
 * replaced by {@link FindBugs2 FindBugs2}.
 *
 * @author Bill Pugh
 * @author David Hovemeyer
 */
public abstract class FindBugs {
    /**
     * Analysis settings for -effort:min.
     */
    public static final AnalysisFeatureSetting[] MIN_EFFORT = new AnalysisFeatureSetting[] {
        new AnalysisFeatureSetting(AnalysisFeatures.CONSERVE_SPACE, true),
        new AnalysisFeatureSetting(AnalysisFeatures.ACCURATE_EXCEPTIONS, false),
        new AnalysisFeatureSetting(AnalysisFeatures.MERGE_SIMILAR_WARNINGS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.MODEL_INSTANCEOF, false),
        new AnalysisFeatureSetting(AnalysisFeatures.SKIP_HUGE_METHODS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.INTERATIVE_OPCODE_STACK_ANALYSIS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_GUARANTEED_VALUE_DEREFS_IN_NULL_POINTER_ANALYSIS, false),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_VALUE_NUMBERS_IN_NULL_POINTER_ANALYSIS, false),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS, false),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS_OF_REFERENCED_CLASSES, false), };

    /**
     * Analysis settings for -effort:less.
     */
    public static final AnalysisFeatureSetting[] LESS_EFFORT = new AnalysisFeatureSetting[] {
        new AnalysisFeatureSetting(AnalysisFeatures.CONSERVE_SPACE, false),
        new AnalysisFeatureSetting(AnalysisFeatures.ACCURATE_EXCEPTIONS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.MERGE_SIMILAR_WARNINGS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.MODEL_INSTANCEOF, true),
        new AnalysisFeatureSetting(AnalysisFeatures.SKIP_HUGE_METHODS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.INTERATIVE_OPCODE_STACK_ANALYSIS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_GUARANTEED_VALUE_DEREFS_IN_NULL_POINTER_ANALYSIS, false),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_VALUE_NUMBERS_IN_NULL_POINTER_ANALYSIS, false),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS, false),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS_OF_REFERENCED_CLASSES, false), };

    /**
     * Analysis settings for -effort:default.
     */
    public static final AnalysisFeatureSetting[] DEFAULT_EFFORT = new AnalysisFeatureSetting[] {
        new AnalysisFeatureSetting(AnalysisFeatures.CONSERVE_SPACE, false),
        new AnalysisFeatureSetting(AnalysisFeatures.ACCURATE_EXCEPTIONS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.MERGE_SIMILAR_WARNINGS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.MODEL_INSTANCEOF, true),
        new AnalysisFeatureSetting(AnalysisFeatures.SKIP_HUGE_METHODS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.INTERATIVE_OPCODE_STACK_ANALYSIS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_GUARANTEED_VALUE_DEREFS_IN_NULL_POINTER_ANALYSIS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_VALUE_NUMBERS_IN_NULL_POINTER_ANALYSIS, true),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS, true),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS_OF_REFERENCED_CLASSES, false), };

    /**
     * Analysis settings for -effort:more.
     */
    public static final AnalysisFeatureSetting[] MORE_EFFORT = new AnalysisFeatureSetting[] {
        new AnalysisFeatureSetting(AnalysisFeatures.CONSERVE_SPACE, false),
        new AnalysisFeatureSetting(AnalysisFeatures.ACCURATE_EXCEPTIONS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.MERGE_SIMILAR_WARNINGS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.MODEL_INSTANCEOF, true),
        new AnalysisFeatureSetting(AnalysisFeatures.SKIP_HUGE_METHODS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.INTERATIVE_OPCODE_STACK_ANALYSIS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_GUARANTEED_VALUE_DEREFS_IN_NULL_POINTER_ANALYSIS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_VALUE_NUMBERS_IN_NULL_POINTER_ANALYSIS, true),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS, true),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS_OF_REFERENCED_CLASSES, false), };

    /**
     * Analysis settings for -effort:max.
     */
    public static final AnalysisFeatureSetting[] MAX_EFFORT = new AnalysisFeatureSetting[] {
        new AnalysisFeatureSetting(AnalysisFeatures.CONSERVE_SPACE, false),
        new AnalysisFeatureSetting(AnalysisFeatures.ACCURATE_EXCEPTIONS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.MERGE_SIMILAR_WARNINGS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.MODEL_INSTANCEOF, true),
        new AnalysisFeatureSetting(AnalysisFeatures.SKIP_HUGE_METHODS, false),
        new AnalysisFeatureSetting(AnalysisFeatures.INTERATIVE_OPCODE_STACK_ANALYSIS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_GUARANTEED_VALUE_DEREFS_IN_NULL_POINTER_ANALYSIS, true),
        new AnalysisFeatureSetting(AnalysisFeatures.TRACK_VALUE_NUMBERS_IN_NULL_POINTER_ANALYSIS, true),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS, true),
        new AnalysisFeatureSetting(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS_OF_REFERENCED_CLASSES, true), };

    /**
     * Debug tracing.
     */
    public static final boolean DEBUG = Boolean.getBoolean("findbugs.debug");

    /**
     * FindBugs home directory.
     */
    private static String home = System.getProperty("findbugs.home");

    private static boolean noAnalysis = Boolean.getBoolean("findbugs.noAnalysis");

    /**
     * Disable analysis within FindBugs. Turns off loading of bug detectors.
     */
    public static void setNoAnalysis() {
        noAnalysis = true;
    }
    /**
     * @return Returns the noAnalysis.
     */
    public static boolean isNoAnalysis() {
        return noAnalysis;
    }

    private static boolean noMains = Boolean.getBoolean("findbugs.noMains");

    /**
     * Disable loading of FindBugsMain classes.
     */
    public static void setNoMains() {
        noMains = true;
    }

    /**
     * @return Returns the noMains.
     */
    public static boolean isNoMains() {
        return noMains;
    }

    public static final Logger LOGGER = Logger.getLogger(FindBugs.class.getPackage().getName());

    static {
        LOGGER.setLevel(Level.WARNING);
    }

    /**
     * Known URL protocols. Filename URLs that do not have an explicit protocol
     * are assumed to be files.
     */
    @StaticConstant
    static public final Set<String> knownURLProtocolSet = new HashSet<String>();
    static {
        knownURLProtocolSet.add("file");
        knownURLProtocolSet.add("http");
        knownURLProtocolSet.add("https");
        knownURLProtocolSet.add("jar");
    }

    /**
     * Set the FindBugs home directory.
     */
    public static void setHome(String home) {
        FindBugs.home = home;
    }

    /**
     * Get the FindBugs home directory.
     */
    public static String getHome() {
        return home;
    }

    /**
     * Configure training databases.
     *
     * @param findBugs
     *            the IFindBugsEngine to configure
     * @throws IOException
     */
    public static void configureTrainingDatabases(IFindBugsEngine findBugs) throws IOException {
        if (findBugs.emitTrainingOutput()) {
            String trainingOutputDir = findBugs.getTrainingOutputDir();

            if (!new File(trainingOutputDir).isDirectory()) {
                throw new IOException("Training output directory " + trainingOutputDir + " does not exist");
            }
            AnalysisContext.currentAnalysisContext().setDatabaseOutputDir(trainingOutputDir);
            // XXX: hack
            System.setProperty("findbugs.checkreturn.savetraining", new File(trainingOutputDir, "checkReturn.db").getPath());
        }
        if (findBugs.useTrainingInput()) {
            String trainingInputDir = findBugs.getTrainingInputDir();

            if (!new File(trainingInputDir).isDirectory()) {
                throw new IOException("Training input directory " + trainingInputDir + " does not exist");
            }
            AnalysisContext.currentAnalysisContext().setDatabaseInputDir(trainingInputDir);
            AnalysisContext.currentAnalysisContext().loadInterproceduralDatabases();
            // XXX: hack
            System.setProperty("findbugs.checkreturn.loadtraining", new File(trainingInputDir, "checkReturn.db").getPath());
        } else {
            AnalysisContext.currentAnalysisContext().loadDefaultInterproceduralDatabases();
        }
    }

    /**
     * Determines whether or not given DetectorFactory should be enabled.
     *
     * @param findBugs
     *            the IFindBugsEngine
     * @param factory
     *            the DetectorFactory
     * @param rankThreshold
     *            TODO
     * @return true if the DetectorFactory should be enabled, false otherwise
     */
    public static boolean isDetectorEnabled(IFindBugsEngine findBugs, DetectorFactory factory, int rankThreshold) {

        if (!findBugs.getUserPreferences().isDetectorEnabled(factory)) {
            return false;
        }

        if (!factory.isEnabledForCurrentJRE()) {
            return false;
        }

        // Slow first pass detectors are usually disabled, but may be explicitly
        // enabled
        if (!AnalysisContext.currentAnalysisContext().getBoolProperty(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS)
                && factory.isDetectorClassSubtypeOf(InterproceduralFirstPassDetector.class)) {
            return false;
        }

        int maxRank = Integer.MAX_VALUE;
        Set<BugPattern> reportedBugPatterns = factory.getReportedBugPatterns();
        boolean isNonReportingDetector = factory.isDetectorClassSubtypeOf(FirstPassDetector.class);
        if (!isNonReportingDetector && !reportedBugPatterns.isEmpty()) {
            for (BugPattern b : reportedBugPatterns) {
                int rank = BugRanker.findRank(b, factory);
                if (maxRank > rank) {
                    maxRank = rank;
                }
            }
            if (maxRank > rankThreshold) {
                if (false) {
                    System.out.println("Detector " + factory.getShortName() + " has max rank " + maxRank + ", disabling");
                    System.out.println("Reports : " + reportedBugPatterns);
                }
                return false;
            }
        }

        // Training detectors are enabled if, and only if, we are emitting
        // training output
        boolean isTrainingDetector = factory.isDetectorClassSubtypeOf(TrainingDetector.class);
        if (findBugs.emitTrainingOutput()) {
            return isTrainingDetector || isNonReportingDetector;
        }

        if (isTrainingDetector) {
            return false;
        }

        return true;
    }

    /**
     * Process -bugCategories option.
     *
     * @param categories
     *            comma-separated list of bug categories
     *
     * @return Set of categories to be used
     */
    public static Set<String> handleBugCategories(String categories) {
        // Parse list of bug categories
        Set<String> categorySet = new HashSet<String>();
        StringTokenizer tok = new StringTokenizer(categories, ",");
        while (tok.hasMoreTokens()) {
            categorySet.add(tok.nextToken());
        }

        return categorySet;
    }

    /**
     * Process the command line.
     *
     * @param commandLine
     *            the TextUICommandLine object which will parse the command line
     * @param argv
     *            the command line arguments
     * @param findBugs
     *            the IFindBugsEngine to configure
     * @throws IOException
     * @throws FilterException
     */
    public static void processCommandLine(TextUICommandLine commandLine, String[] argv, IFindBugsEngine findBugs)
            throws IOException, FilterException {
        // Expand option files in command line.
        // An argument beginning with "@" is treated as specifying
        // the name of an option file.
        // Each line of option files are treated as a single argument.
        // Blank lines and comment lines (beginning with "#")
        // are ignored.
        try {
            argv = commandLine.expandOptionFiles(argv, true, true);
        } catch (HelpRequestedException e) {
            showHelp(commandLine);
        }

        int argCount = 0;
        try {
            argCount = commandLine.parse(argv);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
            showHelp(commandLine);
        } catch (HelpRequestedException e) {
            showHelp(commandLine);
        }

        Project project = commandLine.getProject();
        for (int i = argCount; i < argv.length; ++i) {
            project.addFile(argv[i]);
        }
        commandLine.handleXArgs();

        commandLine.configureEngine(findBugs);
        if (commandLine.getProject().getFileCount() == 0 &&
                !commandLine.justPrintConfiguration() && !commandLine.justPrintVersion()) {
            System.out.println("No files to be analyzed");

            showHelp(commandLine);
        }
    }

    /**
     * Show -help message.
     *
     * @param commandLine
     */
    @SuppressFBWarnings("DM_EXIT")
    public static void showHelp(TextUICommandLine commandLine) {
        showSynopsis();
        ShowHelp.showGeneralOptions();
        FindBugs.showCommandLineOptions(commandLine);
        System.exit(1);
    }

    /**
     * Given a fully-configured IFindBugsEngine and the TextUICommandLine used
     * to configure it, execute the analysis.
     *
     * @param findBugs
     *            a fully-configured IFindBugsEngine
     * @param commandLine
     *            the TextUICommandLine used to configure the IFindBugsEngine
     */
    @SuppressFBWarnings("DM_EXIT")
    public static void runMain(IFindBugsEngine findBugs, TextUICommandLine commandLine) throws IOException {
        boolean verbose = !commandLine.quiet() || commandLine.setExitCode();

        FutureValue<Collection<UpdateChecker.PluginUpdate>>
        updateHolder = null;
        if (verbose) {
            updateHolder  = DetectorFactoryCollection.instance().getUpdates();
        }
        try {
            findBugs.execute();
        } catch (InterruptedException e) {
            assert false; // should not occur
            checkExitCodeFail(commandLine, e);
            throw new RuntimeException(e);
        } catch (RuntimeException e) {
            checkExitCodeFail(commandLine, e);
            throw e;
        } catch (IOException e) {
            checkExitCodeFail(commandLine, e);
            throw e;
        }

        int bugCount = findBugs.getBugCount();
        int missingClassCount = findBugs.getMissingClassCount();
        int errorCount = findBugs.getErrorCount();

        if (verbose) {
            if (bugCount > 0) {
                System.err.println("Warnings generated: " + bugCount);
            }
            if (missingClassCount > 0) {
                System.err.println("Missing classes: " + missingClassCount);
            }
            if (errorCount > 0) {
                System.err.println("Analysis errors: " + errorCount);
            }
            if (updateHolder.isDone()) {
                try {
                    Collection<PluginUpdate> updates = updateHolder.get();
                    if (!DetectorFactoryCollection.instance().getUpdateChecker().updatesHaveBeenSeenBefore(updates)) {
                        for(UpdateChecker.PluginUpdate u : updates) {
                            System.err.println(u);
                        }
                    }
                } catch (InterruptedException e) {
                    assert true;
                }

            }
        }

        if (commandLine.setExitCode()) {
            int exitCode = 0;
            System.err.println("Calculating exit code...");
            if (errorCount > 0) {
                exitCode |= ExitCodes.ERROR_FLAG;
                System.err.println("Setting 'errors encountered' flag (" + ExitCodes.ERROR_FLAG + ")");
            }
            if (missingClassCount > 0) {
                exitCode |= ExitCodes.MISSING_CLASS_FLAG;
                System.err.println("Setting 'missing class' flag (" + ExitCodes.MISSING_CLASS_FLAG + ")");
            }
            if (bugCount > 0) {
                exitCode |= ExitCodes.BUGS_FOUND_FLAG;
                System.err.println("Setting 'bugs found' flag (" + ExitCodes.BUGS_FOUND_FLAG + ")");
            }
            System.err.println("Exit code set to: " + exitCode);

            System.exit(exitCode);
        }
    }

    /**
     * @param commandLine
     * @param e
     */
    private static void checkExitCodeFail(TextUICommandLine commandLine, Exception e) {
        if (commandLine.setExitCode()) {
            e.printStackTrace(System.err);
            System.exit(ExitCodes.ERROR_FLAG);
        }
    }

    /**
     * Print command line options synopses to stdout.
     */
    public static void showCommandLineOptions() {
        showCommandLineOptions(new TextUICommandLine());
    }

    /**
     * Print command line options synopses to stdout.
     *
     * @param commandLine
     *            the TextUICommandLine whose options should be printed
     */
    public static void showCommandLineOptions(TextUICommandLine commandLine) {
        commandLine.printUsage(System.out);
    }

    /**
     * Show the overall FindBugs command synopsis.
     */
    public static void showSynopsis() {
        System.out
        .println("Usage: findbugs [general options] -textui [command line options...] [jar/zip/class files, directories...]");
    }

    /**
     * Configure the (bug instance) Filter for the given DelegatingBugReporter.
     *
     * @param bugReporter
     *            a DelegatingBugReporter
     * @param filterFileName
     *            filter file name
     * @param include
     *            true if the filter is an include filter, false if it's an
     *            exclude filter
     * @throws java.io.IOException
     * @throws edu.umd.cs.findbugs.filter.FilterException
     */
    public static BugReporter configureFilter(BugReporter bugReporter, String filterFileName, boolean include)
            throws IOException, FilterException {
        Filter filter = new Filter(filterFileName);
        return new FilterBugReporter(bugReporter, filter, include);

    }

    /**
     * Configure a baseline bug instance filter.
     *
     * @param bugReporter
     *            a DelegatingBugReporter
     * @param baselineFileName
     *            filename of baseline Filter
     * @throws java.io.IOException
     * @throws org.dom4j.DocumentException
     */
    public static BugReporter configureBaselineFilter(BugReporter bugReporter, String baselineFileName) throws IOException,
    DocumentException {
        return new ExcludingHashesBugReporter(bugReporter, baselineFileName);
    }

    /**
     * Configure the BugCollection (if the BugReporter being used is
     * constructing one).
     *
     * @param findBugs
     *            the IFindBugsEngine
     */
    public static void configureBugCollection(IFindBugsEngine findBugs) {
        BugCollection bugs = findBugs.getBugReporter().getBugCollection();

        if (bugs != null) {
            bugs.setReleaseName(findBugs.getReleaseName());

            Project project = findBugs.getProject();

            String projectName = project.getProjectName();

            if (projectName == null) {
                projectName = findBugs.getProjectName();
                project.setProjectName(projectName);
            }

            long timestamp = project.getTimestamp();
            if (FindBugs.validTimestamp(timestamp)) {
                bugs.setTimestamp(timestamp);
                bugs.getProjectStats().setTimestamp(timestamp);
            }

        }
    }

    /** Date of release of Java 1.0 */
    public final static long MINIMUM_TIMESTAMP = new GregorianCalendar(1996, 0, 23).getTime().getTime();

    public static boolean validTimestamp(long timestamp) {
        return timestamp > MINIMUM_TIMESTAMP;
    }
}

