/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2008 David H. Hovemeyer <david.hovemeyer@gmail.com>
 * Copyright (C) 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.FileFilter;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.objectweb.asm.ClassReader;

import edu.umd.cs.findbugs.ba.ClassNotFoundExceptionParser;
import edu.umd.cs.findbugs.classfile.CheckedAnalysisException;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.IClassFactory;
import edu.umd.cs.findbugs.classfile.IClassPath;
import edu.umd.cs.findbugs.classfile.IClassPathBuilder;
import edu.umd.cs.findbugs.classfile.IClassPathBuilderProgress;
import edu.umd.cs.findbugs.classfile.ICodeBaseEntry;
import edu.umd.cs.findbugs.classfile.IErrorLogger;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.classfile.analysis.ClassInfo;
import edu.umd.cs.findbugs.classfile.engine.ClassParserUsingASM;
import edu.umd.cs.findbugs.classfile.impl.ClassFactory;

/**
 * Based on the contents of the application directories/archives in a Project,
 * and a "root" source directory (under which some number of "real" source
 * directories may be located), scan to find the source directories containing
 * the application's source files.
 * 
 * @author David Hovemeyer
 */
public class DiscoverSourceDirectories {
    private static boolean DEBUG = SystemProperties.getBoolean("findbugs.dsd.debug");

    /**
     * Progress callback interface for reporting the progress of source
     * directory discovery.
     */
    public interface Progress extends IClassPathBuilderProgress {
        public void startRecursiveDirectorySearch();

        public void doneRecursiveDirectorySearch();

        public void startScanningArchives(int numArchivesToScan);

        public void doneScanningArchives();

        public void startScanningClasses(int numClassesToScan);

        public void finishClass();

        public void doneScanningClasses();
    }

    private static class NoOpErrorLogger implements IErrorLogger {

        public void reportMissingClass(ClassNotFoundException ex) {
        }

        public void reportMissingClass(ClassDescriptor classDescriptor) {
        }

        public void logError(String message) {
        }

        public void logError(String message, Throwable e) {
        }

        public void reportSkippedAnalysis(MethodDescriptor method) {
        }
    };

    private static class NoOpProgress implements Progress {
        public void startScanningArchives(int numArchivesToScan) {
        }

        public void doneScanningArchives() {
        }

        public void startScanningClasses(int numClassesToScan) {
        }

        public void finishClass() {
        }

        public void doneScanningClasses() {
        }

        public void finishArchive() {
        }

        public void startRecursiveDirectorySearch() {
        }

        public void doneRecursiveDirectorySearch() {
        }

        public void startArchive(String name) {
        }

    }

    private Project project;

    private String rootSourceDirectory;

    private boolean scanForNestedArchives;

    private IErrorLogger errorLogger;

    private Progress progress;

    private List<String> discoveredSourceDirectoryList;

    /**
     * Constructor.
     */
    public DiscoverSourceDirectories() {
        this.errorLogger = new NoOpErrorLogger();
        this.progress = new NoOpProgress();
        this.discoveredSourceDirectoryList = new LinkedList<String>();
    }

    /**
     * Set the Project for which we want to find source directories.
     * 
     * @param project
     *            Project for which we want to find source directories
     */
    public void setProject(Project project) {
        this.project = project;
    }

    /**
     * Set the "root" source directory: we expect all of the actual source
     * directories to be underneath it.
     * 
     * @param rootSourceDirectory
     *            the root source directory
     */
    public void setRootSourceDirectory(String rootSourceDirectory) {
        this.rootSourceDirectory = rootSourceDirectory;
    }

    /**
     * Set whether or not to scan the project for nested archives (i.e., if
     * there is a WAR or EAR file that contains jar files inside it.) Default is
     * false.
     * 
     * @param scanForNestedArchives
     *            true if nested archives should be scanned, false otherwise
     */
    public void setScanForNestedArchives(boolean scanForNestedArchives) {
        this.scanForNestedArchives = scanForNestedArchives;
    }

    /**
     * Set the error logger to use to report errors during scanning. By default,
     * a no-op error logger is used.
     * 
     * @param errorLogger
     *            error logger to use to report errors during scanning
     */
    public void setErrorLogger(IErrorLogger errorLogger) {
        this.errorLogger = errorLogger;
    }

    /**
     * Set the progress callback to which scanning progress should be reported.
     * 
     * @param progress
     *            the progress callback
     */
    public void setProgress(Progress progress) {
        this.progress = progress;
    }

