/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * ClassCache.java
 * Copyright (C) 2010-2012 University of Waikato, Hamilton, New Zealand
 */

package weka.core;

import java.io.File;
import java.io.FileFilter;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * A singleton that stores all classes on the classpath.
 *
 * @author  fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 8034 $
 */
public class ClassCache
  implements RevisionHandler {

  /**
   * For filtering classes.
   *
   * @author  fracpete (fracpete at waikato dot ac dot nz)
   * @version $Revision: 8034 $
   */
  public static class ClassFileFilter
    implements FileFilter {

    /**
     * Checks whether the file is a class.
     *
     * @param pathname	the file to check
     * @return		true if a class file
     */
    public boolean accept(File pathname) {
      return pathname.getName().endsWith(".class");
    }
  }

  /**
   * For filtering classes.
   *
   * @author  fracpete (fracpete at waikato dot ac dot nz)
   * @version $Revision: 8034 $
   */
  public static class DirectoryFilter
    implements FileFilter {

    /**
     * Checks whether the file is a directory.
     *
     * @param pathname	the file to check
     * @return		true if a directory
     */
    public boolean accept(File pathname) {
      return pathname.isDirectory();
    }
  }

  /** whether to output some debug information. */
  public final static boolean VERBOSE = false;

  /** the key for the default package. */
  public final static String DEFAULT_PACKAGE = "DEFAULT";

  /** for caching all classes on the class path (package-name &lt;-&gt; HashSet with classnames). */
  protected Hashtable<String,HashSet<String>> m_Cache;

  static {
    // notify if VERBOSE is still on
    if (VERBOSE)
      System.err.println(ClassCache.class.getName() + ": VERBOSE ON");
  }

  /**
   * Initializes the cache.
   */
  public ClassCache() {
    super();
    initialize();
  }

  /**
   * Fixes the classname, turns "/" and "\" into "." and removes ".class".
   *
   * @param classname	the classname to process
   * @return		the processed classname
   */
  protected String cleanUp(String classname) {
    String	result;

    result = classname;

    if (result.indexOf("/") > -1)
      result = result.replace("/", ".");
    if (result.indexOf("\\") > -1)
      result = result.replace("\\", ".");
    if (result.endsWith(".class"))
      result = result.substring(0, result.length() - 6);

    return result;
  }

  /**
   * Extracts the package name from the (clean) classname.
   *
   * @param classname	the classname to extract the package from
   * @return		the package name
   */
  protected String extractPackage(String classname) {
    if (classname.indexOf(".") > -1)
      return classname.substring(0, classname.lastIndexOf("."));
    else
      return DEFAULT_PACKAGE;
  }

  /**
   * Adds the classname to the cache.
   *
   * @param classname	the classname, automatically removes ".class" and
   * 			turns "/" or "\" into "."
   * @return		true if adding changed the cache
   */
  public boolean add(String classname) {
    String		pkgname;
    HashSet<String>	names;

    // classname and package
    classname = cleanUp(classname);
    pkgname   = extractPackage(classname);

    // add to cache
    if (!m_Cache.containsKey(pkgname))
      m_Cache.put(pkgname, new HashSet<String>());
    names = m_Cache.get(pkgname);
    return names.add(classname);
  }

  /**
   * Removes the classname from the cache.
   *
   * @param classname	the classname to remove
   * @return		true if the removal changed the cache
   */
  public boolean remove(String classname) {
    String		pkgname;
    HashSet<String>	names;

    classname = cleanUp(classname);
    pkgname   = extractPackage(classname);
    names     = m_Cache.get(pkgname);
    if (names != null)
      return names.remove(classname);
    else
      return false;
  }

  /**
   * Fills the class cache with classes in the specified directory.
   *
   * @param prefix	the package prefix so far, null for default package
   * @param dir		the directory to search
   */
  protected void initFromDir(String prefix, File dir) {
    File[]	files;

    // check classes
    files = dir.listFiles(new ClassFileFilter());
    for (File file: files) {
      if (prefix == null)
	add(file.getName());
      else
	add(prefix + "." + file.getName());
    }

    // descend in directories
    files = dir.listFiles(new DirectoryFilter());
    for (File file: files) {
      if (prefix == null)
	initFromDir(file.getName(), file);
      else
	initFromDir(prefix + "." + file.getName(), file);
    }
  }

  /**
   * Fills the class cache with classes in the specified directory.
   *
   * @param dir		the directory to search
   */
  protected void initFromDir(File dir) {
    if (VERBOSE)
      System.out.println("Analyzing directory: " + dir);
    initFromDir(null, dir);
  }

  /**
   * Fills the class cache with classes from the specified jar.
   *
   * @param file		the jar to inspect
   */
  protected void initFromJar(File file) {
    JarFile		jar;
    JarEntry		entry;
    Enumeration		enm;

    if (VERBOSE)
      System.out.println("Analyzing jar: " + file);

    if (!file.exists()) {
      System.out.println("Jar does not exist: " + file);
      return;
    }

    try {
      jar = new JarFile(file);
      enm = jar.entries();
      while (enm.hasMoreElements()) {
        entry = (JarEntry) enm.nextElement();
        if (entry.getName().endsWith(".class"))
          add(entry.getName());
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * Returns all the stored packages.
   *
   * @return		the package names
   */
  public Enumeration<String> packages() {
    return m_Cache.keys();
  }

  /**
   * Returns all the classes for the given package.
   *
   * @param pkgname	the package to get the classes for
   * @return		the classes (sorted by name)
   */
  public HashSet<String> getClassnames(String pkgname) {
    if (m_Cache.containsKey(pkgname))
      return m_Cache.get(pkgname);
    else
      return new HashSet<String>();
  }

  /**
   * Initializes the cache.
   */
  protected void initialize() {
    String		part;
    File		file;
    URLClassLoader 	sysLoader;
    URL[] 		urls;

    m_Cache = new Hashtable<String,HashSet<String>>();

    sysLoader = (URLClassLoader) getClass().getClassLoader();
    urls      = sysLoader.getURLs();
    for (URL url: urls) {
      if (VERBOSE)
        System.out.println("Classpath-part: " + part);

      file = null;
      part = url.toString();
      if (part.startsWith("file:")) {
        part = part.replace(" ", "%20");
        try {
          file = new File(new java.net.URI(part));
        }
        catch (URISyntaxException e) {
          e.printStackTrace();
        }
      }
      else {
	file = new File(part);
      }
      if (file == null) {
	System.err.println("Skipping: " + part);
	continue;
      }

      // find classes
      if (file.isDirectory())
	initFromDir(file);
      else if (file.exists())
	initFromJar(file);
    }
  }

  /**
   * Find all classes that have the supplied matchText String in
   * their suffix.
   *
   * @param matchText 	the text to match
   * @return 		an array list of matching fully qualified class names.
   */
  public ArrayList<String> find(String matchText) {
    ArrayList<String>	result;
    Enumeration<String>	packages;
    Iterator<String>	names;
    String		name;

    result = new ArrayList<String>();

    packages = m_Cache.keys();
    while (packages.hasMoreElements()) {
      names = m_Cache.get(packages.nextElement()).iterator();
      while (names.hasNext()) {
	name = names.next();
	if (name.contains(matchText))
	  result.add(name);
      }
    }

    if (result.size() > 1)
      Collections.sort(result);

    return result;
  }

  /**
   * Returns the revision string.
   *
   * @return		the revision
   */
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 8034 $");
  }

  /**
   * For testing only.
   *
   * @param args	ignored
   */
  public static void main(String[] args) {
    ClassCache cache = new ClassCache();
    Enumeration<String> packages = cache.packages();
    while (packages.hasMoreElements()) {
      String key = packages.nextElement();
      System.out.println(key + ": " + cache.getClassnames(key).size());
    }
  }
}