    /**
     * Get the list of discovered source directories. These can be added to a
     * Project.
     * 
     * @return list of discovered source directories.
     */
    public List<String> getDiscoveredSourceDirectoryList() {
        return Collections.unmodifiableList(discoveredSourceDirectoryList);
    }

    /**
     * Execute the search for source directories.
     * 
     * @throws edu.umd.cs.findbugs.classfile.CheckedAnalysisException
     * @throws java.io.IOException
     * @throws java.lang.InterruptedException
     */
    public void execute() throws CheckedAnalysisException, IOException, InterruptedException {
        File dir = new File(rootSourceDirectory);
        if (!dir.isDirectory()) {
            throw new IOException("Path " + rootSourceDirectory + " is not a directory");
        }

        // Find all directories underneath the root source directory
        progress.startRecursiveDirectorySearch();
        RecursiveFileSearch rfs = new RecursiveFileSearch(rootSourceDirectory, new FileFilter() {
            public boolean accept(File pathname) {
                return pathname.isDirectory();
            }
        });
        rfs.search();
        progress.doneRecursiveDirectorySearch();
        List<String> candidateSourceDirList = rfs.getDirectoriesScanned();

        // Build the classpath
        IClassPath classPath = null;
        try {
            IClassFactory factory = ClassFactory.instance();
            IClassPathBuilder builder = factory.createClassPathBuilder(errorLogger);

            classPath = buildClassPath(builder, factory);

            // From the application classes, find the full list of
            // fully-qualified source file names.
            List<String> fullyQualifiedSourceFileNameList = findFullyQualifiedSourceFileNames(builder, classPath);

            // Attempt to find source directories for all source files,
            // and add them to the discoveredSourceDirectoryList
            if (DEBUG) {
                System.out.println("looking for " + fullyQualifiedSourceFileNameList.size() + " files");
            }
            findSourceDirectoriesForAllSourceFiles(fullyQualifiedSourceFileNameList, candidateSourceDirList);
        } finally {
            if (classPath != null) {
                classPath.close();
            }
        }
    }

    private IClassPath buildClassPath(IClassPathBuilder builder, IClassFactory factory) throws InterruptedException, IOException,
            CheckedAnalysisException {

        progress.startScanningArchives(project.getFileCount());

        for (String path : project.getFileList()) {
            builder.addCodeBase(factory.createFilesystemCodeBaseLocator(path), true);
        }

        for (String path : project.getAuxClasspathEntryList()) {
            builder.addCodeBase(factory.createFilesystemCodeBaseLocator(path), false);
        }

        IClassPath classPath = factory.createClassPath();

        builder.build(classPath, progress);

        progress.doneScanningArchives();

        return classPath;
    }

    private String findFullyQualifiedSourceFileName(IClassPath classPath, ClassDescriptor classDesc) throws IOException,
            CheckedAnalysisException {
        try {
            // Open and parse the class file to attempt
            // to discover the source file name.
            ICodeBaseEntry codeBaseEntry = classPath.lookupResource(classDesc.toResourceName());

            ClassParserUsingASM classParser = new ClassParserUsingASM(new ClassReader(codeBaseEntry.openResource()), classDesc,
                    codeBaseEntry);

            ClassInfo.Builder classInfoBuilder = new ClassInfo.Builder();
            classParser.parse(classInfoBuilder);
            ClassInfo classInfo = classInfoBuilder.build();

            // Construct the fully-qualified source file name
            // based on the package name and source file name.
            String packageName = classDesc.getPackageName();
            String sourceFile = classInfo.getSource();

            if (!packageName.equals("")) {
                packageName = packageName.replace('.', '/');
                packageName += "/";
            }

            String fullyQualifiedSourceFile = packageName + sourceFile;

            return fullyQualifiedSourceFile;
        } catch (CheckedAnalysisException e) {
            errorLogger.logError("Could scan class " + classDesc.toDottedClassName(), e);
            throw e;
        } finally {
            progress.finishClass();
        }
    }

    private List<String> findFullyQualifiedSourceFileNames(IClassPathBuilder builder, IClassPath classPath) {

        List<ClassDescriptor> appClassList = builder.getAppClassList();

        progress.startScanningClasses(appClassList.size());

        List<String> fullyQualifiedSourceFileNameList = new LinkedList<String>();

        for (ClassDescriptor classDesc : appClassList) {
            try {
                String fullyQualifiedSourceFileName = findFullyQualifiedSourceFileName(classPath, classDesc);
                fullyQualifiedSourceFileNameList.add(fullyQualifiedSourceFileName);
            } catch (IOException e) {
                errorLogger.logError("Couldn't scan class " + classDesc.toDottedClassName(), e);
            } catch (CheckedAnalysisException e) {
                errorLogger.logError("Couldn't scan class " + classDesc.toDottedClassName(), e);
            }
        }

        progress.doneScanningClasses();

        return fullyQualifiedSourceFileNameList;
    }

    private void findSourceDirectoriesForAllSourceFiles(List<String> fullyQualifiedSourceFileNameList,
            List<String> candidateSourceDirList) {

        Set<String> sourceDirsFound = new HashSet<String>();

        // For each source file discovered, try to locate it in one of
        // the candidate source directories.
        for (String fullyQualifiedSourceFileName : fullyQualifiedSourceFileNameList) {

            checkCandidateSourceDirs: for (String candidateSourceDir : candidateSourceDirList) {
                String path = candidateSourceDir + File.separatorChar + fullyQualifiedSourceFileName;
                File f = new File(path);
                if (DEBUG) {
                    System.out.print("Checking " + f.getPath() + "...");
                }

                boolean found = f.exists() && !f.isDirectory();

                if (DEBUG) {
                    System.out.println(found ? "FOUND" : "not found");
                }

                if (found) {
                    // Bingo!
                    if (sourceDirsFound.add(candidateSourceDir)) {
                        discoveredSourceDirectoryList.add(candidateSourceDir);
                        sourceDirsFound.add(candidateSourceDir);
                    }
                    break checkCandidateSourceDirs;

                }
            }
        }
    }

    /**
     * Just for testing.
     */
    public static void main(String[] args) throws IOException, CheckedAnalysisException, InterruptedException {
        if (args.length != 2) {
            System.err.println("Usage: " + DiscoverSourceDirectories.class.getName() + " <project file> <root source dir>");
            System.exit(1);
        }

        Project project = Project.readProject(args[0]);

        IErrorLogger errorLogger = new IErrorLogger() {

            public void reportMissingClass(ClassNotFoundException ex) {
                String className = ClassNotFoundExceptionParser.getMissingClassName(ex);
                if (className != null) {
                    logError("Missing class: " + className);
                } else {
                    logError("Missing class: " + ex);
                }
            }

            public void reportMissingClass(ClassDescriptor classDescriptor) {
                logError("Missing class: " + classDescriptor.toDottedClassName());
            }

            public void logError(String message) {
                System.err.println("Error: " + message);
            }

            public void logError(String message, Throwable e) {
                logError(message + ": " + e.getMessage());
            }

            public void reportSkippedAnalysis(MethodDescriptor method) {
                logError("Skipped analysis of method " + method.toString());
            }

        };

        DiscoverSourceDirectories.Progress progress = new DiscoverSourceDirectories.Progress() {

            public void startRecursiveDirectorySearch() {
                System.out.print("Scanning directories...");
                System.out.flush();
            }

            public void doneRecursiveDirectorySearch() {
                System.out.println("done");
            }

            public void startScanningArchives(int numArchivesToScan) {
                System.out.print("Scanning " + numArchivesToScan + " archives..");
                System.out.flush();
            }

            public void doneScanningArchives() {
                System.out.println("done");
            }

            public void startScanningClasses(int numClassesToScan) {
                System.out.print("Scanning " + numClassesToScan + " classes...");
                System.out.flush();
            }

            public void finishClass() {
                System.out.print(".");
                System.out.flush();
            }

            public void doneScanningClasses() {
                System.out.println("done");
            }

            public void finishArchive() {
                System.out.print(".");
                System.out.flush();
            }

            public void startArchive(String name) {
                // noop
            }
        };

        DiscoverSourceDirectories discoverSourceDirectories = new DiscoverSourceDirectories();
        discoverSourceDirectories.setProject(project);
        discoverSourceDirectories.setRootSourceDirectory(args[1]);
        discoverSourceDirectories.setErrorLogger(errorLogger);
        discoverSourceDirectories.setProgress(progress);

        discoverSourceDirectories.execute();

        System.out.println("Found source directories:");
        for (String srcDir : discoverSourceDirectories.getDiscoveredSourceDirectoryList()) {
            System.out.println("  " + srcDir);
        }
    }
}
